Files
jiang13-bbs/frontend/components/ThemeSync.tsx
freefire 833bcd33a1 chore: 去掉未上线前的旧兼容垫片,板块流收口到首页
鉴权只认 cookie、SEO 只走 Next、sort 与推荐对齐;删除 Bearer、Go sitemap、post_type 回填等冗余路径。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 05:04:47 +08:00

77 lines
2.9 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";
import { realtime, RT_SETTINGS_CHANGED, type RtSettingsData } from "@/lib/realtime";
import {
VISIBLE_RECONCILE_GAP_MS,
onDocumentVisible,
} from "@/lib/visibilityReconcile";
// 站点主题色同步器:
// 主题色由管理员在后台修改后,其他用户已打开的标签页不会因软导航重新执行根布局
// SSR,本组件通过 WS 推送 + 标签页重新可见时探测公开设置,发现变化即热替换
// accent CSS 变量(复用 lib/theme 的运行时 style 节点),无需 F5 即可换肤。
// 焦点/可见性事件可能短时间连续触发,限制实际请求间隔
const MIN_FETCH_GAP_MS = VISIBLE_RECONCILE_GAP_MS;
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(() => {
// 外观设置页存在"未保存的本地实时预览",该页面内暂停同步,
// 避免 HTTP 校准覆盖管理员正在预览的颜色;离开页面后 effect 重跑立即对齐
if (pathname?.startsWith("/admin/appearance")) return;
let cancelled = false;
const apply = (next: string) => {
if (cancelled || next === currentRef.current) return;
currentRef.current = next;
applyAccentTheme(next || null);
};
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;
apply((accent || "").trim().toLowerCase());
} catch {
// 主题同步失败静默处理,下次回前台再试,不打扰用户
} finally {
inFlightRef.current = false;
}
};
// WebSocket 推送优先:管理员保存即换肤;回前台 HTTP 校准兜底
const offPush = realtime.on<RtSettingsData>(RT_SETTINGS_CHANGED, (data) => {
if (data) apply((data.accent || "").trim().toLowerCase());
});
// 仅 visibility:window focus 与 visibility 常连发,统一走限频即可
const offVisible = onDocumentVisible(() => void sync(), MIN_FETCH_GAP_MS);
// 离开外观页等 pathname 变化时立刻对齐一次(受 MIN_FETCH_GAP 限频)
void sync();
return () => {
cancelled = true;
offPush();
offVisible();
};
}, [pathname]);
return null;
}