2025-12-28 10:01:30 +08:00
|
|
|
|
"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";
|
2025-12-28 22:05:37 +08:00
|
|
|
|
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";
|
2025-12-28 10:01:30 +08:00
|
|
|
|
|
|
|
|
|
|
type MindMapInstance = {
|
|
|
|
|
|
execCommand: (...args: unknown[]) => void;
|
|
|
|
|
|
destroy: () => void;
|
|
|
|
|
|
setData: (data: unknown) => void;
|
|
|
|
|
|
getData: (withConfig?: boolean) => unknown;
|
|
|
|
|
|
on?: (event: string, handler: (...args: unknown[]) => void) => void;
|
2025-12-28 22:05:37 +08:00
|
|
|
|
emit?: (event: string, ...args: unknown[]) => void;
|
|
|
|
|
|
off?: (event: string, handler: (...args: unknown[]) => void) => void;
|
|
|
|
|
|
setMode?: (mode: string) => void;
|
2025-12-28 10:01:30 +08:00
|
|
|
|
command: { clearHistory: () => void };
|
2025-12-28 22:05:37 +08:00
|
|
|
|
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 };
|
2025-12-28 10:01:30 +08:00
|
|
|
|
painter?: { startPainter: () => void };
|
|
|
|
|
|
doExport?: { export: (type: string, isDownload?: boolean, name?: string) => Promise<unknown> };
|
2025-12-28 22:05:37 +08:00
|
|
|
|
width: number;
|
|
|
|
|
|
height: number;
|
|
|
|
|
|
miniMap?: unknown;
|
|
|
|
|
|
getThemeConfig: (key: string) => unknown;
|
|
|
|
|
|
setThemeConfig: (config: unknown) => void;
|
|
|
|
|
|
setTheme: (theme: string) => void;
|
|
|
|
|
|
setLayout: (layout: string) => void;
|
2025-12-28 10:01:30 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const applyToActiveNodes = (
|
|
|
|
|
|
mindmap: MindMapInstance | null,
|
|
|
|
|
|
handler: (node: unknown) => void,
|
|
|
|
|
|
) => {
|
2025-12-28 22:05:37 +08:00
|
|
|
|
if (!mindmap) {
|
|
|
|
|
|
window.alert("思维导图尚未初始化");
|
2025-12-28 10:01:30 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-12-28 22:05:37 +08:00
|
|
|
|
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);
|
2025-12-28 10:01:30 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const MindmapBlockView = ({
|
|
|
|
|
|
block,
|
|
|
|
|
|
editor,
|
|
|
|
|
|
fullscreen = false,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
block: Block<CustomBlockSchema, "mindmap">;
|
|
|
|
|
|
editor: BlockNoteEditor<CustomBlockSchema>;
|
|
|
|
|
|
fullscreen?: boolean;
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
|
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
|
|
|
|
const [mindmap, setMindmap] = useState<MindMapInstance | null>(null);
|
|
|
|
|
|
const [canBack, setCanBack] = useState(false);
|
|
|
|
|
|
const [canForward, setCanForward] = useState(false);
|
2025-12-28 22:05:37 +08:00
|
|
|
|
const [activeNodes, setActiveNodes] = useState<unknown[]>([]);
|
|
|
|
|
|
const activeCount = activeNodes.length;
|
2025-12-28 10:01:30 +08:00
|
|
|
|
const [painterMode, setPainterMode] = useState(false);
|
2025-12-28 22:05:37 +08:00
|
|
|
|
const [showMiniMap, setShowMiniMap] = useState(false);
|
|
|
|
|
|
const [activeSidebar, setActiveSidebar] = useState<SidebarPanel | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
const getRootNode = useCallback((mm?: MindMapInstance | null) => {
|
|
|
|
|
|
const instance = mm ?? mindmap;
|
|
|
|
|
|
return instance?.renderer?.root ?? instance?.renderer?.renderTree?._node ?? null;
|
|
|
|
|
|
}, [mindmap]);
|
|
|
|
|
|
|
2025-12-28 10:01:30 +08:00
|
|
|
|
const autosaveKey = useMemo(() => `${STORAGE_PREFIX}${block.id}`, [block.id]);
|
|
|
|
|
|
const initialDataRef = useRef<unknown>(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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const persistData = useCallback(
|
|
|
|
|
|
(data: unknown) => {
|
|
|
|
|
|
if (!editor) return;
|
|
|
|
|
|
window.localStorage.setItem(autosaveKey, JSON.stringify(data));
|
|
|
|
|
|
editor.updateBlock(block, { props: { ...block.props, data } });
|
|
|
|
|
|
},
|
|
|
|
|
|
[autosaveKey, block, editor],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const debouncedPersist = useDebouncedCallback((data: unknown) => {
|
|
|
|
|
|
persistData(data);
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
|
2025-12-28 22:05:37 +08:00
|
|
|
|
// 初始选中根节点,后续不强制抢焦点,允许用户自由选择
|
|
|
|
|
|
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]);
|
|
|
|
|
|
|
2025-12-28 10:01:30 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
let destroyed = false;
|
|
|
|
|
|
(async () => {
|
|
|
|
|
|
if (!containerRef.current) return;
|
|
|
|
|
|
const [
|
|
|
|
|
|
{ default: MindMap },
|
|
|
|
|
|
{ default: Painter },
|
|
|
|
|
|
{ default: AssociativeLine },
|
|
|
|
|
|
{ default: OuterFrame },
|
|
|
|
|
|
{ default: Exporter },
|
|
|
|
|
|
{ default: Formula },
|
|
|
|
|
|
{ default: FormulaStyle },
|
|
|
|
|
|
{ default: RichText },
|
2025-12-28 22:05:37 +08:00
|
|
|
|
{ default: MiniMapPlugin },
|
|
|
|
|
|
{ default: Select },
|
|
|
|
|
|
{ default: Drag },
|
|
|
|
|
|
{ default: KeyboardNavigation },
|
2025-12-28 10:01:30 +08:00
|
|
|
|
] = 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/FormulaStyle.js"),
|
|
|
|
|
|
import("simple-mind-map/src/plugins/RichText.js"),
|
2025-12-28 22:05:37 +08:00
|
|
|
|
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"),
|
2025-12-28 10:01:30 +08:00
|
|
|
|
]);
|
|
|
|
|
|
|
2025-12-28 22:05:37 +08:00
|
|
|
|
const plugins = [
|
2025-12-28 10:01:30 +08:00
|
|
|
|
{ name: "Painter", plugin: Painter },
|
|
|
|
|
|
{ name: "AssociativeLine", plugin: AssociativeLine },
|
|
|
|
|
|
{ name: "OuterFrame", plugin: OuterFrame },
|
|
|
|
|
|
{ name: "Export", plugin: Exporter },
|
|
|
|
|
|
{ name: "Formula", plugin: Formula },
|
|
|
|
|
|
{ name: "FormulaStyle", plugin: FormulaStyle },
|
|
|
|
|
|
{ name: "RichText", plugin: RichText },
|
2025-12-28 22:05:37 +08:00
|
|
|
|
{ name: "MiniMap", plugin: MiniMapPlugin },
|
|
|
|
|
|
{ name: "Select", plugin: Select },
|
|
|
|
|
|
{ name: "Drag", plugin: Drag },
|
|
|
|
|
|
{ name: "KeyboardNavigation", plugin: KeyboardNavigation },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
plugins.forEach(({ name, plugin }) => {
|
2025-12-28 10:01:30 +08:00
|
|
|
|
if (!plugin) {
|
|
|
|
|
|
console.warn(`思维导图插件加载失败:${name}`);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// @ts-expect-error 第三方库缺少类型定义
|
|
|
|
|
|
if (MindMap.hasPlugin(plugin) === -1) {
|
2025-12-28 22:05:37 +08:00
|
|
|
|
console.log(`注册插件: ${name}`);
|
2025-12-28 10:01:30 +08:00
|
|
|
|
// @ts-expect-error 第三方库缺少类型定义
|
|
|
|
|
|
MindMap.usePlugin(plugin);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const instance = new MindMap({
|
|
|
|
|
|
el: containerRef.current,
|
|
|
|
|
|
data: initialDataRef.current,
|
|
|
|
|
|
theme: "classic",
|
|
|
|
|
|
layout: "logicalStructure",
|
|
|
|
|
|
mousewheelAction: "zoom",
|
|
|
|
|
|
enableFreeDrag: true,
|
|
|
|
|
|
enableCtrlKeyNodeSelection: true,
|
|
|
|
|
|
fit: true,
|
2025-12-28 22:05:37 +08:00
|
|
|
|
useLeftKeySelectionRightKeyDrag: true,
|
2025-12-28 10:01:30 +08:00
|
|
|
|
}) as MindMapInstance;
|
2025-12-28 22:05:37 +08:00
|
|
|
|
instance.setMode?.("edit");
|
2025-12-28 10:01:30 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
2025-12-28 22:05:37 +08:00
|
|
|
|
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 第三方库节点对象
|
|
|
|
|
|
node.active();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 兜底:手动维护激活列表
|
|
|
|
|
|
// @ts-expect-error 第三方库缺少类型定义
|
|
|
|
|
|
instance.renderer?.clearActiveNodeList?.();
|
|
|
|
|
|
// @ts-expect-error
|
|
|
|
|
|
instance.renderer?.addNodeToActiveList?.(node, true);
|
|
|
|
|
|
// @ts-expect-error
|
|
|
|
|
|
instance.renderer?.emitNodeActiveEvent?.(node);
|
|
|
|
|
|
}
|
|
|
|
|
|
const list = instance.renderer?.activeNodeList ?? [];
|
|
|
|
|
|
setActiveNodes(list.length === 0 && node ? [node] : (list as unknown[]));
|
2025-12-28 10:01:30 +08:00
|
|
|
|
});
|
|
|
|
|
|
instance.on?.("painter_start", () => setPainterMode(true));
|
|
|
|
|
|
instance.on?.("painter_end", () => setPainterMode(false));
|
|
|
|
|
|
instance.on?.("data_change", (data: unknown) => {
|
|
|
|
|
|
debouncedPersist(data);
|
|
|
|
|
|
});
|
2025-12-28 22:05:37 +08:00
|
|
|
|
const rootNode = getRootNode(instance);
|
2025-12-28 10:01:30 +08:00
|
|
|
|
if (rootNode) {
|
2025-12-28 22:05:37 +08:00
|
|
|
|
setActiveNodes([rootNode]);
|
2025-12-28 10:01:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
};
|
2025-12-28 22:05:37 +08:00
|
|
|
|
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();
|
|
|
|
|
|
if (mm) {
|
|
|
|
|
|
window.setTimeout(() => mm.execCommand("ADD_GENERALIZATION"), 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
const handleAssociativeLine = () => {
|
|
|
|
|
|
const mm = ensureActiveBefore();
|
|
|
|
|
|
if (mm) {
|
|
|
|
|
|
window.setTimeout(() => mm.execCommand("ADD_ASSOCIATIVE_LINE"), 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
const handleOuterFrame = () => {
|
|
|
|
|
|
const mm = ensureActiveBefore();
|
|
|
|
|
|
if (mm) {
|
|
|
|
|
|
window.setTimeout(() => mm.execCommand("ADD_OUTER_FRAME"), 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2025-12-28 10:01:30 +08:00
|
|
|
|
|
|
|
|
|
|
const handleImage = () => {
|
|
|
|
|
|
const url = createSimplePrompt("请输入图片链接");
|
|
|
|
|
|
if (!url) return;
|
|
|
|
|
|
applyToActiveNodes(mindmap, (node) => {
|
|
|
|
|
|
mindmap?.execCommand("SET_NODE_IMAGE", node, { url, width: 260, height: 200, title: "" });
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleIcon = () => {
|
|
|
|
|
|
const value = createSimplePrompt("请输入内置图标键(示例:priority_1 或 progress_2)", "priority_1");
|
|
|
|
|
|
if (!value) return;
|
|
|
|
|
|
applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_ICON", node, [value]));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleLink = () => {
|
|
|
|
|
|
const href = createSimplePrompt("请输入超链接");
|
|
|
|
|
|
if (!href) return;
|
|
|
|
|
|
applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_HYPERLINK", node, href, href));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleNote = () => {
|
|
|
|
|
|
const note = createSimplePrompt("请输入备注内容");
|
|
|
|
|
|
if (!note) return;
|
|
|
|
|
|
applyToActiveNodes(mindmap, (node) => mindmap?.execCommand("SET_NODE_NOTE", node, note));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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 = () => {
|
|
|
|
|
|
const formula = createSimplePrompt("输入 LaTeX 公式(示例:a^2+b^2=c^2)", "a^2+b^2=c^2");
|
|
|
|
|
|
if (!formula) return;
|
|
|
|
|
|
mindmap?.execCommand("INSERT_FORMULA", formula, mindmap?.renderer?.activeNodeList ?? []);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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 = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
|
const file = event.target.files?.[0];
|
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const data = JSON.parse(String(reader.result));
|
|
|
|
|
|
mindmap?.setData(data);
|
|
|
|
|
|
mindmap?.command.clearHistory();
|
|
|
|
|
|
debouncedPersist(data);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(error);
|
|
|
|
|
|
window.alert("导入失败:文件格式错误");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
event.target.value = "";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.readAsText(file, "utf-8");
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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 handleExportPng = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await mindmap?.doExport?.export("png", true, "mindmap");
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(error);
|
|
|
|
|
|
window.alert("导出 PNG 失败,请稍后再试");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSaveAs = () => handleExportJson();
|
|
|
|
|
|
|
|
|
|
|
|
const toolbarProps = {
|
|
|
|
|
|
canBack,
|
|
|
|
|
|
canForward,
|
|
|
|
|
|
activeCount,
|
|
|
|
|
|
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,
|
|
|
|
|
|
onExportJson: handleExportJson,
|
|
|
|
|
|
onExportPng: handleExportPng,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (fullscreen) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="relative h-screen w-screen overflow-hidden bg-white">
|
|
|
|
|
|
<div className="fixed left-1/2 top-4 z-20 flex w-fit -translate-x-1/2 transform items-center justify-center gap-4">
|
|
|
|
|
|
<MindmapToolbar {...toolbarProps} />
|
|
|
|
|
|
</div>
|
2025-12-28 22:05:37 +08:00
|
|
|
|
<div className="relative h-full w-full">
|
2025-12-28 10:01:30 +08:00
|
|
|
|
<div ref={containerRef} className="h-full w-full" />
|
2025-12-28 22:05:37 +08:00
|
|
|
|
|
|
|
|
|
|
<MindmapSidebarTrigger
|
|
|
|
|
|
activeSidebar={activeSidebar}
|
|
|
|
|
|
onSelect={setActiveSidebar}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* @ts-expect-error mindmap type */}
|
|
|
|
|
|
<MindmapSidebar
|
|
|
|
|
|
mindmap={mindmap}
|
|
|
|
|
|
activeNodes={activeNodes} // @ts-expect-error type
|
|
|
|
|
|
activeTab={activeSidebar}
|
|
|
|
|
|
onClose={() => setActiveSidebar(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<MindmapNavigator
|
|
|
|
|
|
mindmap={mindmap}
|
|
|
|
|
|
fullscreen={fullscreen}
|
|
|
|
|
|
onToggleMiniMap={() => setShowMiniMap(!showMiniMap)}
|
|
|
|
|
|
miniMapOpen={showMiniMap}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<MindmapCount mindmap={mindmap} />
|
|
|
|
|
|
{/* @ts-expect-error mindmap type */}
|
|
|
|
|
|
<MindmapMiniMap mindmap={mindmap} show={showMiniMap} />
|
2025-12-28 10:01:30 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="not-prose my-4 w-full overflow-hidden rounded-xl border border-gray-200 bg-[#f8fafc] shadow-sm">
|
|
|
|
|
|
<div className="flex flex-col gap-3 border-b border-gray-100 bg-white px-4 py-3">
|
|
|
|
|
|
<div className="w-full">
|
|
|
|
|
|
<MindmapToolbar {...toolbarProps} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="relative h-[520px] w-full overflow-hidden bg-white">
|
|
|
|
|
|
<div ref={containerRef} className="h-full w-full" />
|
2025-12-28 22:05:37 +08:00
|
|
|
|
|
|
|
|
|
|
<MindmapSidebarTrigger
|
|
|
|
|
|
activeSidebar={activeSidebar}
|
|
|
|
|
|
onSelect={setActiveSidebar}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{/* @ts-expect-error mindmap type */}
|
|
|
|
|
|
<MindmapSidebar
|
|
|
|
|
|
mindmap={mindmap}
|
|
|
|
|
|
activeNodes={activeNodes} // @ts-expect-error type
|
|
|
|
|
|
activeTab={activeSidebar}
|
|
|
|
|
|
onClose={() => setActiveSidebar(null)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<MindmapNavigator
|
|
|
|
|
|
mindmap={mindmap}
|
|
|
|
|
|
fullscreen={fullscreen}
|
|
|
|
|
|
onToggleMiniMap={() => setShowMiniMap(!showMiniMap)}
|
|
|
|
|
|
miniMapOpen={showMiniMap}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<MindmapCount mindmap={mindmap} />
|
|
|
|
|
|
{/* @ts-expect-error mindmap type */}
|
|
|
|
|
|
<MindmapMiniMap mindmap={mindmap} show={showMiniMap} />
|
2025-12-28 10:01:30 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export { MindmapBlockView };
|
|
|
|
|
|
|
|
|
|
|
|
export const mindmapBlock = createReactBlockSpec(
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "mindmap",
|
|
|
|
|
|
propSchema: {
|
|
|
|
|
|
data: { default: defaultMindmapData },
|
|
|
|
|
|
},
|
|
|
|
|
|
content: "none",
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
render: (props) => <MindmapBlockView {...props} />,
|
|
|
|
|
|
},
|
2025-12-28 22:05:37 +08:00
|
|
|
|
);
|