"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 copy from "copy-to-clipboard"; 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"; 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" }, ]; export default function AssetsPage() { const { message } = App.useApp(); const [form] = Form.useForm(); const coverInputRef = useRef(null); const imageInputRef = 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"), [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 copyText = async (asset: Asset) => { if (asset.kind !== "text") return; copy(asset.data.content); message.success("文本已复制"); }; const downloadImage = (asset: Asset) => { if (asset.kind !== "image") return; const link = document.createElement("a"); link.href = asset.data.dataUrl; link.download = `${asset.title || "asset"}.${asset.data.mimeType.split("/")[1] || "png"}`; link.click(); }; 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={copyText} 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={copyText} onDownload={downloadImage} /> 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 === "text" ? : null} {asset.kind === "image" ? : 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.tags || []).map((tag) => {tag})}
内容 {asset.kind === "text" ? {asset.data.content} : {asset.data.width}x{asset.data.height} · {formatBytes(asset.data.bytes)} · {asset.data.mimeType}}
{asset.note ?
备注{asset.note}
: null} {asset.kind === "text" ? : null} {asset.kind === "image" ? : 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(); }