素材云下载

This commit is contained in:
孙佳艺
2026-06-25 11:11:06 +08:00
parent 5fc1e43d4e
commit 87e412ef74
15 changed files with 767 additions and 911 deletions
+49 -34
View File
@@ -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(() => {
+98 -10
View File
@@ -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' }}
+86
View File
@@ -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: [],
}),
}));