diff --git a/electron/main.ts b/electron/main.ts index 26954ac..b377515 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -439,6 +439,7 @@ const pruneChatHistoryPayloadStore = (): void => { } type WindowCloseBehavior = 'ask' | 'tray' | 'quit' +type CloseRestoreMethod = 'tray' | 'dock' // 更新下载状态管理(Issue #294 修复) let isDownloadInProgress = false @@ -812,29 +813,47 @@ const isSilentStartupEnabled = (): boolean => { return configService?.get('silentStartup') === true } +const getCloseRestoreMethod = (): CloseRestoreMethod | null => { + if (tray) return 'tray' + if (process.platform === 'darwin') return 'dock' + return null +} + +const canKeepMainWindowInBackground = (): boolean => { + return getCloseRestoreMethod() !== null +} + +const getPlatformIconName = (): string => { + if (process.platform === 'linux') return 'icon.png' + if (process.platform === 'darwin') return 'icon.icns' + return 'icon.ico' +} + +const resolveAppIconPath = (): string => { + const iconName = getPlatformIconName() + if (!process.env.VITE_DEV_SERVER_URL) { + return join(process.resourcesPath, iconName) + } + if (process.platform === 'darwin') { + return join(__dirname, '../resources/icons/macos/icon.icns') + } + return join(__dirname, `../public/${iconName}`) +} + const requestMainWindowCloseConfirmation = (win: BrowserWindow): void => { if (isClosePromptVisible) return isClosePromptVisible = true + const restoreMethod = getCloseRestoreMethod() win.webContents.send('window:confirmCloseRequested', { - canMinimizeToTray: Boolean(tray) + canMinimizeToTray: restoreMethod !== null, + restoreMethod: restoreMethod ?? undefined }) } function createWindow(options: { autoShow?: boolean } = {}) { // 获取图标路径 - 打包后在 resources 目录 const { autoShow = true } = options - let iconName = 'icon.ico'; - if (process.platform === 'linux') { - iconName = 'icon.png'; - } else if (process.platform === 'darwin') { - iconName = 'icon.icns'; - } - - const isDev = !!process.env.VITE_DEV_SERVER_URL - - const iconPath = isDev - ? join(__dirname, `../public/${iconName}`) - : join(process.resourcesPath, iconName); + const iconPath = resolveAppIconPath() const win = new BrowserWindow({ width: 1400, @@ -907,7 +926,7 @@ function createWindow(options: { autoShow?: boolean } = {}) { return } - if (closeBehavior === 'tray' && tray) { + if (closeBehavior === 'tray' && canKeepMainWindowInBackground()) { win.hide() return } @@ -2167,7 +2186,7 @@ function registerIpcHandlers() { try { if (action === 'tray') { - if (tray) { + if (canKeepMainWindowInBackground()) { mainWindow.hide() return true } @@ -4313,18 +4332,7 @@ app.whenReady().then(async () => { ensureWeChatRequestHeaderInterceptor() mainWindow = createWindow({ autoShow: false }) - let iconName = 'icon.ico'; - if (process.platform === 'linux') { - iconName = 'icon.png'; - } else if (process.platform === 'darwin') { - iconName = 'icon.icns'; - } - - const isDev = !!process.env.VITE_DEV_SERVER_URL - - const resolvedTrayIcon = isDev - ? join(__dirname, `../public/${iconName}`) - : join(process.resourcesPath, iconName); + const resolvedTrayIcon = resolveAppIconPath() try { @@ -4402,6 +4410,14 @@ app.whenReady().then(async () => { await httpService.autoStart() app.on('activate', () => { + if (mainWindow && !mainWindow.isDestroyed()) { + if (!mainWindow.isVisible()) { + mainWindow.show() + } + mainWindow.focus() + return + } + if (BrowserWindow.getAllWindows().length === 0) { mainWindow = createWindow() } @@ -4447,4 +4463,3 @@ app.on('window-all-closed', () => { app.quit() } }) - diff --git a/electron/preload.ts b/electron/preload.ts index 0a5e187..a5ff64d 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -1,5 +1,10 @@ import { contextBridge, ipcRenderer } from 'electron' +type CloseConfirmPayload = { + canMinimizeToTray: boolean + restoreMethod?: 'tray' | 'dock' +} + // 暴露给渲染进程的 API contextBridge.exposeInMainWorld('electronAPI', { // 配置 @@ -106,8 +111,8 @@ contextBridge.exposeInMainWorld('electronAPI', { return () => ipcRenderer.removeListener('window:maximizeStateChanged', listener) }, close: () => ipcRenderer.send('window:close'), - onCloseConfirmRequested: (callback: (payload: { canMinimizeToTray: boolean }) => void) => { - const listener = (_: unknown, payload: { canMinimizeToTray: boolean }) => callback(payload) + onCloseConfirmRequested: (callback: (payload: CloseConfirmPayload) => void) => { + const listener = (_: unknown, payload: CloseConfirmPayload) => callback(payload) ipcRenderer.on('window:confirmCloseRequested', listener) return () => ipcRenderer.removeListener('window:confirmCloseRequested', listener) }, diff --git a/src/App.tsx b/src/App.tsx index f217457..78cdd84 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -94,6 +94,7 @@ function App() { const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [showCloseDialog, setShowCloseDialog] = useState(false) const [canMinimizeToTray, setCanMinimizeToTray] = useState(false) + const [closeRestoreMethod, setCloseRestoreMethod] = useState<'tray' | 'dock'>('tray') // 锁定状态 // const [isLocked, setIsLocked] = useState(false) // Moved to store @@ -120,6 +121,7 @@ function App() { useEffect(() => { const removeCloseConfirmListener = window.electronAPI.window.onCloseConfirmRequested((payload) => { setCanMinimizeToTray(Boolean(payload.canMinimizeToTray)) + setCloseRestoreMethod(payload.restoreMethod === 'dock' ? 'dock' : 'tray') setShowCloseDialog(true) }) @@ -685,6 +687,7 @@ function App() { handleWindowCloseAction(action, rememberChoice)} onCancel={() => handleWindowCloseAction('cancel')} /> diff --git a/src/components/WindowCloseDialog.tsx b/src/components/WindowCloseDialog.tsx index ea838ea..85c6f2e 100644 --- a/src/components/WindowCloseDialog.tsx +++ b/src/components/WindowCloseDialog.tsx @@ -5,6 +5,7 @@ import './WindowCloseDialog.scss' interface WindowCloseDialogProps { open: boolean canMinimizeToTray: boolean + restoreMethod?: 'tray' | 'dock' onSelect: (action: 'tray' | 'quit', rememberChoice: boolean) => void onCancel: () => void } @@ -12,10 +13,12 @@ interface WindowCloseDialogProps { export default function WindowCloseDialog({ open, canMinimizeToTray, + restoreMethod = 'tray', onSelect, onCancel }: WindowCloseDialogProps) { const [rememberChoice, setRememberChoice] = useState(false) + const isDockRestore = restoreMethod === 'dock' useEffect(() => { if (!open) return @@ -57,7 +60,9 @@ export default function WindowCloseDialog({

关闭 WeFlow

{canMinimizeToTray - ? '你可以保留后台进程与本地 API,或者直接完全退出应用。' + ? isDockRestore + ? '你可以隐藏主窗口并保留后台进程与本地 API,稍后可从 Dock 或重新打开应用恢复。' + : '你可以保留后台进程与本地 API,或者直接完全退出应用。' : '当前系统托盘不可用,本次只能完全退出应用。'}

@@ -73,8 +78,12 @@ export default function WindowCloseDialog({ - 最小化到系统托盘 - 继续保留后台进程和本地 API,稍后可从托盘恢复。 + {isDockRestore ? '隐藏主窗口' : '最小化到系统托盘'} + + {isDockRestore + ? '继续保留后台进程和本地 API,稍后可从 Dock 或重新打开应用恢复。' + : '继续保留后台进程和本地 API,稍后可从托盘恢复。'} + )} diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 9936f6a..cdf804d 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -278,6 +278,11 @@ export interface BackupManifest { } } +export type CloseConfirmPayload = { + canMinimizeToTray: boolean + restoreMethod?: 'tray' | 'dock' +} + export interface ElectronAPI { window: { minimize: () => void @@ -285,7 +290,7 @@ export interface ElectronAPI { isMaximized: () => Promise onMaximizeStateChanged: (callback: (isMaximized: boolean) => void) => () => void close: () => void - onCloseConfirmRequested: (callback: (payload: { canMinimizeToTray: boolean }) => void) => () => void + onCloseConfirmRequested: (callback: (payload: CloseConfirmPayload) => void) => () => void respondCloseConfirm: (action: 'tray' | 'quit' | 'cancel') => Promise openAgreementWindow: () => Promise completeOnboarding: () => Promise