feat: 修复
This commit is contained in:
parent
4d210760c9
commit
03431e0e52
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue