feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS

站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 05:27:33 +08:00
parent 5f7193042f
commit d784b0ea7a
68 changed files with 2588 additions and 768 deletions

View File

@@ -4,29 +4,33 @@ 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 {
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";
// 站点外观同步器:
// 主题色 / 自定义 CSS 由管理员在后台修改后,其他用户已打开的标签页不会因软导航重新执行根布局
// SSR,本组件通过 WS 推送 + 标签页重新可见时探测公开设置,发现变化即热替换。
// 自定义 JS 只在整页加载时执行,不在此热替换(避免重复绑定)。
// 站点主题色 / 背景图同步器:
// 管理员保存后,其他已打开标签页不会因软导航重跑根布局 SSR。
// WS 推送 + 标签页重新可见时探测公开设置,热替换 accent 与背景。
const MIN_FETCH_GAP_MS = VISIBLE_RECONCILE_GAP_MS;
export default function ThemeSync({
initialAccent,
initialCustomCSS,
initialBg,
}: {
initialAccent: string;
initialCustomCSS: string;
initialBg: SiteBgConfig;
}) {
const pathname = usePathname();
const currentRef = useRef((initialAccent || "").trim().toLowerCase());
const cssRef = useRef((initialCustomCSS || "").trim());
const bgRef = useRef(initialBg);
const lastFetchAtRef = useRef(0);
const inFlightRef = useRef(false);
@@ -42,12 +46,11 @@ export default function ThemeSync({
applyAccentTheme(next || null);
};
const applyCSS = (next: string) => {
const applyBg = (raw: SiteBgInput) => {
if (cancelled) return;
const text = (next || "").trim();
if (text === cssRef.current) return;
cssRef.current = text;
applyCustomCSS(text);
const next = bgConfigFromSettings(raw);
bgRef.current = next;
applySiteBackground(next);
};
const sync = async () => {
@@ -57,10 +60,10 @@ export default function ThemeSync({
lastFetchAtRef.current = now;
inFlightRef.current = true;
try {
const settings = await apiGetSettings();
const s = await apiGetSettings();
if (cancelled) return;
applyAccent((settings.accent || "").trim().toLowerCase());
applyCSS(settings.custom_css || "");
applyAccent((s.accent || "").trim().toLowerCase());
applyBg(s);
} catch {
// 同步失败静默,下次回前台再试
} finally {
@@ -71,7 +74,7 @@ export default function ThemeSync({
const offPush = realtime.on<RtSettingsData>(RT_SETTINGS_CHANGED, (data) => {
if (!data) return;
applyAccent((data.accent || "").trim().toLowerCase());
if (typeof data.custom_css === "string") applyCSS(data.custom_css);
applyBg(data);
});
const offVisible = onDocumentVisible(() => void sync(), MIN_FETCH_GAP_MS);