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 (
<SidebarProvider>
<div className='flex min-h-screen min-w-screen'>
<Sidebar className='h-screen'>
<Sidebar className='h-screen pl-2'>
<SidebarContent>
<div className='text-xl font-bold mb-8 px-4 pt-4'>Outfit </div>
<SidebarMenu>

View File

@ -38,3 +38,22 @@ export async function getAllVideoTasks(): Promise<any[]> {
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>
<input ref={fileInputRef} type='file' accept='image/*' multiple className='hidden' onChange={handleImageChange} />
</div>

View File

@ -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<string[]>([]);
// 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 <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 (
<div className='p-6'>
<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>
<Button variant='default' onClick={handleBatchDownload} disabled={selectedIds.length === 0}>
</Button>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<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'>
<TableHeader>
<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'>ID</TableHead>
<TableHead className='p-2'>ID</TableHead>
@ -112,30 +189,42 @@ const TryOnVideoTaskPage: React.FC = () => {
</TableRow>
</TableHeader>
<TableBody>
{tasks.map(task => (
<TableRow key={task.id}>
<TableCell className='p-2 text-center'>
{task.img_url ? (
<img
src={task.img_url}
alt='img'
className='w-16 h-16 object-cover rounded cursor-pointer transition hover:scale-105 inline-block'
onClick={() => {
setPreviewImg(task.img_url);
setDialogOpen(true);
}}
{tasks.map(task => {
const s = statusMap[task.task_id];
const canDownload = s?.status === 'finished' && s.video_url;
return (
<TableRow key={task.id}>
<TableCell className='p-2 text-center'>
<Checkbox
checked={selectedIds.includes(task.id)}
onCheckedChange={() => handleSelectOne(task.id)}
disabled={!canDownload}
aria-label='选择任务'
/>
) : (
<span className='text-gray-400'></span>
)}
</TableCell>
<TableCell>{task.id}</TableCell>
<TableCell>{task.task_id}</TableCell>
<TableCell>{task.prompt}</TableCell>
<TableCell>{task.create_time ? new Date(task.create_time).toLocaleString() : '-'}</TableCell>
<TableCell>{statusLoading ? '查询中...' : renderStatus(task)}</TableCell>
</TableRow>
))}
</TableCell>
<TableCell className='p-2 text-center'>
{task.img_url ? (
<img
src={task.img_url}
alt='img'
className='w-16 h-16 object-cover rounded cursor-pointer transition hover:scale-105 inline-block'
onClick={() => {
setPreviewImg(task.img_url);
setDialogOpen(true);
}}
/>
) : (
<span className='text-gray-400'></span>
)}
</TableCell>
<TableCell>{task.id}</TableCell>
<TableCell>{task.task_id}</TableCell>
<TableCell>{task.prompt}</TableCell>
<TableCell>{task.create_time ? new Date(task.create_time).toLocaleString() : '-'}</TableCell>
<TableCell>{statusLoading ? '查询中...' : renderStatus(task)}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>