From b9a7b6a8891f2a92effdc5baaff6aa8344894f29 Mon Sep 17 00:00:00 2001 From: AoXuan Date: Fri, 5 Sep 2025 00:27:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E8=AE=B0=E5=BF=86?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E4=BD=8D=E7=BD=AE=EF=BC=8C=E6=9C=80=E5=B0=8F?= =?UTF-8?q?=E5=8C=96=E5=88=B0=E6=89=98=E7=9B=98=E5=92=8C=E6=89=98=E7=9B=98?= =?UTF-8?q?=E5=8C=BA=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/electron/main.ts | 401 +++++++++++++++++++++++++++++--- frontend/electron/preload.ts | 3 + frontend/src/types/settings.ts | 4 +- frontend/src/views/Settings.vue | 18 +- 4 files changed, 394 insertions(+), 32 deletions(-) diff --git a/frontend/electron/main.ts b/frontend/electron/main.ts index 4e8cc36..c634dd3 100644 --- a/frontend/electron/main.ts +++ b/frontend/electron/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain, dialog, shell } from 'electron' +import { app, BrowserWindow, ipcMain, dialog, shell, Tray, Menu, nativeImage } from 'electron' import * as path from 'path' import * as fs from 'fs' import { spawn } from 'child_process' @@ -59,16 +59,232 @@ function restartAsAdmin(): void { } let mainWindow: BrowserWindow | null = null +let tray: Tray | null = null +let isQuitting = false +let saveWindowStateTimeout: NodeJS.Timeout | null = null + +// 配置接口 +interface AppConfig { + UI: { + IfShowTray: boolean + IfToTray: boolean + location: string + maximized: boolean + size: string + } + Start: { + IfMinimizeDirectly: boolean + IfSelfStart: boolean + } + [key: string]: any +} + +// 默认配置 +const defaultConfig: AppConfig = { + UI: { + IfShowTray: false, + IfToTray: false, + location: '100,100', + maximized: false, + size: '1600,1000' + }, + Start: { + IfMinimizeDirectly: false, + IfSelfStart: false + } +} + +// 加载配置 +function loadConfig(): AppConfig { + try { + const appRoot = getAppRoot() + const configPath = path.join(appRoot, 'config', 'frontend_config.json') + + if (fs.existsSync(configPath)) { + const configData = fs.readFileSync(configPath, 'utf8') + const config = JSON.parse(configData) + return { ...defaultConfig, ...config } + } + } catch (error) { + log.error('加载配置失败:', error) + } + return defaultConfig +} + +// 保存配置 +function saveConfig(config: AppConfig) { + try { + const appRoot = getAppRoot() + const configDir = path.join(appRoot, 'config') + const configPath = path.join(configDir, 'frontend_config.json') + + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }) + } + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8') + } catch (error) { + log.error('保存配置失败:', error) + } +} + +// 创建托盘 +function createTray() { + if (tray) return + + // 尝试多个可能的图标路径 + const iconPaths = [ + path.join(__dirname, '../public/AUTO-MAS.ico'), + path.join(process.resourcesPath, 'assets/AUTO-MAS.ico'), + path.join(app.getAppPath(), 'public/AUTO-MAS.ico'), + path.join(app.getAppPath(), 'dist/AUTO-MAS.ico') + ] + + let trayIcon + + try { + // 尝试加载图标 + for (const iconPath of iconPaths) { + if (fs.existsSync(iconPath)) { + trayIcon = nativeImage.createFromPath(iconPath) + if (!trayIcon.isEmpty()) { + log.info(`成功加载托盘图标: ${iconPath}`) + break + } + } + } + + // 如果所有路径都失败,创建一个默认图标 + if (!trayIcon || trayIcon.isEmpty()) { + log.warn('无法加载托盘图标,使用默认图标') + trayIcon = nativeImage.createEmpty() + } + } catch (error) { + log.error('加载托盘图标失败:', error) + trayIcon = nativeImage.createEmpty() + } + + tray = new Tray(trayIcon) + + const contextMenu = Menu.buildFromTemplate([ + { + label: '显示窗口', + click: () => { + if (mainWindow) { + if (mainWindow.isMinimized()) { + mainWindow.restore() + } + mainWindow.setSkipTaskbar(false) // 恢复任务栏图标 + mainWindow.show() + mainWindow.focus() + } + } + }, + { + label: '隐藏窗口', + click: () => { + if (mainWindow) { + const currentConfig = loadConfig() + if (currentConfig.UI.IfToTray) { + mainWindow.setSkipTaskbar(true) // 隐藏任务栏图标 + } + mainWindow.hide() + } + } + }, + { type: 'separator' }, + { + label: '退出', + click: () => { + isQuitting = true + app.quit() + } + } + ]) + + tray.setContextMenu(contextMenu) + tray.setToolTip('AUTO_MAA') + + // 双击托盘图标显示/隐藏窗口 + tray.on('double-click', () => { + if (mainWindow) { + const currentConfig = loadConfig() + if (mainWindow.isVisible()) { + if (currentConfig.UI.IfToTray) { + mainWindow.setSkipTaskbar(true) // 隐藏任务栏图标 + } + mainWindow.hide() + } else { + if (mainWindow.isMinimized()) { + mainWindow.restore() + } + mainWindow.setSkipTaskbar(false) // 恢复任务栏图标 + mainWindow.show() + mainWindow.focus() + } + } + }) +} + +// 销毁托盘 +function destroyTray() { + if (tray) { + tray.destroy() + tray = null + } +} + +// 更新托盘状态 +function updateTrayVisibility(config: AppConfig) { + // 根据需求逻辑判断是否应该显示托盘 + let shouldShowTray = false + + if (config.UI.IfShowTray && config.UI.IfToTray) { + // 勾选常驻显示托盘和最小化到托盘,就一直展示托盘 + shouldShowTray = true + } else if (config.UI.IfShowTray && !config.UI.IfToTray) { + // 勾选常驻显示托盘但没有最小化到托盘,就一直展示托盘 + shouldShowTray = true + } else if (!config.UI.IfShowTray && config.UI.IfToTray) { + // 没有常驻显示托盘但勾选最小化到托盘,有窗口时就只有窗口,最小化后任务栏消失,只有托盘 + shouldShowTray = !mainWindow || !mainWindow.isVisible() + } else { + // 没有常驻显示托盘也没有最小化到托盘,托盘一直不展示 + shouldShowTray = false + } + + // 特殊情况:如果没有窗口显示且没有托盘,强制显示托盘避免程序成为幽灵 + if (!shouldShowTray && (!mainWindow || !mainWindow.isVisible()) && !tray) { + shouldShowTray = true + log.warn('防幽灵机制:强制显示托盘图标') + } + + if (shouldShowTray && !tray) { + createTray() + log.info('托盘图标已创建') + } else if (!shouldShowTray && tray) { + destroyTray() + log.info('托盘图标已销毁') + } +} function createWindow() { log.info('开始创建主窗口') + const config = loadConfig() + + // 解析窗口大小 + const [width, height] = config.UI.size.split(',').map(s => parseInt(s.trim()) || 1600) + const [x, y] = config.UI.location.split(',').map(s => parseInt(s.trim()) || 100) + mainWindow = new BrowserWindow({ - width: 1600, - height: 1000, + width: Math.max(width, 800), + height: Math.max(height, 600), + x, + y, minWidth: 800, minHeight: 600, - icon: path.join(__dirname, '../src/assets/AUTO-MAS.ico'), + icon: path.join(__dirname, '../public/AUTO-MAS.ico'), frame: false, // 去掉系统标题栏 titleBarStyle: 'hidden', // 隐藏标题栏 webPreferences: { @@ -77,8 +293,14 @@ function createWindow() { contextIsolation: true, }, autoHideMenuBar: true, + show: !config.Start.IfMinimizeDirectly, // 根据配置决定是否直接显示 }) + // 如果配置为最大化,则最大化窗口 + if (config.UI.maximized) { + mainWindow.maximize() + } + mainWindow.setMenuBarVisibility(false) const devServer = process.env.VITE_DEV_SERVER_URL if (devServer) { @@ -90,11 +312,72 @@ function createWindow() { mainWindow.loadFile(indexHtmlPath) } + // 窗口事件处理 + mainWindow.on('close', (event) => { + const currentConfig = loadConfig() + + if (!isQuitting && currentConfig.UI.IfToTray) { + // 如果启用了最小化到托盘,阻止关闭并隐藏窗口 + event.preventDefault() + mainWindow?.hide() + mainWindow?.setSkipTaskbar(true) + + // 更新托盘状态 + updateTrayVisibility(currentConfig) + + log.info('窗口已最小化到托盘,任务栏图标已隐藏') + } else { + // 保存窗口状态 + saveWindowState() + } + }) + mainWindow.on('closed', () => { log.info('主窗口已关闭') mainWindow = null }) + // 窗口最小化事件 + mainWindow.on('minimize', () => { + const currentConfig = loadConfig() + + if (currentConfig.UI.IfToTray) { + // 如果启用了最小化到托盘,隐藏窗口并从任务栏移除 + mainWindow?.hide() + mainWindow?.setSkipTaskbar(true) + + // 更新托盘状态 + updateTrayVisibility(currentConfig) + + log.info('窗口已最小化到托盘,任务栏图标已隐藏') + } + }) + + // 窗口显示/隐藏事件,用于更新托盘状态 + mainWindow.on('show', () => { + const currentConfig = loadConfig() + // 窗口显示时,恢复任务栏图标 + mainWindow?.setSkipTaskbar(false) + updateTrayVisibility(currentConfig) + log.info('窗口已显示,任务栏图标已恢复') + }) + + mainWindow.on('hide', () => { + const currentConfig = loadConfig() + // 窗口隐藏时,根据配置决定是否隐藏任务栏图标 + if (currentConfig.UI.IfToTray) { + mainWindow?.setSkipTaskbar(true) + log.info('窗口已隐藏,任务栏图标已隐藏') + } + updateTrayVisibility(currentConfig) + }) + + // 窗口移动和调整大小时保存状态 + mainWindow.on('moved', saveWindowState) + mainWindow.on('resized', saveWindowState) + mainWindow.on('maximize', saveWindowState) + mainWindow.on('unmaximize', saveWindowState) + // 设置各个服务的主窗口引用 if (mainWindow) { setDownloadMainWindow(mainWindow) @@ -102,6 +385,40 @@ function createWindow() { setGitMainWindow(mainWindow) log.info('主窗口创建完成,服务引用已设置') } + + // 根据配置初始化托盘 + updateTrayVisibility(config) +} + +// 保存窗口状态(带防抖) +function saveWindowState() { + if (!mainWindow) return + + // 清除之前的定时器 + if (saveWindowStateTimeout) { + clearTimeout(saveWindowStateTimeout) + } + + // 设置新的定时器,500ms后保存 + saveWindowStateTimeout = setTimeout(() => { + try { + const config = loadConfig() + const bounds = mainWindow!.getBounds() + const isMaximized = mainWindow!.isMaximized() + + // 只有在窗口不是最大化状态时才保存位置和大小 + if (!isMaximized) { + config.UI.size = `${bounds.width},${bounds.height}` + config.UI.location = `${bounds.x},${bounds.y}` + } + config.UI.maximized = isMaximized + + saveConfig(config) + log.info('窗口状态已保存') + } catch (error) { + log.error('保存窗口状态失败:', error) + } + }, 500) } // IPC处理函数 @@ -158,7 +475,7 @@ ipcMain.handle('select-file', async (event, filters = []) => { }) // 在系统默认浏览器中打开URL -ipcMain.handle('open-url', async (event, url: string) => { +ipcMain.handle('open-url', async (_event, url: string) => { try { await shell.openExternal(url) return { success: true } @@ -180,7 +497,7 @@ ipcMain.handle('check-environment', async () => { }) // Python相关 -ipcMain.handle('download-python', async (event, mirror = 'tsinghua') => { +ipcMain.handle('download-python', async (_event, mirror = 'tsinghua') => { const appRoot = getAppRoot() return downloadPython(appRoot, mirror) }) @@ -190,7 +507,7 @@ ipcMain.handle('install-pip', async () => { return installPipPackage(appRoot) }) -ipcMain.handle('install-dependencies', async (event, mirror = 'tsinghua') => { +ipcMain.handle('install-dependencies', async (_event, mirror = 'tsinghua') => { const appRoot = getAppRoot() return installDependencies(appRoot, mirror) }) @@ -208,7 +525,7 @@ ipcMain.handle('download-git', async () => { ipcMain.handle( 'clone-backend', - async (event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') => { + async (_event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') => { const appRoot = getAppRoot() return cloneBackend(appRoot, repoUrl) } @@ -216,14 +533,14 @@ ipcMain.handle( ipcMain.handle( 'update-backend', - async (event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') => { + async (_event, repoUrl = 'https://github.com/DLmaster361/AUTO_MAA.git') => { const appRoot = getAppRoot() return cloneBackend(appRoot, repoUrl) // 使用相同的逻辑,会自动判断是pull还是clone } ) // 配置文件操作 -ipcMain.handle('save-config', async (event, config) => { +ipcMain.handle('save-config', async (_event, config) => { try { const appRoot = getAppRoot() const configDir = path.join(appRoot, 'config') @@ -236,12 +553,36 @@ ipcMain.handle('save-config', async (event, config) => { fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8') console.log(`配置已保存到: ${configPath}`) + + // 如果是UI配置更新,需要更新托盘状态 + if (config.UI) { + updateTrayVisibility(config) + } } catch (error) { console.error('保存配置文件失败:', error) throw error } }) +// 新增:实时更新托盘状态的IPC处理器 +ipcMain.handle('update-tray-settings', async (_event, uiSettings) => { + try { + // 先更新配置文件 + const currentConfig = loadConfig() + currentConfig.UI = { ...currentConfig.UI, ...uiSettings } + saveConfig(currentConfig) + + // 立即更新托盘状态 + updateTrayVisibility(currentConfig) + + log.info('托盘设置已更新:', uiSettings) + return true + } catch (error) { + log.error('更新托盘设置失败:', error) + throw error + } +}) + ipcMain.handle('load-config', async () => { try { const appRoot = getAppRoot() @@ -285,7 +626,7 @@ ipcMain.handle('get-log-path', async () => { } }) -ipcMain.handle('get-log-files', async () => { +ipcMain.handle('get-log-files', async (_event) => { try { return getLogFiles() } catch (error) { @@ -294,7 +635,7 @@ ipcMain.handle('get-log-files', async () => { } }) -ipcMain.handle('get-logs', async (event, lines?: number, fileName?: string) => { +ipcMain.handle('get-logs', async (_event, lines?: number, fileName?: string) => { try { let logFilePath: string @@ -325,7 +666,7 @@ ipcMain.handle('get-logs', async (event, lines?: number, fileName?: string) => { } }) -ipcMain.handle('clear-logs', async (event, fileName?: string) => { +ipcMain.handle('clear-logs', async (_event, fileName?: string) => { try { let logFilePath: string @@ -348,7 +689,7 @@ ipcMain.handle('clear-logs', async (event, fileName?: string) => { } }) -ipcMain.handle('clean-old-logs', async (event, daysToKeep = 7) => { +ipcMain.handle('clean-old-logs', async (_event, daysToKeep = 7) => { try { cleanOldLogs(daysToKeep) log.info(`已清理${daysToKeep}天前的旧日志文件`) @@ -359,7 +700,7 @@ ipcMain.handle('clean-old-logs', async (event, daysToKeep = 7) => { }) // 保留原有的日志操作方法以兼容现有代码 -ipcMain.handle('save-logs-to-file', async (event, logs: string) => { +ipcMain.handle('save-logs-to-file', async (_event, logs: string) => { try { const appRoot = getAppRoot() const logsDir = path.join(appRoot, 'logs') @@ -423,17 +764,25 @@ app.on('second-instance', () => { app.on('before-quit', async event => { // 只处理一次,避免多重触发 - event.preventDefault() - log.info('应用准备退出') - try { - await stopBackend() - log.info('后端服务已停止') - } catch (e) { - log.error('停止后端时出错:', e) - console.error('停止后端时出错:', e) - } finally { - log.info('应用退出') - app.exit(0) + if (!isQuitting) { + event.preventDefault() + isQuitting = true + + log.info('应用准备退出') + + // 清理托盘 + destroyTray() + + try { + await stopBackend() + log.info('后端服务已停止') + } catch (e) { + log.error('停止后端时出错:', e) + console.error('停止后端时出错:', e) + } finally { + log.info('应用退出') + app.exit(0) + } } }) diff --git a/frontend/electron/preload.ts b/frontend/electron/preload.ts index 6aae513..2f19132 100644 --- a/frontend/electron/preload.ts +++ b/frontend/electron/preload.ts @@ -35,6 +35,9 @@ contextBridge.exposeInMainWorld('electronAPI', { saveConfig: (config: any) => ipcRenderer.invoke('save-config', config), loadConfig: () => ipcRenderer.invoke('load-config'), resetConfig: () => ipcRenderer.invoke('reset-config'), + + // 托盘设置实时更新 + updateTraySettings: (uiSettings: any) => ipcRenderer.invoke('update-tray-settings', uiSettings), // 日志文件操作 getLogPath: () => ipcRenderer.invoke('get-log-path'), diff --git a/frontend/src/types/settings.ts b/frontend/src/types/settings.ts index adfdbd7..8488f4c 100644 --- a/frontend/src/types/settings.ts +++ b/frontend/src/types/settings.ts @@ -40,8 +40,8 @@ export interface SettingsData { IfSelfStart: boolean } UI: { - IfShowTray: boolean - IfToTray: boolean + IfShowTray: boolean // 常驻显示托盘 + IfToTray: boolean // 最小化到托盘 location: string maximized: boolean size: string diff --git a/frontend/src/views/Settings.vue b/frontend/src/views/Settings.vue index 68cc749..8709f0e 100644 --- a/frontend/src/views/Settings.vue +++ b/frontend/src/views/Settings.vue @@ -4,13 +4,11 @@ import { useRouter } from 'vue-router' import { Button, Card, - Divider, Radio, Select, Space, Switch, Tabs, - InputNumber, Input, message, } from 'ant-design-vue' @@ -172,6 +170,17 @@ const saveSettings = async (category: keyof SettingsData, changes: any) => { const handleSettingChange = async (category: keyof SettingsData, key: string, value: any) => { const changes = { [key]: value } await saveSettings(category, changes) + + // 如果是UI设置的托盘相关配置,立即更新托盘状态 + if (category === 'UI' && (key === 'IfShowTray' || key === 'IfToTray')) { + try { + if ((window as any).electronAPI && (window as any).electronAPI.updateTraySettings) { + await (window as any).electronAPI.updateTraySettings({ [key]: value }) + } + } catch (error) { + console.error('更新托盘设置失败:', error) + } + } } const handleThemeModeChange = (e: any) => { @@ -570,20 +579,21 @@ onMounted(() => {

系统托盘

+

配置系统托盘的显示和行为

- 显示系统托盘图标 + 常驻显示托盘图标
- 关闭时最小化到托盘 + 最小化到托盘