"use client"; import "simple-mind-map/dist/simpleMindMap.esm.css"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createReactBlockSpec } from "@blocknote/react"; import type { Block, BlockNoteEditor } from "@blocknote/core"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import type { CustomBlockSchema } from "../schema"; import { MindmapToolbar } from "./MindmapToolbar"; import { MindmapSidebar } from "./MindmapSidebar"; import { MindmapSidebarTrigger } from "./MindmapSidebarTrigger"; import { MindmapNavigator } from "./MindmapNavigator"; import { MindmapMiniMap } from "./MindmapMiniMap"; import { MindmapCount } from "./MindmapCount"; import type { SidebarPanel } from "./mindmapSidebarConfig"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react"; import iconConfig from "./mindmapIconConfig"; import { emitAssetsChanged } from "@/lib/events"; // 避免 SSR/Node 环境提前评估 simple-mind-map 内部依赖 document // @ts-expect-error 第三方库缺少类型定义 const loadIconModules = async () => { const { nodeIconList } = await import("simple-mind-map/src/svg/icons.js"); const { mergerIconList } = await import("simple-mind-map/src/utils/index.js"); return { nodeIconList, mergerIconList }; }; // 安全获取图片尺寸 const getImageSizeSafe = (url: string): Promise<{ width: number; height: number } | null> => new Promise((resolve) => { if (!url) return resolve(null); const img = new Image(); img.onload = () => resolve({ width: img.naturalWidth, height: img.naturalHeight }); img.onerror = () => resolve(null); img.src = url; }); type MindMapInstance = { execCommand: (...args: unknown[]) => void; destroy: () => void; setData: (data: unknown) => void; getData: (withConfig?: boolean) => unknown; on?: (event: string, handler: (...args: unknown[]) => void) => void; emit?: (event: string, ...args: unknown[]) => void; off?: (event: string, handler: (...args: unknown[]) => void) => void; setMode?: (mode: string) => void; command: { clearHistory: () => void }; view: { fit: () => void; scale: number; enlarge: () => void; narrow: () => void; setScale: (scale: number, cx: number, cy: number) => void }; renderer: { activeNodeList: unknown[]; renderTree: { _node: unknown }; setRootNodeCenter: () => void }; painter?: { startPainter: () => void }; doExport?: { export: (type: string, isDownload?: boolean, name?: string) => Promise }; width: number; height: number; miniMap?: unknown; getThemeConfig: (key: string) => unknown; setThemeConfig: (config: unknown) => void; setTheme: (theme: string) => void; setLayout: (layout: string) => void; }; type MindMapNode = { getStyle?: (key: string, inherit?: boolean) => unknown; getData?: (key: string) => unknown; nodeData?: { data?: Record }; data?: Record; }; type MindMapData = { data: Record; children?: unknown[]; }; export const defaultMindmapData = { data: { text: "中心主题" }, children: [], }; const STORAGE_PREFIX = "wolai-mindmap-autosave-"; function downloadJson(data: unknown, name: string) { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = `${name}.json`; anchor.click(); URL.revokeObjectURL(url); } const createSimplePrompt = (title: string, placeholder = "") => { const value = window.prompt(title, placeholder); if (!value || !value.trim()) return null; return value.trim(); }; // 修补 svg.js rbox 在节点未挂载时抛出的异常 const patchSvgRbox = async () => { // 仅在浏览器环境生效 if (typeof window === "undefined") return; const svgModule = await import("@svgdotjs/svg.js"); const candidates = [(svgModule as any).Element, (svgModule as any).G]; candidates.forEach((Ctor) => { if (!Ctor?.prototype) return; if (Ctor.prototype.__wolaiRboxPatched) return; const original = Ctor.prototype.rbox; if (typeof original !== "function") return; // @ts-expect-error 动态扩展第三方原型 Ctor.prototype.rbox = function patchedRbox(ref?: unknown) { try { return original.call(this, ref); } catch (error) { console.warn("rbox 失败,返回空边界框以避免崩溃", error); return { x: 0, y: 0, width: 0, height: 0, x2: 0, y2: 0, cx: 0, cy: 0, }; } }; Ctor.prototype.__wolaiRboxPatched = true; }); }; const applyToActiveNodes = ( mindmap: MindMapInstance | null, handler: (node: unknown) => void, ) => { if (!mindmap) { window.alert("思维导图尚未初始化"); return; } const list = mindmap.renderer?.activeNodeList; if (!list || list.length === 0) { // 尝试兜底选中根节点 const root = mindmap.renderer?.root ?? mindmap.renderer?.renderTree?._node; if (root) { mindmap.execCommand?.("SET_NODE_ACTIVE", root, true); } else { window.alert("请选择至少一个节点再执行该操作"); return; } } (mindmap.renderer?.activeNodeList ?? []).forEach(handler); }; // 确保存在激活节点后再执行命令,若无则自动选中根节点 const ensureActiveBefore = (mindmap?: MindMapInstance | null) => { const mm = mindmap ?? null; if (!mm) { window.alert("思维导图尚未初始化"); return null; } const list = mm.renderer?.activeNodeList ?? []; if (!list || list.length === 0) { const root = mm.renderer?.root ?? mm.renderer?.renderTree?._node; if (root) { mm.execCommand?.("SET_NODE_ACTIVE", root, true); } else { window.alert("请先选中一个节点"); return null; } } return mm; }; const MindmapBlockView = ({ block, editor, fullscreen = false, }: { block: Block; editor: BlockNoteEditor; fullscreen?: boolean; }) => { const containerRef = useRef(null); const fileInputRef = useRef(null); const [mindmap, setMindmap] = useState(null); const [canBack, setCanBack] = useState(false); const [canForward, setCanForward] = useState(false); const [activeNodes, setActiveNodes] = useState([]); const [painterMode, setPainterMode] = useState(false); const [showMiniMap, setShowMiniMap] = useState(false); const [activeSidebar, setActiveSidebar] = useState(null); const [showNoteModal, setShowNoteModal] = useState(false); const [noteContent, setNoteContent] = useState(""); const getRootNode = useCallback((mm?: MindMapInstance | null) => { const instance = mm ?? mindmap; return instance?.renderer?.root ?? instance?.renderer?.renderTree?._node ?? null; }, [mindmap]); const docId = useMemo( () => block.props.docId || (typeof window !== "undefined" ? window.location.pathname.split("/").pop() ?? "" : ""), [block.props.docId], ); const autosaveKey = useMemo( () => `${STORAGE_PREFIX}${docId || block.id}`, [block.id, docId], ); const initialDataRef = useRef(null); if (initialDataRef.current === null) { const cached = typeof window !== "undefined" ? window.localStorage.getItem(autosaveKey) : null; if (cached) { try { initialDataRef.current = JSON.parse(cached); } catch { initialDataRef.current = block.props.data ?? defaultMindmapData; } } else { initialDataRef.current = block.props.data ?? defaultMindmapData; } } // 优先加载本地文件,其次 Supabase(通过后端 API) useEffect(() => { let cancelled = false; if (!docId) return; (async () => { try { const resp = await fetch(`/api/mindmap/${docId}`); if (!resp.ok) return; const payload = await resp.json().catch(() => null); const data = payload?.data; if (!data || cancelled) return; initialDataRef.current = data; if (mindmap) { mindmap.setData(data); mindmap.command.clearHistory(); } } catch (error) { console.warn("加载本地/远端思维导图失败", error); } })(); return () => { cancelled = true; }; }, [docId, mindmap]); const persistData = useCallback( (data: unknown) => { if (!editor) return; window.localStorage.setItem(autosaveKey, JSON.stringify(data)); editor.updateBlock(block, { props: { ...block.props, data } }); if (docId) { // 同步到本地文件 + Supabase(弱依赖) fetch(`/api/mindmap/${docId}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data }), }) .then((resp) => { if (resp.ok) { emitAssetsChanged(docId, { id: `mindmap-${docId}`, document_id: docId, asset_type: "mindmap" }); } }) .catch((err) => console.warn("思维导图同步失败", err)); } }, [autosaveKey, block, docId, editor], ); const debouncedPersist = useDebouncedCallback((data: unknown) => { persistData(data); }, 500); const initialSyncDone = useRef(false); useEffect(() => { if (!docId || !mindmap || initialSyncDone.current) return; initialSyncDone.current = true; const data = mindmap.getData?.() ?? initialDataRef.current ?? defaultMindmapData; void fetch(`/api/mindmap/${docId}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ data }), }) .then((resp) => { if (resp.ok) { emitAssetsChanged(docId, { id: `mindmap-${docId}`, document_id: docId, asset_type: "mindmap" }); } }) .catch((err) => console.warn("初次创建思维导图文件失败", err)); }, [docId, mindmap, initialDataRef]); // 初始选中根节点,后续不强制抢焦点,允许用户自由选择 useEffect(() => { if (!mindmap || activeNodes.length > 0) return; const rootNode = mindmap.renderer?.root ?? mindmap.renderer?.renderTree?._node; if (rootNode) { setActiveNodes([rootNode]); mindmap.renderer.activeNodeList = [rootNode]; // @ts-expect-error 第三方库字段 mindmap.renderer.lastActiveNodeList = [rootNode]; mindmap.emit?.("node_active", rootNode, [rootNode]); } }, [mindmap, activeNodes.length]); useEffect(() => { let destroyed = false; (async () => { if (!containerRef.current) return; const [ { default: MindMap }, { default: Painter }, { default: AssociativeLine }, { default: OuterFrame }, { default: Exporter }, { default: Formula }, { default: RichText }, { default: MiniMapPlugin }, { default: Select }, { default: Drag }, { default: KeyboardNavigation }, { default: NodeImgAdjust }, { default: Scrollbar }, { default: RainbowLines }, { default: Watermark }, { default: TouchEvent }, { default: Cooperate }, { default: Demonstrate }, { default: MindMapLayoutPro }, { default: NodeBase64ImageStorage }, { default: ExportPDF }, { default: ExportXMind }, ] = await Promise.all([ import("simple-mind-map"), import("simple-mind-map/src/plugins/Painter.js"), import("simple-mind-map/src/plugins/AssociativeLine.js"), import("simple-mind-map/src/plugins/OuterFrame.js"), import("simple-mind-map/src/plugins/Export.js"), import("simple-mind-map/src/plugins/Formula.js"), import("simple-mind-map/src/plugins/RichText.js"), import("simple-mind-map/src/plugins/MiniMap.js"), import("simple-mind-map/src/plugins/Select.js"), import("simple-mind-map/src/plugins/Drag.js"), import("simple-mind-map/src/plugins/KeyboardNavigation.js"), import("simple-mind-map/src/plugins/NodeImgAdjust.js"), import("simple-mind-map/src/plugins/Scrollbar.js"), import("simple-mind-map/src/plugins/RainbowLines.js"), import("simple-mind-map/src/plugins/Watermark.js"), import("simple-mind-map/src/plugins/TouchEvent.js"), import("simple-mind-map/src/plugins/Cooperate.js"), import("simple-mind-map/src/plugins/Demonstrate.js"), import("simple-mind-map/src/plugins/MindMapLayoutPro.js"), import("simple-mind-map/src/plugins/NodeBase64ImageStorage.js"), import("simple-mind-map/src/plugins/ExportPDF.js"), import("simple-mind-map/src/plugins/ExportXMind.js"), ]); const plugins = [ { name: "Painter", plugin: Painter }, { name: "AssociativeLine", plugin: AssociativeLine }, { name: "OuterFrame", plugin: OuterFrame }, { name: "Export", plugin: Exporter }, { name: "Formula", plugin: Formula }, { name: "RichText", plugin: RichText }, { name: "MiniMap", plugin: MiniMapPlugin }, { name: "Select", plugin: Select }, { name: "Drag", plugin: Drag }, { name: "KeyboardNavigation", plugin: KeyboardNavigation }, { name: "NodeImgAdjust", plugin: NodeImgAdjust }, { name: "Scrollbar", plugin: Scrollbar }, { name: "RainbowLines", plugin: RainbowLines }, { name: "Watermark", plugin: Watermark }, { name: "TouchEvent", plugin: TouchEvent }, { name: "Cooperate", plugin: Cooperate }, { name: "Demonstrate", plugin: Demonstrate }, { name: "MindMapLayoutPro", plugin: MindMapLayoutPro }, { name: "NodeBase64ImageStorage", plugin: NodeBase64ImageStorage }, { name: "ExportPDF", plugin: ExportPDF }, { name: "ExportXMind", plugin: ExportXMind }, ]; plugins.forEach(({ name, plugin }) => { if (!plugin) { console.warn(`思维导图插件加载失败:${name}`); return; } // @ts-expect-error 第三方库缺少类型定义 if (MindMap.hasPlugin(plugin) === -1) { console.log(`注册插件: ${name}`); const registerPlugin = // @ts-expect-error simple-mind-map 插件注册无类型声明 (MindMap as { usePlugin?: (p: unknown) => void }).usePlugin; if (registerPlugin) { registerPlugin(plugin); } } }); const { nodeIconList, mergerIconList } = await loadIconModules(); await patchSvgRbox(); const hostEl = containerRef.current ?? document.body; const instance = new MindMap({ el: hostEl, data: initialDataRef.current, theme: "classic", layout: "logicalStructure", mousewheelAction: "zoom", enableFreeDrag: true, enableCtrlKeyNodeSelection: true, fit: true, useLeftKeySelectionRightKeyDrag: true, // 传入扩展图标表,和官方一致 iconList: mergerIconList([ ...nodeIconList, ...(iconConfig as unknown[]), ]), }) as MindMapInstance; // 确保测量节点的缓存容器始终挂在有效 DOM 上,避免 appendChild 空指针 if (!(instance as any).commonCaches) { (instance as any).commonCaches = {}; } if (!(instance as any).commonCaches.measureRichtextNodeTextSizeEl) { const measureDiv = document.createElement("div"); measureDiv.style.position = "fixed"; measureDiv.style.left = "-999999px"; ((instance as any).commonCaches).measureRichtextNodeTextSizeEl = measureDiv; (hostEl ?? document.body).appendChild(measureDiv); } instance.setMode?.("edit"); if (typeof window !== "undefined") { // 便于开发阶段在控制台直接调试实例 // @ts-expect-error 调试用全局变量 window.__mindmapInstance = instance; } setMindmap(instance); instance.on?.("back_forward", (index: number, len: number) => { setCanBack(index > 0); setCanForward(index < len - 1); }); instance.on?.("node_active", (_node: unknown, list: unknown[]) => { if (!list || list.length === 0) return; setActiveNodes(list || []); }); instance.on?.("node_click", (node: unknown) => { // 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致) // @ts-expect-error 第三方库节点对象 if (typeof node?.active === "function") { // @ts-expect-error simple-mind-map 节点对象缺少类型 node.active(); } else { // 兜底:手动维护激活列表 // @ts-expect-error simple-mind-map 渲染器缺少类型 instance.renderer?.clearActiveNodeList?.(); // @ts-expect-error simple-mind-map 渲染器缺少类型 instance.renderer?.addNodeToActiveList?.(node, true); // @ts-expect-error simple-mind-map 渲染器缺少类型 instance.renderer?.emitNodeActiveEvent?.(node); } const list = instance.renderer?.activeNodeList ?? []; setActiveNodes(list.length === 0 && node ? [node] : (list as unknown[])); }); instance.on?.("painter_start", () => setPainterMode(true)); instance.on?.("painter_end", () => setPainterMode(false)); instance.on?.("data_change", (data: unknown) => { debouncedPersist(data); }); const rootNode = getRootNode(instance); if (rootNode) { setActiveNodes([rootNode]); } if (destroyed) { instance.destroy(); } })(); return () => { destroyed = true; setMindmap((prev) => { prev?.destroy(); return null; }); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [block.id]); const handleUndo = () => mindmap?.execCommand("BACK"); const handleRedo = () => mindmap?.execCommand("FORWARD"); const handlePainter = () => { if (!mindmap?.painter) { window.alert("格式刷插件未就绪"); return; } mindmap.painter.startPainter(); }; const handleSibling = () => { if (!mindmap) return; window.setTimeout(() => mindmap.execCommand("INSERT_NODE"), 0); }; const handleChild = () => { if (!mindmap) return; window.setTimeout(() => mindmap.execCommand("INSERT_CHILD_NODE"), 0); }; const handleDelete = () => { if (!mindmap) return; window.setTimeout(() => mindmap.execCommand("REMOVE_NODE"), 0); }; const handleSummary = () => { const mm = ensureActiveBefore(mindmap); if (mm) { window.setTimeout(() => mm.execCommand("ADD_GENERALIZATION"), 0); } }; const handleAssociativeLine = () => { const mm = ensureActiveBefore(mindmap); if (mm) { window.setTimeout(() => mm.execCommand("ADD_ASSOCIATIVE_LINE"), 0); } }; const handleOuterFrame = () => { const mm = ensureActiveBefore(mindmap); if (mm) { window.setTimeout(() => mm.execCommand("ADD_OUTER_FRAME"), 0); } }; const [showImageModal, setShowImageModal] = useState(false); const [imageUrl, setImageUrl] = useState(""); const [imageWidth, setImageWidth] = useState(260); const [imageHeight, setImageHeight] = useState(200); const [imageTitle, setImageTitle] = useState(""); const [imagePosition, setImagePosition] = useState("top"); const fileInputForImage = useRef(null); // 图片预览(双击节点图片) const [showImageViewer, setShowImageViewer] = useState(false); const [viewerSrc, setViewerSrc] = useState(""); const [viewerMeta, setViewerMeta] = useState<{ title?: string; width?: number; height?: number }>({}); const imgToolbarRef = useRef(null); const [imgToolbarState, setImgToolbarState] = useState({ show: false, x: 0, y: 0, placement: "top" as "top" | "bottom" | "left" | "right", }); const imgToolbarHover = useRef(false); const handleImage = () => { const first = mindmap?.renderer?.activeNodeList?.[0] as MindMapNode | undefined; if (first?.getStyle) { const placement = first.getStyle("imgPlacement", false) as string; if (placement) setImagePosition(placement); } setShowImageModal(true); }; // 预览:监听 mindmap 事件 useEffect(() => { if (!mindmap) return; const handler = (node: MindMapNode, e?: Event) => { e?.stopPropagation?.(); e?.preventDefault?.(); const src = node?.nodeData?.data?.image || node?.getData?.("image") || node?.data?.image; if (src) { setViewerSrc(src); const size = node?.getData?.("imageSize") || {}; const title = node?.getData?.("imageTitle") || ""; setViewerMeta({ title: typeof title === "string" ? title : "", width: Number(size?.width) || undefined, height: Number(size?.height) || undefined, }); setShowImageViewer(true); } }; const onActive = () => { const list = (mindmap.renderer?.activeNodeList || []) as MindMapNode[]; const has = list.some((n) => !!n?.getData?.("image")); if (!has) setImgToolbarState((s) => ({ ...s, show: false })); }; const showToolbarOnClick = (node: MindMapNode, _svgImg: unknown, evt: Event | undefined) => { const target = evt?.target as Element | undefined; const bbox = target?.getBoundingClientRect?.(); const placement = (node?.getStyle?.("imgPlacement") || "top") as "top" | "bottom" | "left" | "right"; if (!bbox) return; setImagePosition(placement); setImgToolbarState({ show: true, x: bbox.left, y: bbox.top, placement, }); }; const hideToolbar = () => { if (!imgToolbarHover.current) { setImgToolbarState((s) => ({ ...s, show: false })); } }; mindmap.on?.("node_img_dblclick", handler); mindmap.on?.("node_active", onActive); mindmap.on?.("node_img_click", showToolbarOnClick); mindmap.on?.("draw_click", hideToolbar); return () => { mindmap.off?.("node_img_dblclick", handler); mindmap.off?.("node_active", onActive); mindmap.off?.("node_img_click", showToolbarOnClick); mindmap.off?.("draw_click", hideToolbar); }; }, [mindmap]); // 悬浮图片位置工具条 const renderImgToolbar = () => { if (!imgToolbarState.show) return null; const { x, y, placement } = imgToolbarState; const setPlacement = (p: "top" | "bottom" | "left" | "right") => { setImagePosition(p); applyToActiveNodes(mindmap, (node) => mindmap?.execCommand?.("SET_NODE_STYLES", node, { imgPlacement: p }), ); setImgToolbarState((s) => ({ ...s, placement: p })); }; const btn = ( p: typeof placement, Icon: React.ComponentType<{ className?: string }>, title: string, ) => ( ); return (
{ imgToolbarHover.current = true; setImgToolbarState((s) => ({ ...s, show: true })); }} onMouseLeave={() => { imgToolbarHover.current = false; setImgToolbarState((s) => ({ ...s, show: false })); }} > {btn("top", ArrowUp, "顶部")} {btn("bottom", ArrowDown, "底部")} {btn("left", ArrowLeft, "靠左")} {btn("right", ArrowRight, "靠右")}
); }; const handleIcon = () => { setActiveSidebar("icons"); }; const handleLink = () => { const href = createSimplePrompt("请输入超链接"); if (!href) return; applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_HYPERLINK", node, href, href)); }; const handleNote = () => { setShowNoteModal(true); }; const handleTag = () => { const tagsRaw = createSimplePrompt("请输入标签,使用逗号分隔", "重点,待验证"); if (!tagsRaw) return; const tags = tagsRaw.split(",").map((item) => item.trim()).filter(Boolean); applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_TAG", node, tags)); }; const handleFormula = () => { setActiveSidebar("formula"); }; const handleNoteConfirm = () => { const note = noteContent.trim(); setShowNoteModal(false); if (!note) return; applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_NOTE", node, note)); }; const handleImageConfirm = () => { const url = imageUrl.trim(); const width = Number(imageWidth) || 260; const height = Number(imageHeight) || 200; if (!url) { window.alert("请输入图片链接"); return; } setShowImageModal(false); applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_IMAGE", node, { url, width, height, title: imageTitle || "", position: imagePosition || "top", }), ); applyToActiveNodes(mindmap, (node) => mindmap?.execCommand?.("SET_NODE_STYLES", node, { imgPlacement: imagePosition || "top", }), ); }; const handleAttachment = () => { const url = createSimplePrompt("附件链接(http/https)"); if (!url) return; const name = createSimplePrompt("附件名称(可选)", "附件"); applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_ATTACHMENT", node, url, name ?? "")); }; const handleAiPlaceholder = () => { window.alert("AI 能力占位:后续接入大模型生成/优化节点内容。"); }; const handleImport = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const reset = () => { event.target.value = ""; }; const ext = (file.name.split(".").pop() || "").toLowerCase(); try { // JSON / smm if (ext === "json" || ext === "smm") { const text = await file.text(); const data = JSON.parse(text); mindmap?.setData(data); mindmap?.command.clearHistory(); debouncedPersist(data); return; } // XMind if (ext === "xmind") { const xmindParser = await import("simple-mind-map/src/parse/xmind.js"); const blob = new Blob([await file.arrayBuffer()]); const data = await xmindParser.default.parseXmindFile(blob, (content: unknown[]) => { const list = Array.isArray(content) ? content : []; if (list.length > 1) { window.alert("检测到 XMind 多画布,自动导入第一个画布。"); } return list.length > 0 ? list[0] : content; }); mindmap?.setData(data); mindmap?.command.clearHistory(); debouncedPersist(data); return; } // Markdown if (ext === "md" || ext === "markdown") { const { transformMarkdownTo } = await import( "simple-mind-map/src/parse/markdownTo.js" ); const text = await file.text(); const data = transformMarkdownTo(text) as MindMapData; if (!data.data) { data.data = { text: file.name.replace(/\.(md|markdown)$/i, "") || "中心主题" }; } mindmap?.setData(data); mindmap?.command.clearHistory(); debouncedPersist(data); return; } window.alert("暂不支持该文件类型,请选择 .json / .smm / .xmind / .md"); } catch (error) { console.error(error); window.alert("导入失败:文件格式或内容错误"); } finally { reset(); } }; const handleNew = () => { if (!window.confirm("确定要新建空白导图吗?当前未保存的修改将被覆盖。")) return; mindmap?.setData(defaultMindmapData); mindmap?.command.clearHistory(); debouncedPersist(defaultMindmapData); }; const handleOpenDirectory = () => { const saved = window.localStorage.getItem(autosaveKey); if (!saved) { window.alert("暂无本地目录记录,可使用“导入”载入文件。"); return; } try { const data = JSON.parse(saved); mindmap?.setData(data); window.alert("已从本地目录恢复最新自动保存版本。"); } catch { window.alert("本地目录数据损坏,建议重新导入。"); } }; const handleExportJson = () => { const data = mindmap?.getData?.(true) ?? block.props.data ?? defaultMindmapData; downloadJson(data, "mindmap"); }; const handleExport = async (type: string, name = "mindmap") => { try { await mindmap?.doExport?.export(type, true, name); } catch (error) { console.error(error); window.alert(`导出 ${type.toUpperCase()} 失败,请稍后再试`); } }; const handleExportPng = () => handleExport("png"); const handleExportSvg = () => handleExport("svg"); const handleExportPdf = () => handleExport("pdf"); const handleExportMd = () => handleExport("md"); const handleExportTxt = () => handleExport("txt"); const handleExportXmind = () => handleExport("xmind"); const handleSaveAs = () => handleExportJson(); const handleDeleteMindmap = useCallback(async () => { if (!docId) { editor.removeBlocks([block.id]); return; } const confirmed = window.confirm("确认删除当前思维导图?此操作会移除文件并清空侧边栏记录。"); if (!confirmed) return; const resp = await fetch(`/api/mindmap/${docId}`, { method: "DELETE" }); if (!resp.ok) { const payload = await resp.json().catch(() => ({})); window.alert(payload?.error ?? "删除思维导图失败"); return; } try { window.localStorage.removeItem(autosaveKey); } catch { // ignore } emitAssetsChanged(docId); editor.removeBlocks([block.id]); }, [autosaveKey, block.id, docId, editor]); const toolbarProps = { canBack, canForward, painterMode, onUndo: handleUndo, onRedo: handleRedo, onPainter: handlePainter, onSibling: handleSibling, onChild: handleChild, onDelete: handleDelete, onImage: handleImage, onIcon: handleIcon, onLink: handleLink, onNote: handleNote, onTag: handleTag, onSummary: handleSummary, onAssociativeLine: handleAssociativeLine, onFormula: handleFormula, onAttachment: handleAttachment, onOuterFrame: handleOuterFrame, onAi: handleAiPlaceholder, onImport: handleImport, onNew: handleNew, onOpenDirectory: handleOpenDirectory, onSaveAs: handleSaveAs, onDeleteMindmap: handleDeleteMindmap, onExportJson: handleExportJson, onExportPng: handleExportPng, onExportSvg: handleExportSvg, onExportPdf: handleExportPdf, onExportMd: handleExportMd, onExportTxt: handleExportTxt, onExportXmind: handleExportXmind, fileInputRef, }; const noteModal = showNoteModal ? (
备注