diff --git a/apps/web/src/pages/products/ProductLibrary.tsx b/apps/web/src/pages/products/ProductLibrary.tsx old mode 100644 new mode 100755 index 86ae826ce..d8d122b1e --- a/apps/web/src/pages/products/ProductLibrary.tsx +++ b/apps/web/src/pages/products/ProductLibrary.tsx @@ -32,139 +32,9 @@ import { type ReviewStatus, } from "@/api/products" import "./products.css" - -/* ============================================================ - * 类型 & 常量 - * ============================================================ */ -type ProductStatus = "completed" | "processing" | "review" | "failed" - -interface ProductItem { - id: string - name: string - status: ProductStatus - duration: number // 秒 - date: string - gradient: string - duplicateRate: number // 查重率百分比 - isPublished: boolean - resolution: string - fileSize: number // MB - videoUrl?: string - thumbnailUrl?: string - /** 复核状态 */ - reviewStatus?: ReviewStatus - /** 所属项目 ID */ - projectId?: string - /** 所属项目名称 */ - projectName?: string -} - -/** 将后端 status 映射为前端 ProductStatus */ -const normalizeStatus = (s: string): ProductStatus => { - const map: Record = { - completed: "completed", - succeeded: "completed", - processing: "processing", - running: "processing", - review: "review", - failed: "failed", - error: "failed", - } - return map[s] ?? "processing" -} - -/** 渐变色列表(按 id hash 选取) */ -const GRADIENTS = [ - "linear-gradient(135deg, #1e1b4b 0%, #4f46e5 50%, #818cf8 100%)", - "linear-gradient(135deg, #831843 0%, #db2777 50%, #f472b6 100%)", - "linear-gradient(135deg, #064e3b 0%, #059669 50%, #34d399 100%)", - "linear-gradient(135deg, #78350f 0%, #d97706 50%, #fbbf24 100%)", - "linear-gradient(135deg, #1e3a5f 0%, #2563eb 50%, #60a5fa 100%)", - "linear-gradient(135deg, #581c87 0%, #9333ea 50%, #c084fc 100%)", - "linear-gradient(135deg, #7f1d1d 0%, #dc2626 50%, #f87171 100%)", - "linear-gradient(135deg, #134e4a 0%, #0d9488 50%, #5eead4 100%)", - "linear-gradient(135deg, #1e1b4b 0%, #6366f1 50%, #a5b4fc 100%)", - "linear-gradient(135deg, #3b0764 0%, #7c3aed 50%, #a78bfa 100%)", - "linear-gradient(135deg, #052e16 0%, #16a34a 50%, #86efac 100%)", - "linear-gradient(135deg, #450a0e 0%, #e11d48 50%, #fb7185 100%)", -] - -/** 根据 id 生成稳定的渐变色 */ -const gradientForId = (id: string): string => { - let hash = 0 - for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) | 0 - return GRADIENTS[Math.abs(hash) % GRADIENTS.length] -} - -/** 将后端 ProductItem 映射为前端 ProductItem */ -const mapApiProduct = (item: ApiProductItem): ProductItem => ({ - id: item.id, - name: item.title, - status: normalizeStatus(item.status), - duration: item.duration_seconds ?? 0, - date: item.created_at ? formatDateTime(item.created_at) : "—", - gradient: gradientForId(item.id), - duplicateRate: item.duplicate_rate ?? 0, - isPublished: false, // TODO: 后端发布状态字段待补齐 - resolution: item.resolution ?? "1080×1920", - fileSize: item.file_size ? +(item.file_size / (1024 * 1024)).toFixed(1) : 0, - videoUrl: item.video_url, - thumbnailUrl: item.thumbnail_url, - reviewStatus: item.review_status, - projectId: item.project_id, - projectName: item.project_name, -}) - -/* ============================================================ - * 工具函数 - * ============================================================ */ -const statusConfig: Record = { - completed: { text: "已完成", className: "completed" }, - processing: { text: "处理中", className: "processing" }, - review: { text: "待复核", className: "review" }, - failed: { text: "失败", className: "failed" }, -} - -/** 格式化时间 mm:ss */ -const formatTime = (seconds: number): string => { - const mins = Math.floor(seconds / 60) - const secs = Math.floor(seconds % 60) - return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}` -} - -/** 格式化文件大小 */ -const formatSize = (mb: number): string => { - if (mb <= 0) return "-" - if (mb < 1) return `${(mb * 1024).toFixed(0)} KB` - return `${mb.toFixed(1)} MB` -} - -/** 格式化日期时间(MM-DD HH:mm) */ -const formatDateTime = (isoString: string): string => { - const d = new Date(isoString) - const month = (d.getMonth() + 1).toString().padStart(2, "0") - const day = d.getDate().toString().padStart(2, "0") - const hour = d.getHours().toString().padStart(2, "0") - const minute = d.getMinutes().toString().padStart(2, "0") - return `${month}-${day} ${hour}:${minute}` -} - -/** 复核状态配置 */ -const reviewStatusConfig: Record = { - pending_review: { text: "待复核", className: "review-pending" }, - approved: { text: "已通过", className: "review-approved" }, - rejected: { text: "需修改", className: "review-rejected" }, -} - -/** 复核状态循环顺序 */ -const REVIEW_STATUS_CYCLE: ReviewStatus[] = ["pending_review", "approved", "rejected"] - -/** 获取下一个复核状态 */ -const getNextReviewStatus = (current?: ReviewStatus): ReviewStatus => { - if (!current) return "approved" - const idx = REVIEW_STATUS_CYCLE.indexOf(current) - return REVIEW_STATUS_CYCLE[(idx + 1) % REVIEW_STATUS_CYCLE.length] -} +import type { ProductItem } from "./types" +import { statusConfig, reviewStatusConfig } from "./constants" +import { formatTime, formatSize, mapApiProduct, getNextReviewStatus } from "./utils" /* ============================================================ * ProductCard 组件 diff --git a/apps/web/src/pages/products/constants.ts b/apps/web/src/pages/products/constants.ts new file mode 100644 index 000000000..9d08113e4 --- /dev/null +++ b/apps/web/src/pages/products/constants.ts @@ -0,0 +1,34 @@ +import type { ReviewStatus } from "@/api/products" +import type { ProductStatus } from "./types" + +/** 渐变色列表(按 id hash 选取) */ +export const GRADIENTS = [ + "linear-gradient(135deg, #1e1b4b 0%, #4f46e5 50%, #818cf8 100%)", + "linear-gradient(135deg, #831843 0%, #db2777 50%, #f472b6 100%)", + "linear-gradient(135deg, #064e3b 0%, #059669 50%, #34d399 100%)", + "linear-gradient(135deg, #78350f 0%, #d97706 50%, #fbbf24 100%)", + "linear-gradient(135deg, #1e3a5f 0%, #2563eb 50%, #60a5fa 100%)", + "linear-gradient(135deg, #581c87 0%, #9333ea 50%, #c084fc 100%)", + "linear-gradient(135deg, #7f1d1d 0%, #dc2626 50%, #f87171 100%)", + "linear-gradient(135deg, #134e4a 0%, #0d9488 50%, #5eead4 100%)", + "linear-gradient(135deg, #1e1b4b 0%, #6366f1 50%, #a5b4fc 100%)", + "linear-gradient(135deg, #3b0764 0%, #7c3aed 50%, #a78bfa 100%)", + "linear-gradient(135deg, #052e16 0%, #16a34a 50%, #86efac 100%)", + "linear-gradient(135deg, #450a0e 0%, #e11d48 50%, #fb7185 100%)", +] + +export const statusConfig: Record = { + completed: { text: "已完成", className: "completed" }, + processing: { text: "处理中", className: "processing" }, + review: { text: "待复核", className: "review" }, + failed: { text: "失败", className: "failed" }, +} + +export const reviewStatusConfig: Record = { + pending_review: { text: "待复核", className: "review-pending" }, + approved: { text: "已通过", className: "review-approved" }, + rejected: { text: "需修改", className: "review-rejected" }, +} + +/** 复核状态循环顺序 */ +export const REVIEW_STATUS_CYCLE: ReviewStatus[] = ["pending_review", "approved", "rejected"] diff --git a/apps/web/src/pages/products/types.ts b/apps/web/src/pages/products/types.ts new file mode 100644 index 000000000..a3e7cd382 --- /dev/null +++ b/apps/web/src/pages/products/types.ts @@ -0,0 +1,24 @@ +import type { ReviewStatus } from "@/api/products" + +export type ProductStatus = "completed" | "processing" | "review" | "failed" + +export interface ProductItem { + id: string + name: string + status: ProductStatus + duration: number // 秒 + date: string + gradient: string + duplicateRate: number // 查重率百分比 + isPublished: boolean + resolution: string + fileSize: number // MB + videoUrl?: string + thumbnailUrl?: string + /** 复核状态 */ + reviewStatus?: ReviewStatus + /** 所属项目 ID */ + projectId?: string + /** 所属项目名称 */ + projectName?: string +} diff --git a/apps/web/src/pages/products/utils/index.ts b/apps/web/src/pages/products/utils/index.ts new file mode 100644 index 000000000..5fc03116f --- /dev/null +++ b/apps/web/src/pages/products/utils/index.ts @@ -0,0 +1,75 @@ +import type { ProductItem as ApiProductItem } from "@/api/products" +import type { ProductItem, ProductStatus } from "../types" +import { GRADIENTS, REVIEW_STATUS_CYCLE } from "../constants" +import type { ReviewStatus } from "@/api/products" + +/** 格式化时间 mm:ss */ +export const formatTime = (seconds: number): string => { + const mins = Math.floor(seconds / 60) + const secs = Math.floor(seconds % 60) + return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}` +} + +/** 格式化文件大小 */ +export const formatSize = (mb: number): string => { + if (mb <= 0) return "-" + if (mb < 1) return `${(mb * 1024).toFixed(0)} KB` + return `${mb.toFixed(1)} MB` +} + +/** 格式化日期时间(MM-DD HH:mm) */ +export const formatDateTime = (isoString: string): string => { + const d = new Date(isoString) + const month = (d.getMonth() + 1).toString().padStart(2, "0") + const day = d.getDate().toString().padStart(2, "0") + const hour = d.getHours().toString().padStart(2, "0") + const minute = d.getMinutes().toString().padStart(2, "0") + return `${month}-${day} ${hour}:${minute}` +} + +/** 将后端 status 映射为前端 ProductStatus */ +export const normalizeStatus = (s: string): ProductStatus => { + const map: Record = { + completed: "completed", + succeeded: "completed", + processing: "processing", + running: "processing", + review: "review", + failed: "failed", + error: "failed", + } + return map[s] ?? "processing" +} + +/** 根据 id 生成稳定的渐变色 */ +export const gradientForId = (id: string): string => { + let hash = 0 + for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) | 0 + return GRADIENTS[Math.abs(hash) % GRADIENTS.length] +} + +/** 将后端 ProductItem 映射为前端 ProductItem */ +export const mapApiProduct = (item: ApiProductItem): ProductItem => ({ + id: item.id, + name: item.title, + status: normalizeStatus(item.status), + duration: item.duration_seconds ?? 0, + date: item.created_at ? formatDateTime(item.created_at) : "—", + gradient: gradientForId(item.id), + duplicateRate: item.duplicate_rate ?? 0, + isPublished: false, // TODO: 后端发布状态字段待补齐 + resolution: item.resolution ?? "1080×1920", + fileSize: item.file_size ? +(item.file_size / (1024 * 1024)).toFixed(1) : 0, + videoUrl: item.video_url, + thumbnailUrl: item.thumbnail_url, + reviewStatus: item.review_status, + projectId: item.project_id, + projectName: item.project_name, +}) + +/** 获取下一个复核状态 */ +export const getNextReviewStatus = (current?: ReviewStatus): ReviewStatus => { + if (!current) return "approved" + const idx = REVIEW_STATUS_CYCLE.indexOf(current) + return REVIEW_STATUS_CYCLE[(idx + 1) % REVIEW_STATUS_CYCLE.length] +} diff --git a/apps/web/src/test/pages/products/smoke.test.tsx b/apps/web/src/test/pages/products/smoke.test.tsx new file mode 100644 index 000000000..f5f347a8d --- /dev/null +++ b/apps/web/src/test/pages/products/smoke.test.tsx @@ -0,0 +1,23 @@ +/** + * ProductLibrary 模块 smoke test + * 建立完整依赖链,确保 vitest related 模式能匹配到 + * products 目录下所有文件的改动(包括子组件、Hook 和工具函数) + */ +import { describe, it, expect } from "vitest" + +// 主组件 +import "@/pages/products/ProductLibrary" + +// 类型与常量(Phase 1) +import "@/pages/products/types" +import "@/pages/products/constants" + +// 工具函数(Phase 1) +import "@/pages/products/utils/index" + +describe("ProductLibrary module smoke test", () => { + it("should load all product modules", () => { + // 纯模块加载测试,确保所有组件/工具函数能正常 import + expect(true).toBe(true) + }) +})