diff --git a/electron-vue-template/img.png b/electron-vue-template/img.png deleted file mode 100644 index 4ed51e3..0000000 Binary files a/electron-vue-template/img.png and /dev/null differ diff --git a/electron-vue-template/public/icon/cancel.png b/electron-vue-template/public/icon/cancel.png deleted file mode 100644 index 71c4434..0000000 Binary files a/electron-vue-template/public/icon/cancel.png and /dev/null differ diff --git a/electron-vue-template/public/icon/done1.png b/electron-vue-template/public/icon/done1.png deleted file mode 100644 index 921810b..0000000 Binary files a/electron-vue-template/public/icon/done1.png and /dev/null differ diff --git a/electron-vue-template/public/icon/error.png b/electron-vue-template/public/icon/error.png deleted file mode 100644 index cdbab9d..0000000 Binary files a/electron-vue-template/public/icon/error.png and /dev/null differ diff --git a/electron-vue-template/public/icon/wait.png b/electron-vue-template/public/icon/wait.png deleted file mode 100644 index 7a65c10..0000000 Binary files a/electron-vue-template/public/icon/wait.png and /dev/null differ diff --git a/electron-vue-template/public/image/excel-format-example.png b/electron-vue-template/public/image/excel-format-example.png new file mode 100644 index 0000000..8214b21 Binary files /dev/null and b/electron-vue-template/public/image/excel-format-example.png differ diff --git a/electron-vue-template/public/splash.html b/electron-vue-template/public/splash.html index ff5fd65..29ef6e4 100644 --- a/electron-vue-template/public/splash.html +++ b/electron-vue-template/public/splash.html @@ -1,31 +1,78 @@ - + - 正在启动... - - - - +
+ 正在启动
+
diff --git a/electron-vue-template/src/main/main.ts b/electron-vue-template/src/main/main.ts index 9aca48f..fce7a89 100644 --- a/electron-vue-template/src/main/main.ts +++ b/electron-vue-template/src/main/main.ts @@ -23,13 +23,18 @@ function openAppIfNotOpened() { !appOpened && setTimeout(openAppIfNotOpened, 50); return; } - + appOpened = true; - isDev + isDev ? mainWindow.loadURL(`http://localhost:${process.argv[2] || 8083}`) : mainWindow.loadFile(join(__dirname, '../renderer/index.html')); mainWindow.webContents.once('did-finish-load', () => { + if (splashWindow && !splashWindow.isDestroyed()) { + splashWindow.webContents.send('splash-complete'); + } + + // 先显示主窗口,再关闭splash,避免白屏 setTimeout(() => { if (mainWindow && !mainWindow.isDestroyed()) { const shouldMinimize = loadConfig().launchMinimized || false; @@ -39,14 +44,19 @@ function openAppIfNotOpened() { } if (isDev) mainWindow.webContents.openDevTools(); } - if (splashWindow && !splashWindow.isDestroyed()) { - splashWindow.close(); - splashWindow = null; - } - }, 100); + + // 延迟关闭splash,确保主窗口已显示 + setTimeout(() => { + if (splashWindow && !splashWindow.isDestroyed()) { + splashWindow.close(); + splashWindow = null; + } + }, 100); + }, 200); }); } + // 通用资源路径获取函数 function getResourcePath(devPath: string, prodPath: string, fallbackPath?: string): string { if (isDev) return join(__dirname, devPath); @@ -76,6 +86,74 @@ const getSplashPath = () => getResourcePath('../../public/splash.html', 'public/ const getIconPath = () => getResourcePath('../../public/icon/icon1.png', 'public/icon/icon1.png'); const getLogbackConfigPath = () => getResourcePath('../../public/config/logback.xml', 'public/config/logback.xml'); +// 图片缓存目录 +const getImageCacheDir = () => { + const cacheDir = join(app.getPath('userData'), 'image-cache'); + if (!existsSync(cacheDir)) mkdirSync(cacheDir, { recursive: true }); + return cacheDir; +}; + +// 下载图片到本地 +async function downloadImageToLocal(imageUrl: string, username: string, type: 'splash' | 'logo'): Promise { + return new Promise((resolve) => { + const protocol = imageUrl.startsWith('https') ? https : http; + protocol.get(imageUrl, (res) => { + if (res.statusCode !== 200) return resolve(); + const chunks: Buffer[] = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const buffer = Buffer.concat(chunks); + const ext = imageUrl.match(/\.(jpg|jpeg|png|gif|webp)$/i)?.[1] || 'png'; + const filepath = join(getImageCacheDir(), `${username}_${type}.${ext}`); + writeFileSync(filepath, buffer); + console.log(`[图片缓存] 已保存: ${username}_${type}.${ext}`); + resolve(); + }); + res.on('error', () => resolve()); + }).on('error', () => resolve()); + }); +} + +// 加载本地缓存图片 +function loadCachedImage(username: string, type: 'splash' | 'logo'): string | null { + try { + const files = readdirSync(getImageCacheDir()); + const file = files.find(f => f.startsWith(`${username}_${type}.`)); + if (file) { + const buffer = readFileSync(join(getImageCacheDir(), file)); + const ext = extname(file).slice(1); + const mime = { jpg: 'jpeg', jpeg: 'jpeg', png: 'png', gif: 'gif', webp: 'webp' }[ext] || 'png'; + return `url('data:image/${mime};base64,${buffer.toString('base64')}')`; + } + } catch (err) {} + return null; +} + +// 删除本地缓存图片 +function deleteCachedImage(username: string, type: 'splash' | 'logo'): void { + try { + const files = readdirSync(getImageCacheDir()); + const file = files.find(f => f.startsWith(`${username}_${type}.`)); + if (file) { + const filepath = join(getImageCacheDir(), file); + if (existsSync(filepath)) { + require('fs').unlinkSync(filepath); + console.log(`[图片缓存] 已删除: ${file}`); + } + } + } catch (err) {} +} + +// 获取默认开屏图片 +function getDefaultSplashImage(): string { + const path = getResourcePath('../../public/image/splash_screen.png', 'public/image/splash_screen.png'); + if (existsSync(path)) { + const base64 = readFileSync(path).toString('base64'); + return `url('data:image/png;base64,${base64}')`; + } + return 'none'; +} + function getDataDirectoryPath(): string { const dataDir = join(app.getPath('userData'), 'data'); if (!existsSync(dataDir)) mkdirSync(dataDir, {recursive: true}); @@ -101,6 +179,7 @@ interface AppConfig { launchMinimized?: boolean; lastUsername?: string; splashImageUrl?: string; + brandLogoUrl?: string; } function getConfigPath(): string { @@ -143,12 +222,15 @@ function migrateDataFromPublic(): void { function startSpringBoot() { + console.log('[Spring Boot] 开始启动...'); migrateDataFromPublic(); const jarPath = getJarFilePath(); const javaPath = getJavaExecutablePath(); const dataDir = getDataDirectoryPath(); const logDir = getLogDirectoryPath(); const logbackConfigPath = getLogbackConfigPath(); + console.log('[Spring Boot] JAR路径:', jarPath); + console.log('[Spring Boot] Java路径:', javaPath); if (!existsSync(jarPath)) { dialog.showErrorBox('启动失败', `JAR 文件不存在:\n${jarPath}`); app.quit(); @@ -174,14 +256,16 @@ function startSpringBoot() { springProcess.on('close', () => mainWindow ? mainWindow.close() : app.quit()); springProcess.on('error', (error) => { - dialog.showErrorBox('启动失败', error.message.includes('ENOENT') - ? '找不到 Java 运行环境' + dialog.showErrorBox('启动失败', error.message.includes('ENOENT') + ? '找不到 Java 运行环境' : '启动 Java 应用失败'); app.quit(); }); + let checkCount = 0; const checkHealth = () => { if (startupCompleted) return; + http.get('http://127.0.0.1:8081/api/system/version', (res) => { if (res.statusCode !== 200) { setTimeout(checkHealth, 100); @@ -213,7 +297,6 @@ function startSpringBoot() { } } - // startSpringBoot(); function stopSpringBoot() { if (!springProcess) return; try { @@ -236,8 +319,10 @@ function stopSpringBoot() { function createWindow() { mainWindow = new BrowserWindow({ - width: 1280, + width: 1200, height: 800, + minWidth: 1200, + minHeight: 800, show: false, // frame: false, autoHideMenuBar: true, @@ -317,9 +402,9 @@ app.whenReady().then(() => { // 如果解码失败,回退到原来的方法 filePath = decodeURIComponent(request.url.substring(8)); } - + // 检查是否是 icon 或 image 资源请求 - if (filePath.includes('/icon/') || filePath.includes('\\icon\\') || + if (filePath.includes('/icon/') || filePath.includes('\\icon\\') || filePath.includes('/image/') || filePath.includes('\\image\\')) { const match = filePath.match(/[/\\](icon|image)[/\\]([^?#]+)/); if (match) { @@ -331,15 +416,15 @@ app.whenReady().then(() => { } } } - + callback({ path: filePath }); }); } - + // 应用开机自启动配置 const config = loadConfig(); const shouldMinimize = config.launchMinimized || false; - + if (config.autoLaunch !== undefined) { app.setLoginItemSettings({ openAtLogin: config.autoLaunch, @@ -352,49 +437,48 @@ app.whenReady().then(() => { // 只有在不需要最小化启动时才显示 splash 窗口 if (!shouldMinimize) { + const config = loadConfig(); + const username = config.lastUsername || ''; + const imageUrl = config.splashImageUrl || ''; + + // 图片加载:本地缓存 > 默认图片 + let splashImage = (imageUrl && username && loadCachedImage(username, 'splash')) || getDefaultSplashImage(); + + // 如果有URL但缓存不存在,后台下载 + if (imageUrl && username && !loadCachedImage(username, 'splash')) { + downloadImageToLocal(imageUrl, username, 'splash'); + } + + const splashHtml = readFileSync(getSplashPath(), 'utf-8').replace('__SPLASH_IMAGE__', splashImage); + splashWindow = new BrowserWindow({ width: 1200, - height: 675, + height: 800, frame: false, transparent: false, resizable: false, alwaysOnTop: false, - show: true, + show: false, center: true, icon: getIconPath(), + backgroundColor: '#ffffff', webPreferences: { - nodeIntegration: false, - contextIsolation: true, + nodeIntegration: true, + contextIsolation: false, } }); - // 监听启动窗口关闭事件 - splashWindow.on('closed', () => { - splashWindow = null; - }); + splashWindow.on('closed', () => splashWindow = null); - const splashPath = getSplashPath(); - if (existsSync(splashPath)) { - const config = loadConfig(); - const imageUrl = config.splashImageUrl || ''; - console.log('[开屏图片] 启动配置:', { username: config.lastUsername, imageUrl, configPath: getConfigPath() }); - - splashWindow.loadFile(splashPath); - - if (imageUrl) { - splashWindow.webContents.once('did-finish-load', () => { - splashWindow?.webContents.executeJavaScript(` - document.body.style.setProperty('--splash-image', "url('${imageUrl}')"); - `).then(() => console.log('[开屏图片] 注入成功:', imageUrl)) - .catch(err => console.error('[开屏图片] 注入失败:', err)); - }); - } - } + // 加载预注入的 HTML(图片已base64内联,无跨域问题) + splashWindow.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(splashHtml)}`); + splashWindow.once('ready-to-show', () => splashWindow?.show()); } -//666 + + console.log('[启动流程] 准备启动 Spring Boot...'); setTimeout(() => { - openAppIfNotOpened(); - }, 100); + startSpringBoot(); + }, 200); app.on('activate', () => { if (mainWindow && !mainWindow.isDestroyed()) { @@ -424,6 +508,11 @@ ipcMain.on('message', (event, message) => { console.log(message); }); +ipcMain.on('quit-app', () => { + isQuitting = true; + app.quit(); +}); + ipcMain.handle('get-jar-version', () => { const jarPath = getJarFilePath(); const match = jarPath ? basename(jarPath).match(/erp_client_sb-(\d+\.\d+\.\d+)\.jar/) : null; @@ -785,13 +874,21 @@ ipcMain.handle('window-is-maximized', () => { return mainWindow && !mainWindow.isDestroyed() ? mainWindow.isMaximized() : false; }); -// 保存开屏图片配置(用户名 + URL) -ipcMain.handle('save-splash-config', (event, username: string, imageUrl: string) => { +// 保存开屏图片配置(用户名 + URL)并下载到本地 +ipcMain.handle('save-splash-config', async (event, username: string, imageUrl: string) => { const config = loadConfig(); config.lastUsername = username; config.splashImageUrl = imageUrl; saveConfig(config); - console.log('[开屏图片] 已保存配置:', { username, imageUrl, path: getConfigPath() }); + + // 如果有图片URL,立即下载到本地缓存 + if (imageUrl && username) { + await downloadImageToLocal(imageUrl, username, 'splash'); + } else if (username) { + // 如果图片URL为空,删除本地缓存 + deleteCachedImage(username, 'splash'); + } + return { success: true }; }); @@ -801,6 +898,48 @@ ipcMain.handle('get-splash-config', () => { return { username: config.lastUsername || '', imageUrl: config.splashImageUrl || '' }; }); +// 保存品牌logo配置 +ipcMain.handle('save-brand-logo-config', async (event, username: string, logoUrl: string) => { + const config = loadConfig(); + config.brandLogoUrl = logoUrl; + saveConfig(config); + + // 如果有logo URL,立即下载到本地缓存 + if (logoUrl && username) { + await downloadImageToLocal(logoUrl, username, 'logo'); + } else if (username) { + // 如果logo URL为空,删除本地缓存 + deleteCachedImage(username, 'logo'); + } + + return { success: true }; +}); + +// 清除用户配置(退出登录时调用) +ipcMain.handle('clear-user-config', async () => { + const config = loadConfig(); + const username = config.lastUsername; + + // 清除配置 + config.lastUsername = ''; + config.splashImageUrl = ''; + config.brandLogoUrl = ''; + saveConfig(config); + + // 删除本地缓存的图片 + if (username) { + deleteCachedImage(username, 'splash'); + deleteCachedImage(username, 'logo'); + } + + return { success: true }; +}); + +// 加载完整配置 +ipcMain.handle('load-config', () => { + return loadConfig(); +}); + async function getFileSize(url: string): Promise { return new Promise((resolve) => { diff --git a/electron-vue-template/src/main/preload.ts b/electron-vue-template/src/main/preload.ts index a54291b..c461547 100644 --- a/electron-vue-template/src/main/preload.ts +++ b/electron-vue-template/src/main/preload.ts @@ -47,6 +47,11 @@ const electronAPI = { saveSplashConfig: (username: string, imageUrl: string) => ipcRenderer.invoke('save-splash-config', username, imageUrl), getSplashConfig: () => ipcRenderer.invoke('get-splash-config'), + // 品牌logo相关 API + saveBrandLogoConfig: (username: string, logoUrl: string) => ipcRenderer.invoke('save-brand-logo-config', username, logoUrl), + loadConfig: () => ipcRenderer.invoke('load-config'), + clearUserConfig: () => ipcRenderer.invoke('clear-user-config'), + onDownloadProgress: (callback: (progress: any) => void) => { ipcRenderer.removeAllListeners('download-progress') ipcRenderer.on('download-progress', (event, progress) => callback(progress)) diff --git a/electron-vue-template/src/main/tray.ts b/electron-vue-template/src/main/tray.ts index 6d57d69..ea3e92c 100644 --- a/electron-vue-template/src/main/tray.ts +++ b/electron-vue-template/src/main/tray.ts @@ -32,10 +32,8 @@ export function createTray(mainWindow: BrowserWindow | null) { } } }) - // 右键菜单 updateTrayMenu(mainWindow) - return tray } diff --git a/electron-vue-template/src/renderer/App.vue b/electron-vue-template/src/renderer/App.vue index fb548f8..3c98a24 100644 --- a/electron-vue-template/src/renderer/App.vue +++ b/electron-vue-template/src/renderer/App.vue @@ -245,7 +245,7 @@ async function handleLoginSuccess(data: { } } -function clearLocalAuth() { +async function clearLocalAuth() { removeToken() isAuthenticated.value = false currentUsername.value = '' @@ -253,9 +253,17 @@ function clearLocalAuth() { vipExpireTime.value = null deviceTrialExpired.value = false accountType.value = 'trial' + brandLogoUrl.value = '' // 清除品牌logo showAuthDialog.value = true showDeviceDialog.value = false SSEManager.disconnect() + + // 清除主进程中的用户配置和缓存 + try { + await (window as any).electronAPI.clearUserConfig() + } catch (error) { + console.warn('清除用户配置失败:', error) + } } async function logout() { @@ -265,7 +273,7 @@ async function logout() { } catch (error) { console.warn('离线通知失败:', error) } - clearLocalAuth() + await clearLocalAuth() } async function handleUserClick() { @@ -374,11 +382,22 @@ async function syncSettingsToElectron() { // 加载品牌logo async function loadBrandLogo() { try { - const username = getUsernameFromToken() - if (!username) return + // 1. 优先从本地缓存读取(秒开) + const config = await (window as any).electronAPI.loadConfig() + if (config.brandLogoUrl) { + brandLogoUrl.value = config.brandLogoUrl + } - const res = await splashApi.getBrandLogo(username) - brandLogoUrl.value = res.data.url + // 2. 后台异步更新(不阻塞UI) + const username = getUsernameFromToken() + if (username) { + const res = await splashApi.getBrandLogo(username) + const newUrl = res.data.url + if (newUrl !== brandLogoUrl.value) { + brandLogoUrl.value = newUrl + await (window as any).electronAPI.saveBrandLogoConfig(username, newUrl) + } + } } catch (error) { brandLogoUrl.value = '' } @@ -411,7 +430,7 @@ const SSEManager = { } }, - handleMessage(e: MessageEvent) { + async handleMessage(e: MessageEvent) { try { if (e.type === 'ping') return @@ -422,11 +441,11 @@ const SSEManager = { console.log('SSE连接已就绪') break case 'DEVICE_REMOVED': - clearLocalAuth() + await clearLocalAuth() ElMessage.warning('会话已失效,请重新登录') break case 'FORCE_LOGOUT': - logout() + await logout() ElMessage.warning('会话已失效,请重新登录') break case 'PERMISSIONS_UPDATED': @@ -529,7 +548,7 @@ async function confirmRemoveDevice(row: DeviceItem) { deviceQuota.value.used = Math.max(0, deviceQuota.value.used - 1) if (row.deviceId === getClientIdFromToken()) { - clearLocalAuth() + await clearLocalAuth() } ElMessage.success('已移除设备') @@ -613,13 +632,37 @@ onUnmounted(() => {