From 9bc0e1f2e64f9f958e8b713c014ce6b2a7749547 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Sun, 12 Jul 2026 19:02:27 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=E4=BF=AE=E5=A4=8D=E4=B8=BB=E9=A2=98?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E6=8C=89=E9=92=AE=E7=9A=84=E6=B0=B4=E5=90=88?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4=EF=BC=88=E5=A4=9C=E8=AF=BB=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=88=B7=E6=96=B0=E6=8A=A5=E9=94=99=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 首帧 mode 用默认值(与服务端一致)→ 消除 SSR/客户端 aria-label/图标不一致 - 图标改由 CSS 按 显隐(两个图标始终在 DOM)→ 无首帧闪烁、无结构差异 - 挂载后 useEffect 以 localStorage 校正 mode → label 恢复正确文案 - 去掉会锁死服务端陈旧 label 的 suppressHydrationWarning --- apps/web/app/globals.css | 15 +++++++++++++++ apps/web/components/ThemeToggle.tsx | 18 +++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index dc332cc..0743278 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -134,6 +134,21 @@ body { } } +/* 主题切换图标:显隐由 data-theme 决定(而非 JS 状态),两个图标始终在 DOM 里, + 服务端/客户端首帧结构一致——既避免水合不一致,又无首帧闪烁。 */ +.theme-icon-moon { + display: inline-block; +} +.theme-icon-sun { + display: none; +} +[data-theme="night"] .theme-icon-moon { + display: none; +} +[data-theme="night"] .theme-icon-sun { + display: inline-block; +} + /* Toast 入场:淡入 + 轻微上移(尊重 prefers-reduced-motion,见下)。 */ .toast-enter { animation: toast-in var(--dur-base) var(--ease-standard); diff --git a/apps/web/components/ThemeToggle.tsx b/apps/web/components/ThemeToggle.tsx index 165b5f6..53f01e3 100644 --- a/apps/web/components/ThemeToggle.tsx +++ b/apps/web/components/ThemeToggle.tsx @@ -26,13 +26,6 @@ function readStoredThemeMode(): ThemeMode { } } -// 初始模式直接读 ThemeScript 在水合前写入的 ,避免夜读首帧渲染错误图标。 -// SSR 守卫:服务端无 document → 回落默认(与 ThemeScript 的默认一致)。 -function initialThemeMode(): ThemeMode { - if (typeof document === "undefined") return DEFAULT_THEME_MODE; - return normalizeThemeMode(document.documentElement.dataset.theme); -} - function writeStoredThemeMode(mode: ThemeMode) { try { window.localStorage.setItem(THEME_STORAGE_KEY, mode); @@ -42,7 +35,9 @@ function writeStoredThemeMode(mode: ThemeMode) { } export function ThemeToggle() { - const [mode, setMode] = useState(initialThemeMode); + // 首帧用默认模式(与服务端一致)避免水合不一致;挂载后以 localStorage 校正。 + // 可见的图标由 CSS 按 决定(ThemeScript 已在水合前写入),故无首帧闪烁。 + const [mode, setMode] = useState(DEFAULT_THEME_MODE); useEffect(() => { // 水合后以 localStorage 为权威源校正(dataset 已由 ThemeScript 同样依据写入,通常一致)。 @@ -60,8 +55,8 @@ export function ThemeToggle() { }); }; - const Icon = mode === "night" ? Sun : Moon; - + // 图标显隐交给 CSS(按 data-theme),两个图标始终渲染 → 首帧结构与服务端一致、无闪烁。 + // 首帧 label 用默认模式(与服务端一致),挂载后随 mode 校正为正确文案。 return ( );