鉴权只认 cookie、SEO 只走 Next、sort 与推荐对齐;删除 Bearer、Go sitemap、post_type 回填等冗余路径。 Co-authored-by: Cursor <cursoragent@cursor.com>
77 lines
2.9 KiB
TypeScript
77 lines
2.9 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 { 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<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;
|
||
}
|