/** * 成品 / 视频相关 API * 后端实际接口:/videos */ import apiClient from "./client" /** 复核状态 */ export type ReviewStatus = "pending_review" | "approved" | "rejected" /** 成品条目 */ export interface ProductItem { id: string title: string video_url?: string thumbnail_url?: string duration_seconds?: number file_size?: number resolution?: string status: "processing" | "completed" | "failed" /** 复核状态 */ review_status?: ReviewStatus /** 所属项目 ID */ project_id?: string /** 所属项目名称 */ project_name?: string /** 查重率(百分比) */ duplicate_rate?: number created_at?: string updated_at?: string } /** 列表查询参数 */ export interface ProductListParams { page?: number page_size?: number project_id?: string review_status?: ReviewStatus | "all" } /** 分页响应 */ export interface ProductListResponse { items: ProductItem[] total: number page: number page_size: number } /** 批量下载任务状态 */ export interface BatchDownloadStatus { job_id: string status: "processing" | "completed" | "failed" /** 完成后返回的下载 URL */ download_url?: string /** 进度百分比 */ 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 } /** * 将后端 VideoItem 映射为 ProductItem 格式 */ function mapVideoToProductItem(video: VideoItem): ProductItem { return { 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: video.status === "completed" ? "completed" : video.status === "failed" ? "failed" : "processing", 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, } } /** 获取成品列表(支持分页和筛选) */ export const getProducts = async (params?: ProductListParams): Promise => { 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) } /** 获取单个成品详情 */ export const getProduct = async (productId: string): Promise => { const response = await apiClient.get(`/videos/${productId}`) return mapVideoToProductItem(response.data as VideoItem) } /** * 删除成品 * 注意:后端暂未实现 /videos DELETE 接口,调用会返回 405 * 待后端实现后自动生效 */ export const deleteProduct = async (productId: string): Promise => { await apiClient.delete(`/videos/${productId}`) } /** * 获取成品下载链接 * 直接使用列表返回的 download_url(带OSS签名) */ export const getProductDownloadUrl = async ( productId: string, ): Promise<{ url: string; expires_at: string }> => { // 优先从列表缓存取;如果没有则调详情接口 const product = await getProduct(productId) if (!product.video_url) throw new Error("下载链接不可用") return { url: product.video_url, expires_at: "" } } /** 更新复核状态 — TODO: 后端暂无对应端点,暂存本地状态 */ export const updateReviewStatus = async ( productId: string, status: ReviewStatus, ): Promise => { // 后端暂无 /videos/{id}/review 端点 // 暂时返回当前状态,后续可扩展 const product = await getProduct(productId) return { ...product, review_status: status } } /** 发起批量下载 — TODO: 后端暂无对应端点 */ export const batchDownload = async (videoIds: string[]): Promise<{ job_id: string }> => { // 后端暂无 /videos/batch-download 端点 // 暂时返回模拟 job_id,后续可扩展 console.warn("[batchDownload] 后端暂无批量下载端点", videoIds) return { job_id: `mock-${Date.now()}` } } /** 查询批量下载状态 — TODO: 后端暂无对应端点 */ export const getBatchDownloadStatus = async (jobId: string): Promise => { // 后端暂无 /videos/batch-download/{jobId} 端点 // 暂时返回模拟状态,后续可扩展 console.warn("[getBatchDownloadStatus] 后端暂无批量下载状态端点", jobId) return { job_id: jobId, status: "processing", progress: 0 } }