Files
video-gen/video-gen-app/src/pages/InitialInfo.tsx
T
2026-06-24 09:10:45 +08:00

1406 lines
98 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { Button, Typography, Collapse, Space, Modal, Input, Table, message } from 'antd';
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined } from '@ant-design/icons';
import { useNavigate, useParams } from 'react-router-dom';
import { getReplicationList, getReplicationDetail, gettwo, getthree, getfour, getEngine, updateHotOpeningVideoPromptSchema, updateImagePrompt } from '../api/index';
import VideoPromptSchemaEditor from '../components/VideoPromptSchemaEditor';
import { validateVideoPromptSchemaByConfig } from '../utils/videoPromptSchema';
import './css/InitialInfo.css';
const { Title, Text } = Typography;
const { TextArea } = Input;
function clonePlain<T>(value: T): T {
return value === undefined ? value : JSON.parse(JSON.stringify(value));
}
function InitialInfo() {
const navigate = useNavigate();
const { creatID } = useParams<{ creatID: string }>();
const [modalVisible, setModalVisible] = useState(false);
const [promptText, setPromptText] = useState('');
const [isModalOpen, setIsModalOpen] = useState(false);
const [currentType, setCurrentType] = useState<string>('image');
const [formData, setFormData] = useState<any>({});
const [videoSchemaConfigSnapshot, setVideoSchemaConfigSnapshot] = useState<any>(null);
const [editingPromptStepId, setEditingPromptStepId] = useState<string>('');
const [promptSaving, setPromptSaving] = useState(false);
const [pollingTimer, setPollingTimer] = useState<any>(null);
// 引擎和视频参数相关状态
const [enginesele, setEnginesele] = useState<any>({});
const [countType, setCountType] = useState('');
const [showEngineModal, setShowEngineModal] = useState(false);
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
const [videoDuration, setVideoDuration] = useState(5);
const [videoAspectRatio, setVideoAspectRatio] = useState<string>('16:9');
const [videoResolution, setVideoResolution] = useState<string>('480p');
const [engineOptions, setEngineOptions] = useState<{
ratios: string[];
resolutions: string[];
durations: number[];
}>({
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
resolutions: ['480p', '720p', '1080p'],
durations: [5, 8, 10, 12, 15],
});
// 创作记录表格数据
const [tableData, setTableData] = useState<any[]>([]);
// 分页状态
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [total, setTotal] = useState(0);
const [searchKeyword, setSearchKeyword] = useState('');
// 任务详情数据
const [taskDetail, setTaskDetail] = useState<any>({});
// API 返回的步骤数据
const [apiSteps, setApiSteps] = useState<any[]>([]);
// 当前展开的步骤
const [activeKey, setActiveKey] = useState<string[]>([]);
//
const baseSteps = [
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
];
// 合并基础步骤和API返回的状态
const steps = baseSteps.map((step, index) => ({
...step,
status: apiSteps[index]?.status || '',
id: apiSteps[index]?.id || index,
output: apiSteps[index]?.output || '',
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
}));
// 当apiSteps更新时,逆向遍历找到第一个已完成或失败的步骤并展开
useEffect(() => {
for (let i = steps.length - 1; i >= 0; i--) {
if (steps[i].status === 'completed' || steps[i].status === 'failed') {
setActiveKey([String(steps[i].childId)]);
return;
}
}
setActiveKey([]);
}, [apiSteps]);
// console.log('steps', steps);
// 获取列表数据的函数
const fetchList = (page: number, size: number, keyword?: string) => {
getReplicationList(page, size, keyword).then((res: any) => {
if (res.items) {
setTableData(res.items);
}
if (res.total !== undefined) {
setTotal(res.total);
}
}).catch((error: any) => {
});
};
// 分页变化处理
const handlePageChange = (page: number, size: number) => {
setCurrentPage(page);
setPageSize(size);
fetchList(page, size, searchKeyword);
};
// 搜索处理
const handleSearch = () => {
setCurrentPage(1);
fetchList(1, pageSize, searchKeyword);
};
// 获取复刻列表数据
useEffect(() => {
fetchList(currentPage, pageSize);
}, []);
// 获取引擎列表
useEffect(() => {
getEngine()
.then((data: any) => {
setEnginesele(data.engine || {});
// 默认选中第一个视频引擎
if (data.engine?.video && data.engine.video.length > 0) {
setCountType(data.engine.video[0].id);
// 设置默认引擎参数
const firstEngine = data.engine.video[0];
setEngineOptions({
ratios: firstEngine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
resolutions: firstEngine.supportedResolutions || ['480p', '720p', '1080p'],
durations: firstEngine.supportedDurations || [5, 8, 10, 12, 15],
});
}
})
.catch((error: any) => {
});
}, []);
// 组件卸载时清理定时器
useEffect(() => {
return () => {
if (pollingTimer) {
clearInterval(pollingTimer);
setPollingTimer(null);
}
};
}, [pollingTimer]);
// 点击外部关闭弹窗
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest('.image-settings-trigger') && !target.closest('.image-settings-popover')) {
setShowEngineModal(false);
setShowVideoSettingsModal(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// 获取任务详情数据
useEffect(() => {
if (creatID) {
getReplicationDetail(creatID).then((res: any) => {
setTaskDetail(res);
if (res.steps) {
setApiSteps(res.steps);
}
}).catch((error: any) => {
});
}
}, [creatID]);
// 监听合并后的 steps 数据,判断第五步状态并启动/停止轮询
useEffect(() => {
// 检查第五步的状态(索引 4
const fifthStep = steps[4];
if (fifthStep && fifthStep.status !== 'completed' && fifthStep.status !== 'failed') {
// 第五步未完成,启动轮询
if (!pollingTimer) {
const timer = setInterval(pollTaskDetail, 30000);
setPollingTimer(timer);
} else {
}
} else {
// 第五步已完成或失败,停止轮询
if (fifthStep) {
if (pollingTimer) {
clearInterval(pollingTimer);
setPollingTimer(null);
}
}
}
}, [steps]);
const handleOpenModal = (prompt?: any, type?: string, stepId?: string | number, schemaConfigSnapshot?: any) => {
setCurrentType(type || 'image');
setEditingPromptStepId(stepId ? String(stepId) : '');
if (type === 'video') {
setVideoSchemaConfigSnapshot(schemaConfigSnapshot || null);
setFormData(prompt && typeof prompt === 'object' ? clonePlain(prompt) : {});
setPromptText('');
} else {
setVideoSchemaConfigSnapshot(null);
setPromptText(prompt || '');
setFormData({});
}
setModalVisible(true);
};
const handleConfirm = async () => {
if (currentType === 'image') {
if (!taskDetail?.id || !editingPromptStepId) {
message.warning('缺少任务或步骤 ID,无法保存图片提示词');
return;
}
if (!promptText || !promptText.trim()) {
message.warning('图片提示词不能为空');
return;
}
setPromptSaving(true);
try {
await updateImagePrompt(taskDetail.id, editingPromptStepId, {
prompt: promptText.trim(),
});
message.success('图片提示词已保存');
refreshTaskDetail();
setModalVisible(false);
setEditingPromptStepId('');
} catch (error: any) {
message.error(error?.message || '保存图片提示词失败');
} finally {
setPromptSaving(false);
}
return;
}
if (!taskDetail?.id || !editingPromptStepId) {
message.warning('缺少任务或步骤 ID,无法保存视频提示词');
return;
}
if (!formData || typeof formData !== 'object' || Object.keys(formData).length === 0) {
message.warning('视频提示词不能为空');
return;
}
if (!videoSchemaConfigSnapshot) {
message.warning('视频提词配置缺失,暂不能保存');
return;
}
const schemaError = validateVideoPromptSchemaByConfig(formData, videoSchemaConfigSnapshot);
if (schemaError) {
message.warning(schemaError);
return;
}
setPromptSaving(true);
try {
await updateHotOpeningVideoPromptSchema(taskDetail.id, editingPromptStepId, {
prompt_schema: formData,
});
message.success('视频提示词已保存');
refreshTaskDetail();
setModalVisible(false);
setEditingPromptStepId('');
} catch (error: any) {
message.error(error?.message || '保存视频提示词失败');
} finally {
setPromptSaving(false);
}
};
// 轮询任务详情
const pollTaskDetail = () => {
if (!creatID) {
return;
}
getReplicationDetail(creatID).then((res: any) => {
setTaskDetail(res);
if (res.steps) {
setApiSteps(res.steps);
}
}).catch((error: any) => {
});
};
// 刷新任务详情(用于点击下一步后更新数据)
const refreshTaskDetail = () => {
if (!creatID) return;
getReplicationDetail(creatID).then((res: any) => {
setTaskDetail(res);
if (res.steps) {
setApiSteps(res.steps);
}
}).catch((error: any) => {
});
};
const createimage = (stepId: number) => {
let params = {
engine_id: "0019e3dac0b795b925b",
image_proportion: "1:1",
image_px: "2048x2048",
image_size: "2K"
}
gettwo(taskDetail.id, stepId.toString(), params).then((res: any) => {
// 重新获取任务详情以更新数据
refreshTaskDetail();
}).catch((error: any) => {
});
}
const newcreateimage = () => {
// console.log('下一步:', stepId);
let params = {
engine_id: "0019e3dac0b795b925b",
image_proportion: "1:1",
image_px: "2048x2048",
image_size: "2K"
}
gettwo(taskDetail.id, steps[1].id.toString(), params).then((res: any) => {
refreshTaskDetail();
}).catch((error: any) => {
});
}
// 下一步按钮点击处理
const handleNextStep = (stepId: number) => {
// 获取引擎 ID 和视频参数
const engineId = countType;
const videoParams = {
engine_id: countType,
duration: videoDuration,
aspect_ratio: videoAspectRatio,
resolution: videoResolution,
target_platform: "抖音",
};
// console.log('引擎 ID:', engineId);
getthree(taskDetail.id, stepId.toString(), videoParams).then((res: any) => {
// 重新获取任务详情以更新数据
refreshTaskDetail();
}).catch((error: any) => {
});
// 这里可以添加下一步的逻辑,比如调用接口等
};
const createvideo = (stepId: number, engineId: string) => {
let params = {
engine_id: engineId,
}
getfour(taskDetail.id, stepId.toString(), params).then((res: any) => {
// 重新获取任务详情以更新数据
refreshTaskDetail();
}).catch((error: any) => {
});
}
const agincreatevideo = () => {
let params = {
engine_id: steps[3].engineId,
}
getfour(taskDetail.id, steps[2].id.toString(), params).then((res: any) => {
// 重新获取任务详情以更新数据
refreshTaskDetail();
}).catch((error: any) => {
});
}
return (
<React.Fragment>
<div
className="initialinfo-container"
style={{
margin: '-24px -32px -32px',
height: 'calc(100vh - 34px)',
// background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 50%, #f0f9ff 100%)',
position: 'relative',
// padding: '20px 32px',
boxSizing: 'border-box',
overflow: 'hidden',
display: 'flex',
flexDirection: 'column',
}}
>
<div style={{ flex: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'stretch', gap: '2%', position: 'relative', zIndex: 1, minHeight: 0 }}>
<div
className="replication-preview"
style={{
flex: 4,
background: 'rgba(255,255,255,0.85)',
backdropFilter: 'blur(20px)',
borderRadius: 20,
overflow: 'hidden',
border: '1px solid rgba(99, 102, 241, 0.1)',
boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)',
display: 'flex',
flexDirection: 'column',
minHeight: 0,
}}
>
<div style={{ borderBottom: '1px solid rgba(99, 102, 241, 0.08)', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '16px 24px', background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', boxSizing: 'border-box' }}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate(-1)}
style={{
color: '#64748b',
borderRadius: 10,
fontSize: 13,
height: 32,
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
返回
</Button>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} />
<h3 style={{ margin: 0, fontSize: 16, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
爆款开头复刻 - 任务详情
</h3>
<div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #8b5cf6, #6366f1, transparent)', borderRadius: 1 }} />
</div>
<Button
type="primary"
ghost
style={{
borderRadius: 10,
padding: '4px 16px',
fontSize: 13,
background: 'rgba(99, 102, 241, 0.1)',
borderColor: 'rgba(99, 102, 241, 0.2)',
color: '#6366f1',
}}
onClick={() => setIsModalOpen(true)}
>
创作记录
</Button>
</div>
<div style={{ flex: 1, overflowY: 'auto' }}>
<div className="material_box">
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'flex-start' }}>
<div style={{ flex: 1, minWidth: 250 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ width: 3, height: 14, background: 'linear-gradient(180deg, #6366f1, #8b5cf6)', borderRadius: 2, display: 'inline-block' }} />
视频
</span>
<div className="medio_box" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)' }}>
{taskDetail?.material?.materialVideoUrl ? (
<video
controls
src={taskDetail.material.materialVideoUrl}
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
) : (
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}>暂无视频</div>
)}
</div>
</div>
<div className="product_img" style={{ flex: 1, minWidth: 250 }}>
<span style={{ fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ width: 3, height: 14, background: 'linear-gradient(180deg, #6366f1, #8b5cf6)', borderRadius: 2, display: 'inline-block' }} />
产品图片
</span>
<div className="medio_box" style={{ aspectRatio: '1/1', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)' }}>
{taskDetail?.material?.materialImageUrl ? (
<img src={taskDetail.material.materialImageUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
) : (
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}>暂无图片</div>
)}
</div>
</div>
</div>
{taskDetail?.finalImageUrl && (
<div style={{ marginTop: 16 }}>
<div>
<span style={{ fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ width: 3, height: 14, background: 'linear-gradient(180deg, #6366f1, #8b5cf6)', borderRadius: 2, display: 'inline-block' }} />
生成图片
</span>
<div className="medio_box" style={{ aspectRatio: '1/1', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)' }}>
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalImageUrl}`}
alt=""
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
</div>
</div>
</div>
)}
{taskDetail?.finalVideoUrl && (
<div style={{ marginTop: 16 }}>
<div>
<span style={{ fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10, display: 'flex', alignItems: 'center', gap: 6 }}>
<span style={{ width: 3, height: 14, background: 'linear-gradient(180deg, #6366f1, #8b5cf6)', borderRadius: 2, display: 'inline-block' }} />
生成视频
</span>
<div className="medio_box" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)' }}>
<video
controls
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`}
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
/>
</div>
</div>
</div>
)}
</div>
</div>
</div>
<div
className="replication-form"
style={{
flex: 1,
minWidth: '400px',
background: 'rgba(255,255,255,0.85)',
backdropFilter: 'blur(20px)',
borderRadius: 20,
overflow: 'hidden',
border: '1px solid rgba(99, 102, 241, 0.1)',
boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)',
display: 'flex',
flexDirection: 'column',
minHeight: 0,
}}
>
<div style={{ padding: '16px 24px', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', display: 'flex', alignItems: 'center', gap: 10, background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)' }}>
<div style={{ width: 4, height: 18, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
<span style={{ fontSize: 15, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>生成步骤</span>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 16px 16px' }}>
<Collapse
activeKey={activeKey}
onChange={setActiveKey}
ghost
bordered={false}
style={{ background: 'transparent' }}
expandIconPlacement="end"
items={steps.map((step) => ({
key: String(step.childId),
label: (
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 16, height: 16 }}>
{step.status === 'completed' ? (
<CheckCircleOutlined style={{ fontSize: 16, color: '#22c55e' }} />
) : step.status === 'processing' ? (
<div style={{ width: 16, height: 16, borderRadius: '50%', border: '2px solid #e2e8f0', borderTopColor: '#6366f1', animation: 'spinSlow 1s linear infinite' }} />
) : step.status === 'failed' ? (
<div style={{ width: 16, height: 16, borderRadius: '50%', background: '#ef4444', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: '#fff', fontSize: 10 }}>×</span>
</div>
) : (
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#cbd5e1' }} />
)}
</div>
<span style={{ fontSize: 14, color: step.status === 'processing' || step.status === 'waiting_user' ? '#6366f1' : '#1e293b', fontWeight: 500 }}>
{step.title}
</span>
{(step.status === 'processing' || step.status === 'waiting_user') && (
<span style={{ fontSize: 12, color: '#6366f1', marginLeft: 8 }}>生成中</span>
)}
{step.status === 'failed' && (
<span style={{ fontSize: 12, color: '#ef4444', marginLeft: 8 }}>失败</span>
)}
{step.status === '' && (
<span style={{ fontSize: 12, color: '#717d8b', marginLeft: 8 }}>待生成</span>
)}
</div>
</div>
),
children: (
<div style={{ padding: '16px', background: 'linear-gradient(135deg, rgba(248,250,252,0.6) 0%, rgba(238,242,255,0.4) 100%)', borderRadius: 12, marginTop: 8, border: '1px solid rgba(99, 102, 241, 0.06)' }}>
{/* 步骤1: 原始素材 */}
{step.childId === 1 && (
<>
<div style={{ display: 'flex', gap: 20, marginBottom: 20, alignItems: 'flex-start', flexWrap: 'wrap' }}>
<div style={{ flex: 1, minWidth: 200 }}>
<Text style={{ color: '#475569', fontSize: 13, marginBottom: 10, display: 'block', fontWeight: 500 }}>原视频</Text>
<div style={{ height: 180, borderRadius: 12, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid rgba(99, 102, 241, 0.08)', boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)' }}>
{taskDetail?.material?.materialVideoUrl ? (
<video
src={taskDetail.material.materialVideoUrl}
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
controls
/>
) : (
<div style={{ textAlign: 'center', color: '#94a3b8' }}>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ marginBottom: 8 }}>
<rect x="2" y="2" width="20" height="16" rx="2" ry="2" />
<path d="M8 12l4 2 4-2" />
</svg>
<div style={{ fontSize: 13 }}>暂无视频</div>
</div>
)}
</div>
</div>
<div style={{ flex: 1, minWidth: 200 }}>
<Text style={{ color: '#64748b', fontSize: 13, marginBottom: 10, display: 'block', fontWeight: 500 }}>产品图片</Text>
<div style={{ height: 180, borderRadius: 10, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{taskDetail?.material?.materialImageUrl ? (
<img
src={taskDetail.material.materialImageUrl}
alt="产品图片"
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
/>
) : (
<div style={{ textAlign: 'center', color: '#94a3b8' }}>
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ marginBottom: 8 }}>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" />
<path d="M21 15l-5-5L5 21" />
</svg>
<div style={{ fontSize: 13 }}>暂无图片</div>
</div>
)}
</div>
</div>
</div>
</>
)}
{/* 步骤2: 生成提示词 */}
{step.childId === 2 && (
<>
<div style={{ padding: '12px 14px', background: 'rgba(255,255,255,0.6)', borderRadius: 10, border: '1px solid rgba(99, 102, 241, 0.08)', marginBottom: 4 }}>
<Text style={{ color: '#475569', fontSize: 13, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>
{step?.output?.payload?.prompt || '暂无提示词'}
</Text>
</div>
<Space style={{ marginTop: 16, gap: 12, width: '100%' }}>
<Button
type="default"
icon={<EditOutlined />}
onClick={() => handleOpenModal(step?.output?.payload?.prompt, 'image', step.id)}
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
disabled={step.status !== 'completed'}
>
修改提示词
</Button>
<Button
onClick={() => { message.info('正在生成图片,请稍候...'); createimage(step.id); }}
type="primary"
style={{ flex: 1, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
disabled={step.status !== 'completed'}
>
下一步:生成图片
</Button>
</Space>
</>
)}
{/* 步骤3: 生成产品融合图 */}
{step.childId === 3 && (
<>
<div style={{ borderRadius: 12, overflow: 'hidden', marginBottom: 16, background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', border: '1px solid rgba(99, 102, 241, 0.08)', boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)', position: 'relative' }}>
{taskDetail.finalImageUrl ? (
<img src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalImageUrl}`} alt="融合图" style={{ width: '100%', height: 180, objectFit: 'cover' }} />
) : (
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 14 }}>暂无融合图</div>
)}
<Button
type="default"
icon={<EditOutlined />}
onClick={() => newcreateimage()}
disabled={step.status !== 'completed'}
style={{
position: 'absolute',
bottom: 8,
left: 8,
borderRadius: 8,
borderColor: 'rgba(99, 102, 241, 0.3)',
color: '#6366f1',
height: 30,
fontWeight: 500,
background: 'rgba(255,255,255,0.85)',
backdropFilter: 'blur(4px)',
padding: '0 12px',
fontSize: 12,
}}
>
重新生成
</Button>
</div>
{/* 引擎选择器和视频参数设置 */}
<p style={{marginBottom:6,fontSize: 14, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>视频参数选择:</p>
<div style={{ width:'100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
<button
onClick={() => {
setShowEngineModal(!showEngineModal);
setShowVideoSettingsModal(false);
}}
className="image-settings-trigger"
style={{
width: '100%',
padding: '4px 12px',
height: 34,
borderRadius: 10,
border: '1px solid rgba(99, 102, 241, 0.15)',
backgroundColor: 'rgba(255,255,255,0.7)',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
transition: 'all 0.2s',
}}
>
<SettingOutlined style={{ fontSize: 14, color: '#6366f1' }} />
<Text style={{
fontSize: 14,
fontWeight: 500,
color: '#64748b',
}}>
{enginesele.video?.find((e: any) => e.id === countType)?.name || '选择引擎'}
</Text>
</button>
{showEngineModal && (
<div
className="image-settings-popover"
style={{
position: 'absolute',
bottom: 'calc(100% + 8px)',
left: -10,
width: 350,
backgroundColor: '#fff',
borderRadius: 16,
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
padding: 16,
border: 'none',
zIndex: 9999,
}}
onClick={(e) => e.stopPropagation()}
>
{/* 选择引擎 */}
<div style={{ marginBottom: 8 }}>
<Text style={{
display: 'block',
marginBottom: 8,
fontSize: 12,
fontWeight: 500,
color: '#666666',
}}>
选择引擎
</Text>
<div style={{
display: 'flex',
flexDirection: 'column',
gap: 4,
}}>
{enginesele.video?.map((engine: any) => (
<button
key={engine.id}
onClick={() => {
setCountType(engine.id);
// 根据选中的引擎更新视频参数选项
setEngineOptions({
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
durations: engine.supportedDurations || [5, 8, 10, 12, 15],
});
// 更新默认选中值,确保在新引擎支持的范围内
if (!engine.supportedRatios?.includes(videoAspectRatio)) {
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
}
if (!engine.supportedResolutions?.includes(videoResolution)) {
setVideoResolution(engine.supportedResolutions?.[0] || '720p');
}
if (!engine.supportedDurations?.includes(videoDuration)) {
setVideoDuration(engine.supportedDurations?.[0] || 5);
}
setShowEngineModal(false);
}}
style={{
flex: 1,
minHeight: 48,
borderRadius: 8,
border: countType === engine.id
? '2px solid #6366f1'
: '1px solid #e5e7eb',
backgroundColor: countType === engine.id
? '#fff'
: '#f9fafb',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'flex-start',
padding: '8px 12px',
transition: 'all 0.2s',
textAlign: 'left',
}}
>
<span style={{
fontSize: 13,
fontWeight: countType === engine.id ? 600 : 500,
color: countType === engine.id ? '#6366f1' : '#4b5563',
marginBottom: 2,
}}>
{engine.name}
</span>
<span style={{
fontSize: 11,
color: '#9ca3af',
}}>
</span>
</button>
))}
</div>
</div>
</div>
)}
</div>
{/* 视频参数设置 */}
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
<button
onClick={() => {
setShowVideoSettingsModal(!showVideoSettingsModal);
setShowEngineModal(false);
}}
className="image-settings-trigger"
style={{
width: '100%',
padding: '4px 12px',
height: 34,
borderRadius: 10,
border: '1px solid rgba(99, 102, 241, 0.15)',
backgroundColor: 'rgba(255,255,255,0.7)',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
transition: 'all 0.2s',
}}
>
<LayoutOutlined style={{ fontSize: 14, color: '#6366f1' }} />
<Text style={{
fontSize: 14,
fontWeight: 500,
color: '#64748b',
}}>
{videoAspectRatio} · {videoDuration}s · {videoResolution}
</Text>
</button>
{showVideoSettingsModal && (
<div
className="image-settings-popover"
style={{
position: 'absolute',
bottom: 'calc(100% + 8px)',
left: -160,
width: 350,
backgroundColor: '#fff',
borderRadius: 16,
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
padding: 16,
border: 'none',
zIndex: 9999,
}}
onClick={(e) => e.stopPropagation()}
>
{/* 选择比例 */}
<div style={{ marginBottom: 16 }}>
<Text style={{
display: 'block',
marginBottom: 8,
fontSize: 12,
fontWeight: 500,
color: '#666666',
}}>
选择比例
</Text>
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: 4,
}}>
{engineOptions.ratios.map((ratio) => (
<button
key={ratio}
onClick={() => setVideoAspectRatio(ratio)}
style={{
flex: '0 0 calc(14.28% - 4px)',
minWidth: 44,
height: 52,
borderRadius: 6,
border: videoAspectRatio === ratio
? '2px solid #6366f1'
: '1px solid #e5e7eb',
backgroundColor: videoAspectRatio === ratio
? '#fff'
: '#f9fafb',
cursor: 'pointer',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
transition: 'all 0.2s',
}}
>
<div style={{
width: 18,
height: 18,
border: `2px solid ${videoAspectRatio === ratio ? '#6366f1' : '#9ca3af'}`,
borderRadius: 2,
marginBottom: 2,
}} />
<span style={{
fontSize: 9,
color: videoAspectRatio === ratio
? '#6366f1'
: '#6b7280',
fontWeight: videoAspectRatio === ratio ? 600 : 400,
}}>
{ratio}
</span>
</button>
))}
</div>
</div>
{/* 选择时长 */}
<div style={{ marginBottom: 16 }}>
<Text style={{
display: 'block',
marginBottom: 8,
fontSize: 12,
fontWeight: 500,
color: '#666666',
}}>
选择时长
</Text>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ flex: 1, position: 'relative', height: 24, display: 'flex', alignItems: 'center' }}>
{/* 背景轨道 */}
<div style={{
position: 'absolute',
top: '50%',
left: 0,
right: 0,
height: 6,
borderRadius: 3,
background: '#e5e7eb',
transform: 'translateY(-50%)',
}} />
{/* 已滑动部分 */}
<div style={{
position: 'absolute',
top: '50%',
left: 0,
height: 6,
borderRadius: 3,
background: '#6366f1',
width: `${((videoDuration - Math.min(...engineOptions.durations)) / (Math.max(...engineOptions.durations) - Math.min(...engineOptions.durations))) * 100}%`,
transform: 'translateY(-50%)',
}} />
{/* 滑块 */}
<input
type="range"
min={Math.min(...engineOptions.durations)}
max={Math.max(...engineOptions.durations)}
value={videoDuration}
onChange={(e) => setVideoDuration(Number(e.target.value))}
style={{
position: 'relative',
width: '100%',
height: 24,
borderRadius: 3,
background: 'transparent',
outline: 'none',
appearance: 'none',
cursor: 'pointer',
zIndex: 1,
}}
/>
</div>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 4,
padding: '4px 12px',
backgroundColor: '#f1f5f9',
borderRadius: 6,
}}>
<span style={{ fontSize: 14, fontWeight: 600, color: '#64748b' }}>
{videoDuration}
</span>
<span style={{ fontSize: 12, color: '#9ca3af' }}></span>
</div>
</div>
</div>
{/* 选择分辨率 */}
<div>
<Text style={{
display: 'block',
marginBottom: 8,
fontSize: 12,
fontWeight: 500,
color: '#666666',
}}>
选择分辨率
</Text>
<div style={{ display: 'flex', gap: 6 }}>
{engineOptions.resolutions.map((resolution) => (
<button
key={resolution}
onClick={() => setVideoResolution(resolution)}
style={{
flex: 1,
height: 42,
borderRadius: 6,
border: videoResolution === resolution
? '2px solid #6366f1'
: '1px solid #e5e7eb',
backgroundColor: videoResolution === resolution
? '#6366f1'
: '#f9fafb',
cursor: 'pointer',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
transition: 'all 0.2s',
}}
>
<span style={{
fontSize: 12,
fontWeight: 600,
color: videoResolution === resolution
? '#fff'
: '#4b5563',
}}>
{resolution}
</span>
</button>
))}
</div>
</div>
</div>
)}
</div>
</div>
<Button
type="primary"
style={{ width: '100%', borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
onClick={() => { message.info('正在生成视频提示词,请稍候...'); handleNextStep(step.id); }}
disabled={step.status !== 'completed'}
>
下一步:生成视频提示词
</Button>
</>
)}
{/* 步骤4: 生成视频提示词 */}
{step.childId === 4 && (
<>
<div style={{ padding: '12px 14px', background: 'rgba(255,255,255,0.6)', borderRadius: 10, border: '1px solid rgba(99, 102, 241, 0.08)', marginBottom: 4 }}>
<Text style={{ color: '#64748b', fontSize: 13, lineHeight: 1.8 }}>
视频提词已生成,可点击按钮查看/修改
</Text>
</div>
<Space style={{ marginTop: 16, gap: 12, width: '100%' }}>
<Button
type="default"
icon={<EditOutlined />}
onClick={() => handleOpenModal(taskDetail?.videoGeneration?.promptSchema, 'video', step.id, taskDetail?.videoGeneration?.schemaConfigSnapshot)}
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
disabled={step.status !== 'completed' || !taskDetail?.videoGeneration?.promptSchema || !taskDetail?.videoGeneration?.schemaConfigSnapshot}
>
查看/修改视频提词
</Button>
<Button
onClick={() => { message.info('正在生成视频,请稍候...'); createvideo(step.id, step.engineId); }}
type="primary"
style={{ flex: 1, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
disabled={step.status !== 'completed'}
>
下一步:生成视频
</Button>
</Space>
</>
)}
{/* 步骤5: 生成最终视频 */}
{step.childId === 5 && (
<>
<div style={{ borderRadius: 12, overflow: 'hidden', marginBottom: 16, position: 'relative', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', border: '1px solid rgba(99, 102, 241, 0.08)', boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)' }}>
{step.status === 'completed' && taskDetail?.finalVideoUrl ? (
<video src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`} style={{ width: '100%', height: 180, objectFit: 'cover' }} controls />
) : step.status === 'processing' ? (
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8 }}>
<div style={{ width: 20, height: 20, borderRadius: '50%', border: '2px solid #e2e8f0', borderTopColor: '#6366f1', animation: 'spinSlow 1s linear infinite' }} />
<span style={{ color: '#6366f1', fontSize: 14 }}>视频生成中...</span>
</div>
) : step.status === 'failed' ? (
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ef4444', fontSize: 14 }}>视频生成失败</div>
) : (
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 14 }}>暂无视频</div>
)}
</div>
<Space style={{ width: '100%', gap: 12 }}>
<Button
onClick={() => agincreatevideo()}
type="default"
icon={<EditOutlined />}
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
disabled={step.status !== 'completed'}
>
重新生成
</Button>
<Button
type="primary"
icon={<DownloadOutlined />}
style={{ flex: 1, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
disabled={step.status !== 'completed'}
onClick={() => {
const videoUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail?.finalVideoUrl}&download=1`;
const link = document.createElement('a');
link.href = videoUrl;
link.download = `video_${Date.now()}.mp4`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}}
>
下载视频
</Button>
</Space>
</>
)}
</div>
),
}))}
/>
</div>
</div>
</div>
</div>
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 4, height: 18, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
<span style={{ fontSize: 15, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
{currentType === 'video' ? '查看/编辑视频提词' : '修改提示词'}
</span>
</div>
}
open={modalVisible}
onCancel={() => { if (!promptSaving) setModalVisible(false); }}
footer={[
<Button key="cancel" onClick={() => setModalVisible(false)} disabled={promptSaving} style={{ borderRadius: 8 }}>取消</Button>,
<Button key="confirm" type="primary" onClick={handleConfirm} loading={promptSaving} disabled={promptSaving} style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none' }}>确认</Button>,
]}
width={800}
style={{ borderRadius: 16 }}
styles={{
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
body: { background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 50%, #f0f9ff 100%)', padding: '20px 24px' },
}}
>
{currentType === 'image' ? (
<TextArea
value={promptText}
onChange={(e) => setPromptText(e.target.value)}
rows={8}
style={{ width: '100%', height: 300, borderRadius: 10, border: '1px solid rgba(99, 102, 241, 0.15)', background: 'rgba(255,255,255,0.8)' }}
placeholder="请输入提示词"
/>
) : (
<div style={{ maxHeight: 500, overflowY: 'auto', paddingRight: 10 }}>
<VideoPromptSchemaEditor value={formData} schemaConfigSnapshot={videoSchemaConfigSnapshot} onChange={setFormData} />
</div>
)}
</Modal>
{/* 创作记录弹窗 */}
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div style={{ width: 4, height: 18, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
<span style={{ fontSize: 15, fontWeight: 700, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent', backgroundClip: 'text' }}>
创作记录
</span>
</div>
}
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
width={850}
footer={null}
style={{ borderRadius: 16 }}
styles={{
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
body: { background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 50%, #f0f9ff 100%)', },
}}
>
{/* 搜索区域 */}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
<Input
placeholder="搜索产品名称"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={handleSearch}
style={{ width: 200, borderRadius: 8, marginRight: 8, border: '1px solid rgba(99, 102, 241, 0.15)', background: 'rgba(255,255,255,0.8)' }}
/>
<Button type="primary" onClick={handleSearch} style={{ borderRadius: 8, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}>
搜索
</Button>
</div>
{/* 表格 */}
<Table
columns={[
{
title: '产品名称',
dataIndex: 'targetProjectName',
key: 'targetProjectName',
align: 'center',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
render: (text: string, record: any) => {
const status = text;
const currentStepCode = record.current_step_code || record.currentStepCode;
const getStatusText = () => {
if (currentStepCode === 'image_prompt_optimize') {
switch (status) {
case 'waiting_user':
return { text: '等待融合图生成', color: '#f59e0b' };
case 'processing':
return { text: '图片提示词生成中', color: '#f59e0b' };
case 'completed':
return { text: '图片提示词生成成功', color: '#10b981' };
case 'failed':
return { text: '图片提示词生成失败', color: '#ef4444' };
default:
return { text: status || '-', color: '#666' };
}
} else if (currentStepCode === 'image_generate') {
switch (status) {
case 'waiting_user':
return { text: '等待生成视频提示词', color: '#f59e0b' };
case 'processing':
return { text: '融合图生成中', color: '#f59e0b' };
case 'completed':
return { text: '融合图生成成功', color: '#10b981' };
case 'failed':
return { text: '融合图生成失败', color: '#ef4444' };
default:
return { text: status || '-', color: '#666' };
}
} else if (currentStepCode === 'video_prompt_optimize') {
switch (status) {
case 'waiting_user':
return { text: '等待最终视频生成', color: '#f59e0b' };
case 'processing':
return { text: '视频提示词生成中', color: '#f59e0b' };
case 'completed':
return { text: '视频提示词生成成功', color: '#10b981' };
case 'failed':
return { text: '视频提示词生成失败', color: '#ef4444' };
default:
return { text: status || '-', color: '#666' };
}
} else if (currentStepCode === 'video_generate') {
switch (status) {
case 'waiting_user':
return { text: '', color: '#666' };
case 'processing':
return { text: '最终视频生成中', color: '#f59e0b' };
case 'completed':
return { text: '任务完成', color: '#10b981' };
case 'failed':
return { text: '最终视频生成失败', color: '#ef4444' };
default:
return { text: status || '-', color: '#666' };
}
} else if (currentStepCode === 'material_input') {
switch (status) {
case 'waiting_user':
return { text: '等待生成图片提示词', color: '#f59e0b' };
case 'processing':
return { text: '素材处理中', color: '#f59e0b' };
case 'completed':
return { text: '素材上传成功', color: '#10b981' };
case 'failed':
return { text: '素材上传失败', color: '#ef4444' };
default:
return { text: status || '-', color: '#666' };
}
} else {
const statusMap: Record<string, { text: string; color: string }> = {
'pending': { text: '子任务待处理', color: '#f59e0b' },
'waiting_user': { text: '等待用户确认或触发', color: '#f59e0b' },
'processing': { text: '子任务处理中', color: '#f59e0b' },
'completed': { text: '子任务完成', color: '#10b981' },
'failed': { text: '子任务失败', color: '#ef4444' },
'cancelled': { text: '子任务取消', color: '#94a3b8' },
};
return statusMap[status] || { text: status || '-', color: '#666' };
}
};
const result = getStatusText() as { text: string; color: string };
if (typeof result === 'string') {
return <span style={{ fontSize: 12, color: '#666', fontWeight: 500 }}>{result}</span>;
}
return <span style={{ fontSize: 12, color: result.color, fontWeight: 500 }}>{result.text}</span>;
},
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
align: 'center',
render: (text: string) => {
if (!text) return '-';
const date = new Date(text);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
},
},
{
title: '操作',
key: 'action',
align: 'center',
render: (_, record) => (
<button
onClick={() => navigate(`/initial/${record.id}/initialinfo`)}
style={{ color: '#6366f1', textDecoration: 'none', fontSize: 12, border: 'none', background: 'rgba(99, 102, 241, 0.08)', padding: '4px 12px', borderRadius: 8, cursor: 'pointer', transition: 'all 0.2s', fontWeight: 500 }}
>
查看详情
</button>
),
},
]}
dataSource={tableData}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
onChange: handlePageChange,
}}
style={{ fontSize: 13 }}
scroll={{ y: 350 }}
/>
</Modal>
</React.Fragment>
);
}
export default InitialInfo;