媒体生成百分比显示

This commit is contained in:
sjy
2026-07-16 17:52:49 +08:00
parent 5036efe9aa
commit f0ff573650
8 changed files with 533 additions and 215 deletions
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { LoadingOutlined, PlayCircleFilled, WarningOutlined } from '@ant-design/icons';
export interface GenerationTaskResourceItem {
@@ -12,6 +12,7 @@ export interface GenerationTaskResourceItem {
videoCoverUrl?: string | null;
errorMessage?: string | null;
generationIndex?: number | null;
createdAt?: string;
}
export interface GenerationTaskResourceGroup extends GenerationTaskResourceItem {
@@ -49,6 +50,125 @@ const isPending = (item: GenerationTaskResourceItem): boolean => {
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 = 180;
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();
const now = Date.now();
const elapsedSeconds = (now - createdTime) / 1000;
if (elapsedSeconds >= MAX_DURATION_SECONDS) {
return 99;
}
return Math.min(99, elapsedSeconds * PROGRESS_RATE);
};
const ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingProp, isCompleted, onAnimationComplete, onProgressChange }) => {
const [progress, setProgress] = useState<number>(0);
const [displayProgress, setDisplayProgress] = useState<number>(0);
const [isFinishing, setIsFinishing] = useState(false);
const intervalRef = useRef<number | null>(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 (
<div style={{
marginTop: 4,
}}>
<div style={{
textAlign: 'center',
fontSize: 13,
color: '#8b5cf6',
fontWeight: 700,
textShadow: '0 0 8px rgba(139, 92, 246, 0.3)',
transition: 'all 0.3s ease-out',
}}>
{Math.round(displayProgress)}%
</div>
</div>
);
};
const GenerationTaskResourceGrid: React.FC<Props> = ({ 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));
@@ -62,8 +182,49 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
pipelineStage: task.pipelineStage,
generationIndex: index + 1,
errorMessage: task.errorMessage,
createdAt: task.createdAt,
})) : [task]);
const [finishingItems, setFinishingItems] = useState<Set<string>>(new Set());
const [fullProgressItems, setFullProgressItems] = useState<Set<string>>(new Set());
const processedItems = useRef<Set<string>>(new Set());
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 (
<div style={{ width: '100%', height: '100%', display: 'grid', gridTemplateColumns: 'repeat(6, minmax(0, 1fr))', gridAutoRows: 'minmax(0, 1fr)', gap: count > 1 ? 6 : 0 }}>
{items.slice(0, 5).map((item, index) => {
@@ -73,6 +234,14 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
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 (
<div
key={item.id || `${index}`}
@@ -87,7 +256,7 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
border: '1px solid #E7EAF0',
}}
>
{hasResource && displayStatus !== 'deleted' && displayStatus !== 'failed' && displayStatus !== 'download_failed' && !isPending(item) ? (
{hasResource && displayStatus !== 'deleted' && displayStatus !== 'failed' && displayStatus !== 'download_failed' && shouldShowContent ? (
<button
type="button"
onClick={() => onPreview(isVideo ? videoUrl : imageUrl, isVideo ? 'video' : 'image')}
@@ -103,9 +272,10 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
</button>
) : (
<div style={{ width: '100%', height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 7, padding: 8, textAlign: 'center' }}>
{isPending(item) ? <LoadingOutlined spin style={{ color: '#8b5cf6', fontSize: items.length > 2 ? 20 : 34 }} /> : <WarningOutlined style={{ color: displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B', fontSize: items.length > 2 ? 20 : 34 }} />}
<span style={{ fontSize: items.length > 2 ? 10 : 12, color: isPending(item) ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusText(item)}</span>
{!isPending(item) && item.errorMessage && items.length <= 2 ? <span style={{ fontSize: 10, color: '#A45B5B', lineHeight: 1.3, maxHeight: 28, overflow: 'hidden' }}>{item.errorMessage}</span> : null}
{pending || isFinishing ? <LoadingOutlined spin style={{ color: '#8b5cf6', fontSize: items.length > 2 ? 20 : 34 }} /> : <WarningOutlined style={{ color: displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B', fontSize: items.length > 2 ? 20 : 34 }} />}
<span style={{ fontSize: items.length > 2 ? 10 : 12, color: pending || isFinishing ? '#8b5cf6' : (displayStatus === 'deleted' ? '#98A2B3' : '#A45B5B'), fontWeight: 500 }}>{statusLabel}</span>
<ProgressItem item={item} isPending={pending || isFinishing} isCompleted={completed} onAnimationComplete={() => handleFinishAnimation(itemId)} onProgressChange={(progress) => handleProgressChange(itemId, progress)} />
{!pending && item.errorMessage && items.length <= 2 ? <span style={{ fontSize: 10, color: '#A45B5B', lineHeight: 1.3, maxHeight: 28, overflow: 'hidden' }}>{item.errorMessage}</span> : null}
</div>
)}
{items.length > 1 ? <span style={{ position: 'absolute', top: 5, left: 5, zIndex: 2, padding: '1px 6px', borderRadius: 10, background: 'rgba(17,24,39,.58)', color: '#fff', fontSize: 10 }}>#{item.generationIndex || index + 1}</span> : null}