From b1de0e14f1d3ed6e20a406df7e3ba13eb40a85d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A2=81=E6=B6=9B?= Date: Fri, 17 Apr 2026 23:28:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E6=89=98=E7=9B=98=E5=8A=9F=E8=83=BD=E5=8F=8A=E5=BF=AB=E9=80=9F?= =?UTF-8?q?=E5=88=87=E6=8D=A2API=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 13 ++++++ main.js | 129 +++++++++++++++++++++++++++++++++++++++++++++++++-- preload.js | 11 +++-- src/App.vue | 11 ++++- 4 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..590ba89 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# 更新日志 + +## [1.5.1] - 2026-04-17 + +### 新增 +- **系统托盘功能** + - 窗口关闭时最小化到托盘而非退出应用 + - 托盘右键菜单:显示主窗口、切换 API 配置、退出 + - 双击托盘图标恢复显示主窗口 + - 从托盘快速切换 API 配置 + +### 优化 +- API 配置编辑对话框的数据回填逻辑,使用 `(profile && profile.xxx) || fallback` 模式 diff --git a/main.js b/main.js index 0109e96..9f6369a 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, ipcMain, dialog } = require('electron') +const { app, BrowserWindow, ipcMain, dialog, Tray, Menu, nativeImage } = require('electron') const path = require('path') const fs = require('fs') console.log('main.js loaded') @@ -6,7 +6,123 @@ console.log('app.getPath("home"):', app.getPath('home')) const SETTINGS_FILE = path.join(app.getPath('home'), '.iflow', 'settings.json') console.log('SETTINGS_FILE:', SETTINGS_FILE) let mainWindow +let tray const isDev = process.argv.includes('--dev') + +// 创建系统托盘 +function createTray() { + // 使用内置图标或创建空白图标 + const iconPath = path.join(__dirname, 'build', 'icon.ico') + let trayIcon + if (fs.existsSync(iconPath)) { + trayIcon = nativeImage.createFromPath(iconPath) + } else { + // 创建一个简单的图标 + trayIcon = nativeImage.createEmpty() + } + // 调整图标大小以适应托盘 + trayIcon = trayIcon.resize({ width: 16, height: 16 }) + + tray = new Tray(trayIcon) + tray.setToolTip('iFlow 设置编辑器') + + updateTrayMenu() + + // 双击托盘显示主窗口 + tray.on('double-click', () => { + if (mainWindow) { + mainWindow.show() + mainWindow.focus() + } + }) +} + +// 更新托盘菜单 +function updateTrayMenu() { + const settings = readSettings() + const profiles = settings?.apiProfiles || {} + const currentProfile = settings?.currentApiProfile || 'default' + const profileList = Object.keys(profiles).length > 0 + ? Object.keys(profiles) + : ['default'] + + const profileMenuItems = profileList.map(name => ({ + label: name + (name === currentProfile ? ' ✓' : ''), + type: 'radio', + checked: name === currentProfile, + click: () => switchApiProfileFromTray(name) + })) + + const contextMenu = Menu.buildFromTemplate([ + { + label: '显示主窗口', + click: () => { + if (mainWindow) { + mainWindow.show() + mainWindow.focus() + } + } + }, + { type: 'separator' }, + { + label: '切换 API 配置', + submenu: profileMenuItems + }, + { type: 'separator' }, + { + label: '退出', + click: () => { + app.isQuitting = true + app.quit() + } + } + ]) + + tray.setContextMenu(contextMenu) +} + +// 从托盘切换 API 配置 +function switchApiProfileFromTray(profileName) { + try { + const settings = readSettings() + if (!settings) return + + const profiles = settings.apiProfiles || {} + if (!profiles[profileName]) return + + // 保存当前配置到 apiProfiles + const currentProfile = settings.currentApiProfile || 'default' + if (profiles[currentProfile]) { + const currentConfig = {} + for (const field of API_FIELDS) { + if (settings[field] !== undefined) { + currentConfig[field] = settings[field] + } + } + profiles[currentProfile] = currentConfig + } + + // 从 apiProfiles 加载新配置到主字段 + const newConfig = profiles[profileName] + for (const field of API_FIELDS) { + if (newConfig[field] !== undefined) { + settings[field] = newConfig[field] + } + } + settings.currentApiProfile = profileName + settings.apiProfiles = profiles + writeSettings(settings) + + updateTrayMenu() + // 通知渲染进程刷新 + if (mainWindow && mainWindow.webContents) { + mainWindow.webContents.send('api-profile-switched', profileName) + } + } catch (error) { + console.error('切换API配置失败:', error) + } +} + function createWindow() { console.log('Creating window...') mainWindow = new BrowserWindow({ @@ -42,6 +158,7 @@ function createWindow() { mainWindow.once('ready-to-show', () => { console.log('Window ready to show') mainWindow.show() + createTray() }) mainWindow.on('closed', () => { mainWindow = null @@ -49,7 +166,7 @@ function createWindow() { } app.whenReady().then(createWindow) app.on('window-all-closed', () => { - if (process.platform !== 'darwin') { + if (process.platform !== 'darwin' && app.isQuitting) { app.quit() } }) @@ -67,7 +184,13 @@ ipcMain.on('window-maximize', () => { mainWindow.maximize() } }) -ipcMain.on('window-close', () => mainWindow.close()) +ipcMain.on('window-close', () => { + if (!app.isQuitting) { + mainWindow.hide() + } else { + mainWindow.close() + } +}) ipcMain.handle('is-maximized', () => mainWindow.isMaximized()) // API 配置相关的字段 const API_FIELDS = ['selectedAuthType', 'apiKey', 'baseUrl', 'modelName', 'searchApiKey', 'cna'] diff --git a/preload.js b/preload.js index 4b7f546..96ba334 100644 --- a/preload.js +++ b/preload.js @@ -5,18 +5,23 @@ contextBridge.exposeInMainWorld('electronAPI', { loadSettings: () => ipcRenderer.invoke('load-settings'), saveSettings: (data) => ipcRenderer.invoke('save-settings', data), showMessage: (options) => ipcRenderer.invoke('show-message', options), - + // 窗口控制 isMaximized: () => ipcRenderer.invoke('is-maximized'), minimize: () => ipcRenderer.send('window-minimize'), maximize: () => ipcRenderer.send('window-maximize'), close: () => ipcRenderer.send('window-close'), - + // API 配置管理(单文件内多配置) listApiProfiles: () => ipcRenderer.invoke('list-api-profiles'), switchApiProfile: (profileName) => ipcRenderer.invoke('switch-api-profile', profileName), createApiProfile: (name) => ipcRenderer.invoke('create-api-profile', name), deleteApiProfile: (name) => ipcRenderer.invoke('delete-api-profile', name), renameApiProfile: (oldName, newName) => ipcRenderer.invoke('rename-api-profile', oldName, newName), - duplicateApiProfile: (sourceName, newName) => ipcRenderer.invoke('duplicate-api-profile', sourceName, newName) + duplicateApiProfile: (sourceName, newName) => ipcRenderer.invoke('duplicate-api-profile', sourceName, newName), + + // 托盘事件监听 + onApiProfileSwitched: (callback) => { + ipcRenderer.on('api-profile-switched', (event, profileName) => callback(profileName)) + } }) \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 7fc8855..420f588 100644 --- a/src/App.vue +++ b/src/App.vue @@ -391,7 +391,7 @@