From d59e4c0bb95b4460afcaa9509142412af513c285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B2=A1=E5=A4=8D=E4=B9=A0?= <2353956224@qq.com> Date: Mon, 9 Feb 2026 21:04:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=9B=BD=E5=AE=B6=E6=A3=80?= =?UTF-8?q?=E6=B5=8Bbug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/locales/en.js | 20 +++++ src/locales/zh.js | 20 +++++ src/pages/ConfigPage.vue | 45 ++++++++++- src/stores/countryStore.js | 143 ++++++++++++++++++++++++++++++++++ src/utils/pythonBridge.js | 113 ++++++++++++++++++++++++--- src/views/tk/FanWorkbench.vue | 34 ++++---- src/views/tk/Workbenches.vue | 115 ++++++++++++++++----------- 7 files changed, 419 insertions(+), 71 deletions(-) create mode 100644 src/stores/countryStore.js diff --git a/src/locales/en.js b/src/locales/en.js index 6dbf8fc..25804f7 100644 --- a/src/locales/en.js +++ b/src/locales/en.js @@ -48,6 +48,17 @@ export default { prompt: 'Stop crawling specified number', setHostNum: 'Set crawling quantity', unlimitedQuantity: 'Unlimited crawling quantity', + refreshCountry: 'Refresh Country', + refreshSuccess: 'Refresh Successful', + refreshFailed: 'Refresh Failed', + enterCountryPrompt: 'Unable to automatically obtain country information due to network issues. Please manually enter the country name (in Chinese)', + enterCountryTitle: 'Failed to Get Country', + confirm: 'Confirm', + cancel: 'Cancel', + countryPlaceholder: 'e.g., 美国, 日本, 英国', + countryRequired: 'Please enter country name', + countrySetSuccess: 'Country Set Successfully', + unknown: 'Unknown', }, hostList: { placeCountry: 'Select country', @@ -156,6 +167,15 @@ export default { starting: 'Starting...', pleaseEnterCountryName: 'Please enter the country name in Chinese', getCountryFailed: 'Failed to get country', + refreshCountry: 'Refresh Country', + refreshSuccess: 'Refresh Successful', + refreshFailed: 'Refresh Failed', + enterCountryPrompt: 'Unable to automatically obtain country information due to network issues. Please manually enter the country name (in Chinese)', + enterCountryTitle: 'Failed to Get Country', + countryPlaceholder: 'e.g., 美国, 日本, 英国', + countryRequired: 'Please enter country name', + countrySetSuccess: 'Country Set Successfully', + unknown: 'Unknown', }, countries: { // ... (truncated common countries for brevity, or include all if critical. I'll include a subset or all if possible. The file read showed all.) diff --git a/src/locales/zh.js b/src/locales/zh.js index de34039..7cd6ead 100644 --- a/src/locales/zh.js +++ b/src/locales/zh.js @@ -48,6 +48,17 @@ export default { prompt: '达到数量后停止爬取', setHostNum: '设置爬取数量', unlimitedQuantity: '不限爬取数量', + refreshCountry: '刷新国家', + refreshSuccess: '刷新成功', + refreshFailed: '刷新失败', + enterCountryPrompt: '由于网络原因无法自动获取国家信息,请手动输入当前网络所在国家(中文名)', + enterCountryTitle: '获取国家失败', + confirm: '确定', + cancel: '取消', + countryPlaceholder: '例如:美国、日本、英国', + countryRequired: '请输入国家名称', + countrySetSuccess: '国家设置成功', + unknown: '未知', }, hostList: { placeCountry: '选择国家', @@ -156,6 +167,15 @@ export default { stopping: '正在停止...', starting: '正在启动...', enterRoomId: '请输入直播间id', + refreshCountry: '刷新国家', + refreshSuccess: '刷新成功', + refreshFailed: '刷新失败', + enterCountryPrompt: '由于网络原因无法自动获取国家信息,请手动输入当前网络所在国家(中文名)', + enterCountryTitle: '获取国家失败', + countryPlaceholder: '例如:美国、日本、英国', + countryRequired: '请输入国家名称', + countrySetSuccess: '国家设置成功', + unknown: '未知', }, countries: { AD: "安道尔", AE: "阿拉伯联合酋长国", AF: "阿富汗", AG: "安提瓜和巴布达", AI: "安圭拉", AL: "阿尔巴尼亚", AM: "亚美尼亚", AO: "安哥拉", AQ: "南极洲", AR: "阿根廷", AS: "美属萨摩亚", AT: "奥地利", AU: "澳大利亚", AU1: "澳大利亚", AW: "阿鲁巴", AX: "奥兰群岛", AZ: "阿塞拜疆", diff --git a/src/pages/ConfigPage.vue b/src/pages/ConfigPage.vue index 731debe..ce0c3aa 100644 --- a/src/pages/ConfigPage.vue +++ b/src/pages/ConfigPage.vue @@ -325,6 +325,34 @@ + + + +
+
+ +
+ +
+
正在预热视图
+
+ 这会提升后台视图渲染稳定性,请稍候… +
+ + +
+ + + +
+
+
+
+
@@ -635,7 +663,7 @@ const handleSleepTimeInput = (val) => { config.value.sleepTime = parseInt(val) || 0 } } - +const warmingUp = ref(false) // Start/Stop const handleStart = async (specificGroupIndex) => { const activeGroupIndex = specificGroupIndex ?? 0 @@ -710,7 +738,8 @@ const handleStart = async (specificGroupIndex) => { } } - // 预热所有视图,确保后台视图完成渲染,解决自动化执行失败问题 + // 预热所有视图,确保后台视图完成渲染,解决自动化执行失败问题 + warmingUp.value = true try { console.log('[ConfigPage] 预热所有视图...') await window.electronAPI.warmUpViews() @@ -758,6 +787,7 @@ const handleStart = async (specificGroupIndex) => { const status = await window.electronAPI.getRotationStatus() rotationStatus.value = status handleStatusChange(status) + warmingUp.value = false //关闭遮罩 emit('goToBrowser') } @@ -823,3 +853,14 @@ const togglePasswordVisibility = (gIndex, aIndex) => { showPasswordMap.value[key] = !showPasswordMap.value[key] } + \ No newline at end of file diff --git a/src/stores/countryStore.js b/src/stores/countryStore.js new file mode 100644 index 0000000..18161fe --- /dev/null +++ b/src/stores/countryStore.js @@ -0,0 +1,143 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { ElMessage, ElMessageBox } from 'element-plus' +import { getCountryName } from '@/utils/countryUtil' + +export const useCountryStore = defineStore('country', () => { + // 状态 + const countryData = ref('') // 中文国家名 + const countryDataEN = ref('') // 英文国家名 + const isLoading = ref(false) // 是否正在获取 + const hasInitialized = ref(false) // 是否已初始化 + const lastFetchTime = ref(null) // 上次获取时间 + + /** + * 获取 IP 国家信息 + * @param {Function} t - 国际化函数 + * @param {boolean} showDialog - 获取失败时是否显示弹窗(默认 true,只在第一次获取时显示) + */ + const fetchCountryInfo = async (t, showDialog = true) => { + if (isLoading.value) return + + isLoading.value = true + try { + const response = await fetch('https://ipapi.co/json/') + if (!response.ok) { + throw new Error('请求失败') + } + const data = await response.json() + + countryDataEN.value = data.country_name + countryData.value = getCountryName(data.country) + lastFetchTime.value = Date.now() + hasInitialized.value = true + + return { success: true } + } catch (error) { + console.error('获取国家信息失败:', error) + + // 只在允许显示弹窗且未初始化时才显示 + if (showDialog && !hasInitialized.value) { + showCountryInputDialog(t) + } + + return { success: false, error } + } finally { + isLoading.value = false + } + } + + /** + * 刷新国家信息 + * @param {Function} t - 国际化函数 + */ + const refreshCountry = async (t) => { + if (isLoading.value) return + + try { + const result = await fetchCountryInfo(t, false) // 刷新时不自动弹窗 + + if (result.success) { + ElMessage.success(t('workbenchesSetup.refreshSuccess') || t('hostsList.refreshSuccess') || '刷新成功') + } else { + // 刷新失败时,给用户选择是否手动输入 + ElMessageBox.confirm( + t('workbenchesSetup.refreshFailed') || t('hostsList.refreshFailed') || '刷新失败,是否手动输入国家?', + t('workbenchesSetup.enterCountryTitle') || t('hostsList.enterCountryTitle') || '提示', + { + confirmButtonText: t('workbenchesSetup.confirm') || t('hostsList.confirm') || '手动输入', + cancelButtonText: t('workbenchesSetup.cancel') || t('hostsList.cancel') || '取消', + type: 'warning', + } + ).then(() => { + showCountryInputDialog(t) + }).catch(() => { + // 用户取消 + }) + } + } catch (error) { + ElMessage.error(t('workbenchesSetup.refreshFailed') || t('hostsList.refreshFailed') || '刷新失败') + } + } + + /** + * 显示手动输入国家的弹窗 + * @param {Function} t - 国际化函数 + */ + const showCountryInputDialog = (t) => { + ElMessageBox.prompt( + t('workbenchesSetup.enterCountryPrompt') || t('hostsList.enterCountryPrompt') || '由于网络原因无法自动获取国家信息,请手动输入当前网络所在国家(中文名)', + t('workbenchesSetup.enterCountryTitle') || t('hostsList.enterCountryTitle') || '获取国家失败', + { + confirmButtonText: t('workbenchesSetup.confirm') || t('hostsList.confirm') || '确定', + cancelButtonText: t('workbenchesSetup.cancel') || t('hostsList.cancel') || '取消', + inputPlaceholder: t('workbenchesSetup.countryPlaceholder') || t('hostsList.countryPlaceholder') || '例如:美国、日本、英国', + inputValidator: (value) => { + if (!value || value.trim() === '') { + return t('workbenchesSetup.countryRequired') || t('hostsList.countryRequired') || '请输入国家名称' + } + return true + } + } + ).then(({ value }) => { + countryData.value = value.trim() + countryDataEN.value = value.trim() + hasInitialized.value = true + lastFetchTime.value = Date.now() + ElMessage.success(t('workbenchesSetup.countrySetSuccess') || t('hostsList.countrySetSuccess') || '国家设置成功') + }).catch(() => { + // 用户取消输入 + if (!hasInitialized.value) { + countryData.value = t('workbenchesSetup.unknown') || t('hostsList.unknown') || '未知' + countryDataEN.value = 'Unknown' + hasInitialized.value = true + } + }) + } + + /** + * 初始化国家信息(只在第一次调用时获取) + * @param {Function} t - 国际化函数 + */ + const initCountryInfo = async (t) => { + if (hasInitialized.value) { + return // 已经初始化过,不再重复获取 + } + await fetchCountryInfo(t, true) + } + + return { + // 状态 + countryData, + countryDataEN, + isLoading, + hasInitialized, + lastFetchTime, + + // 方法 + fetchCountryInfo, + refreshCountry, + showCountryInputDialog, + initCountryInfo, + } +}) diff --git a/src/utils/pythonBridge.js b/src/utils/pythonBridge.js index 5488f3c..b73e3aa 100644 --- a/src/utils/pythonBridge.js +++ b/src/utils/pythonBridge.js @@ -53,16 +53,22 @@ export function usePythonBridge() { await window.electronAPI.tk.visitAnchor(id); }; - // backStageloginStatus - const backStageloginStatus = async () => { + // 查询后台登录状态(合并接口,通过 account 参数区分) + // account: 公会账号,不传则返回所有账号状态 + const backStageloginStatus = async (account) => { if (!inElectron) return null; - return await window.electronAPI.tk.checkBackStageLoginStatus(); + try { + const res = await window.electronAPI.tk.checkBackStageLoginStatus(account); + return typeof res === 'string' ? JSON.parse(res) : res; + } catch (e) { + console.error('backStageloginStatus error:', e); + return null; + } }; - // backStageloginStatusCopy - const backStageloginStatusCopy = async () => { - if (!inElectron) return null; - return await window.electronAPI.tk.checkBackStageLoginStatusCopy(); + // 兼容旧接口:查询副账号登录状态(内部调用合并后的接口) + const backStageloginStatusCopy = async (account) => { + return await backStageloginStatus(account); }; // exportToExcel @@ -125,9 +131,17 @@ export function usePythonBridge() { await window.electronAPI.tk.openRoom(id); }; - // Clipboard helper - maybe use navigator.clipboard directly in Vue component? - // Original used python bridge for clipboard. + // Clipboard helper - 优先使用 Python RPC,fallback 到浏览器 API const setClipboards = async (text) => { + if (inElectron) { + try { + const result = await window.electronAPI.tk.setClipboard(text); + if (result.success) return true; + } catch (e) { + console.warn('Electron clipboard failed, fallback to browser:', e); + } + } + // Fallback to browser API try { await navigator.clipboard.writeText(text); return true; @@ -137,6 +151,78 @@ export function usePythonBridge() { } } + // ========== 新增接口 ========== + + // 启动大哥监控(TikTok 登录) + const startBrotherMonitor = async () => { + if (!inElectron) return { success: false, error: 'Not in Electron' }; + try { + return await window.electronAPI.tk.startBrotherMonitor(); + } catch (e) { + console.error('startBrotherMonitor error:', e); + return { success: false, error: String(e) }; + } + }; + + // 获取大哥模块 TikTok 登录状态 + const getBrotherLoginStatus = async () => { + if (!inElectron) return { isLoggedIn: false }; + try { + const res = await window.electronAPI.tk.getBrotherLoginStatus(); + return JSON.parse(res); + } catch (e) { + console.error('getBrotherLoginStatus error:', e); + return { isLoggedIn: false }; + } + }; + + // 打开大哥个人主页 + const visitGifter = async (data) => { + if (!inElectron) return { success: false }; + try { + // data 可以是 { id: 'xxx' } 或 { uniqueId: 'xxx' } + return await window.electronAPI.tk.visitGifter(JSON.stringify(data)); + } catch (e) { + console.error('visitGifter error:', e); + return { success: false, error: String(e) }; + } + }; + + // 关闭所有浏览器 + const closeAllBrowsers = async () => { + if (!inElectron) return { success: false, error: 'Not in Electron' }; + try { + return await window.electronAPI.tk.closeAllBrowsers(); + } catch (e) { + console.error('closeAllBrowsers error:', e); + return { success: false, error: String(e) }; + } + }; + + // 加密存储账号信息 + const storageAccountInfo = async (data) => { + if (!inElectron) return { success: false, error: 'Not in Electron' }; + try { + // data: { key: string, data: object } + return await window.electronAPI.tk.storageAccount(JSON.stringify(data)); + } catch (e) { + console.error('storageAccountInfo error:', e); + return { success: false, error: String(e) }; + } + }; + + // 解密读取账号信息 + const readAccountInfo = async (key) => { + if (!inElectron) return { status: 'error', message: 'Not in Electron', data: null }; + try { + const res = await window.electronAPI.tk.readAccount(JSON.stringify({ key })); + return JSON.parse(res); + } catch (e) { + console.error('readAccountInfo error:', e); + return { status: 'error', message: String(e), data: null }; + } + }; + return { fetchDataConfig, fetchDataCount, @@ -156,6 +242,13 @@ export function usePythonBridge() { storageSetInfos, readSetInfos, openAnchorIdRooms, - setClipboards + setClipboards, + // 新增接口 + startBrotherMonitor, + getBrotherLoginStatus, + visitGifter, + closeAllBrowsers, + storageAccountInfo, + readAccountInfo }; } diff --git a/src/views/tk/FanWorkbench.vue b/src/views/tk/FanWorkbench.vue index b5fdbdb..e0ce8d4 100644 --- a/src/views/tk/FanWorkbench.vue +++ b/src/views/tk/FanWorkbench.vue @@ -109,9 +109,14 @@ -
+
{{ $t('hostsList.currentNetwork') || '当前网络' }}: - {{ countryData }} + {{ countryData }} +
@@ -218,6 +223,7 @@ import { getUser } from "@/utils/storage"; import { getCountryName } from "@/utils/countryUtil"; import { ElMessage, ElMessageBox, ElLoading } from "element-plus"; import { useI18n } from 'vue-i18n'; +import { useCountryStore } from '@/stores/countryStore'; // Mock API calls if not present // Ideally we should import these from api file, but for simplicity I will mock them or use empty callbacks @@ -228,6 +234,7 @@ import { useI18n } from 'vue-i18n'; import { tkhostdata, getCountryinfo } from "@/api/account"; const { t, locale } = useI18n(); +const countryStore = useCountryStore(); // Component State const queryFormData = ref({ @@ -263,7 +270,9 @@ const streamdialogVisible = ref(false); const streamdialogVisibletext = ref(false); const filterdialogVisible = ref(false); const textarea = ref(""); -const countryData = ref(""); +// 使用共享 store 的国家信息 +const countryData = computed(() => countryStore.countryData); +const isRefreshingCountry = computed(() => countryStore.isLoading); const userInfo = ref({}); const options = ref([]); @@ -311,7 +320,10 @@ const timerId = ref(null); // Lifecycle onMounted(async () => { userInfo.value = getUser() || { tenantId: 0, id: 0 }; - getIpInfo(); + + // 使用共享 store 初始化国家信息 + await countryStore.initCountryInfo(t); + getCountry(); getlist(); @@ -321,7 +333,7 @@ onMounted(async () => { // savedSettings might be object already if backend returned object, or string const data = typeof savedSettings === 'string' ? JSON.parse(savedSettings) : savedSettings; queryFormData.value = data; - + if (data.anchor_ids && data.anchor_ids.length > 0) { streamdialogVisibletext.value = true; textarea.value = data.anchor_ids.join("\n"); @@ -546,15 +558,9 @@ function openTikTok() { loginTikTok(); } -// IP / Country -const getIpInfo = async () => { - try { - const response = await fetch("https://ipapi.co/json/"); - if (response.ok) { - const data = await response.json(); - countryData.value = getCountryName(data.country); - } - } catch {} +// 刷新国家信息 - 使用共享 store +const refreshCountry = async () => { + await countryStore.refreshCountry(t); }; function getCountry() { diff --git a/src/views/tk/Workbenches.vue b/src/views/tk/Workbenches.vue index 7834417..6a1bc23 100644 --- a/src/views/tk/Workbenches.vue +++ b/src/views/tk/Workbenches.vue @@ -115,9 +115,15 @@

{{ $t('workbenchesSetup.workbenches') }}

-
{{ $t('workbenchesSetup.network') }}: {{ - locale - == 'zh' ? countryData : countryDataEN }}
+
+ {{ $t('workbenchesSetup.network') }}: + {{ locale == 'zh' ? countryData : countryDataEN }} + +
指定国家: