From 3a76aaa3c0dd4d8ff32d79e63db099061c2b8abe Mon Sep 17 00:00:00 2001 From: zhangzijienbplus <17738440858@163.com> Date: Fri, 24 Oct 2025 13:43:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E5=AE=9E=E7=8E=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E6=95=B0=E6=8D=AE=E9=9A=94=E7=A6=BB=E4=B8=8E=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=BB=91=E5=AE=9A=E4=BC=98=E5=8C=96-=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E7=94=A8=E6=88=B7=E4=BC=9A=E8=AF=9DID=E6=9E=84?= =?UTF-8?q?=E5=BB=BA=E9=80=BB=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=8C=89=E7=94=A8=E6=88=B7=E9=9A=94=E7=A6=BB-=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87=E7=BB=91=E5=AE=9A=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=EF=BC=8C=E6=94=AF=E6=8C=81=E8=AE=BE=E5=A4=87=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9B=B4=E6=96=B0=E5=92=8C=E7=BB=91=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E5=90=8C=E6=AD=A5-=20=E5=AE=9E=E7=8E=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=BC=93=E5=AD=98=E6=B8=85=E7=90=86=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=8C=E4=BB=85=E6=B8=85=E9=99=A4=E5=BD=93=E5=89=8D=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=9A=84=E6=95=B0=E6=8D=AE-=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=E8=B4=A6=E5=8F=B7=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E7=BA=A7=E8=81=94=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=95=B0=E6=8D=AE=20-=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=9C=A8=E7=BA=BF=E6=9F=A5=E8=AF=A2=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E7=A1=AE=E4=BF=9D=E5=8F=AA=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E6=B4=BB=E8=B7=83=E7=BB=91=E5=AE=9A=E7=9A=84=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=20-=20=E4=BC=98=E5=8C=96=E8=AF=95=E7=94=A8=E6=9C=9F=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=E7=B2=BE=E7=A1=AE=E8=AE=A1=E7=AE=97=E8=BF=87?= =?UTF-8?q?=E6=9C=9F=E6=97=B6=E9=97=B4=E5=92=8C=E7=B1=BB=E5=9E=8B-=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=B4=A6=E5=8F=B7=E7=AE=A1=E7=90=86=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E5=92=8C=E7=9B=B8=E5=85=B3=E7=8A=B6=E6=80=81=E6=B3=A8?= =?UTF-8?q?=E5=85=A5=20-=E4=BF=AE=E5=A4=8D=E8=B7=9F=E5=8D=96=E7=B2=BE?= =?UTF-8?q?=E7=81=B5=E6=8C=89=E9=92=AE=E5=8A=A0=E8=BD=BD=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E9=97=AE=E9=A2=98=20-=20=E5=A2=9E=E5=BC=BA?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=8C=BA=E5=9F=9FUI?= =?UTF-8?q?=EF=BC=8C=E6=98=BE=E7=A4=BA=E9=80=89=E4=B8=AD=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=90=8D=20-=20=E8=B0=83=E6=95=B4=E5=88=86=E9=A1=B5=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E6=A0=B7=E5=BC=8F=EF=BC=8C=E4=BC=98=E5=8C=96=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E5=B1=95=E7=A4=BA=E6=95=88=E6=9E=9C-=20=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=8F=8D=E9=A6=88=E6=97=A5=E5=BF=97=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E9=80=BB=E8=BE=91=EF=BC=8C=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=94=A8=E6=88=B7=E7=9B=AE=E5=BD=95=20-=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E5=86=97=E4=BD=99=E4=BB=A3=E7=A0=81=E5=92=8C?= =?UTF-8?q?=E6=97=A0=E7=94=A8=E5=AF=BC=E5=85=A5=EF=BC=8C=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=95=B4=E6=B4=81=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron-vue-template/package.json | 2 +- .../public/config/logback.xml | 4 +- electron-vue-template/src/main/main.ts | 187 ++++--------- electron-vue-template/src/renderer/App.vue | 146 ++++++---- .../components/amazon/AmazonDashboard.vue | 37 ++- .../renderer/components/auth/LoginDialog.vue | 1 + .../components/auth/RegisterDialog.vue | 6 +- .../components/common/AccountManager.vue | 2 - .../components/common/SettingsDialog.vue | 72 ++--- .../components/common/UpdateDialog.vue | 251 +++++------------- .../components/layout/NavigationBar.vue | 121 ++++++++- .../components/rakuten/RakutenDashboard.vue | 34 ++- .../components/zebra/ZebraDashboard.vue | 26 +- .../src/renderer/utils/deviceId.ts | 4 - .../src/renderer/utils/settings.ts | 73 +++-- electron-vue-template/update-helper.bat | 5 + erp_client_sb/pom.xml | 7 +- .../erp/controller/AmazonController.java | 17 +- .../erp/controller/BanmaOrderController.java | 17 +- .../erp/controller/RakutenController.java | 34 ++- .../erp/controller/SystemController.java | 10 +- .../repository/AmazonProductRepository.java | 6 + .../erp/repository/BanmaOrderRepository.java | 6 + .../repository/RakutenProductRepository.java | 16 ++ .../com/tashow/erp/service/CacheService.java | 45 +++- .../erp/service/RakutenCacheService.java | 4 +- .../erp/service/RakutenScrapingService.java | 4 +- .../service/impl/Alibaba1688ServiceImpl.java | 1 - .../erp/service/impl/GenmaiServiceImpl.java | 2 - .../service/impl/RakutenCacheServiceImpl.java | 21 +- .../impl/RakutenScrapingServiceImpl.java | 12 +- .../java/com/tashow/erp/utils/JwtUtil.java | 108 ++++++++ erp_client_sb/src/main/resources/logback.xml | 24 +- .../main/java/com/ruoyi/RuoYiApplication.java | 2 + .../monitor/ClientFeedbackController.java | 13 +- .../system/ClientDeviceController.java | 13 +- .../impl/ClientAccountServiceImpl.java | 36 +++ .../java/com/ruoyi/web/sse/SseHubService.java | 9 + .../system/domain/ClientAccountDevice.java | 3 - .../system/mapper/BanmaAccountMapper.java | 1 + .../mapper/ClientAccountDeviceMapper.java | 10 + .../system/mapper/ClientFeedbackMapper.java | 5 + .../system/mapper/ClientMonitorMapper.java | 8 + .../system/mapper/RefreshTokenMapper.java | 5 + .../mapper/system/BanmaAccountMapper.xml | 4 + .../system/ClientAccountDeviceMapper.xml | 18 ++ .../mapper/system/ClientDeviceMapper.xml | 6 +- .../mapper/system/ClientFeedbackMapper.xml | 4 + .../mapper/system/ClientMonitorMapper.xml | 4 + .../mapper/system/RefreshTokenMapper.xml | 4 + 50 files changed, 860 insertions(+), 590 deletions(-) create mode 100644 erp_client_sb/src/main/java/com/tashow/erp/utils/JwtUtil.java diff --git a/electron-vue-template/package.json b/electron-vue-template/package.json index 38f571f..658b917 100644 --- a/electron-vue-template/package.json +++ b/electron-vue-template/package.json @@ -1,5 +1,5 @@ { - "name": "electron-vue-template", + "name": "erpClient", "version": "0.1.0", "description": "A minimal Electron + Vue application", "main": "main/main.js", diff --git a/electron-vue-template/public/config/logback.xml b/electron-vue-template/public/config/logback.xml index 144f031..787ef75 100644 --- a/electron-vue-template/public/config/logback.xml +++ b/electron-vue-template/public/config/logback.xml @@ -1,7 +1,7 @@ - - + + diff --git a/electron-vue-template/src/main/main.ts b/electron-vue-template/src/main/main.ts index bf0ada9..e16b702 100644 --- a/electron-vue-template/src/main/main.ts +++ b/electron-vue-template/src/main/main.ts @@ -82,6 +82,18 @@ function getDataDirectoryPath(): string { return dataDir; } +function getUpdateDirectoryPath(): string { + const updateDir = join(app.getPath('userData'), 'updates'); + if (!existsSync(updateDir)) mkdirSync(updateDir, {recursive: true}); + return updateDir; +} + +function getLogDirectoryPath(): string { + const logDir = join(app.getPath('userData'), 'logs'); + if (!existsSync(logDir)) mkdirSync(logDir, {recursive: true}); + return logDir; +} + interface AppConfig { closeAction?: 'quit' | 'minimize' | 'tray'; autoLaunch?: boolean; @@ -135,12 +147,11 @@ function migrateDataFromPublic(): void { function startSpringBoot() { migrateDataFromPublic(); - const jarPath = getJarFilePath(); const javaPath = getJavaExecutablePath(); const dataDir = getDataDirectoryPath(); + const logDir = getLogDirectoryPath(); const logbackConfigPath = getLogbackConfigPath(); - if (!existsSync(jarPath)) { dialog.showErrorBox('启动失败', `JAR 文件不存在:\n${jarPath}`); app.quit(); @@ -152,7 +163,8 @@ function startSpringBoot() { '-jar', jarPath, `--spring.datasource.url=jdbc:sqlite:${dataDir}/erp-cache.db`, `--server.port=8081`, - `--logging.config=file:${logbackConfigPath}` + `--logging.config=file:${logbackConfigPath}`, + `--logging.file.path=${logDir}` ]; springProcess = spawn(javaPath, springArgs, { @@ -212,9 +224,7 @@ function startSpringBoot() { app.quit(); } } - startSpringBoot(); - function stopSpringBoot() { if (!springProcess) return; try { @@ -390,49 +400,30 @@ ipcMain.handle('get-jar-version', () => { function checkPendingUpdate() { try { - const asarUpdatePath = join(process.resourcesPath, 'app.asar.update'); - const appAsarPath = join(process.resourcesPath, 'app.asar'); + const updateDir = getUpdateDirectoryPath(); + const asarUpdatePath = join(updateDir, 'app.asar.update'); + const jarUpdatePath = readdirSync(updateDir).find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update')); - // 查找jar更新文件 - let jarUpdatePath = ''; - if (process.resourcesPath && existsSync(process.resourcesPath)) { - const files = readdirSync(process.resourcesPath); - const jarUpdateFile = files.find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update')); - if (jarUpdateFile) { - jarUpdatePath = join(process.resourcesPath, jarUpdateFile); - } - } - - // 如果没有任何更新文件,直接返回 if (!existsSync(asarUpdatePath) && !jarUpdatePath) return; const appDir = dirname(process.execPath); const helperPath = join(appDir, 'update-helper.bat'); + if (!existsSync(helperPath)) return; - if (!existsSync(helperPath)) { - console.error('[UPDATE] 更新助手不存在:', helperPath); - return; - } - + const appAsarPath = join(process.resourcesPath, 'app.asar'); const vbsPath = join(app.getPath('temp'), 'update-silent.vbs'); const vbsContent = `Set WshShell = CreateObject("WScript.Shell") -WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${asarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${jarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`; +WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${asarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${jarUpdatePath ? join(updateDir, jarUpdatePath).replace(/\\/g, '\\\\') : ''}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${updateDir.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`; writeFileSync(vbsPath, vbsContent); - - spawn('wscript.exe', [vbsPath], { - detached: true, - stdio: 'ignore', - shell: false - }); - + spawn('wscript.exe', [vbsPath], {detached: true, stdio: 'ignore', shell: false}); setTimeout(() => app.quit(), 1000); } catch (error: unknown) { console.error('[UPDATE] 更新失败:', error); } } -ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string, jarUrl?: string}) => { +ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string, jarUrl?: string, latestVersion?: string}) => { if (isDownloading) return {success: false, error: '正在下载中'}; if (downloadedFilePath === 'completed') return {success: true, filePath: 'already-completed'}; @@ -454,8 +445,9 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string, combinedTotalSize = asarSize + jarSize; if (downloadUrls.asarUrl && !currentDownloadAbortController.signal.aborted) { - const tempAsarPath = join(app.getPath('temp'), 'app.asar.new'); - await downloadFile(downloadUrls.asarUrl, tempAsarPath, (progress) => { + const updateDir = getUpdateDirectoryPath(); + const asarUpdatePath = join(updateDir, 'app.asar.update'); + await downloadFile(downloadUrls.asarUrl, asarUpdatePath, (progress) => { const combinedProgress = { percentage: combinedTotalSize > 0 ? Math.round(((totalDownloaded + progress.downloaded) / combinedTotalSize) * 100) : 0, current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`, @@ -468,30 +460,17 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string, }); if (currentDownloadAbortController.signal.aborted) throw new Error('下载已取消'); - - const asarUpdatePath = join(process.resourcesPath, 'app.asar.update'); - await fs.copyFile(tempAsarPath, asarUpdatePath); - await fs.unlink(tempAsarPath); downloadedAsarPath = asarUpdatePath; totalDownloaded = asarSize; } if (downloadUrls.jarUrl && !currentDownloadAbortController.signal.aborted) { - let jarFileName = basename(downloadUrls.jarUrl); - if (!jarFileName.match(/^erp_client_sb-[\d.]+\.jar$/)) { - const currentJar = getJarFilePath(); - const versionMatch = currentJar ? basename(currentJar).match(/erp_client_sb-([\d.]+)\.jar/) : null; - if (versionMatch && versionMatch[1]) { - const versionParts = versionMatch[1].split('.'); - versionParts[2] = String(Number(versionParts[2]) + 1); - jarFileName = `erp_client_sb-${versionParts.join('.')}.jar`; - } else { - jarFileName = 'erp_client_sb-2.4.7.jar'; - } - } - - const tempJarPath = join(app.getPath('temp'), jarFileName); - await downloadFile(downloadUrls.jarUrl, tempJarPath, (progress) => { + const jarFileName = downloadUrls.latestVersion + ? `erp_client_sb-${downloadUrls.latestVersion}.jar` + : basename(downloadUrls.jarUrl); + const updateDir = getUpdateDirectoryPath(); + const jarUpdatePath = join(updateDir, jarFileName + '.update'); + await downloadFile(downloadUrls.jarUrl, jarUpdatePath, (progress) => { const combinedProgress = { percentage: combinedTotalSize > 0 ? Math.round(((totalDownloaded + progress.downloaded) / combinedTotalSize) * 100) : 0, current: `${((totalDownloaded + progress.downloaded) / 1024 / 1024).toFixed(1)} MB`, @@ -504,10 +483,6 @@ ipcMain.handle('download-update', async (event, downloadUrls: {asarUrl?: string, }); if (currentDownloadAbortController.signal.aborted) throw new Error('下载已取消'); - - const jarUpdatePath = join(process.resourcesPath, jarFileName + '.update'); - await fs.copyFile(tempJarPath, jarUpdatePath); - await fs.unlink(tempJarPath); downloadedJarPath = jarUpdatePath; } @@ -533,43 +508,28 @@ ipcMain.handle('get-download-progress', () => { ipcMain.handle('install-update', async () => { try { - const asarUpdatePath = join(process.resourcesPath, 'app.asar.update'); - const hasAsarUpdate = existsSync(asarUpdatePath); - - // 查找jar更新文件 - let jarUpdatePath = ''; - if (process.resourcesPath && existsSync(process.resourcesPath)) { - const files = readdirSync(process.resourcesPath); - const jarUpdateFile = files.find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update')); - if (jarUpdateFile) { - jarUpdatePath = join(process.resourcesPath, jarUpdateFile); - } - } + const updateDir = getUpdateDirectoryPath(); + const asarUpdatePath = join(updateDir, 'app.asar.update'); + const jarUpdateFile = readdirSync(updateDir).find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update')); + const jarUpdatePath = jarUpdateFile ? join(updateDir, jarUpdateFile) : ''; - if (!hasAsarUpdate && !jarUpdatePath) { + if (!existsSync(asarUpdatePath) && !jarUpdatePath) { return {success: false, error: '更新文件不存在'}; } const appDir = dirname(process.execPath); const helperPath = join(appDir, 'update-helper.bat'); - if (!existsSync(helperPath)) { return {success: false, error: '更新助手不存在'}; } const appAsarPath = join(process.resourcesPath, 'app.asar'); - const vbsPath = join(app.getPath('temp'), 'update-install.vbs'); const vbsContent = `Set WshShell = CreateObject("WScript.Shell") -WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${asarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${jarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`; +WshShell.Run Chr(34) & "${helperPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${appAsarPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${asarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${jarUpdatePath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${process.execPath.replace(/\\/g, '\\\\')}" & Chr(34) & " " & Chr(34) & "${updateDir.replace(/\\/g, '\\\\')}" & Chr(34), 0, False`; writeFileSync(vbsPath, vbsContent); - - spawn('wscript.exe', [vbsPath], { - detached: true, - stdio: 'ignore', - shell: false - }); + spawn('wscript.exe', [vbsPath], {detached: true, stdio: 'ignore', shell: false}); setTimeout(() => { downloadedFilePath = null; @@ -608,31 +568,18 @@ ipcMain.handle('get-update-status', () => { ipcMain.handle('check-pending-update', () => { try { - const asarUpdatePath = join(process.resourcesPath, 'app.asar.update'); - const hasAsarUpdate = existsSync(asarUpdatePath); - - // 查找jar更新文件 - let jarUpdatePath = ''; - if (process.resourcesPath && existsSync(process.resourcesPath)) { - const files = readdirSync(process.resourcesPath); - const jarUpdateFile = files.find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update')); - if (jarUpdateFile) { - jarUpdatePath = join(process.resourcesPath, jarUpdateFile); - } - } + const updateDir = getUpdateDirectoryPath(); + const asarUpdatePath = join(updateDir, 'app.asar.update'); + const jarUpdateFile = readdirSync(updateDir).find(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update')); + const jarUpdatePath = jarUpdateFile ? join(updateDir, jarUpdateFile) : ''; return { - hasPendingUpdate: hasAsarUpdate || !!jarUpdatePath, - asarUpdatePath: hasAsarUpdate ? asarUpdatePath : null, + hasPendingUpdate: existsSync(asarUpdatePath) || !!jarUpdatePath, + asarUpdatePath: existsSync(asarUpdatePath) ? asarUpdatePath : null, jarUpdatePath: jarUpdatePath || null }; } catch (error) { - console.error('检查待安装更新失败:', error); - return { - hasPendingUpdate: false, - asarUpdatePath: null, - jarUpdatePath: null - }; + return {hasPendingUpdate: false, asarUpdatePath: null, jarUpdatePath: null}; } }); @@ -655,24 +602,10 @@ ipcMain.handle('clear-update-files', async () => { async function cleanupDownloadFiles() { try { - const tempAsarPath = join(app.getPath('temp'), 'app.asar.new'); - if (existsSync(tempAsarPath)) await fs.unlink(tempAsarPath).catch(() => {}); - - const asarUpdatePath = join(process.resourcesPath, 'app.asar.update'); - if (existsSync(asarUpdatePath)) await fs.unlink(asarUpdatePath).catch(() => {}); - - if (process.resourcesPath && existsSync(process.resourcesPath)) { - const files = readdirSync(process.resourcesPath); - const jarUpdateFiles = files.filter(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar.update')); - for (const file of jarUpdateFiles) { - await fs.unlink(join(process.resourcesPath, file)).catch(() => {}); - } - - const tempJarFiles = files.filter(f => f.startsWith('erp_client_sb-') && f.endsWith('.jar') && !f.includes('update')); - for (const file of tempJarFiles) { - const tempJarPath = join(app.getPath('temp'), file); - if (existsSync(tempJarPath)) await fs.unlink(tempJarPath).catch(() => {}); - } + const updateDir = getUpdateDirectoryPath(); + const files = readdirSync(updateDir); + for (const file of files) { + await fs.unlink(join(updateDir, file)).catch(() => {}); } } catch (error) {} } @@ -696,34 +629,23 @@ ipcMain.handle('write-file', async (event, filePath: string, data: Uint8Array) = // 获取日志日期列表 ipcMain.handle('get-log-dates', async () => { try { - const logDir = 'C:/ProgramData/erp-logs'; - if (!existsSync(logDir)) { - return { dates: [] }; - } - + const logDir = getLogDirectoryPath(); const files = await fs.readdir(logDir); const dates: string[] = []; - - // 获取今天的日期(YYYY-MM-DD格式) const today = new Date().toISOString().split('T')[0]; files.forEach(file => { if (file === 'spring-boot.log') { - // 当天的日志文件,使用今天的日期 dates.push(today); } else if (file.startsWith('spring-boot-') && file.endsWith('.log')) { - // 历史日志文件,提取日期 const date = file.replace('spring-boot-', '').replace('.log', ''); dates.push(date); } }); - // 排序,最新的在前面 dates.sort().reverse(); - return { dates }; } catch (error) { - console.error('获取日志日期列表失败:', error); return { dates: [] }; } }); @@ -731,12 +653,10 @@ ipcMain.handle('get-log-dates', async () => { // 读取指定日期的日志文件 ipcMain.handle('read-log-file', async (event, logDate: string) => { try { - const logDir = 'C:/ProgramData/erp-logs'; + const logDir = getLogDirectoryPath(); const today = new Date().toISOString().split('T')[0]; - - // 如果是今天的日期,读取 spring-boot.log,否则读取带日期的文件 const fileName = logDate === today ? 'spring-boot.log' : `spring-boot-${logDate}.log`; - const logFilePath = `${logDir}/${fileName}`; + const logFilePath = join(logDir, fileName); if (!existsSync(logFilePath)) { return { success: false, error: '日志文件不存在' }; @@ -745,7 +665,6 @@ ipcMain.handle('read-log-file', async (event, logDate: string) => { const content = await fs.readFile(logFilePath, 'utf-8'); return { success: true, content }; } catch (error) { - console.error('读取日志文件失败:', error); return { success: false, error: error instanceof Error ? error.message : '读取失败' }; } }); diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue index 7e9ffb2..fadd3c8 100644 --- a/electron-vue-template/src/renderer/App.vue +++ b/electron-vue-template/src/renderer/App.vue @@ -18,6 +18,7 @@ import ZebraDashboard from './components/zebra/ZebraDashboard.vue' import UpdateDialog from './components/common/UpdateDialog.vue' import SettingsDialog from './components/common/SettingsDialog.vue' import TrialExpiredDialog from './components/common/TrialExpiredDialog.vue' +import AccountManager from './components/common/AccountManager.vue' const dashboardsMap: Record = { rakuten: RakutenDashboard, @@ -56,15 +57,36 @@ const vipExpireTime = ref(null) const deviceTrialExpired = ref(false) const accountType = ref('trial') const vipStatus = computed(() => { - if (!vipExpireTime.value) return { isVip: false, daysLeft: 0, status: 'expired' } + if (!vipExpireTime.value) return { isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType: 'account' } + const now = new Date() const expire = new Date(vipExpireTime.value) - const daysLeft = Math.ceil((expire.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) + const msLeft = expire.getTime() - now.getTime() - if (daysLeft <= 0) return { isVip: false, daysLeft: 0, status: 'expired' } - if (daysLeft <= 7) return { isVip: true, daysLeft, status: 'warning' } - if (daysLeft <= 30) return { isVip: true, daysLeft, status: 'normal' } - return { isVip: true, daysLeft, status: 'active' } + // 精确判断:当前时间 >= 过期时间,则已过期(与后端逻辑一致) + if (msLeft <= 0) { + const accountExpired = true + const deviceExpired = deviceTrialExpired.value + let expiredType: 'device' | 'account' | 'both' | 'subscribe' = 'account' + if (deviceExpired && accountExpired) expiredType = 'both' + else if (accountExpired) expiredType = 'account' + else if (deviceExpired) expiredType = 'device' + + return { isVip: false, daysLeft: 0, hoursLeft: 0, status: 'expired', expiredType } + } + + const hoursLeft = Math.floor(msLeft / (1000 * 60 * 60)) + const daysLeft = Math.floor(msLeft / (1000 * 60 * 60 * 24)) + + let expiredType: 'device' | 'account' | 'both' | 'subscribe' = 'subscribe' + if (accountType.value === 'trial' && deviceTrialExpired.value) { + expiredType = 'device' // 试用账号且设备过期 + } + + if (daysLeft === 0) return { isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType } + if (daysLeft <= 7) return { isVip: true, daysLeft, hoursLeft, status: 'warning', expiredType } + if (daysLeft <= 30) return { isVip: true, daysLeft, hoursLeft, status: 'normal', expiredType } + return { isVip: true, daysLeft, hoursLeft, status: 'active', expiredType } }) // 功能可用性(账号VIP + 设备试用期) @@ -86,6 +108,12 @@ const showSettingsDialog = ref(false) const showTrialExpiredDialog = ref(false) const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('device') +// 账号管理对话框状态 +const showAccountManager = ref(false) + +// 当前版本 +const currentVersion = ref('') + // 菜单配置 - 复刻ERP客户端格式 const menuConfig = [ {key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R'}, @@ -185,30 +213,20 @@ async function handleLoginSuccess(data: { token: string; permissions?: string; e os: navigator.platform }) SSEManager.connect() - - // 根据不同场景显示提示 - const accountExpired = vipExpireTime.value && new Date() > vipExpireTime.value - const deviceExpired = deviceTrialExpired.value - const isPaid = accountType.value === 'paid' - if (deviceExpired && accountExpired) { - // 场景4: 试用已到期,请订阅 - trialExpiredType.value = 'both' - showTrialExpiredDialog.value = true - } else if (accountExpired) { - // 场景3: 账号试用已到期,请订阅 - trialExpiredType.value = 'account' - showTrialExpiredDialog.value = true - } else if (deviceExpired) { - // 场景2: 设备试用已到期,请更换设备或订阅 - trialExpiredType.value = 'device' + // 同步当前账号的设置到 Electron 主进程 + syncSettingsToElectron() + + // 根据VIP状态显示对应提示 + if (!vipStatus.value.isVip || deviceTrialExpired.value) { + trialExpiredType.value = vipStatus.value.expiredType showTrialExpiredDialog.value = true } } catch (e: any) { isAuthenticated.value = false showAuthDialog.value = true removeToken() - ElMessage.error(e?.message || '设备注册失败') + } } @@ -228,7 +246,7 @@ function clearLocalAuth() { async function logout() { try { const deviceId = getClientIdFromToken() - if (deviceId) await deviceApi.remove({ deviceId, username: currentUsername.value }) + if (deviceId) await deviceApi.offline({ deviceId, username: currentUsername.value }) } catch (error) { console.warn('离线通知失败:', error) } @@ -283,6 +301,9 @@ async function checkAuth() { } SSEManager.connect() + + // 同步当前账号的设置到 Electron 主进程 + syncSettingsToElectron() } catch { removeToken() if (['rakuten', 'amazon', 'zebra', 'shopee'].includes(activeMenu.value)) { @@ -309,31 +330,34 @@ async function refreshVipStatus() { } } -// 判断过期类型 -function checkExpiredType(): 'device' | 'account' | 'both' | 'subscribe' { - const accountExpired = vipExpireTime.value && new Date() > vipExpireTime.value - const deviceExpired = deviceTrialExpired.value - - if (deviceExpired && accountExpired) return 'both' - if (accountExpired) return 'account' - if (deviceExpired) return 'device' - return 'account' // 默认 -} - // 打开订阅对话框 function openSubscriptionDialog() { - // 如果VIP有效,显示订阅/续费提示;如果已过期,显示过期提示 - if (vipStatus.value.isVip) { - trialExpiredType.value = 'subscribe' - } else { - trialExpiredType.value = checkExpiredType() - } + trialExpiredType.value = vipStatus.value.expiredType showTrialExpiredDialog.value = true } +// 同步设置到 Electron 主进程 +async function syncSettingsToElectron() { + try { + const username = getUsernameFromToken() + const settings = getSettings(username) + + // 同步关闭行为 + await (window as any).electronAPI.setCloseAction(settings.closeAction || 'quit') + + // 同步启动配置 + await (window as any).electronAPI.setLaunchConfig({ + autoLaunch: settings.autoLaunch || false, + launchMinimized: settings.launchMinimized || false + }) + } catch (error) { + console.warn('同步设置到主进程失败:', error) + } +} + // 提供给子组件使用 provide('refreshVipStatus', refreshVipStatus) -provide('checkExpiredType', checkExpiredType) +provide('vipStatus', vipStatus) const SSEManager = { connection: null as EventSource | null, @@ -427,6 +451,20 @@ function openSettings() { showSettingsDialog.value = true } +function openAccountManager() { + if (!isAuthenticated.value) { + showAuthDialog.value = true + return + } + showAccountManager.value = true +} + +async function handleCheckUpdate() { + if (updateDialogRef.value) { + await updateDialogRef.value.checkForUpdatesNow() + } +} + async function fetchDeviceData() { if (!currentUsername.value) { ElMessage.warning('未获取到用户名,请重新登录') @@ -479,6 +517,13 @@ onMounted(async () => { // 检查是否有待安装的更新 await checkPendingUpdate() + // 加载当前版本 + try { + currentVersion.value = await (window as any).electronAPI.getJarVersion() + } catch (error) { + console.warn('获取当前版本失败:', error) + } + // 全局阻止文件拖拽到窗口(避免意外打开文件) // 只在指定的 dropzone 区域处理拖拽上传 document.addEventListener('dragover', (e) => { @@ -557,9 +602,12 @@ onUnmounted(() => { 即将到期 订阅中 +
- 有效期至:{{ vipExpireTime ? new Date(vipExpireTime).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) : '-' }} + 有效期至:{{ new Date(vipExpireTime).toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }) }}
@@ -571,12 +619,17 @@ onUnmounted(() => { :can-go-back="canGoBack" :can-go-forward="canGoForward" :active-menu="activeMenu" + :is-authenticated="isAuthenticated" + :current-username="currentUsername" + :current-version="currentVersion" @go-back="goBack" @go-forward="goForward" @reload="reloadPage" - @user-click="handleUserClick" + @logout="handleUserClick" @open-device="openDeviceManager" - @open-settings="openSettings"/> + @open-settings="openSettings" + @open-account-manager="openAccountManager" + @check-update="handleCheckUpdate"/>
{ + + + import('../common/TrialExpiredDialog.vue')) @@ -34,7 +35,7 @@ const amazonUpload = ref(null) const showTrialExpiredDialog = ref(false) const trialExpiredType = ref<'device' | 'account' | 'both' | 'subscribe'>('account') -const checkExpiredType = inject<() => 'device' | 'account' | 'both' | 'subscribe'>('checkExpiredType') +const vipStatus = inject('vipStatus') // 计算属性 - 当前页数据 const paginatedData = computed(() => { @@ -51,6 +52,7 @@ const regionOptions = [ { label: '美国 (USA)', value: 'US', flag: '🇺🇸' }, ] const pendingAsins = ref([]) +const selectedFileName = ref('') // 通用消息提示(Element Plus) function showMessage(message: string, type: 'success' | 'warning' | 'error' | 'info' = 'info') { @@ -73,6 +75,7 @@ async function processExcelFile(file: File) { return } pendingAsins.value = asinList + selectedFileName.value = file.name } catch (error: any) { showMessage(error.message || '处理文件失败', 'error') } finally { @@ -102,7 +105,7 @@ async function batchGetProductInfo(asinList: string[]) { // VIP检查 if (!props.isVip) { - if (checkExpiredType) trialExpiredType.value = checkExpiredType() + if (vipStatus) trialExpiredType.value = vipStatus.value.expiredType showTrialExpiredDialog.value = true return } @@ -158,6 +161,7 @@ async function batchGetProductInfo(asinList: string[]) { // 处理完成状态更新 progressPercentage.value = 100 currentAsin.value = '处理完成' + selectedFileName.value = '' @@ -165,6 +169,7 @@ async function batchGetProductInfo(asinList: string[]) { if (error.name !== 'AbortError') { showMessage(error.message || '批量获取产品信息失败', 'error') currentAsin.value = '处理失败' + selectedFileName.value = '' } } finally { tableLoading.value = false @@ -217,7 +222,8 @@ async function exportToExcel() { const blob = new Blob([html], { type: 'application/vnd.ms-excel' }) const fileName = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xls` - const success = await handlePlatformFileExport('amazon', blob, fileName) + const username = getUsernameFromToken() + const success = await handlePlatformFileExport('amazon', blob, fileName, username) if (success) { showMessage('Excel文件导出成功!', 'success') @@ -246,6 +252,7 @@ function stopFetch() { abortController = null loading.value = false currentAsin.value = '已停止' + selectedFileName.value = '' showMessage('已停止获取产品数据', 'info') } @@ -326,9 +333,10 @@ onMounted(async () => { 📦 ASIN查询
-
- 🔍 - 跟卖精灵 +
+ 🔍 + + {{ genmaiLoading ? '启动中...' : '跟卖精灵' }}
操作流程:
@@ -350,6 +358,10 @@ onMounted(async () => {
支持 .xls .xlsx
+
+ + {{ selectedFileName }} +
@@ -375,7 +387,6 @@ onMounted(async () => { {{ loading ? '处理中...' : '获取数据' }} 停止获取 -
已导入 {{ pendingAsins.length }} 个 ASIN
@@ -462,9 +473,8 @@ onMounted(async () => { -
+
{ .tab-item:last-child { border-radius: 0 3px 3px 0; border-left: none; } .tab-item:hover { background: #e8f4ff; color: #409EFF; } .tab-item.active { background: #1677FF; color: #fff; border-color: #1677FF; cursor: default; } +.tab-item.loading { background: #e8f4ff; color: #409EFF; cursor: not-allowed; opacity: 0.8; } .tab-icon { font-size: 12px; } +.spinner-icon { animation: spin 1s linear infinite; display: inline-block; } .tab-text { line-height: 1; } .body-layout { display: flex; gap: 12px; flex: 1; overflow: hidden; } @@ -501,7 +513,6 @@ onMounted(async () => { .steps-flow { position: relative; } .steps-flow:before { content: ''; position: absolute; left: 11px; top: 20px; bottom: 0; width: 1px; background: rgba(229, 231, 235, 0.6); } .flow-item { position: relative; display: grid; grid-template-columns: 22px 1fr; gap: 10px; padding: 8px 0; } -.flow-item + .flow-item { border-top: 1px dashed #ebeef5; } .flow-item .step-index { position: static; width: 22px; height: 22px; line-height: 22px; text-align: center; border-radius: 50%; background: #1677FF; color: #fff; font-size: 12px; font-weight: 600; margin-top: 2px; } .flow-item:after { display: none; } .step-card { border: none; border-radius: 0; padding: 0; background: transparent; } @@ -520,6 +531,9 @@ onMounted(async () => { .dz-icon { font-size: 20px; margin-bottom: 6px; } .dz-text { color: #303133; font-size: 13px; } .dz-sub { color: #909399; font-size: 12px; } +.file-chip { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f5f7fa; border-radius: 4px; font-size: 12px; color: #606266; margin-top: 6px; } +.file-chip .dot { width: 6px; height: 6px; background: #409EFF; border-radius: 50%; display: inline-block; } +.file-chip .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .single-input.left { display: flex; gap: 8px; } .action-buttons.column { display: flex; flex-direction: column; gap: 8px; } .form-row { margin-bottom: 10px; } @@ -573,7 +587,8 @@ onMounted(async () => { .table-loading { position: absolute; inset: 0; background: rgba(255, 255, 255, 0.95); display: flex; flex-direction: column; justify-content: center; align-items: center; font-size: 14px; color: #606266; } .spinner { font-size: 24px; animation: spin 1s linear infinite; margin-bottom: 8px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } -.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #f9f9f9; border-radius: 4px; display: flex; justify-content: center; border-top: 1px solid #ebeef5; margin-top: 8px; } +.pagination-fixed { flex-shrink: 0; padding: 8px 12px; background: #fff; display: flex; justify-content: flex-end; margin-top: 8px; } +.pagination-fixed :deep(.el-pager li.is-active) { border: 1px solid #1677FF; border-radius: 4px; color: #1677FF; background: #fff; } .empty-tip { text-align: center; color: #909399; padding: 16px 0; } .import-section[draggable], .import-section.drag-active { border: 1px dashed #409EFF; border-radius: 6px; } .empty-container { text-align: center; } diff --git a/electron-vue-template/src/renderer/components/auth/LoginDialog.vue b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue index 90f69eb..65fb81c 100644 --- a/electron-vue-template/src/renderer/components/auth/LoginDialog.vue +++ b/electron-vue-template/src/renderer/components/auth/LoginDialog.vue @@ -110,6 +110,7 @@ function showRegister() { size="large" style="margin-bottom: 20px;" :disabled="authLoading" + show-password @keyup.enter="handleAuth"> diff --git a/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue b/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue index eaa2e13..13abf53 100644 --- a/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue +++ b/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue @@ -150,7 +150,8 @@ function backToLogin() { type="password" size="large" style="margin-bottom: 15px;" - :disabled="registerLoading"> + :disabled="registerLoading" + show-password> + :disabled="registerLoading" + show-password>
diff --git a/electron-vue-template/src/renderer/components/common/AccountManager.vue b/electron-vue-template/src/renderer/components/common/AccountManager.vue index 2ebcfca..b6626a7 100644 --- a/electron-vue-template/src/renderer/components/common/AccountManager.vue +++ b/electron-vue-template/src/renderer/components/common/AccountManager.vue @@ -3,9 +3,7 @@ import { ref, onMounted, computed } from 'vue' import { zebraApi, type BanmaAccount } from '../../api/zebra' import { ElMessageBox, ElMessage } from 'element-plus' import { getUsernameFromToken } from '../../utils/token' - type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon' - const props = defineProps<{ modelValue: boolean; platform?: PlatformKey }>() const emit = defineEmits(['update:modelValue', 'add', 'refresh']) const visible = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) }) diff --git a/electron-vue-template/src/renderer/components/common/SettingsDialog.vue b/electron-vue-template/src/renderer/components/common/SettingsDialog.vue index d496d49..7a14d09 100644 --- a/electron-vue-template/src/renderer/components/common/SettingsDialog.vue +++ b/electron-vue-template/src/renderer/components/common/SettingsDialog.vue @@ -1,5 +1,5 @@ diff --git a/electron-vue-template/src/renderer/components/common/UpdateDialog.vue b/electron-vue-template/src/renderer/components/common/UpdateDialog.vue index 9fa19ca..42ca9e6 100644 --- a/electron-vue-template/src/renderer/components/common/UpdateDialog.vue +++ b/electron-vue-template/src/renderer/components/common/UpdateDialog.vue @@ -1,9 +1,5 @@