0.1.05 修复放大缩小
This commit is contained in:
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user