本地修改
This commit is contained in:
+88
-88
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -28,8 +28,8 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-CVSmzRXM.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Bsz_Xon-.css">
|
||||
<script type="module" crossorigin src="/assets/index-Bkv0KkOY.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-IrbjYl3V.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -674,8 +674,11 @@ export async function splitCustom(taskSetId: string, params: any): Promise<any>
|
||||
return api.post(`/shot-replications/task-sets/${taskSetId}/split-custom`, params);
|
||||
}
|
||||
// 拆镜列表
|
||||
export async function Removelist(taskSetId: string): Promise<any> {
|
||||
return api.get(`/shot-replications/task-sets/${taskSetId}/segments`);
|
||||
export async function Removelist(taskSetId: string, page: number = 1, pageSize: number = 20): Promise<any> {
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', String(page));
|
||||
params.set('page_size', String(pageSize));
|
||||
return api.get(`/shot-replications/task-sets/${taskSetId}/segments?${params.toString()}`);
|
||||
}
|
||||
// 生成视频
|
||||
export async function removeCreate(recordId: string, params: any): Promise<any> {
|
||||
|
||||
@@ -241,7 +241,7 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
||||
onHistorySelect?.(items);
|
||||
setHistoryModalVisible(false);
|
||||
}}
|
||||
allowedTypes={accept.includes('audio') ? ['image', 'video', 'audio'] : accept.includes('video') ? ['image', 'video'] : ['image']}
|
||||
allowedTypes={accept.includes('audio') ? ['image', 'video', 'audio'] : ['image', 'video']}
|
||||
maxImageCount={maxImageCount}
|
||||
maxVideoCount={maxVideoCount}
|
||||
usedImageCount={usedImageCount}
|
||||
@@ -253,6 +253,7 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
|
||||
maxAudioDuration={maxAudioDuration}
|
||||
usedAudioDuration={usedAudioDuration}
|
||||
hideLimitHint={hideLimitHint}
|
||||
defaultResourceType={mediaType === 'image' ? 'image' : ''}
|
||||
/>
|
||||
|
||||
<PrivatePortraitAssetPicker
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Alert, Input, Radio, Space, Tag, Typography } from 'antd';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Alert, Input, Menu, Radio, Space, Tag, Typography } from 'antd';
|
||||
import type {
|
||||
JsonValue,
|
||||
VideoPromptSchemaFieldConfig,
|
||||
@@ -31,9 +31,9 @@ function isRecord(value: unknown): value is Record<string, JsonValue> {
|
||||
function ReadonlyBlock({ value }: { value: JsonValue }) {
|
||||
const text = value === 'true' ? '是' : value === 'false' ? '否' : stringifySchemaValue(value);
|
||||
return shouldUseSchemaTextArea(value) ? (
|
||||
<TextArea value={text} rows={Math.min(6, Math.max(2, Math.ceil(text.length / 42)))} disabled style={{ borderRadius: 8, color: '#64748b' }} />
|
||||
<TextArea value={text} rows={Math.min(6, Math.max(2, Math.ceil(text.length / 42)))} disabled style={{ borderRadius: 10, color: '#64748b', border: '1px solid #e2e8f0', background: '#f8fafc' }} />
|
||||
) : (
|
||||
<Input value={text} disabled style={{ borderRadius: 8, color: '#64748b' }} />
|
||||
<Input value={text} disabled style={{ borderRadius: 10, color: '#64748b', border: '1px solid #e2e8f0', background: '#f8fafc' }} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ function EditableInput({ value, maxLength, onChange }: { value: JsonValue; maxLe
|
||||
maxLength,
|
||||
showCount: !!maxLength,
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => onChange(event.target.value),
|
||||
style: { borderRadius: 8 },
|
||||
style: { borderRadius: 10, border: '1px solid #e2e8f0', transition: 'all 0.2s ease' },
|
||||
};
|
||||
|
||||
if (shouldUseSchemaTextArea(value)) {
|
||||
@@ -68,13 +68,78 @@ function BooleanRadio({ value, onChange }: { value: JsonValue; onChange: (nextVa
|
||||
);
|
||||
}
|
||||
|
||||
// function renderFieldTag(field: VideoPromptSchemaFieldConfig) {
|
||||
// return field.editable ? <Tag color="processing">可编辑</Tag> : <Tag color="default">只读</Tag>;
|
||||
// }
|
||||
|
||||
const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value, schemaConfigSnapshot, onChange }) => {
|
||||
const safeValue = isRecord(value) ? value : {};
|
||||
const sections = normalizeSchemaSections(schemaConfigSnapshot);
|
||||
const [selectedKey, setSelectedKey] = useState<string>('');
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMenuSelect = (info: { key: string }) => {
|
||||
setSelectedKey(info.key);
|
||||
const element = document.getElementById(`section-${info.key}`);
|
||||
if (element && contentRef.current) {
|
||||
const containerRect = contentRef.current.getBoundingClientRect();
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const scrollTop = elementRect.top - containerRect.top + contentRef.current.scrollTop;
|
||||
contentRef.current.scrollTo({ top: scrollTop, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const validSections = sections.filter((section) => section.key in safeValue);
|
||||
if (validSections.length > 0 && !selectedKey) {
|
||||
setSelectedKey(validSections[0].key);
|
||||
}
|
||||
}, [sections, safeValue, selectedKey]);
|
||||
|
||||
// 监听滚动,自动更新菜单选中状态
|
||||
useEffect(() => {
|
||||
const container = contentRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const handleScroll = () => {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const halfHeight = containerRect.height / 2;
|
||||
|
||||
let currentSectionKey = '';
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const section of sections) {
|
||||
if (!(section.key in safeValue)) continue;
|
||||
const element = document.getElementById(`section-${section.key}`);
|
||||
if (!element) continue;
|
||||
|
||||
const elementRect = element.getBoundingClientRect();
|
||||
const elementTop = elementRect.top;
|
||||
const elementBottom = elementRect.bottom;
|
||||
|
||||
// 检查元素是否在可视区域内
|
||||
const isVisible = elementBottom > containerRect.top && elementTop < containerRect.bottom;
|
||||
|
||||
if (isVisible) {
|
||||
// 计算元素中心点相对于容器中心点的距离
|
||||
const elementCenter = (elementTop + elementBottom) / 2;
|
||||
const containerCenter = containerRect.top + halfHeight;
|
||||
const distance = Math.abs(elementCenter - containerCenter);
|
||||
|
||||
// 选择最接近容器中心的元素
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
currentSectionKey = section.key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentSectionKey && currentSectionKey !== selectedKey) {
|
||||
setSelectedKey(currentSectionKey);
|
||||
}
|
||||
};
|
||||
|
||||
container.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
container.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [sections, safeValue]);
|
||||
|
||||
if (!hasSchemaConfigSnapshot(schemaConfigSnapshot)) {
|
||||
return (
|
||||
@@ -104,18 +169,17 @@ const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value
|
||||
if (!fields.length) return null;
|
||||
|
||||
return (
|
||||
<div key={section.key} style={{ marginBottom: 18 }}>
|
||||
<Space style={{ marginBottom: 8 }}>
|
||||
<Text strong style={{ color: '#334155', fontSize: 13 }}>{section.label}</Text>
|
||||
{/* {renderFieldTag(section)} */}
|
||||
</Space>
|
||||
<div style={{ borderLeft: '3px solid #6366f1', paddingLeft: 12, marginLeft: 4 }}>
|
||||
<div key={section.key} style={{ marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<div style={{ width: 3, height: 16, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
|
||||
<Text strong style={{ color: '#334155', fontSize: 14, fontWeight: 600 }}>{section.label}</Text>
|
||||
</div>
|
||||
<div style={{ padding: '16px', backgroundColor: '#fff', borderRadius: 12, border: '1px solid #e2e8f0', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
|
||||
{fields.map((field) => (
|
||||
<div key={field.key} style={{ marginBottom: 12 }}>
|
||||
<Space style={{ marginBottom: 4 }}>
|
||||
<div key={field.key} style={{ marginBottom: 16 }}>
|
||||
<div style={{ marginBottom: 6 }}>
|
||||
<Text style={{ color: '#64748b', fontSize: 12 }}>{field.label}</Text>
|
||||
{/* {renderFieldTag(field)} */}
|
||||
</Space>
|
||||
</div>
|
||||
{field.editable ? (
|
||||
field.type === 'boolean' ? (
|
||||
<BooleanRadio
|
||||
@@ -145,26 +209,26 @@ const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value
|
||||
const displayFields = [{ key: '时间段', label: '时间段', enabled: true, editable: false } as VideoPromptSchemaFieldConfig, ...fields];
|
||||
|
||||
return (
|
||||
<div key={section.key} style={{ marginBottom: 18 }}>
|
||||
<Space style={{ marginBottom: 8 }}>
|
||||
<Text strong style={{ color: '#334155', fontSize: 13 }}>{section.label}</Text>
|
||||
<Tag color="default">锁定条目和时间段</Tag>
|
||||
</Space>
|
||||
<div style={{ border: '1px solid #e5e7eb', borderRadius: 10, padding: 12, background: '#fff' }}>
|
||||
<div key={section.key} style={{ marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<div style={{ width: 3, height: 16, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
|
||||
<Text strong style={{ color: '#334155', fontSize: 14, fontWeight: 600 }}>{section.label}</Text>
|
||||
<Tag style={{ borderRadius: 4, fontSize: 11, padding: '2px 8px', background: '#f1f5f9', color: '#64748b', border: 'none' }}>锁定条目和时间段</Tag>
|
||||
</div>
|
||||
<div style={{ padding: '16px', backgroundColor: '#fff', borderRadius: 12, border: '1px solid #e2e8f0', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
|
||||
{rows.length === 0 ? (
|
||||
<Text style={{ color: '#94a3b8', fontSize: 12 }}>暂无内容</Text>
|
||||
) : (
|
||||
rows.map((item, index) => {
|
||||
const row = isRecord(item) ? item : {};
|
||||
return (
|
||||
<div key={`${section.key}-${index}`} style={{ marginBottom: index === rows.length - 1 ? 0 : 14, paddingBottom: index === rows.length - 1 ? 0 : 14, borderBottom: index === rows.length - 1 ? 'none' : '1px dashed #e2e8f0' }}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 8, color: '#475569', fontSize: 12 }}>第 {index + 1} 段</Text>
|
||||
<div key={`${section.key}-${index}`} style={{ marginBottom: index === rows.length - 1 ? 0 : 16, paddingBottom: index === rows.length - 1 ? 0 : 16, borderBottom: index === rows.length - 1 ? 'none' : '1px dashed #e2e8f0' }}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 10, color: '#475569', fontSize: 13 }}>第 {index + 1} 段</Text>
|
||||
{displayFields.map((field) => (
|
||||
<div key={field.key} style={{ marginBottom: 10 }}>
|
||||
<Space style={{ marginBottom: 4 }}>
|
||||
<div key={field.key} style={{ marginBottom: 14 }}>
|
||||
<div style={{ marginBottom: 6 }}>
|
||||
<Text style={{ color: '#64748b', fontSize: 12 }}>{field.label}</Text>
|
||||
{/* {renderFieldTag(field)} */}
|
||||
</Space>
|
||||
</div>
|
||||
{field.editable ? (
|
||||
field.type === 'boolean' ? (
|
||||
<BooleanRadio
|
||||
@@ -195,43 +259,77 @@ const VideoPromptSchemaEditor: React.FC<VideoPromptSchemaEditorProps> = ({ value
|
||||
const renderPrimitiveSection = (section: VideoPromptSchemaSectionConfig) => {
|
||||
if (!(section.key in safeValue)) return null;
|
||||
return (
|
||||
<div key={section.key} style={{ marginBottom: 18 }}>
|
||||
<Space style={{ marginBottom: 8 }}>
|
||||
<Text strong style={{ color: '#334155', fontSize: 13 }}>{section.label}</Text>
|
||||
{/* {renderFieldTag(section)} */}
|
||||
</Space>
|
||||
{section.editable ? (
|
||||
<EditableInput
|
||||
value={safeValue[section.key]}
|
||||
maxLength={section.maxLength}
|
||||
onChange={(nextText) => {
|
||||
const next = clonePlain(safeValue);
|
||||
next[section.key] = nextText;
|
||||
onChange(next);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ReadonlyBlock value={safeValue[section.key]} />
|
||||
)}
|
||||
<div key={section.key} style={{ marginBottom: 20 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<div style={{ width: 3, height: 16, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: 2 }} />
|
||||
<Text strong style={{ color: '#334155', fontSize: 14, fontWeight: 600 }}>{section.label}</Text>
|
||||
</div>
|
||||
<div style={{ padding: '16px', backgroundColor: '#fff', borderRadius: 12, border: '1px solid #e2e8f0', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
|
||||
{section.editable ? (
|
||||
<EditableInput
|
||||
value={safeValue[section.key]}
|
||||
maxLength={section.maxLength}
|
||||
onChange={(nextText) => {
|
||||
const next = clonePlain(safeValue);
|
||||
next[section.key] = nextText;
|
||||
onChange(next);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ReadonlyBlock value={safeValue[section.key]} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const menuItems = sections
|
||||
.filter((section) => section.key in safeValue)
|
||||
.map((section) => ({
|
||||
key: section.key,
|
||||
label: section.label,
|
||||
}));
|
||||
|
||||
const renderSection = (section: VideoPromptSchemaSectionConfig) => {
|
||||
if (!(section.key in safeValue)) return null;
|
||||
if (section.type === 'object') return renderObjectSection(section);
|
||||
if (section.type === 'flow') return renderFlowSection(section);
|
||||
return renderPrimitiveSection(section);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* <Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 14 }}
|
||||
message="仅可修改后台 Schema 配置允许编辑的字段"
|
||||
description="视频规格、时间段、流程条目数量、输出规格、质量控制、合规控制等锁定内容由服务端最终校验。"
|
||||
/> */}
|
||||
{sections.map((section) => {
|
||||
if (!(section.key in safeValue)) return null;
|
||||
if (section.type === 'object') return renderObjectSection(section);
|
||||
if (section.type === 'flow') return renderFlowSection(section);
|
||||
return renderPrimitiveSection(section);
|
||||
})}
|
||||
<div style={{ display: 'flex', flexDirection: 'row', maxHeight: 500 }}>
|
||||
{/* 左侧菜单 */}
|
||||
<div style={{ width: 180, flexShrink: 0, background: '#fff', borderRight: '1px solid #e2e8f0', overflowY: 'auto' }}>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[selectedKey]}
|
||||
onClick={handleMenuSelect}
|
||||
style={{ height: '100%', borderRight: 0 }}
|
||||
theme="light"
|
||||
items={menuItems.map(item => ({
|
||||
...item,
|
||||
style: {
|
||||
borderRadius: 8,
|
||||
margin: '4px 8px',
|
||||
padding: '8px 12px',
|
||||
color: '#64748b',
|
||||
fontSize: 13,
|
||||
transition: 'all 0.2s ease',
|
||||
maxHeight: 500,
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
{/* 右侧内容 */}
|
||||
<div ref={contentRef} style={{ flex: 1, padding: '20px', overflowY: 'auto', background: '#f8fafc' }}>
|
||||
{sections.map((section) => (
|
||||
<div key={section.key} id={`section-${section.key}`}>
|
||||
{renderSection(section)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ interface UploadResourceHistoryPickerProps {
|
||||
maxAudioDuration?: number;
|
||||
usedAudioDuration?: number;
|
||||
hideLimitHint?: boolean;
|
||||
defaultResourceType?: MediaTypeFilter;
|
||||
}
|
||||
|
||||
const buildPreviewUrl = (url: string) => {
|
||||
@@ -122,8 +123,9 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
maxAudioDuration,
|
||||
usedAudioDuration,
|
||||
hideLimitHint,
|
||||
defaultResourceType,
|
||||
}) => {
|
||||
const [resourceType, setResourceType] = useState<MediaTypeFilter>('');
|
||||
const [resourceType, setResourceType] = useState<MediaTypeFilter>(defaultResourceType || '');
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [groupPage, setGroupPage] = useState(1);
|
||||
const [groupPageSize, setGroupPageSize] = useState(10);
|
||||
@@ -233,6 +235,14 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
if (open) setCheckedMap(new Map());
|
||||
}, [open]);
|
||||
|
||||
// 当 defaultResourceType 变化时,同步更新 resourceType 状态
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setResourceType(defaultResourceType || '');
|
||||
setGroupPage(1);
|
||||
}
|
||||
}, [open, defaultResourceType]);
|
||||
|
||||
const toggle = async (item: UploadResourceHistoryItem) => {
|
||||
if (selectedIdSet.has(item.id)) {
|
||||
message.warning('该素材已经在参考内容中');
|
||||
@@ -463,7 +473,7 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
|
||||
pageSize={itemPageSize}
|
||||
total={total}
|
||||
showSizeChanger
|
||||
pageSizeOptions={[10, 20, 50, 100]}
|
||||
// pageSizeOptions={[8, 16, 32, 64]}/
|
||||
onChange={(nextPage, nextSize) => {
|
||||
setItemPage(nextPage);
|
||||
setItemPageSize(nextSize);
|
||||
|
||||
@@ -430,6 +430,29 @@ body {
|
||||
border-color: #6366f1 !important;
|
||||
}
|
||||
|
||||
/* ── Video Player Overlay ──────────────── */
|
||||
.video-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.video-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.video-overlay svg {
|
||||
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
/* ── Mobile Bottom Nav ─────────────────── */
|
||||
.mobile-bottom-nav {
|
||||
display: none;
|
||||
|
||||
@@ -1078,13 +1078,6 @@ const AIChatPage: React.FC = () => {
|
||||
|
||||
|
||||
|
||||
console.log(newMessage);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 设置加载状态
|
||||
setLoading(true);
|
||||
@@ -1943,7 +1936,6 @@ const AIChatPage: React.FC = () => {
|
||||
}
|
||||
const link = document.createElement('a');
|
||||
link.href = downloadUrl;
|
||||
console.log(downloadUrl);
|
||||
|
||||
link.download = previewType === 'image' ? 'image.png' : 'video.mp4';
|
||||
document.body.appendChild(link);
|
||||
@@ -2804,6 +2796,7 @@ const AIChatPage: React.FC = () => {
|
||||
<UploadSelector
|
||||
accept="image/*"
|
||||
multiple={false}
|
||||
mediaType="image"
|
||||
onLocalSelect={(files) => {
|
||||
if (files.length > 0) {
|
||||
handleUpload(files[0], 'first');
|
||||
@@ -2896,6 +2889,7 @@ const AIChatPage: React.FC = () => {
|
||||
<UploadSelector
|
||||
accept="image/*"
|
||||
multiple={false}
|
||||
mediaType="image"
|
||||
onLocalSelect={(files) => {
|
||||
if (files.length > 0) {
|
||||
handleUpload(files[0], 'last');
|
||||
|
||||
@@ -2334,6 +2334,7 @@ const GeneratePage: React.FC = () => {
|
||||
))}
|
||||
<UploadSelector
|
||||
accept="image/*,video/*"
|
||||
mediaType={mediaType}
|
||||
onLocalSelect={(files) => {
|
||||
files.forEach(async (file) => {
|
||||
await handlePasteUpload(file);
|
||||
@@ -2667,47 +2668,53 @@ const GeneratePage: React.FC = () => {
|
||||
flexWrap: 'wrap',
|
||||
gap: 4,
|
||||
}}>
|
||||
{engineOptions.ratios.map((ratio) => (
|
||||
<button
|
||||
key={ratio}
|
||||
onClick={() => setVideoAspectRatio(ratio as AspectRatio)}
|
||||
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>
|
||||
))}
|
||||
{engineOptions.ratios.map((ratio) => {
|
||||
const [w, h] = ratio.split(':').map(Number);
|
||||
const maxSize = 18;
|
||||
const rectWidth = w >= h ? maxSize : Math.round(maxSize * (w / h));
|
||||
const rectHeight = h >= w ? maxSize : Math.round(maxSize * (h / w));
|
||||
return (
|
||||
<button
|
||||
key={ratio}
|
||||
onClick={() => setVideoAspectRatio(ratio as AspectRatio)}
|
||||
style={{
|
||||
flex: '0 0 calc(12.5% - 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: rectWidth,
|
||||
height: rectHeight,
|
||||
border: `2px solid ${videoAspectRatio === ratio ? '#6366f1' : '#9ca3af'}`,
|
||||
borderRadius: 3,
|
||||
marginBottom: 2,
|
||||
}} />
|
||||
<span style={{
|
||||
fontSize: 9,
|
||||
color: videoAspectRatio === ratio
|
||||
? '#6366f1'
|
||||
: '#6b7280',
|
||||
fontWeight: videoAspectRatio === ratio ? 600 : 400,
|
||||
}}>
|
||||
{ratio}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
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';
|
||||
@@ -150,54 +150,49 @@ function InitialInfo() {
|
||||
|
||||
const isV2 = flowVersion === 'v2';
|
||||
|
||||
const baseSteps = isV2
|
||||
? [
|
||||
{ id: 1, title: '素材与项目信息', description: '固定素材和项目描述', childId: 1 },
|
||||
{ id: 2, title: '生成视频提示词', description: '生成并确认视频提示词', childId: 4 },
|
||||
{ id: 3, title: '生成最终视频', description: '使用当前视频提示词生成视频', childId: 5 },
|
||||
]
|
||||
: [
|
||||
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
|
||||
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
|
||||
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
|
||||
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
|
||||
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
|
||||
];
|
||||
// 使用 useMemo 缓存 baseSteps,避免每次渲染都创建新数组
|
||||
const baseSteps = useMemo(() => {
|
||||
return isV2
|
||||
? [
|
||||
{ id: 1, title: '素材与项目信息', description: '固定素材和项目描述', childId: 1 },
|
||||
{ id: 2, title: '生成视频提示词', description: '生成并确认视频提示词', childId: 4 },
|
||||
{ id: 3, title: '生成最终视频', description: '使用当前视频提示词生成视频', childId: 5 },
|
||||
]
|
||||
: [
|
||||
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
|
||||
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
|
||||
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
|
||||
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
|
||||
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
|
||||
];
|
||||
}, [isV2]);
|
||||
|
||||
// 合并基础步骤和API返回的状态
|
||||
const steps = baseSteps.map((step, index) => ({
|
||||
...step,
|
||||
status: apiSteps[index]?.status || '',
|
||||
id: apiSteps[index]?.id || index,
|
||||
output: apiSteps[index]?.output || '',
|
||||
input: apiSteps[index]?.input || {},
|
||||
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
||||
}));
|
||||
// 使用 useMemo 缓存 steps,避免每次渲染都创建新数组
|
||||
const steps = useMemo(() => {
|
||||
return baseSteps.map((step, index) => ({
|
||||
...step,
|
||||
status: apiSteps[index]?.status || '',
|
||||
id: apiSteps[index]?.id || index,
|
||||
output: apiSteps[index]?.output || '',
|
||||
input: apiSteps[index]?.input || {},
|
||||
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
||||
errorMessage: apiSteps[index]?.errorMessage || '',
|
||||
}));
|
||||
}, [baseSteps, apiSteps]);
|
||||
|
||||
// 当apiSteps更新时,逆向遍历找到第一个已完成或失败的步骤并展开
|
||||
// 当steps更新时,默认只展开生成中的步骤
|
||||
const lastProcessingKeys = useRef<string>('');
|
||||
useEffect(() => {
|
||||
const completedFailedKeys = new Set<string>();
|
||||
const processingKeys = steps
|
||||
.filter(step => step.status === 'processing')
|
||||
.map(step => String(step.childId));
|
||||
|
||||
activeKey.forEach(key => {
|
||||
const step = steps.find(s => String(s.childId) === key);
|
||||
if (step && (step.status === 'completed' || step.status === 'failed')) {
|
||||
completedFailedKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = steps.length - 1; i >= 0; i--) {
|
||||
if (steps[i].status === 'completed' || steps[i].status === 'failed') {
|
||||
completedFailedKeys.add(String(steps[i].childId));
|
||||
break;
|
||||
}
|
||||
const processingKeysStr = JSON.stringify(processingKeys);
|
||||
if (processingKeysStr !== lastProcessingKeys.current) {
|
||||
lastProcessingKeys.current = processingKeysStr;
|
||||
setActiveKey(processingKeys);
|
||||
}
|
||||
|
||||
if (completedFailedKeys.size > 0) {
|
||||
setActiveKey(Array.from(completedFailedKeys));
|
||||
} else {
|
||||
setActiveKey([]);
|
||||
}
|
||||
}, [apiSteps]);
|
||||
}, [steps]);
|
||||
|
||||
// console.log('steps', steps);
|
||||
|
||||
@@ -894,13 +889,17 @@ function InitialInfo() {
|
||||
<span style={{ width: 3, height: 14, background: 'linear-gradient(180deg, #6366f1, #8b5cf6)', borderRadius: 2, display: 'inline-block' }} />
|
||||
视频
|
||||
</span>
|
||||
<div className="medio_box" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialVideoUrl ? 'pointer' : 'default' }} onClick={() => taskDetail?.material?.materialVideoUrl && openPreview(taskDetail.material.materialVideoUrl, 'video')}>
|
||||
<div className="medio_box video-container" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialVideoUrl ? 'pointer' : 'default', position: 'relative' }} onClick={() => taskDetail?.material?.materialVideoUrl && openPreview(taskDetail.material.materialVideoUrl, 'video')}>
|
||||
{taskDetail?.material?.materialVideoUrl ? (
|
||||
<video
|
||||
|
||||
src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
<>
|
||||
<video
|
||||
src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
<div className="video-overlay">
|
||||
<PlayCircleOutlined style={{ fontSize: 48, color: '#fff', opacity: 0.9 }} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}>暂无视频</div>
|
||||
)}
|
||||
@@ -945,12 +944,14 @@ function InitialInfo() {
|
||||
<span style={{ width: 3, height: 14, background: 'linear-gradient(180deg, #6366f1, #8b5cf6)', borderRadius: 2, display: 'inline-block' }} />
|
||||
生成视频
|
||||
</span>
|
||||
<div className="medio_box" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: 'pointer' }} onClick={() => openPreview(taskDetail.finalVideoUrl, 'video')}>
|
||||
<div className="medio_box video-container" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: 'pointer', position: 'relative' }} onClick={() => openPreview(taskDetail.finalVideoUrl, 'video')}>
|
||||
<video
|
||||
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
<div className="video-overlay">
|
||||
<PlayCircleOutlined style={{ fontSize: 48, color: '#fff', opacity: 0.9 }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1642,14 +1643,25 @@ function InitialInfo() {
|
||||
<>
|
||||
<div style={{ borderRadius: 12, overflow: 'hidden', marginBottom: 16, position: 'relative', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', border: '1px solid rgba(99, 102, 241, 0.08)', boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)' }}>
|
||||
{step.status === 'completed' && taskDetail?.finalVideoUrl ? (
|
||||
<video src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`} style={{ width: '100%', height: 180, objectFit: 'cover' }} controls />
|
||||
<video src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`} style={{ width: '100%', height: 180 }} controls />
|
||||
) : step.status === 'processing' ? (
|
||||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ width: 20, height: 20, borderRadius: '50%', border: '2px solid #e2e8f0', borderTopColor: '#6366f1', animation: 'spinSlow 1s linear infinite' }} />
|
||||
<span style={{ color: '#acacacff', fontSize: 12 }}>预计5分钟</span>
|
||||
|
||||
<span style={{ color: '#6366f1', fontSize: 14 }}>视频生成中...</span>
|
||||
|
||||
</div>
|
||||
) : step.status === 'failed' ? (
|
||||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ef4444', fontSize: 14 }}>视频生成失败</div>
|
||||
<>
|
||||
<div style={{ width: '100%', height: 180, display: 'flex',flexDirection:'column', alignItems: 'center', justifyContent: 'center', color: '#ef4444', fontSize: 14 }}>
|
||||
<p>视频生成失败</p>
|
||||
<p>{step.errorMessage || '未知错误'}</p>
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
||||
|
||||
) : (
|
||||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 14 }}>暂无视频</div>
|
||||
)}
|
||||
|
||||
@@ -899,6 +899,7 @@ const GenerateConver: React.FC = () => {
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
||||
上传复刻视频
|
||||
<span style={{ color: '#f94444' }}>(必选)</span>
|
||||
</p>
|
||||
{videoUrl ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -1015,6 +1016,7 @@ const GenerateConver: React.FC = () => {
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: 0, fontSize: 14, fontWeight: 600, color: '#1e293b', marginBottom: 10 }}>
|
||||
上传产品图片
|
||||
<span>(可选)</span>
|
||||
</p>
|
||||
{imageUrl ? (
|
||||
<div style={{ position: 'relative' }}>
|
||||
@@ -1142,6 +1144,7 @@ const GenerateConver: React.FC = () => {
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#475569', marginBottom: 8 }}>
|
||||
原产品名称
|
||||
<span>(可选)</span>
|
||||
</p>
|
||||
<Input
|
||||
value={originalProductName}
|
||||
@@ -1163,6 +1166,7 @@ const GenerateConver: React.FC = () => {
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#475569', marginBottom: 8 }}>
|
||||
自有产品名称
|
||||
<span>(可选)</span>
|
||||
</p>
|
||||
<Input
|
||||
value={ownProductName}
|
||||
@@ -1184,6 +1188,7 @@ const GenerateConver: React.FC = () => {
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 500, color: '#475569', marginBottom: 8 }}>
|
||||
产品卖点
|
||||
<span>(可选)</span>
|
||||
</p>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<TextArea
|
||||
@@ -1208,7 +1213,9 @@ const GenerateConver: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: 16, display: 'grid', gap: 10 }}>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 600, color: '#475569' }}>视频生成参数(必选)</p>
|
||||
<p style={{ margin: 0, fontSize: 13, fontWeight: 600, color: '#475569' }}>视频生成参数
|
||||
<span style={{ color: '#f94444' }}>(必选)</span>
|
||||
</p>
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
@@ -1399,39 +1406,44 @@ const GenerateConver: React.FC = () => {
|
||||
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
|
||||
{engineOptions.ratios.map((ratio) => {
|
||||
const [w, h] = ratio.split(':').map(Number);
|
||||
const maxSize = 18;
|
||||
const rectWidth = w >= h ? maxSize : Math.round(maxSize * (w / h));
|
||||
const rectHeight = h >= w ? maxSize : Math.round(maxSize * (h / w));
|
||||
return (
|
||||
<button
|
||||
key={ratio}
|
||||
onClick={() => setVideoAspectRatio(ratio)}
|
||||
style={{
|
||||
flex: '0 0 calc(12.5% - 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: rectWidth,
|
||||
height: rectHeight,
|
||||
border: `2px solid ${videoAspectRatio === ratio ? '#6366f1' : '#9ca3af'}`,
|
||||
borderRadius: 3,
|
||||
marginBottom: 2,
|
||||
}} />
|
||||
<span style={{
|
||||
fontSize: 9,
|
||||
color: videoAspectRatio === ratio
|
||||
? '#6366f1'
|
||||
: '#6b7280',
|
||||
fontWeight: videoAspectRatio === ratio ? 600 : 400,
|
||||
@@ -1439,7 +1451,8 @@ const GenerateConver: React.FC = () => {
|
||||
{ratio}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { 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, SettingOutlined, LayoutOutlined } from '@ant-design/icons';
|
||||
@@ -29,6 +29,40 @@ function buildAssetUrl(url?: string): string {
|
||||
return `${API_BASE}${url}`;
|
||||
}
|
||||
|
||||
interface VideoCellProps {
|
||||
record: any;
|
||||
onPreview: (url: string) => void;
|
||||
}
|
||||
|
||||
const VideoCell = React.memo(({ record, onPreview }: VideoCellProps) => {
|
||||
const videoUrl = record.segmentVideoUrl;
|
||||
if (videoUrl) {
|
||||
return (
|
||||
<div style={{ position: 'relative', width: 120, height: 80, borderRadius: 6, overflow: 'hidden', background: '#f1f5f9', cursor: 'pointer' }}
|
||||
onClick={() => onPreview(buildAssetUrl(videoUrl))}>
|
||||
<video
|
||||
src={buildAssetUrl(videoUrl)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', pointerEvents: 'none' }}
|
||||
/>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.3)' }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: '50%', background: 'rgba(255,255,255,0.9)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ width: 0, height: 0, borderTop: '8px solid transparent', borderBottom: '8px solid transparent', borderLeft: '14px solid #6366f1', marginLeft: 2 }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const splitMeta = getShotSplitStatusMeta(record.splitStatus || record.split_status);
|
||||
return (
|
||||
<div style={{ position: 'relative', width: 120, height: 80, borderRadius: 6, overflow: 'hidden', background: '#f1f5f9', cursor: 'pointer' }}>
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontSize: 12 }}>
|
||||
{splitMeta.active ? <Spin size="small" style={{ color: '#f59e0b' }} /> : null}
|
||||
<span style={{ color: splitMeta.failure ? '#ef4444' : (splitMeta.active ? '#f59e0b' : '#94a3b8') }}>{splitMeta.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function RemoveInfo() {
|
||||
const { creatID } = useParams<{ creatID: string }>();
|
||||
const navigate = useNavigate();
|
||||
@@ -63,6 +97,9 @@ function RemoveInfo() {
|
||||
const [creditRules, setCreditRules] = useState<any[]>([]);
|
||||
const [taskDetail, setTaskDetail] = useState<any>(null);
|
||||
const [tableData, setTableData] = useState<any[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(20);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [splitLoading, setSplitLoading] = useState(false);
|
||||
const [autoSplitLoading, setAutoSplitLoading] = useState(false);
|
||||
@@ -134,12 +171,18 @@ function RemoveInfo() {
|
||||
const fetchSegments = useCallback(async () => {
|
||||
if (!creatID) return;
|
||||
try {
|
||||
const res = await Removelist(creatID);
|
||||
const res = await Removelist(creatID, currentPage, pageSize);
|
||||
setTableData(res.items || []);
|
||||
setTotal(res.total || 0);
|
||||
} catch {
|
||||
message.error('获取拆镜列表失败');
|
||||
}
|
||||
}, [creatID]);
|
||||
}, [creatID, currentPage, pageSize]);
|
||||
|
||||
const handleVideoPreview = useCallback((url: string) => {
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewModalVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleReanalyze = useCallback(async () => {
|
||||
if (!creatID) return;
|
||||
@@ -454,11 +497,11 @@ function RemoveInfo() {
|
||||
return splitStatus === 'completed' && !!(record?.segmentVideoUrl || record?.segment_video_url);
|
||||
};
|
||||
|
||||
const columns: any[] = [
|
||||
const columns = useMemo(() => [
|
||||
{
|
||||
title: '片段',
|
||||
width: 100,
|
||||
fixed: 'left',
|
||||
fixed: 'left' as const,
|
||||
align: 'center' as const,
|
||||
render: (_: any, record: any) => (
|
||||
<div>
|
||||
@@ -475,35 +518,7 @@ function RemoveInfo() {
|
||||
width: 150,
|
||||
align: 'center' as const,
|
||||
render: (_: any, record: any) => (
|
||||
<div style={{ position: 'relative', width: 120, height: 80, borderRadius: 6, overflow: 'hidden', background: '#f1f5f9', cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
if (record.segmentVideoUrl) {
|
||||
setPreviewVideoUrl(buildAssetUrl(record.segmentVideoUrl));
|
||||
setPreviewModalVisible(true);
|
||||
}
|
||||
}}>
|
||||
{record.segmentVideoUrl ? (
|
||||
<>
|
||||
<video
|
||||
src={buildAssetUrl(record.segmentVideoUrl)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover', pointerEvents: 'none' }}
|
||||
/>
|
||||
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(0,0,0,0.3)' }}>
|
||||
<div style={{ width: 32, height: 32, borderRadius: '50%', background: 'rgba(255,255,255,0.9)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div style={{ width: 0, height: 0, borderTop: '8px solid transparent', borderBottom: '8px solid transparent', borderLeft: '14px solid #6366f1', marginLeft: 2 }} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (() => {
|
||||
const splitMeta = getShotSplitStatusMeta(record.splitStatus || record.split_status);
|
||||
return (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6, fontSize: 12 }}>
|
||||
{splitMeta.active ? <Spin size="small" style={{ color: '#f59e0b' }} /> : null}
|
||||
<span style={{ color: splitMeta.failure ? '#ef4444' : (splitMeta.active ? '#f59e0b' : '#94a3b8') }}>{splitMeta.label}</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<VideoCell record={record} onPreview={handleVideoPreview} />
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -671,7 +686,7 @@ function RemoveInfo() {
|
||||
{
|
||||
title: '操作',
|
||||
width: 200,
|
||||
fixed: 'right',
|
||||
fixed: 'right' as const,
|
||||
align: 'center' as const,
|
||||
render: (_: any, record: any) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
@@ -727,7 +742,7 @@ function RemoveInfo() {
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
], []);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -1033,7 +1048,17 @@ function RemoveInfo() {
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
pagination={{
|
||||
current: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
onChange: (page, size) => {
|
||||
setCurrentPage(page);
|
||||
setPageSize(size);
|
||||
},
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
bordered={false}
|
||||
rowKey="id"
|
||||
scroll={{ y: 300 }}
|
||||
@@ -1103,7 +1128,7 @@ function RemoveInfo() {
|
||||
onConfirm={handleCustomSplit}
|
||||
/>
|
||||
|
||||
<Drawer
|
||||
<Drawer
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
@@ -1111,12 +1136,23 @@ function RemoveInfo() {
|
||||
<span style={{ fontSize: 18, fontWeight: 600, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>智能视频复刻</span>
|
||||
{currentSegmentName && <span style={{ fontSize: 14, color: '#64748b' }}>- {currentSegmentName}</span>}
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
icon={<XOutlined />}
|
||||
onClick={handleCloseDrawer}
|
||||
style={{ padding: 0, color: '#64748b', fontSize: 14 }}
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleCloseDrawer(); }}
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: '#64748b',
|
||||
fontSize: 20,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
placement="right"
|
||||
@@ -1196,7 +1232,9 @@ function RemoveInfo() {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<label style={{ fontWeight: 500, color: '#334155', marginBottom: 8, display: 'block', fontSize: 14 }}>视频参数
|
||||
<span style={{ color: '#f94444' }}>(必选)</span>
|
||||
</label>
|
||||
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
<div style={{ flex: 1, position: 'relative', display: 'inline-block' }}>
|
||||
<button
|
||||
@@ -1387,47 +1425,53 @@ function RemoveInfo() {
|
||||
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>
|
||||
))}
|
||||
{engineOptions.ratios.map((ratio) => {
|
||||
const [w, h] = ratio.split(':').map(Number);
|
||||
const maxSize = 18;
|
||||
const rectWidth = w >= h ? maxSize : Math.round(maxSize * (w / h));
|
||||
const rectHeight = h >= w ? maxSize : Math.round(maxSize * (h / w));
|
||||
return (
|
||||
<button
|
||||
key={ratio}
|
||||
onClick={() => setVideoAspectRatio(ratio)}
|
||||
style={{
|
||||
flex: '0 0 calc(12.5% - 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: rectWidth,
|
||||
height: rectHeight,
|
||||
border: `2px solid ${videoAspectRatio === ratio ? '#6366f1' : '#9ca3af'}`,
|
||||
borderRadius: 3,
|
||||
marginBottom: 2,
|
||||
}} />
|
||||
<span style={{
|
||||
fontSize: 9,
|
||||
color: videoAspectRatio === ratio
|
||||
? '#6366f1'
|
||||
: '#6b7280',
|
||||
fontWeight: videoAspectRatio === ratio ? 600 : 400,
|
||||
}}>
|
||||
{ratio}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -564,6 +564,7 @@ export default function VideoFrameExtractor() {
|
||||
<Table
|
||||
columns={[
|
||||
{
|
||||
title: '产品名称',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
render: (text: string) => (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
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';
|
||||
@@ -118,54 +118,50 @@ function InitialInfo() {
|
||||
|
||||
const isV2 = flowVersion === 'v2';
|
||||
|
||||
const baseSteps = isV2
|
||||
? [
|
||||
{ id: 1, title: '素材与项目信息', description: '固定片段素材和项目描述', childId: 1 },
|
||||
{ id: 2, title: '生成视频提示词', description: '生成并确认视频提示词', childId: 4 },
|
||||
{ id: 3, title: '生成最终视频', description: '使用当前视频提示词生成视频', childId: 5 },
|
||||
]
|
||||
: [
|
||||
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
|
||||
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
|
||||
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
|
||||
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
|
||||
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
|
||||
];
|
||||
// 使用 useMemo 缓存 baseSteps,避免每次渲染都创建新数组
|
||||
const baseSteps = useMemo(() => {
|
||||
return isV2
|
||||
? [
|
||||
{ id: 1, title: '素材与项目信息', description: '固定片段素材和项目描述', childId: 1 },
|
||||
{ id: 2, title: '生成视频提示词', description: '生成并确认视频提示词', childId: 4 },
|
||||
{ id: 3, title: '生成最终视频', description: '使用当前视频提示词生成视频', childId: 5 },
|
||||
]
|
||||
: [
|
||||
{ id: 1, title: '原始素材', description: '上传原始视频素材', childId: 1 },
|
||||
{ id: 2, title: '生成提示词', description: '根据素材生成描述词', childId: 2 },
|
||||
{ id: 3, title: '生成产品融合图', description: '生成产品与场景融合图', childId: 3 },
|
||||
{ id: 4, title: '生成视频提示词', description: '生成视频生成提示词', childId: 4 },
|
||||
{ id: 5, title: '生成最终视频', description: '合成最终视频', childId: 5 },
|
||||
];
|
||||
}, [isV2]);
|
||||
|
||||
// 合并基础步骤和API返回的状态
|
||||
const steps = baseSteps.map((step, index) => ({
|
||||
...step,
|
||||
status: apiSteps[index]?.status || '',
|
||||
id: apiSteps[index]?.id || index,
|
||||
output: apiSteps[index]?.output || '',
|
||||
input: apiSteps[index]?.input || {},
|
||||
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
||||
}));
|
||||
// 使用 useMemo 缓存 steps,避免每次渲染都创建新数组
|
||||
const steps = useMemo(() => {
|
||||
return baseSteps.map((step, index) => ({
|
||||
...step,
|
||||
status: apiSteps[index]?.status || '',
|
||||
id: apiSteps[index]?.id || index,
|
||||
output: apiSteps[index]?.output || '',
|
||||
input: apiSteps[index]?.input || {},
|
||||
engineId: apiSteps[index]?.input?.payload?.videoConfig?.engineId || '',
|
||||
errorMessage: apiSteps[index]?.errorMessage || '',
|
||||
|
||||
// 当apiSteps更新时,逆向遍历找到第一个已完成或失败的步骤并展开
|
||||
}));
|
||||
}, [baseSteps, apiSteps]);
|
||||
|
||||
// 当steps更新时,默认只展开生成中的步骤
|
||||
const lastProcessingKeys = useRef<string>('');
|
||||
useEffect(() => {
|
||||
const completedFailedKeys = new Set<string>();
|
||||
const processingKeys = steps
|
||||
.filter(step => step.status === 'processing')
|
||||
.map(step => String(step.childId));
|
||||
|
||||
activeKey.forEach(key => {
|
||||
const step = steps.find(s => String(s.childId) === key);
|
||||
if (step && (step.status === 'completed' || step.status === 'failed')) {
|
||||
completedFailedKeys.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = steps.length - 1; i >= 0; i--) {
|
||||
if (steps[i].status === 'completed' || steps[i].status === 'failed') {
|
||||
completedFailedKeys.add(String(steps[i].childId));
|
||||
break;
|
||||
}
|
||||
const processingKeysStr = JSON.stringify(processingKeys);
|
||||
if (processingKeysStr !== lastProcessingKeys.current) {
|
||||
lastProcessingKeys.current = processingKeysStr;
|
||||
setActiveKey(processingKeys);
|
||||
}
|
||||
|
||||
if (completedFailedKeys.size > 0) {
|
||||
setActiveKey(Array.from(completedFailedKeys));
|
||||
} else {
|
||||
setActiveKey([]);
|
||||
}
|
||||
}, [apiSteps]);
|
||||
}, [steps]);
|
||||
|
||||
|
||||
|
||||
@@ -785,13 +781,17 @@ function InitialInfo() {
|
||||
<span style={{ width: 3, height: 14, background: 'linear-gradient(180deg, #6366f1, #8b5cf6)', borderRadius: 2, display: 'inline-block' }} />
|
||||
视频
|
||||
</span>
|
||||
<div className="medio_box" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialVideoUrl ? 'pointer' : 'default' }} onClick={() => taskDetail?.material?.materialVideoUrl && openPreview(taskDetail.material.materialVideoUrl, 'video')}>
|
||||
<div className="medio_box video-container" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: taskDetail?.material?.materialVideoUrl ? 'pointer' : 'default', position: 'relative' }} onClick={() => taskDetail?.material?.materialVideoUrl && openPreview(taskDetail.material.materialVideoUrl, 'video')}>
|
||||
{taskDetail?.material?.materialVideoUrl ? (
|
||||
<video
|
||||
|
||||
src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
<>
|
||||
<video
|
||||
src={buildMediaUrl(taskDetail.material.materialVideoUrl)}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
<div className="video-overlay">
|
||||
<PlayCircleOutlined style={{ fontSize: 48, color: '#fff', opacity: 0.9 }} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 13 }}>暂无视频</div>
|
||||
)}
|
||||
@@ -835,12 +835,14 @@ function InitialInfo() {
|
||||
<span style={{ width: 3, height: 14, background: 'linear-gradient(180deg, #6366f1, #8b5cf6)', borderRadius: 2, display: 'inline-block' }} />
|
||||
生成视频
|
||||
</span>
|
||||
<div className="medio_box" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: 'pointer' }} onClick={() => openPreview(taskDetail.finalVideoUrl, 'video')}>
|
||||
<div className="medio_box video-container" style={{ aspectRatio: '16/9', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', borderRadius: 14, overflow: 'hidden', boxShadow: '0 4px 12px rgba(99, 102, 241, 0.06)', border: '1px solid rgba(99, 102, 241, 0.08)', cursor: 'pointer', position: 'relative' }} onClick={() => openPreview(taskDetail.finalVideoUrl, 'video')}>
|
||||
<video
|
||||
|
||||
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
|
||||
/>
|
||||
<div className="video-overlay">
|
||||
<PlayCircleOutlined style={{ fontSize: 48, color: '#fff', opacity: 0.9 }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1548,14 +1550,21 @@ function InitialInfo() {
|
||||
<>
|
||||
<div style={{ borderRadius: 12, overflow: 'hidden', marginBottom: 16, position: 'relative', background: 'linear-gradient(135deg, rgba(248,250,252,0.8) 0%, rgba(238,242,255,0.6) 100%)', border: '1px solid rgba(99, 102, 241, 0.08)', boxShadow: '0 2px 8px rgba(99, 102, 241, 0.04)' }}>
|
||||
{step.status === 'completed' && taskDetail?.finalVideoUrl ? (
|
||||
<video src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`} style={{ width: '100%', height: 180, objectFit: 'cover' }} controls />
|
||||
<video src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${taskDetail.finalVideoUrl}`} style={{ width: '100%', height: 180 }} controls />
|
||||
) : step.status === 'processing' ? (
|
||||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ width: 20, height: 20, borderRadius: '50%', border: '2px solid #e2e8f0', borderTopColor: '#6366f1', animation: 'spinSlow 1s linear infinite' }} />
|
||||
<span style={{ color: '#acacacff', fontSize: 12 }}>预计5分钟</span>
|
||||
<span style={{ color: '#6366f1', fontSize: 14 }}>视频生成中...</span>
|
||||
</div>
|
||||
) : step.status === 'failed' ? (
|
||||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#ef4444', fontSize: 14 }}>视频生成失败</div>
|
||||
<>
|
||||
<div style={{ width: '100%', height: 180, display: 'flex',flexDirection:'column', alignItems: 'center', justifyContent: 'center', color: '#ef4444', fontSize: 14 }}>
|
||||
<p>视频生成失败</p>
|
||||
<p>{step.errorMessage || '未知错误'}</p>
|
||||
</div>
|
||||
|
||||
</>
|
||||
) : (
|
||||
<div style={{ width: '100%', height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#94a3b8', fontSize: 14 }}>暂无视频</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user