"use client"; import { useMutation, useQuery } from "@tanstack/react-query"; import clsx from "clsx"; import dayjs from "dayjs"; import { AudioLines, LoaderCircle, Search, Trash2, UploadCloud, Video } from "lucide-react"; import { useMemo, useState } from "react"; import { api, ApiError } from "@/lib/api"; import type { AssetItem } from "@/lib/types"; type MediaType = "image" | "video" | "audio"; const mediaTypes = [ { value: "image", label: "图片", accept: "image/*" }, { value: "video", label: "视频", accept: "video/*" }, { value: "audio", label: "音频", accept: "audio/*" }, ] as const satisfies Array<{ value: MediaType; label: string; accept: string; }>; function formatBytes(fileSize: number) { if (fileSize < 1024) { return `${fileSize} B`; } if (fileSize < 1024 * 1024) { return `${Math.round(fileSize / 1024)} KB`; } return `${(fileSize / 1024 / 1024).toFixed(1)} MB`; } function getErrorMessage(error: unknown, fallback: string) { return error instanceof ApiError ? error.message : fallback; } export default function AssetsPage() { const [mediaType, setMediaType] = useState("image"); const [libraryType, setLibraryType] = useState<"all" | MediaType>("all"); const [keyword, setKeyword] = useState(""); const [uploadFile, setUploadFile] = useState(null); const [feedback, setFeedback] = useState<{ tone: "success" | "error"; text: string; } | null>(null); const assetsQuery = useQuery({ queryKey: ["asset-list"], queryFn: () => api.get("/api/v1/assets"), }); const uploadMutation = useMutation({ mutationFn: async () => { if (!uploadFile) { throw new Error("请先选择文件"); } const formData = new FormData(); formData.append("file", uploadFile); formData.append("mediaType", mediaType); return api.post("/api/v1/assets", formData); }, onSuccess(asset) { setFeedback({ tone: "success", text: `素材 ${asset.originalFilename} 上传成功。`, }); setUploadFile(null); void assetsQuery.refetch(); }, onError(error) { setFeedback({ tone: "error", text: getErrorMessage(error, "素材上传失败"), }); }, }); const deleteMutation = useMutation({ mutationFn: (assetId: number) => api.del(`/api/v1/assets/${assetId}`), onSuccess: () => { setFeedback({ tone: "success", text: "素材已删除。", }); void assetsQuery.refetch(); }, onError(error) { setFeedback({ tone: "error", text: getErrorMessage(error, "素材删除失败"), }); }, }); const filteredAssets = useMemo(() => { let items = [...(assetsQuery.data ?? [])].sort( (left, right) => dayjs(right.createdAt).valueOf() - dayjs(left.createdAt).valueOf(), ); if (libraryType !== "all") { items = items.filter((asset) => asset.mediaType === libraryType); } if (keyword.trim()) { const loweredKeyword = keyword.trim().toLowerCase(); items = items.filter((asset) => [asset.originalFilename, asset.assetNo] .filter(Boolean) .some((field) => field.toLowerCase().includes(loweredKeyword)), ); } return items; }, [assetsQuery.data, keyword, libraryType]); return (
Asset Center

独立素材管理

适合批量上传和清理素材;工作台里也能直接引用这些素材。

总素材数 {assetsQuery.data?.length ?? 0}
{mediaTypes.map((item) => ( ))}
选择类型后再上传,文件会按类型归档
{mediaTypes.map((item) => ( ))}
{feedback ? (
{feedback.text}
) : null}
{filteredAssets.length ? ( filteredAssets.map((asset) => (
{asset.mediaType === "image" ? (
图片
) : asset.mediaType === "video" ? (
) : (
音频
)}
{asset.originalFilename}
{asset.assetNo} · {formatBytes(asset.fileSize)}
{dayjs(asset.createdAt).format("YYYY-MM-DD HH:mm")}
{asset.mediaType === "video" ? ( <>
)) ) : (
当前没有匹配素材,先上传或调整筛选条件。
)}
); }