爆款,拆镜流程修改

This commit is contained in:
sjy
2026-07-22 14:26:57 +08:00
parent 0aa884304d
commit eec5862465
9 changed files with 2085 additions and 911 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+36 -36
View File
@@ -1,37 +1,37 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>民众智创</title>
<script>
(function() {
var cached = localStorage.getItem('siteInfo');
if (cached) {
try {
var info = JSON.parse(cached);
if (info.siteName) {
document.title = info.siteName;
}
if (info.siteLogo) {
var link = document.querySelector('link[rel="icon"]');
if (link) {
link.href = info.siteLogo;
link.type = 'image/png';
}
}
} catch (e) {}
}
})();
</script>
<script type="module" crossorigin src="/assets/index-DZg1zviY.js"></script>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet" />
<title>民众智创</title>
<script>
(function() {
var cached = localStorage.getItem('siteInfo');
if (cached) {
try {
var info = JSON.parse(cached);
if (info.siteName) {
document.title = info.siteName;
}
if (info.siteLogo) {
var link = document.querySelector('link[rel="icon"]');
if (link) {
link.href = info.siteLogo;
link.type = 'image/png';
}
}
} catch (e) {}
}
})();
</script>
<script type="module" crossorigin src="/assets/index-Dej_BHGv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
</head>
<body>
<div id="root"></div>
</body>
</html>
</head>
<body>
<div id="root"></div>
</body>
</html>
+1 -1
View File
@@ -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
+392 -119
View File
@@ -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>
);
}
+394 -30
View File
@@ -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>
+382 -34
View File
@@ -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>
+4 -4
View File
@@ -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 }}>
MP4MOV 100MB 1
MP4MOV 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)';
}
}}
+263 -74
View File
@@ -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>