爆款,拆镜流程修改
This commit is contained in:
@@ -386,7 +386,7 @@ const HomePage: React.FC = () => {
|
||||
|
||||
|
||||
return (
|
||||
<div className="content_box">
|
||||
<div className="content_box" style={{backgroundColor: '#fff', borderRadius: 16, }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<h3>首页工作台 </h3>
|
||||
<p
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button, Typography, Collapse, Space, Modal, Input, Table, message, Tooltip } from 'antd';
|
||||
import { Button, Typography, Collapse, Space, Modal, Input, Table, message, Tooltip, Select } from 'antd';
|
||||
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined, WarningOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { getReplicationList, getReplicationDetail, gettwo, getthree, getfour, getEngine, updateHotOpeningVideoPromptSchema, updateImagePrompt, calculateCredits, retryHotOpeningVideoPromptV2, updateHotOpeningVideoPromptSchemaV2, generateHotOpeningVideoV2 } from '../api/index';
|
||||
@@ -49,6 +49,7 @@ function InitialInfo() {
|
||||
const [imageEngineId, setImageEngineId] = useState<string>('');
|
||||
const [showEngineModal, setShowEngineModal] = useState(false);
|
||||
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
|
||||
const [showVideoSettingsModalStep4, setShowVideoSettingsModalStep4] = useState(false);
|
||||
const [videoDuration, setVideoDuration] = useState(5);
|
||||
const [videoAspectRatio, setVideoAspectRatio] = useState<string>('16:9');
|
||||
const [videoResolution, setVideoResolution] = useState<string>('480p');
|
||||
@@ -74,6 +75,7 @@ function InitialInfo() {
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
// 任务详情数据
|
||||
const [taskDetail, setTaskDetail] = useState<any>({});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// API 返回的步骤数据
|
||||
const [apiSteps, setApiSteps] = useState<any[]>([]);
|
||||
// 当前展开的步骤
|
||||
@@ -352,6 +354,7 @@ function InitialInfo() {
|
||||
// 获取任务详情数据
|
||||
useEffect(() => {
|
||||
if (creatID) {
|
||||
setIsLoading(true);
|
||||
getReplicationDetail(creatID, flowVersion).then((res: any) => {
|
||||
setTaskDetail(res);
|
||||
if (res.steps) {
|
||||
@@ -359,6 +362,8 @@ function InitialInfo() {
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
message.error(error?.message || '加载任务详情失败');
|
||||
}).finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}, [creatID, flowVersion]);
|
||||
@@ -628,11 +633,11 @@ function InitialInfo() {
|
||||
setRetryPromptModalVisible(true);
|
||||
};
|
||||
|
||||
const submitRegenerateVideoPrompt = async () => {
|
||||
if (!isV2 || !taskDetail?.id || !retryPromptStepId || !countType) return;
|
||||
setRetryPromptSubmitting(true);
|
||||
const submitRegenerateVideoPrompt = async (stepId: string) => {
|
||||
if (!isV2 || !taskDetail?.id || !stepId || !countType) return;
|
||||
// setRetryPromptSubmitting(true);
|
||||
try {
|
||||
await retryHotOpeningVideoPromptV2(taskDetail.id, retryPromptStepId, {
|
||||
await retryHotOpeningVideoPromptV2(taskDetail.id, stepId, {
|
||||
video_config: {
|
||||
engine_id: countType,
|
||||
duration: videoDuration,
|
||||
@@ -641,13 +646,11 @@ function InitialInfo() {
|
||||
},
|
||||
});
|
||||
message.info('正在按新视频参数重新生成视频提示词,请稍候...');
|
||||
setRetryPromptModalVisible(false);
|
||||
setRetryPromptStepId('');
|
||||
refreshTaskDetail();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '重新生成视频提示词失败');
|
||||
} finally {
|
||||
setRetryPromptSubmitting(false);
|
||||
// setRetryPromptSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -670,7 +673,106 @@ function InitialInfo() {
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
|
||||
{isLoading && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(135deg, rgba(250, 248, 255, 0.95) 0%, rgba(245, 243, 255, 0.95) 50%, rgba(240, 249, 255, 0.95) 100%)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
zIndex: 9999,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 20,
|
||||
opacity: 1,
|
||||
transition: 'opacity 0.3s ease',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: 60,
|
||||
height: 60,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: '2px solid rgba(99, 102, 241, 0.08)',
|
||||
borderRadius: '50%',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: '2px solid transparent',
|
||||
borderTopColor: '#6366f1',
|
||||
borderRightColor: '#8b5cf6',
|
||||
borderRadius: '50%',
|
||||
animation: 'spinSlow 0.8s linear infinite',
|
||||
}} />
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
width: 40,
|
||||
height: 40,
|
||||
border: '2px solid transparent',
|
||||
borderBottomColor: '#a855f7',
|
||||
borderLeftColor: '#c084fc',
|
||||
borderRadius: '50%',
|
||||
animation: 'spinSlow 1.2s linear infinite reverse',
|
||||
}} />
|
||||
<div style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)',
|
||||
borderRadius: '50%',
|
||||
boxShadow: '0 0 12px rgba(99, 102, 241, 0.6)',
|
||||
}} />
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 16,
|
||||
color: '#4f46e5',
|
||||
fontWeight: 600,
|
||||
letterSpacing: 0.5,
|
||||
}}>加载中</span>
|
||||
<span style={{
|
||||
fontSize: 12,
|
||||
color: '#94a3b8',
|
||||
fontWeight: 400,
|
||||
}}>正在获取任务详情...</span>
|
||||
</div>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
gap: 4,
|
||||
marginTop: 8,
|
||||
}}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 4,
|
||||
height: 4,
|
||||
background: '#a5b4fc',
|
||||
borderRadius: '50%',
|
||||
animation: `spinSlow ${0.8 + i * 0.2}s ease-in-out infinite`,
|
||||
animationDelay: `${i * 0.15}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ flex: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'stretch', gap: '2%', position: 'relative', zIndex: 1, minHeight: 0 }}>
|
||||
|
||||
@@ -1355,7 +1457,7 @@ function InitialInfo() {
|
||||
fontWeight: 500,
|
||||
boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)'
|
||||
}}
|
||||
onClick={() => {
|
||||
onClick={() => {
|
||||
if ((user?.credits || 0) < estimatedCredits) {
|
||||
message.warning('积分不足,请更换参数/充值积分');
|
||||
return;
|
||||
@@ -1366,7 +1468,7 @@ function InitialInfo() {
|
||||
>
|
||||
下一步:生成视频提示词
|
||||
<span style={{ color: '#fff', marginLeft: 8 }}>
|
||||
预估积分:{estimatedCredits}
|
||||
视频生成预估积分:{estimatedCredits}
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -1381,12 +1483,13 @@ function InitialInfo() {
|
||||
视频提词已生成,可点击按钮查看/修改
|
||||
</Text>
|
||||
</div>
|
||||
<Space style={{ marginTop: 16, gap: 12, width: '100%' }}>
|
||||
|
||||
<Space style={{ marginTop: 16, gap: 12, width: '100%', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenModal(taskDetail?.videoGeneration?.promptSchema, 'video', step.id, taskDetail?.videoGeneration?.schemaConfigSnapshot)}
|
||||
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
|
||||
style={{ flex: '0 0 auto', minWidth: 160, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
|
||||
disabled={step.status !== 'completed' || !taskDetail?.videoGeneration?.promptSchema || !taskDetail?.videoGeneration?.schemaConfigSnapshot}
|
||||
>
|
||||
查看/修改视频提词
|
||||
@@ -1394,21 +1497,24 @@ function InitialInfo() {
|
||||
{isV2 && (
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => openRegenerateVideoPrompt(step.id)}
|
||||
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
||||
onClick={() => {
|
||||
setRetryPromptStepId(String(step.id));
|
||||
setRetryPromptModalVisible(true);
|
||||
}}
|
||||
style={{ flex: '0 0 auto', minWidth: 160, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
||||
disabled={step.status === 'processing' || steps[2]?.status === 'processing'}
|
||||
>
|
||||
重新生成视频提词
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => { createvideo(step.id, step.engineId); }}
|
||||
type="primary"
|
||||
style={{ flex: 1, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||
disabled={step.status !== 'completed'}
|
||||
>
|
||||
下一步:生成视频
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => createvideo(step.id, step.engineId)}
|
||||
type="primary"
|
||||
style={{ flex: '1 0 auto', minWidth: 140, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||
disabled={step.status !== 'completed'}
|
||||
>
|
||||
下一步:生成视频
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
@@ -1467,101 +1573,6 @@ function InitialInfo() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
title="重新生成视频提词"
|
||||
open={retryPromptModalVisible}
|
||||
onCancel={() => {
|
||||
if (!retryPromptSubmitting) {
|
||||
setRetryPromptModalVisible(false);
|
||||
setRetryPromptStepId('');
|
||||
}
|
||||
}}
|
||||
onOk={submitRegenerateVideoPrompt}
|
||||
confirmLoading={retryPromptSubmitting}
|
||||
okText="按新参数生成提词"
|
||||
cancelText="取消"
|
||||
width={720}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>视频引擎</Text>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }}>
|
||||
{(enginesele.video || []).map((engine: any) => (
|
||||
<button
|
||||
key={engine.id}
|
||||
type="button"
|
||||
onClick={() => applyRetryVideoEngine(String(engine.id), {
|
||||
duration: videoDuration,
|
||||
aspectRatio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
})}
|
||||
style={{
|
||||
minHeight: 46,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
border: countType === String(engine.id) ? '2px solid #6366f1' : '1px solid #e5e7eb',
|
||||
background: countType === String(engine.id) ? 'rgba(99,102,241,0.08)' : '#fff',
|
||||
color: countType === String(engine.id) ? '#4f46e5' : '#374151',
|
||||
cursor: 'pointer',
|
||||
textAlign: 'left',
|
||||
fontWeight: countType === String(engine.id) ? 600 : 400,
|
||||
}}
|
||||
>
|
||||
{engine.name || engine.id}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>视频时长</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.durations.map((duration) => (
|
||||
<Button
|
||||
key={duration}
|
||||
type={videoDuration === duration ? 'primary' : 'default'}
|
||||
onClick={() => setVideoDuration(duration)}
|
||||
>
|
||||
{duration} 秒
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>画面比例</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.ratios.map((ratio) => (
|
||||
<Button
|
||||
key={ratio}
|
||||
type={videoAspectRatio === ratio ? 'primary' : 'default'}
|
||||
onClick={() => setVideoAspectRatio(ratio)}
|
||||
>
|
||||
{ratio}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>分辨率</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.resolutions.map((resolution) => (
|
||||
<Button
|
||||
key={resolution}
|
||||
type={videoResolution === resolution ? 'primary' : 'default'}
|
||||
onClick={() => setVideoResolution(resolution)}
|
||||
>
|
||||
{resolution}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ padding: '10px 12px', borderRadius: 8, background: '#f8fafc', color: '#64748b' }}>
|
||||
当前参数:{enginesele.video?.find((engine: any) => String(engine.id) === countType)?.name || countType}
|
||||
{' · '}{videoDuration} 秒 · {videoAspectRatio} · {videoResolution}
|
||||
<span style={{ marginLeft: 12 }}>最终视频预估积分:{estimatedCredits}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||
@@ -1775,7 +1786,10 @@ function InitialInfo() {
|
||||
|
||||
render: (_, record) => (
|
||||
<button
|
||||
onClick={() => navigate(`/initial/${record.id}/initialinfo?flow_version=${record.flowVersion === 'v2' ? 'v2' : 'v1'}`)}
|
||||
onClick={() => {
|
||||
setIsModalOpen(false);
|
||||
navigate(`/initial/${record.id}/initialinfo?flow_version=${record.flowVersion === 'v2' ? 'v2' : 'v1'}`);
|
||||
}}
|
||||
style={{ color: '#6366f1', textDecoration: 'none', fontSize: 12, border: 'none', background: 'rgba(99, 102, 241, 0.08)', padding: '4px 12px', borderRadius: 8, cursor: 'pointer', transition: 'all 0.2s', fontWeight: 500 }}
|
||||
>
|
||||
查看详情
|
||||
@@ -1854,6 +1868,265 @@ function InitialInfo() {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={retryPromptModalVisible}
|
||||
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, color: '#6366f1', letterSpacing: 0.4 }}>
|
||||
重新生成视频提词
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
onCancel={() => setRetryPromptModalVisible(false)}
|
||||
width={400}
|
||||
footer={null}
|
||||
centered
|
||||
style={{ borderRadius: 16 }}
|
||||
styles={{
|
||||
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择引擎
|
||||
</span>
|
||||
<Select
|
||||
value={countType}
|
||||
onChange={(value) => {
|
||||
const engine = enginesele.video?.find((e: any) => e.id === value);
|
||||
if (engine) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%', borderRadius: 8 }}
|
||||
options={enginesele.video?.map((engine: any) => ({
|
||||
value: engine.id,
|
||||
label: engine.name,
|
||||
}))}
|
||||
placeholder="请选择引擎"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择比例
|
||||
</span>
|
||||
<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 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择时长
|
||||
</span>
|
||||
<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 style={{ marginBottom: 16 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择分辨率
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{engineOptions.resolutions.map((resolution) => (
|
||||
<button
|
||||
key={resolution}
|
||||
onClick={() => setVideoResolution(resolution)}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 42,
|
||||
borderRadius: 8,
|
||||
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 style={{ display: 'flex', gap: 12, marginTop: 16 }}>
|
||||
<Button
|
||||
onClick={() => setRetryPromptModalVisible(false)}
|
||||
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!countType) {
|
||||
message.warning('请选择视频引擎');
|
||||
return;
|
||||
}
|
||||
submitRegenerateVideoPrompt(retryPromptStepId);
|
||||
setRetryPromptModalVisible(false);
|
||||
}}
|
||||
type="primary"
|
||||
style={{ flex: 1, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||
disabled={!countType}
|
||||
>
|
||||
确认生成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,17 +12,21 @@ import {
|
||||
Pagination,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Tooltip
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
VideoCameraOutlined,
|
||||
PictureOutlined,
|
||||
LoadingOutlined,
|
||||
SettingOutlined,
|
||||
LayoutOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { uploadHotOpeningVideo, uploadHotOpeningImage, generateReplication, getReplicationList, deleteHotOpeningReplicationTask, getEngine, calculateCredits } from '../api';
|
||||
import bg1 from '../assets/bg1.png';
|
||||
import UploadSelector from '../components/UploadSelector';
|
||||
import { useAuthStore } from '../store/useAuthStore';
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +51,7 @@ const buildAssetUrl = (url?: string): string => {
|
||||
|
||||
const GenerateConver: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const [tableData, setTableData] = useState<any[]>(
|
||||
[]
|
||||
@@ -84,6 +89,17 @@ const GenerateConver: React.FC = () => {
|
||||
const [videoDuration, setVideoDuration] = useState(5);
|
||||
const [videoAspectRatio, setVideoAspectRatio] = useState('16:9');
|
||||
const [videoResolution, setVideoResolution] = useState('480p');
|
||||
const [showEngineModal, setShowEngineModal] = useState(false);
|
||||
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
|
||||
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 [creditRules, setCreditRules] = useState<any[]>([]);
|
||||
|
||||
// 弹窗状态
|
||||
@@ -465,12 +481,13 @@ const GenerateConver: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
|
||||
<div className="replication-preview" style={{ width: '70%', background: '#f1f2f3', backdropFilter: 'blur(20px)', display: 'flex', flexDirection: 'column', borderRadius: 20, overflow: 'hidden',
|
||||
// border: '1px solid rgba(99, 102, 241, 0.1)',
|
||||
<div className="replication-preview" style={{
|
||||
width: '68%', background: '#f1f2f3', 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,
|
||||
|
||||
}}>
|
||||
position: 'relative', zIndex: 10,
|
||||
|
||||
}}>
|
||||
|
||||
|
||||
|
||||
@@ -531,7 +548,7 @@ const GenerateConver: React.FC = () => {
|
||||
flex: 1,
|
||||
background: 'transparent',
|
||||
borderRight: '1px solid rgba(99, 102, 241, 0.08)', overflowY: 'auto',
|
||||
scrollbarWidth: 'none',
|
||||
scrollbarWidth: 'none',
|
||||
|
||||
}}>
|
||||
{cardData.length > 0 ? (
|
||||
@@ -852,12 +869,13 @@ const GenerateConver: React.FC = () => {
|
||||
|
||||
|
||||
<div className="replication-form" style={{
|
||||
flex: 1, overflow: 'auto', width: '28%',
|
||||
overflow: 'auto', width: '30%',
|
||||
background: 'transparent',
|
||||
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
|
||||
position: 'relative', zIndex: 10,
|
||||
scrollbarWidth: 'none',
|
||||
}}>
|
||||
{/* 右侧表单区域 */}
|
||||
<div className="replication-form-content" style={{
|
||||
@@ -1179,35 +1197,375 @@ const GenerateConver: React.FC = () => {
|
||||
|
||||
<div style={{ marginBottom: 20, display: 'grid', gap: 10 }}>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 600, color: '#475569' }}>视频生成参数(必选)</p>
|
||||
<Select value={engineId || undefined} placeholder="选择视频引擎" onChange={(value) => {
|
||||
setEngineId(value);
|
||||
const engine = videoEngines.find((item: any) => item.id === value);
|
||||
setVideoAspectRatio(engine?.supportedRatios?.[0] || '16:9');
|
||||
setVideoResolution(engine?.supportedResolutions?.[0] || '480p');
|
||||
setVideoDuration(engine?.supportedDurations?.[0] || 5);
|
||||
}} options={videoEngines.map((item: any) => ({
|
||||
value: item.id,
|
||||
label: imageUrl && !supportsReferenceImage(item) ? `${item.name}(不支持参考图)` : item.name,
|
||||
disabled: Boolean(imageUrl) && !supportsReferenceImage(item),
|
||||
}))} />
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}>
|
||||
<Select value={videoDuration} onChange={setVideoDuration} options={(selectedEngine?.supportedDurations || [5]).map((value: number) => ({ value, label: `${value}秒` }))} />
|
||||
<Select value={videoAspectRatio} onChange={setVideoAspectRatio} options={(selectedEngine?.supportedRatios || ['16:9']).map((value: string) => ({ value, label: value }))} />
|
||||
<Select value={videoResolution} onChange={setVideoResolution} options={(selectedEngine?.supportedResolutions || ['480p']).map((value: string) => ({ value, label: value }))} />
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowEngineModal(!showEngineModal);
|
||||
setShowVideoSettingsModal(false);
|
||||
}}
|
||||
className="image-settings-trigger"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px 12px',
|
||||
height: 34,
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(99, 102, 241, 0.15)',
|
||||
backgroundColor: 'rgba(255,255,255,0.7)',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<SettingOutlined style={{ fontSize: 14, color: '#6366f1' }} />
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
color: '#64748b',
|
||||
}}>
|
||||
{videoEngines.find((e: any) => e.id === engineId)?.name || '选择引擎'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showEngineModal && (
|
||||
<div
|
||||
className="image-settings-popover"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: -10,
|
||||
width: 350,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||||
padding: 16,
|
||||
border: 'none',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择引擎
|
||||
</span>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
}}>
|
||||
{videoEngines.filter((item: any) => item.supportsUniversalReference !== false).map((engine: any) => (
|
||||
<button
|
||||
key={engine.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEngineId(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: engineId === engine.id
|
||||
? '2px solid #6366f1'
|
||||
: '1px solid #e5e7eb',
|
||||
backgroundColor: engineId === 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: engineId === engine.id ? 600 : 500,
|
||||
color: engineId === engine.id ? '#6366f1' : '#4b5563',
|
||||
marginBottom: 2,
|
||||
}}>
|
||||
{engine.name}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: 11,
|
||||
color: '#9ca3af',
|
||||
}}>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowVideoSettingsModal(!showVideoSettingsModal);
|
||||
setShowEngineModal(false);
|
||||
}}
|
||||
className="image-settings-trigger"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px 12px',
|
||||
height: 34,
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(99, 102, 241, 0.15)',
|
||||
backgroundColor: 'rgba(255,255,255,0.7)',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<LayoutOutlined style={{ fontSize: 14, color: '#6366f1' }} />
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
color: '#64748b',
|
||||
}}>
|
||||
{videoAspectRatio} · {videoDuration}s · {videoResolution}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showVideoSettingsModal && (
|
||||
<div
|
||||
className="image-settings-popover"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: -160,
|
||||
width: 350,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||||
padding: 16,
|
||||
border: 'none',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择比例
|
||||
</span>
|
||||
<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 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择时长
|
||||
</span>
|
||||
<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>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择分辨率
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{engineOptions.resolutions.map((resolution) => (
|
||||
<button
|
||||
key={resolution}
|
||||
onClick={() => setVideoResolution(resolution)}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 42,
|
||||
borderRadius: 8,
|
||||
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>
|
||||
<span style={{ fontSize: 12, color: selectedEngineSupportsImage ? '#8b5cf6' : '#ef4444' }}>
|
||||
{selectedEngineSupportsImage
|
||||
? `预估视频积分:${estimatedCredits || '-'};素材图片可不传,最终生成只会携带图片,不会携带参考视频。`
|
||||
: '当前引擎不支持参考图片,请移除素材图片或切换引擎。'}
|
||||
</span>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 立即生成按钮 */}
|
||||
<Button
|
||||
<Tooltip
|
||||
title={((user?.credits || 0) < estimatedCredits) ? '积分不足,请更换参数/充值积分' : ''}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
onClick={handleGenerate}
|
||||
onClick={() => {
|
||||
if ((user?.credits || 0) < estimatedCredits) {
|
||||
message.warning('积分不足,请更换参数/充值积分');
|
||||
return;
|
||||
}
|
||||
handleGenerate();
|
||||
}}
|
||||
disabled={!selectedEngineSupportsImage}
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
@@ -1221,7 +1579,13 @@ const GenerateConver: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
立即生成+
|
||||
<span style={{ color: '#fff', marginLeft: 8, fontSize: 13 }}>
|
||||
预估积分:{estimatedCredits}
|
||||
</span>
|
||||
</Button>
|
||||
|
||||
</Tooltip>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Button, Drawer, Input, Modal, Popconfirm, Select, Spin, Table, Tag, Tooltip, message } from 'antd';
|
||||
import { ArrowLeftOutlined, DeleteOutlined, PlusOutlined, XOutlined } from '@ant-design/icons';
|
||||
import { ArrowLeftOutlined, DeleteOutlined, PlusOutlined, XOutlined, SettingOutlined, LayoutOutlined } from '@ant-design/icons';
|
||||
|
||||
import { uploadShotReplicateImage, createRemoveLens, deleteSegment, getShotReplicationDetail, reanalyzeSegment, retrySplit, Removelist, removeCreate, reanalyzeShotReplication, splitCustom, getEngine, calculateCredits } from '../api';
|
||||
|
||||
@@ -48,6 +48,17 @@ function RemoveInfo() {
|
||||
const [videoDuration, setVideoDuration] = useState(5);
|
||||
const [videoAspectRatio, setVideoAspectRatio] = useState('16:9');
|
||||
const [videoResolution, setVideoResolution] = useState('480p');
|
||||
const [showEngineModal, setShowEngineModal] = useState(false);
|
||||
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
|
||||
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 [creditRules, setCreditRules] = useState<any[]>([]);
|
||||
const [taskDetail, setTaskDetail] = useState<any>(null);
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
@@ -1058,7 +1069,9 @@ function RemoveInfo() {
|
||||
}
|
||||
placement="top"
|
||||
trigger="hover"
|
||||
overlayInnerStyle={{ backgroundColor: '#fff', height: 300, overflowY: 'auto', border: '1px solid rgba(99, 102, 241, 0.1)', borderRadius: 12, boxShadow: '0 8px 24px rgba(99, 102, 241, 0.12)' }}
|
||||
styles={{
|
||||
container: { backgroundColor: '#fff', height: 300, overflowY: 'auto', border: '1px solid rgba(99, 102, 241, 0.1)', borderRadius: 12, boxShadow: '0 8px 24px rgba(99, 102, 241, 0.12)' }
|
||||
}}
|
||||
>
|
||||
<span style={{ marginLeft: 8, fontSize: 13, opacity: 0.9 }}>(AI 镜头拆分方案)</span>
|
||||
</Tooltip>
|
||||
@@ -1251,25 +1264,359 @@ function RemoveInfo() {
|
||||
|
||||
<div style={{ background: '#fff', borderRadius: 12, padding: 16, border: '1px solid rgba(99, 102, 241, 0.1)' }}>
|
||||
<label style={{ fontWeight: 500, color: '#334155', marginBottom: 8, display: 'block', fontSize: 14 }}>视频参数</label>
|
||||
<Select
|
||||
value={engineId || undefined}
|
||||
onChange={handleEngineChange}
|
||||
placeholder="选择视频引擎"
|
||||
style={{ width: '100%', marginBottom: 12 }}
|
||||
options={videoEngines.map((item: any) => ({
|
||||
value: item.id,
|
||||
label: productImage && !supportsReferenceImage(item) ? `${item.name}(不支持参考图)` : item.name,
|
||||
disabled: Boolean(productImage) && !supportsReferenceImage(item),
|
||||
}))}
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||
<Select value={videoDuration} onChange={setVideoDuration} style={{ flex: 1 }} options={(selectedEngine?.supportedDurations || [5]).map((item: number) => ({ value: item, label: `${item}秒` }))} />
|
||||
<Select value={videoAspectRatio} onChange={setVideoAspectRatio} style={{ flex: 1 }} options={(selectedEngine?.supportedRatios || ['16:9']).map((item: string) => ({ value: item, label: item }))} />
|
||||
<Select value={videoResolution} onChange={setVideoResolution} style={{ flex: 1 }} options={(selectedEngine?.supportedResolutions || ['480p']).map((item: string) => ({ value: item, label: item }))} />
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowEngineModal(!showEngineModal);
|
||||
setShowVideoSettingsModal(false);
|
||||
}}
|
||||
className="image-settings-trigger"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px 12px',
|
||||
height: 34,
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(99, 102, 241, 0.15)',
|
||||
backgroundColor: 'rgba(255,255,255,0.7)',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<SettingOutlined style={{ fontSize: 14, color: '#6366f1' }} />
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
color: '#64748b',
|
||||
}}>
|
||||
{videoEngines.find((e: any) => e.id === engineId)?.name || '选择引擎'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showEngineModal && (
|
||||
<div
|
||||
className="image-settings-popover"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: -10,
|
||||
width: 350,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||||
padding: 16,
|
||||
border: 'none',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择引擎
|
||||
</span>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
}}>
|
||||
{videoEngines.filter((item: any) => item.supportsUniversalReference !== false).map((engine: any) => (
|
||||
<button
|
||||
key={engine.id}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEngineId(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: engineId === engine.id
|
||||
? '2px solid #6366f1'
|
||||
: '1px solid #e5e7eb',
|
||||
backgroundColor: engineId === 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: engineId === engine.id ? 600 : 500,
|
||||
color: engineId === engine.id ? '#6366f1' : '#4b5563',
|
||||
marginBottom: 2,
|
||||
}}>
|
||||
{engine.name}
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: 11,
|
||||
color: '#9ca3af',
|
||||
}}>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowVideoSettingsModal(!showVideoSettingsModal);
|
||||
setShowEngineModal(false);
|
||||
}}
|
||||
className="image-settings-trigger"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '4px 12px',
|
||||
height: 34,
|
||||
borderRadius: 10,
|
||||
border: '1px solid rgba(99, 102, 241, 0.15)',
|
||||
backgroundColor: 'rgba(255,255,255,0.7)',
|
||||
cursor: 'pointer',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<LayoutOutlined style={{ fontSize: 14, color: '#6366f1' }} />
|
||||
<span style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
color: '#64748b',
|
||||
}}>
|
||||
{videoAspectRatio} · {videoDuration}s · {videoResolution}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{showVideoSettingsModal && (
|
||||
<div
|
||||
className="image-settings-popover"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 'calc(100% + 8px)',
|
||||
left: -160,
|
||||
width: 350,
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 16,
|
||||
boxShadow: '0 10px 40px rgba(0,0,0,0.15)',
|
||||
padding: 16,
|
||||
border: 'none',
|
||||
zIndex: 9999,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择比例
|
||||
</span>
|
||||
<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 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择时长
|
||||
</span>
|
||||
<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>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择分辨率
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{engineOptions.resolutions.map((resolution) => (
|
||||
<button
|
||||
key={resolution}
|
||||
onClick={() => setVideoResolution(resolution)}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 42,
|
||||
borderRadius: 8,
|
||||
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>
|
||||
<div style={{ color: creditsInsufficient || !selectedEngineSupportsImage ? '#ef4444' : '#6366f1', fontSize: 13 }}>
|
||||
<div style={{ color: !selectedEngineSupportsImage ? '#ef4444' : '#6366f1', fontSize: 13 }}>
|
||||
{selectedEngineSupportsImage
|
||||
? `预估积分:${estimatedCredits},当前积分:${Number(user?.credits || 0).toFixed(2)}`
|
||||
? ''
|
||||
: '当前引擎不支持参考图片,请移除素材图片或切换引擎。'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1277,30 +1624,31 @@ function RemoveInfo() {
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 8 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleManualGenerate}
|
||||
onClick={() => {
|
||||
if ((user?.credits || 0) < estimatedCredits) {
|
||||
message.warning('积分不足,请更换参数/充值积分');
|
||||
return;
|
||||
}
|
||||
handleManualGenerate();
|
||||
}}
|
||||
loading={loading}
|
||||
disabled={loading || creditsInsufficient || !engineId || !selectedEngineSupportsImage}
|
||||
disabled={loading || !engineId || !selectedEngineSupportsImage || ((user?.credits || 0) < estimatedCredits)}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 48,
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
border: 'none',
|
||||
boxShadow: '0 4px 15px rgba(99, 102, 241, 0.3)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.boxShadow = '0 6px 20px rgba(99, 102, 241, 0.4)';
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.boxShadow = '0 4px 15px rgba(99, 102, 241, 0.3)';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #a855f7 100%)',
|
||||
boxShadow: '0 4px 16px rgba(99, 102, 241, 0.3)',
|
||||
transition: 'all 0.25s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
>
|
||||
{loading ? '生成中...' : '手动生成'}
|
||||
<span style={{ color: '#fff', marginLeft: 8, fontSize: 13 }}>
|
||||
预估积分:{estimatedCredits}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -78,8 +78,8 @@ export default function VideoFrameExtractor() {
|
||||
video.src = URL.createObjectURL(file);
|
||||
});
|
||||
|
||||
if (duration > 60) {
|
||||
setError('视频时长不能超过 1分钟');
|
||||
if (duration > 190) {
|
||||
setError('视频时长不能超过 3分钟');
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ export default function VideoFrameExtractor() {
|
||||
点击或拖拽到此上传视频文件
|
||||
</p>
|
||||
<p style={{ fontSize: 13, color: '#64748b', margin: 0 }}>
|
||||
支持 MP4、MOV 格式视频,最大支持 100MB 的文件,支持上传 1 分钟内的视频
|
||||
支持 MP4、MOV 格式视频,最大支持 100MB 的文件,支持上传 3 分钟内的视频
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
@@ -479,7 +479,7 @@ export default function VideoFrameExtractor() {
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (videoUrl && productName.trim() && !loading) {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.transform = 'traslnateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 6px 20px rgba(99, 102, 241, 0.3)';
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { Button, Typography, Collapse, Space, Modal, Input, Table, message, Tooltip } from 'antd';
|
||||
import { Button, Typography, Collapse, Space, Modal, Input, Table, message, Tooltip, Select } from 'antd';
|
||||
import { ArrowLeftOutlined, PlayCircleOutlined, CheckCircleOutlined, EditOutlined, DownloadOutlined, SettingOutlined, LayoutOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { getShotReplicationList, removeDetail, removeone, removetwo, removethree, removefour, getEngine, updateShotImagePrompt, updateShotVideoPromptSchema, calculateCredits, retryShotVideoPromptV2, updateShotVideoPromptSchemaV2, generateShotVideoV2 } from '../api/index';
|
||||
@@ -629,11 +629,11 @@ function InitialInfo() {
|
||||
setRetryPromptModalVisible(true);
|
||||
};
|
||||
|
||||
const submitRegenerateVideoPrompt = async () => {
|
||||
if (!isV2 || !taskDetail?.id || !retryPromptStepId || !countType) return;
|
||||
const submitRegenerateVideoPrompt = async (stepId: string) => {
|
||||
if (!isV2 || !taskDetail?.id || !stepId || !countType) return;
|
||||
setRetryPromptSubmitting(true);
|
||||
try {
|
||||
await retryShotVideoPromptV2(taskDetail.id, retryPromptStepId, {
|
||||
await retryShotVideoPromptV2(taskDetail.id, stepId, {
|
||||
video_config: {
|
||||
engine_id: countType,
|
||||
duration: videoDuration,
|
||||
@@ -642,8 +642,6 @@ function InitialInfo() {
|
||||
},
|
||||
});
|
||||
message.info('正在按新视频参数重新生成视频提示词,请稍候...');
|
||||
setRetryPromptModalVisible(false);
|
||||
setRetryPromptStepId('');
|
||||
refreshTaskDetail();
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || '重新生成视频提示词失败');
|
||||
@@ -1357,7 +1355,7 @@ function InitialInfo() {
|
||||
>
|
||||
下一步:生成视频提示词
|
||||
<span style={{ color: '#fff', marginLeft: 8 }}>
|
||||
预估积分:{estimatedCredits}
|
||||
视频生成预估积分:{estimatedCredits}
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
@@ -1371,24 +1369,40 @@ function InitialInfo() {
|
||||
视频提词已生成,可点击按钮查看/修改
|
||||
</Text>
|
||||
</div>
|
||||
<Space style={{ marginTop: 16, gap: 12, width: '100%' }}>
|
||||
|
||||
|
||||
|
||||
<Space style={{ marginTop: 16, gap: 12, width: '100%', flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleOpenModal(taskDetail?.videoGeneration?.promptSchema, 'video', step.id, taskDetail?.videoGeneration?.schemaConfigSnapshot)}
|
||||
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
|
||||
style={{ flex: '0 0 auto', minWidth: 160, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500, background: 'rgba(99, 102, 241, 0.04)' }}
|
||||
disabled={step.status !== 'completed' || !taskDetail?.videoGeneration?.promptSchema || !taskDetail?.videoGeneration?.schemaConfigSnapshot}
|
||||
>
|
||||
查看/修改视频提词
|
||||
</Button>
|
||||
{isV2 && (
|
||||
<Button type="default" onClick={() => openRegenerateVideoPrompt(step.id)} style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }} disabled={step.status === 'processing' || steps[2]?.status === 'processing'}>
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
setRetryPromptStepId(String(step.id));
|
||||
setRetryPromptModalVisible(true);
|
||||
}}
|
||||
style={{ flex: '0 0 auto', minWidth: 160, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
||||
disabled={step.status === 'processing' || steps[2]?.status === 'processing'}
|
||||
>
|
||||
重新生成视频提词
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => { createvideo(step.id, step.engineId); }} type="primary" style={{ flex: 1, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }} disabled={step.status !== 'completed'}>
|
||||
下一步:生成视频
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => createvideo(step.id, step.engineId)}
|
||||
type="primary"
|
||||
style={{ flex: '1 0 auto', minWidth: 140, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||
disabled={step.status !== 'completed'}
|
||||
>
|
||||
下一步:生成视频
|
||||
</Button>
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
@@ -1442,85 +1456,260 @@ function InitialInfo() {
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
title="重新生成视频提词"
|
||||
open={retryPromptModalVisible}
|
||||
onCancel={() => {
|
||||
if (!retryPromptSubmitting) {
|
||||
setRetryPromptModalVisible(false);
|
||||
setRetryPromptStepId('');
|
||||
}
|
||||
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, color: '#6366f1', letterSpacing: 0.4 }}>
|
||||
重新生成视频提词
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
onCancel={() => setRetryPromptModalVisible(false)}
|
||||
width={400}
|
||||
footer={null}
|
||||
centered
|
||||
style={{ borderRadius: 16 }}
|
||||
styles={{
|
||||
header: { background: 'rgba(255,255,255,0.6)', backdropFilter: 'blur(10px)', borderBottom: '1px solid rgba(99, 102, 241, 0.08)', padding: '16px 24px' },
|
||||
}}
|
||||
onOk={submitRegenerateVideoPrompt}
|
||||
confirmLoading={retryPromptSubmitting}
|
||||
okText="按新参数生成提词"
|
||||
cancelText="取消"
|
||||
width={720}
|
||||
destroyOnClose
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>视频引擎</Text>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }}>
|
||||
{(enginesele.video || []).map((engine: any) => (
|
||||
<div style={{ padding: '16px 0' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择引擎
|
||||
</span>
|
||||
<Select
|
||||
value={countType}
|
||||
onChange={(value) => {
|
||||
const engine = enginesele.video?.find((e: any) => e.id === value);
|
||||
if (engine) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
style={{ width: '100%', borderRadius: 8 }}
|
||||
options={enginesele.video?.map((engine: any) => ({
|
||||
value: engine.id,
|
||||
label: engine.name,
|
||||
}))}
|
||||
placeholder="请选择引擎"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择比例
|
||||
</span>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: 4,
|
||||
}}>
|
||||
{engineOptions.ratios.map((ratio) => (
|
||||
<button
|
||||
key={engine.id}
|
||||
type="button"
|
||||
onClick={() => applyRetryVideoEngine(String(engine.id), {
|
||||
duration: videoDuration,
|
||||
aspectRatio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
})}
|
||||
key={ratio}
|
||||
onClick={() => setVideoAspectRatio(ratio)}
|
||||
style={{
|
||||
minHeight: 46,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
border: countType === String(engine.id) ? '2px solid #6366f1' : '1px solid #e5e7eb',
|
||||
background: countType === String(engine.id) ? 'rgba(99,102,241,0.08)' : '#fff',
|
||||
color: countType === String(engine.id) ? '#4f46e5' : '#374151',
|
||||
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',
|
||||
textAlign: 'left',
|
||||
fontWeight: countType === String(engine.id) ? 600 : 400,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
{engine.name || engine.id}
|
||||
<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>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>视频时长</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.durations.map((duration) => (
|
||||
<Button key={duration} type={videoDuration === duration ? 'primary' : 'default'} onClick={() => setVideoDuration(duration)}>
|
||||
{duration} 秒
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择时长
|
||||
</span>
|
||||
<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 strong style={{ display: 'block', marginBottom: 10 }}>画面比例</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{engineOptions.ratios.map((ratio) => (
|
||||
<Button key={ratio} type={videoAspectRatio === ratio ? 'primary' : 'default'} onClick={() => setVideoAspectRatio(ratio)}>
|
||||
{ratio}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10 }}>分辨率</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{
|
||||
display: 'block',
|
||||
marginBottom: 8,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
color: '#666666',
|
||||
}}>
|
||||
选择分辨率
|
||||
</span>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
{engineOptions.resolutions.map((resolution) => (
|
||||
<Button key={resolution} type={videoResolution === resolution ? 'primary' : 'default'} onClick={() => setVideoResolution(resolution)}>
|
||||
{resolution}
|
||||
</Button>
|
||||
<button
|
||||
key={resolution}
|
||||
onClick={() => setVideoResolution(resolution)}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 42,
|
||||
borderRadius: 8,
|
||||
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 style={{ padding: '10px 12px', borderRadius: 8, background: '#f8fafc', color: '#64748b' }}>
|
||||
当前参数:{enginesele.video?.find((engine: any) => String(engine.id) === countType)?.name || countType}
|
||||
{' · '}{videoDuration} 秒 · {videoAspectRatio} · {videoResolution}
|
||||
<span style={{ marginLeft: 12 }}>最终视频预估积分:{estimatedCredits}</span>
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, marginTop: 16 }}>
|
||||
<Button
|
||||
onClick={() => setRetryPromptModalVisible(false)}
|
||||
style={{ flex: 1, borderRadius: 10, borderColor: 'rgba(99, 102, 241, 0.3)', color: '#6366f1', height: 36, fontWeight: 500 }}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!countType) {
|
||||
message.warning('请选择视频引擎');
|
||||
return;
|
||||
}
|
||||
submitRegenerateVideoPrompt(retryPromptStepId);
|
||||
setRetryPromptModalVisible(false);
|
||||
}}
|
||||
type="primary"
|
||||
style={{ flex: 1, borderRadius: 10, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', border: 'none', height: 36, fontWeight: 500, boxShadow: '0 4px 12px rgba(99, 102, 241, 0.3)' }}
|
||||
disabled={!countType}
|
||||
>
|
||||
确认生成
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user