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 @@