合并修改
This commit is contained in:
+116
-116
File diff suppressed because one or more lines are too long
Vendored
+2
-1
@@ -28,11 +28,12 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-mLPAhw5a.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-CnKJ4EKg.js"></script>
|
||||
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { LoadingOutlined, PlayCircleFilled, WarningOutlined } from '@ant-design/icons';
|
||||
import { resolveGenerationUiState } from '../../utils/generationTaskStatus';
|
||||
|
||||
export interface GenerationTaskResourceItem {
|
||||
id?: string;
|
||||
@@ -33,22 +34,7 @@ const spanByCount = (count: number, index: number): number => {
|
||||
return 2;
|
||||
};
|
||||
|
||||
const statusText = (item: GenerationTaskResourceItem): string => {
|
||||
const status = item.displayStatus || item.pipelineStage || item.status || 'generating';
|
||||
const labels: Record<string, string> = {
|
||||
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 statusText = (item: GenerationTaskResourceItem): string => resolveGenerationUiState(item).label;
|
||||
|
||||
const MAX_DURATION_SECONDS = 300;
|
||||
|
||||
@@ -57,7 +43,6 @@ interface ProgressItemProps {
|
||||
isPending: boolean;
|
||||
isCompleted: boolean;
|
||||
onAnimationComplete?: () => void;
|
||||
onProgressChange?: (progress: number) => void;
|
||||
}
|
||||
|
||||
const PROGRESS_RATE = 0.6;
|
||||
@@ -74,21 +59,13 @@ const calculateProgressValue = (createdAt?: string): number => {
|
||||
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 ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingProp, isCompleted, onAnimationComplete }) => {
|
||||
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]);
|
||||
|
||||
@@ -108,8 +85,7 @@ const ProgressItem: React.FC<ProgressItemProps> = ({ item, isPending: isPendingP
|
||||
|
||||
const updateProgress = () => {
|
||||
const newProgress = calculateProgressValue(item.createdAt);
|
||||
setProgress(newProgress);
|
||||
setDisplayProgress(newProgress);
|
||||
setDisplayProgress(newProgress);
|
||||
};
|
||||
|
||||
intervalRef.current = window.setInterval(updateProgress, 1000);
|
||||
@@ -187,7 +163,6 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
||||
})) : [task]);
|
||||
|
||||
const [finishingItems, setFinishingItems] = useState<Set<string>>(new Set());
|
||||
const [fullProgressItems, setFullProgressItems] = useState<Set<string>>(new Set());
|
||||
const processedItems = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
@@ -255,22 +230,11 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
||||
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 uiState = resolveGenerationUiState(item);
|
||||
const pending = uiState.isActive;
|
||||
const completed = uiState.isSuccess;
|
||||
const itemId = item.id || `${index}`;
|
||||
if (completed && !processedItems.current.has(itemId) && pending === false) {
|
||||
processedItems.current.add(itemId);
|
||||
@@ -294,17 +258,17 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
||||
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) => {
|
||||
const displayStatus = item.displayStatus || item.pipelineStage || item.status || 'generating';
|
||||
const imageUrl = resolveUrl(`/static${item.imageUrl}&w=300&q=50`);
|
||||
const uiState = resolveGenerationUiState(item);
|
||||
const displayStatus = uiState.effectiveKey;
|
||||
const imageUrl = item.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 coverUrl = item.videoCoverUrl ? 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 pending = uiState.isActive;
|
||||
const completed = uiState.isSuccess;
|
||||
const itemId = item.id || `${index}`;
|
||||
const isFinishing = finishingItems.has(itemId);
|
||||
const isFullProgress = fullProgressItems.has(itemId);
|
||||
const shouldShowContent = completed && !isFinishing;
|
||||
const statusLabel = isFinishing ? '加载中' : statusText(item);
|
||||
|
||||
@@ -322,7 +286,7 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
||||
border: '1px solid #E7EAF0',
|
||||
}}
|
||||
>
|
||||
{hasResource && displayStatus !== 'deleted' && displayStatus !== 'failed' && displayStatus !== 'download_failed' && shouldShowContent ? (
|
||||
{hasResource && uiState.isSuccess && shouldShowContent ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPreview(isVideo ? videoUrl : imageUrl, isVideo ? 'video' : 'image')}
|
||||
@@ -341,7 +305,7 @@ const GenerationTaskResourceGrid: React.FC<Props> = ({ task, onPreview, resolveU
|
||||
{pending || isFinishing ? <span className="gen-task-aurora-3" /> : 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)} />
|
||||
<ProgressItem item={item} isPending={pending || isFinishing} isCompleted={completed} onAnimationComplete={() => handleFinishAnimation(itemId)} />
|
||||
{!pending && item.errorMessage && items.length <= 2 ? <span style={{ fontSize: 10, color: '#A45B5B', lineHeight: 1.3, maxHeight: 28, overflow: 'hidden' }}>{item.errorMessage}</span> : null}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -57,12 +57,12 @@ import {
|
||||
deleteUpload,
|
||||
updateRecordPrompt,
|
||||
getCreditRatios,
|
||||
login,
|
||||
getRecordsPage,
|
||||
} from "../api";
|
||||
import { formatDate } from "../utils/formatDate";
|
||||
import UploadSelector from "../components/UploadSelector";
|
||||
import { generateUUID } from "../utils/uuid";
|
||||
import { resolveGenerationUiState } from "../utils/generationTaskStatus";
|
||||
|
||||
interface PortalDropdownProps {
|
||||
label: string;
|
||||
@@ -1753,44 +1753,21 @@ const GeneratePage: React.FC = () => {
|
||||
};
|
||||
|
||||
const getRecordStatus = (record: GenerationRecord) => {
|
||||
// console.log('返回的数据:', record);
|
||||
|
||||
const state = recordStates[record.id];
|
||||
if (state === "generating") return "generating";
|
||||
if (state === "done") return "completed";
|
||||
if (state === "failed") return "failed";
|
||||
return record.status;
|
||||
const localState = recordStates[record.id];
|
||||
const localStatus = localState === "done" ? "completed" : localState;
|
||||
return resolveGenerationUiState({
|
||||
status: localStatus || record.status,
|
||||
pipelineStage: localState ? null : record.pipelineStage,
|
||||
});
|
||||
};
|
||||
|
||||
const statusConfig: Record<
|
||||
string,
|
||||
{ color: string; text: string; icon: React.ReactNode }
|
||||
> = {
|
||||
optimizing: {
|
||||
color: "processing",
|
||||
text: "优化中",
|
||||
icon: (
|
||||
<LoadingOutlined style={{ animation: "spinSlow 1s linear infinite" }} />
|
||||
),
|
||||
},
|
||||
prompt_optimized: {
|
||||
color: "processing",
|
||||
text: "待生成",
|
||||
icon: <ClockCircleOutlined />,
|
||||
},
|
||||
generating: {
|
||||
color: "warning",
|
||||
text: "生成中",
|
||||
icon: (
|
||||
<LoadingOutlined style={{ animation: "spinSlow 1s linear infinite" }} />
|
||||
),
|
||||
},
|
||||
completed: {
|
||||
color: "success",
|
||||
text: "已完成",
|
||||
icon: <CheckCircleOutlined />,
|
||||
},
|
||||
failed: { color: "error", text: "失败", icon: <CloseCircleOutlined /> },
|
||||
const renderGenerationStatusIcon = (state: ReturnType<typeof resolveGenerationUiState>) => {
|
||||
if (state.isActive) {
|
||||
return <LoadingOutlined style={{ animation: "spinSlow 1s linear infinite" }} />;
|
||||
}
|
||||
if (state.isSuccess) return <CheckCircleOutlined />;
|
||||
if (state.isFailure) return <CloseCircleOutlined />;
|
||||
return <ClockCircleOutlined />;
|
||||
};
|
||||
return (
|
||||
<div
|
||||
@@ -3731,7 +3708,8 @@ const GeneratePage: React.FC = () => {
|
||||
className="stagger-children"
|
||||
>
|
||||
{projectRecords.map((record, i) => {
|
||||
const status = getRecordStatus(record);
|
||||
const statusState = getRecordStatus(record);
|
||||
const status = statusState.effectiveKey;
|
||||
const prompt =
|
||||
editablePrompts[record.id] ??
|
||||
(record.optimizedPrompt || record.originalPrompt);
|
||||
@@ -3804,11 +3782,11 @@ const GeneratePage: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
<Tag
|
||||
color={statusConfig[status]?.color}
|
||||
icon={statusConfig[status]?.icon}
|
||||
color={statusState.color}
|
||||
icon={renderGenerationStatusIcon(statusState)}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
{statusConfig[status]?.text}
|
||||
{statusState.label}
|
||||
</Tag>
|
||||
<Typography.Text
|
||||
style={{
|
||||
@@ -3876,7 +3854,7 @@ const GeneratePage: React.FC = () => {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* <Space>
|
||||
{status === "failed" && (
|
||||
{statusState.isFailure && (
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
@@ -3954,7 +3932,7 @@ const GeneratePage: React.FC = () => {
|
||||
>
|
||||
优化后的提示词{" "}
|
||||
{(status === "prompt_optimized" ||
|
||||
status === "failed") &&
|
||||
statusState.isFailure) &&
|
||||
editingRecordId !== record.id && (
|
||||
<span style={{ color: "#6366f1" }}>
|
||||
(可编辑)
|
||||
@@ -3973,7 +3951,7 @@ const GeneratePage: React.FC = () => {
|
||||
/>
|
||||
</Tooltip>
|
||||
{(status === "prompt_optimized" ||
|
||||
status === "failed") && (
|
||||
statusState.isFailure) && (
|
||||
<Tooltip title="编辑">
|
||||
<Button
|
||||
type="text"
|
||||
@@ -4186,7 +4164,7 @@ const GeneratePage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Error message for failed records */}
|
||||
{/* {status === 'failed' && record.errorMessage && (
|
||||
{/* {statusState.isFailure && record.errorMessage && (
|
||||
<div style={{ marginTop: 14, padding: '12px 14px', borderRadius: 10, background: 'rgba(239,68,68,0.04)', border: '1px solid rgba(239,68,68,0.15)' }}>
|
||||
<Typography.Text style={{ fontSize: 12, color: '#ef4444' }}>
|
||||
<CloseCircleOutlined style={{ marginRight: 6 }} />
|
||||
@@ -4198,7 +4176,7 @@ const GeneratePage: React.FC = () => {
|
||||
{/* Reference materials */}
|
||||
{((record.references &&
|
||||
record.references.length > 0) ||
|
||||
status === "failed") && (
|
||||
statusState.isFailure) && (
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<Typography.Text
|
||||
style={{
|
||||
@@ -4209,7 +4187,7 @@ const GeneratePage: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
参考素材{" "}
|
||||
{status === "failed" && (
|
||||
{statusState.isFailure && (
|
||||
<span style={{ color: "#6366f1" }}>
|
||||
(可删除/添加后重试)
|
||||
</span>
|
||||
@@ -4287,7 +4265,7 @@ const GeneratePage: React.FC = () => {
|
||||
>
|
||||
{ref.name}
|
||||
</Tag>
|
||||
{status === "failed" && (
|
||||
{statusState.isFailure && (
|
||||
<div
|
||||
className="ref-delete"
|
||||
onClick={(e) => {
|
||||
@@ -4335,7 +4313,7 @@ const GeneratePage: React.FC = () => {
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
{status === "failed" && (
|
||||
{statusState.isFailure && (
|
||||
<Upload
|
||||
accept="image/*,video/*"
|
||||
showUploadList={false}
|
||||
@@ -4727,7 +4705,7 @@ const GeneratePage: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{status === "failed" && (
|
||||
{statusState.isFailure && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
@@ -4754,7 +4732,7 @@ const GeneratePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{status === "generating" ? (
|
||||
{statusState.isActive ? (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
@@ -4780,7 +4758,7 @@ const GeneratePage: React.FC = () => {
|
||||
{type === "video" ? "视频" : "图片"}生成中...
|
||||
</Typography.Text>
|
||||
</div>
|
||||
) : (status === "completed" && record.videoUrl) ||
|
||||
) : (statusState.isSuccess && record.videoUrl) ||
|
||||
record.imageUrl ? (
|
||||
<div
|
||||
style={{
|
||||
@@ -4885,7 +4863,7 @@ const GeneratePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : status === "failed" ? (
|
||||
) : statusState.isFailure ? (
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
@@ -5006,7 +4984,7 @@ const GeneratePage: React.FC = () => {
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{status === 'failed' && (
|
||||
{statusState.isFailure && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Button type="primary" danger size="large" icon={<PlayCircleOutlined />} block
|
||||
loading={generating[record.id]}
|
||||
|
||||
@@ -31,17 +31,10 @@ import {
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
import type { GenerationStatus, AspectRatio, Resolution } from '../types';
|
||||
import type { AspectRatio, Resolution } from '../types';
|
||||
import { formatDate } from '../utils/formatDate';
|
||||
import { copyToClipboard } from '../utils/clipboard';
|
||||
|
||||
const statusConfig: Record<GenerationStatus, { color: string; text: string; icon: React.ReactNode }> = {
|
||||
optimizing: { color: 'processing', text: '优化中', icon: <LoadingOutlined spin /> },
|
||||
prompt_optimized: { color: 'processing', text: '待生成', icon: <ClockCircleOutlined /> },
|
||||
generating: { color: 'warning', text: '生成中', icon: <LoadingOutlined spin /> },
|
||||
completed: { color: 'success', text: '已完成', icon: <CheckCircleOutlined /> },
|
||||
failed: { color: 'error', text: '失败', icon: <CloseCircleOutlined /> },
|
||||
};
|
||||
import { resolveGenerationUiState } from '../utils/generationTaskStatus';
|
||||
|
||||
const RecordsPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -179,8 +172,7 @@ const RecordsPage: React.FC = () => {
|
||||
const isGenerating = generating[record.id];
|
||||
const isExpanded = expandedId === record.id;
|
||||
const type: any = record.genType || 'image';
|
||||
|
||||
|
||||
const uiState = resolveGenerationUiState(record);
|
||||
|
||||
return (
|
||||
<div key={record.id} className="animate-slideInCard" style={{ animationDelay: `${i * 0.04}s` }}>
|
||||
@@ -207,8 +199,12 @@ const RecordsPage: React.FC = () => {
|
||||
: <CaretRightOutlined style={{ color: '#cbd5e1', fontSize: 12, flexShrink: 0 }} />}
|
||||
|
||||
{/* Status */}
|
||||
<Tag color={statusConfig[record.status].color} icon={statusConfig[record.status].icon} style={{ margin: 0 }}>
|
||||
{statusConfig[record.status].text}
|
||||
<Tag
|
||||
color={uiState.color}
|
||||
icon={uiState.isActive ? <LoadingOutlined spin /> : (uiState.isSuccess ? <CheckCircleOutlined /> : (uiState.isFailure ? <CloseCircleOutlined /> : <ClockCircleOutlined />))}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
{uiState.label}
|
||||
</Tag>
|
||||
|
||||
{/* Project */}
|
||||
@@ -246,12 +242,12 @@ const RecordsPage: React.FC = () => {
|
||||
生成
|
||||
</Button>
|
||||
)}
|
||||
{record.status === 'generating' && (
|
||||
{uiState.isActive && (
|
||||
<Tag color="processing" icon={<LoadingOutlined spin />} style={{ padding: '4px 12px', fontSize: 12 }}>
|
||||
生成中...
|
||||
</Tag>
|
||||
)}
|
||||
{record.status === 'failed' && (
|
||||
{uiState.isFailure && (
|
||||
<Button type="primary" danger size="small" icon={<PlayCircleOutlined />}
|
||||
loading={isGenerating}
|
||||
onClick={() => openGenModal(record)}
|
||||
@@ -396,7 +392,7 @@ const RecordsPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Failed retry */}
|
||||
{record.status === 'failed' && (
|
||||
{uiState.isFailure && (
|
||||
<div style={{ marginTop: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<Button type="primary" danger size="large" icon={<PlayCircleOutlined />}
|
||||
loading={isGenerating}
|
||||
@@ -410,7 +406,7 @@ const RecordsPage: React.FC = () => {
|
||||
|
||||
{/* Right: video preview */}
|
||||
<div style={{ flex: '1 1 40%', minWidth: 240 }}>
|
||||
{record.status === 'generating' ? (
|
||||
{uiState.isActive ? (
|
||||
<div style={{
|
||||
height: '100%', minHeight: 200, borderRadius: 14,
|
||||
background: 'linear-gradient(135deg, rgba(99,102,241,0.04), rgba(139,92,246,0.04))',
|
||||
@@ -421,7 +417,7 @@ const RecordsPage: React.FC = () => {
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}>{type === 'video' ? '视频' : '图片'}生成中...</Typography.Text>
|
||||
<Typography.Text style={{ color: '#cbd5e1', fontSize: 12 }}>请耐心等待,生成完成后将自动展示</Typography.Text>
|
||||
</div>
|
||||
) : record.status === 'completed' && (record.videoUrl || record.imageUrl) ? (
|
||||
) : uiState.isSuccess && (record.videoUrl || record.imageUrl) ? (
|
||||
<div style={{ borderRadius: 14, overflow: 'hidden', border: '1px solid #f0f0f5', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', top: 10, right: 10, zIndex: 10 }}>
|
||||
<Tooltip title={`下载${type === 'video' ? '视频' : '图片'}`}>
|
||||
@@ -465,7 +461,7 @@ const RecordsPage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : record.status === 'failed' ? (
|
||||
) : uiState.isFailure ? (
|
||||
<div style={{
|
||||
height: '100%', minHeight: 200, borderRadius: 14,
|
||||
background: 'rgba(239,68,68,0.03)',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { uploadShotReplicateImage, createRemoveLens, deleteSegment, getShotRepli
|
||||
import VideoTrimPicker from '../components/VideoTrimPicker';
|
||||
import UploadSelector from '../components/UploadSelector';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
import { getShotAnalysisStatusMeta, getShotSplitStatusMeta, isShotSegmentActive } from '../utils/shotReplicateStatus';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
@@ -67,6 +68,7 @@ function RemoveInfo() {
|
||||
const [autoSplitLoading, setAutoSplitLoading] = useState(false);
|
||||
const pollingIntervalRef = useRef<number | null>(null);
|
||||
const analysisPollingRef = useRef<number | null>(null);
|
||||
const analysisStartedAtRef = useRef<number | null>(null);
|
||||
|
||||
const videoUrl = useMemo(() => buildAssetUrl(taskDetail?.videoUrl), [taskDetail?.videoUrl]);
|
||||
|
||||
@@ -133,70 +135,7 @@ function RemoveInfo() {
|
||||
if (!creatID) return;
|
||||
try {
|
||||
const res = await Removelist(creatID);
|
||||
const items = res.items || [];
|
||||
setTableData(items);
|
||||
|
||||
// 检查是否需要开启轮询
|
||||
const needsPolling = items.some(
|
||||
(item: any) =>
|
||||
item.moduleProjectCurrentStepCode !== 'video_generate' &&
|
||||
item.moduleProjectStatus !== 'completed' &&
|
||||
item.moduleProjectStatus !== 'failed'
|
||||
);
|
||||
|
||||
if (needsPolling) {
|
||||
// 如果没有轮询在运行,则开启轮询
|
||||
if (!pollingIntervalRef.current) {
|
||||
pollingIntervalRef.current = window.setInterval(async () => {
|
||||
try {
|
||||
const pollRes = await Removelist(creatID);
|
||||
const pollItems = pollRes.items || [];
|
||||
|
||||
// 只更新未完成的项目,并检查是否需要自动分析
|
||||
setTableData((prevData) =>
|
||||
prevData.map((prevItem) => {
|
||||
const pollItem = pollItems.find((p: any) => p.id === prevItem.id);
|
||||
if (pollItem) {
|
||||
const prevSplitStatus = prevItem.split_status || prevItem.splitStatus;
|
||||
const currSplitStatus = pollItem.split_status || pollItem.splitStatus;
|
||||
|
||||
if (
|
||||
prevItem.moduleProjectCurrentStepCode !== 'video_generate' ||
|
||||
(prevItem.moduleProjectStatus !== 'completed' && prevItem.moduleProjectStatus !== 'failed')
|
||||
) {
|
||||
return pollItem;
|
||||
}
|
||||
}
|
||||
return prevItem;
|
||||
})
|
||||
);
|
||||
|
||||
// 检查是否所有项目都已完成
|
||||
const allCompleted = pollItems.every(
|
||||
(item: any) =>
|
||||
item.moduleProjectCurrentStepCode === 'video_generate' ||
|
||||
item.moduleProjectStatus === 'completed' ||
|
||||
item.moduleProjectStatus === 'failed'
|
||||
);
|
||||
|
||||
if (allCompleted) {
|
||||
// 停止轮询
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}, 30000); // 30秒轮询
|
||||
}
|
||||
} else {
|
||||
// 如果不需要轮询,停止现有轮询
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
}
|
||||
setTableData(res.items || []);
|
||||
} catch {
|
||||
message.error('获取拆镜列表失败');
|
||||
}
|
||||
@@ -207,21 +146,8 @@ function RemoveInfo() {
|
||||
try {
|
||||
await reanalyzeShotReplication(creatID);
|
||||
message.success('重新分析已提交');
|
||||
fetchTaskDetail();
|
||||
if (!analysisPollingRef.current) {
|
||||
analysisPollingRef.current = window.setInterval(async () => {
|
||||
try {
|
||||
const res = await getShotReplicationDetail(creatID);
|
||||
setTaskDetail(res);
|
||||
if (res.analysisStatus === 'completed' || res.analysisStatus === 'failed') {
|
||||
if (analysisPollingRef.current) {
|
||||
clearInterval(analysisPollingRef.current);
|
||||
analysisPollingRef.current = null;
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}, 3000);
|
||||
}
|
||||
analysisStartedAtRef.current = Date.now();
|
||||
await fetchTaskDetail();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '重新分析失败');
|
||||
}
|
||||
@@ -242,49 +168,10 @@ function RemoveInfo() {
|
||||
await retrySplit(segmentId);
|
||||
message.success('重新切割已提交');
|
||||
await fetchSegments();
|
||||
if (!pollingIntervalRef.current && creatID) {
|
||||
pollingIntervalRef.current = window.setInterval(async () => {
|
||||
try {
|
||||
const pollRes = await Removelist(creatID);
|
||||
const pollItems = pollRes.items || [];
|
||||
setTableData((prevData) =>
|
||||
prevData.map((prevItem) => {
|
||||
const pollItem = pollItems.find((p: any) => p.id === prevItem.id);
|
||||
if (pollItem) {
|
||||
if (
|
||||
prevItem.moduleProjectCurrentStepCode !== 'video_generate' ||
|
||||
(prevItem.moduleProjectStatus !== 'completed' && prevItem.moduleProjectStatus !== 'failed')
|
||||
) {
|
||||
return pollItem;
|
||||
}
|
||||
}
|
||||
return prevItem;
|
||||
})
|
||||
);
|
||||
const allCompleted = pollItems.every(
|
||||
(item: any) =>
|
||||
item.moduleProjectCurrentStepCode === 'video_generate' ||
|
||||
item.moduleProjectStatus === 'completed' ||
|
||||
item.moduleProjectStatus === 'failed'
|
||||
);
|
||||
if (allCompleted) {
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '重新切割失败');
|
||||
}
|
||||
}, [creatID, fetchSegments]);
|
||||
}, [fetchSegments]);
|
||||
|
||||
const handleDeleteSegment = useCallback(async (segmentId: string) => {
|
||||
try {
|
||||
@@ -302,46 +189,79 @@ function RemoveInfo() {
|
||||
|
||||
useEffect(() => {
|
||||
refreshPageData();
|
||||
|
||||
return () => {
|
||||
if (pollingIntervalRef.current) {
|
||||
clearInterval(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
if (analysisPollingRef.current) {
|
||||
clearInterval(analysisPollingRef.current);
|
||||
analysisPollingRef.current = null;
|
||||
}
|
||||
if (pollingIntervalRef.current) window.clearTimeout(pollingIntervalRef.current);
|
||||
if (analysisPollingRef.current) window.clearTimeout(analysisPollingRef.current);
|
||||
};
|
||||
}, [refreshPageData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!taskDetail) return;
|
||||
|
||||
if (taskDetail.analysisStatus === 'processing') {
|
||||
|
||||
analysisPollingRef.current = window.setInterval(async () => {
|
||||
try {
|
||||
const res = await getShotReplicationDetail(creatID);
|
||||
setTaskDetail(res);
|
||||
} catch {
|
||||
}
|
||||
}, 3000);
|
||||
} else {
|
||||
const processing = taskDetail?.analysisStatus === 'processing';
|
||||
if (!processing || !creatID) {
|
||||
analysisStartedAtRef.current = null;
|
||||
if (analysisPollingRef.current) {
|
||||
clearInterval(analysisPollingRef.current);
|
||||
window.clearTimeout(analysisPollingRef.current);
|
||||
analysisPollingRef.current = null;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (analysisPollingRef.current) {
|
||||
clearInterval(analysisPollingRef.current);
|
||||
analysisPollingRef.current = null;
|
||||
if (!analysisStartedAtRef.current) analysisStartedAtRef.current = Date.now();
|
||||
const elapsed = Date.now() - analysisStartedAtRef.current;
|
||||
const delay = elapsed < 60_000 ? 3_000 : (elapsed < 600_000 ? 10_000 : 30_000);
|
||||
|
||||
const poll = async () => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
try {
|
||||
const res = await getShotReplicationDetail(creatID);
|
||||
setTaskDetail(res);
|
||||
} catch {
|
||||
// 保留处理中状态,下一轮继续重试。
|
||||
}
|
||||
};
|
||||
|
||||
analysisPollingRef.current = window.setTimeout(() => void poll(), delay);
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === 'visible') void poll();
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
return () => {
|
||||
if (analysisPollingRef.current) {
|
||||
window.clearTimeout(analysisPollingRef.current);
|
||||
analysisPollingRef.current = null;
|
||||
}
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
};
|
||||
}, [taskDetail?.analysisStatus, creatID]);
|
||||
|
||||
const hasActiveSegments = tableData.some((item) => isShotSegmentActive(item));
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasActiveSegments || !creatID) {
|
||||
if (pollingIntervalRef.current) {
|
||||
window.clearTimeout(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const poll = async () => {
|
||||
if (document.visibilityState !== 'visible') return;
|
||||
await fetchSegments();
|
||||
};
|
||||
pollingIntervalRef.current = window.setTimeout(() => void poll(), 30_000);
|
||||
const handleVisibility = () => {
|
||||
if (document.visibilityState === 'visible') void poll();
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibility);
|
||||
return () => {
|
||||
if (pollingIntervalRef.current) {
|
||||
window.clearTimeout(pollingIntervalRef.current);
|
||||
pollingIntervalRef.current = null;
|
||||
}
|
||||
document.removeEventListener('visibilitychange', handleVisibility);
|
||||
};
|
||||
}, [creatID, fetchSegments, hasActiveSegments]);
|
||||
|
||||
const handleGenerate = (segmentId: string, segmentName: string) => {
|
||||
setCurrentSegment(segmentId);
|
||||
setCurrentSegmentName(segmentName);
|
||||
@@ -496,7 +416,8 @@ function RemoveInfo() {
|
||||
};
|
||||
|
||||
const canCreateReplication = (record: any) => {
|
||||
return record?.splitStatus === 'completed' && !!record?.segmentVideoUrl;
|
||||
const splitStatus = record?.splitStatus || record?.split_status;
|
||||
return splitStatus === 'completed' && !!(record?.segmentVideoUrl || record?.segment_video_url);
|
||||
};
|
||||
|
||||
const columns: any[] = [
|
||||
@@ -539,23 +460,15 @@ function RemoveInfo() {
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontSize: 12 }}>
|
||||
{record.splitStatus === 'failed' ? (
|
||||
<span style={{ color: '#94a3b8' }}>切割失败</span>
|
||||
) : record.splitStatus === 'pending' ? (
|
||||
<>
|
||||
<Spin size="small" style={{ color: '#f59e0b' }} />
|
||||
<span style={{ color: '#f59e0b' }}>切割中</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Spin size="small" style={{ color: '#f59e0b' }} />
|
||||
<span style={{ color: '#f59e0b' }}>切割中</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : (() => {
|
||||
const splitMeta = getShotSplitStatusMeta(record.splitStatus || record.split_status);
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontSize: 12 }}>
|
||||
{splitMeta.active ? <Spin size="small" style={{ color: '#f59e0b' }} /> : null}
|
||||
<span style={{ color: splitMeta.failure ? '#ef4444' : (splitMeta.active ? '#f59e0b' : '#94a3b8') }}>{splitMeta.label}</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -564,52 +477,38 @@ function RemoveInfo() {
|
||||
width: 500,
|
||||
align: 'left' as const,
|
||||
render: (_: any, record: any) => {
|
||||
const splitStatus = record.split_status || record.splitStatus;
|
||||
|
||||
if (splitStatus === 'pending') {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Spin size="small" style={{ color: '#f59e0b' }} />
|
||||
<span style={{ fontSize: 14, color: '#f59e0b' }}>切割中</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (splitStatus === 'processing') {
|
||||
const splitMeta = getShotSplitStatusMeta(record.split_status || record.splitStatus);
|
||||
if (splitMeta.active) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Spin size="small" />
|
||||
<span style={{ fontSize: 14, color: '#f59e0b' }}>切割中</span>
|
||||
<span style={{ fontSize: 14, color: '#f59e0b' }}>{splitMeta.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (splitMeta.failure) {
|
||||
return <div style={{ fontSize: 14, color: '#ef4444', lineHeight: 1.6 }}>{splitMeta.label}</div>;
|
||||
}
|
||||
|
||||
if (record.source_mode === 'ai_suggestion' || record.sourceMode === 'ai_suggestion') {
|
||||
return <div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>{record.segmentContent || record.lastError || '-'}</div>;
|
||||
}
|
||||
|
||||
const analysisStatus = record.analysis_status || record.analysisStatus;
|
||||
const statusMap: Record<string, string> = {
|
||||
'not_required': '无需单独分析',
|
||||
'pending': '等待分析',
|
||||
'processing': '分析中',
|
||||
'failed': '分析失败',
|
||||
};
|
||||
|
||||
if (analysisStatus === 'processing') {
|
||||
const analysisMeta = getShotAnalysisStatusMeta(record.analysis_status || record.analysisStatus);
|
||||
if (analysisMeta.active) {
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<Spin size="small" />
|
||||
<span style={{ fontSize: 14, color: '#f59e0b' }}>分析中</span>
|
||||
<span style={{ fontSize: 14, color: '#f59e0b' }}>{analysisMeta.label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (analysisStatus === 'failed') {
|
||||
return <div style={{ fontSize: 14, color: '#ef4444', lineHeight: 1.6 }}>分析失败</div>;
|
||||
if (analysisMeta.failure) {
|
||||
return <div style={{ fontSize: 14, color: '#ef4444', lineHeight: 1.6 }}>{analysisMeta.label}</div>;
|
||||
}
|
||||
|
||||
const displayText = analysisStatus && statusMap[analysisStatus] ? statusMap[analysisStatus] : (record.segmentContent || record.lastError || '-');
|
||||
const displayText = (analysisMeta.key && analysisMeta.key !== 'completed')
|
||||
? analysisMeta.label
|
||||
: (record.segmentContent || record.lastError || '-');
|
||||
return <div style={{ fontSize: 14, color: '#333', lineHeight: 1.6 }}>{displayText}</div>;
|
||||
},
|
||||
},
|
||||
@@ -760,7 +659,7 @@ function RemoveInfo() {
|
||||
{canCreateReplication(record) ? '视频生成' : '待切割完成'}
|
||||
</Button>
|
||||
)}
|
||||
{record.analysisStatus === 'failed' && (
|
||||
{getShotAnalysisStatusMeta(record.analysisStatus || record.analysis_status).failure && (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => handleSegmentReanalyze(String(record.id))}
|
||||
@@ -769,7 +668,7 @@ function RemoveInfo() {
|
||||
重新分析
|
||||
</Button>
|
||||
)}
|
||||
{record.splitStatus === 'failed' && (
|
||||
{getShotSplitStatusMeta(record.splitStatus || record.split_status).failure && (
|
||||
<Button
|
||||
type="text"
|
||||
onClick={() => handleRetrySplit(String(record.id))}
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
||||
import { Button, Modal, Input, Table, Upload, Popconfirm, Tag, Space, message } from 'antd';
|
||||
import { FileTextOutlined, CloudUploadOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { uploadShotReplicateVideo, createShotReplication, getShotReplicationList, deleteShotReplicationProject } from '../api';
|
||||
|
||||
import bg1 from '../assets/bg1.png';
|
||||
|
||||
const statusConfig: Record<string, { label: string; color: string }> = {
|
||||
pending_analysis: { label: '等待分析', color: 'default' },
|
||||
analyzing: { label: '分析中', color: 'processing' },
|
||||
analysis_completed: { label: '分析完成', color: 'blue' },
|
||||
analysis_failed: { label: '分析失败', color: 'red' },
|
||||
splitting: { label: '拆镜中', color: 'processing' },
|
||||
split_completed: { label: '拆镜完成', color: 'green' },
|
||||
partial_failed: { label: '部分失败', color: 'orange' },
|
||||
failed: { label: '失败', color: 'red' },
|
||||
deleted: { label: '已软删', color: 'default' },
|
||||
};
|
||||
import { getShotTaskStatusMeta } from '../utils/shotReplicateStatus';
|
||||
|
||||
const renderStatus = (status: string) => {
|
||||
const config = statusConfig[status] || { label: status, color: 'default' };
|
||||
const config = getShotTaskStatusMeta(status);
|
||||
return <Tag color={config.color}>{config.label}</Tag>;
|
||||
};
|
||||
|
||||
@@ -147,7 +135,7 @@ export default function VideoFrameExtractor() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchList = async (page: number, size: number, keyword?: string) => {
|
||||
const fetchList = useCallback(async (page: number, size: number, keyword?: string) => {
|
||||
try {
|
||||
const res = await getShotReplicationList(page, size, keyword);
|
||||
setTableData(res.items || []);
|
||||
@@ -157,7 +145,7 @@ export default function VideoFrameExtractor() {
|
||||
} catch (err) {
|
||||
message.error('获取列表失败');
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handlePageChange = (page: number, size: number) => {
|
||||
fetchList(page, size, searchKeyword);
|
||||
@@ -172,6 +160,23 @@ export default function VideoFrameExtractor() {
|
||||
fetchList(1, 10, searchKeyword);
|
||||
};
|
||||
|
||||
const hasActiveRecords = tableData.some((item) => getShotTaskStatusMeta(item.status).active);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isModalOpen || !hasActiveRecords) return undefined;
|
||||
const refresh = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
void fetchList(currentPage, pageSize, searchKeyword);
|
||||
}
|
||||
};
|
||||
const timer = window.setInterval(refresh, 30000);
|
||||
document.addEventListener('visibilitychange', refresh);
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
document.removeEventListener('visibilitychange', refresh);
|
||||
};
|
||||
}, [currentPage, fetchList, hasActiveRecords, isModalOpen, pageSize, searchKeyword]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -122,9 +122,38 @@ export type Resolution = '480p' | '720p' | '1080p' | string;
|
||||
export type GenerationStatus =
|
||||
| 'optimizing'
|
||||
| 'prompt_optimized'
|
||||
| 'pending'
|
||||
| 'generating'
|
||||
| 'completed'
|
||||
| 'failed';
|
||||
| 'failed'
|
||||
| 'timeout'
|
||||
| 'download_failed'
|
||||
| 'upscale_failed'
|
||||
| (string & {});
|
||||
|
||||
export type GenerationPipelineStage =
|
||||
| 'queued'
|
||||
| 'preparing'
|
||||
| 'creating_provider_task'
|
||||
| 'provider_result_staged'
|
||||
| 'waiting_remote'
|
||||
| 'polling'
|
||||
| 'result_ready'
|
||||
| 'download_queued'
|
||||
| 'downloading'
|
||||
| 'retry_waiting'
|
||||
| 'upscale_queued'
|
||||
| 'upscale_processing'
|
||||
| 'upscale_polling'
|
||||
| 'upscale_downloading'
|
||||
| 'upscale_finalizing'
|
||||
| 'upscale_retry_waiting'
|
||||
| 'upscale_failed'
|
||||
| 'done'
|
||||
| 'failed'
|
||||
| 'timeout'
|
||||
| 'download_failed'
|
||||
| (string & {});
|
||||
|
||||
export interface MediaReference {
|
||||
url: string;
|
||||
@@ -156,7 +185,10 @@ export interface GenerationRecord {
|
||||
aspectRatio?: AspectRatio;
|
||||
resolution?: Resolution;
|
||||
status: GenerationStatus;
|
||||
pipelineStage?: GenerationPipelineStage | null;
|
||||
videoUrl?: string;
|
||||
videoCoverUrl?: string;
|
||||
videoUpscaleEnabled?: boolean;
|
||||
references?: MediaReference[];
|
||||
textCreditsCost: number;
|
||||
textTokensUsed: number;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
export interface GenerationStatusLike {
|
||||
status?: string | null;
|
||||
displayStatus?: string | null;
|
||||
pipelineStage?: string | null;
|
||||
}
|
||||
|
||||
export type GenerationUiColor = 'default' | 'processing' | 'warning' | 'success' | 'error' | 'blue' | 'orange' | 'purple';
|
||||
|
||||
export interface GenerationUiState {
|
||||
status: string;
|
||||
displayStatus: string;
|
||||
pipelineStage: string;
|
||||
effectiveKey: string;
|
||||
label: string;
|
||||
color: GenerationUiColor;
|
||||
isActive: boolean;
|
||||
isSuccess: boolean;
|
||||
isFailure: boolean;
|
||||
isTerminal: boolean;
|
||||
}
|
||||
|
||||
const ACTIVE_STATUS_KEYS = new Set([
|
||||
'pending',
|
||||
'optimizing',
|
||||
'prompt_optimized',
|
||||
'generating',
|
||||
]);
|
||||
|
||||
const ACTIVE_PIPELINE_STAGES = new Set([
|
||||
'queued',
|
||||
'preparing',
|
||||
'creating_provider_task',
|
||||
'provider_result_staged',
|
||||
'waiting_remote',
|
||||
'polling',
|
||||
'result_ready',
|
||||
'download_queued',
|
||||
'downloading',
|
||||
'retry_waiting',
|
||||
'upscale_queued',
|
||||
'upscale_processing',
|
||||
'upscale_polling',
|
||||
'upscale_downloading',
|
||||
'upscale_finalizing',
|
||||
'upscale_retry_waiting',
|
||||
]);
|
||||
|
||||
const SUCCESS_KEYS = new Set(['completed', 'done']);
|
||||
const FAILURE_KEYS = new Set(['failed', 'timeout', 'download_failed', 'upscale_failed']);
|
||||
const TERMINAL_KEYS = new Set([...SUCCESS_KEYS, ...FAILURE_KEYS, 'deleted']);
|
||||
|
||||
const LABELS: Record<string, string> = {
|
||||
pending: '待处理',
|
||||
optimizing: '优化中',
|
||||
prompt_optimized: '待生成',
|
||||
generating: '生成中',
|
||||
queued: '排队中',
|
||||
preparing: '准备中',
|
||||
creating_provider_task: '创建任务中',
|
||||
provider_result_staged: '结果处理中',
|
||||
waiting_remote: '等待生成',
|
||||
polling: '查询生成进度',
|
||||
result_ready: '结果处理中',
|
||||
download_queued: '等待下载',
|
||||
downloading: '下载中',
|
||||
retry_waiting: '等待重试',
|
||||
upscale_queued: '等待画质增强',
|
||||
upscale_processing: '画质增强中',
|
||||
upscale_polling: '查询增强进度',
|
||||
upscale_downloading: '下载增强结果',
|
||||
upscale_finalizing: '结果处理中',
|
||||
upscale_retry_waiting: '画质增强等待重试',
|
||||
completed: '已完成',
|
||||
done: '已完成',
|
||||
timeout: '生成超时',
|
||||
download_failed: '下载失败',
|
||||
upscale_failed: '画质增强失败',
|
||||
failed: '生成失败',
|
||||
deleted: '已删除',
|
||||
};
|
||||
|
||||
const COLOR_MAP: Record<string, GenerationUiColor> = {
|
||||
pending: 'default',
|
||||
optimizing: 'processing',
|
||||
prompt_optimized: 'processing',
|
||||
generating: 'warning',
|
||||
queued: 'processing',
|
||||
preparing: 'processing',
|
||||
creating_provider_task: 'processing',
|
||||
provider_result_staged: 'processing',
|
||||
waiting_remote: 'processing',
|
||||
polling: 'processing',
|
||||
result_ready: 'processing',
|
||||
download_queued: 'processing',
|
||||
downloading: 'processing',
|
||||
retry_waiting: 'orange',
|
||||
upscale_queued: 'purple',
|
||||
upscale_processing: 'purple',
|
||||
upscale_polling: 'purple',
|
||||
upscale_downloading: 'purple',
|
||||
upscale_finalizing: 'purple',
|
||||
upscale_retry_waiting: 'orange',
|
||||
completed: 'success',
|
||||
done: 'success',
|
||||
failed: 'error',
|
||||
timeout: 'error',
|
||||
download_failed: 'error',
|
||||
upscale_failed: 'error',
|
||||
deleted: 'default',
|
||||
};
|
||||
|
||||
const normalize = (value?: string | null): string => String(value || '').trim().toLowerCase();
|
||||
|
||||
export const getGenerationStageLabel = (key?: string | null): string => {
|
||||
const normalized = normalize(key);
|
||||
return LABELS[normalized] || normalized || '未知状态';
|
||||
};
|
||||
|
||||
export const getGenerationStatusColor = (key?: string | null): GenerationUiColor => {
|
||||
const normalized = normalize(key);
|
||||
return COLOR_MAP[normalized] || 'default';
|
||||
};
|
||||
|
||||
const firstMatching = (values: string[], keys: Set<string>): string => (
|
||||
values.find((value) => keys.has(value)) || ''
|
||||
);
|
||||
|
||||
export const resolveGenerationUiState = (value: GenerationStatusLike): GenerationUiState => {
|
||||
const status = normalize(value.status);
|
||||
const displayStatus = normalize(value.displayStatus);
|
||||
const pipelineStage = normalize(value.pipelineStage);
|
||||
const values = [displayStatus, status, pipelineStage].filter(Boolean);
|
||||
|
||||
const failureKey = FAILURE_KEYS.has(pipelineStage)
|
||||
? pipelineStage
|
||||
: firstMatching([displayStatus, status], FAILURE_KEYS);
|
||||
const deletedKey = firstMatching(values, new Set(['deleted']));
|
||||
const successKey = firstMatching(values, SUCCESS_KEYS);
|
||||
|
||||
let effectiveKey = '';
|
||||
if (failureKey) {
|
||||
effectiveKey = failureKey;
|
||||
} else if (deletedKey) {
|
||||
effectiveKey = deletedKey;
|
||||
} else if (ACTIVE_PIPELINE_STAGES.has(pipelineStage)) {
|
||||
effectiveKey = pipelineStage;
|
||||
} else if (successKey) {
|
||||
effectiveKey = successKey;
|
||||
} else if (pipelineStage) {
|
||||
effectiveKey = pipelineStage;
|
||||
} else {
|
||||
effectiveKey = displayStatus || status || 'pending';
|
||||
}
|
||||
|
||||
const isFailure = FAILURE_KEYS.has(effectiveKey);
|
||||
const isSuccess = SUCCESS_KEYS.has(effectiveKey);
|
||||
const isActive = !isFailure && !isSuccess && effectiveKey !== 'deleted' && (
|
||||
ACTIVE_PIPELINE_STAGES.has(pipelineStage)
|
||||
|| ACTIVE_STATUS_KEYS.has(displayStatus)
|
||||
|| ACTIVE_STATUS_KEYS.has(status)
|
||||
|| ACTIVE_PIPELINE_STAGES.has(effectiveKey)
|
||||
);
|
||||
|
||||
return {
|
||||
status,
|
||||
displayStatus,
|
||||
pipelineStage,
|
||||
effectiveKey,
|
||||
label: getGenerationStageLabel(effectiveKey),
|
||||
color: getGenerationStatusColor(effectiveKey),
|
||||
isActive,
|
||||
isSuccess,
|
||||
isFailure,
|
||||
isTerminal: TERMINAL_KEYS.has(effectiveKey),
|
||||
};
|
||||
};
|
||||
|
||||
export const isGenerationActive = (value: GenerationStatusLike): boolean => resolveGenerationUiState(value).isActive;
|
||||
export const isGenerationSuccess = (value: GenerationStatusLike): boolean => resolveGenerationUiState(value).isSuccess;
|
||||
export const isGenerationFailure = (value: GenerationStatusLike): boolean => resolveGenerationUiState(value).isFailure;
|
||||
export const isGenerationTerminal = (value: GenerationStatusLike): boolean => resolveGenerationUiState(value).isTerminal;
|
||||
@@ -0,0 +1,77 @@
|
||||
export interface ShotStatusLike {
|
||||
status?: string | null;
|
||||
analysisStatus?: string | null;
|
||||
analysis_status?: string | null;
|
||||
splitStatus?: string | null;
|
||||
split_status?: string | null;
|
||||
moduleProjectStatus?: string | null;
|
||||
module_project_status?: string | null;
|
||||
}
|
||||
|
||||
export interface ShotStatusMeta {
|
||||
key: string;
|
||||
label: string;
|
||||
color: string;
|
||||
active: boolean;
|
||||
terminal: boolean;
|
||||
failure: boolean;
|
||||
}
|
||||
|
||||
const ANALYSIS_META: Record<string, Omit<ShotStatusMeta, 'key'>> = {
|
||||
not_required: { label: '无需单独分析', color: 'default', active: false, terminal: true, failure: false },
|
||||
pending: { label: '等待分析', color: 'default', active: true, terminal: false, failure: false },
|
||||
processing: { label: '分析中', color: 'processing', active: true, terminal: false, failure: false },
|
||||
completed: { label: '分析完成', color: 'success', active: false, terminal: true, failure: false },
|
||||
failed: { label: '分析失败', color: 'error', active: false, terminal: true, failure: true },
|
||||
};
|
||||
|
||||
const SPLIT_META: Record<string, Omit<ShotStatusMeta, 'key'>> = {
|
||||
pending: { label: '等待切割', color: 'default', active: true, terminal: false, failure: false },
|
||||
processing: { label: '切割中', color: 'processing', active: true, terminal: false, failure: false },
|
||||
retry_waiting: { label: '等待切割重试', color: 'warning', active: true, terminal: false, failure: false },
|
||||
completed: { label: '切割完成', color: 'success', active: false, terminal: true, failure: false },
|
||||
failed: { label: '切割失败', color: 'error', active: false, terminal: true, failure: true },
|
||||
};
|
||||
|
||||
const TASK_META: Record<string, Omit<ShotStatusMeta, 'key'>> = {
|
||||
pending_analysis: { label: '等待分析', color: 'default', active: true, terminal: false, failure: false },
|
||||
analyzing: { label: '分析中', color: 'processing', active: true, terminal: false, failure: false },
|
||||
analysis_completed: { label: '分析完成', color: 'blue', active: false, terminal: false, failure: false },
|
||||
analysis_failed: { label: '分析失败', color: 'error', active: false, terminal: true, failure: true },
|
||||
splitting: { label: '拆镜中', color: 'processing', active: true, terminal: false, failure: false },
|
||||
split_completed: { label: '拆镜完成', color: 'success', active: false, terminal: true, failure: false },
|
||||
partial_failed: { label: '部分失败', color: 'warning', active: false, terminal: true, failure: true },
|
||||
failed: { label: '失败', color: 'error', active: false, terminal: true, failure: true },
|
||||
deleted: { label: '已删除', color: 'default', active: false, terminal: true, failure: false },
|
||||
};
|
||||
|
||||
const MODULE_ACTIVE = new Set(['pending', 'waiting_user', 'processing']);
|
||||
const normalize = (value?: string | null): string => String(value || '').trim().toLowerCase();
|
||||
const buildMeta = (key: string, map: Record<string, Omit<ShotStatusMeta, 'key'>>, fallback: string): ShotStatusMeta => ({
|
||||
key,
|
||||
...(map[key] || { label: key || fallback, color: 'default', active: false, terminal: false, failure: false }),
|
||||
});
|
||||
|
||||
export const getShotAnalysisStatusMeta = (status?: string | null): ShotStatusMeta => {
|
||||
const key = normalize(status);
|
||||
return buildMeta(key, ANALYSIS_META, '未知分析状态');
|
||||
};
|
||||
|
||||
export const getShotSplitStatusMeta = (status?: string | null): ShotStatusMeta => {
|
||||
const key = normalize(status);
|
||||
return buildMeta(key, SPLIT_META, '未知切割状态');
|
||||
};
|
||||
|
||||
export const getShotTaskStatusMeta = (status?: string | null): ShotStatusMeta => {
|
||||
const key = normalize(status);
|
||||
return buildMeta(key, TASK_META, '未知任务状态');
|
||||
};
|
||||
|
||||
export const isShotSegmentActive = (item: ShotStatusLike): boolean => {
|
||||
const analysisStatus = normalize(item.analysisStatus || item.analysis_status);
|
||||
const splitStatus = normalize(item.splitStatus || item.split_status);
|
||||
const moduleStatus = normalize(item.moduleProjectStatus || item.module_project_status);
|
||||
return getShotAnalysisStatusMeta(analysisStatus).active
|
||||
|| getShotSplitStatusMeta(splitStatus).active
|
||||
|| MODULE_ACTIVE.has(moduleStatus);
|
||||
};
|
||||
Reference in New Issue
Block a user