"use client"; import { Copy, Download, PencilLine, Search, Trash2, Upload } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { App, Button, Card, Drawer, Empty, Form, Image, Input, Modal, Pagination, Select, Space, Tag, Typography } from "antd"; import { useCopyText } from "@/hooks/use-copy-text"; import { formatBytes, readFileAsDataUrl } from "@/lib/image-utils"; import { uploadImage } from "@/services/image-storage"; import { cn } from "@/lib/utils"; import { useAssetStore, type Asset, type AssetKind, type ImageAsset } from "@/stores/use-asset-store"; import { exportAssets, readAssetPackage } from "./asset-transfer"; type AssetFormValues = { kind: AssetKind; title: string; coverUrl: string; tags: string[]; source?: string; note?: string; content?: string; }; type ImageDraft = ImageAsset["data"] | null; const kindOptions = [ { label: "全部", value: "all" }, { label: "文本", value: "text" }, { label: "图片", value: "image" }, { label: "视频", value: "video" }, ]; export default function AssetsPage() { const { message } = App.useApp(); const copyText = useCopyText(); const [form] = Form.useForm(); const coverInputRef = useRef(null); const imageInputRef = useRef(null); const assetInputRef = useRef(null); const assets = useAssetStore((state) => state.assets); const addAsset = useAssetStore((state) => state.addAsset); const updateAsset = useAssetStore((state) => state.updateAsset); const removeAsset = useAssetStore((state) => state.removeAsset); const [keyword, setKeyword] = useState(""); const [kindFilter, setKindFilter] = useState("all"); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); const [editingAsset, setEditingAsset] = useState(null); const [isAssetOpen, setIsAssetOpen] = useState(false); const [previewAsset, setPreviewAsset] = useState(null); const [deletingAsset, setDeletingAsset] = useState(null); const [formKind, setFormKind] = useState("text"); const [imageDraft, setImageDraft] = useState(null); const coverUrl = Form.useWatch("coverUrl", form) || ""; const title = Form.useWatch("title", form) || ""; const tags = Form.useWatch("tags", form) || []; const content = Form.useWatch("content", form) || ""; const validAssets = useMemo(() => assets.filter((asset) => asset.kind === "text" || asset.kind === "image" || asset.kind === "video"), [assets]); const filteredAssets = useMemo(() => { const query = keyword.trim().toLowerCase(); return validAssets.filter((asset) => { if (kindFilter !== "all" && asset.kind !== kindFilter) return false; if (!query) return true; return assetSearchText(asset).includes(query); }); }, [validAssets, keyword, kindFilter]); const visibleAssets = useMemo(() => { const start = (page - 1) * pageSize; return filteredAssets.slice(start, start + pageSize); }, [filteredAssets, page, pageSize]); useEffect(() => { const maxPage = Math.max(1, Math.ceil(filteredAssets.length / pageSize)); setPage((value) => Math.min(value, maxPage)); }, [filteredAssets.length, pageSize]); const openCreate = () => { setEditingAsset(null); setImageDraft(null); setFormKind("text"); form.setFieldsValue({ kind: "text", title: "", coverUrl: "", tags: [], source: "手动添加", note: "", content: "" }); setIsAssetOpen(true); }; const openEdit = (asset: Asset) => { setEditingAsset(asset); setFormKind(asset.kind); setImageDraft(asset.kind === "image" ? asset.data : null); form.setFieldsValue({ kind: asset.kind, title: asset.title, coverUrl: asset.coverUrl, tags: asset.tags || [], source: asset.source, note: asset.note, content: asset.kind === "text" ? asset.data.content : "", }); setIsAssetOpen(true); }; const saveAsset = async () => { const values = await form.validateFields(); const base = { title: values.title.trim(), coverUrl: values.coverUrl?.trim() || (values.kind === "image" && imageDraft ? imageDraft.dataUrl : ""), tags: values.tags || [], source: values.source?.trim(), note: values.note?.trim(), metadata: editingAsset?.metadata || { source: "manual" }, }; if (values.kind === "text") { const asset = { ...base, kind: "text" as const, data: { content: (values.content || "").trim() } }; editingAsset ? updateAsset(editingAsset.id, asset) : addAsset(asset); } else { if (!imageDraft) { message.error("请选择图片文件"); return; } const asset = { ...base, kind: "image" as const, data: imageDraft }; editingAsset ? updateAsset(editingAsset.id, asset) : addAsset(asset); } message.success(editingAsset ? "素材已更新" : "素材已保存"); setIsAssetOpen(false); }; const readCoverFile = async (file?: File) => { if (!file) return; const dataUrl = await readFileAsDataUrl(file); form.setFieldValue("coverUrl", dataUrl); }; const readImageFile = async (file?: File) => { if (!file || !file.type.startsWith("image/")) return; const image = await uploadImage(file); const draft = { dataUrl: image.url, storageKey: image.storageKey, width: image.width, height: image.height, bytes: image.bytes, mimeType: image.mimeType }; setImageDraft(draft); if (!form.getFieldValue("coverUrl")) form.setFieldValue("coverUrl", draft.dataUrl); if (!form.getFieldValue("title")) form.setFieldValue("title", file.name); }; const copyAssetText = async (asset: Asset) => { if (asset.kind !== "text") return; copyText(asset.data.content, "文本已复制"); }; const downloadImage = (asset: Asset) => { if (asset.kind !== "image" && asset.kind !== "video") return; const link = document.createElement("a"); link.href = asset.kind === "video" ? asset.data.url : asset.data.dataUrl; link.download = `${asset.title || "asset"}.${asset.data.mimeType.split("/")[1] || "png"}`; link.click(); }; const exportAllAssets = async () => { if (!validAssets.length) { message.warning("暂无素材可导出"); return; } await exportAssets(validAssets); }; const importAssetZip = async (file?: File) => { if (!file) return; try { const importedAssets = await readAssetPackage(file); importedAssets.forEach((asset) => { const payload = { ...asset } as Record; delete payload.id; delete payload.createdAt; delete payload.updatedAt; addAsset(payload as Parameters[0]); }); message.success(`已导入 ${importedAssets.length} 个素材`); } catch { message.error("导入失败,请选择有效的素材压缩包"); } finally { if (assetInputRef.current) assetInputRef.current.value = ""; } }; const confirmDelete = () => { if (!deletingAsset) return; removeAsset(deletingAsset.id); message.success("素材已删除"); setDeletingAsset(null); }; return (

我的素材

收藏常用文本和图片,按类型、标题和标签快速查找。

} value={keyword} placeholder="搜索标题、内容、标签或来源" onChange={(event) => { setPage(1); setKeyword(event.target.value); }} onSearch={(value) => { setPage(1); setKeyword(value); }} />
类型
{kindOptions.map((option) => ( { setPage(1); setKindFilter(option.value as AssetKind | "all"); }} > {option.label} ))}
{visibleAssets.map((asset) => ( setPreviewAsset(asset)} onEdit={() => openEdit(asset)} onCopy={copyAssetText} onDownload={downloadImage} onDelete={() => setDeletingAsset(asset)} /> ))}
{!visibleAssets.length ? : null}
{ setPage(nextPage); setPageSize(nextPageSize); }} />
setIsAssetOpen(false)} onOk={() => void saveAsset()} okText="保存" cancelText="取消" destroyOnHidden>
{formKind === "text" ? ( ) : (
{imageDraft ? ( {imageDraft.width}x{imageDraft.height} · {formatBytes(imageDraft.bytes)} ) : ( 未选择图片 )}
)}
预览
{coverUrl || imageDraft?.dataUrl ? ( ) : (
{content || "暂无封面"}
)}
{title || "未命名素材"}
{tags.length ? ( tags.map((tag) => ( {tag} )) ) : ( 未打标签 )}
{ void readCoverFile(event.target.files?.[0]); event.target.value = ""; }} /> { void readImageFile(event.target.files?.[0]); event.target.value = ""; }} /> setPreviewAsset(null)} onCopy={copyAssetText} onDownload={downloadImage} /> void importAssetZip(event.target.files?.[0])} /> setDeletingAsset(null)} onOk={confirmDelete} okText="删除" okButtonProps={{ danger: true }} cancelText="取消"> 确定删除「{deletingAsset?.title}」吗?删除后会从我的素材中移除。 ); } function AssetCard({ asset, onOpen, onEdit, onCopy, onDownload, onDelete }: { asset: Asset; onOpen: () => void; onEdit: () => void; onCopy: (asset: Asset) => void; onDownload: (asset: Asset) => void; onDelete: () => void }) { const cover = asset.coverUrl || (asset.kind === "image" ? asset.data.dataUrl : ""); const summary = assetSummary(asset); return ( {cover ? ( {asset.title} ) : (
{asset.kind === "text" ? asset.data.content : "暂无封面"}
)} } >
{asset.kind !== "video" ? ( ) : null} {asset.kind === "text" ? ( ) : null} {asset.kind === "image" || asset.kind === "video" ? ( ) : null}
); } function AssetDrawer({ asset, onClose, onCopy, onDownload }: { asset: Asset | null; onClose: () => void; onCopy: (asset: Asset) => void; onDownload: (asset: Asset) => void }) { const cover = asset ? asset.coverUrl || (asset.kind === "image" ? asset.data.dataUrl : "") : ""; return ( {asset ? (
{cover ? ( {asset.title} ) : (
{asset.kind === "text" ? asset.data.content : "暂无封面"}
)}
{asset.title} {asset.kind === "image" ? "图片" : asset.kind === "video" ? "视频" : "文本"} {(asset.tags || []).map((tag) => ( {tag} ))}
内容 {asset.kind === "text" ? ( {asset.data.content} ) : asset.kind === "video" ? (
{asset.note ? (
备注 {asset.note}
) : null} {asset.kind === "text" ? ( ) : null} {asset.kind === "image" || asset.kind === "video" ? ( ) : null}
) : null}
); } function assetSummary(asset: Asset) { if (asset.kind === "text") return asset.data.content; return `${asset.data.width}x${asset.data.height} · ${formatBytes(asset.data.bytes)} · ${asset.data.mimeType}`; } function assetSearchText(asset: Asset) { return [asset.title, asset.source || "", asset.note || "", (asset.tags || []).join(" "), asset.kind === "text" ? asset.data.content : asset.data.mimeType].join(" ").toLowerCase(); }