diff --git a/src/App.tsx b/src/App.tsx index 547d291..2275df9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -19,7 +19,7 @@ function App() { return (
- +
Outfit 管理
diff --git a/src/lib/indexeddb.ts b/src/lib/indexeddb.ts index 925367b..12955d1 100644 --- a/src/lib/indexeddb.ts +++ b/src/lib/indexeddb.ts @@ -38,3 +38,22 @@ export async function getAllVideoTasks(): Promise { req.onerror = () => reject(req.error); }); } + +export async function updateVideoTask(id: string, partial: Partial<{ img_url: string; prompt: string; create_time: string; task_id: string; video_url?: string }>) { + const db = await openDB(); + return new Promise((resolve, reject) => { + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + const req = store.get(id); + req.onsuccess = () => { + const task = req.result; + if (task) { + const updated = { ...task, ...partial }; + store.put(updated); + } + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }; + req.onerror = () => reject(req.error); + }); +} diff --git a/src/pages/TryOnPage/index.tsx b/src/pages/TryOnPage/index.tsx index 735aaf8..63ce4d5 100644 --- a/src/pages/TryOnPage/index.tsx +++ b/src/pages/TryOnPage/index.tsx @@ -218,7 +218,7 @@ const TryOnPage: React.FC = () => { ); })}
diff --git a/src/pages/TryOnVideoTaskPage/index.tsx b/src/pages/TryOnVideoTaskPage/index.tsx index 93327c3..1960b69 100644 --- a/src/pages/TryOnVideoTaskPage/index.tsx +++ b/src/pages/TryOnVideoTaskPage/index.tsx @@ -1,9 +1,11 @@ import React, { useEffect, useState } from 'react'; -import { getAllVideoTasks } from '@/lib/indexeddb'; +import { getAllVideoTasks, updateVideoTask } from '@/lib/indexeddb'; import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { api } from '@/lib/api'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; +import { Checkbox } from '@/components/ui/checkbox'; +import { toast } from 'sonner'; interface LocalVideoTask { id: string; @@ -11,6 +13,7 @@ interface LocalVideoTask { prompt: string; create_time: string; task_id: string; + video_url?: string; } interface TaskStatusMap { @@ -27,6 +30,7 @@ const TryOnVideoTaskPage: React.FC = () => { const [dialogOpen, setDialogOpen] = useState(false); const [loading, setLoading] = useState(true); const [statusLoading, setStatusLoading] = useState(false); + const [selectedIds, setSelectedIds] = useState([]); // 1. 读取本地任务 useEffect(() => { @@ -39,7 +43,21 @@ const TryOnVideoTaskPage: React.FC = () => { // 2. 查询后端任务状态 useEffect(() => { if (!tasks.length) return; - const job_ids = tasks.map(t => t.task_id).filter(Boolean); + // 先补全本地已完成的任务到 statusMap + setStatusMap(prev => { + const newMap = { ...prev }; + tasks.forEach(t => { + if (t.video_url) { + newMap[t.task_id] = { status: 'finished', video_url: t.video_url }; + } + }); + return newMap; + }); + // 只查本地没有 video_url 的任务 + const job_ids = tasks + .filter(t => !t.video_url) + .map(t => t.task_id) + .filter(Boolean); if (!job_ids.length) return; setStatusLoading(true); api.Service.batchQueryVideoStatusApiJmBatchQueryStatusPost({ requestBody: { job_ids } }) @@ -49,6 +67,13 @@ const TryOnVideoTaskPage: React.FC = () => { if (res?.data) { (res.data.finished || []).forEach((item: any) => { map[item.job_id] = { status: 'finished', video_url: item.video_url }; + if (item.video_url) { + const localTask = tasks.find(t => t.task_id === item.job_id); + if (localTask && !localTask.video_url) { + updateVideoTask(localTask.id, { video_url: item.video_url }); + setTasks(prev => prev.map(t => (t.id === localTask.id ? { ...t, video_url: item.video_url } : t))); + } + } }); (res.data.running || []).forEach((job_id: string) => { map[job_id] = { status: 'running' }; @@ -57,7 +82,7 @@ const TryOnVideoTaskPage: React.FC = () => { map[job_id] = { status: 'failed' }; }); } - setStatusMap(map); + setStatusMap(prev => ({ ...prev, ...map })); }) .finally(() => setStatusLoading(false)); }, [tasks]); @@ -81,13 +106,62 @@ const TryOnVideoTaskPage: React.FC = () => { return 未知; }; + // 计算可选的任务ID(已完成且有video_url) + const downloadableIds = tasks.filter(t => statusMap[t.task_id]?.status === 'finished' && statusMap[t.task_id]?.video_url).map(t => t.id); + + const isAllSelected = downloadableIds.length > 0 && downloadableIds.every(id => selectedIds.includes(id)); + + const handleSelectAll = () => { + if (isAllSelected) { + setSelectedIds([]); + } else { + setSelectedIds(downloadableIds); + } + }; + + const handleSelectOne = (id: string) => { + setSelectedIds(prev => (prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])); + }; + + // 批量下载逻辑 + const handleBatchDownload = async () => { + if (selectedIds.length === 0) { + toast.warning('请选择要下载的视频任务'); + return; + } + const selectedTasks = tasks.filter(t => selectedIds.includes(t.id)); + let count = 0; + for (const task of selectedTasks) { + const s = statusMap[task.task_id]; + if (s?.status === 'finished' && s.video_url) { + try { + // 直接用 a 标签下载,避免 CORS + const a = document.createElement('a'); + a.href = s.video_url; + a.download = `${task.id}.mp4`; + a.target = '_blank'; // 可选:新标签页 + document.body.appendChild(a); + a.click(); + a.remove(); + count++; + } catch (e) { + toast.error(`下载失败: ${task.id}`); + } + } + } + toast.success(`成功下载${count}个视频`); + }; + return (

视频任务进度

-
+
+
@@ -103,6 +177,9 @@ const TryOnVideoTaskPage: React.FC = () => { + + + 图片 ID 任务ID @@ -112,30 +189,42 @@ const TryOnVideoTaskPage: React.FC = () => { - {tasks.map(task => ( - - - {task.img_url ? ( - img { - setPreviewImg(task.img_url); - setDialogOpen(true); - }} + {tasks.map(task => { + const s = statusMap[task.task_id]; + const canDownload = s?.status === 'finished' && s.video_url; + return ( + + + handleSelectOne(task.id)} + disabled={!canDownload} + aria-label='选择任务' /> - ) : ( - - )} - - {task.id} - {task.task_id} - {task.prompt} - {task.create_time ? new Date(task.create_time).toLocaleString() : '-'} - {statusLoading ? '查询中...' : renderStatus(task)} - - ))} + + + {task.img_url ? ( + img { + setPreviewImg(task.img_url); + setDialogOpen(true); + }} + /> + ) : ( + + )} + + {task.id} + {task.task_id} + {task.prompt} + {task.create_time ? new Date(task.create_time).toLocaleString() : '-'} + {statusLoading ? '查询中...' : renderStatus(task)} + + ); + })}