"use client"; import { Drawnix, type DrawnixProps } from "@drawnix/drawnix"; import type { BoardChangeData } from "@plait-board/react-board"; import type { PlaitElement, PlaitTheme, ThemeColorMode, Viewport, } from "@plait/core"; import { ChevronLeft, Save } from "lucide-react"; import { useTheme } from "next-themes"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; type FlowBoardWorkspaceProps = { siteName: string; siteLogo: string | null; siteLogoDark: string | null; }; type FlowBoardSnapshot = { children: PlaitElement[]; viewport?: Viewport; theme?: PlaitTheme; updatedAt?: string; }; const FLOW_BOARD_DRAFT_API = "/api/flow-board/draft"; function normalizeSnapshot(value: unknown): FlowBoardSnapshot { const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Partial) : {}; return { children: Array.isArray(raw.children) ? raw.children : [], viewport: raw.viewport, theme: raw.theme, updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : undefined, }; } function formatSavedAt(value: Date | null) { if (!value) return "数据库自动保存"; return `已保存 ${value.toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit", })}`; } export function DrawnixWorkspace({ siteName, siteLogo, siteLogoDark, }: FlowBoardWorkspaceProps) { const { resolvedTheme } = useTheme(); const [snapshot, setSnapshot] = useState({ children: [], }); const [isReady, setIsReady] = useState(false); const [lastSavedAt, setLastSavedAt] = useState(null); const snapshotRef = useRef({ children: [] }); const saveTimerRef = useRef | null>(null); const displayLogo = useMemo(() => { if (resolvedTheme === "dark" && siteLogoDark) { return siteLogoDark; } return siteLogo || siteLogoDark; }, [resolvedTheme, siteLogo, siteLogoDark]); useEffect(() => { let cancelled = false; const loadDraft = async () => { try { const response = await fetch(FLOW_BOARD_DRAFT_API, { cache: "no-store", credentials: "include", }); if (!response.ok) { throw new Error(`Failed to load flow board draft: ${response.status}`); } const result = (await response.json()) as { data?: { snapshot?: unknown; updatedAt?: string | null }; }; if (!cancelled) { const nextSnapshot = normalizeSnapshot(result.data?.snapshot); snapshotRef.current = nextSnapshot; setSnapshot(nextSnapshot); setLastSavedAt( result.data?.updatedAt ? new Date(result.data.updatedAt) : nextSnapshot.updatedAt ? new Date(nextSnapshot.updatedAt) : null ); } } catch (error) { console.error("[FlowBoard] Failed to load draft:", error); toast.error("流程白板草稿加载失败"); } finally { if (!cancelled) { setIsReady(true); } } }; void loadDraft(); return () => { cancelled = true; if (saveTimerRef.current) { clearTimeout(saveTimerRef.current); } }; }, []); const scheduleSave = useCallback((nextSnapshot: FlowBoardSnapshot) => { if (saveTimerRef.current) { clearTimeout(saveTimerRef.current); } saveTimerRef.current = setTimeout(async () => { try { const response = await fetch(FLOW_BOARD_DRAFT_API, { body: JSON.stringify({ snapshot: nextSnapshot }), credentials: "include", headers: { "Content-Type": "application/json", }, method: "PUT", }); if (!response.ok) { throw new Error(`Failed to save flow board draft: ${response.status}`); } const result = (await response.json()) as { data?: { updatedAt?: string | null }; }; setLastSavedAt( result.data?.updatedAt ? new Date(result.data.updatedAt) : new Date(nextSnapshot.updatedAt || Date.now()) ); } catch (error) { console.error("[FlowBoard] Failed to save draft:", error); toast.error("流程白板草稿保存到数据库失败"); } }, 350); }, []); const updateSnapshot = useCallback( (partialSnapshot: Partial) => { const nextSnapshot = normalizeSnapshot({ ...snapshotRef.current, ...partialSnapshot, updatedAt: new Date().toISOString(), }); snapshotRef.current = nextSnapshot; setSnapshot(nextSnapshot); scheduleSave(nextSnapshot); }, [scheduleSave] ); const handleDrawnixChange = useCallback( (value: BoardChangeData) => { const shouldPersist = value.operations.some( (operation) => operation.type !== "set_selection" ); if (!shouldPersist) return; updateSnapshot({ children: value.children, theme: value.theme, viewport: value.viewport, }); }, [updateSnapshot] ); const handleValueChange = useCallback( (children: PlaitElement[]) => { updateSnapshot({ children }); }, [updateSnapshot] ); const handleViewportChange = useCallback( (viewport: Viewport) => { updateSnapshot({ viewport }); }, [updateSnapshot] ); const handleThemeChange = useCallback( (themeColorMode: ThemeColorMode) => { updateSnapshot({ theme: { themeColorMode } }); }, [updateSnapshot] ); if (!isReady) { return (
正在恢复流程白板...
); } return (
{displayLogo ? ( {siteName} ) : (
{siteName.slice(0, 1).toUpperCase()}
)} 流程白板 {siteName} {formatSavedAt(lastSavedAt)}
} onThemeChange={handleThemeChange} onValueChange={handleValueChange} onViewportChange={handleViewportChange} theme={snapshot.theme} tutorial={false} value={snapshot.children} viewport={snapshot.viewport} />
); }