Files
jiang13-bbs/frontend/components/ThemeSync.tsx
freefire 29db9ae9b5 feat(frontend): add theme real-time sync feature
1. 新增ThemeSync组件实现站点主题色无刷新同步
2. 给获取站点设置的接口添加no-store缓存策略确保拿到最新配置
3. 调整主题应用代码的属性设置顺序避免逻辑问题
2026-09-15 05:04:25 +08:00

71 lines
2.7 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, useRef } from "react";
import { usePathname } from "next/navigation";
import { apiGetSettings } from "@/lib/api";
import { applyAccentTheme } from "@/lib/theme";
// 站点主题色同步器:
// 主题色由管理员在后台修改后,其他用户已打开的标签页不会因软导航重新执行根布局
// SSR,本组件通过轮询 + 标签页重新可见/获焦时探测公开设置,发现变化即热替换
// accent CSS 变量(复用 lib/theme 的运行时 style 节点),无需 F5 即可换肤。
const POLL_INTERVAL_MS = 60_000;
// 焦点/可见性事件可能短时间连续触发,限制实际请求间隔
const MIN_FETCH_GAP_MS = 10_000;
export default function ThemeSync({ initialAccent }: { initialAccent: string }) {
const pathname = usePathname();
// 当前已应用的已保存色(空串=内置默认);与 SSR 注入的 style 节点保持一致
const currentRef = useRef((initialAccent || "").trim().toLowerCase());
const lastFetchAtRef = useRef(0);
const inFlightRef = useRef(false);
useEffect(() => {
// 外观设置页存在"未保存的本地实时预览",该页面内暂停同步,
// 避免轮询结果覆盖管理员正在预览的颜色;离开页面后 effect 重跑立即对齐
if (pathname?.startsWith("/admin/appearance")) return;
let cancelled = false;
const sync = async () => {
if (inFlightRef.current) return;
const now = Date.now();
if (now - lastFetchAtRef.current < MIN_FETCH_GAP_MS) return;
lastFetchAtRef.current = now;
inFlightRef.current = true;
try {
// no-store:必须拿到管理员刚保存的最新值,不能走浏览器缓存
const { accent } = await apiGetSettings();
if (cancelled) return;
const next = (accent || "").trim().toLowerCase();
if (next !== currentRef.current) {
currentRef.current = next;
applyAccentTheme(next || null);
}
} catch {
// 主题同步失败静默处理,下个周期/下次回前台再试,不打扰用户
} finally {
inFlightRef.current = false;
}
};
const onActive = () => {
if (document.visibilityState === "visible") void sync();
};
document.addEventListener("visibilitychange", onActive);
window.addEventListener("focus", onActive);
// 离开外观页等 pathname 变化时立刻对齐一次(受 MIN_FETCH_GAP 限频)
void sync();
const timer = setInterval(() => void sync(), POLL_INTERVAL_MS);
return () => {
cancelled = true;
document.removeEventListener("visibilitychange", onActive);
window.removeEventListener("focus", onActive);
clearInterval(timer);
};
}, [pathname]);
return null;
}