From 103e36b1c5b43c1b74ec116d13c3b338aa18a75a Mon Sep 17 00:00:00 2001 From: liaibo Date: Wed, 7 Jan 2026 07:25:07 +0800 Subject: [PATCH] =?UTF-8?q?0.1.03=20mindmap=E9=97=AE=E9=A2=98=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/editor/blocks/MindmapBlock.tsx | 360 ++++++++++-------- 1 file changed, 206 insertions(+), 154 deletions(-) diff --git a/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx b/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx index 8b539a3a..2ee9cb57 100644 --- a/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx +++ b/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx @@ -11,7 +11,14 @@ import React, { } from "react"; import { createPortal } from "react-dom"; import { createReactBlockSpec } from "@blocknote/react"; -import type { Block, BlockNoteEditor } from "@blocknote/core"; +import type { + BlockConfig, + BlockNoteEditor, + DefaultInlineContentSchema, + DefaultStyleSchema, + PropSchema, + SpecificBlock, +} from "@blocknote/core"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import type { CustomBlockSchema } from "../schema"; import { MindmapToolbar } from "./MindmapToolbar"; @@ -21,17 +28,16 @@ import { MindmapNavigator } from "./MindmapNavigator"; import { MindmapMiniMap } from "./MindmapMiniMap"; import { MindmapCount } from "./MindmapCount"; import type { SidebarPanel } from "./mindmapSidebarConfig"; +import type { MindMapNode } from "./mindmapTypes"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; -import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react"; +import { ArrowUp, ArrowDown, ArrowLeft, ArrowRight } from "lucide-react"; import iconConfig from "./mindmapIconConfig"; import { emitAssetsChanged } from "@/lib/events"; -import { useEditorBridgeStore } from "@/store/editor-bridge"; // 避免 SSR/Node 环境提前评估 simple-mind-map 内部依赖 document -// @ts-expect-error 第三方库缺少类型定义 const loadIconModules = async () => { - const { nodeIconList } = await import("simple-mind-map/src/svg/icons.js"); + 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 }; }; @@ -47,17 +53,37 @@ const getImageSizeSafe = (url: string): Promise<{ width: number; height: number }); type MindMapInstance = { - execCommand: (...args: unknown[]) => void; + execCommand: (command: string, ...args: unknown[]) => void; destroy: () => void; setData: (data: unknown) => void; getData: (withConfig?: boolean) => unknown; - on?: (event: string, handler: (...args: unknown[]) => void) => void; + on?: (event: string, handler: (...args: TArgs) => void) => void; emit?: (event: string, ...args: unknown[]) => void; - off?: (event: string, handler: (...args: unknown[]) => void) => void; + off?: (event: string, handler: (...args: TArgs) => 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 }; + editNodeClassList?: string[]; + view: { + fit: () => void; + scale: number; + enlarge: () => void; + narrow: () => void; + setScale: (scale: number, cx: number, cy: number) => void; + }; + renderer: { + activeNodeList: unknown[]; + lastActiveNodeList?: unknown[]; + root?: unknown; + renderTree: { _node: unknown }; + setRootNodeCenter?: () => void; + clearActiveNodeList?: () => void; + addNodeToActiveList?: (node: unknown, isActive?: boolean) => void; + emitNodeActiveEvent?: (node: unknown) => void; + findNodeByUid?: (uid: string) => unknown; + copy?: () => void; + paste?: () => void; + beingCopyData?: unknown; + }; painter?: { startPainter: () => void }; doExport?: { export: (type: string, isDownload?: boolean, name?: string) => Promise }; width: number; @@ -69,13 +95,6 @@ type MindMapInstance = { 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[]; @@ -109,22 +128,27 @@ 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]; + type SvgCtorPrototype = { + rbox?: (ref?: unknown) => unknown; + __wolaiRboxPatched?: boolean; + }; + type SvgCtor = { prototype?: SvgCtorPrototype } | undefined; + const svgModuleTyped = svgModule as unknown as { Element?: SvgCtor; G?: SvgCtor }; + const candidates = [svgModuleTyped.Element, svgModuleTyped.G]; let warned = false; 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) { // 退化到 DOM 的 getBoundingClientRect 计算,避免初次渲染时出现 (0,0) - const el = - // svg.js Element 实例通常持有 node 属性 - (this as any)?.node ?? (this as any)?.el ?? null; + type SvgElementLike = { node?: Element | null; el?: Element | null }; + const maybeThis = this as unknown as SvgElementLike; + const el = maybeThis?.node ?? maybeThis?.el ?? null; const rect = el && typeof el.getBoundingClientRect === "function" ? el.getBoundingClientRect() @@ -178,7 +202,7 @@ const applyToActiveNodes = ( const list = renderer?.activeNodeList; if (!list || list.length === 0) { // 优先尝试 lastActiveNodeList(部分操作会短暂清空 activeNodeList) - const lastActive = (renderer as any)?.lastActiveNodeList?.[0]; + const lastActive = renderer?.lastActiveNodeList?.[0]; const root = renderer?.root ?? renderer?.renderTree?._node; const fallback = lastActive ?? root; if (fallback) { @@ -202,15 +226,15 @@ const ensureActiveBefore = (mindmap?: MindMapInstance | null) => { const renderer = mm.renderer; const list = renderer?.activeNodeList ?? []; if (!list || list.length === 0) { - const lastActive = (renderer as any)?.lastActiveNodeList?.[0]; + const lastActive = renderer?.lastActiveNodeList?.[0]; const root = renderer?.root ?? renderer?.renderTree?._node; const fallback = lastActive ?? root; if (fallback) { // 主动维护激活列表,避免首次插入子节点时因无激活节点导致插入位置错误 // 先确保根节点居中,避免后续插入节点跑到 (0,0) - renderer?.setRootNodeCenter && renderer.setRootNodeCenter(); + if (renderer?.setRootNodeCenter) renderer.setRootNodeCenter(); mm.view?.fit?.(); - renderer?.clearActiveNodeList && renderer.clearActiveNodeList(); + if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList(); if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(fallback, true); if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(fallback); mm.execCommand?.("SET_NODE_ACTIVE", fallback, true); @@ -227,23 +251,25 @@ const MindmapBlockView = ({ editor, fullscreen = false, }: { - block: Block; + block: SpecificBlock< + CustomBlockSchema, + "mindmap", + DefaultInlineContentSchema, + DefaultStyleSchema + >; editor: BlockNoteEditor; fullscreen?: boolean; }) => { - const currentDocumentId = useEditorBridgeStore( - (state) => state.currentDocumentId, - ); const containerRef = useRef(null); const fileInputRef = useRef(null); - const [mindmap, setMindmap] = useState(null); + const [mindmap, setMindmap] = useState(null); const mindmapReadyRef = useRef(false); const mindmapRef = useRef(null); const hasLocalEditsRef = useRef(false); const applyingRemoteRef = useRef(false); const [canBack, setCanBack] = useState(false); const [canForward, setCanForward] = useState(false); - const [activeNodes, setActiveNodes] = useState([]); + const [activeNodes, setActiveNodes] = useState([]); const [painterMode, setPainterMode] = useState(false); const [showMiniMap, setShowMiniMap] = useState(false); const [activeSidebar, setActiveSidebar] = useState(null); @@ -264,11 +290,10 @@ const MindmapBlockView = ({ const docId = useMemo( () => block.props.docId || - currentDocumentId || (typeof window !== "undefined" ? window.location.pathname.split("/").pop() ?? "" : ""), - [block.props.docId, currentDocumentId], + [block.props.docId], ); useEffect(() => { @@ -387,8 +412,10 @@ const MindmapBlockView = ({ if (document.fullscreenElement) return; // 必须在“用户手势回调”中同步调用,避免 Fullscreen API 被浏览器拒绝 try { - const target = document.documentElement as any; - const p = target?.requestFullscreen?.(); + const target = document.documentElement as unknown as { + requestFullscreen?: () => Promise; + }; + const p = target.requestFullscreen?.(); if (p && typeof p.catch === "function") { p.catch(() => { // ignore:失败则保持 Portal 伪全屏 @@ -457,14 +484,14 @@ const MindmapBlockView = ({ if (!mm || !mindmapReadyRef.current) return; const target = e.target as HTMLElement | null; - const editClasses = (mm as any)?.editNodeClassList as string[] | undefined; + const editClasses = mm.editNodeClassList; const wrapper = wrapperRef.current; const isInWrapper = Boolean(wrapper && target && wrapper.contains(target)); if (target && isInWrapper) { const tag = target.tagName; if (tag === "INPUT" || tag === "TEXTAREA") return; // contenteditable 输入中不拦截:避免破坏节点文本编辑/粘贴 - if ((target as any).isContentEditable) return; + if (target.isContentEditable) return; const editableAncestor = target.closest?.('[contenteditable="true"]'); if (editableAncestor) return; if (editClasses && target.classList) { @@ -484,11 +511,11 @@ const MindmapBlockView = ({ }; const pickTargetNode = (inst: MindMapInstance) => { - const renderer = inst.renderer as any; - const active: any[] = renderer?.activeNodeList ?? []; - const last: any[] = renderer?.lastActiveNodeList ?? []; - const root = renderer?.root ?? renderer?.renderTree?._node ?? null; - return (active && active[0]) || (last && last[0]) || root || null; + const renderer = inst.renderer; + const active = renderer.activeNodeList ?? []; + const last = renderer.lastActiveNodeList ?? []; + const root = renderer.root ?? renderer.renderTree?._node ?? null; + return active[0] ?? last[0] ?? root ?? null; }; if (key === "Enter" && !e.shiftKey && !e.altKey && !isMod) { @@ -517,8 +544,7 @@ const MindmapBlockView = ({ stop(); const inst = ensureActiveBefore(mm); if (!inst) return; - const renderer = inst.renderer as any; - if (typeof renderer?.copy === "function") renderer.copy(); + inst.renderer.copy?.(); return; } @@ -526,13 +552,13 @@ const MindmapBlockView = ({ stop(); const inst = ensureActiveBefore(mm); if (!inst) return; - const renderer = inst.renderer as any; - const copyData = renderer?.beingCopyData ?? null; + const renderer = inst.renderer; + const copyData = renderer.beingCopyData ?? null; if (copyData) { inst.execCommand?.("PASTE_NODE", copyData); return; } - if (typeof renderer?.paste === "function") renderer.paste(); + renderer.paste?.(); } }; @@ -614,9 +640,8 @@ const MindmapBlockView = ({ if (!mindmap || activeNodes.length > 0) return; const rootNode = mindmap.renderer?.root ?? mindmap.renderer?.renderTree?._node; if (rootNode) { - setActiveNodes([rootNode]); + setActiveNodes([rootNode as MindMapNode]); mindmap.renderer.activeNodeList = [rootNode]; - // @ts-expect-error 第三方库字段 mindmap.renderer.lastActiveNodeList = [rootNode]; mindmap.emit?.("node_active", rootNode, [rootNode]); } @@ -695,33 +720,41 @@ const MindmapBlockView = ({ if (destroyed) return; // 兜底修补 simple-mind-map 在创建富文本节点时 host/缓存未就绪导致的空指针 - const MindMapNodeCtor = (MindMapNodeModule as any)?.default; + type MindMapNodeCtorLike = { + prototype: { + __wolaiRichtextPatched?: boolean; + createRichTextNode?: (...args: unknown[]) => unknown; + }; + }; + const MindMapNodeCtor = (MindMapNodeModule as unknown as { default?: MindMapNodeCtorLike })?.default; if ( MindMapNodeCtor && !MindMapNodeCtor.prototype.__wolaiRichtextPatched ) { const originalCreate = MindMapNodeCtor.prototype.createRichTextNode; MindMapNodeCtor.prototype.__wolaiRichtextPatched = true; - // @ts-expect-error 第三方库原型扩展 MindMapNodeCtor.prototype.createRichTextNode = function patched(...args: unknown[]) { + type RichtextThis = { + mindMap?: { el?: HTMLElement | null; commonCaches?: Record } | null; + }; + const self = this as unknown as RichtextThis; // host 兜底:优先使用实例容器,否则退回 body - const host: HTMLElement = - (this?.mindMap?.el as HTMLElement | null) ?? document.body; - if (!this.mindMap) { - this.mindMap = { el: host, commonCaches: {} } as any; + const host: HTMLElement = (self?.mindMap?.el as HTMLElement | null) ?? document.body; + if (!self.mindMap) { + self.mindMap = { el: host, commonCaches: {} }; } - if (!this.mindMap.el || typeof (this.mindMap.el as any).appendChild !== "function") { - this.mindMap.el = host; + const el = self.mindMap.el; + if (!el || typeof el.appendChild !== "function") { + self.mindMap.el = host; } - if (!this.mindMap.commonCaches) { - this.mindMap.commonCaches = {}; - } - if (!this.mindMap.commonCaches.measureRichtextNodeTextSizeEl) { + const caches = self.mindMap.commonCaches ?? (self.mindMap.commonCaches = {}); + const measureKey = "measureRichtextNodeTextSizeEl"; + if (!caches[measureKey]) { const measureDiv = document.createElement("div"); measureDiv.style.position = "fixed"; measureDiv.style.left = "-999999px"; - (this.mindMap.el ?? document.body).appendChild(measureDiv); - this.mindMap.commonCaches.measureRichtextNodeTextSizeEl = measureDiv; + (self.mindMap.el ?? document.body).appendChild(measureDiv); + caches[measureKey] = measureDiv; } if (typeof originalCreate === "function") { return originalCreate.apply(this, args); @@ -759,15 +792,17 @@ const MindmapBlockView = ({ console.warn(`思维导图插件加载失败:${name}`); return; } - // @ts-expect-error 第三方库缺少类型定义 - if (MindMap.hasPlugin(plugin) === -1) { + const MindMapCtor = MindMap as unknown as { + hasPlugin?: (p: unknown) => number; + usePlugin?: (p: unknown) => void; + }; + const hasPlugin = MindMapCtor.hasPlugin; + const notRegistered = + typeof hasPlugin === "function" ? hasPlugin(plugin) === -1 : true; + const registerPlugin = MindMapCtor.usePlugin; + if (notRegistered && typeof registerPlugin === "function") { console.log(`注册插件: ${name}`); - const registerPlugin = - // @ts-expect-error simple-mind-map 插件注册无类型声明 - (MindMap as { usePlugin?: (p: unknown) => void }).usePlugin; - if (registerPlugin) { - registerPlugin(plugin); - } + registerPlugin(plugin); } }); @@ -785,7 +820,22 @@ const MindmapBlockView = ({ hostEl.innerHTML = ""; } - const instance = new MindMap({ + type MindMapConstructor = new (options: { + el: HTMLElement; + data: unknown; + theme: string; + layout: string; + mousewheelAction: string; + enableFreeDrag: boolean; + enableCtrlKeyNodeSelection: boolean; + fit: boolean; + useLeftKeySelectionRightKeyDrag: boolean; + createNewNodeBehavior: string; + iconList: unknown[]; + }) => MindMapInstance; + const MindMapCtor = MindMap as unknown as MindMapConstructor; + + const instance = new MindMapCtor({ el: hostEl, data: initialDataRef.current, theme: "classic", @@ -802,22 +852,25 @@ const MindmapBlockView = ({ ...nodeIconList, ...(iconConfig as unknown[]), ]), - }) as MindMapInstance; + }); createdInstance = instance; mindmapRef.current = instance; // 尽早标记为可用:主页面通过 `/mind` 插入后,若依赖 render_end/尺寸探测,可能导致按钮永远不可用 //(render_end 可能早于监听注册触发,或容器尺寸长时间为 0)。命令本身不需要等待 render_end。 mindmapReadyRef.current = true; // 确保测量节点的缓存容器始终挂在有效 DOM 上,避免 appendChild 空指针 - if (!(instance as any).commonCaches) { - (instance as any).commonCaches = {}; + type MindMapWithCaches = MindMapInstance & { commonCaches?: Record }; + const instanceWithCaches = instance as MindMapWithCaches; + if (!instanceWithCaches.commonCaches) { + instanceWithCaches.commonCaches = {}; } - if (!(instance as any).commonCaches.measureRichtextNodeTextSizeEl) { + const measureKey = "measureRichtextNodeTextSizeEl"; + if (!instanceWithCaches.commonCaches[measureKey]) { const measureDiv = document.createElement("div"); measureDiv.style.position = "fixed"; measureDiv.style.left = "-999999px"; - ((instance as any).commonCaches).measureRichtextNodeTextSizeEl = measureDiv; - (hostEl ?? document.body).appendChild(measureDiv); + instanceWithCaches.commonCaches[measureKey] = measureDiv; + hostEl.appendChild(measureDiv); } instance.setMode?.("edit"); const centerAndFit = (retry = 0) => { @@ -833,11 +886,7 @@ const MindmapBlockView = ({ //(尤其在首次构造时同步渲染),因此不要完全依赖 render_end 来标记就绪。 mindmapReadyRef.current = true; const renderer = instance.renderer; - const rootNode = - // @ts-expect-error 第三方库字段 - renderer?.root ?? - // @ts-expect-error 第三方库字段 - renderer?.renderTree?._node; + const rootNode = renderer?.root ?? renderer?.renderTree?._node; if (rootNode && renderer?.setRootNodeCenter) { renderer.setRootNodeCenter(); } @@ -855,7 +904,6 @@ const MindmapBlockView = ({ if (typeof window !== "undefined") { // 便于开发阶段在控制台直接调试实例 - // @ts-expect-error 调试用全局变量 window.__mindmapInstance = instance; } @@ -892,28 +940,28 @@ const MindmapBlockView = ({ } }; instance.on?.("render_end", onRenderEndOnce); - instance.on?.("node_active", (_node: unknown, list: unknown[]) => { + instance.on?.("node_active", (_node: unknown, list: unknown[]) => { if (!list || list.length === 0) return; - setActiveNodes(list || []); + setActiveNodes((list || []) as MindMapNode[]); }); instance.on?.("node_click", (node: unknown) => { // 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致) - // @ts-expect-error 第三方库节点对象 const renderer = instance.renderer; - if (typeof (node as any)?.active === "function") { - // @ts-expect-error simple-mind-map 节点对象缺少类型 - node.active(); + const nodeWithActive = node as unknown as { active?: () => void }; + if (typeof nodeWithActive.active === "function") { + nodeWithActive.active(); } else { // 兜底:手动维护激活列表 - // @ts-expect-error simple-mind-map 渲染器缺少类型 - renderer?.clearActiveNodeList && renderer.clearActiveNodeList(); - // @ts-expect-error simple-mind-map 渲染器缺少类型 - renderer?.addNodeToActiveList && renderer.addNodeToActiveList(node, true); - // @ts-expect-error simple-mind-map 渲染器缺少类型 - renderer?.emitNodeActiveEvent && renderer.emitNodeActiveEvent(node); + if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList(); + if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(node, true); + if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(node); } const list = renderer?.activeNodeList ?? []; - setActiveNodes(list.length === 0 && node ? [node] : (list as unknown[])); + setActiveNodes( + list.length === 0 && node + ? [node as MindMapNode] + : (list as MindMapNode[]), + ); }); instance.on?.("painter_start", () => setPainterMode(true)); instance.on?.("painter_end", () => setPainterMode(false)); @@ -927,19 +975,19 @@ const MindmapBlockView = ({ const rootNode = getRootNode(instance); if (rootNode) { // 初始化时主动标记根节点为选中,确保后续插入子节点有合法的父节点 - renderer?.clearActiveNodeList && renderer.clearActiveNodeList(); - renderer?.addNodeToActiveList && renderer.addNodeToActiveList(rootNode, true); - renderer?.emitNodeActiveEvent && renderer.emitNodeActiveEvent(rootNode); - setActiveNodes([rootNode]); + if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList(); + if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(rootNode, true); + if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(rootNode); + setActiveNodes([rootNode as MindMapNode]); } // 若首次渲染时 root 仍未就绪,设置兜底延迟激活 window.setTimeout(() => { const root = getRootNode(instance); if (!root) return; - renderer?.clearActiveNodeList && renderer.clearActiveNodeList(); - renderer?.addNodeToActiveList && renderer.addNodeToActiveList(root, true); - renderer?.emitNodeActiveEvent && renderer.emitNodeActiveEvent(root); - setActiveNodes([root]); + if (renderer?.clearActiveNodeList) renderer.clearActiveNodeList(); + if (renderer?.addNodeToActiveList) renderer.addNodeToActiveList(root, true); + if (renderer?.emitNodeActiveEvent) renderer.emitNodeActiveEvent(root); + setActiveNodes([root as MindMapNode]); }, 120); if (destroyed) { @@ -981,9 +1029,7 @@ const MindmapBlockView = ({ // ignore } if (typeof window !== "undefined") { - // @ts-expect-error 调试用全局变量 if (window.__mindmapInstance === createdInstance) { - // @ts-expect-error 调试用全局变量 window.__mindmapInstance = null; } } @@ -997,8 +1043,8 @@ const MindmapBlockView = ({ const getInstanceCandidate = () => mindmap ?? (typeof window !== "undefined" - ? // @ts-expect-error 调试用全局变量 - (window.__mindmapInstance as MindMapInstance | undefined | null) + ? ((window as unknown as { __mindmapInstance?: MindMapInstance | null }) + .__mindmapInstance ?? null) : null); const runWhenReady = ( @@ -1030,11 +1076,16 @@ const MindmapBlockView = ({ const pickActiveOrRoot = (mm: MindMapInstance) => { const list = mm.renderer?.activeNodeList; - const last = (mm.renderer as any)?.lastActiveNodeList; - // @ts-expect-error 第三方库类型缺失 + const last = mm.renderer?.lastActiveNodeList; return (list && list[0]) || (last && last[0]) || getRootNode(mm) || null; }; + const getNodeUid = (node: unknown): string | null => { + const maybeNode = node as unknown as { getData?: (key: string) => unknown; uid?: unknown }; + const raw = typeof maybeNode.getData === "function" ? maybeNode.getData("uid") : maybeNode.uid; + return typeof raw === "string" && raw ? raw : null; + }; + const handleUndo = () => runWhenReady((mm) => mm.execCommand("BACK")); const handleRedo = () => runWhenReady((mm) => mm.execCommand("FORWARD")); const handlePainter = () => { @@ -1049,11 +1100,7 @@ const MindmapBlockView = ({ execWithReflow((mm) => { const target = pickActiveOrRoot(mm); if (!target) return; - const targetUid = - // @ts-expect-error simple-mind-map 节点缺少类型 - (typeof (target as any)?.getData === "function" - ? (target as any).getData("uid") - : (target as any)?.uid) ?? null; + const targetUid = getNodeUid(target); mm.execCommand?.("SET_NODE_ACTIVE", target, true); // openEdit=false:保持与 createNewNodeBehavior=activeOnly 的策略一致,避免强制进入编辑模式带来的时序问题 mm.execCommand?.("INSERT_NODE", false, [target]); @@ -1062,13 +1109,15 @@ const MindmapBlockView = ({ // 这里尝试在下一帧将新插入的节点设为激活(插入同级节点时,新节点位于 target 后)。 window.setTimeout(() => { try { - const renderer = mm.renderer as any; - if ((renderer?.activeNodeList?.length ?? 0) > 0) return; - if (!targetUid || typeof renderer?.findNodeByUid !== "function") return; - const currentTarget = renderer.findNodeByUid(targetUid); - const parent = currentTarget?.parent; - const siblings: any[] = parent?.children ?? []; - const idx = siblings.indexOf(currentTarget); + const renderer = mm.renderer; + if ((renderer.activeNodeList?.length ?? 0) > 0) return; + if (!targetUid || typeof renderer.findNodeByUid !== "function") return; + const currentTarget = renderer.findNodeByUid(targetUid) as unknown as { + parent?: { children?: unknown[] } | null; + }; + const parent = currentTarget?.parent ?? null; + const siblings = (parent?.children ?? []) as unknown[]; + const idx = siblings.indexOf(currentTarget as unknown); const inserted = idx >= 0 ? siblings[idx + 1] : null; if (!inserted) return; renderer?.clearActiveNodeList?.(); @@ -1085,11 +1134,7 @@ const MindmapBlockView = ({ execWithReflow((mm) => { const parent = pickActiveOrRoot(mm); if (!parent) return; - const parentUid = - // @ts-expect-error simple-mind-map 节点缺少类型 - (typeof (parent as any)?.getData === "function" - ? (parent as any).getData("uid") - : (parent as any)?.uid) ?? null; + const parentUid = getNodeUid(parent); mm.execCommand?.("SET_NODE_ACTIVE", parent, true); // openEdit=false:同上,避免插入瞬间强制进入编辑导致异常 mm.execCommand?.("INSERT_CHILD_NODE", false, [parent]); @@ -1097,11 +1142,13 @@ const MindmapBlockView = ({ // 兜底:确保新插入的子节点成为激活节点,保证随后点击“同级节点/删除节点”可用 window.setTimeout(() => { try { - const renderer = mm.renderer as any; - if ((renderer?.activeNodeList?.length ?? 0) > 0) return; - if (!parentUid || typeof renderer?.findNodeByUid !== "function") return; - const currentParent = renderer.findNodeByUid(parentUid); - const children: any[] = currentParent?.children ?? []; + const renderer = mm.renderer; + if ((renderer.activeNodeList?.length ?? 0) > 0) return; + if (!parentUid || typeof renderer.findNodeByUid !== "function") return; + const currentParent = renderer.findNodeByUid(parentUid) as unknown as { + children?: unknown[]; + }; + const children = (currentParent?.children ?? []) as unknown[]; const inserted = children.length > 0 ? children[children.length - 1] : null; if (!inserted) return; renderer?.clearActiveNodeList?.(); @@ -1182,18 +1229,23 @@ const MindmapBlockView = ({ const handler = (node: MindMapNode, e?: Event) => { e?.stopPropagation?.(); e?.preventDefault?.(); - const src = + const srcRaw = node?.nodeData?.data?.image || node?.getData?.("image") || node?.data?.image; + const src = typeof srcRaw === "string" ? srcRaw : ""; if (src) { setViewerSrc(src); - const size = node?.getData?.("imageSize") || {}; + const sizeRaw = node?.getData?.("imageSize"); + const size = + sizeRaw && typeof sizeRaw === "object" + ? (sizeRaw as { width?: unknown; height?: unknown }) + : ({} as { width?: unknown; height?: unknown }); const title = node?.getData?.("imageTitle") || ""; setViewerMeta({ title: typeof title === "string" ? title : "", - width: Number(size?.width) || undefined, - height: Number(size?.height) || undefined, + width: Number(size.width) || undefined, + height: Number(size.height) || undefined, }); setShowImageViewer(true); } @@ -1377,10 +1429,10 @@ const MindmapBlockView = ({ 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 : []; + const data = await xmindParser.default.parseXmindFile(blob, (content) => { + const list = content; if (list.length > 1) { - window.alert("检测到 XMind 多画布,自动导入第一个画布。"); + window.alert("检测到 XMind 多画布,自动导入第一个画布。"); } return list.length > 0 ? list[0] : content; }); @@ -1773,10 +1825,9 @@ const MindmapBlockView = ({ activeSidebar={activeSidebar} onSelect={setActiveSidebar} /> - {/* @ts-expect-error mindmap type */} setActiveSidebar(null)} /> @@ -1795,7 +1846,6 @@ const MindmapBlockView = ({ miniMapOpen={showMiniMap} /> - {/* @ts-expect-error mindmap type */} {imgToolbar} {noteModal} @@ -1854,14 +1904,13 @@ const MindmapBlockView = ({ contentEditable={false} /> - - {/* @ts-expect-error mindmap type */} - setActiveSidebar(null)} /> @@ -1880,7 +1929,6 @@ const MindmapBlockView = ({ miniMapOpen={showMiniMap} /> - {/* @ts-expect-error mindmap type */} {imgToolbar} {noteModal} @@ -1893,6 +1941,8 @@ const MindmapBlockView = ({ export { MindmapBlockView }; +type MindmapBlockViewProps = Parameters[0]; + export const mindmapBlock = createReactBlockSpec( { type: "mindmap", @@ -1901,8 +1951,10 @@ export const mindmapBlock = createReactBlockSpec( data: { default: defaultMindmapData }, }, content: "none", - }, + } as unknown as BlockConfig<"mindmap", PropSchema, "none">, { - render: (props) => , + render: (props) => ( + + ), }, );