合并修改

This commit is contained in:
sjy
2026-07-22 14:52:08 +08:00
67 changed files with 6127 additions and 1923 deletions
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -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>
)}
+32 -54
View File
@@ -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]}
+15 -19
View File
@@ -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)',
+92 -193
View File
@@ -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))}
+22 -17
View File
@@ -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={{
+33 -1
View File
@@ -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);
};