1214 lines
81 KiB
TypeScript
1214 lines
81 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 } from '../api/index';
|
||
import './css/InitialInfo.css';
|
||
|
||
const { Title, Text } = Typography;
|
||
const { TextArea } = Input;
|
||
|
||
function InitialInfo() {
|
||
const navigate = useNavigate();
|
||
const { creatID } = useParams<{ creatID: string }>();
|
||
|
||
const [modalVisible, setModalVisible] = useState(false);
|
||
const [promptText, setPromptText] = useState('');
|
||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||
const [currentType, setCurrentType] = useState<string>('image');
|
||
const [formData, setFormData] = useState<any>({});
|
||
const [pollingTimer, setPollingTimer] = useState<any>(null);
|
||
|
||
// 引擎和视频参数相关状态
|
||
const [enginesele, setEnginesele] = useState<any>({});
|
||
const [countType, setCountType] = useState('');
|
||
const [showEngineModal, setShowEngineModal] = useState(false);
|
||
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
|
||
const [videoDuration, setVideoDuration] = useState(5);
|
||
const [videoAspectRatio, setVideoAspectRatio] = useState<string>('16:9');
|
||
const [videoResolution, setVideoResolution] = useState<string>('480p');
|
||
const [engineOptions, setEngineOptions] = useState<{
|
||
ratios: string[];
|
||
resolutions: string[];
|
||
durations: number[];
|
||
}>({
|
||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||
resolutions: ['480p', '720p', '1080p'],
|
||
durations: [5, 8, 10, 12, 15],
|
||
});
|
||
|
||
// 创作记录表格数据
|
||
const [tableData, setTableData] = useState<any[]>([]);
|
||
// 分页状态
|
||
const [currentPage, setCurrentPage] = useState(1);
|
||
const [pageSize, setPageSize] = useState(20);
|
||
const [total, setTotal] = useState(0);
|
||
// 任务详情数据
|
||
const [taskDetail, setTaskDetail] = useState<any>({});
|
||
// API 返回的步骤数据
|
||
const [apiSteps, setApiSteps] = useState<any[]>([]);
|
||
|
||
//
|
||
const baseSteps = [
|
||
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
|
||
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
|
||
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
|
||
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
|
||
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
|
||
];
|
||
|
||
// 合并基础步骤和API返回的状态
|
||
const steps = baseSteps.map((step, index) => ({
|
||
...step,
|
||
status: apiSteps[index]?.status || '',
|
||
id: apiSteps[index]?.id || index,
|
||
output: apiSteps[index]?.output || '',
|
||
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
||
}));
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
// 获取列表数据的函数
|
||
const fetchList = (page: number, size: number) => {
|
||
getReplicationList(page, size).then((res: any) => {
|
||
if (res.items) {
|
||
setTableData(res.items);
|
||
}
|
||
if (res.total !== undefined) {
|
||
setTotal(res.total);
|
||
}
|
||
}).catch((error: any) => {
|
||
});
|
||
};
|
||
|
||
// 分页变化处理
|
||
const handlePageChange = (page: number, size: number) => {
|
||
setCurrentPage(page);
|
||
setPageSize(size);
|
||
fetchList(page, size);
|
||
};
|
||
|
||
// 获取复刻列表数据
|
||
useEffect(() => {
|
||
fetchList(currentPage, pageSize);
|
||
}, []);
|
||
|
||
// 获取引擎列表
|
||
useEffect(() => {
|
||
getEngine()
|
||
.then((data: any) => {
|
||
setEnginesele(data.engine || {});
|
||
// 默认选中第一个视频引擎
|
||
if (data.engine?.video && data.engine.video.length > 0) {
|
||
setCountType(data.engine.video[0].id);
|
||
// 设置默认引擎参数
|
||
const firstEngine = data.engine.video[0];
|
||
setEngineOptions({
|
||
ratios: firstEngine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||
resolutions: firstEngine.supportedResolutions || ['480p', '720p', '1080p'],
|
||
durations: firstEngine.supportedDurations || [5, 8, 10, 12, 15],
|
||
});
|
||
}
|
||
})
|
||
.catch((error: any) => {
|
||
});
|
||
}, []);
|
||
|
||
// 组件卸载时清理定时器
|
||
useEffect(() => {
|
||
return () => {
|
||
if (pollingTimer) {
|
||
clearInterval(pollingTimer);
|
||
setPollingTimer(null);
|
||
}
|
||
};
|
||
}, [pollingTimer]);
|
||
|
||
// 点击外部关闭弹窗
|
||
useEffect(() => {
|
||
const handleClickOutside = (e: MouseEvent) => {
|
||
const target = e.target as HTMLElement;
|
||
if (!target.closest('.image-settings-trigger') && !target.closest('.image-settings-popover')) {
|
||
setShowEngineModal(false);
|
||
setShowVideoSettingsModal(false);
|
||
}
|
||
};
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
return () => {
|
||
document.removeEventListener('mousedown', handleClickOutside);
|
||
};
|
||
}, []);
|
||
|
||
// 获取任务详情数据
|
||
useEffect(() => {
|
||
if (creatID) {
|
||
getReplicationDetail(creatID).then((res: any) => {
|
||
setTaskDetail(res);
|
||
if (res.steps) {
|
||
setApiSteps(res.steps);
|
||
}
|
||
}).catch((error: any) => {
|
||
});
|
||
}
|
||
}, [creatID]);
|
||
|
||
// 监听合并后的 steps 数据,判断第五步状态并启动/停止轮询
|
||
useEffect(() => {
|
||
|
||
// 检查第五步的状态(索引 4)
|
||
const fifthStep = steps[4];
|
||
|
||
if (fifthStep && fifthStep.status !== 'completed' && fifthStep.status !== 'failed') {
|
||
// 第五步未完成,启动轮询
|
||
if (!pollingTimer) {
|
||
const timer = setInterval(pollTaskDetail, 30000);
|
||
setPollingTimer(timer);
|
||
} else {
|
||
}
|
||
} else {
|
||
// 第五步已完成或失败,停止轮询
|
||
if (fifthStep) {
|
||
if (pollingTimer) {
|
||
clearInterval(pollingTimer);
|
||
setPollingTimer(null);
|
||
}
|
||
}
|
||
}
|
||
}, [steps]);
|
||
|
||
const handleOpenModal = (prompt?: any, type?: string) => {
|
||
|
||
setCurrentType(type || 'image');
|
||
|
||
if (type === 'video' && typeof prompt === 'object') {
|
||
setFormData(prompt);
|
||
setPromptText('');
|
||
} else {
|
||
setPromptText(prompt || '');
|
||
setFormData({});
|
||
}
|
||
|
||
setModalVisible(true);
|
||
};
|
||
|
||
const handleConfirm = () => {
|
||
setModalVisible(false);
|
||
};
|
||
|
||
// 轮询任务详情
|
||
const pollTaskDetail = () => {
|
||
if (!creatID) {
|
||
return;
|
||
}
|
||
|
||
getReplicationDetail(creatID).then((res: any) => {
|
||
setTaskDetail(res);
|
||
if (res.steps) {
|
||
setApiSteps(res.steps);
|
||
}
|
||
}).catch((error: any) => {
|
||
});
|
||
};
|
||
|
||
// 刷新任务详情(用于点击下一步后更新数据)
|
||
const refreshTaskDetail = () => {
|
||
if (!creatID) return;
|
||
|
||
getReplicationDetail(creatID).then((res: any) => {
|
||
setTaskDetail(res);
|
||
if (res.steps) {
|
||
setApiSteps(res.steps);
|
||
}
|
||
}).catch((error: any) => {
|
||
});
|
||
};
|
||
|
||
const createimage = (stepId: number) => {
|
||
let params = {
|
||
engine_id: "0019e3dac0b795b925b",
|
||
image_proportion: "1:1",
|
||
image_px: "2048x2048",
|
||
image_size: "2K"
|
||
}
|
||
gettwo(taskDetail.id, stepId.toString(), params).then((res: any) => {
|
||
// 重新获取任务详情以更新数据
|
||
refreshTaskDetail();
|
||
}).catch((error: any) => {
|
||
});
|
||
}
|
||
const newcreateimage = () => {
|
||
// console.log('下一步:', stepId);
|
||
let params = {
|
||
engine_id: "0019e3dac0b795b925b",
|
||
image_proportion: "1:1",
|
||
image_px: "2048x2048",
|
||
image_size: "2K"
|
||
}
|
||
|
||
gettwo(taskDetail.id, steps[1].id.toString(), params).then((res: any) => {
|
||
refreshTaskDetail();
|
||
|
||
}).catch((error: any) => {
|
||
});
|
||
}
|
||
|
||
// 下一步按钮点击处理
|
||
const handleNextStep = (stepId: number) => {
|
||
// 获取引擎 ID 和视频参数
|
||
const engineId = countType;
|
||
const videoParams = {
|
||
engine_id : countType,
|
||
duration: videoDuration,
|
||
aspect_ratio: videoAspectRatio,
|
||
resolution: videoResolution,
|
||
target_platform: "抖音",
|
||
};
|
||
// console.log('引擎 ID:', engineId);
|
||
getthree(taskDetail.id, stepId.toString(), videoParams).then((res: any) => {
|
||
// 重新获取任务详情以更新数据
|
||
refreshTaskDetail();
|
||
}).catch((error: any) => {
|
||
});
|
||
// 这里可以添加下一步的逻辑,比如调用接口等
|
||
};
|
||
|
||
const createvideo = (stepId: number,engineId: string) => {
|
||
let params = {
|
||
engine_id: engineId,
|
||
}
|
||
|
||
|
||
getfour(taskDetail.id, stepId.toString(), params).then((res: any) => {
|
||
// 重新获取任务详情以更新数据
|
||
refreshTaskDetail();
|
||
}).catch((error: any) => {
|
||
});
|
||
}
|
||
const agincreatevideo = () => {
|
||
let params = {
|
||
engine_id: steps[2].engineId,
|
||
}
|
||
|
||
|
||
getfour(taskDetail.id, steps[2].id.toString(), params).then((res: any) => {
|
||
// 重新获取任务详情以更新数据
|
||
refreshTaskDetail();
|
||
}).catch((error: any) => {
|
||
});
|
||
}
|
||
|
||
|
||
|
||
return (
|
||
<React.Fragment>
|
||
<div style={{ height: '94vh', background: '#f8fafc' }}>
|
||
<div style={{ height: 'calc(94vh)', }}>
|
||
<div style={{ height: '100%', display: 'flex', justifyContent: 'space-between', gap: '2%' }}>
|
||
<div style={{ width: '70%', background: '#fff', borderRadius: 12, overflowY: 'auto' }}>
|
||
<div style={{ borderBottom: '1px solid #e2e8f0', width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px', boxSizing: 'border-box' }}>
|
||
<Button
|
||
type="text"
|
||
icon={<ArrowLeftOutlined />}
|
||
onClick={() => navigate(-1)}
|
||
style={{ color: '#64748b' }}
|
||
>
|
||
返回
|
||
</Button>
|
||
<h3 style={{ margin: 0 }}>
|
||
爆款开头复刻 - 任务详情
|
||
</h3>
|
||
<Button type="primary" ghost style={{ borderRadius: 6, padding: '4px 12px', fontSize: 12, borderColor: '#6366f1', color: '#6366f1' }} onClick={() => setIsModalOpen(true)}>
|
||
创作记录
|
||
</Button>
|
||
</div>
|
||
<div className="material_box">
|
||
<div style={{ display: 'flex', gap: 24, flexWrap: 'wrap', alignItems: 'flex-start' }}>
|
||
<div style={{ flex: 1, minWidth: 250 }}>
|
||
<span style={{ fontSize: 14, fontWeight: 500, color: '#374151', marginBottom: 8, display: 'block' }}>视频</span>
|
||
<div className="medio_box" style={{ aspectRatio: '16/9', background: '#f1f5f9', borderRadius: 8, overflow: 'hidden' }}>
|
||
{taskDetail?.material?.materialVideoUrl ? (
|
||
<video
|
||
controls
|
||
src={taskDetail.material.materialVideoUrl}
|
||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||
/>
|
||
) : (
|
||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 12 }}>暂无视频</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div className="product_img" style={{ flex: 1, minWidth: 250 }}>
|
||
<span style={{ fontSize: 14, fontWeight: 500, color: '#374151', marginBottom: 8, display: 'block' }}>产品图片</span>
|
||
<div className="medio_box" style={{ aspectRatio: '1/1', background: '#f1f5f9', borderRadius: 8, overflow: 'hidden' }}>
|
||
{taskDetail?.material?.materialImageUrl ? (
|
||
<img src={taskDetail.material.materialImageUrl} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
|
||
) : (
|
||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 12 }}>暂无图片</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{taskDetail?.finalImageUrl && (
|
||
<div style={{ marginTop: 16 }}>
|
||
<div>
|
||
<span style={{ fontSize: 14, fontWeight: 500, color: '#374151', marginBottom: 8, display: 'block' }}>生成图片</span>
|
||
<div className="medio_box" style={{ aspectRatio: '1/1', background: '#f1f5f9', borderRadius: 8, overflow: 'hidden' }}>
|
||
<img
|
||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalImageUrl}`}
|
||
alt=""
|
||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
{taskDetail?.finalVideoUrl && (
|
||
<div style={{ marginTop: 16 }}>
|
||
<div>
|
||
<span style={{ fontSize: 14, fontWeight: 500, color: '#374151', marginBottom: 8, display: 'block' }}>生成视频</span>
|
||
<div className="medio_box" style={{ aspectRatio: '16/9', background: '#f1f5f9', borderRadius: 8, overflow: 'hidden' }}>
|
||
<video
|
||
controls
|
||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`}
|
||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div style={{ width: '28%', minWidth: '400px', background: '#fff', borderRadius: 12, overflowY: 'auto' }}>
|
||
<div style={{ padding: '14px' }}> 生成步骤</div>
|
||
<Collapse
|
||
defaultActiveKey={['']}
|
||
ghost
|
||
bordered={false}
|
||
style={{ background: 'transparent' }}
|
||
expandIconPlacement="end"
|
||
items={steps.map((step) => ({
|
||
key: String(step.id),
|
||
label: (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, width: '100%', justifyContent: 'space-between' }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: 16, height: 16 }}>
|
||
{step.status === 'completed' ? (
|
||
<CheckCircleOutlined style={{ fontSize: 16, color: '#22c55e' }} />
|
||
) : step.status === 'processing' ? (
|
||
<div style={{ width: 16, height: 16, borderRadius: '50%', border: '2px solid #e2e8f0', borderTopColor: '#6366f1', animation: 'spinSlow 1s linear infinite' }} />
|
||
) : step.status === 'failed' ? (
|
||
<div style={{ width: 16, height: 16, borderRadius: '50%', background: '#ef4444', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
<span style={{ color: '#fff', fontSize: 10 }}>×</span>
|
||
</div>
|
||
) : (
|
||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#cbd5e1' }} />
|
||
)}
|
||
</div>
|
||
<span style={{ fontSize: 14, color: step.status === 'processing' || step.status === 'waiting_user' ? '#6366f1' : '#1e293b', fontWeight: 500 }}>
|
||
{step.title}
|
||
</span>
|
||
{(step.status === 'processing' || step.status === 'waiting_user') && (
|
||
<span style={{ fontSize: 12, color: '#6366f1', marginLeft: 8 }}>生成中</span>
|
||
)}
|
||
{step.status === 'failed' && (
|
||
<span style={{ fontSize: 12, color: '#ef4444', marginLeft: 8 }}>失败</span>
|
||
)}
|
||
{step.status === '' && (
|
||
<span style={{ fontSize: 12, color: '#717d8b', marginLeft: 8 }}>待生成</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
),
|
||
children: (
|
||
<div style={{ padding: '16px', background: '#f8fafc', borderRadius: 8, marginTop: 8 }}>
|
||
{/* 步骤1: 原始素材 */}
|
||
{step.childId === 1 && (
|
||
<>
|
||
<div style={{ display: 'flex', gap: 20, marginBottom: 20, alignItems: 'flex-start', flexWrap: 'wrap' }}>
|
||
<div style={{ flex: 1, minWidth: 200 }}>
|
||
<Text style={{ color: '#64748b', fontSize: 13, marginBottom: 10, display: 'block', fontWeight: 500 }}>原视频</Text>
|
||
<div style={{ height: 180, borderRadius: 10, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
{taskDetail?.material?.materialVideoUrl ? (
|
||
<video
|
||
src={taskDetail.material.materialVideoUrl}
|
||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
|
||
controls
|
||
/>
|
||
) : (
|
||
<div style={{ textAlign: 'center', color: '#94a3b8' }}>
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ marginBottom: 8 }}>
|
||
<rect x="2" y="2" width="20" height="16" rx="2" ry="2"/>
|
||
<path d="M8 12l4 2 4-2"/>
|
||
</svg>
|
||
<div style={{ fontSize: 13 }}>暂无视频</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<div style={{ flex: 1, minWidth: 200 }}>
|
||
<Text style={{ color: '#64748b', fontSize: 13, marginBottom: 10, display: 'block', fontWeight: 500 }}>产品图片</Text>
|
||
<div style={{ height: 180, borderRadius: 10, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||
{taskDetail?.material?.materialImageUrl ? (
|
||
<img
|
||
src={taskDetail.material.materialImageUrl}
|
||
alt="产品图片"
|
||
style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain', borderRadius: 10 }}
|
||
/>
|
||
) : (
|
||
<div style={{ textAlign: 'center', color: '#94a3b8' }}>
|
||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ marginBottom: 8 }}>
|
||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||
<circle cx="8.5" cy="8.5" r="1.5" fill="currentColor"/>
|
||
<path d="M21 15l-5-5L5 21"/>
|
||
</svg>
|
||
<div style={{ fontSize: 13 }}>暂无图片</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</>
|
||
)}
|
||
{/* 步骤2: 生成提示词 */}
|
||
{step.childId === 2 && (
|
||
<>
|
||
{/* <Text style={{ color: '#475569', fontSize: 13, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>
|
||
{taskDetail?.generationPrompt || '暂无提示词'}
|
||
</Text> */}
|
||
<Text style={{ color: '#475569', fontSize: 13, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>
|
||
{/* {taskDetail?.generationPrompt || '暂无提示词'} */}
|
||
{step?.output?.payload?.prompt || '暂无提示词'}
|
||
</Text>
|
||
<Space style={{ marginTop: 16, gap: 12, width: '100%' }}>
|
||
<Button
|
||
type="default"
|
||
icon={<EditOutlined />}
|
||
onClick={() => handleOpenModal(step?.output?.payload?.prompt)}
|
||
style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }}
|
||
disabled={step.status !== 'completed'}
|
||
>
|
||
修改提示词
|
||
</Button>
|
||
<Button onClick={() => { message.info('正在生成图片,请稍候...'); createimage(step.id); }} type="primary" style={{ flex: 1, borderRadius: 8, background: '#6366f1', borderColor: '#6366f1', height: 36 }} disabled={step.status !== 'completed'}>
|
||
下一步:生成图片
|
||
</Button>
|
||
</Space>
|
||
</>
|
||
)}
|
||
{/* 步骤3: 生成产品融合图 */}
|
||
{step.childId === 3 && (
|
||
<>
|
||
<div style={{ borderRadius: 8, overflow: 'hidden', marginBottom: 16, background: '#f1f5f9' }}>
|
||
{/* {step.output.result.result_image_url} */}
|
||
|
||
|
||
{taskDetail.finalImageUrl ? (
|
||
<img src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalImageUrl}`} alt="融合图" style={{ width: '100%', height: 180, objectFit: 'cover' }} />
|
||
) : (
|
||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 14 }}>暂无融合图</div>
|
||
)}
|
||
</div>
|
||
{/* 引擎选择器 */}
|
||
<div style={{ marginBottom: 12 }}>
|
||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||
<button
|
||
onClick={() => {
|
||
setShowEngineModal(!showEngineModal);
|
||
setShowVideoSettingsModal(false);
|
||
}}
|
||
className="image-settings-trigger"
|
||
style={{
|
||
minWidth: 200,
|
||
padding: '4px 12px',
|
||
height: 34,
|
||
borderRadius: 8,
|
||
border: 'none',
|
||
backgroundColor: '#f1f5f9',
|
||
cursor: 'pointer',
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
transition: 'all 0.2s',
|
||
}}
|
||
>
|
||
<SettingOutlined style={{ fontSize: 14, color: '#64748b' }} />
|
||
<Text style={{
|
||
fontSize: 14,
|
||
fontWeight: 500,
|
||
color: '#64748b',
|
||
}}>
|
||
{enginesele.video?.find((e: any) => e.id === countType)?.name || '选择引擎'}
|
||
</Text>
|
||
</button>
|
||
|
||
{showEngineModal && (
|
||
<div
|
||
className="image-settings-popover"
|
||
style={{
|
||
position: 'absolute',
|
||
bottom: 'calc(100% + 8px)',
|
||
left: -20,
|
||
width: 350,
|
||
backgroundColor: '#fff',
|
||
borderRadius: 16,
|
||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||
padding: 16,
|
||
border: 'none',
|
||
zIndex: 9999,
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
{/* 选择引擎 */}
|
||
<div style={{ marginBottom: 8 }}>
|
||
<Text style={{
|
||
display: 'block',
|
||
marginBottom: 8,
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: '#666666',
|
||
}}>
|
||
选择引擎
|
||
</Text>
|
||
<div style={{
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
gap: 4,
|
||
}}>
|
||
{enginesele.video?.map((engine: any) => (
|
||
<button
|
||
key={engine.id}
|
||
onClick={() => {
|
||
setCountType(engine.id);
|
||
// 根据选中的引擎更新视频参数选项
|
||
setEngineOptions({
|
||
ratios: engine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||
resolutions: engine.supportedResolutions || ['480p', '720p', '1080p'],
|
||
durations: engine.supportedDurations || [5, 8, 10, 12, 15],
|
||
});
|
||
// 更新默认选中值,确保在新引擎支持的范围内
|
||
if (!engine.supportedRatios?.includes(videoAspectRatio)) {
|
||
setVideoAspectRatio(engine.supportedRatios?.[0] || '16:9');
|
||
}
|
||
if (!engine.supportedResolutions?.includes(videoResolution)) {
|
||
setVideoResolution(engine.supportedResolutions?.[0] || '720p');
|
||
}
|
||
if (!engine.supportedDurations?.includes(videoDuration)) {
|
||
setVideoDuration(engine.supportedDurations?.[0] || 5);
|
||
}
|
||
setShowEngineModal(false);
|
||
}}
|
||
style={{
|
||
flex: 1,
|
||
minHeight: 48,
|
||
borderRadius: 8,
|
||
border: countType === engine.id
|
||
? '2px solid #6366f1'
|
||
: '1px solid #e5e7eb',
|
||
backgroundColor: countType === engine.id
|
||
? '#fff'
|
||
: '#f9fafb',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'center',
|
||
alignItems: 'flex-start',
|
||
padding: '8px 12px',
|
||
transition: 'all 0.2s',
|
||
textAlign: 'left',
|
||
}}
|
||
>
|
||
<span style={{
|
||
fontSize: 13,
|
||
fontWeight: countType === engine.id ? 600 : 500,
|
||
color: countType === engine.id ? '#6366f1' : '#4b5563',
|
||
marginBottom: 2,
|
||
}}>
|
||
{engine.name}
|
||
</span>
|
||
<span style={{
|
||
fontSize: 11,
|
||
color: '#9ca3af',
|
||
}}>
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 视频参数设置 */}
|
||
<div style={{ marginBottom: 16 }}>
|
||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||
<button
|
||
onClick={() => {
|
||
setShowVideoSettingsModal(!showVideoSettingsModal);
|
||
setShowEngineModal(false);
|
||
}}
|
||
className="image-settings-trigger"
|
||
style={{
|
||
minWidth: 220,
|
||
padding: '4px 12px',
|
||
height: 34,
|
||
borderRadius: 8,
|
||
border: 'none',
|
||
backgroundColor: '#f1f5f9',
|
||
cursor: 'pointer',
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 6,
|
||
transition: 'all 0.2s',
|
||
}}
|
||
>
|
||
<LayoutOutlined style={{ fontSize: 14, color: '#64748b' }} />
|
||
<Text style={{
|
||
fontSize: 14,
|
||
fontWeight: 500,
|
||
color: '#64748b',
|
||
}}>
|
||
{videoAspectRatio} · {videoDuration}s · {videoResolution}
|
||
</Text>
|
||
</button>
|
||
|
||
{showVideoSettingsModal && (
|
||
<div
|
||
className="image-settings-popover"
|
||
style={{
|
||
position: 'absolute',
|
||
bottom: 'calc(100% + 8px)',
|
||
left: -20,
|
||
width: 350,
|
||
backgroundColor: '#fff',
|
||
borderRadius: 16,
|
||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||
padding: 16,
|
||
border: 'none',
|
||
zIndex: 9999,
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
{/* 选择比例 */}
|
||
<div style={{ marginBottom: 16 }}>
|
||
<Text style={{
|
||
display: 'block',
|
||
marginBottom: 8,
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: '#666666',
|
||
}}>
|
||
选择比例
|
||
</Text>
|
||
<div style={{
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
gap: 4,
|
||
}}>
|
||
{engineOptions.ratios.map((ratio) => (
|
||
<button
|
||
key={ratio}
|
||
onClick={() => setVideoAspectRatio(ratio)}
|
||
style={{
|
||
flex: '0 0 calc(14.28% - 4px)',
|
||
minWidth: 44,
|
||
height: 52,
|
||
borderRadius: 6,
|
||
border: videoAspectRatio === ratio
|
||
? '2px solid #6366f1'
|
||
: '1px solid #e5e7eb',
|
||
backgroundColor: videoAspectRatio === ratio
|
||
? '#fff'
|
||
: '#f9fafb',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
transition: 'all 0.2s',
|
||
}}
|
||
>
|
||
<div style={{
|
||
width: 18,
|
||
height: 18,
|
||
border: `2px solid ${videoAspectRatio === ratio ? '#6366f1' : '#9ca3af'}`,
|
||
borderRadius: 2,
|
||
marginBottom: 2,
|
||
}} />
|
||
<span style={{
|
||
fontSize: 9,
|
||
color: videoAspectRatio === ratio
|
||
? '#6366f1'
|
||
: '#6b7280',
|
||
fontWeight: videoAspectRatio === ratio ? 600 : 400,
|
||
}}>
|
||
{ratio}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 选择时长 */}
|
||
<div style={{ marginBottom: 16 }}>
|
||
<Text style={{
|
||
display: 'block',
|
||
marginBottom: 8,
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: '#666666',
|
||
}}>
|
||
选择时长
|
||
</Text>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||
<div style={{ flex: 1, position: 'relative', height: 24, display: 'flex', alignItems: 'center' }}>
|
||
{/* 背景轨道 */}
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: '50%',
|
||
left: 0,
|
||
right: 0,
|
||
height: 6,
|
||
borderRadius: 3,
|
||
background: '#e5e7eb',
|
||
transform: 'translateY(-50%)',
|
||
}} />
|
||
{/* 已滑动部分 */}
|
||
<div style={{
|
||
position: 'absolute',
|
||
top: '50%',
|
||
left: 0,
|
||
height: 6,
|
||
borderRadius: 3,
|
||
background: '#6366f1',
|
||
width: `${((videoDuration - Math.min(...engineOptions.durations)) / (Math.max(...engineOptions.durations) - Math.min(...engineOptions.durations))) * 100}%`,
|
||
transform: 'translateY(-50%)',
|
||
}} />
|
||
{/* 滑块 */}
|
||
<input
|
||
type="range"
|
||
min={Math.min(...engineOptions.durations)}
|
||
max={Math.max(...engineOptions.durations)}
|
||
value={videoDuration}
|
||
onChange={(e) => setVideoDuration(Number(e.target.value))}
|
||
style={{
|
||
position: 'relative',
|
||
width: '100%',
|
||
height: 24,
|
||
borderRadius: 3,
|
||
background: 'transparent',
|
||
outline: 'none',
|
||
appearance: 'none',
|
||
cursor: 'pointer',
|
||
zIndex: 1,
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
padding: '4px 12px',
|
||
backgroundColor: '#f1f5f9',
|
||
borderRadius: 6,
|
||
}}>
|
||
<span style={{ fontSize: 14, fontWeight: 600, color: '#64748b' }}>
|
||
{videoDuration}
|
||
</span>
|
||
<span style={{ fontSize: 12, color: '#9ca3af' }}>秒</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 选择分辨率 */}
|
||
<div>
|
||
<Text style={{
|
||
display: 'block',
|
||
marginBottom: 8,
|
||
fontSize: 12,
|
||
fontWeight: 500,
|
||
color: '#666666',
|
||
}}>
|
||
选择分辨率
|
||
</Text>
|
||
<div style={{ display: 'flex', gap: 6 }}>
|
||
{engineOptions.resolutions.map((resolution) => (
|
||
<button
|
||
key={resolution}
|
||
onClick={() => setVideoResolution(resolution)}
|
||
style={{
|
||
flex: 1,
|
||
height: 42,
|
||
borderRadius: 6,
|
||
border: videoResolution === resolution
|
||
? '2px solid #6366f1'
|
||
: '1px solid #e5e7eb',
|
||
backgroundColor: videoResolution === resolution
|
||
? '#6366f1'
|
||
: '#f9fafb',
|
||
cursor: 'pointer',
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
transition: 'all 0.2s',
|
||
}}
|
||
>
|
||
<span style={{
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
color: videoResolution === resolution
|
||
? '#fff'
|
||
: '#4b5563',
|
||
}}>
|
||
{resolution}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
<Space style={{ width: '100%', gap: 12 }}>
|
||
<Button type="default" icon={<EditOutlined />} style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }
|
||
} onClick={() => newcreateimage()} disabled={step.status !== 'completed'}>
|
||
重新生成
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
style={{ flex: 1, borderRadius: 8, background: '#6366f1', borderColor: '#6366f1', height: 36 }}
|
||
onClick={() => { message.info('正在生成视频提示词,请稍候...'); handleNextStep(step.id); }}
|
||
disabled={step.status !== 'completed'}
|
||
>
|
||
下一步:生成视频提示词
|
||
</Button>
|
||
</Space>
|
||
</>
|
||
)}
|
||
{/* 步骤4: 生成视频提示词 */}
|
||
{step.childId === 4 && (
|
||
<>
|
||
<Text style={{ color: '#475569', fontSize: 13, lineHeight: 1.8, whiteSpace: 'pre-wrap' }}>
|
||
{/* {taskDetail?.videoPrompt || '暂无视频提示词'} */}
|
||
{step?.output?.payload?.finalPrompt || '暂无提示词'}
|
||
|
||
</Text>
|
||
<Space style={{ marginTop: 16, gap: 12, width: '100%' }}>
|
||
<Button
|
||
type="default"
|
||
icon={<EditOutlined />}
|
||
onClick={() => handleOpenModal(step?.output?.payload?.promptSchema,'video')}
|
||
style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }}
|
||
disabled={step.status !== 'completed'}
|
||
>
|
||
修改提示词
|
||
</Button>
|
||
<Button onClick={() => { message.info('正在生成视频,请稍候...'); createvideo(step.id,step.engineId); }} type="primary" style={{ flex: 1, borderRadius: 8, background: '#6366f1', borderColor: '#6366f1', height: 36 }} disabled={step.status !== 'completed'}>
|
||
下一步:生成视频
|
||
</Button>
|
||
</Space>
|
||
</>
|
||
)}
|
||
{/* 步骤5: 生成最终视频 */}
|
||
{step.childId === 5 && (
|
||
<>
|
||
<div style={{ borderRadius: 8, overflow: 'hidden', marginBottom: 16, position: 'relative', background: '#f1f5f9' }}>
|
||
{step.status === 'completed' && taskDetail?.finalVideoUrl ? (
|
||
<video src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`} style={{ width: '100%', height: 180, objectFit: 'cover' }} controls />
|
||
) : step.status === 'processing' ? (
|
||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8 }}>
|
||
<div style={{ width: 20, height: 20, borderRadius: '50%', border: '2px solid #e2e8f0', borderTopColor: '#6366f1', animation: 'spinSlow 1s linear infinite' }} />
|
||
<span style={{ color: '#6366f1', fontSize: 14 }}>视频生成中...</span>
|
||
</div>
|
||
) : step.status === 'failed' ? (
|
||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ef4444', fontSize: 14 }}>视频生成失败</div>
|
||
) : (
|
||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 14 }}>暂无视频</div>
|
||
)}
|
||
</div>
|
||
<Space style={{ width: '100%', gap: 12 }}>
|
||
<Button onClick={() => agincreatevideo()} type="default" icon={<EditOutlined />} style={{ flex: 1, borderRadius: 8, borderColor: '#6366f1', color: '#6366f1', height: 36 }} disabled={step.status !== 'completed'}>
|
||
重新生成
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
icon={<DownloadOutlined />}
|
||
style={{ flex: 1, borderRadius: 8, background: '#6366f1', borderColor: '#6366f1', height: 36 }}
|
||
disabled={step.status !== 'completed'}
|
||
onClick={() => {
|
||
const videoUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail?.finalVideoUrl}&download=1`;
|
||
const link = document.createElement('a');
|
||
link.href = videoUrl;
|
||
link.download = `video_${Date.now()}.mp4`;
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
}}
|
||
>
|
||
下载视频
|
||
</Button>
|
||
</Space>
|
||
</>
|
||
)}
|
||
</div>
|
||
),
|
||
}))}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Modal
|
||
title="修改提示词"
|
||
open={modalVisible}
|
||
onCancel={() => setModalVisible(false)}
|
||
footer={[
|
||
<Button key="cancel" onClick={() => setModalVisible(false)}>取消</Button>,
|
||
<Button key="confirm" type="primary" onClick={handleConfirm}>确认</Button>,
|
||
]}
|
||
width={800}
|
||
>
|
||
{currentType === 'image' ? (
|
||
<TextArea
|
||
value={promptText}
|
||
onChange={(e) => setPromptText(e.target.value)}
|
||
rows={8}
|
||
style={{ width: '100%', height: 300 }}
|
||
placeholder="请输入提示词"
|
||
/>
|
||
) : (
|
||
<div style={{ maxHeight: 500, overflowY: 'auto', paddingRight: 10 }}>
|
||
<FormRenderer data={formData} onChange={setFormData} />
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
{/* 创作记录弹窗 */}
|
||
<Modal
|
||
title="创作记录"
|
||
open={isModalOpen}
|
||
onCancel={() => setIsModalOpen(false)}
|
||
width={800}
|
||
footer={null}
|
||
style={{ borderRadius: 12 }}
|
||
>
|
||
{/* 搜索区域 */}
|
||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
|
||
<Input
|
||
placeholder="搜索产品名称"
|
||
style={{ width: 200, borderRadius: 6, marginRight: 8 }}
|
||
/>
|
||
<Button type="primary" style={{ borderRadius: 6 }}>
|
||
搜索
|
||
</Button>
|
||
</div>
|
||
|
||
{/* 表格 */}
|
||
<Table
|
||
columns={[
|
||
{
|
||
title: '产品名称',
|
||
dataIndex: 'targetProjectName',
|
||
key: 'targetProjectName',
|
||
},
|
||
{
|
||
title: '状态',
|
||
dataIndex: 'status',
|
||
key: 'status',
|
||
render: (text: string) => {
|
||
const statusMap: Record<string, string> = {
|
||
'pending': '子任务待处理',
|
||
'waiting_user': '等待用户确认或触发',
|
||
'processing': '子任务处理中',
|
||
'completed': '子任务完成',
|
||
'failed': '子任务失败',
|
||
'cancelled': '子任务取消',
|
||
};
|
||
return statusMap[text] || text || '-';
|
||
},
|
||
},
|
||
{
|
||
title: '创建时间',
|
||
dataIndex: 'createdAt',
|
||
key: 'createdAt',
|
||
render: (text: string) => {
|
||
if (!text) return '-';
|
||
const date = new Date(text);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
},
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
render: (_, record) => (
|
||
<button
|
||
onClick={() => navigate(`/initial/${record.id}/initialinfo`)}
|
||
style={{ color: '#6366f1', textDecoration: 'none', fontSize: 12, border: 'none', background: 'none', cursor: 'pointer' }}
|
||
>
|
||
查看详情
|
||
</button>
|
||
),
|
||
},
|
||
]}
|
||
dataSource={tableData}
|
||
rowKey="id"
|
||
pagination={{
|
||
current: currentPage,
|
||
pageSize: pageSize,
|
||
total: total,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: (total) => `共 ${total} 条`,
|
||
onChange: handlePageChange,
|
||
}}
|
||
style={{ fontSize: 13 }}
|
||
/>
|
||
</Modal>
|
||
</React.Fragment>
|
||
);
|
||
}
|
||
|
||
const FormRenderer = ({ data, onChange }: { data: any; onChange: (data: any) => void }) => {
|
||
const handleFieldChange = (path: string[], value: any) => {
|
||
const newData = { ...data };
|
||
let current = newData;
|
||
for (let i = 0; i < path.length - 1; i++) {
|
||
current = current[path[i]];
|
||
}
|
||
current[path[path.length - 1]] = value;
|
||
onChange(newData);
|
||
};
|
||
|
||
const handleArrayItemChange = (path: string[], index: number, value: any) => {
|
||
const newData = { ...data };
|
||
let current = newData;
|
||
for (let i = 0; i < path.length; i++) {
|
||
if (i === path.length - 1) {
|
||
current[path[i]] = [...current[path[i]]];
|
||
current[path[i]][index] = value;
|
||
} else {
|
||
current = current[path[i]];
|
||
}
|
||
}
|
||
onChange(newData);
|
||
};
|
||
|
||
const handleArrayAdd = (path: string[]) => {
|
||
const newData = { ...data };
|
||
let current = newData;
|
||
for (let i = 0; i < path.length; i++) {
|
||
if (i === path.length - 1) {
|
||
current[path[i]] = [...current[path[i]], ''];
|
||
} else {
|
||
current = current[path[i]];
|
||
}
|
||
}
|
||
onChange(newData);
|
||
};
|
||
|
||
const handleArrayRemove = (path: string[], index: number) => {
|
||
const newData = { ...data };
|
||
let current = newData;
|
||
for (let i = 0; i < path.length; i++) {
|
||
if (i === path.length - 1) {
|
||
current[path[i]] = current[path[i]].filter((_: any, i: number) => i !== index);
|
||
} else {
|
||
current = current[path[i]];
|
||
}
|
||
}
|
||
onChange(newData);
|
||
};
|
||
|
||
const renderField = (key: string, value: any, path: string[]) => {
|
||
if (Array.isArray(value)) {
|
||
return (
|
||
<div key={key} style={{ marginBottom: 16 }}>
|
||
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 8 }}>
|
||
<Text strong style={{ color: '#374151', fontSize: 13 }}>{key}</Text>
|
||
<Button
|
||
type="text"
|
||
size="small"
|
||
onClick={() => handleArrayAdd(path)}
|
||
style={{ marginLeft: 8, color: '#6366f1', fontSize: 12 }}
|
||
>
|
||
+ 添加
|
||
</Button>
|
||
</div>
|
||
<div style={{ border: '1px solid #e5e7eb', borderRadius: 8, padding: 12, background: '#f9fafb' }}>
|
||
{value.map((item: any, index: number) => (
|
||
<div key={index} style={{ display: 'flex', alignItems: 'flex-start', gap: 8, marginBottom: 8 }}>
|
||
<span style={{ color: '#9ca3af', fontSize: 12, marginTop: 6 }}>{index + 1}.</span>
|
||
<div style={{ flex: 1 }}>
|
||
{typeof item === 'object' ? (
|
||
<FormRenderer
|
||
data={item}
|
||
onChange={(newItem) => handleArrayItemChange(path, index, newItem)}
|
||
/>
|
||
) : (
|
||
<Input
|
||
value={item}
|
||
onChange={(e) => handleArrayItemChange(path, index, e.target.value)}
|
||
style={{ width: '100%', borderRadius: 6 }}
|
||
/>
|
||
)}
|
||
</div>
|
||
<Button
|
||
type="text"
|
||
danger
|
||
onClick={() => handleArrayRemove(path, index)}
|
||
style={{ marginTop: 4 }}
|
||
>
|
||
删除
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (typeof value === 'object' && value !== null) {
|
||
return (
|
||
<div key={key} style={{ marginBottom: 16 }}>
|
||
<Text strong style={{ color: '#374151', fontSize: 13, marginBottom: 8, display: 'block' }}>
|
||
{key}
|
||
</Text>
|
||
<div style={{ borderLeft: '3px solid #6366f1', paddingLeft: 12, marginLeft: 4 }}>
|
||
<FormRenderer data={value} onChange={(newValue) => handleFieldChange(path, newValue)} />
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div key={key} style={{ marginBottom: 12 }}>
|
||
<Text style={{ color: '#6b7280', fontSize: 12, marginBottom: 4, display: 'block' }}>{key}</Text>
|
||
<Input
|
||
value={value}
|
||
onChange={(e) => handleFieldChange(path, e.target.value)}
|
||
style={{ width: '100%', borderRadius: 6 }}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
{Object.entries(data).map(([key, value]) => renderField(key, value, [key]))}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default InitialInfo;
|