0.1.07 文件树拖拽与多思维导图
This commit is contained in:
@@ -145,6 +145,7 @@ const syncProgressMeters = (editorInstance: ReturnType<typeof useCreateBlockNote
|
||||
progressStats.forEach((stat, progressId) => {
|
||||
const block = findBlockById(blocks, progressId);
|
||||
if (!block) return;
|
||||
if (block.type !== "progressMeter") return;
|
||||
const weightedDone = stat.done + stat.doing * 0.5;
|
||||
const percent = stat.total === 0 ? 0 : Math.min(100, Math.round((weightedDone / stat.total) * 100));
|
||||
const summary = stat.total === 0 ? "暂无条目" : `${stat.done}/${stat.total} 完成`;
|
||||
@@ -237,17 +238,20 @@ export function BlockNoteEditor({
|
||||
|
||||
const debouncedSave = useDebouncedCallback(saveContent, 800);
|
||||
const previousAssetsRef = useRef<Set<string>>(new Set());
|
||||
const hadMindmapRef = useRef(false);
|
||||
const clearMindmapAutosaveCache = useCallback((targetDocumentId: string) => {
|
||||
const previousMindmapBlockIdsRef = useRef<Set<string>>(new Set());
|
||||
const clearMindmapAutosaveCache = useCallback((targetDocumentId: string, mindmapId?: string) => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
const prefix = "wolai-mindmap-autosave-";
|
||||
const targetPrefix = `${prefix}${targetDocumentId}`;
|
||||
const directKey = mindmapId ? `${targetPrefix}:${mindmapId}` : targetPrefix;
|
||||
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)) {
|
||||
if (mindmapId) {
|
||||
if (k === directKey) keys.push(k);
|
||||
} else if (k === targetPrefix || k.startsWith(`${targetPrefix}:`)) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
@@ -256,19 +260,20 @@ export function BlockNoteEditor({
|
||||
// ignore
|
||||
}
|
||||
}, []);
|
||||
const markMindmapDeleting = useCallback((targetDocumentId: string) => {
|
||||
const markMindmapDeleting = useCallback((targetDocumentId: string, mindmapId?: string) => {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
const w = window as unknown as {
|
||||
__wolaiMindmapDeletingDocIds?: Set<string>;
|
||||
__wolaiMindmapDeletingKeys?: Set<string>;
|
||||
};
|
||||
if (!w.__wolaiMindmapDeletingDocIds) {
|
||||
w.__wolaiMindmapDeletingDocIds = new Set<string>();
|
||||
if (!w.__wolaiMindmapDeletingKeys) {
|
||||
w.__wolaiMindmapDeletingKeys = new Set<string>();
|
||||
}
|
||||
w.__wolaiMindmapDeletingDocIds.add(targetDocumentId);
|
||||
const key = mindmapId ? `${targetDocumentId}:${mindmapId}` : targetDocumentId;
|
||||
w.__wolaiMindmapDeletingKeys.add(key);
|
||||
window.setTimeout(() => {
|
||||
try {
|
||||
w.__wolaiMindmapDeletingDocIds?.delete(targetDocumentId);
|
||||
w.__wolaiMindmapDeletingKeys?.delete(key);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -280,7 +285,7 @@ export function BlockNoteEditor({
|
||||
|
||||
const collectAssets = useCallback((blocks: Block<CustomBlockSchema>[]) => {
|
||||
const assetIds = new Set<string>();
|
||||
let hasMindmap = false;
|
||||
const mindmapBlockIds = new Set<string>();
|
||||
const walk = (target: Block<CustomBlockSchema>[]) => {
|
||||
target.forEach((b) => {
|
||||
if (b.type === "media") {
|
||||
@@ -288,7 +293,7 @@ export function BlockNoteEditor({
|
||||
if (id) assetIds.add(id);
|
||||
}
|
||||
if (b.type === "mindmap") {
|
||||
hasMindmap = true;
|
||||
mindmapBlockIds.add(b.id);
|
||||
}
|
||||
if (Array.isArray(b.children) && b.children.length > 0) {
|
||||
walk(b.children as Block<CustomBlockSchema>[]);
|
||||
@@ -296,7 +301,7 @@ export function BlockNoteEditor({
|
||||
});
|
||||
};
|
||||
walk(blocks);
|
||||
return { assetIds, hasMindmap };
|
||||
return { assetIds, mindmapBlockIds };
|
||||
}, []);
|
||||
|
||||
const deleteAssets = useCallback(
|
||||
@@ -319,17 +324,28 @@ export function BlockNoteEditor({
|
||||
[documentId],
|
||||
);
|
||||
|
||||
const deleteMindmap = useCallback(async () => {
|
||||
const resp = await fetch(`/api/mindmap/${documentId}`, { method: "DELETE" });
|
||||
if (!resp.ok) {
|
||||
console.error("删除思维导图失败", await resp.text());
|
||||
return;
|
||||
}
|
||||
// 重要:删除思维导图文件后也要清理本地 autosave,否则用户再次插入导图会从旧缓存恢复,表现为“删除不干净/重复出现”
|
||||
markMindmapDeleting(documentId);
|
||||
clearMindmapAutosaveCache(documentId);
|
||||
emitAssetsChanged(documentId);
|
||||
}, [clearMindmapAutosaveCache, documentId, markMindmapDeleting]);
|
||||
const deleteMindmapAssets = useCallback(
|
||||
async (mindmapIds: string[]) => {
|
||||
if (mindmapIds.length === 0) return;
|
||||
await Promise.all(
|
||||
mindmapIds.map(async (mindmapId) => {
|
||||
try {
|
||||
const resp = await fetch(`/api/mindmap/${documentId}/${mindmapId}`, { method: "DELETE" });
|
||||
if (!resp.ok) {
|
||||
console.error("删除思维导图失败", mindmapId, await resp.text().catch(() => ""));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("删除思维导图失败", mindmapId, error);
|
||||
} finally {
|
||||
markMindmapDeleting(documentId, mindmapId);
|
||||
clearMindmapAutosaveCache(documentId, mindmapId);
|
||||
}
|
||||
}),
|
||||
);
|
||||
emitAssetsChanged(documentId, undefined, undefined, false, mindmapIds);
|
||||
},
|
||||
[clearMindmapAutosaveCache, documentId, markMindmapDeleting],
|
||||
);
|
||||
|
||||
// 监听侧边栏删除事件,主动移除编辑区遗留块
|
||||
useEffect(() => {
|
||||
@@ -338,24 +354,42 @@ export function BlockNoteEditor({
|
||||
docId?: string;
|
||||
assetIds?: string[];
|
||||
mindmapDeleted?: boolean;
|
||||
mindmapAssetIds?: string[];
|
||||
};
|
||||
if (!detail || detail.docId !== documentId) return;
|
||||
const assetIds = detail.assetIds ?? [];
|
||||
const mindmapDeleted = Boolean(detail.mindmapDeleted);
|
||||
if (assetIds.length === 0 && !mindmapDeleted) return;
|
||||
if (mindmapDeleted) {
|
||||
const mindmapAssetIds = Array.isArray(detail.mindmapAssetIds)
|
||||
? (detail.mindmapAssetIds.filter((id) => typeof id === "string") as string[])
|
||||
: [];
|
||||
if (assetIds.length === 0 && !mindmapDeleted && mindmapAssetIds.length === 0) return;
|
||||
if (mindmapDeleted || mindmapAssetIds.length > 0) {
|
||||
// 标记“删除中”,避免 MindmapBlock 卸载清理里把 autosave 写回导致“复活”
|
||||
markMindmapDeleting(documentId);
|
||||
if (mindmapAssetIds.length > 0) {
|
||||
mindmapAssetIds.forEach((id) => markMindmapDeleting(documentId, id));
|
||||
} else {
|
||||
markMindmapDeleting(documentId);
|
||||
}
|
||||
// 双保险:等卸载完成后再清一次 autosave,避免删除后再次插入出现旧内容(重复)
|
||||
window.setTimeout(() => clearMindmapAutosaveCache(documentId), 0);
|
||||
window.setTimeout(() => {
|
||||
if (mindmapAssetIds.length > 0) {
|
||||
mindmapAssetIds.forEach((id) => clearMindmapAutosaveCache(documentId, id));
|
||||
} else {
|
||||
clearMindmapAutosaveCache(documentId);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
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 (b.type === "mindmap") {
|
||||
if (mindmapDeleted) {
|
||||
toRemove.push(b.id);
|
||||
} else if (mindmapAssetIds.length > 0 && mindmapAssetIds.includes(b.id)) {
|
||||
toRemove.push(b.id);
|
||||
}
|
||||
}
|
||||
if (assetIds.length > 0 && b.type === "media") {
|
||||
const id = (b.props as { assetId?: string })?.assetId;
|
||||
@@ -370,7 +404,11 @@ export function BlockNoteEditor({
|
||||
};
|
||||
walk(blocks);
|
||||
if (toRemove.length > 0) {
|
||||
editor.removeBlocks(toRemove);
|
||||
try {
|
||||
editor.removeBlocks(toRemove);
|
||||
} catch {
|
||||
// ignore:可能已被其它链路先行删除(例如块菜单/工具栏触发的 removeBlocks)
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener(ASSETS_CHANGED_EVENT, handler);
|
||||
@@ -397,28 +435,30 @@ export function BlockNoteEditor({
|
||||
onSnapshot?.({ blocks: blocks as Json, stats });
|
||||
|
||||
// 检测块删除 -> 同步删除附件/思维导图并刷新侧边栏
|
||||
const { assetIds, hasMindmap } = collectAssets(typedBlocks);
|
||||
const { assetIds, mindmapBlockIds } = 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();
|
||||
const prevMindmaps = previousMindmapBlockIdsRef.current;
|
||||
const removedMindmaps = [...prevMindmaps].filter((id) => !mindmapBlockIds.has(id));
|
||||
if (removedMindmaps.length > 0) {
|
||||
void deleteMindmapAssets(removedMindmaps);
|
||||
}
|
||||
hadMindmapRef.current = hasMindmap;
|
||||
previousMindmapBlockIdsRef.current = mindmapBlockIds;
|
||||
};
|
||||
|
||||
runSync();
|
||||
const unsubscribe = editor.onEditorContentChange(runSync);
|
||||
const unsubscribe = editor.onEditorContentChange(runSync) as unknown as
|
||||
| undefined
|
||||
| (() => void);
|
||||
return () => {
|
||||
disposed = true;
|
||||
if (typeof unsubscribe === "function") {
|
||||
unsubscribe();
|
||||
}
|
||||
unsubscribe?.();
|
||||
};
|
||||
}, [collectAssets, debouncedSave, deleteAssets, deleteMindmap, editor, onSnapshot, onStatsChange]);
|
||||
}, [collectAssets, debouncedSave, deleteAssets, deleteMindmapAssets, editor, onSnapshot, onStatsChange]);
|
||||
|
||||
const jumpToHeading = useCallback((headingId: string) => {
|
||||
const target = document.querySelector<HTMLElement>(`[data-id="${headingId}"]`);
|
||||
@@ -453,15 +493,15 @@ const trimTrailingCharacter = (
|
||||
if (!editorInstance) {
|
||||
return;
|
||||
}
|
||||
const content = Array.isArray(block.content) ? [...block.content] : [];
|
||||
const content = (Array.isArray(block.content) ? [...block.content] : []) as any[];
|
||||
for (let index = content.length - 1; index >= 0; index -= 1) {
|
||||
const node = content[index] as { text?: string };
|
||||
const node = content[index] as any;
|
||||
if (typeof node?.text === "string" && node.text.endsWith(char)) {
|
||||
const nextText = node.text.slice(0, -1);
|
||||
if (nextText.length === 0) {
|
||||
content.splice(index, 1);
|
||||
} else {
|
||||
content[index] = { ...node, text: nextText };
|
||||
content[index] = { ...(node as any), text: nextText } as any;
|
||||
}
|
||||
editorInstance.updateBlock(block, { content });
|
||||
break;
|
||||
@@ -482,7 +522,7 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
const accumulate = (targetBlocks: Block<CustomBlockSchema>[]) => {
|
||||
targetBlocks.forEach((block) => {
|
||||
if (Array.isArray(block.content)) {
|
||||
block.content.forEach((node: { text?: string }) => {
|
||||
(block.content as any[]).forEach((node: any) => {
|
||||
if (typeof node.text === "string") {
|
||||
const text = node.text;
|
||||
characterCount += text.length;
|
||||
@@ -537,7 +577,7 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
assetId: asset.id,
|
||||
assetType: asset.asset_type ?? "image",
|
||||
fileName: asset.file_name ?? "",
|
||||
fileSize: asset.file_size ?? null,
|
||||
fileSize: asset.file_size ?? undefined,
|
||||
mimeType: asset.mime_type ?? "",
|
||||
ocrStatus: asset.ocr_status ?? "idle",
|
||||
documentId,
|
||||
@@ -598,7 +638,7 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
href: buildDocumentPath(target.id),
|
||||
content: text,
|
||||
},
|
||||
{ type: "text", text: " " },
|
||||
" ",
|
||||
]);
|
||||
return { blockId };
|
||||
},
|
||||
@@ -729,6 +769,7 @@ const computeDocumentStats = (blocks: Block<CustomBlockSchema>[]): DocumentStats
|
||||
<BlockNoteView
|
||||
editor={editor}
|
||||
theme="light"
|
||||
slashMenu={false}
|
||||
data-heading-numbering={pageOptions.showHeadingNumbers ? "true" : "false"}
|
||||
editable={!pageOptions.protectEditing}
|
||||
className={blocknoteClass}
|
||||
|
||||
Reference in New Issue
Block a user