"use client"; import { useEffect, useRef } from "react"; import { usePathname } from "next/navigation"; import { apiGetSettings } from "@/lib/api"; import { applyAccentTheme } from "@/lib/theme"; import { applyCustomCSS } from "@/lib/customCode"; import { realtime, RT_SETTINGS_CHANGED, type RtSettingsData } from "@/lib/realtime"; import { VISIBLE_RECONCILE_GAP_MS, onDocumentVisible, } from "@/lib/visibilityReconcile"; // 站点外观同步器: // 主题色 / 自定义 CSS 由管理员在后台修改后,其他用户已打开的标签页不会因软导航重新执行根布局 // SSR,本组件通过 WS 推送 + 标签页重新可见时探测公开设置,发现变化即热替换。 // 自定义 JS 只在整页加载时执行,不在此热替换(避免重复绑定)。 const MIN_FETCH_GAP_MS = VISIBLE_RECONCILE_GAP_MS; export default function ThemeSync({ initialAccent, initialCustomCSS, }: { initialAccent: string; initialCustomCSS: string; }) { const pathname = usePathname(); const currentRef = useRef((initialAccent || "").trim().toLowerCase()); const cssRef = useRef((initialCustomCSS || "").trim()); const lastFetchAtRef = useRef(0); const inFlightRef = useRef(false); useEffect(() => { // 外观设置页存在未保存的本地实时预览,暂停同步避免覆盖草稿 if (pathname?.startsWith("/admin/appearance")) return; let cancelled = false; const applyAccent = (next: string) => { if (cancelled || next === currentRef.current) return; currentRef.current = next; applyAccentTheme(next || null); }; const applyCSS = (next: string) => { if (cancelled) return; const text = (next || "").trim(); if (text === cssRef.current) return; cssRef.current = text; applyCustomCSS(text); }; 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 { const settings = await apiGetSettings(); if (cancelled) return; applyAccent((settings.accent || "").trim().toLowerCase()); applyCSS(settings.custom_css || ""); } catch { // 同步失败静默,下次回前台再试 } finally { inFlightRef.current = false; } }; const offPush = realtime.on(RT_SETTINGS_CHANGED, (data) => { if (!data) return; applyAccent((data.accent || "").trim().toLowerCase()); if (typeof data.custom_css === "string") applyCSS(data.custom_css); }); const offVisible = onDocumentVisible(() => void sync(), MIN_FETCH_GAP_MS); void sync(); return () => { cancelled = true; offPush(); offVisible(); }; }, [pathname]); return null; }