站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。 Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
2.6 KiB
TypeScript
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;
|
|
}
|