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, PictureOutlined, FolderOpenOutlined, FileTextOutlined, DownloadOutlined, XOutlined, ClockCircleOutlined, UploadOutlined, } from '@ant-design/icons'; import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory } 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 [selectedDate, setSelectedDate] = useState(''); const [uploading, setUploading] = useState(false); // 上传配置弹窗相关状态 const [uploadConfigModalVisible, setUploadConfigModalVisible] = useState(false); const [accountIdLists, setAccountIdLists] = useState<{ accountId: string; }[][]>([[]]); const [accountIdInputs, setAccountIdInputs] = useState(['']); const [oauthList, setOauthList] = useState([]); const [oauthLoading, setOauthLoading] = useState(false); const [oauthTotal, setOauthTotal] = useState(0); const [selectedOauthItems, setSelectedOauthItems] = useState<({ value: string; label: string } | undefined)[]>([undefined]); const [materialFileNames, setMaterialFileNames] = useState>(new Map()); const [unifiedFileName, setUnifiedFileName] = useState(''); const updateFilenameDebounceRef = useRef | null>(null); const [oauthPage, setOauthPage] = useState(1); const [oauthPageSize, setOauthPageSize] = useState(10); const [oauthSelectOpens, setOauthSelectOpens] = useState([false]); // 上传任务历史弹窗相关状态 const [uploadHistoryModalVisible, setUploadHistoryModalVisible] = useState(false); const [uploadHistoryList, setUploadHistoryList] = useState([]); const [uploadHistoryTotal, setUploadHistoryTotal] = useState(0); const [uploadHistoryPage, setUploadHistoryPage] = useState(1); const [uploadHistoryPageSize, setUploadHistoryPageSize] = useState(10); const [uploadHistoryLoading, setUploadHistoryLoading] = useState(false); const [uploadHistoryStatus, setUploadHistoryStatus] = useState(''); // 多选相关状态 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.generatedResourceId || 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); }; // 获取item的资源ID(优先使用generatedResourceId,否则使用id) const getItemResourceId = (item: any): string => { return item.generatedResourceId || item.id; }; // 判断item是否有generatedResourceId const hasGeneratedResourceId = (item: any): boolean => { return Boolean(item.generatedResourceId); }; // 多选相关函数 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) => getItemResourceId(item)) ); if (selectedItems.size === allItemIds.length) { setSelectedItems(new Set()); } else { setSelectedItems(new Set(allItemIds)); } }; 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('请先选择要上传的媒体'); return; } setAccountIdLists([[]]); setAccountIdInputs(['']); setSelectedOauthItems([undefined]); setUploadConfigModalVisible(true); }; const handleSinglePushToMedia = () => { if (!previewItem) return; const resourceId = getItemResourceId(previewItem); setSelectedItems(new Set([resourceId])); setAccountIdLists([[]]); setAccountIdInputs(['']); setSelectedOauthItems([undefined]); setMaterialFileNames(new Map()); setUnifiedFileName(''); setUploadConfigModalVisible(true); }; const loadOAuthList = async (page: number, pageSize: number) => { setOauthLoading(true); try { const res = await getOAuthList({ page, page_size: pageSize }); const data = res?.data || res; setOauthList(data || []); setOauthTotal(res.pagination.total || 0); } catch (error) { console.error('加载授权列表失败:', error); setOauthList([]); setOauthTotal(0); } finally { setOauthLoading(false); } }; const loadUploadHistory = async () => { setUploadHistoryLoading(true); try { const res = await getUploadHistory({ status: uploadHistoryStatus || undefined, page: uploadHistoryPage, pageSize: uploadHistoryPageSize, }); const data = res?.data || res; setUploadHistoryList(data || []); setUploadHistoryTotal(res.pagination?.total || res.total || 0); } catch (error) { console.error('加载上传历史失败:', error); setUploadHistoryList([]); setUploadHistoryTotal(0); } finally { setUploadHistoryLoading(false); } }; const handleOpenUploadHistory = () => { setUploadHistoryModalVisible(true); setUploadHistoryPage(1); setUploadHistoryStatus(''); loadUploadHistory(); }; const handleUploadHistorySearch = () => { setUploadHistoryPage(1); loadUploadHistory(); }; const handleUploadHistoryPageChange = (page: number, pageSize: number) => { setUploadHistoryPage(page); setUploadHistoryPageSize(pageSize); loadUploadHistory(); }; // 批量上传素材 const handleStartBatchUpload = async () => { const validOauthItems = selectedOauthItems.filter(item => item !== undefined); if (validOauthItems.length === 0) { message.warning('请先选择授权账户'); return; } if (selectedItems.size === 0) { message.warning('请先选择要上传的媒体'); return; } setUploading(true); try { const tasks: { advertiser_ids: string[]; resource_ids: string[]; oauth_id: string; source_model: string; }[] = []; const resourceIds = Array.from(selectedItems); for (let i = 0; i < validOauthItems.length; i++) { const oauthItem = validOauthItems[i]; const advertiserIds = accountIdLists[i]?.map(account => account.accountId) || []; if (advertiserIds.length === 0) { message.warning(`第 ${i + 1} 组授权账户未设置账户ID,已跳过`); continue; } tasks.push({ advertiser_ids: advertiserIds, resource_ids: resourceIds, oauth_id: oauthItem.value, source_model: filterType === 'project' ? 'generation_records' : 'chat_generation_tasks', }); } await asyncBatchUploadMaterial({ tasks }); message.success(`已提交 ${tasks.length} 个上传任务,后台异步处理中`); setIsSelectionMode(false); setSelectedItems(new Set()); // 关闭弹窗并清理状态 setUploadConfigModalVisible(false); setAccountIdLists([[]]); setAccountIdInputs(['']); setSelectedOauthItems([undefined]); setMaterialFileNames(new Map()); setUnifiedFileName(''); } catch (error: any) { console.error('批量上传失败:', error); message.error(error.message || '批量上传失败'); } finally { setUploading(false); } }; // 更新文件名函数 const handleUpdateFileName = async (sourceId: string, newFileName: string) => { if (!newFileName.trim()) return; try { const response = await updateFilename({ filenames: [{ source_id: sourceId, file_name: newFileName }], }); // 更新 recordlist 中的文件名,使用 API 返回的 new_file_name const result = response?.results?.find((r: any) => r.source_id === sourceId); const actualFileName = result?.new_file_name || newFileName; setRecordList(prevList => { return prevList.map(group => ({ ...group, items: group.items.map((item: any) => { const resourceId = getItemResourceId(item); if (resourceId === sourceId) { return { ...item, fileName: actualFileName }; } return item; }), })); }); message.success('文件名更新成功'); } catch (error: any) { console.error('文件名更新失败:', error); message.error(error.message || '文件名更新失败'); } }; // 批量更新文件名函数 const handleBatchUpdateFileName = async (sourceIds: string[], newFileName: string) => { if (!newFileName.trim() || sourceIds.length === 0) return; try { const filenames = sourceIds.map(sourceId => ({ source_id: sourceId, file_name: newFileName, })); const response = await updateFilename({ filenames }); // 批量更新 recordlist 中的文件名,使用 API 返回的 new_file_name const resultsMap = new Map(); response?.results?.forEach((r: any) => { if (r.success && r.newFileName) { resultsMap.set(r.sourceId, r.newFileName); } }); setRecordList(prevList => { const sourceIdSet = new Set(sourceIds); return prevList.map(group => ({ ...group, items: group.items.map((item: any) => { const resourceId = getItemResourceId(item); if (sourceIdSet.has(resourceId)) { const actualFileName = resultsMap.get(resourceId) || newFileName; return { ...item, fileName: actualFileName }; } return item; }), })); }); console.log(response); const successCount = response?.successCount || 0; message.success(`已更新 ${successCount} 个文件名`); } catch (error: any) { console.error('文件名更新失败:', error); message.error(error.message || '文件名更新失败'); } }; // 日期选择器变化处理函数 const handleDateChange = (dateString: string) => { setSelectedDate(dateString); }; 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}`; } if (selectedDate) { let parameters = ``; if (filterType === 'project') { parameters = `${selectedDate}?gen_type=${filterMedia}&history_source=generation_record&page=${Pagebreak.page}&page_size=${Pagebreak.pageSize}`; } else { parameters = `${selectedDate}?gen_type=${filterMedia}&page=${Pagebreak.page}&page_size=${Pagebreak.pageSize}`; } gethistoryItems(parameters).then((res: any) => { const data = Array.isArray(res) ? res : (res?.items || []); let recordList = [{ generatedDate: res.generatedDate, items: data, total: res.total, page: res.page, }]; if (recordList && recordList[0].items.length > 0) { setRecordList(recordList); } else { setRecordList([]); } }).catch((err) => { }).finally(() => { setLoading(false); }); try { } catch (err) { } finally { } } else { 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, selectedDate]); // 加载更多 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 (
{/* 操作栏:筛选 + 上传按钮 */}
{/* 多选模式按钮 */} {isSelectionMode ? ( ) : ( )}
{/* Second row filter: 视频 / 图片 */}
媒体类型: handleDateChange(dateString || '')} format="YYYY-MM-DD" style={{ width: 160, borderRadius: 8, border: '1px solid #e2e8f0' }} placeholder="选择日期" /> {selectedDate && ( )}
{/* Content area */} {recordlist.length === 0 ? ( ) : (
{recordlist.map((group: any, index: number) => (
{/* Date label */}
{group.generatedDate}
{/* Media grid */}
{group.items.map((item: any) => ( isSelectionMode ? handleToggleSelect(getItemResourceId(item)) : handlePreview(item)} isSelected={selectedItems.has(getItemResourceId(item))} onToggleSelect={handleToggleSelect} isSelectionMode={isSelectionMode} /> ))}
{/* 分组内加载更多 */} {group.total && group.total > group.items.length && (
)}
))} {/* 加载更多按钮 */} {recordlist.length > 0 && Totalnumber > recordlist.length && (
)}
)} {/* 上传配置弹窗 */} { setUploadConfigModalVisible(false); setAccountIdLists([[]]); setAccountIdInputs(['']); setSelectedOauthItems([undefined]); setMaterialFileNames(new Map()); setUnifiedFileName(''); }} footer={null} width={900} mask={{ closable: false }} >
选中素材 ({selectedItems.size}个)
统一修改名称: setUnifiedFileName(e.target.value)} placeholder="输入名称后点击应用" style={{ flex: 1, borderRadius: 8 }} size="small" />
{(() => { const itemMap = new Map(); recordlist.forEach((group: any) => { group.items.forEach((item: any) => { const resourceId = getItemResourceId(item); itemMap.set(resourceId, item); }); }); return Array.from(selectedItems).map((itemId) => { const item = itemMap.get(itemId); return (
{(() => { const coverField = filterMedia === 'video' ? item?.videoCoverUrl : item?.imageUrl; if (!coverField) { return 预览; } const baseUrl = import.meta.env.VITE_API_BASE || "http://localhost:8000"; const cleanPath = coverField.startsWith('/') ? coverField.slice(1) : coverField; const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; const coverUrl = `${cleanBase}/static/${cleanPath}&w=300&q=50`; return ( ); })()}
{item?.fileName || `素材 ${item.id}`}
{ const newName = e.target.value; const newNames = new Map(materialFileNames); newNames.set(itemId, newName); setMaterialFileNames(newNames); // 防抖调用API if (updateFilenameDebounceRef.current) { clearTimeout(updateFilenameDebounceRef.current); } updateFilenameDebounceRef.current = setTimeout(() => { handleUpdateFileName(itemId, newName); }, 800); }} placeholder="输入新名称" style={{ width: 200, borderRadius: 4 }} size="small" />
); }); })()}
{selectedOauthItems.map((oauthItem, index) => (
授权账户 {index + 1} {selectedOauthItems.length > 1 && ( )}
setUploadHistoryStatus(value)} placeholder="选择状态" style={{ width: 200, marginRight: 12 }} options={[ { value: '1', label: '待上传' }, { value: '2', label: '上传中' }, { value: '3', label: '上传成功' }, { value: '4', label: '上传失败' }, ]} allowClear />
{ // const statusColorMap: Record = { // 1: '#f59e0b', // 2: '#6366f1', // 3: '#10b981', // 4: '#ef4444', // }; // return ( // // {record.status_text || status} // // ); // }, // }, { title: '状态', dataIndex: 'status', key: 'status', width: 100, render: (status: string) => { const statusMap: Record = { '1': '待上传', '2': '上传中', '3': '上传成功', '4': '上传失败', }; const statusColorMap: Record = { '1': '#f59e0b', '2': '#6366f1', '3': '#10b981', '4': '#ef4444', }; return ( {statusMap[status] || status} ); }, }, { title: '备注', dataIndex: 'note', key: 'note', width: 250, ellipsis: true, render: (note: string) => ( {note || '-'} ), }, { title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180, render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'), }, ]} loading={uploadHistoryLoading} scroll={{ x: 'max-content' }} pagination={{ current: uploadHistoryPage, pageSize: uploadHistoryPageSize, total: uploadHistoryTotal, showSizeChanger: true, showTotal: (total) => `共 ${total} 条记录`, onChange: handleUploadHistoryPageChange, }} rowKey={(record, index) => record.task_id || record.resource_id || index} size="small" /> {/* 预览弹窗 */} {previewVisible && previewItem && (
e.stopPropagation()} > {/* 头部 */}
{previewItem.title || '预览'}
{/* 内容区域 */}
{/* 媒体预览 */}
{/* 检查媒体是否过期 */} {isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? (
⚠️

图片/视频资源已过期,请刷新重新加载~

) : filterMedia === 'video' ? (
{/* 参数信息 */}
文件信息
{/* ID */} {/*
ID {previewItem.id || '-'}
*/} {/* 类型 */}
类型 {filterMedia === 'video' ? '视频' : '图片'}
请求 {previewItem.originalPrompt}
{/* 分辨率 */} {filterMedia === 'image' && ( <>
比例 {previewItem.imageProportion}
分辨率 {previewItem.imageSize}
尺寸 {previewItem.imagePx}
)} {filterMedia === 'video' && ( <>
比例 {previewItem.aspectRatio}
分辨率 {previewItem.resolution}
{/*
尺寸 {previewItem.imagePx}
*/} )} {/* 时长(视频) */} {filterMedia === 'video' && (
时长 {`${previewItem.duration}秒` || '-'}
)} {/* 文件大小 */} {/*
文件大小 {previewItem.size ? formatFileSize(previewItem.size) : '-'}
*/} {/* 创建时间 */}
创建时间 {formatDateTime(previewItem.createdAt || previewItem.generatedDate)}
{/* 生成引擎 */}
生成引擎 {previewItem.engine || previewItem.engineName || '-'}
{/* 分隔线 */}
{previewItem.mediaReferences && previewItem.mediaReferences.length > 0 && ( <> 依靠附件 {previewItem.mediaReferences.map((item, index) => (
{ // 暂停视频 if (videoRef.current) { videoRef.current.pause(); } const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.url}`; window.open(url, '_blank'); }} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: 8, borderRadius: 6, cursor: 'pointer', backgroundColor: '#f1f5f9', marginBottom: 4, transition: 'background-color 0.2s', }} onMouseEnter={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = '#e2e8f0'; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.backgroundColor = '#f1f5f9'; }} > {item.type === 'image' ? ( ) : ( )} {item.name || `媒体${index + 1}`}
))} )} {/* 操作按钮 */}
)}
); }; // 文件大小格式化 function formatFileSize(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } // 添加旋转动画样式 const styleSheet = document.createElement('style'); styleSheet.textContent = ` @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } `; document.head.appendChild(styleSheet); // 日期格式化(年月日时分秒) function formatDateTime(dateString: string): string { if (!dateString) return '-'; const date = new Date(dateString); if (isNaN(date.getTime())) return '-'; const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } export default GeneratedRecord;