import { useEffect, useMemo, useRef, useState } from "react"; import { App, Modal } from "antd"; import { Check, Maximize2, X } from "lucide-react"; import { Tldraw, createTLStore, getSnapshot, loadSnapshot } from "tldraw"; import type { Editor } from "tldraw"; import "tldraw/tldraw.css"; import { useThemeStore } from "@/stores/use-theme-store"; import { loadCanvasDrawing, saveCanvasDrawing, type CanvasDrawingRenderDraft, type CanvasDrawingSnapshot } from "@/lib/canvas/canvas-drawing-storage"; import type { CanvasNodeData } from "@/types/canvas"; const SINGLE_PAGE_OPTIONS = { maxPages: 1 } as const; const DRAWING_RENDER_MAX_DIMENSION = 2048; const DRAWING_RENDER_PADDING = 24; type CanvasDrawingEditorModalProps = { open: boolean; projectId: string; node: CanvasNodeData | null; onClose: () => void; onSaved: (nodeId: string, summary: Pick) => void; }; export function CanvasDrawingEditorModal({ open, projectId, node, onClose, onSaved }: CanvasDrawingEditorModalProps) { const { message } = App.useApp(); const colorScheme = useThemeStore((state) => state.theme); const store = useMemo(() => createTLStore(), [node?.id]); const currentRef = useRef(null); const editorRef = useRef(null); const [ready, setReady] = useState(false); const [saving, setSaving] = useState(false); useEffect(() => { if (!open || !node?.metadata?.drawingId) return; let cancelled = false; setReady(false); currentRef.current = null; void loadCanvasDrawing(projectId, node.metadata.drawingId).then((saved) => { if (cancelled) return; currentRef.current = saved; if (saved?.snapshot) loadSnapshot(store, saved.snapshot as never); setReady(true); }).catch(() => { if (!cancelled) setReady(true); }); return () => { cancelled = true; }; }, [open, projectId, node?.metadata?.drawingId, store]); const handleSave = async () => { if (!node?.metadata?.drawingId || !ready) return false; setSaving(true); try { const render = await createDrawingRender(editorRef.current); const saved = await saveCanvasDrawing(projectId, node.metadata.drawingId, getSnapshot(store), currentRef.current, render?.blob || null, render); currentRef.current = saved; onSaved(node.id, saved); return true; } catch (error) { message.error(error instanceof Error ? `绘图保存失败:${error.message}` : "绘图保存失败"); return false; } finally { setSaving(false); } }; const handleClose = async () => { if (ready && !(await handleSave())) return; onClose(); }; return ( void handleClose()} footer={null} closable={false} destroyOnHidden width="100vw" centered styles={{ body: { padding: 0 }, container: { padding: 0, overflow: "hidden" } }} className="canvas-drawing-editor-modal" >
{node?.title || "绘图"} {ready ? "已加载" : "正在加载"}
{ready ? ( { editorRef.current = editor; // 每个绘图节点只允许一个页面;旧快照若包含多页,也在进入编辑器时收敛为首页。 const [primaryPage, ...extraPages] = editor.getPages(); if (primaryPage) editor.setCurrentPage(primaryPage.id); extraPages.forEach((page) => editor.deletePage(page.id)); editor.setCurrentTool("draw"); return () => { if (editorRef.current === editor) editorRef.current = null; }; }} /> ) :
正在准备绘图画布...
}
); } async function createDrawingRender(editor: Editor | null): Promise { if (!editor) throw new Error("绘图编辑器尚未准备完成"); const shapeIds = [...editor.getCurrentPageShapeIds()]; if (!shapeIds.length) return null; const bounds = editor.getShapesPageBounds(shapeIds); if (!bounds) throw new Error("无法读取绘图内容边界"); const sourceDimension = Math.max(bounds.width, bounds.height) + DRAWING_RENDER_PADDING * 2; const scale = Math.min(4, DRAWING_RENDER_MAX_DIMENSION / Math.max(1, sourceDimension)); const image = await editor.toImage(shapeIds, { format: "png", background: true, padding: DRAWING_RENDER_PADDING, scale, pixelRatio: 1, darkMode: false, }); return { blob: image.blob, pageId: editor.getCurrentPageId(), width: image.width, height: image.height, mimeType: image.blob.type || "image/png", background: "white", }; }