From f2b1b07f5803eb753f0a1358d0caf111e950918d Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Mon, 16 Mar 2026 17:21:59 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AF=A2=E9=97=AE=E7=AA=97?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main.ts | 33 ++++++++++++-- electron/services/config.ts | 2 + src/App.tsx | 16 +++++-- src/components/WindowCloseDialog.scss | 66 +++++++++++++++++++++++++++ src/components/WindowCloseDialog.tsx | 29 +++++++++--- src/pages/SettingsPage.tsx | 64 +++++++++++++++++++++++++- src/services/config.ts | 13 ++++++ 7 files changed, 206 insertions(+), 17 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 0727f62..34084c9 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -99,6 +99,8 @@ let isAppQuitting = false let tray: Tray | null = null let isClosePromptVisible = false +type WindowCloseBehavior = 'ask' | 'tray' | 'quit' + // 更新下载状态管理(Issue #294 修复) let isDownloadInProgress = false let downloadProgressHandler: ((progress: any) => void) | null = null @@ -254,6 +256,19 @@ const setupCustomTitleBarWindow = (win: BrowserWindow): void => { win.webContents.on('did-finish-load', emitMaximizeState) } +const getWindowCloseBehavior = (): WindowCloseBehavior => { + const behavior = configService?.get('windowCloseBehavior') + return behavior === 'tray' || behavior === 'quit' ? behavior : 'ask' +} + +const requestMainWindowCloseConfirmation = (win: BrowserWindow): void => { + if (isClosePromptVisible) return + isClosePromptVisible = true + win.webContents.send('window:confirmCloseRequested', { + canMinimizeToTray: Boolean(tray) + }) +} + function createWindow(options: { autoShow?: boolean } = {}) { // 获取图标路径 - 打包后在 resources 目录 const { autoShow = true } = options @@ -357,12 +372,20 @@ function createWindow(options: { autoShow?: boolean } = {}) { win.on('close', (e) => { if (isAppQuitting || win !== mainWindow) return e.preventDefault() - if (isClosePromptVisible) return + const closeBehavior = getWindowCloseBehavior() - isClosePromptVisible = true - win.webContents.send('window:confirmCloseRequested', { - canMinimizeToTray: Boolean(tray) - }) + if (closeBehavior === 'quit') { + isAppQuitting = true + app.quit() + return + } + + if (closeBehavior === 'tray' && tray) { + win.hide() + return + } + + requestMainWindowCloseConfirmation(win) }) win.on('closed', () => { diff --git a/electron/services/config.ts b/electron/services/config.ts index 689521b..d783c49 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -50,6 +50,7 @@ interface ConfigSchema { notificationPosition: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' notificationFilterMode: 'all' | 'whitelist' | 'blacklist' notificationFilterList: string[] + windowCloseBehavior: 'ask' | 'tray' | 'quit' wordCloudExcludeWords: string[] } @@ -116,6 +117,7 @@ export class ConfigService { notificationPosition: 'top-right', notificationFilterMode: 'all', notificationFilterList: [], + windowCloseBehavior: 'ask', wordCloudExcludeWords: [] } }) diff --git a/src/App.tsx b/src/App.tsx index 8b9a903..6f41759 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -327,8 +327,19 @@ function App() { setUpdateInfo(null) } - const handleWindowCloseAction = async (action: 'tray' | 'quit' | 'cancel') => { + const handleWindowCloseAction = async ( + action: 'tray' | 'quit' | 'cancel', + rememberChoice = false + ) => { setShowCloseDialog(false) + if (rememberChoice && action !== 'cancel') { + try { + await configService.setWindowCloseBehavior(action) + } catch (error) { + console.error('保存关闭偏好失败:', error) + } + } + try { await window.electronAPI.window.respondCloseConfirm(action) } catch (error) { @@ -617,8 +628,7 @@ function App() { handleWindowCloseAction('tray')} - onQuit={() => handleWindowCloseAction('quit')} + onSelect={(action, rememberChoice) => handleWindowCloseAction(action, rememberChoice)} onCancel={() => handleWindowCloseAction('cancel')} /> diff --git a/src/components/WindowCloseDialog.scss b/src/components/WindowCloseDialog.scss index 282ac55..ecc6907 100644 --- a/src/components/WindowCloseDialog.scss +++ b/src/components/WindowCloseDialog.scss @@ -140,6 +140,72 @@ justify-content: flex-end; } +.window-close-dialog-remember { + display: flex; + align-items: center; + gap: 10px; + margin: 4px 24px 0; + padding: 12px 14px; + border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); + border-radius: 16px; + background: color-mix(in srgb, var(--bg-secondary) 76%, transparent); + cursor: pointer; + user-select: none; + + input { + position: absolute; + opacity: 0; + pointer-events: none; + } +} + +.window-close-dialog-checkbox { + width: 18px; + height: 18px; + flex: 0 0 18px; + border: 1.5px solid color-mix(in srgb, var(--border-color) 88%, transparent); + border-radius: 6px; + background: var(--bg-primary); + position: relative; + transition: + border-color 0.18s ease, + background 0.18s ease, + box-shadow 0.18s ease; + + &::after { + content: ''; + position: absolute; + left: 5px; + top: 1px; + width: 5px; + height: 10px; + border: solid white; + border-width: 0 2px 2px 0; + transform: rotate(45deg) scale(0.7); + opacity: 0; + transition: + opacity 0.18s ease, + transform 0.18s ease; + } +} + +.window-close-dialog-remember input:checked + .window-close-dialog-checkbox { + background: var(--primary); + border-color: var(--primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 16%, transparent); +} + +.window-close-dialog-remember input:checked + .window-close-dialog-checkbox::after { + opacity: 1; + transform: rotate(45deg) scale(1); +} + +.window-close-dialog-remember-text { + font-size: 13px; + line-height: 1.5; + color: var(--text-secondary); +} + .window-close-dialog-cancel { min-width: 112px; padding: 12px 18px; diff --git a/src/components/WindowCloseDialog.tsx b/src/components/WindowCloseDialog.tsx index 3c992c9..ea838ea 100644 --- a/src/components/WindowCloseDialog.tsx +++ b/src/components/WindowCloseDialog.tsx @@ -1,24 +1,25 @@ import { Minimize2, Power, X } from 'lucide-react' -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import './WindowCloseDialog.scss' interface WindowCloseDialogProps { open: boolean canMinimizeToTray: boolean - onTray: () => void - onQuit: () => void + onSelect: (action: 'tray' | 'quit', rememberChoice: boolean) => void onCancel: () => void } export default function WindowCloseDialog({ open, canMinimizeToTray, - onTray, - onQuit, + onSelect, onCancel }: WindowCloseDialogProps) { + const [rememberChoice, setRememberChoice] = useState(false) + useEffect(() => { if (!open) return + setRememberChoice(false) const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { @@ -63,7 +64,11 @@ export default function WindowCloseDialog({
{canMinimizeToTray && ( -
+ +
))} + +
+ +
+ + 设置点击关闭按钮后的默认行为;选择“每次询问”时会弹出关闭确认。 +
+
setCloseBehaviorDropdownOpen(!closeBehaviorDropdownOpen)} + > + + {windowCloseBehavior === 'tray' + ? '最小化到系统托盘' + : windowCloseBehavior === 'quit' + ? '完全关闭' + : '每次询问'} + + +
+
+ {[ + { + value: 'ask' as const, + label: '每次询问', + successMessage: '已恢复关闭确认弹窗' + }, + { + value: 'tray' as const, + label: '最小化到系统托盘', + successMessage: '关闭按钮已改为最小化到托盘' + }, + { + value: 'quit' as const, + label: '完全关闭', + successMessage: '关闭按钮已改为完全关闭' + } + ].map(option => ( +
{ + setWindowCloseBehavior(option.value) + setCloseBehaviorDropdownOpen(false) + await configService.setWindowCloseBehavior(option.value) + showMessage(option.successMessage, true) + }} + > + {option.label} + {windowCloseBehavior === option.value && } +
+ ))} +
+
+
) diff --git a/src/services/config.ts b/src/services/config.ts index 2cd8787..5fce0f1 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -62,6 +62,7 @@ export const CONFIG_KEYS = { NOTIFICATION_POSITION: 'notificationPosition', NOTIFICATION_FILTER_MODE: 'notificationFilterMode', NOTIFICATION_FILTER_LIST: 'notificationFilterList', + WINDOW_CLOSE_BEHAVIOR: 'windowCloseBehavior', // 词云 WORD_CLOUD_EXCLUDE_WORDS: 'wordCloudExcludeWords', @@ -85,6 +86,8 @@ export interface ExportDefaultMediaConfig { emojis: boolean } +export type WindowCloseBehavior = 'ask' | 'tray' | 'quit' + const DEFAULT_EXPORT_MEDIA_CONFIG: ExportDefaultMediaConfig = { images: true, videos: true, @@ -1188,6 +1191,16 @@ export async function setNotificationFilterList(list: string[]): Promise { await config.set(CONFIG_KEYS.NOTIFICATION_FILTER_LIST, list) } +export async function getWindowCloseBehavior(): Promise { + const value = await config.get(CONFIG_KEYS.WINDOW_CLOSE_BEHAVIOR) + if (value === 'tray' || value === 'quit') return value + return 'ask' +} + +export async function setWindowCloseBehavior(behavior: WindowCloseBehavior): Promise { + await config.set(CONFIG_KEYS.WINDOW_CLOSE_BEHAVIOR, behavior) +} + // 获取词云排除词列表 export async function getWordCloudExcludeWords(): Promise { const value = await config.get(CONFIG_KEYS.WORD_CLOUD_EXCLUDE_WORDS)