双向删除同步
This commit is contained in:
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user