双向删除同步
This commit is contained in:
@@ -17,9 +17,15 @@ 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 { nodeIconList } from "simple-mind-map/src/svg/icons.js";
|
||||
import { emitAssetsChanged } from "@/lib/events";
|
||||
|
||||
// 避免 SSR/Node 环境提前评估 simple-mind-map 内部依赖 document
|
||||
// @ts-expect-error 第三方库缺少类型定义
|
||||
import { mergerIconList } from "simple-mind-map/src/utils/index.js";
|
||||
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> =>
|
||||
@@ -54,7 +60,19 @@ type MindMapInstance = {
|
||||
setLayout: (layout: string) => void;
|
||||
};
|
||||
|
||||
const defaultMindmapData = {
|
||||
type MindMapNode = {
|
||||
getStyle?: (key: string, inherit?: boolean) => unknown;
|
||||
getData?: (key: string) => unknown;
|
||||
nodeData?: { data?: Record<string, unknown> };
|
||||
data?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type MindMapData = {
|
||||
data: Record<string, unknown>;
|
||||
children?: unknown[];
|
||||
};
|
||||
|
||||
export const defaultMindmapData = {
|
||||
data: { text: "中心主题" },
|
||||
children: [],
|
||||
};
|
||||
@@ -77,6 +95,39 @@ const createSimplePrompt = (title: string, placeholder = "") => {
|
||||
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,
|
||||
@@ -134,7 +185,6 @@ const MindmapBlockView = ({
|
||||
const [canBack, setCanBack] = useState(false);
|
||||
const [canForward, setCanForward] = useState(false);
|
||||
const [activeNodes, setActiveNodes] = useState<unknown[]>([]);
|
||||
const activeCount = activeNodes.length;
|
||||
const [painterMode, setPainterMode] = useState(false);
|
||||
const [showMiniMap, setShowMiniMap] = useState(false);
|
||||
const [activeSidebar, setActiveSidebar] = useState<SidebarPanel | null>(null);
|
||||
@@ -146,7 +196,19 @@ const MindmapBlockView = ({
|
||||
return instance?.renderer?.root ?? instance?.renderer?.renderTree?._node ?? null;
|
||||
}, [mindmap]);
|
||||
|
||||
const autosaveKey = useMemo(() => `${STORAGE_PREFIX}${block.id}`, [block.id]);
|
||||
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<unknown>(null);
|
||||
if (initialDataRef.current === null) {
|
||||
const cached = typeof window !== "undefined" ? window.localStorage.getItem(autosaveKey) : null;
|
||||
@@ -154,26 +216,83 @@ const MindmapBlockView = ({
|
||||
try {
|
||||
initialDataRef.current = JSON.parse(cached);
|
||||
} catch {
|
||||
initialDataRef.current = block.props.data ?? defaultMindmapData;
|
||||
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 } });
|
||||
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, editor],
|
||||
[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;
|
||||
@@ -204,6 +323,16 @@ const MindmapBlockView = ({
|
||||
{ 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"),
|
||||
@@ -217,6 +346,16 @@ const MindmapBlockView = ({
|
||||
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 = [
|
||||
@@ -231,6 +370,16 @@ const MindmapBlockView = ({
|
||||
{ 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 }) => {
|
||||
@@ -241,13 +390,23 @@ const MindmapBlockView = ({
|
||||
// @ts-expect-error 第三方库缺少类型定义
|
||||
if (MindMap.hasPlugin(plugin) === -1) {
|
||||
console.log(`注册插件: ${name}`);
|
||||
// @ts-expect-error 第三方库缺少类型定义
|
||||
MindMap.usePlugin(plugin);
|
||||
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: containerRef.current,
|
||||
el: hostEl,
|
||||
data: initialDataRef.current,
|
||||
theme: "classic",
|
||||
layout: "logicalStructure",
|
||||
@@ -262,6 +421,17 @@ const MindmapBlockView = ({
|
||||
...(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") {
|
||||
@@ -284,15 +454,15 @@ const MindmapBlockView = ({
|
||||
// 确保点击即可激活(调用节点自身的 active 逻辑,保持和官方一致)
|
||||
// @ts-expect-error 第三方库节点对象
|
||||
if (typeof node?.active === "function") {
|
||||
// @ts-expect-error 第三方库节点对象
|
||||
// @ts-expect-error simple-mind-map 节点对象缺少类型
|
||||
node.active();
|
||||
} else {
|
||||
// 兜底:手动维护激活列表
|
||||
// @ts-expect-error 第三方库缺少类型定义
|
||||
// @ts-expect-error simple-mind-map 渲染器缺少类型
|
||||
instance.renderer?.clearActiveNodeList?.();
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error simple-mind-map 渲染器缺少类型
|
||||
instance.renderer?.addNodeToActiveList?.(node, true);
|
||||
// @ts-expect-error
|
||||
// @ts-expect-error simple-mind-map 渲染器缺少类型
|
||||
instance.renderer?.emitNodeActiveEvent?.(node);
|
||||
}
|
||||
const list = instance.renderer?.activeNodeList ?? [];
|
||||
@@ -374,9 +544,7 @@ const MindmapBlockView = ({
|
||||
// 图片预览(双击节点图片)
|
||||
const [showImageViewer, setShowImageViewer] = useState(false);
|
||||
const [viewerSrc, setViewerSrc] = useState("");
|
||||
const [viewerNode, setViewerNode] = useState<any>(null);
|
||||
const [viewerMeta, setViewerMeta] = useState<{ title?: string; width?: number; height?: number }>({});
|
||||
const [hasActiveImg, setHasActiveImg] = useState(false);
|
||||
const imgToolbarRef = useRef<HTMLDivElement | null>(null);
|
||||
const [imgToolbarState, setImgToolbarState] = useState({
|
||||
show: false,
|
||||
@@ -387,9 +555,9 @@ const MindmapBlockView = ({
|
||||
const imgToolbarHover = useRef(false);
|
||||
|
||||
const handleImage = () => {
|
||||
const first = mindmap?.renderer?.activeNodeList?.[0] as any;
|
||||
const first = mindmap?.renderer?.activeNodeList?.[0] as MindMapNode | undefined;
|
||||
if (first?.getStyle) {
|
||||
const placement = first.getStyle("imgPlacement", false) as string;
|
||||
const placement = first.getStyle("imgPlacement", false) as string;
|
||||
if (placement) setImagePosition(placement);
|
||||
}
|
||||
setShowImageModal(true);
|
||||
@@ -398,7 +566,7 @@ const MindmapBlockView = ({
|
||||
// 预览:监听 mindmap 事件
|
||||
useEffect(() => {
|
||||
if (!mindmap) return;
|
||||
const handler = (node: any, e: any) => {
|
||||
const handler = (node: MindMapNode, e?: Event) => {
|
||||
e?.stopPropagation?.();
|
||||
e?.preventDefault?.();
|
||||
const src =
|
||||
@@ -407,7 +575,6 @@ const MindmapBlockView = ({
|
||||
node?.data?.image;
|
||||
if (src) {
|
||||
setViewerSrc(src);
|
||||
setViewerNode(node);
|
||||
const size = node?.getData?.("imageSize") || {};
|
||||
const title = node?.getData?.("imageTitle") || "";
|
||||
setViewerMeta({
|
||||
@@ -419,14 +586,14 @@ const MindmapBlockView = ({
|
||||
}
|
||||
};
|
||||
const onActive = () => {
|
||||
const list = mindmap.renderer?.activeNodeList || [];
|
||||
const has = list.some((n: any) => !!n?.getData?.("image"));
|
||||
setHasActiveImg(has);
|
||||
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: any, svgImg: any, evt: any) => {
|
||||
const bbox = evt?.target?.getBoundingClientRect?.();
|
||||
const placement = (node?.getStyle?.("imgPlacement") || "top") as any;
|
||||
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({
|
||||
@@ -460,11 +627,15 @@ const MindmapBlockView = ({
|
||||
const setPlacement = (p: "top" | "bottom" | "left" | "right") => {
|
||||
setImagePosition(p);
|
||||
applyToActiveNodes(mindmap, (node) =>
|
||||
mindmap?.execCommand?.("SET_NODE_STYLES", node, { imgPlacement: p }),
|
||||
mindmap?.execCommand?.("SET_NODE_STYLES", node, { imgPlacement: p }),
|
||||
);
|
||||
setImgToolbarState((s) => ({ ...s, placement: p }));
|
||||
};
|
||||
const btn = (p: typeof placement, Icon: any, title: string) => (
|
||||
const btn = (
|
||||
p: typeof placement,
|
||||
Icon: React.ComponentType<{ className?: string }>,
|
||||
title: string,
|
||||
) => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
@@ -571,24 +742,64 @@ const MindmapBlockView = ({
|
||||
window.alert("AI 能力占位:后续接入大模型生成/优化节点内容。");
|
||||
};
|
||||
|
||||
const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleImport = async (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));
|
||||
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);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.alert("导入失败:文件格式错误");
|
||||
} finally {
|
||||
event.target.value = "";
|
||||
return;
|
||||
}
|
||||
};
|
||||
reader.readAsText(file, "utf-8");
|
||||
|
||||
// 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 = () => {
|
||||
@@ -618,21 +829,49 @@ const MindmapBlockView = ({
|
||||
downloadJson(data, "mindmap");
|
||||
};
|
||||
|
||||
const handleExportPng = async () => {
|
||||
const handleExport = async (type: string, name = "mindmap") => {
|
||||
try {
|
||||
await mindmap?.doExport?.export("png", true, "mindmap");
|
||||
await mindmap?.doExport?.export(type, true, name);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
window.alert("导出 PNG 失败,请稍后再试");
|
||||
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,
|
||||
activeCount,
|
||||
painterMode,
|
||||
onUndo: handleUndo,
|
||||
onRedo: handleRedo,
|
||||
@@ -655,8 +894,14 @@ const MindmapBlockView = ({
|
||||
onNew: handleNew,
|
||||
onOpenDirectory: handleOpenDirectory,
|
||||
onSaveAs: handleSaveAs,
|
||||
onDeleteMindmap: handleDeleteMindmap,
|
||||
onExportJson: handleExportJson,
|
||||
onExportPng: handleExportPng,
|
||||
onExportSvg: handleExportSvg,
|
||||
onExportPdf: handleExportPdf,
|
||||
onExportMd: handleExportMd,
|
||||
onExportTxt: handleExportTxt,
|
||||
onExportXmind: handleExportXmind,
|
||||
fileInputRef,
|
||||
};
|
||||
|
||||
@@ -708,9 +953,10 @@ const MindmapBlockView = ({
|
||||
关闭
|
||||
</button>
|
||||
<div className="mb-2 flex items-center justify-between gap-4 px-1 text-xs text-gray-200">
|
||||
<span className="truncate">{viewerMeta.title || "图片预览"}</span>
|
||||
<span className="truncate">{viewerMeta.title || "图片预览"}</span>
|
||||
<span className="shrink-0">{viewerMeta.width && viewerMeta.height ? `${viewerMeta.width} × ${viewerMeta.height}px` : ""}</span>
|
||||
</div>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={viewerSrc}
|
||||
alt={viewerMeta.title || "预览"}
|
||||
@@ -884,7 +1130,11 @@ const MindmapBlockView = ({
|
||||
<MindmapToolbar {...toolbarProps} />
|
||||
</div>
|
||||
<div className="relative h-full w-full">
|
||||
<div ref={containerRef} className="h-full w-full" />
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-full w-full"
|
||||
contentEditable={false}
|
||||
/>
|
||||
|
||||
<MindmapSidebarTrigger
|
||||
activeSidebar={activeSidebar}
|
||||
@@ -923,7 +1173,11 @@ const MindmapBlockView = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-[520px] w-full overflow-hidden bg-white">
|
||||
<div ref={containerRef} className="h-full w-full" />
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="h-full w-full"
|
||||
contentEditable={false}
|
||||
/>
|
||||
|
||||
<MindmapSidebarTrigger
|
||||
activeSidebar={activeSidebar}
|
||||
@@ -960,6 +1214,7 @@ export const mindmapBlock = createReactBlockSpec(
|
||||
{
|
||||
type: "mindmap",
|
||||
propSchema: {
|
||||
docId: { default: "" },
|
||||
data: { default: defaultMindmapData },
|
||||
},
|
||||
content: "none",
|
||||
|
||||
Reference in New Issue
Block a user