From c8ad49c7362b84e995683ebe5755bf87eb10ea47 Mon Sep 17 00:00:00 2001 From: Lrd <13001933075@sina.cn> Date: Thu, 18 Jun 2026 17:11:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B4=A0=E6=9D=90=E4=BA=91=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- video-gen-app/src/App.tsx | 4 +- video-gen-app/src/pages/AuthorizationPage.tsx | 178 +- .../src/pages/GeneratedRecord copy.tsx | 1891 +++++++++++++++++ video-gen-app/src/pages/GeneratedRecord.tsx | 458 +++- video-gen-app/src/pages/PreTest.tsx | 2 +- 5 files changed, 2425 insertions(+), 108 deletions(-) create mode 100644 video-gen-app/src/pages/GeneratedRecord copy.tsx diff --git a/video-gen-app/src/App.tsx b/video-gen-app/src/App.tsx index 4abf459b..f0936d95 100644 --- a/video-gen-app/src/App.tsx +++ b/video-gen-app/src/App.tsx @@ -105,8 +105,6 @@ const App = () => { } /> } /> {/* } /> */} - - } /> } /> } /> @@ -120,4 +118,4 @@ const App = () => { ); }; -export default App; +export default App; \ No newline at end of file diff --git a/video-gen-app/src/pages/AuthorizationPage.tsx b/video-gen-app/src/pages/AuthorizationPage.tsx index 73a957bb..d9e71343 100644 --- a/video-gen-app/src/pages/AuthorizationPage.tsx +++ b/video-gen-app/src/pages/AuthorizationPage.tsx @@ -16,6 +16,27 @@ const OPEN_TYPE_MAP: Record = { 10: '腾讯营销K3', }; +const PORT_TYPE_MAP: Record = { + 1: '巨量', + 2: '磁力', + 3: '巨量星图', + 4: '服务单', + 5: '腾讯', +}; + +// 格式化时间 2026-06-12T03:47:28.542988Z -> 2026-06-12 03:47:28 +const formatDateTime = (dateStr: string) => { + if (!dateStr) return ''; + const date = new Date(dateStr); + 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}`; +}; + interface AuthorizationData { id: string; status: string; @@ -136,84 +157,109 @@ const AuthorizationPage: React.FC = () => { setSelectedOpenType(undefined); } }; - - // account_id - // : - // "1836691333531724" - // account_name - // : - // "BP-1836691333531724" - // account_role - // : - // "CUSTOMER_ADMIN" - // account_userid - // : - // null - // account_username - // : - // "a***3@minzhong.cn" - // appid - // : - // "1866261617455433" - // created_at - // : - // "2026-06-15T10:00:22.734565Z" - // id - // : - // "0019ecab9b8bc57d964" - // material_auth_status - // : - // true - // open_type - // : - // 2 - // port_type - // : - // 1 - // updated_at - // : - // "2026-06-17T06:41:33.012018Z" - // user_id - // : - // "0019e96d23e57a808e2" - const columns = [ { - title: '序号', - dataIndex: 'index', - key: 'index', - width: 80, - render: (text: number) => {text}, - }, - { - title: '授权ID', + title: 'ID', dataIndex: 'id', key: 'id', - ellipsis: true, - render: (text: string) => ( - {text} + }, + { + title: '授权账户ID', + dataIndex: 'accountId', + key: 'accountId', + width: 160, + }, + { + title: '授权账户名称', + dataIndex: 'accountName', + key: 'accountName', + }, + { + title: '授权账户角色', + dataIndex: 'accountRole', + key: 'accountRole', + render: (role: string) => { + const roleMap: Record = { + ADVERTISER: '客户', + CUSTOMER_ADMIN: '普通版工作台-管理员', + CUSTOMER_OPERATOR: '普通版工作台-协作者', + AGENT: '代理商', + CHILD_AGENT: '二级代理商', + PLATFORM_ROLE_STAR: '星图账户', + PLATFORM_ROLE_SHOP_ACCOUNT: '抖音店铺账户', + PLATFORM_ROLE_QIANCHUAN_AGENT: '千川代理商', + PLATFORM_ROLE_STAR_AGENT: '星图代理商', + PLATFORM_ROLE_AWEME: '抖音号', + PLATFORM_ROLE_STAR_MCN: '星图MCN机构', + PLATFORM_ROLE_STAR_ISV: '星图服务商', + AGENT_SYSTEM_ACCOUNT: '代理商系统账户', + PLATFORM_ROLE_LOCAL_AGENT: '本地推代理商', + PLATFORM_ROLE_YUNTU_BRAND_ISV_ADMIN: '云图品牌服务商管理员', + PLATFORM_ROLE_LIFE: '抖音来客账户', + PLATFORM_ROLE_ENTERPRISE_BP_ADMIN: '升级版工作台管理员', + PLATFORM_ROLE_ENTERPRISE_BP_OPERATOR: '升级版工作台协作者', + }; + return roleMap[role] || role; + }, + }, + { + title: '授权账户用户ID', + dataIndex: 'accountUserid', + key: 'accountUserid', + render: (text: string) => {text || '-'}, + }, + { + title: '授权账户用户名', + dataIndex: 'accountUsername', + key: 'accountUsername', + }, + { + title: '授权应用ID', + dataIndex: 'appid', + key: 'appid', + }, + { + title: '是否敏感物料授权', + dataIndex: 'materialAuthStatus', + key: 'materialAuthStatus', + width: 100, + render: (text: boolean) => ( + + {text ? '是' : '否'} + ), }, { title: '开户方式', - dataIndex: 'open_type', - key: 'open_type', - width: 140, + dataIndex: 'openType', + key: 'openType', render: (text: number) => {OPEN_TYPE_MAP[text] || text}, }, { - title: '授权状态', - dataIndex: 'status', - key: 'status', - width: 140, - render: (text: string) => renderStatus(text), + title: '平台端口', + dataIndex: 'portType', + key: 'portType', + ellipsis: true, + render: (text: number) => {PORT_TYPE_MAP[text] || text}, + }, + { + title: '用户id', + dataIndex: 'userId', + key: 'userId', }, { - title: '描述', - dataIndex: 'description', - key: 'description', - ellipsis: true, - render: (text: string) => {text}, + title: '创建时间', + dataIndex: 'createdAt', + key: 'createdAt', + width: 160, + render: (text: string) => {formatDateTime(text)}, + }, + { + title: '更新时间', + dataIndex: 'updatedAt', + key: 'updatedAt', + width: 160, + render: (text: string) => {formatDateTime(text)}, }, ]; @@ -224,7 +270,7 @@ const AuthorizationPage: React.FC = () => { })); return ( -
+

授权管理 diff --git a/video-gen-app/src/pages/GeneratedRecord copy.tsx b/video-gen-app/src/pages/GeneratedRecord copy.tsx new file mode 100644 index 00000000..b1f7ea47 --- /dev/null +++ b/video-gen-app/src/pages/GeneratedRecord copy.tsx @@ -0,0 +1,1891 @@ +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 }} + /> +
+
+ 前测模板 + { + setSelectedOauthItems(value as string[]); + const selectedAccounts = oauthList + .filter(item => value.includes(String(item.id))) + .map(item => ({ accountId: String(item.accountId), authStatus: 'authorized' as const })); + + const textAccounts = accountIdInput.split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0) + .map(id => ({ accountId: id, authStatus: 'pending' as const })); + + const mergedAccounts = [...selectedAccounts, ...textAccounts]; + const seen = new Set(); + const finalAccounts = mergedAccounts.filter(a => { + if (seen.has(a.accountId)) return false; + seen.add(a.accountId); + return true; + }); + + setAccountIdList(finalAccounts); + }} + placeholder="点击选择授权账户" + style={{ width: '100%', marginBottom: 16, borderRadius: 8 }} + popupRender={() => ( +
+ { + const roleMap: Record = { + ADVERTISER: '客户', + CUSTOMER_ADMIN: '普通版工作台-管理员', + CUSTOMER_OPERATOR: '普通版工作台-协作者', + AGENT: '代理商', + CHILD_AGENT: '二级代理商', + PLATFORM_ROLE_STAR: '星图账户', + PLATFORM_ROLE_SHOP_ACCOUNT: '抖音店铺账户', + PLATFORM_ROLE_QIANCHUAN_AGENT: '千川代理商', + PLATFORM_ROLE_STAR_AGENT: '星图代理商', + PLATFORM_ROLE_AWEME: '抖音号', + PLATFORM_ROLE_STAR_MCN: '星图MCN机构', + PLATFORM_ROLE_STAR_ISV: '星图服务商', + AGENT_SYSTEM_ACCOUNT: '代理商系统账户', + PLATFORM_ROLE_LOCAL_AGENT: '本地推代理商', + PLATFORM_ROLE_YUNTU_BRAND_ISV_ADMIN: '云图品牌服务商管理员', + PLATFORM_ROLE_LIFE: '抖音来客账户', + PLATFORM_ROLE_ENTERPRISE_BP_ADMIN: '升级版工作台管理员', + PLATFORM_ROLE_ENTERPRISE_BP_OPERATOR: '升级版工作台协作者', + }; + return roleMap[role] || role; + }, + }, + { + title: '授权账户用户名', + dataIndex: 'accountUsername', + key: 'accountUsername', + width: 120, + render: (text: string) => {text || '-'}, + }, + ]} + loading={oauthLoading} + pagination={{ + current: oauthPage, + pageSize: oauthPageSize, + total: oauthTotal, + showSizeChanger: true, + showTotal: (total) => `共 ${total} 条记录`, + onChange: (page, size) => { + setOauthPage(page); + setOauthPageSize(size); + loadOAuthList(page, size); + }, + }} + rowKey="id" + size="small" + onRow={(record) => ({ + onClick: () => { + const id = String(record.id); + setSelectedOauthItems(prev => { + if (prev.includes(id)) { + return prev.filter(item => item !== id); + } + return [...prev, id]; + }); + }, + style: { + cursor: 'pointer', + backgroundColor: selectedOauthItems.includes(String(record.id)) ? '#e6f7ff' : undefined, + }, + })} + /> + + )} + open={oauthSelectOpen} + onOpenChange={(open) => { + setOauthSelectOpen(open); + if (open) { + loadOAuthList(1, oauthPageSize); + } + }} + /> + + + 粘贴账户ID(每行一个) + + + { + const value = e.target.value; + setAccountIdInput(value); + + const ids = value.split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0); + + const uniqueIds = [...new Set(ids)]; + const textAccounts = uniqueIds.map(id => ({ accountId: id, authStatus: 'pending' as const })); + + const oauthAccounts = oauthList + .filter(item => selectedOauthItems.includes(String(item.id))) + .map(item => ({ accountId: String(item.accountId), authStatus: 'authorized' as const })); + + const mergedAccounts = [...oauthAccounts, ...textAccounts]; + const seen = new Set(); + const finalAccounts = mergedAccounts.filter(a => { + if (seen.has(a.accountId)) return false; + seen.add(a.accountId); + return true; + }); + + setAccountIdList(finalAccounts); + }} + placeholder="粘贴账户ID,每行一个,例如: +10001 +10002 +10003" + rows={4} + style={{ borderRadius: 8, marginBottom: 16 }} + /> + + {accountIdList.length > 0 && ( + <> + + 已导入 {accountIdList.length} 个账户 + + +
({ ...item, key: index }))} + columns={[ + { + title: '账户ID', + dataIndex: 'accountId', + key: 'accountId', + width: '70%', + }, + { + title: '授权状态', + dataIndex: 'authStatus', + key: 'authStatus', + width: '30%', + render: (status: 'pending' | 'authorized' | 'failed') => { + if (status === 'authorized') { + return 已授权; + } + if (status === 'failed') { + return 授权失败; + } + return 待授权; + }, + }, + ]} + pagination={false} + size="small" + style={{ + maxHeight: 300, + overflow: 'auto', + border: '1px solid #e8e8e8', + borderRadius: 8, + }} + /> + + + + )} + + {/* 上传进度区域 */} + {batchUploadProgress.length > 0 && ( +
+ + 上传进度 ({batchUploadProgress.filter(p => p.status === 'success').length}/{batchUploadProgress.length}) + + {batchUploadProgress.map((progress) => ( +
+
+ {progress.status === 'success' && } + {progress.status === 'error' && } + {progress.status === 'uploading' && } + {progress.status === 'pending' && } +
+
+
+ {progress.itemId} +
+ {progress.message && ( +
+ {progress.message} +
+ )} +
+
+ ))} +
+ )} + + {/* 操作按钮 */} +
+ + +
+ + + {/* 预览弹窗 */} {previewVisible && previewItem && (
{ }; return ( -
+

前测管理