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 */} + {/* + + 生成历史 + + + 查看所有生成的视频和图片记录 + + */} + + {/* 操作栏:筛选 + 上传按钮 */} + + + + + setFilterType('project')} + style={{ + borderRadius: 8, + background: filterType === 'project' + ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' + : '#f8f9fc', + border: filterType === 'project' ? 'none' : '1px solid #e2e8f0', + color: filterType === 'project' ? '#fff' : '#64748b', + fontWeight: 600, + }} + icon={} + > + 项目记录 + + setFilterType('creation')} + style={{ + borderRadius: 8, + background: filterType === 'creation' + ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' + : '#f8f9fc', + border: filterType === 'creation' ? 'none' : '1px solid #e2e8f0', + color: filterType === 'creation' ? '#fff' : '#64748b', + fontWeight: 600, + }} + icon={} + > + 创作记录 + + + + + {/* 多选模式按钮 */} + {isSelectionMode ? ( + + + {selectedItems.size === recordlist.reduce((sum: number, group: any) => sum + group.items.length, 0) ? '取消全选' : '全选'} + + { + setIsSelectionMode(false); + setSelectedItems(new Set()); + }} + style={{ + borderRadius: 8, + background: '#f8f9fc', + border: '1px solid #e2e8f0', + color: '#64748b', + fontWeight: 600, + }} + > + 取消选择 + + + {uploading ? '上传中...' : `上传选中 (${selectedItems.size})`} + + + ) : ( + } + onClick={() => setIsSelectionMode(true)} + style={{ + borderRadius: 8, + background: 'linear-gradient(135deg, #10b981, #059669)', + border: 'none', + fontWeight: 600, + }} + > + 批量上传 + + )} + + + + {/* Second row filter: 视频 / 图片 */} + + 媒体类型: + + setFilterMedia('video')} + style={{ + borderRadius: 8, + background: filterMedia === 'video' + ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' + : '#f8f9fc', + border: filterMedia === 'video' ? 'none' : '1px solid #e2e8f0', + color: filterMedia === 'video' ? '#fff' : '#64748b', + fontWeight: 600, + }} + icon={} + > + 视频 + + setFilterMedia('image')} + style={{ + borderRadius: 8, + background: filterMedia === 'image' + ? 'linear-gradient(135deg, #6366f1, #8b5cf6)' + : '#f8f9fc', + border: filterMedia === 'image' ? 'none' : '1px solid #e2e8f0', + color: filterMedia === 'image' ? '#fff' : '#64748b', + fontWeight: 600, + }} + icon={} + > + 图片 + + + + + {/* 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 && ( + + handleGroupLoadMore(group.generatedDate, group.items, group.page)} + loading={loadingGroups.has(group.generatedDate)} + disabled={loadingGroups.has(group.generatedDate)} + size="small" + style={{ + borderRadius: 6, + background: 'transparent', + border: '1px dashed #cbd5e1', + color: '#64748b', + fontSize: 12, + }} + > + {loadingGroups.has(group.date) ? '加载中...' : `查看全部 (${group.total})`} + + + )} + + ))} + {/* 加载更多按钮 */} + {recordlist.length > 0 && Totalnumber > recordlist.length && ( + + + {loading ? '加载中...' : '加载更多'} + + + )} + + )} + + {/* 批量上传弹窗 */} + { + 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 && ( + + )} + + } + onClick={() => handleRemoveFile(file)} + style={{ + background: 'transparent', + border: 'none', + color: '#999', + }} + /> + + ))} + + + )} + + {/* 操作按钮 */} + + { + setUploadModalVisible(false); + setUploadFiles([]); + }} + style={{ borderRadius: 8 }} + > + 取消 + + + {uploading ? '上传中...' : '开始上传'} + + + + + + {/* 上传配置弹窗 */} + { + 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 }} + /> + + + 前测模板 + { + const newConfigs = { ...selectedMediaConfigs }; + newConfigs[media.id] = [...configs]; + newConfigs[media.id][configIndex].templateId = value; + setSelectedMediaConfigs(newConfigs); + }} + placeholder="请选择前测模板" + loading={preTestLoading} + style={{ width: '100%', borderRadius: 6 }} + options={preTestTemplates.map((template: any) => ({ + value: template.id, + label: template.name || template.id, + }))} + /> + + } + onClick={() => { + if (configs.length > 1) { + const newConfigs = { ...selectedMediaConfigs }; + newConfigs[media.id] = configs.filter((_, i) => i !== configIndex); + setSelectedMediaConfigs(newConfigs); + } + }} + disabled={configs.length === 1} + style={{ + background: 'transparent', + border: 'none', + color: '#999', + }} + /> + + ))} + + {/* 该媒体的添加配置按钮 */} + { + const newConfigs = { ...selectedMediaConfigs }; + newConfigs[media.id] = [...configs, { accountId: '', templateId: '' }]; + setSelectedMediaConfigs(newConfigs); + }} + icon={} + size="small" + style={{ + borderRadius: 6, + }} + > + 添加配置 + + + + ); + })} + + + {/* 上传进度区域 */} + {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} + + )} + + + ))} + + )} + + {/* 操作按钮 */} + + { + setUploadConfigModalVisible(false); + setSelectedMediaList([]); + setSelectedMediaConfigs({}); + setBatchUploadProgress([]); + }} + style={{ borderRadius: 8 }} + > + 取消 + + { + const configs = selectedMediaConfigs[media.id] || []; + return configs.length > 0 && configs.every(c => c.accountId && c.templateId); + })} + style={{ borderRadius: 8 }} + > + {uploading ? '上传中...' : '开始上传'} + + + + + + {/* 预览弹窗 */} + {previewVisible && previewItem && ( + + e.stopPropagation()} + > + {/* 头部 */} + + + {previewItem.title || '预览'} + + } + onClick={handleClosePreview} + style={{ + background: 'transparent', + border: 'none', + color: '#94a3b8', + fontSize: 16, + }} + /> + + + {/* 内容区域 */} + + {/* 媒体预览 */} + + {/* 检查媒体是否过期 */} + {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}`} + + + ))} + > + )} + {/* 操作按钮 */} + + + } + onClick={() => { + // 暂停视频 + if (videoRef.current) { + videoRef.current.pause(); + } + const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${previewItem.videoUrl || previewItem.imageUrl}&download=1`; + window.open(url, '_blank'); + }} + style={{ flex: 1, borderRadius: 8 }} + disabled={isMediaExpired(previewItem.videoUrl || previewItem.imageUrl)} + > + {isMediaExpired(previewItem.videoUrl || previewItem.imageUrl) ? '资源已过期' : '下载'} + + + 关闭 + + + + + + 推送媒体后台 + + + + + + + + + + )} + + ); +}; + +// 文件大小格式化 +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; diff --git a/video-gen-app/src/pages/GeneratedRecord.tsx b/video-gen-app/src/pages/GeneratedRecord.tsx index 0db42f0c..3c067919 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, Upload, Modal, Progress } from 'antd'; +import { Button, Empty, Input, Select, Space, Typography, Tag, message, Upload, Modal, Progress, Table } from 'antd'; import { SearchOutlined, FilterOutlined, @@ -13,7 +13,7 @@ import { UploadOutlined, PlusOutlined, } from '@ant-design/icons'; -import { gethistory, gethistoryItems } from '../api'; +import { gethistory, gethistoryItems, getDefaultPreTest, getOAuthList } from '../api'; const { Search } = Input; const { Text } = Typography; @@ -39,6 +39,34 @@ const GeneratedRecord: React.FC = () => { 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 [accountIdList, setAccountIdList] = useState<{ + accountId: string; + authStatus: 'pending' | 'authorized' | 'failed'; + }[]>([]); + const [accountIdInput, setAccountIdInput] = useState(''); + + const [oauthList, setOauthList] = useState([]); + const [oauthLoading, setOauthLoading] = useState(false); + const [oauthTotal, setOauthTotal] = useState(0); + const [selectedOauthItems, setSelectedOauthItems] = useState([]); + const [oauthPage, setOauthPage] = useState(1); + const [oauthPageSize, setOauthPageSize] = useState(10); + const [oauthSelectOpen, setOauthSelectOpen] = useState(false); + + const [batchUploadProgress, setBatchUploadProgress] = useState<{ + itemId: string; + status: 'pending' | 'uploading' | 'success' | 'error'; + message: string; + }[]>([]); + // 多选相关状态 const [isSelectionMode, setIsSelectionMode] = useState(false); const [selectedItems, setSelectedItems] = useState>(new Set()); @@ -619,23 +647,49 @@ const GeneratedRecord: React.FC = () => { } }; - const handleBatchUploadSelected = async () => { + const handleBatchUploadSelected = () => { if (selectedItems.size === 0) { message.warning('请先选择要上传的媒体'); return; } + setAccountIdList([]); + setAccountIdInput(''); + setBatchUploadProgress([]); + 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; + console.log(res); + setOauthList(data|| []); + setOauthTotal(res.pagination.total || 0); + } catch (error) { + console.error('加载授权列表失败:', error); + setOauthList([]); + setOauthTotal(0); + } finally { + setOauthLoading(false); + } + }; + + const handleStartBatchUpload = async () => { setUploading(true); - const uploadProgressMap: { [key: string]: number } = {}; + 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 = ''; - // 找到对应的 item for (const group of recordlist) { const found = group.items.find((i: any) => i.id === itemId); if (found) { @@ -646,7 +700,6 @@ const GeneratedRecord: React.FC = () => { if (!item) continue; - // 根据媒体类型获取 URL if (filterMedia === 'video' && item.videoUrl) { mediaUrl = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${item.videoUrl}`; mediaType = 'video'; @@ -657,12 +710,10 @@ const GeneratedRecord: React.FC = () => { continue; } - // 初始化进度 - uploadProgressMap[itemId] = 0; - setUploadProgress({ ...uploadProgressMap }); + 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(); @@ -670,37 +721,39 @@ const GeneratedRecord: React.FC = () => { 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); + for (const accountItem of accountIdList) { + const form = new FormData(); + form.append('file', file); + form.append('media_type', mediaType); + form.append('original_id', itemId); + form.append('account_id', accountItem.accountId); - 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 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 || '上传失败'); } - ); + } - if (!uploadResponse.ok) throw new Error('上传失败'); - - // 更新进度 - uploadProgressMap[itemId] = 100; - setUploadProgress({ ...uploadProgressMap }); - - } catch (error) { + uploadProgressMap[itemId] = { status: 'success', message: '上传成功' }; + } catch (error: any) { console.error(`上传失败: ${itemId}`, error); - uploadProgressMap[itemId] = -1; // 标记失败 - setUploadProgress({ ...uploadProgressMap }); + uploadProgressMap[itemId] = { status: 'error', message: error.message || '上传失败' }; } + setBatchUploadProgress(Object.entries(uploadProgressMap).map(([itemId, value]) => ({ itemId, ...value }))); } - const successCount = Object.values(uploadProgressMap).filter(p => p === 100).length; - const failCount = Object.values(uploadProgressMap).filter(p => p === -1).length; + 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} 个文件`); @@ -708,11 +761,8 @@ const GeneratedRecord: React.FC = () => { message.warning(`上传完成:成功 ${successCount} 个,失败 ${failCount} 个`); } - // 退出选择模式 setIsSelectionMode(false); setSelectedItems(new Set()); - setUploadProgress({}); - } catch (error) { message.error('批量上传失败'); console.error(error); @@ -751,6 +801,22 @@ const GeneratedRecord: React.FC = () => { }); }, [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; @@ -1208,6 +1274,322 @@ const GeneratedRecord: React.FC = () => { + {/* 上传配置弹窗 */} + { + setUploadConfigModalVisible(false); + setAccountIdList([]); + setAccountIdInput(''); + setSelectedOauthItems([]); + setBatchUploadProgress([]); + }} + footer={null} + width={900} + > + + + 选择授权账户(可多选) + + + { + 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, + }} + /> + + { + setAccountIdList([]); + setAccountIdInput(''); + setSelectedOauthItems([]); + }} + style={{ marginTop: 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} + + )} + + + ))} + + )} + + {/* 操作按钮 */} + + { + setUploadConfigModalVisible(false); + setAccountIdList([]); + setAccountIdInput(''); + setSelectedOauthItems([]); + setBatchUploadProgress([]); + }} + style={{ borderRadius: 8 }} + > + 取消 + + + {uploading ? '上传中...' : '开始上传'} + + + + + {/* 预览弹窗 */} {previewVisible && previewItem && ( { }; return ( - + 前测管理
图片/视频资源已过期,请刷新重新加载~