1、初始化仓库
							
								
								
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal 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
									
								
							
							
						
						| @ -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. | ||||
							
								
								
									
										
											BIN
										
									
								
								build/extraResources/dll/myDllDemo.dll
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										1
									
								
								build/extraResources/read.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1 @@ | ||||
| 建议第三方软件放置在此目录中,打包时会将资源加入安装包内。 | ||||
							
								
								
									
										
											BIN
										
									
								
								build/icons/256x256.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.8 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/32x32.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 2.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/512x512.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 31 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/64x64.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.7 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										
											BIN
										
									
								
								build/icons/icon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 256 KiB | 
							
								
								
									
										0
									
								
								build/script/installer.nsh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										199
									
								
								cmd/bin.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										38
									
								
								cmd/builder-mac-arm64.json
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						
							
								
								
									
										72
									
								
								electron/config/config.default.js
									
									
									
									
									
										Normal 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的api,true->需要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: '/', | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										13
									
								
								electron/config/config.local.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,13 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| /** | ||||
|  * Development environment configuration, coverage config.default.js | ||||
|  */ | ||||
| module.exports = () => { | ||||
|   return { | ||||
|     openDevTools: true, | ||||
|     jobs: { | ||||
|       messageLog: false | ||||
|     } | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										10
									
								
								electron/config/config.prod.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,10 @@ | ||||
| 'use strict'; | ||||
|  | ||||
| /** | ||||
|  *  coverage config.default.js | ||||
|  */ | ||||
| module.exports = () => { | ||||
|   return { | ||||
|     openDevTools: false, | ||||
|   }; | ||||
| }; | ||||
							
								
								
									
										34
									
								
								electron/controller/contactInfo.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										42
									
								
								electron/controller/quickreply.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										47
									
								
								electron/controller/system.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										63
									
								
								electron/controller/translate.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										75
									
								
								electron/controller/window.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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(); | ||||
							
								
								
									
										9
									
								
								electron/preload/bridge.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,9 @@ | ||||
| /* | ||||
|  * 如果启用了上下文隔离,渲染进程无法使用electron的api, | ||||
|  * 可通过contextBridge 导出api给渲染进程使用 | ||||
|  */ | ||||
| const { contextBridge, ipcRenderer } = require('electron') | ||||
|  | ||||
| contextBridge.exposeInMainWorld('electronAPI', { | ||||
|   ipcRenderer: ipcRenderer, | ||||
| }) | ||||
							
								
								
									
										22
									
								
								electron/preload/bridges/Telegram.js
									
									
									
									
									
										Normal 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); | ||||
|     } | ||||
| }) | ||||
							
								
								
									
										22
									
								
								electron/preload/bridges/TikTok.js
									
									
									
									
									
										Normal 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); | ||||
|     } | ||||
| }) | ||||
							
								
								
									
										22
									
								
								electron/preload/bridges/WhatsApp.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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 | ||||
|  } | ||||
							
								
								
									
										762
									
								
								electron/preload/lifecycle.js
									
									
									
									
									
										Normal 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, | ||||
| }; | ||||
							
								
								
									
										5
									
								
								electron/scripts/CustomWeb.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,5 @@ | ||||
| const getCurrentUserId = ()=>{} | ||||
| const inputMsg = (text)=>{} | ||||
| const quickReply = async (args)=>{ | ||||
|     const {type,text} = args; | ||||
| } | ||||
							
								
								
									
										705
									
								
								electron/scripts/Telegram.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										630
									
								
								electron/scripts/WhatsApp.js
									
									
									
									
									
										Normal 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										123
									
								
								electron/service/contactInfo.js
									
									
									
									
									
										Normal 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() | ||||
| }; | ||||
							
								
								
									
										100
									
								
								electron/service/quickreply.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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() | ||||
| }; | ||||
							
								
								
									
										559
									
								
								electron/service/translate.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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 }; | ||||
							
								
								
									
										60
									
								
								electron/utils/CommonUtils.js
									
									
									
									
									
										Normal 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 // 可选导出基础生成函数 | ||||
| }; | ||||
							
								
								
									
										169
									
								
								electron/utils/DatabaseUtils.js
									
									
									
									
									
										Normal 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; | ||||
							
								
								
									
										2
									
								
								frontend/.env.development
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,2 @@ | ||||
| VITE_TITLE="" | ||||
| VITE_GO_URL="http://localhost:8081" | ||||
							
								
								
									
										2
									
								
								frontend/.env.production
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,2 @@ | ||||
| VITE_TITLE="" | ||||
| VITE_GO_URL="http://www.test.com" | ||||
							
								
								
									
										6
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| @ -0,0 +1,6 @@ | ||||
| node_modules | ||||
| .DS_Store | ||||
| dist | ||||
| dist-ssr | ||||
| *.local | ||||
| package-lock.json | ||||
							
								
								
									
										106
									
								
								frontend/index.html
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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 | ||||
| } | ||||
|  | ||||
							
								
								
									
										8
									
								
								frontend/src/assets/global.less
									
									
									
									
									
										Normal 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%; | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								frontend/src/assets/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.4 KiB | 
							
								
								
									
										99
									
								
								frontend/src/components/global/LanguageSwitch.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										62
									
								
								frontend/src/components/global/ThemeSwitch.vue
									
									
									
									
									
										Normal 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>  | ||||
							
								
								
									
										10
									
								
								frontend/src/components/global/index.js
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										14
									
								
								frontend/src/components/icons/CleanIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										14
									
								
								frontend/src/components/icons/FoldLeftIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										14
									
								
								frontend/src/components/icons/FoldRightIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										15
									
								
								frontend/src/components/icons/GoogleIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										15
									
								
								frontend/src/components/icons/LogoIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										15
									
								
								frontend/src/components/icons/QuickReplyIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										14
									
								
								frontend/src/components/icons/ServerIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										15
									
								
								frontend/src/components/icons/TelegramIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										15
									
								
								frontend/src/components/icons/TiktikIcon.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										14
									
								
								frontend/src/components/icons/TranslateIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										15
									
								
								frontend/src/components/icons/TranslateSettingIcon.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										15
									
								
								frontend/src/components/icons/WhatsAppIcon.vue
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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' | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										17
									
								
								frontend/src/i18n/index.js
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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') | ||||
							
								
								
									
										46
									
								
								frontend/src/router/index.js
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										23
									
								
								frontend/src/router/routerMap.js
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										175
									
								
								frontend/src/stores/menuStore.js
									
									
									
									
									
										Normal 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; | ||||
|         }, | ||||
|     }, | ||||
| }); | ||||
							
								
								
									
										46
									
								
								frontend/src/utils/common.js
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
							
								
								
									
										33
									
								
								frontend/src/utils/ipcRenderer.js
									
									
									
									
									
										Normal 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 | ||||
| }; | ||||
|  | ||||
							
								
								
									
										71
									
								
								frontend/src/views/components/un-known/index.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										908
									
								
								frontend/src/views/home/index.vue
									
									
									
									
									
										Normal 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> | ||||
|  | ||||
							
								
								
									
										933
									
								
								frontend/src/views/index.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										754
									
								
								frontend/src/views/login/index.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										30
									
								
								frontend/src/views/more-setting/index.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										561
									
								
								frontend/src/views/quick-reply/index.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										296
									
								
								frontend/src/views/right-menu/ProxyConfig.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										282
									
								
								frontend/src/views/right-menu/QuickReply.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										440
									
								
								frontend/src/views/right-menu/TranslateConfig.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										485
									
								
								frontend/src/views/right-menu/UserInfo.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										256
									
								
								frontend/src/views/right-menu/index.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										681
									
								
								frontend/src/views/session-list/index.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										127
									
								
								frontend/src/views/translate-config/BaiduTranslate.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										98
									
								
								frontend/src/views/translate-config/GoogleTranslate.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										128
									
								
								frontend/src/views/translate-config/HuoShanTranslage.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										120
									
								
								frontend/src/views/translate-config/XiaoNiuTranslate.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										123
									
								
								frontend/src/views/translate-config/YouDaoTranslate.vue
									
									
									
									
									
										Normal 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> | ||||
							
								
								
									
										395
									
								
								frontend/src/views/translate-config/index.vue
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @ -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
									
								
							
							
						
						| @ -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" | ||||
|   } | ||||
| } | ||||