1473 lines
59 KiB
TypeScript
1473 lines
59 KiB
TypeScript
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 }}>
|
||
支持的文件类型:MP4、MOV | 视频最大时长: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 }}>
|
||
支持的文件类型:MP4、MOV | 视频最大时长: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; |