You've already forked iFlow-Settings-Editor-GUI
新增 系统托盘功能及快速切换API配置
This commit is contained in:
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 更新日志
|
||||||
|
|
||||||
|
## [1.5.1] - 2026-04-17
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
- **系统托盘功能**
|
||||||
|
- 窗口关闭时最小化到托盘而非退出应用
|
||||||
|
- 托盘右键菜单:显示主窗口、切换 API 配置、退出
|
||||||
|
- 双击托盘图标恢复显示主窗口
|
||||||
|
- 从托盘快速切换 API 配置
|
||||||
|
|
||||||
|
### 优化
|
||||||
|
- API 配置编辑对话框的数据回填逻辑,使用 `(profile && profile.xxx) || fallback` 模式
|
||||||
129
main.js
129
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 path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
console.log('main.js loaded')
|
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')
|
const SETTINGS_FILE = path.join(app.getPath('home'), '.iflow', 'settings.json')
|
||||||
console.log('SETTINGS_FILE:', SETTINGS_FILE)
|
console.log('SETTINGS_FILE:', SETTINGS_FILE)
|
||||||
let mainWindow
|
let mainWindow
|
||||||
|
let tray
|
||||||
const isDev = process.argv.includes('--dev')
|
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() {
|
function createWindow() {
|
||||||
console.log('Creating window...')
|
console.log('Creating window...')
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
@@ -42,6 +158,7 @@ function createWindow() {
|
|||||||
mainWindow.once('ready-to-show', () => {
|
mainWindow.once('ready-to-show', () => {
|
||||||
console.log('Window ready to show')
|
console.log('Window ready to show')
|
||||||
mainWindow.show()
|
mainWindow.show()
|
||||||
|
createTray()
|
||||||
})
|
})
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
mainWindow = null
|
mainWindow = null
|
||||||
@@ -49,7 +166,7 @@ function createWindow() {
|
|||||||
}
|
}
|
||||||
app.whenReady().then(createWindow)
|
app.whenReady().then(createWindow)
|
||||||
app.on('window-all-closed', () => {
|
app.on('window-all-closed', () => {
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin' && app.isQuitting) {
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -67,7 +184,13 @@ ipcMain.on('window-maximize', () => {
|
|||||||
mainWindow.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())
|
ipcMain.handle('is-maximized', () => mainWindow.isMaximized())
|
||||||
// API 配置相关的字段
|
// API 配置相关的字段
|
||||||
const API_FIELDS = ['selectedAuthType', 'apiKey', 'baseUrl', 'modelName', 'searchApiKey', 'cna']
|
const API_FIELDS = ['selectedAuthType', 'apiKey', 'baseUrl', 'modelName', 'searchApiKey', 'cna']
|
||||||
|
|||||||
@@ -18,5 +18,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
createApiProfile: (name) => ipcRenderer.invoke('create-api-profile', name),
|
createApiProfile: (name) => ipcRenderer.invoke('create-api-profile', name),
|
||||||
deleteApiProfile: (name) => ipcRenderer.invoke('delete-api-profile', name),
|
deleteApiProfile: (name) => ipcRenderer.invoke('delete-api-profile', name),
|
||||||
renameApiProfile: (oldName, newName) => ipcRenderer.invoke('rename-api-profile', oldName, newName),
|
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))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
11
src/App.vue
11
src/App.vue
@@ -391,7 +391,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||||
import { Save, Config, Key, Server, Globe, Setting, Add, Edit, Delete, Exchange, Copy } from '@icon-park/vue-next'
|
import { Save, Config, Key, Server, Globe, Setting, Add, Edit, Delete, Exchange, Copy } from '@icon-park/vue-next'
|
||||||
const settings = ref({
|
const settings = ref({
|
||||||
language: 'zh-CN',
|
language: 'zh-CN',
|
||||||
@@ -851,6 +851,15 @@ watch(
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadApiProfiles()
|
await loadApiProfiles()
|
||||||
await loadSettings()
|
await loadSettings()
|
||||||
|
// 监听托盘切换 API 配置事件
|
||||||
|
window.electronAPI.onApiProfileSwitched(async (profileName) => {
|
||||||
|
currentApiProfile.value = profileName
|
||||||
|
await loadSettings()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
// 清理监听器
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
Reference in New Issue
Block a user