Files
jiang13-bbs/frontend/components/ThemeSync.tsx
freefire 8c5de13899 fix: 空评论列表 500,并完善撤回审计与角色说明
空切片 JSON 统一为数组;撤回保留正文供站长阅读,成员侧居中提示与重新编辑;头像支持拖放;角色弹窗补全职责说明并汇入封禁。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 00:23:55 +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(() => {
// 外观设置页存在"未保存的本地实时预览",该页面内暂停同步,
// 避免轮询结果覆盖管理员正在预览的颜色;离开页面后 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;
}