/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useEffect, useMemo, useState } from "react"; // 说明:legacy/reference/compat 组件,仅作为 Phase 6 UI shell 迁移素材。 // 3000 文档页默认 mindmap 主链是 leptos-tiptap NodeView + Leptos/Rust shell。 import Image from "next/image"; import { Label } from "@/components/ui/label"; import { Input } from "@/components/ui/input"; import { Toggle } from "@/components/ui/toggle"; import { Bold, Italic, Underline, Type, Strikethrough, Palette, X, Network } from "lucide-react"; import { fontFamilyList, fontSizeList, lineHeightList, borderDasharrayList, borderRadiusList, borderWidthList, lineWidthList, shapeList, lineStyleList, themeList, layoutList, outlineDepthOptions, backgroundRepeatList, backgroundPositionList, backgroundSizeList, } from "./mindmapOptions"; import iconConfig from "./mindmapIconConfig"; import imageConfig from "./mindmapImageConfig"; import { sidebarTitles, type SidebarPanel } from "./mindmapSidebarConfig"; import type { MindMapNode } from "./mindmapTypes"; import { MindmapAiAgentPanel } from "./MindmapAiAgentPanel"; // 避免 SSR 阶段触发浏览器依赖,改为运行时动态引入 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 }; }; type SidebarProps = { documentId: string; mindmapId: string; mindmap: any; activeNodes: MindMapNode[]; activeTab: SidebarPanel | null; onClose: () => void; }; // 旧 Next route 已退场;补完节点能力保留在 AI Agent 的 mindmap_expand_node 工具中。 const MINDMAP_LEGACY_EXPAND_NODE_ENTRY_ENABLED = false; const ColorInput = ({ value, onChange, label, }: { value: string; onChange: (val: string) => void; label?: string; }) => (
{label && {label}}
onChange(e.target.value)} className="absolute inset-[-50%] h-[200%] w-[200%] cursor-pointer border-0 p-0" />
); const NativeSelect = ({ value, onChange, options, placeholder, disabled, }: { value: string | number; onChange: (val: string) => void; options: { label: string | number; value: string | number }[]; placeholder?: string; disabled?: boolean; }) => (
); const StylePanel = ({ activeNodes }: { activeNodes: MindMapNode[] }) => { const [style, setStyle] = useState>({}); useEffect(() => { if (activeNodes.length > 0) { const node = activeNodes[0]; const newStyle: Record = {}; [ "fontFamily", "fontSize", "lineHeight", "color", "fontWeight", "fontStyle", "textDecoration", "borderWidth", "borderColor", "fillColor", "shape", "lineColor", "lineWidth", "lineDasharray", "borderRadius", "borderDasharray" ].forEach((prop) => { newStyle[prop] = node.getStyle(prop, false); }); // 异步更新,避免同步 setState 抛 lint Promise.resolve().then(() => setStyle(newStyle)); } }, [activeNodes]); const updateStyle = (prop: string, value: any) => { setStyle((prev) => ({ ...prev, [prop]: value })); activeNodes.forEach((node) => { node.setStyle?.(prop, value); }); }; if (activeNodes.length === 0) { return (

请选择一个节点以编辑样式

); } return (
({ label: f.name, value: f.value }))} onChange={(v) => updateStyle("fontFamily", v)} />
({ label: `${s}px`, value: s }))} onChange={(v) => updateStyle("fontSize", Number(v))} /> ({ label: h, value: h }))} onChange={(v) => updateStyle("lineHeight", Number(v))} />
updateStyle("fontWeight", pressed ? "bold" : "normal")} > updateStyle("fontStyle", pressed ? "italic" : "normal")} > updateStyle("textDecoration", pressed ? "underline" : "none")} > updateStyle("textDecoration", pressed ? "line-through" : "none")} >
updateStyle("color", v)} />
({label: s.name, value: s.value}))} onChange={(v) => updateStyle("shape", v)} /> ({label: s.name, value: s.value}))} onChange={(v) => updateStyle("borderDasharray", v)} />
({ label: `${n}px`, value: n }))} onChange={(v) => updateStyle("borderWidth", Number(v))} /> ({ label: `${n}px`, value: n }))} onChange={(v) => updateStyle("borderRadius", Number(v))} />
updateStyle("borderColor", v)} /> updateStyle("fillColor", v)} />
({label: s.name, value: s.value}))} onChange={(v) => updateStyle("lineDasharray", v)} /> ({ label: `${n}px`, value: n }))} onChange={(v) => updateStyle("lineWidth", Number(v))} />
updateStyle("lineColor", v)} />
); }; const BaseStylePanel = ({ mindmap }: { mindmap: any }) => { const [style, setStyle] = useState>({}); useEffect(() => { if (!mindmap) return; const initStyle = () => { const newStyle: Record = {}; [ "backgroundColor", "lineColor", "lineWidth", "lineStyle", "paddingX", "paddingY", "backgroundImage", "backgroundRepeat", "backgroundPosition", "backgroundSize", ].forEach(key => { newStyle[key] = mindmap.getThemeConfig(key); }); setStyle(newStyle); }; initStyle(); }, [mindmap]); const updateThemeConfig = (key: string, value: any) => { if (!mindmap) return; setStyle(prev => ({ ...prev, [key]: value })); mindmap.setThemeConfig({ [key]: value }); }; if (!mindmap) return null; return (
updateThemeConfig("backgroundColor", v)} />
updateThemeConfig("backgroundImage", e.target.value || "")} placeholder="可粘贴图片地址" />
({ label: i.name, value: i.value }))} onChange={(v) => updateThemeConfig("backgroundRepeat", v)} /> ({ label: i.name, value: i.value }))} onChange={(v) => updateThemeConfig("backgroundPosition", v)} /> ({ label: i.name, value: i.value }))} onChange={(v) => updateThemeConfig("backgroundSize", v)} />
({ label: s.name, value: s.value }))} onChange={(v) => updateThemeConfig("lineStyle", v)} /> ({ label: `${n}px`, value: n }))} onChange={(v) => updateThemeConfig("lineWidth", Number(v))} />
updateThemeConfig("lineColor", v)} />
水平 updateThemeConfig("paddingX", Number(e.target.value))} />
垂直 updateThemeConfig("paddingY", Number(e.target.value))} />
); }; const ThemePanel = ({ mindmap }: { mindmap: any }) => { const changeTheme = (theme: string) => { if (!mindmap) return; mindmap.setTheme(theme); }; if (!mindmap) return null; return (
{themeList.map(t => ( ))}
) } const StructurePanel = ({ mindmap }: { mindmap: any }) => { const changeLayout = (layout: string) => { if (!mindmap) return; mindmap.setLayout(layout); }; if (!mindmap) return null; return (
{layoutList.map(l => ( ))}
) } const IconPanel = ({ activeNodes }: { activeNodes: MindMapNode[] }) => { const [activeSubTab, setActiveSubTab] = useState<"icon" | "sticker">("icon"); const [{ iconGroups, loading }, setIconGroups] = useState<{ iconGroups: any[]; loading: boolean; }>({ iconGroups: [], loading: true }); useEffect(() => { let mounted = true; (async () => { const { nodeIconList, mergerIconList } = await loadIconModules(); if (!mounted) return; setIconGroups({ iconGroups: mergerIconList([ ...nodeIconList, ...(iconConfig as unknown as any[]), ]), loading: false, }); })(); return () => { mounted = false; }; }, []); const [stickerGroups] = useState(imageConfig); const addIcon = (type: string, name: string) => { const key = `${type}_${name}`; activeNodes.forEach((node) => { const rawIcons = node.getData("icon"); const icons = Array.isArray(rawIcons) ? (rawIcons as string[]) : []; const newIcons = icons.filter((i) => !i.startsWith(`${type}_`)); newIcons.push(key); node.setIcon?.(newIcons); }); }; const removeIcon = (type: string) => { activeNodes.forEach((node) => { const rawIcons = node.getData("icon"); const icons = Array.isArray(rawIcons) ? (rawIcons as string[]) : []; const newIcons = icons.filter((i) => !i.startsWith(`${type}_`)); node.setIcon?.(newIcons); }); }; const setSticker = (img: { url: string; width?: number; height?: number }) => { activeNodes.forEach((node) => { // simple-mind-map 支持 setImage 接收对象,包含 url/width/height node.setImage?.({ url: img.url, width: img.width || 100, height: img.height || 100, }); }); }; const clearSticker = () => { activeNodes.forEach((node) => { // 传入 null 以清除贴纸 node.setImage?.(null); }); }; if (loading) return
图标加载中…
; if (iconGroups.length === 0) return
暂无图标
; if (activeNodes.length === 0) return
请选择节点
; return (
{activeSubTab === "icon" && iconGroups.map((group) => (
{group.list.map((item: any) => ( ))}
))} {activeSubTab === "sticker" && (
{stickerGroups.map((group: any) => (
{group.name}
{group.list.map((item: any, idx: number) => ( ))}
))}
)}
); }; const OutlinePanel = ({ mindmap }: { mindmap: any }) => { const [depth, setDepth] = useState(-1); const nodes = useMemo(() => { const root = mindmap?.renderer?.renderTree?._node; const list: { node: any; text: string; depth: number }[] = []; const walk = (n: any, d: number) => { if (!n) return; const raw = n.nodeData?.data?.text || n.nodeData?.text || n.data?.text || n.data?.data?.text || "未命名"; const text = typeof raw === "string" ? raw.replace(/<[^>]+>/g, "").trim() || "未命名" : "未命名"; list.push({ node: n, text, depth: d }); (n.children || []).forEach((c: any) => walk(c, d + 1)); }; if (root) walk(root, 0); return list; }, [mindmap]); const visibleNodes = useMemo( () => nodes.filter((item) => depth < 0 || item.depth < depth), [nodes, depth], ); const activate = (node: any) => { const r = mindmap?.renderer; if (!node || !r) return; // 仅通过已有方法触发激活,避免直接改 renderer 属性 if (typeof r.clearActiveNodeList === "function") { r.clearActiveNodeList(); } if (typeof r.addNodeToActiveList === "function") { r.addNodeToActiveList(node, true); } else { // 兜底:仍保留最小副作用写入 try { r?.setActiveNode?.(node); } catch { // 最后兜底:不再直接改引用,避免 lint 报错 } } mindmap.emit?.("node_active", node, [node]); r.setRootNodeCenter?.(); }; if (!nodes.length) return
暂无节点
; return (
({ label: o.name, value: o.value }))} onChange={(v) => setDepth(Number(v))} />
{visibleNodes.map((item, idx) => ( ))}
); }; const SettingsPanel = ({ mindmap }: { mindmap: any }) => { const [config, setConfig] = useState({ openPerformance: !!mindmap?.opt?.openPerformance, enableFreeDrag: !!mindmap?.opt?.enableFreeDrag, mousewheelAction: mindmap?.opt?.mousewheelAction || "zoom", mousewheelZoomActionReverse: !!mindmap?.opt?.mousewheelZoomActionReverse, openRealtimeRenderOnNodeTextEdit: !!mindmap?.opt?.openRealtimeRenderOnNodeTextEdit, alwaysShowExpandBtn: !!mindmap?.opt?.alwaysShowExpandBtn, enableAutoEnterTextEditWhenKeydown: !!mindmap?.opt?.enableAutoEnterTextEditWhenKeydown, createNewNodeBehavior: mindmap?.opt?.createNewNodeBehavior || "default", imgTextMargin: mindmap?.opt?.imgTextMargin ?? 5, textContentMargin: mindmap?.opt?.textContentMargin ?? 2, }); const [aiOn, setAiOn] = useState(mindmap?.opt?.enableAi ?? true); const [watermark, setWatermark] = useState({ show: !!mindmap?.opt?.watermarkConfig?.text, onlyExport: mindmap?.opt?.watermarkConfig?.onlyExport ?? false, belowNode: mindmap?.opt?.watermarkConfig?.belowNode ?? false, text: mindmap?.opt?.watermarkConfig?.text ?? "", lineSpacing: mindmap?.opt?.watermarkConfig?.lineSpacing ?? 100, textSpacing: mindmap?.opt?.watermarkConfig?.textSpacing ?? 100, angle: mindmap?.opt?.watermarkConfig?.angle ?? 30, textStyle: { color: mindmap?.opt?.watermarkConfig?.textStyle?.color ?? "#999", opacity: mindmap?.opt?.watermarkConfig?.textStyle?.opacity ?? 0.5, fontSize: mindmap?.opt?.watermarkConfig?.textStyle?.fontSize ?? 14, }, }); useEffect(() => { const opt = mindmap?.opt || {}; Promise.resolve().then(() => setConfig({ openPerformance: !!opt.openPerformance, enableFreeDrag: !!opt.enableFreeDrag, mousewheelAction: opt.mousewheelAction || "zoom", mousewheelZoomActionReverse: !!opt.mousewheelZoomActionReverse, openRealtimeRenderOnNodeTextEdit: !!opt.openRealtimeRenderOnNodeTextEdit, alwaysShowExpandBtn: !!opt.alwaysShowExpandBtn, enableAutoEnterTextEditWhenKeydown: !!opt.enableAutoEnterTextEditWhenKeydown, createNewNodeBehavior: opt.createNewNodeBehavior || "default", imgTextMargin: opt.imgTextMargin ?? 5, textContentMargin: opt.textContentMargin ?? 2, }), ); const wm = opt.watermarkConfig || {}; Promise.resolve().then(() => setWatermark({ show: !!wm.text, onlyExport: wm.onlyExport ?? false, belowNode: wm.belowNode ?? false, text: wm.text ?? "", lineSpacing: wm.lineSpacing ?? 100, textSpacing: wm.textSpacing ?? 100, angle: wm.angle ?? 30, textStyle: { color: wm.textStyle?.color ?? "#999", opacity: wm.textStyle?.opacity ?? 0.5, fontSize: wm.textStyle?.fontSize ?? 14, }, }), ); Promise.resolve().then(() => setAiOn(opt.enableAi ?? true)); }, [mindmap]); const updateOpt = (key: string, value: any, needRender = false) => { setConfig((prev) => ({ ...prev, [key]: value })); mindmap?.updateConfig?.({ [key]: value }); if (needRender && mindmap?.reRender) { mindmap.reRender(); } }; const applyWatermark = (next: any) => { const { show, ...cfg } = next; const finalCfg = show ? cfg : { ...cfg, text: "" }; mindmap?.watermark?.updateWatermark?.(finalCfg); mindmap?.updateConfig?.({ watermarkConfig: finalCfg }); }; const updateWatermark = (patch: any) => { setWatermark((prev) => { const next = { ...prev, ...patch, textStyle: { ...prev.textStyle, ...(patch.textStyle || {}) }, }; applyWatermark(next); return next; }); }; const updateAi = (val: boolean) => { setAiOn(val); mindmap?.updateConfig?.({ enableAi: val }); }; return (
updateWatermark({ show: v })} > {watermark.show ? "开" : "关"}
{watermark.show && (
updateWatermark({ onlyExport: v })} > {watermark.onlyExport ? "开" : "关"}
updateWatermark({ belowNode: v })} > {watermark.belowNode ? "开" : "关"}
updateWatermark({ text: e.target.value })} />
updateWatermark({ textStyle: { color: val } })} />
updateWatermark({ textStyle: { opacity: Math.min(1, Math.max(0, Number(e.target.value) || 0)), }, }) } />
updateWatermark({ textStyle: { fontSize: Number(e.target.value) || 14 }, }) } />
updateWatermark({ angle: Number(e.target.value) || 0 })} />
updateWatermark({ lineSpacing: Number(e.target.value) || 0 })} />
updateWatermark({ textSpacing: Number(e.target.value) || 0 })} />
)}
updateOpt("openPerformance", v)} > {config.openPerformance ? "开" : "关"}
updateOpt("enableFreeDrag", v)} > {config.enableFreeDrag ? "开" : "关"}
updateOpt("openRealtimeRenderOnNodeTextEdit", v)} > {config.openRealtimeRenderOnNodeTextEdit ? "开" : "关"}
updateOpt("alwaysShowExpandBtn", v, true)} > {config.alwaysShowExpandBtn ? "开" : "关"}
updateOpt("enableAutoEnterTextEditWhenKeydown", v)} > {config.enableAutoEnterTextEditWhenKeydown ? "开" : "关"}
updateOpt("mousewheelAction", v)} options={[ { label: "缩放", value: "zoom" }, { label: "平移", value: "move" }, ]} />
{config.mousewheelAction === "zoom" && (
updateOpt("mousewheelZoomActionReverse", v === "true")} options={[ { label: "常规", value: "false" }, { label: "反转", value: "true" }, ]} />
)}
updateOpt("createNewNodeBehavior", v)} options={[ { label: "默认", value: "default" }, { label: "不激活", value: "notActive" }, { label: "仅激活新节点", value: "activeOnly" }, ]} />
updateOpt("imgTextMargin", Number(e.target.value) || 0, true)} />
updateOpt("textContentMargin", Number(e.target.value) || 0, true)} />
{aiOn ? "开" : "关"}
); }; const FormulaPanel = ({ mindmap }: { mindmap: any }) => { const [formula, setFormula] = useState("a^2+b^2=c^2"); const insert = () => { const active = mindmap?.renderer?.activeNodeList || []; mindmap?.execCommand?.("INSERT_FORMULA", formula, active); }; return (
setFormula(e.target.value)} />
); }; const NotePanel = ({ mindmap, activeNodes }: { mindmap: any; activeNodes: MindMapNode[] }) => { const [note, setNote] = useState(""); const readNote = (node: any) => { try { return ( node?.getData?.("note") ?? node?.nodeData?.data?.note ?? node?.data?.note ?? "" ); } catch { return ""; } }; // 当选中节点变化时,自动展示首个节点的备注 useEffect(() => { if (!activeNodes?.length) { Promise.resolve().then(() => setNote("")); return; } const current = readNote(activeNodes[0]); Promise.resolve().then(() => setNote(typeof current === "string" ? current : ""), ); }, [activeNodes]); const getActiveList = () => mindmap?.renderer?.activeNodeList?.length ? mindmap.renderer.activeNodeList : activeNodes || []; const apply = () => { const list = getActiveList(); list.forEach((node: any) => mindmap?.execCommand?.("SET_NODE_NOTE", node, note)); }; const clearNote = () => { const list = getActiveList(); list.forEach((node: any) => mindmap?.execCommand?.("SET_NODE_NOTE", node, "")); setNote(""); }; if (!activeNodes?.length) { return
请选择节点后编辑备注
; } return (