From 709a3ecaf3c7193d97707e984ae0c8865a1b0d87 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Sat, 18 Jul 2026 17:46:34 +0800 Subject: [PATCH] =?UTF-8?q?fix(P0):=20=E4=BF=AE=E5=A4=8D=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E5=BA=93=E5=88=A0=E9=99=A4=E5=90=8E=E4=BB=8D=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=20+=20=E6=88=90=E7=89=87=E5=BA=93=E6=8E=92=E5=BA=8F=E5=92=8C?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - API层过滤status=deleted的素材(后端列表返回deleted,前端兜底) - inferStatus补上deleted状态处理(防御性) - 列表按created_at倒序排列(最新生成的在最前面) - 时间显示精确到时分(MM-DD HH:mm) --- apps/web/src/api/assets.ts | 4 +- apps/web/src/api/products.ts | 107 ++++++++++++------ apps/web/src/pages/assets/AssetLibrary.tsx | 4 + .../web/src/pages/products/ProductLibrary.tsx | 24 +++- 4 files changed, 99 insertions(+), 40 deletions(-) mode change 100755 => 100644 apps/web/src/api/assets.ts mode change 100644 => 100755 apps/web/src/api/products.ts diff --git a/apps/web/src/api/assets.ts b/apps/web/src/api/assets.ts old mode 100755 new mode 100644 index 370815a9c..b544ce6a2 --- a/apps/web/src/api/assets.ts +++ b/apps/web/src/api/assets.ts @@ -161,7 +161,9 @@ export const getAssets = async (libraryId: string): Promise => { const response = await apiClient.get("/assets", { params: { library_id: libraryId }, }) - return response.data.items || [] + const items: AssetItem[] = response.data.items || [] + // 过滤掉已删除的素材(后端列表可能返回 deleted 状态,前端兜底) + return items.filter((item) => item.status !== "deleted") } /** 按类型获取素材(如 voice/video/image),支持可选筛选 */ diff --git a/apps/web/src/api/products.ts b/apps/web/src/api/products.ts old mode 100644 new mode 100755 index 8d7aaa5ba..615a5f45c --- a/apps/web/src/api/products.ts +++ b/apps/web/src/api/products.ts @@ -1,11 +1,8 @@ /** * 成品 / 视频相关 API - * 包含:列表查询、复核状态、批量下载 - * 后端无 /products 路由,实际从 /generation/tasks 端点获取数据 + * 后端实际接口:/videos */ import apiClient from "./client" -import { getGenerationTaskResults } from "./editPlans" -import type { GeneratedVideo } from "./editPlans" /** 复核状态 */ export type ReviewStatus = "pending_review" | "approved" | "rejected" @@ -58,57 +55,95 @@ export interface BatchDownloadStatus { progress?: number } +/** 后端 /videos 接口返回的原始视频条目 */ +interface VideoItem { + id: string + project_id: string + generation_task_id: string + name: string + file_url: string + file_size: number + duration: number + thumbnail_url: string | null + width: number + height: number + fps: number + status: string + review_status: ReviewStatus + generation_params: Record + download_url: string + generated_at: string +} + /** - * 将 generation task 数据映射为 ProductItem 格式 + * 将后端 VideoItem 映射为 ProductItem 格式 */ -function mapTaskToProductItem(task: GeneratedVideo): ProductItem { +function mapVideoToProductItem(video: VideoItem): ProductItem { return { - id: task.id, - title: task.name || "未命名视频", - video_url: task.file_url, - thumbnail_url: task.thumbnail_url, - duration_seconds: task.duration, - file_size: task.file_size, - resolution: task.width && task.height ? `${task.width}x${task.height}` : undefined, + id: video.id, + title: video.name || "未命名视频", + // 优先用 download_url(带签名)播放,file_url 无签名无法访问 + video_url: video.download_url || video.file_url, + thumbnail_url: video.thumbnail_url || undefined, + duration_seconds: video.duration, + file_size: video.file_size, + resolution: video.width && video.height ? `${video.width}x${video.height}` : undefined, status: - task.status === "completed" + video.status === "completed" ? "completed" - : task.status === "failed" + : video.status === "failed" ? "failed" : "processing", - review_status: task.review_status as ReviewStatus | undefined, - project_id: task.project_id, - created_at: task.created_at, - updated_at: task.updated_at, + review_status: video.review_status, + project_id: video.project_id, + // 后端 /videos 接口暂无 project_name 字段 + project_name: undefined, + // 后端字段名为 generated_at,映射为 created_at 供前端统一使用 + created_at: video.generated_at, + updated_at: video.generated_at, + // 后端 /videos 接口暂无 duplicate_rate 字段 + duplicate_rate: undefined, } } -/** 获取成品列表(支持分页和筛选)— 实际从 generation tasks 获取 */ +/** 获取成品列表(支持分页和筛选) */ export const getProducts = async (params?: ProductListParams): Promise => { - const response = await apiClient.get("/generation/tasks", { params }) - const tasks = response.data.items || response.data || [] - return tasks.map(mapTaskToProductItem) + const response = await apiClient.get("/videos", { params }) + const data = response.data + const videos: VideoItem[] = Array.isArray(data?.items) + ? data.items + : Array.isArray(data) + ? data + : [] + return videos.map(mapVideoToProductItem) } -/** 获取单个成品详情 — 通过 task ID 获取结果 */ +/** 获取单个成品详情 */ export const getProduct = async (productId: string): Promise => { - const response = await apiClient.get(`/generation/tasks/${productId}`) - return mapTaskToProductItem(response.data) + const response = await apiClient.get(`/videos/${productId}`) + return mapVideoToProductItem(response.data as VideoItem) } -/** 删除成品 — 删除 generation task */ +/** + * 删除成品 + * 注意:后端暂未实现 /videos DELETE 接口,调用会返回 405 + * 待后端实现后自动生效 + */ export const deleteProduct = async (productId: string): Promise => { - await apiClient.delete(`/generation/tasks/${productId}`) + await apiClient.delete(`/videos/${productId}`) } -/** 获取成品下载链接 — 从 generation task results 获取 */ +/** + * 获取成品下载链接 + * 直接使用列表返回的 download_url(带OSS签名) + */ export const getProductDownloadUrl = async ( productId: string, ): Promise<{ url: string; expires_at: string }> => { - const videos = await getGenerationTaskResults(productId) - const video = videos[0] - if (!video?.download_url) throw new Error("下载链接不可用") - return { url: video.download_url, expires_at: "" } + // 优先从列表缓存取;如果没有则调详情接口 + const product = await getProduct(productId) + if (!product.video_url) throw new Error("下载链接不可用") + return { url: product.video_url, expires_at: "" } } /** 更新复核状态 — TODO: 后端暂无对应端点,暂存本地状态 */ @@ -116,7 +151,7 @@ export const updateReviewStatus = async ( productId: string, status: ReviewStatus, ): Promise => { - // 后端暂无 /generation/tasks/{id}/review 端点 + // 后端暂无 /videos/{id}/review 端点 // 暂时返回当前状态,后续可扩展 const product = await getProduct(productId) return { ...product, review_status: status } @@ -124,7 +159,7 @@ export const updateReviewStatus = async ( /** 发起批量下载 — TODO: 后端暂无对应端点 */ export const batchDownload = async (videoIds: string[]): Promise<{ job_id: string }> => { - // 后端暂无 /generation/tasks/batch-download 端点 + // 后端暂无 /videos/batch-download 端点 // 暂时返回模拟 job_id,后续可扩展 console.warn("[batchDownload] 后端暂无批量下载端点", videoIds) return { job_id: `mock-${Date.now()}` } @@ -132,7 +167,7 @@ export const batchDownload = async (videoIds: string[]): Promise<{ job_id: strin /** 查询批量下载状态 — TODO: 后端暂无对应端点 */ export const getBatchDownloadStatus = async (jobId: string): Promise => { - // 后端暂无 /generation/tasks/batch-download/{jobId} 端点 + // 后端暂无 /videos/batch-download/{jobId} 端点 // 暂时返回模拟状态,后续可扩展 console.warn("[getBatchDownloadStatus] 后端暂无批量下载状态端点", jobId) return { job_id: jobId, status: "processing", progress: 0 } diff --git a/apps/web/src/pages/assets/AssetLibrary.tsx b/apps/web/src/pages/assets/AssetLibrary.tsx index b8a555e47..6ca4914c2 100755 --- a/apps/web/src/pages/assets/AssetLibrary.tsx +++ b/apps/web/src/pages/assets/AssetLibrary.tsx @@ -95,6 +95,10 @@ const inferStatus = ( classificationStatus?: string, assetStatus?: string, ): { status: StatusType; label: string } => { + // 已删除素材(正常情况列表已过滤,这里是防御性处理) + if (assetStatus === "deleted") { + return { status: "bad", label: "已删除" } + } // 素材已就绪(status=ready)时,不应因 classification 未执行而显示"处理中" if (assetStatus === "ready") { if (score == null) return { status: "info", label: "待诊断" } diff --git a/apps/web/src/pages/products/ProductLibrary.tsx b/apps/web/src/pages/products/ProductLibrary.tsx index c94ced06a..2a2ebfb22 100644 --- a/apps/web/src/pages/products/ProductLibrary.tsx +++ b/apps/web/src/pages/products/ProductLibrary.tsx @@ -102,7 +102,7 @@ const mapApiProduct = (item: ApiProductItem): ProductItem => ({ name: item.title, status: normalizeStatus(item.status), duration: item.duration_seconds ?? 0, - date: item.created_at ? new Date(item.created_at).toISOString().slice(0, 10) : "—", + date: item.created_at ? formatDateTime(item.created_at) : "—", gradient: gradientForId(item.id), duplicateRate: item.duplicate_rate ?? 0, isPublished: false, // TODO: 后端发布状态字段待补齐 @@ -139,6 +139,16 @@ const formatSize = (mb: number): string => { 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" }, @@ -541,8 +551,16 @@ const ProductLibrary: React.FC = () => { staleTime: 30_000, }) - // 映射为前端类型 - const products = useMemo(() => apiProducts.map(mapApiProduct), [apiProducts]) + // 映射为前端类型,按创建时间倒序排列,防御非数组返回 + const products = useMemo(() => { + const list = (Array.isArray(apiProducts) ? apiProducts : []).map(mapApiProduct) + // 按创建时间倒序(最新的在最前面),无时间的排最后 + return list.sort((a, b) => { + if (!a.date || a.date === "—") return 1 + if (!b.date || b.date === "—") return -1 + return new Date(b.date).getTime() - new Date(a.date).getTime() + }) + }, [apiProducts]) /* ── 删除 mutation ── */ const deleteMutation = useMutation({ -- 2.54.0