This commit is contained in:
sjy
2026-07-09 10:30:59 +08:00
parent ef5044dc9b
commit 581507d15d
7 changed files with 552 additions and 247 deletions
+2 -2
View File
@@ -646,8 +646,8 @@ export async function reanalyzeSegment(segmentId: string): Promise<any> {
return api.post(`/shot-replications/segments/${segmentId}/reanalyze`); return api.post(`/shot-replications/segments/${segmentId}/reanalyze`);
} }
// 删除拆镜项目 // 删除拆镜项目
export async function deleteShotReplicationProject(projectId: string): Promise<void> { export async function deleteShotReplicationProject(taskSetId: string): Promise<void> {
await api.delete(`/shot-replications/projects/${projectId}`); await api.delete(`/shot-replications/task-sets/${taskSetId}`);
} }
// 删除爆款开头复刻任务 // 删除爆款开头复刻任务
export async function deleteHotOpeningReplicationTask(taskId: string): Promise<void> { export async function deleteHotOpeningReplicationTask(taskId: string): Promise<void> {
@@ -109,7 +109,11 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
}} }}
> >
<FolderOpenOutlined style={{ fontSize: 14, color: '#64748b' }} /> <FolderOpenOutlined style={{ fontSize: 14, color: '#64748b' }} />
<span style={{ fontSize: 14, color: '#334155' }}>{multiple ? '本地上传(可多选拖拽)' : '本地上传'}</span> <span style={{ fontSize: 14, color: '#334155' }}>
{/* {multiple ? '本地上传(可多选拖拽)' : '本地上传'} */}
</span>
</div> </div>
<div <div
onClick={handleHistorySelect} onClick={handleHistorySelect}
@@ -218,18 +218,18 @@ const UploadResourceHistoryPanel: React.FC = () => {
<div style={{ fontSize: 18, fontWeight: 700, color: '#1e293b' }}>{group.generatedDate}</div> <div style={{ fontSize: 18, fontWeight: 700, color: '#1e293b' }}>{group.generatedDate}</div>
<Text type="secondary"> {group.total} </Text> <Text type="secondary"> {group.total} </Text>
</div> </div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(190px, 1fr))', gap: 16 }}> <div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'flex-start',}}>
{group.items.map((item) => { {group.items.map((item) => {
const checked = selectedIds.has(item.id); const checked = selectedIds.has(item.id);
return ( return (
<div key={item.id} style={{ border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0', borderRadius: 16, overflow: 'hidden', background: '#fff', boxShadow: '0 10px 24px rgba(15,23,42,0.06)' }}> <div key={item.id} style={{ width: '17%', minWidth: 240, margin: 16, border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0', borderRadius: 16, overflow: 'hidden', background: '#fff', boxShadow: '0 10px 24px rgba(15,23,42,0.06)' }}>
<div style={{ position: 'relative', background: '#f1f5f9' }}> <div style={{ position: 'relative', background: '#f1f5f9' }}>
{renderMedia(item)} {renderMedia(item)}
<Checkbox checked={checked} onChange={() => toggle(item.id)} style={{ position: 'absolute', top: 10, left: 10, background: '#fff', borderRadius: 6, padding: 4 }} /> <Checkbox checked={checked} onChange={() => toggle(item.id)} style={{ position: 'absolute', top: 10, left: 10, background: '#fff', borderRadius: 6, padding: 4 }} />
<Tag color="purple" icon={typeIcon(item.resourceType)} style={{ position: 'absolute', top: 10, right: 10, margin: 0 }}>{typeLabel(item.resourceType)}</Tag> <Tag color="purple" icon={typeIcon(item.resourceType)} style={{ position: 'absolute', top: 10, right: 10, margin: 0 }}>{typeLabel(item.resourceType)}</Tag>
</div> </div>
<div style={{ padding: 12 }}> <div style={{ padding: 12 ,paddingTop: 0}}>
<Text ellipsis title={item.fileName || item.id} style={{ display: 'block', fontWeight: 600, color: '#334155' }}>{item.fileName || item.id}</Text> {/* <Text ellipsis title={item.fileName || item.id} style={{ display: 'block', fontWeight: 600, color: '#334155' }}>{item.fileName || item.id}</Text> */}
<Space size={4} wrap style={{ marginTop: 8 }}> <Space size={4} wrap style={{ marginTop: 8 }}>
<Tag style={{ margin: 0 }}>{item.moduleLabel}</Tag> <Tag style={{ margin: 0 }}>{item.moduleLabel}</Tag>
<Tag style={{ margin: 0 }}>{bytesText(item.fileSizeBytes)}</Tag> <Tag style={{ margin: 0 }}>{bytesText(item.fileSizeBytes)}</Tag>
@@ -246,7 +246,7 @@ const UploadResourceHistoryPanel: React.FC = () => {
})} })}
</div> </div>
{group.items.length < group.total && ( {group.items.length < group.total && (
<div style={{ textAlign: 'center', marginTop: 14 }}> <div style={{ textAlign: 'left', marginTop: 14 }}>
<Button onClick={() => handleLoadMoreDay(group)}></Button> <Button onClick={() => handleLoadMoreDay(group)}></Button>
</div> </div>
)} )}
@@ -260,7 +260,11 @@ const UploadResourceHistoryPanel: React.FC = () => {
<Pagination current={page} pageSize={pageSize} total={totalDays} showSizeChanger pageSizeOptions={[5, 10]} onChange={(nextPage, nextSize) => { setPage(nextPage); setPageSize(nextSize); }} /> <Pagination current={page} pageSize={pageSize} total={totalDays} showSizeChanger pageSizeOptions={[5, 10]} onChange={(nextPage, nextSize) => { setPage(nextPage); setPageSize(nextSize); }} />
</div> </div>
<Modal open={!!previewItem} title={previewItem?.fileName || '预览'} footer={null} width={900} centered destroyOnHidden onCancel={() => setPreviewItem(null)}> <Modal open={!!previewItem}
// title={previewItem?.fileName || '预览'}
title={'预览'}
footer={null} width={900} centered destroyOnHidden onCancel={() => setPreviewItem(null)}>
{previewItem ? renderMedia(previewItem, 'preview') : null} {previewItem ? renderMedia(previewItem, 'preview') : null}
</Modal> </Modal>
</div> </div>
@@ -177,73 +177,74 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
{items.length === 0 ? ( {items.length === 0 ? (
<Empty description="暂无可复用的历史上传素材" style={{ padding: '48px 0' }} /> <Empty description="暂无可复用的历史上传素材" style={{ padding: '48px 0' }} />
) : ( ) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 14, minHeight: 260 }}> <div style={{ height: '500px', overflowY: 'auto' }}>
{items.map((item) => { <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 14 }}>
const checked = checkedMap.has(item.id); {items.map((item) => {
const disabled = selectedIdSet.has(item.id); const checked = checkedMap.has(item.id);
const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl); const disabled = selectedIdSet.has(item.id);
return ( const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl);
<div return (
key={item.id} <div
onClick={() => !disabled && toggle(item)} key={item.id}
style={{ onClick={() => !disabled && toggle(item)}
position: 'relative', style={{
borderRadius: 14, position: 'relative',
border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0', borderRadius: 14,
background: disabled ? '#f8fafc' : '#fff', border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0',
opacity: disabled ? 0.55 : 1, background: disabled ? '#f8fafc' : '#fff',
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.55 : 1,
overflow: 'hidden', cursor: disabled ? 'not-allowed' : 'pointer',
boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)', overflow: 'hidden',
}} boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)',
> }}
<div style={{ height: 112, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> >
{item.resourceType === 'image' ? ( <div style={{ height: 112, background: '#f1f5f9', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<img src={preview} alt={item.fileName || item.id} style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> {item.resourceType === 'image' ? (
) : item.resourceType === 'video' ? ( <img src={preview} alt={item.fileName || item.id} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<video src={preview} muted style={{ width: '100%', height: '100%', objectFit: 'cover' }} /> ) : item.resourceType === 'video' ? (
) : ( <video src={preview} muted style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
<div style={{ width: 58, height: 58, borderRadius: 18, background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 24 }}> ) : (
<AudioOutlined /> <div style={{ width: 58, height: 58, borderRadius: 18, background: 'linear-gradient(135deg, #8b5cf6, #a78bfa)', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontSize: 24 }}>
<AudioOutlined />
</div>
)}
</div>
<div style={{ padding: 10 }}>
<Space size={6} style={{ marginBottom: 6 }}>
<Tag color="purple" icon={typeIcon(item.resourceType)} style={{ margin: 0 }}>{typeLabel(item.resourceType)}</Tag>
{item.durationSeconds ? <Tag style={{ margin: 0 }}>{Number(item.durationSeconds).toFixed(1)}s</Tag> : null}
</Space>
<Text ellipsis style={{ display: 'block', fontSize: 13, color: '#334155' }} title={item.fileName || item.id}>
{item.fileName || item.id}
</Text>
<Text type="secondary" style={{ fontSize: 12 }}>{item.moduleLabel || '普通上传'}</Text>
</div>
{checked && (
<div style={{ position: 'absolute', top: 8, right: 8, width: 24, height: 24, borderRadius: 12, background: '#8b5cf6', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<CheckOutlined />
</div> </div>
)} )}
{disabled && <div style={{ position: 'absolute', top: 8, right: 8 }}><Tag></Tag></div>}
</div> </div>
<div style={{ padding: 10 }}> );
<Space size={6} style={{ marginBottom: 6 }}> })}
<Tag color="purple" icon={typeIcon(item.resourceType)} style={{ margin: 0 }}>{typeLabel(item.resourceType)}</Tag> </div>
{item.durationSeconds ? <Tag style={{ margin: 0 }}>{Number(item.durationSeconds).toFixed(1)}s</Tag> : null} <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16, paddingBottom: 8 }}>
</Space> <Pagination
<Text ellipsis style={{ display: 'block', fontSize: 13, color: '#334155' }} title={item.fileName || item.id}> current={page}
{item.fileName || item.id} pageSize={pageSize}
</Text> total={total}
<Text type="secondary" style={{ fontSize: 12 }}>{item.moduleLabel || '普通上传'}</Text> showSizeChanger
</div> pageSizeOptions={[10, 20, 50, 100]}
{checked && ( onChange={(nextPage, nextSize) => {
<div style={{ position: 'absolute', top: 8, right: 8, width: 24, height: 24, borderRadius: 12, background: '#8b5cf6', color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> setPage(nextPage);
<CheckOutlined /> setPageSize(nextSize);
</div> }}
)} />
{disabled && <div style={{ position: 'absolute', top: 8, right: 8 }}><Tag></Tag></div>} </div>
</div>
);
})}
</div> </div>
)} )}
</Spin> </Spin>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
<Pagination
current={page}
pageSize={pageSize}
total={total}
showSizeChanger
pageSizeOptions={[10, 20, 50, 100]}
onChange={(nextPage, nextSize) => {
setPage(nextPage);
setPageSize(nextSize);
}}
/>
</div>
</Modal> </Modal>
); );
}; };
@@ -93,7 +93,7 @@ const AuthorizationWaitingPage: React.FC = () => {
<div style={{ textAlign: 'center', padding: '60px 80px', background: '#fff', borderRadius: 16, boxShadow: '0 10px 40px rgba(0,0,0,0.1)' }}> <div style={{ textAlign: 'center', padding: '60px 80px', background: '#fff', borderRadius: 16, boxShadow: '0 10px 40px rgba(0,0,0,0.1)' }}>
<div style={{ width: 80, height: 80, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: 'center', margin: '0 auto 24px' }}> <div style={{ width: 80, height: 80, background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', borderRadius: '50%', display: 'flex', justifyContent: 'center', alignItems: 'center', margin: '0 auto 24px' }}>
{isChecking && countdown < 10 ? ( {isChecking && countdown < 10 ? (
<Spin size="large" tip="加载中" style={{ color: '#fff' }} /> <Spin size="large" description="加载中" style={{ color: '#fff' }} />
) : ( ) : (
<ClockCircleOutlined style={{ fontSize: 40, color: '#fff' }} /> <ClockCircleOutlined style={{ fontSize: 40, color: '#fff' }} />
)} )}
+183 -110
View File
@@ -14,6 +14,7 @@ import {
Tooltip, Tooltip,
Popconfirm, Popconfirm,
Modal, Modal,
App,
} from 'antd'; } from 'antd';
import bg1 from '../assets/bg1.png'; import bg1 from '../assets/bg1.png';
@@ -122,6 +123,7 @@ const mockUpload = (file: File): Promise<{ url: string }> => {
const AIChatPage: React.FC = () => { const AIChatPage: React.FC = () => {
// ==================== 状态定义 ==================== // ==================== 状态定义 ====================
const { message: antdMessage } = App.useApp();
const [collapsed, setCollapsed] = useState<boolean>(false); const [collapsed, setCollapsed] = useState<boolean>(false);
@@ -355,9 +357,7 @@ const AIChatPage: React.FC = () => {
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false); const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
const [ratioOptions, setRatioOptions] = useState([]); const [ratioOptions, setRatioOptions] = useState([]);
const [resolutionOptions, setResolutionOptions] = useState([]); const [resolutionOptions, setResolutionOptions] = useState([]);
const [widthandheight, setWidthandHeight] = useState([]); const [currentEngineSupportedSizes, setCurrentEngineSupportedSizes] = useState<Record<string, Record<string, string>>>({});
const [blindex, setBlindex] = useState<any>(0);
const [fblindex, setFBlindex] = useState<any>(0);
const [showEngineModal, setShowEngineModal] = useState(false); const [showEngineModal, setShowEngineModal] = useState(false);
const [showMediaTypeModal, setShowMediaTypeModal] = useState(false); const [showMediaTypeModal, setShowMediaTypeModal] = useState(false);
@@ -579,32 +579,48 @@ const AIChatPage: React.FC = () => {
} }
} }
let supportedSizes = (data as any).engine?.image?.[0]?.supportedSizes || {}; if (data.engine.image && data.engine.image.length > 0) {
let supportedResolutions: string[] = []; const savedImageEngine = data.engine.image.find((e: any) => e.id === countType);
let twokwidth: string[] = []; const targetImageEngine = savedImageEngine || data.engine.image[0];
let fourkwidth: string[] = [];
for (let key in supportedSizes["2K"]) { const supportedSizes = targetImageEngine.supportedSizes || {};
supportedResolutions.push(key); const resolutionLevels = Object.keys(supportedSizes).sort((a, b) => {
twokwidth.push(supportedSizes["2K"][key]); const levelOrder = { '1K': 0, '2K': 1, '4K': 2 };
return (levelOrder[a] || 0) - (levelOrder[b] || 0);
});
const primaryResolution = resolutionLevels[0] || '2K';
const supportedResolutions = Object.keys(supportedSizes[primaryResolution] || {});
const newRatioOptions = supportedResolutions.map((res: any) => ({
value: res,
label: res,
}));
setRatioOptions(newRatioOptions);
setCurrentEngineSupportedSizes(supportedSizes);
setResolutionOptions(resolutionLevels.map((level) => {
const match = level.match(/(\d+)K/);
const num = match ? parseInt(match[1]) : 1;
const labels: Record<number, string> = { 1: '标清', 2: '高清', 4: '超清' };
return {
value: level,
label: `${labels[num] || '高清'} ${level}`,
};
}));
if (supportedSizes[primaryResolution] && supportedSizes[primaryResolution][supportedResolutions[0]]) {
const defaultSize = supportedSizes[primaryResolution][supportedResolutions[0]];
const [w, h] = defaultSize.split(/[×x]/);
setWidth(Number(w));
setHeight(Number(h));
setSelectedRatio(supportedResolutions[0]);
setSelectedResolution(primaryResolution);
}
if (countType === '请选择') {
setCountType(targetImageEngine.id);
}
} }
const newRatioOptions = supportedResolutions.map((res: any) => ({
value: res,
label: res,
}));
setRatioOptions(newRatioOptions);
for (let key in supportedSizes["4K"]) {
fourkwidth.push(supportedSizes["4K"][key]);
}
const newWidthandHeight = [twokwidth, fourkwidth];
setWidthandHeight(newWidthandHeight);
setResolutionOptions([
{ value: '2K', label: '高清 2K' },
{ value: '4K', label: '超清 4K' },
]);
}) })
.catch(() => { .catch(() => {
}); });
@@ -726,34 +742,56 @@ const AIChatPage: React.FC = () => {
return () => document.removeEventListener("click", handleClickOutside); return () => document.removeEventListener("click", handleClickOutside);
}, [showVideoSettingsModal]); }, [showVideoSettingsModal]);
// 监听 blindex 状态变化 // 根据比例和分辨率计算尺寸
useEffect(() => {
if (
widthandheight[fblindex] &&
widthandheight[fblindex][blindex] !== undefined
) {
setWidth(Number(widthandheight[fblindex][blindex].substring(0, 4)));
setHeight(Number(widthandheight[fblindex][blindex].substring(5)));
}
}, [blindex]);
useEffect(() => {
if (
widthandheight[fblindex] &&
widthandheight[fblindex][blindex] !== undefined
) {
setWidth(Number(widthandheight[fblindex][blindex].substring(0, 4)));
setHeight(Number(widthandheight[fblindex][blindex].substring(5)));
}
}, [fblindex]);
// 根据比例计算尺寸
const calculateSizeFromRatione = (ratio: string) => { const calculateSizeFromRatione = (ratio: string) => {
setBlindex(ratio); const resolutionLevels = Object.keys(currentEngineSupportedSizes).sort((a, b) => {
const levelOrder: Record<string, number> = { '1K': 0, '2K': 1, '4K': 2 };
return (levelOrder[a] || 0) - (levelOrder[b] || 0);
});
const targetResolution = currentEngineSupportedSizes[selectedResolution] && currentEngineSupportedSizes[selectedResolution][ratio]
? selectedResolution
: resolutionLevels.find(level => currentEngineSupportedSizes[level] && currentEngineSupportedSizes[level][ratio]) || resolutionLevels[0];
if (targetResolution && currentEngineSupportedSizes[targetResolution] && currentEngineSupportedSizes[targetResolution][ratio]) {
const size = currentEngineSupportedSizes[targetResolution][ratio];
const [w, h] = size.split(/[×x]/);
setWidth(Number(w));
setHeight(Number(h));
setSelectedRatio(ratio);
setSelectedResolution(targetResolution);
}
}; };
const calculateSizeFromRatiotwo = (ratio: string) => { const calculateSizeFromRatiotwo = (resolution: string) => {
setFBlindex(parseInt(ratio === '2K' ? '0' : '1')); if (!currentEngineSupportedSizes[resolution]) {
const resolutionLevels = Object.keys(currentEngineSupportedSizes).sort((a, b) => {
const levelOrder: Record<string, number> = { '1K': 0, '2K': 1, '4K': 2 };
return (levelOrder[a] || 0) - (levelOrder[b] || 0);
});
resolution = resolutionLevels[0] || resolution;
}
const supportedRatios = currentEngineSupportedSizes[resolution] ? Object.keys(currentEngineSupportedSizes[resolution]) : [];
const newRatioOptions = supportedRatios.map((res: any) => ({
value: res,
label: res,
}));
setRatioOptions(newRatioOptions);
const targetRatio = supportedRatios.includes(selectedRatio)
? selectedRatio
: supportedRatios[0] || '';
if (targetRatio && currentEngineSupportedSizes[resolution] && currentEngineSupportedSizes[resolution][targetRatio]) {
const size = currentEngineSupportedSizes[resolution][targetRatio];
const [w, h] = size.split(/[×x]/);
setWidth(Number(w));
setHeight(Number(h));
setSelectedRatio(targetRatio);
setSelectedResolution(resolution);
}
}; };
// 交换宽高 // 交换宽高
@@ -813,7 +851,7 @@ const AIChatPage: React.FC = () => {
setFirstFrame(null); setFirstFrame(null);
setLastFrame(null); setLastFrame(null);
// 显示提示消息 // 显示提示消息
message.info('已开启新对话'); antdMessage.info('已开启新对话');
}; };
@@ -830,7 +868,7 @@ const AIChatPage: React.FC = () => {
); );
} }
// 显示成功提示 // 显示成功提示
message.success('对话已删除'); antdMessage.success('对话已删除');
}; };
@@ -847,12 +885,12 @@ const AIChatPage: React.FC = () => {
if (isFirstLastFrameMode) { if (isFirstLastFrameMode) {
if (!inputValue.trim()) { if (!inputValue.trim()) {
message.warning('请输入内容'); antdMessage.warning('请输入内容');
return; return;
} }
} else { } else {
if (!inputValue.trim() && currentMedia.length === 0) { if (!inputValue.trim() && currentMedia.length === 0) {
message.warning('请输入内容或上传图片/视频'); antdMessage.warning('请输入内容或上传图片/视频');
return; return;
} }
} }
@@ -954,7 +992,7 @@ const AIChatPage: React.FC = () => {
errorMessage = error.message; errorMessage = error.message;
} }
message.error({ antdMessage.error({
content: errorMessage, content: errorMessage,
duration: 3, duration: 3,
}); });
@@ -1136,16 +1174,16 @@ const AIChatPage: React.FC = () => {
if (mediaType === 'video' && referenceMode === 'first_last_frame') { if (mediaType === 'video' && referenceMode === 'first_last_frame') {
if (!isImage) { if (!isImage) {
message.error('首尾帧模式仅支持上传图片'); antdMessage.error('首尾帧模式仅支持上传图片');
return false; return false;
} }
if (file.size / 1024 / 1024 > 10) { if (file.size / 1024 / 1024 > 10) {
message.error('图片大小不能超过10MB'); antdMessage.error('图片大小不能超过10MB');
return false; return false;
} }
const effectiveUploadTarget = frameTarget || uploadTarget; const effectiveUploadTarget = frameTarget || uploadTarget;
if (!effectiveUploadTarget) { if (!effectiveUploadTarget) {
message.error('请选择首帧或尾帧上传位置'); antdMessage.error('请选择首帧或尾帧上传位置');
return false; return false;
} }
@@ -1163,9 +1201,9 @@ const AIChatPage: React.FC = () => {
} else { } else {
setLastFrame(mediaRef); setLastFrame(mediaRef);
} }
message.success('图片上传成功'); antdMessage.success('图片上传成功');
} catch (error) { } catch (error) {
message.error('上传失败'); antdMessage.error('上传失败');
} finally { } finally {
setUploading(false); setUploading(false);
setUploadTarget(null); setUploadTarget(null);
@@ -1176,32 +1214,32 @@ const AIChatPage: React.FC = () => {
const isAudio = file.type.startsWith('audio/'); const isAudio = file.type.startsWith('audio/');
if (!isImage && !isVideo && !isAudio) { if (!isImage && !isVideo && !isAudio) {
message.error('仅支持图片、视频或音频文件'); antdMessage.error('仅支持图片、视频或音频文件');
return false; return false;
} }
if (isAudio) { if (isAudio) {
const audioExt = file.name.split('.').pop()?.toLowerCase(); const audioExt = file.name.split('.').pop()?.toLowerCase();
if (!['wav', 'mp3'].includes(audioExt || '')) { if (!['wav', 'mp3'].includes(audioExt || '')) {
message.error('音频仅支持wav和mp3格式'); antdMessage.error('音频仅支持wav和mp3格式');
return false; return false;
} }
} }
if (mediaType === 'image' && (isVideo || isAudio)) { if (mediaType === 'image' && (isVideo || isAudio)) {
message.error('图片模式仅支持上传图片'); antdMessage.error('图片模式仅支持上传图片');
return false; return false;
} }
if (isAudio && mediaType !== 'video') { if (isAudio && mediaType !== 'video') {
message.error('仅视频模式支持上传音频'); antdMessage.error('仅视频模式支持上传音频');
return false; return false;
} }
const maxMB = isVideo ? 100 : (isAudio ? 50 : 10); const maxMB = isVideo ? 100 : (isAudio ? 50 : 10);
const fileTypeText = isVideo ? '视频' : (isAudio ? '音频' : '图片'); const fileTypeText = isVideo ? '视频' : (isAudio ? '音频' : '图片');
if (file.size / 1024 / 1024 > maxMB) { if (file.size / 1024 / 1024 > maxMB) {
message.error(`${fileTypeText}大小不能超过${maxMB}MB`); antdMessage.error(`${fileTypeText}大小不能超过${maxMB}MB`);
return false; return false;
} }
@@ -1211,30 +1249,30 @@ const AIChatPage: React.FC = () => {
const { width, height } = await getImageDimensions(file); const { width, height } = await getImageDimensions(file);
const error = validateImageDimensions(width, height); const error = validateImageDimensions(width, height);
if (error) { if (error) {
message.error(error); antdMessage.error(error);
return false; return false;
} }
} catch { } catch {
message.error('无法读取图片尺寸,请检查文件是否损坏'); antdMessage.error('无法读取图片尺寸,请检查文件是否损坏');
return false; return false;
} }
} }
const imageCount = currentMedia.filter((m) => m.type === 'image').length; const imageCount = currentMedia.filter((m) => m.type === 'image').length;
if (isImage && imageCount >= maxImage) { if (isImage && imageCount >= maxImage) {
message.error(`该引擎最多上传${maxImage}张图片`); antdMessage.error(`该引擎最多上传${maxImage}张图片`);
return false; return false;
} }
const videoCount = currentMedia.filter((m) => m.type === 'video').length; const videoCount = currentMedia.filter((m) => m.type === 'video').length;
if (isVideo && videoCount >= maxVideo) { if (isVideo && videoCount >= maxVideo) {
message.error(`该引擎最多上传${maxVideo}个视频`); antdMessage.error(`该引擎最多上传${maxVideo}个视频`);
return false; return false;
} }
const audioCount = currentMedia.filter((m) => m.type === 'audio').length; const audioCount = currentMedia.filter((m) => m.type === 'audio').length;
if (isAudio && audioCount >= maxAudio) { if (isAudio && audioCount >= maxAudio) {
message.error(`该引擎最多上传${maxAudio}个音频`); antdMessage.error(`该引擎最多上传${maxAudio}个音频`);
return false; return false;
} }
@@ -1244,25 +1282,25 @@ const AIChatPage: React.FC = () => {
try { try {
videoDuration = await getVideoDuration(file); videoDuration = await getVideoDuration(file);
if (videoDuration < 2) { if (videoDuration < 2) {
message.error('视频素材最短不能少于 2 秒'); antdMessage.error('视频素材最短不能少于 2 秒');
return false; return false;
} }
const existingVideoDuration = currentMedia const existingVideoDuration = currentMedia
.filter((m) => m.type === 'video') .filter((m) => m.type === 'video')
.reduce((sum, m) => sum + (m.duration || 0), 0); .reduce((sum, m) => sum + (m.duration || 0), 0);
if (existingVideoDuration + videoDuration > 15) { if (existingVideoDuration + videoDuration > 15) {
message.error(`所有视频素材总时长不能超过 15 秒,当前 ${(existingVideoDuration + videoDuration).toFixed(1)}`); antdMessage.error(`所有视频素材总时长不能超过 15 秒,当前 ${(existingVideoDuration + videoDuration).toFixed(1)}`);
return false; return false;
} }
// 视频尺寸校验 // 视频尺寸校验
const { width, height } = await getVideoDimensions(file); const { width, height } = await getVideoDimensions(file);
const error = validateVideoDimensions(width, height); const error = validateVideoDimensions(width, height);
if (error) { if (error) {
message.error(error); antdMessage.error(error);
return false; return false;
} }
} catch { } catch {
message.error('无法获取视频信息,请检查文件是否损坏'); antdMessage.error('无法获取视频信息,请检查文件是否损坏');
return false; return false;
} }
} }
@@ -1274,18 +1312,18 @@ const AIChatPage: React.FC = () => {
try { try {
audioDuration = await getAudioDuration(file); audioDuration = await getAudioDuration(file);
if (audioDuration < 2) { if (audioDuration < 2) {
message.error('音频素材最短不能少于 2 秒'); antdMessage.error('音频素材最短不能少于 2 秒');
return false; return false;
} }
const existingAudioDuration = currentMedia const existingAudioDuration = currentMedia
.filter((m) => m.type === 'audio') .filter((m) => m.type === 'audio')
.reduce((sum, m) => sum + (m.duration || 0), 0); .reduce((sum, m) => sum + (m.duration || 0), 0);
if (existingAudioDuration + audioDuration > 15) { if (existingAudioDuration + audioDuration > 15) {
message.error(`所有音频素材总时长不能超过 15 秒,当前 ${(existingAudioDuration + audioDuration).toFixed(1)}`); antdMessage.error(`所有音频素材总时长不能超过 15 秒,当前 ${(existingAudioDuration + audioDuration).toFixed(1)}`);
return false; return false;
} }
} catch { } catch {
message.error('无法获取音频信息,请检查文件是否损坏'); antdMessage.error('无法获取音频信息,请检查文件是否损坏');
return false; return false;
} }
} }
@@ -1307,7 +1345,7 @@ const AIChatPage: React.FC = () => {
const labels = generateMediaLabels(newList); const labels = generateMediaLabels(newList);
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] }))); setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
} catch (error) { } catch (error) {
message.error('上传失败'); antdMessage.error('上传失败');
} finally { } finally {
setUploading(false); setUploading(false);
} }
@@ -1321,20 +1359,20 @@ const AIChatPage: React.FC = () => {
const isAudio = file.type.startsWith('audio/'); const isAudio = file.type.startsWith('audio/');
if (!isImage && !isVideo && !isAudio) { if (!isImage && !isVideo && !isAudio) {
message.error('仅支持图片、视频或音频文件'); antdMessage.error('仅支持图片、视频或音频文件');
return false; return false;
} }
const maxMB = isVideo ? 100 : (isAudio ? 50 : 10); const maxMB = isVideo ? 100 : (isAudio ? 50 : 10);
if (file.size / 1024 / 1024 > maxMB) { if (file.size / 1024 / 1024 > maxMB) {
message.error(`${isVideo ? '视频' : (isAudio ? '音频' : '图片')}大小不能超过${maxMB}MB`); antdMessage.error(`${isVideo ? '视频' : (isAudio ? '音频' : '图片')}大小不能超过${maxMB}MB`);
return false; return false;
} }
if (isAudio) { if (isAudio) {
const audioExt = file.name.split('.').pop()?.toLowerCase(); const audioExt = file.name.split('.').pop()?.toLowerCase();
if (!['wav', 'mp3'].includes(audioExt || '')) { if (!['wav', 'mp3'].includes(audioExt || '')) {
message.error('音频仅支持wav和mp3格式'); antdMessage.error('音频仅支持wav和mp3格式');
return false; return false;
} }
} }
@@ -1345,7 +1383,7 @@ const AIChatPage: React.FC = () => {
try { try {
videoDuration = await getVideoDuration(file); videoDuration = await getVideoDuration(file);
if (videoDuration < 2) { if (videoDuration < 2) {
message.error('视频素材最短不能少于 2 秒'); antdMessage.error('视频素材最短不能少于 2 秒');
return false; return false;
} }
const latestMedia = useAppStore.getState().currentMedia; const latestMedia = useAppStore.getState().currentMedia;
@@ -1353,11 +1391,11 @@ const AIChatPage: React.FC = () => {
.filter((m) => m.type === 'video') .filter((m) => m.type === 'video')
.reduce((sum, m) => sum + (m.duration || 0), 0); .reduce((sum, m) => sum + (m.duration || 0), 0);
if (existingVideoDuration + videoDuration > 15) { if (existingVideoDuration + videoDuration > 15) {
message.error(`所有视频素材总时长不能超过 15 秒,当前 ${(existingVideoDuration + videoDuration).toFixed(1)}`); antdMessage.error(`所有视频素材总时长不能超过 15 秒,当前 ${(existingVideoDuration + videoDuration).toFixed(1)}`);
return false; return false;
} }
} catch { } catch {
message.error('无法获取视频信息,请检查文件是否损坏'); antdMessage.error('无法获取视频信息,请检查文件是否损坏');
return false; return false;
} }
} }
@@ -1366,7 +1404,7 @@ const AIChatPage: React.FC = () => {
try { try {
audioDuration = await getAudioDuration(file); audioDuration = await getAudioDuration(file);
if (audioDuration < 2) { if (audioDuration < 2) {
message.error('音频素材最短不能少于 2 秒'); antdMessage.error('音频素材最短不能少于 2 秒');
return false; return false;
} }
const latestMedia = useAppStore.getState().currentMedia; const latestMedia = useAppStore.getState().currentMedia;
@@ -1374,11 +1412,11 @@ const AIChatPage: React.FC = () => {
.filter((m) => m.type === 'audio') .filter((m) => m.type === 'audio')
.reduce((sum, m) => sum + (m.duration || 0), 0); .reduce((sum, m) => sum + (m.duration || 0), 0);
if (existingAudioDuration + audioDuration > 15) { if (existingAudioDuration + audioDuration > 15) {
message.error(`所有音频素材总时长不能超过 15 秒,当前 ${(existingAudioDuration + audioDuration).toFixed(1)}`); antdMessage.error(`所有音频素材总时长不能超过 15 秒,当前 ${(existingAudioDuration + audioDuration).toFixed(1)}`);
return false; return false;
} }
} catch { } catch {
message.error('无法获取音频信息,请检查文件是否损坏'); antdMessage.error('无法获取音频信息,请检查文件是否损坏');
return false; return false;
} }
} }
@@ -1408,7 +1446,7 @@ const AIChatPage: React.FC = () => {
...(pendingMedia.duration !== undefined && { duration: pendingMedia.duration }), ...(pendingMedia.duration !== undefined && { duration: pendingMedia.duration }),
}; };
} catch (error) { } catch (error) {
message.error('上传失败'); antdMessage.error('上传失败');
return false; return false;
} }
}; };
@@ -1436,7 +1474,7 @@ const AIChatPage: React.FC = () => {
} }
if (successCount > 0) { if (successCount > 0) {
message.success(`成功上传${successCount}个文件${failCount > 0 ? `${failCount}个文件上传失败` : ''}`); antdMessage.success(`成功上传${successCount}个文件${failCount > 0 ? `${failCount}个文件上传失败` : ''}`);
} }
}; };
@@ -1450,12 +1488,12 @@ const AIChatPage: React.FC = () => {
if (!incoming.length) return false; if (!incoming.length) return false;
if (mediaType === 'image' && incoming.some((item) => item.type !== 'image')) { if (mediaType === 'image' && incoming.some((item) => item.type !== 'image')) {
message.error('图片模式仅支持添加图片素材'); antdMessage.error('图片模式仅支持添加图片素材');
return false; return false;
} }
if (incoming.some((item) => item.type === 'audio') && mediaType !== 'video') { if (incoming.some((item) => item.type === 'audio') && mediaType !== 'video') {
message.error('仅视频模式支持添加音频素材'); antdMessage.error('仅视频模式支持添加音频素材');
return false; return false;
} }
@@ -1467,15 +1505,15 @@ const AIChatPage: React.FC = () => {
const incomingAudioCount = incoming.filter((m) => m.type === 'audio').length; const incomingAudioCount = incoming.filter((m) => m.type === 'audio').length;
if (imageCount + incomingImageCount > maxImage) { if (imageCount + incomingImageCount > maxImage) {
message.error(`该引擎最多上传${maxImage}张图片,当前还能添加 ${Math.max(0, maxImage - imageCount)}`); antdMessage.error(`该引擎最多上传${maxImage}张图片,当前还能添加 ${Math.max(0, maxImage - imageCount)}`);
return false; return false;
} }
if (videoCount + incomingVideoCount > maxVideo) { if (videoCount + incomingVideoCount > maxVideo) {
message.error(`该引擎最多上传${maxVideo}个视频,当前还能添加 ${Math.max(0, maxVideo - videoCount)}`); antdMessage.error(`该引擎最多上传${maxVideo}个视频,当前还能添加 ${Math.max(0, maxVideo - videoCount)}`);
return false; return false;
} }
if (audioCount + incomingAudioCount > maxAudio) { if (audioCount + incomingAudioCount > maxAudio) {
message.error(`该引擎最多上传${maxAudio}个音频,当前还能添加 ${Math.max(0, maxAudio - audioCount)}`); antdMessage.error(`该引擎最多上传${maxAudio}个音频,当前还能添加 ${Math.max(0, maxAudio - audioCount)}`);
return false; return false;
} }
@@ -1483,28 +1521,28 @@ const AIChatPage: React.FC = () => {
if (item.type !== 'video') continue; if (item.type !== 'video') continue;
const duration = Number(item.duration); const duration = Number(item.duration);
if (!Number.isFinite(duration) || duration <= 0) { if (!Number.isFinite(duration) || duration <= 0) {
message.error(`${item.name || '视频素材'}缺少视频秒数,不能用于 AI 创作`); antdMessage.error(`${item.name || '视频素材'}缺少视频秒数,不能用于 AI 创作`);
return false; return false;
} }
if (duration < 2) { if (duration < 2) {
message.error(`${item.name || '视频素材'}最短不能少于 2 秒`); antdMessage.error(`${item.name || '视频素材'}最短不能少于 2 秒`);
return false; return false;
} }
if (duration > 15) { if (duration > 15) {
message.error(`${item.name || '视频素材'}最长不能超过 15 秒`); antdMessage.error(`${item.name || '视频素材'}最长不能超过 15 秒`);
return false; return false;
} }
} }
const totalVideoDuration = getMediaDurationTotal(baseMedia, 'video') + getMediaDurationTotal(incoming, 'video'); const totalVideoDuration = getMediaDurationTotal(baseMedia, 'video') + getMediaDurationTotal(incoming, 'video');
if (totalVideoDuration > 15) { if (totalVideoDuration > 15) {
message.error(`所有视频素材总时长不能超过 15 秒,当前 ${totalVideoDuration.toFixed(1)}`); antdMessage.error(`所有视频素材总时长不能超过 15 秒,当前 ${totalVideoDuration.toFixed(1)}`);
return false; return false;
} }
const totalAudioDuration = getMediaDurationTotal(baseMedia, 'audio') + getMediaDurationTotal(incoming, 'audio'); const totalAudioDuration = getMediaDurationTotal(baseMedia, 'audio') + getMediaDurationTotal(incoming, 'audio');
if (totalAudioDuration > 15) { if (totalAudioDuration > 15) {
message.error(`所有音频素材总时长不能超过 15 秒,当前 ${totalAudioDuration.toFixed(1)}`); antdMessage.error(`所有音频素材总时长不能超过 15 秒,当前 ${totalAudioDuration.toFixed(1)}`);
return false; return false;
} }
@@ -1517,11 +1555,11 @@ const AIChatPage: React.FC = () => {
const refType = (media?.type || item.resourceType) as 'image' | 'video' | 'audio'; const refType = (media?.type || item.resourceType) as 'image' | 'video' | 'audio';
const url = media?.url || item.resourceUrl || item.displayUrl || item.previewUrl || ''; const url = media?.url || item.resourceUrl || item.displayUrl || item.previewUrl || '';
if (!url) { if (!url) {
message.error(`${item.fileName || item.id} 缺少素材地址,不能用于 AI 创作`); antdMessage.error(`${item.fileName || item.id} 缺少素材地址,不能用于 AI 创作`);
return null; return null;
} }
if (refType === 'audio' && mediaType !== 'video') { if (refType === 'audio' && mediaType !== 'video') {
message.error('仅视频模式支持添加音频素材'); antdMessage.error('仅视频模式支持添加音频素材');
return null; return null;
} }
const duration = Number(media?.duration ?? item.durationSeconds); const duration = Number(media?.duration ?? item.durationSeconds);
@@ -1550,12 +1588,12 @@ const AIChatPage: React.FC = () => {
const newList = [...latestMedia, ...normalized]; const newList = [...latestMedia, ...normalized];
const labels = generateMediaLabels(newList); const labels = generateMediaLabels(newList);
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] }))); setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
message.success(`已添加 ${normalized.length} 个历史上传素材参考`); antdMessage.success(`已添加 ${normalized.length} 个历史上传素材参考`);
}; };
const normalizePrivatePortraitAsset = (asset: PrivatePortraitSelectableAsset): MediaReference | null => { const normalizePrivatePortraitAsset = (asset: PrivatePortraitSelectableAsset): MediaReference | null => {
if (asset.assetType === 'Audio') { if (asset.assetType === 'Audio') {
message.error('音频私域素材暂不支持用于 AI 创作'); antdMessage.error('音频私域素材暂不支持用于 AI 创作');
return null; return null;
} }
@@ -1593,7 +1631,7 @@ const AIChatPage: React.FC = () => {
const newList = [...latestMedia, ...normalized]; const newList = [...latestMedia, ...normalized];
const labels = generateMediaLabels(newList); const labels = generateMediaLabels(newList);
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] }))); setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
message.success(`已添加 ${normalized.length}${privateAssetPickerLibraryType === 'aigc_virtual' ? '虚拟' : '真人'}素材参考`); antdMessage.success(`已添加 ${normalized.length}${privateAssetPickerLibraryType === 'aigc_virtual' ? '虚拟' : '真人'}素材参考`);
}; };
const buildPreviewUrl = (url: string) => { const buildPreviewUrl = (url: string) => {
@@ -2583,7 +2621,7 @@ const AIChatPage: React.FC = () => {
label: '', label: '',
}; };
setFirstFrame(mediaRef); setFirstFrame(mediaRef);
message.success('成功添加首帧'); antdMessage.success('成功添加首帧');
} }
}} }}
onPortraitSelect={(assets) => { onPortraitSelect={(assets) => {
@@ -2598,7 +2636,7 @@ const AIChatPage: React.FC = () => {
label: '', label: '',
}; };
setFirstFrame(mediaRef); setFirstFrame(mediaRef);
message.success('成功添加首帧'); antdMessage.success('成功添加首帧');
} }
}} }}
uploading={uploading} uploading={uploading}
@@ -2673,7 +2711,7 @@ const AIChatPage: React.FC = () => {
label: '', label: '',
}; };
setLastFrame(mediaRef); setLastFrame(mediaRef);
message.success('成功添加尾帧'); antdMessage.success('成功添加尾帧');
} }
}} }}
onPortraitSelect={(assets) => { onPortraitSelect={(assets) => {
@@ -2688,7 +2726,7 @@ const AIChatPage: React.FC = () => {
label: '', label: '',
}; };
setLastFrame(mediaRef); setLastFrame(mediaRef);
message.success('成功添加尾帧'); antdMessage.success('成功添加尾帧');
} }
}} }}
uploading={uploading} uploading={uploading}
@@ -3341,6 +3379,41 @@ const AIChatPage: React.FC = () => {
setVideoDuration(engine.supportedDurations?.[0] || 5); setVideoDuration(engine.supportedDurations?.[0] || 5);
} }
} }
if (mediaType === 'image') {
const supportedSizes = engine.supportedSizes || {};
const resolutionLevels = Object.keys(supportedSizes).sort((a, b) => {
const levelOrder = { '1K': 0, '2K': 1, '4K': 2 };
return (levelOrder[a] || 0) - (levelOrder[b] || 0);
});
const primaryResolution = resolutionLevels[0] || '2K';
const supportedResolutions = Object.keys(supportedSizes[primaryResolution] || {});
const newRatioOptions = supportedResolutions.map((res: any) => ({
value: res,
label: res,
}));
setRatioOptions(newRatioOptions);
setCurrentEngineSupportedSizes(supportedSizes);
setResolutionOptions(resolutionLevels.map((level) => {
const match = level.match(/(\d+)K/);
const num = match ? parseInt(match[1]) : 1;
const labels: Record<number, string> = { 1: '标清', 2: '高清', 4: '超清' };
return {
value: level,
label: `${labels[num] || '高清'} ${level}`,
};
}));
if (supportedSizes[primaryResolution] && supportedSizes[primaryResolution][supportedResolutions[0]]) {
const defaultSize = supportedSizes[primaryResolution][supportedResolutions[0]];
const [w, h] = defaultSize.split(/[×x]/);
setWidth(Number(w));
setHeight(Number(h));
setSelectedRatio(supportedResolutions[0]);
setSelectedResolution(primaryResolution);
}
}
setShowEngineModal(false); setShowEngineModal(false);
setCurrentMedia([]); setCurrentMedia([]);
setInputValue(''); setInputValue('');
+289 -66
View File
@@ -10,6 +10,9 @@ import {
PictureOutlined, PictureOutlined,
ThunderboltOutlined, ThunderboltOutlined,
PlayCircleOutlined, PlayCircleOutlined,
HeartOutlined,
ShareAltOutlined,
StarOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { getmedit ,getHomeCaseHeader,getHomeCaseButton} from '../api'; import { getmedit ,getHomeCaseHeader,getHomeCaseButton} from '../api';
@@ -43,7 +46,7 @@ const HomePage: React.FC = () => {
const [caseAssets, setCaseAssets] = useState<any[]>([]); const [caseAssets, setCaseAssets] = useState<any[]>([]);
const [previewAsset, setPreviewAsset] = useState<any>(null); const [previewAsset, setPreviewAsset] = useState<any>(null);
const previewVideoRef = useRef<HTMLVideoElement>(null); const previewVideoRef = useRef<HTMLVideoElement>(null);
const [activeContentTab, setActiveContentTab] = useState<'works' | 'cases'>('works'); const [activeContentTab, setActiveContentTab] = useState<'works' | 'cases'>('cases');
useEffect(() => { useEffect(() => {
getHomeCaseHeader().then((res: any) => { getHomeCaseHeader().then((res: any) => {
@@ -592,12 +595,13 @@ const HomePage: React.FC = () => {
{/* 外层Tab切换:近期作品 / 素材案例 */} {/* 外层Tab切换:近期作品 / 素材案例 */}
<div style={{ display: 'flex', gap: 8 }}> <div style={{ display: 'flex', gap: 8 }}>
{[ {[
{ key: 'works', label: '近期作品' },
{ key: 'cases', label: '素材案例' }, { key: 'cases', label: '素材案例' },
{ key: 'works', label: '近期作品' },
].map((item) => ( ].map((item) => (
<button <button
key={item.key} key={item.key}
onClick={() => setActiveContentTab(item.key as 'works' | 'cases')} onClick={() => setActiveContentTab(item.key as 'cases' | 'works')}
style={{ style={{
padding: '6px 16px', padding: '6px 16px',
borderRadius: 8, borderRadius: 8,
@@ -790,10 +794,10 @@ const HomePage: React.FC = () => {
</div> </div>
{/* 素材案例网格 */} {/* 素材案例网格 */}
<div className="stagger-children" style={{ display: 'grid', gridTemplateColumns: 'repeat(5, 1fr)', gap: 16 }}> <div className="stagger-children" style={{ display: 'flex', flexWrap: 'wrap', gap: 16, justifyContent: 'space-between', overflowX: 'auto', paddingBottom: 8 }}>
{caseAssets.length === 0 ? ( {caseAssets.length === 0 ? (
<div style={{ <div style={{
gridColumn: '1 / -1', flex: 1,
padding: '60px 0', padding: '60px 0',
textAlign: 'center', textAlign: 'center',
color: '#94a3b8', color: '#94a3b8',
@@ -811,11 +815,12 @@ const HomePage: React.FC = () => {
borderRadius: 12, borderRadius: 12,
overflow: 'hidden', overflow: 'hidden',
cursor: 'pointer', cursor: 'pointer',
background: '#fff', // background: '#0f172a',
border: '1px solid #e2e8f0', flexShrink: 0,
width: "18%",
}} }}
> >
<div style={{ position: 'relative', aspectRatio: '16/9', }}> <div style={{ position: 'relative', aspectRatio: '9/16', }}>
{asset.mediaType === 'video' ? ( {asset.mediaType === 'video' ? (
<> <>
<video <video
@@ -830,9 +835,18 @@ const HomePage: React.FC = () => {
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
background: 'rgba(0,0,0,0.2)',
}}> }}>
<VideoCameraOutlined style={{ fontSize: 28, color: '#fff' }} /> <div style={{
width: 40,
height: 40,
borderRadius: '50%',
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<PlayCircleOutlined style={{ fontSize: 24, color: '#fff' }} />
</div>
</div> </div>
</> </>
) : ( ) : (
@@ -842,22 +856,43 @@ const HomePage: React.FC = () => {
style={{ width: '100%', height: '100%', objectFit: 'cover' }} style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/> />
)} )}
{/* {asset.mediaType === 'video' && (
<div style={{
position: 'absolute',
top: 8,
right: 8,
fontSize: 10,
color: '#fff',
background: 'rgba(0,0,0,0.6)',
padding: '2px 6px',
borderRadius: 4,
}}>
AI生成
</div>
)} */}
</div> </div>
<div style={{ {/* <div style={{ padding: '8px 10px' }}>
padding: '10px 12px',
background: '#f8fafc',
}}>
<div style={{ <div style={{
fontSize: 12, fontSize: 11,
color: '#64748b', color: '#94a3b8',
textAlign: 'center',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
marginBottom: 4,
}}> }}>
{asset.title || `素材 ${index + 1}`} {asset.title || `素材 ${index + 1}`}
</div> </div>
</div> <div style={{
display: 'flex',
alignItems: 'center',
gap: 4,
fontSize: 10,
color: '#94a3b8',
}}>
<HeartOutlined style={{ fontSize: 12 }} />
<span>{asset.likes || Math.floor(Math.random() * 1000)}</span>
</div>
</div> */}
</div> </div>
))} ))}
</div> </div>
@@ -873,60 +908,248 @@ const HomePage: React.FC = () => {
setPreviewAsset(null); setPreviewAsset(null);
}} }}
footer={null} footer={null}
width={760} width={900}
centered centered
className="preview-modal" className="preview-modal"
bodyStyle={{ style={{ borderRadius: 16, overflow: 'hidden' }}
padding: 0,
background: '#fff',
borderRadius: 16,
overflow: 'hidden',
}}
> >
{/* 固定比例容器 16:9 */} <div style={{ display: 'flex', height: 580 }}>
<div style={{ {/* 左侧:素材预览 */}
width: '100%', <div style={{ flex: 1, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
paddingTop: '56.25%', <div style={{ width: 280, aspectRatio: '9/16', borderRadius: 12, overflow: 'hidden', background: '#0f172a' }}>
position: 'relative', {previewAsset?.mediaType === 'video' ? (
// background: '#000', <video
}}> ref={previewVideoRef}
<div style={{ src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset.url}`}
position: 'absolute', controls
inset: 0, autoPlay
display: 'flex', style={{ width: '100%', height: '100%', objectFit: 'cover' }}
alignItems: 'center', webkit-playsinline="true"
justifyContent: 'center', />
}}> ) : (
{previewAsset?.mediaType === 'video' ? ( <img
<video src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset?.url}`}
ref={previewVideoRef} alt={previewAsset?.title}
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset.url}`} style={{ width: '100%', height: '100%', objectFit: 'cover' }}
controls />
autoPlay )}
style={{ width: '100%', height: '100%', objectFit: 'contain' }} {previewAsset?.mediaType === 'video' && (
/> <div style={{
) : ( position: 'absolute',
<img top: 32,
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset?.url}`} right: 32,
alt={previewAsset?.title} fontSize: 11,
style={{ width: '100%', height: '100%', objectFit: 'contain' }} color: '#fff',
/> background: 'rgba(0,0,0,0.6)',
)} padding: '3px 8px',
borderRadius: 4,
}}>
AI生成
</div>
)}
</div>
</div>
{/* 右侧:创意详情 */}
<div style={{ flex: 1, padding: 24, overflowY: 'auto' }}>
<div style={{ fontSize: 16, fontWeight: 600, color: '#fff', marginBottom: 16 }}>
</div>
{/* 热度 */}
{/* <div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 16 }}>
<div style={{ fontSize: 12, color: '#94a3b8' }}>
<span style={{ color: '#f59e0b' }}>热度:</span>
{previewAsset?.likes || Math.floor(Math.random() * 5000)}
</div>
<div style={{ fontSize: 12, color: '#94a3b8' }}>
<span style={{ color: '#6366f1' }}>热度:</span>
{Math.floor(Math.random() * 1000)}
</div>
</div> */}
{/* 视频提示词 */}
<div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 12, color: '#000000ff', marginBottom: 8 }}></div>
<div style={{ fontSize: 13, color: '#000000ff', lineHeight: 1.6 }}>
{previewAsset?.prompt || '动态描述:女性抬手整理头发,随后手持口服液用手指向产品讲解;画面切换为双手将口服液中的棕黄色液体缓缓倒入透明玻璃杯;再次切换女性讲解画面,双手做出展示动作指向产品;最后双手在胸前做出托手姿势后摊开手掌微笑描述:女性讲解的...'}
</div>
</div>
{/* 视频参考图 */}
<div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 8 }}></div>
<div style={{ display: 'flex', gap: 8 }}>
<div style={{ width: 80, height: 80, borderRadius: 8, overflow: 'hidden', cursor: 'pointer' }}>
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset?.url}`}
alt="参考图"
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
</div>
<div style={{
width: 80,
height: 80,
borderRadius: 8,
border: '1px dashed #475569',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#64748b',
fontSize: 12,
}}>
</div>
</div>
</div>
{/* 口播脚本台词 */}
<div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 12, color: '#000000ff', marginBottom: 8 }}></div>
<div style={{ fontSize: 13, color: '#000000ff', lineHeight: 1.6 }}>
{previewAsset?.script || '还在为宝宝不爱喝水发愁?试试这款天然果蔬汁!零添加糖分,维生素满满,口感清甜宝宝超爱喝。现在下单还送专属吸管杯,手慢无!点下方链接把健康带回家~'}
</div>
{/* <div style={{
fontSize: 12,
color: '#6366f1',
marginTop: 8,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 4,
}}>
完整内容女声
</div> */}
</div>
{/* 视频标签 */}
{/* <div style={{ marginBottom: 16 }}>
<div style={{ fontSize: 12, color: '#94a3b8', marginBottom: 8 }}>视频标签</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
{previewAsset?.tags?.split?.(',')?.map((tag: string, i: number) => (
<span
key={i}
style={{
padding: '4px 10px',
borderRadius: 16,
background: '#334155',
color: '#94a3b8',
fontSize: 11,
}}
>
{tag.trim()}
</span>
)) || ['互联网电商服务', '美妆', '美妆', '服装配饰', '母婴宠物', '带货主播', '口播'].map((tag, i) => (
<span
key={i}
style={{
padding: '4px 10px',
borderRadius: 16,
background: '#334155',
color: '#94a3b8',
fontSize: 11,
}}
>
{tag}
</span>
))}
</div>
</div> */}
</div> </div>
</div> </div>
{/* 底部标题栏 */}
{previewAsset?.title && ( {/* 底部操作栏 */}
<div style={{ <div style={{
padding: '14px 20px', padding: '16px 24px',
fontSize: 14, borderTop: '1px solid #334155',
color: '#4b5563', display: 'flex',
fontWeight: 500, alignItems: 'center',
borderTop: '1px solid #f1f5f9', justifyContent: 'flex-end',
textAlign: 'center', }}>
}}> {/* <div style={{ display: 'flex', gap: 16 }}>
{previewAsset.title} <button
onClick={() => message.info('收藏功能开发中')}
style={{
width: 36,
height: 36,
borderRadius: '50%',
border: 'none',
background: '#334155',
color: '#94a3b8',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.background = '#475569';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.background = '#334155';
}}
>
<StarOutlined style={{ fontSize: 16 }} />
</button>
<button
onClick={() => message.info('分享功能开发中')}
style={{
width: 36,
height: 36,
borderRadius: '50%',
border: 'none',
background: '#334155',
color: '#000000ff',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s',
}}
onMouseEnter={(e) => {
(e.currentTarget as HTMLElement).style.background = '#475569';
}}
onMouseLeave={(e) => {
(e.currentTarget as HTMLElement).style.background = '#334155';
}}
>
<ShareAltOutlined style={{ fontSize: 16 }} />
</button>
</div> */}
<div style={{ display: 'flex', gap: 12 }}>
{previewAsset?.mediaType !== 'video' && (
<Button
// onClick={() => navigate('/generate')}
style={{
padding: '8px 24px',
borderRadius: 8,
background: '#3b82f6',
border: 'none',
color: '#fff',
fontSize: 13,
fontWeight: 500,
}}
>
AI创作
</Button>
)}
<Button
// onClick={() => navigate('/initial')}
style={{
padding: '8px 24px',
borderRadius: 8,
background: '#8b5cf6',
border: 'none',
color: '#fff',
fontSize: 13,
fontWeight: 500,
}}
>
</Button>
</div> </div>
)} </div>
</Modal> </Modal>
</div> </div>
); );