feat(admin): 独立后台壳、站点分析与聊天监管,公开站拆出主题切换与访问埋点
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
79
frontend/components/ThemeToggle.tsx
Normal file
79
frontend/components/ThemeToggle.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
function readThemeCookie(): Theme {
|
||||
if (typeof document === "undefined") return "light";
|
||||
return document.cookie.match(/(?:^|;\s*)j13-theme=dark/) ? "dark" : "light";
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
document.documentElement.classList.toggle("dark", theme === "dark");
|
||||
document.cookie = `j13-theme=${theme}; path=/; max-age=31536000`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 明暗主题切换:cookie 为真相源;公开顶栏与后台顶栏共用。
|
||||
* 首屏严格用 initialTheme,挂载后再按 cookie 校正,避免 hydration 不一致。
|
||||
*/
|
||||
export default function ThemeToggle({
|
||||
initialTheme = "light",
|
||||
className,
|
||||
style,
|
||||
/** icon = 仅图标按钮;labeled = 带「浅色/深色模式」文案(移动抽屉用) */
|
||||
labelMode = "icon",
|
||||
iconSize = 17,
|
||||
}: {
|
||||
initialTheme?: Theme;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
labelMode?: "icon" | "labeled";
|
||||
iconSize?: number;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const [theme, setTheme] = useState<Theme>(initialTheme);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = readThemeCookie();
|
||||
document.documentElement.classList.toggle("dark", saved === "dark");
|
||||
setTheme(saved);
|
||||
setReady(true);
|
||||
}, [pathname]);
|
||||
|
||||
const toggle = () => {
|
||||
const next: Theme = theme === "dark" ? "light" : "dark";
|
||||
setTheme(next);
|
||||
applyTheme(next);
|
||||
};
|
||||
|
||||
// 未挂载完成前始终展示 SSR 初值,保证与服务端 HTML 一致
|
||||
const shown = ready ? theme : initialTheme;
|
||||
|
||||
if (labelMode === "labeled") {
|
||||
return (
|
||||
<button type="button" onClick={toggle} className={className} style={style}>
|
||||
{shown === "dark" ? <Sun size={iconSize} /> : <Moon size={iconSize} />}
|
||||
{shown === "dark" ? "浅色模式" : "深色模式"}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
className={className}
|
||||
style={style}
|
||||
aria-label="切换主题"
|
||||
data-tip={shown === "dark" ? "切换浅色" : "切换深色"}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{shown === "dark" ? <Sun size={iconSize} /> : <Moon size={iconSize} />}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user