feat(admin): 独立后台壳、站点分析与聊天监管,公开站拆出主题切换与访问埋点

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-17 04:00:14 +08:00
parent cda5fad2e5
commit 9a55c2f6f2
41 changed files with 5655 additions and 1810 deletions

View 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>
);
}