双向删除同步

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
@@ -5,16 +5,18 @@ import {
useRef,
useState,
useMemo,
useCallback,
type JSX,
type MouseEvent as ReactMouseEvent,
} from "react";
import { createReactBlockSpec } from "@blocknote/react";
import type { Block, BlockNoteEditor } from "@blocknote/core";
import { Download, Image as ImageIcon, Link as LinkIcon, MoreHorizontal, Paperclip, RefreshCcw, Type } from "lucide-react";
import { Download, Image as ImageIcon, Link as LinkIcon, MoreHorizontal, Paperclip, RefreshCcw, Trash, Type } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { useImagePicker } from "@/components/media/image-picker-context";
import type { MediaKind } from "@/types/media";
import { emitAssetsChanged } from "@/lib/events";
import {
DropdownMenu,
DropdownMenuContent,
@@ -73,7 +75,7 @@ const formatFileSize = (size?: number | null) => {
return `${current.toFixed(current >= 10 ? 0 : 1)} ${units[idx]}`;
};
const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
const { openPicker } = useImagePicker();
const [busy, setBusy] = useState(false);
const fileUrl = block.props.fileUrl as string;
@@ -96,6 +98,16 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
const [captionEditing, setCaptionEditing] = useState(false);
const shouldShowCaption = captionEditing || Boolean(block.props.caption);
const officeBase = process.env.NEXT_PUBLIC_ONLYOFFICE_BASE_URL;
const resolveDocumentId = useCallback(() => {
if (typeof window !== "undefined") {
const [, tail] = window.location.pathname.split("/documents/");
if (tail) {
const id = tail.split(/[/?#]/)[0];
if (id) return id;
}
}
return (block.props as { documentId?: string })?.documentId || "";
}, [block.props]);
const extension = useMemo(() => {
const name = (block.props.fileName || deriveFileName(fileUrl)).toLowerCase();
const match = /\.([a-z0-9]+)$/.exec(name);
@@ -121,9 +133,10 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
assetId: selection.assetId,
assetType: selection.assetType ?? rawAssetType,
fileName: selection.fileName ?? block.props.fileName ?? "",
fileSize: selection.fileSize ?? block.props.fileSize ?? null,
fileSize: selection.fileSize ?? block.props.fileSize ?? null,
mimeType: selection.mimeType ?? block.props.mimeType ?? "",
ocrStatus: "idle",
documentId: resolveDocumentId(),
},
});
},
@@ -255,6 +268,27 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
anchor.click();
};
const handleDeleteAsset = async () => {
const assetId = (block.props as { assetId?: string })?.assetId;
if (!assetId) {
editor.removeBlocks([block.id]);
return;
}
const docId = resolveDocumentId();
const resp = await fetch("/api/media/batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "delete", assetIds: [assetId] }),
});
if (!resp.ok) {
const payload = await resp.json().catch(() => ({}));
window.alert(payload?.error ?? "删除附件失败");
return;
}
editor.removeBlocks([block.id]);
emitAssetsChanged(docId);
};
const triggerOcr = async () => {
if (!block.props.assetId) {
window.alert("请先上传图片后再执行 OCR");
@@ -395,6 +429,12 @@ const MediaBlockContent = ({ block, editor }: MediaBlockRenderProps) => {
icon: <Download className="h-4 w-4" />,
onClick: downloadAsset,
},
{
key: "delete",
label: `删除${typeLabel}`,
icon: <Trash className="h-4 w-4" />,
onClick: handleDeleteAsset,
},
].filter((action): action is QuickAction => Boolean(action));
const dropdownLinkLabel = block.props.linkUrl ? "编辑链接" : "添加链接";
@@ -504,6 +544,7 @@ export const mediaBlock = createReactBlockSpec(
mimeType: { default: "", type: "string" },
width: { default: 0, type: "number" },
ocrStatus: { default: "idle", type: "string" },
documentId: { default: "", type: "string" },
},
content: "none",
},