0.1.05 修复放大缩小
This commit is contained in:
@@ -105,6 +105,44 @@ export const defaultMindmapData = {
|
||||
children: [],
|
||||
};
|
||||
|
||||
// simple-mind-map 的 RichText 插件初始化会对节点文本做 HTML 转义;
|
||||
// 若数据里出现 `data.text === undefined`,会在内部调用 `undefined.replace(...)` 直接崩溃。
|
||||
// 这里在“保存/恢复”链路上做一次兜底归一化,确保跨视图重建实例时不会白屏。
|
||||
const normalizeMindmapData = (input: unknown): unknown => {
|
||||
if (!input || typeof input !== "object") return defaultMindmapData;
|
||||
const root = (input as { root?: unknown }).root ?? input;
|
||||
|
||||
const walk = (node: any) => {
|
||||
if (!node || typeof node !== "object") return;
|
||||
if (!node.data || typeof node.data !== "object") node.data = {};
|
||||
const rawText = (node.data as any).text;
|
||||
(node.data as any).text = typeof rawText === "string" ? rawText : String(rawText ?? "");
|
||||
// 概要(generalization)数据结构里也存在 text 字段,缺失会导致 RichText 初始化崩溃
|
||||
const gen = (node.data as any).generalization;
|
||||
const fixGen = (g: any) => {
|
||||
if (!g || typeof g !== "object") return;
|
||||
const t = (g as any).text;
|
||||
(g as any).text = typeof t === "string" ? t : String(t ?? "");
|
||||
};
|
||||
if (Array.isArray(gen)) gen.forEach(fixGen);
|
||||
else fixGen(gen);
|
||||
if (Array.isArray(node.children)) node.children.forEach(walk);
|
||||
};
|
||||
|
||||
walk(root);
|
||||
return input;
|
||||
};
|
||||
|
||||
// 持久化/初始化统一使用“根节点对象”作为数据载体,避免把包含额外字段的 wrapper 误传给 simple-mind-map
|
||||
// 从而触发 RichText 对 wrapper.data 的处理(wrapper.data.text 可能不存在 → htmlEscape 崩溃)。
|
||||
const canonicalizeMindmapData = (input: unknown): MindMapData => {
|
||||
const normalized = (normalizeMindmapData(input) ?? defaultMindmapData) as any;
|
||||
const root = (normalized && typeof normalized === "object" && "root" in normalized)
|
||||
? (normalized as any).root
|
||||
: normalized;
|
||||
return (normalizeMindmapData(root) ?? defaultMindmapData) as MindMapData;
|
||||
};
|
||||
|
||||
const STORAGE_PREFIX = "wolai-mindmap-autosave-";
|
||||
|
||||
function downloadJson(data: unknown, name: string) {
|
||||
@@ -310,12 +348,12 @@ const MindmapBlockView = ({
|
||||
const cached = typeof window !== "undefined" ? window.localStorage.getItem(autosaveKey) : null;
|
||||
if (cached) {
|
||||
try {
|
||||
initialDataRef.current = JSON.parse(cached);
|
||||
initialDataRef.current = canonicalizeMindmapData(JSON.parse(cached));
|
||||
} catch {
|
||||
initialDataRef.current = block.props.data ?? defaultMindmapData;
|
||||
initialDataRef.current = canonicalizeMindmapData(block.props.data ?? defaultMindmapData);
|
||||
}
|
||||
} else {
|
||||
initialDataRef.current = block.props.data ?? defaultMindmapData;
|
||||
initialDataRef.current = canonicalizeMindmapData(block.props.data ?? defaultMindmapData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,11 +370,11 @@ const MindmapBlockView = ({
|
||||
if (!data || cancelled) return;
|
||||
// 若用户已在本地做过编辑,则不再用远端数据覆盖,避免“插入节点后又被重置”
|
||||
if (hasLocalEditsRef.current) return;
|
||||
initialDataRef.current = data;
|
||||
initialDataRef.current = canonicalizeMindmapData(data);
|
||||
if (mindmap) {
|
||||
applyingRemoteRef.current = true;
|
||||
try {
|
||||
mindmap.setData(data);
|
||||
mindmap.setData(initialDataRef.current);
|
||||
mindmap.command.clearHistory();
|
||||
} finally {
|
||||
window.setTimeout(() => {
|
||||
@@ -570,14 +608,18 @@ const MindmapBlockView = ({
|
||||
const persistData = useCallback(
|
||||
(data: unknown) => {
|
||||
if (!editor) return;
|
||||
window.localStorage.setItem(autosaveKey, JSON.stringify(data));
|
||||
editor.updateBlock(block, { props: { ...block.props, data } });
|
||||
// 关键:全屏/非全屏切换会重建 mindmap 实例,初始化数据必须跟随最新编辑
|
||||
// 否则切换视图时会用旧数据,表现为“看起来没保存”
|
||||
const safe = canonicalizeMindmapData(data);
|
||||
initialDataRef.current = safe;
|
||||
window.localStorage.setItem(autosaveKey, JSON.stringify(safe));
|
||||
editor.updateBlock(block, { props: { ...block.props, data: safe } });
|
||||
if (docId) {
|
||||
// 同步到本地文件 + Supabase(弱依赖)
|
||||
fetch(`/api/mindmap/${docId}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ data }),
|
||||
body: JSON.stringify({ data: safe }),
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp.ok) {
|
||||
@@ -604,7 +646,9 @@ const MindmapBlockView = ({
|
||||
useEffect(() => {
|
||||
if (!docId || !mindmap || initialSyncDone.current) return;
|
||||
initialSyncDone.current = true;
|
||||
const data = mindmap.getData?.() ?? initialDataRef.current ?? defaultMindmapData;
|
||||
const data = canonicalizeMindmapData(
|
||||
mindmap.getData?.(true) ?? mindmap.getData?.() ?? initialDataRef.current ?? defaultMindmapData,
|
||||
);
|
||||
(async () => {
|
||||
try {
|
||||
const resp = await fetch(`/api/mindmap/${docId}`, {
|
||||
@@ -662,8 +706,18 @@ const MindmapBlockView = ({
|
||||
let destroyed = false;
|
||||
let createdInstance: MindMapInstance | null = null;
|
||||
(async () => {
|
||||
if (!containerRef.current) return;
|
||||
const hostContainer = containerRef.current;
|
||||
// 进入/退出全屏会切换渲染树,ref 在某些时序下可能短暂为 null。
|
||||
// 若这里直接 return,会导致新实例永远不创建,表现为“切换视图后空白/像没保存”。
|
||||
const waitForContainer = async (): Promise<HTMLDivElement | null> => {
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
if (destroyed) return null;
|
||||
if (containerRef.current) return containerRef.current;
|
||||
await new Promise((r) => window.setTimeout(r, 50));
|
||||
}
|
||||
return containerRef.current;
|
||||
};
|
||||
const hostContainer = await waitForContainer();
|
||||
if (!hostContainer) return;
|
||||
const [
|
||||
{ default: MindMap },
|
||||
{ default: Painter },
|
||||
@@ -819,6 +873,29 @@ const MindmapBlockView = ({
|
||||
hostEl.innerHTML = "";
|
||||
}
|
||||
|
||||
// 全屏/非全屏切换会重建实例:创建新实例前优先从本地缓存读取最新数据
|
||||
// 以避免“全屏里编辑 → 退出全屏后内容消失 / 反之亦然”。
|
||||
let dataForInitSource = "unknown";
|
||||
const rawForInit = (() => {
|
||||
if (typeof window === "undefined") {
|
||||
dataForInitSource = "ssr-fallback";
|
||||
return block.props.data ?? initialDataRef.current ?? defaultMindmapData;
|
||||
}
|
||||
try {
|
||||
const cached = window.localStorage.getItem(autosaveKey);
|
||||
if (cached) {
|
||||
dataForInitSource = "localStorage";
|
||||
return JSON.parse(cached);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
dataForInitSource = block.props.data ? "block.props.data" : "initialDataRef";
|
||||
return block.props.data ?? initialDataRef.current ?? defaultMindmapData;
|
||||
})();
|
||||
const dataForInit = canonicalizeMindmapData(rawForInit);
|
||||
initialDataRef.current = dataForInit;
|
||||
|
||||
type MindMapConstructor = new (options: {
|
||||
el: HTMLElement;
|
||||
data: unknown;
|
||||
@@ -836,7 +913,7 @@ const MindmapBlockView = ({
|
||||
|
||||
const instance = new MindMapCtor({
|
||||
el: hostEl,
|
||||
data: initialDataRef.current,
|
||||
data: dataForInit,
|
||||
theme: "classic",
|
||||
layout: "logicalStructure",
|
||||
mousewheelAction: "zoom",
|
||||
@@ -964,11 +1041,16 @@ const MindmapBlockView = ({
|
||||
});
|
||||
instance.on?.("painter_start", () => setPainterMode(true));
|
||||
instance.on?.("painter_end", () => setPainterMode(false));
|
||||
instance.on?.("data_change", (data: unknown) => {
|
||||
instance.on?.("data_change", () => {
|
||||
if (!applyingRemoteRef.current) {
|
||||
hasLocalEditsRef.current = true;
|
||||
}
|
||||
debouncedPersist(data);
|
||||
// data_change 的参数在部分场景下可能不是“可直接序列化的完整数据”
|
||||
//(例如 children 非数组、text 缺失等),全屏切换重建时会触发 RichText 初始化崩溃。
|
||||
// 这里统一改为从实例读取快照,确保保存/恢复的数据结构稳定。
|
||||
const snapshot =
|
||||
instance.getData?.(true) ?? instance.getData?.() ?? null;
|
||||
if (snapshot) debouncedPersist(snapshot);
|
||||
});
|
||||
const renderer = instance.renderer;
|
||||
const rootNode = getRootNode(instance);
|
||||
@@ -1002,13 +1084,15 @@ const MindmapBlockView = ({
|
||||
const data =
|
||||
createdInstance.getData?.(true) ?? createdInstance.getData?.();
|
||||
if (data) {
|
||||
const safe = canonicalizeMindmapData(data);
|
||||
initialDataRef.current = safe;
|
||||
try {
|
||||
window.localStorage.setItem(autosaveKey, JSON.stringify(data));
|
||||
window.localStorage.setItem(autosaveKey, JSON.stringify(safe));
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
editor?.updateBlock(block, { props: { ...block.props, data } });
|
||||
editor?.updateBlock(block, { props: { ...block.props, data: safe } });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -1016,7 +1100,7 @@ const MindmapBlockView = ({
|
||||
fetch(`/api/mindmap/${docId}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ data }),
|
||||
body: JSON.stringify({ data: safe }),
|
||||
}).catch(() => {
|
||||
// ignore
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user