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