57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { usePathname } from "next/navigation";
|
||
import { apiOperations } from "@/lib/api";
|
||
import { realtime, RT_SETTINGS_CHANGED } from "@/lib/realtime";
|
||
import {
|
||
VISIBLE_RECONCILE_GAP_MS,
|
||
onDocumentVisible,
|
||
} from "@/lib/visibilityReconcile";
|
||
|
||
/** 只读维护条:WS settings:changed 优先,回前台再校准(无定时轮询) */
|
||
export default function OperationalBanner() {
|
||
const path = usePathname();
|
||
const [notice, setNotice] = useState("");
|
||
|
||
useEffect(() => {
|
||
let active = true;
|
||
|
||
const apply = (mode: string | undefined, message: string | undefined) => {
|
||
if (!active) return;
|
||
setNotice(
|
||
mode === "readonly"
|
||
? message || "站点暂时只读,可以浏览,暂不能提交或修改内容。"
|
||
: ""
|
||
);
|
||
};
|
||
|
||
const refresh = () => {
|
||
if (document.visibilityState !== "visible") return;
|
||
void apiOperations<{
|
||
maintenance: { mode: string; title: string; message: string };
|
||
}>("/api/site-state")
|
||
.then((s) => apply(s.maintenance?.mode, s.maintenance?.message))
|
||
.catch(() => {});
|
||
};
|
||
|
||
refresh();
|
||
const offPush = realtime.on(RT_SETTINGS_CHANGED, () => refresh());
|
||
const offVisible = onDocumentVisible(refresh, VISIBLE_RECONCILE_GAP_MS);
|
||
|
||
return () => {
|
||
active = false;
|
||
offPush();
|
||
offVisible();
|
||
};
|
||
}, [path]);
|
||
|
||
if (path.startsWith("/admin") || !notice) return null;
|
||
return (
|
||
<div role="status" className="panel mx-auto my-3 w-full max-w-5xl p-4 text-sm">
|
||
<strong>只读模式:</strong>
|
||
{notice}
|
||
</div>
|
||
);
|
||
}
|