diff --git a/electron-vue-template/electron-builder.json b/electron-vue-template/electron-builder.json index 964d535..62cb538 100644 --- a/electron-vue-template/electron-builder.json +++ b/electron-vue-template/electron-builder.json @@ -6,7 +6,10 @@ }, "compression": "maximum", "asarUnpack": [ - "public/**/*" + "public/jre/**/*", + "public/icon/**/*", + "public/image/**/*", + "public/splash.html" ], "directories": { "output": "dist" @@ -40,15 +43,28 @@ "filter": ["**/*"] }, { + "from": "src/main/static", "to": "static", "filter": ["**/*"] }, + { + "from": "public", + "to": "assets", + "filter": [ + "erp_client_sb-*.jar" + ] + }, { "from": "public", "to": "public", "filter": [ - "**/*", + "jre/**/*", + "icon/**/*", + "image/**/*", + "splash.html", + "!erp_client_sb-*.jar", + "!data/**/*", "!jre/bin/jabswitch.exe", "!jre/bin/jaccessinspector.exe", "!jre/bin/jaccesswalker.exe", diff --git a/electron-vue-template/src/main/main.ts b/electron-vue-template/src/main/main.ts index 3e85ce8..3e0ce71 100644 --- a/electron-vue-template/src/main/main.ts +++ b/electron-vue-template/src/main/main.ts @@ -1,28 +1,29 @@ import {app, BrowserWindow, ipcMain, Menu, screen, dialog} from 'electron'; -import {existsSync, createWriteStream, promises as fs} from 'fs'; +import {existsSync, createWriteStream, promises as fs, mkdirSync, copyFileSync} from 'fs'; import {join, dirname} from 'path'; import {spawn, ChildProcess} from 'child_process'; import * as https from 'https'; import * as http from 'http'; - let springProcess: ChildProcess | null = null; let mainWindow: BrowserWindow | null = null; let splashWindow: BrowserWindow | null = null; let appOpened = false; - let downloadProgress = {percentage: 0, current: '0 MB', total: '0 MB', speed: ''}; let isDownloading = false; let downloadedFilePath: string | null = null; - function openAppIfNotOpened() { if (appOpened) return; appOpened = true; - if (mainWindow) { + if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.once('did-finish-load', () => { setTimeout(() => { - mainWindow?.show(); - mainWindow?.focus(); - if (splashWindow) { + if (mainWindow && !mainWindow.isDestroyed()) { + mainWindow.show(); + mainWindow.focus(); + } + + // 安全关闭启动画面 + if (splashWindow && !splashWindow.isDestroyed()) { splashWindow.close(); splashWindow = null; } @@ -61,12 +62,24 @@ function getJarFilePath(): string { return join(__dirname, '../../public/erp_client_sb-2.4.7.jar'); } - const bundledJarPath = join(process.resourcesPath, 'app.asar.unpacked/public/erp_client_sb-2.4.7.jar'); - if (existsSync(bundledJarPath)) { - return bundledJarPath; + // 生产环境:需要将JAR包从asar提取到临时位置 + const tempDir = join(app.getPath('temp'), 'erp-client'); + const tempJarPath = join(tempDir, 'erp_client_sb-2.4.7.jar'); + + // 确保临时目录存在 + if (!existsSync(tempDir)) { + mkdirSync(tempDir, { recursive: true }); } - return join(__dirname, '../../public/erp_client_sb-2.4.7.jar'); + // 如果临时JAR不存在,从asar中复制 + if (!existsSync(tempJarPath)) { + const asarJarPath = join(__dirname, '../assets/erp_client_sb-2.4.7.jar'); + if (existsSync(asarJarPath)) { + copyFileSync(asarJarPath, tempJarPath); + } + } + + return tempJarPath; } function getSplashPath(): string { @@ -95,9 +108,48 @@ function getIconPath(): string { return join(__dirname, '../renderer/icon/icon.png'); } +function getDataDirectoryPath(): string { + // 将用户数据目录放在可写的应用数据目录下 + const userDataPath = app.getPath('userData'); + const dataDir = join(userDataPath, 'data'); + + // 确保数据目录存在 + if (!existsSync(dataDir)) { + mkdirSync(dataDir, { recursive: true }); + } + + return dataDir; +} + +function migrateDataFromPublic(): void { + // 如果是首次运行,尝试从public/data迁移数据 + const oldDataPath = join(__dirname, '../../public/data'); + const newDataPath = getDataDirectoryPath(); + + if (process.env.NODE_ENV === 'development' && existsSync(oldDataPath)) { + try { + const files = require('fs').readdirSync(oldDataPath); + for (const file of files) { + const srcFile = join(oldDataPath, file); + const destFile = join(newDataPath, file); + + if (!existsSync(destFile)) { + require('fs').copyFileSync(srcFile, destFile); + } + } + } catch (error) { + console.log('数据迁移失败,使用默认配置'); + } + } +} + function startSpringBoot() { + // 首先迁移数据(如果需要) + migrateDataFromPublic(); + const jarPath = getJarFilePath(); const javaPath = getJavaExecutablePath(); + const dataDir = getDataDirectoryPath(); if (!existsSync(jarPath)) { dialog.showErrorBox('启动失败', `JAR 文件不存在:\n${jarPath}`); @@ -106,15 +158,31 @@ function startSpringBoot() { } try { - springProcess = spawn(javaPath, ['-jar', jarPath], { - cwd: dirname(jarPath), - detached: false + // Spring Boot启动参数配置 + const springArgs = [ + '-jar', jarPath, + `--spring.datasource.url=jdbc:sqlite:${dataDir}/erp-cache.db`, + `--logging.file.path=${dataDir}`, + `--server.port=8081` + ]; + + // 工作目录设为数据目录,这样Spring Boot会在数据目录下创建临时文件 + springProcess = spawn(javaPath, springArgs, { + cwd: dataDir, + detached: false, + env: { + ...process.env, + 'ERP_DATA_DIR': dataDir, + 'USER_DATA_DIR': dataDir + } }); let startupCompleted = false; springProcess.stdout?.on('data', (data) => { const output = data.toString(); + console.log('[Spring Boot]', output.trim()); + if (!startupCompleted && (output.includes('Started Success') || output.includes('Started ErpClientSbApplication'))) { startupCompleted = true; openAppIfNotOpened(); @@ -184,6 +252,16 @@ function createWindow() { Menu.setApplicationMenu(null); mainWindow.setMenuBarVisibility(false); + + // 打开开发者工具 + if (process.env.NODE_ENV === 'development') { + mainWindow.webContents.openDevTools(); + } + + // 监听窗口关闭事件,确保正确清理引用 + mainWindow.on('closed', () => { + mainWindow = null; + }); mainWindow.webContents.once('did-finish-load', () => { setTimeout(() => checkPendingUpdate(), 500); @@ -212,6 +290,11 @@ app.whenReady().then(() => { } }); + // 监听启动窗口关闭事件 + splashWindow.on('closed', () => { + splashWindow = null; + }); + const splashPath = getSplashPath(); if (existsSync(splashPath)) { splashWindow.loadFile(splashPath); @@ -278,31 +361,6 @@ ipcMain.handle('download-update', async (event, downloadUrl: string) => { isDownloading = true; try { - const isRealDev = process.env.NODE_ENV === 'development' && !app.isPackaged; - - if (isRealDev) { - for (let i = 0; i <= 100; i += 10) { - setTimeout(() => { - downloadProgress = { - percentage: i, - current: (i * 0.5).toFixed(1) + ' MB', - total: '50.0 MB', - speed: '2.5 MB/s' - }; - if (mainWindow) { - mainWindow.webContents.send('download-progress', downloadProgress); - } - }, i * 100); - } - - setTimeout(() => { - downloadedFilePath = 'completed'; - isDownloading = false; - }, 1100); - - return {success: true, filePath: 'dev-mode-simulated', dev: true}; - } - const tempPath = join(app.getPath('temp'), 'app.asar.new'); await downloadFile(downloadUrl, tempPath, (progress) => { @@ -353,13 +411,6 @@ ipcMain.handle('get-download-progress', () => { ipcMain.handle('install-update', async () => { try { - const isRealDev = process.env.NODE_ENV === 'development' && !app.isPackaged; - - if (isRealDev) { - downloadedFilePath = null; - return {success: true, message: '开发环境模拟重启'}; - } - const updateFilePath = join(process.resourcesPath, 'app.asar.update'); const hasUpdateFile = existsSync(updateFilePath); @@ -406,38 +457,23 @@ ipcMain.handle('cancel-download', () => { }); ipcMain.handle('get-update-status', () => { - const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged || process.defaultApp; - return {downloadedFilePath, isDownloading, downloadProgress, isPackaged: app.isPackaged, isDev}; + return {downloadedFilePath, isDownloading, downloadProgress, isPackaged: app.isPackaged}; }); // 添加文件保存对话框处理器 ipcMain.handle('show-save-dialog', async (event, options) => { - if (!mainWindow) { - return {canceled: true}; - } - - try { - const result = await dialog.showSaveDialog(mainWindow, options); - return result; - } catch (error) { - console.error('文件保存对话框错误:', error); - return {canceled: true, error: error instanceof Error ? error.message : '对话框打开失败'}; - } + return await dialog.showSaveDialog(mainWindow!, options); }); // 添加文件夹选择对话框处理器 ipcMain.handle('show-open-dialog', async (event, options) => { - if (!mainWindow) { - return {canceled: true}; - } + return await dialog.showOpenDialog(mainWindow!, options); +}); - try { - const result = await dialog.showOpenDialog(mainWindow, options); - return result; - } catch (error) { - console.error('文件夹选择对话框错误:', error); - return {canceled: true, error: error instanceof Error ? error.message : '对话框打开失败'}; - } +// 添加文件写入处理器 +ipcMain.handle('write-file', async (event, filePath: string, data: Uint8Array) => { + await fs.writeFile(filePath, Buffer.from(data)); + return { success: true }; }); diff --git a/electron-vue-template/src/main/preload.ts b/electron-vue-template/src/main/preload.ts index 6d66293..00e7400 100644 --- a/electron-vue-template/src/main/preload.ts +++ b/electron-vue-template/src/main/preload.ts @@ -13,6 +13,8 @@ const electronAPI = { showSaveDialog: (options: any) => ipcRenderer.invoke('show-save-dialog', options), // 添加文件夹选择对话框 API showOpenDialog: (options: any) => ipcRenderer.invoke('show-open-dialog', options), + // 添加文件写入 API + writeFile: (filePath: string, data: Uint8Array) => ipcRenderer.invoke('write-file', filePath, data), onDownloadProgress: (callback: (progress: any) => void) => { ipcRenderer.on('download-progress', (event, progress) => callback(progress)) diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue index 9ea4692..df838ff 100644 --- a/electron-vue-template/src/renderer/App.vue +++ b/electron-vue-template/src/renderer/App.vue @@ -12,6 +12,7 @@ const RakutenDashboard = defineAsyncComponent(() => import('./components/rakuten const AmazonDashboard = defineAsyncComponent(() => import('./components/amazon/AmazonDashboard.vue')) const ZebraDashboard = defineAsyncComponent(() => import('./components/zebra/ZebraDashboard.vue')) const UpdateDialog = defineAsyncComponent(() => import('./components/common/UpdateDialog.vue')) +const SettingsDialog = defineAsyncComponent(() => import('./components/common/SettingsDialog.vue')) const dashboardsMap: Record = { rakuten: RakutenDashboard, @@ -48,6 +49,9 @@ const userPermissions = ref('') // 更新对话框状态 const showUpdateDialog = ref(false) +// 设置对话框状态 +const showSettingsDialog = ref(false) + // 菜单配置 - 复刻ERP客户端格式 const menuConfig = [ {key: 'rakuten', name: 'Rakuten', index: 'rakuten', icon: 'R'}, @@ -69,8 +73,6 @@ function hasPermission(module: string) { if (!permissions) { return defaultModules.includes(module) // 没有权限信息时显示默认菜单 } - - // 简化权限检查:直接检查模块名是否在权限字符串中 return permissions.includes(module) } @@ -132,6 +134,7 @@ function handleMenuSelect(key: string) { async function handleLoginSuccess(data: { token: string; permissions?: string }) { isAuthenticated.value = true showAuthDialog.value = false + showRegDialog.value = false // 确保注册对话框也关闭 try { // 保存token到本地数据库 @@ -209,11 +212,6 @@ function showRegisterDialog() { showRegDialog.value = true } -function handleRegisterSuccess() { - showRegDialog.value = false - showAuthDialog.value = true -} - function backToLogin() { showRegDialog.value = false showAuthDialog.value = true @@ -362,6 +360,10 @@ async function openDeviceManager() { await fetchDeviceData() } +function openSettings() { + showSettingsDialog.value = true +} + async function fetchDeviceData() { if (!currentUsername.value) { ElMessage({ @@ -472,7 +474,8 @@ onUnmounted(() => { @go-forward="goForward" @reload="reloadPage" @user-click="handleUserClick" - @open-device="openDeviceManager"/> + @open-device="openDeviceManager" + @open-settings="openSettings"/>
{ + + + (`/tool/banma/accounts/${id}`); }, - // 业务采集(仍走客户端微服务 8081) + // 业务采集 getShops(params?: { accountId?: number }) { return http.get<{ data?: { list?: Array<{ id: string; shopName: string }> } }>( '/api/banma/shops', params as unknown as Record diff --git a/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue b/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue index 350f5bb..7c0e03c 100644 --- a/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue +++ b/electron-vue-template/src/renderer/components/amazon/AmazonDashboard.vue @@ -2,6 +2,7 @@ import { ref, computed, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { amazonApi } from '../../api/amazon' +import { handlePlatformFileExport } from '../../utils/settings' // 响应式状态 const loading = ref(false) // 主加载状态 @@ -196,11 +197,9 @@ async function exportToExcel() { html += '' const blob = new Blob([html], { type: 'application/vnd.ms-excel' }) - const link = document.createElement('a') - link.href = URL.createObjectURL(blob) - link.download = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xls` - link.click() - URL.revokeObjectURL(link.href) + const fileName = `Amazon产品数据_${new Date().toISOString().slice(0, 10)}.xls` + + await handlePlatformFileExport('amazon', blob, fileName) clearInterval(progressInterval) exportProgress.value = 100 diff --git a/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue b/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue index f4a364f..4b22485 100644 --- a/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue +++ b/electron-vue-template/src/renderer/components/auth/RegisterDialog.vue @@ -10,7 +10,7 @@ interface Props { interface Emits { (e: 'update:modelValue', value: boolean): void - (e: 'registerSuccess'): void + (e: 'loginSuccess', data: { token: string; user: any }): void (e: 'backToLogin'): void } @@ -53,12 +53,25 @@ async function handleRegister() { registerLoading.value = true try { - const result = await authApi.register({ + // 1. 注册 + await authApi.register({ username: registerForm.value.username, password: registerForm.value.password }) - ElMessage.success(result.message || '注册成功,请登录') - emit('registerSuccess') + + // 2. 注册成功后直接登录 + const loginData = await authApi.login({ + username: registerForm.value.username, + password: registerForm.value.password + }) + + emit('loginSuccess', { + token: loginData.token, + user: { + username: loginData.username, + permissions: loginData.permissions + } + }) resetForm() } catch (err) { ElMessage.error((err as Error).message) diff --git a/electron-vue-template/src/renderer/components/common/AccountManager.vue b/electron-vue-template/src/renderer/components/common/AccountManager.vue index 5d988cd..4dcff18 100644 --- a/electron-vue-template/src/renderer/components/common/AccountManager.vue +++ b/electron-vue-template/src/renderer/components/common/AccountManager.vue @@ -6,7 +6,7 @@ import { ElMessageBox, ElMessage } from 'element-plus' type PlatformKey = 'zebra' | 'shopee' | 'rakuten' | 'amazon' const props = defineProps<{ modelValue: boolean; platform?: PlatformKey }>() -const emit = defineEmits(['update:modelValue', 'add']) +const emit = defineEmits(['update:modelValue', 'add', 'refresh']) const visible = computed({ get: () => props.modelValue, set: v => emit('update:modelValue', v) }) const curPlatform = ref(props.platform || 'zebra') const PLATFORM_LABEL: Record = { @@ -23,6 +23,9 @@ async function load() { const list = (res as any)?.data ?? res accounts.value = Array.isArray(list) ? list : [] } + +// 暴露方法供父组件调用 +defineExpose({ load }) onMounted(load) function switchPlatform(p: PlatformKey) { @@ -38,11 +41,12 @@ function formatDate(a: any) { async function onDelete(a: any) { const id = a?.id try { - await ElMessageBox.confirm(`确定删除账号 “${a?.name || a?.username || id}” 吗?`, '提示', { type: 'warning' }) + await ElMessageBox.confirm(`确定删除账号 "${a?.name || a?.username || id}" 吗?`, '提示', { type: 'warning' }) } catch { return } await zebraApi.removeAccount(id) ElMessage({ message: '删除成功', type: 'success' }) await load() + emit('refresh') // 通知外层组件刷新账号列表 } diff --git a/electron-vue-template/src/renderer/components/common/UpdateDialog.vue b/electron-vue-template/src/renderer/components/common/UpdateDialog.vue index abcaeb1..28ab8b7 100644 --- a/electron-vue-template/src/renderer/components/common/UpdateDialog.vue +++ b/electron-vue-template/src/renderer/components/common/UpdateDialog.vue @@ -106,11 +106,13 @@ type Stage = 'check' | 'downloading' | 'completed' const stage = ref('check') const appName = ref('我了个电商') const version = ref('2.0.0') -const prog = ref({ percentage: 0, current: '0 MB', total: '0 MB', speed: '' as string | undefined }) +const prog = ref({ percentage: 0, current: '0 MB', total: '0 MB', speed: '' }) const info = ref({ latestVersion: '2.4.8', downloadUrl: '', - updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 增加了新的功能模块\n• 优化了数据处理性能' + updateNotes: '• 优化了用户界面体验\n• 修复了已知问题\n• 提升了系统稳定性\n• 增加了新的功能模块\n• 优化了数据处理性能', + currentVersion: '', + hasUpdate: false }) async function autoCheck() { @@ -143,26 +145,24 @@ async function autoCheck() { async function start() { if (!info.value.downloadUrl) { - ElMessage({ message: '下载链接不可用', type: 'error' }) - return + ElMessage({ message: '下载链接不可用', type: 'error' }); + return; } - stage.value = 'downloading' - prog.value = { percentage: 0, current: '0 MB', total: '0 MB', speed: '' } + stage.value = 'downloading'; + prog.value = { percentage: 0, current: '0 MB', total: '0 MB', speed: '' }; - - - window.electronAPI.onDownloadProgress((progress) => { + (window as any).electronAPI.onDownloadProgress((progress: any) => { prog.value = { percentage: progress.percentage || 0, current: progress.current || '0 MB', total: progress.total || '0 MB', speed: progress.speed || '' - } - }) + }; + }); try { - const response = await window.electronAPI.downloadUpdate(info.value.downloadUrl) + const response = await (window as any).electronAPI.downloadUpdate(info.value.downloadUrl) if (response.success) { stage.value = 'completed' @@ -181,10 +181,8 @@ async function start() { async function cancelDownload() { try { - if (window.electronAPI) { - window.electronAPI.removeDownloadProgressListener() - await window.electronAPI.cancelDownload() - } + (window as any).electronAPI.removeDownloadProgressListener() + await (window as any).electronAPI.cancelDownload() show.value = false stage.value = 'check' } catch (error) { @@ -205,7 +203,7 @@ async function installUpdate() { type: 'warning' } ) - const response = await window.electronAPI.installUpdate() + const response = await (window as any).electronAPI.installUpdate() if (response.success) { ElMessage({ message: '应用即将重启', type: 'success' }) @@ -231,9 +229,7 @@ onMounted(async () => { }) onUnmounted(() => { - if (window.electronAPI) { - window.electronAPI.removeDownloadProgressListener() - } + (window as any).electronAPI.removeDownloadProgressListener() }) diff --git a/electron-vue-template/src/renderer/components/layout/NavigationBar.vue b/electron-vue-template/src/renderer/components/layout/NavigationBar.vue index 4d1fed2..4fe2a58 100644 --- a/electron-vue-template/src/renderer/components/layout/NavigationBar.vue +++ b/electron-vue-template/src/renderer/components/layout/NavigationBar.vue @@ -13,6 +13,7 @@ interface Emits { (e: 'reload'): void (e: 'user-click'): void (e: 'open-device'): void + (e: 'open-settings'): void } defineProps() @@ -45,7 +46,7 @@ defineEmits() -