feat: 修复

This commit is contained in:
张德辉 2025-06-27 19:11:40 +08:00
parent 4d210760c9
commit 03431e0e52
4 changed files with 137 additions and 29 deletions

View File

@ -19,7 +19,7 @@ function App() {
return ( return (
<SidebarProvider> <SidebarProvider>
<div className='flex min-h-screen min-w-screen'> <div className='flex min-h-screen min-w-screen'>
<Sidebar className='h-screen'> <Sidebar className='h-screen pl-2'>
<SidebarContent> <SidebarContent>
<div className='text-xl font-bold mb-8 px-4 pt-4'>Outfit </div> <div className='text-xl font-bold mb-8 px-4 pt-4'>Outfit </div>
<SidebarMenu> <SidebarMenu>

View File

@ -38,3 +38,22 @@ export async function getAllVideoTasks(): Promise<any[]> {
req.onerror = () => reject(req.error); 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<void>((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);
});
}

View File

@ -218,7 +218,7 @@ const TryOnPage: React.FC = () => {
); );
})} })}
<Button type='button' onClick={() => fileInputRef.current?.click()} className='w-40 self-center' disabled={clothing_images.length >= 5}> <Button type='button' onClick={() => fileInputRef.current?.click()} className='w-40 self-center' disabled={clothing_images.length >= 5}>
</Button> </Button>
<input ref={fileInputRef} type='file' accept='image/*' multiple className='hidden' onChange={handleImageChange} /> <input ref={fileInputRef} type='file' accept='image/*' multiple className='hidden' onChange={handleImageChange} />
</div> </div>

View File

@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react'; 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 { Dialog, DialogContent } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { api } from '@/lib/api'; import { api } from '@/lib/api';
import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table'; import { Table, TableHeader, TableBody, TableRow, TableHead, TableCell } from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'sonner';
interface LocalVideoTask { interface LocalVideoTask {
id: string; id: string;
@ -11,6 +13,7 @@ interface LocalVideoTask {
prompt: string; prompt: string;
create_time: string; create_time: string;
task_id: string; task_id: string;
video_url?: string;
} }
interface TaskStatusMap { interface TaskStatusMap {
@ -27,6 +30,7 @@ const TryOnVideoTaskPage: React.FC = () => {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [statusLoading, setStatusLoading] = useState(false); const [statusLoading, setStatusLoading] = useState(false);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
// 1. 读取本地任务 // 1. 读取本地任务
useEffect(() => { useEffect(() => {
@ -39,7 +43,21 @@ const TryOnVideoTaskPage: React.FC = () => {
// 2. 查询后端任务状态 // 2. 查询后端任务状态
useEffect(() => { useEffect(() => {
if (!tasks.length) return; 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; if (!job_ids.length) return;
setStatusLoading(true); setStatusLoading(true);
api.Service.batchQueryVideoStatusApiJmBatchQueryStatusPost({ requestBody: { job_ids } }) api.Service.batchQueryVideoStatusApiJmBatchQueryStatusPost({ requestBody: { job_ids } })
@ -49,6 +67,13 @@ const TryOnVideoTaskPage: React.FC = () => {
if (res?.data) { if (res?.data) {
(res.data.finished || []).forEach((item: any) => { (res.data.finished || []).forEach((item: any) => {
map[item.job_id] = { status: 'finished', video_url: item.video_url }; 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) => { (res.data.running || []).forEach((job_id: string) => {
map[job_id] = { status: 'running' }; map[job_id] = { status: 'running' };
@ -57,7 +82,7 @@ const TryOnVideoTaskPage: React.FC = () => {
map[job_id] = { status: 'failed' }; map[job_id] = { status: 'failed' };
}); });
} }
setStatusMap(map); setStatusMap(prev => ({ ...prev, ...map }));
}) })
.finally(() => setStatusLoading(false)); .finally(() => setStatusLoading(false));
}, [tasks]); }, [tasks]);
@ -81,13 +106,62 @@ const TryOnVideoTaskPage: React.FC = () => {
return <span className='text-gray-400'></span>; return <span className='text-gray-400'></span>;
}; };
// 计算可选的任务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 ( return (
<div className='p-6'> <div className='p-6'>
<h2 className='text-xl font-bold mb-4'></h2> <h2 className='text-xl font-bold mb-4'></h2>
<div className='mb-6 flex justify-end'> <div className='mb-6 flex justify-between items-center'>
<Button variant='outline' onClick={() => window.location.reload()}> <Button variant='outline' onClick={() => window.location.reload()}>
</Button> </Button>
<Button variant='default' onClick={handleBatchDownload} disabled={selectedIds.length === 0}>
</Button>
</div> </div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent style={{ maxHeight: '80vh' }} className='flex items-center justify-center' showCloseButton={false}> <DialogContent style={{ maxHeight: '80vh' }} className='flex items-center justify-center' showCloseButton={false}>
@ -103,6 +177,9 @@ const TryOnVideoTaskPage: React.FC = () => {
<Table className='min-w-full text-sm'> <Table className='min-w-full text-sm'>
<TableHeader> <TableHeader>
<TableRow className='bg-gray-100'> <TableRow className='bg-gray-100'>
<TableHead className='p-2 text-center'>
<Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label='全选' disabled={downloadableIds.length === 0} />
</TableHead>
<TableHead className='p-2 text-center'></TableHead> <TableHead className='p-2 text-center'></TableHead>
<TableHead className='p-2'>ID</TableHead> <TableHead className='p-2'>ID</TableHead>
<TableHead className='p-2'>ID</TableHead> <TableHead className='p-2'>ID</TableHead>
@ -112,8 +189,19 @@ const TryOnVideoTaskPage: React.FC = () => {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{tasks.map(task => ( {tasks.map(task => {
const s = statusMap[task.task_id];
const canDownload = s?.status === 'finished' && s.video_url;
return (
<TableRow key={task.id}> <TableRow key={task.id}>
<TableCell className='p-2 text-center'>
<Checkbox
checked={selectedIds.includes(task.id)}
onCheckedChange={() => handleSelectOne(task.id)}
disabled={!canDownload}
aria-label='选择任务'
/>
</TableCell>
<TableCell className='p-2 text-center'> <TableCell className='p-2 text-center'>
{task.img_url ? ( {task.img_url ? (
<img <img
@ -135,7 +223,8 @@ const TryOnVideoTaskPage: React.FC = () => {
<TableCell>{task.create_time ? new Date(task.create_time).toLocaleString() : '-'}</TableCell> <TableCell>{task.create_time ? new Date(task.create_time).toLocaleString() : '-'}</TableCell>
<TableCell>{statusLoading ? '查询中...' : renderStatus(task)}</TableCell> <TableCell>{statusLoading ? '查询中...' : renderStatus(task)}</TableCell>
</TableRow> </TableRow>
))} );
})}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>