Files
jiang13-bbs/frontend/components/ThemeSync.tsx
freefire d784b0ea7a feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS
站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-18 05:27:33 +08:00

92 lines
2.6 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";
import { apiGetSettings } from "@/lib/api";
import { applyAccentTheme } from "@/lib/theme";
import {
applySiteBackground,
bgConfigFromSettings,
type SiteBgConfig,
type SiteBgInput,
} from "@/lib/bg";
import { realtime, RT_SETTINGS_CHANGED, type RtSettingsData } from "@/lib/realtime";
import {
VISIBLE_RECONCILE_GAP_MS,
onDocumentVisible,
} from "@/lib/visibilityReconcile";
// 站点主题色 / 背景图同步器:
// 管理员保存后,其他已打开标签页不会因软导航重跑根布局 SSR。
// WS 推送 + 标签页重新可见时探测公开设置,热替换 accent 与背景。
const MIN_FETCH_GAP_MS = VISIBLE_RECONCILE_GAP_MS;
export default function ThemeSync({
initialAccent,
initialBg,
}: {
initialAccent: string;
initialBg: SiteBgConfig;
}) {
const pathname = usePathname();
const currentRef = useRef((initialAccent || "").trim().toLowerCase());
const bgRef = useRef(initialBg);
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 applyBg = (raw: SiteBgInput) => {
if (cancelled) return;
const next = bgConfigFromSettings(raw);
bgRef.current = next;
applySiteBackground(next);
};
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 s = await apiGetSettings();
if (cancelled) return;
applyAccent((s.accent || "").trim().toLowerCase());
applyBg(s);
} catch {
// 同步失败静默,下次回前台再试
} finally {
inFlightRef.current = false;
}
};
const offPush = realtime.on<RtSettingsData>(RT_SETTINGS_CHANGED, (data) => {
if (!data) return;
applyAccent((data.accent || "").trim().toLowerCase());
applyBg(data);
});
const offVisible = onDocumentVisible(() => void sync(), MIN_FETCH_GAP_MS);
void sync();
return () => {
cancelled = true;
offPush();
offVisible();
};
}, [pathname]);
return null;
}