Files
jiang13-bbs/frontend/components/ThemeToggle.tsx

80 lines
2.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}