双向删除同步

This commit is contained in:
liaibo
2026-01-02 07:25:50 +08:00
parent 1db64c1c55
commit b1288487af
48 changed files with 2406 additions and 8841 deletions
@@ -4,7 +4,7 @@ import "@blocknote/core/style.css";
import "@blocknote/react/style.css";
import "@blocknote/mantine/style.css";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { BlockNoteView } from "@blocknote/mantine";
import {
SideMenuController,
@@ -22,11 +22,12 @@ import { customBlockSchema, type CustomBlockSchema } from "./schema";
import { CustomSideMenu } from "./menus/CustomSideMenu";
import { CustomSlashMenu } from "./menus/CustomSlashMenu";
import { useDebouncedCallback } from "@/hooks/use-debounced-callback";
import { DocumentToc, type TocEntry } from "@/components/editor/document-toc";
import { DocumentToc, type TocEntry } from "@/components/editor/document-toc";
import { useSearchPaletteStore } from "@/store/search-palette";
import { useEditorBridgeStore } from "@/store/editor-bridge";
import type { ReferenceTarget } from "@/types/search";
import FullScreenTableEditor from "@/components/online-table/FullScreenTableEditor";
import { ASSETS_CHANGED_EVENT, emitAssetsChanged } from "@/lib/events";
interface BlockNoteEditorProps {
documentId: string;
@@ -235,6 +236,98 @@ export function BlockNoteEditor({
);
const debouncedSave = useDebouncedCallback(saveContent, 800);
const previousAssetsRef = useRef<Set<string>>(new Set());
const hadMindmapRef = useRef(false);
const collectAssets = useCallback((blocks: Block<CustomBlockSchema>[]) => {
const assetIds = new Set<string>();
let hasMindmap = false;
const walk = (target: Block<CustomBlockSchema>[]) => {
target.forEach((b) => {
if (b.type === "media") {
const id = (b.props as { assetId?: string })?.assetId;
if (id) assetIds.add(id);
}
if (b.type === "mindmap") {
hasMindmap = true;
}
if (Array.isArray(b.children) && b.children.length > 0) {
walk(b.children as Block<CustomBlockSchema>[]);
}
});
};
walk(blocks);
return { assetIds, hasMindmap };
}, []);
const deleteAssets = useCallback(
async (assetIds: string[]) => {
if (assetIds.length === 0) return;
const resp = await fetch("/api/media/batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "delete", assetIds }),
});
if (!resp.ok) {
// 如果后端返回未找到,说明已被其他端删除,忽略即可
if (resp.status !== 404) {
console.error("删除附件失败", await resp.text());
}
return;
}
emitAssetsChanged(documentId);
},
[documentId],
);
const deleteMindmap = useCallback(async () => {
const resp = await fetch(`/api/mindmap/${documentId}`, { method: "DELETE" });
if (!resp.ok) {
console.error("删除思维导图失败", await resp.text());
return;
}
emitAssetsChanged(documentId);
}, [documentId]);
// 监听侧边栏删除事件,主动移除编辑区遗留块
useEffect(() => {
const handler = (event: Event) => {
const detail = (event as CustomEvent)?.detail as {
docId?: string;
assetIds?: string[];
mindmapDeleted?: boolean;
};
if (!detail || detail.docId !== documentId) return;
const assetIds = detail.assetIds ?? [];
const mindmapDeleted = Boolean(detail.mindmapDeleted);
if (assetIds.length === 0 && !mindmapDeleted) return;
const blocks = editor?.topLevelBlocks as Block<CustomBlockSchema>[] | undefined;
if (!blocks || blocks.length === 0 || !editor) return;
const toRemove: string[] = [];
const walk = (target: Block<CustomBlockSchema>[]) => {
target.forEach((b) => {
if (mindmapDeleted && b.type === "mindmap") {
toRemove.push(b.id);
}
if (assetIds.length > 0 && b.type === "media") {
const id = (b.props as { assetId?: string })?.assetId;
if (id && assetIds.includes(id)) {
toRemove.push(b.id);
}
}
if (Array.isArray(b.children) && b.children.length > 0) {
walk(b.children as Block<CustomBlockSchema>[]);
}
});
};
walk(blocks);
if (toRemove.length > 0) {
editor.removeBlocks(toRemove);
}
};
window.addEventListener(ASSETS_CHANGED_EVENT, handler);
return () => window.removeEventListener(ASSETS_CHANGED_EVENT, handler);
}, [documentId, editor]);
useEffect(() => {
if (!editor) {
@@ -254,6 +347,19 @@ export function BlockNoteEditor({
const stats = computeDocumentStats(typedBlocks);
onStatsChange?.(stats);
onSnapshot?.({ blocks: blocks as Json, stats });
// 检测块删除 -> 同步删除附件/思维导图并刷新侧边栏
const { assetIds, hasMindmap } = collectAssets(typedBlocks);
const prevAssets = previousAssetsRef.current;
const removedAssets = [...prevAssets].filter((id) => !assetIds.has(id));
if (removedAssets.length > 0) {
void deleteAssets(removedAssets);
}
previousAssetsRef.current = assetIds;
if (hadMindmapRef.current && !hasMindmap) {
void deleteMindmap();
}
hadMindmapRef.current = hasMindmap;
};
runSync();
@@ -264,7 +370,7 @@ export function BlockNoteEditor({
unsubscribe();
}
};
}, [editor, debouncedSave, onSnapshot, onStatsChange]);
}, [collectAssets, debouncedSave, deleteAssets, deleteMindmap, editor, onSnapshot, onStatsChange]);
const jumpToHeading = useCallback((headingId: string) => {
const target = document.querySelector<HTMLElement>(`[data-id="${headingId}"]`);
@@ -386,6 +492,7 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
fileSize: asset.file_size ?? null,
mimeType: asset.mime_type ?? "",
ocrStatus: asset.ocr_status ?? "idle",
documentId,
},
},
],
@@ -393,7 +500,7 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
"after",
);
},
[editor],
[documentId, editor],
);
const uploadClipboardMedia = useCallback(
@@ -417,6 +524,7 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
if (!payload.asset) {
throw new Error("上传返回数据缺失");
}
emitAssetsChanged(documentId, payload.asset);
return payload.asset;
},
[documentId, workspaceId],