import { App, Button, Input, Skeleton, type InputRef } from "antd"; import { AlertTriangle, Brush, KeyRound, RefreshCw, RotateCcw, Save, Shapes } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useBlocker } from "react-router"; import { drawingEngineLabel, isDrawingEngineAvailable, type CanvasDrawingEngine, type CanvasDrawingEngineSetting } from "@/lib/canvas/canvas-drawing-engine"; import { cn } from "@/lib/utils"; import { getAdminDrawingEngineSetting, updateAdminDrawingEngineSetting } from "@/services/api/auth"; import { useUserStore } from "@/stores/use-user-store"; import { AdminPageFrame } from "../components/admin-shell"; import { AdminStatusBadge, SettingsSectionCard } from "../components/admin-ui"; type DrawingEngineDraft = { defaultEngine: CanvasDrawingEngine; tldrawLicenseKey: string; }; const engineOptions: Array<{ value: CanvasDrawingEngine; title: string; description: string; }> = [ { value: "excalidraw", title: "Excalidraw", description: "无需授权,适合草图、结构标注和轻量协作。", }, { value: "tldraw", title: "tldraw", description: "需要浏览器端 License Key,也用于打开已有 tldraw 绘图。", }, ]; export default function DrawingEngineSettingsPage() { const { message, modal } = App.useApp(); const setDrawingEngine = useUserStore((state) => state.setDrawingEngine); const [savedSetting, setSavedSetting] = useState(null); const [draft, setDraft] = useState(null); 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 licenseInputRef = useRef(null); const load = useCallback( async (initial = false, announce = false) => { const requestVersion = ++requestVersionRef.current; if (initial) setLoading(true); else setRefreshing(true); setLoadError(""); try { const result = await getAdminDrawingEngineSetting(); const setting = parseDrawingEngineSetting(result.setting); if (requestVersion !== requestVersionRef.current) return; setSavedSetting(setting); setDraft(toDraft(setting)); setDrawingEngine(setting); setSaveError(""); 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, setDrawingEngine], ); useEffect(() => { void load(true); return () => { requestVersionRef.current += 1; }; }, [load]); const normalizedDraft = draft ? normalizeDraft(draft) : null; const dirtyFields = useMemo(() => { if (!savedSetting || !normalizedDraft) return []; const fields: Array<"defaultEngine" | "tldrawLicenseKey"> = []; if (savedSetting.defaultEngine !== normalizedDraft.defaultEngine) fields.push("defaultEngine"); if ((savedSetting.tldrawLicenseKey || "").trim() !== normalizedDraft.tldrawLicenseKey) fields.push("tldrawLicenseKey"); return fields; }, [normalizedDraft, savedSetting]); const dirty = dirtyFields.length > 0; 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: `当前有 ${dirtyFields.length} 项调整尚未保存,离开后暂存内容会丢失。`, 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-drawing-command-actions button"); const target = navigationTriggerRef.current?.isConnected ? navigationTriggerRef.current : fallback; target?.focus(); navigationTriggerRef.current = null; }); }, }); }, [blocker, dirtyFields.length, modal]); const tldrawAvailable = draft ? isDrawingEngineAvailable("tldraw", draft.tldrawLicenseKey) : false; const draftHasStoredLicense = Boolean(normalizedDraft?.tldrawLicenseKey); const usingEnvironmentLicense = tldrawAvailable && !draftHasStoredLicense; const selectedEngineAvailable = draft ? isDrawingEngineAvailable(draft.defaultEngine, draft.tldrawLicenseKey) : false; const resetDraft = () => { if (!savedSetting || saving) return; setDraft(toDraft(savedSetting)); setSaveError(""); message.info("已撤销本页尚未保存的调整"); }; const requestRefresh = () => { if (!dirty) { void load(false, true); return; } modal.confirm({ title: "放弃调整并重新读取?", content: `当前有 ${dirtyFields.length} 项调整尚未保存。重新读取会丢弃暂存内容,并以服务端当前配置为准。`, okText: "放弃并刷新", cancelText: "继续编辑", okButtonProps: { danger: true }, onOk: () => load(false, true), }); }; const selectEngine = (engine: CanvasDrawingEngine) => { if (!draft || saving) return; setDraft({ ...draft, defaultEngine: engine }); setSaveError(""); }; async function save(override?: DrawingEngineDraft) { const expected = normalizeDraft(override || draft || { defaultEngine: "excalidraw", tldrawLicenseKey: "" }); const hasChanges = Boolean(savedSetting && (savedSetting.defaultEngine !== expected.defaultEngine || (savedSetting.tldrawLicenseKey || "").trim() !== expected.tldrawLicenseKey)); if (!savedSetting || !hasChanges || saving) return; if (!isDrawingEngineAvailable(expected.defaultEngine, expected.tldrawLicenseKey)) { setSaveError("当前选择的 tldraw 不可用,请先配置授权或改用 Excalidraw。"); licenseInputRef.current?.focus(); throw new Error("请先配置 tldraw License Key"); } setSaving(true); setSaveError(""); try { const result = await updateAdminDrawingEngineSetting(expected); const setting = parseDrawingEngineSetting(result.setting); if (setting.defaultEngine !== expected.defaultEngine || (setting.tldrawLicenseKey || "").trim() !== expected.tldrawLicenseKey) { throw new Error("服务端返回的绘图工具配置与本次保存内容不一致,请重新读取后核对"); } setSavedSetting(setting); setDraft(toDraft(setting)); setDrawingEngine(setting); message.success("绘图工具配置已保存"); } catch (error) { const errorMessage = error instanceof Error ? error.message : "保存绘图工具配置失败"; setSaveError(`${errorMessage}。未自动重试,请重新读取当前配置后再决定是否保存。`); message.error(errorMessage); throw error; } finally { setSaving(false); } } const submitSave = async () => { try { await save(); } catch { // 校验或保存错误已在 save 中就地提示。 } }; if (loading && !draft) { return (
); } if (!draft || !savedSetting) { return (

无法读取绘图工具配置

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

); } const licenseStatus = draftHasStoredLicense ? "后台已配置" : usingEnvironmentLicense ? "部署环境提供" : "未配置"; return (
{dirty ? `${dirtyFields.length} 项调整待保存` : "绘图工具配置已同步"}
{dirty ? ( ) : null}
{loadError || saveError ? (
) : null}
); } function toDraft(setting: CanvasDrawingEngineSetting): DrawingEngineDraft { return { defaultEngine: setting.defaultEngine, tldrawLicenseKey: setting.tldrawLicenseKey || "", }; } function normalizeDraft(draft: DrawingEngineDraft): DrawingEngineDraft { return { defaultEngine: draft.defaultEngine, tldrawLicenseKey: draft.tldrawLicenseKey.trim(), }; } function parseDrawingEngineSetting(value: unknown): CanvasDrawingEngineSetting { if (!value || typeof value !== "object") throw new Error("绘图工具配置响应格式无效"); const record = value as Record; if (record.defaultEngine !== "excalidraw" && record.defaultEngine !== "tldraw") throw new Error("绘图工具配置缺少有效的默认编辑器"); if (record.tldrawLicenseKey !== undefined && typeof record.tldrawLicenseKey !== "string") throw new Error("绘图工具配置中的 tldraw 授权格式无效"); return { defaultEngine: record.defaultEngine, tldrawLicenseKey: typeof record.tldrawLicenseKey === "string" ? record.tldrawLicenseKey : "", configured: typeof record.configured === "boolean" ? record.configured : undefined, updatedBy: typeof record.updatedBy === "string" ? record.updatedBy : undefined, createdAt: typeof record.createdAt === "string" ? record.createdAt : undefined, updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : undefined, }; }