80 lines
2.3 KiB
TypeScript
80 lines
2.3 KiB
TypeScript
"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>
|
||
);
|
||
}
|