1406 lines
98 KiB
TypeScript
1406 lines
98 KiB
TypeScript
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;
|