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:
@@ -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"
|
||||
|
||||
@@ -57,6 +57,8 @@ const metaTitle = `${title} · ${t('nav.brand')}`;
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
{/* 首屏渲染前设好 data-theme,避免明/暗切换闪烁(FOUC)。postbuild 会给此内联脚本加 CSP hash。 */}
|
||||
<script is:inline>(function(){try{var t=localStorage.getItem('pg_site_theme');if(t==='dark'||t==='light')document.documentElement.dataset.theme=t;}catch(e){}})()</script>
|
||||
<title>{metaTitle}</title>
|
||||
{desc && <meta name="description" content={desc} />}
|
||||
<meta name="robots" content="index,follow" />
|
||||
|
||||
@@ -70,6 +70,8 @@ const headerT = {
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
{/* 首屏渲染前设好 data-theme,避免明/暗切换闪烁(FOUC)。postbuild 会给此内联脚本加 CSP hash。 */}
|
||||
<script is:inline>(function(){try{var t=localStorage.getItem('pg_site_theme');if(t==='dark'||t==='light')document.documentElement.dataset.theme=t;}catch(e){}})()</script>
|
||||
<title>{t('meta.title')}</title>
|
||||
<meta name="description" content={t('meta.desc')} />
|
||||
<meta name="robots" content="index,follow" />
|
||||
|
||||
@@ -42,6 +42,11 @@ img,svg{display:block}
|
||||
.linklogin{font-size:14.5px;font-weight:600;color:var(--fg1);cursor:pointer}
|
||||
.linklogin:hover{color:var(--accent)}
|
||||
|
||||
/* 明/暗主题切换(顶栏图标按钮,随 token 自适配) */
|
||||
.themetoggle{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;flex-shrink:0;border:1.5px solid var(--border-strong);border-radius:var(--radius-full);background:var(--surface);color:var(--fg2);cursor:pointer;transition:border-color var(--dur-fast) var(--ease-out),color var(--dur-fast) var(--ease-out)}
|
||||
.themetoggle:hover{border-color:var(--accent);color:var(--accent)}
|
||||
.themetoggle svg{width:18px;height:18px}
|
||||
|
||||
/* ---------- language dropdown ---------- */
|
||||
.langwrap{position:relative;display:inline-block}
|
||||
.langsel{display:inline-flex;align-items:center;gap:6px;border:1.5px solid var(--border-strong);border-radius:var(--radius-full);padding:8px 14px;background:var(--surface);color:var(--fg1);font-family:var(--font-sans);font-size:14px;font-weight:600;cursor:pointer;transition:border-color var(--dur-fast) var(--ease-out),color var(--dur-fast) var(--ease-out)}
|
||||
|
||||
Reference in New Issue
Block a user