0.1.06文件树的选择问题及思维导图删除问题
This commit is contained in:
@@ -238,6 +238,45 @@ export function BlockNoteEditor({
|
|||||||
const debouncedSave = useDebouncedCallback(saveContent, 800);
|
const debouncedSave = useDebouncedCallback(saveContent, 800);
|
||||||
const previousAssetsRef = useRef<Set<string>>(new Set());
|
const previousAssetsRef = useRef<Set<string>>(new Set());
|
||||||
const hadMindmapRef = useRef(false);
|
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<string>;
|
||||||
|
};
|
||||||
|
if (!w.__wolaiMindmapDeletingDocIds) {
|
||||||
|
w.__wolaiMindmapDeletingDocIds = new Set<string>();
|
||||||
|
}
|
||||||
|
w.__wolaiMindmapDeletingDocIds.add(targetDocumentId);
|
||||||
|
window.setTimeout(() => {
|
||||||
|
try {
|
||||||
|
w.__wolaiMindmapDeletingDocIds?.delete(targetDocumentId);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
const collectAssets = useCallback((blocks: Block<CustomBlockSchema>[]) => {
|
const collectAssets = useCallback((blocks: Block<CustomBlockSchema>[]) => {
|
||||||
const assetIds = new Set<string>();
|
const assetIds = new Set<string>();
|
||||||
@@ -286,8 +325,11 @@ export function BlockNoteEditor({
|
|||||||
console.error("删除思维导图失败", await resp.text());
|
console.error("删除思维导图失败", await resp.text());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// 重要:删除思维导图文件后也要清理本地 autosave,否则用户再次插入导图会从旧缓存恢复,表现为“删除不干净/重复出现”
|
||||||
|
markMindmapDeleting(documentId);
|
||||||
|
clearMindmapAutosaveCache(documentId);
|
||||||
emitAssetsChanged(documentId);
|
emitAssetsChanged(documentId);
|
||||||
}, [documentId]);
|
}, [clearMindmapAutosaveCache, documentId, markMindmapDeleting]);
|
||||||
|
|
||||||
// 监听侧边栏删除事件,主动移除编辑区遗留块
|
// 监听侧边栏删除事件,主动移除编辑区遗留块
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -301,6 +343,12 @@ export function BlockNoteEditor({
|
|||||||
const assetIds = detail.assetIds ?? [];
|
const assetIds = detail.assetIds ?? [];
|
||||||
const mindmapDeleted = Boolean(detail.mindmapDeleted);
|
const mindmapDeleted = Boolean(detail.mindmapDeleted);
|
||||||
if (assetIds.length === 0 && !mindmapDeleted) return;
|
if (assetIds.length === 0 && !mindmapDeleted) return;
|
||||||
|
if (mindmapDeleted) {
|
||||||
|
// 标记“删除中”,避免 MindmapBlock 卸载清理里把 autosave 写回导致“复活”
|
||||||
|
markMindmapDeleting(documentId);
|
||||||
|
// 双保险:等卸载完成后再清一次 autosave,避免删除后再次插入出现旧内容(重复)
|
||||||
|
window.setTimeout(() => clearMindmapAutosaveCache(documentId), 0);
|
||||||
|
}
|
||||||
const blocks = editor?.topLevelBlocks as Block<CustomBlockSchema>[] | undefined;
|
const blocks = editor?.topLevelBlocks as Block<CustomBlockSchema>[] | undefined;
|
||||||
if (!blocks || blocks.length === 0 || !editor) return;
|
if (!blocks || blocks.length === 0 || !editor) return;
|
||||||
const toRemove: string[] = [];
|
const toRemove: string[] = [];
|
||||||
@@ -327,7 +375,7 @@ export function BlockNoteEditor({
|
|||||||
};
|
};
|
||||||
window.addEventListener(ASSETS_CHANGED_EVENT, handler);
|
window.addEventListener(ASSETS_CHANGED_EVENT, handler);
|
||||||
return () => window.removeEventListener(ASSETS_CHANGED_EVENT, handler);
|
return () => window.removeEventListener(ASSETS_CHANGED_EVENT, handler);
|
||||||
}, [documentId, editor]);
|
}, [clearMindmapAutosaveCache, documentId, editor, markMindmapDeleting]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editor) {
|
if (!editor) {
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ const MindmapBlockView = ({
|
|||||||
const hotkeyScopeRef = useRef(false);
|
const hotkeyScopeRef = useRef(false);
|
||||||
const recentNodeDblclickRef = useRef(false);
|
const recentNodeDblclickRef = useRef(false);
|
||||||
const [fullscreenApiActive, setFullscreenApiActive] = useState(false);
|
const [fullscreenApiActive, setFullscreenApiActive] = useState(false);
|
||||||
|
const deletingRef = useRef(false);
|
||||||
|
|
||||||
const getRootNode = useCallback((mm?: MindMapInstance | null) => {
|
const getRootNode = useCallback((mm?: MindMapInstance | null) => {
|
||||||
const instance = mm ?? mindmap;
|
const instance = mm ?? mindmap;
|
||||||
@@ -1080,7 +1081,20 @@ const MindmapBlockView = ({
|
|||||||
destroyed = true;
|
destroyed = true;
|
||||||
try {
|
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<string>;
|
||||||
|
};
|
||||||
|
return Boolean(w.__wolaiMindmapDeletingDocIds?.has(docId));
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
if (!skipPersist && createdInstance && typeof window !== "undefined") {
|
||||||
const data =
|
const data =
|
||||||
createdInstance.getData?.(true) ?? createdInstance.getData?.();
|
createdInstance.getData?.(true) ?? createdInstance.getData?.();
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -1607,15 +1621,18 @@ const MindmapBlockView = ({
|
|||||||
|
|
||||||
const handleDeleteMindmap = useCallback(async () => {
|
const handleDeleteMindmap = useCallback(async () => {
|
||||||
if (!docId) {
|
if (!docId) {
|
||||||
|
deletingRef.current = true;
|
||||||
editor.removeBlocks([block.id]);
|
editor.removeBlocks([block.id]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const confirmed = window.confirm("确认删除当前思维导图?此操作会移除文件并清空侧边栏记录。");
|
const confirmed = window.confirm("确认删除当前思维导图?此操作会移除文件并清空侧边栏记录。");
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
|
deletingRef.current = true;
|
||||||
const resp = await fetch(`/api/mindmap/${docId}`, { method: "DELETE" });
|
const resp = await fetch(`/api/mindmap/${docId}`, { method: "DELETE" });
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
const payload = await resp.json().catch(() => ({}));
|
const payload = await resp.json().catch(() => ({}));
|
||||||
window.alert(payload?.error ?? "删除思维导图失败");
|
window.alert(payload?.error ?? "删除思维导图失败");
|
||||||
|
deletingRef.current = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
getDefaultReactSlashMenuItems,
|
getDefaultReactSlashMenuItems,
|
||||||
type DefaultReactSuggestionItem,
|
type DefaultReactSuggestionItem,
|
||||||
} from "@blocknote/react";
|
} 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 { useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
FileImage,
|
FileImage,
|
||||||
@@ -171,6 +171,34 @@ export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
|||||||
aliases: ["mindmap", "swdt", "导图"],
|
aliases: ["mindmap", "swdt", "导图"],
|
||||||
icon: <Spline className="h-4 w-4 text-[#2563eb]" />,
|
icon: <Spline className="h-4 w-4 text-[#2563eb]" />,
|
||||||
onItemClick: () => {
|
onItemClick: () => {
|
||||||
|
// mindmap.json / /api/mindmap/:docId 是“按页面(documentId)维度”存储的,
|
||||||
|
// 同一页面插入多个导图会共享同一份数据,用户会感知为“重复/镜像”。
|
||||||
|
// 这里先限制一页只允许一个导图块,避免产生歧义。
|
||||||
|
const blocks = editor.topLevelBlocks as Block<CustomBlockSchema>[];
|
||||||
|
let existingMindmapId: string | null = null;
|
||||||
|
const walk = (target: Block<CustomBlockSchema>[]) => {
|
||||||
|
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<CustomBlockSchema>[]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
walk(blocks);
|
||||||
|
if (existingMindmapId) {
|
||||||
|
try {
|
||||||
|
const el = document.querySelector<HTMLElement>(`[data-id="${existingMindmapId}"]`);
|
||||||
|
el?.scrollIntoView?.({ behavior: "smooth", block: "center" });
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
window.alert("当前页面已存在思维导图,暂不支持插入多个。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
editor.insertBlocks(
|
editor.insertBlocks(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import type { FileTreeRow } from "@/lib/file-tree/types";
|
import type { FileTreeRow } from "@/lib/file-tree/types";
|
||||||
import { getFileTreeRowLabel } from "@/lib/file-tree/types";
|
import { getFileTreeRowLabel } from "@/lib/file-tree/types";
|
||||||
import { isRealFileAsset } from "@/lib/file-tree/asset";
|
import { isRealFileAsset } from "@/lib/file-tree/asset";
|
||||||
|
import { isTextInputTarget } from "@/lib/file-tree/clipboard";
|
||||||
|
|
||||||
interface FileTreeProps {
|
interface FileTreeProps {
|
||||||
rows: FileTreeRow[];
|
rows: FileTreeRow[];
|
||||||
@@ -69,7 +70,7 @@ export function FileTree({
|
|||||||
const draggable =
|
const draggable =
|
||||||
row.kind === "doc" || (row.kind === "asset" && isRealFileAsset(row.asset));
|
row.kind === "doc" || (row.kind === "asset" && isRealFileAsset(row.asset));
|
||||||
const baseClass =
|
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 =
|
const activeClass =
|
||||||
row.kind === "index"
|
row.kind === "index"
|
||||||
? "text-[#2563eb] font-medium"
|
? "text-[#2563eb] font-medium"
|
||||||
@@ -87,6 +88,13 @@ export function FileTree({
|
|||||||
onClick={(event) => onRowClick(row, event)}
|
onClick={(event) => onRowClick(row, event)}
|
||||||
onDoubleClick={(event) => onRowDoubleClick(row, event)}
|
onDoubleClick={(event) => onRowDoubleClick(row, event)}
|
||||||
onContextMenu={(event) => onRowContextMenu(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}
|
draggable={draggable}
|
||||||
onDragStart={(event) => {
|
onDragStart={(event) => {
|
||||||
if (!draggable) return;
|
if (!draggable) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user