From a7fdab5bb24357e5101086349de638eae8f7bbe0 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 29 Jun 2026 19:52:22 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(theme):=20=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E8=B7=9F=E9=9A=8F=E7=B3=BB=E7=BB=9F=E4=B8=BB=E9=A2=98=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加主题偏好持久化并兼容旧版主题状态恢复 - 监听系统深浅色变化并同步窗口与应用主题 - 补齐主题设置文案与持久化相关测试 Fixes #407 --- frontend/src/App.tsx | 61 ++++++++++++++++++++++++++++--- frontend/src/i18n/catalog.test.ts | 2 + frontend/src/store.test.ts | 23 ++++++++++++ frontend/src/store.ts | 29 +++++++++++++-- shared/i18n/de-DE.json | 2 + shared/i18n/en-US.json | 2 + shared/i18n/ja-JP.json | 2 + shared/i18n/ru-RU.json | 2 + shared/i18n/zh-CN.json | 2 + shared/i18n/zh-TW.json | 2 + 10 files changed, 118 insertions(+), 9 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1bd4650c..7912d1bb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { Layout, Button, ConfigProvider, theme, message, Spin, Slider, Progress, Switch, Input, InputNumber, Select, Segmented, Tooltip } from 'antd'; import { PlusOutlined, ConsoleSqlOutlined, UploadOutlined, DownloadOutlined, CloudDownloadOutlined, BugOutlined, ToolOutlined, GlobalOutlined, InfoCircleOutlined, GithubOutlined, SkinOutlined, CheckOutlined, MinusOutlined, BorderOutlined, CloseOutlined, SettingOutlined, LinkOutlined, BgColorsOutlined, AppstoreOutlined, RobotOutlined, FolderOpenOutlined, HddOutlined, SafetyCertificateOutlined, SwitcherOutlined, CodeOutlined, RightOutlined } from '@ant-design/icons'; -import { BrowserOpenURL, Environment, EventsOn, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowIsMinimised, WindowIsNormal, WindowMaximise, WindowMinimise, WindowSetPosition, WindowSetSize, WindowUnfullscreen, WindowUnmaximise } from '../wailsjs/runtime'; +import { BrowserOpenURL, Environment, EventsOn, WindowFullscreen, WindowGetPosition, WindowGetSize, WindowIsFullscreen, WindowIsMaximised, WindowIsMinimised, WindowIsNormal, WindowMaximise, WindowMinimise, WindowSetDarkTheme, WindowSetLightTheme, WindowSetPosition, WindowSetSize, WindowSetSystemDefaultTheme, WindowUnfullscreen, WindowUnmaximise } from '../wailsjs/runtime'; import Sidebar from './components/Sidebar'; import TabManager from './components/TabManager'; import ConnectionModal from './components/ConnectionModal'; @@ -166,6 +166,13 @@ const readCurrentVisibleViewport = () => ({ availTop: (window.screen as Screen & { availTop?: number })?.availTop || 0, }); +const getSystemThemeMode = (): 'light' | 'dark' => { + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { + return 'light'; + } + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +}; + const mergeSavedConnections = (current: SavedConnection[], imported: SavedConnection[]): SavedConnection[] => { const merged = new Map(); @@ -223,7 +230,9 @@ function App() { const connectionModalWarmupDoneRef = useRef(false); const windowState = useStore(state => state.windowState); const themeMode = useStore(state => state.theme); + const themePreference = useStore(state => state.themePreference); const setTheme = useStore(state => state.setTheme); + const setThemePreference = useStore(state => state.setThemePreference); const appearance = useStore(state => state.appearance); const setAppearance = useStore(state => state.setAppearance); const uiScale = useStore(state => state.uiScale); @@ -240,6 +249,7 @@ function App() { const shortcutOptions = useStore(state => state.shortcutOptions); const updateShortcut = useStore(state => state.updateShortcut); const resetShortcutOptions = useStore(state => state.resetShortcutOptions); + const [systemThemeMode, setSystemThemeMode] = useState<'light' | 'dark'>(() => getSystemThemeMode()); const darkMode = themeMode === 'dark'; const isV2Ui = appearance.uiVersion === 'v2'; const effectiveUiScale = Math.min(MAX_UI_SCALE, Math.max(MIN_UI_SCALE, Number(uiScale) || DEFAULT_UI_SCALE)); @@ -285,6 +295,44 @@ function App() { (key: TabDisplayElementKey) => t(TAB_DISPLAY_ELEMENT_META[key].descriptionKey), [t], ); + useEffect(() => { + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { + return; + } + const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)'); + const applySystemTheme = (matches: boolean) => { + setSystemThemeMode(matches ? 'dark' : 'light'); + }; + applySystemTheme(mediaQueryList.matches); + const handleChange = (event: MediaQueryListEvent) => { + applySystemTheme(event.matches); + }; + if (typeof mediaQueryList.addEventListener === 'function') { + mediaQueryList.addEventListener('change', handleChange); + return () => { + mediaQueryList.removeEventListener('change', handleChange); + }; + } + mediaQueryList.addListener(handleChange); + return () => { + mediaQueryList.removeListener(handleChange); + }; + }, []); + useEffect(() => { + const resolvedTheme = themePreference === 'system' ? systemThemeMode : themePreference; + if (themeMode !== resolvedTheme) { + setTheme(resolvedTheme); + } + if (themePreference === 'system') { + void safeWindowRuntimeCall(() => WindowSetSystemDefaultTheme(), undefined); + return; + } + if (resolvedTheme === 'dark') { + void safeWindowRuntimeCall(() => WindowSetDarkTheme(), undefined); + return; + } + void safeWindowRuntimeCall(() => WindowSetLightTheme(), undefined); + }, [setTheme, systemThemeMode, themeMode, themePreference]); const setTabDisplaySettings = useCallback((settings: Partial) => { setAppearance({ tabDisplay: applyTabDisplaySettingsPatch(tabDisplaySettings, settings), @@ -2814,7 +2862,7 @@ function App() { handleToggleLogPanel(); break; case 'toggleTheme': - setTheme(themeMode === 'dark' ? 'light' : 'dark'); + setThemePreference(themeMode === 'dark' ? 'light' : 'dark'); break; case 'openShortcutManager': setIsShortcutModalOpen(true); @@ -2834,7 +2882,7 @@ function App() { return () => { window.removeEventListener('keydown', handleGlobalShortcut, true); }; - }, [activeShortcutPlatform, handleCreateConnection, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, handleToggleLogPanel, isMacRuntime, shortcutOptions, switchActiveTabByOffset, themeMode, setTheme, toggleAIPanel, useNativeMacWindowControls]); + }, [activeShortcutPlatform, handleCreateConnection, handleManualResetWindowZoom, handleNewQuery, handleTitleBarWindowToggle, handleToggleLogPanel, isMacRuntime, shortcutOptions, switchActiveTabByOffset, themeMode, setThemePreference, toggleAIPanel, useNativeMacWindowControls]); useEffect(() => { if (!capturingShortcutAction) { @@ -4573,17 +4621,18 @@ function App() {
{t('app.theme.mode_title')}
-
+
{[ { key: 'light', label: t('app.theme.mode.light.label'), description: t('app.theme.mode.light.description') }, { key: 'dark', label: t('app.theme.mode.dark.label'), description: t('app.theme.mode.dark.description') }, + { key: 'system', label: t('app.theme.mode.system.label'), description: t('app.theme.mode.system.description') }, ].map((item) => { - const active = themeMode === item.key; + const active = themePreference === item.key; return (