From b8fc287a5ec3c69b0e364c6361ba33e599e8350e Mon Sep 17 00:00:00 2001 From: Lrd <13001933075@sina.cn> Date: Mon, 15 Jun 2026 17:46:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=89=B9=E9=87=8F=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-app/src/pages/GeneratedRecord.tsx | 555 ++++++++++++++++++-- 1 file changed, 507 insertions(+), 48 deletions(-) diff --git a/video-gen-app/src/pages/GeneratedRecord.tsx b/video-gen-app/src/pages/GeneratedRecord.tsx index bd4446f6..45c083d7 100644 --- a/video-gen-app/src/pages/GeneratedRecord.tsx +++ b/video-gen-app/src/pages/GeneratedRecord.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react'; -import { Button, Empty, Input, Select, Space, Typography, Tag, message } from 'antd'; +import { Button, Empty, Input, Select, Space, Typography, Tag, message, Upload, Modal, Progress } from 'antd'; import { SearchOutlined, FilterOutlined, @@ -10,6 +10,8 @@ import { DownloadOutlined, XOutlined, ClockCircleOutlined, + UploadOutlined, + PlusOutlined, } from '@ant-design/icons'; import { gethistory, gethistoryItems } from '../api'; @@ -31,6 +33,16 @@ const GeneratedRecord: React.FC = () => { 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 [isSelectionMode, setIsSelectionMode] = useState(false); + const [selectedItems, setSelectedItems] = useState>(new Set()); + // 全局 Intersection Observer 实例(复用,避免创建过多实例) let globalObserver: IntersectionObserver | null = null; const observerCallbacks = new Map void>(); @@ -160,7 +172,10 @@ const GeneratedRecord: React.FC = () => { item: any; mediaType: 'video' | 'image'; onClick: () => void; - }> = ({ item, mediaType, onClick }) => { + 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); @@ -251,14 +266,18 @@ const GeneratedRecord: React.FC = () => { position: 'relative', backgroundColor: '#f1f5f9', }} - onClick={onClick} + onClick={!isSelectionMode ? onClick : undefined} onMouseEnter={(e) => { - (e.currentTarget as HTMLElement).style.transform = 'scale(1.05)'; - (e.currentTarget as HTMLElement).style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)'; + 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) => { - (e.currentTarget as HTMLElement).style.transform = 'scale(1)'; - (e.currentTarget as HTMLElement).style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; + 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)'; + } }} > {/* 加载占位符 - 显示渐变背景和加载状态 */} @@ -408,7 +427,7 @@ const GeneratedRecord: React.FC = () => { )} {/* 点击提示 */} - {!isExpired && ( + {!isExpired && !isSelectionMode && (
{ 点击预览
)} + + {/* 选择模式下的选择框 */} + {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 && ( +
+ )}
); }; @@ -448,7 +520,6 @@ const GeneratedRecord: React.FC = () => { // 预览文件 const handlePreview = (item: any) => { - setPreviewItem(item); setPreviewVisible(true); // 触发事件通知布局组件关闭浮动按钮 @@ -471,6 +542,187 @@ const GeneratedRecord: React.FC = () => { 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 = async () => { + if (selectedItems.size === 0) { + message.warning('请先选择要上传的媒体'); + return; + } + + setUploading(true); + const uploadProgressMap: { [key: string]: number } = {}; + + try { + // 遍历所有选中的项 + for (const itemId of selectedItems) { + let item: any = null; + let mediaUrl = ''; + let mediaType = ''; + + // 找到对应的 item + for (const group of recordlist) { + const found = group.items.find((i: any) => i.id === itemId); + if (found) { + item = found; + break; + } + } + + if (!item) continue; + + // 根据媒体类型获取 URL + 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] = 0; + setUploadProgress({ ...uploadProgressMap }); + + 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 form = new FormData(); + form.append('file', file); + form.append('media_type', mediaType); + form.append('original_id', itemId); + + 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, + } + ); + + if (!uploadResponse.ok) throw new Error('上传失败'); + + // 更新进度 + uploadProgressMap[itemId] = 100; + setUploadProgress({ ...uploadProgressMap }); + + } catch (error) { + console.error(`上传失败: ${itemId}`, error); + uploadProgressMap[itemId] = -1; // 标记失败 + setUploadProgress({ ...uploadProgressMap }); + } + } + + const successCount = Object.values(uploadProgressMap).filter(p => p === 100).length; + const failCount = Object.values(uploadProgressMap).filter(p => p === -1).length; + + if (failCount === 0) { + message.success(`成功上传 ${successCount} 个文件`); + } else { + message.warning(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`); + } + + // 退出选择模式 + setIsSelectionMode(false); + setSelectedItems(new Set()); + setUploadProgress({}); + + } catch (error) { + message.error('批量上传失败'); + console.error(error); + } finally { + setUploading(false); + } + }; + useEffect(() => { setLoading(true); let parameters = ''; @@ -508,8 +760,6 @@ const GeneratedRecord: React.FC = () => { ...prev, page: prev.page + 1 })); - - }; // 分组加载更多 @@ -575,7 +825,7 @@ const GeneratedRecord: React.FC = () => { */} - {/* First row filter: 项目记录 / 创作记录 */} + {/* 操作栏:筛选 + 上传按钮 */}
{ borderRadius: 12, background: '#fff', border: '1px solid #f0f0f5', + justifyContent: 'space-between', }}> - - - - - +
+ + + + + +
+
+ {/* 多选模式按钮 */} + {isSelectionMode ? ( + + + + + + ) : ( + + )} +
{/* Second row filter: 视频 / 图片 */} @@ -703,7 +1018,10 @@ const GeneratedRecord: React.FC = () => { key={item.id} item={item} mediaType={filterMedia} - onClick={() => handlePreview(item)} + onClick={() => isSelectionMode ? handleToggleSelect(item.id) : handlePreview(item)} + isSelected={selectedItems.has(item.id)} + onToggleSelect={handleToggleSelect} + isSelectionMode={isSelectionMode} /> ))} @@ -751,6 +1069,147 @@ const GeneratedRecord: React.FC = () => { )} + {/* 批量上传弹窗 */} + { + 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 && ( + + )} +
+
+ ))} +
+
+ )} + + {/* 操作按钮 */} +
+ + +
+
+
+ {/* 预览弹窗 */} {previewVisible && previewItem && (