import React, { useState, useEffect, useRef } from 'react'; import { LoadingOutlined, PlayCircleFilled, WarningOutlined } from '@ant-design/icons'; export interface GenerationTaskResourceItem { id?: string; genType?: string; status?: string; displayStatus?: string | null; pipelineStage?: string | null; imageUrl?: string | null; videoUrl?: string | null; videoCoverUrl?: string | null; errorMessage?: string | null; generationIndex?: number | null; createdAt?: string; } export interface GenerationTaskResourceGroup extends GenerationTaskResourceItem { generationCount?: number | null; childItems?: GenerationTaskResourceItem[] | null; } interface Props { task: GenerationTaskResourceGroup; onPreview: (url: string, type: 'image' | 'video') => void; resolveUrl?: (url?: string | null) => string; } const spanByCount = (count: number, index: number): number => { if (count <= 1) return 6; if (count === 2 || count === 4) return 3; if (count === 3) return 3; return 2; }; const statusText = (item: GenerationTaskResourceItem): string => { const status = item.displayStatus || item.pipelineStage || item.status || 'generating'; const labels: Record = { pending: '待处理', queued: '排队中', preparing: '准备中', generating: '生成中', creating_provider_task: '创建任务中', waiting_remote: '等待生成', polling: '轮询中', result_ready: '结果就绪', download_queued: '等待下载', downloading: '下载中', retry_waiting: '等待重试', completed: '已完成', failed: '生成失败', download_failed: '下载失败', deleted: '已删除', }; return labels[status] || status; }; const isPending = (item: GenerationTaskResourceItem): boolean => { const status = item.displayStatus || item.pipelineStage || item.status; return !status || ['pending', 'queued', 'preparing', 'generating', 'creating_provider_task', 'waiting_remote', 'polling', 'result_ready', 'download_queued', 'downloading', 'retry_waiting'].includes(status); }; const MAX_DURATION_SECONDS = 300; interface ProgressItemProps { item: GenerationTaskResourceItem; isPending: boolean; isCompleted: boolean; onAnimationComplete?: () => void; onProgressChange?: (progress: number) => void; } const PROGRESS_RATE = 0.6; const calculateProgressValue = (createdAt?: string): number => { if (!createdAt) return 0; const createdTime = new Date(createdAt).getTime(); if (isNaN(createdTime)) return 0; const now = Date.now(); const elapsedSeconds = Math.max(0, (now - createdTime) / 1000); if (elapsedSeconds >= MAX_DURATION_SECONDS) { return 99; } return Math.min(99, elapsedSeconds * PROGRESS_RATE); }; const ProgressItem: React.FC = ({ item, isPending: isPendingProp, isCompleted, onAnimationComplete, onProgressChange }) => { const [progress, setProgress] = useState(0); const [displayProgress, setDisplayProgress] = useState(0); const [isFinishing, setIsFinishing] = useState(false); const intervalRef = useRef(null); useEffect(() => { if (onProgressChange) { onProgressChange(displayProgress); } }, [displayProgress, onProgressChange]); useEffect(() => { const newProgress = calculateProgressValue(item.createdAt); setProgress(newProgress); setDisplayProgress(newProgress); }, [item]); useEffect(() => { if (isCompleted && !isFinishing) { setIsFinishing(true); return; } if (!isPendingProp && !isFinishing) { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } return; } const updateProgress = () => { const newProgress = calculateProgressValue(item.createdAt); setProgress(newProgress); setDisplayProgress(newProgress); }; intervalRef.current = window.setInterval(updateProgress, 1000); return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [item.createdAt, isPendingProp, isCompleted, isFinishing, item.id]); useEffect(() => { if (isFinishing) { const targetProgress = 100; const currentProgress = displayProgress; const duration = 800; const startTime = Date.now(); const animate = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); const easeProgress = 1 - Math.pow(1 - progress, 3); const newProgress = currentProgress + (targetProgress - currentProgress) * easeProgress; setDisplayProgress(newProgress); if (progress < 1) { requestAnimationFrame(animate); } else { setTimeout(() => { onAnimationComplete?.(); }, 100); } }; requestAnimationFrame(animate); } }, [isFinishing, displayProgress, onAnimationComplete]); if (!isPendingProp && !isFinishing) return null; return (
{Math.round(displayProgress)}%
); }; const GenerationTaskResourceGrid: React.FC = ({ task, onPreview, resolveUrl = (url) => url || '' }) => { const count = Math.max(1, Math.min(5, Number(task.generationCount || task.childItems?.length || 1))); const children = [...(task.childItems || [])].sort((a, b) => Number(a.generationIndex || 0) - Number(b.generationIndex || 0)); const items: GenerationTaskResourceItem[] = children.length > 0 ? children : (count > 1 ? Array.from({ length: count }, (_, index) => ({ id: `${task.id || 'task'}-placeholder-${index + 1}`, genType: task.genType, status: task.status, displayStatus: task.displayStatus, pipelineStage: task.pipelineStage, generationIndex: index + 1, errorMessage: task.errorMessage, createdAt: task.createdAt, })) : [task]); const [finishingItems, setFinishingItems] = useState>(new Set()); const [fullProgressItems, setFullProgressItems] = useState>(new Set()); const processedItems = useRef>(new Set()); useEffect(() => { const styleId = 'gen-task-resource-grid-animations'; if (document.getElementById(styleId)) return; const style = document.createElement('style'); style.id = styleId; style.textContent = ` .gen-task-loading-bg { position: relative; overflow: hidden; background: linear-gradient(135deg, #faf8ff 0%, #f5f3ff 100%); } .gen-task-loading-bg::before, .gen-task-loading-bg::after, .gen-task-loading-bg .gen-task-aurora-3 { content: ''; position: absolute; border-radius: 50%; filter: blur(30px); pointer-events: none; z-index: 0; } .gen-task-loading-bg::before { width: 220px; height: 220px; background: radial-gradient(circle, rgba(139, 92, 246, 0.45) 0%, rgba(168, 85, 247, 0.25) 50%, transparent 100%); animation: genTaskAurora1 6s linear infinite; } .gen-task-loading-bg::after { width: 240px; height: 240px; background: radial-gradient(circle, rgba(168, 85, 247, 0.35) 0%, rgba(192, 132, 252, 0.2) 50%, transparent 100%); animation: genTaskAurora2 7.5s linear infinite; } .gen-task-loading-bg .gen-task-aurora-3 { width: 180px; height: 180px; background: radial-gradient(circle, rgba(99, 102, 241, 0.3) 0%, rgba(139, 92, 246, 0.15) 50%, transparent 100%); animation: genTaskAurora3 5s linear infinite; } .gen-task-loading-bg > * { position: relative; z-index: 1; } @keyframes genTaskAurora1 { 0% { transform: translate(-100px, -80px); } 25% { transform: translate(100px, -60px); } 50% { transform: translate(120px, 80px); } 75% { transform: translate(-60px, 100px); } 100% { transform: translate(-100px, -80px); } } @keyframes genTaskAurora2 { 0% { transform: translate(120px, 100px); } 25% { transform: translate(-80px, 120px); } 50% { transform: translate(-100px, -60px); } 75% { transform: translate(80px, -80px); } 100% { transform: translate(120px, 100px); } } @keyframes genTaskAurora3 { 0% { transform: translate(60px, -40px); } 25% { transform: translate(-40px, 60px); } 50% { transform: translate(40px, 100px); } 75% { transform: translate(100px, -20px); } 100% { transform: translate(60px, -40px); } } `; document.head.appendChild(style); }, []); const handleProgressChange = (itemId: string, progress: number) => { if (progress >= 100) { setFullProgressItems(prev => { if (prev.has(itemId)) return prev; const next = new Set(prev); next.add(itemId); return next; }); } }; useEffect(() => { items.slice(0, 5).forEach((item, index) => { const displayStatus = item.displayStatus || item.pipelineStage || item.status || 'generating'; const pending = isPending(item); const completed = !pending && displayStatus !== 'failed' && displayStatus !== 'download_failed' && displayStatus !== 'deleted'; const itemId = item.id || `${index}`; if (completed && !processedItems.current.has(itemId) && pending === false) { processedItems.current.add(itemId); setFinishingItems(prev => { const next = new Set(prev); next.add(itemId); return next; }); } }); }, [items]); const handleFinishAnimation = (itemId: string) => { setFinishingItems(prev => { const next = new Set(prev); next.delete(itemId); return next; }); }; return (
1 ? 6 : 0 }}> {items.slice(0, 5).map((item, index) => { const displayStatus = item.displayStatus || item.pipelineStage || item.status || 'generating'; const imageUrl = resolveUrl(`/static${item.imageUrl}&w=300&q=50`); const videoUrl = resolveUrl(item.videoUrl); const coverUrl = resolveUrl(`/static${item.videoCoverUrl}&w=300&q=50`); const isVideo = (item.genType || task.genType) === 'video'; const hasResource = isVideo ? !!videoUrl : !!imageUrl; const pending = isPending(item); const completed = !pending && displayStatus !== 'failed' && displayStatus !== 'download_failed' && displayStatus !== 'deleted'; const itemId = item.id || `${index}`; const isFinishing = finishingItems.has(itemId); const isFullProgress = fullProgressItems.has(itemId); const shouldShowContent = completed && !isFinishing; const statusLabel = isFinishing ? '加载中' : statusText(item); return (
{hasResource && displayStatus !== 'deleted' && displayStatus !== 'failed' && displayStatus !== 'download_failed' && shouldShowContent ? ( ) : (
{pending || isFinishing ? : null} {pending || isFinishing ? 2 ? 20 : 34 }} /> : 2 ? 20 : 34 }} />} 2 ? 10 : 12, color: pending || isFinishing ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusLabel} handleFinishAnimation(itemId)} onProgressChange={(progress) => handleProgressChange(itemId, progress)} /> {!pending && item.errorMessage && items.length <= 2 ? {item.errorMessage} : null}
)} {items.length > 1 ? #{item.generationIndex || index + 1} : null}
); })}
); }; export default GenerationTaskResourceGrid;