0.1.05 修复放大缩小

This commit is contained in:
liaibo
2026-01-07 21:21:22 +08:00
parent 294d1d5fb1
commit afad811d85
@@ -105,6 +105,44 @@ export const defaultMindmapData = {
children: [], 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-"; const STORAGE_PREFIX = "wolai-mindmap-autosave-";
function downloadJson(data: unknown, name: string) { function downloadJson(data: unknown, name: string) {
@@ -310,12 +348,12 @@ const MindmapBlockView = ({
const cached = typeof window !== "undefined" ? window.localStorage.getItem(autosaveKey) : null; const cached = typeof window !== "undefined" ? window.localStorage.getItem(autosaveKey) : null;
if (cached) { if (cached) {
try { try {
initialDataRef.current = JSON.parse(cached); initialDataRef.current = canonicalizeMindmapData(JSON.parse(cached));
} catch { } catch {
initialDataRef.current = block.props.data ?? defaultMindmapData; initialDataRef.current = canonicalizeMindmapData(block.props.data ?? defaultMindmapData);
} }
} else { } 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 (!data || cancelled) return;
// 若用户已在本地做过编辑,则不再用远端数据覆盖,避免“插入节点后又被重置” // 若用户已在本地做过编辑,则不再用远端数据覆盖,避免“插入节点后又被重置”
if (hasLocalEditsRef.current) return; if (hasLocalEditsRef.current) return;
initialDataRef.current = data; initialDataRef.current = canonicalizeMindmapData(data);
if (mindmap) { if (mindmap) {
applyingRemoteRef.current = true; applyingRemoteRef.current = true;
try { try {
mindmap.setData(data); mindmap.setData(initialDataRef.current);
mindmap.command.clearHistory(); mindmap.command.clearHistory();
} finally { } finally {
window.setTimeout(() => { window.setTimeout(() => {
@@ -570,14 +608,18 @@ const MindmapBlockView = ({
const persistData = useCallback( const persistData = useCallback(
(data: unknown) => { (data: unknown) => {
if (!editor) return; if (!editor) return;
window.localStorage.setItem(autosaveKey, JSON.stringify(data)); // 关键:全屏/非全屏切换会重建 mindmap 实例,初始化数据必须跟随最新编辑
editor.updateBlock(block, { props: { ...block.props, data } }); // 否则切换视图时会用旧数据,表现为“看起来没保存”
const safe = canonicalizeMindmapData(data);
initialDataRef.current = safe;
window.localStorage.setItem(autosaveKey, JSON.stringify(safe));
editor.updateBlock(block, { props: { ...block.props, data: safe } });
if (docId) { if (docId) {
// 同步到本地文件 + Supabase(弱依赖) // 同步到本地文件 + Supabase(弱依赖)
fetch(`/api/mindmap/${docId}`, { fetch(`/api/mindmap/${docId}`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data }), body: JSON.stringify({ data: safe }),
}) })
.then((resp) => { .then((resp) => {
if (resp.ok) { if (resp.ok) {
@@ -604,7 +646,9 @@ const MindmapBlockView = ({
useEffect(() => { useEffect(() => {
if (!docId || !mindmap || initialSyncDone.current) return; if (!docId || !mindmap || initialSyncDone.current) return;
initialSyncDone.current = true; initialSyncDone.current = true;
const data = mindmap.getData?.() ?? initialDataRef.current ?? defaultMindmapData; const data = canonicalizeMindmapData(
mindmap.getData?.(true) ?? mindmap.getData?.() ?? initialDataRef.current ?? defaultMindmapData,
);
(async () => { (async () => {
try { try {
const resp = await fetch(`/api/mindmap/${docId}`, { const resp = await fetch(`/api/mindmap/${docId}`, {
@@ -662,8 +706,18 @@ const MindmapBlockView = ({
let destroyed = false; let destroyed = false;
let createdInstance: MindMapInstance | null = null; let createdInstance: MindMapInstance | null = null;
(async () => { (async () => {
if (!containerRef.current) return; // 进入/退出全屏会切换渲染树,ref 在某些时序下可能短暂为 null。
const hostContainer = containerRef.current; // 若这里直接 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 [ const [
{ default: MindMap }, { default: MindMap },
{ default: Painter }, { default: Painter },
@@ -819,6 +873,29 @@ const MindmapBlockView = ({
hostEl.innerHTML = ""; 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: { type MindMapConstructor = new (options: {
el: HTMLElement; el: HTMLElement;
data: unknown; data: unknown;
@@ -836,7 +913,7 @@ const MindmapBlockView = ({
const instance = new MindMapCtor({ const instance = new MindMapCtor({
el: hostEl, el: hostEl,
data: initialDataRef.current, data: dataForInit,
theme: "classic", theme: "classic",
layout: "logicalStructure", layout: "logicalStructure",
mousewheelAction: "zoom", mousewheelAction: "zoom",
@@ -964,11 +1041,16 @@ const MindmapBlockView = ({
}); });
instance.on?.("painter_start", () => setPainterMode(true)); instance.on?.("painter_start", () => setPainterMode(true));
instance.on?.("painter_end", () => setPainterMode(false)); instance.on?.("painter_end", () => setPainterMode(false));
instance.on?.("data_change", (data: unknown) => { instance.on?.("data_change", () => {
if (!applyingRemoteRef.current) { if (!applyingRemoteRef.current) {
hasLocalEditsRef.current = true; 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 renderer = instance.renderer;
const rootNode = getRootNode(instance); const rootNode = getRootNode(instance);
@@ -1002,13 +1084,15 @@ const MindmapBlockView = ({
const data = const data =
createdInstance.getData?.(true) ?? createdInstance.getData?.(); createdInstance.getData?.(true) ?? createdInstance.getData?.();
if (data) { if (data) {
const safe = canonicalizeMindmapData(data);
initialDataRef.current = safe;
try { try {
window.localStorage.setItem(autosaveKey, JSON.stringify(data)); window.localStorage.setItem(autosaveKey, JSON.stringify(safe));
} catch { } catch {
// ignore // ignore
} }
try { try {
editor?.updateBlock(block, { props: { ...block.props, data } }); editor?.updateBlock(block, { props: { ...block.props, data: safe } });
} catch { } catch {
// ignore // ignore
} }
@@ -1016,7 +1100,7 @@ const MindmapBlockView = ({
fetch(`/api/mindmap/${docId}`, { fetch(`/api/mindmap/${docId}`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ data }), body: JSON.stringify({ data: safe }),
}).catch(() => { }).catch(() => {
// ignore // ignore
}); });