247 lines
9.8 KiB
TypeScript
247 lines
9.8 KiB
TypeScript
import React, { useCallback, useEffect, useState } from 'react';
|
||
import { useApi } from '@/hooks/useApi';
|
||
import { api } from '@/lib/api';
|
||
import type { VideoDataPaginationRequest } from '@/api/models/VideoDataPaginationRequest';
|
||
import type { PaginationResponse } from '@/api/models/PaginationResponse';
|
||
import { Table, TableHeader, TableBody, TableHead, TableRow, TableCell } from '@/components/ui/table';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Skeleton } from '@/components/ui/skeleton';
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||
import { Checkbox } from '@/components/ui/checkbox';
|
||
import { ACTION_PROMPTS } from '@/lib/prompt';
|
||
import { toast } from 'sonner';
|
||
import { addVideoTasks } from '@/lib/indexeddb';
|
||
import type { BatchVideoTaskRequest } from '@/api';
|
||
|
||
const PAGE_SIZE = 10;
|
||
|
||
const promptGetter = (list: string[]) => {
|
||
let index = 0;
|
||
return () => {
|
||
index++;
|
||
return list[index % list.length];
|
||
};
|
||
};
|
||
|
||
const TryOnTasksPage: React.FC = () => {
|
||
const [page, setPage] = useState(1);
|
||
const [previewImg, setPreviewImg] = useState<string | null>(null);
|
||
const [dialogOpen, setDialogOpen] = useState(false);
|
||
const [selectedRows, setSelectedRows] = useState<{ id: number; img_url: string; task_id: string }[]>([]);
|
||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||
const [selectedPrompts, setSelectedPrompts] = useState<string[]>([]);
|
||
const [creating, setCreating] = useState(false);
|
||
|
||
const r = useCallback(async (params: VideoDataPaginationRequest) => {
|
||
const res = await api.DefaultService.getImageDataListApiImageDataListPost({ requestBody: params });
|
||
return res;
|
||
}, []);
|
||
|
||
const { data, loading, error, execute } = useApi<PaginationResponse>(r, null);
|
||
|
||
useEffect(() => {
|
||
execute({ page, page_size: PAGE_SIZE });
|
||
}, [page, execute]);
|
||
|
||
const handlePrev = () => setPage(p => Math.max(1, p - 1));
|
||
const handleNext = () => setPage(p => p + 1);
|
||
|
||
// 判断某行是否被选中
|
||
const isRowSelected = (id: number) => selectedRows.some(row => row.id === id);
|
||
|
||
// 当前页所有可选行id
|
||
const currentPageIds = data?.data?.map(item => item.id) || [];
|
||
// 当前页是否全选
|
||
const isAllCurrentPageSelected = currentPageIds.length > 0 && currentPageIds.every(id => isRowSelected(id));
|
||
|
||
// 单行选择
|
||
const handleRowSelect = (item: { id: number; img_url?: string; task_id?: string }) => {
|
||
setSelectedRows(prev => {
|
||
if (isRowSelected(item.id)) {
|
||
return prev.filter(row => row.id !== item.id);
|
||
} else {
|
||
return [...prev, { id: item.id, img_url: item.img_url || '', task_id: item.task_id || '' }];
|
||
}
|
||
});
|
||
};
|
||
// 当前页全选/取消
|
||
const handleSelectAllCurrentPage = () => {
|
||
if (isAllCurrentPageSelected) {
|
||
setSelectedRows(prev => prev.filter(row => !currentPageIds.includes(row.id)));
|
||
} else {
|
||
const toAdd = (data?.data || [])
|
||
.filter(item => !isRowSelected(item.id))
|
||
.map(item => ({ id: item.id, img_url: item.img_url || '', task_id: item.task_id || '' }));
|
||
setSelectedRows(prev => [...prev, ...toAdd]);
|
||
}
|
||
};
|
||
|
||
// 创建视频任务提交
|
||
const handleCreateTask = async () => {
|
||
if (!selectedRows.length || !selectedPrompts.length) return;
|
||
setCreating(true);
|
||
const getPrompt = promptGetter(selectedPrompts);
|
||
try {
|
||
const tasks: BatchVideoTaskRequest['tasks'] = selectedRows.map(row => ({
|
||
prompt: getPrompt(),
|
||
img_url: row.img_url,
|
||
task_id: row.task_id,
|
||
}));
|
||
const res = (await api.Service.submitVideoTaskApiJmSubmitTaskPost({ requestBody: { tasks } })) as {
|
||
status: boolean;
|
||
data: { success: { task_id: string; img_url: string }[]; failed: string[] };
|
||
msg: string;
|
||
};
|
||
// 只存储成功的任务
|
||
const successList = res?.data?.success || [];
|
||
// 用于 prompt 匹配
|
||
const promptMap = new Map();
|
||
tasks.forEach(t => {
|
||
if (!promptMap.has(t.img_url)) promptMap.set(t.img_url, []);
|
||
promptMap.get(t.img_url).push(t.prompt);
|
||
});
|
||
const localTasks = successList.map((item: any) => {
|
||
// 取出 prompt(如有多 prompt,依次取出)
|
||
let prompt = '';
|
||
if (promptMap.has(item.img_url)) {
|
||
const arr = promptMap.get(item.img_url);
|
||
prompt = arr.shift();
|
||
if (arr.length === 0) promptMap.delete(item.img_url);
|
||
}
|
||
return {
|
||
id: `${item.task_id}`,
|
||
img_url: item.img_url,
|
||
prompt,
|
||
create_time: new Date().toISOString(),
|
||
job_id: item.job_id,
|
||
task_id: item.task_id,
|
||
};
|
||
});
|
||
await addVideoTasks(localTasks);
|
||
toast.success('任务提交成功');
|
||
setCreateDialogOpen(false);
|
||
setSelectedPrompts([]);
|
||
setSelectedRows([]);
|
||
} catch (e: any) {
|
||
toast.error(e?.message || '任务提交失败');
|
||
} finally {
|
||
setCreating(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className='p-6'>
|
||
{/* 创建视频任务弹窗 */}
|
||
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
||
<DialogContent style={{ maxWidth: 480 }}>
|
||
<DialogHeader>
|
||
<DialogTitle>选择提示词</DialogTitle>
|
||
</DialogHeader>
|
||
<div className='max-h-60 overflow-y-auto space-y-2 my-4'>
|
||
{ACTION_PROMPTS.map(prompt => (
|
||
<label key={prompt} className='flex items-center gap-2 cursor-pointer'>
|
||
<Checkbox
|
||
checked={selectedPrompts.includes(prompt)}
|
||
onCheckedChange={() => {
|
||
setSelectedPrompts(prev => (prev.includes(prompt) ? prev.filter(p => p !== prompt) : [...prev, prompt]));
|
||
}}
|
||
/>
|
||
<span className='text-sm'>{prompt}</span>
|
||
</label>
|
||
))}
|
||
</div>
|
||
<DialogFooter>
|
||
<Button variant='outline' onClick={() => setCreateDialogOpen(false)} disabled={creating}>
|
||
取消
|
||
</Button>
|
||
<Button onClick={handleCreateTask} disabled={creating || !selectedPrompts.length || !selectedRows.length}>
|
||
{creating ? '提交中...' : '提交'}
|
||
</Button>
|
||
</DialogFooter>
|
||
</DialogContent>
|
||
</Dialog>
|
||
{/* 图片预览 Dialog */}
|
||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||
<DialogContent style={{ maxHeight: '80vh' }} className='flex items-center justify-center' showCloseButton={false}>
|
||
{previewImg && <img src={previewImg} alt='预览图片' className='object-contain rounded shadow-lg max-h-[calc(80vh-100px)]' />}
|
||
</DialogContent>
|
||
</Dialog>
|
||
<div className='flex justify-between items-center mb-4'>
|
||
<h2 className='text-xl font-bold'>任务列表</h2>
|
||
<Button onClick={() => setCreateDialogOpen(true)} disabled={!selectedRows.length}>
|
||
创建视频任务
|
||
</Button>
|
||
</div>
|
||
{loading ? (
|
||
<Skeleton className='h-40 w-full mb-4' />
|
||
) : error ? (
|
||
<div className='text-red-500 mb-4'>{error}</div>
|
||
) : (
|
||
<Table>
|
||
<TableHeader>
|
||
<TableRow>
|
||
<TableHead>
|
||
<Checkbox checked={isAllCurrentPageSelected} onCheckedChange={handleSelectAllCurrentPage} />
|
||
</TableHead>
|
||
<TableHead>图片</TableHead>
|
||
<TableHead>ID</TableHead>
|
||
<TableHead>任务ID</TableHead>
|
||
<TableHead>图片状态</TableHead>
|
||
<TableHead>视频状态</TableHead>
|
||
<TableHead>创建时间</TableHead>
|
||
</TableRow>
|
||
</TableHeader>
|
||
<TableBody>
|
||
{data?.data?.length ? (
|
||
data.data.map(item => (
|
||
<TableRow key={item.id}>
|
||
<TableCell>
|
||
<Checkbox checked={isRowSelected(item.id)} onCheckedChange={() => handleRowSelect(item)} />
|
||
</TableCell>
|
||
<TableCell>
|
||
{item.img_url ? (
|
||
<img
|
||
src={item.img_url}
|
||
alt='img'
|
||
className='w-16 h-16 object-cover rounded cursor-pointer transition hover:scale-105'
|
||
onClick={() => {
|
||
setPreviewImg(item.img_url!);
|
||
setDialogOpen(true);
|
||
}}
|
||
/>
|
||
) : (
|
||
<span className='text-gray-400'>无</span>
|
||
)}
|
||
</TableCell>
|
||
<TableCell>{item.id}</TableCell>
|
||
<TableCell>{item.task_id}</TableCell>
|
||
<TableCell>{item.img_status || '-'}</TableCell>
|
||
<TableCell>{item.video_status || '-'}</TableCell>
|
||
<TableCell>{item.create_time || '-'}</TableCell>
|
||
</TableRow>
|
||
))
|
||
) : (
|
||
<TableRow>
|
||
<TableCell colSpan={7} className='text-center text-gray-400'>
|
||
暂无数据
|
||
</TableCell>
|
||
</TableRow>
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
)}
|
||
<div className='flex justify-between items-center mt-4'>
|
||
<Button variant='outline' onClick={handlePrev} disabled={page === 1 || loading}>
|
||
上一页
|
||
</Button>
|
||
<span>第 {page} 页</span>
|
||
<Button variant='outline' onClick={handleNext} disabled={loading || (data?.data && data.data.length < PAGE_SIZE)}>
|
||
下一页
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default TryOnTasksPage;
|