fix: allow macOS close fallback without tray

This commit is contained in:
J1amo
2026-05-26 03:45:37 +09:00
parent 305bdcb629
commit cb497d83c7
5 changed files with 71 additions and 34 deletions

View File

@@ -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()
}
})

View File

@@ -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)
},

View File

@@ -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() {
<WindowCloseDialog
open={showCloseDialog}
canMinimizeToTray={canMinimizeToTray}
restoreMethod={closeRestoreMethod}
onSelect={(action, rememberChoice) => handleWindowCloseAction(action, rememberChoice)}
onCancel={() => handleWindowCloseAction('cancel')}
/>

View File

@@ -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({
<h2 id="window-close-dialog-title"> WeFlow</h2>
<p>
{canMinimizeToTray
? '你可以保留后台进程与本地 API或者直接完全退出应用。'
? isDockRestore
? '你可以隐藏主窗口并保留后台进程与本地 API稍后可从 Dock 或重新打开应用恢复。'
: '你可以保留后台进程与本地 API或者直接完全退出应用。'
: '当前系统托盘不可用,本次只能完全退出应用。'}
</p>
</div>
@@ -73,8 +78,12 @@ export default function WindowCloseDialog({
<Minimize2 size={18} />
</span>
<span className="window-close-dialog-option-text">
<strong></strong>
<span> API</span>
<strong>{isDockRestore ? '隐藏主窗口' : '最小化到系统托盘'}</strong>
<span>
{isDockRestore
? '继续保留后台进程和本地 API稍后可从 Dock 或重新打开应用恢复。'
: '继续保留后台进程和本地 API稍后可从托盘恢复。'}
</span>
</span>
</button>
)}

View File

@@ -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<boolean>
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<boolean>
openAgreementWindow: () => Promise<boolean>
completeOnboarding: () => Promise<boolean>