"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(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; }