1、初始化仓库

This commit is contained in:
2025-06-16 14:21:17 +08:00
commit aa52db5c1f
108 changed files with 14619 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
node_modules
out/
logs/
run/
myUserData/
.idea/
package-lock.json
data/
public/dist/assets/*
.vscode/launch.json
public/electron/
pnpm-lock.yaml

201
LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 哆啦好梦
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

1
README.md Normal file
View File

@ -0,0 +1 @@
Telegram 协议版多开

Binary file not shown.

View File

@ -0,0 +1 @@
建议第三方软件放置在此目录中,打包时会将资源加入安装包内。

BIN
build/icons/256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
build/icons/32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
build/icons/512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
build/icons/64x64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
build/icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
build/icons/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View File

199
cmd/bin.js Normal file
View File

@ -0,0 +1,199 @@
/**
* ee-bin 配置
* 仅适用于开发环境
*/
module.exports = {
/**
* development serve ("frontend" "electron" )
* ee-bin dev
*/
dev: {
frontend: {
directory: './frontend',
cmd: 'npm',
args: ['run', 'dev'],
port: 8080,
},
electron: {
directory: './',
cmd: 'electron',
args: ['.', '--env=local'],
watch: false,
}
},
/**
* 构建
* ee-bin build
*/
build: {
frontend: {
directory: './frontend',
cmd: 'npm',
args: ['run', 'build'],
},
electron: {
type: 'javascript',
bundleType: 'copy'
},
win64: {
cmd: 'electron-builder',
directory: './',
args: ['--config=./cmd/builder.json', '-w=nsis', '--x64'],
},
win32: {
args: ['--config=./cmd/builder.json', '-w=nsis', '--ia32'],
},
win_e: {
args: ['--config=./cmd/builder.json', '-w=portable', '--x64'],
},
win_7z: {
args: ['--config=./cmd/builder.json', '-w=7z', '--x64'],
},
mac: {
args: ['--config=./cmd/builder-mac.json', '-m'],
},
mac_arm64: {
args: ['--config=./cmd/builder-mac-arm64.json', '-m', '--arm64'],
},
linux: {
args: ['--config=./cmd/builder-linux.json', '-l=deb', '--x64'],
},
linux_arm64: {
args: ['--config=./cmd/builder-linux.json', '-l=deb', '--arm64'],
},
go_w: {
directory: './go',
cmd: 'go',
args: ['build', '-o=../build/extraResources/goapp.exe'],
},
go_m: {
directory: './go',
cmd: 'go',
args: ['build', '-o=../build/extraResources/goapp'],
},
go_l: {
directory: './go',
cmd: 'go',
args: ['build', '-o=../build/extraResources/goapp'],
},
python: {
directory: './python',
cmd: 'python',
args: ['./setup.py', 'build'],
},
},
/**
* 移动资源
* ee-bin move
*/
move: {
frontend_dist: {
src: './frontend/dist',
dest: './public/dist'
},
go_static: {
src: './frontend/dist',
dest: './go/public/dist'
},
go_config: {
src: './go/config',
dest: './go/public/config'
},
go_package: {
src: './package.json',
dest: './go/public/package.json'
},
go_images: {
src: './public/images',
dest: './go/public/images'
},
python_dist: {
src: './python/dist',
dest: './build/extraResources/py'
},
},
/**
* 预发布模式prod
* ee-bin start
*/
start: {
directory: './',
cmd: 'electron',
args: ['.', '--env=prod']
},
/**
* 加密
*/
encrypt: {
frontend: {
type: 'none',
files: [
'./public/dist/**/*.(js|json)',
],
cleanFiles: ['./public/dist'],
confusionOptions: {
compact: true,
stringArray: true,
stringArrayEncoding: ['none'],
stringArrayCallsTransform: true,
numbersToExpressions: true,
target: 'browser',
}
},
electron: {
type: 'confusion',
files: [
'./public/electron/**/*.(js|json)',
],
cleanFiles: ['./public/electron'],
specificFiles: [
'./public/electron/main.js',
'./public/electron/preload/bridge.js',
],
confusionOptions: {
compact: true,
stringArray: true,
stringArrayEncoding: ['rc4'],
deadCodeInjection: false,
stringArrayCallsTransform: true,
numbersToExpressions: true,
target: 'node',
}
}
},
/**
* 执行自定义命令
* ee-bin exec
*/
exec: {
// 单独调试air 实现 go 热重载
go: {
directory: './go',
cmd: 'air',
args: ['-c=config/.air.toml' ],
},
// windows 单独调试air 实现 go 热重载
go_w: {
directory: './go',
cmd: 'air',
args: ['-c=config/.air.windows.toml' ],
},
// 单独调试,以基础方式启动 go
go2: {
directory: './go',
cmd: 'go',
args: ['run', './main.go', '--env=dev','--basedir=../', '--port=7073'],
},
python: {
directory: './python',
cmd: 'python',
args: ['./main.py', '--port=7074'],
stdio: "inherit", // ignore
},
},
};

40
cmd/builder-linux.json Normal file
View File

@ -0,0 +1,40 @@
{
"productName": "SeaBox",
"appId": "com.bilibili.ee",
"copyright": "© 2025 mr Technology Co., Ltd.",
"directories": {
"output": "out"
},
"asar": true,
"files": [
"**/*",
"!cmd/",
"!data/",
"!electron/",
"!frontend/",
"!logs/",
"!out/",
"!go/",
"!python/"
],
"extraResources": [
{
"from": "build/extraResources",
"to": "extraResources"
}
],
"publish": [
{
"provider": "generic",
"url": ""
}
],
"linux": {
"icon": "build/icons/icon.icns",
"artifactName": "${productName}-${os}-${version}-${arch}.${ext}",
"target": [
"deb"
],
"category": "Utility"
}
}

View File

@ -0,0 +1,38 @@
{
"productName": "量子翻译",
"appId": "com.bilibili.ee",
"copyright": "© 2025 mr Technology Co., Ltd.",
"directories": {
"output": "out"
},
"asar": true,
"files": [
"**/*",
"!cmd/",
"!data/",
"!electron/",
"!frontend/",
"!logs/",
"!out/",
"!go/",
"!python/"
],
"extraResources": [
{
"from": "build/extraResources",
"to": "extraResources"
}
],
"publish": [
{
"provider": "generic",
"url": ""
}
],
"mac": {
"icon": "build/icons/icon.icns",
"artifactName": "${productName}-${os}-${version}-${arch}.${ext}",
"darkModeSupport": true,
"hardenedRuntime": false
}
}

38
cmd/builder-mac.json Normal file
View File

@ -0,0 +1,38 @@
{
"productName": "量子翻译",
"appId": "com.bilibili.ee",
"copyright": "© 2025 mr Technology Co., Ltd.",
"directories": {
"output": "out"
},
"asar": true,
"files": [
"**/*",
"!cmd/",
"!data/",
"!electron/",
"!frontend/",
"!logs/",
"!out/",
"!go/",
"!python/"
],
"extraResources": [
{
"from": "build/extraResources",
"to": "extraResources"
}
],
"publish": [
{
"provider": "generic",
"url": ""
}
],
"mac": {
"icon": "build/icons/icon.icns",
"artifactName": "${productName}-${os}-${version}-${arch}.${ext}",
"darkModeSupport": true,
"hardenedRuntime": false
}
}

50
cmd/builder.json Normal file
View File

@ -0,0 +1,50 @@
{
"productName": "liangzi",
"appId": "com.liangzi.ee",
"copyright": "© 2025 mr Technology Co., Ltd.",
"directories": {
"output": "out"
},
"asar": true,
"files": [
"**/*",
"!cmd/",
"!data/",
"!electron/",
"!frontend/",
"!logs/",
"!out/",
"!go/",
"!python/"
],
"extraResources": {
"from": "build/extraResources/",
"to": "extraResources"
},
"nsis": {
"oneClick": false,
"allowElevation": true,
"allowToChangeInstallationDirectory": true,
"installerIcon": "build/icons/icon.ico",
"uninstallerIcon": "build/icons/icon.ico",
"installerHeaderIcon": "build/icons/icon.ico",
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "liangzi"
},
"publish": [
{
"provider": "generic",
"url": "http://apiweb.net/static/"
}
],
"win": {
"icon": "build/icons/icon.ico",
"artifactName": "${productName}-${os}-${version}-${arch}.${ext}",
"target": [
{
"target": "nsis"
}
]
}
}

BIN
electron/.DS_Store vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,72 @@
'use strict';
const path = require('path');
const { getBaseDir } = require('ee-core/ps');
/**
* 默认配置
*/
module.exports = () => {
return {
openDevTools: false,
singleLock: true,
windowsOption: {
title: '量子翻译',
width: 1000,
height: 700,
minWidth: 1000,
minHeight: 700,
webPreferences: {
// webSecurity: false,
contextIsolation: false, // false -> 可在渲染进程中使用electron的apitrue->需要bridge.js(contextBridge)
nodeIntegration: true,
//preload: path.join(getElectronDir(), 'preload', 'bridge.js'),
},
titleBarStyle: 'hidden',
frame: true,
show: false,
icon: path.join(getBaseDir(), 'public', 'images', 'logo-32.png'),
},
logger: {
rotator: 'day',
level: 'INFO',
outputJSON: false,
appLogName: 'ee.log',
coreLogName: 'ee-core.log',
errorLogName: 'ee-error.log',
},
remote: {
enable: false,
url: ''
},
socketServer: {
enable: false,
port: 7070,
path: "/socket.io/",
connectTimeout: 45000,
pingTimeout: 30000,
pingInterval: 25000,
maxHttpBufferSize: 1e8,
transports: ["polling", "websocket"],
cors: {
origin: true,
},
channel: 'socket-channel'
},
httpServer: {
enable: false,
https: {
enable: false,
key: '/public/ssl/localhost+1.key',
cert: '/public/ssl/localhost+1.pem'
},
host: '127.0.0.1',
port: 7071,
},
mainServer: {
indexPath: '/public/dist/index.html',
channelSeparator: '/',
}
}
}

View File

@ -0,0 +1,13 @@
'use strict';
/**
* Development environment configuration, coverage config.default.js
*/
module.exports = () => {
return {
openDevTools: true,
jobs: {
messageLog: false
}
};
};

View File

@ -0,0 +1,10 @@
'use strict';
/**
* coverage config.default.js
*/
module.exports = () => {
return {
openDevTools: false,
};
};

View File

@ -0,0 +1,34 @@
'use strict';
const { logger } = require('ee-core/log');
const {contactInfoService} = require("../service/contactInfo");
/**
* 联系人信息api
* @class
*/
class ContactInfoController {
async getContactInfo(args,event) {
return await contactInfoService.getContactInfo(args,event);
}
async updateContactInfo(args,event) {
return await contactInfoService.updateContactInfo(args,event);
}
async getFollowRecord(args,event) {
return await contactInfoService.getFollowRecord(args,event);
}
async addFollowRecord(args,event) {
return await contactInfoService.addFollowRecord(args,event);
}
async updateFollowRecord(args,event) {
return await contactInfoService.updateFollowRecord(args,event);
}
async deleteFollowRecord(args,event) {
return await contactInfoService.deleteFollowRecord(args,event);
}
}
ContactInfoController.toString = () => '[class ContactInfoController]';
module.exports = ContactInfoController;

View File

@ -0,0 +1,42 @@
'use strict';
const { logger } = require('ee-core/log');
const {quickReplyService} = require("../service/quickreply");
/**
* 快捷回复 api
* @class
*/
class QuickReplyController {
async getGroups(args,event) {
return await quickReplyService.getGroups(args,event);
}
async getContentByGroupId(args,event) {
return await quickReplyService.getContentByGroupId(args,event);
}
async addGroup(args,event) {
return await quickReplyService.addGroup(args,event);
}
async editGroup(args,event) {
return await quickReplyService.editGroup(args,event);
}
async deleteGroup(args,event) {
return await quickReplyService.deleteGroup(args,event);
}
async addReply(args,event) {
return await quickReplyService.addReply(args,event);
}
async editReply(args,event) {
return await quickReplyService.editReply(args,event);
}
async deleteReply(args,event) {
return await quickReplyService.deleteReply(args,event);
}
async deleteAllReply(args,event) {
return await quickReplyService.deleteAllReply(args,event);
}
}
QuickReplyController.toString = () => '[class QuickReplyController]';
module.exports = QuickReplyController;

View File

@ -0,0 +1,47 @@
'use strict';
const { logger } = require('ee-core/log');
const {systemService} = require("../service/system");
const {app} = require("electron");
const {windowService} = require("../service/window");
/**
* SystemController 获取系统基本信息
* @class
*/
class SystemController {
async getBaseInfo(args,event) {
return await systemService.getBaseInfo(args,event);
}
async login(args,event) {
return await systemService.login(args,event);
}
async logOut(args, event) {
for (const [key, view] of app.viewsMap) {
await windowService._destroyView(key)
}
return {status:true,message:'操作成功'}
}
async userLogin(args, event) {
return await systemService.userLogin(args, event);
}
async createSub(args, event) {
return await systemService.createSub(args, event);
}
async listSub(args, event) {
return await systemService.listSub(args, event);
}
async deleteSub(args, event) {
return await systemService.deleteSub(args, event);
}
}
SystemController.toString = () => '[class SystemController]';
module.exports = SystemController;

View File

@ -0,0 +1,63 @@
'use strict';
const { logger } = require('ee-core/log');
const {translateService} = require("../service/translate");
const {app} = require("electron");
// Imports the Google Cloud client library
const {Translate} = require('@google-cloud/translate').v2;
/**
* 翻译配置
* @class
*/
class TranslateController {
async getConfigInfo(args,event) {
return await translateService.getConfigInfo(args,event);
}
async changeAloneStatus(args,event) {
return await translateService.changeAloneStatus(args,event);
}
async updateTranslateConfig(args,event) {
return await translateService.updateTranslateConfig(args,event);
}
async getLanguageList(args,event) {
return await translateService.getLanguageList(args,event);
}
async addLanguage(args,event) {
return await translateService.addLanguage(args,event);
}
async deleteLanguage(args,event) {
return await translateService.deleteLanguage(args,event);
}
async editLanguage(args,event) {
return await translateService.editLanguage(args,event);
}
async addTranslateRoute(args,event) {
return await translateService.addTranslateRoute(args,event);
}
async editTranslateRoute(args,event) {
return await translateService.editTranslateRoute(args,event);
}
async getRouteConfig(args,event) {
return await translateService.getRouteConfig(args,event);
}
async getRouteList(args, event) {
return await translateService.getRouteList(args,event);
}
async testRoute(args,event) {
return await translateService.testRoute(args,event);
}
async translateText(args,event) {
return await translateService.translateText(args,event);
}
}
TranslateController.toString = () => '[class TranslateController]';
module.exports = TranslateController;

View File

@ -0,0 +1,75 @@
'use strict';
const { logger } = require('ee-core/log');
const {windowService} = require("../service/window");
/**
* SystemController 获取系统基本信息
* @class
*/
class WindowController {
// 添加会话窗口
async addSession(args,event) {
return await windowService.addSession(args,event);
}
// 修改会话信息
async editSession(args,event) {
return await windowService.editSession(args,event);
}
// 启动会话
async startSession(args,event) {
return await windowService.startSession(args,event);
}
// 获取所有会话信息
async getSessions(args,event) {
return await windowService.getSessions(args,event);
}
// 根据分区id查询会话信息
async getSessionByPartitionId(args,event) {
return await windowService.getSessionByPartitionId(args,event);
}
// 设置会话窗口位置
async setWindowLocation(args,event) {
return await windowService.setWindowLocation(args,event);
}
//隐藏会话窗口
async hiddenSession(args,event) {
return await windowService.hiddenSession(args,event);
}
//显示会话窗口
async showSession(args,event) {
return await windowService.showSession(args,event);
}
//关闭会话窗口
async closeSession(args,event) {
return await windowService.closeSession(args,event);
}
//刷新会话
async refreshSession(args,event) {
return await windowService.refreshSession(args,event);
}
//删除会话窗口
async deleteSession(args,event) {
return await windowService.deleteSession(args,event);
}
//获取窗口代理配置信息
async getProxyInfo(args,event) {
return await windowService.getProxyInfo(args,event);
}
//修改窗口代理配置信息
async editProxyInfo(args,event) {
return await windowService.editProxyInfo(args,event);
}
//修改窗口代理配置信息(多属性一次性修改)
async saveProxyInfo(args,event) {
return await windowService.saveProxyInfo(args,event);
}
//打开当前会话控制台
async openSessionDevTools(args,event) {
return await windowService.openSessionDevTools(args,event);
}
}
WindowController.toString = () => '[class WindowController]';
module.exports = WindowController;

25
electron/main.js Normal file
View File

@ -0,0 +1,25 @@
const { ElectronEgg } = require('ee-core');
const { Lifecycle } = require('./preload/lifecycle');
const { preload } = require('./preload');
const path = require('path');
const { app: electronApp } = require('electron');
// new app
const app = new ElectronEgg();
// register lifecycle
const life = new Lifecycle();
app.register("ready", life.ready);
app.register("electron-app-ready", life.electronAppReady);
app.register("window-ready", life.windowReady);
app.register("before-close", life.beforeClose);
// register preload
app.register("preload", preload);
// 设置用户数据路径
const customUserDataPath = path.join(process.cwd(), 'myUserData');
console.log('customUserDataPath', customUserDataPath);
electronApp.setPath('userData', customUserDataPath);
// run
app.run();

View File

@ -0,0 +1,9 @@
/*
* 如果启用了上下文隔离渲染进程无法使用electron的api
* 可通过contextBridge 导出api给渲染进程使用
*/
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer: ipcRenderer,
})

View File

@ -0,0 +1,22 @@
/*
* 如果启用了上下文隔离渲染进程无法使用electron的api
* 可通过contextBridge 导出api给渲染进程使用
*/
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer: ipcRenderer,
loginNotify: (args) => {
return ipcRenderer.invoke('online-notify', args);
},
infoUpdate: (args) => {
return ipcRenderer.invoke('info-update', args);
},
getTranslateConfig: (args) => {
return ipcRenderer.invoke('translate-config', args);
},
//{provider,text,code}
translateText: (args) => {
return ipcRenderer.invoke('text-translate', args);
}
})

View File

@ -0,0 +1,22 @@
/*
* 如果启用了上下文隔离渲染进程无法使用electron的api
* 可通过contextBridge 导出api给渲染进程使用
*/
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer: ipcRenderer,
loginNotify: (args) => {
return ipcRenderer.invoke('online-notify', args);
},
infoUpdate: (args) => {
return ipcRenderer.invoke('info-update', args);
},
getTranslateConfig: (args) => {
return ipcRenderer.invoke('translate-config', args);
},
//{provider,text,code}
translateText: (args) => {
return ipcRenderer.invoke('text-translate', args);
}
})

View File

@ -0,0 +1,22 @@
/*
* 如果启用了上下文隔离渲染进程无法使用electron的api
* 可通过contextBridge 导出api给渲染进程使用
*/
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer: ipcRenderer,
loginNotify: (args) => {
return ipcRenderer.invoke('online-notify', args);
},
infoUpdate: (args) => {
return ipcRenderer.invoke('info-update', args);
},
getTranslateConfig: (args) => {
return ipcRenderer.invoke('translate-config', args);
},
//{provider,text,code}
translateText: (args) => {
return ipcRenderer.invoke('text-translate', args);
}
})

172
electron/preload/index.js Normal file
View File

@ -0,0 +1,172 @@
/*************************************************
** preload为预加载模块该文件将会在程序启动时加载 **
*************************************************/
const { logger } = require('ee-core/log');
const { app, nativeTheme, ipcMain ,shell} = require('electron')
const { getMainWindow } = require('ee-core/electron');
const {translateService} = require("../service/translate");
const {contactInfoService} = require("../service/contactInfo");
const preload = async () => {
ipcMainListener()
}
const ipcMainListener = ()=>{
// 窗口控制按钮监听
ipcMain.on('window-control', (event, data) => {
const mainWindow = getMainWindow();
const {action} = data;
if (!mainWindow) return
switch(action) {
case 'minimize':
mainWindow.minimize()
break
case 'toggle-maximize':
mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize()
break
case 'toggle-fullscreen':
mainWindow.setFullScreen(!mainWindow.isFullScreen())
break
case 'close':
mainWindow.close()
break
}
})
// 暴露一个方法用于打开链接
ipcMain.handle('open-external-link', (event, url) => {
shell.openExternal(url)
.then(() => {
logger.info(`Successfully opened the link : ${url}`);
})
.catch(err => {
logger.error(`Unable to open link in external browser: ${url}`, err);
});
});
//登录通知
ipcMain.handle('online-notify', async (event, args) => {
const { onlineStatus, avatarUrl, platform, nickName, phoneNumber, userName, msgCount } = args;
const senderId = event.sender.id;
const mainWin = getMainWindow();
// 校验主窗口是否存在且未被销毁
if (!mainWin || mainWin.isDestroyed()) {
return; // 主窗口不存在时终止处理
}
const sessionObj = await app.sdb.selectOne('session_list', { windowId: senderId });
if (sessionObj) {
const status = String(onlineStatus);
if (onlineStatus) {
await app.sdb.update('session_list',
{ onlineStatus: status, avatarUrl, userName, nickName, msgCount },
{ platform, windowId: senderId }
);
} else {
await app.sdb.update('session_list',
{ onlineStatus: status },
{ platform, windowId: senderId }
);
}
const data = await app.sdb.selectOne('session_list', { platform, windowId: senderId });
// 发送前再次校验窗口状态
if (!mainWin.isDestroyed()) {
mainWin.webContents.send('online-notify', { data });
}
}
});
//会话切换,推送配置更新
ipcMain.handle('info-update', async (event, args) => {
const {platform,userId} = args;
const senderId = event.sender.id;
const mainWin = getMainWindow();
//推送翻译配置更新
const trsRes = await translateService.getConfigInfo({userId:userId,platform:platform},event);
if (trsRes.status) {
const data = trsRes.data;
mainWin.webContents.send('translate-config-update', {data:data});
}
//推送联系人信息更新
const contactRes = await contactInfoService.getContactInfo({userId:userId,platform:platform},event);
if (contactRes.status) {
const data = contactRes.data;
mainWin.webContents.send('contact-data-update', {data});
}
});
//会话切换,获取翻译配置信息
ipcMain.handle('translate-config', async (event, args) => {
const {platform,userId} = args;
//推送翻译配置更新
const trsRes = await translateService.getConfigInfo({userId:userId,platform:platform},event);
if (trsRes) {
const data = trsRes.data;
const route = await app.sdb.selectOne('translate_route', {name:data.translateRoute});
if (route) {
trsRes.data['zhName'] = route.zhName
trsRes.data['enName'] = route.enName
}
return trsRes;
}
});
// 文本翻译
ipcMain.handle('text-translate', async (event, args) => {
const { text, from, to,route ,refresh = 'false',isFilter = 'false',mode} = args;
if (text && text.trim() && to && route) {
//查询判断翻译服务商是否支持这个编码
const languageObj = await app.sdb.selectOne('language_list',{code:to})
if (languageObj) {
// 翻译服务
const toCode = languageObj[route];
if (toCode) {
const windowId = event.sender.id;
const sessionObj = await app.sdb.selectOne('session_list',{windowId:windowId});
const nArgs = {
mode:mode,
text:text,
to:toCode,
sourceTo:to,
route:route,
from:from,
partitionId:sessionObj?.partitionId,
isFilter:isFilter,
}
if (refresh ==='true' && isFilter === 'false') {
await app.sdb.delete('translate_cache',{partitionId:sessionObj.partitionId,toCode:toCode,text:text})
}
return await translateService.translateText(nArgs);
}else {
return { status: false, message: `${languageObj?.zhName || route}不支持当前翻译语言!请检查翻译编码配置!`};
}
}else {
return { status: false, message: `${languageObj?.zhName || route}不支持当前翻译语言!请检查翻译编码配置!`};
}
} else {
logger.info('参数不能为空:',args)
return { status: false, message: '翻译文本或编码不能为空!', data: text };
}
});
// 快捷回复 调用发送函数
ipcMain.handle('send-msg', async (event, args) => {
const { text,partitionId,type} = args;
if (!text || !partitionId) {
return {status:false,message:'内容或窗口会话ID不能为空'}
}
const view = app.viewsMap.get(partitionId);
if (view && !view.webContents.isDestroyed()) {
const data = { type, text };
await view.webContents.executeJavaScript(`quickReply(${JSON.stringify(data)})`);
}
});
ipcMain.handle('theme-change', async (event, args) => {
const { theme} = args;
if (theme) {
nativeTheme.themeSource = theme;
}
});
}
/**
* 预加载模块入口
*/
module.exports = {
preload
}

View File

@ -0,0 +1,762 @@
"use strict";
const { logger } = require("ee-core/log");
const { getConfig } = require("ee-core/config");
const { getMainWindow } = require("ee-core/electron");
const { app, shell } = require("electron");
const Database = require("../utils/DatabaseUtils");
const { getTimeStr } = require("../utils/CommonUtils");
const { initUpdater } = require("../updater");
const baseUrl = "http://127.0.0.1:8000/api";
// const baseUrl = 'http://haiapp.org/api'
const initializeDatabase = async () => {
// 定义表结构
const tables = {
parameter: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT", // 添加自增主键字段
key: "TEXT", // 键
value: "TEXT", // 值
},
constraints: [],
},
tg_sessions: {
columns: {
phoneNumber: "TEXT",
sessionStr: "TEXT",
},
constraints: ["PRIMARY KEY(phoneNumber)"],
},
session_list: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
partitionId: "TEXT",
windowId: "INTEGER",
windowStatus: "TEXT",
platform: "TEXT",
onlineStatus: "TEXT",
createTime: "TEXT",
remarks: "TEXT",
webUrl: "TEXT",
avatarUrl: "TEXT",
userName: "TEXT",
nickName: "TEXT",
msgCount: "INTEGER",
userAgent: "TEXT",
},
constraints: [],
},
translate_config: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
userId: "TEXT",
platform: "TEXT",
mode: 'TEXT DEFAULT "cloud"', //翻译模式 本地 local 云端 cloud
translateRoute: "TEXT",
receiveTranslateStatus: "TEXT",
receiveSourceLanguage: "TEXT",
receiveTargetLanguage: "TEXT",
sendTranslateStatus: "TEXT",
sendSourceLanguage: "TEXT",
sendTargetLanguage: "TEXT",
friendTranslateStatus: "TEXT",
showAloneBtn: "TEXT",
chineseDetectionStatus: "TEXT",
translatePreview: "TEXT",
},
constraints: [],
},
translate_route: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
name: "TEXT",
zhName: "TEXT",
enName: "TEXT",
otherArgs: "TEXT",
},
constraints: [],
},
contact_info: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
userId: "TEXT",
platform: "TEXT",
phoneNumber: "TEXT",
nickName: "TEXT",
country: "TEXT",
gender: "TEXT",
gradeActivity: "INTEGER",
customLevel: "INTEGER",
remarks: "TEXT",
},
constraints: [],
},
follow_record: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
userId: "TEXT",
platform: "TEXT",
content: "TEXT",
timestamp: "TEXT",
},
constraints: [],
},
language_list: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
code: "TEXT",
zhName: "TEXT",
enName: "TEXT",
baidu: "TEXT",
youDao: "TEXT",
huoShan: "TEXT",
xiaoNiu: "TEXT",
google: "TEXT",
timestamp: "TEXT",
},
constraints: [],
},
translate_cache: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
route: "TEXT",
text: "TEXT",
translateText: "TEXT",
fromCode: "TEXT",
toCode: "TEXT",
partitionId: "TEXT",
timestamp: "TEXT",
},
constraints: [],
},
proxy_config: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
partitionId: "TEXT",
proxyStatus: "TEXT",
proxyType: "TEXT",
proxyIp: "TEXT",
proxyPort: "TEXT",
userVerifyStatus: "TEXT",
username: "TEXT",
password: "TEXT",
},
constraints: [],
},
group_manage: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
name: "TEXT",
},
constraints: [],
},
quick_reply_record: {
columns: {
id: "INTEGER PRIMARY KEY AUTOINCREMENT",
groupId: "TEXT",
type: "TEXT",
remark: "TEXT",
content: "TEXT",
url: "TEXT",
},
constraints: [],
},
};
// 同步每个表的结构
for (const [tableName, { columns, constraints }] of Object.entries(tables)) {
await new Database().syncTableStructure(tableName, columns, constraints);
}
app.sdb = new Database();
};
const initializePlatform = async () => {
const platforms = [
{ platform: "Telegram", url: "https://web.telegram.org/a/" },
{ platform: "WhatsApp", url: "https://web.whatsapp.com/" },
{ platform: "TikTok", url: "https://www.tiktok.com/messages?lang=zh-Hans" },
];
app.platforms = platforms;
};
const initializeTableData = async () => {
await app.sdb.update(
"session_list",
{ windowStatus: "false", msgCount: 0, onlineStatus: "false", windowId: 0 },
{}
);
// 初始化翻译线路
const translationRoute = [
{
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-api.baidu.com/api/trans/vip/translate","appId": "","apiKey": ""}`,
},
{
name: "huoShan",
zhName: "火山翻译",
enName: "VolcEngine Translate",
otherArgs: `{"apiUrl": "https://translate.volcengineapi.com","apiKey": "","secretKey": ""}`,
},
{
name: "xiaoNiu",
zhName: "小牛翻译",
enName: "Niutrans",
otherArgs: `{"apiUrl": "https://api.niutrans.com/NiuTransServer/translation","apiKey": ""}`,
},
{
name: "google",
zhName: "谷歌翻译",
enName: "Google Translate",
otherArgs: `{"apiUrl": "https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto"}`,
},
];
for (let item of translationRoute) {
const args = {
name: item.name,
zhName: item.zhName,
enName: item.enName,
otherArgs: item.otherArgs,
};
const route = await app.sdb.selectOne("translate_route", {
name: item.name,
});
if (!route) {
// 初始化翻译线路
await app.sdb.insert("translate_route", args);
}
}
//初始化翻译编码信息
// 插入中文(简体)
const zh = await app.sdb.selectOne("language_list", { code: "zh-CN" });
if (!zh) {
app.sdb.insert("language_list", {
code: "zh-CN",
zhName: "简体中文",
enName: "Simplified Chinese",
timestamp: getTimeStr(),
baidu: "zh", // 百度:中文(zh)
youDao: "zh-CHS", // 有道:中文(zh-CHS)
huoShan: "zh-Hans", // 火山:中文(zh-Hans)
xiaoNiu: "zh", // 小牛:中文(zh)
tengXun: "zh", // 腾讯:中文(zh)
google: "zh-CN", // 谷歌:中文(zh-CN)
});
}
// 插入英语
const en = await app.sdb.selectOne("language_list", { code: "en" });
if (!en) {
app.sdb.insert("language_list", {
code: "en",
zhName: "英语",
enName: "English",
timestamp: getTimeStr(),
baidu: "en", // 百度
youDao: "en", // 有道
huoShan: "en", // 火山
xiaoNiu: "en", // 小牛
tengXun: "en", //腾讯
google: "en", // 谷歌
});
}
// 插入西班牙语
const es = await app.sdb.selectOne("language_list", { code: "es" });
if (!es) {
app.sdb.insert("language_list", {
code: "es",
zhName: "西班牙语",
enName: "Spanish",
timestamp: getTimeStr(),
baidu: "spa", // 百度使用西班牙语缩写
youDao: "es", // 有道
huoShan: "es", // 火山
xiaoNiu: "es", // 小牛
tengXun: "es", // 腾讯
google: "es", // 谷歌
});
}
// 插入法语
const fr = await app.sdb.selectOne("language_list", { code: "fr" });
if (!fr) {
app.sdb.insert("language_list", {
code: "fr",
zhName: "法语",
enName: "French",
timestamp: getTimeStr(),
baidu: "fra", // 百度使用法语缩写
youDao: "fr", // 有道
huoShan: "fr", // 火山
xiaoNiu: "fr", // 小牛
tengXun: "fr", // 腾讯
google: "fr", // 谷歌
});
}
// 插入阿拉伯语
const ar = await app.sdb.selectOne("language_list", { code: "ar" });
if (!ar) {
app.sdb.insert("language_list", {
code: "ar",
zhName: "阿拉伯语",
enName: "Arabic",
timestamp: getTimeStr(),
baidu: "ara", // 百度使用标准代码
youDao: "ar", // 有道
huoShan: "ar", // 火山
xiaoNiu: "ar", // 小牛
tengXun: "ar", // 腾讯
google: "ar", // 谷歌
});
}
// 插入葡萄牙语
const pt = await app.sdb.selectOne("language_list", { code: "pt" });
if (!pt) {
app.sdb.insert("language_list", {
code: "pt",
zhName: "葡萄牙语",
enName: "Portuguese",
timestamp: getTimeStr(),
baidu: "pt", // 百度
youDao: "pt", // 有道
huoShan: "pt", // 火山
xiaoNiu: "pt", // 小牛
tengXun: "pt", // 腾讯
google: "pt", // 谷歌
});
}
// 插入俄语
const ru = await app.sdb.selectOne("language_list", { code: "ru" });
if (!ru) {
app.sdb.insert("language_list", {
code: "ru",
zhName: "俄语",
enName: "Russian",
timestamp: getTimeStr(),
baidu: "ru", // 百度
youDao: "ru", // 有道
huoShan: "ru", // 火山
xiaoNiu: "ru", // 小牛
tengXun: "ru", // 腾讯
google: "ru", // 谷歌
});
}
// 插入德语
const de = await app.sdb.selectOne("language_list", { code: "de" });
if (!de) {
app.sdb.insert("language_list", {
code: "de",
zhName: "德语",
enName: "German",
timestamp: getTimeStr(),
baidu: "de", // 百度
youDao: "de", // 有道
huoShan: "de", // 火山
xiaoNiu: "de", // 小牛
tengXun: "de", // 腾讯
google: "de", // 谷歌
});
}
// 插入日语
const ja = await app.sdb.selectOne("language_list", { code: "ja" });
if (!ja) {
app.sdb.insert("language_list", {
code: "ja",
zhName: "日语",
enName: "Japanese",
timestamp: getTimeStr(),
baidu: "jp", // 百度特殊代码
youDao: "ja", // 有道
huoShan: "ja", // 火山
xiaoNiu: "ja", // 小牛
tengXun: "ja", // 腾讯
google: "ja", // 谷歌
});
}
// 插入韩语
const ko = await app.sdb.selectOne("language_list", { code: "ko" });
if (!ko) {
app.sdb.insert("language_list", {
code: "ko",
zhName: "韩语",
enName: "Korean",
timestamp: getTimeStr(),
baidu: "kor", // 百度使用韩语缩写
youDao: "ko", // 有道
huoShan: "ko", // 火山
xiaoNiu: "ko", // 小牛
tengXun: "ko", // 腾讯
google: "ko", // 谷歌
});
}
// 插入意大利语
const it = await app.sdb.selectOne("language_list", { code: "it" });
if (!it) {
app.sdb.insert("language_list", {
code: "it",
zhName: "意大利语",
enName: "Italian",
timestamp: getTimeStr(),
baidu: "it", // 百度
youDao: "it", // 有道
huoShan: "it", // 火山
xiaoNiu: "it", // 小牛
tengXun:"it",// 腾讯
google: "it", // 谷歌
});
}
// 插入荷兰语
const nl = await app.sdb.selectOne("language_list", { code: "nl" });
if (!nl) {
app.sdb.insert("language_list", {
code: "nl",
zhName: "荷兰语",
enName: "Dutch",
timestamp: getTimeStr(),
baidu: "nl", // 百度
youDao: "nl", // 有道
huoShan: "nl", // 火山
xiaoNiu: "nl", // 小牛
google: "nl", // 谷歌
});
}
// 插入土耳其语
const tr = await app.sdb.selectOne("language_list", { code: "tr" });
if (!tr) {
app.sdb.insert("language_list", {
code: "tr",
zhName: "土耳其语",
enName: "Turkish",
timestamp: getTimeStr(),
baidu: "tr", // 百度
youDao: "tr", // 有道
huoShan: "tr", // 火山
xiaoNiu: "tr", // 小牛
tengXun: "tr", // 腾讯
google: "tr", // 谷歌
});
}
// 插入越南语
const vi = await app.sdb.selectOne("language_list", { code: "vi" });
if (!vi) {
app.sdb.insert("language_list", {
code: "vi",
zhName: "越南语",
enName: "Vietnamese",
timestamp: getTimeStr(),
baidu: "vie", // 百度使用越南语缩写
youDao: "vi", // 有道
huoShan: "vi", // 火山
xiaoNiu: "vi", // 小牛
tengXun: "vi", // 腾讯
google: "vi", // 谷歌
});
}
// 插入泰语
const th = await app.sdb.selectOne("language_list", { code: "th" });
if (!th) {
app.sdb.insert("language_list", {
code: "th",
zhName: "泰语",
enName: "Thai",
timestamp: getTimeStr(),
baidu: "th", // 百度
youDao: "th", // 有道
huoShan: "th", // 火山
xiaoNiu: "th", // 小牛
tengXun: "th", // 腾讯
google: "th", // 谷歌
});
}
// 插入波兰语
const pl = await app.sdb.selectOne("language_list", { code: "pl" });
if (!pl) {
app.sdb.insert("language_list", {
code: "pl",
zhName: "波兰语",
enName: "Polish",
timestamp: getTimeStr(),
baidu: "pl", // 百度
youDao: "pl", // 有道
huoShan: "pl", // 火山
xiaoNiu: "pl", // 小牛
google: "pl", // 谷歌
});
}
// 插入印尼语
const id = await app.sdb.selectOne("language_list", { code: "id" });
if (!id) {
app.sdb.insert("language_list", {
code: "id",
zhName: "印尼语",
enName: "Indonesian",
timestamp: getTimeStr(),
baidu: "id", // 百度
youDao: "id", // 有道
huoShan: "id", // 火山
xiaoNiu: "id", // 小牛
google: "id", // 谷歌
});
}
// 插入乌克兰语
const uk = await app.sdb.selectOne("language_list", { code: "uk" });
if (!uk) {
app.sdb.insert("language_list", {
code: "uk",
zhName: "乌克兰语",
enName: "Ukrainian",
timestamp: getTimeStr(),
baidu: "ukr", // 百度使用乌克兰语缩写
youDao: "uk", // 有道
huoShan: "uk", // 火山
xiaoNiu: "uk", // 小牛
google: "uk", // 谷歌
});
}
// 插入希腊语
const el = await app.sdb.selectOne("language_list", { code: "el" });
if (!el) {
app.sdb.insert("language_list", {
code: "el",
zhName: "希腊语",
enName: "Greek",
timestamp: getTimeStr(),
baidu: "el", // 百度
youDao: "el", // 有道
huoShan: "el", // 火山
xiaoNiu: "el", // 小牛
google: "el", // 谷歌
});
}
// 插入捷克语
const cs = await app.sdb.selectOne("language_list", { code: "cs" });
if (!cs) {
app.sdb.insert("language_list", {
code: "cs",
zhName: "捷克语",
enName: "Czech",
timestamp: getTimeStr(),
baidu: "cs", // 百度
youDao: "cs", // 有道
huoShan: "cs", // 火山
xiaoNiu: "cs", // 小牛
google: "cs", // 谷歌
});
}
// 插入瑞典语
const sv = await app.sdb.selectOne("language_list", { code: "sv" });
if (!sv) {
app.sdb.insert("language_list", {
code: "sv",
zhName: "瑞典语",
enName: "Swedish",
timestamp: getTimeStr(),
baidu: "swe", // 百度使用瑞典语缩写
youDao: "sv", // 有道
huoShan: "sv", // 火山
xiaoNiu: "sv", // 小牛
google: "sv", // 谷歌
});
}
// 插入罗马尼亚语
const ro = await app.sdb.selectOne("language_list", { code: "ro" });
if (!ro) {
app.sdb.insert("language_list", {
code: "ro",
zhName: "罗马尼亚语",
enName: "Romanian",
timestamp: getTimeStr(),
baidu: "ro", // 百度
youDao: "ro", // 有道
huoShan: "ro", // 火山
xiaoNiu: "ro", // 小牛
google: "ro", // 谷歌
});
}
// 插入匈牙利语
const hu = await app.sdb.selectOne("language_list", { code: "hu" });
if (!hu) {
app.sdb.insert("language_list", {
code: "hu",
zhName: "匈牙利语",
enName: "Hungarian",
timestamp: getTimeStr(),
baidu: "hu", // 百度
youDao: "hu", // 有道
huoShan: "hu", // 火山
xiaoNiu: "hu", // 小牛
google: "hu", // 谷歌
});
}
// 插入丹麦语
const da = await app.sdb.selectOne("language_list", { code: "da" });
if (!da) {
app.sdb.insert("language_list", {
code: "da",
zhName: "丹麦语",
enName: "Danish",
timestamp: getTimeStr(),
baidu: "dan", // 百度使用丹麦语缩写
youDao: "da", // 有道
huoShan: "da", // 火山
xiaoNiu: "da", // 小牛
google: "da", // 谷歌
});
}
// 插入芬兰语
const fi = await app.sdb.selectOne("language_list", { code: "fi" });
if (!fi) {
app.sdb.insert("language_list", {
code: "fi",
zhName: "芬兰语",
enName: "Finnish",
timestamp: getTimeStr(),
baidu: "fin", // 百度使用芬兰语缩写
youDao: "fi", // 有道
huoShan: "fi", // 火山
xiaoNiu: "fi", // 小牛
google: "fi", // 谷歌
});
}
// 插入挪威语
const no = await app.sdb.selectOne("language_list", { code: "no" });
if (!no) {
app.sdb.insert("language_list", {
code: "no",
zhName: "挪威语",
enName: "Norwegian",
timestamp: getTimeStr(),
baidu: "nor", // 百度使用挪威语缩写
youDao: "no", // 有道
huoShan: "no", // 火山
xiaoNiu: "no", // 小牛
google: "no", // 谷歌
});
}
// 插入希伯来语
const iw = await app.sdb.selectOne("language_list", { code: "iw" });
if (!iw) {
app.sdb.insert("language_list", {
code: "iw",
zhName: "希伯来语",
enName: "Hebrew",
timestamp: getTimeStr(),
baidu: "heb", // 百度使用希伯来语缩写
youDao: "he", // 有道
huoShan: "he", // 火山
xiaoNiu: "he", // 小牛
google: "iw", // 谷歌使用旧代码iw需注意兼容性
});
}
// 插入高棉语
const km = await app.sdb.selectOne("language_list", { code: "km" });
if (!km) {
app.sdb.insert("language_list", {
code: "km",
zhName: "高棉语",
enName: "Khmer",
timestamp: getTimeStr(),
baidu: "hkm", // 百度使用希伯来语缩写
youDao: "km", // 有道
huoShan: "km", // 火山
xiaoNiu: "km", // 小牛
google: "km", // 谷歌
});
}
};
class Lifecycle {
/**
* core app have been loaded
*/
async ready() {
logger.info("[lifecycle] ready");
}
/**
* electron app ready
*/
async electronAppReady() {
logger.info("[lifecycle] electron-app-ready");
await initializeDatabase();
await initializePlatform();
await initializeTableData();
app.viewsMap = new Map();
app.baseUrl = baseUrl;
console.log("app.baseUrl======: ", app.baseUrl);
logger.info("init dataBase success");
// 🔥 添加自动更新逻辑
initUpdater();
}
ready;
/**
* main window have been loaded
*/
async windowReady() {
logger.info("[lifecycle] window-ready");
// 延迟加载,无白屏
const { windowsOption } = getConfig();
if (windowsOption.show == false) {
const win = getMainWindow();
win.once("ready-to-show", () => {
win.show();
win.focus();
});
// 主进程中全局拦截
app.on("web-contents-created", (_, webContents) => {
webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url).catch((err) => {});
return { action: "deny" }; // 关键阻止Electron创建窗口
});
});
}
}
/**
* before app close
*/
async beforeClose() {
logger.info("[lifecycle] before-close");
}
}
Lifecycle.toString = () => "[class Lifecycle]";
module.exports = {
Lifecycle,
};

View File

@ -0,0 +1,5 @@
const getCurrentUserId = ()=>{}
const inputMsg = (text)=>{}
const quickReply = async (args)=>{
const {type,text} = args;
}

View File

@ -0,0 +1,705 @@
const ipc = window.electronAPI;
let userInfo = {};
let trcConfig = {}
//========================用户基本信息获取开始============
const getCurrentUserId = (item) => {
if (!item){
// 获取当前页面 URL 中 # 号后的部分
const hashValue = window.location.hash;
// 去除开头的 '#'
const id = hashValue.slice(1);
return id;
}
//获取当前会话的userId
const peerElement = item ? item.querySelector('[data-peer-id]') : null;
// 获取 data-peer-id 的值
const peerId = peerElement ? peerElement.getAttribute('data-peer-id') : null;
return peerId;
}
const onlineStatusCheck = ()=> {
setInterval(async () => {
const element = localStorage.dc1_auth_key;
const main = document.getElementById('Main');
const {avatarUrl,nickName,phoneNumber,userName} = await getUserInfo();
if (element || main) {
const msgCount = await getNewMsgCount()
const args = {
platform: 'Telegram',
onlineStatus: true,
avatarUrl: avatarUrl,
nickName:nickName,
phoneNumber:phoneNumber,
userName:userName,
msgCount:msgCount
}
ipc.loginNotify(args);
userInfo = args;
} else {
const args = {
platform: 'Telegram',
onlineStatus: false,
avatarUrl: avatarUrl,
nickName:nickName,
phoneNumber:phoneNumber,
userName:userName,
msgCount:0
}
ipc.loginNotify(args);
userInfo = args;
}
}, 3000); // 每隔5000毫秒3秒调用一次
}
const getUserInfo = () => {
return new Promise((resolve, reject) => {
// 统一获取用户ID的逻辑避免重复
const getUserId = () => {
try {
const userAuthString = localStorage.getItem("user_auth");
if (!userAuthString) return null;
return JSON.parse(userAuthString).id;
} catch (error) {
console.error("解析用户ID失败:", error);
return null;
}
};
const request = indexedDB.open("tt-data");
request.onsuccess = function(event) {
const db = event.target.result;
const transaction = db.transaction("store", "readonly");
const objectStore = transaction.objectStore("store");
const getRequest = objectStore.get("tt-global-state");
// 初始化返回对象
const userInfo = {
phoneNumber: '',
nickName: '',
userName: '',
avatarUrl: ''
};
getRequest.onsuccess = function(event) {
const data = event.target.result;
if (!data?.users) {
resolve(userInfo); // 返回空数据
return;
}
const userId = getUserId();
if (!userId) {
resolve(userInfo);
return;
}
// 处理基础用户信息
const user = data.users.byId?.[userId];
if (user) {
userInfo.phoneNumber = user.phoneNumber || '';
const firstName = user.firstName || '';
const lastName = user.lastName || '';
userInfo.nickName = `${firstName} ${lastName}`.trim();
if (user.usernames?.length > 0) {
userInfo.userName = `@${user.usernames[0].username}`;
}
}
// 处理头像信息
const profilePhoto = data.users.fullInfoById?.[userId]?.profilePhoto;
if (profilePhoto?.thumbnail?.dataUri) {
userInfo.avatarUrl = profilePhoto.thumbnail.dataUri;
}
resolve(userInfo); // 最终返回数据
};
getRequest.onerror = () => reject(new Error('读取数据库失败'));
};
request.onerror = () => reject(new Error('打开数据库失败'));
});
};
const getNewMsgCount = () => {
try {
let newCount = 0;
// 查询所有未读且未静音的聊天标识
const chatNodes = document.querySelectorAll("div.ChatBadge.unread:not(.muted)");
chatNodes.forEach(node => {
const span = node.querySelector("span");
if (span) {
const rawValue = span.textContent.trim();
const value = parseInt(rawValue, 10);
// 累加有效数值
if (!isNaN(value) && value >= 0) {
newCount += value;
} else {
console.debug(`忽略无效值:${rawValue}`); // 调试日志代替警告
}
}
});
return newCount; // 直接返回统计结果
} catch (e) {
console.error('消息统计异常:', e.message);
return 0; // 异常时返回默认值
}
};
onlineStatusCheck();
//========================用户基本信息获取结束============
//中文检测
const containsChinese = (text)=> {
const regex = /[\u4e00-\u9fa5]/; // 匹配中文字符的正则表达式
return regex.test(text); // 如果包含中文字符返回 true否则返回 false
}
const sendMsg = ()=> {
let sendButton = document.querySelectorAll('button.Button.send.main-button.default.secondary.round.click-allowed')[0]
if (sendButton) {
sendButton.click();
}else {
console.log('发送按钮不存在!')
}
}
const sendTranslate = async (text,to)=>{
if (text && text.trim()) {
const route = trcConfig.translateRoute;
styledTextarea.setContent('翻译中...')
styledTextarea.setIsProcessing(true);
const mode = trcConfig.mode;
const res = await ipc.translateText({route: route, text: text, to: to,isFilter:'true',mode:mode});
if (res.status) {
//调用ipc获取翻译结果
styledTextarea.setContent(res.data);
styledTextarea.setTranslateStatus(true)
styledTextarea.setIsProcessing(false);
}else {
styledTextarea.setContent(res.message);
styledTextarea.setTranslateStatus(false)
styledTextarea.setIsProcessing(false);
}
}
}
const createStyledTextarea = () => {
const container = document.createElement('div');
Object.assign(container.style, {
display: 'flex',
flexDirection: 'column',
});
container.id = 'custom-translate-textarea';
// 标题元素
const label = document.createElement('span');
Object.assign(label.style, {
fontSize: '12px',
color: 'green',
marginLeft: '20px',
marginBottom: '10px',
marginTop: '10px',
});
label.textContent = ''
// 文本域容器
const textareaWrapper = document.createElement('div');
textareaWrapper.style.overflowY = 'auto';
textareaWrapper.style.maxHeight = '100px';
const textarea = document.createElement('textarea');
Object.assign(textarea.style, {
fontSize: '12px',
paddingLeft: '20px',
paddingRight: '20px',
width: '100%',
border: 'none',
resize: 'none',
overflow: 'hidden',
outline: 'none',
backgroundColor: 'var(--color-background)',
pointerEvents: 'none', // 禁用交互
opacity: '0.7' // 添加这行来设置透明度
});
textarea.tabIndex = -1; // 禁用键盘聚焦
textarea.value = '...';
// 自动高度调整
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = this.scrollHeight + 'px';
});
// 组装元素
textareaWrapper.appendChild(textarea);
container.appendChild(label);
container.appendChild(textareaWrapper);
//翻译状态
let translateStatus = false;
let isProcessing = false;
//初始化属性数据
const initData = () =>{
translateStatus = false;
isProcessing = false;
textarea.value = '...';
// 手动触发高度调整
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
}
// 暴露设置方法
const setContent = (content) => {
if (content) {
textarea.value = content;
// 手动触发高度调整
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
}
};
const getContent = () => {
return textarea.value;
};
const setTitle = (title) => {
if (title) label.textContent = title;
};
const setTranslateStatus = (status) => {
translateStatus = status;
};
const getTranslateStatus = () => {
return translateStatus; // 直接返回状态值
};
const setIsProcessing = (status) => {
isProcessing = status;
};
const getIsProcessing = () => {
return isProcessing; // 直接返回状态值
};
return {
initData,
container, // DOM节点
setTitle, //设置翻译线路
setContent, // 设置内容的方法
setTranslateStatus,//设置翻译状态
getTranslateStatus,
getIsProcessing,
setIsProcessing,
getContent
};
};
const styledTextarea = createStyledTextarea()
const addTranslatePreview = ()=>{
// 更安全的选择器写法
const msgDiv = document.querySelector('div.message-input-wrapper')
if (msgDiv) {
// 创建父容器
const container = document.createElement('div');
// 将原元素和新元素包裹进容器
msgDiv.parentNode.insertBefore(container, msgDiv);
container.appendChild(styledTextarea.container);
container.appendChild(msgDiv);
}
}
const addTranslateListener = () => {
// 获取元素
const editableDiv = document.getElementById('editable-message-text');
// 防抖函数
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId); // 清除之前的定时器
timeoutId = setTimeout(() => func.apply(this, args), delay); // 设置新的定时器
};
};
// 处理输入事件的逻辑
const handleInput = async () => {
const flag = trcConfig.translatePreview;
const textContent = editableDiv.textContent.trim();
const sendStatus = trcConfig.sendTranslateStatus;
if(!textContent) {
styledTextarea.setContent('...')
}
if (flag && flag === 'true' && sendStatus === 'true') {
const to = trcConfig.sendTargetLanguage;
styledTextarea.setTranslateStatus(false)
await sendTranslate(textContent,to);
}
};
// 监听输入事件,使用防抖
editableDiv.addEventListener('input', debounce(handleInput, 300)); // 延时 300ms
let isProcessing = false;
editableDiv.addEventListener('keydown', async (event) => {
if (event.key === 'Enter' && !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (isProcessing) return;
const sendTranslateStatus = trcConfig.sendTranslateStatus;
const sendPreview = trcConfig.translatePreview;
const textContent = editableDiv.textContent.trim();
const from = trcConfig.sendSourceLanguage;
const to = trcConfig.sendTargetLanguage;
const route = trcConfig.translateRoute;
const mode = trcConfig.mode;
const args = {text:textContent,from:from,to: to,route:route,mode:mode};
if (sendPreview === 'true'&& sendTranslateStatus === 'true') {
const status = styledTextarea.getTranslateStatus()
const isProcess = styledTextarea.getIsProcessing()
if (status === true && isProcess === false) {
const translateText = styledTextarea.getContent();
await inputMsg(translateText)
sendMsg()
styledTextarea.setTranslateStatus(false)
styledTextarea.setContent('...')
return;
}else {
console.log('正在处理翻译中:')
return;
}
}else if (textContent && sendTranslateStatus === 'true') {
isProcessing = true;
styledTextarea.setContent('翻译中...')
const res = await ipc.translateText(args)
if (res.status) {
const translateText = res.data;
await inputMsg(translateText)
styledTextarea.setContent(translateText);
setTimeout(()=>{
isProcessing = false;
sendMsg()
styledTextarea.setContent('...');
},500)
}else {
styledTextarea.setContent(res.message);
isProcessing = false;
return;
}
}
const chineseDetectionStatus = trcConfig.chineseDetectionStatus;
if (chineseDetectionStatus === 'true' && sendTranslateStatus === 'true') {
const textContent = editableDiv.textContent.trim();
if (containsChinese(textContent)) {
return;
}
}
sendMsg();
}
},true);
// 监听父容器的事件
document.body.addEventListener('click', function(event) {
// 通过特征匹配目标元素
const target = event.target.closest('button[aria-label="Send Message"]');
if (target && target.classList.contains('send')) {
const status = styledTextarea.getTranslateStatus()
const isProcess = styledTextarea.getIsProcessing()
const sendTranslateStatus = trcConfig.sendTranslateStatus;
console.log('当前状态:',{
styStatus:status,
styIsProcess:isProcess,
sendTranslateStatus:sendTranslateStatus,
})
if (isProcess === true) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}else {
if (event.isSimulated === true) {
//不做处理
}else if (sendTranslateStatus === 'true' && status === true) {
const translateText = styledTextarea.getContent();
inputMsg(translateText).then(()=>{})
}
}
}
});
};
const updateConfigInfo = async () => {
const currentUserId = getCurrentUserId();
const args = {platform: 'Telegram', userId: currentUserId || ''};
const res = await ipc.getTranslateConfig(args)
if (res.status) {
styledTextarea.setTitle(res.data.zhName);
trcConfig = res.data;
console.log('获取配置信息args:',args)
}else {
trcConfig = {};
console.error('获取配置信息失败:',res);
}
}
//会话列表切换触发函数
const sessionChange = async () => {
const currentUserId = getCurrentUserId();
const args = {platform: 'Telegram', userId: currentUserId};
ipc.infoUpdate(args)
await updateConfigInfo()
const myNode = document.getElementById('custom-translate-textarea')
if (!myNode) {
addTranslatePreview();
addTranslateListener();
}
styledTextarea.initData()
}
const debouncedSessionChange = debounce(sessionChange,500);
//========================翻译列表处理开始============
const getMsgText = (parentNode) => {
if (!parentNode) {
console.error("parentNode is null or undefined");
return "";
}
// 获取 parentNode 下的第一个包含 'text-content' 类的节点
const textContentNode = parentNode.querySelector('.text-content');
if (!textContentNode) {
console.error("没有找到 '.text-content' 类的节点");
return "";
}
let allText = "";
const textContentNodes = textContentNode.childNodes;
// 遍历所有子节点,获取并合并文本
textContentNodes.forEach((node) => {
if (node.nodeName === "BR") {
allText += "\n"; // 如果是 <br> 标签,添加换行符
} else if (node.nodeType === Node.TEXT_NODE) {
allText += node.textContent; // 获取文本节点的内容
}
});
// 返回合并后的文本字符串
return allText?.trim(); // 去除结尾多余的换行符
}
const monitorMainNode = ()=> {
// 监听整个 body 的 DOM 变化,等待 #main 节点的出现
const observer = new MutationObserver(async (mutationsList, observer) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
//设置授权码
// 检查是否已经存在 id="main" 的节点
const mainNode = document.getElementById('LeftColumn-main');
if (mainNode) {
// 停止对 body 的观察,避免不必要的性能开销
observer.disconnect();
// 开始监听 #main 节点的子节点变化
observePaneSide(mainNode);
break;
}
}
}
});
// 开始观察 body 的子节点变化
observer.observe(document.body, { childList: true, subtree: true });
// 监听会话列表切换
const observePaneSide = (paneSideNode)=> {
const observer = new MutationObserver(async (mutationsList) => {
for (const mutation of mutationsList) {
// 确保是 class 属性变化事件
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const targetNode = mutation.target;
// 检查 class 是否包含 selected
if (targetNode.classList.contains("selected")) {
observeChatChanges();
debouncedSessionChange()
}
}
}
});
// 配置 MutationObserver监听 class 属性变化
const config = { attributes: true, subtree: true, attributeFilter: ["class"] };
// 监听 pane-side 节点
observer.observe(paneSideNode, config);
}
/**
* 监视聊天窗口的变化
*/
const observeChatChanges = () => {
const chatContainer = document.querySelector("#MiddleColumn");
if (chatContainer) {
const observer = new MutationObserver((mutations) => {
observer.disconnect(); // 暂时断开观察器以避免循环触发
debouncedAddTranslateButtonToAllMessages()
observer.observe(chatContainer, {
childList: true,
subtree: true,
});
});
observer.observe(chatContainer, {
childList: true,
subtree: true,
});
}
};
const addTranslateButtonToAllMessages = async () => {
const messageElements = document.querySelectorAll("div[data-message-id].message-list-item");
await updateConfigInfo();
// 从最后一个元素开始循环
for (let i = messageElements.length - 1; i >= 0; i--) {
const item = messageElements[i];
const msgSpan = item.querySelector('div[dir="auto"].text-content.clearfix.with-meta');
if (msgSpan) {
const flag = msgSpan.hasAttribute('data-translated');
if (flag === false) {
msgSpan.setAttribute('data-translated', 'yes');
await createTranslateButtonForMessage(msgSpan);
}
}
}
};
const createTranslateButtonForMessage = async (msgSpan) => {
let text = getMsgText(msgSpan.parentNode);
if (!text) return;
const translateDiv = document.createElement("div");
translateDiv.style.cssText = `
min-height: 20px;
display: flex;`;
const leftDiv = document.createElement("div");
leftDiv.style.cssText = `
min-height: 20px;
font-size: 13px;
float: left;
color: var(--color-text);
filter: opacity(70%);
`;
const rightDiv = document.createElement("div");
rightDiv.style.cssText = `
cursor: pointer;
width: 20px;
height: 20px;
float:left;
user-select: none;
margin-left:5px;
`;
rightDiv.textContent = '🔄'
rightDiv.addEventListener('click', async (e) => {
let text = getMsgText(msgSpan.parentNode);
text = text.replace(/\n/g, '<br>');
leftDiv.style.color = 'var(--color-text)';
leftDiv.textContent = `翻译中...`;
rightDiv.style.display = 'none';
//发送请求获取翻译结果
const route = trcConfig.translateRoute;
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});
if (res.status) {
leftDiv.innerHTML = res.data;
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
rightDiv.style.display = '';
}
});
// 组装结构
translateDiv.appendChild(leftDiv);
translateDiv.appendChild(rightDiv);
msgSpan.style.borderBottom = '1px dashed var(--color-text)';
msgSpan.style.paddingBottom = '5px';
// 插入到消息元素右侧
msgSpan.parentNode.insertBefore(translateDiv, msgSpan.nextSibling);
const receiveTranslateStatus = trcConfig.receiveTranslateStatus;
if (receiveTranslateStatus === 'true') {
let text = getMsgText(msgSpan.parentNode);
text = text.replace(/\n/g, '<br>');
leftDiv.textContent = `翻译中...`;
rightDiv.style.display = 'none';
//发送请求获取翻译结果
const route = trcConfig.translateRoute;
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,mode:mode});
if (res.status) {
leftDiv.innerHTML = res.data;
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
rightDiv.style.display = '';
}
}
};
const debouncedAddTranslateButtonToAllMessages = debounce(addTranslateButtonToAllMessages,200);
}
/**
* 函数防抖,用于优化频繁触发的操作
* @param {Function} func 需要防抖的函数
* @param {number} delay 防抖延迟时间(毫秒)
*/
function debounce (func, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
};
monitorMainNode()
//========================翻译列表处理结束============
//快捷回复相关部分
async function inputMsg(translation) {
// 查找富文本输入框
const richTextInput = document.getElementById('editable-message-text');
// 1. 检测操作系统
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
// 2. 聚焦输入框
richTextInput.focus();
// 3. 模拟 Ctrl+A/Command+A
const selectAll = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
ctrlKey: !isMac,
metaKey: isMac,
bubbles: true
});
richTextInput.dispatchEvent(selectAll);
// 4. 模拟退格键
const backspace = new KeyboardEvent('keydown', {
key: 'Backspace',
code: 'Backspace',
bubbles: true
});
richTextInput.dispatchEvent(backspace);
richTextInput.innerText = translation;
// 7. 触发输入事件
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: translation
});
richTextInput.dispatchEvent(inputEvent);
}
const quickReply = async (args)=>{
const {type,text} = args;
if (type === 'input') {
await inputMsg(text);
}
if (type === 'send') {
await inputMsg(text);
let sendButton = document.querySelectorAll('button.Button.send.main-button.default.secondary.round.click-allowed')[0]
if (sendButton) {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true
});
event.isSimulated = true; // 标记为模拟事件
sendButton.dispatchEvent(event);
}
}
}

611
electron/scripts/TikTok.js Normal file
View File

@ -0,0 +1,611 @@
const ipc = window.electronAPI;
let userInfo = {};
let trcConfig = {}
const getCurrentUserId = () => {
let userId;
// 获取 data-e2e="chat-uniqueid" 的 p 元素
const uniqueIdElement = document.querySelector('[data-e2e="chat-uniqueid"]');
// 检查元素是否存在
if (uniqueIdElement) {
// 获取该 p 元素的文本内容(即 @xxe39821b5j
userId = uniqueIdElement.textContent;
}
return userId;
}
const getNewMsgCount = () => {
let msgCount = 0; // 全局消息计数器
try {
// 获取消息数量的 <sup> 标签
const msgBadge = document.querySelector('[data-e2e="top-dm-icon"] .css-r2juw9-SupMsgBadge');
// 如果找到 <sup> 标签,获取其文本内容,否则设置为 0
let newMsgCount = msgBadge ? parseInt(msgBadge.textContent, 10) : 0;
// 累加消息数量
if (msgCount !== newMsgCount) {
msgCount = newMsgCount; // 更新全局消息计数器
}
} catch (e) {
console.error("获取新消息数量发生错误:", e.message);
}
return msgCount;
};
const getAvatarImg = ()=> {
// 获取 #header-more-menu-icon 节点
const element = document.querySelector("#header-more-menu-icon");
// 获取该元素的背景图片的 URL
const backgroundImageUrl = element.style.backgroundImage;
// 提取 URL 部分
const url = backgroundImageUrl.slice(5, -2); // 去掉 "url(" 和 ")" 字符
return url
}
const onlineStatusCheck = ()=> {
setInterval(async () => {
let userSessionStr = sessionStorage.user_session;
if (userSessionStr) {
// 假设从存储中获取的 user_session 值
const userSession = JSON.parse(userSessionStr);
// 判断 uid 是否为 "0" 来确定是否登录
let onlineStatus = !(userSession && userSession.uid === "0");
const avatarUrl = getAvatarImg();
const msgCount = getNewMsgCount()
const args = {
platform: 'TikTok',
onlineStatus: onlineStatus,
avatarUrl: avatarUrl,
nickName:'',
phoneNumber:userSession.uid,
userName:'',
msgCount:msgCount,
}
ipc.loginNotify(args);
userInfo = args;
} else {
const args = {
platform: 'TikTok',
onlineStatus: false,
avatarUrl: '',
nickName:'',
phoneNumber:'',
userName:'',
msgCount:0
}
ipc.loginNotify(args);
userInfo = args;
}
}, 3000); // 每隔5000毫秒5秒调用一次
}
onlineStatusCheck();
const sendMsg = ()=> {
const element = document.querySelector("svg[data-e2e=message-send]");
if (element){
// 创建一个点击事件
const clickEvent = new MouseEvent('click', {
bubbles: true, // 事件是否冒泡
cancelable: true, // 事件是否可以取消
});
// 触发事件
element.dispatchEvent(clickEvent);
}else {
console.error("发送按钮节点无效")
}
}
const getMsgText = (node) => {
const spans = node.querySelectorAll('span [data-text=true]');
// 用于存储每一行的内容
let content = [];
spans.forEach(span => {
// 获取 span 的文本内容并去除前后空格
const text = span.textContent.trim();
// 如果内容为空,则添加空字符串,否则添加文本内容
content.push(text === '' ? '' : text);
});
// 将内容数组拼接成一个字符串,用换行符分隔
return content.join('\n');
};
const sendTranslate = async (text,to)=>{
if (text && text.trim()) {
const route = trcConfig.translateRoute;
const mode = trcConfig.mode;
styledTextarea.setContent('翻译中...')
styledTextarea.setIsProcessing(true);
// console.log('发送翻译请求:')
const res = await ipc.translateText({route: route, text: text, to: to,isFilter:'true',mode:mode});
if (res.status) {
//调用ipc获取翻译结果
styledTextarea.setContent(res.data);
styledTextarea.setTranslateStatus(true)
styledTextarea.setIsProcessing(false);
}else {
styledTextarea.setContent(res.message);
styledTextarea.setTranslateStatus(false)
styledTextarea.setIsProcessing(false);
}
}
}
const createStyledTextarea = () => {
const container = document.createElement('div');
Object.assign(container.style, {
display: 'flex',
flexDirection: 'column',
});
container.id = 'custom-translate-textarea';
container.classList.add('x1bmpntp');
container.style.boxSizing = 'border-box'
// 标题元素
const label = document.createElement('span');
Object.assign(label.style, {
fontSize: '12px',
color: 'green',
marginLeft: '20px',
});
label.textContent = ''
// 文本域容器
const textareaWrapper = document.createElement('div');
textareaWrapper.style.overflowY = 'auto';
textareaWrapper.style.maxHeight = '100px';
const textarea = document.createElement('textarea');
textarea.style.color = 'var(--ui-text-1)'
Object.assign(textarea.style, {
fontSize: '12px',
opacity: '0.7', // 添加这行来设置透明度
width: '100%',
border: 'none',
resize: 'none',
overflow: 'hidden',
boxSizing: 'border-box',
paddingLeft: '20px',
paddingRight: '40px',
outline: 'none',
overflowY: 'auto',
pointerEvents: 'none', // 禁用交互
});
textarea.tabIndex = -1; // 禁用键盘聚焦
textarea.value = '...';
// 自动高度调整
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = this.scrollHeight + 'px';
});
// 组装元素
textareaWrapper.appendChild(textarea);
container.appendChild(label);
container.appendChild(textareaWrapper);
//翻译状态
let translateStatus = false;
let isProcessing = false;
//初始化属性数据
const initData = () =>{
translateStatus = false;
isProcessing = false;
textarea.value = '...';
// 手动触发高度调整
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
}
// 暴露设置方法
const setContent = (content) => {
if (content) {
textarea.value = content;
// 手动触发高度调整
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
}
};
const getContent = () => {
return textarea.value;
};
const setTitle = (title) => {
if (title) label.textContent = title;
};
const setTranslateStatus = (status) => {
translateStatus = status;
};
const getTranslateStatus = () => {
return translateStatus; // 直接返回状态值
};
const setIsProcessing = (status) => {
isProcessing = status;
};
const getIsProcessing = () => {
return isProcessing; // 直接返回状态值
};
return {
initData,
container, // DOM节点
setTitle, //设置翻译线路
setContent, // 设置内容的方法
setTranslateStatus,//设置翻译状态
getTranslateStatus,
getIsProcessing,
setIsProcessing,
getContent
};
};
const styledTextarea = createStyledTextarea()
const addTranslatePreview = ()=>{
const footerDiv = document.querySelector("div [data-e2e=message-input-area]")
const msgDiv = footerDiv.parentNode
if (msgDiv) {
// 创建父容器
const container = document.createElement('div');
msgDiv.parentNode.insertBefore(container, msgDiv);
container.appendChild(styledTextarea.container);
container.appendChild(msgDiv);
}
}
const addTranslateListener = () => {
// 获取元素
const editableDiv = document.querySelector('div[data-e2e="message-input-area"]');
// 1⃣ 移除旧的监听器(如果存在)
if (editableDiv._previousKeydownHandler) {
editableDiv.removeEventListener('keydown', editableDiv._previousKeydownHandler);
}
// 防抖函数
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId); // 清除之前的定时器
timeoutId = setTimeout(() => func.apply(this, args), delay); // 设置新的定时器
};
};
// 处理输入事件的逻辑
const handleInput = async () => {
const flag = trcConfig.translatePreview;
const textContent = getMsgText(editableDiv)
const sendStatus = trcConfig.sendTranslateStatus;
if(textContent === '' || !textContent) {
styledTextarea.setContent('...')
return;
}
const isProcessing = styledTextarea.getIsProcessing();
if (flag && flag === 'true' && sendStatus === 'true' && isProcessing === false) {
const to = trcConfig.sendTargetLanguage;
styledTextarea.setTranslateStatus(false)
await sendTranslate(textContent,to);
}
};
// 键盘回车事件函数
const handleKeyDown = async (event) => {
debouncedHandleInput()
if (event.key === 'Enter') {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (isProcessing) return;
const sendTranslateStatus = trcConfig.sendTranslateStatus;
const sendPreview = trcConfig.translatePreview;
const textContent = getMsgText(editableDiv);
const from = trcConfig.sendSourceLanguage;
const to = trcConfig.sendTargetLanguage;
const route = trcConfig.translateRoute;
const mode = trcConfig.mode;
const args = {text:textContent,from:from,to: to,route:route,mode:mode};
if (sendPreview === 'true'&& sendTranslateStatus === 'true') {
const status = styledTextarea.getTranslateStatus()
const isProcess = styledTextarea.getIsProcessing()
if (status === true && isProcess === false) {
const translateText = styledTextarea.getContent();
// 获取所有需要翻译的文本元素
let msgElements = document.querySelectorAll('span[data-text=true]');
//判断发送的内容是否全是表情
if (msgElements.length<=0) {
sendMsg()
return;
}
await inputMsg(translateText)
// 延迟确保状态更新
setTimeout(() => {
sendMsg();
styledTextarea.setTranslateStatus(false);
styledTextarea.setContent('...');
}, 100);
}
}else if (textContent && textContent !=='' && sendTranslateStatus === 'true') {
isProcessing = true;
styledTextarea.setContent('翻译中...')
const res = await ipc.translateText(args)
if (res.status) {
const translateText = res.data;
await inputMsg(translateText)
setTimeout(()=>{
sendMsg();
},500)
isProcessing = false;
styledTextarea.setTranslateStatus(false)
styledTextarea.setContent('...')
}else {
styledTextarea.setContent(res.message);
isProcessing = false;
}
}else {
sendMsg();
}
}
}
// 创建防抖后的 handleInput 函数
const debouncedHandleInput = debounce(handleInput, 500);
let isProcessing = false;
// 3⃣ 保存当前处理函数的引用
editableDiv._previousKeydownHandler = handleKeyDown;
editableDiv.addEventListener('keydown', handleKeyDown);
};
const updateConfigInfo = async () => {
const currentUserId = getCurrentUserId();
const args = {platform: 'TikTok', userId: currentUserId || ''};
const res = await ipc.getTranslateConfig(args)
if (res.status) {
styledTextarea.setTitle(res.data.zhName);
trcConfig = res.data;
}else {
trcConfig = {};
console.error('获取配置信息失败:',res);
}
}
//会话列表切换触发函数
const sessionChange = async () => {
const currentUserId = getCurrentUserId();
const args = {platform: 'TikTok', userId: currentUserId};
ipc.infoUpdate(args)
await updateConfigInfo()
const myNode = document.getElementById('custom-translate-textarea')
if (!myNode) {
addTranslatePreview();
addTranslateListener();
}
styledTextarea.initData()
}
const debouncedSessionChange = debounce(sessionChange,200);
const monitorMainNode = ()=> {
// 监听整个 body 的 DOM 变化,等待 #main 节点的出现
const observer = new MutationObserver(async (mutationsList, observer) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
//设置授权码
// 检查是否已经存在 id="main" 的节点
const mainNode = document.getElementById('main-content-messages');
if (mainNode) {
// 停止对 body 的观察,避免不必要的性能开销
observer.disconnect();
// 开始监听 #main 节点的子节点变化
let leftNode = document.querySelector("div.css-149gota-DivScrollWrapper")
if (leftNode) {
observePaneSide(leftNode);
}else {
console.error('找不到左侧会话节点:')
}
break;
}
}
}
});
observer.observe(document.body, {childList: true, subtree: true});
// 监听会话列表切换
const observePaneSide = (paneSideNode)=> {
const observer = new MutationObserver(async (mutationsList) => {
for (const mutation of mutationsList) {
// 确保是 class 属性变化事件
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const targetNode = mutation.target;
// 检查 class 是否包含 selected
if (targetNode.classList.contains("css-19mzsj3-DivItemWrapper") || targetNode.classList.contains("css-u2mook-DivItemWrapper")) {
observeChatChanges();
debouncedSessionChange();
}
}
}
});
// 配置 MutationObserver监听 class 属性变化
const config = {attributes: true, subtree: true, attributeFilter: ["class"]};
// 监听 pane-side 节点
observer.observe(paneSideNode, config);
}
const observeChatChanges = () => {
// 获取具有 data-e2e="chat-item" 属性的元素
let chatItem = document.querySelector('[data-e2e="chat-item"]');
// 获取父节点
let chatContainer = chatItem.parentNode;
if (chatContainer) {
const observer = new MutationObserver((mutations) => {
observer.disconnect(); // 暂时断开观察器以避免循环触发
debouncedAddTranslateButtonToAllMessages()
observer.observe(chatContainer, {
childList: true,
subtree: true,
});
});
observer.observe(chatContainer, {
childList: true,
subtree: true,
});
}
};
const addTranslateButtonToAllMessages = async () => {
const messageElements = document.querySelectorAll('[data-e2e="chat-item"]');
for (let i = messageElements.length - 1; i >= 0; i--) {
const msgSpan = messageElements[i].querySelector('p');
const img = messageElements[i].querySelector('img')
if (msgSpan && img) {
const flag = msgSpan.hasAttribute('data-translated');
if (flag === false) {
//添加属性表示已经处理过翻译
msgSpan.setAttribute('data-translated', 'yes');
await createTranslateButtonForMessage(msgSpan);
}
}
}
};
//为每条消息创建翻译按钮
const createTranslateButtonForMessage = async ( msgSpan) => {
let text = msgSpan?.childNodes[0].textContent;
if (!text) return;
const translateDiv = document.createElement("div");
translateDiv.style.cssText = `
min-height: 20px;
justify-content: space-between;
margin-top: 5px;
border-top:1px dashed gray;
color:var(--secondary);
display: flex;`;
const leftDiv = document.createElement("div");
leftDiv.style.cssText = `
min-height: 20px;
font-size:13px;
filter: opacity(70%);
color: green`;
const rightDiv = document.createElement("div");
rightDiv.style.cssText = `
cursor: pointer;
width: 20px;
height: 20px;
user-select: none;
margin-left:5px;
`;
rightDiv.textContent = '🔄'
rightDiv.addEventListener('click', async (e) => {
let text = msgSpan?.childNodes[0].textContent;
text = text.replace(/\n/g, '<br>');
leftDiv.style.color = 'green';
leftDiv.textContent = `翻译中...`;
rightDiv.style.display = 'none';
//发送请求获取翻译结果
const route = trcConfig.translateRoute;
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});
if (res.status) {
leftDiv.innerHTML = res.data;
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
rightDiv.style.display = '';
}
});
// 组装结构
translateDiv.appendChild(leftDiv);
translateDiv.appendChild(rightDiv);
// 插入到消息元素右侧
msgSpan.appendChild(translateDiv);
const receiveTranslateStatus = trcConfig.receiveTranslateStatus;
if (receiveTranslateStatus === 'true') {
let text = msgSpan?.childNodes[0].textContent;
text = text.replace(/\n/g, '<br>');
leftDiv.style.color = 'green';
leftDiv.textContent = `翻译中...`;
rightDiv.style.display = 'none';
//发送请求获取翻译结果
const route = trcConfig.translateRoute;
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,mode:mode});
if (res.status) {
leftDiv.innerHTML = res.data;
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
rightDiv.style.display = '';
}
}
};
const debouncedAddTranslateButtonToAllMessages = debounce(addTranslateButtonToAllMessages,200);
}
function debounce (func, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
};
monitorMainNode()
//快捷回复相关部分
function inputMsg(translation) {
const editableDiv = document.querySelector("div.public-DraftStyleDefault-block")
// const editableDiv = document.querySelector('div[contenteditable=true]');
// 1. 检测操作系统
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
// 2. 聚焦输入框
editableDiv.focus();
// 3. 模拟 Ctrl+A/Command+A
const selectAll = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
ctrlKey: !isMac,
metaKey: isMac,
bubbles: true
});
editableDiv.dispatchEvent(selectAll);
// 4. 模拟退格键
const backspace = new KeyboardEvent('keydown', {
key: 'Backspace',
code: 'Backspace',
bubbles: true
});
editableDiv.dispatchEvent(backspace);
// 查找富文本输入框
let richTextInput = editableDiv.querySelector("span[data-text=true]")
let cloneSpan = editableDiv.querySelector('span #myCloneSpan')
if (!richTextInput && !cloneSpan) {
const parentSpan = editableDiv.querySelector('span');
cloneSpan = parentSpan.cloneNode(true); // 包含所有子节点
// 分开属性名和值作为两个参数
cloneSpan.setAttribute('data-text', 'true');
cloneSpan.id='myCloneSpan'
parentSpan.appendChild(cloneSpan)
richTextInput = cloneSpan;
}
richTextInput.innerText = translation;
// 7. 触发输入事件
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: translation
});
richTextInput.dispatchEvent(inputEvent);
if (cloneSpan) cloneSpan.remove()
}
const quickReply = async (args)=>{
await updateConfigInfo();
const {type,text} = args;
if (type === 'input') {
await inputMsg(text);
}
if (type === 'send') {
await inputMsg(text);
let sendButton = document.querySelector("svg[data-e2e=message-send]");
if (sendButton) {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true
});
event.isSimulated = true; // 标记为模拟事件
sendButton.dispatchEvent(event);
const cloneSpan = document.querySelector('span #myCloneSpan')
if (cloneSpan) cloneSpan.remove();
}
}
}

View File

@ -0,0 +1,630 @@
const ipc = window.electronAPI;
let userInfo = {};
let trcConfig = {}
//========================用户基本信息获取开始============
const getCurrentUserId = (item) => {
const element = document.querySelector('div#main');
let userNum = null;
// 获取 React 的属性对象
let reactProps = null;
for (let key in element) {
if (key.startsWith('__reactProps$')) {
reactProps = element[key];
break;
}
}
if (reactProps && reactProps.children && reactProps.children.key) {
userNum = reactProps.children.key;
userNum = userNum.replace(/@.*/, '');
}
return userNum;
}
const onlineStatusCheck = ()=> {
setInterval(async () => {
const element = localStorage["last-wid-md"]
if (element) {
const phoneNumber = element.split(":")[0].replace(/"/g, "");
const profilePicUrl = localStorage.WACachedProfilePicEURL;
const avatarUrl = profilePicUrl.replace(/^"|"$/g, '');
const msgCount = await getNewMsgCount()
const args = {
platform: 'WhatsApp',
onlineStatus: true,
avatarUrl: avatarUrl,
nickName:'',
phoneNumber:phoneNumber,
userName:phoneNumber,
msgCount:msgCount
}
ipc.loginNotify(args);
userInfo = args;
}else {
const args = {
platform: 'WhatsApp',
onlineStatus: false,
avatarUrl: '',
nickName:'',
phoneNumber:'',
userName:'',
msgCount:0
}
ipc.loginNotify(args);
userInfo = args;
}
}, 3000); // 每隔5000毫秒3秒调用一次
}
const getNewMsgCount = () => {
try {
let newMsgCount = 0; // 累加消息数量
// 获取所有符合条件的 `div[role='listitem']` 节点
const chatNodes = document.querySelectorAll("div[role='listitem']");
// 遍历所有节点并提取 `span` 的值
chatNodes.forEach((node) => {
// 定义目标 `span` 节点的类选择器
const targetClassSelector = `
.x1rg5ohu.x173ssrc.x1xaadd7.x682dto.x1e01kqd.x12j7j87
.x9bpaai.x1pg5gke.x1s688f.xo5v014.x1u28eo4.x2b8uid
.x16dsc37.x18ba5f9.x1sbl2l.xy9co9w.x5r174s.x7h3shv
`.replace(/\s+/g, ''); // 移除空格确保选择器格式正确
const span = node.querySelector(`span${targetClassSelector}[aria-label]`); // 查询特定类的 `span`
const svg = node.querySelector('span[data-icon="muted"]');
if (svg) return;
if (span) {
const textContent = span.textContent.trim(); // 获取文本内容并去掉空格
const value = parseInt(textContent, 10); // 转换为整数
if (!isNaN(value)) {
newMsgCount += value; // 累加有效值
}
}
});
// 直接返回消息数量
return newMsgCount;
} catch (e) {
console.error("获取新消息数量发生错误:", e.message);
return 0; // 发生错误时返回 0
}
};
onlineStatusCheck();
//中文检测
const containsChinese = (text)=> {
const regex = /[\u4e00-\u9fa5]/; // 匹配中文字符的正则表达式
return regex.test(text); // 如果包含中文字符返回 true否则返回 false
}
const sendMsg = () => {
let sendButton = getSendBtn();
if (sendButton) {
sendButton.click();
}
}
const sendTranslate = async (text,to)=>{
if (text && text.trim()) {
const route = trcConfig.translateRoute;
const mode = trcConfig.mode;
styledTextarea.setContent('翻译中...')
styledTextarea.setIsProcessing(true);
// console.log('发送翻译请求:')
const res = await ipc.translateText({route: route, text: text, to: to,isFilter:'true',mode:mode});
if (res.status) {
//调用ipc获取翻译结果
styledTextarea.setContent(res.data);
styledTextarea.setTranslateStatus(true)
styledTextarea.setIsProcessing(false);
}else {
styledTextarea.setContent(res.message);
styledTextarea.setTranslateStatus(false)
styledTextarea.setIsProcessing(false);
}
}
}
const createStyledTextarea = () => {
const container = document.createElement('div');
Object.assign(container.style, {
display: 'flex',
flexDirection: 'column',
});
container.id = 'custom-translate-textarea';
container.classList.add('x1bmpntp');
container.style.boxSizing = 'border-box'
// 标题元素
const label = document.createElement('span');
Object.assign(label.style, {
fontSize: '12px',
color: 'green',
marginLeft: '70px',
marginTop: '10px',
marginBottom: '10px',
});
label.textContent = ''
// 文本域容器
const textareaWrapper = document.createElement('div');
textareaWrapper.style.overflowY = 'auto';
textareaWrapper.style.maxHeight = '100px';
const textarea = document.createElement('textarea');
textarea.classList.add("x1bmpntp")
textarea.classList.add("_amk4")
Object.assign(textarea.style, {
fontSize: '12px',
opacity: '0.7', // 添加这行来设置透明度
width: '100%',
border: 'none',
resize: 'none',
overflow: 'hidden',
boxSizing: 'border-box',
paddingLeft: '70px',
paddingRight: '60px',
outline: 'none',
overflowY: 'auto',
pointerEvents: 'none', // 禁用交互
});
textarea.tabIndex = -1; // 禁用键盘聚焦
textarea.value = '...';
// 自动高度调整
textarea.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = this.scrollHeight + 'px';
});
// 组装元素
textareaWrapper.appendChild(textarea);
container.appendChild(label);
container.appendChild(textareaWrapper);
//翻译状态
let translateStatus = false;
let isProcessing = false;
//初始化属性数据
const initData = () =>{
translateStatus = false;
isProcessing = false;
textarea.value = '...';
// 手动触发高度调整
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
}
// 暴露设置方法
const setContent = (content) => {
if (content) {
textarea.value = content;
// 手动触发高度调整
const event = new Event('input', { bubbles: true });
textarea.dispatchEvent(event);
}
};
const getContent = () => {
return textarea.value;
};
const setTitle = (title) => {
if (title) label.textContent = title;
};
const setTranslateStatus = (status) => {
translateStatus = status;
};
const getTranslateStatus = () => {
return translateStatus; // 直接返回状态值
};
const setIsProcessing = (status) => {
isProcessing = status;
};
const getIsProcessing = () => {
return isProcessing; // 直接返回状态值
};
return {
initData,
container, // DOM节点
setTitle, //设置翻译线路
setContent, // 设置内容的方法
setTranslateStatus,//设置翻译状态
getTranslateStatus,
getIsProcessing,
setIsProcessing,
getContent
};
};
const styledTextarea = createStyledTextarea()
const addTranslatePreview = ()=>{
const footerDiv = document.querySelector("#main > footer")
const msgDiv = footerDiv.childNodes[0]
if (msgDiv) {
// 创建父容器
const container = document.createElement('div');
msgDiv.parentNode.insertBefore(container, msgDiv);
container.appendChild(styledTextarea.container);
container.appendChild(msgDiv);
}
}
const addTranslateListener = () => {
// 获取元素
const editableDiv = document.querySelector('footer div[aria-owns="emoji-suggestion"][contenteditable="true"]')
// 防抖函数
const debounce = (func, delay) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId); // 清除之前的定时器
timeoutId = setTimeout(() => func.apply(this, args), delay); // 设置新的定时器
};
};
// 处理输入事件的逻辑
const handleInput = async () => {
const flag = trcConfig.translatePreview;
const textContent = getMsgText(editableDiv)
const sendStatus = trcConfig.sendTranslateStatus;
if(textContent === '' || !textContent) {
styledTextarea.setContent('...')
return;
}
const isProcessing = styledTextarea.getIsProcessing();
if (flag && flag === 'true' && sendStatus === 'true' && isProcessing === false) {
const to = trcConfig.sendTargetLanguage;
styledTextarea.setTranslateStatus(false)
await sendTranslate(textContent,to);
}
};
// 创建防抖后的 handleInput 函数
const debouncedHandleInput = debounce(handleInput, 500);
let isProcessing = false;
editableDiv.addEventListener('keydown', async (event) => {
debouncedHandleInput()
if (event.key === 'Enter' && !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
if (isProcessing) return;
const sendTranslateStatus = trcConfig.sendTranslateStatus;
const sendPreview = trcConfig.translatePreview;
const textContent = getMsgText(editableDiv);
const from = trcConfig.sendSourceLanguage;
const to = trcConfig.sendTargetLanguage;
const route = trcConfig.translateRoute;
const mode = trcConfig.mode;
const args = {text:textContent,from:from,to: to,route:route,mode:mode};
if (sendPreview === 'true'&& sendTranslateStatus === 'true') {
const status = styledTextarea.getTranslateStatus()
const isProcess = styledTextarea.getIsProcessing()
if (status === true && isProcess === false) {
const translateText = styledTextarea.getContent();
// 获取所有需要翻译的文本元素
let msgElements = document.querySelectorAll('span.selectable-text.copyable-text[data-lexical-text="true"]');
//判断发送的内容是否全是表情
if (editableDiv.querySelector('span') && msgElements.length<=0) {
sendMsg()
return;
}
await inputMsg(translateText)
// 发送消息,确保翻译完成后再发送
sendMsg();
styledTextarea.setTranslateStatus(false)
styledTextarea.setContent('...')
}
}else if (textContent && textContent !=='' && sendTranslateStatus === 'true') {
isProcessing = true;
styledTextarea.setContent('翻译中...')
const res = await ipc.translateText(args)
if (res.status) {
const translateText = res.data;
await inputMsg(translateText)
// 发送消息,确保翻译完成后再发送
sendMsg();
isProcessing = false;
}else {
styledTextarea.setContent(res.message);
isProcessing = false;
}
}else {
sendMsg();
}
}
},true);
// 监听父容器的事件
document.body.addEventListener('click', function(event) {
// 通过特征匹配目标元素
const target = event.target.closest('span[data-icon="send"]') || event.target.closest('span[data-icon="wds-ic-send-filled"]');
if (target && event.isSimulated !== true) {
const status = styledTextarea.getTranslateStatus()
const isProcess = styledTextarea.getIsProcessing()
const sendTranslateStatus = trcConfig.sendTranslateStatus;
// console.log(`发送按钮被点击status: ${status} isProcess: ${isProcess} sendTranslateStatus: ${sendTranslateStatus}`)
if (isProcess === true) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}else {
if (sendTranslateStatus === 'true' && status === true) {
const translateText = styledTextarea.getContent();
inputMsg(translateText).then(()=>{
styledTextarea.setTranslateStatus(false)
})
}else {
const to = trcConfig.sendTargetLanguage;
styledTextarea.setTranslateStatus(false)
let textContent = getMsgText(editableDiv)
// console.log('翻译文本',textContent)
sendTranslate(textContent,to);
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
}
}
}
});
};
const updateConfigInfo = async () => {
const currentUserId = getCurrentUserId();
const args = {platform: 'WhatsApp', userId: currentUserId || ''};
const res = await ipc.getTranslateConfig(args)
if (res.status) {
styledTextarea.setTitle(res.data.zhName);
trcConfig = res.data;
}else {
trcConfig = {};
console.error('获取配置信息失败:',res);
}
}
//会话列表切换触发函数
const sessionChange = async () => {
const currentUserId = getCurrentUserId();
const args = {platform: 'WhatsApp', userId: currentUserId};
ipc.infoUpdate(args)
await updateConfigInfo()
const myNode = document.getElementById('custom-translate-textarea')
if (!myNode) {
addTranslatePreview();
addTranslateListener();
}
styledTextarea.initData()
}
const debouncedSessionChange = debounce(sessionChange,200);
const getMsgText = (node) => {
// 获取所有 span 元素
const spans = node.querySelectorAll('span');
// 用于存储每一行的内容
let content = [];
spans.forEach(span => {
// 获取 span 的文本内容并去除前后空格
const text = span.textContent.trim();
// 如果内容为空,则添加空字符串,否则添加文本内容
content.push(text === '' ? '' : text);
});
// 将内容数组拼接成一个字符串,用换行符分隔
return content.join('\n');
};
const monitorMainNode = ()=> {
// 监听整个 body 的 DOM 变化,等待 #main 节点的出现
const observer = new MutationObserver(async (mutationsList, observer) => {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
//设置授权码
// 检查是否已经存在 id="main" 的节点
const mainNode = document.getElementById('pane-side');
if (mainNode) {
// 停止对 body 的观察,避免不必要的性能开销
observer.disconnect();
// 开始监听 #main 节点的子节点变化
observePaneSide(mainNode);
}
}
}
});
// 监听左侧消息列表切换
const observePaneSide = (paneSideNode) => {
const observer = new MutationObserver(async (mutationsList) => {
for (const mutation of mutationsList) {
// 确保是属性变化事件,并且是 aria-selected 属性
if (mutation.type === 'attributes' && mutation.attributeName === 'aria-selected') {
const targetNode = mutation.target;
// 检查 aria-selected 属性值
if (targetNode.getAttribute('aria-selected') === 'true') {
observeChatChanges();
debouncedSessionChange()
}
}
}
});
// 配置 MutationObserver监听 aria-selected 属性变化
const config = { attributes: true, subtree: true, attributeFilter: ['aria-selected'] };
// 监听 pane-side 节点
observer.observe(paneSideNode, config);
}
// 每次切换会话后重新获取监听消息列表处理消息翻译
const observeChatChanges = () => {
const chatContainer = document.querySelector("#app");
if (chatContainer) {
const observer = new MutationObserver((mutations) => {
observer.disconnect(); // 暂时断开观察器以避免循环触发
debouncedAddTranslateButtonToAllMessages()
observer.observe(chatContainer, {
childList: true,
subtree: true,
});
});
observer.observe(chatContainer, {
childList: true,
subtree: true,
});
}
};
/**
* 为所有消息添加翻译按钮
*/
const addTranslateButtonToAllMessages = async () => {
// 选择发送和接收的消息元素
const messageElements = document.querySelectorAll("div[role='row'] .message-out, div[role='row'] .message-in");
for (let i = messageElements.length - 1; i >= 0; i--) {
const msgSpan = messageElements[i].querySelector('span[dir="ltr"]');
if (msgSpan) {
const flag = msgSpan.hasAttribute('data-translated');
if (flag === false) {
//添加属性表示已经处理过翻译
msgSpan.setAttribute('data-translated', 'yes');
await createTranslateButtonForMessage(msgSpan);
}
}
}
};
//为每条消息创建翻译按钮
const createTranslateButtonForMessage = async ( msgSpan) => {
let text = getMsgText(msgSpan);
if (!text) return;
const translateDiv = document.createElement("div");
translateDiv.style.cssText = `
min-height: 20px;
justify-content: space-between;
margin-top: 5px;
border-top:1px dashed var(--message-primary);
color:var(--secondary);
display: flex;`;
const leftDiv = document.createElement("div");
leftDiv.style.cssText = `
min-height: 20px;
font-size:12px;
color:var(--WDS-content-action-emphasized);`;
const rightDiv = document.createElement("div");
rightDiv.style.cssText = `
cursor: pointer;
width: 20px;
height: 20px;
user-select: none;
margin-left:5px;
`;
rightDiv.textContent = '🔄'
rightDiv.addEventListener('click', async (e) => {
let text = getMsgText(msgSpan);
text = text.replace(/\n/g, '<br>');
leftDiv.style.color = 'var(--WDS-content-action-emphasized)';
leftDiv.textContent = `翻译中...`;
rightDiv.style.display = 'none';
//发送请求获取翻译结果
const route = trcConfig.translateRoute;
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});
if (res.status) {
leftDiv.innerHTML = res.data;
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
rightDiv.style.display = '';
}
});
// 组装结构
translateDiv.appendChild(leftDiv);
translateDiv.appendChild(rightDiv);
// 插入到消息元素右侧
msgSpan.appendChild(translateDiv);
const receiveTranslateStatus = trcConfig.receiveTranslateStatus;
if (receiveTranslateStatus === 'true') {
let text = getMsgText(msgSpan);
text = text.replace(/\n/g, '<br>');
leftDiv.style.color = 'var(--WDS-content-action-emphasized)';
leftDiv.textContent = `翻译中...`;
rightDiv.style.display = 'none';
//发送请求获取翻译结果
const route = trcConfig.translateRoute;
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,mode:mode});
if (res.status) {
leftDiv.innerHTML = res.data;
rightDiv.style.display = '';
}else {
leftDiv.style.color = 'red';
leftDiv.textContent = `${res.message}`;
rightDiv.style.display = '';
}
}
};
const debouncedAddTranslateButtonToAllMessages = debounce(
addTranslateButtonToAllMessages,
200
);
// 开始观察 body 的子节点变化
observer.observe(document.body, { childList: true, subtree: true });
}
/**
* 函数防抖,用于优化频繁触发的操作
* @param {Function} func 需要防抖的函数
* @param {number} delay 防抖延迟时间(毫秒)
*/
function debounce (func, delay){
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
};
monitorMainNode()
function getSendBtn (){
return document.querySelector('footer span[data-icon="send"]')?.parentNode
|| document.querySelector('footer span[data-icon="wds-ic-send-filled"]')?.parentNode;
}
//快捷回复相关部分
async function inputMsg(translation) {
// 从 footer 开始查找输入框
const footer = document.querySelector('footer._ak1i');
if (!footer) {
console.error('未找到输入框容器');
return;
}
// 查找富文本输入框
const richTextInput = footer.querySelector('.lexical-rich-text-input div[contenteditable="true"]');
// 1. 检测操作系统
const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
// 2. 聚焦输入框
richTextInput.focus();
// 3. 模拟 Ctrl+A/Command+A
const selectAll = new KeyboardEvent('keydown', {
key: 'a',
code: 'KeyA',
ctrlKey: !isMac,
metaKey: isMac,
bubbles: true
});
richTextInput.dispatchEvent(selectAll);
// 4. 模拟退格键
const backspace = new KeyboardEvent('keydown', {
key: 'Backspace',
code: 'Backspace',
bubbles: true
});
richTextInput.dispatchEvent(backspace);
// 7. 触发输入事件
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: translation
});
richTextInput.dispatchEvent(inputEvent);
}
const quickReply = async (args)=>{
const {type,text} = args;
if (type === 'input') {
await inputMsg(text);
}
if (type === 'send') {
await inputMsg(text);
let sendButton = getSendBtn()
if (sendButton) {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true
});
event.isSimulated = true; // 标记为模拟事件
sendButton.dispatchEvent(event);
}
}
}

View File

@ -0,0 +1,123 @@
'use strict';
const { logger } = require('ee-core/log');
const { app, BrowserWindow } = require('electron')
class ContactInfoService {
async getContactInfo(args,event) {
const {userId,partitionId,platform} = args;
if (!platform?.trim() && !userId?.trim() || (!partitionId?.trim() && !platform?.trim())) {
return {status:false,message:'参数有误'}
}
if (partitionId?.trim()) {
//获取窗口对象并查询是否有userid
const view = app.viewsMap.get(partitionId)
if (view && !view.webContents.isDestroyed()) {
const nUserId = await view.webContents.executeJavaScript('getCurrentUserId()')
if (nUserId?.trim()) {
//更新数据
const userInfo = await app.sdb.selectOne('contact_info', {userId:nUserId,platform:platform})
if (userInfo) {
let records = [];
const followRecords = await app.sdb.select('follow_record', {userId:nUserId,platform:platform})
if (followRecords?.length > 0) {
records = followRecords;
}
return {status:true,message:'查询成功!',data:{userInfo:userInfo,records:records}}
}else {
//初始化用户信息
await app.sdb.insert('contact_info',{userId:nUserId,platform:platform,phoneNumber:nUserId})
return {status:true,message:'初始化用户信息成功!',data:{userInfo:{platform:platform,userId:nUserId},records:[]}}
}
}else {
return {status:false,message:'请选择会话'}
}
}
}
const userInfo = await app.sdb.selectOne('contact_info',{userId:userId,platform:platform})
if (userInfo) {
let records = [];
const followRecords = await app.sdb.select('follow_record', {userId:userId,platform:platform})
if (followRecords?.length > 0) {
records = followRecords;
}
return {status:true,message:'查询成功!',data:{userInfo:userInfo,records:records}}
}else {
//初始化用户信息
await app.sdb.insert('contact_info',{userId:userId,platform:platform,phoneNumber:userId})
const userInfo = await app.sdb.selectOne('contact_info',{userId:userId,platform:platform})
return {status:true,message:'初始化用户信息成功!',data:{userInfo:userInfo,records:[]}}
}
}
async updateContactInfo(args,event) {
// 参数解构
const { key, value, id } = args;
// 参数校验
if (!id) throw new Error('缺少必要参数id');
if (!key) throw new Error('缺少必要参数key');
if (typeof value === 'undefined') return;
try {
// 构建更新对象(使用动态属性名)
const updateData = {
[key]: value
};
// 执行更新
await app.sdb.update(
'contact_info',
updateData,
{ id:id }
);
// 返回最新配置(可选)
const updatedConfig = await app.sdb.selectOne('contact_info', { id:id });
return {
status: true,
message:'更新成功',
data: updatedConfig
};
} catch (error) {
return {status:true,message:error.message};
}
}
async getFollowRecord(args,event) {
const {userId,platform} = args;
if (!platform?.trim() && !userId?.trim()) {
return {status:false,message:'参数有误'}
}
const records = await app.sdb.select('follow_record',{userId,platform});
return {status:true,message:'查询成功',data:records};
}
async addFollowRecord(args,event) {
const {userId,platform,content,timestamp} = args;
if (!platform || !userId) {
return {status:false,message:'参数有误'}
}
try{
const id = await app.sdb.insert('follow_record',{userId:userId,platform:platform,content:content,timestamp:timestamp});
if (id) {
const row = {id:id,userId:userId,platform:platform,content:content,timestamp:timestamp};
return {status:true,data:row,message:'添加成功'}
}
return {status:false,message:'添加失败'};
}catch(err){
return {status:false,message:`添加失败:${err.message}`};
}
}
async updateFollowRecord(args,event) {
const {id,content} = args;
if (!id) return {status:false,message:'参数不能为空'}
const count = await app.sdb.update('follow_record',{content:content},{id:id});
return {status:true,message:'更新成功'};
}
async deleteFollowRecord(args,event) {
const {id} = args;
if (!id) {
return {status:false,message:'参数有误'}
}
const count = await app.sdb.delete('follow_record',{id:id});
return {status:true,message:'删除成功'};
}
}
ContactInfoService.toString = () => '[class ContactInfoService]';
module.exports = {
ContactInfoService,
contactInfoService: new ContactInfoService()
};

View File

@ -0,0 +1,100 @@
'use strict';
const { logger } = require('ee-core/log');
const { app, BrowserWindow } = require('electron')
class QuickReplyService {
async getGroups(args,event) {
const groups = await app.sdb.select('group_manage');
for (let group of groups) {
const records = await app.sdb.select('quick_reply_record',{groupId:group.id})
group.contents = records;
group.contentCount = records.length;
}
return {status:true,message:'查询成功',data:groups};
}
async getContentByGroupId(args,event) {
const {groupId} = args;
const records = await app.sdb.select('quick_reply_record',{groupId:groupId})
return {status:true,message:'查询成功',data:records};
}
async addGroup(args,event) {
const {name} = args;
await app.sdb.insert('group_manage',{name:name})
return {status:true,message:'添加成功'};
}
async editGroup(args,event) {
const {name,id} = args;
await app.sdb.update('group_manage',{name:name},{id:id})
return {status:true,message:'修改成功'};
}
async deleteGroup(args,event) {
const {id} = args;
await app.sdb.delete('group_manage',{id:id})
await app.sdb.delete('quick_reply_record',{groupId:id})
return {status:true,message:'删除成功'};
}
async addReply(args,event) {
const {remark,content,url,type,groupId} = args;
if (!groupId) return {status:false,message:'分组id不能为空'};
// 创建要更新的字段对象
const rows = { remark,content,url,type,groupId };
// 过滤掉值为空的字段,避免更新无效字段
Object.keys(rows).forEach(key => {
if (rows[key] === undefined || rows[key] === null) {
delete rows[key];
}
});
try {
await app.sdb.insert('quick_reply_record',rows)
return {status:true,message:'新增成功'}
}catch(err) {
return {status:false,message:`系统错误:${err.message}`};
}
}
async editReply(args,event) {
const { id, remark,content,url } = args;
// 参数验证
if (!id || !remark) {
return { status: false, message: '参数缺失,请检查' };
}
// 创建要更新的字段对象
const rows = { remark,content,url };
// 过滤掉值为空的字段,避免更新无效字段
Object.keys(rows).forEach(key => {
if (rows[key] === undefined || rows[key] === null) {
delete rows[key];
}
});
// 执行更新
try {
await app.sdb.update('quick_reply_record', rows, { id });
return { status: true, message: `更新成功` };
} catch (error) {
return { status: false, message: `系统错误:${error.message}` };
}
}
async deleteReply(args,event) {
const {id} = args;
if (!id) return {status:false,message:'id不能为空'}
await app.sdb.delete('quick_reply_record',{id:id})
return {status:true,message:'删除成功'}
}
async deleteAllReply(args,event) {
const {groupId} = args;
if (!groupId) return {status:false,message:'分组id不能为空'}
await app.sdb.delete('quick_reply_record',{groupId:groupId})
return {status:true,message:'清空数据成功'}
}
}
QuickReplyService.toString = () => '[class QuickReplyService]';
module.exports = {
QuickReplyService,
quickReplyService: new QuickReplyService()
};

191
electron/service/system.js Normal file
View File

@ -0,0 +1,191 @@
'use strict';
const { machineIdSync } = require('node-machine-id');
const { logger } = require('ee-core/log');
const os = require('os')
const { app, BrowserWindow } = require('electron')
const { post, get } = require("axios");
const { timestampToString } = require("../utils/CommonUtils");
class SystemService {
async getBaseInfo(args, event) {
// 获取基于硬件的原始机器码(更难伪造)
const hardwareId = machineIdSync(true);
// 获取操作系统信息
const systemInfo = {
name: app.getName(),
platform: process.platform, // 操作系统类型
version: app.getVersion(),
osName: this.getOSName(), // 操作系统名称
arch: os.arch(), // 系统架构
machineCode: hardwareId //机器码
}
return systemInfo;
}
// * 错误码说明:
// * {401} 参数缺失 - 请求缺少必要参数或参数格式错误
// * {402} 账户不存在 - 提供的凭证未关联任何注册账户
// * {403} 账户禁用 - 账户已被系统管理员停用
// * {404} 设备授权过期 - 当前设备授权已超过有效期
// * {405} 设备禁用 - 账户已主动禁用该设备访问权限
async login(args, event) {
const { authKey } = args;
const machineCode = machineIdSync(true);
// 参数校验:邀请码不能为空
if (!authKey) return { status: false, message: 'login.errors.emptyAuthCode' };
try {
// 统一转换为小写比较
const lowerCaseCode = machineCode.toLowerCase();
const url = app.baseUrl + '/check_login'
console.log('url======', url)
const reqData = { key: authKey, device: lowerCaseCode }
const res = await post(url, reqData, { timeout: 30000 })
logger.info('res======',res.data)
const { code, message, device_valid_until, user_remaining_chars, device_status, user_name,userApiKey } = res.data
logger.info('resp======',res.data)
// 根据响应code处理不同情况
switch (code) {
case 2000: // 请求成功
const timestamp = device_valid_until
const data = {
expireTime: timestampToString(timestamp),//失效时间
totalChars: user_remaining_chars,//剩余字符数
authKey: userApiKey,//授权码
machineCode: machineCode,//机器码
userName: user_name//用户名
}
app.authInfo = data;
return { status: true, message: 'login.success', data: data };
case 401: // 参数缺失 - 请求缺少必要参数或参数格式错误
return { status: false, message: 'login.errors.invalidParams' };
case 402: // 账户不存在 - 提供的凭证未关联任何注册账户
return { status: false, message: 'login.errors.accountNotExist' };
case 403: // 账户禁用 - 账户已被系统管理员停用
return { status: false, message: 'login.errors.accountDisabled' };
case 404: // 设备授权过期 - 当前设备授权已超过有效期
return { status: false, message: 'login.errors.expired' };
case 405: // 设备禁用 - 账户已主动禁用该设备访问权限
return { status: false, message: 'login.errors.deviceDisabled' };
default: // 其他未知错误
return { status: false, message: 'login.errors.unknown' };
}
} catch (err) {
// app.authInfo = null;
console.log('err======', err)
logger.error('login request error:', err.message);
// 网络请求异常
return {
status: false,
message: 'login.errors.networkError'
};
}
}
async userLogin(args, event) {
const { username, password, device } = args;
const machineCode = machineIdSync(true);
if (!username || !password || !device) {
return { code: 401, message: '用户名、密码或设备码不能为空' };
}
try {
const url = app.baseUrl + '/user_login';
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)
// 根据响应code处理不同情况
switch (code) {
case 2000: // 请求成功
const timestamp = device_valid_until
const data = {
expireTime: timestampToString(timestamp),//失效时间
totalChars: user_remaining_chars,//剩余字符数
authKey: userApiKey,//授权码
machineCode: machineCode,//机器码
userName: user_name,//用户名
parentId: parent_id,//父账户id
userId: user_id//用户id
}
app.authInfo = data;
return { status: true, message: 'login.success', data: data };
case 401: // 参数缺失 - 请求缺少必要参数或参数格式错误
return { status: false, message: 'login.errors.invalidParams' };
case 402: // 账户不存在 - 提供的凭证未关联任何注册账户
return { status: false, message: 'login.errors.accountNotExist' };
case 403: // 账户禁用 - 账户已被系统管理员停用
return { status: false, message: 'login.errors.accountDisabled' };
case 404: // 设备授权过期 - 当前设备授权已超过有效期
return { status: false, message: 'login.errors.expired' };
case 405: // 设备禁用 - 账户已主动禁用该设备访问权限
return { status: false, message: 'login.errors.deviceDisabled' };
default: // 其他未知错误
return { status: false, message: 'login.errors.unknown' };
}
} catch (err) {
logger.error('userLogin request error:', err.message);
return {
code: 500,
message: 'login.errors.networkError'
};
}
}
getOSName() {
switch (process.platform) {
case 'win32': return 'Windows'
case 'darwin': return 'macOS'
case 'linux': return 'Linux'
default: return 'Unknown'
}
}
async createSub(args, event) {
const { username, password, parent_id } = args;
if (!username || !password) {
return { code: 401, message: '用户名密码不能为空' };
}
const url = app.baseUrl + '/user/create_sub';
const reqData = { username, password, parent_id };
const res = await post(url, reqData, { timeout: 30000 });
console.log('res======', res)
return { status: true, message: 'success' };
}
async listSub(args, event) {
const { parentId } = args;
const url = app.baseUrl + '/user/list_sub';
const reqData = { parentId };
const res = await get(url, { params: reqData, timeout: 30000 });
return { status: true, message: 'success', data: res.data };
}
async deleteSub(args, event) {
const { id } = args;
const url = app.baseUrl + '/user/delete_sub/' + id;
const res = await get(url, { timeout: 30000 });
return { status: true, message: 'success' };
}
}
SystemService.toString = () => '[class SystemService]';
module.exports = {
SystemService,
systemService: new SystemService()
};

View File

@ -0,0 +1,559 @@
'use strict';
const { logger } = require('ee-core/log');
const os = require('os')
const { app, BrowserWindow } = require('electron')
const {Translate} = require('@google-cloud/translate').v2;
const CryptoJS = require("crypto-js");
const {post, get} = require("axios");
const VolcEngineSDK = require("volcengine-sdk");
const {getTimeStr} = require("../utils/CommonUtils");
const { ApiInfo, ServiceInfo, Credentials, API, Request } = VolcEngineSDK;
class TranslateService {
async getConfigInfo(args,event) {
let {platform,userId,partitionId} = args;
if (!platform?.trim() && !userId?.trim()) {
return {
status: false,
message: '参数不能同时为空,请至少填写一个有效参数'
}
}
if (partitionId) {
//获取窗口对象并查询是否有userid
const view = app.viewsMap.get(partitionId)
if (view && !view.webContents.isDestroyed()) {
const nUserId = await view.webContents.executeJavaScript('getCurrentUserId()')
if (nUserId?.trim()) {
let configById = await app.sdb.selectOne('translate_config',{userId:nUserId})
if (configById?.friendTranslateStatus && configById.friendTranslateStatus === 'true') {
configById.showAloneBtn = 'true'
return {status:true,message:'查询成功',data:configById}
}else {
let data = await app.sdb.selectOne('translate_config',{platform:platform})
if (data) {
data.showAloneBtn = 'true'
return {status:true,message:'查询成功',data:data}
}
}
}else {
let data = await app.sdb.selectOne('translate_config',{platform:platform})
if (data) {
data.showAloneBtn = 'false'
return {status:true,message:'查询成功',data:data}
}
}
}
}
const configByPlatform = await app.sdb.selectOne('translate_config',{platform:platform})
const configById = await app.sdb.selectOne('translate_config',{userId:userId})
if (!configByPlatform && platform?.trim()) {
//初始化平台翻译配置信息
const initialData = {
userId: "",
platform: platform,
receiveTranslateStatus: 'false',
translateRoute:'youDao',
receiveSourceLanguage: "auto",
receiveTargetLanguage: "zh-CN",
sendTranslateStatus: "true",
sendSourceLanguage: "auto",
sendTargetLanguage: "en",
friendTranslateStatus: "false",
showAloneBtn:'false',
chineseDetectionStatus: "false",
translatePreview: "true"
}
await app.sdb.insert('translate_config', initialData);
}
if (!configById && userId?.trim()) {
//初始化平台翻译配置信息
const initialData = {
userId: userId,
platform: '',
translateRoute:'youDao',
receiveTranslateStatus: 'false',
receiveSourceLanguage: "auto",
receiveTargetLanguage: "zh-CN",
sendTranslateStatus: "true",
sendSourceLanguage: "auto",
sendTargetLanguage: "en",
friendTranslateStatus: "false",
showAloneBtn:'true',
chineseDetectionStatus: "false",
translatePreview: "true"
}
await app.sdb.insert('translate_config', initialData);
}
if (configById?.friendTranslateStatus && configById.friendTranslateStatus === 'true') {
configById.showAloneBtn = 'true'
return {status:true,message:'查询成功',data:configById}
}else {
const data = await app.sdb.selectOne('translate_config',{platform:platform})
if (userId !== null && userId !== '') {
data.showAloneBtn='true';
}else {
data.showAloneBtn='false';
}
return {status:true,message:'查询成功',data:data}
}
}
// 后端服务方法
async updateTranslateConfig(args,event) {
// 参数解构
const { key, value, id ,partitionId} = args;
// 参数校验
if (!id) throw new Error('缺少必要参数id');
if (!key) throw new Error('缺少必要参数key');
if (typeof value === 'undefined') return;
try {
// 构建更新对象(使用动态属性名)
const updateData = {
[key]: value
};
// 执行更新
const changes = await app.sdb.update(
'translate_config',
updateData,
{ id:id }
);
// 返回最新配置(可选)
const updatedConfig = await app.sdb.selectOne('translate_config', { id:id });
if (partitionId) await this._routeUpdateNotify(partitionId)
return {
success: true,
data: updatedConfig
};
} catch (error) {
console.error('配置更新失败:', error);
throw new Error(`配置更新失败: ${error.message}`);
}
}
async changeAloneStatus(args,event) {
const { status, partitionId } = args;
if (!status?.trim() && !partitionId?.trim()) {
return {
status: false,
message: '参数传递错误'
}
}
//获取窗口对象并查询是否有userid
const view = app.viewsMap.get(partitionId)
if (view && !view.webContents.isDestroyed()) {
const nUserId = await view.webContents.executeJavaScript('getCurrentUserId()')
if (nUserId?.trim()) {
const configById = await app.sdb.selectOne('translate_config',{userId:nUserId})
if (configById) {
//更新数据
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:false,message:'配置不存在或窗口被关闭'}
}
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:[]}
}
async addLanguage(args, event) {
const { code, zhName, enName, youDao, baidu, huoShan, xiaoNiu, google, timestamp } = args;
// 参数验证
if (!code || !zhName) {
return { status: false, message: '缺少必要参数,编码和名称为必填项' };
}
// 检查编码是否已存在
const info = await app.sdb.selectOne('language_list', { code: code });
if (info) {
return { status: false, message: '当前编码已存在,请使用不同的编码' };
}
// 准备插入的数据
const rows = { code: code, zhName: zhName };
// 动态添加可选字段
if (enName) rows['enName'] = enName;
if (zhName) rows['zhName'] = zhName;
if (timestamp) rows['timestamp'] = timestamp;
if (youDao !== undefined) rows['youDao'] = youDao;
if (baidu !== undefined) rows['baidu'] = baidu;
if (huoShan !== undefined) rows['huoShan'] = huoShan;
if (xiaoNiu !== undefined) rows['xiaoNiu'] = xiaoNiu;
if (google !== undefined) rows['google'] = google;
// 执行插入
try {
const id = await app.sdb.insert('language_list', rows);
if (!id) {
return { status: false, message: '数据写入失败,请稍后重试' };
}
// 查询插入后的数据
const data = await app.sdb.selectOne('language_list', { id: id });
return { status: true, message: '语言配置添加成功', data: data };
} catch (error) {
return { status: false, message: `添加失败,系统错误:${error.message}` };
}
}
async deleteLanguage(args,event) {
const {id} = args;
if (!id) return {status:false,message:'id不能为空'}
const count = await app.sdb.delete('language_list',{id:id})
if (count >0) return {status:true,message:`成功删除${count}条数据`}
return {status:false,message:`没有查询到这条数据`}
}
async editLanguage(args, event) {
const { id, zhName, enName, code, youDao, baidu, huoShan, xiaoNiu, google } = args;
// 参数验证
if (!id || !zhName || !code) {
return { status: false, message: '参数缺失,请检查 ID、名称和code。' };
}
// 创建要更新的字段对象
const rows = { code, zhName, enName, youDao, baidu, huoShan, xiaoNiu, google };
// 过滤掉值为空的字段,避免更新无效字段
Object.keys(rows).forEach(key => {
if (rows[key] === undefined || rows[key] === null) {
delete rows[key];
}
});
// 执行更新
try {
const count = await app.sdb.update('language_list', rows, { id });
// 检查更新结果
if (count > 0) {
return { status: true, message: `语言配置更新成功` };
} else {
return { status: false, message: `没有找到对应的语言配置,更新失败。` };
}
} catch (error) {
return { status: false, message: `更新失败,系统错误:${error.message}` };
}
}
async addTranslateRoute(args,event) {
const {name,zhName,enName,otherArgs} = args;
const info = await app.sdb.selectOne('translate_route',{name:name})
if (info) return;
const rows = {name:name};
if (enName) rows['enName'] = enName;
if (zhName) rows['zhName'] = zhName;
if (otherArgs) rows['otherArgs'] = otherArgs;
await app.sdb.insert('translate_route',rows)
}
async editTranslateRoute(args,event) {
const {name,dataJson} = args;
const config = await app.sdb.selectOne('translate_route',{name:name})
if (config) {
const count = await app.sdb.update('translate_route',{otherArgs:dataJson},{name:name})
if (count > 0) return {status:true,message:'修改成功'}
return {status:false,message:'修改失败'}
}
}
async getRouteConfig(args, event) {
const { name } = args;
const config = await app.sdb.selectOne('translate_route', { name: name });
if (config) {
try {
const dataJson = JSON.parse(config.otherArgs); // 将 JSON 字符串转换为对象
return {status:true,message:'查询成功',data:dataJson};
} catch (error) {
console.error('JSON 解析失败:', error);
return {status:false,message:`解析参数失败:%${error.message}`};
}
}
return {status:false,message:`找不到配置信息!`};
}
async getRouteList(args, event) {
const routeList = await app.sdb.select('translate_route', {});
if (routeList) {
return {status:true,message:'查询成功',data:routeList};
}
return {status:false,message:`暂无翻译线路!`,data:[]};
}
async testRoute(args, event) {
const { name } = args;
const config = await app.sdb.selectOne('translate_route', { name: name });
if (config) {
try {
const args = {
route:config.name,
text:`你好!${config.zhName}`,
from:'auto',
to:'en',
mode:'local'
}
return await this.translateText(args)
} catch (error) {
return {status:false,message:`翻译失败!${error.message}`};
}
} return {status:false,message:`找不到配置信息!`};
}
async translateText(args, event) {
const { route, text, from, to ,partitionId,isFilter,mode,sourceTo} = args;
const routeMap = {
google: this._googleTranslateText,
baidu: this._baiduTranslateText,
youDao: this._youDaoTranslateText,
huoShan: this._huoShanTranslateText,
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 };
}
}
// logger.info('文本翻译:', args);
try {
if (mode === 'cloud') {
const authInfo = app.authInfo;
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
}
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()});
}
return { status: true, message: '翻译成功', data: result };
}
} catch (err) {
const msg = err.response?.data?.error || err.message;
logger.error('翻译请求失败:',msg)
return { status: false, message: '翻译失败!请重试或更换其它翻译线路!' };
}
}
//翻译线路更改通知
async _routeUpdateNotify (partitionId) {
if (!partitionId) return;
const view = app.viewsMap.get(partitionId);
if (view && !view.webContents.isDestroyed()) {
await view.webContents.executeJavaScript('updateConfigInfo()')
}
}
async _googleTranslateText(text,target,route) {
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
const response = await get(url);
const data = await response.data;
if (data && data[0]) {
// 合并所有翻译段落
const translation = data[0]
.filter(item => item && item[0]) // 过滤掉空值
.map(item => item[0]) // 提取翻译文本
.join('\n'); // 用换行符连接
return translation;
} else {
throw new Error('Translation result format error');
}
}
async _baiduTranslateText(text,target,route) {
const config = await app.sdb.selectOne('translate_route',{name:route})
if (!config) {
throw new Error('找不到当前翻译线路配置!')
}
try {
const configData = JSON.parse(config.otherArgs); // 将 JSON 字符串转换为对象
const appid = configData?.appId;
const key = configData?.apiKey;
const url = configData?.apiUrl;
const salt = Date.now(); // 生成随机数
const sign = CryptoJS.MD5(appid + text + salt + key).toString(); // 计算 MD5 签名
const response = await get(url, {
params: {
q: text,
from: 'auto',
to: target,
appid: appid,
salt: salt,
sign: sign
}
});
const data = response.data;
if (data.trans_result) {
return data.trans_result.map(item => item.dst).join("\n");
} else {
throw new Error(`翻译失败:${data?.error_code} : ${data?.error_msg}`)
}
} catch (error) {
throw new Error(`${error.message}`)
}
}
async _youDaoTranslateText(text,target,route) {
const config = await app.sdb.selectOne('translate_route',{name:route})
if (!config) {
throw new Error('找不到当前翻译线路配置!')
}
try {
const configData = JSON.parse(config.otherArgs); // 将 JSON 字符串转换为对象
const appKey = configData?.appId;
const appSecret = configData?.apiKey;
const url = configData?.apiUrl;
// 生成签名所需参数
const salt = Date.now().toString();
const curtime = Math.floor(Date.now() / 1000).toString();
// 处理长文本(截取规则)
const truncate = (q) => {
if (q.length <= 20) return q;
return q.substring(0, 10) + q.length + q.substring(q.length - 10);
};
// 生成签名
const signStr = appKey + truncate(text) + salt + curtime + appSecret;
const sign = CryptoJS.SHA256(signStr).toString(CryptoJS.enc.Hex);
// 构造请求参数
const params = new URLSearchParams();
params.append('q', text);
params.append('from', 'auto');
params.append('to', target);
params.append('appKey', appKey);
params.append('salt', salt);
params.append('sign', sign);
params.append('signType', 'v3');
params.append('curtime', curtime);
params.append('strict', "true");
// 发送请求
const response = await post(
url,
params,
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
);
// 错误处理
if (response.data.errorCode !== '0') {
throw new Error(`翻译失败,错误码:${response.data.errorCode}`);
}
// 返回翻译结果
return response.data.translation[0];
} catch (error) {
throw error;
}
}
async _huoShanTranslateText(text, target, route) {
const config = await app.sdb.selectOne('translate_route',{name:route})
if (!config) {
throw new Error('找不到当前翻译线路配置!')
}
const configData = JSON.parse(config.otherArgs);
const AK = configData.apiKey
const SK = configData.secretKey
// 翻译目标语言、翻译文本列表
const toLang = target;
const textList = [text];
// api凭证
const credentials = new Credentials(AK, SK, 'translate', 'cn-north-1');
// 设置请求的 header、query、body
const header = new Request.Header({
'Content-Type': 'application/json'
});
const query = new Request.Query({
'Action': 'TranslateText',
'Version': '2020-06-01'
});
const body = new Request.Body({
'TargetLanguage': toLang,
'TextList': textList
});
// 设置 service、api信息
const serviceInfo = new ServiceInfo(
'open.volcengineapi.com',
header,
credentials
);
const apiInfo = new ApiInfo('POST', '/', query, body);
// 生成 API
const api = API(serviceInfo, apiInfo);
const res = await post(api.url, api.params, api.config)
const translationList = res.data?.TranslationList;
const responseMetadata = res.data?.ResponseMetadata;
// logger.info(translationList);
// logger.info(responseMetadata);
if (translationList && translationList.length > 0) {
// 提取第一个翻译结果的 Translation 字段
return translationList[0].Translation;
} else {
throw new Error(`翻译出错:${responseMetadata.Error.CodeN} : ${responseMetadata.Error.Code}`);
}
}
async _xiaoNiuTranslateText(text,target,route) {
const config = await app.sdb.selectOne('translate_route',{name:route})
if (!config) {
throw new Error('找不到当前翻译线路配置!')
}
const configData = JSON.parse(config.otherArgs);
const apikey = configData?.apiKey;
const url = configData?.apiUrl;
// 构造请求参数
const params = {
from: 'auto',
to: target,
apikey: apikey,
src_text: text
};
// 使用 axios 发送 GET 请求
const response = await get(url, { params });
// 如果返回数据中包含翻译结果,则返回翻译结果
if (response.data && response.data.tgt_text) {
return response.data.tgt_text;
}else {
throw new Error(`翻译出错:${response.data.error_code} ${response.data.error_msg}`)
}
}
}
TranslateService.toString = () => '[class TranslateService]';
module.exports = {
TranslateService,
translateService: new TranslateService()
};

532
electron/service/window.js Normal file
View File

@ -0,0 +1,532 @@
'use strict';
const { logger } = require('ee-core/log');
const os = require('os')
const { app, BrowserWindow, WebContentsView ,session,shell} = require('electron');
const fs = require('fs').promises; // 使用 Promise 版本 API
const path = require('path');
const { generateUniquePartitionId,generateRandomString } = require('../utils/CommonUtils');
const {getMainWindow} = require("ee-core/electron");
class WindowService {
async addSession (args,event) {
const {platform,url,nickname} = args;
try {
//生成唯一分区id
const partitionId = await generateUniquePartitionId();
const createTime = this._getTimeFormat()
if ('CustomWeb' === platform) {
await app.sdb.insert('session_list',{partitionId:partitionId,windowStatus:'false',createTime:createTime,platform:platform,nickName:nickname,msgCount:0,onlineStatus:'false',webUrl:url});
return {status:true,message:'新增成功',data: {partitionId}}
}else {
const webUrl = app.platforms.find(item => item.platform === platform)?.url;
await app.sdb.insert('session_list',{partitionId:partitionId,windowStatus:'false',createTime:createTime,platform:platform,nickName:platform.toLowerCase(),msgCount:0,onlineStatus:'false',webUrl:webUrl});
return {status:true,message:'新增成功',data: {partitionId}}
}
}catch(err){
return {status:false,message:`添加失败:${err.message}`};
}
}
async editSession (args,event) {
const { id,windowId,windowStatus,onlineStatus,remarks,webUrl,avatarUrl,userName,msgCount,nickName} = args;
// 参数验证
if (!id) {
return { status: false, message: '参数缺失,请检查 ID' };
}
// 创建要更新的字段对象
const rows = { windowId,windowStatus,onlineStatus,remarks,webUrl,avatarUrl,userName,msgCount,nickName };
// 过滤掉值为空的字段,避免更新无效字段
Object.keys(rows).forEach(key => {
if (rows[key] === undefined || rows[key] === null) {
delete rows[key];
}
});
// 执行更新
try {
const count = await app.sdb.update('session_list', rows, { id });
// 检查更新结果
if (count > 0) {
return { status: true, message: `会话数据更新成功` };
} else {
return { status: false, message: `没有找到对应的会话配置,更新失败。` };
}
} catch (error) {
return { status: false, message: `更新失败,系统错误:${error.message}` };
}
}
//启动窗口
async startSession (args,event) {
const {platform, partitionId: inputPartitionId} = args; // 重命名解构变量
try {
const sessionObj = await app.sdb.selectOne("session_list",{
platform: platform,
partitionId: inputPartitionId // 使用重命名后的变量
});
if (!sessionObj) return {status:false,message:'没有这个会话记录!'}
const oldView = app.viewsMap.get(inputPartitionId)
if (oldView && !oldView.webContents.isDestroyed() && sessionObj.windowStatus ==='true') {
return {status:true,message:'窗口已经启动!',}
}
// 使用新的变量名
const sessionPartitionId = sessionObj.partitionId;
const webUrl = sessionObj.webUrl;
// 创建这个会话窗口
const view = await this._createWebView(sessionPartitionId) // 使用新变量名
let userAgent = sessionObj.userAgent;
if (!userAgent) {
// 设置 User-Agent
userAgent = this.generateUserAgent();
// userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.5359.60 Safari/537.36'
}
view.webContents.setUserAgent(userAgent);
app.viewsMap.set(sessionPartitionId, view) // 使用新变量名
// view.webContents.openDevTools();
const mainWin = getMainWindow()
mainWin.contentView.addChildView(view);
//加载配置以及处理代理配置信息
this._loadConfig(view,sessionPartitionId,platform).then(async () => {
try {
await view.webContents.loadURL(webUrl, {userAgent: userAgent});
}catch (err){
logger.error('加载 URL 出错:',err.message);
}
})
view.setVisible(false);
await app.sdb.update("session_list",{windowStatus:'true',windowId:view.webContents.id,userAgent:userAgent},{platform:platform,partitionId:inputPartitionId});
sessionObj.windowStatus = 'true'
sessionObj.windowId=view.webContents.id
return {status:true,message:'启动成功',data:sessionObj}
} catch(err) {
return {status:false,message:`启动失败:${err.message}`};
}
}
//获取所有窗口会话信息
async getSessions (args,event) {
const {platform} = args;
try {
const sessions = await app.sdb.select("session_list",{platform:platform});
return {status:true,message:'查询成功',data:{sessions:sessions}};
}catch(err){
return {status:false,message:`查询失败:${err.message}`};
}
}
async getSessionByPartitionId (args,event) {
const {partitionId,platform} = args;
try {
const session = await app.sdb.selectOne("session_list",{partitionId:partitionId});
return {status:true,message:'查询成功',data:{session:session}};
}catch(err){
return {status:false,message:`查询失败:${err.message}`};
}
}
//设置会话窗口位置
async setWindowLocation (args,event) {
const {x,y,width,height} = args;
const location = {x:x, y:y,width:width,height:height};
try {
//获取所有窗口视图
app.viewsMap.forEach((view, key) => {
if (view && !view.webContents.isDestroyed()) {
this._setViewLocation(view,location)
}
});
return {status:true,message:'设置成功'};
}catch(err){
return {status:false,message:`设置失败:${err.message}`};
}
}
//隐藏会话窗口
async hiddenSession (args,event) {
try {
//获取所有窗口视图
app.viewsMap.forEach((view, key) => {
if (view && !view.webContents.isDestroyed()) {
view.setVisible(false);
}
});
return {status:true,message:'操作成功'};
}catch(err){
return {status:false,message:`操作失败:${err.message}`};
}
}
async showSession (args,event) {
const {platform,partitionId} = args;
try {
const sessionObj = await app.sdb.selectOne("session_list",{platform:platform,partitionId:partitionId});
if (!sessionObj) return {status:false,message:'暂无当前会话记录!'}
let view = app.viewsMap.get(partitionId)
if (view && !view.webContents.isDestroyed()) {
await this.hiddenSession()
view.setVisible(true);
return {status:true,message:'操作成功!'};
}else {
return {status:true,message:'会话不存在!请启动!'};
}
}catch(err){
return {status:false,message:`操作失败:${err.message}`};
}
}
async closeSession (args,event) {
const {platform,partitionId} = args;
try {
const sessionObj = await app.sdb.selectOne("session_list",{partitionId:partitionId});
if (!sessionObj) return {status:false,message:'暂无当前会话记录!'}
await this._destroyView(partitionId);
await app.sdb.update('session_list',{windowStatus:'false',onlineStatus:'false',msgCount:0,windowId:0},{partitionId:partitionId});
sessionObj.windowStatus='false';
sessionObj.onlineStatus='false';
sessionObj.msgCount = 0;
sessionObj.windowId = 0;
return {status:true,message:'关闭成功',data:sessionObj}
}catch(err){
return {status:false,message:`关闭会话出错:${err.message}`};
}
}
//刷新会话
async refreshSession(args,event) {
const {platform,partitionId} = args;
const view = app.viewsMap.get(partitionId)
if (view && !view.webContents.isDestroyed()) {
// 刷新
view.webContents.reload();
}
return {status:true,message:'刷新成功'}
}
async deleteSession (args,event) {
const {partitionId} = args;
try {
const sessionObj = await app.sdb.selectOne("session_list",{partitionId:partitionId});
if (!sessionObj) return {status:false,message:'暂无当前会话记录!'}
await this._destroyView(partitionId);
await this._deleteSessionAllData(partitionId)
return {status:true,message:'删除会话成功!'}
}catch(err){
return {status:false,message:`删除会话出错:${err.message}`};
}
}
//获取代理配置信息
async getProxyInfo(args,event) {
const {partitionId} = args;
const config = await app.sdb.selectOne("proxy_config",{partitionId:partitionId});
if (config) return {status:true,message:'查询成功',data:config};
//初始化代理配置信息
const row = {
partitionId:partitionId,
proxyStatus:'false',
proxyType:'http',
proxyIp:'',
proxyPort:'',
userVerifyStatus:'false',
username:'',
password:'',
}
const id = await app.sdb.insert('proxy_config',row);
if (id) {
row.id = id;
return {status:true,message:'初始化代理配置成功',data:row};
}
return {status:false,message:'初始化代理配置出错'}
}
//修改窗口代理配置
async editProxyInfo(args,event) {
const { id,key,value} = args;
// 参数校验
if (!id) throw new Error('缺少必要参数id');
if (!key) throw new Error('缺少必要参数key');
if (typeof value === 'undefined') return;
try {
// 构建更新对象(使用动态属性名)
const updateData = {
[key]: value
};
// 执行更新
await app.sdb.update('proxy_config', updateData, { id:id });
return {status:true,message:'修改配置成功'}
} catch (error) {
logger.error('代理配置修改失败:', error);
return {status:false,message:`修改配置出错:${error.message}`};
}
}
async saveProxyInfo(args, event) {
// 解构参数
const {
id,
proxyStatus,
proxyType,
proxyIp,
proxyPort,
userVerifyStatus,
username,
password,
} = args;
// 参数校验
if (!id) {
throw new Error('缺少必要参数id');
}
// 构建更新对象(动态生成需要更新的字段)
const updateData = {};
if (proxyStatus !== undefined) updateData.proxyStatus = proxyStatus;
if (proxyType !== undefined) updateData.proxyType = proxyType;
if (proxyIp !== undefined) updateData.proxyIp = proxyIp;
if (proxyPort !== undefined) updateData.proxyPort = proxyPort;
if (userVerifyStatus !== undefined) updateData.userVerifyStatus = userVerifyStatus;
if (username !== undefined) updateData.username = username;
if (password !== undefined) updateData.password = password;
// 检查是否有需要更新的字段
if (Object.keys(updateData).length === 0) {
return { status: false, message: '未提供需要更新的字段' };
}
try {
// 执行更新
await app.sdb.update('proxy_config', updateData, { id });
// 返回成功响应
return { status: true, message: '代理配置更新成功' };
} catch (error) {
// 记录错误日志
logger.error('代理配置更新失败:', error);
// 返回错误响应
return { status: false, message: `代理配置更新失败:${error.message}` };
}
}
async openSessionDevTools (args,event) {
const {partitionId,status} = args;
const view = app.viewsMap.get(partitionId)
if (view && !view.webContents.isDestroyed()) {
if (status){
view.webContents.openDevTools()
}else view.webContents.closeDevTools()
}
}
_setViewLocation (view,location) {
view.setBounds({ x: location.x, y: location.y, width: location.width, height:location.height });
}
async _createWebView(partitionId) {
const winSession = session.fromPartition(`persist:${partitionId}`);
const sessionObj = await app.sdb.selectOne("session_list", {partitionId: partitionId});
const platform = sessionObj.platform;
let preloadPath = path.join(__dirname, `../preload/bridges`, `${platform}.js`)
try {
await fs.access(preloadPath); // 检查文件是否存在
} catch (err) {
preloadPath = path.join(__dirname, `../preload`, 'bridge.js')
}
const view = new WebContentsView({
webPreferences: {
session: winSession,
sandbox: true,
plugins: true,
partition: `persist:${partitionId}`,
nodeIntegration: false,
contextIsolation: true,
preload: preloadPath
},
});
view.webContents.on('did-finish-load', () => {
this._loadScriptFile(partitionId).then(() => {
})
});
return view;
}
_destroyView(partitionId) {
const view = app.viewsMap.get(partitionId);
// 检查 view 是否存在且未销毁
if (view && !view.webContents.isDestroyed()) {
view.setVisible(false)
const mainWindow = getMainWindow();
view.webContents.destroy()
mainWindow.contentView.removeChildView(view);
// 从 app.viewsMap 中删除该视图的引用
app.viewsMap.delete(partitionId);
} else {
logger.warn(`未找到 ${partitionId} 的有效视图或视图已被销毁`);
}
}
async _loadScriptFile(partitionId) {
const view = app.viewsMap.get(partitionId);
if (!view || view.webContents.isDestroyed()) {
logger.info('会话已被销毁,不加载脚本!')
return;
}
try {
const sessionObj = await app.sdb.selectOne("session_list",{partitionId:partitionId});
if (!sessionObj) return;
const platform = sessionObj.platform;
// 读取指定文件(示例路径:项目根目录的 scripts/ 目录)
const scriptPath = path.join(__dirname, '../scripts', `${platform}.js`);
const scriptContent = await fs.readFile(scriptPath, 'utf-8');
// 注入并执行脚本
await view.webContents.executeJavaScript(scriptContent);
logger.info('execute jsCode successfully');
} catch (err) {
logger.error('脚本读取或执行失败:', err.message);
}
}
async _loadConfig(view, partitionId, platform) {
try {
// await view.webContents.session.setProxy({mode: 'system'});
// // 1. 清除 HTTP 缓存
// await view.webContents.session.clearCache();
//
// // 2. 清除 DNS 缓存
// await view.webContents.session.clearHostResolverCache();
//
// // 3. 关闭所有活跃连接
// if (view.webContents.session.closeAllConnections) {
// await view.webContents.session.closeAllConnections();
// }
// 获取代理配置(优先按 partitionId 查询,其次按 platform
const config = await app.sdb.selectOne("proxy_config", {partitionId}) ||
await app.sdb.selectOne("proxy_config", {partitionId: platform});
// 未启用代理或配置无效时,使用系统代理设置
if (!config || config.proxyStatus !== 'true' || !config.proxyIp || !config.proxyPort) {
await view.webContents.session.setProxy({ mode: 'system' });
logger.info(`[${partitionId}] Proxy: 使用系统代理设置`);
return;
}
let proxyRules;
const server = `${config.proxyIp}:${config.proxyPort}`;
switch (config.proxyType) {
case 'http':
proxyRules = `http://${server}`;
break;
case 'https':
// 注意Electron 的 proxyRules 不直接区分 http 和 https 协议,
// 它们都使用 'http=' 或 'https=' 前缀指定代理服务器。
// 通常代理服务器本身会处理目标 URL 的协议。
// 这里我们统一使用 http 规则,代理服务器需能处理 https 流量。
// 如果需要为 https 单独指定代理,可以写成 `https=${credentials}${server}` 或组合规则。
proxyRules = `http://${server}`; // 或者根据需要指定为 https=...
logger.warn(`[${partitionId}] Proxy: HTTPS 代理将通过 HTTP 规则 (${proxyRules}) 进行。请确保代理服务器支持。`);
break;
case 'socks4':
// SOCKS5 代理通常不需要用户名/密码在 URL 中Electron 会通过其他机制处理(如果需要)
// 但如果代理服务器实现要求在 URL 中包含,可以按需添加 credentials
// Electron 文档倾向于 socks5=host:port 格式
proxyRules = `socks4=socks4://${server}`;
break;
case 'socks5':
// SOCKS5 代理通常不需要用户名/密码在 URL 中Electron 会通过其他机制处理(如果需要)
// 但如果代理服务器实现要求在 URL 中包含,可以按需添加 credentials
// Electron 文档倾向于 socks5=host:port 格式
proxyRules = `socks5=socks5://${server}`;
break;
default:
logger.error(`[${partitionId}] Proxy: 不支持的代理类型 ${config.proxyType}`);
await view.webContents.session.setProxy({ mode: 'system' }); // 回退到系统代理
return;
}
const proxyConfig = {
mode: 'fixed_servers',
proxyRules: proxyRules,
// proxyBypassRules: '<local>' // 可选:绕过本地地址的代理
};
// 应用代理配置
await view.webContents.session.setProxy(proxyConfig);
logger.info(`[${partitionId}] Proxy: 应用代理配置成功: ${JSON.stringify(proxyConfig)}`);
// 代理认证处理(如果需要更精细的控制)
// 注意Electron 的 setProxy 通常会自动处理基于 URL 的凭据,但对于某些 SOCKS 或需要 NTLM/Kerberos 的场景,
// 可能需要监听 'login' 事件进行手动认证。
// 监听登录事件以处理代理身份验证
if (config.userVerifyStatus === 'true' && config.username && config.password) {
view.webContents.on('login', (event, request, authInfo, callback) => {
event.preventDefault();
if (authInfo.isProxy && authInfo.host === config.proxyIp && authInfo.port === Number(config.proxyPort)) {
callback(config.username, config.password);
logger.info(`会话${partitionId} 触发login事件`);
}
});
}
// 禁止打开新窗口 (这个逻辑似乎与代理配置不直接相关,但保留在这里)
view.webContents.setWindowOpenHandler((detail) => {
const url = detail.url;
logger.info(`[${partitionId}] WindowOpenHandler: 尝试打开新窗口 ${url}, 将使用外部浏览器打开。`)
// 调用系统默认浏览器打开
shell.openExternal(url);
// 拒绝 Electron 创建新窗口
return { action: 'deny' };
});
} catch (err) {
logger.error(`[${partitionId}] 配置代理出错:`, err);
try {
// 尝试回退到系统代理
await view.webContents.session.setProxy({ mode: 'system' });
logger.info(`[${partitionId}] Proxy: 因配置错误,已切换至系统代理设置`);
} catch (fallbackErr) {
logger.error(`[${partitionId}] Proxy: 回退到系统代理失败:`, fallbackErr);
}
}
}
generateUserAgent() {
const chromeVersions = ['114.0.0.0', '115.0.0.0', '116.0.0.0', '117.0.0.0', '118.0.0.0', '119.0.0.0'];
const platforms = {
win: {
os: 'Windows NT 10.0; Win64; x64',
platform: 'Windows'
},
mac: {
os: 'Macintosh; Intel Mac OS X 10_15_7',
platform: 'macOS'
},
linux: {
os: 'X11; Linux x86_64',
platform: 'Linux'
}
};
// 通过 Node.js 的 os 模块检测系统类型
const platformMap = {
win32: 'win',
darwin: 'mac',
linux: 'linux'
};
const currentPlatform = os.platform();
const platformKey = platformMap[currentPlatform] || 'win32';
// 获取对应平台的配置信息
const selectedPlatform = platforms[platformKey];
const randomChrome = chromeVersions[Math.floor(Math.random() * chromeVersions.length)];
return `Mozilla/5.0 (${selectedPlatform.os}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${randomChrome} Safari/537.36`;
}
_getTimeFormat() {
const now = new Date();
// 补零函数(确保两位数字)
const padZero = (n) => String(n).padStart(2, "0");
// 提取时间组件
const month = padZero(now.getMonth() + 1), // 月份从 0 开始
day = padZero(now.getDate()),
hours = padZero(now.getHours()),
minutes = padZero(now.getMinutes());
// 拼接目标格式
return `${month}-${day} ${hours}:${minutes}`;
}
//删除会话相关的所有数据信息
async _deleteSessionAllData (partitionId) {
//会话记录表数据删除
await app.sdb.delete('session_list',{partitionId:partitionId});
//删除会话相关的翻译缓存
await app.sdb.delete('translate_cache',{partitionId:partitionId});
}
}
WindowService.toString = () => '[class WindowService]';
module.exports = {
WindowService,
windowService: new WindowService()
};

32
electron/updater.js Normal file
View File

@ -0,0 +1,32 @@
const { autoUpdater } = require('electron-updater');
function initUpdater() {
autoUpdater.autoDownload = false;
autoUpdater.on('checking-for-update', () => {
console.log('🟡 正在检查更新...');
});
autoUpdater.on('update-available', (info) => {
console.log('🟢 有新版本可用:', info.version);
autoUpdater.downloadUpdate();
});
autoUpdater.on('update-not-available', () => {
console.log('✅ 当前已经是最新版本');
});
autoUpdater.on('update-downloaded', () => {
console.log('📦 更新已下载完毕,准备退出并安装');
autoUpdater.quitAndInstall();
});
autoUpdater.on('error', (err) => {
console.error('❌ 自动更新出错:', err);
});
// 启动更新检查
autoUpdater.checkForUpdates();
}
module.exports = { initUpdater };

View File

@ -0,0 +1,60 @@
const { app } = require('electron');
// 生成随机字符串的纯函数
function generateRandomString(length = 8) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
return Array.from({ length }, () => {
const randomIndex = Math.floor(Math.random() * chars.length);
return chars[randomIndex];
}).join('');
}
// 生成唯一分区ID的主函数
async function generateUniquePartitionId(options = {}) {
const {
length = 8,
maxRetry = 10,
tableName = 'session_list',
idField = 'partitionId'
} = options;
let retryCount = 0;
while (retryCount < maxRetry) {
const partitionId = generateRandomString(length);
try {
const existing = await app.sdb.selectOne(tableName, { [idField]: partitionId });
if (!existing) return partitionId;
retryCount++;
} catch (error) {
throw new Error(`Database query failed: ${error.message}`);
}
}
throw new Error(`Failed to generate unique ID after ${maxRetry} attempts`);
}
// 获取当前时间字符串
const getTimeStr = (date = new Date())=> {
const pad = n => n.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ` +
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
const timestampToString = (timestamp) => {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
// 导出函数
module.exports = {
timestampToString,
generateUniquePartitionId,
getTimeStr,
generateRandomString // 可选导出基础生成函数
};

View File

@ -0,0 +1,169 @@
const {SqliteStorage,Database} = require('ee-core/storage');
const { logger } = require('ee-core/log');
class DatabaseUtils {
constructor() {
const storage = new SqliteStorage("session.db");
this.db = storage.db;
}
// 创建表
createTable(tableName, columns, constraints = []) {
// 定义字段
const columnsDefinition = Object.entries(columns)
.map(([name, type]) => `${name} ${type}`)
.join(', ');
// 拼接字段定义和约束定义
const tableDefinition = [columnsDefinition, ...constraints].join(', ');
// 构建 SQL
const sql = `CREATE TABLE IF NOT EXISTS ${tableName} (${tableDefinition})`;
try {
// 执行创建表
this.db.prepare(sql).run();
logger.info(`${tableName} 创建成功`);
} catch (error) {
logger.error(`创建表 ${tableName} 时出错:`, error.message);
}
}
// 插入数据
insert(tableName, data) {
const columns = Object.keys(data).join(', ');
const placeholders = Object.keys(data).map(() => '?').join(', ');
const values = Object.values(data);
const sql = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders})`;
const stmt = this.db.prepare(sql);
const info = stmt.run(values);
logger.info(`数据插入成功行ID为 ${info.lastInsertRowid}`);
return info.lastInsertRowid;
}
// 查询数据
select(tableName, conditions = {}) {
let sql = `SELECT * FROM ${tableName}`;
const keys = Object.keys(conditions);
const values = Object.values(conditions);
if (keys.length > 0) {
const whereClause = keys.map((key) => `${key} = ?`).join(' AND ');
sql += ` WHERE ${whereClause}`;
}
const stmt = this.db.prepare(sql);
const rows = stmt.all(...values);
return rows;
}
// 查询单条数据
selectOne(tableName, conditions = {}) {
let sql = `SELECT * FROM ${tableName}`;
const keys = Object.keys(conditions);
const values = Object.values(conditions);
if (keys.length > 0) {
const whereClause = keys.map((key) => `${key} = ?`).join(' AND ');
sql += ` WHERE ${whereClause}`;
}
const stmt = this.db.prepare(sql);
const row = stmt.get(...values);
return row;
}
// 更新数据
update(tableName, data, conditions = {}) {
const updates = Object.keys(data).map((key) => `${key} = ?`).join(', ');
const values = [...Object.values(data)];
let whereClause = '';
if (Object.keys(conditions).length > 0) {
whereClause = 'WHERE ' + Object.keys(conditions)
.map((key) => `${key} = ?`)
.join(' AND ');
values.push(...Object.values(conditions));
}
const sql = `UPDATE ${tableName} SET ${updates} ${whereClause}`;
const stmt = this.db.prepare(sql);
const info = stmt.run(...values);
// logger.info(`更新成功,受影响的行数为 ${info.changes}`);
return info.changes;
}
// 删除数据
delete(tableName, conditions) {
const whereClause = Object.keys(conditions)
.map((key) => `${key} = ?`)
.join(' AND ');
const values = Object.values(conditions);
const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`;
const stmt = this.db.prepare(sql);
const info = stmt.run(...values);
logger.info(`删除成功,受影响的行数为 ${info.changes}`);
return info.changes;
}
// 获取现有表的字段信息
getExistingColumns(tableName) {
try {
const result = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
if (result.length === 0) {
return null;
}
const columns = {};
result.forEach(row => {
columns[row.name] = row.type;
});
return columns;
} catch (error) {
logger.error(`Failed to fetch columns for table ${tableName}:`, error);
return null;
}
}
// 更新表的字段(增加或删除)
updateTableColumns(tableName, definedColumns, existingColumns) {
// 添加不存在的字段
for (const [columnName, columnType] of Object.entries(definedColumns)) {
if (!(columnName in existingColumns)) {
const sql = `ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${columnType}`;
this.db.prepare(sql).run();
logger.info(`Added column ${columnName} to table ${tableName}.`);
}
}
// 删除多余的字段
for (const existingColumn of Object.keys(existingColumns)) {
if (!(existingColumn in definedColumns)) {
logger.warn(`Column ${existingColumn} exists in ${tableName} but is not defined in schema. Consider removing it manually if not needed.`);
// SQLite 不支持直接删除列,通常建议创建一个新表然后迁移数据
}
}
}
// 同步表结构
async syncTableStructure(tableName, definedColumns, constraints = []) {
const existingColumns = this.getExistingColumns(tableName);
if (existingColumns === null) {
// 表不存在,直接创建
await this.createTable(tableName, definedColumns, constraints);
logger.info(`${tableName} 不存在,已自动创建`);
} else {
// 表已存在,检查字段并更新
await this.updateTableColumns(tableName, definedColumns, existingColumns);
}
}
// 关闭数据库
close() {
this.db.close();
logger.info('数据库连接已关闭');
}
}
module.exports = DatabaseUtils;

View File

@ -0,0 +1,2 @@
VITE_TITLE=""
VITE_GO_URL="http://localhost:8081"

2
frontend/.env.production Normal file
View File

@ -0,0 +1,2 @@
VITE_TITLE=""
VITE_GO_URL="http://www.test.com"

6
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
package-lock.json

106
frontend/index.html Normal file
View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0, minimum-scale=1.0" />
<title></title>
<!-- 优化vue渲染未完成之前先加一个css动画 -->
<style>
#loadingPage {
background-color: #dedede;
font-size: 12px;
}
.base {
height: 9em;
left: 50%;
margin: -7.5em;
padding: 3em;
position: absolute;
top: 50%;
width: 9em;
transform: rotateX(45deg) rotateZ(45deg);
transform-style: preserve-3d;
}
.cube,
.cube:after,
.cube:before {
content: '';
float: left;
height: 3em;
position: absolute;
width: 3em;
}
/* Top */
.cube {
background-color: #06cf68;
position: relative;
transform: translateZ(3em);
transform-style: preserve-3d;
transition: .25s;
box-shadow: 13em 13em 1.5em rgba(0, 0, 0, 0.1);
animation: anim 1s infinite;
}
.cube:after {
background-color: #05a151;
transform: rotateX(-90deg) translateY(3em);
transform-origin: 100% 100%;
}
.cube:before {
background-color: #026934;
transform: rotateY(90deg) translateX(3em);
transform-origin: 100% 0;
}
.cube:nth-child(1) {
animation-delay: 0.05s;
}
.cube:nth-child(2) {
animation-delay: 0.1s;
}
.cube:nth-child(3) {
animation-delay: 0.15s;
}
.cube:nth-child(4) {
animation-delay: 0.2s;
}
.cube:nth-child(5) {
animation-delay: 0.25s;
}
.cube:nth-child(6) {
animation-delay: 0.3s;
}
.cube:nth-child(7) {
animation-delay: 0.35s;
}
.cube:nth-child(8) {
animation-delay: 0.4s;
}
.cube:nth-child(9) {
animation-delay: 0.45s;
}
@keyframes anim {
50% {
transform: translateZ(0.5em);
}
}
</style>
</head>
<body style="padding: 0; margin: 0;">
<div id="loadingPage">
<div class='base'>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
<div class='cube'></div>
</div>
</div>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

29
frontend/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "ee",
"version": "4.0.0",
"scripts": {
"dev": "vite --host --port 8080",
"serve": "vite --host --port 8080",
"build-staging": "vite build --mode staging",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"element-plus": "^2.8.6",
"lodash-es": "^4.17.21",
"pinia": "^2.2.6",
"qrcode.vue": "^3.6.0",
"vue": "^3.5.12",
"vue-i18n": "^9.0.0",
"vue-router": "^4.0.14"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"@vue/compiler-sfc": "^3.2.33",
"less": "^4.1.2",
"less-loader": "^10.2.0",
"terser": "^5.19.1",
"vite": "^5.4.11"
}
}

101
frontend/src/App.vue Normal file
View File

@ -0,0 +1,101 @@
<template>
<div id="app">
<div class="header">
<div class="left">
</div>
<div class="center">
<el-text>{{barTitle}}</el-text>
</div>
<div class="right">
<el-button v-if="appPlatform !== 'darwin'" size="small" :icon="Minus" @click="winControl('minimize')"/>
<el-button v-if="appPlatform !== 'darwin'" size="small" :icon="FullScreen" @click="winControl('toggle-fullscreen')" />
<el-button v-if="appPlatform !== 'darwin'" size="small" :icon="Close" @click="winControl('close')" />
</div>
</div>
<div>
<router-view/>
</div>
</div>
</template>
<script setup>
import {onMounted, ref, watch} from 'vue';
import {Close, FullScreen, Minus} from "@element-plus/icons-vue";
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import { useMenuStore } from '@/stores/menuStore';
const menuStore = useMenuStore();
const barTitle = ref('');
const appPlatform = ref('');
onMounted(async () => {
const loadingElement = document.getElementById('loadingPage');
if (loadingElement) {
loadingElement.remove();
}
const res = await ipc.invoke(ipcApiRoute.getSystemInfo, {});
const {name,platform,version} = res;
barTitle.value = name + ` v${version}`;
appPlatform.value = platform;
});
const winControl = async (action)=>{
await ipc.send('window-control', {action});
}
onMounted(async () => {
const res = await ipc.invoke(ipcApiRoute.getRouteList, {});
if (res.status) {
menuStore.setTranslationRoute(res.data)
}
})
import { useDark } from '@vueuse/core'
const isDark = useDark()
// 监听 isDark 的变化
watch(isDark, (newVal) => {
if (newVal===true) {
ipc.invoke('theme-change', {theme:'dark'});
}
if (newVal===false) {
ipc.invoke('theme-change', {theme:'light'});
}
},{immediate:true})
</script>
<style lang="less">
.header{
display: flex;
-webkit-app-region: drag;
height: 30px;
/* 三等分核心代码 */
.left,
.center,
.right {
height: 30px;
flex: 1; /* 关键属性:均分剩余空间 */
display: flex; // 必须声明为 flex 容器
align-items: center; // 垂直居中(核心)
min-width: 0; /* 防止内容溢出破坏布局 */
}
/* 可选:内容对齐 */
.left {
justify-content: flex-start; /* 左对齐 */
}
.center {
user-select: none; /* 禁止文本选择 */
font-size: 12px;
font-weight: bold;
justify-content: center; /* 居中对齐 */
}
.right {
margin-right: 5px;
justify-content: flex-end; // 水平靠右
gap: 5px; // 按钮间距(可选)
.el-button {
-webkit-app-region: no-drag;
margin-left: 0;
height: 20px;
width: 20px
}
}
}
</style>

74
frontend/src/api/index.js Normal file
View File

@ -0,0 +1,74 @@
/**
* 主进程与渲染进程通信频道定义
* Definition of communication channels between main process and rendering process
*/
const ipcApiRoute = {
getSystemInfo: 'controller/system/getBaseInfo',
login: 'controller/system/login',
logOut: 'controller/system/logOut',
userLogin: 'controller/system/userLogin',
createSub: 'controller/system/createSub',
listSub: 'controller/system/listSub',
deleteSub: 'controller/system/deleteSub',
//session会话相关
getSessions: 'controller/window/getSessions',
getSessionByPartitionId: 'controller/window/getSessionByPartitionId',
addSession: 'controller/window/addSession',
editSession: 'controller/window/editSession',
startSession: 'controller/window/startSession',
setWindowLocation: 'controller/window/setWindowLocation',
hiddenSession: 'controller/window/hiddenSession',
showSession: 'controller/window/showSession',
closeSession: 'controller/window/closeSession',
refreshSession: 'controller/window/refreshSession',
deleteSession: 'controller/window/deleteSession',
// 代理配置相关
getProxyInfo: 'controller/window/getProxyInfo',
editProxyInfo: 'controller/window/editProxyInfo',
saveProxyInfo: 'controller/window/saveProxyInfo',
openSessionDevTools: 'controller/window/openSessionDevTools',
//翻译相关
getTrsConfig: 'controller/translate/getConfigInfo',
updateTranslateConfig: 'controller/translate/updateTranslateConfig',
changeAloneStatus: 'controller/translate/changeAloneStatus',
getLanguageList: 'controller/translate/getLanguageList',
addLanguage: 'controller/translate/addLanguage',
deleteLanguage: 'controller/translate/deleteLanguage',
editLanguage: 'controller/translate/editLanguage',
addTranslateRoute: 'controller/translate/addTranslateRoute',
editTranslateRoute: 'controller/translate/editTranslateRoute',
getRouteConfig: 'controller/translate/getRouteConfig',
getRouteList: 'controller/translate/getRouteList',
testRoute: 'controller/translate/testRoute',
translateText: 'controller/translate/translateText',
//联系人信息相关
getContactInfo: 'controller/contactInfo/getContactInfo',
updateContactInfo: 'controller/contactInfo/updateContactInfo',
getFollowRecord: 'controller/contactInfo/getFollowRecord',
addFollowRecord: 'controller/contactInfo/addFollowRecord',
updateFollowRecord: 'controller/contactInfo/updateFollowRecord',
deleteFollowRecord: 'controller/contactInfo/deleteFollowRecord',
//快捷回复相关
getGroups: 'controller/quickreply/getGroups',
getContentByGroupId: 'controller/quickreply/getContentByGroupId',
addGroup: 'controller/quickreply/addGroup',
editGroup: 'controller/quickreply/editGroup',
deleteGroup: 'controller/quickreply/deleteGroup',
addReply: 'controller/quickreply/addReply',
editReply: 'controller/quickreply/editReply',
deleteReply: 'controller/quickreply/deleteReply',
deleteAllReply: 'controller/quickreply/deleteAllReply',
}
export {
ipcApiRoute
}

View File

@ -0,0 +1,8 @@
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
height: 100%;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -0,0 +1,99 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { ref, computed } from 'vue';
// 定义props
const props = defineProps({
size: {
type: Number,
default: 32
}
});
const { locale } = useI18n();
const currentLanguage = ref(locale.value);
// 计算样式
const switchStyle = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
}));
const fontSize = computed(() => ({
fontSize: `${Math.max(props.size * 0.4375, 12)}px` // 保持字体大小为容器大小的0.4375倍最小12px
}));
const handleLanguageChange = (lang) => {
locale.value = lang;
currentLanguage.value = lang;
};
</script>
<template>
<div class="action-item language-switch">
<el-dropdown @command="handleLanguageChange" trigger="click">
<div class="switch-content" :style="switchStyle">
<span class="language-text" :style="fontSize">
{{ currentLanguage === 'zh' ? '中' : currentLanguage === 'en' ? 'En' : 'ខ្មែរ' }}
</span>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="zh">中文</el-dropdown-item>
<el-dropdown-item command="en">English</el-dropdown-item>
<el-dropdown-item command="km"></el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<style scoped>
.language-switch {
display: flex;
align-items: center;
padding: 0;
transition: all 0.3s;
}
.switch-content {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: var(--el-fill-color-light);
border-radius: 50%;
transition: all 0.3s;
}
.switch-content:hover {
transform: translateY(-1px);
background: var(--el-fill-color);
}
.language-text {
font-weight: 500;
color: var(--el-text-color-regular);
}
:deep(.el-dropdown-menu) {
border-radius: 8px;
padding: 4px;
background: var(--el-bg-color-overlay);
border: 1px solid var(--el-border-color-light);
min-width: 100px;
}
:deep(.el-dropdown-menu__item) {
border-radius: 4px;
padding: 8px 16px;
font-size: 14px;
color: var(--el-text-color-regular);
text-align: center;
}
:deep(.el-dropdown-menu__item:hover) {
background-color: var(--el-fill-color-light);
color: var(--el-color-primary);
}
</style>

View File

@ -0,0 +1,62 @@
<script setup>
import { Sunny, Moon } from '@element-plus/icons-vue'
import { useDark } from '@vueuse/core'
import { computed } from 'vue'
// 定义props
const props = defineProps({
size: {
type: Number,
default: 32
}
});
const isDark = useDark()
// 计算样式
const switchStyle = computed(() => ({
width: `${props.size}px`,
height: `${props.size}px`,
}));
const iconSize = computed(() => ({
fontSize: `${Math.max(props.size * 0.5625, 14)}px` // 保持图标大小为容器大小的0.5625倍最小14px
}));
</script>
<template>
<div class="theme-switch">
<div class="switch-content" :style="switchStyle">
<el-icon v-if="isDark" @click="isDark = false" :style="iconSize"><Sunny /></el-icon>
<el-icon v-else @click="isDark = true" :style="iconSize"><Moon /></el-icon>
</div>
</div>
</template>
<style scoped>
.theme-switch {
display: flex;
align-items: center;
padding: 0;
transition: all 0.3s;
}
.switch-content {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: var(--el-fill-color-light);
border-radius: 50%;
transition: all 0.3s;
}
.switch-content:hover {
transform: translateY(-1px);
background: var(--el-fill-color);
}
:deep(.el-icon) {
color: var(--el-text-color-regular);
}
</style>

View File

@ -0,0 +1,10 @@
const modules = import.meta.glob('./*.vue', { eager: true })
const map = {}
Object.keys(modules).forEach(file => {
const modulesName = file.replace('./', '').replace('.vue', '')
map[modulesName] = modules[file].default
})
const globalComponents = {
...map,
}
export default globalComponents

View File

@ -0,0 +1,14 @@
<template>
<svg t="1740650326413" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8806" :width="size" :height="size"><path d="M954.368 920.832l-60.586667-349.269333h20.48c16.440889 0 29.696-13.255111 29.696-29.724445v-219.420444c0-16.440889-13.255111-29.724444-29.696-29.724445h-281.144889V84.707556c0-16.469333-13.255111-29.724444-29.724444-29.724445h-182.869333c-16.440889 0-29.696 13.255111-29.696 29.724445v208.014222H109.681778c-16.469333 0-29.724444 13.255111-29.724445 29.696v219.420444c0 16.469333 13.255111 29.724444 29.724445 29.724445h20.48l-60.586667 349.269333a29.667556 29.667556 0 0 0 29.240889 34.730667h826.311111a29.582222 29.582222 0 0 0 29.269333-34.730667zM159.971556 372.707556h310.840888V134.997333h82.289778v237.710223h310.869334v118.869333H159.971556v-118.869333z m534.840888 502.869333v-178.289778a9.159111 9.159111 0 0 0-9.130666-9.159111h-54.869334a9.159111 9.159111 0 0 0-9.130666 9.159111v178.289778h-219.420445v-178.289778a9.159111 9.159111 0 0 0-9.159111-9.159111h-54.840889a9.159111 9.159111 0 0 0-9.159111 9.159111v178.289778H158.606222l51.541334-297.159111h603.534222l51.541333 297.159111h-170.382222z" :fill="color" p-id="8807"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

View File

@ -0,0 +1,14 @@
<template>
<svg t="1740504451872" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6396" :width="size" :height="size"><path d="M0 51.2m38.4 0l947.2 0q38.4 0 38.4 38.4l0 0q0 38.4-38.4 38.4l-947.2 0q-38.4 0-38.4-38.4l0 0q0-38.4 38.4-38.4Z" :fill="color" p-id="6397"></path><path d="M460.8 332.8m38.4 0l486.4 0q38.4 0 38.4 38.4l0 0q0 38.4-38.4 38.4l-486.4 0q-38.4 0-38.4-38.4l0 0q0-38.4 38.4-38.4Z" :fill="color" p-id="6398"></path><path d="M460.8 614.4m38.4 0l486.4 0q38.4 0 38.4 38.4l0 0q0 38.4-38.4 38.4l-486.4 0q-38.4 0-38.4-38.4l0 0q0-38.4 38.4-38.4Z" :fill="color" p-id="6399"></path><path d="M0 896m38.4 0l947.2 0q38.4 0 38.4 38.4l0 0q0 38.4-38.4 38.4l-947.2 0q-38.4 0-38.4-38.4l0 0q0-38.4 38.4-38.4Z" :fill="color" p-id="6400"></path><path d="M20.1984 489.1136l275.5584-137.7792a25.6 25.6 0 0 1 37.0432 22.8864v275.5584a25.6 25.6 0 0 1-37.0432 22.8864L20.1984 534.8864a25.6 25.6 0 0 1 0-45.7728z" :fill="color" p-id="6401"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

View File

@ -0,0 +1,14 @@
<template>
<svg t="1740504526624" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6566" :width="size" :height="size"><path d="M985.6 51.2h-947.2a38.4 38.4 0 0 0 0 76.8h947.2a38.4 38.4 0 0 0 0-76.8z m-460.8 281.6h-486.4a38.4 38.4 0 0 0 0 76.8h486.4a38.4 38.4 0 0 0 0-76.8z m0 281.6h-486.4a38.4 38.4 0 0 0 0 76.8h486.4a38.4 38.4 0 0 0 0-76.8z m460.8 281.6h-947.2a38.4 38.4 0 0 0 0 76.8h947.2a38.4 38.4 0 0 0 0-76.8z m18.2016-406.8864l-275.5584-137.7792a25.6 25.6 0 0 0-37.0432 22.8864v275.5584a25.6 25.6 0 0 0 37.0432 22.8864l275.5584-137.7792a25.6 25.6 0 0 0 0-45.7728z" :fill="color" p-id="6567"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

View File

@ -0,0 +1,15 @@
<template>
<svg t="1740397464506" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12850" :width="size" :height="size"><path d="M154 283.788C229.456 165.47 361.86 87 512.594 87c164.43 0 307.05 93.379 377.724 230H512.594c-87.46 0-161.47 57.577-186.196 136.894L154 283.788z" fill="#DD5044" p-id="12851"></path><path d="M494.83 936.635l0.476-0.989-0.569 0.986-0.54-0.023 61.103-234.3C642.455 682.836 707.594 605.025 707.594 512c0-107.56-87.085-194.78-194.594-195h377.318c30.212 58.403 47.276 124.709 47.276 195 0 234.721-190.28 425-425 425-5.95 0-11.873-0.122-17.764-0.365z" fill="#FFCD41" p-id="12852"></path><path d="M491.977 936.509C266.83 925.755 87.594 739.809 87.594 512c0-70.105 16.974-136.246 47.037-194.537l17.04-29.981c0.77-1.236 1.546-2.467 2.329-3.694l172.398 170.106c-5.722 18.353-8.804 37.87-8.804 58.106 0 107.696 87.304 195 195 195 14.67 0 28.962-1.62 42.707-4.69l-58.797 225.454L494 935l-2.023 1.509z" fill="#17A05D" p-id="12853"></path><path d="M494.747 936.615l-0.545-0.023 61.074-234.187c53.77-11.885 99.2-45.966 126.192-92.14l1.273 0.735-187.994 325.615zM154.373 283.84l0.033-0.052 172.398 170.106C321.083 472.247 318 491.764 318 512c0 35.834 9.666 69.41 26.532 98.265l-1.273 0.735-188.886-327.16zM890.723 317l0.035 0.067-233.125 64.14C621.951 341.774 570.37 317 513 317h377.724z" opacity=".047" p-id="12854"></path><path d="M317.594 512a195 195 0 1 0 390 0 195 195 0 1 0-390 0z" fill="#FFFFFF" p-id="12855"></path><path d="M357.594 512a155 155 0 1 0 310 0 155 155 0 1 0-310 0z" :fill="color" p-id="12856"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

View File

@ -0,0 +1,15 @@
<template>
<svg t="1740822264894" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="806" :width="size" :height="size"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#00BAFF" p-id="807"></path><path d="M209.934 436.17L742.86 969.096C909.648 884.69 1024 711.708 1024 512c0-35.338-3.58-69.84-10.398-103.162l-195.928-195.93-607.74 223.262z" fill="#008CC3" p-id="808"></path><path d="M205.844 922.416C291.236 986.218 397.206 1024 512 1024c119.654 0 229.716-41.054 316.884-109.834L700.5 648h-377l-117.656 274.416z" fill="#E0E0E0" p-id="809"></path><path d="M828.884 914.166L700.5 648h-188.596v376H512c119.654 0 229.716-41.054 316.884-109.834z" fill="#C0C0C0" p-id="810"></path><path d="M231.416 442.482c-21.954 0-39.75-17.796-39.75-39.75v-158.998c0-21.954 17.796-39.75 39.75-39.75h119.25c32.756 0 51.454 37.396 31.8 63.6l-119.25 158.998a39.748 39.748 0 0 1-31.8 15.9z" fill="#E0E0E0" p-id="811"></path><path d="M792.584 442.484c21.954 0 39.75-17.796 39.75-39.75v-158.998c0-21.954-17.796-39.75-39.75-39.75h-119.25c-32.756 0-51.454 37.394-31.8 63.6l119.25 158.998a39.754 39.754 0 0 0 31.8 15.9z" fill="#C0C0C0" p-id="812"></path><path d="M512 773.172c-135.834 0-245.95-110.116-245.95-245.95v-92.32c0-135.834 110.116-245.95 245.95-245.95s245.95 110.116 245.95 245.95v92.32c0 135.834-110.116 245.95-245.95 245.95z" fill="#FFFFFF" p-id="813"></path><path d="M512 188.952l-0.096 0.002v584.218l0.096 0.002c135.834 0 245.95-110.116 245.95-245.95v-92.32c0-135.836-110.116-245.952-245.95-245.952z" fill="#E0E0E0" p-id="814"></path><path d="M607.712 208.27c-28.826 41.844-49.656 151.426-49.656 183.398-0.002 39.792 32.258 72.05 72.05 72.05 29.108 0 94.46-17.27 127.482-42.114-5.12-96.144-65.454-177.636-149.876-213.334zM262.234 790.896c56.516 68.576 147.332 113.068 249.766 113.068 105.368 0 198.468-47.06 254.562-119.004l-29.984-62.166c-56.514 53.098-136.228 86.192-224.576 86.192-86.87 0-165.37-32.01-221.698-83.556l-28.07 65.466z" fill="#B3B3B3" p-id="815"></path><path d="M736.576 722.794c-56.514 53.098-136.228 86.192-224.576 86.192h-0.096v94.976l0.096 0.002c105.368 0 198.468-47.06 254.562-119.004l-29.986-62.166z" fill="#999999" p-id="816"></path><path d="M588.232 565.444c0 16.606-13.51 30.116-30.118 30.116-16.606 0-30.116-13.51-30.116-30.116v-46.698l36.936-36.98c12.244-12.26 3.574-33.204-13.744-33.194l-78.384 0.046c-17.316 0.01-25.988 20.964-13.744 33.21L496 518.766v46.68c0 16.606-13.51 30.116-30.116 30.116s-30.116-13.51-30.116-30.116h-32c0 34.25 27.866 62.116 62.116 62.116 18.28 0 34.738-7.936 46.116-20.544 11.376 12.608 27.836 20.544 46.116 20.544 34.252 0 62.118-27.866 62.118-62.116h-32.002v-0.002z" fill="#333333" p-id="817"></path><path d="M588.232 565.444c0 16.606-13.51 30.116-30.118 30.116-16.606 0-30.116-13.51-30.116-30.116v-46.698l36.936-36.98c12.244-12.26 3.574-33.204-13.744-33.194l-40.478 0.024v159.774c0.43-0.45 0.868-0.892 1.286-1.354 11.376 12.608 27.836 20.544 46.116 20.544 34.252 0 62.118-27.866 62.118-62.116h-32z" fill="#1A1A1A" p-id="818"></path><path d="M395.42 391.68m-22.826 0a22.826 22.826 0 1 0 45.652 0 22.826 22.826 0 1 0-45.652 0Z" fill="#333333" p-id="819"></path><path d="M628.58 391.68m-22.826 0a22.826 22.826 0 1 0 45.652 0 22.826 22.826 0 1 0-45.652 0Z" fill="#1A1A1A" p-id="820"></path><path d="M512 992l-68.876-68.876L512 854.218l68.878 68.876z" fill="#FFB344" p-id="821"></path><path d="M580.876 923.094L512 854.218l-0.096 0.096v137.59L512 992z" :fill="color" p-id="822"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

View File

@ -0,0 +1,15 @@
<template>
<svg t="1740504035865" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5036" :width="size" :height="size"><path d="M661.418667 853.546667A278.272 278.272 0 0 1 512 1002.965333a278.314667 278.314667 0 0 1-149.418667-149.418666h96.426667c13.909333 20.821333 31.872 38.912 52.992 53.034666 21.12-14.08 39.082667-32.213333 52.992-53.034666h96.426667zM768 631.850667l85.333333 96.810666v82.218667H170.666667v-82.218667l85.333333-96.810666V384.213333c0-148.608 106.837333-275.072 256-321.92 149.162667 46.848 256 173.312 256 321.92v247.637334z m-31.146667 93.696L682.666667 664.106667v-279.893334c0-98.901333-66.986667-189.013333-170.666667-231.296-103.68 42.24-170.666667 132.394667-170.666667 231.253334v279.936l-54.186666 61.44h449.706666z m-224.853333-256a85.333333 85.333333 0 1 1 0-170.666667 85.333333 85.333333 0 0 1 0 170.666667z" :fill="color" p-id="5037"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

View File

@ -0,0 +1,14 @@
<template>
<svg t="1740503946498" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="743" :width="size" :height="size"><path d="M864 138.666667v768h-704v-768h704z m-64 533.333333h-576v170.666667h576v-170.666667zM704 725.333333v64h-128v-64h128z m96-288h-576v170.666667h576v-170.666667zM704 490.666667v64h-128v-64h128z m96-288h-576v170.666666h576v-170.666666zM704 256v64h-128v-64h128z" :fill="color" p-id="744"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

View File

@ -0,0 +1,15 @@
<template>
<svg t="1740395779028" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3030" :width="size" :height="size"><path d="M512 66.4C264.6 66.4 64 265.9 64 512c0 246.1 200.6 445.6 448 445.6S960 758.1 960 512c0-246.1-200.6-445.6-448-445.6z m232.1 243.3l-80.7 420.5c-5.6 29.9-22 37.1-44.5 23.3L496.1 653.1l-59 63.3c-6.8 7.6-12.5 13.8-24.6 13.8l8.3-138.9 227.8-227.2c10.1-10.2-2.1-15.1-15.4-6.2L351.9 554.6l-121.6-42.7c-26.1-8.2-26.4-28.2 5.9-42.7L709.9 267c21.7-10.8 42.4 5.9 34.2 42.7z" :fill="color" p-id="3031"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,14 @@
<template>
<svg t="1740503978968" class="icon" viewBox="0 0 1204 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1073" width="128" height="128"><path d="M90.352941 873.411765a60.235294 60.235294 0 0 0 60.235294 60.235294h632.470589a30.117647 30.117647 0 0 1 30.117647 30.117647v30.117647a30.117647 30.117647 0 0 1-30.117647 30.117647H120.470588a120.470588 120.470588 0 0 1-120.470588-120.470588V120.470588a120.470588 120.470588 0 0 1 120.470588-120.470588h783.058824a120.470588 120.470588 0 0 1 120.470588 120.470588v150.588236a30.117647 30.117647 0 0 1-30.117647 30.117647h-30.117647a30.117647 30.117647 0 0 1-30.117647-30.117647V150.588235a60.235294 60.235294 0 0 0-60.235294-60.235294H150.588235a60.235294 60.235294 0 0 0-60.235294 60.235294v722.82353z m843.294118-331.294118v-90.352941a30.117647 30.117647 0 0 1 30.117647-30.117647h30.117647a30.117647 30.117647 0 0 1 30.117647 30.117647v90.352941h150.588235a30.117647 30.117647 0 0 1 30.117647 30.117647v240.941177a30.117647 30.117647 0 0 1-30.117647 30.117647h-150.588235v150.588235a30.117647 30.117647 0 0 1-30.117647 30.117647h-30.117647a30.117647 30.117647 0 0 1-30.117647-30.117647v-150.588235h-150.588235a30.117647 30.117647 0 0 1-30.117648-30.117647v-240.941177a30.117647 30.117647 0 0 1 30.117648-30.117647h150.588235z m0 90.352941h-90.352941v120.470588h90.352941v-120.470588z m90.352941 0v120.470588h90.352941v-120.470588h-90.352941z m-613.496471-120.470588h82.522353L451.764706 376.470588 410.503529 512z m-27.497411 90.352941l-30.147765 99.117177a30.117647 30.117647 0 0 1-28.822588 21.353411H283.045647a30.117647 30.117647 0 0 1-28.521412-39.845647l143.841883-421.647058A30.117647 30.117647 0 0 1 426.857412 240.941176h49.814588a30.117647 30.117647 0 0 1 28.491294 20.389648l143.841882 421.647058A30.117647 30.117647 0 0 1 620.483765 722.823529h-40.990118a30.117647 30.117647 0 0 1-28.822588-21.353411L520.523294 602.352941h-137.517176z" :fill="color" p-id="1074"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

View File

@ -0,0 +1,15 @@
<template>
<svg t="1740830395839" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7184" :width="size" :height="size"><path d="M608 416h288c35.36 0 64 28.48 64 64v416c0 35.36-28.48 64-64 64H480c-35.36 0-64-28.48-64-64v-288H128c-35.36 0-64-28.48-64-64V128c0-35.36 28.48-64 64-64h416c35.36 0 64 28.48 64 64v288z m0 64v64c0 35.36-28.48 64-64 64h-64v256.032c0 17.664 14.304 31.968 31.968 31.968H864a31.968 31.968 0 0 0 31.968-31.968V512a31.968 31.968 0 0 0-31.968-31.968H608zM128 159.968V512c0 17.664 14.304 31.968 31.968 31.968H512a31.968 31.968 0 0 0 31.968-31.968V160A31.968 31.968 0 0 0 512.032 128H160A31.968 31.968 0 0 0 128 159.968z m64 244.288V243.36h112.736V176h46.752c6.4 0.928 9.632 1.824 9.632 2.752a10.56 10.56 0 0 1-1.376 4.128c-2.752 7.328-4.128 16.032-4.128 26.112v34.368h119.648v156.768h-50.88v-20.64h-68.768v118.272H306.112v-118.272H238.752v24.768H192z m46.72-122.368v60.48h67.392V281.92H238.752z m185.664 60.48V281.92h-68.768v60.48h68.768z m203.84 488H576L668.128 576h64.64l89.344 254.4h-54.976l-19.264-53.664h-100.384l-19.232 53.632z m33.024-96.256h72.864l-34.368-108.608h-1.376l-37.12 108.608zM896 320h-64a128 128 0 0 0-128-128V128a192 192 0 0 1 192 192zM128 704h64a128 128 0 0 0 128 128v64a192 192 0 0 1-192-192z" :fill="color" p-id="7185"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

View File

@ -0,0 +1,15 @@
<template>
<svg t="1740394685255" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9825" :width="size" :height="size"><path d="M544.059897 959.266898h-64.949141c-228.633593 0-415.697442-187.063849-415.697442-415.697442v-64.949141c0-228.633593 187.063849-415.697442 415.697442-415.697442h64.949141c228.633593 0 415.697442 187.063849 415.697442 415.697442v64.949141C959.756315 772.203049 772.692466 959.266898 544.059897 959.266898z" fill="#4DC247" p-id="9826"></path><path d="M576.882589 541.680388c-8.480842 0-24.804646 31.690275-34.608348 31.690274-2.554594-0.107508-5.03342-0.893852-7.181531-2.280192-18.284544-9.164798-34.288896-18.626522-49.313389-32.989585-12.424848-11.764442-26.127506-29.410082-33.286512-44.754028-0.988049-1.452893-1.563473-3.147423-1.663814-4.901339 0-7.523509 22.570528-21.544595 22.570528-33.970467 0-3.260051-16.643256-47.649575-18.968499-53.23487-3.260051-8.480842-4.878814-11.103012-13.679108-11.103012-4.263458 0-8.207465-0.979858-12.082871-0.979859-6.885629 0-12.082871 2.62217-17.007759 7.181532-15.685923 14.705041-23.527861 30.048987-24.166765 51.616107v2.598621c-0.341978 22.570528 10.761035 45.072456 23.185883 63.380549 28.042171 41.493977 57.133825 77.743613 103.825043 98.94623 14.043611 6.543651 46.395316 20.245285 62.05769 20.245285 18.626522 0 49.017486-11.740893 56.49492-30.048987 2.964148-7.523509 5.562769-16.643256 5.562769-24.804645 0-1.321836 0-3.282576-0.683955-4.90134C635.656678 569.449182 582.445358 541.680388 576.882589 541.680388zM510.583967 714.790727c-39.829139 0-79.338826-12.082871-112.671413-33.970467l-79.042923 25.124098 25.808053-76.078775c-25.459932-34.906298-39.189211-76.990033-39.213784-120.194922 0-112.967316 92.106676-205.073992 205.119043-205.073992s205.142592 92.084151 205.142592 205.051466C715.725535 622.684051 623.619883 714.790727 510.583967 714.790727zM510.583967 263.423169c-135.879821 0-246.225991 110.39122-246.225991 246.22599 0 44.776553 12.082871 88.869151 35.246228 127.079527l-44.41205 132.277793 136.199274-43.773145c119.12701 65.765178 269.012559 22.506023 334.776713-96.62201 20.106036-36.419601 30.662294-77.338154 30.685843-118.939639 0-135.834771-110.39122-246.225991-246.271041-246.225991L510.583967 263.423169z" :fill="color" p-id="9827"></path></svg>
</template>
<script setup>
defineProps({
size: [Number, String],
color: {
type: String,
default: 'currentColor' // 默认继承父级颜色
}
})
</script>

282
frontend/src/i18n/en.js Normal file
View File

@ -0,0 +1,282 @@
export default {
common: {
add: 'Add',
edit: 'Edit',
delete: 'Delete',
save: 'Save',
cancel: 'Cancel',
confirm: 'Confirm',
search: 'Search',
loading: 'Loading...',
success: 'Success',
failed: 'Failed',
error: 'Error',
warning: 'Warning',
tip: 'Tip',
yes: 'Yes',
no: 'No',
operation: 'Operation',
copy: 'Copy',
systemTip: 'System Tip'
},
menu: {
home: 'Home',
subtitle: 'liangzi',
quickReply: 'Quick Reply',
moreSetting: 'More Settings',
translateConfig: 'Translate Config',
customWeb: 'CustomWeb'
},
home: {
// Header
time: 'Current Time',
logout: 'Logout',
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: {
multiAccount: {
title: 'Multi-Account Login',
desc: 'Support multiple accounts online simultaneously'
},
messageManagement: {
title: 'Unified Message Management',
desc: 'Centralized chat and notification handling'
},
translation: {
title: 'Real-time Translation',
desc: 'Bi-directional translation supporting 10+ languages'
},
localTranslation: {
title: 'Local Translation',
desc: 'Local API translation for data privacy'
}
},
// Support and Help
supportAndHelp: 'Support & Help',
officialChannel: 'liangzi Channel',
channelDesc: 'Join our Telegram channel for latest announcements and tips',
tags: {
updates: 'Updates',
support: 'Support',
feedback: 'Feedback'
},
joinChannel: 'Join Channel'
},
login: {
title: 'Login',
username: 'Username',
password: 'Password',
remember: 'Remember me',
login: 'Login',
register: 'Register',
welcome: 'Welcome',
slogan: 'Use liangzi to boost your global business growth',
deviceId: 'Device ID',
copyTip: 'Click to copy',
copied: 'Copied to clipboard!',
copyFailed: 'Copy failed, please select manually!',
gettingDevice: 'Getting device info...',
authCode: 'Invitation Code',
enterAuthCode: 'Please enter your invitation code',
keepLogin: 'Keep me logged in',
loginButton: 'Login',
initializing: 'Initializing...',
loginSuccess: 'Login successful!',
loginFailed: 'Login failed! Please check your network connection',
pleaseEnterAuth: 'Please enter invitation code!',
brandDesc: 'One-Stop Cross-border E-commerce Solution',
features: {
account: {
title: 'Account Assistant',
desc: 'Unified management for multiple accounts'
},
translate: {
title: 'Translation Assistant',
desc: 'Multi-language localization support'
},
price: {
title: 'Best Value',
desc: 'Most cost-effective among similar tools'
}
},
copyright: '© 2024-2025 liangzi',
themeSwitcher: {
toDark: 'Switch to dark theme',
toLight: 'Switch to light theme'
},
errors: {
emptyAuthCode: 'Invitation code cannot be empty',
invalidParams: 'Invalid request parameters',
accountNotExist: 'Account does not exist',
accountDisabled: 'Account has been disabled by system',
expired: 'Device authorization has expired',
deviceDisabled: 'Device has been disabled',
unknown: 'Unknown error',
networkError: 'Network error, please check your connection',
invalidAuth: 'Invalid invitation code, please check and try again',
authFailed: 'Authentication failed! Please login again'
},
success: 'Login successful'
},
session: {
newChat: 'New Chat',
nickname: 'Nickname',
url: 'URL',
proxyConfig: 'Proxy Config',
proxyStatus: 'Proxy Status',
proxyType: 'Proxy Type',
proxyIp: 'Proxy IP',
proxyPort: 'Proxy Port',
username: 'Username',
password: 'Password',
sessionList: 'Session List',
startAll: 'Start All',
closeAll: 'Close All',
batchDelete: 'Batch Delete',
confirmDelete: 'Confirm delete selected sessions?',
confirmDeleteSingle: 'Confirm delete this session?',
remarks: 'Remarks',
searchPlaceholder: 'Remarks, Username',
createTime: 'Created At',
sessionRecord: 'Session Record',
show: 'Show',
start: 'Start',
close: 'Close',
starting: 'Starting session, please wait!',
closeSuccess: 'Session closed successfully',
urlRequired: 'URL is required',
proxyEnabled: 'Enable Authentication',
proxyDisabled: 'Disable Authentication',
delete: 'Delete',
proxySettings: 'Proxy Settings',
proxyConfigSaveSuccess: 'Proxy configuration saved successfully',
enableProxy: 'Enable Proxy Server',
proxyProtocol: 'Proxy Protocol',
selectProxyProtocol: 'Select Proxy Protocol',
hostAddress: 'Host Address',
enterHostAddress: 'Please enter host address',
portNumber: 'Port Number',
enterPortNumber: 'Please enter port number',
enableProxyAuth: 'Enable Proxy Authentication',
enterUsername: 'Please enter username',
enterPassword: 'Please enter password',
restartRequired: 'Changes will take effect after restart'
},
translate: {
google: 'Google Translate',
baidu: 'Baidu Translate',
youdao: 'YouDao Translate',
huoshan: 'HuoShan Translate',
xiaoniu: 'XiaoNiu Translate',
apiKey: 'API Key',
secretKey: 'Secret Key',
appId: 'App ID',
settings: 'Translation Settings',
clearCacheTitle: 'Confirm clearing translation cache for current session?',
mode: 'Translation Mode',
localTranslate: 'Local',
cloudTranslate: 'Cloud',
tooltipContent: 'Local translation does not consume characters\nPlease configure translation API first',
route: 'Translation Route',
selectRoute: 'Please select translation route',
realTimeReceive: 'Real-time Translation for Received Messages',
realTimeSend: 'Real-time Translation for Sent Messages',
sourceLanguage: 'Source Language',
targetLanguage: 'Target Language',
autoDetect: 'Auto Detect',
friendIndependent: 'Independent Translation Switch for Friends',
preview: 'Translation Preview'
},
quickReply: {
title: 'Quick Reply',
tooltipContent: 'Click to translate in input box\nDouble click to send original text',
searchPlaceholder: 'Filter by title or content',
noData: 'No Data',
send: 'Send'
},
userInfo: {
title: 'Contact Information',
phoneNumber: 'Phone Number',
nickname: 'Nickname',
country: 'Country',
gender: 'Gender',
male: 'Male',
female: 'Female',
salesInfo: 'Sales Information',
tradeActivity: 'Trade Activity',
customerLevel: 'Customer Level',
remarks: 'Remarks',
enterRemarks: 'Please enter',
followUpRecords: 'Follow-up Records',
selectPlaceholder: 'Please select',
// 交易活动状态
activityStatus: {
negotiating: 'Negotiating',
scheduled: 'Scheduled',
ordered: 'Ordered',
paid: 'Paid',
shipped: 'Shipped'
},
// 客户等级
customerLevels: {
normal: 'Normal',
medium: 'Medium',
important: 'Important',
critical: 'Critical'
}
},
rightMenu: {
refreshTip: 'About to refresh current page',
refresh: 'Refresh',
close: 'Close',
devTools: 'Developer Tools'
},
quickReplyConfig: {
group: {
title: 'Quick Reply Groups',
addGroup: 'Add Group',
editGroup: 'Edit Group',
groupName: 'Group Name',
enterGroupName: 'Please enter group name',
deleteConfirm: 'Are you sure to delete this group?',
filterPlaceholder: 'Filter by group name'
},
reply: {
title: 'Quick Reply',
addReply: 'Add Quick Reply',
editReply: 'Edit Quick Reply',
clearReply: 'Clear Replies',
clearConfirm: 'Are you sure to clear all replies?',
deleteConfirm: 'Are you sure to delete this quick reply?',
selectGroup: 'Please select a group first',
remark: 'Remark',
enterRemark: 'Please enter remark',
type: 'Type',
content: 'Content',
enterContent: 'Please enter content',
text: 'Text',
image: 'Image',
video: 'Video',
resource: 'Resource'
},
message: {
addSuccess: 'Added successfully',
editSuccess: 'Modified successfully',
deleteSuccess: 'Deleted successfully',
clearSuccess: 'Cleared successfully',
operationFailed: 'Operation failed'
}
}
}

View File

@ -0,0 +1,17 @@
import { createI18n } from 'vue-i18n'
import en from './en'
import zh from './zh'
import km from './km'
const i18n = createI18n({
legacy: false,
locale: 'zh',
fallbackLocale: 'en',
messages: {
en,
zh,
km
}
})
export default i18n

273
frontend/src/i18n/km.js Normal file
View File

@ -0,0 +1,273 @@
export default {
common: {
add: 'បន្ថែម',
edit: 'កែប្រែ',
delete: 'លុប',
save: 'រក្សាទុក',
cancel: 'បោះបង់',
confirm: 'បញ្ជាក់',
search: 'ស្វែងរក',
loading: 'កំពុងផ្ទុក...',
success: 'ជោគជ័យ',
failed: 'បរាជ័យ',
error: 'កំហុស',
warning: 'ព្រមាន',
tip: 'ព័ត៌មាន',
yes: 'បាទ/ចាស',
no: 'ទេ',
operation: 'ប្រតិបត្តិការ',
systemTip: 'ព័ត៌មានប្រព័ន្ធ',
copy: 'ចម្លង'
},
menu: {
home: 'ទំព័រដើម',
subtitle: 'SeaBox ជំនួយការ',
quickReply: 'ឆ្លើយតបរហ័ស',
moreSetting: 'ការកំណត់បន្ថែម',
translateConfig: 'ការកំណត់ការបកប្រែ',
customWeb: 'គេហទំព័រផ្ទាល់ខ្លួន'
},
home: {
time: 'ម៉ោងបច្ចុប្បន្ន',
logout: 'ចាកចេញ',
logoutConfirm: 'តើអ្នកប្រាកដជាចង់ចាកចេញមែនទេ?',
copySuccess: 'ចម្លងដោយជោគជ័យ!',
copyFailed: 'ចម្លងបរាជ័យ!',
accountInfo: 'ព័ត៌មានគណនី',
availableChars: 'តួអក្សរដែលមាន',
expirationTime: 'ពេលវេលាផុតកំណត់',
remainingDays: 'នៅសល់ {days} ថ្ងៃ',
deviceId: 'លេខសម្គាល់ឧបករណ៍',
coreFeatures: 'មុខងារសំខាន់ៗ',
features: {
multiAccount: {
title: 'ចូលគណនីច្រើន',
desc: 'គាំទ្រការគ្រប់គ្រងគណនីច្រើនក្នុងពេលតែមួយ'
},
messageManagement: {
title: 'ការគ្រប់គ្រងសាររួមបញ្ចូលគ្នា',
desc: 'គ្រប់គ្រងការជជែកនិងការជូនដំណឹងរួមគ្នា'
},
translation: {
title: 'ការបកប្រែជាមួយពេលវេលាពិត',
desc: 'ការបកប្រែទ្វេភាគយន្តគាំទ្រភាសាច្រើនជាង 10'
},
localTranslation: {
title: 'ការបកប្រែក្នុងតំបន់',
desc: 'ការបកប្រែ API ក្នុងតំបន់សម្រាប់ភាពឯកជននៃទិន្នន័យ'
}
},
supportAndHelp: 'ការគាំទ្រ និងជំនួយ',
officialChannel: 'ឆានែលផ្លូវការ SeaBox',
channelDesc: 'ចូលរួមឆានែល Telegram របស់យើងសម្រាប់ដំណឹងនិងគន្លឹះថ្មីៗ',
tags: {
updates: 'ការធ្វើបច្ចុប្បន្នភាព',
support: 'ការគាំទ្រ',
feedback: 'មតិត្រឡប់'
},
joinChannel: 'ចូលរួមឆានែល'
},
login: {
title: 'ចូលគណនី',
username: 'ឈ្មោះអ្នកប្រើប្រាស់',
password: 'ពាក្យសម្ងាត់',
remember: 'ចងចាំខ្ញុំ',
login: 'ចូលគណនី',
register: 'ចុះឈ្មោះ',
welcome: 'សូមស្វាគមន៍',
slogan: 'ប្រើប្រាស់ SeaBox ជំនួយការដើម្បីជំរុញការលូតលាស់អាជីវកម្មអន្តរជាតិរបស់អ្នក',
deviceId: 'លេខសម្គាល់ឧបករណ៍',
copyTip: 'ចុចដើម្បីចម្លង',
copied: 'បានចម្លងទៅក្តារតម្បៀតខ្ទាស់!',
copyFailed: 'ចម្លងបរាជ័យ សូមជ្រើសរើសដោយដៃ!',
gettingDevice: 'កំពុងទទួលព័ត៌មានឧបករណ៍...',
authCode: 'កូដអញ្ជើញ',
enterAuthCode: 'សូមបញ្ចូលកូដអញ្ជើញរបស់អ្នក',
keepLogin: 'រក្សាស្ថានភាពចូលគណនី',
loginButton: 'ចូលគណនី',
initializing: 'កំពុងចាប់ផ្តើម...',
loginSuccess: 'ចូលគណនីជោគជ័យ!',
loginFailed: 'ចូលគណនីបរាជ័យ! សូមពិនិត្យការតភ្ជាប់បណ្តាញរបស់អ្នក',
pleaseEnterAuth: 'សូមបញ្ចូលកូដអញ្ជើញ!',
brandDesc: 'ដំណោះស្រាយអាជីវកម្មអន្តរជាតិរួមបញ្ចូលគ្នា',
features: {
account: {
title: 'ជំនួយការគណនី',
desc: 'ការគ្រប់គ្រងគណនីច្រើនរួមបញ្ចូលគ្នា'
},
translate: {
title: 'ជំនួយការបកប្រែ',
desc: 'គាំទ្រការបកប្រែភាសាច្រើន'
},
price: {
title: 'តម្លៃល្អបំផុត',
desc: 'មានតម្លៃថោកបំផុតក្នុងចំណោមឧបករណ៍ស្រដៀងគ្នា'
}
},
copyright: '© 2024-2025 SeaBox ជំនួយការ',
themeSwitcher: {
toDark: 'ប្តូរទៅរចនាបថងងឹត',
toLight: 'ប្តូរទៅរចនាបថភ្លឺ'
},
errors: {
emptyAuthCode: 'កូដអញ្ជើញមិនអាចទទេបានទេ',
invalidParams: 'ប៉ារ៉ាម៉ែត្រសំណើមិនត្រឹមត្រូវ',
accountNotExist: 'គណនីមិនមានទេ',
accountDisabled: 'គណនីត្រូវបានបិទដោយប្រព័ន្ធ',
expired: 'ការអនុញ្ញាតឧបករណ៍បានផុតកំណត់',
deviceDisabled: 'ឧបករណ៍ត្រូវបានបិទ',
unknown: 'កំហុសមិនស្គាល់',
networkError: 'កំហុសបណ្តាញ សូមពិនិត្យការតភ្ជាប់របស់អ្នក',
invalidAuth: 'កូដអញ្ជើញមិនត្រឹមត្រូវ សូមពិនិត្យឡើងវិញ',
authFailed: 'ការផ្ទៀងផ្ទាត់បរាជ័យ! សូមចូលគណនីម្តងទៀត'
},
success: 'ចូលគណនីជោគជ័យ'
},
session: {
newChat: 'ជជែកថ្មី',
nickname: 'ឈ្មោះហៅក្រៅ',
url: 'URL',
proxyConfig: 'ការកំណត់រចនាសម្ព័ន្ធ Proxy',
proxyStatus: 'ស្ថានភាព Proxy',
proxyType: 'ប្រភេទ Proxy',
proxyIp: 'IP Proxy',
proxyPort: 'ច្រក Proxy',
username: 'ឈ្មោះអ្នកប្រើប្រាស់',
password: 'ពាក្យសម្ងាត់',
sessionList: 'បញ្ជីជជែក',
startAll: 'ចាប់ផ្តើមទាំងអស់',
closeAll: 'បិទទាំងអស់',
batchDelete: 'លុបជាក្រុម',
confirmDelete: 'បញ្ជាក់លុបជជែកដែលបានជ្រើសរើស?',
confirmDeleteSingle: 'បញ្ជាក់លុបជជែកនេះ?',
remarks: 'ចំណាំ',
searchPlaceholder: 'ចំណាំ, ឈ្មោះអ្នកប្រើប្រាស់',
createTime: 'ពេលវេលាបង្កើត',
sessionRecord: 'កំណត់ត្រាជជែក',
show: 'បង្ហាញ',
start: 'ចាប់ផ្តើម',
close: 'បិទ',
starting: 'កំពុងចាប់ផ្តើមជជែក សូមរង់ចាំ!',
closeSuccess: 'បិទជជែកដោយជោគជ័យ',
urlRequired: 'URL ត្រូវការ',
proxyEnabled: 'បើកការផ្ទៀងផ្ទាត់',
proxyDisabled: 'បិទការផ្ទៀងផ្ទាត់',
delete: 'លុប',
proxySettings: 'ការកំណត់ Proxy',
proxyConfigSaveSuccess: 'រក្សាទុកការកំណត់រចនាសម្ព័ន្ធ Proxy ដោយជោគជ័យ',
enableProxy: 'បើកម៉ាស៊ីនមេ Proxy',
proxyProtocol: 'ពិធីការ Proxy',
selectProxyProtocol: 'ជ្រើសរើសពិធីការ Proxy',
hostAddress: 'អាសយដ្ឋានម៉ាស៊ីនមេ',
enterHostAddress: 'សូមបញ្ចូលអាសយដ្ឋានម៉ាស៊ីនមេ',
portNumber: 'លេខច្រក',
enterPortNumber: 'សូមបញ្ចូលលេខច្រកម៉ាស៊ីនមេ',
enableProxyAuth: 'បើកការផ្ទៀងផ្ទាត់ម៉ាស៊ីនមេ Proxy',
enterUsername: 'សូមបញ្ចូលឈ្មោះអ្នកប្រើប្រាស់',
enterPassword: 'សូមបញ្ចូលពាក្យសម្ងាត់',
restartRequired: 'ការផ្លាស់ប្តូរនឹងចាប់ផ្តើមឡើងវិញបន្ទាប់ពីចាប់ផ្តើមឡើងវិញ'
},
translate: {
google: 'Google បកប្រែ',
baidu: 'Baidu បកប្រែ',
youdao: 'YouDao បកប្រែ',
huoshan: 'HuoShan បកប្រែ',
xiaoniu: 'XiaoNiu បកប្រែ',
apiKey: 'ពាក្យគន្លឹះ API',
secretKey: 'ពាក្យគន្លឹះសម្ងាត់',
appId: 'លេខសម្គាល់កម្មវិធី',
settings: 'ការកំណត់ការបកប្រែ',
clearCacheTitle: 'បញ្ជាក់ជម្រះឃ្លាំងសម្ងាត់ការបកប្រែសម្រាប់ជជែកបច្ចុប្បន្ន?',
mode: 'របៀបបកប្រែ',
localTranslate: 'ក្នុងតំបន់',
cloudTranslate: 'ពពក',
tooltipContent: 'ការបកប្រែក្នុងតំបន់មិនប្រើប្រាស់តួអក្សរ\nសូមកំណត់រចនាសម្ព័ន្ធ API បកប្រែជាមុនសិន',
route: 'ផ្លូវបកប្រែ',
selectRoute: 'សូមជ្រើសរើសផ្លូវបកប្រែ',
realTimeReceive: 'បកប្រែសារទទួលបានជាមួយពេលវេលាពិត',
realTimeSend: 'បកប្រែសារផ្ញើជាមួយពេលវេលាពិត',
sourceLanguage: 'ភាសាប្រភព',
targetLanguage: 'ភាសាគោលដៅ',
autoDetect: 'រកឃើញដោយស្វ័យប្រវត្តិ',
friendIndependent: 'បើក/បិទការបកប្រែឯករាជ្យសម្រាប់មិត្តភក្តិ',
preview: 'មើលជាមុនការបកប្រែ'
},
quickReply: {
title: 'ឆ្លើយតបរហ័ស',
tooltipContent: 'ចុចដើម្បីបកប្រែក្នុងប្រអប់បញ្ចូល\nចុចទ្វេដងដើម្បីផ្ញើអត្ថបទដើម',
searchPlaceholder: 'ត្រងតាមចំណងជើងឬមាតិកា',
noData: 'គ្មានទិន្នន័យ',
send: 'ផ្ញើ'
},
userInfo: {
title: 'ព័ត៌មានទំនាក់ទំនង',
phoneNumber: 'លេខទូរស័ព្ទ',
nickname: 'ឈ្មោះហៅក្រៅ',
country: 'ប្រទេស',
gender: 'ភេទ',
male: 'ប្រុស',
female: 'ស្រី',
salesInfo: 'ព័ត៌មានលក់',
tradeActivity: 'សកម្មភាពជួញដូរ',
customerLevel: 'កម្រិតអតិថិជន',
remarks: 'ចំណាំ',
enterRemarks: 'សូមបញ្ចូល',
followUpRecords: 'កំណត់ត្រាការតាមដាន',
selectPlaceholder: 'សូមជ្រើសរើស',
activityStatus: {
negotiating: 'កំពុងចរចា',
scheduled: 'បានកំណត់ពេល',
ordered: 'បានបញ្ជាទិញ',
paid: 'បានបង់',
shipped: 'បានដឹកជញ្ជូន'
},
customerLevels: {
normal: 'ធម្មតា',
medium: 'មធ្យម',
important: 'សំខាន់',
critical: 'សំខាន់ខ្លាំង'
}
},
rightMenu: {
refreshTip: 'នឹងធ្វើឱ្យថ្មីទំព័របច្ចុប្បន្ន',
refresh: 'ធ្វើឱ្យថ្មី',
close: 'បិទ',
devTools: 'ឧបករណ៍អ្នកអភិវឌ្ឍ'
},
quickReplyConfig: {
group: {
title: 'ក្រុមឆ្លើយតបរហ័ស',
addGroup: 'បន្ថែមក្រុម',
editGroup: 'កែប្រែក្រុម',
groupName: 'ឈ្មោះក្រុម',
enterGroupName: 'សូមបញ្ចូលឈ្មោះក្រុម',
deleteConfirm: 'បញ្ជាក់លុបក្រុមនេះ?',
filterPlaceholder: 'ត្រងតាមឈ្មោះក្រុម'
},
reply: {
title: 'ឆ្លើយតបរហ័ស',
addReply: 'បន្ថែមឆ្លើយតបរហ័ស',
editReply: 'កែប្រែឆ្លើយតបរហ័ស',
clearReply: 'ជម្រះឆ្លើយតប',
clearConfirm: 'បញ្ជាក់ជម្រះឆ្លើយតបទាំងអស់?',
deleteConfirm: 'បញ្ជាក់លុបឆ្លើយតបរហ័សនេះ?',
selectGroup: 'សូមជ្រើសរើសក្រុមជាមុនសិន',
remark: 'ចំណាំ',
enterRemark: 'សូមបញ្ចូលចំណាំ',
type: 'ប្រភេទ',
content: 'មាតិកា',
enterContent: 'សូមបញ្ចូលមាតិកា',
text: 'អត្ថបទ',
image: 'រូបភាព',
video: 'វីដេអូ',
resource: 'ធនធាន'
},
message: {
addSuccess: 'បន្ថែមដោយជោគជ័យ',
editSuccess: 'កែប្រែដោយជោគជ័យ',
deleteSuccess: 'លុបដោយជោគជ័យ',
clearSuccess: 'ជម្រះដោយជោគជ័យ',
operationFailed: 'ប្រតិបត្តិការបរាជ័យ'
}
}
}

273
frontend/src/i18n/zh.js Normal file
View File

@ -0,0 +1,273 @@
export default {
common: {
add: '添加',
edit: '编辑',
delete: '删除',
save: '保存',
cancel: '取消',
confirm: '确认',
search: '搜索',
loading: '加载中...',
success: '成功',
failed: '失败',
error: '错误',
warning: '警告',
tip: '提示',
yes: '是',
no: '否',
operation: '操作',
systemTip: '系统提示',
copy: '复制'
},
menu: {
home: '首页',
subtitle: '量子翻译',
quickReply: '快速回复',
moreSetting: '更多设置',
translateConfig: '翻译配置',
customWeb: '自定义网页'
},
home: {
time: '当前时间',
logout: '退出登录',
logoutConfirm: '确定要退出登录吗?',
copySuccess: '复制成功!',
copyFailed: '复制失败!',
accountInfo: '账户信息',
availableChars: '可用字符数',
expirationTime: '到期时间',
remainingDays: '剩余 {days} 天',
deviceId: '设备标识',
coreFeatures: '核心功能',
features: {
multiAccount: {
title: '多账号同时登录',
desc: '支持同一平台多个账号同时在线管理'
},
messageManagement: {
title: '统一消息管理',
desc: '集中处理所有平台的聊天和通知'
},
translation: {
title: '高效沟通实时翻译',
desc: '双向翻译支持 10+ 种语言'
},
localTranslation: {
title: '本地翻译',
desc: '支持本地API翻译功能保护数据隐私'
}
},
supportAndHelp: '支持与帮助',
officialChannel: '量子翻译 官方频道',
channelDesc: '加入我们的 Telegram 频道,获取最新公告和使用技巧',
tags: {
updates: '产品更新',
support: '技术支持',
feedback: '问题反馈'
},
joinChannel: '加入频道'
},
login: {
title: '登录',
username: '用户名',
password: '密码',
remember: '记住我',
login: '登录',
register: '注册',
welcome: '欢迎登录',
slogan: '使用量子翻译,助力全球业务增长',
deviceId: '设备标识',
copyTip: '点击复制',
copied: '已复制到剪贴板!',
copyFailed: '复制失败,请手动选择复制!',
gettingDevice: '正在获取设备信息...',
authCode: '邀请码',
enterAuthCode: '请输入您的邀请码',
keepLogin: '记住登录状态',
loginButton: '登 录',
initializing: '正在初始化...',
loginSuccess: '登录成功!',
loginFailed: '登录失败!请检查网络连接',
pleaseEnterAuth: '请输入邀请码!',
brandDesc: '一站式跨境电商运营解决方案',
features: {
account: {
title: '账号助手',
desc: '多账号统一管理,便捷切换'
},
translate: {
title: '翻译助手',
desc: '多语言本地化,优化海外表现'
},
price: {
title: '超值优惠',
desc: '同类工具中性价比最高'
}
},
copyright: '© 2024-2025 量子翻译',
themeSwitcher: {
toDark: '切换暗色主题',
toLight: '切换亮色主题'
},
errors: {
emptyAuthCode: '邀请码不能为空',
invalidParams: '请求参数错误',
accountNotExist: '账户不存在',
accountDisabled: '账户已被系统禁用',
expired: '设备授权已过期',
deviceDisabled: '设备已被禁用',
unknown: '未知错误',
networkError: '网络错误,请检查网络连接',
invalidAuth: '无效的邀请码,请检查后重试',
authFailed: '用户验证失败!请重新登录'
},
success: '登录成功'
},
session: {
newChat: '新建会话',
nickname: '昵称',
url: '链接',
proxyConfig: '代理配置',
proxyStatus: '代理状态',
proxyType: '代理类型',
proxyIp: '代理IP',
proxyPort: '代理端口',
username: '用户名',
password: '密码',
sessionList: '会话列表',
startAll: '一键启动',
closeAll: '一键关闭',
batchDelete: '批量删除',
confirmDelete: '确认删除所选会话?',
confirmDeleteSingle: '确认删除该会话吗?',
remarks: '备注',
searchPlaceholder: '备注、用户名',
createTime: '创建于',
sessionRecord: '会话记录',
show: '显示',
start: '启动',
close: '关闭',
starting: '会话启动中 请稍等!',
closeSuccess: '会话关闭成功',
urlRequired: '网址不能为空',
proxyEnabled: '开启验证',
proxyDisabled: '关闭验证',
delete: '删除',
proxySettings: '代理设置',
proxyConfigSaveSuccess: '代理配置保存成功',
enableProxy: '启用代理服务器',
proxyProtocol: '代理协议',
selectProxyProtocol: '选择代理协议',
hostAddress: '主机地址',
enterHostAddress: '请输入主机地址',
portNumber: '端口号',
enterPortNumber: '请输入主机端口号',
enableProxyAuth: '启用代理服务器验证',
enterUsername: '请输入用户名',
enterPassword: '请输入密码',
restartRequired: '重启会话后生效'
},
translate: {
google: '谷歌翻译',
baidu: '百度翻译',
youdao: '有道翻译',
huoshan: '火山翻译',
xiaoniu: '小牛翻译',
apiKey: 'API密钥',
secretKey: '密钥',
appId: '应用ID',
settings: '翻译设置',
clearCacheTitle: '确认清理当前会话历史翻译缓存吗?',
mode: '翻译模式',
localTranslate: '本地翻译',
cloudTranslate: '云端翻译',
tooltipContent: '本地翻译不消耗字符数\n使用前请先配置翻译API',
route: '翻译线路',
selectRoute: '请选择翻译线路',
realTimeReceive: '接收消息实时翻译',
realTimeSend: '发送消息实时翻译',
sourceLanguage: '源语言',
targetLanguage: '目标语言',
autoDetect: '自动检测',
friendIndependent: '好友独立发送翻译开关',
preview: '翻译预览'
},
quickReply: {
title: '快捷回复',
tooltipContent: '单击到输入框进行翻译\n双击按钮发送原文',
searchPlaceholder: '请输入标题或者关键内容过滤',
noData: '暂无数据',
send: '发送'
},
userInfo: {
title: '联系人信息',
phoneNumber: '手机号',
nickname: '昵称',
country: '国家',
gender: '性别',
male: '男',
female: '女',
salesInfo: '销售信息',
tradeActivity: '交易活动',
customerLevel: '客户等级',
remarks: '备注',
enterRemarks: '请输入',
followUpRecords: '跟进记录',
selectPlaceholder: '请选择',
activityStatus: {
negotiating: '沟通中',
scheduled: '已预约',
ordered: '已下单',
paid: '已付款',
shipped: '已发货'
},
customerLevels: {
normal: '普通',
medium: '一般',
important: '重要',
critical: '核心'
}
},
rightMenu: {
refreshTip: '即将刷新当前页面',
refresh: '刷新',
close: '关闭',
devTools: '开发者工具'
},
quickReplyConfig: {
group: {
title: '快捷分组',
addGroup: '添加分组',
editGroup: '修改分组',
groupName: '分组名称',
enterGroupName: '请输入分组名称',
deleteConfirm: '确认删除该分组吗?',
filterPlaceholder: '请输入分组名称过滤'
},
reply: {
title: '快捷回复',
addReply: '新增快捷回复',
editReply: '修改快捷回复',
clearReply: '清空回复',
clearConfirm: '是否确认清空所有回复?',
deleteConfirm: '确认删除该快捷回复?',
selectGroup: '请先选择分组',
remark: '备注',
enterRemark: '请输入备注',
type: '类型',
content: '内容',
enterContent: '请输入内容',
text: '文字',
image: '图片',
video: '视频',
resource: '资源'
},
message: {
addSuccess: '添加成功',
editSuccess: '修改成功',
deleteSuccess: '删除成功',
clearSuccess: '清空成功',
operationFailed: '操作失败'
}
}
}

22
frontend/src/main.js Normal file
View File

@ -0,0 +1,22 @@
import { createApp } from 'vue';
import App from './App.vue';
import './assets/global.less';
import components from './components/global';
import Router from './router/index';
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css';
import {createPinia} from "pinia";
import i18n from './i18n';
const pinia = createPinia()
const app = createApp(App)
// components
for (const i in components) {
app.component(i, components[i])
}
app.use(ElementPlus)
app.use(pinia)
app.use(i18n)
app.use(Router).mount('#app')

View File

@ -0,0 +1,46 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import routerMap from './routerMap'
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import { useMenuStore } from '@/stores/menuStore';
const Router = createRouter({
history: createWebHashHistory(),
routes: routerMap,
})
// 添加路由历史控制
let isNavigatingBack = false
Router.beforeEach(async (to, from, next) => {
// 如果是浏览器后退触发的导航,且从 index 页面后退
if (isNavigatingBack && from.path === '/index') {
isNavigatingBack = false
next(false) // 取消导航
return
}
// 原有的路由守卫逻辑
if (from.path === '/index' && (to.path === '/login' || to.path === '/')) {
await ipc.invoke(ipcApiRoute.hiddenSession, {})
const menuStore = useMenuStore();
menuStore.setCurrentMenu('Home')
menuStore.setRightFoldStatus(false)
}
next()
})
// 监听浏览器后退事件
window.addEventListener('popstate', () => {
isNavigatingBack = true
})
// 禁用鼠标侧键
window.addEventListener('mouseup', (e) => {
if (e.button === 3 || e.button === 4) { // 鼠标侧键
e.preventDefault()
e.stopPropagation()
}
}, true)
export default Router

View File

@ -0,0 +1,23 @@
/**
* 基础路由
* @type { *[] }
*/
const constantRouterMap = [
{
path: '/',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue')
},
{
path: '/index',
name: 'Index',
component: () => import('@/views/index.vue')
},
]
export default constantRouterMap

View File

@ -0,0 +1,175 @@
import { defineStore } from 'pinia';
import whatsapp from '@/components/icons/WhatsAppIcon.vue';
import tiktok from '@/components/icons/TiktikIcon.vue';
import telegram from '@/components/icons/TelegramIcon.vue';
import google from '@/components/icons/GoogleIcon.vue';
import {markRaw} from "vue";
export const useMenuStore = defineStore('platform', {
state: () => ({
currentMenu:'Home',
platform:'',
currentPartitionId:'',
locationInfo: {
x:0,
y:0,
width:0,
height:0,
},
menus : [
{
id: 'WhatsApp',
title: 'WhatsApp',
icon: markRaw(whatsapp),
color:'white',
openChildren:false,
children: []
},
{
id: 'Telegram',
title: 'Telegram',
icon: markRaw(telegram),
color:'#30ace1',
openChildren:false,
children: [],
},
{
id: 'TikTok',
title: 'TikTok',
icon: markRaw(tiktok),
color:'white',
openChildren:false,
children: []
},
{
id: 'CustomWeb',
title: 'menu.customWeb',
color:'#30ace1',
icon: markRaw(google),
children: [],
},
],
// 可用的翻译平台组件
translationRoute : [],
isChildMenu:false,
rightContent:'TranslateConfig',
rightFoldStatus:false,
userInfo:{}
}),
actions: {
setUserInfo (user){
this.userInfo = user;
},
setTranslationRoute(list) {
this.translationRoute = list;
},
setCurrentMenu(menuName) {
this.currentMenu = menuName;
},
setCurrentPartitionId(partitionId) {
this.currentPartitionId = partitionId;
},
setCurrentPlatform(platform) {
this.platform = platform;
},
setLocationInfo(info) {
this.locationInfo = info;
},
addChildrenMenu(childMenu) {
const pMenu = this.menus.find(item => item.id === childMenu.platform)
if (pMenu) {
const oldMenu = pMenu.children.find(item => item.partitionId === childMenu.partitionId)
if (!oldMenu) {
pMenu.children.push(childMenu)
}
}
},
updateChildrenMenu(childMenu) {
const pMenu = this.menus.find(item => item.id === childMenu?.platform)
if (pMenu) {
const index = pMenu.children.findIndex(item => item.partitionId === childMenu.partitionId)
if (index !== -1) {
pMenu.children.splice(index, 1,childMenu)
}
}
},
deleteChildrenMenu(childMenu) {
const pMenu = this.menus.find(item => item.id === childMenu.platform)
if (pMenu) {
const dMenu = pMenu.children.find(item => item.partitionId === childMenu.partitionId)
if (dMenu) {
const index = pMenu.children.findIndex(item => item.partitionId === childMenu.partitionId);
if (index !== -1) {
pMenu.children.splice(index, 1);
}
}
}
},
setIsChildMenu(partitionId) {
if ('MoreSetting' === partitionId || 'QuickReply'===partitionId || 'Home'===partitionId|| 'TranslateConfig'===partitionId) {
this.isChildMenu = false;
return;
}
const pMenu = this.menus.find(item => item.id === partitionId)
this.isChildMenu = !pMenu;
},
setRightContent(name) {
this.rightContent = name;
},
setRightFoldStatus(status) {
this.rightFoldStatus = status;
},
setMenuChildren(menuName, childArr) {
const menu = this.menus.find(item => item.id === menuName);
if (!menu || !childArr?.length) return; // 空值检查
// 创建已有子项ID的快速查找集合
const existingIds = new Set(menu.children.map(child => child.id));
// 过滤出需要添加的新项
const newItems = childArr.filter(newItem =>
!existingIds.has(newItem.id) // O(1)时间复杂度查找
);
if (newItems.length > 0) {
// 使用响应式数组更新Vue/React等框架需要
menu.children = [
...menu.children,
...newItems
];
// 或者直接修改原数组(非响应式场景)
// menu.children.push(...newItems);
}
}
},
getters: {
getCurrentMenu(state) {
return state.currentMenu;
},
getLocationInfo(state) {
return state.locationInfo;
},
getMenuById: (state) => (id) => {
return state.menus.find(menu => menu.id === id);
},
getMenus: (state) => () => {
return state.menus;
},
getCurrentChildren: (state) => () => {
const menu = state.menus.find(menu => menu.id === state.currentMenu)
return menu.children
},
getParentMenuById: (state) => (id) => {
for (const menu of state.menus) {
if (menu.children && menu.children.some(child => child.partitionId === id)) {
return menu;
}
}
return null;
},
getIsChildMenu: (state) => () => {
return state.isChildMenu;
},
},
});

View File

@ -0,0 +1,46 @@
export function searchCollection(collection, keyword, deepSearch) {
const results = [];
if (!collection || !Array.isArray(collection)) return results;
const lowerKeyword = keyword.toLowerCase();
for (const item of collection) {
// 检查当前项是否匹配关键字
let isMatched = false;
if (item !== null && typeof item === 'object') {
// 处理对象,检查所有属性值(包括嵌套对象)
isMatched = checkObject(item, lowerKeyword);
} else {
// 处理原始类型(字符串、数字等)
isMatched = String(item).toLowerCase().includes(lowerKeyword);
}
if (isMatched) {
results.push(item);
}
// 深度查找子元素(假设子元素在`children`数组中)
if (deepSearch && item && typeof item === 'object' && Array.isArray(item.children)) {
const childResults = searchCollection(item.children, keyword, deepSearch);
results.push(...childResults);
}
}
return results;
}
// 递归检查对象及其嵌套属性值
function checkObject(obj, lowerKeyword) {
for (const key in obj) {
const value = obj[key];
if (typeof value === 'string') {
if (value.toLowerCase().includes(lowerKeyword)) return true;
} else if (typeof value === 'number' || typeof value === 'boolean') {
if (String(value).toLowerCase().includes(lowerKeyword)) return true;
} else if (value !== null && typeof value === 'object') {
// 递归检查嵌套对象或数组
if (checkObject(value, lowerKeyword)) return true;
}
}
return false;
}

View File

@ -0,0 +1,33 @@
const Renderer = (window.require && window.require('electron')) || window.electron || {};
/**
* ipc
* 官方api说明https://www.electronjs.org/zh/docs/latest/api/ipc-renderer
*
* 属性/方法
* ipc.invoke(channel, param) - 发送异步消息invoke/handle 模型)
* ipc.sendSync(channel, param) - 发送同步消息send/on 模型)
* ipc.on(channel, listener) - 监听 channel, 当新消息到达,调用 listener
* ipc.once(channel, listener) - 添加一次性 listener 函数
* ipc.removeListener(channel, listener) - 为特定的 channel 从监听队列中删除特定的 listener 监听者
* ipc.removeAllListeners(channel) - 移除所有的监听器,当指定 channel 时只移除与其相关的所有监听器
* ipc.send(channel, ...args) - 通过channel向主进程发送异步消息
* ipc.postMessage(channel, message, [transfer]) - 发送消息到主进程
* ipc.sendTo(webContentsId, channel, ...args) - 通过 channel 发送消息到带有 webContentsId 的窗口
* ipc.sendToHost(channel, ...args) - 消息会被发送到 host 页面上的 <webview> 元素
*/
/**
* ipc
*/
const ipc = Renderer.ipcRenderer || undefined;
/**
* 是否为EE环境
*/
const isEE = ipc ? true : false;
export {
Renderer, ipc, isEE
};

View File

@ -0,0 +1,71 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import { debounce } from 'lodash-es'
// 获取目标元素的引用
const targetRef = ref(null)
let resizeObserver = null
let currentRect = null // 缓存上一次的位置信息
const locationInfo = ref({x: 0, y: 0,height: 0, width: 0})
// 自定义逻辑函数(根据需求修改)
const handleChange = debounce(async (width, height, rect) => {
// 这里执行你的自定义逻辑
locationInfo.value.x = rect.left + window.scrollX;
locationInfo.value.y = rect.top + window.scrollY;
locationInfo.value.width = width;
locationInfo.value.height = height -10;
await ipc.invoke(ipcApiRoute.setWindowLocation, {
x: locationInfo.value.x,
y: locationInfo.value.y,
width: locationInfo.value.width,
height: locationInfo.value.height
});
},50)
// 初始化观察者
const initObserver = () => {
resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect
const newRect = targetRef.value.getBoundingClientRect()
// 检测位置变化(比较上一次位置)
const isPositionChanged =
!currentRect ||
newRect.x !== currentRect.x ||
newRect.y !== currentRect.y
if (isPositionChanged || width !== currentRect?.width || height !== currentRect?.height) {
currentRect = newRect
handleChange(width, height, newRect)
}
}
})
if (targetRef.value) {
resizeObserver.observe(targetRef.value)
currentRect = targetRef.value.getBoundingClientRect() // 初始位置
}
}
// 生命周期钩子
onMounted(initObserver)
onBeforeUnmount(() => {
if (resizeObserver) resizeObserver.disconnect()
})
</script>
<template>
<div ref="targetRef" class="un-known">
<!-- 该组件用户显示加载的 webContentView窗口 -->
</div>
</template>
<style scoped lang="less">
.un-known {
height: calc(100vh - 40px);
width: 100%;
}
</style>

View File

@ -0,0 +1,908 @@
<script setup>
// 1. 图标组件导入优化 - 按功能分组
// 主题相关
// import { Sunny, Moon } from '@element-plus/icons-vue'
// 用户信息相关
import { DocumentCopy, Key, Platform, Clock, Coin, SwitchButton } from '@element-plus/icons-vue'
// 功能相关
import {
Connection,
User,
ChatDotRound,
Histogram,
Setting,
Promotion,
QuestionFilled,
Link
} from '@element-plus/icons-vue'
// Element Plus 组件
import { ElMessage, ElMessageBox } from 'element-plus'
// 工具和hooks
import { ipc } from "@/utils/ipcRenderer"
import { useMenuStore } from '@/stores/menuStore'
import { computed, markRaw, onMounted, ref, watch, onUnmounted, reactive } from "vue"
import router from "@/router"
import { ipcApiRoute } from "@/api"
import { useDark } from '@vueuse/core'
import LanguageSwitch from '@/components/global/LanguageSwitch.vue'
import ThemeSwitch from '@/components/global/ThemeSwitch.vue'
import { useI18n } from 'vue-i18n'
import axios from 'axios'
// 状态管理
const isDark = useDark()
const menuStore = useMenuStore()
const { t } = useI18n()
// 添加时间相关的状态和方法
const currentTime = ref('')
const updateTime = () => {
const now = new Date()
const year = now.getFullYear()
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
currentTime.value = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
// 设置定时器并在组件卸载时清除
let timer
onMounted(() => {
updateTime()
timer = setInterval(updateTime, 1000)
})
onUnmounted(() => {
clearInterval(timer)
})
// 用户认证相关
const checkLogin = async () => {
const authKey = menuStore.userInfo?.authKey;
if (authKey) {
const res = await ipc.invoke(ipcApiRoute.login, {authKey});
if (res.status) {
menuStore.setUserInfo(res.data)
}
}
}
// 生命周期钩子
onMounted(async () => {
await checkLogin()
})
// 计算属性
const userInfoCards = computed(() => {
// 解析到期时间字符串
const expireTimeStr = menuStore.userInfo.expireTime;
const expireDate = new Date(expireTimeStr);
const today = new Date();
// 计算剩余天数
const remainingDays = Math.ceil((expireDate - today) / (1000 * 60 * 60 * 24));
return [
{
icon: markRaw(Coin),
title: t('home.availableChars'),
value: menuStore.userInfo.totalChars,
color: 'var(--el-color-purple)',
bgColor: 'var(--el-color-purple-light-9)',
},
{
icon: markRaw(Clock),
title: t('home.expirationTime'),
value: menuStore.userInfo.expireTime,
subValue: t('home.remainingDays', { days: remainingDays }),
color: 'var(--el-color-danger)',
bgColor: 'var(--el-color-danger-light-9)',
},
{
icon: markRaw(Platform),
title: t('home.deviceId'),
value: menuStore.userInfo.machineCode,
color: 'var(--el-color-primary)',
bgColor: 'var(--el-color-primary-light-9)',
copyValue: menuStore.userInfo.machineCode,
showCopy: true
}
];
});
// 功能数据
const mainFeatures = computed(() => [
{
icon: markRaw(User),
title: t('home.features.multiAccount.title'),
desc: t('home.features.multiAccount.desc'),
color: "#3498DB"
},
{
icon: markRaw(Connection),
title: t('home.features.messageManagement.title'),
desc: t('home.features.messageManagement.desc'),
color: "#2ECC71"
},
{
icon: markRaw(ChatDotRound),
title: t('home.features.translation.title'),
desc: t('home.features.translation.desc'),
color: "#F39C12"
},
{
icon: markRaw(Histogram),
title: t('home.features.localTranslation.title'),
desc: t('home.features.localTranslation.desc'),
color: "#9B59B6"
}
]);
// 交互方法
const copyText = (text) => {
navigator.clipboard.writeText(text)
.then(() => ElMessage({
message: t('home.copySuccess'),
type: 'success',
offset: 40,
}))
.catch(() => ElMessage({
message: t('home.copyFailed'),
type: 'error',
offset: 40,
}))
}
const confirmLogout = () => {
ElMessageBox.confirm(t('home.logoutConfirm'), t('common.tip'), {
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
showClose: false,
type: 'warning',
icon: markRaw(QuestionFilled),
closeOnClickModal: false,
closeOnPressEscape: false
}).then(async () => {
await router.push('/login')
})
}
const openTelegram = () => {
ipc.invoke('open-external-link', 'https://t.me/liangzifanyi')
}
const formatAuthKey = (key) => {
if (!key) return 'ST';
return `${key.slice(0, 4)}...${key.slice(-2)}`;
}
const showAddSubDialog = ref(false)
const createLoading = ref(false)
const loading = ref(false)
const subAccounts = ref([])
const subFormRef = ref(null)
const subForm = reactive({
username: '',
password: '',
confirmPassword: ''
})
const subFormRules = {
username: [{ required: true, message: t('userInfo.username'), trigger: 'blur' }],
password: [{ required: true, message: t('userInfo.password'), trigger: 'blur' }],
confirmPassword: [
{ required: true, message: t('userInfo.confirmPassword'), trigger: 'blur' },
{ validator: (rule, value) => value === subForm.password, message: t('userInfo.passwordNotMatch'), trigger: 'blur' }
]
}
const fetchSubAccounts = async () => {
if (!menuStore.userInfo?.userId) return
loading.value = true
try {
const res = await ipc.invoke(ipcApiRoute.listSub, { parentId: menuStore.userInfo.userId })
if (res.status) {
subAccounts.value = res.data.data
} else {
ElMessage.error(res.data.msg || '获取子账户失败')
}
} catch (error) {
console.log(error)
} finally {
loading.value = false
}
}
const handleCreateSub = () => {
subFormRef.value.validate(async (valid) => {
if (!valid) return
if (subForm.password !== subForm.confirmPassword) {
ElMessage.error("密码不匹配")
return
}
createLoading.value = true
try {
const res = await ipc.invoke(ipcApiRoute.createSub, {
username: subForm.username,
password: subForm.password,
parent_id: menuStore.userInfo.userId
})
if (res.status) {
ElMessage.success("添加成功")
showAddSubDialog.value = false
subForm.username = ''
subForm.password = ''
subForm.confirmPassword = ''
fetchSubAccounts()
} else if (res.data.msg === 'USERNAME_EXISTS') {
ElMessage.error("用户名已存在")
} else {
ElMessage.error(res.data.msg || "添加失败")
}
} finally {
createLoading.value = false
}
})
}
const handleDelete = (row) => {
ElMessageBox.confirm("确定删除该子账户吗?", "提示", {
type: 'warning'
}).then(async () => {
await ipc.invoke(ipcApiRoute.deleteSub, { id: row.id })
ElMessage.success("删除成功")
fetchSubAccounts()
})
}
onMounted(() => {
if (menuStore.userInfo?.parentId === null) {
console.log('fetchSubAccounts')
fetchSubAccounts()
}
})
</script>
<template>
<div class="modern-home-container" :class="{ 'dark-theme': isDark }">
<!-- 顶部导航栏 -->
<div class="app-header">
<!-- 左侧区域品牌和时间 -->
<div class="header-left">
<!-- 可以考虑添加品牌Logo -->
<div class="time-display">
<el-icon><Clock /></el-icon>
<span>{{ currentTime }}</span>
</div>
</div>
<!-- 右侧区域功能按钮组 -->
<div class="header-right">
<!-- 工具组语言和主题 -->
<div class="tool-group">
<LanguageSwitch :size="32"/>
<ThemeSwitch :size="32"/>
</div>
<!-- 用户组用户信息和退出 -->
<div class="user-group">
<div class="action-item user-profile" @click="copyText(menuStore.userInfo.authKey)">
<!-- <div class="avatar">{{ menuStore.userInfo.authKey?.substring(0, 2) || 'ST' }}</div> -->
<!-- <span class="auth-key">{{ formatAuthKey(menuStore.userInfo.authKey) }}</span> -->
</div>
<div class="action-item logout-btn" @click="confirmLogout">
<el-icon><SwitchButton /></el-icon>
<span>{{ t('home.logout') }}</span>
</div>
</div>
</div>
</div>
<!-- 主内容区域 -->
<div class="content-area">
<!-- 账户信息部分 -->
<section class="content-section">
<div class="modern-header">
<div class="header-line"></div>
<h2>{{ t('home.accountInfo') }}</h2>
</div>
<!-- 用户信息卡片 -->
<div class="info-cards">
<div
v-for="(card, index) in userInfoCards"
:key="index"
class="info-card"
:class="{ 'clickable': card.showCopy }"
:style="{ '--card-color': card.color, '--card-bg-color': card.bgColor }"
@click="card.showCopy ? copyText(card.copyValue) : null"
>
<div class="card-icon">
<el-icon><component :is="card.icon" /></el-icon>
</div>
<div class="card-content">
<div class="card-header">
<h3 class="card-title">{{ card.title }}</h3>
<el-button
v-if="card.showCopy"
type="primary"
circle
size="small"
@click.stop="copyText(card.copyValue)"
>
<el-icon><DocumentCopy /></el-icon>
</el-button>
</div>
<p class="card-value">{{ card.value }}</p>
<p v-if="card.subValue" class="card-sub-value">{{ card.subValue }}</p>
</div>
</div>
</div>
<!-- 子账户管理卡片 -->
<el-card class="subaccount-card" v-if="menuStore.userInfo?.parentId === null">
<div class="subaccount-header" style="display: flex; justify-content: space-between; align-items: center;">
<span class="sub-title">子账户列表</span>
<el-button size="small" type="primary" @click="showAddSubDialog = true">添加</el-button>
</div>
<el-table :data="subAccounts" style="margin-top: 16px;" v-loading="loading">
<el-table-column prop="username" label="用户名" />
<el-table-column label="操作" width="200">
<template #default="{ row }">
<el-button type="danger" link @click="handleDelete(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="showAddSubDialog" title="添加子账户" width="450px">
<el-form :model="subForm" :rules="subFormRules" ref="subFormRef" label-width="130px">
<el-form-item label="用户名" prop="username">
<el-input v-model="subForm.username" placeholder="用户名" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="subForm.password" type="password" placeholder="密码" />
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="subForm.confirmPassword" type="password" placeholder="确认密码" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddSubDialog = false">取消</el-button>
<el-button type="primary" @click="handleCreateSub" :loading="createLoading">
确认
</el-button>
</template>
</el-dialog>
</section>
<!-- 功能部分 -->
<section id="features" class="content-section">
<div class="modern-header">
<div class="header-line"></div>
<h2>{{ t('home.coreFeatures') }}</h2>
</div>
<div class="features-grid">
<div
v-for="(feature, index) in mainFeatures"
:key="index"
class="feature-card"
:style="{ '--feature-color': feature.color }"
>
<div class="feature-icon">
<el-icon :size="32"><component :is="feature.icon" /></el-icon>
</div>
<div class="feature-content">
<h3>{{ feature.title }}</h3>
<p>{{ feature.desc }}</p>
</div>
</div>
</div>
</section>
<!-- 修改支持和帮助部分 -->
<section class="content-section support-section">
<div class="modern-header">
<div class="header-line"></div>
<h2>{{ t('home.supportAndHelp') }}</h2>
</div>
<div class="support-card">
<div class="support-content">
<div class="support-main">
<div class="support-icon-wrapper">
<div class="support-icon">
<el-icon :size="32"><Promotion /></el-icon>
</div>
<div class="icon-bg"></div>
</div>
<div class="support-info">
<div class="support-text">
<h3>{{ t('home.officialChannel') }}</h3>
<p>{{ t('home.channelDesc') }}</p>
</div>
</div>
</div>
<div class="support-footer">
<div class="support-tags">
<span>{{ t('home.tags.updates') }}</span>
<span>{{ t('home.tags.support') }}</span>
<span>{{ t('home.tags.feedback') }}</span>
</div>
<el-button
type="primary"
@click="openTelegram"
class="join-button"
:icon="markRaw(Link)"
>
{{ t('home.joinChannel') }}
</el-button>
</div>
</div>
</div>
</section>
</div>
</div>
</template>
<style scoped lang="less">
.modern-home-container {
height: calc(100vh - 50px); // 设置容器高度
background: var(--el-bg-color); // 使用 el-plus 背景色
color: var(--el-text-color-primary);
width: 100%;
box-sizing: border-box;
// 顶部导航栏
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1.5rem;
height: 60px;
background: var(--el-bg-color-overlay);
box-shadow: var(--el-box-shadow-light);
position: sticky;
top: 0;
z-index: 100;
// 左侧区域:品牌和时间
.header-left {
.time-display {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0 12px;
background: var(--el-fill-color-light);
border-radius: 16px;
transition: all 0.3s;
height: 32px;
cursor: pointer;
user-select: none;
.el-icon {
font-size: 14px;
color: var(--el-text-color-regular);
}
span {
font-family: 'Roboto Mono', monospace;
font-size: 13px;
color: var(--el-text-color-regular);
letter-spacing: 0.5px;
user-select: none;
}
&:hover {
transform: translateY(-1px);
background: var(--el-fill-color);
}
}
}
// 右侧区域:功能按钮组
.header-right {
display: flex;
align-items: center;
gap: 1.2rem; // 增加主要功能组之间的间距
// 工具组
.tool-group {
display: flex;
align-items: center;
gap: 0.6rem; // 相关功能之间间距较小
padding-right: 1.2rem; // 与用户组分隔
border-right: 1px solid var(--el-border-color-lighter); // 添加分隔线
}
// 用户组
.user-group {
display: flex;
align-items: center;
gap: 0.8rem;
}
// 用户信息按钮
.user-profile {
gap: 0.6rem;
background: var(--el-fill-color-light);
height: 32px;
padding: 0 12px;
border-radius: 16px;
max-width: 140px;
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
&:hover {
transform: translateY(-1px);
background: var(--el-fill-color);
}
.avatar {
width: 20px; // 调小头像尺寸
height: 20px;
border-radius: 4px;
background: linear-gradient(135deg, var(--el-color-primary), var(--el-color-success));
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px; // 调整字体大小
font-weight: 600;
flex-shrink: 0;
user-select: none;
}
.auth-key {
font-size: 13px;
color: var(--el-text-color-regular);
white-space: nowrap;
user-select: none;
}
}
// 退出登录按钮
.logout-btn {
gap: 0.5rem;
color: var(--el-text-color-regular);
height: 32px;
padding: 0 12px;
border-radius: 16px;
background: var(--el-fill-color-light);
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
&:hover {
color: var(--el-color-danger);
transform: translateY(-1px);
background: var(--el-fill-color);
}
.el-icon {
font-size: 14px; // 调小图标尺寸
}
span {
font-size: 13px;
user-select: none;
}
}
}
}
// 主内容区域
.content-area {
margin: 0 auto;
padding: 1rem 2rem;
box-sizing: border-box;
overflow-y: auto;
height: calc(100vh - 120px);
// 添加以下样式来隐藏滚动条
&::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
// 标题样式
.modern-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
.header-line {
width: 4px;
height: 24px;
background: var(--el-color-primary);
border-radius: 2px;
}
h2 {
font-size: 1.4rem;
font-weight: 600;
margin: 0;
}
}
// 信息卡片样式
.info-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.2rem;
margin-bottom: 2rem;
.info-card {
background: var(--el-bg-color-overlay);
border-radius: 0.8rem;
padding: 1.2rem;
box-shadow: var(--el-box-shadow-light);
display: flex;
align-items: flex-start;
gap: 1rem;
transition: all 0.3s;
height: 80%;
&.clickable {
cursor: pointer;
}
&:hover {
transform: translateY(-2px);
box-shadow: var(--el-box-shadow);
}
.card-icon {
width: 40px;
height: 40px;
border-radius: 8px;
background: var(--card-bg-color);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 2px;
i {
font-size: 1.2rem;
color: var(--card-color);
}
}
.card-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.5rem;
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 24px;
.card-title {
font-size: 0.9rem;
color: var(--el-text-color-secondary);
margin: 0;
line-height: 1;
}
}
.card-value {
color: var(--card-color);
font-size: 1.1rem;
font-weight: 600;
margin: 0;
line-height: 1.4;
text-align: left;
}
.card-sub-value {
color: var(--card-color);
font-size: 0.85rem;
opacity: 0.8;
margin: 0;
line-height: 1.2;
text-align: left;
}
}
}
}
// 功能卡片样式
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
.feature-card {
background: var(--el-bg-color-overlay);
border-radius: 1rem;
box-shadow: var(--el-box-shadow-light);
overflow: hidden;
cursor: pointer;
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: var(--el-box-shadow);
}
.feature-icon {
height: 80px;
background: var(--feature-color);
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.feature-content {
padding: 1rem;
h3 {
color: var(--feature-color);
margin-bottom: 0.5rem;
font-size: 1.1rem;
}
p {
color: var(--el-text-color-regular);
margin: 0;
font-size: 0.9rem;
line-height: 1.4;
}
}
}
}
// 支持卡片样式
.support-card {
background: var(--el-bg-color-overlay);
border-radius: 1rem;
padding: 2rem;
box-shadow: var(--el-box-shadow-light);
position: relative;
overflow: hidden;
border: 1px solid var(--el-border-color-light);
transition: all 0.3s;
&:hover {
transform: translateY(-2px);
box-shadow: var(--el-box-shadow);
border-color: var(--el-color-primary-light-5);
.support-icon-wrapper {
.icon-bg {
transform: scale(1.1);
}
}
}
.support-content {
.support-main {
display: flex;
align-items: flex-start;
gap: 2rem;
margin-bottom: 1.5rem;
.support-icon-wrapper {
position: relative;
width: 64px;
height: 64px;
.support-icon {
width: 64px;
height: 64px;
border-radius: 16px;
background: var(--el-color-primary);
color: white;
display: flex;
align-items: center;
justify-content: center;
position: relative;
z-index: 2;
box-shadow: 0 4px 12px rgba(var(--el-color-primary-rgb), 0.3);
}
.icon-bg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
background: var(--el-color-primary);
opacity: 0.2;
border-radius: 16px;
z-index: 1;
transition: transform 0.3s;
}
}
.support-info {
flex: 1;
min-width: 0;
.support-text {
h3 {
font-size: 1.3rem;
font-weight: 600;
margin: 0 0 0.8rem;
color: var(--el-text-color-primary);
}
p {
color: var(--el-text-color-regular);
margin: 0;
font-size: 1rem;
line-height: 1.5;
}
}
}
}
.support-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid var(--el-border-color-lighter);
.support-tags {
display: flex;
gap: 0.8rem;
flex-wrap: wrap;
span {
color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
padding: 0.4rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
transition: all 0.3s;
border: 1px solid var(--el-color-primary-light-5);
&:hover {
background: var(--el-color-primary-light-8);
transform: translateY(-1px);
}
}
}
.join-button {
height: 40px;
padding: 0 24px;
font-size: 1rem;
border-radius: 20px;
font-weight: 500;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(var(--el-color-primary-rgb), 0.2);
}
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,933 @@
<template>
<div class="my-index-page">
<div ref="leftMenu" class="left-menu">
<div class="left-menu-header">
<div
@click="toggleMenuHeader(homeMenuData.id)"
:class="activeMenu === 'Home' ? 'active-header-menu':'default-header-menu'">
<div class="header-menu-line"/>
<div class="header-menu-img">
<el-badge
:offset="[-3,6]"
:value="homeMenuData.children.length"
:max="99"
:show-zero="false"
type="info"
>
<el-avatar>
<el-icon size="40" :color="homeMenuData.color">
<component :is="homeMenuData.icon"/>
</el-icon>
</el-avatar>
</el-badge>
</div>
<div v-if="isCollapse" class="header-menu-title">
<el-text>{{ homeMenuData.title }}</el-text>
</div>
</div>
</div>
<el-scrollbar class="menu-item-group">
<div v-for="menu in menuStore.menus" :key="menu.id">
<div
@click="toggleMenu(menu)"
:class="{
'active-menu-item-child': (isActive(menu.id) || hasActiveChild(menu)) && (hasChildren(menu) || hasActiveChild(menu)),
'active-menu-item': (isActive(menu.id) || hasActiveChild(menu)) && !hasChildren(menu) && !hasActiveChild(menu),
'default-menu-item': !isActive(menu.id) && !hasActiveChild(menu),
}"
>
<div class="menu-item-line"/>
<div class="menu-item-img">
<el-badge
:offset="[-6,2]"
:value="filteredChildren(menu.children).length"
:max="99"
:show-zero="false"
type="info"
>
<el-avatar>
<el-icon size="50" :color="menu.color">
<component :is="menu.icon"/>
</el-icon>
</el-avatar>
</el-badge>
</div>
<div v-if="isCollapse" class="menu-item-text">
<el-text>{{ menu.title.startsWith('menu.') ? t(menu.title) : menu.title }}</el-text>
</div>
<div v-if="isCollapse && filteredChildren(menu.children).length>0" class="menu-item-icon">
<el-icon v-if="!(menu.openChildren || hasActiveChild(menu))" size="15"><ArrowDown /></el-icon>
<el-icon v-if="menu.openChildren || hasActiveChild(menu)" size="15"><ArrowUp /></el-icon>
</div>
</div >
<!-- 子菜单区域-->
<div
v-if="menu.openChildren === true && menu.children && menu.children.length >0"
class="submenu-container"
:key="`${menu.id}-${filteredChildren(menu.children).length}`"
>
<div
v-for="child in filteredChildren(menu.children)"
:key="child.partitionId"
@click="toggleSubMenu(child)"
:class="{
'active-child-menu-item': menuStore.currentMenu === child.partitionId,
'default-child-menu-item': menuStore.currentMenu !== child.partitionId
}">
<div class="child-menu-item-line"/>
<div class="child-menu-item-img" :class="{'margin-top-10':!isEmpty(child.avatarUrl)}">
<el-badge
:offset="[-38,15]"
is-dot
:type="child.onlineStatus === 'true' ? 'success' : 'info' "
>
<el-badge
v-if="!isCollapse && child.onlineStatus === 'true'"
:offset="[0,0]"
:value="child.msgCount"
:max="99"
:show-zero="false"
type="success"
class="msg-badge"
>
<!-- 如果有头像展示头像 -->
<el-avatar
v-if="child.avatarUrl"
:src="child.avatarUrl"
style="height: 30px;width: 30px;"
/>
<!-- 如果没有头像展示默认头像 -->
<template v-else>
<el-avatar style="height: 30px;width: 30px;">
<el-icon v-if="!isCustomWeb(menu)">
<User/>
</el-icon>
<el-icon color="#30ace1" v-else>
<component :is="google"/>
</el-icon>
</el-avatar>
</template>
</el-badge>
<el-badge
v-else
:offset="[0,0]"
:value="0"
:max="99"
:show-zero="false"
type="danger"
>
<!-- 如果有头像展示头像 -->
<el-avatar
v-if="child.avatarUrl"
:src="child.avatarUrl"
style="height: 30px;width: 30px;"
/>
<!-- 如果没有头像展示默认头像 -->
<template v-else>
<el-avatar style="height: 30px;width: 30px;">
<el-icon v-if="!isCustomWeb(menu)">
<User/>
</el-icon>
<el-icon size="35" color="#30ace1" v-else>
<component :is="google"/>
</el-icon>
</el-avatar>
</template>
</el-badge>
</el-badge>
</div>
<!-- 折叠隐藏部分-->
<div v-if="isCollapse" class="child-menu-item-text">
<div class="text-center-container">
<div v-if="child.nickName && child.nickName" class="child-menu-item-text-nickname">
<el-text>{{child.nickName}}</el-text>
</div>
<div v-if="child.userName && child.userName" class="child-menu-item-text-username">
<el-text>{{child.userName}}</el-text>
</div>
<div v-if="child.remarks && child.remarks !== ''" class="child-menu-item-text-remarks">
<el-text>{{child.remarks}}</el-text>
</div>
</div>
</div>
<div v-if="isCollapse" class="child-menu-item-icon margin-top-10">
<el-badge v-if="child.onlineStatus && child.onlineStatus==='true'" :offset="[-15,0]" type="success" :show-zero="false" :max="99" :value="child?.msgCount" ></el-badge>
</div>
</div>
</div>
</div>
</el-scrollbar>
<!-- 底部区域-->
<div class="fold-area">
<div class="fold-header"></div>
<div class="fold-menu">
<div @click="toggleBottomMenu('QuickReply')" :class="activeMenu === 'QuickReply'?'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="quickReply"/>
</el-icon>
</el-avatar>
</div>
<div v-if="isCollapse" class="menu-item-text">
<el-text>{{ t('menu.quickReply') }}</el-text>
</div>
</div>
<div @click="toggleBottomMenu('TranslateConfig')" :class="activeMenu === 'TranslateConfig'?'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="translateSetting"/>
</el-icon>
</el-avatar>
</div>
<div v-if="isCollapse" class="menu-item-text">
<el-text>{{ t('menu.translateConfig') }}</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">-->
<!-- <el-avatar>-->
<!-- <el-icon size="25"><Setting /></el-icon>-->
<!-- </el-avatar>-->
<!-- </div>-->
<!-- <div v-if="isCollapse" class="menu-item-text">-->
<!-- <el-text>更多设置</el-text>-->
<!-- </div>-->
<!-- </div>-->
</div>
<div @click="toggleCollapse" class="fold-button">
<el-button
:icon="isCollapse ? ArrowLeft : ArrowRight"
circle
class="collapse-btn"
:class="{'is-collapsed': isCollapse}"
/>
</div>
</div>
</div>
<div class="main-content">
<component :is="currentComponent" />
</div>
<div v-show="menuStore.isChildMenu" class="right-menu">
<RightMenu/>
</div>
</div>
</template>
<script setup>
import RightMenu from './right-menu/index.vue'
import Home from "@/views/home/index.vue"
import SessionList from "@/views/session-list/index.vue"
import QuickReply from "@/views/quick-reply/index.vue"
import TranslateConfig from "@/views/translate-config/index.vue"
import Unknown from "@/views/components/un-known/index.vue"
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 {
ArrowLeft,
ArrowRight,
User,
ArrowDown,
ArrowUp,
Fold,
Expand
} from '@element-plus/icons-vue'
import {ref, markRaw, watch, onMounted, onUnmounted, computed, nextTick} from 'vue'
import {ElButton, ElMessageBox} from 'element-plus'
import { useMenuStore } from '@/stores/menuStore'
import {ipcApiRoute} from "@/api"
import {ipc} from "@/utils/ipcRenderer"
import router from "@/router"
import { useI18n } from 'vue-i18n'
const menuStore = useMenuStore()
const { t } = useI18n()
const currentComponent = ref(markRaw(Home))
const isCollapse = ref(true)
const activeMenu = ref('Home')
const leftMenu = ref(null)
const homeMenuData = computed(() => ({
id: 'Home',
title: t('menu.home'),
color: '#30ace1',
subtitle: t('menu.subtitle'),
icon: markRaw(logo),
openChildren: false,
children: []
}))
const isCustomWeb = (menu) => {
return menu.id === 'CustomWeb'
}
const isEmpty = (value) => {
if (value == null) return true
if (typeof value === 'string') {
return value.trim().length === 0
}
return false
}
let timer = null
const clearTimer = () => {
if (timer) {
clearInterval(timer)
}
}
const filteredChildren = computed(() => (menu) => {
const nMenus = menu.filter(
child => child.windowStatus === 'true'
) || []
return nMenus
})
const isActive = (menuId) => menuStore.currentMenu === menuId
const hasActiveChild = (menu) => {
const nMenus = menu.children.filter(
child => child.windowStatus === 'true'
) || []
return nMenus.some(child => child.partitionId === menuStore.currentMenu)
}
const hasChildren = (menu) => {
const nMenus = menu.children.filter(
child => child.windowStatus === 'true'
) || []
return nMenus.length > 0
}
const toggleCollapse = () => {
isCollapse.value = !isCollapse.value
if (leftMenu.value) {
leftMenu.value.style.width = isCollapse.value ? '200px' : '65px'
}
}
const toggleMenu = async (menu) => {
if (menuStore.currentMenu === menu.id && menu?.children?.length === 0) {
return
}
activeMenu.value = menu.id
menu.openChildren = !menu.openChildren
menuStore.setCurrentMenu(menu.id)
await ipc.invoke(ipcApiRoute.hiddenSession, {})
}
const toggleMenuHeader = async (menuName) => {
if (menuStore.currentMenu === menuName) {
return
}
activeMenu.value = menuName
menuStore.setCurrentMenu(menuName)
await ipc.invoke(ipcApiRoute.hiddenSession, {})
menuStore.setIsChildMenu(menuName)
}
const toggleBottomMenu = async (menuName) => {
if (menuStore.currentMenu === menuName) {
return
}
await ipc.invoke(ipcApiRoute.hiddenSession, {})
activeMenu.value = menuName
menuStore.setCurrentMenu(menuName)
menuStore.setIsChildMenu(menuName)
}
const toggleSubMenu = async (child) => {
if (menuStore.currentMenu === child.partitionId) {
return
}
await ipc.invoke(ipcApiRoute.showSession, {
platform: child.platform,
partitionId: child.partitionId
})
activeMenu.value = child.partitionId
menuStore.setCurrentMenu(child.partitionId)
menuStore.setCurrentPlatform(child.platform)
menuStore.setCurrentPartitionId(child.partitionId)
}
const authExpire = (msg) => {
ElMessageBox.confirm(
`${msg}`,
t('common.systemTip'),
{
showClose: false,
showCancelButton: false,
confirmButtonText: t('common.confirm'),
type: 'error',
closeOnClickModal: false,
closeOnPressEscape: false
}
)
.then(async () => {
await router.push('/login')
})
.catch(() => {})
}
const networkErrorCount = ref(0) // 新增网络错误计数
const checkLogin = async () => {
const authKey = menuStore.userInfo?.authKey
if (authKey) {
const res = await ipc.invoke(ipcApiRoute.login, {authKey})
// 处理网络错误的特殊情况
if (!res.status && res.message === 'login.errors.networkError') {
networkErrorCount.value++
// 只有当网络错误超过3次才执行登出逻辑
if (networkErrorCount.value >= 3) {
clearTimer()
await ipc.invoke(ipcApiRoute.hiddenSession, {})
authExpire(t(res.message))
networkErrorCount.value = 0 // 重置网络错误计数
}
return
}
// 处理其他错误情况
if (!res.status) {
clearTimer()
await ipc.invoke(ipcApiRoute.hiddenSession, {})
authExpire(t(res.message))
} else {
// 授权成功时重置所有计数
networkErrorCount.value = 0
menuStore.setUserInfo(res.data)
}
console.log('check user auth:', res.data)
} else {
clearTimer()
await ipc.invoke(ipcApiRoute.hiddenSession, {})
authExpire(t('login.errors.authFailed'))
}
}
const menuItems = [
{ id: 'Home', component: markRaw(Home) },
{ id: 'Telegram', component: markRaw(SessionList) },
{ id: 'WhatsApp', component: markRaw(SessionList) },
{ id: 'TikTok', component: markRaw(SessionList) },
{ id: 'QuickReply', component: markRaw(QuickReply) },
{ id: 'TranslateConfig', component: markRaw(TranslateConfig) },
{ id: 'CustomWeb', component: markRaw(SessionList) },
{ id: 'Unknown', component: markRaw(Unknown) },
]
watch(
() => menuStore.currentMenu,
(newValue) => {
const selectedMenuItem = menuItems.find(item => item.id === newValue)
currentComponent.value = selectedMenuItem
? selectedMenuItem.component
: markRaw(Unknown)
menuStore.setIsChildMenu(newValue)
}
)
onMounted(() => {
const handleOnlineNotify = (event, args) => {
const { data } = args
menuStore.updateChildrenMenu(data)
}
ipc.removeAllListeners('online-notify')
ipc.on('online-notify', handleOnlineNotify)
initMenuSessions()
clearTimer()
timer = setInterval(checkLogin, 60000 * 10)
})
onUnmounted(() => {
clearTimer()
})
const initMenuSessions = async () => {
for (let menu of menuStore.menus) {
const res = await ipc.invoke(ipcApiRoute.getSessions, {platform: menu.id})
if (res.status) {
const arr = res.data.sessions
menuStore.setMenuChildren(menu.id, arr)
}
}
}
</script>
<style scoped lang="less">
/* 常用边距工具类 */
.margin-top-10 {
margin-top: 10px;
}
.margin-right-15 {
padding-right: 15px;
}
/* 主页面布局 */
.my-index-page {
height: calc(100vh - 30px);
display: flex;
justify-content: flex-start;
background-color: var(--el-bg-color);
color: var(--el-text-color-primary);
}
/* 左侧菜单区域 */
.left-menu {
width: 200px;
height: 100%;
cursor: pointer;
user-select: none;
position: relative;
background-color: var(--el-bg-color);
/* 顶部菜单头部 */
.left-menu-header {
height: 60px;
/* 菜单项基础样式 */
.default-header-menu, .active-header-menu {
display: flex;
height: 60px;
.header-menu-line {
width: 3px;
}
.header-menu-img {
margin-left: 10px;
margin-top: 10px;
margin-right: 15px;
width: 30px;
height: 30px;
:deep(.el-avatar) {
--el-avatar-bg-color: var(--el-bg-color);
}
}
.header-menu-title, .header-menu-subtitle {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
height: 60px;
text-align: left;
color: var(--el-text-color-primary);
}
}
/* 活跃菜单样式 */
.active-header-menu {
background-color: var(--el-color-primary-light-9);
.header-menu-line {
background-color: var(--el-color-primary);
}
}
/* 默认菜单样式 */
.default-header-menu {
.header-menu-line {
background-color: var(--el-bg-color);
}
}
}
/* 菜单项组 */
.menu-item-group {
height: calc(100% - 235px);
:deep(.el-scrollbar__wrap) {
overflow-x: hidden;
}
:deep(.el-scrollbar__bar.is-horizontal) {
display: none;
}
:deep(.el-scrollbar__bar.is-vertical) {
display: none;
}
:deep(.msg-badge .el-badge__content) {
font-size: 9px;
height: 16px;
line-height: 16px;
padding: 0 4px;
border-radius: 8px;
}
/* 菜单项通用样式 */
.default-menu-item, .active-menu-item, .active-menu-item-child {
display: flex;
height: 60px;
transition: all 0.3s ease;
.menu-item-line {
width: 3px;
transition: background-color 0.3s ease;
}
.menu-item-img {
margin-left: 10px;
margin-top: 10px;
margin-right: 15px;
width: 30px;
height: 30px;
transition: all 0.3s ease;
:deep(.el-avatar) {
--el-avatar-bg-color: var(--el-bg-color);
}
}
.menu-item-text, .active-menu-item-text {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
height: 60px;
text-align: left;
transition: color 0.3s ease;
}
.menu-item-icon {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
transition: all 0.3s ease;
}
}
/* 默认菜单项样式 */
.default-menu-item {
color: var(--el-text-color-primary);
.menu-item-line {
background-color: var(--el-bg-color);
}
}
/* 有子菜单的激活菜单项 */
.active-menu-item-child {
background-color: var(--el-bg-color);
:deep(.el-text) {
--el-text-color: var(--el-color-primary);
}
.menu-item-line {
background-color: var(--el-bg-color);
}
.menu-item-text {
color: var(--el-color-primary);
}
}
/* 激活菜单项样式 */
.active-menu-item {
background-color: var(--el-color-primary-light-9);
.menu-item-line {
background-color: var(--el-color-primary);
}
.menu-item-icon {
justify-content: flex-start;
}
}
/* 子菜单项样式 */
.default-child-menu-item, .active-child-menu-item {
display: flex;
height: 60px;
align-items: center;
padding-right: 0;
box-sizing: border-box;
position: relative;
transition: all 0.3s ease;
.child-menu-item-line {
width: 3px;
transition: all 0.3s ease;
}
.child-menu-item-img {
display: flex;
flex: 0 0 30px;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
margin-left: 20px;
transition: all 0.3s ease;
}
.child-menu-item-text {
height: 60px;
display: flex;
flex: 1;
flex-direction: column;
min-width: 0;
padding-left: 10px;
justify-content: center;
transition: all 0.3s ease;
.text-center-container {
display: flex;
flex-direction: column;
justify-content: center;
gap: 2px;
transition: all 0.3s ease;
}
&-nickname,
&-username,
&-remarks {
min-height: 0;
padding: 0 4px;
display: flex;
align-items: center;
line-height: 1.2;
transition: all 0.3s ease;
.el-text {
font-size: 10px;
color: var(--el-text-color-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.3s ease;
}
}
}
.child-menu-item-icon {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
flex: 0 0 30px;
margin-right: 5px;
transition: all 0.3s ease;
}
}
/* 默认子菜单项 */
.default-child-menu-item {
color: var(--el-text-color-primary);
.child-menu-item-line {
background-color: var(--el-bg-color);
}
}
/* 激活子菜单项 */
.active-child-menu-item {
background-color: var(--el-color-primary-light-9);
color: var(--el-text-color-primary);
.child-menu-item-line {
display: flex;
width: 3px;
height: 60px;
background-color: var(--el-color-primary);
}
.child-menu-item-text {
.text-center-container {
.child-menu-item-text-nickname,
.child-menu-item-text-username,
.child-menu-item-text-remarks {
.el-text {
color: var(--el-color-primary);
}
}
}
}
}
}
/* 底部折叠区域 */
.fold-area {
height: 175px;
width: 100%;
position: absolute;
bottom: 0;
.fold-header {
height: 5px;
width: 100%;
background-color: var(--el-bg-color-page);
}
/* 底部菜单 */
.fold-menu {
height: 120px;
/* 通用底部菜单项样式 */
.default-fold-menu-item, .active-fold-menu-item {
display: flex;
height: 60px;
.menu-item-line {
width: 3px;
}
.menu-item-img {
margin-left: 10px;
margin-top: 10px;
margin-right: 15px;
width: 30px;
height: 30px;
:deep(.el-avatar) {
--el-avatar-bg-color: var(--el-color-primary);
}
}
.menu-item-text {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
height: 60px;
text-align: left;
}
.menu-item-icon {
display: flex;
align-items: center;
justify-content: flex-end;
flex: 1;
}
}
/* 默认底部菜单项 */
.default-fold-menu-item {
.menu-item-line {
background-color: var(--el-bg-color);
}
}
/* 激活底部菜单项 */
.active-fold-menu-item {
background-color: var(--el-color-primary-light-9);
.menu-item-line {
background-color: var(--el-color-primary);
}
}
}
/* 折叠按钮区域 */
.fold-button {
display: flex;
height: 50px;
align-items: center;
justify-content: center;
border-top: 1px solid var(--el-border-color);
box-sizing: border-box;
.collapse-btn {
width: 32px;
height: 32px;
padding: 0;
border: 1px solid var(--el-border-color);
background-color: var(--el-bg-color);
&:hover {
background-color: var(--el-color-primary-light-9);
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
&.is-collapsed {
/* 移除旋转效果 */
}
:deep(.el-icon) {
font-size: 16px;
}
}
}
}
}
/* 内容区域 */
.main-content {
flex: 1;
min-width: 0;
background-color: var(--el-bg-color-page);
color: var(--el-text-color-primary);
padding: 10px;
}
/* 右侧菜单 */
.right-menu {
position: relative;
height: calc(100vh - 30px);
background-color: var(--el-bg-color);
}
/* 子菜单展开/收起动画 */
.submenu-container {
/* 移除过渡效果 */
/* 移除 height transition */
/* 移除 will-change 属性 */
}
/* 删除所有这些动画相关的类 */
.submenu-enter-from,
.submenu-leave-to {
height: 0 !important;
}
.submenu-enter-active,
.submenu-leave-active {
transition: height 0.3s ease-in-out;
overflow: hidden;
}
.submenu-enter-to,
.submenu-leave-from {
height: auto;
}
/* 子菜单项样式 */
.default-child-menu-item, .active-child-menu-item {
// ... existing styles ...
}
</style>

View File

@ -0,0 +1,754 @@
<script setup>
import {Connection, DocumentCopy, Moon, Sunny, Key, User, Discount, Message, Setting, Monitor} from '@element-plus/icons-vue'
import {ref, onMounted} from 'vue'
import {ElMessage, ElTooltip} from 'element-plus'
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import {useRouter} from 'vue-router'
import {useMenuStore} from '@/stores/menuStore';
import {useDark, useToggle} from '@vueuse/core'
import LogoIcon from '@/components/icons/LogoIcon.vue'
import LanguageSwitch from '@/components/global/LanguageSwitch.vue'
import { useI18n } from 'vue-i18n'
const menuStore = useMenuStore();
const router = useRouter()
const machineCode = ref('')
const authCode = ref('')
const isLoading = ref(false)
const keepAuth = ref(false)
const username = ref('')
const password = ref('')
// 暗黑模式控制
const isDark = useDark()
const toggleDark = useToggle(isDark)
// 添加 i18n 引用
const { t } = useI18n()
// 初始化时尝试读取本地保存的授权码
onMounted(async () => {
await initData()
const savedAuth = menuStore.authKey || localStorage.getItem('authKey')
if (savedAuth) {
authCode.value = savedAuth
keepAuth.value = true
}
})
const initData = async () => {
try {
const systemInfo = await ipc.invoke(ipcApiRoute.getSystemInfo, {})
machineCode.value = systemInfo?.machineCode || ''
} catch (error) {
ElMessage.error(`初始化失败:${error.message}`)
}
}
const copyCode = async () => {
try {
await navigator.clipboard.writeText(machineCode.value)
ElMessage({
message: t('login.copied'),
type: 'success',
offset: 40,
})
} catch (err) {
ElMessage({
message: t('login.copyFailed'),
type: 'error',
offset: 40,
})
}
}
const handleLogin = async () => {
// 旧邀请码登录逻辑,已废弃
/*
if (!authCode.value) {
ElMessage({
message: t('login.errors.emptyAuthCode'),
type: 'error',
offset: 40,
})
return
}
try {
isLoading.value = true
const res = await ipc.invoke(ipcApiRoute.login, {authKey: authCode.value})
if (keepAuth.value) {
localStorage.setItem('authKey', authCode.value)
} else {
localStorage.removeItem('authKey')
}
if (res.status) {
ElMessage({
message: t(res.message),
type: 'success',
offset: 40,
})
menuStore.setUserInfo(res.data)
await router.push('/index')
} else {
ElMessage({
message: t(res.message),
type: 'error',
offset: 40,
})
}
} catch (error) {
ElMessage({
message: t('login.errors.networkError'),
type: 'error',
offset: 40,
})
} finally {
isLoading.value = false
}
*/
// 新用户名密码登录逻辑
if (!username.value || !password.value) {
ElMessage({
message: '请输入用户名和密码',
type: 'error',
offset: 40,
})
return
}
try {
isLoading.value = true
const res = await ipc.invoke(ipcApiRoute.userLogin, {
username: username.value,
password: password.value,
device: machineCode.value
})
console.log(res)
if (res.status) {
ElMessage({
message: '登录成功',
type: 'success',
offset: 40,
})
menuStore.setUserInfo(res.data)
await router.push('/index')
} else {
ElMessage({
message: res.message || '登录失败',
type: 'error',
offset: 40,
})
}
} catch (error) {
ElMessage({
message: '网络错误',
type: 'error',
offset: 40,
})
} finally {
isLoading.value = false
}
}
</script>
<template>
<div class="login-container" :class="{'dark-mode': isDark}">
<div class="bg-decoration">
<div class="bg-circle circle-1"></div>
<div class="bg-circle circle-2"></div>
<div class="bg-circle circle-3"></div>
</div>
<div class="login-card">
<div class="tools-group">
<LanguageSwitch :size="32"/>
<div class="theme-switcher" @click="toggleDark()">
<el-tooltip :content="isDark ? t('login.themeSwitcher.toLight') : t('login.themeSwitcher.toDark')" placement="top">
<el-icon :size="18"><Moon v-if="isDark" /><Sunny v-else /></el-icon>
</el-tooltip>
</div>
</div>
<div class="card-left">
<div class="brand">
<div class="logo-container">
<div class="logo-icon">
<LogoIcon :size="90" color="#FFB344" />
</div>
</div>
<h1 class="brand-title"><span>{{ t('menu.subtitle') }}</span></h1>
<p class="brand-desc">{{ t('login.brandDesc') }}</p>
<div class="feature-cards">
<div class="feature-card">
<el-icon class="feature-icon"><User /></el-icon>
<div class="feature-info">
<h3>{{ t('login.features.account.title') }}</h3>
<p>{{ t('login.features.account.desc') }}</p>
</div>
</div>
<div class="feature-card">
<el-icon class="feature-icon"><Message /></el-icon>
<div class="feature-info">
<h3>{{ t('login.features.translate.title') }}</h3>
<p>{{ t('login.features.translate.desc') }}</p>
</div>
</div>
<div class="feature-card">
<el-icon class="feature-icon"><Discount /></el-icon>
<div class="feature-info">
<h3>{{ t('login.features.price.title') }}</h3>
<p>{{ t('login.features.price.desc') }}</p>
</div>
</div>
</div>
</div>
</div>
<div class="card-right">
<div class="login-form">
<div class="form-header">
<h2>{{ t('login.welcome') }}</h2>
<p>{{ t('login.slogan') }}</p>
</div>
<div class="form-body">
<div class="form-group">
<label class="form-label">{{ t('login.username') }}</label>
<el-input
v-model="username"
size="large"
:placeholder="t('session.enterUsername')"
:prefix-icon="User"
clearable
:disabled="!machineCode"
@keyup.enter="handleLogin"
class="auth-input"
/>
</div>
<div class="form-group">
<label class="form-label">{{ t('login.password') }}</label>
<el-input
v-model="password"
size="large"
type="password"
:placeholder="t('session.enterPassword')"
:prefix-icon="Key"
clearable
:disabled="!machineCode"
@keyup.enter="handleLogin"
class="auth-input"
/>
</div>
<div class="form-options">
<el-checkbox v-model="keepAuth">{{ t('login.keepLogin') }}</el-checkbox>
</div>
<el-button
type="primary"
size="large"
:loading="isLoading"
@click="handleLogin"
class="submit-btn"
:disabled="!machineCode"
>
{{ machineCode ? t('login.loginButton') : t('login.initializing') }}
</el-button>
</div>
<div class="form-footer">
<p>{{ t('login.copyright') }}</p>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="less">
.login-container {
width: 100%;
height: calc(100vh - 30px);
display: flex;
align-items: center;
justify-content: center;
background-color: var(--el-bg-color-page);
position: relative;
padding: 20px;
box-sizing: border-box;
transition: all 0.3s ease;
overflow: hidden;
&.dark-mode {
background-color: var(--el-bg-color-darker);
.bg-circle {
opacity: 0.15;
}
.feature-card {
background: rgba(255, 255, 255, 0.2) !important;
&:hover {
background: rgba(255, 255, 255, 0.3) !important;
}
}
}
.bg-decoration {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
z-index: 0;
overflow: hidden;
.bg-circle {
position: absolute;
border-radius: 50%;
transition: all 0.3s ease;
&.circle-1 {
width: 500px;
height: 500px;
background: linear-gradient(140deg, var(--el-color-primary-light-5), var(--el-color-primary));
top: -250px;
right: -100px;
opacity: 0.2;
}
&.circle-2 {
width: 600px;
height: 600px;
background: linear-gradient(210deg, var(--el-color-success-light-5), var(--el-color-success));
bottom: -350px;
left: -200px;
opacity: 0.15;
}
&.circle-3 {
width: 300px;
height: 300px;
background: linear-gradient(20deg, var(--el-color-warning-light-5), var(--el-color-warning));
top: 40%;
right: 10%;
opacity: 0.1;
}
}
}
.login-card {
position: relative;
z-index: 10;
width: 85%;
max-width: 950px;
height: 85%;
max-height: 620px;
min-height: 500px;
background: var(--el-bg-color);
border-radius: 24px;
box-shadow: 0 20px 60px var(--el-box-shadow);
display: flex;
overflow: hidden;
.tools-group {
position: absolute;
top: 20px;
right: 20px;
z-index: 100;
display: flex;
gap: 8px;
align-items: center;
}
.theme-switcher {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--el-fill-color-light);
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
background: var(--el-fill-color);
}
.el-icon {
color: var(--el-text-color-regular);
transition: all 0.3s ease;
}
}
.card-left {
width: 40%;
min-width: 300px;
max-width: 380px;
background: linear-gradient(135deg, var(--el-color-primary-light-8), var(--el-color-primary-light-3));
padding: 30px;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--el-text-color-primary);
.brand {
text-align: center;
width: 100%;
.logo-container {
margin-bottom: 15px;
.logo-icon {
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(10px);
display: inline-flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
overflow: hidden;
}
}
.brand-title {
font-size: 28px;
font-weight: 600;
margin: 0 0 6px 0;
letter-spacing: 0.5px;
span {
font-weight: 400;
opacity: 0.8;
}
}
.brand-desc {
font-size: 14px;
opacity: 0.8;
margin-bottom: 35px;
}
.feature-cards {
display: flex;
flex-direction: column;
gap: 12px;
.feature-card {
display: flex;
align-items: center;
text-align: left;
padding: 12px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(10px);
transition: all 0.3s ease;
&:hover {
background: rgba(255, 255, 255, 0.8);
transform: translateX(5px);
}
.feature-icon {
font-size: 22px;
margin-right: 12px;
padding: 8px;
border-radius: 10px;
background: rgba(var(--el-color-primary-rgb), 0.1);
color: var(--el-color-primary);
}
.feature-info {
h3 {
margin: 0 0 3px 0;
font-size: 14px;
font-weight: 600;
}
p {
margin: 0;
font-size: 12px;
opacity: 0.8;
}
}
}
}
}
}
.card-right {
flex: 1;
padding: 30px 40px;
display: flex;
align-items: center;
justify-content: center;
.login-form {
width: 100%;
max-width: 400px;
min-width: 280px;
.form-header {
margin-bottom: 30px;
h2 {
font-size: 24px;
color: var(--el-text-color-primary);
margin: 0 0 10px 0;
font-weight: 600;
}
p {
color: var(--el-text-color-secondary);
margin: 0;
font-size: 16px;
}
}
.form-body {
.form-group {
margin-bottom: 24px;
.form-label {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
color: var(--el-text-color-regular);
font-weight: 500;
.copy-btn {
opacity: 0;
pointer-events: none;
}
}
.machine-code-box {
background: var(--el-fill-color-light);
border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
padding: 0 16px;
position: relative;
font-family: monospace;
font-size: 14px;
color: var(--el-text-color-primary);
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
width: 100%;
box-sizing: border-box;
height: 40px;
cursor: pointer;
.prefix-icon {
font-size: 16px;
color: var(--el-text-color-secondary);
flex-shrink: 0;
}
.code-text {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
min-width: 0;
display: block;
line-height: 1;
}
.copy-icon {
font-size: 16px;
color: var(--el-text-color-secondary);
opacity: 0;
transition: all 0.3s ease;
margin-left: 4px;
flex-shrink: 0;
&.show {
opacity: 1;
}
}
&:hover {
border-color: var(--el-color-primary-light-5);
.copy-icon.show {
opacity: 1;
color: var(--el-color-primary);
}
}
}
.auth-input {
:deep(.el-input__wrapper) {
box-shadow: 0 0 0 1px var(--el-border-color-lighter);
border-radius: 12px;
font-size: 14px;
padding: 0 16px;
height: 40px;
line-height: 40px;
transition: all 0.3s;
&:hover {
box-shadow: 0 0 0 1px var(--el-color-primary-light-5);
}
&.is-focus {
box-shadow: 0 0 0 1px var(--el-color-primary) !important;
}
}
}
}
.form-options {
margin-bottom: 30px;
display: flex;
justify-content: space-between;
}
.submit-btn {
width: 100%;
height: 48px;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
letter-spacing: 1px;
transition: all 0.3s ease;
&:not(:disabled):hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(var(--el-color-primary-rgb), 0.25);
}
}
}
.form-footer {
margin-top: 40px;
text-align: center;
p {
color: var(--el-text-color-secondary);
font-size: 12px;
}
}
}
}
}
}
@media (max-width: 1100px) {
.login-container {
.login-card {
width: 90%;
max-width: 800px;
}
}
}
@media (max-width: 900px) {
.login-container {
padding: 15px;
.login-card {
width: 100%;
height: auto;
min-height: calc(100vh - 60px);
max-height: none;
border-radius: 20px;
flex-direction: column;
.card-left {
width: 100%;
min-width: auto;
max-width: none;
height: auto;
padding: 25px 20px;
.brand {
.logo-container {
margin-bottom: 15px;
.logo-icon {
width: 60px;
height: 60px;
}
}
.brand-title {
font-size: 24px;
}
.brand-desc {
margin-bottom: 25px;
}
.feature-cards {
max-width: 400px;
margin: 0 auto;
}
}
}
.card-right {
padding: 25px 20px;
.login-form {
min-width: auto;
.form-header {
margin-bottom: 25px;
}
}
}
.tools-group {
top: 15px;
right: 15px;
}
}
}
}
@media (max-width: 480px) {
.login-container {
padding: 10px;
.login-card {
border-radius: 16px;
.card-left {
padding: 20px 15px;
.brand {
.feature-cards {
display: none;
}
}
}
.card-right {
padding: 20px 15px;
}
.tools-group {
top: 10px;
right: 10px;
}
}
}
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<div class="more-setting">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick" type="border-card">
<el-tab-pane label="翻译设置" name="translate">翻译设置</el-tab-pane>
<el-tab-pane label="软件设置" name="platform"> 软件设置</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import {ref} from "vue";
const activeName = ref('translate')
const handleClick = (tab,event)=>{
console.log(tab,event);
}
</script>
<style scoped lang="less">
.more-setting {
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column; /* 纵向排列 */
background-color: white;
:deep(.el-tabs__content) {
height: calc(100vh - 120px);
}
}
</style>

View File

@ -0,0 +1,561 @@
<template>
<div class="quick-reply">
<!--新建分组弹出层-->
<el-dialog v-model="groupVisible" :title="groupTitle" width="400">
<div class="add-group-dialog-form">
<el-form :model="groupForm" :rules="rules" ref="groupFormRef" label-width="80px" @submit.prevent>
<el-form-item :label="t('quickReplyConfig.group.groupName')" prop="name">
<el-input v-model="groupForm.name" :placeholder="t('quickReplyConfig.group.enterGroupName')" @keyup.enter.prevent="handleSaveGroup"></el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="add-group-dialog-footer">
<el-button @click="groupVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="handleSaveGroup">{{ t('common.confirm') }}</el-button>
</span>
</template>
</el-dialog>
<!--新建快捷回复弹出层-->
<el-dialog v-model="replyVisible" :title="replyTitle" width="600">
<div class="add-group-dialog-form">
<el-form :model="replyForm" :rules="rules" ref="replyFormRef" label-width="80px" @submit.prevent>
<el-form-item :label="t('quickReplyConfig.reply.remark')" prop="remark">
<el-input v-model="replyForm.remark" :placeholder="t('quickReplyConfig.reply.enterRemark')" @keyup.enter.prevent="handleSaveReply"></el-input>
</el-form-item>
<el-form-item :label="t('quickReplyConfig.reply.type')" prop="type">
<el-radio-group v-model="replyForm.type">
<el-radio value="text">{{ t('quickReplyConfig.reply.text') }}</el-radio>
<el-radio disabled value="image">{{ t('quickReplyConfig.reply.image') }}</el-radio>
<el-radio disabled value="video">{{ t('quickReplyConfig.reply.video') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('quickReplyConfig.reply.content')" prop="content">
<el-input :autosize="{ minRows: 4, maxRows: 8 }" v-model="replyForm.content" type="textarea" :placeholder="t('quickReplyConfig.reply.enterContent')" @keyup.enter.prevent></el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="add-group-dialog-footer">
<el-button @click="replyVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="handleSaveReply">{{ t('common.confirm') }}</el-button>
</span>
</template>
</el-dialog>
<!-- 左侧操作栏 -->
<div class="quick-reply-left">
<div class="reply-item-title">
<div class="left-header">
<div class="header-title">
<el-text tag="b" size="large">{{ t('quickReplyConfig.group.title') }}</el-text>
</div>
<div class="header-icon">
<el-icon @click="handleAddGroup(null)"><Plus /></el-icon>
</div>
</div>
<div class="header-search">
<el-input :placeholder="t('quickReplyConfig.group.enterGroupName')" @input="handleSearch" v-model="groupName" :suffix-icon="Search"></el-input>
</div>
</div>
<div class="left-content">
<div @click="handleChangeGroup(item)" class="content-item" v-for="item in groups" :class="{ 'item-selected': currentGroupId === item.id }">
<div class="item-left">
<el-text truncated>{{item.name}}</el-text>
<el-text>({{ item.contentCount }})</el-text>
</div>
<div class="item-right">
<div class="edit-icon">
<el-icon @click.stop="handleAddGroup(item)"><Edit /></el-icon>
</div>
<div class="delete-icon" @click.stop="()=>{}">
<el-popconfirm
width="180"
:confirm-button-text="t('common.yes')"
:cancel-button-text="t('common.no')"
@confirm="handleDeleteGroup(item)"
:title="t('quickReplyConfig.group.deleteConfirm')">
<template #reference>
<el-icon><Delete /></el-icon>
</template>
</el-popconfirm>
</div>
</div>
</div>
<el-empty :image-size="100" v-if="groups.length <= 0" :description="t('quickReply.noData')" />
</div>
</div>
<!-- 右侧内容部分 -->
<div class="quick-reply-right">
<div class="right-header">
<el-button size="small" plain type="primary" @click="handleAddReply(null)">{{ t('quickReplyConfig.reply.addReply') }}</el-button>
<el-button size="small" plain type="danger" :disabled="!(tableData.length > 0)" @click="handleClearReply">{{ t('quickReplyConfig.reply.clearReply') }}</el-button>
</div>
<div class="right-content">
<el-table
height="100%"
:data="tableData"
border
>
<el-table-column align="center" prop="id" width="80" label="ID"></el-table-column>
<el-table-column align="center" :show-overflow-tooltip="true" prop="remark" :label="t('quickReplyConfig.reply.remark')"></el-table-column>
<el-table-column align="center" :show-overflow-tooltip="true" prop="content" :label="t('quickReplyConfig.reply.content')"></el-table-column>
<el-table-column align="center" prop="url" :label="t('quickReplyConfig.reply.resource')" min-width="160">
-
</el-table-column>
<el-table-column align="center" :label="t('common.operation')" width="150" fixed="right">
<template #default="{ row }">
<el-button @click="handleAddReply(row)" size="small" round type="primary">{{ t('common.edit') }}</el-button>
<el-popconfirm
placement="left"
:confirm-button-text="t('common.yes')"
:cancel-button-text="t('common.no')"
@confirm="handleDeleteReply(row)"
:title="t('quickReplyConfig.reply.deleteConfirm')">
<template #reference>
<el-button round size="small" type="danger">{{ t('common.delete') }}</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
<script setup>
import {Delete, Edit, Plus, Search} from "@element-plus/icons-vue";
import {nextTick, onMounted, ref, watch} from "vue";
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import {ElButton, ElMessage, ElMessageBox, ElTable, ElTableColumn} from "element-plus";
import {searchCollection} from "@/utils/common";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const groupName = ref('');
const handleSearch = async () => {
if (groupName.value === "") {
await initData()
}
groups.value = searchCollection(groups.value, groupName.value, true);
}
const currentGroupId = ref(0);
const groups = ref([])
const tableData = ref([])
const initData = async () => {
const res = await ipc.invoke(ipcApiRoute.getGroups,{})
if (res.status) {
groups.value = res.data
}
await getTableData();
}
const getTableData = async () => {
// 查找对应的分组对象
const row = groups.value.find(item => item.id === currentGroupId.value);
if (row) {
tableData.value = row.contents || [];
}
}
const groupVisible = ref(false);
const replyVisible = ref(false);
// 表单校验规则
const rules = ref({
name: [
{ required: true, message: t('quickReplyConfig.group.enterGroupName'), trigger: 'blur' }
],
type: [
{ required: true, message: t('quickReplyConfig.reply.type'), trigger: 'blur' }
],
remark: [
{ required: true, message: t('quickReplyConfig.reply.enterRemark'), trigger: 'blur' }
],
content: [
{ required: true, message: t('quickReplyConfig.reply.enterContent'), trigger: 'blur' }
]
});
const groupFormRef = ref(null);
const replyFormRef = ref(null);
const groupForm = ref({
name:'',
});
const replyForm = ref({
id:'',
type:'',
remark:'',
content:'',
});
const groupTitle = ref('')
const replyTitle = ref('')
const handleAddGroup = async (group) => {
await nextTick(() => {
groupFormRef.value?.clearValidate();
});
if (group) {
groupTitle.value = t('quickReplyConfig.group.editGroup')
groupForm.value.id = group.id
groupForm.value.name = group.name
}else {
groupTitle.value = t('quickReplyConfig.group.addGroup')
groupForm.value = {}
}
groupVisible.value = true;
}
const handleDeleteGroup = async (group) => {
const res = await ipc.invoke(ipcApiRoute.deleteGroup,{id: group.id})
if (res.status) {
ElMessage({
message: t('quickReplyConfig.message.deleteSuccess'),
type: 'success',
offset: 40,
})
if (group.id === currentGroupId.value) {
currentGroupId.value = 0;
tableData.value = [];
}
await initData();
}else {
ElMessage({
message: t('quickReplyConfig.message.operationFailed'),
type: 'error',
offset: 40,
})
}
}
const handleSaveGroup = async () => {
groupFormRef.value.validate(async (valid) => {
if (valid) {
const id = groupForm.value.id;
if (id) {
const res = await ipc.invoke(ipcApiRoute.editGroup,{name:groupForm.value.name,id:id});
if (res.status) {
ElMessage({
message: t('quickReplyConfig.message.editSuccess'),
type: 'success',
offset: 40,
})
groupVisible.value = false;
await initData();
}else {
ElMessage({
message: t('quickReplyConfig.message.operationFailed'),
type: 'error',
offset: 40,
})
}
}else {
const res = await ipc.invoke(ipcApiRoute.addGroup,{name:groupForm.value.name});
if (res.status) {
ElMessage({
message: t('quickReplyConfig.message.addSuccess'),
type: 'success',
offset: 40,
})
groupVisible.value = false;
await initData();
}else {
ElMessage({
message: t('quickReplyConfig.message.operationFailed'),
type: 'error',
offset: 40,
})
}
}
}
})
}
const handleSaveReply = async () => {
replyFormRef.value.validate(async (valid) => {
if (valid) {
//判断是新增还是编辑
const id = replyForm.value.id;
if (id) {
const args = {
id:id,
remark:replyForm.value.remark,
content:replyForm.value.content,
type:replyForm.value.type,
url:'',
groupId:currentGroupId.value,
}
const res = await ipc.invoke(ipcApiRoute.editReply,args);
if (res.status) {
ElMessage({
message: t('quickReplyConfig.message.editSuccess'),
type: 'success',
offset: 40,
})
replyVisible.value = false;
await initData();
}else {
ElMessage({
message: t('quickReplyConfig.message.operationFailed'),
type: 'error',
offset: 40,
})
}
}else {
const args = {
remark:replyForm.value.remark,
content:replyForm.value.content,
type:replyForm.value.type,
url:'',
groupId:currentGroupId.value,
}
const res = await ipc.invoke(ipcApiRoute.addReply,args);
if (res.status) {
ElMessage({
message: t('quickReplyConfig.message.addSuccess'),
type: 'success',
offset: 40,
})
replyVisible.value = false;
await initData();
}else {
ElMessage({
message: t('quickReplyConfig.message.operationFailed'),
type: 'error',
offset: 40,
})
}
}
}
})
}
const handleAddReply = async (record)=>{
await nextTick(() => {
replyFormRef.value?.clearValidate();
});
if (currentGroupId.value === 0) {
ElMessage({
message: t('quickReplyConfig.reply.selectGroup'),
type: 'error',
offset: 40,
})
return;
}
replyVisible.value = true;
if (record) {
replyTitle.value = t('quickReplyConfig.reply.editReply');
replyForm.value.id = record.id;
replyForm.value.remark = record.remark;
replyForm.value.type = record.type;
replyForm.value.groupId = record.groupId;
replyForm.value.content = record.content;
}else {
replyForm.value = {}
replyForm.value.type = 'text'
replyTitle.value = t('quickReplyConfig.reply.addReply')
}
}
const handleClearReply = async ()=>{
ElMessageBox.confirm(
t('quickReplyConfig.reply.clearConfirm'),
t('common.tip'),
{
confirmButtonText: t('common.confirm'),
cancelButtonText: t('common.cancel'),
type: 'warning',
}
)
.then(async () => {
const groupId = currentGroupId.value;
const res = await ipc.invoke(ipcApiRoute.deleteAllReply, {groupId: groupId});
if (res.status) {
ElMessage({
message: t('quickReplyConfig.message.clearSuccess'),
type: 'success',
offset: 40,
})
await initData();
} else {
ElMessage({
message: t('quickReplyConfig.message.operationFailed'),
type: 'error',
offset: 40,
})
}
})
.catch(() => {})
}
const handleDeleteReply = async (row)=>{
const id = row.id;
const res = await ipc.invoke(ipcApiRoute.deleteReply,{id:id});
if (res.status) {
ElMessage({
message: t('quickReplyConfig.message.deleteSuccess'),
type: 'success',
offset: 40,
})
await initData();
}else {
ElMessage({
message: t('quickReplyConfig.message.operationFailed'),
type: 'error',
offset: 40,
})
}
}
onMounted(async () => {
await initData()
if (currentGroupId.value === 0 && groups.value.length > 0) {
currentGroupId.value = groups.value[0].id;
}
})
watch(
() => currentGroupId.value,
async (newValue) => {
// 查找对应的分组对象
await getTableData()
},
{ immediate: true }
);
const editItem = async (row)=>{
}
const deleteItem = async (row)=>{
}
const handleChangeGroup = async (item)=>{
currentGroupId.value = item.id;
}
</script>
<style scoped lang="less">
// 定义颜色变量
@selected-color: #f0f0f0;
@border-color: #e1e1e1;
@bg-color: #F0F2F5FF;
.quick-reply {
height: 100%;
background-color: var(--el-bg-color);;
display: flex;
//弹出层部分
.add-group-dialog-form {
:deep(.el-input__wrapper) {
border-radius: 0;
background-color: var(--el-bg-color-overlay);
border-color: var(--el-border-color);
}
:deep(.el-textarea__inner) {
border-radius: 0;
background-color: var(--el-bg-color-overlay);
color: var(--el-text-color-primary);
}
}
.add-group-dialog-footer {
:deep(.el-button) {
border-radius: 0;
border-color: var(--el-color-primary);
}
}
.quick-reply-left {
display: flex;
flex-direction: column; /* 修改为列布局 */
min-width: 240px;
width: 240px;
:deep(.el-text) {
--el-text-color: var(--el-text-color-primary);
}
:deep(.el-input__wrapper) {
border-radius: 0;
}
.reply-item-title {
display: flex;
flex-direction: column; /* 修改为列布局 */
padding: 20px 20px 0 20px;
.left-header {
height: 30px;
width: 100%;
display: flex;
user-select: none;
.header-title {
display: flex;
flex: 1;
}
.header-icon {
display: flex;
flex: 1;
align-items: center;
justify-content: end;
cursor: pointer;
}
}
.header-search {
width: 100%; /* 确保宽度占满父容器 */
margin-top: 15px; /* 添加一些间距 */
}
}
.left-content {
width: 100%;
height: 100%;
overflow-y: auto;
margin-top: 10px;
background-color: var(--el-bg-color);
.content-item {
width: 100%;
height: 45px;
display: flex;
cursor: pointer;
border-bottom: 1px solid var(--el-border-color);
.item-left{
display: flex;
flex: 4;
max-width: 140px;
justify-content: start;
align-items: center;
gap: 5px;
user-select: none;
margin-left: 20px;
}
.item-right{
display: flex;
flex: 1;
justify-content: end;
align-items: center;
gap: 5px;
.edit-icon {
display: flex;
}
.delete-icon {
display: flex;
margin-right: 20px;
}
}
}
.item-selected {
background-color: var(--el-color-primary-light-9);
}
}
}
.quick-reply-right {
display: flex;
flex-direction: column;
flex: 1;
width: calc(100% - 240px);
background-color: var(--el-bg-color-page);
.right-header {
padding-left: 10px;
height: 45px;
display: flex;
align-items: center;
}
.right-content {
display: flex;
overflow: auto;
flex: 1;
padding-left: 10px;
}
}
}
</style>

View File

@ -0,0 +1,296 @@
<template>
<div class="proxy-config">
<!-- 顶部标题-->
<div class="header-container">
<div class="header-title">
<el-text tag="b" size="large">{{ t('session.proxyConfig') }}</el-text>
</div>
</div>
<!-- 代理开关-->
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('session.enableProxy') }}</el-text>
</div>
<div class="content-right">
<el-switch
:active-value="'true'"
:inactive-value="'false'"
size="default"
v-model="proxyInfo.proxyStatus"
/>
</div>
</div>
<!-- 代理协议类型-->
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('session.proxyProtocol') }}</el-text>
</div>
<div class="content-right">
<el-select
v-model="proxyInfo.proxyType"
:placeholder="t('session.selectProxyProtocol')"
size="default"
style="width: 100%"
>
<el-option label="HTTP" value="http"/>
<el-option label="HTTPS" value="https"/>
<el-option label="SOCKS4" value="socks4"/>
<el-option label="SOCKS5" value="socks5"/>
</el-select>
</div>
</div>
<!-- 代理主机 ip-->
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('session.hostAddress') }}</el-text>
</div>
<div class="content-right">
<el-input :placeholder="t('session.enterHostAddress')" v-model="proxyInfo.proxyIp"></el-input>
</div>
</div>
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('session.portNumber') }}</el-text>
</div>
<div class="content-right">
<el-input :placeholder="t('session.enterPortNumber')" v-model="proxyInfo.proxyPort"></el-input>
</div>
</div>
<!-- 代理服务器验证开关-->
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('session.enableProxyAuth') }}</el-text>
</div>
<div class="content-right">
<el-switch
:active-value="'true'"
:inactive-value="'false'"
size="default"
v-model="proxyInfo.userVerifyStatus"
/>
</div>
</div>
<!-- 用户名 密码-->
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('session.username') }}</el-text>
</div>
<div class="content-right">
<el-input :placeholder="t('session.enterUsername')" v-model="proxyInfo.username"></el-input>
</div>
</div>
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('session.password') }}</el-text>
</div>
<div class="content-right">
<el-input :placeholder="t('session.enterPassword')" v-model="proxyInfo.password"></el-input>
</div>
</div>
</div>
</template>
<script setup>
import {onMounted, ref, unref, watch} from "vue";
import { ipc } from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import { useMenuStore } from '@/stores/menuStore';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const menuStore = useMenuStore();
import {ElMessage} from "element-plus";
const proxyInfo = ref({
id:'',
proxyStatus:'false',
proxyType:'http',
proxyIp:'',
proxyPort:'',
userVerifyStatus:'false',
username:'',
password:'',
})
watch(
() => menuStore.currentPartitionId,
async (newValue, oldValue) => {
if (newValue) {
await getConfigInfo()
}
}
);
// 定义需要监听的字段
const propertiesToWatch = [
'proxyStatus',
"proxyType",
"proxyIp",
"proxyPort",
"userVerifyStatus",
"username",
"password",
];
let watchers = []; // 存储所有字段的监听器
// 初始化字段监听逻辑
const addWatchers = ()=> {
removeWatchers(); // 确保不会重复绑定监听器
watchers = propertiesToWatch.map((property) =>
watch(
() => unref(proxyInfo.value[property]),
(newValue, oldValue) => {
if (newValue !== "" && newValue !== oldValue) {
handlePropertyChange(property, newValue);
}
}
)
);
}
// 自定义逻辑
const handlePropertyChange = async (property, value) => {
const args = {key: property, value: value, id:proxyInfo.value.id};
await ipc.invoke(ipcApiRoute.editProxyInfo, args);
if (property === 'proxyStatus' || property === 'userVerifyStatus') {
ElMessage({
message: t('session.restartRequired'),
type: 'warning',
offset: 0,
})
}
}
// 移除所有字段的监听器
const removeWatchers = ()=> {
watchers.forEach((stopWatcher) => stopWatcher()); // 调用每个监听器的停止方法
watchers = [];
}
const getConfigInfo = async () => {
removeWatchers()
try {
const args = {
partitionId: menuStore.currentPartitionId
}
const res = await ipc.invoke(ipcApiRoute.getProxyInfo, args);
if (res.status) {
Object.assign(proxyInfo.value, res.data); // 更新表单数据
}else {
console.log(res);
}
}catch(err) {
}finally {
addWatchers()
}
}
onMounted(() => {
getConfigInfo()
})
</script>
<style scoped lang="less">
.proxy-config {
width: 300px;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: var(--el-bg-color);
:deep(.el-text) {
--el-text-color:var(--el-text-color-primary);
}
:deep(.el-divider--horizontal) {
margin:14px 0;
}
.header-container {
width: 100%;
height: 30px;
display: flex;
align-items: center;
margin-bottom: 10px;
user-select: none;
justify-content: flex-start;
.header-title {
display: flex;
flex:1;
height: 30px;
}
.header-icon {
height: 30px;
display: flex;
flex:1;
}
.header-icon-add {
height: 30px;
display: flex;
align-items: center;
justify-content: flex-end;
flex:1;
cursor: pointer;
}
}
.content-container-select {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-select__wrapper) {
border-radius: 0;
}
:deep(.el-textarea__inner) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
align-items: center;
flex: 1;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
align-items: center;
flex: 2;
}
}
.content-container-input {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
flex: 1;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 2;
align-items: center;
}
}
.content-container-radio {
height: 50px;
width: 100%;
display: flex;
justify-content: flex-start;
.content-left {
height: 50px;
align-items: center;
display: flex;
flex: 2;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 1;
align-items: center;
}
}
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<div class="quick-reply">
<div class="header-container">
<div class="header-title">
<el-text tag="b" size="large">{{ t('quickReply.title') }}</el-text>
<el-tooltip
effect="dark"
placement="top"
>
<template #content>
<div style="max-width: 200px;">{{ t('quickReply.tooltipContent') }}</div>
</template>
<el-icon><QuestionFilled /></el-icon>
</el-tooltip>
</div>
</div>
<div class="header-search">
<el-input
:placeholder="t('quickReply.searchPlaceholder')"
@input="handleSearch"
v-model="searchKey"
:suffix-icon="Search">
</el-input>
</div>
<div class="content-container">
<el-empty
v-if="groups.length <= 0"
:description="t('quickReply.noData')"
/>
<el-collapse class="collapse-item" v-model="activeNames">
<el-collapse-item v-for="item in groups" :name="item.id">
<template #title>
<div class="collapse-item-title border">
<div class="left-icon">
<el-icon>
<ArrowDown v-if="isActive(item.id)" />
<ArrowRight v-else />
</el-icon>
</div>
<div class="right-text">
<el-text truncated>{{item.name}}</el-text>
</div>
</div>
</template>
<template #default>
<div class="collapse-item-content border-bottom">
<el-empty
v-if="item.contents.length <= 0"
:image-size="60"
:description="t('quickReply.noData')"
/>
<div
:style="{backgroundColor: record.bgColor || ''}"
v-for="record in item?.contents || []"
@click="handleClick(record)"
@mousedown="changeBgColor(record,'var(--el-border-color)')"
@mouseup="changeBgColor(record,'')">
<div class="remark">
<div class="left">
<el-text tag="b" truncated>
{{record.remark}}
</el-text>
</div>
<div class="right">
<el-button
size="small"
@click.stop="handleSend(record)"
plain>{{ t('quickReply.send') }}</el-button>
</div>
</div>
<div class="content">
<el-tooltip
:visible="record.visible || false"
effect="dark"
:content="record.content"
placement="top-start"
>
<template #content>
<div style="max-width: 220px;max-height: 150px;overflow: auto">
{{record.content}}
</div>
</template>
<el-text
@mouseenter="record.visible = true"
@mouseleave="record.visible = false"
truncated>{{record.content}}</el-text>
</el-tooltip>
</div>
</div>
</div>
</template>
</el-collapse-item>
</el-collapse>
</div>
</div>
</template>
<script setup>
import {ArrowDown, ArrowRight, CaretBottom, CaretTop, QuestionFilled, Search} from "@element-plus/icons-vue";
import {computed, onMounted, ref} from "vue";
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import { useMenuStore } from '@/stores/menuStore';
import {searchCollection} from "@/utils/common";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const menuStore = useMenuStore();
onMounted(async () => {
await initData();
})
const groups = ref([]);
const initData = async () => {
const res = await ipc.invoke(ipcApiRoute.getGroups, {})
if (res.status) {
groups.value = res.data
}
}
const searchKey = ref('')
const handleSearch = async () => {
if (searchKey.value === "") {
await initData();
}
groups.value = searchCollection(groups.value, searchKey.value, true);
}
const activeNames = ref([])
// 判断是否展开的通用方法
const isActive = computed(() => (name) => {
return activeNames.value.includes(name)
})
const changeBgColor = (record,color) => {
if (!record.bgColor) {
record.bgColor = ''; // 初始化 bgColor 属性
}
record.bgColor = color;
}
const handleClick = (record) => {
const args = {
text: record.content,
partitionId: menuStore.currentPartitionId,
type:'input',
}
ipc.invoke('send-msg',args)
}
const handleSend = (record) => {
const args = {
text: record.content,
partitionId: menuStore.currentPartitionId,
type:'send',
}
ipc.invoke('send-msg',args)
}
</script>
<style scoped lang="less">
.border-top {
border-top: 1px solid var(--el-border-color);
}
.border-bottom {
border-bottom: 1px solid var(--el-border-color);
}
.border {
border-width: 1px 0; /* 上下边框宽度为 1px左右边框宽度为 0 */
border-style: solid;
border-color: var(--el-border-color);
}
.quick-reply {
width: 300px;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
box-sizing: border-box;
background-color: var(--el-bg-color);
color: var(--el-text-color-primary);
:deep(.el-input__wrapper) {
border-radius: 0;
}
.header-container {
width: 100%;
height: 30px;
display: flex;
align-items: center;
margin-bottom: 20px;
user-select: none;
justify-content: flex-start;
:deep(.el-text) {
--el-text-color: var(--el-text-color-primary);
}
.header-title {
display: flex;
align-items: center;
flex:1;
gap: 5px;
height: 30px;
}
}
.header-search {
width: 100%;
margin-bottom: 20px;
}
.content-container {
height: 100%;
width: 100%;
overflow: auto;
border: 1px solid var(--el-border-color);
:deep(.el-collapse-item__header) {
border-bottom: none;
}
:deep(.el-collapse-item__wrap) {
border-bottom: none;
}
:deep(.el-collapse) {
border-bottom: none;
border-top: none;
}
:deep(.el-collapse-item__content) {
padding-bottom: 0;
}
.collapse-item {
width: 100%;
height: 50px;
.collapse-item-title {
background-color: var(--el-bg-color);
gap: 5px;
display: flex;
justify-content: start;
align-items: center;
width: 100%;
.left-icon {
display: flex;
justify-content: end;
align-items: center;
width: 20px;
}
.right-text {
display: flex;
justify-content: start;
align-items: center;
max-width: 200px;
flex:1
}
}
.collapse-item-content {
width: 100%;
cursor: pointer;
user-select: none;
.remark {
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
height: 40px;
width: 100%;
.left {
display: flex;
align-items: center;
flex:1;
height:40px;
max-width: 200px;
padding-left: 10px;
}
.right {
height: 40px;
display: flex;
align-items: center;
min-width: 50px;
}
}
.content{
height: 30px;
display: flex;
padding-left: 10px;
flex:1
}
}
}
.collapse-item :deep(.el-collapse-item__header .el-collapse-item__arrow) {
display: none !important;
}
}
}
</style>

View File

@ -0,0 +1,440 @@
<template>
<div class="translate-config">
<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">
<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>
<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 class="content-container-select">
<div class="content-left">
<el-text>{{ t('translate.route') }}</el-text>
</div>
<div class="content-right">
<el-select
v-model="configInfo.translateRoute"
:placeholder="t('translate.selectRoute')"
size="default"
style="width: 100%"
>
<el-option
v-for="item in menuStore.translationRoute"
:key="item.name"
:label="item[currentLanguageName]"
:value="item.name"
/>
</el-select>
</div>
</div>
<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-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 languageList"
: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.receiveTargetLanguage"
placeholder=""
size="default"
style="width: 100%"
>
<el-option
v-for="item in languageList"
: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 languageList"
: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 languageList"
: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.friendIndependent') }}</el-text>
</div>
<div class="content-right">
<el-switch
:active-value="'true'"
:inactive-value="'false'"
:disabled="configInfo.showAloneBtn === 'false'"
size="default"
v-model="configInfo.friendTranslateStatus"
/>
</div>
</div>
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('translate.preview') }}</el-text>
</div>
<div class="content-right">
<el-switch
:active-value="'true'"
:inactive-value="'false'"
size="default"
v-model="configInfo.translatePreview"
/>
</div>
</div>
</div>
</template>
<script setup>
import {computed, onMounted, onUnmounted, ref, unref, watch} from "vue";
import { ipc } from "@/utils/ipcRenderer";
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 { useI18n } from 'vue-i18n';
const menuStore = useMenuStore();
const configInfo = ref({})
const { t, locale } = useI18n();
// 修改计算属性名称使其更通用
const currentLanguageName = computed(() => {
return locale.value === 'zh' ? 'zhName' : 'enName';
});
watch(
() => configInfo.value.friendTranslateStatus,
async (newValue, oldValue) => {
if (newValue && oldValue) {
const res = await ipc.invoke(ipcApiRoute.changeAloneStatus, {partitionId: menuStore.currentPartitionId, status: newValue});
if (res.status) {
await getConfigInfo()
}
}
}
);
watch(
() => menuStore.currentPartitionId,
async (newValue, oldValue) => {
if (newValue) {
await getConfigInfo()
}
}
);
// 定义需要监听的字段
const propertiesToWatch = [
'mode',
'translateRoute',
"receiveTranslateStatus",
"receiveSourceLanguage",
"receiveTargetLanguage",
"sendTranslateStatus",
"sendSourceLanguage",
"sendTargetLanguage",
"chineseDetectionStatus",
"translatePreview",
];
let watchers = []; // 存储所有字段的监听器
// 初始化字段监听逻辑
const addWatchers = ()=> {
removeWatchers(); // 确保不会重复绑定监听器
watchers = propertiesToWatch.map((property) =>
watch(
() => unref(configInfo.value[property]),
(newValue, oldValue) => {
if (newValue !== "" && newValue !== oldValue) {
handlePropertyChange(property, newValue);
}
}
)
);
}
// 自定义逻辑
const handlePropertyChange = async (property, value) => {
const args = {key: property, value: value,id:configInfo.value.id,partitionId: menuStore.currentPartitionId};
await ipc.invoke(ipcApiRoute.updateTranslateConfig, args);
}
// 移除所有字段的监听器
const removeWatchers = ()=> {
watchers.forEach((stopWatcher) => stopWatcher()); // 调用每个监听器的停止方法
watchers = [];
}
const getConfigInfo = async () => {
removeWatchers()
const platform = await menuStore.platform;
const partitionId = await menuStore.currentPartitionId;
try {
const args = {
platform: menuStore.platform,
userId: '',
partitionId: menuStore.currentPartitionId
}
const res = await ipc.invoke(ipcApiRoute.getTrsConfig, args);
if (res.status) {
Object.assign(configInfo.value, res.data); // 更新表单数据
}else {
// console.log(res);
}
}catch(err) {
}finally {
addWatchers()
}
}
onMounted(() => {
getConfigInfo()
})
// 清理缓存
const cleanMsgCache = () => {
// 实现清理逻辑
}
// 语言列表
const languageList = ref([])
const getLanguageList = async () => {
const res = await ipc.invoke(ipcApiRoute.getLanguageList, {});
// console.log(res)
if (res.status) {
languageList.value = res.data;
}else {
ElMessage({
message: `${res.message}`,
type: 'error',
offset: 40,
})
}
}
onMounted(() => {
getLanguageList()
})
// 生命周期
onMounted(() => {
ipc.removeAllListeners('translate-config-update')
ipc.on('translate-config-update', (event, args) => {
const {data} = args
configInfo.value = data
})
})
onUnmounted(() => {
ipc.removeAllListeners('translate-config-update')
})
</script>
<style scoped lang="less">
.translate-config {
width: 300px;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
box-sizing: border-box;
:deep(.el-text) {
--el-text-color:var(--el-text-color-primary);
}
:deep(.el-divider--horizontal) {
margin:14px 0;
}
.header-container {
width: 100%;
height: 30px;
display: flex;
align-items: center;
margin-bottom: 10px;
user-select: none;
justify-content: flex-start;
.header-title {
display: flex;
flex:1;
height: 30px;
}
.header-icon {
height: 30px;
display: flex;
margin-left: 5px;
cursor: pointer;
}
}
.content-container-select {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-select__wrapper) {
border-radius: 0;
}
.content-left {
height: 50px;
align-items: center;
display: flex;
flex: 1;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 2;
align-items: center;
}
}
.content-container-radio {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
.content-left {
height: 50px;
align-items: center;
display: flex;
flex: 2;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 1;
align-items: center;
}
}
.content-container-radio-group {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
.content-left {
height: 50px;
align-items: center;
display: flex;
flex: 1;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 2;
align-items: center;
}
}
}
</style>

View File

@ -0,0 +1,485 @@
<template>
<div class="user-info">
<div class="header-container">
<div class="header-title">
<el-text tag="b" size="large">{{ t('userInfo.title') }}</el-text>
</div>
</div>
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('userInfo.phoneNumber') }}</el-text>
</div>
<div class="content-right">
<el-input :disabled="true" v-model="userInfo.phoneNumber"></el-input>
</div>
</div>
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('userInfo.nickname') }}</el-text>
</div>
<div class="content-right">
<el-input :disabled="hasUserId" :placeholder="t('userInfo.nickname')" v-model="userInfo.nickName"></el-input>
</div>
</div>
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('userInfo.country') }}</el-text>
</div>
<div class="content-right">
<el-input :disabled="hasUserId" :placeholder="t('userInfo.country')" v-model="userInfo.country"></el-input>
</div>
</div>
<div class="content-container-radio">
<div class="content-left">
<el-text>{{ t('userInfo.gender') }}</el-text>
</div>
<div class="content-right">
<el-radio-group :disabled="hasUserId" v-model="userInfo.gender">
<el-radio value="man">{{ t('userInfo.male') }}</el-radio>
<el-radio value="woman">{{ t('userInfo.female') }}</el-radio>
</el-radio-group>
</div>
</div>
<el-divider />
<div class="header-container">
<el-text class="header-title" tag="b" size="large">{{ t('userInfo.salesInfo') }}</el-text>
</div>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('userInfo.tradeActivity') }}</el-text>
</div>
<div class="content-right">
<el-select
:disabled="hasUserId"
v-model="userInfo.gradeActivity"
:placeholder="t('userInfo.selectPlaceholder')"
size="default"
style="width: 100%"
>
<el-option
v-for="item in activityArr"
:key="item.id"
:label="t(`userInfo.activityStatus.${item.name}`)"
:value="item.id"
/>
</el-select>
</div>
</div>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('userInfo.customerLevel') }}</el-text>
</div>
<div class="content-right">
<el-select
:disabled="hasUserId"
v-model="userInfo.customLevel"
:placeholder="t('userInfo.selectPlaceholder')"
size="default"
style="width: 100%"
>
<el-option
v-for="item in levelArr"
:key="item.id"
:label="t(`userInfo.customerLevels.${item.name}`)"
:value="item.id"
/>
</el-select>
</div>
</div>
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('userInfo.remarks') }}</el-text>
</div>
<div class="content-right">
<el-input
:disabled="hasUserId"
type="textarea"
v-model="userInfo.remarks"
:placeholder="t('userInfo.enterRemarks')"
size="default"
style="width: 100%"
>
</el-input>
</div>
</div>
<el-divider />
<div class="header-container">
<div class="header-title">
<el-text tag="b" size="large">{{ t('userInfo.followUpRecords') }}</el-text>
</div>
<div class="header-icon-add">
<el-icon @click="handleAdd">
<CirclePlus />
</el-icon>
</div>
</div>
<div class="content-container-scroll">
<el-scrollbar>
<el-timeline placement="bottom" style="width: 220px;">
<el-timeline-item
v-for="(activity, index) in sortedFollowRecords"
:key="activity.id">
<div class="scroll-content">
<div class="content-input">
<div class="input-top">
<el-input @focus="handleFocus(activity)" @blur="handleBlur(activity)" v-model="activity.content" type="textarea" :autosize="{ minRows: 1, maxRows: 6 }"/>
</div>
<div class="input-bottom">
<el-text type="info" size="small" >{{activity.timestamp}}</el-text>
</div>
</div>
<div class="content-icon">
<div v-show="activity.id !== ''" class="delete-icon">
<el-icon @click="handleDelete(activity)">
<Delete />
</el-icon>
</div>
</div>
</div>
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</div>
</div>
</template>
<script setup>
import CleanIcon from "@/components/icons/CleanIcon.vue";
import {computed, onMounted, onUnmounted, ref, unref, watch} from "vue";
import {Check, CirclePlus, Delete, Edit} from "@element-plus/icons-vue";
import { ipc } from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import { useMenuStore } from '@/stores/menuStore';
import { useI18n } from 'vue-i18n';
const menuStore = useMenuStore();
const { t } = useI18n();
// 获取当前时间字符串
const getTimeStr = (date = new Date())=> {
const pad = n => n.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ` +
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
const userInfo = ref({})
watch(
() => menuStore.currentPartitionId,
async (newValue, oldValue) => {
if (newValue) {
await getUserInfo()
}
}
);
const hasUserId = computed(()=>{
return !userInfo.value.id;
})
// 添加计算属性
const sortedFollowRecords = computed(() => {
// 创建新数组避免修改原数组
return [...followRecordArr.value].sort((a, b) => {
// 升序排列a.id - b.id
// 降序排列b.id - a.id (假设 id 是数字)
return b.id - a.id;
});
});
// 定义需要监听的字段
const propertiesToWatch = [
"phoneNumber",
"nickName",
"country",
"gender",
"gradeActivity",
"customLevel",
"remarks",
];
let watchers = []; // 存储所有字段的监听器
// 初始化字段监听逻辑
const addWatchers = ()=> {
removeWatchers(); // 确保不会重复绑定监听器
watchers = propertiesToWatch.map((property) =>
watch(
() => unref(userInfo.value[property]),
(newValue, oldValue) => {
if (newValue !== "" && newValue !== oldValue) {
handlePropertyChange(property, newValue);
}
}
)
);
}
// 自定义逻辑
const handlePropertyChange = async (property, value) => {
if (userInfo && userInfo.value?.id){
const args = {key: property, value: value,id:userInfo.value.id};
await ipc.invoke(ipcApiRoute.updateContactInfo, args);
}
}
// 移除所有字段的监听器
const removeWatchers = ()=> {
watchers.forEach((stopWatcher) => stopWatcher()); // 调用每个监听器的停止方法
watchers = [];
}
const followRecordArr = ref([])
const getUserInfo = async () => {
removeWatchers()
try {
const args = {
platform: menuStore.platform,
userId: '',
partitionId: menuStore.currentPartitionId
}
const res = await ipc.invoke(ipcApiRoute.getContactInfo, args);
if (res.status) {
Object.assign(userInfo.value, res.data.userInfo); // 更新表单数据
Object.assign(followRecordArr.value, res.data.records); // 更新表单数据
}else {
userInfo.value = {};
followRecordArr.value = []
}
}catch(err) {
console.error("获取配置失败:", error.message);
}finally {
addWatchers()
}
}
onMounted(async () => {
await getUserInfo()
})
// 生命周期
onMounted(async () => {
await ipc.removeAllListeners('contact-data-update')
await ipc.on('contact-data-update', (event, args) => {
const {data} = args
userInfo.value = data.userInfo;
followRecordArr.value = data.records;
})
})
onUnmounted(() => {
ipc.removeAllListeners('contact-data-update')
})
const activityArr = ref([
{id:1, name:'negotiating'},
{id:2, name:'scheduled'},
{id:3, name:'ordered'},
{id:4, name:'paid'},
{id:5, name:'shipped'}
])
const levelArr = ref([
{id: 1, name: 'normal'},
{id: 2, name: 'medium'},
{id: 3, name: 'important'},
{id: 4, name: 'critical'}
])
const handleFocus = (activity) => {
}
const handleBlur = async (activity) => {
const args = {id: activity.id,content: activity.content}
const res = await ipc.invoke(ipcApiRoute.updateFollowRecord, args);
}
const handleAdd = async () => {
if (!hasUserId) return;
const data = {
userId: userInfo.value.userId,
platform: menuStore.platform,
content: '',
timestamp: getTimeStr(),
}
const res = await ipc.invoke(ipcApiRoute.addFollowRecord, data)
if (res.status) {
followRecordArr.value.unshift(res.data)
}
}
const handleDelete = async (activity) => {
const index = followRecordArr.value.indexOf(activity)
if (index > -1) {
const args = {
id:activity.id,
userId: userInfo.value.id,
platform: menuStore.platform,
}
const res = await ipc.invoke(ipcApiRoute.deleteFollowRecord, args)
if (res.status) {
followRecordArr.value.splice(index, 1)
}
}
}
</script>
<style scoped lang="less">
.user-info {
width: 300px;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
box-sizing: border-box;
:deep(.el-text) {
--el-text-color:var(--el-text-color-primary);
}
:deep(.el-divider--horizontal) {
margin:14px 0;
}
.header-container {
width: 100%;
height: 30px;
display: flex;
align-items: center;
margin-bottom: 10px;
user-select: none;
justify-content: flex-start;
.header-title {
display: flex;
flex:1;
height: 30px;
}
.header-icon {
height: 30px;
display: flex;
flex:1;
}
.header-icon-add {
height: 30px;
display: flex;
align-items: center;
justify-content: flex-end;
flex:1;
cursor: pointer;
}
}
.content-container-select {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-select__wrapper) {
border-radius: 0;
}
:deep(.el-textarea__inner) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
align-items: center;
flex: 1;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
align-items: center;
flex: 2;
}
}
.content-container-input {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
flex: 1;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 2;
align-items: center;
}
}
.content-container-radio {
height: 50px;
width: 100%;
display: flex;
justify-content: flex-start;
.content-left {
height: 50px;
align-items: center;
display: flex;
flex: 1;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 2;
align-items: center;
}
}
.content-container-scroll {
height: calc(100% - 530px);
width: 100%;
box-sizing: border-box;
display: flex;
justify-content: flex-start;
:deep(.el-timeline-item__wrapper) {
top:0;
right:15px;
}
.scroll-content {
width: 100%;
min-height: 50px;
display: flex;
:deep(.el-text) {
--el-text-color:var(--el-color-info);
}
.content-input {
:deep(.el-textarea__inner) {
border-radius: 0;
}
min-height: 50px;
display: flex;
flex: 4;
flex-direction: column; /* 垂直排列 */
.input-top {
display: flex;
flex:1;
}
.input-bottom {
display: flex;
flex:1;
}
}
.content-icon {
height: 100%;
width: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column; /* 垂直排列 */
.add-icon {
cursor: pointer;
display: flex;
align-items: end;
flex: 1;
}
.delete-icon {
cursor: pointer;
display: flex;
align-items: start;
flex: 1;
}
//background-color: #343534;
}
}
}
}
</style>

View File

@ -0,0 +1,256 @@
<template>
<div ref="rightMenu" class="right-menu">
<div v-show="isCollapsed" class="right-menu-container">
<component :is="currentPage" />
</div>
<div class="right-menu-header">
<!-- 折叠按钮 -->
<div
class="menu-item collapse-button"
:class="{ active: isCollapsed }"
@click="toggleCollapse"
>
<el-icon :size="22" class="menu-icon">
<component :is="isCollapsed ? FoldLeftIcon :FoldRightIcon" />
</el-icon>
</div>
<!-- 操作按钮 -->
<div
v-for="(item, index) in menuItems"
:key="index"
class="menu-item"
:class="{ active: activeMenu === item.action }"
@click="selectMenuItem(item.action)"
>
<el-icon :size="24" class="menu-icon">
<component :is="item.icon" />
</el-icon>
</div>
<div class="menu-item" @click="handleRefresh">
<el-icon :size="24" class="menu-icon">
<component :is="Refresh" />
</el-icon>
</div>
<div class="menu-item" @click="handleClose">
<el-icon :size="24" class="menu-icon">
<component :is="CircleClose" />
</el-icon>
</div>
<!-- <div class="menu-item" @click="handleDevTools">
<el-icon :size="24" class="menu-icon">
<component :is="ChromeFilled" />
</el-icon>
</div> -->
</div>
</div>
</template>
<script setup>
import {markRaw, onMounted, ref, watch} from 'vue'
import {ChromeFilled, CircleClose, Refresh, User} from '@element-plus/icons-vue'
import TranslateIcon from '@/components/icons/TranslateIcon.vue';
import QuickReplyIcon from '@/components/icons/QuickReplyIcon.vue';
import ServerIcon from '@/components/icons/ServerIcon.vue';
import FoldLeftIcon from "@/components/icons/FoldLeftIcon.vue";
import FoldRightIcon from "@/components/icons/FoldRightIcon.vue";
import TranslateConfig from "@/views/right-menu/TranslateConfig.vue";
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 {useMenuStore} from '@/stores/menuStore';
import {ipcApiRoute} from "@/api";
import {ipc} from "@/utils/ipcRenderer";
import {ElMessage} from "element-plus";
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
const menuStore = useMenuStore();
// 菜单页面配置
const pageItems = [
{name:'TranslateConfig', component: markRaw(TranslateConfig) },
{name:'UserInfo', component: markRaw(UserInfo) },
{name:'QuickReply', component: markRaw(QuickReply) },
{name:'ProxyConfig', component: markRaw(ProxyConfig) }
]
// 菜单图标配置
const menuItems = [
{ icon: markRaw(TranslateIcon), action: 'TranslateConfig' },
{ icon: markRaw(User), action: 'UserInfo' },
{ icon: markRaw(QuickReplyIcon), action: 'QuickReply' },
{ icon: markRaw(ServerIcon), action: 'ProxyConfig' }
]
// 响应式状态
const rightMenu = ref(null);
const currentPage = ref(markRaw(TranslateConfig));
const activeMenu = ref('TranslateConfig');
const isCollapsed = ref(menuStore.rightFoldStatus);
const devToolsStatus = ref(false);
// 监听当前激活的菜单,更新显示的组件
watch(
() => activeMenu.value,
(action) => {
currentPage.value = pageItems.find(item => item.name === action)?.component
},
{ immediate: true }
);
// 处理菜单宽度变化
const processWidth = () => {
if (!rightMenu.value) return;
const width = isCollapsed.value ? '350px' : '50px';
rightMenu.value.style.width = width;
menuStore.setRightFoldStatus(isCollapsed.value);
}
// 监听折叠状态变化
watch(() => isCollapsed.value, processWidth, { immediate: false });
// 菜单项选择处理
const selectMenuItem = (action) => {
if (action === activeMenu.value) {
// 当点击相同的菜单项时,切换折叠状态
isCollapsed.value = !isCollapsed.value;
return;
}
// 点击不同菜单项时,切换菜单并展开
activeMenu.value = action;
menuStore.setRightContent(activeMenu.value);
isCollapsed.value = true;
}
// 折叠/展开处理
const toggleCollapse = () => {
isCollapsed.value = !isCollapsed.value;
};
// 刷新当前会话
const handleRefresh = async () => {
ElMessage({
message: t('rightMenu.refreshTip'),
type: 'warning',
offset: 0,
});
await ipc.invoke(ipcApiRoute.refreshSession, {
partitionId: menuStore.currentMenu
});
}
// 关闭当前会话
const handleClose = async () => {
await ipc.invoke(ipcApiRoute.closeSession, {
partitionId: menuStore.currentMenu
});
const menuRes = await ipc.invoke(ipcApiRoute.getSessionByPartitionId, {
partitionId: menuStore.currentMenu
});
if (menuRes.status) {
menuStore.updateChildrenMenu(menuRes.data.session);
menuStore.setCurrentMenu(menuRes.data.session.platform);
}
}
// 开发者工具控制
const handleDevTools = async () => {
await ipc.invoke(ipcApiRoute.openSessionDevTools, {
partitionId: menuStore.currentMenu,
status: devToolsStatus.value
});
devToolsStatus.value = !devToolsStatus.value;
}
</script>
<style scoped lang="less">
.right-menu {
position: relative;
height: calc(100vh - 30px);
width: 50px;
background-color: var(--el-bg-color);
.right-menu-header {
position: absolute;
right: 0;
top: 0;
width: 50px;
display: flex;
flex-direction: column;
align-items: center;
background: var(--el-bg-color);
z-index: 2;
.menu-item {
width: 50px;
height: 50px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
cursor: pointer;
&:not(.collapse-button).active {
&::before {
content: '';
position: absolute;
left: 0;
top: 0;
width: 2px;
height: 50px;
background: var(--el-color-primary);
border-radius: 0 2px 2px 0;
animation: slideIn 0.3s ease;
}
}
.menu-icon {
color: var(--el-text-color-primary);
transition: color 0.3s ease;
}
&:hover {
background-color: var(--el-color-primary-light-9);
.menu-icon {
color: var(--el-color-primary-light-3);
}
}
&.active {
.menu-icon {
color: var(--el-color-primary);
transform: scale(1.1);
transition: all 0.3s ease;
}
}
}
}
.right-menu-container {
left: 0;
top: 0;
width: 300px;
height: calc(100vh - 30px);
overflow-y: auto;
z-index: 1;
background-color: var(--el-bg-color);
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
</style>

View File

@ -0,0 +1,681 @@
<script setup>
import {CirclePlus, CirclePlusFilled, EditPen, Search, User} from "@element-plus/icons-vue";
import {computed, nextTick, onMounted, ref, watch} from 'vue';
import { useI18n } from 'vue-i18n';
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import google from '@/components/icons/GoogleIcon.vue';
import { useMenuStore } from '@/stores/menuStore';
import {ElMessage} from "element-plus";
const { t } = useI18n();
const menuStore = useMenuStore();
// 存储选中ID的集合
const selectedRows = ref([])
const tableData = ref([]);
const getTableData = async () => {
tableData.value = await menuStore.getCurrentChildren();
}
onMounted(async () => {
await getTableData();
})
// 监听 currentMenu 的变化
watch(
() => menuStore.currentMenu, // 监听的 store 中的值
async (newValue, oldValue) => {
await getTableData();
}
);
const isCustomWeb = computed( () => {
return menuStore.currentMenu === 'CustomWeb'
})
const dialogVisible = ref(false);
const proxyDialogVisible = ref(false);
const form = ref({
url: '',
nickname: ''
});
const proxyForm = ref({
id:'',
proxyStatus:'',
proxyType:'',
proxyIp:'',
proxyPort:'',
userVerifyStatus:'',
username:'',
password:'',
});
const getProxyInfo = async (row) => {
const partitionId = row.partitionId;
const res = await ipc.invoke(ipcApiRoute.getProxyInfo, {partitionId})
if (res.status) {
proxyForm.value = res.data;
}
proxyDialogVisible.value = true;
}
const saveProxyConfig = async () => {
// 构建参数对象
const args = {
id: proxyForm.value.id,
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,
};
// 调用主进程方法
const res = await ipc.invoke(ipcApiRoute.saveProxyInfo, args);
// 根据返回结果处理
if (res.status) {
ElMessage({
message: t('session.proxyConfigSaveSuccess'),
type: 'success',
offset: 40,
})
proxyDialogVisible.value = false;
} else {
ElMessage({
message: t('common.error'),
type: 'error',
offset: 40,
})
}
}
// 表单校验规则
const rules = ref({
url: [
{ required: true, message: '网址不能为空', trigger: 'blur' }
]
});
const formRef = ref(null);
const handleConfirm = async () => {
formRef.value.validate(async (valid) => {
if (valid) {
const args = {
platform:menuStore.currentMenu,
url: form.value.url,
nickname: form.value.nickname,
}
const res = await ipc.invoke(ipcApiRoute.addSession, args)
if (res.status) {
ElMessage({
message: `${res.message}`,
type: 'success',
offset: 40,
})
form.value = {}
dialogVisible.value = false;
const partitionId = res.data.partitionId
const res2 = await ipc.invoke(ipcApiRoute.getSessionByPartitionId, {platform: menuStore.currentMenu,partitionId: partitionId})
if (res2.status) {
menuStore.addChildrenMenu(res2.data.session)
}
}else {
ElMessage({
message: `${res.message}`,
type: 'error',
offset: 40,
})
}
}
});
};
const setWindowLocation = async ()=> {
const locationInfo = menuStore.locationInfo;
await ipc.invoke(ipcApiRoute.setWindowLocation,{x:locationInfo.x,y:locationInfo.y,width:locationInfo.width,height:locationInfo.height});
}
// 选择变化时的处理函数
const handleSelectionChange = (rows) => {
selectedRows.value = rows
}
const handleAddSession = async () => {
if (isCustomWeb.value) {
//自定义 web 逻辑
dialogVisible.value = true
}else {
const res = await ipc.invoke(ipcApiRoute.addSession, {platform: menuStore.currentMenu})
if (res.status) {
const partitionId = res.data.partitionId
const res2 = await ipc.invoke(ipcApiRoute.getSessionByPartitionId, {platform: menuStore.currentMenu,partitionId: partitionId})
if (res2.status) {
menuStore.addChildrenMenu(res2.data.session)
}
}
}
}
const handleStartSession = async (row) => {
if (row.windowStatus === 'true') {
const res = await ipc.invoke(ipcApiRoute.showSession, {platform: row.platform,partitionId: row.partitionId})
if (res.status) {
await setWindowLocation()
const pMenu = menuStore.getParentMenuById(row.partitionId)
if (pMenu) {
pMenu.openChildren = true
}
await ipc.invoke(ipcApiRoute.showSession, {platform:row.platform,partitionId: row.partitionId})
menuStore.setCurrentMenu(row.partitionId);
menuStore.setCurrentPlatform(row.platform);
menuStore.setCurrentPartitionId(row.partitionId);
}
}else {
ElMessage({
message: `${t('session.starting')}`,
type: 'warning',
offset: 40,
})
// 启动这个会话
const res = await ipc.invoke(ipcApiRoute.startSession, {platform: row.platform,partitionId: row.partitionId})
if (res.status) {
menuStore.updateChildrenMenu(res.data);
}else {
ElMessage({
message: `${res.message}`,
type: 'error',
offset: 40,
})
}
}
}
const startAll = async ()=>{
ElMessage({
message: `${t('session.starting')}`,
type: 'warning',
offset: 40,
})
const platform = menuStore.currentMenu;
for (let item of tableData.value) {
const res = await ipc.invoke(ipcApiRoute.startSession, {platform:platform,partitionId: item?.partitionId})
if (res.status) {
menuStore.addChildrenMenu(item);
menuStore.updateChildrenMenu(res.data);
}
}
}
const closeAll = async ()=>{
const platform = menuStore.currentMenu;
for (let item of tableData.value) {
const res = await ipc.invoke(ipcApiRoute.closeSession, {platform:platform,partitionId: item?.partitionId})
if (res.status) {
menuStore.updateChildrenMenu(res.data);
}
}
ElMessage({
message: `${t('session.closeSuccess')}`,
type: 'success',
offset: 40,
})
}
const deleteAll = async () => {
const platform = menuStore.currentMenu;
for (const row of selectedRows.value) {
const res = await ipc.invoke(ipcApiRoute.deleteSession, {platform: platform, partitionId: row.partitionId});
if (res.status) {
menuStore.deleteChildrenMenu(row);
}
}
await getTableData();
};
const deleteSession = async (row)=>{
const platform = row.platform;
const partitionId = row.partitionId;
const res = await ipc.invoke(ipcApiRoute.deleteSession, {platform:platform,partitionId: partitionId})
if (res.status) {
menuStore.deleteChildrenMenu(row);
}
}
const closeSession = async (row)=>{
const platform = row.platform;
const partitionId = row.partitionId;
const res = await ipc.invoke(ipcApiRoute.closeSession, {platform:platform,partitionId: partitionId})
if (res.status) {
menuStore.updateChildrenMenu(res.data);
}
}
const changeRemarks = async (row)=>{
// 通过解构直接创建 args
const args = (({
id, windowId, windowStatus, onlineStatus, remarks,
webUrl, avatarUrl, userName, msgCount, nickName
}) => ({
id, windowId, windowStatus, onlineStatus, remarks,
webUrl, avatarUrl, userName, msgCount, nickName
}))(row)
await ipc.invoke(ipcApiRoute.editSession, args)
}
</script>
<template>
<div class="session-list">
<!--新建会话弹出层-->
<el-dialog v-model="dialogVisible" :title="t('session.newChat')" width="600" height="250">
<div class="add-session-dialog-form">
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
<el-form-item :label="t('session.url')" prop="url">
<el-input v-model="form.url" placeholder="https://"></el-input>
</el-form-item>
<el-form-item :label="t('session.nickname')">
<el-input v-model="form.nickname" :placeholder="t('session.nickname')"></el-input>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="add-session-dialog-footer">
<el-button @click="dialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="handleConfirm">{{ t('common.confirm') }}</el-button>
</span>
</template>
</el-dialog>
<!--代理设置弹出层-->
<el-dialog v-model="proxyDialogVisible" :title="t('session.proxyConfig')" width="550">
<div class="proxy-config-dialog-form">
<!-- 代理开关-->
<div class="content-container-radio">
<div class="content-left">
<el-switch
:inactive-text="t('session.proxyStatus')"
:active-text="t('session.proxyStatus')"
:active-value="'true'"
:inactive-value="'false'"
size="default"
v-model="proxyForm.proxyStatus"/>
</div>
</div>
<!-- 代理协议类型-->
<div class="content-container-select">
<div class="content-left">
<el-text>{{ t('session.proxyType') }}</el-text>
</div>
<div class="content-right">
<el-select
v-model=" proxyForm.proxyType"
:placeholder="t('session.proxyType')"
size="default"
style="width: 100%"
>
<el-option label="HTTP" value="http"/>
<el-option label="HTTPS" value="https"/>
<el-option label="SOCKS4" value="socks4"/>
<el-option label="SOCKS5" value="socks5"/>
</el-select>
</div>
</div>
<!-- 代理主机 ip-->
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('session.proxyIp') }}</el-text>
</div>
<div class="content-right">
<el-input :placeholder="t('session.proxyIp')" v-model="proxyForm.proxyIp"></el-input>
</div>
</div>
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('session.proxyPort') }}</el-text>
</div>
<div class="content-right">
<el-input :placeholder="t('session.proxyPort')" v-model="proxyForm.proxyPort"></el-input>
</div>
</div>
<!-- 代理服务器验证开关-->
<div class="content-container-radio">
<div class="content-left">
<el-switch
:inactive-text="t('session.proxyDisabled')"
:active-text="t('session.proxyEnabled')"
:active-value="'true'"
:inactive-value="'false'"
size="default"
v-model="proxyForm.userVerifyStatus"
/>
</div>
</div>
<!-- 用户名 密码-->
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('session.username') }}</el-text>
</div>
<div class="content-right">
<el-input :placeholder="t('session.username')" v-model="proxyForm.username"></el-input>
</div>
</div>
<div class="content-container-input">
<div class="content-left">
<el-text>{{ t('session.password') }}</el-text>
</div>
<div class="content-right">
<el-input :placeholder="t('session.password')" v-model="proxyForm.password"></el-input>
</div>
</div>
</div>
<template #footer>
<span class="proxy-config-dialog-footer">
<el-button @click="proxyDialogVisible = false">{{ t('common.cancel') }}</el-button>
<el-button type="primary" @click="saveProxyConfig">{{ t('common.confirm') }}</el-button>
</span>
</template>
</el-dialog>
<!--顶部操作栏-->
<div class="session-list-header">
<div class="session-list-header-title">
<el-text tag="b">{{ t('session.sessionList') }}</el-text>
</div>
<div class="session-list-header-add" @click="handleAddSession">
<el-icon :size="20" color="gray"><CirclePlusFilled /></el-icon>
<el-text>{{ t('session.newChat') }}</el-text>
</div>
<div class="session-list-header-btn-group">
<el-button size="small" type="primary" round @click="startAll">{{ t('session.startAll') }}</el-button>
<el-button size="small" type="danger" round @click="closeAll">{{ t('session.closeAll') }}</el-button>
<el-popconfirm
:confirm-button-text="t('common.yes')"
:cancel-button-text="t('common.no')"
@confirm="deleteAll"
:title="t('session.confirmDelete')">
<template #reference>
<el-button size="small" type="danger" round :disabled="selectedRows.length === 0">{{ t('session.batchDelete') }}</el-button>
</template>
</el-popconfirm>
<el-popover
placement="bottom"
:width="200"
trigger="click">
<template #reference>
<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')"/>
</div>
</el-popover>
</div>
</div>
<!--表格部分-->
<div class="table-data">
<el-table
border
height="100%"
:data="tableData"
@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">
<template #default="{ row }" v-if="!isCustomWeb">
<div class="session-record">
<div class="session-record-avatar">
<el-badge
:offset="[-40,15]"
is-dot
:type="row.onlineStatus === 'true' ? 'success' : 'info'"
>
<!-- 如果有头像展示头像 -->
<el-avatar
v-if="row.avatarUrl"
:src="row.avatarUrl"
style="height: 30px;width: 30px;"
/>
<el-avatar v-else style="height: 30px;width: 30px;">
<el-icon><User /></el-icon>
</el-avatar>
</el-badge>
</div>
<div class="session-record-nickname">
<el-text>{{row.nickName ? row.nickName : '-'}}</el-text>
</div>
</div>
</template>
<template #default="{ row }" v-if="isCustomWeb">
<div class="session-record-custom-web">
<div class="avatar">
<el-icon size="35" color="#30ace1">
<component :is="google"/>
</el-icon>
</div>
<div class="nickname">
<el-text :truncated="true">
{{row.nickName ? row.nickName : '-'}}
</el-text>
</div>
</div>
</template>
</el-table-column>
<el-table-column v-if="!isCustomWeb" align="center" prop="userName" :show-overflow-tooltip="true" :label="t('session.username')">
<template #default="{ row }">
<el-text>{{row.userName? row.userName:'-'}}</el-text>
</template>
</el-table-column>
<el-table-column v-if="isCustomWeb" align="center" prop="webUrl" :show-overflow-tooltip="true" :label="t('session.url')">
<template #default="{ row }">
<el-text>{{row.webUrl? row.webUrl:'-'}}</el-text>
</template>
</el-table-column>
<el-table-column align="center" :show-overflow-tooltip="true" prop="remarks" :label="t('session.remarks')">
<template #default="{ row }">
<div class="remarks-column">
<div class="remarks-input">
<el-input
:placeholder="t('session.remarks')"
@input="changeRemarks(row)"
v-model="row.remarks"/>
</div>
</div>
</template>
</el-table-column>
<el-table-column align="center" :label="t('common.operation')" min-width="180" fixed="right">
<template #default="{ row }">
<el-button style="margin-left: 5px" :type=" row.windowStatus === 'true' ? 'success' : 'primary'" size="small" @click="handleStartSession(row)" round>{{ row.windowStatus === 'true' ? t('session.show') : t('session.start') }}</el-button>
<el-button v-if="row.windowStatus === 'true'" style="margin-left: 5px" type="danger" size="small" @click="closeSession(row)" round>{{ t('session.close') }}</el-button>
<el-popconfirm
:confirm-button-text="t('common.yes')"
:cancel-button-text="t('common.no')"
@confirm="deleteSession(row)"
v-if="row.windowStatus === 'false'"
:title="t('session.confirmDeleteSingle')">
<template #reference>
<el-button style="margin-left: 5px" type="danger" size="small" round>{{ t('session.delete') }}</el-button>
</template>
</el-popconfirm>
<el-button style="margin-left: 5px" type="primary" size="small" @click="getProxyInfo(row)" round>{{ t('session.proxySettings') }}</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>
<style scoped lang="less">
.session-list {
:deep(.el-form-item__label) {
color: var(--el-text-color-primary) !important;
}
height: 100%;
width: 100%; /* 新增:确保占满父容器 */
background-color: var(--el-bg-color);
display: flex; /* 新增启用flex布局 */
flex-direction: column; /* 纵向排列 */
.add-session-dialog-form {
padding-top:10px;
:deep(.el-input__wrapper) {
border-radius: 0;
}
}
.add-session-dialog-footer {
:deep(.el-button) {
border-radius: 0;
}
}
.proxy-config-dialog-form {
display: flex;
flex-direction: column;
width:100%;
:deep(.el-input__wrapper) {
border-radius: 0;
}
.content-container-select {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-select__wrapper) {
border-radius: 0;
}
:deep(.el-textarea__inner) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
align-items: center;
flex: 1;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
align-items: center;
flex: 2;
}
}
.content-container-input {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
flex: 1;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 2;
align-items: center;
}
}
.content-container-radio {
height: 50px;
width: 100%;
display: flex;
justify-content: flex-start;
.content-left {
height: 50px;
align-items: center;
display: flex;
flex: 2;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 1;
align-items: center;
}
}
}
.proxy-config-dialog-footer {
:deep(.el-button) {
border-radius: 0;
}
}
.session-list-header {
flex-shrink: 0; /* 禁止头部收缩 */
user-select: none; /* 禁止文本选择 */
display: flex;
height: 45px;
width: 100%;
background-color: var(--el-bg-color-page);
.session-list-header-title {
display: flex;
justify-content: center;
height: 45px;
}
.session-list-header-add {
margin-left: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 45px;
}
.session-list-header-btn-group {
margin-left: 10px;
display: flex;
flex: 1;
justify-content: start;
align-items: center;
height: 45px;
.search-bth {
display: flex;
:deep(.el-input__wrapper) {
border-radius: 0;
}
}
}
}
.table-data {
flex: 1; /* 占据剩余空间 */
min-height: 0; /* 修复Safari的flex计算问题 */
overflow: auto;
:deep(.el-table__row) {
max-height: 54px;
}
.session-record {
display: flex;
width: 100%;
.session-record-avatar {
display: flex;
flex: 1;
justify-content: flex-end;
}
.session-record-nickname {
margin-left: 10px;
display: flex;
flex: 1;
justify-content: flex-start;
}
}
//自定义 web 会话记录表格列样式
.session-record-custom-web {
display: flex;
width: 100%;
.avatar {
display: flex;
flex: 2;
justify-content: flex-end;
}
.nickname {
margin-left: 10px;
display: flex;
flex: 3;
align-items: center;
justify-content: flex-start;
}
}
.remarks-column {
display: flex;
flex-direction: row;
:deep(.el-input__wrapper) {
border-radius: 0;
}
.remarks-input{
justify-content: center;
display: flex;
flex: 1;
}
}
}
}
</style>

View File

@ -0,0 +1,127 @@
<template>
<div class="baidu-translate">
<el-divider content-position="left">基础信息配置</el-divider>
<div class="bd-content-input">
<div class="content-left">
<el-text>URL</el-text>
</div>
<div class="content-right">
<el-input disabled v-model="config.apiUrl" placeholder="百度翻译API请求地址">
<!-- 使用 append 插槽来添加按钮 -->
<template #append>
<el-button type="success" @click="handleTest" size="small">测试连接</el-button>
</template>
</el-input>
</div>
</div>
<div class="bd-content-input">
<div class="content-left">
<el-text>appId</el-text>
</div>
<div class="content-right">
<el-input v-model="config.appId" type="password" show-password placeholder="百度翻译应用ID"></el-input>
</div>
</div>
<div class="bd-content-input">
<div class="content-left">
<el-text>apiKey</el-text>
</div>
<div class="content-right">
<el-input v-model="config.apiKey" type="password" show-password placeholder="百度翻译授权API密钥"></el-input>
</div>
</div>
</div>
</template>
<script setup>
import {ref, computed, watch, onMounted} from 'vue';
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import {ElMessage} from "element-plus";
const props = defineProps({
tabName: {
type: String,
required: true
}
});
const config = ref({
apiUrl:'',
appId:'',
apiKey:'',
});
const editConfig = async () => {
const args = {
name: props.tabName,
dataJson: jsonConfig.value
}
await ipc.invoke(ipcApiRoute.editTranslateRoute, args)
}
const handleTest = async () => {
const res = await ipc.invoke(ipcApiRoute.testRoute, {name: props.tabName})
if (res.status) {
ElMessage({
message: `翻译成功:${res.data}`,
type: 'success',
})
}else {
ElMessage({
message: `翻译失败:${res.message}`,
type: 'error',
offset: 40,
})
}
}
// 计算属性:将 config 转换成 JSON 格式
const jsonConfig = computed(() => {
return JSON.stringify(config.value, null, 2); // 格式化 JSON 数据
});
onMounted(async () => {
const res = await ipc.invoke(ipcApiRoute.getRouteConfig, {name: props.tabName})
if (res.status) {
config.value = res.data;
}
})
// 监听config变化在必要时调用editConfig
watch(config, async (newVal, oldVal) => {
await editConfig();
}, { deep: true, immediate: false });
</script>
<style scoped lang="less">
.baidu-translate {
height: 200px;
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
.bd-content-input {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
width: 100px;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 1;
align-items: center;
}
}
}
</style>

View File

@ -0,0 +1,98 @@
<script setup>
import { ElIcon } from 'element-plus'
import { InfoFilled } from '@element-plus/icons-vue'
</script>
<template>
<div class="google-translate">
<!-- <el-divider content-position="left">基础信息配置</el-divider>-->
<div class="translate-notice">
<el-alert
title="提示"
type="info"
:closable="false"
class="custom-alert"
>
<template #default>
<div class="alert-content">
<el-icon :size="20" class="notice-icon">
<InfoFilled />
</el-icon>
<div class="notice-text">
<!-- <p class="main-text">当前谷歌翻译为网页版 API</p>-->
<p class="sub-text">谷歌翻译网页版接口</p>
<p class="hint-text">* 该接口可能不稳定如无法使用请使用其它翻译服务</p>
</div>
</div>
</template>
</el-alert>
</div>
</div>
</template>
<style scoped lang="less">
.google-translate {
height: 200px;
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
background-color: var(--el-bg-color);
color: var(--el-text-color-primary);
.translate-notice {
padding: 0 24px;
margin-top: 16px;
.custom-alert {
border-radius: 12px;
background-color: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-8);
:deep(.el-alert__title) {
font-size: var(--el-font-size-base);
color: var(--el-text-color-primary);
font-weight: 600;
}
.alert-content {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 8px 0;
.notice-icon {
color: var(--el-color-primary);
flex-shrink: 0;
margin-top: 2px;
font-size: 18px;
}
.notice-text {
.main-text {
font-size: var(--el-font-size-base);
color: var(--el-text-color-primary);
margin: 0 0 6px 0;
font-weight: 500;
}
.sub-text {
font-size: var(--el-font-size-extra-small);
color: var(--el-text-color-regular);
margin: 0 0 4px 0;
line-height: 1.4;
}
.hint-text {
font-size: var(--el-font-size-extra-small);
color: var(--el-text-color-secondary);
margin: 6px 0 0 0;
font-style: italic;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,128 @@
<template>
<div class="huoshan-translate">
<el-divider content-position="left">基础信息配置</el-divider>
<div class="hs-content-input">
<div class="content-left">
<el-text>URL</el-text>
</div>
<div class="content-right">
<el-input disabled v-model="config.apiUrl" placeholder="火山翻译API请求地址">
<!-- 使用 append 插槽来添加按钮 -->
<template #append>
<el-button type="success" @click="handleTest" size="small">测试连接</el-button>
</template>
</el-input>
</div>
</div>
<div class="hs-content-input">
<div class="content-left">
<el-text>apiKey</el-text>
</div>
<div class="content-right">
<el-input v-model="config.apiKey" type="password" show-password placeholder="火山翻译授权密钥KEY"></el-input>
</div>
</div>
<div class="hs-content-input">
<div class="content-left">
<el-text>secretKey</el-text>
</div>
<div class="content-right">
<el-input v-model="config.secretKey" type="password" show-password placeholder="火山翻译授权私钥"></el-input>
</div>
</div>
</div>
</template>
<script setup>
import {ref, computed, watch, onMounted} from 'vue';
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import {ElMessage} from "element-plus";
const props = defineProps({
tabName: {
type: String,
required: true
}
});
const config = ref({
apiUrl:'',
appId:'',
apiKey:'',
});
const editConfig = async () => {
const args = {
name: props.tabName,
dataJson: jsonConfig.value
}
await ipc.invoke(ipcApiRoute.editTranslateRoute, args)
}
const handleTest = async () => {
const res = await ipc.invoke(ipcApiRoute.testRoute, {name: props.tabName})
if (res.status) {
ElMessage({
message: `翻译成功:${res.data}`,
type: 'success',
offset: 40,
})
}else {
ElMessage({
message: `翻译失败:${res.message}`,
type: 'error',
offset: 40,
})
}
}
// 计算属性:将 config 转换成 JSON 格式
const jsonConfig = computed(() => {
return JSON.stringify(config.value, null, 2); // 格式化 JSON 数据
});
onMounted(async () => {
const res = await ipc.invoke(ipcApiRoute.getRouteConfig, {name: props.tabName})
if (res.status) {
config.value = res.data;
}
})
// 监听config变化在必要时调用editConfig
watch(config, async (newVal, oldVal) => {
await editConfig();
}, { deep: true, immediate: false });
</script>
<style scoped lang="less">
.huoshan-translate {
height: 200px;
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
.hs-content-input {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
width: 100px;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 1;
align-items: center;
}
}
}
</style>

View File

@ -0,0 +1,120 @@
<template>
<div class="xiaoniu-translate">
<el-divider content-position="left">基础信息配置</el-divider>
<div class="xn-content-input">
<div class="content-left">
<el-text>URL</el-text>
</div>
<div class="content-right">
<el-input disabled v-model="config.apiUrl" placeholder="小牛翻译API请求地址">
<!-- 使用 append 插槽来添加按钮 -->
<template #append>
<el-button type="success" @click="handleTest" size="small">测试连接</el-button>
</template>
</el-input>
</div>
</div>
<div class="xn-content-input">
<div class="content-left">
<el-text>apiKey</el-text>
</div>
<div class="content-right">
<el-input v-model="config.apiKey" type="password" show-password placeholder="小牛翻译授权API密钥"></el-input>
</div>
</div>
</div>
</template>
<script setup>
import {ref, computed, watch, onMounted} from 'vue';
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import {ElMessage} from "element-plus";
const props = defineProps({
tabName: {
type: String,
required: true
}
});
const config = ref({
apiUrl:'',
appId:'',
apiKey:'',
});
const editConfig = async () => {
const args = {
name: props.tabName,
dataJson: jsonConfig.value
}
await ipc.invoke(ipcApiRoute.editTranslateRoute, args)
}
const handleTest = async () => {
const res = await ipc.invoke(ipcApiRoute.testRoute, {name: props.tabName})
if (res.status) {
ElMessage({
message: `翻译成功:${res.data}`,
type: 'success',
offset: 40,
})
}else {
ElMessage({
message: `翻译失败:${res.message}`,
type: 'error',
offset: 40,
})
}
}
// 计算属性:将 config 转换成 JSON 格式
const jsonConfig = computed(() => {
return JSON.stringify(config.value, null, 2); // 格式化 JSON 数据
});
onMounted(async () => {
const res = await ipc.invoke(ipcApiRoute.getRouteConfig, {name: props.tabName})
if (res.status) {
config.value = res.data;
}
})
// 监听config变化在必要时调用editConfig
watch(config, async (newVal, oldVal) => {
await editConfig();
}, { deep: true, immediate: false });
</script>
<style scoped lang="less">
.xiaoniu-translate {
height: 200px;
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
.xn-content-input {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
width: 100px;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 1;
align-items: center;
}
}
}
</style>

View File

@ -0,0 +1,123 @@
<template>
<div class="youdao-translate">
<el-divider content-position="left">基础信息配置</el-divider>
<div class="yd-content-input">
<div class="content-left">
<el-text>URL</el-text>
</div>
<div class="content-right">
<el-input disabled v-model="config.apiUrl" placeholder="有道翻译API请求地址">
<!-- 使用 append 插槽来添加按钮 -->
<template #append>
<el-button type="success" @click="handleTest" size="small">测试连接</el-button>
</template>
</el-input>
</div>
</div>
<div class="yd-content-input">
<div class="content-left">
<el-text>appId</el-text>
</div>
<div class="content-right">
<el-input v-model="config.appId" type="password" show-password placeholder="有道翻译应用ID"></el-input>
</div>
</div>
<div class="yd-content-input">
<div class="content-left">
<el-text>apiKey</el-text>
</div>
<div class="content-right">
<el-input v-model="config.apiKey" type="password" show-password placeholder="有道翻译授权API密钥"></el-input>
</div>
</div>
</div>
</template>
<script setup>
import {ref, computed, watch, onMounted} from 'vue';
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import {ElMessage} from "element-plus";
const props = defineProps({
tabName: {
type: String,
required: true
}
});
const config = ref({});
const editConfig = async () => {
const args = {
name: props.tabName,
dataJson: jsonConfig.value
}
await ipc.invoke(ipcApiRoute.editTranslateRoute, args)
}
const handleTest = async () => {
const res = await ipc.invoke(ipcApiRoute.testRoute, {name: props.tabName})
if (res.status) {
ElMessage({
message: `翻译成功:${res.data}`,
type: 'success',
offset: 40,
})
}else {
ElMessage({
message: `翻译失败:${res.message}`,
type: 'error',
offset: 40,
})
}
}
// 计算属性:将 config 转换成 JSON 格式
const jsonConfig = computed(() => {
return JSON.stringify(config.value, null, 2); // 格式化 JSON 数据
});
onMounted(async () => {
const res = await ipc.invoke(ipcApiRoute.getRouteConfig, {name: props.tabName})
if (res.status) {
config.value = res.data;
}
})
// 监听config变化在必要时调用editConfig
watch(config, async (newVal, oldVal) => {
await editConfig();
}, { deep: true, immediate: false });
</script>
<style scoped lang="less">
.youdao-translate {
height: 200px;
width: 100%;
display: flex;
flex: 1;
flex-direction: column;
.yd-content-input {
height: 50px;
width: 100%;
display: flex;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 0;
}
.content-left {
height: 50px;
display: flex;
width: 100px;
user-select: none;
}
.content-right {
height: 50px;
display: flex;
flex: 1;
align-items: center;
}
}
}
</style>

View File

@ -0,0 +1,395 @@
<template>
<div class="global-translate-config">
<el-tabs v-model="activeName" type="border-card">
<!-- 标签页 -->
<div class="component-tab" >
<el-tab-pane v-for="item in menuStore.translationRoute" :label="item.zhName" :name="item.name">
<component :is="getTrcComponentByName(item.name)" :tabName="item.name"/>
</el-tab-pane>
</div>
<!-- 操作按钮 -->
<div class="table-top-btn">
<el-button @click="openAddDialog" type="primary">新增编码</el-button>
<el-popconfirm
placement="right"
confirm-button-text=""
cancel-button-text=""
@confirm="batchDelete"
title="确认删除所选编码?">
<template #reference>
<el-button type="danger" :disabled="selectedItems.length === 0">批量删除</el-button>
</template>
</el-popconfirm>
</div>
<div class="table-data">
<!-- 公共部分表格 -->
<el-table
height="100%"
:data="tableData"
border
:key="activeName"
@selection-change="handleSelectionChange"
>
<el-table-column align="center" type="selection"></el-table-column>
<el-table-column align="center" prop="code" width="90" label="编码"></el-table-column>
<el-table-column align="center" :show-overflow-tooltip="true" prop="zhName" label="中文名"></el-table-column>
<el-table-column align="center" :show-overflow-tooltip="true" prop="enName" label="英文名"></el-table-column>
<el-table-column align="center" prop="youDao" label="有道编码" min-width="90"></el-table-column>
<el-table-column align="center" prop="baidu" label="百度编码" min-width="90"></el-table-column>
<el-table-column align="center" prop="huoShan" label="火山编码" min-width="90"></el-table-column>
<el-table-column align="center" prop="xiaoNiu" label="小牛编码" min-width="90"></el-table-column>
<el-table-column align="center" prop="google" label="谷歌编码" min-width="90"></el-table-column>
<el-table-column align="center" :show-overflow-tooltip="true" prop="timestamp" label="创建时间" min-width="100"></el-table-column>
<el-table-column align="center" label="操作" min-width="160" fixed="right">
<template #default="{ row }">
<el-button @click="editItem(row)" type="primary">编辑</el-button>
<el-popconfirm
placement="left"
confirm-button-text=""
cancel-button-text=""
@confirm="deleteItem(row)"
title="确认删除该编码?">
<template #reference>
<el-button type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
</el-tabs>
<!-- 新增对话框 -->
<el-dialog v-model="addDialogVisible" title="新增编码" width="40%">
<el-form :model="addForm" label-width="120px">
<el-form-item label="本地语言编码">
<el-input v-model="addForm.code" />
</el-form-item>
<el-form-item label="中文名称">
<el-input v-model="addForm.zhName" />
</el-form-item>
<el-form-item label="英文名称">
<el-input v-model="addForm.enName" />
</el-form-item>
<el-form-item label="有道翻译编码">
<el-input v-model="addForm.youDao" />
</el-form-item>
<el-form-item label="百度翻译编码">
<el-input v-model="addForm.baidu" />
</el-form-item>
<el-form-item label="火山翻译编码">
<el-input v-model="addForm.huoShan" />
</el-form-item>
<el-form-item label="小牛翻译编码">
<el-input v-model="addForm.xiaoNiu" />
</el-form-item>
<el-form-item label="谷歌翻译编码">
<el-input v-model="addForm.google" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveAdd">保存</el-button>
</template>
</el-dialog>
<!-- 编辑对话框 -->
<el-dialog v-model="editDialogVisible" title="编辑语言" width="40%">
<el-form :model="editForm" label-width="120px">
<el-form-item label="编码">
<el-input v-model="editForm.code" />
</el-form-item>
<el-form-item label="中文名称">
<el-input v-model="editForm.zhName" />
</el-form-item>
<el-form-item label="英文名称">
<el-input v-model="editForm.enName" />
</el-form-item>
<el-form-item label="有道翻译编码">
<el-input v-model="editForm.youDao" />
</el-form-item>
<el-form-item label="百度翻译编码">
<el-input v-model="editForm.baidu" />
</el-form-item>
<el-form-item label="火山翻译编码">
<el-input v-model="editForm.huoShan" />
</el-form-item>
<el-form-item label="小牛翻译编码">
<el-input v-model="editForm.xiaoNiu" />
</el-form-item>
<el-form-item label="谷歌翻译编码">
<el-input v-model="editForm.google" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEdit">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref, onMounted, watch, markRaw} from 'vue';
import {
ElTable,
ElTableColumn,
ElButton,
ElTabs,
ElTabPane,
ElDialog,
ElForm,
ElFormItem,
ElInput,
ElMessage
} from 'element-plus';
import GoogleTrc from '@/views/translate-config/GoogleTranslate.vue';
import YouDaoTrc from '@/views/translate-config/YouDaoTranslate.vue';
import BaiduTrc from '@/views/translate-config/BaiduTranslate.vue';
import HuoShanTrc from '@/views/translate-config/HuoShanTranslage.vue';
import XiaoNiuTrc from '@/views/translate-config/XiaoNiuTranslate.vue';
import {ipc} from "@/utils/ipcRenderer";
import {ipcApiRoute} from "@/api";
import { useMenuStore } from '@/stores/menuStore';
const menuStore = useMenuStore();
const trcComponent = ref([
{name:'google',component:markRaw(GoogleTrc)},
{name:'youDao',component:markRaw(YouDaoTrc)},
{name:'baidu',component:markRaw(BaiduTrc)},
{name:'huoShan',component:markRaw(HuoShanTrc)},
{name:'xiaoNiu',component:markRaw(XiaoNiuTrc)},
])
const getTrcComponentByName = (name)=> {
const found = trcComponent.value.find(item => item.name === name);
return found ? found.component : null;
}
// 获取当前时间字符串
const getTimeStr = (date = new Date())=> {
const pad = n => n.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth()+1)}-${pad(date.getDate())} ` +
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
}
// 当前激活的 tab 页
const activeName = ref('youDao');
const getTableData = async () => {
const provider = activeName.value;
if (!provider) return;
const res = await ipc.invoke(ipcApiRoute.getLanguageList,{provider:provider});
if (res.status) {
tableData.value = res.data;
}
}
watch(activeName, async (nVal, oVal) => {
await getTableData()
},{immediate: true})
// 表格数据
const tableData = ref([]);
// 选中的项
const selectedItems = ref([]);
// 新增对话框的显示状态
const addDialogVisible = ref(false);
// 编辑对话框的显示状态
const editDialogVisible = ref(false);
// 新增表单数据
const addForm = ref({
provider:activeName.value,
zhName: '',
enName: '',
code: '',
google: '',
youDao: '',
baidu: '',
huoShan: '',
xiaoNiu: '',
});
// 编辑表单数据
const editForm = ref({
id: null,
zhName: '',
enName: '',
code: '',
google: '',
youDao: '',
baidu: '',
huoShan: '',
xiaoNiu: '',
});
// 打开新增对话框
const openAddDialog = () => {
addForm.value = { zhName: '', enName: '', code: '' }; // 清空表单
addDialogVisible.value = true;
};
// 保存新增
const saveAdd = async () => {
if (!addForm.value.code || !addForm.value.zhName) {
ElMessage({
message: `本地编码或中文名称不能为空`,
type: 'error',
offset: 40,
})
return;
}
const newItem = {
provider:activeName.value,
zhName: addForm.value.zhName,
enName: addForm.value.enName,
code: addForm.value.code,
youDao: addForm.value.youDao,
baidu: addForm.value.baidu,
huoShan: addForm.value.huoShan,
xiaoNiu: addForm.value.xiaoNiu,
google: addForm.value.google,
timestamp: getTimeStr()
};
const res = await ipc.invoke(ipcApiRoute.addLanguage, newItem);
if (res.status) {
tableData.value.push(res.data);
addDialogVisible.value = false;
ElMessage({
message: `添加成功!`,
type: 'success',
offset: 40,
})
}else {
ElMessage({
message: `${res.message}`,
type: 'error',
offset: 40,
})
}
};
// 编辑功能
const editItem = (row) => {
editForm.value = { ...row };
editDialogVisible.value = true;
};
// 保存编辑
const saveEdit = async () => {
if (!editForm.value.code || !editForm.value.zhName) {
ElMessage({
message: `本地编码或中文名称不能为空`,
type: 'error',
offset: 40,
})
return;
}
const index = tableData.value.findIndex(item => item.id === editForm.value.id);
if (index !== -1) {
const args = {
id: editForm.value.id,
code: editForm.value.code,
enName: editForm.value.enName,
zhName: editForm.value.zhName,
youDao: editForm.value.youDao,
baidu: editForm.value.baidu,
huoShan: editForm.value.huoShan,
xiaoNiu: editForm.value.xiaoNiu,
google: editForm.value.google,
};
const res = await ipc.invoke(ipcApiRoute.editLanguage, args);
if (res.status) {
tableData.value[index] = {...editForm.value};
editDialogVisible.value = false;
ElMessage({
message: `编辑成功!`,
type: 'success',
offset: 40, // 设置距离顶部的位置,单位为像素
})
}else {
ElMessage({
offset: 40, // 设置距离顶部的位置,单位为像素
message: `${res.message}`,
type: 'error',
})
}
}
};
// 删除功能
const deleteItem = async (row) => {
const index = tableData.value.findIndex(item => item.id === row.id);
if (index !== -1) {
const res = await ipc.invoke(ipcApiRoute.deleteLanguage, {id: row.id});
if (res.status) {
tableData.value.splice(index, 1);
}
}
};
// 批量删除功能
const batchDelete = async () => {
const selectedIds = selectedItems.value.map(item => item.id);
for (let id of selectedIds) {
const res = await ipc.invoke(ipcApiRoute.deleteLanguage, {id: id});
if (res.status) {
const index = tableData.value.findIndex(item => item.id === id);
if (index !== -1) {
tableData.value.splice(index, 1);
}
}
}
};
// 处理选中项
const handleSelectionChange = (selection) => {
selectedItems.value = selection;
};
</script>
<style scoped lang="less">
.global-translate-config {
height: 100%;
width: 100%;
//padding: 10px;
box-sizing: border-box;
background-color: white;
//border-radius: 5px;
display: flex;
flex-direction: column; /* 纵向排列 */
:deep(.el-input__wrapper) {
border-radius: 0;
}
:deep(.el-tabs__content) {
height: calc(100vh - 120px);
}
.component-tab {
//display: flex;
height: 200px;
width: 100%;
}
.table-top-btn {
display: flex;
height: 40px;
width: 100%;
align-items: center;
}
.table-data {
display: flex;
overflow: auto;
flex: 1;
height: calc(100vh - 350px);
max-height: calc(100vh - 350px);
}
}
</style>

49
frontend/vite.config.js Normal file
View File

@ -0,0 +1,49 @@
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig(() => {
return {
// 项目插件
plugins: [
vue(),
],
// 基础配置
base: './',
publicDir: 'public',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
css: {
preprocessorOptions: {
less: {
modifyVars: {
'@border-color-base': '#dce3e8',
},
javascriptEnabled: true,
},
},
},
build: {
outDir: 'dist',
assetsDir: 'assets',
assetsInlineLimit: 4096,
cssCodeSplit: true,
brotliSize: false,
sourcemap: false,
minify: 'terser',
terserOptions: {
compress: {
// 生产环境去除console及debug
drop_console: false,
drop_debugger: true,
},
},
},
}
})

54
package.json Normal file
View File

@ -0,0 +1,54 @@
{
"name": "liangzi",
"version": "1.0.9",
"description": "量子翻译",
"main": "./public/electron/main.js",
"scripts": {
"dev": "ee-bin dev",
"build": "npm run build-frontend && npm run build-electron && ee-bin encrypt",
"start": "ee-bin start",
"dev-frontend": "ee-bin dev --serve=frontend",
"dev-electron": "ee-bin dev --serve=electron",
"build-frontend": "ee-bin build --cmds=frontend && ee-bin move --flag=frontend_dist",
"build-electron": "ee-bin build --cmds=electron",
"encrypt": "ee-bin encrypt",
"icon": "ee-bin icon -i /public/images/logo.png -o /build/icons/",
"re-sqlite": "electron-rebuild -f -w better-sqlite3",
"build-w": "ee-bin build --cmds=win64",
"build-we": "ee-bin build --cmds=win_e",
"build-w7z": "ee-bin build --cmds=win_7z",
"build-m": "ee-bin build --cmds=mac",
"build-m-arm64": "ee-bin build --cmds=mac_arm64",
"build-l": "ee-bin build --cmds=linux"
},
"repository": "https://github.com/dromara/electron-egg.git",
"keywords": [
"Electron",
"electron-egg",
"ElectronEgg"
],
"author": "JackSeng",
"license": "Apache",
"devDependencies": {
"@electron/rebuild": "^3.7.1",
"@types/node": "^20.16.0",
"debug": "^4.4.0",
"ee-bin": "^4.1.2",
"electron": "^31.0.0",
"electron-builder": "^23.6.0",
"icon-gen": "^5.0.0"
},
"dependencies": {
"@google-cloud/translate": "^8.5.1",
"@vueuse/core": "^13.0.0",
"axios": "^1.8.1",
"better-sqlite3": "^11.5.0",
"crypto-js": "^4.2.0",
"ee-core": "^4.0.0",
"electron-updater": "^6.3.8",
"input": "^1.0.1",
"node-machine-id": "^1.1.12",
"telegram": "^2.26.22",
"volcengine-sdk": "^0.0.2"
}
}

Some files were not shown because too many files have changed in this diff Show More