素材云下载
This commit is contained in:
@@ -26,6 +26,8 @@ import {
|
||||
uploadVideo, getCreditRatios, deleteHistory, calculateCredits
|
||||
} from '../api';
|
||||
|
||||
import { useAppStore } from '../store/useAppStore';
|
||||
|
||||
import {
|
||||
PlusOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
@@ -112,9 +114,31 @@ const AIChatPage: React.FC = () => {
|
||||
|
||||
const [inputValue, setInputValue] = useState<string>('');
|
||||
|
||||
const [mediaType, setMediaType] = useState<string>('image');
|
||||
|
||||
const [countType, setCountType] = useState<string>('请选择');
|
||||
// 从 store 读取生成配置状态
|
||||
const {
|
||||
mediaType,
|
||||
countType,
|
||||
selectedRatio,
|
||||
selectedResolution,
|
||||
width,
|
||||
height,
|
||||
videoDuration,
|
||||
videoAspectRatio,
|
||||
videoResolution,
|
||||
engineOptions,
|
||||
enginesele,
|
||||
setMediaType,
|
||||
setCountType,
|
||||
setSelectedRatio,
|
||||
setSelectedResolution,
|
||||
setWidth,
|
||||
setHeight,
|
||||
setVideoDuration,
|
||||
setVideoAspectRatio,
|
||||
setVideoResolution,
|
||||
setEngineOptions,
|
||||
setEnginesele,
|
||||
} = useAppStore();
|
||||
|
||||
const [uploading, setUploading] = useState<boolean>(false);
|
||||
|
||||
@@ -183,11 +207,6 @@ const AIChatPage: React.FC = () => {
|
||||
const [attachmentPopupMessageId, setAttachmentPopupMessageId] = useState<string | null>(null);
|
||||
const [attachmentPopupPosition, setAttachmentPopupPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
// 图片设置相关状态
|
||||
const [selectedRatio, setSelectedRatio] = useState<string>('1:1');
|
||||
const [selectedResolution, setSelectedResolution] = useState<string>('2K');
|
||||
const [width, setWidth] = useState<number>(2048);
|
||||
const [height, setHeight] = useState<number>(2048);
|
||||
const [showImageSettingsModal, setShowImageSettingsModal] = useState(false);
|
||||
const [showVideoSettingsModal, setShowVideoSettingsModal] = useState(false);
|
||||
const [ratioOptions, setRatioOptions] = useState([]);
|
||||
@@ -196,22 +215,8 @@ const AIChatPage: React.FC = () => {
|
||||
const [blindex, setBlindex] = useState<any>(0);
|
||||
const [fblindex, setFBlindex] = useState<any>(0);
|
||||
|
||||
// 视频设置相关状态
|
||||
const [videoDuration, setVideoDuration] = useState(5);
|
||||
const [videoAspectRatio, setVideoAspectRatio] = useState<string>('16:9');
|
||||
const [videoResolution, setVideoResolution] = useState<string>('720p');
|
||||
const [showEngineModal, setShowEngineModal] = useState(false);
|
||||
const [engineOptions, setEngineOptions] = useState<{
|
||||
ratios: string[];
|
||||
resolutions: string[];
|
||||
durations: number[];
|
||||
}>({
|
||||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
resolutions: ['480p', '720p', '1080p'],
|
||||
durations: [5, 8, 10, 12, 15],
|
||||
});
|
||||
|
||||
const [enginesele, setEnginesele] = useState<any>([]);
|
||||
const [creditRatios, setCreditRatios] = useState<any[]>([]);
|
||||
const [cimage, setCimage] = useState<any[]>([]);
|
||||
const [creditCalculationData, setCreditCalculationData] = useState<any[]>([]);
|
||||
@@ -359,23 +364,33 @@ const AIChatPage: React.FC = () => {
|
||||
// 如果是视频模式,初始化视频参数选项
|
||||
if (data.engine.video && data.engine.video.length > 0) {
|
||||
const defaultEngine = data.engine.video[0];
|
||||
|
||||
// 查找用户之前选择的引擎(如果存在)
|
||||
const savedEngine = data.engine.video.find((e: any) => e.id === countType);
|
||||
const targetEngine = savedEngine || defaultEngine;
|
||||
|
||||
// 更新引擎选项为当前引擎支持的参数
|
||||
setEngineOptions({
|
||||
ratios: defaultEngine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
resolutions: defaultEngine.supportedResolutions || ['480p', '720p', '1080p'],
|
||||
durations: defaultEngine.supportedDurations || [5, 8, 10, 12, 15],
|
||||
ratios: targetEngine.supportedRatios || ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
resolutions: targetEngine.supportedResolutions || ['480p', '720p', '1080p'],
|
||||
durations: targetEngine.supportedDurations || [5, 8, 10, 12, 15],
|
||||
});
|
||||
// 设置默认选中值
|
||||
if (defaultEngine.supportedRatios?.length > 0) {
|
||||
setVideoAspectRatio(defaultEngine.supportedRatios[0]);
|
||||
|
||||
// 确保视频参数在引擎支持的范围内
|
||||
if (!targetEngine.supportedRatios?.includes(videoAspectRatio)) {
|
||||
setVideoAspectRatio(targetEngine.supportedRatios?.[0] || '16:9');
|
||||
}
|
||||
if (defaultEngine.supportedResolutions?.length > 0) {
|
||||
setVideoResolution(defaultEngine.supportedResolutions[0]);
|
||||
if (!targetEngine.supportedResolutions?.includes(videoResolution)) {
|
||||
setVideoResolution(targetEngine.supportedResolutions?.[0] || '720p');
|
||||
}
|
||||
if (defaultEngine.supportedDurations?.length > 0) {
|
||||
setVideoDuration(defaultEngine.supportedDurations[0]);
|
||||
if (!targetEngine.supportedDurations?.includes(videoDuration)) {
|
||||
setVideoDuration(targetEngine.supportedDurations?.[0] || 5);
|
||||
}
|
||||
|
||||
// 如果之前没有选择过引擎(还是默认值),才设置默认引擎
|
||||
if (countType === '请选择') {
|
||||
setCountType(targetEngine.id);
|
||||
}
|
||||
// 设置默认引擎
|
||||
setCountType(defaultEngine.id);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react';
|
||||
import { Button, Empty, Input, Select, Space, Typography, Tag, message, Modal, Table, DatePicker } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import JSZip from 'jszip';
|
||||
import {
|
||||
FilterOutlined,
|
||||
VideoCameraOutlined,
|
||||
@@ -601,6 +602,81 @@ const GeneratedRecord: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownloadSelected = async () => {
|
||||
if (selectedItems.size === 0) {
|
||||
message.warning('请先选择要下载的媒体');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查下载数量限制
|
||||
if (selectedItems.size > 10) {
|
||||
message.warning('最多只能单次下载10个文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedContent: any[] = [];
|
||||
recordlist.forEach((group: any) => {
|
||||
group.items.forEach((item: any) => {
|
||||
if (selectedItems.has(getItemResourceId(item))) {
|
||||
selectedContent.push(item);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const zip = new JSZip();
|
||||
const baseUrl = import.meta.env.VITE_API_BASE || "http://localhost:8000";
|
||||
const folder = zip.folder('downloads');
|
||||
let hasError = false;
|
||||
let successCount = 0;
|
||||
|
||||
message.loading({ content: '下载中,请稍候...', key: 'downloadProgress' });
|
||||
|
||||
for (const item of selectedContent) {
|
||||
const url = `${baseUrl}${item.videoUrl || item.imageUrl}`;
|
||||
const filename = ((item.videoUrl || item.imageUrl).split('/').pop() || `file_${Date.now()}`).split('?')[0];
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const blob = await response.blob();
|
||||
folder?.file(filename, blob);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
console.warn(`文件下载失败(CORS限制): ${filename},将使用备用方式下载`);
|
||||
hasError = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
// Fallback: 逐个打开文件下载(不受 CORS 限制)
|
||||
message.destroy('downloadProgress');
|
||||
message.info('由于跨域限制,将逐个下载文件');
|
||||
selectedContent.forEach((item, index) => {
|
||||
setTimeout(() => {
|
||||
const url = `${baseUrl}${item.videoUrl || item.imageUrl}&download=1`;
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}, index * 500);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(zipBlob);
|
||||
link.download = `downloads_${Date.now()}.zip`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
|
||||
message.destroy('downloadProgress');
|
||||
message.success(`下载完成,共 ${successCount} 个文件`);
|
||||
};
|
||||
|
||||
|
||||
const handleBatchUploadSelected = () => {
|
||||
if (selectedItems.size === 0) {
|
||||
message.warning('请先选择要上传的媒体');
|
||||
@@ -692,7 +768,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
itemMap.set(resourceId, item);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
for (const itemId of selectedItems) {
|
||||
const item = itemMap.get(itemId);
|
||||
// 根据item是否有generatedResourceId来决定source_model
|
||||
@@ -702,7 +778,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
} else {
|
||||
sourceModel = filterType === 'project' ? 'generation_records' : 'chat_generation_tasks';
|
||||
}
|
||||
|
||||
|
||||
tasks.push({
|
||||
advertiser_ids: advertiserIds,
|
||||
resource_ids: [itemId],
|
||||
@@ -827,8 +903,8 @@ const GeneratedRecord: React.FC = () => {
|
||||
page: res.page,
|
||||
}];
|
||||
if (recordList && recordList[0].items.length > 0) {
|
||||
setRecordList(recordList);
|
||||
}else{
|
||||
setRecordList(recordList);
|
||||
} else {
|
||||
setRecordList([]);
|
||||
}
|
||||
}).catch((err) => {
|
||||
@@ -927,7 +1003,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
}, [filterType, filterMedia]);
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: 'calc(100vh - 90px)', background: '#ffffffff' , overflowY: 'auto'}} >
|
||||
<div style={{ minHeight: 'calc(100vh - 90px)', background: '#ffffffff', overflowY: 'auto' }} >
|
||||
{/* 操作栏:筛选 + 上传按钮 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
@@ -1008,6 +1084,18 @@ const GeneratedRecord: React.FC = () => {
|
||||
>
|
||||
取消选择
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleDownloadSelected()}
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
background: '#f8f9fc',
|
||||
border: '1px solid #e2e8f0',
|
||||
color: '#222222ff',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
下载{selectedItems.size}个
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleBatchUploadSelected}
|
||||
@@ -1036,9 +1124,9 @@ const GeneratedRecord: React.FC = () => {
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
批量上传
|
||||
批量操作
|
||||
</Button>
|
||||
|
||||
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1057,7 +1145,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
||||
<Typography.Text style={{ color: '#94a3b8', fontSize: 14 }}>媒体类型:</Typography.Text>
|
||||
|
||||
|
||||
<Space>
|
||||
<Button
|
||||
type={filterMedia === 'video' ? 'primary' : 'default'}
|
||||
@@ -1534,7 +1622,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
open={uploadHistoryModalVisible}
|
||||
onCancel={() => setUploadHistoryModalVisible(false)}
|
||||
footer={null}
|
||||
width={ 800 }
|
||||
width={800}
|
||||
style={{ borderRadius: 8 }}
|
||||
mask={{ closable: false }}
|
||||
>
|
||||
@@ -1638,7 +1726,7 @@ const GeneratedRecord: React.FC = () => {
|
||||
width: 180,
|
||||
render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||||
},
|
||||
|
||||
|
||||
]}
|
||||
loading={uploadHistoryLoading}
|
||||
scroll={{ x: 'max-content' }}
|
||||
|
||||
@@ -15,6 +15,23 @@ interface AppState {
|
||||
records: api.GenerationRecordPageListOut;
|
||||
loading: boolean;
|
||||
|
||||
// 生成配置状态 - 页面跳转时保留,刷新时重置
|
||||
mediaType: string;
|
||||
countType: string;
|
||||
selectedRatio: string;
|
||||
selectedResolution: string;
|
||||
width: number;
|
||||
height: number;
|
||||
videoDuration: number;
|
||||
videoAspectRatio: string;
|
||||
videoResolution: string;
|
||||
engineOptions: {
|
||||
ratios: string[];
|
||||
resolutions: string[];
|
||||
durations: number[];
|
||||
};
|
||||
enginesele: any;
|
||||
|
||||
fetchProjects: () => Promise<void>;
|
||||
createProject: (name: string, industry: Industry) => Promise<Project>;
|
||||
deleteProject: (id: string) => Promise<void>;
|
||||
@@ -23,6 +40,23 @@ interface AppState {
|
||||
optimizePrompt: (projectId: string, params: OptimizeParams) => Promise<OptimizeResult>;
|
||||
generateVideo: (recordId: string, params: GenerateParams) => Promise<GenerationRecord>;
|
||||
updateRecordReferences: (recordId: string, references: MediaReference[]) => void;
|
||||
|
||||
// 生成配置状态更新方法
|
||||
setMediaType: (mediaType: string) => void;
|
||||
setCountType: (countType: string) => void;
|
||||
setImageSettings: (ratio: string, resolution: string, width: number, height: number) => void;
|
||||
setVideoSettings: (duration: number, aspectRatio: string, resolution: string) => void;
|
||||
setEngineOptions: (options: { ratios: string[]; resolutions: string[]; durations: number[] }) => void;
|
||||
// 单独的 setter 方法
|
||||
setSelectedRatio: (ratio: string) => void;
|
||||
setSelectedResolution: (resolution: string) => void;
|
||||
setWidth: (width: number) => void;
|
||||
setHeight: (height: number) => void;
|
||||
setVideoDuration: (duration: number) => void;
|
||||
setVideoAspectRatio: (aspectRatio: string) => void;
|
||||
setVideoResolution: (resolution: string) => void;
|
||||
setEnginesele: (enginesele: any) => void;
|
||||
resetGenerationConfig: () => void;
|
||||
}
|
||||
|
||||
export const useAppStore = create<AppState>((set, get) => ({
|
||||
@@ -30,6 +64,23 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
records: emptyRecordsPage(),
|
||||
loading: false,
|
||||
|
||||
// 生成配置状态初始值
|
||||
mediaType: 'image',
|
||||
countType: '请选择',
|
||||
selectedRatio: '1:1',
|
||||
selectedResolution: '2K',
|
||||
width: 2048,
|
||||
height: 2048,
|
||||
videoDuration: 5,
|
||||
videoAspectRatio: '16:9',
|
||||
videoResolution: '720p',
|
||||
engineOptions: {
|
||||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
resolutions: ['480p', '720p', '1080p'],
|
||||
durations: [5, 8, 10, 12, 15],
|
||||
},
|
||||
enginesele: [],
|
||||
|
||||
fetchProjects: async () => {
|
||||
set({ loading: true });
|
||||
try {
|
||||
@@ -99,4 +150,39 @@ export const useAppStore = create<AppState>((set, get) => ({
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 生成配置状态更新方法
|
||||
setMediaType: (mediaType) => set({ mediaType }),
|
||||
setCountType: (countType) => set({ countType }),
|
||||
setImageSettings: (ratio, resolution, width, height) =>
|
||||
set({ selectedRatio: ratio, selectedResolution: resolution, width, height }),
|
||||
setVideoSettings: (duration, aspectRatio, resolution) =>
|
||||
set({ videoDuration: duration, videoAspectRatio: aspectRatio, videoResolution: resolution }),
|
||||
setEngineOptions: (options) => set({ engineOptions: options }),
|
||||
// 单独的 setter 方法
|
||||
setSelectedRatio: (ratio) => set({ selectedRatio: ratio }),
|
||||
setSelectedResolution: (resolution) => set({ selectedResolution: resolution }),
|
||||
setWidth: (width) => set({ width }),
|
||||
setHeight: (height) => set({ height }),
|
||||
setVideoDuration: (duration) => set({ videoDuration: duration }),
|
||||
setVideoAspectRatio: (aspectRatio) => set({ videoAspectRatio: aspectRatio }),
|
||||
setVideoResolution: (resolution) => set({ videoResolution: resolution }),
|
||||
setEnginesele: (enginesele) => set({ enginesele }),
|
||||
resetGenerationConfig: () => set({
|
||||
mediaType: 'image',
|
||||
countType: '请选择',
|
||||
selectedRatio: '1:1',
|
||||
selectedResolution: '2K',
|
||||
width: 2048,
|
||||
height: 2048,
|
||||
videoDuration: 5,
|
||||
videoAspectRatio: '16:9',
|
||||
videoResolution: '720p',
|
||||
engineOptions: {
|
||||
ratios: ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'],
|
||||
resolutions: ['480p', '720p', '1080p'],
|
||||
durations: [5, 8, 10, 12, 15],
|
||||
},
|
||||
enginesele: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user