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 }}
+
+
指定国家: