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`);
}
// 删除拆镜项目
export async function deleteShotReplicationProject(projectId: string): Promise<void> {
await api.delete(`/shot-replications/projects/${projectId}`);
export async function deleteShotReplicationProject(taskSetId: string): Promise<void> {
await api.delete(`/shot-replications/task-sets/${taskSetId}`);
}
// 删除爆款开头复刻任务
export async function deleteHotOpeningReplicationTask(taskId: string): Promise<void> {
@@ -109,7 +109,11 @@ const UploadSelector: React.FC<UploadSelectorProps> = ({
}}
>
<FolderOpenOutlined style={{ fontSize: 14, color: '#64748b' }} />
<span style={{ fontSize: 14, color: '#334155' }}>{multiple ? '本地上传(可多选拖拽)' : '本地上传'}</span>
<span style={{ fontSize: 14, color: '#334155' }}>
{/* {multiple ? '本地上传(可多选拖拽)' : '本地上传'} */}
</span>
</div>
<div
onClick={handleHistorySelect}
@@ -218,18 +218,18 @@ const UploadResourceHistoryPanel: React.FC = () => {
<div style={{ fontSize: 18, fontWeight: 700, color: '#1e293b' }}>{group.generatedDate}</div>
<Text type="secondary"> {group.total} </Text>
</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) => {
const checked = selectedIds.has(item.id);
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' }}>
{renderMedia(item)}
<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>
</div>
<div style={{ padding: 12 }}>
<Text ellipsis title={item.fileName || item.id} style={{ display: 'block', fontWeight: 600, color: '#334155' }}>{item.fileName || item.id}</Text>
<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> */}
<Space size={4} wrap style={{ marginTop: 8 }}>
<Tag style={{ margin: 0 }}>{item.moduleLabel}</Tag>
<Tag style={{ margin: 0 }}>{bytesText(item.fileSizeBytes)}</Tag>
@@ -246,7 +246,7 @@ const UploadResourceHistoryPanel: React.FC = () => {
})}
</div>
{group.items.length < group.total && (
<div style={{ textAlign: 'center', marginTop: 14 }}>
<div style={{ textAlign: 'left', marginTop: 14 }}>
<Button onClick={() => handleLoadMoreDay(group)}></Button>
</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); }} />
</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}
</Modal>
</div>
@@ -177,7 +177,8 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
{items.length === 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' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 14 }}>
{items.map((item) => {
const checked = checkedMap.has(item.id);
const disabled = selectedIdSet.has(item.id);
@@ -228,10 +229,7 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
);
})}
</div>
)}
</Spin>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16 }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 16, paddingBottom: 8 }}>
<Pagination
current={page}
pageSize={pageSize}
@@ -244,6 +242,9 @@ const UploadResourceHistoryPicker: React.FC<UploadResourceHistoryPickerProps> =
}}
/>
</div>
</div>
)}
</Spin>
</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={{ 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 ? (
<Spin size="large" tip="加载中" style={{ color: '#fff' }} />
<Spin size="large" description="加载中" style={{ color: '#fff' }} />
) : (
<ClockCircleOutlined style={{ fontSize: 40, color: '#fff' }} />
)}
+175 -102
View File
@@ -14,6 +14,7 @@ import {
Tooltip,
Popconfirm,
Modal,
App,
} from 'antd';
import bg1 from '../assets/bg1.png';
@@ -122,6 +123,7 @@ const mockUpload = (file: File): Promise<{ url: string }> => {
const AIChatPage: React.FC = () => {
// ==================== 状态定义 ====================
const { message: antdMessage } = App.useApp();
const [collapsed, setCollapsed] = useState<boolean>(false);
@@ -355,9 +357,7 @@ const AIChatPage: React.FC = () => {
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
const [ratioOptions, setRatioOptions] = useState([]);
const [resolutionOptions, setResolutionOptions] = useState([]);
const [widthandheight, setWidthandHeight] = useState([]);
const [blindex, setBlindex] = useState<any>(0);
const [fblindex, setFBlindex] = useState<any>(0);
const [currentEngineSupportedSizes, setCurrentEngineSupportedSizes] = useState<Record<string, Record<string, string>>>({});
const [showEngineModal, setShowEngineModal] = useState(false);
const [showMediaTypeModal, setShowMediaTypeModal] = useState(false);
@@ -579,32 +579,48 @@ const AIChatPage: React.FC = () => {
}
}
let supportedSizes = (data as any).engine?.image?.[0]?.supportedSizes || {};
let supportedResolutions: string[] = [];
let twokwidth: string[] = [];
let fourkwidth: string[] = [];
if (data.engine.image && data.engine.image.length > 0) {
const savedImageEngine = data.engine.image.find((e: any) => e.id === countType);
const targetImageEngine = savedImageEngine || data.engine.image[0];
for (let key in supportedSizes["2K"]) {
supportedResolutions.push(key);
twokwidth.push(supportedSizes["2K"][key]);
}
const supportedSizes = targetImageEngine.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}`,
};
}));
for (let key in supportedSizes["4K"]) {
fourkwidth.push(supportedSizes["4K"][key]);
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);
}
const newWidthandHeight = [twokwidth, fourkwidth];
setWidthandHeight(newWidthandHeight);
setResolutionOptions([
{ value: '2K', label: '高清 2K' },
{ value: '4K', label: '超清 4K' },
]);
if (countType === '请选择') {
setCountType(targetImageEngine.id);
}
}
})
.catch(() => {
});
@@ -726,34 +742,56 @@ const AIChatPage: React.FC = () => {
return () => document.removeEventListener("click", handleClickOutside);
}, [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) => {
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) => {
setFBlindex(parseInt(ratio === '2K' ? '0' : '1'));
const calculateSizeFromRatiotwo = (resolution: string) => {
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);
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 (!inputValue.trim()) {
message.warning('请输入内容');
antdMessage.warning('请输入内容');
return;
}
} else {
if (!inputValue.trim() && currentMedia.length === 0) {
message.warning('请输入内容或上传图片/视频');
antdMessage.warning('请输入内容或上传图片/视频');
return;
}
}
@@ -954,7 +992,7 @@ const AIChatPage: React.FC = () => {
errorMessage = error.message;
}
message.error({
antdMessage.error({
content: errorMessage,
duration: 3,
});
@@ -1136,16 +1174,16 @@ const AIChatPage: React.FC = () => {
if (mediaType === 'video' && referenceMode === 'first_last_frame') {
if (!isImage) {
message.error('首尾帧模式仅支持上传图片');
antdMessage.error('首尾帧模式仅支持上传图片');
return false;
}
if (file.size / 1024 / 1024 > 10) {
message.error('图片大小不能超过10MB');
antdMessage.error('图片大小不能超过10MB');
return false;
}
const effectiveUploadTarget = frameTarget || uploadTarget;
if (!effectiveUploadTarget) {
message.error('请选择首帧或尾帧上传位置');
antdMessage.error('请选择首帧或尾帧上传位置');
return false;
}
@@ -1163,9 +1201,9 @@ const AIChatPage: React.FC = () => {
} else {
setLastFrame(mediaRef);
}
message.success('图片上传成功');
antdMessage.success('图片上传成功');
} catch (error) {
message.error('上传失败');
antdMessage.error('上传失败');
} finally {
setUploading(false);
setUploadTarget(null);
@@ -1176,32 +1214,32 @@ const AIChatPage: React.FC = () => {
const isAudio = file.type.startsWith('audio/');
if (!isImage && !isVideo && !isAudio) {
message.error('仅支持图片、视频或音频文件');
antdMessage.error('仅支持图片、视频或音频文件');
return false;
}
if (isAudio) {
const audioExt = file.name.split('.').pop()?.toLowerCase();
if (!['wav', 'mp3'].includes(audioExt || '')) {
message.error('音频仅支持wav和mp3格式');
antdMessage.error('音频仅支持wav和mp3格式');
return false;
}
}
if (mediaType === 'image' && (isVideo || isAudio)) {
message.error('图片模式仅支持上传图片');
antdMessage.error('图片模式仅支持上传图片');
return false;
}
if (isAudio && mediaType !== 'video') {
message.error('仅视频模式支持上传音频');
antdMessage.error('仅视频模式支持上传音频');
return false;
}
const maxMB = isVideo ? 100 : (isAudio ? 50 : 10);
const fileTypeText = isVideo ? '视频' : (isAudio ? '音频' : '图片');
if (file.size / 1024 / 1024 > maxMB) {
message.error(`${fileTypeText}大小不能超过${maxMB}MB`);
antdMessage.error(`${fileTypeText}大小不能超过${maxMB}MB`);
return false;
}
@@ -1211,30 +1249,30 @@ const AIChatPage: React.FC = () => {
const { width, height } = await getImageDimensions(file);
const error = validateImageDimensions(width, height);
if (error) {
message.error(error);
antdMessage.error(error);
return false;
}
} catch {
message.error('无法读取图片尺寸,请检查文件是否损坏');
antdMessage.error('无法读取图片尺寸,请检查文件是否损坏');
return false;
}
}
const imageCount = currentMedia.filter((m) => m.type === 'image').length;
if (isImage && imageCount >= maxImage) {
message.error(`该引擎最多上传${maxImage}张图片`);
antdMessage.error(`该引擎最多上传${maxImage}张图片`);
return false;
}
const videoCount = currentMedia.filter((m) => m.type === 'video').length;
if (isVideo && videoCount >= maxVideo) {
message.error(`该引擎最多上传${maxVideo}个视频`);
antdMessage.error(`该引擎最多上传${maxVideo}个视频`);
return false;
}
const audioCount = currentMedia.filter((m) => m.type === 'audio').length;
if (isAudio && audioCount >= maxAudio) {
message.error(`该引擎最多上传${maxAudio}个音频`);
antdMessage.error(`该引擎最多上传${maxAudio}个音频`);
return false;
}
@@ -1244,25 +1282,25 @@ const AIChatPage: React.FC = () => {
try {
videoDuration = await getVideoDuration(file);
if (videoDuration < 2) {
message.error('视频素材最短不能少于 2 秒');
antdMessage.error('视频素材最短不能少于 2 秒');
return false;
}
const existingVideoDuration = currentMedia
.filter((m) => m.type === 'video')
.reduce((sum, m) => sum + (m.duration || 0), 0);
if (existingVideoDuration + videoDuration > 15) {
message.error(`所有视频素材总时长不能超过 15 秒,当前 ${(existingVideoDuration + videoDuration).toFixed(1)}`);
antdMessage.error(`所有视频素材总时长不能超过 15 秒,当前 ${(existingVideoDuration + videoDuration).toFixed(1)}`);
return false;
}
// 视频尺寸校验
const { width, height } = await getVideoDimensions(file);
const error = validateVideoDimensions(width, height);
if (error) {
message.error(error);
antdMessage.error(error);
return false;
}
} catch {
message.error('无法获取视频信息,请检查文件是否损坏');
antdMessage.error('无法获取视频信息,请检查文件是否损坏');
return false;
}
}
@@ -1274,18 +1312,18 @@ const AIChatPage: React.FC = () => {
try {
audioDuration = await getAudioDuration(file);
if (audioDuration < 2) {
message.error('音频素材最短不能少于 2 秒');
antdMessage.error('音频素材最短不能少于 2 秒');
return false;
}
const existingAudioDuration = currentMedia
.filter((m) => m.type === 'audio')
.reduce((sum, m) => sum + (m.duration || 0), 0);
if (existingAudioDuration + audioDuration > 15) {
message.error(`所有音频素材总时长不能超过 15 秒,当前 ${(existingAudioDuration + audioDuration).toFixed(1)}`);
antdMessage.error(`所有音频素材总时长不能超过 15 秒,当前 ${(existingAudioDuration + audioDuration).toFixed(1)}`);
return false;
}
} catch {
message.error('无法获取音频信息,请检查文件是否损坏');
antdMessage.error('无法获取音频信息,请检查文件是否损坏');
return false;
}
}
@@ -1307,7 +1345,7 @@ const AIChatPage: React.FC = () => {
const labels = generateMediaLabels(newList);
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
} catch (error) {
message.error('上传失败');
antdMessage.error('上传失败');
} finally {
setUploading(false);
}
@@ -1321,20 +1359,20 @@ const AIChatPage: React.FC = () => {
const isAudio = file.type.startsWith('audio/');
if (!isImage && !isVideo && !isAudio) {
message.error('仅支持图片、视频或音频文件');
antdMessage.error('仅支持图片、视频或音频文件');
return false;
}
const maxMB = isVideo ? 100 : (isAudio ? 50 : 10);
if (file.size / 1024 / 1024 > maxMB) {
message.error(`${isVideo ? '视频' : (isAudio ? '音频' : '图片')}大小不能超过${maxMB}MB`);
antdMessage.error(`${isVideo ? '视频' : (isAudio ? '音频' : '图片')}大小不能超过${maxMB}MB`);
return false;
}
if (isAudio) {
const audioExt = file.name.split('.').pop()?.toLowerCase();
if (!['wav', 'mp3'].includes(audioExt || '')) {
message.error('音频仅支持wav和mp3格式');
antdMessage.error('音频仅支持wav和mp3格式');
return false;
}
}
@@ -1345,7 +1383,7 @@ const AIChatPage: React.FC = () => {
try {
videoDuration = await getVideoDuration(file);
if (videoDuration < 2) {
message.error('视频素材最短不能少于 2 秒');
antdMessage.error('视频素材最短不能少于 2 秒');
return false;
}
const latestMedia = useAppStore.getState().currentMedia;
@@ -1353,11 +1391,11 @@ const AIChatPage: React.FC = () => {
.filter((m) => m.type === 'video')
.reduce((sum, m) => sum + (m.duration || 0), 0);
if (existingVideoDuration + videoDuration > 15) {
message.error(`所有视频素材总时长不能超过 15 秒,当前 ${(existingVideoDuration + videoDuration).toFixed(1)}`);
antdMessage.error(`所有视频素材总时长不能超过 15 秒,当前 ${(existingVideoDuration + videoDuration).toFixed(1)}`);
return false;
}
} catch {
message.error('无法获取视频信息,请检查文件是否损坏');
antdMessage.error('无法获取视频信息,请检查文件是否损坏');
return false;
}
}
@@ -1366,7 +1404,7 @@ const AIChatPage: React.FC = () => {
try {
audioDuration = await getAudioDuration(file);
if (audioDuration < 2) {
message.error('音频素材最短不能少于 2 秒');
antdMessage.error('音频素材最短不能少于 2 秒');
return false;
}
const latestMedia = useAppStore.getState().currentMedia;
@@ -1374,11 +1412,11 @@ const AIChatPage: React.FC = () => {
.filter((m) => m.type === 'audio')
.reduce((sum, m) => sum + (m.duration || 0), 0);
if (existingAudioDuration + audioDuration > 15) {
message.error(`所有音频素材总时长不能超过 15 秒,当前 ${(existingAudioDuration + audioDuration).toFixed(1)}`);
antdMessage.error(`所有音频素材总时长不能超过 15 秒,当前 ${(existingAudioDuration + audioDuration).toFixed(1)}`);
return false;
}
} catch {
message.error('无法获取音频信息,请检查文件是否损坏');
antdMessage.error('无法获取音频信息,请检查文件是否损坏');
return false;
}
}
@@ -1408,7 +1446,7 @@ const AIChatPage: React.FC = () => {
...(pendingMedia.duration !== undefined && { duration: pendingMedia.duration }),
};
} catch (error) {
message.error('上传失败');
antdMessage.error('上传失败');
return false;
}
};
@@ -1436,7 +1474,7 @@ const AIChatPage: React.FC = () => {
}
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 (mediaType === 'image' && incoming.some((item) => item.type !== 'image')) {
message.error('图片模式仅支持添加图片素材');
antdMessage.error('图片模式仅支持添加图片素材');
return false;
}
if (incoming.some((item) => item.type === 'audio') && mediaType !== 'video') {
message.error('仅视频模式支持添加音频素材');
antdMessage.error('仅视频模式支持添加音频素材');
return false;
}
@@ -1467,15 +1505,15 @@ const AIChatPage: React.FC = () => {
const incomingAudioCount = incoming.filter((m) => m.type === 'audio').length;
if (imageCount + incomingImageCount > maxImage) {
message.error(`该引擎最多上传${maxImage}张图片,当前还能添加 ${Math.max(0, maxImage - imageCount)}`);
antdMessage.error(`该引擎最多上传${maxImage}张图片,当前还能添加 ${Math.max(0, maxImage - imageCount)}`);
return false;
}
if (videoCount + incomingVideoCount > maxVideo) {
message.error(`该引擎最多上传${maxVideo}个视频,当前还能添加 ${Math.max(0, maxVideo - videoCount)}`);
antdMessage.error(`该引擎最多上传${maxVideo}个视频,当前还能添加 ${Math.max(0, maxVideo - videoCount)}`);
return false;
}
if (audioCount + incomingAudioCount > maxAudio) {
message.error(`该引擎最多上传${maxAudio}个音频,当前还能添加 ${Math.max(0, maxAudio - audioCount)}`);
antdMessage.error(`该引擎最多上传${maxAudio}个音频,当前还能添加 ${Math.max(0, maxAudio - audioCount)}`);
return false;
}
@@ -1483,28 +1521,28 @@ const AIChatPage: React.FC = () => {
if (item.type !== 'video') continue;
const duration = Number(item.duration);
if (!Number.isFinite(duration) || duration <= 0) {
message.error(`${item.name || '视频素材'}缺少视频秒数,不能用于 AI 创作`);
antdMessage.error(`${item.name || '视频素材'}缺少视频秒数,不能用于 AI 创作`);
return false;
}
if (duration < 2) {
message.error(`${item.name || '视频素材'}最短不能少于 2 秒`);
antdMessage.error(`${item.name || '视频素材'}最短不能少于 2 秒`);
return false;
}
if (duration > 15) {
message.error(`${item.name || '视频素材'}最长不能超过 15 秒`);
antdMessage.error(`${item.name || '视频素材'}最长不能超过 15 秒`);
return false;
}
}
const totalVideoDuration = getMediaDurationTotal(baseMedia, 'video') + getMediaDurationTotal(incoming, 'video');
if (totalVideoDuration > 15) {
message.error(`所有视频素材总时长不能超过 15 秒,当前 ${totalVideoDuration.toFixed(1)}`);
antdMessage.error(`所有视频素材总时长不能超过 15 秒,当前 ${totalVideoDuration.toFixed(1)}`);
return false;
}
const totalAudioDuration = getMediaDurationTotal(baseMedia, 'audio') + getMediaDurationTotal(incoming, 'audio');
if (totalAudioDuration > 15) {
message.error(`所有音频素材总时长不能超过 15 秒,当前 ${totalAudioDuration.toFixed(1)}`);
antdMessage.error(`所有音频素材总时长不能超过 15 秒,当前 ${totalAudioDuration.toFixed(1)}`);
return false;
}
@@ -1517,11 +1555,11 @@ const AIChatPage: React.FC = () => {
const refType = (media?.type || item.resourceType) as 'image' | 'video' | 'audio';
const url = media?.url || item.resourceUrl || item.displayUrl || item.previewUrl || '';
if (!url) {
message.error(`${item.fileName || item.id} 缺少素材地址,不能用于 AI 创作`);
antdMessage.error(`${item.fileName || item.id} 缺少素材地址,不能用于 AI 创作`);
return null;
}
if (refType === 'audio' && mediaType !== 'video') {
message.error('仅视频模式支持添加音频素材');
antdMessage.error('仅视频模式支持添加音频素材');
return null;
}
const duration = Number(media?.duration ?? item.durationSeconds);
@@ -1550,12 +1588,12 @@ const AIChatPage: React.FC = () => {
const newList = [...latestMedia, ...normalized];
const labels = generateMediaLabels(newList);
setCurrentMedia(newList.map((m, i) => ({ ...m, label: labels[i] })));
message.success(`已添加 ${normalized.length} 个历史上传素材参考`);
antdMessage.success(`已添加 ${normalized.length} 个历史上传素材参考`);
};
const normalizePrivatePortraitAsset = (asset: PrivatePortraitSelectableAsset): MediaReference | null => {
if (asset.assetType === 'Audio') {
message.error('音频私域素材暂不支持用于 AI 创作');
antdMessage.error('音频私域素材暂不支持用于 AI 创作');
return null;
}
@@ -1593,7 +1631,7 @@ const AIChatPage: React.FC = () => {
const newList = [...latestMedia, ...normalized];
const labels = generateMediaLabels(newList);
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) => {
@@ -2583,7 +2621,7 @@ const AIChatPage: React.FC = () => {
label: '',
};
setFirstFrame(mediaRef);
message.success('成功添加首帧');
antdMessage.success('成功添加首帧');
}
}}
onPortraitSelect={(assets) => {
@@ -2598,7 +2636,7 @@ const AIChatPage: React.FC = () => {
label: '',
};
setFirstFrame(mediaRef);
message.success('成功添加首帧');
antdMessage.success('成功添加首帧');
}
}}
uploading={uploading}
@@ -2673,7 +2711,7 @@ const AIChatPage: React.FC = () => {
label: '',
};
setLastFrame(mediaRef);
message.success('成功添加尾帧');
antdMessage.success('成功添加尾帧');
}
}}
onPortraitSelect={(assets) => {
@@ -2688,7 +2726,7 @@ const AIChatPage: React.FC = () => {
label: '',
};
setLastFrame(mediaRef);
message.success('成功添加尾帧');
antdMessage.success('成功添加尾帧');
}
}}
uploading={uploading}
@@ -3341,6 +3379,41 @@ const AIChatPage: React.FC = () => {
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);
setCurrentMedia([]);
setInputValue('');
+273 -50
View File
@@ -10,6 +10,9 @@ import {
PictureOutlined,
ThunderboltOutlined,
PlayCircleOutlined,
HeartOutlined,
ShareAltOutlined,
StarOutlined,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { getmedit ,getHomeCaseHeader,getHomeCaseButton} from '../api';
@@ -43,7 +46,7 @@ const HomePage: React.FC = () => {
const [caseAssets, setCaseAssets] = useState<any[]>([]);
const [previewAsset, setPreviewAsset] = useState<any>(null);
const previewVideoRef = useRef<HTMLVideoElement>(null);
const [activeContentTab, setActiveContentTab] = useState<'works' | 'cases'>('works');
const [activeContentTab, setActiveContentTab] = useState<'works' | 'cases'>('cases');
useEffect(() => {
getHomeCaseHeader().then((res: any) => {
@@ -592,12 +595,13 @@ const HomePage: React.FC = () => {
{/* 外层Tab切换:近期作品 / 素材案例 */}
<div style={{ display: 'flex', gap: 8 }}>
{[
{ key: 'works', label: '近期作品' },
{ key: 'cases', label: '素材案例' },
{ key: 'works', label: '近期作品' },
].map((item) => (
<button
key={item.key}
onClick={() => setActiveContentTab(item.key as 'works' | 'cases')}
onClick={() => setActiveContentTab(item.key as 'cases' | 'works')}
style={{
padding: '6px 16px',
borderRadius: 8,
@@ -790,10 +794,10 @@ const HomePage: React.FC = () => {
</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 ? (
<div style={{
gridColumn: '1 / -1',
flex: 1,
padding: '60px 0',
textAlign: 'center',
color: '#94a3b8',
@@ -811,11 +815,12 @@ const HomePage: React.FC = () => {
borderRadius: 12,
overflow: 'hidden',
cursor: 'pointer',
background: '#fff',
border: '1px solid #e2e8f0',
// background: '#0f172a',
flexShrink: 0,
width: "18%",
}}
>
<div style={{ position: 'relative', aspectRatio: '16/9', }}>
<div style={{ position: 'relative', aspectRatio: '9/16', }}>
{asset.mediaType === 'video' ? (
<>
<video
@@ -830,9 +835,18 @@ const HomePage: React.FC = () => {
display: 'flex',
alignItems: '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>
</>
) : (
@@ -842,22 +856,43 @@ const HomePage: React.FC = () => {
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
)}
</div>
{/* {asset.mediaType === 'video' && (
<div style={{
padding: '10px 12px',
background: '#f8fafc',
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 style={{ padding: '8px 10px' }}>
<div style={{
fontSize: 12,
color: '#64748b',
textAlign: 'center',
fontSize: 11,
color: '#94a3b8',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
marginBottom: 4,
}}>
{asset.title || `素材 ${index + 1}`}
</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>
@@ -873,60 +908,248 @@ const HomePage: React.FC = () => {
setPreviewAsset(null);
}}
footer={null}
width={760}
width={900}
centered
className="preview-modal"
bodyStyle={{
padding: 0,
background: '#fff',
borderRadius: 16,
overflow: 'hidden',
}}
style={{ borderRadius: 16, overflow: 'hidden' }}
>
{/* 固定比例容器 16:9 */}
<div style={{
width: '100%',
paddingTop: '56.25%',
position: 'relative',
// background: '#000',
}}>
<div style={{
position: 'absolute',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}>
<div style={{ display: 'flex', height: 580 }}>
{/* 左侧:素材预览 */}
<div style={{ flex: 1, position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
<div style={{ width: 280, aspectRatio: '9/16', borderRadius: 12, overflow: 'hidden', background: '#0f172a' }}>
{previewAsset?.mediaType === 'video' ? (
<video
ref={previewVideoRef}
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset.url}`}
controls
autoPlay
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
webkit-playsinline="true"
/>
) : (
<img
src={`${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewAsset?.url}`}
alt={previewAsset?.title}
style={{ width: '100%', height: '100%', objectFit: 'contain' }}
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
/>
)}
</div>
</div>
{/* 底部标题栏 */}
{previewAsset?.title && (
{previewAsset?.mediaType === 'video' && (
<div style={{
padding: '14px 20px',
fontSize: 14,
color: '#4b5563',
fontWeight: 500,
borderTop: '1px solid #f1f5f9',
textAlign: 'center',
position: 'absolute',
top: 32,
right: 32,
fontSize: 11,
color: '#fff',
background: 'rgba(0,0,0,0.6)',
padding: '3px 8px',
borderRadius: 4,
}}>
{previewAsset.title}
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 style={{
padding: '16px 24px',
borderTop: '1px solid #334155',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
}}>
{/* <div style={{ display: 'flex', gap: 16 }}>
<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>
</Modal>
</div>
);