diff --git a/apps/web/src/pages/assets/AssetLibrary.tsx b/apps/web/src/pages/assets/AssetLibrary.tsx index 4bee329b0..de1193f9d 100644 --- a/apps/web/src/pages/assets/AssetLibrary.tsx +++ b/apps/web/src/pages/assets/AssetLibrary.tsx @@ -52,140 +52,26 @@ import { type BatchOperationResult, } from "@/api/assets" import { Button, Input, Select } from "@/components/ui" +import { + type AssetItem, + type AssetKind, + type StatusType, + mapLibrary, + mapAsset, +} from "@/pages/assets/types" +import { + MAX_FILE_SIZE, + LARGE_FILE_THRESHOLD, + TYPE_FILTER_OPTIONS, + TIME_FILTER_OPTIONS, + LIBRARY_KIND_OPTIONS, + CATEGORY_OPTIONS, +} from "@/pages/assets/constants" +import { kindLabel, thumbGradient } from "@/pages/assets/utils/asset" import "./assets.css" /* ============================================================ - * 类型 - * ============================================================ */ -type AssetKind = "video" | "image" | "voice" -type StatusType = "ok" | "warn" | "bad" | "info" - -interface LibraryItem { - id: string - name: string - kind: AssetKind - count: number -} - -interface AssetItem { - id: string - name: string - kind: AssetKind - thumbUrl?: string - fileUrl?: string - status: StatusType - statusLabel: string - /** 是否处于处理中状态(上传中/入库中/诊断中) */ - loading?: boolean - duration?: string - size: number - createdAt: string -} - -/* ============================================================ - * 映射:后端 → 前端 - * ============================================================ */ - -/** 根据 mime_type 推断前端 AssetKind */ -const inferKind = (mimeType: string): AssetKind => { - if (mimeType.startsWith("video/")) return "video" - if (mimeType.startsWith("audio/")) return "voice" - return "image" -} - -/** 根据 quality_score / classification_status / asset status 推断前端状态 */ -const inferStatus = ( - score?: number, - classificationStatus?: string, - assetStatus?: string, -): { status: StatusType; label: string; loading?: boolean } => { - // 已删除素材(正常情况列表已过滤,这里是防御性处理) - if (assetStatus === "deleted") { - return { status: "bad", label: "已删除" } - } - // 处理中状态:上传中 / 入库中 / 处理中 - if ( - assetStatus === "uploading" || - assetStatus === "ingesting" || - assetStatus === "processing" || - assetStatus === "pending" - ) { - return { status: "info", label: "处理中", loading: true } - } - // 失败状态 - if (assetStatus === "error" || assetStatus === "failed") { - return { status: "bad", label: "处理失败" } - } - // 素材已就绪(status=ready)时,不应因 classification 未执行而显示"处理中" - if (assetStatus === "ready") { - if (score == null) return { status: "info", label: "待诊断" } - if (score >= 70) return { status: "ok", label: "合格" } - if (score >= 40) return { status: "warn", label: "待优化" } - return { status: "bad", label: "不合格" } - } - // 素材未就绪:classification 正在处理中 - if (classificationStatus === "processing" || classificationStatus === "pending") { - return { status: "info", label: "处理中", loading: true } - } - if (score == null) return { status: "info", label: "待诊断" } - if (score >= 70) return { status: "ok", label: "合格" } - if (score >= 40) return { status: "warn", label: "待优化" } - return { status: "bad", label: "不合格" } -} - -/** 格式化时长(秒 → mm:ss) */ -const formatDuration = (seconds: number): string => { - const m = Math.floor(seconds / 60) - const s = Math.floor(seconds % 60) - return `${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}` -} - -/** 将后端 AssetLibraryItem 映射为前端 LibraryItem */ -const mapLibrary = (item: AssetLibraryItem): LibraryItem => ({ - id: item.id, - name: item.name, - kind: item.kind || inferKind("video"), - count: item.asset_count ?? 0, -}) - -/** 将后端 ApiAssetItem 映射为前端 AssetItem */ -const mapAsset = (item: ApiAssetItem): AssetItem => { - const { status, label, loading } = inferStatus( - item.quality_score ?? undefined, - item.classification_status ?? undefined, - item.status ?? undefined, - ) - const metadata = item.metadata || {} - const kind = inferKind(item.mime_type || "") - return { - id: item.id, - name: item.name, - kind, - // 视频类型不能用 file_url 做缩略图(是视频文件, 无法渲染) - // 处理中的素材没有缩略图,显示占位符 - thumbUrl: loading - ? undefined - : (item.thumbnail_url as string | undefined) || - (metadata.thumbnail_url as string | undefined) || - (kind !== "video" ? (item.file_url as string | undefined) : undefined), - fileUrl: (item.file_url as string | undefined) || (metadata.file_url as string | undefined), - status, - statusLabel: label, - loading, - duration: metadata.duration != null ? formatDuration(metadata.duration as number) : undefined, - size: item.file_size ? +(item.file_size / (1024 * 1024)).toFixed(1) : 0, - createdAt: item.created_at ? new Date(item.created_at).toISOString().slice(0, 10) : "—", - } -} - -/* ============================================================ - * 常量 - * ============================================================ */ -const MAX_FILE_SIZE = 2048 * 1024 * 1024 -const LARGE_FILE_THRESHOLD = 100 * 1024 * 1024 - -/* ============================================================ - * 工具函数 + * 工具函数(UI 相关) * ============================================================ */ const kindIcon = (kind: AssetKind) => { switch (kind) { @@ -198,29 +84,6 @@ const kindIcon = (kind: AssetKind) => { } } -const kindLabel = (kind: AssetKind) => { - switch (kind) { - case "video": - return "视频" - case "image": - return "图片" - case "voice": - return "配音" - } -} - -/** 根据素材类型返回渐变背景 */ -const thumbGradient = (kind: AssetKind): string => { - switch (kind) { - case "video": - return "linear-gradient(135deg, #312e81 0%, #4f46e5 50%, #6366f1 100%)" - case "image": - return "linear-gradient(135deg, #78350f 0%, #d97706 50%, #f59e0b 100%)" - case "voice": - return "linear-gradient(135deg, #064e3b 0%, #059669 50%, #10b981 100%)" - } -} - /* ============================================================ * StatusPill 组件 * ============================================================ */ @@ -922,22 +785,13 @@ const AssetLibrary: React.FC = () => { value={filterType} onChange={setFilterType} style={{ width: 120 }} - options={[ - { value: "all", label: "全部类型" }, - { value: "video", label: "视频" }, - { value: "image", label: "图片" }, - ]} + options={TYPE_FILTER_OPTIONS} />