Files
video-gen/video-gen-app/src/pages/InitialReplication.tsx
T
2026-07-08 17:15:19 +08:00

1473 lines
59 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, useRef } from 'react';
import {
Layout,
Button,
Input,
Upload,
message,
Card,
Modal,
Table,
Space,
Pagination,
Popconfirm,
} from 'antd';
import {
PlusOutlined,
VideoCameraOutlined,
PictureOutlined,
LoadingOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { uploadVideo, uploadImage, generateReplication, getReplicationList, getone, getReplicationDetail, deleteHotOpeningReplicationTask } from '../api';
const { Header, Content } = Layout;
const { TextArea } = Input;
const GenerateConver: React.FC = () => {
const navigate = useNavigate();
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 [cardData, setCardData] = useState<any[]>([]);
const [cardCurrentPage, setCardCurrentPage] = useState(1);
const [cardPageSize, setCardPageSize] = useState(8);
const [cardTotal, setCardTotal] = useState(0);
// 表单数据
const [originalProductName, setOriginalProductName] = useState<string>('');
const [ownProductName, setOwnProductName] = useState<string>('');
const [productSellingPoints, setProductSellingPoints] = useState<string>('');
// 文件状态
const [videoFile, setVideoFile] = useState<File | null>(null);
const [videoUrl, setVideoUrl] = useState<string>('');
const [imageFile, setImageFile] = useState<File | null>(null);
const [imageUrl, setImageUrl] = useState<string>('');
const [videoUploading, setVideoUploading] = useState(false);
const [imageUploading, setImageUploading] = useState(false);
// 弹窗状态
const [isModalOpen, setIsModalOpen] = useState(false);
// 卡片列表轮询定时器(使用 ref 避免闭包问题)
const cardPollingTimer = useRef<any>(null);
// 表格轮询定时器(使用 ref 避免闭包问题)
const tablePollingTimer = useRef<any>(null);
// 获取卡片列表数据(独立于表格)
const fetchCardList = (page: number, size: number, isPolling = false) => {
getReplicationList(page, size).then((res: any) => {
if (res.items) {
if (isPolling) {
// 轮询时:增量更新,只更新状态发生变化的项目
setCardData(prevData => {
return prevData.map(existingItem => {
const newItem = res.items.find((item: any) => item.id === existingItem.id);
if (newItem && newItem.status !== existingItem.status) {
return newItem;
}
return existingItem;
});
});
} else {
// 首次加载或分页变化时:直接替换
setCardData(res.items);
}
// 检查是否有 processing 状态
const hasProcessing = res.items.some(item => item.status === 'processing');
// 如果有 processing 状态且轮询未启动,启动轮询
if (hasProcessing && !cardPollingTimer.current) {
cardPollingTimer.current = setInterval(() => {
fetchCardList(cardCurrentPage, cardPageSize, true);
}, 5000);
}
// 如果没有 processing 状态且轮询正在运行,停止轮询
else if (!hasProcessing && cardPollingTimer.current) {
clearInterval(cardPollingTimer.current);
cardPollingTimer.current = null;
} else {
}
}
if (res.total !== undefined) {
setCardTotal(res.total);
}
}).catch((error: any) => {
});
};
// 卡片列表分页变化处理
const handleCardPageChange = (page: number) => {
setCardCurrentPage(page);
fetchCardList(page, cardPageSize);
};
// 获取列表数据的函数
const fetchList = (page: number, size: number, isPolling = false, keyword?: string) => {
getReplicationList(page, size, keyword).then((res: any) => {
if (res.items) {
if (isPolling) {
// 轮询时:增量更新,只更新状态发生变化的项目
setTableData(prevData => {
return prevData.map(existingItem => {
const newItem = res.items.find((item: any) => item.id === existingItem.id);
if (newItem && newItem.status !== existingItem.status) {
return newItem;
}
return existingItem;
});
});
} else {
// 首次加载或分页变化时:直接替换
setTableData(res.items);
}
// 检查是否有 processing 状态
const hasProcessing = res.items.some(item => item.status === 'processing');
// 如果有 processing 状态且轮询未启动,启动轮询
if (hasProcessing && !tablePollingTimer.current) {
tablePollingTimer.current = setInterval(() => {
fetchList(currentPage, pageSize, true, searchKeyword);
}, 5000);
}
// 如果没有 processing 状态且轮询正在运行,停止轮询
else if (!hasProcessing && tablePollingTimer.current) {
clearInterval(tablePollingTimer.current);
tablePollingTimer.current = null;
}
}
if (res.total !== undefined) {
setTotal(res.total);
}
}).catch((error: any) => {
});
};
// 组件挂载时获取复刻列表
useEffect(() => {
fetchList(currentPage, pageSize);
fetchCardList(cardCurrentPage, cardPageSize);
// 组件卸载时清理定时器
return () => {
if (cardPollingTimer.current) {
clearInterval(cardPollingTimer.current);
cardPollingTimer.current = null;
}
if (tablePollingTimer.current) {
clearInterval(tablePollingTimer.current);
tablePollingTimer.current = null;
}
};
}, []);
// 分页变化处理
const handlePageChange = (page: number, size: number) => {
setCurrentPage(page);
setPageSize(size);
fetchList(page, size, false, searchKeyword);
};
// 搜索处理
const handleSearch = () => {
setCurrentPage(1);
fetchList(1, pageSize, false, searchKeyword);
};
// 文件上传前校验
const beforeVideoUpload = async (file: File): Promise<boolean> => {
const isVideo = file.type.startsWith('video/');
if (!isVideo) {
message.error('只能上传视频文件');
return false;
}
const isLt50M = file.size / 1024 / 1024 < 50;
if (!isLt50M) {
message.error('视频大小不能超过50MB');
return false;
}
// 检查视频时长
const video = document.createElement('video');
video.preload = 'metadata';
return new Promise((resolve) => {
video.onloadedmetadata = async () => {
if (video.duration < 2) {
message.error('视频时长不能少于2秒');
URL.revokeObjectURL(video.src);
resolve(false);
return;
}
if (video.duration >= 16) {
message.error('视频时长不能超过15秒');
URL.revokeObjectURL(video.src);
resolve(false);
return;
}
// 调用上传接口
setVideoUploading(true);
try {
const res = await uploadVideo(file);
setVideoFile(file);
setVideoUrl(`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${res.url}`);
message.success('视频上传成功');
resolve(false);
} catch (error) {
// 获取接口返回的错误信息
let errorMsg = '';
if (error instanceof Error) {
try {
const errorData = JSON.parse(error.message);
errorMsg = errorData.detail || error.message;
} catch {
errorMsg = error.message;
}
}
message.error(errorMsg || '视频上传失败');
resolve(false);
} finally {
setVideoUploading(false);
URL.revokeObjectURL(video.src);
}
};
video.onerror = () => {
message.error('视频文件无效');
URL.revokeObjectURL(video.src);
resolve(false);
};
video.src = URL.createObjectURL(file);
});
};
const beforeImageUpload = async (file: File): Promise<boolean> => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件');
return false;
}
const isLt10M = file.size / 1024 / 1024 < 10;
if (!isLt10M) {
message.error('图片大小不能超过10MB');
return false;
}
// 检查图片比例
const img = new Image();
return new Promise((resolve) => {
img.onload = async () => {
const ratio = img.width / img.height;
const isRatioValid = Math.abs(ratio - 0.75) < 0.1 || Math.abs(ratio - 0.5625) < 0.1;
// if (!isRatioValid) {
// message.warning('建议使用3:4或9:16比例的图片以获得最佳效果');
// }
// 调用上传接口
setImageUploading(true);
try {
const res = await uploadImage(file);
setImageFile(file);
setImageUrl(`${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${res.url}`);
message.success('图片上传成功');
resolve(false);
} catch (error) {
// 获取接口返回的错误信息
let errorMsg = '';
if (error instanceof Error) {
try {
const errorData = JSON.parse(error.message);
errorMsg = errorData.detail || error.message;
} catch {
errorMsg = error.message;
}
}
message.error(errorMsg || '图片上传失败');
resolve(false);
} finally {
setImageUploading(false);
URL.revokeObjectURL(img.src);
}
};
img.onerror = () => {
message.error('图片文件无效');
URL.revokeObjectURL(img.src);
resolve(false);
};
img.src = URL.createObjectURL(file);
});
};
// 删除视频
const handleRemoveVideo = () => {
if (videoUrl) {
URL.revokeObjectURL(videoUrl);
}
setVideoFile(null);
setVideoUrl('');
};
// 删除图片
const handleRemoveImage = () => {
if (imageUrl) {
URL.revokeObjectURL(imageUrl);
}
setImageFile(null);
setImageUrl('');
};
// 处理生成
const handleGenerate = () => {
if (!videoUrl) {
message.warning('请上传复刻视频');
return;
}
if (!imageUrl) {
message.warning('请上传产品图片');
return;
}
if (!originalProductName.trim()) {
message.warning('请输入原视频产品名称');
return;
}
if (!ownProductName.trim()) {
message.warning('请输入自有产品名称');
return;
}
if (!productSellingPoints.trim()) {
message.warning('请输入产品卖点');
return;
}
// message.success('正在生成爆款开头复刻视频...');
// console.log('生成参数:', {
// videoUrl,
// imageUrl,
// originalProductName,
// ownProductName,
// productSellingPoints,
// });
let params = {
material_video_url: videoUrl,
material_image_url: imageUrl,
source_project_name: originalProductName,
target_project_name: ownProductName,
core_content_point: productSellingPoints,
idempotency_key: Date.now().toString(),
}
// 在这里调用生成视频的API,并传递上述参数
generateReplication(params).then((res) => {
let projectId = '';
let childId = '';
// console.log('生成视频成功:', res);
// message.success('任务开始');
// 清空上传的媒体和文本
setVideoUrl('');
setImageUrl('');
setOriginalProductName('');
setOwnProductName('');
setProductSellingPoints('');
fetchCardList(1, 8);
// 获取第一个的id直接进行下一步
getReplicationList(1, 20).then((res: any) => {
if (res.items) {
const firstId = res.items[0].id;
setTableData(res.items);
getReplicationDetail(res.items[0].id).then((res: any) => {
projectId = res.id;
childId = res.steps[0].id;
getone(projectId, childId).then((res: any) => {
message.loading('创建中...', 3);
setTimeout(() => {
navigate(`/initial/${firstId}/initialinfo`);
}, 3000);
})
}).catch((error: any) => {
});
}
}).catch((error: any) => {
});
}).catch((error) => {
message.error('视频开头复刻失败');
});
};
return (
<div
className="replication-container"
style={{
margin: '-24px -32px -32px',
height: 'calc(100vh - 34px)',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'stretch',
gap: '2%',
// background: 'linear-gradient(135deg, #f8fafc 0%, #eef2ff 50%, #f0f9ff 100%)',
position: 'relative',
// padding: '20px 32px',
boxSizing: 'border-box',
}}
>
<div className="replication-preview" style={{ width: '70%', background: 'rgba(255,255,255,0.85)', backdropFilter: 'blur(20px)', display: 'flex', flexDirection: 'column', borderRadius: 20, overflow: 'hidden', border: '1px solid rgba(99, 102, 241, 0.1)', boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)', position: 'relative', zIndex: 10 }}>
{/* 顶部标题栏 */}
<div className="animate-fadeInUp" style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '16px 24px', borderRadius: 16,
paddingBottom: 0,
background: 'rgba(255,255,255,0.6)',
backdropFilter: 'blur(10px)',
border: '1px solid rgba(99, 102, 241, 0.08)',
position: 'relative', overflow: 'hidden', flexWrap: 'wrap', gap: 12,
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 ,
paddingBottom: 12,
}}>
{/* <div style={{ width: 32, height: 2, background: 'linear-gradient(90deg, transparent, #6366f1, #8b5cf6, transparent)', borderRadius: 1 }} /> */}
<div>
<h2 style={{
margin: 0,
fontSize: 18,
fontWeight: 700,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
WebkitBackgroundClip: 'text',
WebkitTextFillColor: 'transparent',
backgroundClip: 'text',
// textAlign: 'center',
}}>
爆款复刻
</h2>
<p style={{ fontSize: 13, color: '#64748b', margin: '4px 0 0 0' }}>
上传参考视频与产品图片,一键复刻爆款视频开头
</p>
</div>
{/* <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: '6px 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, background: 'rgba(248, 250, 252, 0.5)', borderRight: '1px solid rgba(99, 102, 241, 0.08)', overflowY: 'auto' }}>
{cardData.length > 0 ? (
<div style={{ height: '100%', padding: '24px', }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 24 }}>
<h2 style={{ margin: 0, fontSize: 16, fontWeight: 600, color: '#1e293b' }}>历史生成作品</h2>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(190px, 1fr))', gap: 16 }}>
{cardData.map((item) => (
<div
key={item.id}
style={{
background: 'rgba(255,255,255,0.9)',
border: '1px solid rgba(99, 102, 241, 0.6)',
backdropFilter: 'blur(10px)',
borderRadius: 16,
overflow: 'hidden',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.06)',
cursor: 'pointer',
transition: 'transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
// border: '1px solid rgba(99, 102, 241, 0.06)',
}}
onClick={() => navigate(`/initial/${item.id}/initialinfo`)}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-6px)';
e.currentTarget.style.boxShadow = '0 12px 32px rgba(99, 102, 241, 0.12)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 4px 16px rgba(99, 102, 241, 0.06)';
}}
>
<div style={{ position: 'relative', aspectRatio: '1/1', background: '#f8fafc' }}>
{item.finalVideoUrl ? (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.finalVideoCoverUrl}`}
alt={item.targetProjectName}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
) : (
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: 60, height: 60, borderRadius: '50%', background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<VideoCameraOutlined style={{ fontSize: 24, color: '#fff' }} />
</div>
</div>
)}
<div style={{ position: 'absolute', bottom: 8, right: 8, padding: '4px 12px', background: 'rgba(0,0,0,0.7)', borderRadius: 16, fontSize: 10, color: '#fff', backdropFilter: 'blur(4px)' }}>
{(() => {
const status = item.status;
const currentStepCode = item.current_step_code || item.currentStepCode;
if (currentStepCode === 'image_prompt_optimize') {
switch (status) {
case 'waiting_user':
return '等待融合图生成';
case 'processing':
return '图片提示词生成中';
case 'completed':
return '图片提示词生成成功';
case 'failed':
return '图片提示词生成失败';
default:
return status || '待处理';
}
} else if (currentStepCode === 'image_generate') {
switch (status) {
case 'waiting_user':
return '等待生成视频提示词';
case 'processing':
return '融合图生成中';
case 'completed':
return '融合图生成成功';
case 'failed':
return '融合图生成失败';
default:
return status || '待处理';
}
} else if (currentStepCode === 'video_prompt_optimize') {
switch (status) {
case 'waiting_user':
return '等待最终视频生成';
case 'processing':
return '视频提示词生成中';
case 'completed':
return '视频提示词生成成功';
case 'failed':
return '视频提示词生成失败';
default:
return status || '待处理';
}
} else if (currentStepCode === 'video_generate') {
switch (status) {
case 'waiting_user':
return '';
case 'processing':
return '最终视频生成中';
case 'completed':
return '最终视频生成成功';
case 'failed':
return '最终视频生成失败';
default:
return status || '待处理';
}
} else if (currentStepCode === 'material_input') {
switch (status) {
case 'waiting_user':
return '等待生成图片提示词';
case 'processing':
return '素材处理中';
case 'completed':
return '素材上传成功';
case 'failed':
return '素材上传失败';
default:
return status || '待处理';
}
} else {
const statusMap: Record<string, string> = {
'pending': '子任务待处理',
'waiting_user': '等待用户确认或触发',
'processing': '子任务处理中',
'completed': '子任务完成',
'failed': '子任务失败',
'cancelled': '子任务取消',
};
return statusMap[status] || status || '待处理';
}
})()}
</div>
</div>
<div style={{ padding: 14 }}>
<h4 style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{item.targetProjectName || '未命名作品'}
</h4>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 6 }}>
<p style={{ margin: 0, fontSize: 12, color: '#64748b' }}>
{item.createdAt ? new Date(item.createdAt).toLocaleDateString('zh-CN') : ''}
</p>
{(() => {
const status = item.status;
if (status === 'waiting_user') {
return <span style={{ fontSize: 11, color: '#f59e0b' }}>等待下一步</span>;
} else if (status === 'processing') {
return (
<span style={{ fontSize: 11, color: '#6366f1', display: 'flex', alignItems: 'center' }}>
<span>进行中</span>
<span style={{ animation: 'dots 1s infinite' }}>...</span>
<style>{`
@keyframes dots {
0%, 20% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
`}</style>
</span>
);
} else if (status === 'failed') {
return <span style={{ fontSize: 11, color: '#ef4444' }}>失败</span>;
} else if (status === 'completed' && item.finalVideoUrl) {
return <span style={{ fontSize: 11, color: '#10b981' }}>任务完成</span>;
}
return null;
})()}
</div>
</div>
</div>
))}
</div>
<div style={{ paddingTop: 20, paddingBottom: 20, display: 'flex', justifyContent: 'center' }}>
<Pagination
current={cardCurrentPage}
pageSize={cardPageSize}
total={cardTotal}
onChange={handleCardPageChange}
showSizeChanger={false}
showQuickJumper={false}
showTotal={(total) => `共 ${total} 条`}
itemRender={(current, type, originalElement) => {
if (type === 'prev') {
return (
<button
style={{
minWidth: 32,
height: 32,
border: '1px solid #e5e7eb',
borderRadius: 6,
background: cardCurrentPage === 1 ? '#f9fafb' : '#fff',
cursor: cardCurrentPage === 1 ? 'not-allowed' : 'pointer',
color: cardCurrentPage === 1 ? '#d1d5db' : '#6b7280',
fontSize: 14,
}}
disabled={cardCurrentPage === 1}
>
</button>
);
}
if (type === 'next') {
return (
<button
style={{
minWidth: 32,
height: 32,
border: '1px solid #e5e7eb',
borderRadius: 6,
background: cardCurrentPage >= Math.ceil(cardTotal / cardPageSize) ? '#f9fafb' : '#fff',
cursor: cardCurrentPage >= Math.ceil(cardTotal / cardPageSize) ? 'not-allowed' : 'pointer',
color: cardCurrentPage >= Math.ceil(cardTotal / cardPageSize) ? '#d1d5db' : '#6b7280',
fontSize: 14,
}}
disabled={cardCurrentPage >= Math.ceil(cardTotal / cardPageSize)}
>
</button>
);
}
if (type === 'page') {
return (
<button
style={{
minWidth: 32,
height: 32,
border: 'none',
borderRadius: 6,
background: current === cardCurrentPage ? '#6366f1' : '#fff',
color: current === cardCurrentPage ? '#fff' : '#374151',
fontWeight: current === cardCurrentPage ? 500 : 400,
cursor: 'pointer',
fontSize: 14,
}}
>
{current}
</button>
);
}
return originalElement;
}}
/>
</div>
</div>
) : (
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '40px',
}}
>
<div
style={{
width: 120,
height: 120,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
position: 'relative',
}}
>
<div
style={{
width: 60,
height: 60,
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
borderRadius: 20,
position: 'absolute',
bottom: 0,
right: 10,
}}
/>
<div
style={{
width: 50,
height: 50,
border: '2px solid rgba(99, 102, 241, 0.2)',
borderRadius: 16,
position: 'absolute',
top: 0,
left: 10,
}}
/>
<div
style={{
width: 56,
height: 56,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
borderRadius: 18,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 8px 24px rgba(99, 102, 241, 0.25)',
}}
>
<VideoCameraOutlined style={{ fontSize: 28, color: '#fff' }} />
</div>
</div>
<h3 style={{ margin: 0, fontSize: 16, fontWeight: 600, color: '#1e293b', marginBottom: 8 }}>
暂无生成内容
</h3>
<p style={{ margin: 0, fontSize: 13, color: '#64748b', textAlign: 'center', padding: '0 40px', lineHeight: 1.6 }}>
请完善素材与卖点,生成专属爆款复刻视频
</p>
</div>
)}
</div>
</div>
<div className="replication-form" style={{ flex: 1, overflow: 'auto', width: '28%', background: 'rgba(255,255,255,0.85)', backdropFilter: 'blur(20px)', borderRadius: 20, border: '1px solid rgba(99, 102, 241, 0.1)', boxShadow: '0 8px 32px rgba(99, 102, 241, 0.08)', position: 'relative', zIndex: 10 }}>
{/* 右侧表单区域 */}
<div className="replication-form-content" style={{ width: '100%', height: '100%', background: 'rgba(248, 250, 252, 0.5)', padding: 24, overflowY: 'auto' }}>
{/* 上传视频 */}
<div style={{ marginBottom: 20 }}>
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
上传复刻视频
</p>
{videoUrl ? (
<div style={{ position: 'relative' }}>
<video
src={videoUrl}
controls
style={{ width: '100%', borderRadius: 12, maxHeight: 200, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.08)' }}
/>
<button
onClick={handleRemoveVideo}
style={{
position: 'absolute',
top: 8,
right: 8,
width: 28,
height: 28,
border: 'none',
background: 'rgba(239, 68, 68, 0.9)',
borderRadius: '50%',
cursor: 'pointer',
color: '#fff',
fontSize: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10,
transition: 'all 0.2s',
}}
>
×
</button>
<div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 8 }}>
<VideoCameraOutlined style={{ color: '#6366f1', fontSize: 14 }} />
<span style={{ fontSize: 12, color: '#475569' }}>
{videoFile?.name}
</span>
<span style={{ fontSize: 11, color: '#94a3b8' }}>
({(videoFile?.size ? (videoFile.size / 1024 / 1024).toFixed(2) : 0)}MB)
</span>
</div>
</div>
) : (
<Upload
style={{ width: '100%' }}
beforeUpload={beforeVideoUpload}
showUploadList={false}
accept="video/mp4,video/quicktime,.mp4,.mov"
>
<div
style={{
border: '2px dashed rgba(99, 102, 241, 0.3)',
borderRadius: 12,
padding: 24,
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
background: 'rgba(99, 102, 241, 0.02)',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = '#6366f1';
(e.currentTarget as HTMLElement).style.background = 'rgba(99, 102, 241, 0.05)';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(99, 102, 241, 0.3)';
(e.currentTarget as HTMLElement).style.background = 'rgba(99, 102, 241, 0.02)';
}}
>
{videoUploading ? (
<div>
<div style={{
width: 40,
height: 40,
margin: '0 auto 12px',
border: '3px solid rgba(99, 102, 241, 0.2)',
borderTopColor: '#6366f1',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
}} />
<p style={{ margin: 0, fontSize: 13, color: '#6366f1', fontWeight: 500 }}>
上传中...
</p>
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
支持的文件类型:MP4MOV | 视频最大时长:15 | 最大大小:50M
</p>
</div>
) : (
<>
<div style={{
width: 48,
height: 48,
margin: '0 auto 12px',
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
borderRadius: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<VideoCameraOutlined style={{ fontSize: 24, color: '#6366f1' }} />
</div>
<p style={{ margin: 0, fontSize: 13, color: '#475569', fontWeight: 500 }}>
点击或拖拽上传视频
</p>
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
支持的文件类型:MP4MOV | 视频最大时长:15 | 最大大小:50M
</p>
</>
)}
</div>
</Upload>
)}
</div>
{/* 上传产品图片 */}
<div style={{ marginBottom: 20 }}>
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
上传产品图片
</p>
{imageUrl ? (
<div style={{ position: 'relative' }}>
<img
src={imageUrl}
alt="产品图片"
style={{ width: '100%', borderRadius: 12, maxHeight: 200, objectFit: 'contain', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.08)' }}
/>
<button
onClick={handleRemoveImage}
style={{
position: 'absolute',
top: 8,
right: 8,
width: 28,
height: 28,
border: 'none',
background: 'rgba(239, 68, 68, 0.9)',
borderRadius: '50%',
cursor: 'pointer',
color: '#fff',
fontSize: 16,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 10,
transition: 'all 0.2s',
}}
>
×
</button>
<div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 8 }}>
<PictureOutlined style={{ color: '#6366f1', fontSize: 14 }} />
<span style={{ fontSize: 12, color: '#475569' }}>
{imageFile?.name}
</span>
<span style={{ fontSize: 11, color: '#94a3b8' }}>
({(imageFile?.size ? (imageFile.size / 1024 / 1024).toFixed(2) : 0)}MB)
</span>
</div>
</div>
) : (
<Upload
style={{ width: '100%' }}
beforeUpload={beforeImageUpload}
showUploadList={false}
accept="image/jpeg,image/jpg,image/png,.jpg,.jpeg,.png"
>
<div
style={{
border: '2px dashed rgba(99, 102, 241, 0.3)',
borderRadius: 12,
padding: 20,
textAlign: 'center',
cursor: 'pointer',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
background: 'rgba(99, 102, 241, 0.02)',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = '#6366f1';
(e.currentTarget as HTMLElement).style.background = 'rgba(99, 102, 241, 0.05)';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.borderColor = 'rgba(99, 102, 241, 0.3)';
(e.currentTarget as HTMLElement).style.background = 'rgba(99, 102, 241, 0.02)';
}}
>
{imageUploading ? (
<div>
<div style={{
width: 36,
height: 36,
margin: '0 auto 10px',
border: '3px solid rgba(99, 102, 241, 0.2)',
borderTopColor: '#6366f1',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
}} />
<p style={{ margin: 0, fontSize: 13, color: '#6366f1', fontWeight: 500 }}>
上传中...
</p>
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
支持 JPG, JPEG, PNG 格式,图片比例为 3:4 9:16 效果最佳
</p>
</div>
) : (
<>
<div style={{
width: 44,
height: 44,
margin: '0 auto 10px',
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
borderRadius: 14,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<PictureOutlined style={{ fontSize: 22, color: '#6366f1' }} />
</div>
<p style={{ margin: 0, fontSize: 13, color: '#475569', fontWeight: 500 }}>
+ 点击上传
</p>
<p style={{ margin: 0, fontSize: 11, color: '#94a3b8', marginTop: 4 }}>
支持 JPG, JPEG, PNG 格式,图片比例为 3:4 9:16 效果最佳
</p>
</>
)}
</div>
</Upload>
)}
</div>
{/* 原视频产品名称 */}
<div style={{ marginBottom: 16 }}>
<p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#475569', marginBottom: 8 }}>
原视频产品名称
</p>
<Input
value={originalProductName}
onChange={(e) => setOriginalProductName(e.target.value)}
placeholder="请输入原视频产品名称"
style={{
borderRadius: 10,
height: 40,
fontSize: 13,
border: '1px solid rgba(99, 102, 241, 0.15)',
background: 'rgba(255,255,255,0.8)',
}}
maxLength={10}
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{originalProductName.length}/10</span>}
/>
</div>
{/* 自有产品名称 */}
<div style={{ marginBottom: 16 }}>
<p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#475569', marginBottom: 8 }}>
自有产品名称
</p>
<Input
value={ownProductName}
onChange={(e) => setOwnProductName(e.target.value)}
placeholder="请输入自有产品名称"
style={{
borderRadius: 10,
height: 40,
fontSize: 13,
border: '1px solid rgba(99, 102, 241, 0.15)',
background: 'rgba(255,255,255,0.8)',
}}
maxLength={10}
suffix={<span style={{ color: '#94a3b8', fontSize: 12 }}>{ownProductName.length}/10</span>}
/>
</div>
{/* 产品卖点 */}
<div style={{ marginBottom: 24 }}>
<p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#475569', marginBottom: 8 }}>
产品卖点
</p>
<div style={{ position: 'relative' }}>
<TextArea
value={productSellingPoints}
onChange={(e) => setProductSellingPoints(e.target.value)}
placeholder="请输入产品卖点"
style={{
borderRadius: 10,
fontSize: 13,
height: 80,
resize: 'none',
border: '1px solid rgba(99, 102, 241, 0.15)',
background: 'rgba(255,255,255,0.8)',
}}
rows={3}
maxLength={30}
/>
<span style={{ position: 'absolute', right: 10, bottom: 8, color: '#94a3b8', fontSize: 12 }}>
{productSellingPoints.length}/30
</span>
</div>
</div>
{/* 立即生成按钮 */}
<Button
type="primary"
block
size="large"
onClick={handleGenerate}
style={{
borderRadius: 12,
height: 44,
fontWeight: 600,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
border: 'none',
fontSize: 15,
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.3)',
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
}}
>
立即生成+
</Button>
</div>
</div>
{/* 创作记录弹窗 */}
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 4, height: 20, background: 'linear-gradient(180deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
<span style={{
fontSize: 16,
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: 20 }}
styles={{
body: { height: 580, display: 'flex', flexDirection: 'column', padding: 0 },
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
}}
>
{/* 搜索区域 */}
<div style={{ display: 'flex', justifyContent: 'flex-end', margin: '16px 24px', flexShrink: 0 }}>
<Input
placeholder="搜索产品名称"
value={searchKeyword}
onChange={(e) => setSearchKeyword(e.target.value)}
onPressEnter={handleSearch}
style={{
width: 220,
borderRadius: 10,
marginRight: 10,
border: '1px solid rgba(99, 102, 241, 0.15)',
background: 'rgba(255,255,255,0.8)',
}}
/>
<Button
type="primary"
onClick={handleSearch}
style={{
borderRadius: 10,
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
border: 'none',
}}
>
搜索
</Button>
</div>
{/* 表格容器 */}
<div style={{ flex: 1, overflow: 'auto', padding: '0 24px 24px' }}>
<div style={{
background: 'rgba(255,255,255,0.85)',
backdropFilter: 'blur(10px)',
borderRadius: 16,
overflow: 'hidden',
border: '1px solid rgba(99, 102, 241, 0.08)',
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.06)',
}}>
<Table
columns={[
{
title: '产品名称',
dataIndex: 'targetProjectName',
key: 'targetProjectName',
align: 'center',
width: 100,
render: (text: string) => (
<span style={{ fontSize: 13, color: '#1e293b', fontWeight: 500 }}>
{text || '-'}
</span>
),
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 220,
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: '#64748b' };
}
} 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: '#64748b' };
}
} 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: '#64748b' };
}
} else if (currentStepCode === 'video_generate') {
switch (status) {
case 'waiting_user':
return { text: '', color: '#64748b' };
case 'processing':
return { text: '最终视频生成中', color: '#f59e0b' };
case 'completed':
return { text: '任务完成', color: '#10b981' };
case 'failed':
return { text: '最终视频生成失败', color: '#ef4444' };
default:
return { text: status || '-', color: '#64748b' };
}
} 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: '#64748b' };
}
} 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: '#64748b' };
}
};
const result = getStatusText() as { text: string; color: string };
return <span style={{ fontSize: 12, color: result.color, fontWeight: 500 }}>{result.text}</span>;
},
},
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
align: 'center',
width: 200,
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 <span style={{ fontSize: 12, color: '#64748b' }}>{`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`}</span>;
},
},
{
title: '操作',
key: 'action',
align: 'center',
width: 220,
render: (_, record) => (
<Space>
<button
onClick={() => navigate(`/initial/${record.id}/initialinfo`)}
style={{
color: '#6366f1',
textDecoration: 'none',
fontSize: 13,
border: 'none',
background: 'rgba(99, 102, 241, 0.08)',
padding: '4px 12px',
borderRadius: 8,
cursor: 'pointer',
transition: 'all 0.2s',
}}
>
查看详情
</button>
<Popconfirm
title="确认删除这个爆款开头复刻任务吗?"
onConfirm={async () => {
try {
await deleteHotOpeningReplicationTask(record.id);
message.success('删除成功');
fetchList(1, pageSize, false, searchKeyword);
} catch (err) {
message.error('删除失败');
}
}}
>
<button
style={{
color: '#ef4444',
textDecoration: 'none',
fontSize: 13,
border: 'none',
background: 'rgba(239, 68, 68, 0.08)',
padding: '4px 12px',
borderRadius: 8,
cursor: 'pointer',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.background = 'rgba(239, 68, 68, 0.12)';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.background = 'rgba(239, 68, 68, 0.08)';
}}
>
删除
</button>
</Popconfirm>
</Space>
),
},
]}
dataSource={tableData}
rowKey="id"
pagination={{
current: currentPage,
pageSize: pageSize,
total: total,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `共 ${total} 条`,
onChange: handlePageChange,
itemRender: (current, type, originalElement) => {
if (type === 'prev') {
return (
<button
style={{
minWidth: 32,
height: 32,
border: '1px solid rgba(99, 102, 241, 0.15)',
borderRadius: 8,
background: currentPage === 1 ? 'rgba(243, 244, 246, 0.5)' : '#fff',
cursor: currentPage === 1 ? 'not-allowed' : 'pointer',
color: currentPage === 1 ? '#94a3b8' : '#6366f1',
fontSize: 14,
}}
disabled={currentPage === 1}
>
</button>
);
}
if (type === 'next') {
return (
<button
style={{
minWidth: 32,
height: 32,
border: '1px solid rgba(99, 102, 241, 0.15)',
borderRadius: 8,
background: currentPage >= Math.ceil(total / pageSize) ? 'rgba(243, 244, 246, 0.5)' : '#fff',
cursor: currentPage >= Math.ceil(total / pageSize) ? 'not-allowed' : 'pointer',
color: currentPage >= Math.ceil(total / pageSize) ? '#94a3b8' : '#6366f1',
fontSize: 14,
}}
disabled={currentPage >= Math.ceil(total / pageSize)}
>
</button>
);
}
if (type === 'page') {
return (
<button
style={{
minWidth: 32,
height: 32,
border: 'none',
borderRadius: 8,
background: current === currentPage ? 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)' : '#fff',
color: current === currentPage ? '#fff' : '#475569',
fontWeight: current === currentPage ? 600 : 400,
cursor: 'pointer',
fontSize: 14,
}}
>
{current}
</button>
);
}
return originalElement;
},
}}
style={{ fontSize: 13 }}
scroll={{ y: 350 }}
/>
</div>
</div>
</Modal>
</div>
);
};
export default GenerateConver;