si
This commit is contained in:
@@ -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' }} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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('');
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user