diff --git a/video-gen-admin/src/App.tsx b/video-gen-admin/src/App.tsx index 7478e04c..8d58ddc9 100644 --- a/video-gen-admin/src/App.tsx +++ b/video-gen-admin/src/App.tsx @@ -7,7 +7,7 @@ import AdminAuthoriz from './pages/AdminAuthoriz'; import AdminConsume from './pages/AdminConsume'; import AdminLoginPage from './pages/AdminLoginPage'; import AdminDashboard from './pages/AdminDashboard'; -import AdminPlatform from './pages/Adminplatform'; +import AdminPlatform from './pages/AdminPlatform'; import AdminUsers from './pages/AdminUsers'; import AdminModels from './pages/AdminModels'; import AdminSettings from './pages/AdminSettings'; diff --git a/video-gen-app/src/pages/GeneratedRecord copy.tsx b/video-gen-app/src/pages/GeneratedRecord copy.tsx deleted file mode 100644 index b1f7ea47..00000000 --- a/video-gen-app/src/pages/GeneratedRecord copy.tsx +++ /dev/null @@ -1,1891 +0,0 @@ -import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react'; -import { Button, Empty, Input, Select, Space, Typography, Tag, message, Upload, Modal, Progress } from 'antd'; -import { - SearchOutlined, - FilterOutlined, - VideoCameraOutlined, - PictureOutlined, - FolderOpenOutlined, - FileTextOutlined, - DownloadOutlined, - XOutlined, - ClockCircleOutlined, - UploadOutlined, - PlusOutlined, -} from '@ant-design/icons'; -import { gethistory, gethistoryItems, getDefaultPreTest } from '../api'; - -const { Search } = Input; -const { Text } = Typography; - -const GeneratedRecord: React.FC = () => { - const [filterType, setFilterType] = useState<'project' | 'creation'>('project'); - const [filterMedia, setFilterMedia] = useState<'video' | 'image'>('video'); - const [recordlist, setRecordList] = useState([]); - const [Pagebreak, setPagebreak] = useState({ - page: 1, - pageSize: 10, - }); - const [Totalnumber, setTotalnumber] = useState(0); - const [loading, setLoading] = useState(false); - const [loadingGroups, setLoadingGroups] = useState>(new Set()); - const [previewVisible, setPreviewVisible] = useState(false); - const [previewItem, setPreviewItem] = useState(null); - const videoRef = React.createRef(); - - // 批量上传相关状态 - const [uploadModalVisible, setUploadModalVisible] = useState(false); - const [uploadFiles, setUploadFiles] = useState([]); - const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({}); - const [uploading, setUploading] = useState(false); - - // 上传配置弹窗相关状态 - const [uploadConfigModalVisible, setUploadConfigModalVisible] = useState(false); - const [preTestTemplates, setPreTestTemplates] = useState([ - { id: 'template_001', name: '前测模板A' }, - { id: 'template_002', name: '前测模板B' }, - { id: 'template_003', name: '前测模板C' }, - ]); - const [preTestLoading, setPreTestLoading] = useState(false); - const [selectedMediaList, setSelectedMediaList] = useState([]); - const [selectedMediaConfigs, setSelectedMediaConfigs] = useState<{ - [itemId: string]: { - accountId: string; - templateId: string; - }[]; - }>({}); - const [batchUploadProgress, setBatchUploadProgress] = useState<{ - itemId: string; - status: 'pending' | 'uploading' | 'success' | 'error'; - message: string; - }[]>([]); - - // 多选相关状态 - const [isSelectionMode, setIsSelectionMode] = useState(false); - const [selectedItems, setSelectedItems] = useState>(new Set()); - - // 全局 Intersection Observer 实例(复用,避免创建过多实例) - let globalObserver: IntersectionObserver | null = null; - const observerCallbacks = new Map void>(); - - // 加载队列控制 - 限制同时加载的媒体数量 - // 使用优先级队列,完全在可视区的元素优先加载 - interface LoadTask { - callback: () => void; - priority: number; // 优先级:2=完全可见(立即加载),1=部分可见,0=即将进入 - element: HTMLElement; - } - const loadingQueue: LoadTask[] = []; - const MAX_CONCURRENT_LOADS = 15; // 最大同时加载数量 - const MAX_VISIBLE_LOADS = 8; // 可视区最大并发数(不受队列限制) - let currentLoads = 0; - let visibleLoads = 0; // 当前可视区加载数 - - const enqueueLoad = (callback: () => void, element: HTMLElement, isFullyVisible: boolean) => { - // 完全可见的元素立即加载,不受队列限制 - if (isFullyVisible && visibleLoads < MAX_VISIBLE_LOADS) { - visibleLoads++; - currentLoads++; - callback(); - } else if (currentLoads < MAX_CONCURRENT_LOADS) { - // 部分可见或预加载区域的元素,受队列限制 - currentLoads++; - callback(); - } else { - // 添加到优先级队列 - const task: LoadTask = { - callback, - priority: isFullyVisible ? 2 : 1, - element - }; - loadingQueue.push(task); - // 按优先级排序,高优先级在前 - loadingQueue.sort((a, b) => b.priority - a.priority); - } - }; - - const completeLoad = () => { - currentLoads--; - // 如果是可视区加载完成 - if (visibleLoads > 0) { - visibleLoads--; - } - // 处理队列中的任务 - if (loadingQueue.length > 0) { - // 优先处理高优先级任务 - const nextTask = loadingQueue.shift(); - if (nextTask) { - currentLoads++; - // 如果是完全可见的任务,计入可视区加载数 - if (nextTask.priority === 2) { - visibleLoads++; - } - nextTask.callback(); - } - } - }; - - const getGlobalObserver = (): IntersectionObserver => { - if (!globalObserver) { - globalObserver = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - const callback = observerCallbacks.get(entry.target as HTMLElement); - if (entry.isIntersecting && callback) { - observerCallbacks.delete(entry.target as HTMLElement); - globalObserver?.unobserve(entry.target); - // 判断是否完全可见(intersectionRatio >= 1) - const isFullyVisible = entry.intersectionRatio >= 1; - // 使用优先级队列控制加载 - enqueueLoad(callback, entry.target as HTMLElement, isFullyVisible); - } - }); - }, - { rootMargin: '400px', threshold: [0.01, 0.5, 1.0] } // 提前400px开始加载,多阈值检测 - ); - } - return globalObserver; - }; - - // 从URL中提取exp时间戳(支持相对路径和完整URL) - const extractExpTimestamp = (url: string): number | null => { - if (!url) return null; - - try { - // 尝试作为完整URL解析 - const urlObj = new URL(url); - const expStr = urlObj.searchParams.get('exp'); - if (expStr) { - return parseInt(expStr, 10); - } - return null; - } catch { - // 如果完整URL解析失败,尝试解析相对路径中的查询参数 - try { - const queryStart = url.indexOf('?'); - if (queryStart !== -1) { - const queryString = url.substring(queryStart + 1); - const params = new URLSearchParams(queryString); - const expStr = params.get('exp'); - if (expStr) { - return parseInt(expStr, 10); - } - } - return null; - } catch { - return null; - } - } - }; - - // 检查媒体是否过期 - const isMediaExpired = (url: string): boolean => { - const expTimestamp = extractExpTimestamp(url); - if (!expTimestamp) { - return false; // 没有exp参数,视为不过期 - } - const currentTimestamp = Math.floor(Date.now() / 1000); - return currentTimestamp > expTimestamp; - }; - - // 懒加载媒体组件 - const LazyMedia: React.FC<{ - item: any; - mediaType: 'video' | 'image'; - onClick: () => void; - isSelected?: boolean; - onToggleSelect?: (itemId: string) => void; - isSelectionMode?: boolean; - }> = ({ item, mediaType, onClick, isSelected = false, onToggleSelect, isSelectionMode = false }) => { - const [isLoaded, setIsLoaded] = useState(false); - const [isError, setIsError] = useState(false); - const [isExpired, setIsExpired] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const placeholderRef = useRef(null); - const mediaRef = useRef(null); - const errorTimer = useRef | null>(null); - - // 检查媒体是否过期 - useEffect(() => { - const url = mediaType === 'video' ? item.videoUrl : item.imageUrl; - if (url && isMediaExpired(url)) { - setIsExpired(true); - } - }, [item, mediaType]); - - useEffect(() => { - // 如果已过期,不需要监听 - if (isExpired) return; - - const placeholder = placeholderRef.current; - if (!placeholder) return; - - const observer = getGlobalObserver(); - const callback = () => { - setIsLoading(true); - }; - - observerCallbacks.set(placeholder, callback); - observer.observe(placeholder); - - return () => { - observerCallbacks.delete(placeholder); - observer.unobserve(placeholder); - }; - }, [isExpired]); - - // 安全拼接URL,避免双斜杠 - const buildUrl = (path: string, isImage: boolean = false): string => { - const baseUrl = import.meta.env.VITE_API_BASE || "http://localhost:8000"; - // 移除路径开头的斜杠(如果有) - const cleanPath = path.startsWith('/') ? path.slice(1) : path; - // 移除baseUrl结尾的斜杠(如果有) - const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; - if (isImage) { - return `${cleanBase}/static/${cleanPath}&w=300&q=50`; - } - return `${cleanBase}/${cleanPath}`; - }; - - // 处理加载完成 - const handleLoad = () => { - // 清除可能的错误延迟定时器 - if (errorTimer.current) { - clearTimeout(errorTimer.current); - } - setIsLoaded(true); - setIsLoading(false); - completeLoad(); - }; - - const handleError = () => { - // 使用防抖,只有错误持续一段时间后才显示错误状态 - errorTimer.current = setTimeout(() => { - const mediaUrl = mediaType === 'video' ? buildUrl(item.videoUrl) : buildUrl(item.imageUrl); - // console.warn(`媒体加载失败: ${mediaUrl}`, item); - setIsError(true); - setIsLoading(false); - completeLoad(); - }, 1000); // 1秒防抖延迟 - }; - - // 获取媒体URL - const mediaUrl = mediaType === 'video' ? buildUrl(item.videoUrl) : buildUrl(item.imageUrl, true); - const coverUrl = item.videoCoverUrl ? buildUrl(item.videoCoverUrl, true) : undefined; - - return ( -
{ - if (!isSelectionMode) { - (e.currentTarget as HTMLElement).style.transform = 'scale(1.05)'; - (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)'; - } - }} - onMouseLeave={(e) => { - if (!isSelectionMode) { - (e.currentTarget as HTMLElement).style.transform = 'scale(1)'; - (e.currentTarget as HTMLElement).style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; - } - }} - > - - {/* 媒体内容 - 当isLoading为true时开始渲染,加载完成后显示 */} - {((mediaType === 'video' && item.videoCoverUrl) || mediaType === 'image') && (isLoading || isLoaded) && !isError && ( -
- {/* 加载中遮罩 */} - {isLoading && !isLoaded && ( -
-
-
- )} - {mediaType === 'video' && item.videoCoverUrl && ( - } - src={coverUrl} - style={{ - width: '100%', - height: '100%', - objectFit: 'cover', - opacity: isLoaded ? 1 : 0, - transition: 'opacity 0.3s ease-in-out' - }} - loading="lazy" - onLoad={handleLoad} - onError={handleError} - /> - )} - {mediaType === 'image' && ( - } - src={mediaUrl} - alt="图片预览" - style={{ - width: '100%', - height: '100%', - objectFit: 'cover', - opacity: isLoaded ? 1 : 0, - transition: 'opacity 0.3s ease-in-out' - }} - loading="lazy" - onLoad={handleLoad} - onError={handleError} - /> - )} -
- )} - - {/* 加载占位符 - 显示渐变背景和加载状态 */} - {!isLoading && !isLoaded && !isError && ( -
-
- {mediaType === 'video' ? ( - - ) : ( - - )} -
-
- )} - - {/* 视频无封面时直接显示占位符 */} - {mediaType === 'video' && !item.videoCoverUrl && !isError && ( -
- - 暂无封面 -
- )} - - - {/* 过期占位符 */} - {isExpired && ( -
- - 图片过期 -
- )} - - {/* 错误占位符 */} - {isError && ( -
- 加载失败 -
- )} - - {/* 点击提示 */} - {!isExpired && !isSelectionMode && ( -
{ - (e.currentTarget as HTMLElement).style.opacity = '1'; - }} - onMouseLeave={(e) => { - (e.currentTarget as HTMLElement).style.opacity = '0'; - }} - > - 点击预览 -
- )} - - {/* 选择模式下的选择框 */} - {isSelectionMode && ( -
{ - e.stopPropagation(); - onToggleSelect?.(item.id); - }} - onMouseEnter={(e) => { - e.currentTarget.style.transform = 'scale(1.1)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.transform = 'scale(1)'; - }} - > - {isSelected && ( - - - - )} -
- )} - - {/* 选中状态下的边框高亮 */} - {isSelectionMode && isSelected && ( -
- )} -
- ); - }; - - // 下载文件 - const handleDownload = (item: any) => { - const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl || item.imageUrl}`; - const link = document.createElement('a'); - link.href = url; - link.download = item.title || item.id || 'download'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; - - // 预览文件 - const handlePreview = (item: any) => { - setPreviewItem(item); - setPreviewVisible(true); - // 触发事件通知布局组件关闭浮动按钮 - window.dispatchEvent(new Event('previewOpen')); - }; - - // 关闭预览并暂停视频 - const handleClosePreview = () => { - // 方法1: 使用 ref - if (videoRef.current) { - videoRef.current.pause(); - videoRef.current.currentTime = 0; - } - // 方法2: 直接通过 DOM 查询(备用) - const videoElements = document.querySelectorAll('video'); - videoElements.forEach(video => { - video.pause(); - video.currentTime = 0; - }); - setPreviewVisible(false); - }; - - // 批量上传相关函数 - const handleUploadChange = (info: any) => { - // 过滤文件类型 - const validFiles = info.fileList.filter((file: any) => { - const type = file.type.toLowerCase(); - return type.startsWith('image/') || type.startsWith('video/'); - }); - - // 检查无效文件并提示 - const invalidFiles = info.fileList.filter((file: any) => { - const type = file.type.toLowerCase(); - return !type.startsWith('image/') && !type.startsWith('video/'); - }); - - if (invalidFiles.length > 0) { - message.warning(`已过滤 ${invalidFiles.length} 个无效文件,仅支持图片和视频`); - } - - setUploadFiles(validFiles); - }; - - const handleRemoveFile = (file: any) => { - setUploadFiles(prev => prev.filter(f => f.uid !== file.uid)); - }; - - const handleStartUpload = async () => { - if (uploadFiles.length === 0) { - message.warning('请先选择要上传的文件'); - return; - } - setUploading(true); - // 模拟批量上传过程 - for (let i = 0; i < uploadFiles.length; i++) { - const file = uploadFiles[i]; - setUploadProgress(prev => ({ ...prev, [file.uid]: 0 })); - - // 模拟上传进度 - for (let progress = 0; progress <= 100; progress += 10) { - await new Promise(resolve => setTimeout(resolve, 100)); - setUploadProgress(prev => ({ ...prev, [file.uid]: progress })); - } - } - // 上传完成 - await new Promise(resolve => setTimeout(resolve, 500)); - message.success(`成功上传 ${uploadFiles.length} 个文件`); - setUploading(false); - setUploadFiles([]); - setUploadModalVisible(false); - // 刷新页面数据 - setPagebreak(prev => ({ ...prev, page: 1 })); - }; - - // 多选相关函数 - const handleToggleSelect = (itemId: string) => { - setSelectedItems(prev => { - const newSet = new Set(prev); - if (newSet.has(itemId)) { - newSet.delete(itemId); - } else { - newSet.add(itemId); - } - return newSet; - }); - }; - - const handleSelectAll = () => { - const allItemIds = recordlist.flatMap((group: any) => - group.items.map((item: any) => item.id) - ); - if (selectedItems.size === allItemIds.length) { - setSelectedItems(new Set()); - } else { - setSelectedItems(new Set(allItemIds)); - } - }; - - const handleBatchUploadSelected = () => { - if (selectedItems.size === 0) { - message.warning('请先选择要上传的媒体'); - return; - } - - const mediaList: any[] = []; - const configs: { [itemId: string]: { accountId: string; templateId: string }[] } = {}; - - for (const itemId of selectedItems) { - for (const group of recordlist) { - const found = group.items.find((i: any) => i.id === itemId); - if (found) { - mediaList.push(found); - configs[itemId] = [{ accountId: '', templateId: '' }]; - break; - } - } - } - - setSelectedMediaList(mediaList); - setSelectedMediaConfigs(configs); - setBatchUploadProgress([]); - setUploadConfigModalVisible(true); - }; - - const handleStartBatchUpload = async () => { - setUploading(true); - const uploadProgressMap: { [key: string]: { status: 'pending' | 'uploading' | 'success' | 'error'; message: string } } = {}; - - for (const itemId of selectedItems) { - uploadProgressMap[itemId] = { status: 'pending', message: '' }; - } - setBatchUploadProgress(Object.entries(uploadProgressMap).map(([itemId, value]) => ({ itemId, ...value }))); - - try { - for (const itemId of selectedItems) { - let item: any = null; - let mediaUrl = ''; - let mediaType = ''; - - for (const group of recordlist) { - const found = group.items.find((i: any) => i.id === itemId); - if (found) { - item = found; - break; - } - } - - if (!item) continue; - - if (filterMedia === 'video' && item.videoUrl) { - mediaUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl}`; - mediaType = 'video'; - } else if (filterMedia === 'image' && item.imageUrl) { - mediaUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${item.imageUrl}`; - mediaType = 'image'; - } else { - continue; - } - - uploadProgressMap[itemId] = { status: 'uploading', message: '正在上传...' }; - setBatchUploadProgress(Object.entries(uploadProgressMap).map(([itemId, value]) => ({ itemId, ...value }))); - - try { - const response = await fetch(mediaUrl); - if (!response.ok) throw new Error('下载失败'); - const blob = await response.blob(); - const file = new File([blob], item.title || `media_${itemId}`, { - type: mediaType === 'video' ? 'video/mp4' : 'image/jpeg' - }); - - const configs = selectedMediaConfigs[itemId] || []; - for (const config of configs) { - const form = new FormData(); - form.append('file', file); - form.append('media_type', mediaType); - form.append('original_id', itemId); - form.append('account_id', config.accountId); - form.append('pre_test_template_id', config.templateId); - - const token = localStorage.getItem('auth_token'); - const uploadResponse = await fetch( - `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}/api/generation-records/batch-upload`, - { - method: 'POST', - headers: token ? { Authorization: `Bearer ${token}` } : {}, - body: form, - } - ); - - const result = await uploadResponse.json(); - if (!uploadResponse.ok) { - throw new Error(result.message || '上传失败'); - } - } - - uploadProgressMap[itemId] = { status: 'success', message: '上传成功' }; - } catch (error: any) { - console.error(`上传失败: ${itemId}`, error); - uploadProgressMap[itemId] = { status: 'error', message: error.message || '上传失败' }; - } - setBatchUploadProgress(Object.entries(uploadProgressMap).map(([itemId, value]) => ({ itemId, ...value }))); - } - - const successCount = Object.values(uploadProgressMap).filter(p => p.status === 'success').length; - const failCount = Object.values(uploadProgressMap).filter(p => p.status === 'error').length; - - if (failCount === 0) { - message.success(`成功上传 ${successCount} 个文件`); - } else { - message.warning(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`); - } - - setIsSelectionMode(false); - setSelectedItems(new Set()); - } catch (error) { - message.error('批量上传失败'); - console.error(error); - } finally { - setUploading(false); - } - }; - - useEffect(() => { - setLoading(true); - let parameters = ''; - if (filterType === 'project') { - parameters = `?gen_type=${filterMedia}&history_source=generation_record&page=${Pagebreak.page}&page_size=${Pagebreak.pageSize}`; - } else { - parameters = `?gen_type=${filterMedia}&page=${Pagebreak.page}&page_size=${Pagebreak.pageSize}`; - } - gethistory(parameters).then((res: any) => { - const data = Array.isArray(res) ? res : (res?.groups || []); - data.forEach(group => { - group.page = 1; - }); - // 如果是第一页,替换数据;否则追加数据 - if (Pagebreak.page === 1) { - setRecordList(data); - } else { - setRecordList(prev => [...prev, ...data]); - } - - setTotalnumber(res?.totalDays || 0); - }).catch((err) => { - if (Pagebreak.page === 1) { - setRecordList([]); - } - }).finally(() => { - setLoading(false); - }); - }, [filterType, filterMedia, Pagebreak.page]); - - useEffect(() => { - setPreTestLoading(true); - getDefaultPreTest().then((res: any) => { - const data = res?.data || res; - setPreTestTemplates(Array.isArray(data) ? data : []); - }).catch(() => { - setPreTestTemplates([ - { id: 'template_001', name: '前测模板A' }, - { id: 'template_002', name: '前测模板B' }, - { id: 'template_003', name: '前测模板C' }, - ]); - }).finally(() => { - setPreTestLoading(false); - }); - }, []); - - // 加载更多 - const handleLoadMore = () => { - if (loading) return; - setPagebreak(prev => ({ - ...prev, - page: prev.page + 1 - })); - }; - - // 分组加载更多 - const handleGroupLoadMore = async (time: string, date: string, page: number) => { - if (loadingGroups.has(date)) return; - setLoadingGroups(prev => new Set([...prev, date])); - - const addpage = page + 1; - - let parameters = ``; - - if (filterType === 'project') { - parameters = `${time}?gen_type=${filterMedia}&history_source=generation_record&page=${addpage}&page_size=${Pagebreak.pageSize}`; - } else { - parameters = `${time}?gen_type=${filterMedia}&page=${addpage}&page_size=${Pagebreak.pageSize}`; - } - - try { - const res: any = await gethistoryItems(parameters); - - // gethistoryItems 返回数组,直接使用 - const newItems: any[] = res.items || []; - - if (newItems && newItems.length > 0) { - setRecordList(prev => prev.map(group => { - if (group.generatedDate === time) { - return { - ...group, - items: [...group.items, ...newItems], - page: addpage - }; - } - return group; - })); - } - } catch (err) { - } finally { - setLoadingGroups(prev => { - const next = new Set(prev); - next.delete(date); - return next; - }); - } - }; - - // 当筛选条件改变时,重置页码 - useEffect(() => { - setPagebreak(prev => ({ - ...prev, - page: 1 - })); - }, [filterType, filterMedia]); - - return ( -
- {/* Header */} - {/*
- - 生成历史 - - - 查看所有生成的视频和图片记录 - -
*/} - - {/* 操作栏:筛选 + 上传按钮 */} -
-
- - - - - -
-
- {/* 多选模式按钮 */} - {isSelectionMode ? ( - - - - - - ) : ( - - )} -
-
- - {/* Second row filter: 视频 / 图片 */} -
- 媒体类型: - - - - -
- - {/* Content area */} - {recordlist.length === 0 ? ( - - ) : ( -
- {recordlist.map((group: any, index: number) => ( -
- {/* Date label */} -
- {group.generatedDate} -
- {/* Media grid */} -
- {group.items.map((item: any) => ( - isSelectionMode ? handleToggleSelect(item.id) : handlePreview(item)} - isSelected={selectedItems.has(item.id)} - onToggleSelect={handleToggleSelect} - isSelectionMode={isSelectionMode} - /> - ))} -
- {/* 分组内加载更多 */} - {group.total && group.total > group.items.length && ( -
- -
- )} -
- ))} - {/* 加载更多按钮 */} - {recordlist.length > 0 && Totalnumber > recordlist.length && ( -
- -
- )} -
- )} - - {/* 批量上传弹窗 */} - { - setUploadModalVisible(false); - setUploadFiles([]); - }} - footer={null} - width={600} - > -
- {/* 上传区域 */} - false} // 手动控制上传 - accept="image/*,video/*" - listType="picture-card" - onRemove={handleRemoveFile} - > -
- - 点击上传 -
-
- - {/* 上传列表和进度 */} - {uploadFiles.length > 0 && ( -
- - 已选择 {uploadFiles.length} 个文件 - -
- {uploadFiles.map((file) => ( -
-
- {file.type?.startsWith('image/') ? ( - - ) : file.type?.startsWith('video/') ? ( - - ) : ( - - )} -
-
-
- {file.name} -
- {uploadProgress[file.uid] !== undefined && ( - - )} -
-
- ))} -
-
- )} - - {/* 操作按钮 */} -
- - -
-
-
- - {/* 上传配置弹窗 */} - { - setUploadConfigModalVisible(false); - setSelectedMediaList([]); - setSelectedMediaConfigs({}); - setBatchUploadProgress([]); - }} - footer={null} - width={800} - > -
- - 已选择 {selectedMediaList.length} 个媒体,请为每个媒体配置账户ID和对应的前测模板ID - - - {/* 选中媒体列表 */} -
- {selectedMediaList.map((media, mediaIndex) => { - const configs = selectedMediaConfigs[media.id] || []; - const mediaUrl = filterMedia === 'video' - ? `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${media.videoUrl}` - : `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}/static${media.imageUrl}`; - - return ( -
- {/* 媒体信息 */} -
-
- {filterMedia === 'video' ? ( -
-
- - {media.title || `媒体 ${mediaIndex + 1}`} - - - ID: {media.id} | 类型: {filterMedia === 'video' ? '视频' : '图片'} - -
-
- {mediaIndex + 1} -
-
- - {/* 该媒体的配置列表 */} -
- - 配置(可添加多条账户-模板组合) - - {configs.map((config, configIndex) => ( -
-
- 账户ID - { - const newConfigs = { ...selectedMediaConfigs }; - newConfigs[media.id] = [...configs]; - newConfigs[media.id][configIndex].accountId = e.target.value; - setSelectedMediaConfigs(newConfigs); - }} - placeholder="请输入账户ID" - style={{ borderRadius: 6 }} - /> -
-
- 前测模板 -