Compare commits
	
		
			27 Commits
		
	
	
		
			eee25b9f47
			...
			7b16e3ed5c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 7b16e3ed5c | |||
| b9065d4fce | |||
| edc03710b0 | |||
| 0537b55c24 | |||
| 8fca906f0b | |||
| 6e49dcf58f | |||
| 9e94987255 | |||
| 93a685e6b7 | |||
| 94a8e3928f | |||
| 3d44efacb7 | |||
| 343a766312 | |||
| e6a446e39f | |||
| f928bdd39d | |||
| 2ef1f80014 | |||
| 1e8c629266 | |||
| 0accb47375 | |||
| 85635c1b30 | |||
| 5be250f88d | |||
| 4f5b2585f4 | |||
| 9fde1d96de | |||
| 16e07600ff | |||
| 01ad40e3b9 | |||
| df697d79be | |||
| 302ba16c13 | |||
| 8bc6a746a5 | |||
| 5f407c5ee5 | |||
| a86a2f9ba8 | 
							
								
								
									
										157
									
								
								.augmentignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								.augmentignore
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,157 @@ | ||||
| # Node.js dependencies | ||||
| node_modules/ | ||||
| frontend/node_modules/ | ||||
|  | ||||
| # Build outputs and distributions | ||||
| dist/ | ||||
| build/ | ||||
| frontend/dist/ | ||||
| public/dist/ | ||||
|  | ||||
| # Electron binaries | ||||
| electron/dist/ | ||||
|  | ||||
| # Logs | ||||
| logs/ | ||||
| *.log | ||||
|  | ||||
| # Database files | ||||
| *.db | ||||
| *.sqlite | ||||
| *.sqlite3 | ||||
|  | ||||
| # Temporary files | ||||
| tmp/ | ||||
| temp/ | ||||
| .tmp/ | ||||
|  | ||||
| # Cache directories | ||||
| .cache/ | ||||
| .npm/ | ||||
| .yarn/ | ||||
|  | ||||
| # IDE and editor files | ||||
| .vscode/ | ||||
| .idea/ | ||||
| *.swp | ||||
| *.swo | ||||
| *~ | ||||
|  | ||||
| # OS generated files | ||||
| .DS_Store | ||||
| .DS_Store? | ||||
| ._* | ||||
| .Spotlight-V100 | ||||
| .Trashes | ||||
| ehthumbs.db | ||||
| Thumbs.db | ||||
|  | ||||
| # Package manager lock files (optional - uncomment if you want to exclude) | ||||
| package-lock.json | ||||
| pnpm-lock.yaml | ||||
| # yarn.lock | ||||
|  | ||||
| # Build tools and binaries | ||||
| app-builder-bin/ | ||||
| better-sqlite3/build/ | ||||
| bufferutil/build/ | ||||
| google-gax/build/ | ||||
|  | ||||
| # Large vendor libraries | ||||
| @google-cloud/ | ||||
| @electron/ | ||||
| javascript-obfuscator/dist/ | ||||
| @esbuild/ | ||||
|  | ||||
| # Development and test files | ||||
| coverage/ | ||||
| .nyc_output/ | ||||
| test-results/ | ||||
|  | ||||
| # Documentation build | ||||
| docs/build/ | ||||
| docs/dist/ | ||||
|  | ||||
| # Environment files with sensitive data | ||||
| .env.local | ||||
| .env.production | ||||
| .env.development | ||||
|  | ||||
| # Backup files | ||||
| *.bak | ||||
| *.backup | ||||
| *.old | ||||
|  | ||||
| # Binary executables and libraries | ||||
| *.exe | ||||
| *.dll | ||||
| *.so | ||||
| *.dylib | ||||
| *.bin | ||||
| *.dmg | ||||
| *.pkg | ||||
| *.msi | ||||
| *.deb | ||||
| *.rpm | ||||
| *.iobj | ||||
| *.ipdb | ||||
|  | ||||
| # Large media files (if any) | ||||
| *.mp4 | ||||
| *.avi | ||||
| *.mov | ||||
| *.wmv | ||||
| *.flv | ||||
| *.webm | ||||
| *.mkv | ||||
| *.m4v | ||||
|  | ||||
| # Archive files | ||||
| *.zip | ||||
| *.rar | ||||
| *.7z | ||||
| *.tar | ||||
| *.tar.gz | ||||
| *.tar.bz2 | ||||
|  | ||||
| # SSL certificates and keys | ||||
| ssl/ | ||||
| *.pem | ||||
| *.key | ||||
| *.crt | ||||
| *.cert | ||||
|  | ||||
| # Data directories that might contain large files | ||||
| data/ | ||||
| myUserData/ | ||||
| out/ | ||||
| run/ | ||||
| data/cache/ | ||||
| data/temp/ | ||||
| data/logs/ | ||||
|  | ||||
| # Specific large directories found in this project | ||||
| **/electron/dist/ | ||||
| **/app-builder-bin/ | ||||
| **/better-sqlite3/build/ | ||||
| **/bufferutil/build/ | ||||
| **/google-gax/build/ | ||||
| **/@google-cloud/translate/build/ | ||||
| **/@img/sharp-*/ | ||||
| **/javascript-obfuscator/dist/ | ||||
|  | ||||
| # Frontend build artifacts | ||||
| frontend/dist/ | ||||
| public/dist/ | ||||
|  | ||||
| # Any .git directories (if nested repos exist) | ||||
| **/.git/ | ||||
|  | ||||
| # Large dependency patterns | ||||
| **/@electron/ | ||||
| **/@esbuild/ | ||||
| **/element-plus/dist/ | ||||
| **/vite/dist/ | ||||
| **/rollup/dist/ | ||||
| **/protobufjs/dist/ | ||||
| **/vue/dist/ | ||||
							
								
								
									
										23
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								README.md
									
									
									
									
									
								
							| @ -1 +1,22 @@ | ||||
| Telegram 协议版多开 | ||||
| # 量子翻译应用 | ||||
|  | ||||
| Telegram 协议版多开翻译应用,支持多平台消息翻译和管理。 | ||||
|  | ||||
| ## 开发规范 | ||||
|  | ||||
| ### 多语言国际化 | ||||
| 本项目支持多语言切换,所有开发者必须遵循多语言开发规范: | ||||
|  | ||||
| 📖 **[多语言国际化开发规范](../INTERNATIONALIZATION_GUIDELINES.md)** | ||||
|  | ||||
| **重要提醒**: | ||||
| - 禁止在代码中硬编码中文或其他语言文本 | ||||
| - 所有用户可见的文本必须使用国际化机制 | ||||
| - 新功能开发前请先阅读国际化开发规范 | ||||
| - 提交代码前必须测试语言切换功能 | ||||
|  | ||||
| ### 核心要求 | ||||
| 1. **前端**:使用 Vue I18n 进行国际化 | ||||
| 2. **后端/主进程**:使用 `getI18nText()` 函数 | ||||
| 3. **webview**:实现语言更新函数和事件监听 | ||||
| 4. **测试**:确保语言切换后所有功能正常 | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| 'use strict'; | ||||
| "use strict"; | ||||
|  | ||||
| const path = require('path'); | ||||
| const { getBaseDir } = require('ee-core/ps'); | ||||
| const path = require("path"); | ||||
| const { getBaseDir } = require("ee-core/ps"); | ||||
|  | ||||
| /** | ||||
|  * 默认配置 | ||||
| @ -11,7 +11,7 @@ module.exports = () => { | ||||
|     openDevTools: false, | ||||
|     singleLock: true, | ||||
|     windowsOption: { | ||||
|       title: 'liangzi', | ||||
|       title: "liangzi", | ||||
|       width: 1000, | ||||
|       height: 700, | ||||
|       minWidth: 1000, | ||||
| @ -23,22 +23,22 @@ module.exports = () => { | ||||
|         nodeIntegration: true, | ||||
|         // preload: path.join(getElectronDir(),  'bridge.js'), | ||||
|       }, | ||||
|       titleBarStyle: 'hidden', | ||||
|       titleBarStyle: "hidden", | ||||
|       frame: true, | ||||
|       show: false, | ||||
|       icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'), | ||||
|       show: true, | ||||
|       icon: path.join(getBaseDir(), "public", "images", "logo-32.png"), | ||||
|     }, | ||||
|     logger: { | ||||
|       rotator: 'day', | ||||
|       level: 'INFO', | ||||
|       rotator: "day", | ||||
|       level: "INFO", | ||||
|       outputJSON: false, | ||||
|       appLogName: 'ee.log', | ||||
|       coreLogName: 'ee-core.log', | ||||
|       errorLogName: 'ee-error.log', | ||||
|       appLogName: "ee.log", | ||||
|       coreLogName: "ee-core.log", | ||||
|       errorLogName: "ee-error.log", | ||||
|     }, | ||||
|     remote: { | ||||
|       enable: false, | ||||
|       url: '' | ||||
|       url: "haiapp.org", | ||||
|     }, | ||||
|     socketServer: { | ||||
|       enable: false, | ||||
| @ -52,21 +52,21 @@ module.exports = () => { | ||||
|       cors: { | ||||
|         origin: true, | ||||
|       }, | ||||
|       channel: 'socket-channel' | ||||
|       channel: "socket-channel", | ||||
|     }, | ||||
|     httpServer: { | ||||
|       enable: false, | ||||
|       https: { | ||||
|         enable: false, | ||||
|         key: '/public/ssl/localhost+1.key', | ||||
|         cert: '/public/ssl/localhost+1.pem' | ||||
|         key: "/public/ssl/localhost+1.key", | ||||
|         cert: "/public/ssl/localhost+1.pem", | ||||
|       }, | ||||
|       host: '127.0.0.1', | ||||
|       host: "127.0.0.1", | ||||
|       port: 7071, | ||||
|     }, | ||||
|     mainServer: { | ||||
|       indexPath: '/public/dist/index.html', | ||||
|       channelSeparator: '/', | ||||
|     } | ||||
|   } | ||||
| } | ||||
|       indexPath: "/public/dist/index.html", | ||||
|       channelSeparator: "/", | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| 'use strict'; | ||||
| "use strict"; | ||||
|  | ||||
| /** | ||||
|  * Development environment configuration, coverage config.default.js | ||||
| @ -7,7 +7,12 @@ module.exports = () => { | ||||
|   return { | ||||
|     openDevTools: true, | ||||
|     jobs: { | ||||
|       messageLog: false | ||||
|     } | ||||
|       messageLog: false, | ||||
|     }, | ||||
|     api: { | ||||
|       baseUrl: "http://127.0.0.1:8000/api", | ||||
|       wsBaseUrl: "ws://127.0.0.1:8000", | ||||
|       timeout: 30000, | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| 'use strict'; | ||||
| "use strict"; | ||||
|  | ||||
| /** | ||||
|  *  coverage config.default.js | ||||
| @ -6,5 +6,11 @@ | ||||
| module.exports = () => { | ||||
|   return { | ||||
|     openDevTools: false, | ||||
|     // 生产环境 API 配置 | ||||
|     remote: { | ||||
|       enable: false, | ||||
|       //暂时当成接口endpoint使用 | ||||
|       url: "haiapp.org", | ||||
|     } | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| @ -1,32 +1,39 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| const { logger } = require('ee-core/log'); | ||||
| const {contactInfoService} = require("../service/contactInfo"); | ||||
|  | ||||
| const { contactInfoService } = require("../service/contactInfo"); | ||||
| const { app } = require("electron"); | ||||
| /** | ||||
|  * 联系人信息api | ||||
|  * @class | ||||
|  */ | ||||
| class ContactInfoController { | ||||
|  | ||||
|     async getContactInfo(args,event) { | ||||
|         return await contactInfoService.getContactInfo(args,event); | ||||
|     async getContactInfo(args, event) { | ||||
|         return await contactInfoService.getContactInfo(args, event); | ||||
|     } | ||||
|     async updateContactInfo(args,event) { | ||||
|         return await contactInfoService.updateContactInfo(args,event); | ||||
|     async updateContactInfo(args, event) { | ||||
|         return await contactInfoService.updateContactInfo(args, event); | ||||
|     } | ||||
|  | ||||
|     async getFollowRecord(args,event) { | ||||
|         return await contactInfoService.getFollowRecord(args,event); | ||||
|     async getFollowRecord(args, event) { | ||||
|         return await contactInfoService.getFollowRecord(args, event); | ||||
|     } | ||||
|     async addFollowRecord(args,event) { | ||||
|         return await contactInfoService.addFollowRecord(args,event); | ||||
|     async addFollowRecord(args, event) { | ||||
|         return await contactInfoService.addFollowRecord(args, event); | ||||
|     } | ||||
|     async updateFollowRecord(args,event) { | ||||
|         return await contactInfoService.updateFollowRecord(args,event); | ||||
|     async updateFollowRecord(args, event) { | ||||
|         return await contactInfoService.updateFollowRecord(args, event); | ||||
|     } | ||||
|     async deleteFollowRecord(args,event) { | ||||
|         return await contactInfoService.deleteFollowRecord(args,event); | ||||
|     async deleteFollowRecord(args, event) { | ||||
|         return await contactInfoService.deleteFollowRecord(args, event); | ||||
|     } | ||||
|  | ||||
|     async updateContactRemark(args, event) { | ||||
|         let view = app.viewsMap.get(args.partitionId); | ||||
|         if (view && !view.webContents.isDestroyed()) { | ||||
|             view.webContents.send("message-from-main", args.nickName); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| ContactInfoController.toString = () => '[class ContactInfoController]'; | ||||
|  | ||||
| @ -61,6 +61,10 @@ class SystemController { | ||||
|     async changePassword(args, event) { | ||||
|         return await systemService.changePassword(args, event); | ||||
|     } | ||||
|  | ||||
|     async captureScreen(args, event) { | ||||
|         return await systemService.captureScreen(args, event); | ||||
|     } | ||||
| } | ||||
| SystemController.toString = () => '[class SystemController]'; | ||||
|  | ||||
|  | ||||
| @ -56,11 +56,36 @@ class TranslateController { | ||||
|     async translateText(args,event) { | ||||
|         return await translateService.translateText(args,event); | ||||
|     } | ||||
|     async refreshTranslateRoutes(args,event) { | ||||
|         return await translateService.refreshTranslateRoutes(args,event); | ||||
|     } | ||||
|  | ||||
|     async createTranslateConfig(args,event) { | ||||
|         return await translateService.createTranslateConfig(args,event); | ||||
|     } | ||||
|  | ||||
|     async listNote(args,event) { | ||||
|         return await translateService.listNote(args,event); | ||||
|     } | ||||
|     async createNote(args,event) { | ||||
|         return await translateService.createNote(args,event); | ||||
|     } | ||||
|     async updateNote(args,event) { | ||||
|         return await translateService.updateNote(args,event); | ||||
|     } | ||||
|     async deleteNote(args,event) { | ||||
|         return await translateService.deleteNote(args,event); | ||||
|     } | ||||
|  | ||||
|     async clearTranslateCache(args, event) { | ||||
|         return await translateService.clearTranslateCache(args, event); | ||||
|     } | ||||
|  | ||||
|     async refreshSessionTranslateButtons(args, event) { | ||||
|         return await translateService.refreshSessionTranslateButtons(args, event); | ||||
|     } | ||||
|  | ||||
|  | ||||
| } | ||||
| TranslateController.toString = () => '[class TranslateController]'; | ||||
|  | ||||
|  | ||||
| @ -79,6 +79,10 @@ class WindowController { | ||||
|   async saveProxyInfo(args, event) { | ||||
|     return await windowService.saveProxyInfo(args, event); | ||||
|   } | ||||
|   // 测试代理连通性 | ||||
|   async testProxy(args, event) { | ||||
|     return await windowService.testProxy(args, event); | ||||
|   } | ||||
|   //打开当前会话控制台 | ||||
|   async openSessionDevTools(args, event) { | ||||
|     return await windowService.openSessionDevTools(args, event); | ||||
|  | ||||
| @ -25,7 +25,7 @@ app.register("preload", preload); | ||||
| // 现在 customUserDataPath 安装目录同级 | ||||
| // const customUserDataPath = path.join(appInstallDir, 'liangzi_data'); | ||||
| const customUserDataPath = path.join(path.resolve(process.cwd(), '..'), 'liangzi_data'); | ||||
| console.log('customUserDataPath', customUserDataPath); | ||||
|  | ||||
| electronApp.setPath('userData', customUserDataPath); | ||||
|  | ||||
| // run | ||||
|  | ||||
| @ -31,4 +31,26 @@ contextBridge.exposeInMainWorld("electronAPI", { | ||||
|   getLanguage: (args) => { | ||||
|     return ipcRenderer.invoke("get-language", args); | ||||
|   }, | ||||
|   // 增强监控功能的IPC方法 | ||||
|   avatarChangeNotify: (args) => { | ||||
|     return ipcRenderer.invoke("avatar-change-notify", args); | ||||
|   }, | ||||
|   profileChangeNotify: (args) => { | ||||
|     return ipcRenderer.invoke("profile-change-notify", args); | ||||
|   }, | ||||
|   statusChangeNotify: (args) => { | ||||
|     return ipcRenderer.invoke("status-change-notify", args); | ||||
|   }, | ||||
|   aboutChangeNotify: (args) => { | ||||
|     return ipcRenderer.invoke("about-change-notify", args); | ||||
|   }, | ||||
|   statusUpdateNotify: (args) => { | ||||
|     return ipcRenderer.invoke("status-update-notify", args); | ||||
|   }, | ||||
|   detectLanguage: (args) => { | ||||
|     return ipcRenderer.invoke("detect-language", args); | ||||
|   }, | ||||
|  | ||||
|  | ||||
|   onMessageFromMain: (callback) => ipcRenderer.on('message-from-main', (event, message) => callback(message)), | ||||
| }); | ||||
|  | ||||
| @ -163,6 +163,7 @@ const ipcMainListener = () => { | ||||
|       isFilter = "false", | ||||
|       mode, | ||||
|     } = args; | ||||
|     console.log("text-translate", args); | ||||
|     if (text && text.trim() && to && route) { | ||||
|       //查询判断翻译服务商是否支持这个编码 | ||||
|       const languageObj = await app.sdb.selectOne("language_list", { | ||||
| @ -236,13 +237,65 @@ const ipcMainListener = () => { | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // 语言设置变更处理 | ||||
|   ipcMain.handle("language-change", async (event, args) => { | ||||
|     const { language } = args; | ||||
|     if (language) { | ||||
|       // 存储语言设置到应用配置 | ||||
|       app.globalLanguage = language; | ||||
|  | ||||
|       // 通知所有webview更新语言设置 | ||||
|       const { BrowserWindow } = require('electron'); | ||||
|       const windows = BrowserWindow.getAllWindows(); | ||||
|  | ||||
|       for (const window of windows) { | ||||
|         const webContents = window.webContents; | ||||
|  | ||||
|         // 通知主窗口 | ||||
|         if (webContents === getMainWindow().webContents) { | ||||
|           webContents.send("global-language-changed", { language }); | ||||
|         } | ||||
|  | ||||
|         // 通知所有子视图 | ||||
|         for (const [partitionId, view] of app.viewsMap.entries()) { | ||||
|           if (view && !view.webContents.isDestroyed()) { | ||||
|             try { | ||||
|               await view.webContents.executeJavaScript(` | ||||
|                 if (window.updateLanguage) { | ||||
|                   window.updateLanguage('${language}'); | ||||
|                 } | ||||
|                 // 触发自定义事件 | ||||
|                 window.dispatchEvent(new CustomEvent('languageChanged', { | ||||
|                   detail: { language: '${language}' } | ||||
|                 })); | ||||
|               `); | ||||
|             } catch (error) { | ||||
|               console.warn(`Failed to update language for partition ${partitionId}:`, error); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       return { status: true, message: 'Language updated successfully' }; | ||||
|     } | ||||
|     return { status: false, message: 'Invalid language parameter' }; | ||||
|   }); | ||||
|  | ||||
|   // 获取当前语言设置 | ||||
|   ipcMain.handle("get-current-language", async (event, args) => { | ||||
|     return { | ||||
|       status: true, | ||||
|       data: { language: app.globalLanguage || 'zh' } | ||||
|     }; | ||||
|   }); | ||||
|  | ||||
|   //消息数量变更 | ||||
|   ipcMain.handle("msg-count-change", async (event, args) => { | ||||
|     logger.info("msg-count-change", args); | ||||
|     const { platform, msgCount } = args; | ||||
|     const senderId = event.sender.id; | ||||
|     const mainWin = getMainWindow(); | ||||
|      | ||||
|  | ||||
|     // 校验主窗口是否存在且未被销毁 | ||||
|     if (!mainWin || mainWin.isDestroyed()) { | ||||
|       return; // 主窗口不存在时终止处理 | ||||
| @ -299,6 +352,237 @@ const ipcMainListener = () => { | ||||
|   ipcMain.handle("get-ws-base-url", async (event, args) => { | ||||
|     return { status: true, data: app.wsBaseUrl }; | ||||
|   }); | ||||
|  | ||||
|  | ||||
|   // 语言检测(转发到后端) | ||||
|   ipcMain.handle("detect-language", async (event, args) => { | ||||
|     try { | ||||
|       const text = args?.text; | ||||
|       const texts = args?.texts; | ||||
|       const params = texts ? {} : { params: { text } }; | ||||
|       const url = app.baseUrl + '/detect_language'; | ||||
|       const { get, post } = require('axios'); | ||||
|       let res; | ||||
|       if (texts) { | ||||
|         res = await post(url, { texts }, { timeout: 15000 }); | ||||
|       } else { | ||||
|         res = await get(url, params, { timeout: 10000 }); | ||||
|       } | ||||
|       return res.data || { code: 5000, message: 'no data' }; | ||||
|     } catch (e) { | ||||
|       return { code: 5000, message: String(e) }; | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // 增强监控功能的IPC处理器 | ||||
|  | ||||
|   // 头像变化通知 | ||||
|   ipcMain.handle("avatar-change-notify", async (event, args) => { | ||||
|     logger.info("avatar-change-notify", args); | ||||
|     const { platform, phoneNumber, oldAvatarUrl, newAvatarUrl, changeTime } = args; | ||||
|     const senderId = event.sender.id; | ||||
|     const mainWin = getMainWindow(); | ||||
|  | ||||
|     if (!mainWin || mainWin.isDestroyed()) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // 记录头像变化到数据库 | ||||
|     try { | ||||
|       await app.sdb.insert("follow_record", { | ||||
|         userId: phoneNumber, | ||||
|         platform: platform, | ||||
|         content: JSON.stringify({ | ||||
|           type: "avatar_change", | ||||
|           oldAvatarUrl: oldAvatarUrl, | ||||
|           newAvatarUrl: newAvatarUrl, | ||||
|           changeTime: changeTime | ||||
|         }), | ||||
|         timestamp: changeTime | ||||
|       }); | ||||
|  | ||||
|       console.log(`${platform}: 头像变化已记录 - ${phoneNumber}`); | ||||
|  | ||||
|       // 通知前端 | ||||
|       if (!mainWin.isDestroyed()) { | ||||
|         mainWin.webContents.send("avatar-change-notify", { | ||||
|           platform, | ||||
|           phoneNumber, | ||||
|           oldAvatarUrl, | ||||
|           newAvatarUrl, | ||||
|           changeTime | ||||
|         }); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       logger.error("记录头像变化失败", error); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // 用户资料变化通知 | ||||
|   ipcMain.handle("profile-change-notify", async (event, args) => { | ||||
|     logger.info("profile-change-notify", args); | ||||
|     const { platform, changeType, oldValue, newValue, changeTime } = args; | ||||
|     const senderId = event.sender.id; | ||||
|     const mainWin = getMainWindow(); | ||||
|  | ||||
|     if (!mainWin || mainWin.isDestroyed()) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       await app.sdb.insert("follow_record", { | ||||
|         userId: "current_user", // 可以根据需要调整 | ||||
|         platform: platform, | ||||
|         content: JSON.stringify({ | ||||
|           type: "profile_change", | ||||
|           changeType: changeType, | ||||
|           oldValue: oldValue, | ||||
|           newValue: newValue, | ||||
|           changeTime: changeTime | ||||
|         }), | ||||
|         timestamp: changeTime | ||||
|       }); | ||||
|  | ||||
|       console.log(`${platform}: 用户资料变化已记录 - ${changeType}`); | ||||
|  | ||||
|       // 通知前端 | ||||
|       if (!mainWin.isDestroyed()) { | ||||
|         mainWin.webContents.send("profile-change-notify", { | ||||
|           platform, | ||||
|           changeType, | ||||
|           oldValue, | ||||
|           newValue, | ||||
|           changeTime | ||||
|         }); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       logger.error("记录用户资料变化失败", error); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // 状态变化通知 | ||||
|   ipcMain.handle("status-change-notify", async (event, args) => { | ||||
|     logger.info("status-change-notify", args); | ||||
|     const { platform, changeType, oldValue, newValue, changeTime } = args; | ||||
|     const senderId = event.sender.id; | ||||
|     const mainWin = getMainWindow(); | ||||
|  | ||||
|     if (!mainWin || mainWin.isDestroyed()) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       await app.sdb.insert("follow_record", { | ||||
|         userId: "current_user", | ||||
|         platform: platform, | ||||
|         content: JSON.stringify({ | ||||
|           type: "status_change", | ||||
|           changeType: changeType, | ||||
|           oldValue: oldValue, | ||||
|           newValue: newValue, | ||||
|           changeTime: changeTime | ||||
|         }), | ||||
|         timestamp: changeTime | ||||
|       }); | ||||
|  | ||||
|       console.log(`${platform}: 状态变化已记录 - ${changeType}`); | ||||
|  | ||||
|       // 通知前端 | ||||
|       if (!mainWin.isDestroyed()) { | ||||
|         mainWin.webContents.send("status-change-notify", { | ||||
|           platform, | ||||
|           changeType, | ||||
|           oldValue, | ||||
|           newValue, | ||||
|           changeTime | ||||
|         }); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       logger.error("记录状态变化失败", error); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // 关于信息变化通知 | ||||
|   ipcMain.handle("about-change-notify", async (event, args) => { | ||||
|     logger.info("about-change-notify", args); | ||||
|     const { platform, changeType, oldValue, newValue, changeTime } = args; | ||||
|     const senderId = event.sender.id; | ||||
|     const mainWin = getMainWindow(); | ||||
|  | ||||
|     if (!mainWin || mainWin.isDestroyed()) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       await app.sdb.insert("follow_record", { | ||||
|         userId: "current_user", | ||||
|         platform: platform, | ||||
|         content: JSON.stringify({ | ||||
|           type: "about_change", | ||||
|           changeType: changeType, | ||||
|           oldValue: oldValue, | ||||
|           newValue: newValue, | ||||
|           changeTime: changeTime | ||||
|         }), | ||||
|         timestamp: changeTime | ||||
|       }); | ||||
|  | ||||
|       console.log(`${platform}: 关于信息变化已记录 - ${changeType}`); | ||||
|  | ||||
|       // 通知前端 | ||||
|       if (!mainWin.isDestroyed()) { | ||||
|         mainWin.webContents.send("about-change-notify", { | ||||
|           platform, | ||||
|           changeType, | ||||
|           oldValue, | ||||
|           newValue, | ||||
|           changeTime | ||||
|         }); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       logger.error("记录关于信息变化失败", error); | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   // 状态更新通知(动态/Stories) | ||||
|   ipcMain.handle("status-update-notify", async (event, args) => { | ||||
|     logger.info("status-update-notify", args); | ||||
|     const { platform, updateType, timestamp, details } = args; | ||||
|     const senderId = event.sender.id; | ||||
|     const mainWin = getMainWindow(); | ||||
|  | ||||
|     if (!mainWin || mainWin.isDestroyed()) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       await app.sdb.insert("follow_record", { | ||||
|         userId: "current_user", | ||||
|         platform: platform, | ||||
|         content: JSON.stringify({ | ||||
|           type: "status_update", | ||||
|           updateType: updateType, | ||||
|           details: details, | ||||
|           timestamp: timestamp | ||||
|         }), | ||||
|         timestamp: timestamp | ||||
|       }); | ||||
|  | ||||
|       console.log(`${platform}: 状态更新已记录 - ${updateType}`); | ||||
|  | ||||
|       // 通知前端 | ||||
|       if (!mainWin.isDestroyed()) { | ||||
|         mainWin.webContents.send("status-update-notify", { | ||||
|           platform, | ||||
|           updateType, | ||||
|           timestamp, | ||||
|           details | ||||
|         }); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       logger.error("记录状态更新失败", error); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  | ||||
| @ -9,13 +9,13 @@ const { getTimeStr } = require("../utils/CommonUtils"); | ||||
| // const { initUpdater } = require("../updater"); | ||||
| const { checkUpdate } = require("../updater/index"); | ||||
| const { initializeTranslationCodes } = require("./iniit_language"); | ||||
| const { post, get, put } = require("axios"); | ||||
|  | ||||
| const endpoint=process.env.BASE_URL || "haiapp.org" | ||||
| const wsBaseUrl = `ws://${endpoint}`; | ||||
| // const wsBaseUrl = `ws://192.168.2.22:8000`; | ||||
| // const baseUrl = "http://192.168.2.22:8000/api"; | ||||
| // const baseUrl = "http://haiapp.org/api"; | ||||
| const baseUrl =`http://${endpoint}/api`; | ||||
| // 从配置文件获取API配置 | ||||
| // const { remote } = getConfig(); | ||||
| // const endpoint =remote ? remote.url:"hiapp.org" | ||||
| // const wsBaseUrl = `ws://${endpoint}`; | ||||
| // const baseUrl = `http://${endpoint}}/api`; | ||||
| const initializeDatabase = async () => { | ||||
|   // 定义表结构 | ||||
|   const tables = { | ||||
| @ -72,8 +72,11 @@ const initializeDatabase = async () => { | ||||
|         chineseDetectionStatus: "TEXT", | ||||
|         translatePreview: "TEXT", | ||||
|         interceptChinese: "TEXT", | ||||
|         interceptLanguages: "TEXT", | ||||
|         translateHistory: "TEXT", | ||||
|         autoTranslateGroupMessage: "TEXT", | ||||
|         historyTranslateRoute: "TEXT", | ||||
|         usePersonalConfig: 'TEXT DEFAULT "true"', | ||||
|       }, | ||||
|       constraints: [], | ||||
|     }, | ||||
| @ -84,6 +87,7 @@ const initializeDatabase = async () => { | ||||
|         zhName: "TEXT", | ||||
|         enName: "TEXT", | ||||
|         otherArgs: "TEXT", | ||||
|         enable: "INTEGER", | ||||
|       }, | ||||
|       constraints: [], | ||||
|     }, | ||||
| @ -126,9 +130,9 @@ const initializeDatabase = async () => { | ||||
|         timestamp: "TEXT", | ||||
|         tengXun: "TEXT", | ||||
|         deepl: "TEXT", | ||||
|         deepseek:"TEXT", | ||||
|         deepseek: "TEXT", | ||||
|         bing: "TEXT", | ||||
|         chatGpt4o:"TEXT" | ||||
|         chatGpt4o: "TEXT", | ||||
|       }, | ||||
|       constraints: [], | ||||
|     }, | ||||
| @ -227,80 +231,119 @@ const initializeTableData = async () => { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   // 初始化翻译线路 | ||||
|   const translationRoute = [ | ||||
|     { | ||||
|       name: "deepl", | ||||
|       zhName: "DeepL翻译", | ||||
|       enName: "DeepL Translate", | ||||
|       otherArgs: `{"apiUrl": "https://api-free.deepl.com/v2/translate","apiKey": "","secretKey": ""}`, | ||||
|     }, | ||||
|     { | ||||
|       name: "deepseek", | ||||
|       zhName: "DeepSeek翻译", | ||||
|       enName: "DeepSeek Translate", | ||||
|       otherArgs: `{"apiUrl": "https://api.deepseek.com/api/v1/translate","apiKey": "","secretKey": ""}`, | ||||
|     }, | ||||
|     { | ||||
|       name: "youDao", | ||||
|       zhName: "有道翻译", | ||||
|       enName: "Youdao Translate", | ||||
|       otherArgs: `{"apiUrl": "https://openapi.youdao.com/api","appId": "","apiKey": ""}`, | ||||
|     }, | ||||
|     { | ||||
|       name: "tengXun", | ||||
|       zhName: "腾讯翻译", | ||||
|       enName: "Tencent Translate", | ||||
|       otherArgs: `{"apiUrl": "https://fanyi.qq.com/api","appId": "","apiKey": ""}`, | ||||
|     },{ | ||||
|       name: "baidu", | ||||
|       zhName: "百度翻译", | ||||
|       enName: "Baidu Translate", | ||||
|       otherArgs: `{"apiUrl": "https://fanyi.baidu.com/v2transapi","appId": "","apiKey": "","secretKey": ""}`, | ||||
|     },{ | ||||
|       name:"huoShan", | ||||
|       zhName:"火山翻译", | ||||
|       enName:"HuoShan Translate", | ||||
|       otherArgs:`{"apiUrl": "https://api.hushan.ai/v1/translation","appId": "","apiKey": "","secretKey": ""}`, | ||||
|     },{ | ||||
|       name:"google", | ||||
|       zhName:"谷歌翻译", | ||||
|       enName:"Google Translate", | ||||
|       otherArgs:`{"apiUrl": "https://translation.googleapis.com/language/translate/v2","appId": "","apiKey": "","secretKey": ""}`, | ||||
|     },{ | ||||
|       name:"bing", | ||||
|       zhName:"必应翻译", | ||||
|       enName:"Bing Translate", | ||||
|       otherArgs:`{"apiUrl": "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0","appId": "","apiKey": "","secretKey": ""}`, | ||||
|     },{ | ||||
|       name:"chatGpt4o", | ||||
|       zhName:"ChatGpt4o翻译", | ||||
|       enName:"ChatGpt4o Translate", | ||||
|       otherArgs:`{"apiUrl": "https://api.chatgpt4o.com/translate","appId": "","apiKey": "","secretKey": ""}`, | ||||
|     } | ||||
|   ]; | ||||
|   const { remote } = getConfig(); | ||||
|   const endpoint = remote ? remote.url : "hiapp.org"; | ||||
|   const baseUrl = `http://${endpoint}/api`; | ||||
|   const url = baseUrl + "/translate/list_route"; | ||||
|   console.log("url", url); | ||||
|   const res = await get(url, {}, { timeout: 30000 }); | ||||
|   console.log("res======:", res); | ||||
|  | ||||
|   let translationRoute = []; | ||||
|   const { code, data } = res.data; | ||||
|   if (code === 2000) { | ||||
|     translationRoute = data; | ||||
|   } else { | ||||
|     // 初始化翻译线路 | ||||
|     translationRoute = [ | ||||
|       { | ||||
|         name: "deepl", | ||||
|         zhName: "DeepL翻译", | ||||
|         enName: "DeepL Translate", | ||||
|         otherArgs: `{"apiUrl": "https://api-free.deepl.com/v2/translate","apiKey": "","secretKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: "deepseek", | ||||
|         zhName: "DeepSeek翻译", | ||||
|         enName: "DeepSeek Translate", | ||||
|         otherArgs: `{"apiUrl": "https://api.deepseek.com/api/v1/translate","apiKey": "","secretKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: "youDao", | ||||
|         zhName: "有道翻译", | ||||
|         enName: "Youdao Translate", | ||||
|         otherArgs: `{"apiUrl": "https://openapi.youdao.com/api","appId": "","apiKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: "xiaoNiu", | ||||
|         zhName: "小牛翻译", | ||||
|         enName: "XiaoNiu Translate", | ||||
|         otherArgs: `{"apiUrl": "https://api.xiaoniu.com/translate","appId": "","apiKey": "","secretKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: "tengXun", | ||||
|         zhName: "腾讯翻译", | ||||
|         enName: "Tencent Translate", | ||||
|         otherArgs: `{"apiUrl": "https://fanyi.qq.com/api","appId": "","apiKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: "baidu", | ||||
|         zhName: "百度翻译", | ||||
|         enName: "Baidu Translate", | ||||
|         otherArgs: `{"apiUrl": "https://fanyi.baidu.com/v2transapi","appId": "","apiKey": "","secretKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: "huoShan", | ||||
|         zhName: "火山翻译", | ||||
|         enName: "HuoShan Translate", | ||||
|         otherArgs: `{"apiUrl": "https://api.hushan.ai/v1/translation","appId": "","apiKey": "","secretKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: "google", | ||||
|         zhName: "谷歌翻译", | ||||
|         enName: "Google Translate", | ||||
|         otherArgs: `{"apiUrl": "https://translation.googleapis.com/language/translate/v2","appId": "","apiKey": "","secretKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: "bing", | ||||
|         zhName: "必应翻译", | ||||
|         enName: "Bing Translate", | ||||
|         otherArgs: `{"apiUrl": "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0","appId": "","apiKey": "","secretKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|       { | ||||
|         name: "chatGpt4o", | ||||
|         zhName: "ChatGpt4o翻译", | ||||
|         enName: "ChatGpt4o Translate", | ||||
|         otherArgs: `{"apiUrl": "https://api.chatgpt4o.com/translate","appId": "","apiKey": "","secretKey": ""}`, | ||||
|         enable: 1, | ||||
|       }, | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   // 清空translate_route表 | ||||
|   await app.sdb.deleteAll("translate_route"); | ||||
|  | ||||
|    | ||||
|   for (let item of translationRoute) { | ||||
|     const args = { | ||||
|       name: item.name, | ||||
|       zhName: item.zhName, | ||||
|       enName: item.enName, | ||||
|       otherArgs: item.otherArgs, | ||||
|       enable: item.enable, | ||||
|     }; | ||||
|     const route = await app.sdb.selectOne("translate_route", { | ||||
|       name: item.name, | ||||
|     }); | ||||
|     if (!route) { | ||||
|       // 初始化翻译线路 | ||||
|       await app.sdb.insert("translate_route", args); | ||||
|     } | ||||
|     await app.sdb.insert("translate_route", args); | ||||
|     // const route = await app.sdb.selectOne("translate_route", { | ||||
|     //   name: item.name, | ||||
|     // }); | ||||
|     // if (!route) { | ||||
|     //   // 初始化翻译线路 | ||||
|     //   await app.sdb.insert("translate_route", args); | ||||
|     // } | ||||
|   } | ||||
|   //初始化翻译编码信息 | ||||
|   // 插入中文(简体) | ||||
|   await initializeTranslationCodes(app.sdb); | ||||
|  | ||||
|   //1.0.51单独设置数据  | ||||
|   //1.0.51单独设置数据 | ||||
|   //  await app.sdb.update("translate_config",{translateRoute:"youDao"},{}); | ||||
| }; | ||||
| class Lifecycle { | ||||
| @ -315,13 +358,21 @@ class Lifecycle { | ||||
|    * electron app ready | ||||
|    */ | ||||
|   async electronAppReady() { | ||||
|     logger.info("[lifecycle] electron-app-ready"); | ||||
|     logger.info("[lifecycle] ---- electron-app-ready"); | ||||
|     const { remote } = getConfig(); | ||||
|  | ||||
|     const endpoint = remote ? remote.url : "hiapp.org"; | ||||
|     const wsBaseUrl = `ws://${endpoint}`; | ||||
|     const baseUrl = `http://${endpoint}/api`; | ||||
|     // 将配置保存到app对象供其他模块使用 | ||||
|     app.baseUrl = baseUrl; | ||||
|     app.wsBaseUrl = wsBaseUrl; | ||||
|  | ||||
|     await initializeDatabase(); | ||||
|     await initializePlatform(); | ||||
|     await initializeTableData(); | ||||
|     app.viewsMap = new Map(); | ||||
|     app.baseUrl = baseUrl; | ||||
|     app.wsBaseUrl = wsBaseUrl; | ||||
|  | ||||
|   } | ||||
|   ready; | ||||
|   /** | ||||
|  | ||||
| @ -6,6 +6,29 @@ const quickReply = async (args)=>{ | ||||
|  | ||||
| console.log("指纹屏蔽脚本执行"); | ||||
|  | ||||
| // 全局语言设置 | ||||
| window.currentAppLanguage = 'zh'; // 默认中文 | ||||
|  | ||||
| // 语言更新函数 | ||||
| window.updateLanguage = function(language) { | ||||
|   window.currentAppLanguage = language; | ||||
|   console.log('CustomWeb language updated to:', language); | ||||
|  | ||||
|   // 更新页面语言设置 | ||||
|   document.documentElement.lang = language; | ||||
|  | ||||
|   // 触发自定义语言更新事件 | ||||
|   window.dispatchEvent(new CustomEvent('appLanguageChanged', { | ||||
|     detail: { language } | ||||
|   })); | ||||
| }; | ||||
|  | ||||
| // 监听语言变更事件 | ||||
| window.addEventListener('languageChanged', function(event) { | ||||
|   const { language } = event.detail; | ||||
|   window.updateLanguage(language); | ||||
| }); | ||||
|  | ||||
| // 禁用webdriver | ||||
| Object.defineProperty(navigator, "webdriver", { | ||||
|   get: () => false, | ||||
|  | ||||
| @ -152,12 +152,94 @@ const getNewMsgCount = () => { | ||||
| onlineStatusCheck(); | ||||
| //========================用户基本信息获取结束============ | ||||
|  | ||||
| //中文检测 | ||||
| const containsChinese = (text)=> { | ||||
|     const regex = /[\u4e00-\u9fa5]/;  // 匹配中文字符的正则表达式 | ||||
|     return regex.test(text);  // 如果包含中文字符返回 true,否则返回 false | ||||
| // 语言检测与拦截支持 | ||||
| const containsChinese = (text)=> /[\u4e00-\u9fa5]/.test(text); | ||||
| const containsJapanese = (text)=> /[\u3040-\u30ff]/.test(text); | ||||
| const containsKorean = (text)=> /[\uac00-\ud7af]/i.test(text); | ||||
| const containsArabic = (text)=> /[\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF]/.test(text); | ||||
| const containsRussian = (text)=> /[\u0400-\u04FF\u0500-\u052F]/.test(text); | ||||
| const isLikelyEnglish = (text)=> { | ||||
|   if (!text) return false; | ||||
|   if (/[^\x00-\x7F]/.test(text)) return false; | ||||
|   return /[A-Za-z]/.test(text); | ||||
| } | ||||
| const sendMsg = ()=> { | ||||
| const detectLanguageSet = (text = '') => { | ||||
|   const set = new Set(); | ||||
|   if (!text) return set; | ||||
|   if (containsChinese(text)) set.add('zh'); | ||||
|   if (containsJapanese(text)) set.add('ja'); | ||||
|   if (containsKorean(text)) set.add('ko'); | ||||
|   if (containsArabic(text)) set.add('ar'); | ||||
|   if (containsRussian(text)) set.add('ru'); | ||||
|   if (isLikelyEnglish(text)) set.add('en'); | ||||
|   if (/[àâäæçéèêëîïôœùûüÿñ¡¿]/i.test(text)) { | ||||
|     if (/ñ|¡|¿|á|é|í|ó|ú/i.test(text)) set.add('es'); | ||||
|     if (/ç|à|â|ä|é|è|ê|ë|î|ï|ô|œ|ù|û|ü|ÿ/i.test(text)) set.add('fr'); | ||||
|     if (/ã|õ|á|é|í|ó|ú/i.test(text)) set.add('pt'); | ||||
|   } | ||||
|   return set; | ||||
| }; | ||||
| const parseInterceptLanguages = (val) => { | ||||
|   if (!val) return []; | ||||
|   if (Array.isArray(val)) return val.filter(Boolean); | ||||
|   return String(val).split(',').map(s=>s.trim()).filter(Boolean); | ||||
| }; | ||||
| const shouldInterceptByLanguages = (text) => { | ||||
|   if (!trcConfig || trcConfig.interceptChinese !== 'true') return false; | ||||
|   const list = parseInterceptLanguages(trcConfig.interceptLanguages); | ||||
|   // 修复:如果没有选择任何语言,则不拦截任何内容 | ||||
|   if (list.length === 0) return false; | ||||
|   const targets = list; | ||||
|   const found = detectLanguageSet(text); | ||||
|   return targets.some(code=>found.has(code)); | ||||
| }; | ||||
|  | ||||
| // 详细语言拦截检查:返回是否拦截及原因 | ||||
| const checkInterceptLanguage = async (text) => { | ||||
|   try { | ||||
|     if (!trcConfig || trcConfig.interceptChinese !== 'true') return { blocked: false }; | ||||
|     const list = parseInterceptLanguages(trcConfig.interceptLanguages); | ||||
|     // 修复:如果没有选择任何语言,则不拦截任何内容 | ||||
|     if (list.length === 0) return { blocked: false }; | ||||
|     const targets = list; | ||||
|     const res = await ipc.detectLanguage({ text }); | ||||
|     const lang = String(res?.data?.lang || '').toLowerCase(); | ||||
|     if (lang && targets.includes(lang)) { | ||||
|       return { blocked: true, reason: `语言 ${lang} 在拦截列表(${targets.join(',')})` }; | ||||
|     } | ||||
|     const found = detectLanguageSet(text); | ||||
|     for (const code of targets) { | ||||
|       if (found.has(code)) return { blocked: true, reason: `检测到可能的语言 ${code} 在拦截列表(${targets.join(',')})` }; | ||||
|     } | ||||
|   } catch (e) {} | ||||
|   return { blocked: false }; | ||||
| }; | ||||
|  | ||||
| // 错误消息检测:拦截把错误当消息发送 | ||||
| const isErrorText = (s) => { | ||||
|   const patterns = [ | ||||
|     'HTTPConnectionPool', 'timeout', '超时', '错误', 'Error', | ||||
|   ]; | ||||
|   return patterns.some((p) => s.includes(p)); | ||||
| }; | ||||
| const sendMsg = async ()=> { | ||||
|     // 最终文本验证(含错误/空文本/语言拦截),仅当开启拦截开关时检查语言 | ||||
|     { | ||||
|         // 获取最终文本 | ||||
|         const editableDiv = document.getElementById('editable-message-text'); | ||||
|         const content = editableDiv?.textContent?.trim() || ''; | ||||
|  | ||||
|         // 先拦截错误与空消息(始终启用) | ||||
|         const s = content; | ||||
|         if (!s || !s.trim()) { alert('已拦截:消息为空或仅包含空白'); return; } | ||||
|         if (isErrorText(s)) { alert('已拦截:检测到错误内容(翻译失败或错误信息)'); return; } | ||||
|         // 再按配置拦截语言 | ||||
|         if (trcConfig.interceptChinese === 'true') { | ||||
|             const chk = await checkInterceptLanguage(s); | ||||
|             if (chk.blocked) { alert(`已拦截:${chk.reason}`); return; } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let sendButton = document.querySelectorAll('button.Button.send.main-button.default.secondary.round.click-allowed')[0] | ||||
|     if (sendButton) { | ||||
|         sendButton.click(); | ||||
| @ -178,7 +260,8 @@ const sendTranslate = async (text,to)=>{ | ||||
|             styledTextarea.setTranslateStatus(true) | ||||
|             styledTextarea.setIsProcessing(false); | ||||
|         }else { | ||||
|             styledTextarea.setContent(res.message); | ||||
|             alert('翻译失败,未发送。请稍后重试或更换翻译通道。'); | ||||
|             styledTextarea.setContent('...'); | ||||
|             styledTextarea.setTranslateStatus(false) | ||||
|             styledTextarea.setIsProcessing(false); | ||||
|         } | ||||
| @ -350,7 +433,7 @@ const addTranslateListener = () => { | ||||
|                 if (status === true && isProcess === false) { | ||||
|                     const translateText = styledTextarea.getContent(); | ||||
|                     await inputMsg(translateText) | ||||
|                     sendMsg() | ||||
|                     await sendMsg() | ||||
|                     styledTextarea.setTranslateStatus(false) | ||||
|                     styledTextarea.setContent('...') | ||||
|                     return; | ||||
| @ -366,13 +449,14 @@ const addTranslateListener = () => { | ||||
|                     const translateText = res.data; | ||||
|                     await inputMsg(translateText) | ||||
|                     styledTextarea.setContent(translateText); | ||||
|                     setTimeout(()=>{ | ||||
|                     setTimeout(async ()=>{ | ||||
|                         isProcessing = false; | ||||
|                         sendMsg() | ||||
|                         await sendMsg() | ||||
|                         styledTextarea.setContent('...'); | ||||
|                     },500) | ||||
|                 }else { | ||||
|                     styledTextarea.setContent(res.message); | ||||
|                     alert('翻译失败,未发送。请稍后重试或更换翻译通道。'); | ||||
|                     styledTextarea.setContent('...'); | ||||
|                     isProcessing = false; | ||||
|                     return; | ||||
|                 } | ||||
| @ -384,7 +468,7 @@ const addTranslateListener = () => { | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|             sendMsg(); | ||||
|             await sendMsg(); | ||||
|         } | ||||
|     },true); | ||||
|  | ||||
| @ -551,6 +635,40 @@ const monitorMainNode = ()=> { | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|     // 检查并显示已有的翻译缓存 | ||||
|     const checkAndDisplayExistingTranslation = async (text, leftDiv, rightDiv) => { | ||||
|         try { | ||||
|             const to = trcConfig.receiveTargetLanguage; | ||||
|             // 调用翻译API,但设置isFilter为true,这样只会查询缓存不会实际翻译 | ||||
|             const res = await ipc.translateText({ | ||||
|                 route: trcConfig.historyTranslateRoute || trcConfig.translateRoute, | ||||
|                 text: text, | ||||
|                 from: trcConfig.receiveSourceLanguage, | ||||
|                 to: to, | ||||
|                 refresh: 'false', | ||||
|                 mode: trcConfig.mode, | ||||
|                 isFilter: 'true' // 关键:只查询缓存,不实际翻译 | ||||
|             }); | ||||
|  | ||||
|             if (res.status && res.data) { | ||||
|                 // 找到了缓存的翻译结果,显示它 | ||||
|                 leftDiv.innerHTML = res.data; | ||||
|                 leftDiv.style.color = 'var(--color-text)'; | ||||
|                 // 缓存存在时,刷新按钮正常显示 | ||||
|                 rightDiv.style.display = ''; | ||||
|             } else { | ||||
|                 // 没有缓存,显示默认状态 | ||||
|                 leftDiv.textContent = ''; | ||||
|                 rightDiv.style.display = ''; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.log('检查翻译缓存时出错:', error); | ||||
|             // 出错时显示默认状态 | ||||
|             leftDiv.textContent = ''; | ||||
|             rightDiv.style.display = ''; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const createTranslateButtonForMessage = async (msgSpan) => { | ||||
|         let text = getMsgText(msgSpan.parentNode); | ||||
|         if (!text) return; | ||||
| @ -589,13 +707,15 @@ const monitorMainNode = ()=> { | ||||
|             const from = trcConfig.receiveSourceLanguage; | ||||
|             const to = trcConfig.receiveTargetLanguage; | ||||
|             const mode = trcConfig.mode; | ||||
|             const res = await ipc.translateText({route: route, text: text,from:from, to: to,refresh:'true',mode:mode}); | ||||
|             // 修复:刷新按钮不自动清理缓存,保留翻译历史 | ||||
|             const res = await ipc.translateText({route: route, text: text,from:from, to: to,refresh:'false',mode:mode}); | ||||
|             if (res.status) { | ||||
|                 leftDiv.innerHTML = res.data; | ||||
|                 rightDiv.style.display = ''; | ||||
|             }else { | ||||
|                 leftDiv.style.color = 'red'; | ||||
|                 leftDiv.textContent = `${res.message}`; | ||||
|                 leftDiv.textContent = '翻译失败'; | ||||
|                 alert('翻译失败,未发送。请稍后重试或更换翻译通道。'); | ||||
|                 rightDiv.style.display = ''; | ||||
|             } | ||||
|         }); | ||||
| @ -607,6 +727,9 @@ const monitorMainNode = ()=> { | ||||
|         // 插入到消息元素右侧 | ||||
|         msgSpan.parentNode.insertBefore(translateDiv, msgSpan.nextSibling); | ||||
|  | ||||
|         // 检查是否已有翻译缓存并显示 | ||||
|         await checkAndDisplayExistingTranslation(text, leftDiv, rightDiv); | ||||
|  | ||||
|         const receiveTranslateStatus = trcConfig.receiveTranslateStatus; | ||||
|         logger.info('发送消息 当前配置:',trcConfig) | ||||
|         if (receiveTranslateStatus === 'true') { | ||||
|  | ||||
| @ -120,7 +120,8 @@ const sendTranslate = async (text,to)=>{ | ||||
|             styledTextarea.setTranslateStatus(true) | ||||
|             styledTextarea.setIsProcessing(false); | ||||
|         }else { | ||||
|             styledTextarea.setContent(res.message); | ||||
|             alert('翻译失败,未发送。请稍后重试或更换翻译通道。'); | ||||
|             styledTextarea.setContent('...'); | ||||
|             styledTextarea.setTranslateStatus(false) | ||||
|             styledTextarea.setIsProcessing(false); | ||||
|         } | ||||
| @ -320,7 +321,8 @@ const addTranslateListener = () => { | ||||
|                     styledTextarea.setTranslateStatus(false) | ||||
|                     styledTextarea.setContent('...') | ||||
|                 }else { | ||||
|                     styledTextarea.setContent(res.message); | ||||
|                     alert('翻译失败,未发送。请稍后重试或更换翻译通道。'); | ||||
|                     styledTextarea.setContent('...'); | ||||
|                     isProcessing = false; | ||||
|                 } | ||||
|             }else { | ||||
| @ -448,6 +450,40 @@ const monitorMainNode = ()=> { | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|     // 检查并显示已有的翻译缓存 | ||||
|     const checkAndDisplayExistingTranslation = async (text, leftDiv, rightDiv) => { | ||||
|         try { | ||||
|             const to = trcConfig.receiveTargetLanguage; | ||||
|             // 调用翻译API,但设置isFilter为true,这样只会查询缓存不会实际翻译 | ||||
|             const res = await ipc.translateText({ | ||||
|                 route: trcConfig.historyTranslateRoute || trcConfig.translateRoute, | ||||
|                 text: text, | ||||
|                 from: trcConfig.receiveSourceLanguage, | ||||
|                 to: to, | ||||
|                 refresh: 'false', | ||||
|                 mode: trcConfig.mode, | ||||
|                 isFilter: 'true' // 关键:只查询缓存,不实际翻译 | ||||
|             }); | ||||
|  | ||||
|             if (res.status && res.data) { | ||||
|                 // 找到了缓存的翻译结果,显示它 | ||||
|                 leftDiv.innerHTML = res.data; | ||||
|                 leftDiv.style.color = 'green'; | ||||
|                 // 缓存存在时,刷新按钮正常显示 | ||||
|                 rightDiv.style.display = ''; | ||||
|             } else { | ||||
|                 // 没有缓存,显示默认状态 | ||||
|                 leftDiv.textContent = ''; | ||||
|                 rightDiv.style.display = ''; | ||||
|             } | ||||
|         } catch (error) { | ||||
|             console.log('检查翻译缓存时出错:', error); | ||||
|             // 出错时显示默认状态 | ||||
|             leftDiv.textContent = ''; | ||||
|             rightDiv.style.display = ''; | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     //为每条消息创建翻译按钮 | ||||
|     const createTranslateButtonForMessage = async ( msgSpan) => { | ||||
|         let text = msgSpan?.childNodes[0].textContent; | ||||
| @ -486,13 +522,15 @@ const monitorMainNode = ()=> { | ||||
|             const from = trcConfig.receiveSourceLanguage; | ||||
|             const to = trcConfig.receiveTargetLanguage; | ||||
|             const mode = trcConfig.mode; | ||||
|             const res = await ipc.translateText({route: route, text: text,from:from, to: to,refresh:'true',mode:mode}); | ||||
|             // 修复:刷新按钮不自动清理缓存,保留翻译历史 | ||||
|             const res = await ipc.translateText({route: route, text: text,from:from, to: to,refresh:'false',mode:mode}); | ||||
|             if (res.status) { | ||||
|                 leftDiv.innerHTML = res.data; | ||||
|                 rightDiv.style.display = ''; | ||||
|             }else { | ||||
|                 leftDiv.style.color = 'red'; | ||||
|                 leftDiv.textContent = `${res.message}`; | ||||
|                 leftDiv.textContent = '翻译失败'; | ||||
|                 alert('翻译失败,未发送。请稍后重试或更换翻译通道。'); | ||||
|                 rightDiv.style.display = ''; | ||||
|             } | ||||
|         }); | ||||
| @ -502,6 +540,9 @@ const monitorMainNode = ()=> { | ||||
|         // 插入到消息元素右侧 | ||||
|         msgSpan.appendChild(translateDiv); | ||||
|  | ||||
|         // 检查是否已有翻译缓存并显示 | ||||
|         await checkAndDisplayExistingTranslation(text, leftDiv, rightDiv); | ||||
|  | ||||
|         const receiveTranslateStatus = trcConfig.receiveTranslateStatus; | ||||
|         if (receiveTranslateStatus === 'true') { | ||||
|             let text = msgSpan?.childNodes[0].textContent; | ||||
| @ -520,7 +561,8 @@ const monitorMainNode = ()=> { | ||||
|                 rightDiv.style.display = ''; | ||||
|             }else { | ||||
|                 leftDiv.style.color = 'red'; | ||||
|                 leftDiv.textContent = `${res.message}`; | ||||
|                 leftDiv.textContent = '翻译失败'; | ||||
|                 alert('翻译失败,未发送。请稍后重试或更换翻译通道。'); | ||||
|                 rightDiv.style.display = ''; | ||||
|             } | ||||
|         } | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -67,6 +67,10 @@ class ContactInfoService { | ||||
|       ); | ||||
|       // 返回最新配置(可选) | ||||
|       const updatedConfig = await app.sdb.selectOne('contact_info', { id:id }); | ||||
|  | ||||
|       // 同步到服务器 | ||||
|       await this._syncContactInfoToServer(updatedConfig); | ||||
|  | ||||
|       return { | ||||
|         status: true, | ||||
|         message:'更新成功', | ||||
| @ -76,6 +80,29 @@ class ContactInfoService { | ||||
|       return {status:true,message:error.message}; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 同步联系人信息到服务器 | ||||
|   async _syncContactInfoToServer(contactInfo) { | ||||
|     try { | ||||
|       const authInfo = app.authInfo; | ||||
|       if (!authInfo) return; // 如果没有认证信息,跳过同步 | ||||
|  | ||||
|       const url = app.baseUrl + '/sync_contact_info'; | ||||
|       const data = { | ||||
|         username: authInfo.userName, | ||||
|         key: authInfo.authKey, | ||||
|         device: authInfo.machineCode, | ||||
|         contactInfo: contactInfo | ||||
|       }; | ||||
|  | ||||
|       const { post } = require('ee-core/request'); | ||||
|       await post(url, data, { timeout: 10000 }); | ||||
|       console.log('联系人信息已同步到服务器'); | ||||
|     } catch (error) { | ||||
|       console.warn('同步联系人信息到服务器失败:', error.message); | ||||
|       // 不抛出错误,避免影响本地更新 | ||||
|     } | ||||
|   } | ||||
|   async getFollowRecord(args,event) { | ||||
|     const {userId,platform} = args; | ||||
|     if (!platform?.trim() && !userId?.trim()) { | ||||
|  | ||||
| @ -5,6 +5,8 @@ const os = require('os') | ||||
| const { app, BrowserWindow } = require('electron') | ||||
| const { post, get, put } = require("axios"); | ||||
| const { timestampToString } = require("../utils/CommonUtils"); | ||||
|  | ||||
| const Screenshots = require('electron-screenshots'); | ||||
| class SystemService { | ||||
|  | ||||
|   async getBaseInfo(args, event) { | ||||
| @ -36,15 +38,16 @@ class SystemService { | ||||
|     try { | ||||
|       // 统一转换为小写比较 | ||||
|       const lowerCaseCode = machineCode.toLowerCase(); | ||||
|       console.log("app",app) | ||||
|       const url = app.baseUrl + '/check_login' | ||||
|  | ||||
|       console.log('url======:', url) | ||||
|  | ||||
|       const reqData = { key: authKey, device: lowerCaseCode } | ||||
|       const res = await post(url, reqData, { timeout: 30000 }) | ||||
|        | ||||
|       const { code, message, device_valid_until, user_remaining_chars, device_status, user_name,userApiKey, parent_id, user_id } = res.data | ||||
|        | ||||
|  | ||||
|       const { code, message, device_valid_until, user_remaining_chars, device_status, user_name, userApiKey, parent_id, user_id } = res.data | ||||
|  | ||||
|       // 根据响应code处理不同情况 | ||||
|       switch (code) { | ||||
|         case 2000: // 请求成功 | ||||
| @ -129,13 +132,13 @@ class SystemService { | ||||
|       console.log('url======:', url) | ||||
|       const reqData = { username, password, device }; | ||||
|       const res = await post(url, reqData, { timeout: 30000 }); | ||||
|       const { code, message, device_valid_until, user_remaining_chars, device_status, user_name, parent_id, user_id,userApiKey } = res.data | ||||
|       logger.info('响应数据======:',res.data) | ||||
|       const { code, message, device_valid_until, user_remaining_chars, device_status, user_name, parent_id, user_id, userApiKey } = res.data | ||||
|        | ||||
|       // 根据响应code处理不同情况 | ||||
|       switch (code) { | ||||
|         case 2000: // 请求成功 | ||||
|           const timestamp = device_valid_until | ||||
|          | ||||
|  | ||||
|           const data = { | ||||
|             expireTime: timestampToString(timestamp),//失效时间 | ||||
|             totalChars: user_remaining_chars,//剩余字符数 | ||||
| @ -240,6 +243,11 @@ class SystemService { | ||||
|       return { status: false, message: res.data.message }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async captureScreen(args, event) { | ||||
|     const screenshots = new Screenshots(); | ||||
|     screenshots.startCapture(); | ||||
|   } | ||||
| } | ||||
| SystemService.toString = () => '[class SystemService]'; | ||||
|  | ||||
|  | ||||
| @ -10,6 +10,71 @@ const VolcEngineSDK = require("volcengine-sdk"); | ||||
| const { getTimeStr } = require("../utils/CommonUtils"); | ||||
| const { ApiInfo, ServiceInfo, Credentials, API, Request } = VolcEngineSDK; | ||||
|  | ||||
| // 国际化文本 | ||||
| const i18nTexts = { | ||||
|   zh: { | ||||
|     parameterError: '参数传递错误', | ||||
|     configNotExist: '配置不存在或窗口被关闭', | ||||
|     querySuccess: '查询成功', | ||||
|     missingRequiredParams: '缺少必要参数,编码和名称为必填项', | ||||
|     codeExists: '当前编码已存在,请使用不同的编码', | ||||
|     writeDataFailed: '数据写入失败,请稍后重试', | ||||
|     addSuccess: '语言配置添加成功', | ||||
|     addFailed: '添加失败,系统错误', | ||||
|     idRequired: 'id不能为空', | ||||
|     deleteSuccess: '成功删除{count}条数据', | ||||
|     noDataFound: '没有查询到这条数据', | ||||
|     paramsMissing: '参数缺失,请检查 ID、名称和code。', | ||||
|     updateSuccess: '语言配置更新成功', | ||||
|     updateFailed: '没有找到对应的语言配置,更新失败。', | ||||
|     systemError: '更新失败,系统错误', | ||||
|     translateTextEmpty: '翻译文本或编码不能为空!', | ||||
|     languageNotSupported: '不支持当前翻译语言!请检查翻译编码配置!', | ||||
|     contentOrSessionIdEmpty: '内容或窗口会话ID不能为空', | ||||
|     parameterIncomplete: '参数不完整:缺少partitionId', | ||||
|     dataUpdateSuccess: '数据更新成功', | ||||
|     clearCacheConfirm: '确认清理当前会话历史翻译缓存吗?', | ||||
|     clearCacheSuccess: '清理缓存成功' | ||||
|   }, | ||||
|   en: { | ||||
|     parameterError: 'Parameter error', | ||||
|     configNotExist: 'Configuration does not exist or window is closed', | ||||
|     querySuccess: 'Query successful', | ||||
|     missingRequiredParams: 'Missing required parameters, code and name are required', | ||||
|     codeExists: 'Code already exists, please use a different code', | ||||
|     writeDataFailed: 'Data write failed, please try again later', | ||||
|     addSuccess: 'Language configuration added successfully', | ||||
|     addFailed: 'Add failed, system error', | ||||
|     idRequired: 'ID cannot be empty', | ||||
|     deleteSuccess: 'Successfully deleted {count} records', | ||||
|     noDataFound: 'No data found', | ||||
|     paramsMissing: 'Parameters missing, please check ID, name and code.', | ||||
|     updateSuccess: 'Language configuration updated successfully', | ||||
|     updateFailed: 'Language configuration not found, update failed.', | ||||
|     systemError: 'Update failed, system error', | ||||
|     translateTextEmpty: 'Translation text or code cannot be empty!', | ||||
|     languageNotSupported: 'Current translation language not supported! Please check translation code configuration!', | ||||
|     contentOrSessionIdEmpty: 'Content or window session ID cannot be empty', | ||||
|     parameterIncomplete: 'Incomplete parameters: missing partitionId', | ||||
|     dataUpdateSuccess: 'Data updated successfully', | ||||
|     clearCacheConfirm: 'Confirm clearing translation cache for current session?', | ||||
|     clearCacheSuccess: 'Cache cleared successfully' | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // 获取国际化文本 | ||||
| function getI18nText(key, language = 'zh', params = {}) { | ||||
|   const lang = language || app.globalLanguage || 'zh'; | ||||
|   let text = i18nTexts[lang]?.[key] || i18nTexts.zh[key] || key; | ||||
|  | ||||
|   // 替换参数 | ||||
|   Object.keys(params).forEach(param => { | ||||
|     text = text.replace(`{${param}}`, params[param]); | ||||
|   }); | ||||
|  | ||||
|   return text; | ||||
| } | ||||
|  | ||||
| class TranslateService { | ||||
|  | ||||
|   /** | ||||
| @ -24,15 +89,71 @@ class TranslateService { | ||||
|    */ | ||||
|   async getConfigInfo(args, event) { | ||||
|     let { platform, userId, partitionId } = args; | ||||
|     console.log("getConfigInfo args", args); | ||||
|  | ||||
|     // 平台和用户id都为空,返回错误 | ||||
|     if (!platform?.trim() && !userId?.trim()) { | ||||
|       return { | ||||
|         status: false, | ||||
|         message: '参数不能同时为空,请至少填写一个有效参数' | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 平台和用户id都不为空,哪个有值就查询哪个(用于应用翻译时查询翻译配置,前端会传递两个参数) | ||||
|     if (platform?.trim() && userId?.trim()) { | ||||
|       const configById = await app.sdb.selectOne('translate_config', { userId: userId }) | ||||
|       if (configById) { | ||||
|         console.log('找到用户配置:', configById); | ||||
|         // 检查并修复translateRoute为空的情况 | ||||
|         if (!configById.translateRoute || configById.translateRoute === 'null') { | ||||
|           console.log('用户配置translateRoute为空,正在修复...'); | ||||
|           await app.sdb.update('translate_config', { translateRoute: 'youDao' }, { userId: userId }); | ||||
|           configById.translateRoute = 'youDao'; | ||||
|           console.log('已修复用户配置translateRoute为youDao'); | ||||
|         } | ||||
|         // 如果关闭了个人配置,则优先使用平台全局配置 | ||||
|         if (configById.usePersonalConfig === 'false') { | ||||
|           const globalConfig = await app.sdb.selectOne('translate_config', { platform: platform }) | ||||
|           if (globalConfig) { | ||||
|             if (!globalConfig.translateRoute || globalConfig.translateRoute === 'null') { | ||||
|               await app.sdb.update('translate_config', { translateRoute: 'youDao' }, { platform: platform }); | ||||
|               globalConfig.translateRoute = 'youDao'; | ||||
|             } | ||||
|             return { status: true, message: '查询成功(使用全局配置)', data: globalConfig } | ||||
|           } | ||||
|         } | ||||
|         return { status: true, message: '查询成功', data: configById } | ||||
|       } else { | ||||
|         const config = await app.sdb.selectOne('translate_config', { platform: platform }) | ||||
|         if (config) { | ||||
|           console.log('找到平台配置:', config); | ||||
|           // 检查并修复translateRoute为空的情况 | ||||
|           if (!config.translateRoute || config.translateRoute === 'null') { | ||||
|             console.log('平台配置translateRoute为空,正在修复...'); | ||||
|             await app.sdb.update('translate_config', { translateRoute: 'youDao' }, { platform: platform }); | ||||
|             config.translateRoute = 'youDao'; | ||||
|             console.log('已修复平台配置translateRoute为youDao'); | ||||
|           } | ||||
|           return { status: true, message: '查询成功', data: config } | ||||
|         } else { | ||||
|           console.log('未找到配置,将创建新配置'); | ||||
|           return { status: false, message: '查询失败,配置不存在' } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 如果用户id或平台有一个为空,分别判断(用于创建个性化翻译配置,前端只会传递一个参数) | ||||
|     if (userId?.trim()) { | ||||
|       const configById = await app.sdb.selectOne('translate_config', { userId: userId }) | ||||
|       if (configById) { | ||||
|         console.log('找到用户配置:', configById); | ||||
|         // 检查并修复translateRoute为空的情况 | ||||
|         if (!configById.translateRoute || configById.translateRoute === 'null') { | ||||
|           console.log('用户配置translateRoute为空,正在修复...'); | ||||
|           await app.sdb.update('translate_config', { translateRoute: 'deepl' }, { userId: userId }); | ||||
|           configById.translateRoute = 'deepl'; | ||||
|           console.log('已修复用户配置translateRoute为deepl'); | ||||
|         } | ||||
|         return { status: true, message: '查询成功', data: configById } | ||||
|       } else { | ||||
|         return { status: false, message: '查询失败,配置不存在' } | ||||
| @ -40,12 +161,26 @@ class TranslateService { | ||||
|     } else { | ||||
|       const configByPlatform = await app.sdb.selectOne('translate_config', { platform: platform }) | ||||
|       if (!configByPlatform && platform?.trim()) { | ||||
|  | ||||
|         // 查询默认翻译线路 | ||||
|         const url = app.baseUrl + '/get_default_translate_route_config' | ||||
|         const res = await get(url, {}, { timeout: 30000 }) | ||||
|         const code = res.data.code | ||||
|         const data1 = res.data.data | ||||
|         let defaultTranslateRoute = 'youDao' | ||||
|         let defaultHistoryTranslateRoute = 'youDao' | ||||
|         if (code === 2000) { | ||||
|           const { real_time_route, history_route } = data1 | ||||
|           defaultTranslateRoute = real_time_route | ||||
|           defaultHistoryTranslateRoute = history_route | ||||
|         } | ||||
|  | ||||
|         //初始化平台翻译配置信息 | ||||
|         const initialData = { | ||||
|           userId: "", | ||||
|           platform: platform, | ||||
|           receiveTranslateStatus: 'false', | ||||
|           translateRoute: 'youDao', | ||||
|           translateRoute: defaultTranslateRoute, | ||||
|           receiveSourceLanguage: "auto", | ||||
|           receiveTargetLanguage: "zh-CN", | ||||
|           sendTranslateStatus: "true", | ||||
| @ -56,12 +191,24 @@ class TranslateService { | ||||
|           chineseDetectionStatus: "false", | ||||
|           translatePreview: "false", | ||||
|           interceptChinese: "false", | ||||
|           interceptLanguages: "", | ||||
|           translateHistory: "false", | ||||
|           autoTranslateGroupMessage: "false" | ||||
|           autoTranslateGroupMessage: "false", | ||||
|           historyTranslateRoute: defaultHistoryTranslateRoute | ||||
|         } | ||||
|         await app.sdb.insert('translate_config', initialData); | ||||
|       } | ||||
|       const data = await app.sdb.selectOne('translate_config', { platform: platform }) | ||||
|       console.log('最终返回的配置数据:', data); | ||||
|  | ||||
|       // 检查并修复translateRoute为空的情况 | ||||
|       if (data && (!data.translateRoute || data.translateRoute === 'null')) { | ||||
|         console.log('配置存在但translateRoute为空,正在修复...'); | ||||
|         await app.sdb.update('translate_config', { translateRoute: 'youDao' }, { platform: platform }); | ||||
|         data.translateRoute = 'youDao'; | ||||
|         console.log('已修复translateRoute为youDao'); | ||||
|       } | ||||
|  | ||||
|       return { status: true, message: '查询成功2', data: data } | ||||
|     } | ||||
|     // if (partitionId) { | ||||
| @ -181,9 +328,18 @@ class TranslateService { | ||||
|     if (!key) throw new Error('缺少必要参数:key'); | ||||
|     if (typeof value === 'undefined') return; | ||||
|     try { | ||||
|       // 规范化特殊字段 | ||||
|       let normalizedValue = value; | ||||
|       if (key === 'interceptLanguages') { | ||||
|         if (Array.isArray(value)) { | ||||
|           normalizedValue = value.join(','); | ||||
|         } else if (typeof value === 'object' && value !== null) { | ||||
|           normalizedValue = Object.values(value).join(','); | ||||
|         } | ||||
|       } | ||||
|       // 构建更新对象(使用动态属性名) | ||||
|       const updateData = { | ||||
|         [key]: value | ||||
|         [key]: normalizedValue | ||||
|       }; | ||||
|       console.log('updateData', updateData) | ||||
|       // 执行更新 | ||||
| @ -210,7 +366,7 @@ class TranslateService { | ||||
|     if (!status?.trim() && !partitionId?.trim()) { | ||||
|       return { | ||||
|         status: false, | ||||
|         message: '参数传递错误' | ||||
|         message: getI18nText('parameterError') | ||||
|       } | ||||
|     } | ||||
|     //获取窗口对象并查询是否有userid | ||||
| @ -224,29 +380,29 @@ class TranslateService { | ||||
|           const count = await app.sdb.update('translate_config', { friendTranslateStatus: status }, { id: configById.id }) | ||||
|           logger.info('状态修改成功:', args) | ||||
|           if (partitionId) await this._routeUpdateNotify(partitionId) | ||||
|           return { status: true, message: '数据更新成功' } | ||||
|           return { status: true, message: getI18nText('dataUpdateSuccess') } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return { status: false, message: '配置不存在或窗口被关闭' } | ||||
|     return { status: false, message: getI18nText('configNotExist') } | ||||
|   } | ||||
|  | ||||
|   async getLanguageList(args, event) { | ||||
|     const list = await app.sdb.select('language_list', {}) | ||||
|     if (list) return { status: true, message: '查询成功', data: list } | ||||
|     return { status: true, message: '查询成功', data: [] } | ||||
|     if (list) return { status: true, message: getI18nText('querySuccess'), data: list } | ||||
|     return { status: true, message: getI18nText('querySuccess'), data: [] } | ||||
|   } | ||||
|  | ||||
|   async addLanguage(args, event) { | ||||
|     const { code, zhName, enName, youDao, baidu, huoShan, xiaoNiu, google, timestamp } = args; | ||||
|     // 参数验证 | ||||
|     if (!code || !zhName) { | ||||
|       return { status: false, message: '缺少必要参数,编码和名称为必填项' }; | ||||
|       return { status: false, message: getI18nText('missingRequiredParams') }; | ||||
|     } | ||||
|     // 检查编码是否已存在 | ||||
|     const info = await app.sdb.selectOne('language_list', { code: code }); | ||||
|     if (info) { | ||||
|       return { status: false, message: '当前编码已存在,请使用不同的编码' }; | ||||
|       return { status: false, message: getI18nText('codeExists') }; | ||||
|     } | ||||
|     // 准备插入的数据 | ||||
|     const rows = { code: code, zhName: zhName }; | ||||
| @ -265,23 +421,23 @@ class TranslateService { | ||||
|       const id = await app.sdb.insert('language_list', rows); | ||||
|  | ||||
|       if (!id) { | ||||
|         return { status: false, message: '数据写入失败,请稍后重试' }; | ||||
|         return { status: false, message: getI18nText('writeDataFailed') }; | ||||
|       } | ||||
|  | ||||
|       // 查询插入后的数据 | ||||
|       const data = await app.sdb.selectOne('language_list', { id: id }); | ||||
|  | ||||
|       return { status: true, message: '语言配置添加成功', data: data }; | ||||
|       return { status: true, message: getI18nText('addSuccess'), data: data }; | ||||
|     } catch (error) { | ||||
|       return { status: false, message: `添加失败,系统错误:${error.message}` }; | ||||
|       return { status: false, message: `${getI18nText('addFailed')}:${error.message}` }; | ||||
|     } | ||||
|   } | ||||
|   async deleteLanguage(args, event) { | ||||
|     const { id } = args; | ||||
|     if (!id) return { status: false, message: 'id不能为空' } | ||||
|     if (!id) return { status: false, message: getI18nText('idRequired') } | ||||
|     const count = await app.sdb.delete('language_list', { id: id }) | ||||
|     if (count > 0) return { status: true, message: `成功删除${count}条数据` } | ||||
|     return { status: false, message: `没有查询到这条数据` } | ||||
|     if (count > 0) return { status: true, message: getI18nText('deleteSuccess', undefined, { count }) } | ||||
|     return { status: false, message: getI18nText('noDataFound') } | ||||
|   } | ||||
|   async editLanguage(args, event) { | ||||
|     const { id, zhName, enName, code, youDao, baidu, huoShan, xiaoNiu, google } = args; | ||||
| @ -377,6 +533,60 @@ class TranslateService { | ||||
|   } | ||||
|  | ||||
|  | ||||
|   // 文本分段函数 | ||||
|   _splitTextForTranslation(text, maxLength = 2500) { | ||||
|     // 如果文本长度小于限制,直接返回 | ||||
|     if (text.length <= maxLength) { | ||||
|       return [text]; | ||||
|     } | ||||
|  | ||||
|     const segments = []; | ||||
|     let currentSegment = ''; | ||||
|  | ||||
|     // 按句子分割(优先按句号、问号、感叹号分割) | ||||
|     const sentences = text.split(/([.!?。!?\n])/); | ||||
|  | ||||
|     for (let i = 0; i < sentences.length; i++) { | ||||
|       const sentence = sentences[i]; | ||||
|  | ||||
|       // 如果当前段落加上新句子超过限制 | ||||
|       if ((currentSegment + sentence).length > maxLength) { | ||||
|         if (currentSegment.trim()) { | ||||
|           segments.push(currentSegment.trim()); | ||||
|           currentSegment = sentence; | ||||
|         } else { | ||||
|           // 如果单个句子就超过限制,强制分割 | ||||
|           const words = sentence.split(' '); | ||||
|           let wordSegment = ''; | ||||
|           for (const word of words) { | ||||
|             if ((wordSegment + ' ' + word).length > maxLength) { | ||||
|               if (wordSegment.trim()) { | ||||
|                 segments.push(wordSegment.trim()); | ||||
|                 wordSegment = word; | ||||
|               } else { | ||||
|                 // 单个词就超过限制,直接添加 | ||||
|                 segments.push(word); | ||||
|               } | ||||
|             } else { | ||||
|               wordSegment += (wordSegment ? ' ' : '') + word; | ||||
|             } | ||||
|           } | ||||
|           if (wordSegment.trim()) { | ||||
|             currentSegment = wordSegment; | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         currentSegment += sentence; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (currentSegment.trim()) { | ||||
|       segments.push(currentSegment.trim()); | ||||
|     } | ||||
|  | ||||
|     return segments; | ||||
|   } | ||||
|  | ||||
|   async translateText(args, event) { | ||||
|     const { route, text, from, to, partitionId, isFilter, mode, sourceTo } = args; | ||||
|     const routeMap = { | ||||
| @ -387,13 +597,19 @@ class TranslateService { | ||||
|       xiaoNiu: this._xiaoNiuTranslateText | ||||
|     }; | ||||
|  | ||||
|     if (isFilter === 'false') { | ||||
|       //查询是否存在缓存 | ||||
|       const cache = await app.sdb.selectOne('translate_cache', { partitionId: partitionId, toCode: to, text: text }); | ||||
|       if (cache) { | ||||
|         return { status: true, message: '翻译成功', data: cache.translateText }; | ||||
|       } | ||||
|     // 查询是否存在缓存 | ||||
|     const cache = await app.sdb.selectOne('translate_cache', { partitionId: partitionId, toCode: to, text: text }); | ||||
|     if (cache) { | ||||
|       return { status: true, message: '翻译成功', data: cache.translateText }; | ||||
|     } | ||||
|  | ||||
|     // 如果isFilter为true,只查询缓存,不进行实际翻译 | ||||
|     if (isFilter === 'true') { | ||||
|       return { status: false, message: '未找到翻译缓存' }; | ||||
|     } | ||||
|  | ||||
|     // 检查是否需要分段处理(针对长文本和腾讯翻译) | ||||
|     const needSegmentation = text.length > 2500 || (route === 'tengXun' && text.length > 2000); | ||||
|     // logger.info('文本翻译:', args); | ||||
|     try { | ||||
|       if (mode === 'cloud') { | ||||
| @ -401,38 +617,114 @@ class TranslateService { | ||||
|         const url = app.baseUrl + '/translate_api'; | ||||
|         if (!authInfo) throw new Error('用户信息不存在!') | ||||
|         const { userName, machineCode, authKey } = authInfo; | ||||
|         const data = { | ||||
|           translation_service: route, | ||||
|           username: userName, | ||||
|           source_lang: from, | ||||
|           target_lang: sourceTo, | ||||
|           text: text, | ||||
|           key: authKey, | ||||
|           device: machineCode | ||||
|  | ||||
|         if (needSegmentation) { | ||||
|           // 分段翻译 | ||||
|           const segments = this._splitTextForTranslation(text, route === 'tengXun' ? 2000 : 2500); | ||||
|           const translatedSegments = []; | ||||
|  | ||||
|           for (const segment of segments) { | ||||
|             const data = { | ||||
|               translation_service: route, | ||||
|               username: userName, | ||||
|               source_lang: from, | ||||
|               target_lang: sourceTo, | ||||
|               text: segment, | ||||
|               key: authKey, | ||||
|               device: machineCode | ||||
|             } | ||||
|  | ||||
|             const result = await post(url, data); | ||||
|             if (result?.data?.translate_text) { | ||||
|               translatedSegments.push(result.data.translate_text); | ||||
|             } else { | ||||
|               throw new Error(`分段翻译失败: ${result?.message || '未知错误'}`); | ||||
|             } | ||||
|  | ||||
|             // 添加短暂延迟避免API限制 | ||||
|             await new Promise(resolve => setTimeout(resolve, 100)); | ||||
|           } | ||||
|  | ||||
|           const finalResult = translatedSegments.join(''); | ||||
|           logger.info('translate_api result:', finalResult) | ||||
|  | ||||
|           if (finalResult && isFilter === 'false') { | ||||
|             //写入翻译消息缓存表 | ||||
|             await app.sdb.insert('translate_cache', { route: route, text: text, translateText: finalResult, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() }); | ||||
|           } | ||||
|  | ||||
|           return { status: true, message: '翻译成功', data: finalResult }; | ||||
|         } else { | ||||
|           // 正常翻译 | ||||
|           const data = { | ||||
|             translation_service: route, | ||||
|             username: userName, | ||||
|             source_lang: from, | ||||
|             target_lang: sourceTo, | ||||
|             text: text, | ||||
|             key: authKey, | ||||
|             device: machineCode | ||||
|           } | ||||
|           logger.info('translate_api data:', data) | ||||
|           const result = await post(url, data) | ||||
|           logger.info('translate_api result:', result.data) | ||||
|            | ||||
|           if (result?.data && isFilter === 'false') { | ||||
|             //写入翻译消息缓存表 | ||||
|             await app.sdb.insert('translate_cache', { route: route, text: text, translateText: result.data.translate_text, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() }); | ||||
|           } | ||||
|           return { status: true, message: '翻译成功', data: result.data.translate_text }; | ||||
|         } | ||||
|         logger.info('translate_api data:', data) | ||||
|         const result = await post(url, data) | ||||
|         if (result?.data && isFilter === 'false') { | ||||
|           //写入翻译消息缓存表 | ||||
|           await app.sdb.insert('translate_cache', { route: route, text: text, translateText: result.data.translate_text, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() }); | ||||
|         } | ||||
|         return { status: true, message: '翻译成功', data: result.data.translate_text }; | ||||
|       } | ||||
|       if (mode === 'local') { | ||||
|         if (!routeMap[route]) { | ||||
|           throw new Error(`不支持的翻译服务: ${route}`); | ||||
|         } | ||||
|         // 调用指定的翻译函数 | ||||
|         const result = await routeMap[route].call(this, text, to, route); | ||||
|         // logger.info(result); | ||||
|         if (result && isFilter === 'false') { | ||||
|           //写入翻译消息缓存表 | ||||
|           await app.sdb.insert('translate_cache', { route: route, text: text, translateText: result, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() }); | ||||
|  | ||||
|         if (needSegmentation) { | ||||
|           // 分段翻译 | ||||
|           const segments = this._splitTextForTranslation(text, route === 'tengXun' ? 2000 : 2500); | ||||
|           const translatedSegments = []; | ||||
|  | ||||
|           for (const segment of segments) { | ||||
|             const result = await routeMap[route].call(this, segment, to, route); | ||||
|             if (result) { | ||||
|               translatedSegments.push(result); | ||||
|             } else { | ||||
|               throw new Error(`分段翻译失败`); | ||||
|             } | ||||
|  | ||||
|             // 添加短暂延迟避免API限制 | ||||
|             await new Promise(resolve => setTimeout(resolve, 100)); | ||||
|           } | ||||
|  | ||||
|           const finalResult = translatedSegments.join(''); | ||||
|  | ||||
|           if (finalResult && isFilter === 'false') { | ||||
|             //写入翻译消息缓存表 | ||||
|             await app.sdb.insert('translate_cache', { route: route, text: text, translateText: finalResult, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() }); | ||||
|           } | ||||
|  | ||||
|           return { status: true, message: '翻译成功', data: finalResult }; | ||||
|         } else { | ||||
|           // 正常翻译 | ||||
|           const result = await routeMap[route].call(this, text, to, route); | ||||
|           // logger.info(result); | ||||
|           if (result && isFilter === 'false') { | ||||
|             //写入翻译消息缓存表 | ||||
|             await app.sdb.insert('translate_cache', { route: route, text: text, translateText: result, fromCode: from, toCode: to, partitionId: partitionId, timestamp: getTimeStr() }); | ||||
|           } | ||||
|           return { status: true, message: '翻译成功', data: result }; | ||||
|         } | ||||
|         return { status: true, message: '翻译成功', data: result }; | ||||
|       } | ||||
|     } catch (err) { | ||||
|       const msg = err.response?.data?.error || err.message; | ||||
|       let msg = err.response?.data?.error || err.message; | ||||
|  | ||||
|       // 检查是否是字符不足的错误 | ||||
|       if (msg && (msg.includes('账户可用字符不足') || msg.includes('字符不足') || msg.includes('insufficient'))) { | ||||
|         msg = '第三方接口字符已用完,请充值后继续使用'; | ||||
|       } | ||||
|  | ||||
|       logger.error('翻译请求失败:', msg) | ||||
|       return { status: false, message: msg }; | ||||
|     } | ||||
| @ -443,7 +735,14 @@ class TranslateService { | ||||
|     if (!partitionId) return; | ||||
|     const view = app.viewsMap.get(partitionId); | ||||
|     if (view && !view.webContents.isDestroyed()) { | ||||
|       await view.webContents.executeJavaScript('updateConfigInfo()') | ||||
|       // 设置标志位,避免翻译历史消息 | ||||
|       await view.webContents.executeJavaScript(` | ||||
|         window.isConfigUpdating = true; | ||||
|         updateConfigInfo(); | ||||
|         setTimeout(() => { | ||||
|           window.isConfigUpdating = false; | ||||
|         }, 1000); | ||||
|       `) | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @ -626,11 +925,25 @@ class TranslateService { | ||||
|  | ||||
|   async createTranslateConfig(args, event) { | ||||
|     const { userId } = args; | ||||
|  | ||||
|     // 查询默认翻译线路 | ||||
|     const url = app.baseUrl + '/get_default_translate_route_config' | ||||
|     const res = await get(url, {}, { timeout: 30000 }) | ||||
|     const code = res.data.code | ||||
|     const data1 = res.data.data | ||||
|     let defaultTranslateRoute = 'youDao' | ||||
|     let defaultHistoryTranslateRoute = 'youDao' | ||||
|     if (code === 2000) { | ||||
|       const { real_time_route, history_route } = data1 | ||||
|       defaultTranslateRoute = real_time_route | ||||
|       defaultHistoryTranslateRoute = history_route | ||||
|     } | ||||
|  | ||||
|     //初始化平台翻译配置信息 | ||||
|     const initialData = { | ||||
|       userId: userId, | ||||
|       platform: '', | ||||
|       translateRoute: 'youDao', | ||||
|       translateRoute: defaultTranslateRoute, | ||||
|       receiveTranslateStatus: 'false', | ||||
|       receiveSourceLanguage: "auto", | ||||
|       receiveTargetLanguage: "zh-CN", | ||||
| @ -642,12 +955,275 @@ class TranslateService { | ||||
|       chineseDetectionStatus: "false", | ||||
|       translatePreview: "false", | ||||
|       interceptChinese: "false", | ||||
|       interceptLanguages: "", | ||||
|       translateHistory: "false", | ||||
|       autoTranslateGroupMessage: "false" | ||||
|       autoTranslateGroupMessage: "false", | ||||
|       historyTranslateRoute: defaultHistoryTranslateRoute, | ||||
|       usePersonalConfig: 'true' | ||||
|     } | ||||
|     await app.sdb.insert('translate_config', initialData); | ||||
|   } | ||||
|  | ||||
|   async refreshTranslateRoutes(args, event) { | ||||
|     try { | ||||
|       // 获取远程翻译路由配置 | ||||
|       const url = app.baseUrl + '/translate/list_route' | ||||
|       const res = await get(url, {}, { timeout: 30000 }) | ||||
|  | ||||
|       let translationRoute = [] | ||||
|       const { code, data } = res.data | ||||
|       if (code === 2000) { | ||||
|         translationRoute = data | ||||
|       } else { | ||||
|         // 使用默认翻译线路配置 | ||||
|         translationRoute = [ | ||||
|           { | ||||
|             name: "youDao", | ||||
|             zhName: "有道翻译", | ||||
|             enName: "Youdao Translate", | ||||
|             otherArgs: `{"apiUrl": "https://openapi.youdao.com/api","apiKey": "","secretKey": ""}`, | ||||
|             enable: 1 | ||||
|           }, | ||||
|           { | ||||
|             name: "tengXun", | ||||
|             zhName: "腾讯翻译", | ||||
|             enName: "Tencent Translate", | ||||
|             otherArgs: `{"apiUrl": "https://fanyi.qq.com/api","appId": "","apiKey": ""}`, | ||||
|             enable: 1 | ||||
|           }, | ||||
|           { | ||||
|             name: "baidu", | ||||
|             zhName: "百度翻译", | ||||
|             enName: "Baidu Translate", | ||||
|             otherArgs: `{"apiUrl": "https://fanyi.baidu.com/v2transapi","appId": "","apiKey": "","secretKey": ""}`, | ||||
|             enable: 1 | ||||
|           }, | ||||
|           { | ||||
|             name: "huoShan", | ||||
|             zhName: "火山翻译", | ||||
|             enName: "HuoShan Translate", | ||||
|             otherArgs: `{"apiUrl": "https://api.hushan.ai/v1/translation","appId": "","apiKey": "","secretKey": ""}`, | ||||
|             enable: 1 | ||||
|           }, | ||||
|           { | ||||
|             name: "google", | ||||
|             zhName: "谷歌翻译", | ||||
|             enName: "Google Translate", | ||||
|             otherArgs: `{"apiUrl": "https://translation.googleapis.com/language/translate/v2","appId": "","apiKey": "","secretKey": ""}`, | ||||
|             enable: 1 | ||||
|           }, | ||||
|           { | ||||
|             name: "bing", | ||||
|             zhName: "必应翻译", | ||||
|             enName: "Bing Translate", | ||||
|             otherArgs: `{"apiUrl": "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0","appId": "","apiKey": "","secretKey": ""}`, | ||||
|             enable: 1 | ||||
|           }, | ||||
|           { | ||||
|             name: "chatGpt4o", | ||||
|             zhName: "ChatGpt4o翻译", | ||||
|             enName: "ChatGpt4o Translate", | ||||
|             otherArgs: `{"apiUrl": "https://api.chatgpt4o.com/translate","appId": "","apiKey": "","secretKey": ""}`, | ||||
|             enable: 1 | ||||
|           } | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
|       // 清空并重新插入翻译路由 | ||||
|       await app.sdb.deleteAll("translate_route"); | ||||
|  | ||||
|       for (let item of translationRoute) { | ||||
|         const routeArgs = { | ||||
|           name: item.name, | ||||
|           zhName: item.zhName, | ||||
|           enName: item.enName, | ||||
|           otherArgs: item.otherArgs, | ||||
|           enable: item.enable | ||||
|         }; | ||||
|         await app.sdb.insert("translate_route", routeArgs); | ||||
|       } | ||||
|  | ||||
|       return { status: true, message: '翻译路由刷新成功', data: translationRoute }; | ||||
|     } catch (error) { | ||||
|       console.error('刷新翻译路由失败:', error); | ||||
|       return { status: false, message: `刷新翻译路由失败: ${error.message}` }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async listNote(args, event) { | ||||
|     // args: { userId } | ||||
|     try { | ||||
|       const params = {}; | ||||
|       if (args?.userId) params.userId = args.userId; | ||||
|       const res = await get(app.baseUrl + '/note/list_note', { params }); | ||||
|       if (res.data && res.data.code === 2000) { | ||||
|         return { status: true, data: res.data.data }; | ||||
|       } else { | ||||
|         return { status: false, message: res.data?.msg || '查询失败' }; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       return { status: false, message: e.message }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async createNote(args, event) { | ||||
|     // args: { userId, content } | ||||
|     try { | ||||
|       const res = await post(app.baseUrl + '/note/create_note', { | ||||
|         userId: args.userId, | ||||
|         content: args.content | ||||
|       }); | ||||
|       if (res.data && res.data.code === 2000) { | ||||
|         return { status: true, data: res.data.data }; | ||||
|       } else { | ||||
|         return { status: false, message: res.data?.msg || '创建失败' }; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       return { status: false, message: e.message }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async updateNote(args, event) { | ||||
|     // args: { id, content } | ||||
|     try { | ||||
|       const res = await post(app.baseUrl + '/note/update_note', { | ||||
|         id: args.id, | ||||
|         content: args.content | ||||
|       }); | ||||
|       if (res.data && res.data.code === 2000) { | ||||
|         return { status: true, data: res.data.data }; | ||||
|       } else { | ||||
|         return { status: false, message: res.data?.msg || '更新失败' }; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       return { status: false, message: e.message }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   async deleteNote(args, event) { | ||||
|     // args: { id } | ||||
|     try { | ||||
|       const res = await get(app.baseUrl + '/note/delete_note', { params: { id: args.id } }); | ||||
|       if (res.data && res.data.code === 2000) { | ||||
|         return { status: true }; | ||||
|       } else { | ||||
|         return { status: false, message: res.data?.msg || '删除失败' }; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       return { status: false, message: e.message }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 清除翻译缓存 | ||||
|   async clearTranslateCache(args, event) { | ||||
|     const { partitionId, platform } = args; | ||||
|     try { | ||||
|       let deleteCount = 0; | ||||
|  | ||||
|       if (partitionId) { | ||||
|         // 清除指定会话的翻译缓存 | ||||
|         deleteCount = await app.sdb.delete('translate_cache', { partitionId: partitionId }); | ||||
|       } else if (platform) { | ||||
|         // 清除指定平台的翻译缓存 | ||||
|         const cacheList = await app.sdb.select('translate_cache', {}); | ||||
|         for (const cache of cacheList) { | ||||
|           if (cache.partitionId && cache.partitionId.includes(platform)) { | ||||
|             await app.sdb.delete('translate_cache', { id: cache.id }); | ||||
|             deleteCount++; | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         // 清除所有翻译缓存 | ||||
|         deleteCount = await app.sdb.delete('translate_cache', {}); | ||||
|       } | ||||
|  | ||||
|       return { | ||||
|         status: true, | ||||
|         message: `成功清除${deleteCount}条翻译缓存`, | ||||
|         data: { deleteCount } | ||||
|       }; | ||||
|     } catch (error) { | ||||
|       return { | ||||
|         status: false, | ||||
|         message: `清除翻译缓存失败: ${error.message}` | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 刷新会话翻译按钮 | ||||
|   async refreshSessionTranslateButtons(args, event) { | ||||
|     const { partitionId } = args; | ||||
|     try { | ||||
|       if (!partitionId) { | ||||
|         return { | ||||
|           status: false, | ||||
|           message: '参数不完整:缺少partitionId' | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|       // 获取当前分区的webContents | ||||
|       const { BrowserWindow } = require('electron'); | ||||
|       const windows = BrowserWindow.getAllWindows(); | ||||
|  | ||||
|       let refreshed = false; | ||||
|       for (const window of windows) { | ||||
|         const webContents = window.webContents; | ||||
|  | ||||
|         // 检查是否是目标分区 | ||||
|         if (webContents.session.partition === `persist:${partitionId}`) { | ||||
|           // 先弹出确认对话框,然后清理缓存并刷新对话内容区域 | ||||
|           webContents.executeJavaScript(` | ||||
|             // 获取当前语言设置 | ||||
|             const currentLanguage = window.currentAppLanguage || 'zh'; | ||||
|             const confirmText = currentLanguage === 'zh' ? | ||||
|               '确认清理当前会话历史翻译缓存吗?' : | ||||
|               'Confirm clearing translation cache for current session?'; | ||||
|             const successText = currentLanguage === 'zh' ? | ||||
|               '清理缓存成功' : | ||||
|               'Cache cleared successfully'; | ||||
|  | ||||
|             // 弹出确认对话框 | ||||
|             const confirmed = confirm(confirmText); | ||||
|             if (confirmed) { | ||||
|               // 用户确认后,显示成功提示 | ||||
|               alert(successText); | ||||
|  | ||||
|               // 直接刷新整个WhatsApp页面内容,这样对话框内容会重新加载 | ||||
|               window.location.reload(); | ||||
|             } | ||||
|             confirmed; // 返回确认结果 | ||||
|           `).then((confirmed) => { | ||||
|             if (confirmed) { | ||||
|               console.log(`用户确认清理分区 ${partitionId} 的翻译缓存,页面已刷新`); | ||||
|             } else { | ||||
|               console.log(`用户取消清理分区 ${partitionId} 的翻译缓存`); | ||||
|             } | ||||
|           }).catch(err => { | ||||
|             console.warn(`向分区 ${partitionId} 发送清理确认对话框失败:`, err); | ||||
|           }); | ||||
|  | ||||
|           refreshed = true; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (!refreshed) { | ||||
|         console.warn(`未找到分区 ${partitionId} 的会话页面`); | ||||
|       } | ||||
|  | ||||
|       return { | ||||
|         status: true, | ||||
|         message: refreshed ? '清理缓存操作已发送' : '未找到对应的会话页面' | ||||
|       }; | ||||
|  | ||||
|     } catch (error) { | ||||
|       console.error('发送清理缓存操作失败:', error); | ||||
|       return { | ||||
|         status: false, | ||||
|         message: `发送清理缓存操作失败: ${error.message}` | ||||
|       }; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| TranslateService.toString = () => '[class TranslateService]'; | ||||
|  | ||||
|  | ||||
| @ -16,7 +16,7 @@ const { | ||||
| } = require("../utils/CommonUtils"); | ||||
| const { getMainWindow } = require("ee-core/electron"); | ||||
| const { get } = require("axios"); | ||||
|  | ||||
| const { net } = require("electron"); | ||||
| const { sockProxyRules } = require("electron-session-proxy"); | ||||
|  | ||||
| class WindowService { | ||||
| @ -160,6 +160,10 @@ class WindowService { | ||||
|           view.webContents.setWebRTCIPHandlingPolicy("disable_non_proxied_udp"); | ||||
|  | ||||
|           await view.webContents.loadURL(webUrl, { userAgent: userAgent }); | ||||
|  | ||||
|           // 同步语言设置到新创建的webview | ||||
|           this._syncLanguageToWebview(view); | ||||
|  | ||||
|           // loadWithTimeout | ||||
|         } catch (err) { | ||||
|           logger.error("加载页面失败:", err); | ||||
| @ -607,15 +611,14 @@ class WindowService { | ||||
|       ) { | ||||
|         // 获取全局代理配置 | ||||
|         const globalConfig = await app.sdb.selectOne("global_proxy_config"); | ||||
|         if (globalConfig && globalConfig.proxyStatus === "true") { | ||||
|         if (globalConfig && globalConfig.proxyStatus === "true" && globalConfig.proxyIp && globalConfig.proxyPort) { | ||||
|           config = globalConfig; | ||||
|           logger.info(`[${partitionId}] Proxy: Use global proxy config`); | ||||
|         } else { | ||||
|           await view.webContents.session.setProxy({ mode: 'system' }); | ||||
|           logger.info(`[${partitionId}] Proxy: 使用系统代理设置`); | ||||
|           return; | ||||
|         } | ||||
|         //  else { | ||||
|         //   await view.webContents.session.setProxy({ mode: 'system' }); | ||||
|         //   logger.info(`[${partitionId}] Proxy: 使用系统代理设置`); | ||||
|         //   return; | ||||
|         // } | ||||
|       } | ||||
|  | ||||
|       let proxyRules; | ||||
| @ -856,6 +859,139 @@ class WindowService { | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 测试代理连接是否可用(不修改现有会话配置) | ||||
|    * @param args { proxyType, proxyIp, proxyPort, userVerifyStatus, username, password } | ||||
|    */ | ||||
|   async testProxy(args, event) { | ||||
|     try { | ||||
|       const { | ||||
|         proxyType = "http", | ||||
|         proxyIp = "", | ||||
|         proxyPort = "", | ||||
|         userVerifyStatus = "false", | ||||
|         username = "", | ||||
|         password = "", | ||||
|       } = args || {}; | ||||
|  | ||||
|       if (!proxyIp || !proxyPort) { | ||||
|         return { status: false, message: "请填写完整的代理主机和端口" }; | ||||
|       } | ||||
|  | ||||
|       let server = `${String(proxyIp).trim()}:${String(proxyPort).trim()}`; | ||||
|       let proxyRules; | ||||
|  | ||||
|       const needAuth = userVerifyStatus === "true" && username && password; | ||||
|       if (needAuth) { | ||||
|         server = `${String(username).trim()}:${String(password).trim()}@${server}`; | ||||
|       } | ||||
|  | ||||
|       switch (proxyType) { | ||||
|         case "http": | ||||
|         case "https": | ||||
|           proxyRules = `http://${server}`; | ||||
|           break; | ||||
|         case "socks4": | ||||
|           proxyRules = needAuth | ||||
|             ? await sockProxyRules(`socks4://${server}`) | ||||
|             : `socks4=socks4://${server}`; | ||||
|           break; | ||||
|         case "socks5": | ||||
|           proxyRules = needAuth | ||||
|             ? await sockProxyRules(`socks5://${server}`) | ||||
|             : `socks5=socks5://${server}`; | ||||
|           break; | ||||
|         default: | ||||
|           return { status: false, message: `不支持的代理类型:${proxyType}` }; | ||||
|       } | ||||
|  | ||||
|       // 创建临时会话并设置代理 | ||||
|       const partition = `proxy-test-${Date.now()}-${Math.random()}`; | ||||
|       const tmpSession = session.fromPartition(partition, { cache: false }); | ||||
|       await tmpSession.setProxy({ mode: "fixed_servers", proxyRules }); | ||||
|  | ||||
|       const testUrls = [ | ||||
|         "https://cp.cloudflare.com/generate_204", | ||||
|         "http://www.msftconnecttest.com/connecttest.txt", | ||||
|         "https://www.baidu.com/", | ||||
|       ]; | ||||
|  | ||||
|       const tryRequest = (url) => | ||||
|         new Promise((resolve) => { | ||||
|           const req = net.request({ url, session: tmpSession }); | ||||
|           const timer = setTimeout(() => { | ||||
|             try { req.abort(); } catch (e) {} | ||||
|             resolve({ ok: false, code: "ETIMEOUT" }); | ||||
|           }, 12000); | ||||
|  | ||||
|           req.on("response", (res) => { | ||||
|             clearTimeout(timer); | ||||
|             // 2xx/3xx 认为成功 | ||||
|             if (res.statusCode >= 200 && res.statusCode < 400) { | ||||
|               resolve({ ok: true, code: res.statusCode }); | ||||
|             } else { | ||||
|               resolve({ ok: false, code: res.statusCode }); | ||||
|             } | ||||
|           }); | ||||
|           req.on("error", (err) => { | ||||
|             clearTimeout(timer); | ||||
|             resolve({ ok: false, code: err.code || err.message }); | ||||
|           }); | ||||
|           req.end(); | ||||
|         }); | ||||
|  | ||||
|       let last; | ||||
|       for (const u of testUrls) { | ||||
|         last = await tryRequest(u); | ||||
|         if (last.ok) break; | ||||
|       } | ||||
|  | ||||
|       // 复原临时会话代理 | ||||
|       try { await tmpSession.setProxy({ mode: "system" }); } catch (e) {} | ||||
|  | ||||
|       if (last && last.ok) { | ||||
|         return { status: true, message: "代理连接成功" }; | ||||
|       } | ||||
|       return { status: false, message: `代理连接失败,错误码:${last?.code ?? "UNKNOWN"}` }; | ||||
|     } catch (error) { | ||||
|       logger.error("测试代理失败:", error); | ||||
|       return { status: false, message: `测试代理失败:${error.message}` }; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 同步语言设置到webview | ||||
|    * @param {BrowserView} view - webview实例 | ||||
|    */ | ||||
|   _syncLanguageToWebview(view) { | ||||
|     if (view && !view.webContents.isDestroyed()) { | ||||
|       const currentLanguage = app.globalLanguage || 'zh'; | ||||
|  | ||||
|       // 等待页面加载完成后再设置语言 | ||||
|       view.webContents.once('dom-ready', () => { | ||||
|         try { | ||||
|           view.webContents.executeJavaScript(` | ||||
|             // 设置全局语言变量 | ||||
|             if (window.updateLanguage) { | ||||
|               window.updateLanguage('${currentLanguage}'); | ||||
|             } else { | ||||
|               window.currentAppLanguage = '${currentLanguage}'; | ||||
|             } | ||||
|  | ||||
|             // 触发语言变更事件 | ||||
|             window.dispatchEvent(new CustomEvent('languageChanged', { | ||||
|               detail: { language: '${currentLanguage}' } | ||||
|             })); | ||||
|  | ||||
|             console.log('Language synced to webview:', '${currentLanguage}'); | ||||
|           `); | ||||
|         } catch (error) { | ||||
|           logger.warn('Failed to sync language to webview:', error); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| WindowService.toString = () => "[class WindowService]"; | ||||
|  | ||||
|  | ||||
| @ -30,15 +30,23 @@ import { useMenuStore } from '@/stores/menuStore'; | ||||
| import UpdateProgress from "@/views/components/UpdateProgress.vue"; | ||||
| import router from "@/router" | ||||
| import { ElMessage } from 'element-plus' | ||||
| import { useI18n } from 'vue-i18n' | ||||
|  | ||||
| const menuStore = useMenuStore(); | ||||
| const { locale } = useI18n(); | ||||
| const barTitle = ref(''); | ||||
| const appPlatform = ref(''); | ||||
|  | ||||
| onMounted(async () => { | ||||
|   const loadingElement = document.getElementById('loadingPage'); | ||||
|   if (loadingElement) { | ||||
|     loadingElement.remove(); | ||||
|   } | ||||
|  | ||||
|   // Initialize language from localStorage | ||||
|   const savedLanguage = localStorage.getItem('language') || 'zh'; | ||||
|   locale.value = savedLanguage; | ||||
|  | ||||
|   const res = await ipc.invoke(ipcApiRoute.getSystemInfo, {}); | ||||
|   const { name, platform, version } = res; | ||||
|   barTitle.value = name + ` v${version}`; | ||||
| @ -53,27 +61,30 @@ const winControl = async (action) => { | ||||
|  | ||||
| onMounted(async () => { | ||||
|   const res = await ipc.invoke(ipcApiRoute.getRouteList, {}); | ||||
|   if (res.status&& res.data) { | ||||
|     const desiredOrder = ['youDao','deepl', 'deepseek','tengXun','huoShan','baidu', 'google','bing'];  | ||||
|   if (res.status && res.data) { | ||||
|     // const desiredOrder = ['youDao','deepl', 'deepseek','tengXun','huoShan','baidu', 'google','bing'];  | ||||
|  | ||||
|     let orderedRoutes = []; // 存储排序后的结果 | ||||
|     // let orderedRoutes = []; // 存储排序后的结果 | ||||
|  | ||||
|     // 使用 Map 提高查找效率,并处理过滤掉没有 name 的项 | ||||
|     const routeMap = new Map(); | ||||
|     res.data.filter(item => item.name).forEach(item => { | ||||
|       routeMap.set(item.name, item); | ||||
|     }); | ||||
|     // // 使用 Map 提高查找效率,并处理过滤掉没有 name 的项 | ||||
|     // const routeMap = new Map(); | ||||
|     // res.data.filter(item => item.name).forEach(item => { | ||||
|     //   routeMap.set(item.name, item); | ||||
|     // }); | ||||
|  | ||||
|     // 1. 按照 desiredOrder 添加路由 | ||||
|     for (const name of desiredOrder) { | ||||
|       if (routeMap.has(name)) { | ||||
|         orderedRoutes.push(routeMap.get(name)); | ||||
|         // 不需要从 Map 中删除,因为我们不再处理剩余路由 | ||||
|       } | ||||
|     } | ||||
|     // // 1. 按照 desiredOrder 添加路由 | ||||
|     // for (const name of desiredOrder) { | ||||
|     //   if (routeMap.has(name)) { | ||||
|     //     orderedRoutes.push(routeMap.get(name)); | ||||
|     //     // 不需要从 Map 中删除,因为我们不再处理剩余路由 | ||||
|     //   } | ||||
|     // } | ||||
|  | ||||
|     // 2. 将最终的路由设置为只有 orderedRoutes,不包含未指定顺序的路由 | ||||
|     const finalRoutes = orderedRoutes; | ||||
|     // // 2. 将最终的路由设置为只有 orderedRoutes,不包含未指定顺序的路由 | ||||
|     // const finalRoutes = orderedRoutes; | ||||
|  | ||||
|     // 根据enable决定是否显示 | ||||
|     const finalRoutes = res.data.filter(item => item.enable == 1); | ||||
|     menuStore.setTranslationRoute(finalRoutes) | ||||
|   } | ||||
|  | ||||
|  | ||||
| @ -4,6 +4,8 @@ | ||||
|  */ | ||||
| const ipcApiRoute = { | ||||
|  | ||||
|   captureScreen: 'controller/system/captureScreen', | ||||
|  | ||||
|   getSystemInfo: 'controller/system/getBaseInfo', | ||||
|   login: 'controller/system/login', | ||||
|   logOut: 'controller/system/logOut', | ||||
| @ -35,6 +37,8 @@ const ipcApiRoute = { | ||||
|   editProxyInfo: 'controller/window/editProxyInfo', | ||||
|   editGlobalProxyInfo: 'controller/window/editGlobalProxyInfo', | ||||
|   saveProxyInfo: 'controller/window/saveProxyInfo', | ||||
|   testProxy: 'controller/window/testProxy', | ||||
|   checkProxyStatus: 'controller/window/checkProxyStatus', | ||||
|   openSessionDevTools: 'controller/window/openSessionDevTools', | ||||
|   closeGlobalProxyPasswordVerification: 'controller/window/closeGlobalProxyPasswordVerification', | ||||
|  | ||||
| @ -53,8 +57,13 @@ const ipcApiRoute = { | ||||
|   getRouteList: 'controller/translate/getRouteList', | ||||
|   testRoute: 'controller/translate/testRoute', | ||||
|   translateText: 'controller/translate/translateText', | ||||
|   refreshTranslateRoutes: 'controller/translate/refreshTranslateRoutes', | ||||
|  | ||||
|   createTranslateConfig: 'controller/translate/createTranslateConfig', | ||||
|   listNote: 'controller/translate/listNote', | ||||
|   createNote: 'controller/translate/createNote', | ||||
|   updateNote: 'controller/translate/updateNote', | ||||
|   deleteNote: 'controller/translate/deleteNote', | ||||
|  | ||||
|   //联系人信息相关 | ||||
|   getContactInfo: 'controller/contactInfo/getContactInfo', | ||||
| @ -65,6 +74,9 @@ const ipcApiRoute = { | ||||
|   updateFollowRecord: 'controller/contactInfo/updateFollowRecord', | ||||
|   deleteFollowRecord: 'controller/contactInfo/deleteFollowRecord', | ||||
|  | ||||
|   // 修改联系人备注,同步更新页面 | ||||
|   updateContactRemark: 'controller/contactInfo/updateContactRemark', | ||||
|  | ||||
|   //快捷回复相关 | ||||
|   getGroups: 'controller/quickreply/getGroups', | ||||
|   getContentByGroupId: 'controller/quickreply/getContentByGroupId', | ||||
| @ -76,6 +88,10 @@ const ipcApiRoute = { | ||||
|   editReply: 'controller/quickreply/editReply', | ||||
|   deleteReply: 'controller/quickreply/deleteReply', | ||||
|   deleteAllReply: 'controller/quickreply/deleteAllReply', | ||||
|  | ||||
|   //翻译缓存相关 | ||||
|   clearTranslateCache: 'controller/translate/clearTranslateCache', | ||||
|   refreshSessionTranslateButtons: 'controller/translate/refreshSessionTranslateButtons', | ||||
| } | ||||
|  | ||||
| export { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| <script setup> | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| import { ref, computed } from 'vue'; | ||||
| import { ref, computed, onMounted } from 'vue'; | ||||
|  | ||||
| // 定义props | ||||
| const props = defineProps({ | ||||
| @ -13,6 +13,13 @@ const props = defineProps({ | ||||
| const { locale } = useI18n(); | ||||
| const currentLanguage = ref(locale.value); | ||||
|  | ||||
| // 在组件挂载时读取保存的语言设置 | ||||
| onMounted(() => { | ||||
|   const savedLanguage = localStorage.getItem('language') || 'zh'; | ||||
|   locale.value = savedLanguage; | ||||
|   currentLanguage.value = savedLanguage; | ||||
| }); | ||||
|  | ||||
| // 计算样式 | ||||
| const switchStyle = computed(() => ({ | ||||
|   width: `${props.size}px`, | ||||
| @ -23,9 +30,24 @@ const fontSize = computed(() => ({ | ||||
|   fontSize: `${Math.max(props.size * 0.4375, 12)}px` // 保持字体大小为容器大小的0.4375倍,最小12px | ||||
| })); | ||||
|  | ||||
| const handleLanguageChange = (lang) => { | ||||
| const handleLanguageChange = async (lang) => { | ||||
|   locale.value = lang; | ||||
|   currentLanguage.value = lang; | ||||
|  | ||||
|   // 保存到localStorage,使用统一的键名 | ||||
|   localStorage.setItem('language', lang); | ||||
|  | ||||
|   // 通知主进程语言变更 | ||||
|   if (window.electronAPI && window.electronAPI.ipcRenderer) { | ||||
|     try { | ||||
|       await window.electronAPI.ipcRenderer.invoke('language-change', { language: lang }); | ||||
|       console.log('Language change notified to main process:', lang); | ||||
|     } catch (error) { | ||||
|       console.warn('Failed to notify language change to main process:', error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  | ||||
| }; | ||||
| </script> | ||||
|  | ||||
|  | ||||
							
								
								
									
										9
									
								
								frontend/src/components/icons/NoteIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/components/icons/NoteIcon.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| <template> | ||||
|   <svg t="1717130000000" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1234" width="24" height="24"> | ||||
|     <path d="M832 128H192c-35.2 0-64 28.8-64 64v640c0 35.2 28.8 64 64 64h640c35.2 0 64-28.8 64-64V192c0-35.2-28.8-64-64-64z m0 704H192V192h640v640z m-96-320H288v64h448v-64z m0-128H288v64h448v-64z" fill="#fff" p-id="1235"></path> | ||||
|   </svg> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| // 便签图标 | ||||
| </script>  | ||||
							
								
								
									
										11
									
								
								frontend/src/components/icons/ScreenShotIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/components/icons/ScreenShotIcon.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| <template> | ||||
|   <svg viewBox="0 0 24 24" width="1em" height="1em" fill="currentColor"> | ||||
|     <rect x="3" y="5" width="18" height="14" rx="2" ry="2" stroke="currentColor" stroke-width="2" fill="none"/> | ||||
|     <path d="M8 3v4M16 3v4M4 11h16" stroke="currentColor" stroke-width="2" fill="none"/> | ||||
|     <circle cx="12" cy="16" r="2" fill="currentColor"/> | ||||
|   </svg> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| // Screenshot/Capture Screen Icon | ||||
| </script>  | ||||
| @ -34,14 +34,14 @@ export default { | ||||
|     logoutConfirm: 'Are you sure you want to logout?', | ||||
|     copySuccess: 'Copied successfully!', | ||||
|     copyFailed: 'Copy failed!', | ||||
|      | ||||
|  | ||||
|     // Account Information | ||||
|     accountInfo: 'Account Information', | ||||
|     availableChars: 'Available Characters', | ||||
|     expirationTime: 'Expiration Time', | ||||
|     remainingDays: '{days} days remaining', | ||||
|     deviceId: 'Device ID', | ||||
|      | ||||
|  | ||||
|     // Core Features | ||||
|     coreFeatures: 'Core Features', | ||||
|     features: { | ||||
| @ -62,7 +62,7 @@ export default { | ||||
|         desc: 'Local API translation for data privacy' | ||||
|       } | ||||
|     }, | ||||
|      | ||||
|  | ||||
|     // Support and Help | ||||
|     supportAndHelp: 'Support & Help', | ||||
|     officialChannel: 'liangzi Channel', | ||||
| @ -207,6 +207,7 @@ export default { | ||||
|     appId: 'App ID', | ||||
|     settings: 'Translation Settings', | ||||
|     clearCacheTitle: 'Confirm clearing translation cache for current session?', | ||||
|     clearCacheSuccess: 'Cache cleared successfully', | ||||
|     mode: 'Translation Mode', | ||||
|     localTranslate: 'Local', | ||||
|     cloudTranslate: 'Cloud', | ||||
| @ -223,12 +224,45 @@ export default { | ||||
|     currentContact: 'Current Contact', | ||||
|     globalContact: 'Global Contact', | ||||
|     notFoundPersonalizedTranslation: 'No personalized translation settings found', | ||||
|     interceptChinese: 'Intercept Chinese', | ||||
|     interceptChinese: 'Enable send interception', | ||||
|     interceptLanguagesLabel: 'Intercept languages', | ||||
|     selectInterceptLanguages: 'Select languages to intercept', | ||||
|     translateHistory: 'Translation History Messages', | ||||
|     autoTranslateGroupMessage: 'Auto Translate Group Messages', | ||||
|     usePersonalConfig: 'Enable personal settings for this contact', | ||||
|  | ||||
|     createPersonalizedTranslation: 'Create Personalized Translation', | ||||
|     chooseContactFirst: 'Please select a contact first', | ||||
|     noUserId: 'Unable to get user ID, please select a conversation or refresh the page' | ||||
|     noUserId: 'Unable to get user ID, please select a conversation or refresh the page', | ||||
|     historyTranslateRoute: 'History Message Translation Route', | ||||
|     // Error messages | ||||
|     errors: { | ||||
|       parameterError: 'Parameter error', | ||||
|       configNotExist: 'Configuration does not exist or window is closed', | ||||
|       querySuccess: 'Query successful', | ||||
|       missingRequiredParams: 'Missing required parameters, code and name are required', | ||||
|       codeExists: 'Code already exists, please use a different code', | ||||
|       writeDataFailed: 'Data write failed, please try again later', | ||||
|       addSuccess: 'Language configuration added successfully', | ||||
|       addFailed: 'Add failed, system error', | ||||
|       idRequired: 'ID cannot be empty', | ||||
|       deleteSuccess: 'Successfully deleted {count} records', | ||||
|       noDataFound: 'No data found', | ||||
|       paramsMissing: 'Parameters missing, please check ID, name and code.', | ||||
|       updateSuccess: 'Language configuration updated successfully', | ||||
|       updateFailed: 'Language configuration not found, update failed.', | ||||
|       systemError: 'Update failed, system error', | ||||
|       translateTextEmpty: 'Translation text or code cannot be empty!', | ||||
|       languageNotSupported: 'Current translation language not supported! Please check translation code configuration!', | ||||
|       contentOrSessionIdEmpty: 'Content or window session ID cannot be empty', | ||||
|       parameterIncomplete: 'Incomplete parameters: missing partitionId', | ||||
|       dataUpdateSuccess: 'Data updated successfully' | ||||
|     }, | ||||
|     // Input placeholders | ||||
|     placeholders: { | ||||
|       translateToTarget: 'Translate to target language and send.', | ||||
|       translateTo: 'Translate to [{language}] and send.' | ||||
|     } | ||||
|   }, | ||||
|   quickReply: { | ||||
|     title: 'Quick Reply', | ||||
| @ -279,7 +313,8 @@ export default { | ||||
|     proxyConfig: 'Proxy Config', | ||||
|     devtools: 'Developer Tools', | ||||
|     expand: 'Expand', | ||||
|     collapse: 'Collapse',  | ||||
|     collapse: 'Collapse', | ||||
|     screenshot: 'Screenshot', | ||||
|   }, | ||||
|   quickReplyConfig: { | ||||
|     group: { | ||||
| @ -302,7 +337,7 @@ export default { | ||||
|       remark: 'Remark', | ||||
|       enterRemark: 'Please enter remark', | ||||
|       type: 'Type', | ||||
|       content: 'Content',  | ||||
|       content: 'Content', | ||||
|       enterContent: 'Please enter content', | ||||
|       text: 'Text', | ||||
|       image: 'Image', | ||||
|  | ||||
| @ -3,9 +3,15 @@ import en from './en' | ||||
| import zh from './zh' | ||||
| import km from './km' | ||||
|  | ||||
| // 从localStorage获取保存的语言设置,默认为中文 | ||||
| const getInitialLocale = () => { | ||||
|   const savedLanguage = localStorage.getItem('app-language'); | ||||
|   return savedLanguage || 'zh'; | ||||
| }; | ||||
|  | ||||
| const i18n = createI18n({ | ||||
|   legacy: false, | ||||
|   locale: 'zh', | ||||
|   locale: getInitialLocale(), | ||||
|   fallbackLocale: 'en', | ||||
|   messages: { | ||||
|     en, | ||||
| @ -14,4 +20,16 @@ const i18n = createI18n({ | ||||
|   } | ||||
| }) | ||||
|  | ||||
| // 监听全局语言变更事件 | ||||
| if (window.electronAPI && window.electronAPI.ipcRenderer) { | ||||
|   window.electronAPI.ipcRenderer.on('global-language-changed', (event, data) => { | ||||
|     const { language } = data; | ||||
|     if (language && i18n.global.locale.value !== language) { | ||||
|       i18n.global.locale.value = language; | ||||
|       localStorage.setItem('language', language); | ||||
|       console.log('Language updated from main process:', language); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export default i18n | ||||
| @ -221,7 +221,8 @@ export default { | ||||
|     autoTranslateGroupMessage: 'បកប្រែសារក្រុមដោយស្វ័យប្រវត្តិ', | ||||
|     createPersonalizedTranslation: 'បង្កើតការបកប្រែសម្រាប់ទំនាក់ទំនងបច្ចុប្បន្ន', | ||||
|     chooseContactFirst: 'សូមជ្រើសរើសទំនាក់ទំនងជាមុន', | ||||
|     noUserId: 'មិនអាចទទួលបាន ID របស់អ្នកប្រើប្រាស់ទេ សូមជ្រើសរើសជជែកឬផ្ទាល់ខាតលើសពីទីនេះ' | ||||
|     noUserId: 'មិនអាចទទួលបាន ID របស់អ្នកប្រើប្រាស់ទេ សូមជ្រើសរើសជជែកឬផ្ទាល់ខាតលើសពីទីនេះ', | ||||
|     historyTranslateRoute: 'ផ្លូវបកប្រែសារទាំងអស់' | ||||
|   }, | ||||
|   quickReply: { | ||||
|     title: 'ឆ្លើយតបរហ័ស', | ||||
| @ -271,6 +272,7 @@ export default { | ||||
|     devtools: 'ឧបករណ៍អ្នកអភិវឌ្ឍ', | ||||
|     expand: 'ពង្រួម', | ||||
|     collapse: 'បង្រួម', | ||||
|     screenshot: 'រូបភាព', | ||||
|   }, | ||||
|   quickReplyConfig: { | ||||
|     group: { | ||||
|  | ||||
| @ -187,7 +187,10 @@ export default { | ||||
|     batchCount: '数量', | ||||
|     batchProxySettings: '批量代理设置', | ||||
|     batchProxySetSuccess: '批量代理设置成功', | ||||
|     selectSessionFirst: '请先勾选会话' | ||||
|     selectSessionFirst: '请先勾选会话', | ||||
|     testProxy: '测试代理', | ||||
|     proxyTestSuccess: '代理连接成功', | ||||
|     proxyTestFailed: '代理连接失败' | ||||
|   }, | ||||
|   translate: { | ||||
|     google: '谷歌翻译', | ||||
| @ -200,6 +203,7 @@ export default { | ||||
|     appId: '应用ID', | ||||
|     settings: '翻译设置', | ||||
|     clearCacheTitle: '确认清理当前会话历史翻译缓存吗?', | ||||
|     clearCacheSuccess: '清理缓存成功', | ||||
|     mode: '翻译模式', | ||||
|     localTranslate: '本地翻译', | ||||
|     cloudTranslate: '云端翻译', | ||||
| @ -216,12 +220,44 @@ export default { | ||||
|     currentContact: '当前联系人', | ||||
|     globalContact: '全局联系人', | ||||
|     notFoundPersonalizedTranslation: '未找到个性化翻译设置', | ||||
|     interceptChinese: '拦截中文', | ||||
|     interceptChinese: '启用发送拦截', | ||||
|     interceptLanguagesLabel: '拦截语言', | ||||
|     selectInterceptLanguages: '请选择要拦截的语言', | ||||
|     translateHistory: '翻译历史消息', | ||||
|     autoTranslateGroupMessage: '自动翻译群消息', | ||||
|     usePersonalConfig: '启用当前联系人配置', | ||||
|     createPersonalizedTranslation: '新建个性化翻译', | ||||
|     chooseContactFirst: '请先选择联系人', | ||||
|     noUserId: '无法获取用户ID,请选择会话或刷新页面' | ||||
|     noUserId: '无法获取用户ID,请选择会话或刷新页面', | ||||
|     historyTranslateRoute: '翻译历史消息', | ||||
|     // 错误消息 | ||||
|     errors: { | ||||
|       parameterError: '参数传递错误', | ||||
|       configNotExist: '配置不存在或窗口被关闭', | ||||
|       querySuccess: '查询成功', | ||||
|       missingRequiredParams: '缺少必要参数,编码和名称为必填项', | ||||
|       codeExists: '当前编码已存在,请使用不同的编码', | ||||
|       writeDataFailed: '数据写入失败,请稍后重试', | ||||
|       addSuccess: '语言配置添加成功', | ||||
|       addFailed: '添加失败,系统错误', | ||||
|       idRequired: 'id不能为空', | ||||
|       deleteSuccess: '成功删除{count}条数据', | ||||
|       noDataFound: '没有查询到这条数据', | ||||
|       paramsMissing: '参数缺失,请检查 ID、名称和code。', | ||||
|       updateSuccess: '语言配置更新成功', | ||||
|       updateFailed: '没有找到对应的语言配置,更新失败。', | ||||
|       systemError: '更新失败,系统错误', | ||||
|       translateTextEmpty: '翻译文本或编码不能为空!', | ||||
|       languageNotSupported: '不支持当前翻译语言!请检查翻译编码配置!', | ||||
|       contentOrSessionIdEmpty: '内容或窗口会话ID不能为空', | ||||
|       parameterIncomplete: '参数不完整:缺少partitionId', | ||||
|       dataUpdateSuccess: '数据更新成功' | ||||
|     }, | ||||
|     // 输入框提示 | ||||
|     placeholders: { | ||||
|       translateToTarget: '翻译成目标语言后发送。', | ||||
|       translateTo: '翻译成[{language}]后发送。' | ||||
|     } | ||||
|   }, | ||||
|   quickReply: { | ||||
|     title: '快捷回复', | ||||
| @ -271,6 +307,7 @@ export default { | ||||
|     devtools: '开发者工具', | ||||
|     expand: '展开', | ||||
|     collapse: '折叠', | ||||
|     screenshot: '截图', | ||||
|   }, | ||||
|   quickReplyConfig: { | ||||
|     group: { | ||||
|  | ||||
| @ -55,6 +55,7 @@ export const useMenuStore = defineStore("platform", { | ||||
|     rightContent: "TranslateConfig", | ||||
|     rightFoldStatus: false, | ||||
|     userInfo: {}, | ||||
|     currentUserId: null, | ||||
|   }), | ||||
|   actions: { | ||||
|     setUserInfo(user) { | ||||
| @ -173,6 +174,9 @@ export const useMenuStore = defineStore("platform", { | ||||
|         // menu.children.push(...newItems); | ||||
|       } | ||||
|     }, | ||||
|     setCurrentUserId(userId) { | ||||
|       this.currentUserId = userId; | ||||
|     }, | ||||
|   }, | ||||
|   getters: { | ||||
|     getCurrentMenu(state) { | ||||
| @ -205,5 +209,8 @@ export const useMenuStore = defineStore("platform", { | ||||
|     getIsChildMenu: (state) => () => { | ||||
|       return state.isChildMenu; | ||||
|     }, | ||||
|     getCurrentUserId: (state) => () => { | ||||
|       return state.currentUserId; | ||||
|     }, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| @ -87,6 +87,9 @@ | ||||
|       <div class="content-right"> | ||||
|         <el-input :placeholder="t('session.enterPassword')" v-model="proxyInfo.password"></el-input> | ||||
|       </div> | ||||
|       <div class="footer-actions"> | ||||
|         <el-button type="warning" @click="testGlobalProxy">测试代理</el-button> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @ -222,6 +225,27 @@ const getConfigInfo = async () => { | ||||
|   } | ||||
| } | ||||
|  | ||||
| const testGlobalProxy = async () => { | ||||
|   const args = { | ||||
|     proxyType: proxyInfo.value.proxyType, | ||||
|     proxyIp: proxyInfo.value.proxyIp, | ||||
|     proxyPort: proxyInfo.value.proxyPort, | ||||
|     userVerifyStatus: proxyInfo.value.userVerifyStatus, | ||||
|     username: proxyInfo.value.username, | ||||
|     password: proxyInfo.value.password, | ||||
|   }; | ||||
|   try { | ||||
|     const res = await ipc.invoke(ipcApiRoute.testProxy, args); | ||||
|     if (res.status) { | ||||
|       ElMessage.success(res.message || '代理连接成功'); | ||||
|     } else { | ||||
|       ElMessage.error(res.message || '代理连接失败'); | ||||
|     } | ||||
|   } catch (e) { | ||||
|     ElMessage.error('代理连接失败'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
|   getConfigInfo(); | ||||
| }) | ||||
|  | ||||
| @ -21,8 +21,17 @@ | ||||
|       </div> | ||||
|       <!-- 搜索框区域 --> | ||||
|       <div class="menu-search-bar" style="padding: 8px 12px 0 12px; margin-bottom: 5px;"> | ||||
|         <el-input v-model="menuSearchText" placeholder="搜索联系人/备注/用户名" clearable size="small" | ||||
|           prefix-icon="el-icon-search" /> | ||||
|         <el-input | ||||
|           v-model="menuSearchText" | ||||
|           placeholder="搜索联系人/备注/用户名" | ||||
|           clearable | ||||
|           size="small" | ||||
|           @input="handleSearchInput" | ||||
|           @clear="handleSearchClear"> | ||||
|           <template #prefix> | ||||
|             <el-icon><Search /></el-icon> | ||||
|           </template> | ||||
|         </el-input> | ||||
|       </div> | ||||
|       <!-- 开关,切换普通区和置顶区 --> | ||||
|       <div class="menu-switch" v-if="isCollapse"> | ||||
| @ -212,6 +221,20 @@ | ||||
|               <el-text>{{ t('menu.translateConfig') }}</el-text> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div @click="toggleBottomMenu('Note')" | ||||
|             :class="activeMenu === 'Note' ? 'active-fold-menu-item' : 'default-fold-menu-item'"> | ||||
|             <div class="menu-item-line" /> | ||||
|             <div class="menu-item-img"> | ||||
|               <el-avatar> | ||||
|                 <el-icon size="25"> | ||||
|                   <component :is="NoteIcon" /> | ||||
|                 </el-icon> | ||||
|               </el-avatar> | ||||
|             </div> | ||||
|             <div v-if="isCollapse" class="menu-item-text"> | ||||
|               <el-text>便签</el-text> | ||||
|             </div> | ||||
|           </div> | ||||
|           <!--          <div @click="toggleBottomMenu('MoreSetting')" :class="activeMenu === 'MoreSetting'?'active-fold-menu-item':'default-fold-menu-item'">--> | ||||
|           <!--            <div class="menu-item-line"/>--> | ||||
|           <!--            <div class="menu-item-img">--> | ||||
| @ -257,6 +280,7 @@ import quickReply from '@/components/icons/QuickReplyIcon.vue' | ||||
| import translateSetting from '@/components/icons/TranslateSettingIcon.vue' | ||||
| import logo from '@/components/icons/LogoIcon.vue' | ||||
| import google from "@/components/icons/GoogleIcon.vue" | ||||
| import NoteIcon from '@/components/icons/NoteIcon.vue' | ||||
| import { | ||||
|   ArrowLeft, | ||||
|   ArrowRight, | ||||
| @ -264,7 +288,8 @@ import { | ||||
|   ArrowDown, | ||||
|   ArrowUp, | ||||
|   Fold, | ||||
|   Expand | ||||
|   Expand, | ||||
|   Search | ||||
| } from '@element-plus/icons-vue' | ||||
|  | ||||
| import { ref, markRaw, watch, onMounted, onUnmounted, computed, nextTick } from 'vue' | ||||
| @ -275,6 +300,7 @@ import { ipcApiRoute } from "@/api" | ||||
| import { ipc } from "@/utils/ipcRenderer" | ||||
| import router from "@/router" | ||||
| import { useI18n } from 'vue-i18n' | ||||
| import Note from "@/views/note/index.vue" | ||||
|  | ||||
| const { logger } = require('ee-core/log'); | ||||
| const menuStore = useMenuStore() | ||||
| @ -326,8 +352,20 @@ const clearTimer = () => { | ||||
|  | ||||
| const menuSearchText = ref('') | ||||
|  | ||||
| // 搜索处理函数 | ||||
| const handleSearchInput = (value) => { | ||||
|   console.log('搜索输入:', value) | ||||
|   // 由于使用了computed,这里不需要额外处理,filteredChildren会自动响应 | ||||
| } | ||||
|  | ||||
| const handleSearchClear = () => { | ||||
|   console.log('清空搜索') | ||||
|   menuSearchText.value = '' | ||||
| } | ||||
|  | ||||
| const filteredChildren = computed(() => (children, menuId) => { | ||||
|   let nMenus = children | ||||
|   let nMenus = children || [] | ||||
|  | ||||
|   if (currentArea.value === 'top') { | ||||
|     nMenus = nMenus.filter(child => child.isTop === true || child.isTop === 'true') | ||||
|   } else { | ||||
| @ -515,6 +553,7 @@ const menuItems = [ | ||||
|   { id: 'TikTok', component: markRaw(SessionList) }, | ||||
|   { id: 'QuickReply', component: markRaw(QuickReply) }, | ||||
|   { id: 'TranslateConfig', component: markRaw(TranslateConfig) }, | ||||
|   { id: 'Note', component: markRaw(Note) }, | ||||
|   { id: 'CustomWeb', component: markRaw(SessionList) }, | ||||
|   { id: 'GlobalProxy', component: markRaw(GlobalProxy) }, | ||||
|   { id: 'Unknown', component: markRaw(Unknown) }, | ||||
| @ -732,7 +771,7 @@ const startSession = async (child) => { | ||||
|  | ||||
|   /* 菜单项组 */ | ||||
|   .menu-item-group { | ||||
|     height: calc(100% - 325px); | ||||
|     height: calc(100% - 325px - 40px); | ||||
|  | ||||
|     :deep(.el-scrollbar__wrap) { | ||||
|       overflow-x: hidden; | ||||
| @ -977,7 +1016,7 @@ const startSession = async (child) => { | ||||
|  | ||||
|   /* 底部折叠区域 */ | ||||
|   .fold-area { | ||||
|     height: 170px; | ||||
|     height: 230px; | ||||
|     width: 100%; | ||||
|     position: absolute; | ||||
|     bottom: 0; | ||||
| @ -990,7 +1029,7 @@ const startSession = async (child) => { | ||||
|  | ||||
|     /* 底部菜单 */ | ||||
|     .fold-menu { | ||||
|       height: 120px; | ||||
|       height: 180px; | ||||
|  | ||||
|       /* 通用底部菜单项样式 */ | ||||
|       .default-fold-menu-item, | ||||
|  | ||||
							
								
								
									
										197
									
								
								frontend/src/views/note/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								frontend/src/views/note/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,197 @@ | ||||
| <template> | ||||
|   <div class="note-page"> | ||||
|     <div class="note-actions"> | ||||
|       <el-button type="primary" @click="openDialog()">新增便签</el-button> | ||||
|     </div> | ||||
|     <el-empty v-if="notes.length === 0" description="暂无便签" /> | ||||
|     <div v-else class="note-list"> | ||||
|       <el-card v-for="note in notes" :key="note.id" class="note-card" shadow="hover"> | ||||
|         <div class="note-card-row"> | ||||
|           <div class="note-card-main" @click="openDialog(note)"> | ||||
|             <div class="note-card-content">{{ note.content }}</div> | ||||
|             <div class="note-card-meta"> | ||||
|               <span class="note-time">创建时间:{{ formatTime(note.created_at) }}</span> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="note-card-actions"> | ||||
|             <el-button type="danger" size="small" @click.stop="deleteNote(note.id)">删除</el-button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </el-card> | ||||
|     </div> | ||||
|     <el-dialog v-model="dialogVisible" :title="editingNote ? '编辑便签' : '新增便签'" width="400px" @close="resetDialog"> | ||||
|       <el-input | ||||
|         type="textarea" | ||||
|         v-model="noteContent" | ||||
|         :rows="6" | ||||
|         placeholder="请输入便签内容" | ||||
|         maxlength="500" | ||||
|         show-word-limit | ||||
|       /> | ||||
|       <template #footer> | ||||
|         <el-button @click="dialogVisible = false">取消</el-button> | ||||
|         <el-button type="primary" @click="saveNote">保存</el-button> | ||||
|       </template> | ||||
|     </el-dialog> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { ref, onMounted } from 'vue' | ||||
| import { ElMessage } from 'element-plus' | ||||
| import { ipc } from '@/utils/ipcRenderer' | ||||
| import { ipcApiRoute } from '@/api' | ||||
| import { useMenuStore } from '@/stores/menuStore' | ||||
|  | ||||
| const menuStore = useMenuStore() | ||||
| const notes = ref([]) | ||||
| const dialogVisible = ref(false) | ||||
| const noteContent = ref('') | ||||
| const editingNote = ref(null) | ||||
|  | ||||
| function formatTime(ts) { | ||||
|   if (!ts) return '' | ||||
|   const d = new Date(ts) | ||||
|   return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}` | ||||
| } | ||||
|  | ||||
| async function loadNotes() { | ||||
|   // 优先云端 | ||||
|   const userId = menuStore.userInfo.userId | ||||
|   if (!userId) { | ||||
|     notes.value = [] | ||||
|     return | ||||
|   } | ||||
|   try { | ||||
|     const res = await ipc.invoke(ipcApiRoute.listNote, { userId }) | ||||
|     if (res.status) { | ||||
|       notes.value = res.data || [] | ||||
|       // 兜底本地存储 | ||||
|       localStorage.setItem('my_notes_list', JSON.stringify(notes.value)) | ||||
|     } else { | ||||
|       // 云端失败兜底本地 | ||||
|       const data = localStorage.getItem('my_notes_list') | ||||
|       notes.value = data ? JSON.parse(data) : [] | ||||
|     } | ||||
|   } catch { | ||||
|     const data = localStorage.getItem('my_notes_list') | ||||
|     notes.value = data ? JSON.parse(data) : [] | ||||
|   } | ||||
| } | ||||
|  | ||||
| function openDialog(note = null) { | ||||
|   if (note) { | ||||
|     editingNote.value = note | ||||
|     noteContent.value = note.content | ||||
|   } else { | ||||
|     editingNote.value = null | ||||
|     noteContent.value = '' | ||||
|   } | ||||
|   dialogVisible.value = true | ||||
| } | ||||
|  | ||||
| async function saveNote() { | ||||
|   const content = noteContent.value.trim() | ||||
|   if (!content) { | ||||
|     ElMessage.error('内容不能为空') | ||||
|     return | ||||
|   } | ||||
|   const userId = menuStore.userInfo.userId | ||||
|   if (!userId) { | ||||
|     ElMessage.error('未获取到用户ID') | ||||
|     return | ||||
|   } | ||||
|   if (editingNote.value) { | ||||
|     // 编辑 | ||||
|     const res = await ipc.invoke(ipcApiRoute.updateNote, { id: editingNote.value.id, content }) | ||||
|     if (res.status) { | ||||
|       editingNote.value.content = content | ||||
|       editingNote.value.updatedAt = Date.now() | ||||
|       ElMessage.success('便签已更新') | ||||
|       await loadNotes() | ||||
|     } else { | ||||
|       ElMessage.error(res.message || '更新失败') | ||||
|     } | ||||
|   } else { | ||||
|     // 新增 | ||||
|     const res = await ipc.invoke(ipcApiRoute.createNote, { userId, content }) | ||||
|     if (res.status) { | ||||
|       ElMessage.success('便签已添加') | ||||
|       await loadNotes() | ||||
|     } else { | ||||
|       ElMessage.error(res.message || '添加失败') | ||||
|     } | ||||
|   } | ||||
|   dialogVisible.value = false | ||||
| } | ||||
|  | ||||
| async function deleteNote(id) { | ||||
|   const res = await ipc.invoke(ipcApiRoute.deleteNote, { id }) | ||||
|   if (res.status) { | ||||
|     ElMessage.success('便签已删除') | ||||
|     await loadNotes() | ||||
|   } else { | ||||
|     ElMessage.error(res.message || '删除失败') | ||||
|   } | ||||
| } | ||||
|  | ||||
| function resetDialog() { | ||||
|   noteContent.value = '' | ||||
|   editingNote.value = null | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
|   loadNotes() | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style scoped> | ||||
| .note-page { | ||||
|   padding: 32px; | ||||
|   text-align: center; | ||||
| } | ||||
| .note-actions { | ||||
|   margin-bottom: 16px; | ||||
|   text-align: right; | ||||
| } | ||||
| .note-list { | ||||
|   width: 100%; | ||||
|   margin: 0 auto; | ||||
|   text-align: left; | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   gap: 16px; | ||||
| } | ||||
| .note-card { | ||||
|   width: 100%; | ||||
|   box-sizing: border-box; | ||||
|   /* display: flex; */ | ||||
|   transition: box-shadow 0.2s; | ||||
| } | ||||
| .note-card-row { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   justify-content: space-between; | ||||
| } | ||||
| .note-card-main { | ||||
|   flex: 1; | ||||
|   cursor: pointer; | ||||
|   min-width: 0; | ||||
| } | ||||
| .note-card-content { | ||||
|   min-height: 40px; | ||||
|   word-break: break-all; | ||||
|   font-size: 15px; | ||||
|   margin-bottom: 8px; | ||||
| } | ||||
| .note-card-meta { | ||||
|   color: #888; | ||||
|   font-size: 12px; | ||||
|   margin-bottom: 4px; | ||||
| } | ||||
| .note-card-actions { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
|   margin-left: 16px; | ||||
| } | ||||
| </style>  | ||||
| @ -76,11 +76,11 @@ | ||||
|           v-model="proxyInfo.password" | ||||
|           :type="showPassword ? 'text' : 'password'" | ||||
|         > | ||||
|           <template #suffix> | ||||
|           <!-- <template #suffix> | ||||
|             <el-icon style="cursor:pointer;" @click="showPassword = !showPassword"> | ||||
|               <component :is="showPassword ? Hide : View" /> | ||||
|             </el-icon> | ||||
|           </template> | ||||
|           </template> --> | ||||
|         </el-input> | ||||
|       </div> | ||||
|     </div> | ||||
| @ -114,6 +114,7 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|     <div style="text-align:right;margin-top:16px;"> | ||||
|       <el-button type="warning" size="small" @click="testProxy" style="margin-right: 8px;">{{ t('session.testProxy') || '测试代理' }}</el-button> | ||||
|       <el-button type="primary" size="small" @click="handleSave">{{ t('common.save') }}</el-button> | ||||
|     </div> | ||||
|   </div> | ||||
| @ -159,12 +160,48 @@ const getConfigInfo = async () => { | ||||
|   } catch (err) {} | ||||
| } | ||||
|  | ||||
| const testProxy = async () => { | ||||
|   const args = { | ||||
|     proxyType: proxyInfo.value.proxyType, | ||||
|     proxyIp: proxyInfo.value.proxyIp, | ||||
|     proxyPort: proxyInfo.value.proxyPort, | ||||
|     userVerifyStatus: proxyInfo.value.userVerifyStatus, | ||||
|     username: proxyInfo.value.username, | ||||
|     password: proxyInfo.value.password, | ||||
|   }; | ||||
|   try { | ||||
|     const res = await ipc.invoke(ipcApiRoute.testProxy, args); | ||||
|     if (res.status) { | ||||
|       ElMessage.success(res.message || '代理连接成功'); | ||||
|     } else { | ||||
|       ElMessage.error(res.message || '代理连接失败'); | ||||
|     } | ||||
|   } catch (e) { | ||||
|     ElMessage.error('代理连接失败'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| const handleSave = async () => { | ||||
|   const args = { ...proxyInfo.value }; | ||||
|   await ipc.invoke(ipcApiRoute.editProxyInfo, args); | ||||
|   // 刷新对应会话页面(修复:这里应传 partitionId 而不是 currentMenu) | ||||
|   await ipc.invoke(ipcApiRoute.refreshSession, { | ||||
|     partitionId: menuStore.currentMenu | ||||
|     partitionId: menuStore.currentPartitionId | ||||
|   }); | ||||
|   // 保存后自动刷新当前列表行的代理状态 | ||||
|   try { | ||||
|     const pid = menuStore.currentPartitionId; | ||||
|     const res = await ipc.invoke(ipcApiRoute.checkProxyStatus, { partitionId: pid, platform: menuStore.platform }); | ||||
|     const menu = menuStore.menus.find(m => m.id === menuStore.currentMenu); | ||||
|     const row = menu?.children?.find(c => c.partitionId === pid); | ||||
|     if (row) { | ||||
|       const should = res?.data?.shouldUseProxy; | ||||
|       const using = res?.data?.usingProxy; | ||||
|       row._proxyShould = typeof should === 'boolean' ? should : !!using; | ||||
|       row._proxyReachable = res?.data?.reachable === null ? null : !!res?.data?.reachable; | ||||
|     } | ||||
|   } catch (e) {} | ||||
|  | ||||
|   if (args.defaultLanguage || args.timezone) { | ||||
|     ElMessage({ | ||||
|       message: t('session.restartRequired'), | ||||
| @ -179,6 +216,16 @@ const handleSave = async () => { | ||||
|   }) | ||||
| } | ||||
|  | ||||
| // 监听会话切换,重新获取配置信息 | ||||
| watch( | ||||
|   () => menuStore.currentPartitionId, | ||||
|   async (newValue, oldValue) => { | ||||
|     if (newValue && newValue !== oldValue) { | ||||
|       await getConfigInfo() | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| onMounted(() => { | ||||
|   getConfigInfo() | ||||
| }) | ||||
|  | ||||
| @ -4,147 +4,185 @@ | ||||
|       <el-tab-pane :label="t('translate.currentContact')" name="current" /> | ||||
|       <el-tab-pane :label="t('translate.globalContact')" name="global" /> | ||||
|     </el-tabs> | ||||
|     <template v-if="Object.keys(configInfo).length > 0"> | ||||
|       <div class="header-container"> | ||||
|         <div class="header-title"> | ||||
|           <el-text tag="b" size="large">{{ t('translate.settings') }}</el-text> | ||||
|           <el-popconfirm :title="t('translate.clearCacheTitle')" :confirm-button-text="t('common.confirm')" | ||||
|             :cancel-button-text="t('common.cancel')" @confirm="cleanMsgCache" trigger="hover" width="200"> | ||||
|             <template #reference> | ||||
|               <el-icon class="header-icon"> | ||||
|                 <CleanIcon /> | ||||
|     <el-skeleton v-if="loading" :rows="6" animated /> | ||||
|     <template v-else> | ||||
|       <template v-if="!getConfigInfoIsNull"> | ||||
|         <div class="header-container"> | ||||
|           <div class="header-title"> | ||||
|             <el-text tag="b" size="large">{{ t('translate.settings') }}</el-text> | ||||
|             <el-popconfirm :title="t('translate.clearCacheTitle')" :confirm-button-text="t('common.confirm')" | ||||
|               :cancel-button-text="t('common.cancel')" @confirm="cleanMsgCache" trigger="hover" width="200"> | ||||
|               <template #reference> | ||||
|                 <el-icon class="header-icon"> | ||||
|                   <CleanIcon /> | ||||
|                 </el-icon> | ||||
|               </template> | ||||
|             </el-popconfirm> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="content-container-radio-group" v-if="showLocalTranslate"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.mode') }}</el-text> | ||||
|             <el-tooltip effect="dark" placement="top"> | ||||
|               <template #content> | ||||
|                 <div style="max-width: 180px;">{{ t('translate.tooltipContent') }}</div> | ||||
|               </template> | ||||
|               <el-icon> | ||||
|                 <QuestionFilled /> | ||||
|               </el-icon> | ||||
|             </template> | ||||
|           </el-popconfirm> | ||||
|             </el-tooltip> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-radio-group v-model="configInfo.mode" size="small"> | ||||
|               <el-radio-button :label="t('translate.localTranslate')" value="local" /> | ||||
|               <el-radio-button :label="t('translate.cloudTranslate')" value="cloud" /> | ||||
|             </el-radio-group> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-radio-group" v-if="showLocalTranslate"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.mode') }}</el-text> | ||||
|           <el-tooltip effect="dark" placement="top"> | ||||
|             <template #content> | ||||
|               <div style="max-width: 180px;">{{ t('translate.tooltipContent') }}</div> | ||||
|             </template> | ||||
|             <el-icon> | ||||
|               <QuestionFilled /> | ||||
|             </el-icon> | ||||
|           </el-tooltip> | ||||
|         <!-- 仅在“当前联系人”标签下显示:是否启用个人配置 --> | ||||
|         <div class="content-container-radio" v-if="activeTab === 'current'"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.usePersonalConfig') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|                        v-model="configInfo.usePersonalConfig" /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-radio-group v-model="configInfo.mode" size="small"> | ||||
|             <el-radio-button :label="t('translate.localTranslate')" value="local" /> | ||||
|             <el-radio-button :label="t('translate.cloudTranslate')" value="cloud" /> | ||||
|           </el-radio-group> | ||||
|         <div class="content-container-select"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.route') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right" style="display: flex; gap: 8px;"> | ||||
|             <el-select v-model="configInfo.translateRoute" :placeholder="t('translate.selectRoute')" size="default" | ||||
|               style="flex: 1" @change="handleTranslateRouteChange"> | ||||
|               <el-option v-for="item in menuStore.translationRoute" :key="item.name" :label="item[currentLanguageName]" | ||||
|                 :value="item.name" /> | ||||
|             </el-select> | ||||
|             <el-button size="default" type="primary" :icon="RefreshIcon" @click="refreshTranslateRoutes" | ||||
|               :loading="refreshLoading" title="刷新翻译路由"> | ||||
|             </el-button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-select"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.route') }}</el-text> | ||||
|         <div class="content-container-radio"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.realTimeReceive') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|               v-model="configInfo.receiveTranslateStatus" /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-select v-model="configInfo.translateRoute" :placeholder="t('translate.selectRoute')" size="default" | ||||
|             style="width: 100%" @change="handleTranslateRouteChange"> | ||||
|             <el-option v-for="item in menuStore.translationRoute" :key="item.name" :label="item[currentLanguageName]" | ||||
|               :value="item.name" /> | ||||
|           </el-select> | ||||
|         <div class="content-container-radio" v-if="configInfo.receiveTranslateStatus === 'true'"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.translateHistory') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|               v-model="configInfo.translateHistory" /> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-radio"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.realTimeReceive') }}</el-text> | ||||
|         <div class="content-container-radio" v-if="configInfo.receiveTranslateStatus === 'true'"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.autoTranslateGroupMessage') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|               v-model="configInfo.autoTranslateGroupMessage" /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|             v-model="configInfo.receiveTranslateStatus" /> | ||||
|         <div class="content-container-select"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.historyTranslateRoute') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-select v-model="configInfo.historyTranslateRoute" :placeholder="t('translate.selectRoute')" size="default" | ||||
|               style="width: 100%" @change="handleHistoryTranslateRouteChange"> | ||||
|               <el-option v-for="item in menuStore.translationRoute" :key="item.name" :label="item[currentLanguageName]" | ||||
|                 :value="item.name" /> | ||||
|             </el-select> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-radio" v-if="configInfo.receiveTranslateStatus === 'true'"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.translateHistory') }}</el-text> | ||||
|         <div class="content-container-select"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.sourceLanguage') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-select v-model="configInfo.receiveSourceLanguage" :placeholder="t('translate.sourceLanguage')" | ||||
|               size="default" style="width: 100%"> | ||||
|               <el-option :label="t('translate.autoDetect')" value="auto" /> | ||||
|               <el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" | ||||
|                 :value="item.code" /> | ||||
|             </el-select> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|             v-model="configInfo.translateHistory" /> | ||||
|         <div class="content-container-select"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.targetLanguage') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-select v-model="configInfo.receiveTargetLanguage" placeholder="" size="default" style="width: 100%"> | ||||
|               <el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" | ||||
|                 :value="item.code" /> | ||||
|             </el-select> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-radio" v-if="configInfo.receiveTranslateStatus === 'true'"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.autoTranslateGroupMessage') }}</el-text> | ||||
|         <el-divider /> | ||||
|         <div class="content-container-radio"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.realTimeSend') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|               v-model="configInfo.sendTranslateStatus" /> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|             v-model="configInfo.autoTranslateGroupMessage" /> | ||||
|         <div class="content-container-select"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.sourceLanguage') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-select v-model="configInfo.sendSourceLanguage" :placeholder="t('translate.sourceLanguage')" size="default" | ||||
|               style="width: 100%"> | ||||
|               <el-option :label="t('translate.autoDetect')" value="auto" /> | ||||
|               <el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" | ||||
|                 :value="item.code" /> | ||||
|             </el-select> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-select"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.sourceLanguage') }}</el-text> | ||||
|         <div class="content-container-select"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>{{ t('translate.targetLanguage') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-select v-model="configInfo.sendTargetLanguage" placeholder="" size="default" style="width: 100%"> | ||||
|               <el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" | ||||
|                 :value="item.code" /> | ||||
|             </el-select> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-select v-model="configInfo.receiveSourceLanguage" :placeholder="t('translate.sourceLanguage')" | ||||
|             size="default" style="width: 100%"> | ||||
|             <el-option :label="t('translate.autoDetect')" value="auto" /> | ||||
|             <el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" | ||||
|               :value="item.code" /> | ||||
|           </el-select> | ||||
|         <div class="content-container-radio"> | ||||
|           <div class="content-left intercept-left"> | ||||
|             <el-text>{{ t('translate.interceptLanguagesLabel') }}</el-text> | ||||
|           </div> | ||||
|           <div class="content-right intercept-row"> | ||||
|             <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|               v-model="configInfo.interceptChinese" /> | ||||
|             <el-select | ||||
|               v-if="configInfo.interceptChinese === 'true'" | ||||
|               v-model="configInfo.interceptLanguages" | ||||
|               multiple | ||||
|               collapse-tags | ||||
|               :placeholder="t('translate.selectInterceptLanguages')" | ||||
|               class="intercept-lang-select" | ||||
|               @change="onInterceptLangChange" | ||||
|             > | ||||
|               <el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" :value="item.code" /> | ||||
|             </el-select> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-select"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.targetLanguage') }}</el-text> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-select v-model="configInfo.receiveTargetLanguage" placeholder="" size="default" style="width: 100%"> | ||||
|             <el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" | ||||
|               :value="item.code" /> | ||||
|           </el-select> | ||||
|         </div> | ||||
|       </div> | ||||
|       <el-divider /> | ||||
|       <div class="content-container-radio"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.realTimeSend') }}</el-text> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|             v-model="configInfo.sendTranslateStatus" /> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-select"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.sourceLanguage') }}</el-text> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-select v-model="configInfo.sendSourceLanguage" :placeholder="t('translate.sourceLanguage')" size="default" | ||||
|             style="width: 100%"> | ||||
|             <el-option :label="t('translate.autoDetect')" value="auto" /> | ||||
|             <el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" | ||||
|               :value="item.code" /> | ||||
|           </el-select> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-select"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.targetLanguage') }}</el-text> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-select v-model="configInfo.sendTargetLanguage" placeholder="" size="default" style="width: 100%"> | ||||
|             <el-option v-for="item in platformLanguageList" :key="item.code" :label="item[currentLanguageName]" | ||||
|               :value="item.code" /> | ||||
|           </el-select> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="content-container-radio"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.interceptChinese') }}</el-text> | ||||
|         </div> | ||||
|         <div class="content-right"> | ||||
|           <el-switch :active-value="'true'" :inactive-value="'false'" size="default" | ||||
|             v-model="configInfo.interceptChinese" /> | ||||
|         </div> | ||||
|       </div> | ||||
|       <el-divider /> | ||||
|       <!-- <div class="content-container-radio"> | ||||
|         <el-divider /> | ||||
|         <!-- <div class="content-container-radio"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.friendIndependent') }}</el-text> | ||||
|         </div> | ||||
| @ -158,7 +196,7 @@ | ||||
|           /> | ||||
|         </div> | ||||
|       </div> --> | ||||
|       <!-- <div class="content-container-radio"> | ||||
|         <!-- <div class="content-container-radio"> | ||||
|         <div class="content-left"> | ||||
|           <el-text>{{ t('translate.preview') }}</el-text> | ||||
|         </div> | ||||
| @ -171,16 +209,17 @@ | ||||
|           /> | ||||
|         </div> | ||||
|       </div> --> | ||||
|     </template> | ||||
|     <template v-else-if="Object.keys(configInfo).length === 0 && currentUserId !== null"> | ||||
|       <div class="no-personal-config">{{ t('translate.notFoundPersonalizedTranslation') }}</div> | ||||
|       <el-button type="primary" size="small" style="margin: 16px auto 0; display: block;" | ||||
|         @click="handleCreatePersonalConfig"> | ||||
|         {{ t('translate.createPersonalizedTranslation') || '新建个性化翻译' }} | ||||
|       </el-button> | ||||
|     </template> | ||||
|     <template v-else> | ||||
|       <div class="no-personal-config">{{ t('translate.chooseContactFirst') }}</div> | ||||
|       </template> | ||||
|       <template v-else-if="getConfigInfoIsNull && menuStore.currentUserId !== null"> | ||||
|         <div class="no-personal-config">{{ t('translate.notFoundPersonalizedTranslation') }}</div> | ||||
|         <el-button type="primary" size="small" style="margin: 16px auto 0; display: block;" | ||||
|           @click="handleCreatePersonalConfig"> | ||||
|           {{ t('translate.createPersonalizedTranslation') || '新建个性化翻译' }} | ||||
|         </el-button> | ||||
|       </template> | ||||
|       <template v-else> | ||||
|         <div class="no-personal-config">{{ t('translate.chooseContactFirst') }}</div> | ||||
|       </template> | ||||
|     </template> | ||||
|   </div> | ||||
| </template> | ||||
| @ -192,7 +231,7 @@ import { ipcApiRoute } from "@/api"; | ||||
| import CleanIcon from "@/components/icons/CleanIcon.vue"; | ||||
| import { useMenuStore } from '@/stores/menuStore'; | ||||
| import { ElMessage } from "element-plus"; | ||||
| import { QuestionFilled } from "@element-plus/icons-vue"; | ||||
| import { QuestionFilled, Refresh } from "@element-plus/icons-vue"; | ||||
| import { useI18n } from 'vue-i18n'; | ||||
| import { config, nextTick } from "process"; | ||||
|  | ||||
| @ -201,13 +240,23 @@ const configInfo = ref({}) | ||||
| const { t, locale } = useI18n(); | ||||
| const showLocalTranslate = ref(false); | ||||
| const activeTab = ref('current'); | ||||
| const currentUserId = ref(null); | ||||
| // const currentUserId = ref(null); // 移除本地currentUserId | ||||
| const loading = ref(false); // 新增 loading 状态 | ||||
| const refreshLoading = ref(false); // 刷新按钮loading状态 | ||||
| const RefreshIcon = Refresh; // 刷新图标 | ||||
|  | ||||
| // 修改计算属性名称使其更通用 | ||||
| const currentLanguageName = computed(() => { | ||||
|   return locale.value === 'zh' ? 'zhName' : 'enName'; | ||||
| }); | ||||
|  | ||||
| const getConfigInfoIsNull = computed(() => { | ||||
|   return Object.keys(configInfo.value).length === 0; | ||||
| }); | ||||
|  | ||||
| // 加载/归一化阶段守卫,避免把初始化过程的值写回数据库 | ||||
| const isApplyingConfig = ref(false); | ||||
|  | ||||
| // watch( | ||||
| //     () => configInfo.value.friendTranslateStatus, | ||||
| //     async (newValue, oldValue) => { | ||||
| @ -242,8 +291,11 @@ const propertiesToWatch = [ | ||||
|   "chineseDetectionStatus", | ||||
|   "translatePreview", | ||||
|   "interceptChinese", | ||||
|   "interceptLanguages", | ||||
|   "translateHistory", | ||||
|   "autoTranslateGroupMessage", | ||||
|   "historyTranslateRoute", | ||||
|   "usePersonalConfig" | ||||
| ]; | ||||
|  | ||||
| let watchers = []; // 存储所有字段的监听器 | ||||
| @ -255,6 +307,11 @@ const addWatchers = () => { | ||||
|     watch( | ||||
|       () => unref(configInfo.value[property]), | ||||
|       (newValue, oldValue) => { | ||||
|         if (isApplyingConfig.value) return; // 初始化阶段不写回 | ||||
|         if (property === 'interceptLanguages') { | ||||
|           // 由 @change 显式持久化,避免初始化或平台切换时误写空 | ||||
|           return; | ||||
|         } | ||||
|         if (newValue !== "" && newValue !== oldValue) { | ||||
|           handlePropertyChange(property, newValue); | ||||
|         } | ||||
| @ -264,9 +321,18 @@ const addWatchers = () => { | ||||
| } | ||||
| // 自定义逻辑 | ||||
| const handlePropertyChange = async (property, value) => { | ||||
|   const args = { key: property, value: value, id: configInfo.value.id, partitionId: menuStore.currentPartitionId }; | ||||
|   const id = configInfo.value?.id; | ||||
|   if (!id) return; // 未就绪不提交 | ||||
|   const args = { key: property, value: value, id, partitionId: menuStore.currentPartitionId }; | ||||
|   await ipc.invoke(ipcApiRoute.updateTranslateConfig, args); | ||||
| } | ||||
|  | ||||
| // 显式提交拦截语言的更改 | ||||
| const onInterceptLangChange = async (val) => { | ||||
|   if (isApplyingConfig.value) return; | ||||
|   const list = Array.isArray(val) ? val.filter(Boolean) : []; | ||||
|   await handlePropertyChange('interceptLanguages', list); | ||||
| } | ||||
| // 移除所有字段的监听器 | ||||
| const removeWatchers = () => { | ||||
|   watchers.forEach((stopWatcher) => stopWatcher()); // 调用每个监听器的停止方法 | ||||
| @ -276,15 +342,19 @@ const removeWatchers = () => { | ||||
| // 切换会话的回调 | ||||
| const onTranslateConfigUpdate = (event, args) => { | ||||
|   const { data } = args; | ||||
|   currentUserId.value = data.userId; | ||||
|   menuStore.setCurrentUserId(data.userId); | ||||
|   configInfo.value = {}; | ||||
|   getConfigInfo() | ||||
| } | ||||
|  | ||||
| watch(activeTab, (newVal) => { | ||||
|   getConfigInfo() | ||||
| watch(activeTab, async (newVal) => { | ||||
|   configInfo.value = {}; // 切换tab时先清空内容 | ||||
|   await getConfigInfo(); | ||||
| }); | ||||
|  | ||||
| const getConfigInfo = async () => { | ||||
|   loading.value = true; // 开始加载 | ||||
|   isApplyingConfig.value = true; | ||||
|   removeWatchers(); | ||||
|  | ||||
|   let type = activeTab.value; | ||||
| @ -296,23 +366,36 @@ const getConfigInfo = async () => { | ||||
|  | ||||
|   if (type === 'global') { | ||||
|     args.platform = menuStore.platform; | ||||
|     // args.partitionId = ''; | ||||
|   } else { | ||||
|     args.userId = currentUserId.value; | ||||
|     // args.partitionId = menuStore.currentPartitionId; | ||||
|     args.userId = menuStore.currentUserId; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     const res = await ipc.invoke(ipcApiRoute.getTrsConfig, args); | ||||
|     if (res.status) { | ||||
|       Object.assign(configInfo.value, res.data); // 更新表单数据 | ||||
|       const data = { ...res.data }; | ||||
|       // 兼容旧数据:当前联系人缺少 usePersonalConfig 字段时默认启用个人配置 | ||||
|       if (activeTab.value === 'current' && (data.usePersonalConfig === undefined || data.usePersonalConfig === null || data.usePersonalConfig === '')) { | ||||
|         data.usePersonalConfig = 'true'; | ||||
|       } | ||||
|       // 兼容:逗号字符串 -> 数组 | ||||
|       if (typeof data.interceptLanguages === 'string') { | ||||
|         data.interceptLanguages = data.interceptLanguages.length > 0 | ||||
|           ? data.interceptLanguages.split(',').map(s=>s.trim()).filter(Boolean) | ||||
|           : []; | ||||
|       } | ||||
|       if (!Array.isArray(data.interceptLanguages)) data.interceptLanguages = []; | ||||
|       configInfo.value = data; | ||||
|     } else { | ||||
|       configInfo.value = {}; | ||||
|     } | ||||
|   } catch (err) { | ||||
|     configInfo.value = {}; | ||||
|     console.log(err) | ||||
|   } finally { | ||||
|     isApplyingConfig.value = false; | ||||
|     addWatchers(); | ||||
|     loading.value = false; // 加载结束 | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -338,8 +421,40 @@ onMounted(async () => { | ||||
| }) | ||||
|  | ||||
| // 清理缓存 | ||||
| const cleanMsgCache = () => { | ||||
|   // 实现清理逻辑 | ||||
| const cleanMsgCache = async () => { | ||||
|   try { | ||||
|     const res = await ipc.invoke(ipcApiRoute.clearTranslateCache, { | ||||
|       partitionId: menuStore.currentPartitionId, | ||||
|       platform: menuStore.currentMenu | ||||
|     }); | ||||
|  | ||||
|     if (res.status) { | ||||
|       ElMessage.success(res.message || '清理缓存成功'); | ||||
|  | ||||
|       // 刷新当前会话页面 | ||||
|       await refreshCurrentSession(); | ||||
|     } else { | ||||
|       ElMessage.error(res.message || '清理缓存失败'); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('清理缓存失败:', error); | ||||
|     ElMessage.error('清理缓存失败: ' + (error.message || '未知错误')); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // 刷新当前会话页面 | ||||
| const refreshCurrentSession = async () => { | ||||
|   try { | ||||
|     if (menuStore.currentPartitionId) { | ||||
|       // 通知当前会话页面刷新翻译按钮 | ||||
|       await ipc.invoke(ipcApiRoute.refreshSessionTranslateButtons, { | ||||
|         partitionId: menuStore.currentPartitionId | ||||
|       }); | ||||
|       console.log('当前会话页面已刷新'); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.warn('刷新当前会话页面失败:', error); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // 语言列表 | ||||
| @ -365,87 +480,145 @@ const getLanguageList = async () => { | ||||
| watch( | ||||
|   () => configInfo.value.translateRoute, // 监听的数据源 | ||||
|   async (newValue, oldValue) => { | ||||
|     if (!newValue || typeof newValue !== 'string') { | ||||
|       console.warn("watch(translateRoute): 无效的翻译平台值或值为空。"); | ||||
|       // 如果平台值无效或为空,可以考虑清空 platformLanguageList | ||||
|       // 并且将所有语言设置重置为 "auto",因为没有可用的平台语言 | ||||
|       configInfo.value.receiveSourceLanguage = "auto"; | ||||
|       configInfo.value.receiveTargetLanguage = "auto"; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // 确保 languageList 已经加载,如果为空,可能需要等待数据加载或提示错误 | ||||
|     if (languageList.value.length === 0) { | ||||
|       await getLanguageList(); | ||||
|  | ||||
|       if (languageList.value.length === 0) { | ||||
|         console.warn("watch(translateRoute): 翻译平台列表为空。"); | ||||
|     isApplyingConfig.value = true; | ||||
|     try { | ||||
|       if (!newValue || typeof newValue !== 'string') { | ||||
|         console.warn("watch(translateRoute): 无效的翻译平台值或值为空。"); | ||||
|         // 如果平台值无效或为空,可以考虑清空 platformLanguageList | ||||
|         // 并且将所有语言设置重置为 "auto",因为没有可用的平台语言 | ||||
|         configInfo.value.receiveSourceLanguage = "auto"; | ||||
|         configInfo.value.receiveTargetLanguage = "auto"; | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 筛选出当前平台支持的语言列表 | ||||
|     const items = languageList.value.filter(item => item[newValue]); | ||||
|     platformLanguageList.value = items; // 更新平台支持的语言列表 | ||||
|      | ||||
|     // 检查是否有支持的语言 | ||||
|     if (items.length > 0) { | ||||
|       const findLanguageInItems = (code) => items.find(item => item.code === code); | ||||
|       // 确保 languageList 已经加载,如果为空,可能需要等待数据加载或提示错误 | ||||
|       if (languageList.value.length === 0) { | ||||
|         await getLanguageList(); | ||||
|  | ||||
|       // 处理接收(Receive)相关的语言设置 | ||||
|       if (configInfo.value.receiveSourceLanguage !== "auto") { | ||||
|         const foundReceiveSourceLanguage = findLanguageInItems(configInfo.value.receiveSourceLanguage); | ||||
|         if (!foundReceiveSourceLanguage) { | ||||
|           configInfo.value.receiveSourceLanguage = "auto"; | ||||
|         if (languageList.value.length === 0) { | ||||
|           console.warn("watch(translateRoute): 翻译平台列表为空。"); | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       const foundReceiveTargetLanguage = findLanguageInItems(configInfo.value.receiveTargetLanguage); | ||||
|       if (!foundReceiveTargetLanguage) { | ||||
|         configInfo.value.receiveTargetLanguage = items[0].code; | ||||
|       } | ||||
|       // 筛选出当前平台支持的语言列表 | ||||
|       const items = languageList.value.filter(item => item[newValue]); | ||||
|       platformLanguageList.value = items; // 更新平台支持的语言列表 | ||||
|  | ||||
|       // 处理发送(Send)相关的语言设置 | ||||
|       if (configInfo.value.sendSourceLanguage !== "auto") { | ||||
|         const foundSendSourceLanguage = findLanguageInItems(configInfo.value.sendSourceLanguage); | ||||
|         if (!foundSendSourceLanguage) { | ||||
|           configInfo.value.sendSourceLanguage = "auto"; | ||||
|       // 检查是否有支持的语言 | ||||
|       if (items.length > 0) { | ||||
|         const findLanguageInItems = (code) => items.find(item => item.code === code); | ||||
|  | ||||
|         // 接收 | ||||
|         if (configInfo.value.receiveSourceLanguage !== "auto") { | ||||
|           const ok = findLanguageInItems(configInfo.value.receiveSourceLanguage); | ||||
|           if (!ok) configInfo.value.receiveSourceLanguage = "auto"; | ||||
|         } | ||||
|         if (!findLanguageInItems(configInfo.value.receiveTargetLanguage)) { | ||||
|           configInfo.value.receiveTargetLanguage = items[0].code; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       const foundSendTargetLanguage = findLanguageInItems(configInfo.value.sendTargetLanguage); | ||||
|       if (!foundSendTargetLanguage) { | ||||
|         configInfo.value.sendTargetLanguage = items[0].code; | ||||
|         // 发送 | ||||
|         if (configInfo.value.sendSourceLanguage !== "auto") { | ||||
|           const ok2 = findLanguageInItems(configInfo.value.sendSourceLanguage); | ||||
|           if (!ok2) configInfo.value.sendSourceLanguage = "auto"; | ||||
|         } | ||||
|         if (!findLanguageInItems(configInfo.value.sendTargetLanguage)) { | ||||
|           configInfo.value.sendTargetLanguage = items[0].code; | ||||
|         } | ||||
|  | ||||
|         // 兼容:拦截语言多选列表同样使用平台语言(尽量映射而不是直接丢弃) | ||||
|         if (Array.isArray(configInfo.value.interceptLanguages)) { | ||||
|           const toLower = (s) => String(s || '').toLowerCase(); | ||||
|           const mapToSupported = (code) => { | ||||
|             const c = toLower(code); | ||||
|             // 1) 先精确匹配 | ||||
|             let found = items.find(it => toLower(it.code) === c); | ||||
|             if (found) return found.code; | ||||
|             // 2) 无地域码 -> 用 startsWith 匹配(如 'zh' -> 'zh-CN') | ||||
|             found = items.find(it => toLower(it.code).startsWith(c)); | ||||
|             if (found) return found.code; | ||||
|             // 3) 常见兜底 | ||||
|             if (c.startsWith('zh')) { | ||||
|               found = items.find(it => toLower(it.code).startsWith('zh')); | ||||
|               if (found) return found.code; | ||||
|             } | ||||
|             if (c.startsWith('en')) { | ||||
|               found = items.find(it => toLower(it.code).startsWith('en')); | ||||
|               if (found) return found.code; | ||||
|             } | ||||
|             return null; | ||||
|           }; | ||||
|           const mapped = configInfo.value.interceptLanguages | ||||
|             .map(mapToSupported) | ||||
|             .filter(Boolean); | ||||
|           configInfo.value.interceptLanguages = Array.from(new Set(mapped)); | ||||
|         } | ||||
|       } else { | ||||
|         // 如果当前平台没有支持的语言配置,则全部默认设置为 "auto" | ||||
|         console.warn(`当前平台 (${newValue}) 没有配置任何支持的语言,将语言设置重置为 \"auto\"。`); | ||||
|         configInfo.value.receiveSourceLanguage = "auto"; | ||||
|         configInfo.value.receiveTargetLanguage = "auto"; | ||||
|         configInfo.value.sendSourceLanguage = "auto"; | ||||
|         configInfo.value.sendTargetLanguage = "auto"; | ||||
|       } | ||||
|     } else { | ||||
|       // 如果当前平台没有支持的语言配置,则全部默认设置为 "auto" | ||||
|       console.warn(`当前平台 (${newValue}) 没有配置任何支持的语言,将语言设置重置为 "auto"。`); | ||||
|       configInfo.value.receiveSourceLanguage = "auto"; | ||||
|       configInfo.value.receiveTargetLanguage = "auto"; | ||||
|       configInfo.value.sendSourceLanguage = "auto"; | ||||
|       configInfo.value.sendTargetLanguage = "auto"; | ||||
|     } finally { | ||||
|       isApplyingConfig.value = false; | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     immediate: true, // 立即执行一次监听器,处理组件挂载时的初始值 | ||||
|     deep: false // 对于基本类型或浅层对象,不需要深度监听 | ||||
|   } | ||||
| ); | ||||
|   }, { immediate: true }) | ||||
|  | ||||
| const handleTranslateRouteChange = () => { | ||||
|   // 处理翻译路线变更的逻辑 | ||||
|   console.log('翻译路线已变更:', configInfo.value.translateRoute); | ||||
| } | ||||
|  | ||||
| const handleHistoryTranslateRouteChange = () => { | ||||
|   // 处理历史翻译路线变更的逻辑 | ||||
|   console.log('历史翻译路线已变更:', configInfo.value.historyTranslateRoute); | ||||
| } | ||||
|  | ||||
| const handleCreatePersonalConfig = async () => { | ||||
|   if (!currentUserId.value) { | ||||
|   if (!menuStore.currentUserId) { | ||||
|     ElMessage({ message: t('translate.noUserId') || '无法获取用户ID', type: 'error' }); | ||||
|     return; | ||||
|   } | ||||
|   try { | ||||
|     await ipc.invoke(ipcApiRoute.createTranslateConfig, { userId: currentUserId.value }); | ||||
|     await ipc.invoke(ipcApiRoute.createTranslateConfig, { userId: menuStore.currentUserId }); | ||||
|     ElMessage.success(t('common.success') || '创建成功'); | ||||
|     getConfigInfo(); | ||||
|     await getConfigInfo(); | ||||
|     if (configInfo.value && configInfo.value.id && (configInfo.value.usePersonalConfig === undefined || configInfo.value.usePersonalConfig === null || configInfo.value.usePersonalConfig === '')) { | ||||
|       await handlePropertyChange('usePersonalConfig', 'true'); | ||||
|       configInfo.value.usePersonalConfig = 'true'; | ||||
|     } | ||||
|   } catch (e) { | ||||
|     ElMessage.error((e && e.message) || t('common.failed') || '创建失败'); | ||||
|   } | ||||
| } | ||||
|  | ||||
| // 刷新翻译路由 | ||||
| const refreshTranslateRoutes = async () => { | ||||
|   refreshLoading.value = true; | ||||
|   try { | ||||
|     const res = await ipc.invoke(ipcApiRoute.refreshTranslateRoutes, {}); | ||||
|     if (res.status) { | ||||
|       // 重新获取翻译路由列表 | ||||
|       const routeRes = await ipc.invoke(ipcApiRoute.getRouteList, {}); | ||||
|       if (routeRes.status && routeRes.data) { | ||||
|         const finalRoutes = routeRes.data.filter(item => item.enable == 1); | ||||
|         menuStore.setTranslationRoute(finalRoutes); | ||||
|         ElMessage.success('翻译路由刷新成功'); | ||||
|       } | ||||
|     } else { | ||||
|       ElMessage.error(res.message || '刷新失败'); | ||||
|     } | ||||
|   } catch (error) { | ||||
|     console.error('刷新翻译路由失败:', error); | ||||
|     ElMessage.error('刷新翻译路由失败: ' + (error.message || '未知错误')); | ||||
|   } finally { | ||||
|     refreshLoading.value = false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| // onUnmounted(() => { | ||||
| //   ipc.removeAllListeners('translate-config-update') | ||||
| // }) | ||||
| @ -454,7 +627,7 @@ const handleCreatePersonalConfig = async () => { | ||||
|  | ||||
| <style scoped lang="less"> | ||||
| .translate-config { | ||||
|   width: 300px; | ||||
|   width: 320px; | ||||
|   height: 100%; | ||||
|   padding: 20px; | ||||
|   display: flex; | ||||
| @ -538,6 +711,28 @@ const handleCreatePersonalConfig = async () => { | ||||
|       flex: 1; | ||||
|       align-items: center; | ||||
|     } | ||||
|  | ||||
|     .intercept-left { | ||||
|       flex: 0 0 90px; /* 左侧固定更窄,为右侧选择框腾出空间 */ | ||||
|     } | ||||
|     .intercept-row { | ||||
|       gap: 6px; /* 再缩小中间空隙 */ | ||||
|       flex: 1 1 auto; /* 右侧区域最大化 */ | ||||
|     } | ||||
|     .intercept-lang-select { | ||||
|       flex: 1; | ||||
|       width: 100%; | ||||
|       min-width: 0; /* 允许弹性收缩 */ | ||||
|     } | ||||
|     :deep(.intercept-row .el-select) { | ||||
|       width: 100% !important; /* 强制选择器撑满右侧 */ | ||||
|     } | ||||
|     :deep(.el-select__popper) { | ||||
|       min-width: 320px !important; /* 下拉浮层更宽,避免文字截断 */ | ||||
|     } | ||||
|     :deep(.el-select__tags) { | ||||
|       max-width: 100%; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .content-container-radio-group { | ||||
|  | ||||
| @ -220,8 +220,13 @@ const addWatchers = ()=> { | ||||
| // 自定义逻辑 | ||||
| const handlePropertyChange = async (property, value) => { | ||||
|   if (userInfo && userInfo.value?.id){ | ||||
|     const args = {key: property, value: value,id:userInfo.value.id}; | ||||
|     const args = {key: property, value: value, id: userInfo.value.id}; | ||||
|     await ipc.invoke(ipcApiRoute.updateContactInfo, args); | ||||
|     if (property === 'nickName') { | ||||
|       // 更新昵称时调用 updateContactRemark | ||||
|       const args = {partitionId: menuStore.currentPartitionId, nickName: value}; | ||||
|       await ipc.invoke(ipcApiRoute.updateContactRemark, args); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| // 移除所有字段的监听器 | ||||
| @ -252,6 +257,16 @@ const getUserInfo = async () => { | ||||
|     addWatchers() | ||||
|   } | ||||
| } | ||||
| // 监听会话切换,重新获取用户信息 | ||||
| watch( | ||||
|   () => menuStore.currentPartitionId, | ||||
|   async (newValue, oldValue) => { | ||||
|     if (newValue && newValue !== oldValue) { | ||||
|       await getUserInfo() | ||||
|     } | ||||
|   } | ||||
| ); | ||||
|  | ||||
| onMounted(async () => { | ||||
|   await getUserInfo() | ||||
| }) | ||||
|  | ||||
| @ -67,6 +67,7 @@ import UserInfo from "@/views/right-menu/UserInfo.vue"; | ||||
| import QuickReply from "@/views/right-menu/QuickReply.vue"; | ||||
| import ProxyConfig from "@/views/right-menu/ProxyConfig.vue"; | ||||
| import KefuContact from '@/views/right-menu/KefuContact.vue' | ||||
| import ScreenShotIcon from '@/components/icons/ScreenShotIcon.vue'; | ||||
| import { useMenuStore } from '@/stores/menuStore'; | ||||
| import { ipcApiRoute } from "@/api"; | ||||
| import { ipc } from "@/utils/ipcRenderer"; | ||||
| @ -92,6 +93,7 @@ const menuItems = computed(() => [ | ||||
|   { icon: markRaw(User), action: 'UserInfo' }, | ||||
|   { icon: markRaw(QuickReplyIcon), action: 'QuickReply' }, | ||||
|   { icon: markRaw(ServerIcon), action: 'ProxyConfig' }, | ||||
|   { icon: markRaw(ScreenShotIcon), action: 'ScreenShot' }, | ||||
| ]) | ||||
|  | ||||
| // 响应式状态 | ||||
| @ -123,7 +125,11 @@ const processWidth = () => { | ||||
| watch(() => isCollapsed.value, processWidth, { immediate: false }); | ||||
|  | ||||
| // 菜单项选择处理 | ||||
| const selectMenuItem = (action) => { | ||||
| const selectMenuItem = async (action) => { | ||||
|   if (action === 'ScreenShot') { | ||||
|     await ipc.invoke(ipcApiRoute.captureScreen); | ||||
|     return; | ||||
|   } | ||||
|   if (action === activeMenu.value) { | ||||
|     // 当点击相同的菜单项时,切换折叠状态 | ||||
|     isCollapsed.value = !isCollapsed.value; | ||||
| @ -207,6 +213,7 @@ const menuTooltips = { | ||||
|   UserInfo: t('rightMenu.userInfo'), | ||||
|   QuickReply: t('rightMenu.quickReply'), | ||||
|   ProxyConfig: t('rightMenu.proxyConfig'), | ||||
|   ScreenShot: t('rightMenu.screenshot') || '截图', | ||||
| }; | ||||
|  | ||||
| const tooltip = ref({ visible: false, text: '', x: 0, y: 0, direction: 'bottom' }); | ||||
|  | ||||
| @ -12,9 +12,49 @@ const { t } = useI18n(); | ||||
| const menuStore = useMenuStore(); | ||||
| // 存储选中ID的集合 | ||||
| const selectedRows = ref([]) | ||||
| // 搜索关键字(用于顶部搜索弹窗) | ||||
| const searchQuery = ref(''); | ||||
| // 代理测试结果(当前弹窗): null | 'success' | 'error' | ||||
| const proxyTestResult = ref(null); | ||||
|  | ||||
| // 过滤会话数据(受搜索影响) | ||||
| const filteredTableData = computed(() => { | ||||
|   const menus = (menuStore.getCurrentChildren() || []); | ||||
|   const raw = typeof menus === 'function' ? menus() : menus; // 兼容 getter 写法 | ||||
|   const list = Array.isArray(raw) ? raw : []; | ||||
|   const keyword = (searchQuery.value || '').trim().toLowerCase(); | ||||
|   if (!keyword) return list; | ||||
|   return list.filter((row) => { | ||||
|     const nick = String(row?.nickName || '').toLowerCase(); | ||||
|     const remarks = String(row?.remarks || '').toLowerCase(); | ||||
|     const user = String(row?.userName || '').toLowerCase(); | ||||
|     const webUrl = String(row?.webUrl || '').toLowerCase(); | ||||
|     return ( | ||||
|       nick.includes(keyword) || | ||||
|       remarks.includes(keyword) || | ||||
|       user.includes(keyword) || | ||||
|       webUrl.includes(keyword) | ||||
|     ); | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| const tableData = ref([]); | ||||
| const getTableData = async () => { | ||||
|   tableData.value = await menuStore.getCurrentChildren(); | ||||
|   // 获取每个会话的代理状态 | ||||
|   const rows = Array.isArray(tableData.value) ? tableData.value : []; | ||||
|   for (const row of rows) { | ||||
|     try { | ||||
|       const res = await ipc.invoke(ipcApiRoute.checkProxyStatus, { partitionId: row.partitionId, platform: row.platform }); | ||||
|       const should = res?.data?.shouldUseProxy; | ||||
|       const using = res?.data?.usingProxy; | ||||
|       row._proxyShould = typeof should === 'boolean' ? should : !!using; | ||||
|       row._proxyReachable = res?.data?.reachable === null ? null : !!res?.data?.reachable; | ||||
|     } catch (e) { | ||||
|       row._proxyShould = false; | ||||
|       row._proxyReachable = false; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| onMounted(async () => { | ||||
|   await getTableData(); | ||||
| @ -57,6 +97,29 @@ const getProxyInfo = async (row) => { | ||||
|   } | ||||
|   proxyDialogVisible.value = true; | ||||
| } | ||||
| const testProxyConfig = async () => { | ||||
|   const args = { | ||||
|     proxyStatus: proxyForm.value.proxyStatus, | ||||
|     proxyType: proxyForm.value.proxyType, | ||||
|     proxyIp: proxyForm.value.proxyIp, | ||||
|     proxyPort: proxyForm.value.proxyPort, | ||||
|     userVerifyStatus: proxyForm.value.userVerifyStatus, | ||||
|     username: proxyForm.value.username, | ||||
|     password: proxyForm.value.password, | ||||
|   }; | ||||
|   try { | ||||
|     const res = await ipc.invoke(ipcApiRoute.testProxy, args); | ||||
|     proxyTestResult.value = res.status ? 'success' : 'error'; | ||||
|     if (res.status) { | ||||
|       ElMessage.success(res.message || t('session.proxyTestSuccess') || '代理连接成功'); | ||||
|     } else { | ||||
|       ElMessage.error(res.message || t('session.proxyTestFailed') || '代理连接失败'); | ||||
|     } | ||||
|   } catch (e) { | ||||
|     proxyTestResult.value = 'error'; | ||||
|     ElMessage.error(t('session.proxyTestFailed') || '代理连接失败'); | ||||
|   } | ||||
| } | ||||
| const saveProxyConfig = async () => { | ||||
|   // 构建参数对象 | ||||
|   const args = { | ||||
| @ -73,8 +136,23 @@ const saveProxyConfig = async () => { | ||||
|   }; | ||||
|   // 调用主进程方法 | ||||
|   const res = await ipc.invoke(ipcApiRoute.saveProxyInfo, args); | ||||
|  | ||||
|   // 根据返回结果处理 | ||||
|   if (res.status) { | ||||
|     // 保存成功后,后台会立即应用代理设置。我们顺便刷新当前行状态。 | ||||
|     try { | ||||
|       const pid = proxyForm.value.partitionId || proxyForm.value.id?.partitionId || menuStore.currentPartitionId; | ||||
|       const r = await ipc.invoke(ipcApiRoute.checkProxyStatus, { partitionId: pid, platform: menuStore.platform }); | ||||
|       const menu = menuStore.menus.find(m => m.id === menuStore.currentMenu); | ||||
|       const row = menu?.children?.find(c => c.partitionId === pid); | ||||
|       if (row) { | ||||
|         const should = r?.data?.shouldUseProxy; | ||||
|         const using = r?.data?.usingProxy; | ||||
|         row._proxyShould = typeof should === 'boolean' ? should : !!using; | ||||
|         row._proxyReachable = r?.data?.reachable === null ? null : !!r?.data?.reachable; | ||||
|       } | ||||
|     } catch (e) {} | ||||
|  | ||||
|     ElMessage({ | ||||
|       message: t('session.proxyConfigSaveSuccess'), | ||||
|       type: 'success', | ||||
| @ -200,6 +278,7 @@ const startAll = async () => { | ||||
|       menuStore.addChildrenMenu(item); | ||||
|       menuStore.updateChildrenMenu(res.data); | ||||
|     } | ||||
|  | ||||
|   } | ||||
| } | ||||
| const closeAll = async () => { | ||||
| @ -383,7 +462,7 @@ const handleBatchProxySave = async () => { | ||||
|     </el-dialog> | ||||
|  | ||||
|     <!--代理设置弹出层--> | ||||
|     <el-dialog v-model="proxyDialogVisible" :title="t('session.proxyConfig')" width="550"> | ||||
|     <el-dialog v-model="proxyDialogVisible" :title="t('session.proxyConfig')" width="580"> | ||||
|       <div class="proxy-config-dialog-form"> | ||||
|         <!--    代理开关--> | ||||
|         <div class="content-container-radio"> | ||||
| @ -447,14 +526,22 @@ const handleBatchProxySave = async () => { | ||||
|           <div class="content-right"> | ||||
|             <el-input :placeholder="t('session.password')" v-model="proxyForm.password" | ||||
|               :type="showProxyPassword ? 'text' : 'password'"> | ||||
|               <template #suffix> | ||||
|                 <el-icon style="cursor:pointer;" @click="showProxyPassword = !showProxyPassword"> | ||||
|                   <component :is="showProxyPassword ? Hide : View" /> | ||||
|                 </el-icon> | ||||
|               </template> | ||||
|             </el-input> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- 连接状态提示 --> | ||||
|         <div class="content-container-input" v-if="proxyTestResult"> | ||||
|           <div class="content-left"> | ||||
|             <el-text>连接状态</el-text> | ||||
|           </div> | ||||
|           <div class="content-right"> | ||||
|             <el-tag :type="proxyTestResult === 'success' ? 'success' : 'danger'"> | ||||
|               {{ proxyTestResult === 'success' ? '代理连接成功' : '代理连接失败' }} | ||||
|             </el-tag> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <!--    时区--> | ||||
|         <div class="content-container-input"> | ||||
|           <div class="content-left"> | ||||
| @ -490,6 +577,7 @@ const handleBatchProxySave = async () => { | ||||
|       <template #footer> | ||||
|         <span class="proxy-config-dialog-footer"> | ||||
|           <el-button @click="proxyDialogVisible = false">{{ t('common.cancel') }}</el-button> | ||||
|           <el-button type="warning" @click="testProxyConfig" style="margin-right: 8px;">{{ t('session.testProxy') || '测试代理' }}</el-button> | ||||
|           <el-button type="primary" @click="saveProxyConfig">{{ t('common.confirm') }}</el-button> | ||||
|         </span> | ||||
|       </template> | ||||
| @ -526,8 +614,18 @@ const handleBatchProxySave = async () => { | ||||
|             <el-button size="small" type="primary" :icon="Search" circle /> | ||||
|           </template> | ||||
|           <div class="search-bth"> | ||||
|             <el-input v-if="isCustomWeb" :placeholder="t('session.remarks')" /> | ||||
|             <el-input v-else :placeholder="t('session.searchPlaceholder')" /> | ||||
|             <el-input | ||||
|               v-if="isCustomWeb" | ||||
|               v-model="searchQuery" | ||||
|               :placeholder="t('session.remarks')" | ||||
|               clearable | ||||
|             /> | ||||
|             <el-input | ||||
|               v-else | ||||
|               v-model="searchQuery" | ||||
|               :placeholder="t('session.searchPlaceholder')" | ||||
|               clearable | ||||
|             /> | ||||
|           </div> | ||||
|         </el-popover> | ||||
|         <el-button size="small" type="primary" @click="batchProxyDialogVisible = true" style="margin-left: 8px;"> | ||||
| @ -537,7 +635,7 @@ const handleBatchProxySave = async () => { | ||||
|     </div> | ||||
|     <!--表格部分--> | ||||
|     <div class="table-data"> | ||||
|       <el-table border height="100%" :data="tableData" @selection-change="handleSelectionChange" row-key="partitionId"> | ||||
|       <el-table border height="100%" :data="filteredTableData" @selection-change="handleSelectionChange" row-key="partitionId"> | ||||
|         <el-table-column type="selection" /> | ||||
|         <el-table-column align="center" prop="createTime" :label="t('session.createTime')" /> | ||||
|         <el-table-column align="center" :label="t('session.sessionRecord')" min-width="100"> | ||||
| @ -595,6 +693,18 @@ const handleBatchProxySave = async () => { | ||||
|             </div> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|         <el-table-column align="center" label="代理状态" width="140"> | ||||
|           <template #default="{ row }"> | ||||
|             <template v-if="row._proxyShould === false"> | ||||
|               <el-tag type="info" size="small">未启用</el-tag> | ||||
|             </template> | ||||
|             <template v-else> | ||||
|               <el-tag v-if="row._proxyReachable === true" type="success" size="small">成功</el-tag> | ||||
|               <el-tag v-else-if="row._proxyReachable === false" type="danger" size="small">失败</el-tag> | ||||
|               <el-tag v-else type="warning" size="small">检测中</el-tag> | ||||
|             </template> | ||||
|           </template> | ||||
|         </el-table-column> | ||||
|         <el-table-column align="center" prop="isTop" label="置顶" width="70"> | ||||
|           <template #default="{ row }"> | ||||
|             <el-checkbox v-model="row.isTop" @change="toggleTop(row)" :true-label="'true'" | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| { | ||||
|   "name": "liangzi", | ||||
|   "version": "1.0.42", | ||||
|   "version": "1.0.55", | ||||
|   "description": "量子翻译", | ||||
|   "main": "./public/electron/main.js", | ||||
|   "scripts": { | ||||
|     "dev": "set BASE_URL=127.0.0.1:8000&& chcp 65001&&ee-bin dev", | ||||
|     "build": "set BASE_URL=haiapp.org&& npm run build-frontend && npm run build-electron && ee-bin encrypt", | ||||
|     "start": "ee-bin start", | ||||
|     "start": "chcp 65001 && set NODE_OPTIONS=--max_old_space_size=4096 && ee-bin start", | ||||
|     "dev-frontend": "set BASE_URL=127.0.0.1:8000&& ee-bin dev --serve=frontend", | ||||
|     "dev-electron": "set BASE_URL=127.0.0.1:8000&& ee-bin dev --serve=electron", | ||||
|     "build-frontend": "ee-bin build --cmds=frontend && ee-bin move --flag=frontend_dist", | ||||
| @ -44,6 +44,7 @@ | ||||
|     "better-sqlite3": "^11.5.0", | ||||
|     "crypto-js": "^4.2.0", | ||||
|     "ee-core": "^4.0.0", | ||||
|     "electron-screenshots": "^0.5.27", | ||||
|     "electron-session-proxy": "^1.0.2", | ||||
|     "electron-updater": "^6.3.8", | ||||
|     "input": "^1.0.1", | ||||
|  | ||||
							
								
								
									
										4
									
								
								public/dist/index.html
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								public/dist/index.html
									
									
									
									
										vendored
									
									
								
							| @ -85,8 +85,8 @@ | ||||
|       } | ||||
|  | ||||
|     </style> | ||||
|     <script type="module" crossorigin src="./assets/index-jqD4rXg9.js"></script> | ||||
|     <link rel="stylesheet" crossorigin href="./assets/index-C-v6BnYe.css"> | ||||
|     <script type="module" crossorigin src="./assets/index-DyAPP5Kn.js"></script> | ||||
|     <link rel="stylesheet" crossorigin href="./assets/index-Dq73M9Ka.css"> | ||||
|   </head> | ||||
|   <body style="padding: 0; margin: 0;"> | ||||
|     <div id="loadingPage"> | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	