Files
video-gen/video-gen-app/src/pages/InitialInfo.tsx
T
2026-06-15 15:50:59 +08:00

1214 lines
81 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 } from '../api/index';
import './css/InitialInfo.css';
const { Title, Text } = Typography;
const { TextArea } = Input;
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 [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 [taskDetail, setTaskDetail] = useState<any>({});
// API 返回的步骤数据
const [apiSteps, setApiSteps] = useState<any[]>([]);
//
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 || '',
}));
// 获取列表数据的函数
const fetchList = (page: number, size: number) => {
getReplicationList(page, size).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);
};
// 获取复刻列表数据
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) => {
setCurrentType(type || 'image');
if (type === 'video' && typeof prompt === 'object') {
setFormData(prompt);
setPromptText('');
} else {
setPromptText(prompt || '');
setFormData({});
}
setModalVisible(true);
};
const handleConfirm = () => {
setModalVisible(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[2].engineId,
}
getfour(taskDetail.id, steps[2].id.toString(), params).then((res: any) => {
// 重新获取任务详情以更新数据
refreshTaskDetail();
}).catch((error: any) => {
});
}
return (
<React.Fragment>
<div style={{ height: '94vh', background: '#f8fafc' }}>
<div style={{ height: 'calc(94vh)', }}>
<div style={{ height: '100%', display: 'flex', justifyContent: 'space-between', gap: '2%' }}>
<div style={{ width: '70%', background: '#fff', borderRadius: 12, overflowY: 'auto' }}>
<div style={{ borderBottom: '1px solid #e2e8f0', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px', boxSizing: 'border-box' }}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate(-1)}
style={{ color: '#64748b' }}
>
返回
</Button>
<h3 style={{ margin: 0 }}>
爆款开头复刻 - 任务详情
</h3>
<Button type="primary" ghost style={{ borderRadius: 6, padding: '4px 12px', fontSize: 12, borderColor: '#6366f1', color: '#6366f1' }} onClick={() => setIsModalOpen(true)}>
创作记录
</Button>
</div>
<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: 500, color: '#374151', marginBottom: 8, display: 'block' }}>视频</span>
<div className="medio_box" style={{ aspectRatio: '16/9', background: '#f1f5f9', borderRadius: 8, overflow: 'hidden' }}>
{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: 12 }}>暂无视频</div>
)}
</div>
</div>
<div className="product_img" style={{ flex: 1, minWidth: 250 }}>
<span style={{ fontSize: 14, fontWeight: 500, color: '#374151', marginBottom: 8, display: 'block' }}>产品图片</span>
<div className="medio_box" style={{ aspectRatio: '1/1', background: '#f1f5f9', borderRadius: 8, overflow: 'hidden' }}>
{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: 12 }}>暂无图片</div>
)}
</div>
</div>
</div>
{taskDetail?.finalImageUrl && (
<div style={{ marginTop: 16 }}>
<div>
<span style={{ fontSize: 14, fontWeight: 500, color: '#374151', marginBottom: 8, display: 'block' }}>生成图片</span>
<div className="medio_box" style={{ aspectRatio: '1/1', background: '#f1f5f9', borderRadius: 8, overflow: 'hidden' }}>
<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: 500, color: '#374151', marginBottom: 8, display: 'block' }}>生成视频</span>
<div className="medio_box" style={{ aspectRatio: '16/9', background: '#f1f5f9', borderRadius: 8, overflow: 'hidden' }}>
<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 style={{ width: '28%', minWidth: '400px', background: '#fff', borderRadius: 12, overflowY: 'auto' }}>
<div style={{ padding: '14px' }}> 生成步骤</div>
<Collapse
defaultActiveKey={['']}
ghost
bordered={false}
style={{ background: 'transparent' }}
expandIconPlacement="end"
items={steps.map((step) => ({
key: String(step.id),
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: '#f8fafc', borderRadius: 8, marginTop: 8 }}>
{/* 步骤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: '#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?.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 && (
<>
{/* <Text style={{ color: '#475569', fontSize: 13, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>
{taskDetail?.generationPrompt || '暂无提示词'}
</Text> */}
<Text style={{ color: '#475569', fontSize: 13, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>
{/* {taskDetail?.generationPrompt || '暂无提示词'} */}
{step?.output?.payload?.prompt || '暂无提示词'}
</Text>
<Space style={{ marginTop: 16, gap: 12, width: '100%' }}>
<Button
type="default"
icon={<EditOutlined />}
onClick={() => handleOpenModal(step?.output?.payload?.prompt)}
style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }}
disabled={step.status !== 'completed'}
>
修改提示词
</Button>
<Button onClick={() => { message.info('正在生成图片,请稍候...'); createimage(step.id); }} type="primary" style={{ flex: 1, borderRadius: 8, background: '#6366f1', borderColor: '#6366f1', height: 36 }} disabled={step.status !== 'completed'}>
下一步:生成图片
</Button>
</Space>
</>
)}
{/* 步骤3: 生成产品融合图 */}
{step.childId === 3 && (
<>
<div style={{ borderRadius: 8, overflow: 'hidden', marginBottom: 16, background: '#f1f5f9' }}>
{/* {step.output.result.result_image_url} */}
{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>
)}
</div>
{/* 引擎选择器 */}
<div style={{ marginBottom: 12 }}>
<div style={{ position: 'relative', display: 'inline-block' }}>
<button
onClick={() => {
setShowEngineModal(!showEngineModal);
setShowVideoSettingsModal(false);
}}
className="image-settings-trigger"
style={{
minWidth: 200,
padding: '4px 12px',
height: 34,
borderRadius: 8,
border: 'none',
backgroundColor: '#f1f5f9',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
transition: 'all 0.2s',
}}
>
<SettingOutlined style={{ fontSize: 14, color: '#64748b' }} />
<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: -20,
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>
{/* 视频参数设置 */}
<div style={{ marginBottom: 16 }}>
<div style={{ position: 'relative', display: 'inline-block' }}>
<button
onClick={() => {
setShowVideoSettingsModal(!showVideoSettingsModal);
setShowEngineModal(false);
}}
className="image-settings-trigger"
style={{
minWidth: 220,
padding: '4px 12px',
height: 34,
borderRadius: 8,
border: 'none',
backgroundColor: '#f1f5f9',
cursor: 'pointer',
display: 'inline-flex',
alignItems: 'center',
gap: 6,
transition: 'all 0.2s',
}}
>
<LayoutOutlined style={{ fontSize: 14, color: '#64748b' }} />
<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: -20,
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>
<Space style={{ width: '100%', gap: 12 }}>
<Button type="default" icon={<EditOutlined />} style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }
} onClick={() => newcreateimage()} disabled={step.status !== 'completed'}>
重新生成
</Button>
<Button
type="primary"
style={{ flex: 1, borderRadius: 8, background: '#6366f1', borderColor: '#6366f1', height: 36 }}
onClick={() => { message.info('正在生成视频提示词,请稍候...'); handleNextStep(step.id); }}
disabled={step.status !== 'completed'}
>
下一步:生成视频提示词
</Button>
</Space>
</>
)}
{/* 步骤4: 生成视频提示词 */}
{step.childId === 4 && (
<>
<Text style={{ color: '#475569', fontSize: 13, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>
{/* {taskDetail?.videoPrompt || '暂无视频提示词'} */}
{step?.output?.payload?.finalPrompt || '暂无提示词'}
</Text>
<Space style={{ marginTop: 16, gap: 12, width: '100%' }}>
<Button
type="default"
icon={<EditOutlined />}
onClick={() => handleOpenModal(step?.output?.payload?.promptSchema,'video')}
style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }}
disabled={step.status !== 'completed'}
>
修改提示词
</Button>
<Button onClick={() => { message.info('正在生成视频,请稍候...'); createvideo(step.id,step.engineId); }} type="primary" style={{ flex: 1, borderRadius: 8, background: '#6366f1', borderColor: '#6366f1', height: 36 }} disabled={step.status !== 'completed'}>
下一步:生成视频
</Button>
</Space>
</>
)}
{/* 步骤5: 生成最终视频 */}
{step.childId === 5 && (
<>
<div style={{ borderRadius: 8, overflow: 'hidden', marginBottom: 16, position: 'relative', background: '#f1f5f9' }}>
{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: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }} disabled={step.status !== 'completed'}>
重新生成
</Button>
<Button
type="primary"
icon={<DownloadOutlined />}
style={{ flex: 1, borderRadius: 8, background: '#6366f1', borderColor: '#6366f1', height: 36 }}
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="修改提示词"
open={modalVisible}
onCancel={() => setModalVisible(false)}
footer={[
<Button key="cancel" onClick={() => setModalVisible(false)}>取消</Button>,
<Button key="confirm" type="primary" onClick={handleConfirm}>确认</Button>,
]}
width={800}
>
{currentType === 'image' ? (
<TextArea
value={promptText}
onChange={(e) => setPromptText(e.target.value)}
rows={8}
style={{ width: '100%', height: 300 }}
placeholder="请输入提示词"
/>
) : (
<div style={{ maxHeight: 500, overflowY: 'auto', paddingRight: 10 }}>
<FormRenderer data={formData} onChange={setFormData} />
</div>
)}
</Modal>
{/* 创作记录弹窗 */}
<Modal
title="创作记录"
open={isModalOpen}
onCancel={() => setIsModalOpen(false)}
width={800}
footer={null}
style={{ borderRadius: 12 }}
>
{/* 搜索区域 */}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
<Input
placeholder="搜索产品名称"
style={{ width: 200, borderRadius: 6, marginRight: 8 }}
/>
<Button type="primary" style={{ borderRadius: 6 }}>
搜索
</Button>
</div>
{/* 表格 */}
<Table
columns={[
{
title: '产品名称',
dataIndex: 'targetProjectName',
key: 'targetProjectName',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (text: string) => {
const statusMap: Record<string, string> = {
'pending': '子任务待处理',
'waiting_user': '等待用户确认或触发',
'processing': '子任务处理中',
'completed': '子任务完成',
'failed': '子任务失败',
'cancelled': '子任务取消',
};
return statusMap[text] || text || '-';
},
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
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',
render: (_, record) => (
<button
onClick={() => navigate(`/initial/${record.id}/initialinfo`)}
style={{ color: '#6366f1', textDecoration: 'none', fontSize: 12, border: 'none', background: 'none', cursor: 'pointer' }}
>
查看详情
</button>
),
},
]}
dataSource={tableData}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
onChange: handlePageChange,
}}
style={{ fontSize: 13 }}
/>
</Modal>
</React.Fragment>
);
}
const FormRenderer = ({ data, onChange }: { data: any; onChange: (data: any) => void }) => {
const handleFieldChange = (path: string[], value: any) => {
const newData = { ...data };
let current = newData;
for (let i = 0; i < path.length - 1; i++) {
current = current[path[i]];
}
current[path[path.length - 1]] = value;
onChange(newData);
};
const handleArrayItemChange = (path: string[], index: number, value: any) => {
const newData = { ...data };
let current = newData;
for (let i = 0; i < path.length; i++) {
if (i === path.length - 1) {
current[path[i]] = [...current[path[i]]];
current[path[i]][index] = value;
} else {
current = current[path[i]];
}
}
onChange(newData);
};
const handleArrayAdd = (path: string[]) => {
const newData = { ...data };
let current = newData;
for (let i = 0; i < path.length; i++) {
if (i === path.length - 1) {
current[path[i]] = [...current[path[i]], ''];
} else {
current = current[path[i]];
}
}
onChange(newData);
};
const handleArrayRemove = (path: string[], index: number) => {
const newData = { ...data };
let current = newData;
for (let i = 0; i < path.length; i++) {
if (i === path.length - 1) {
current[path[i]] = current[path[i]].filter((_: any, i: number) => i !== index);
} else {
current = current[path[i]];
}
}
onChange(newData);
};
const renderField = (key: string, value: any, path: string[]) => {
if (Array.isArray(value)) {
return (
<div key={key} style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
<Text strong style={{ color: '#374151', fontSize: 13 }}>{key}</Text>
<Button
type="text"
size="small"
onClick={() => handleArrayAdd(path)}
style={{ marginLeft: 8, color: '#6366f1', fontSize: 12 }}
>
+ 添加
</Button>
</div>
<div style={{ border: '1px solid #e5e7eb', borderRadius: 8, padding: 12, background: '#f9fafb' }}>
{value.map((item: any, index: number) => (
<div key={index} style={{ display: 'flex', alignItems: 'flex-start', gap: 8, marginBottom: 8 }}>
<span style={{ color: '#9ca3af', fontSize: 12, marginTop: 6 }}>{index + 1}.</span>
<div style={{ flex: 1 }}>
{typeof item === 'object' ? (
<FormRenderer
data={item}
onChange={(newItem) => handleArrayItemChange(path, index, newItem)}
/>
) : (
<Input
value={item}
onChange={(e) => handleArrayItemChange(path, index, e.target.value)}
style={{ width: '100%', borderRadius: 6 }}
/>
)}
</div>
<Button
type="text"
danger
onClick={() => handleArrayRemove(path, index)}
style={{ marginTop: 4 }}
>
删除
</Button>
</div>
))}
</div>
</div>
);
}
if (typeof value === 'object' && value !== null) {
return (
<div key={key} style={{ marginBottom: 16 }}>
<Text strong style={{ color: '#374151', fontSize: 13, marginBottom: 8, display: 'block' }}>
{key}
</Text>
<div style={{ borderLeft: '3px solid #6366f1', paddingLeft: 12, marginLeft: 4 }}>
<FormRenderer data={value} onChange={(newValue) => handleFieldChange(path, newValue)} />
</div>
</div>
);
}
return (
<div key={key} style={{ marginBottom: 12 }}>
<Text style={{ color: '#6b7280', fontSize: 12, marginBottom: 4, display: 'block' }}>{key}</Text>
<Input
value={value}
onChange={(e) => handleFieldChange(path, e.target.value)}
style={{ width: '100%', borderRadius: 6 }}
/>
</div>
);
};
return (
<div>
{Object.entries(data).map(([key, value]) => renderField(key, value, [key]))}
</div>
);
};
export default InitialInfo;