媒体生成百分比显示
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user