import { App, Button, Input, Skeleton } from "antd"; import { Switch } from "@/pages/admin/ui/controls"; import { AlertTriangle, BadgeCheck, CircleCheck, CloudDownload, KeyRound, RefreshCw, RotateCcw, Save, Server, ShieldCheck, Trash2, Wifi } from "lucide-react"; import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { useBlocker } from "react-router"; import { cn } from "@/lib/utils"; import { getAdminLibTVSetting, testAdminLibTV, updateAdminLibTVSetting, type AdminLibTVSetting } from "@/services/api/libtv"; import { useAppearanceStore } from "@/stores/use-appearance-store"; import { AdminPageFrame } from "../components/admin-shell"; import { AdminStatusBadge, configuredSecretText, SettingsSectionCard } from "../components/admin-ui"; type ConnectionTestStatus = "idle" | "testing" | "success" | "error"; export default function LibTVSettingsPage() { const { message, modal } = App.useApp(); const brandName = useAppearanceStore((state) => state.appearance.brandName); const [setting, setSetting] = useState(null); const [enabled, setEnabled] = useState(false); const [token, setToken] = useState(""); const [clearTokenDraft, setClearTokenDraft] = useState(false); const [testUuid, setTestUuid] = useState(""); const [testStatus, setTestStatus] = useState("idle"); const [testError, setTestError] = useState(""); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [saving, setSaving] = useState(false); const [loadError, setLoadError] = useState(""); const [saveError, setSaveError] = useState(""); const requestVersionRef = useRef(0); const navigationConfirmOpenRef = useRef(false); const navigationTriggerRef = useRef(null); const draftHasToken = Boolean(token.trim()) || Boolean(setting?.hasToken && !clearTokenDraft); const dirty = Boolean(setting && (enabled !== setting.enabled || Boolean(token.trim()) || clearTokenDraft)); const enabledDirty = Boolean(setting && enabled !== setting.enabled); const credentialDraftLabel = token.trim() ? (setting?.hasToken ? "待替换" : "待新增") : clearTokenDraft ? "待清除" : setting?.hasToken ? "已配置" : "未配置"; const load = useCallback( async (initial = false, announce = false) => { const requestVersion = ++requestVersionRef.current; if (initial) setLoading(true); else setRefreshing(true); setLoadError(""); try { const result = await getAdminLibTVSetting(); if (requestVersion !== requestVersionRef.current) return; if (!isAdminLibTVSetting(result.setting)) throw new Error("服务端返回的 LibTV 配置格式无效"); setSetting(result.setting); setEnabled(result.setting.enabled); setToken(""); setClearTokenDraft(false); setSaveError(""); setTestStatus("idle"); setTestError(""); if (announce) message.success("已重新读取第三方参数配置"); } catch (error) { if (requestVersion !== requestVersionRef.current) return; const errorMessage = error instanceof Error ? error.message : "读取第三方参数配置失败"; setLoadError(errorMessage); if (!initial) message.error(errorMessage); } finally { if (requestVersion === requestVersionRef.current) { setLoading(false); setRefreshing(false); } } }, [message], ); useEffect(() => { void load(true); return () => { requestVersionRef.current += 1; }; }, [load]); const blocker = useBlocker(dirty && !saving); useEffect(() => { const beforeUnload = (event: BeforeUnloadEvent) => { if (!dirty || saving) return; event.preventDefault(); }; window.addEventListener("beforeunload", beforeUnload); return () => window.removeEventListener("beforeunload", beforeUnload); }, [dirty, saving]); useEffect(() => { if (blocker.state !== "blocked" || navigationConfirmOpenRef.current) return; navigationConfirmOpenRef.current = true; navigationTriggerRef.current = document.activeElement instanceof HTMLElement && document.activeElement !== document.body ? document.activeElement : null; modal.confirm({ title: "放弃第三方参数调整?", content: "当前页面有尚未保存的 LibTV 凭据或导入策略草稿,离开后这些内容会丢失。服务端正在使用的配置不会改变。", okText: "放弃并离开", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => { navigationConfirmOpenRef.current = false; navigationTriggerRef.current = null; blocker.proceed(); }, onCancel: () => { navigationConfirmOpenRef.current = false; blocker.reset(); window.requestAnimationFrame(() => { const fallback = document.querySelector(".admin-third-party-command-actions button"); const target = navigationTriggerRef.current?.isConnected ? navigationTriggerRef.current : fallback; target?.focus(); navigationTriggerRef.current = null; }); }, }); }, [blocker, modal]); const resetDraft = () => { if (!setting || saving) return; setEnabled(setting.enabled); setToken(""); setClearTokenDraft(false); setSaveError(""); message.info("已撤销第三方参数的未保存调整"); }; const requestRefresh = () => { if (!dirty) { void load(false, true); return; } modal.confirm({ title: "放弃调整并重新读取?", content: "重新读取会丢弃当前 LibTV 凭据和导入开关草稿,并以服务端配置为准。", okText: "放弃并刷新", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => load(false, true), }); }; const changeToken = (nextToken: string) => { setToken(nextToken); if (nextToken.trim()) setClearTokenDraft(false); setSaveError(""); }; const changeEnabled = (nextEnabled: boolean) => { if (nextEnabled && !draftHasToken) { message.warning("请先填写 LibTV Token,再启用画布导入"); return; } if (!setting || saving || nextEnabled === enabled) return; setEnabled(nextEnabled); setSaveError(""); }; const markTokenForRemoval = () => { setToken(""); setClearTokenDraft(true); setEnabled(false); setSaveError(""); message.info("Token 已标记为待清除,点击保存修改前不会影响服务端配置"); }; const restoreTokenDraft = () => { setClearTokenDraft(false); setSaveError(""); }; async function save() { if (!setting) return; const nextEnabled = enabled; const nextToken = token.trim(); const clearToken = clearTokenDraft; const expectedHasToken = nextToken ? true : clearToken ? false : setting.hasToken; if (nextEnabled && !expectedHasToken) { message.error("启用 LibTV 画布导入前必须配置 Token"); return; } setSaving(true); setSaveError(""); try { const result = await updateAdminLibTVSetting({ enabled: nextEnabled, token: nextToken || undefined, clearToken: clearToken || undefined }); if (!isAdminLibTVSetting(result.setting) || result.setting.enabled !== nextEnabled || result.setting.hasToken !== expectedHasToken) { throw new Error("服务端返回的 LibTV 配置与本次保存内容不一致,请重新读取后核对"); } setSetting(result.setting); setEnabled(result.setting.enabled); setToken(""); setClearTokenDraft(false); setTestStatus("idle"); setTestError(""); message.success("第三方参数配置已保存"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "保存第三方参数配置失败"; setSaveError(`${errorMessage}。未自动重试,请重新读取当前配置后再决定是否保存。`); message.error(errorMessage); throw error; } finally { setSaving(false); } } const submitSave = async () => { if (!setting || !dirty) return; const nextToken = token.trim(); const expectedHasToken = nextToken ? true : clearTokenDraft ? false : setting.hasToken; if (enabled && !expectedHasToken) { message.error("启用 LibTV 画布导入前必须配置 Token"); return; } try { await save(); } catch { // 保存错误已在 save 中就地提示。 } }; const test = async () => { const uuid = testUuid.trim(); if (!setting?.hasToken) { message.error("请先保存 LibTV Token,再验证连接"); return; } if (!uuid) { message.error("请填写用于验证的 LibTV 画布 UUID"); return; } setTestStatus("testing"); setTestError(""); try { await testAdminLibTV(uuid); setTestStatus("success"); message.success("已保存的 LibTV Token 可以读取该画布"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "LibTV 连接验证失败"; setTestStatus("error"); setTestError(errorMessage); message.error(errorMessage); } }; if (loading && !setting) { return (
); } if (!setting) { return (

无法读取第三方参数配置

{loadError || "当前没有可显示的配置,请稍后重试。"}

); } return (
{dirty ? "第三方参数有调整待保存" : "第三方参数已与服务端同步"}

{dirty ? "凭据和导入开关只在本页暂存;保存前可撤销或重新读取。" : "当前仅接入 LibTV;凭据明文只提交到服务端,不会从接口回传。"}

{dirty ? ( ) : null}
{loadError || saveError ? (
) : null}
{draftHasToken ? (
) : null} {setting.hasToken && !clearTokenDraft ? (
) : null}
); } function ThirdPartySaveFooter({ setting, dirty, saving, refreshing, onReset, onSave }: { setting: AdminLibTVSetting; dirty: boolean; saving: boolean; refreshing: boolean; onReset: () => void; onSave: () => Promise }) { return (
{dirty ? ( ) : null}
); } function FlowStep({ icon, label, detail }: { icon: ReactNode; label: string; detail: string }) { return (
{icon}
{label} {detail}
); } function TestStatus({ status }: { status: ConnectionTestStatus }) { if (status === "testing") return ; if (status === "success") return ; if (status === "error") return ; return ; } function isAdminLibTVSetting(value: unknown): value is AdminLibTVSetting { if (!value || typeof value !== "object") return false; const candidate = value as Partial; return typeof candidate.enabled === "boolean" && typeof candidate.hasToken === "boolean" && (candidate.updatedAt === undefined || typeof candidate.updatedAt === "string"); } function hasValidSettingTime(value?: string) { if (!value) return false; const timestamp = new Date(value).getTime(); return Number.isFinite(timestamp) && timestamp > 0; } function formatSettingTime(value: string | undefined, fallback: string) { if (!hasValidSettingTime(value)) return fallback; return `更新于 ${new Intl.DateTimeFormat("zh-CN", { dateStyle: "medium", timeStyle: "short" }).format(new Date(value as string))}`; }