From 9de2d69c55f39dad1ad546dd40c326e9afd55c6f Mon Sep 17 00:00:00 2001 From: liaibo Date: Wed, 7 Jan 2026 22:06:49 +0800 Subject: [PATCH] =?UTF-8?q?0.1.06=E6=96=87=E4=BB=B6=E6=A0=91=E7=9A=84?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E9=97=AE=E9=A2=98=E5=8F=8A=E6=80=9D=E7=BB=B4?= =?UTF-8?q?=E5=AF=BC=E5=9B=BE=E5=88=A0=E9=99=A4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/editor/blocknote-editor.tsx | 52 ++++++++++++++++++- .../components/editor/blocks/MindmapBlock.tsx | 19 ++++++- .../editor/menus/CustomSlashMenu.tsx | 30 ++++++++++- .../src/components/sidebar/file-tree.tsx | 16 ++++-- 4 files changed, 109 insertions(+), 8 deletions(-) diff --git a/wolai-frontend/src/components/editor/blocknote-editor.tsx b/wolai-frontend/src/components/editor/blocknote-editor.tsx index 6b5471b3..04b51122 100644 --- a/wolai-frontend/src/components/editor/blocknote-editor.tsx +++ b/wolai-frontend/src/components/editor/blocknote-editor.tsx @@ -238,6 +238,45 @@ export function BlockNoteEditor({ const debouncedSave = useDebouncedCallback(saveContent, 800); const previousAssetsRef = useRef>(new Set()); const hadMindmapRef = useRef(false); + const clearMindmapAutosaveCache = useCallback((targetDocumentId: string) => { + if (typeof window === "undefined") return; + try { + const prefix = "wolai-mindmap-autosave-"; + const targetPrefix = `${prefix}${targetDocumentId}`; + const keys: string[] = []; + for (let i = 0; i < window.localStorage.length; i += 1) { + const k = window.localStorage.key(i); + if (!k) continue; + if (k === targetPrefix || k.startsWith(targetPrefix)) { + keys.push(k); + } + } + keys.forEach((k) => window.localStorage.removeItem(k)); + } catch { + // ignore + } + }, []); + const markMindmapDeleting = useCallback((targetDocumentId: string) => { + if (typeof window === "undefined") return; + try { + const w = window as unknown as { + __wolaiMindmapDeletingDocIds?: Set; + }; + if (!w.__wolaiMindmapDeletingDocIds) { + w.__wolaiMindmapDeletingDocIds = new Set(); + } + w.__wolaiMindmapDeletingDocIds.add(targetDocumentId); + window.setTimeout(() => { + try { + w.__wolaiMindmapDeletingDocIds?.delete(targetDocumentId); + } catch { + // ignore + } + }, 8000); + } catch { + // ignore + } + }, []); const collectAssets = useCallback((blocks: Block[]) => { const assetIds = new Set(); @@ -286,8 +325,11 @@ export function BlockNoteEditor({ console.error("删除思维导图失败", await resp.text()); return; } + // 重要:删除思维导图文件后也要清理本地 autosave,否则用户再次插入导图会从旧缓存恢复,表现为“删除不干净/重复出现” + markMindmapDeleting(documentId); + clearMindmapAutosaveCache(documentId); emitAssetsChanged(documentId); - }, [documentId]); + }, [clearMindmapAutosaveCache, documentId, markMindmapDeleting]); // 监听侧边栏删除事件,主动移除编辑区遗留块 useEffect(() => { @@ -301,6 +343,12 @@ export function BlockNoteEditor({ const assetIds = detail.assetIds ?? []; const mindmapDeleted = Boolean(detail.mindmapDeleted); if (assetIds.length === 0 && !mindmapDeleted) return; + if (mindmapDeleted) { + // 标记“删除中”,避免 MindmapBlock 卸载清理里把 autosave 写回导致“复活” + markMindmapDeleting(documentId); + // 双保险:等卸载完成后再清一次 autosave,避免删除后再次插入出现旧内容(重复) + window.setTimeout(() => clearMindmapAutosaveCache(documentId), 0); + } const blocks = editor?.topLevelBlocks as Block[] | undefined; if (!blocks || blocks.length === 0 || !editor) return; const toRemove: string[] = []; @@ -327,7 +375,7 @@ export function BlockNoteEditor({ }; window.addEventListener(ASSETS_CHANGED_EVENT, handler); return () => window.removeEventListener(ASSETS_CHANGED_EVENT, handler); - }, [documentId, editor]); + }, [clearMindmapAutosaveCache, documentId, editor, markMindmapDeleting]); useEffect(() => { if (!editor) { diff --git a/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx b/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx index d610009f..52bf75de 100644 --- a/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx +++ b/wolai-frontend/src/components/editor/blocks/MindmapBlock.tsx @@ -319,6 +319,7 @@ const MindmapBlockView = ({ const hotkeyScopeRef = useRef(false); const recentNodeDblclickRef = useRef(false); const [fullscreenApiActive, setFullscreenApiActive] = useState(false); + const deletingRef = useRef(false); const getRootNode = useCallback((mm?: MindMapInstance | null) => { const instance = mm ?? mindmap; @@ -1080,7 +1081,20 @@ const MindmapBlockView = ({ destroyed = true; try { // 切换“内嵌/全屏”会导致实例重建:这里尽量在销毁前同步一次数据,避免丢失最后一次编辑 - if (createdInstance && typeof window !== "undefined") { + const skipPersist = + deletingRef.current || + (() => { + if (!docId || typeof window === "undefined") return false; + try { + const w = window as unknown as { + __wolaiMindmapDeletingDocIds?: Set; + }; + return Boolean(w.__wolaiMindmapDeletingDocIds?.has(docId)); + } catch { + return false; + } + })(); + if (!skipPersist && createdInstance && typeof window !== "undefined") { const data = createdInstance.getData?.(true) ?? createdInstance.getData?.(); if (data) { @@ -1607,15 +1621,18 @@ const MindmapBlockView = ({ const handleDeleteMindmap = useCallback(async () => { if (!docId) { + deletingRef.current = true; editor.removeBlocks([block.id]); return; } const confirmed = window.confirm("确认删除当前思维导图?此操作会移除文件并清空侧边栏记录。"); if (!confirmed) return; + deletingRef.current = true; const resp = await fetch(`/api/mindmap/${docId}`, { method: "DELETE" }); if (!resp.ok) { const payload = await resp.json().catch(() => ({})); window.alert(payload?.error ?? "删除思维导图失败"); + deletingRef.current = false; return; } try { diff --git a/wolai-frontend/src/components/editor/menus/CustomSlashMenu.tsx b/wolai-frontend/src/components/editor/menus/CustomSlashMenu.tsx index fe02b240..ba51654d 100644 --- a/wolai-frontend/src/components/editor/menus/CustomSlashMenu.tsx +++ b/wolai-frontend/src/components/editor/menus/CustomSlashMenu.tsx @@ -7,7 +7,7 @@ import { getDefaultReactSlashMenuItems, type DefaultReactSuggestionItem, } from "@blocknote/react"; -import { filterSuggestionItems, type BlockNoteEditor } from "@blocknote/core"; +import { filterSuggestionItems, type Block, type BlockNoteEditor } from "@blocknote/core"; import { useRouter } from "next/navigation"; import { FileImage, @@ -171,6 +171,34 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => { aliases: ["mindmap", "swdt", "导图"], icon: , onItemClick: () => { + // mindmap.json / /api/mindmap/:docId 是“按页面(documentId)维度”存储的, + // 同一页面插入多个导图会共享同一份数据,用户会感知为“重复/镜像”。 + // 这里先限制一页只允许一个导图块,避免产生歧义。 + const blocks = editor.topLevelBlocks as Block[]; + let existingMindmapId: string | null = null; + const walk = (target: Block[]) => { + target.forEach((b) => { + if (existingMindmapId) return; + if (b.type === "mindmap") { + existingMindmapId = b.id; + return; + } + if (Array.isArray(b.children) && b.children.length > 0) { + walk(b.children as Block[]); + } + }); + }; + walk(blocks); + if (existingMindmapId) { + try { + const el = document.querySelector(`[data-id="${existingMindmapId}"]`); + el?.scrollIntoView?.({ behavior: "smooth", block: "center" }); + } catch { + // ignore + } + window.alert("当前页面已存在思维导图,暂不支持插入多个。"); + return; + } editor.insertBlocks( [ { diff --git a/wolai-frontend/src/components/sidebar/file-tree.tsx b/wolai-frontend/src/components/sidebar/file-tree.tsx index 2de60959..a9f7d548 100644 --- a/wolai-frontend/src/components/sidebar/file-tree.tsx +++ b/wolai-frontend/src/components/sidebar/file-tree.tsx @@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"; import type { FileTreeRow } from "@/lib/file-tree/types"; import { getFileTreeRowLabel } from "@/lib/file-tree/types"; import { isRealFileAsset } from "@/lib/file-tree/asset"; +import { isTextInputTarget } from "@/lib/file-tree/clipboard"; interface FileTreeProps { rows: FileTreeRow[]; @@ -69,7 +70,7 @@ export function FileTree({ const draggable = row.kind === "doc" || (row.kind === "asset" && isRealFileAsset(row.asset)); const baseClass = - "flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-gray-700 hover:bg-[#f5f7fb]"; + "flex select-none items-center gap-2 rounded-md px-2 py-1.5 text-sm text-gray-700 hover:bg-[#f5f7fb]"; const activeClass = row.kind === "index" ? "text-[#2563eb] font-medium" @@ -85,9 +86,16 @@ export function FileTree({ className={cn(baseClass, active && activeClass, selected && "bg-[#e8f2ff] text-[#2563eb]")} style={{ paddingLeft }} onClick={(event) => onRowClick(row, event)} - onDoubleClick={(event) => onRowDoubleClick(row, event)} - onContextMenu={(event) => onRowContextMenu(row, event)} - draggable={draggable} + onDoubleClick={(event) => onRowDoubleClick(row, event)} + onContextMenu={(event) => onRowContextMenu(row, event)} + onMouseDown={(event) => { + // 避免 Shift 多选时触发浏览器默认的文本范围选择(VS Code 资源管理器不会出现该行为) + if (event.button !== 0) return; + if (!event.shiftKey) return; + if (isTextInputTarget(event.target)) return; + event.preventDefault(); + }} + draggable={draggable} onDragStart={(event) => { if (!draggable) return; if (!selectedRowIds.has(row.rowId)) {