feat(website): 官网加明/暗主题切换(默认浅色,无 FOUC)

- Header 加 Moon/Sun 切换按钮(.themetoggle class,走语义 token,无内联 style),
  切 document.documentElement.dataset.theme + 存 localStorage pg_site_theme,
  挂载读回同步;移动端也能切。
- Site.astro + Doc.astro <head> 加 <script is:inline> 无 FOUC 初始化(首屏前读
  localStorage 设 data-theme;postbuild csp-hashes 已加 script hash 放行)。
- 暗色机制:tokens.gen.css 已含 [data-theme=dark] 块,官网 127 处语义 token 自动
  适配;23 处 light 专属原色(公告条/CTA/主推套餐卡 clay 品牌面 + 恒暗页脚)均
  有意固定,保留。
- 审计:所有元素走唯一真相源(check-ds/check-l1-sync/check_ds_code 三闸全绿)。

验证:check-l1-sync ✓ · build ✓(CSP hash 覆盖 FOUC 脚本)· redline 0 · Header
零内联 style。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-08 11:58:35 +08:00
parent 9ed7588ec0
commit 6dcf582225
4 changed files with 38 additions and 1 deletions
+29 -1
View File
@@ -4,7 +4,7 @@
* 语言切换由原型的 JS 文本替换改为「路由跳转」(zh=/, en=/en/),单显不并排(铁律 6)。
*/
import { useEffect, useRef, useState } from 'react';
import { Download, Menu } from 'lucide-react';
import { Download, Menu, Sun, Moon } from 'lucide-react';
import { SITE } from '../config/site';
function Mark() {
@@ -33,6 +33,8 @@ export default function Header({ lang = 'zh', t = {} }) {
const [userOpen, setUserOpen] = useState(false);
const [loggedIn, setLoggedIn] = useState(false);
const [email, setEmail] = useState('');
// 明/暗主题:默认浅色(不跟随系统),持久化 localStorage key pg_site_theme。
const [theme, setTheme] = useState('light');
const langRef = useRef(null);
const userRef = useRef(null);
@@ -61,6 +63,24 @@ export default function Header({ lang = 'zh', t = {} }) {
const onSwitch = () => { clearSession(); window.location.href = loginHref; };
const onLogout = () => { clearSession(); window.location.href = '/'; };
// 挂载时读 localStorage 同步按钮态(首屏无 FOUC 脚本已在 <head> 设好 data-theme)。
useEffect(() => {
try {
const saved = localStorage.getItem('pg_site_theme');
setTheme(saved === 'dark' ? 'dark' : 'light');
} catch { /* ignore */ }
}, []);
// 切换主题:写 <html data-theme> + localStorage,图标随态变。
const toggleTheme = () => {
setTheme((prev) => {
const next = prev === 'dark' ? 'light' : 'dark';
try { localStorage.setItem('pg_site_theme', next); } catch { /* ignore */ }
document.documentElement.dataset.theme = next;
return next;
});
};
useEffect(() => {
if (!langOpen) return;
const onDoc = (e) => { if (langRef.current && !langRef.current.contains(e.target)) setLangOpen(false); };
@@ -113,6 +133,14 @@ export default function Header({ lang = 'zh', t = {} }) {
))}
</nav>
<div class="right">
<button
type="button"
class="themetoggle"
onClick={toggleTheme}
aria-label={theme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
>
{theme === 'dark' ? <Sun /> : <Moon />}
</button>
<div ref={langRef} class="langwrap">
<button
type="button"