glam-web/src/pages/TryOnTasksPage/index.tsx

247 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;