diff --git a/video-gen-app/src/api/index.ts b/video-gen-app/src/api/index.ts index 136943cb..172e333d 100644 --- a/video-gen-app/src/api/index.ts +++ b/video-gen-app/src/api/index.ts @@ -664,6 +664,7 @@ export async function getResourcesMaterialList(params: ResourcesMaterialListPara if (params.page_size !== undefined) query.set('page_size', String(params.page_size)); return api.get(`/resources-material/list?${query.toString()}`); } + // home 获取各模块媒体 export async function getmedit(limit:number): Promise { return api.get(`/recent-generations?limit=${limit}&modules=project&modules=chat_ai&modules=hot_opening_replicate&modules=shot_replicate`); @@ -708,3 +709,8 @@ export async function deleteOAuthAccount(params: DeleteOAuthAccountParams): Prom export async function getAllOAuthAccountList(): Promise { return api.post(`/upload-material/oauth_account_list`); } + +// 获取用户全部授权账户列表 resources_material_ids pre_test_template_id +export async function submitPreTest(params: { resources_material_ids: any[]; pre_test_template_id: string }): Promise { + return api.post(`/resources-material/pre-commit`, params); +} \ No newline at end of file diff --git a/video-gen-app/src/components/PreResultDisplay.tsx b/video-gen-app/src/components/PreResultDisplay.tsx index 435d337d..3249edb3 100644 --- a/video-gen-app/src/components/PreResultDisplay.tsx +++ b/video-gen-app/src/components/PreResultDisplay.tsx @@ -57,10 +57,12 @@ const PreResultDisplay: React.FC = ({ preResult }) => { const isYes = value === 'YES'; const isUnknown = value === 'UNKNOWN'; let displayLabel, displayColor; - if (isUnknown) { - displayLabel = config.unknownLabel; - displayColor = config.unknownColor; - } else if (isYes) { + // if (isUnknown) { + // displayLabel = config.unknownLabel; + // displayColor = config.unknownColor; + // } else + + if (isYes) { displayLabel = config.label; displayColor = config.yesColor; } else { diff --git a/video-gen-app/src/index.css b/video-gen-app/src/index.css index 24078d5d..a06ab3d1 100644 --- a/video-gen-app/src/index.css +++ b/video-gen-app/src/index.css @@ -885,4 +885,28 @@ body { .homepage-tabs .ant-tabs-ink-bar { background: linear-gradient(135deg, #6366f1, #8b5cf6); +} + +.media-download-btn { + position: absolute; + top: 8px; + right: 8px; + z-index: 10; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s; +} + +.media-card:hover .media-download-btn { + opacity: 1; + pointer-events: auto; +} + +.media-preview-tip { + opacity: 0; + transition: opacity 0.2s; +} + +.media-card:hover .media-preview-tip { + opacity: 1; } \ No newline at end of file diff --git a/video-gen-app/src/pages/ConsumePage.tsx b/video-gen-app/src/pages/ConsumePage.tsx index b7987ce3..c030cfc1 100644 --- a/video-gen-app/src/pages/ConsumePage.tsx +++ b/video-gen-app/src/pages/ConsumePage.tsx @@ -101,13 +101,6 @@ const ConsumePage: React.FC = () => { loadData(1, pageSize); }; - const handleReset = () => { - setAdvertiserId(''); - setConsumeDateRange(undefined); - setCurrentPage(1); - loadData(1, pageSize, null, ''); - }; - const handleSync = () => { Modal.confirm({ title: '拉取消耗数据', @@ -164,7 +157,9 @@ const ConsumePage: React.FC = () => { return (
-
diff --git a/video-gen-app/src/pages/GeneratedRecord.tsx b/video-gen-app/src/pages/GeneratedRecord.tsx index 884f7691..546a9c3a 100644 --- a/video-gen-app/src/pages/GeneratedRecord.tsx +++ b/video-gen-app/src/pages/GeneratedRecord.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useLayoutEffect, useRef, useCallback } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Button, Empty, Input, Select, Space, Typography, Tag, message, Modal, Table, DatePicker, Tabs, Transfer, Tooltip } from 'antd'; import dayjs from 'dayjs'; import JSZip from 'jszip'; @@ -9,18 +9,20 @@ import { FolderOpenOutlined, FileTextOutlined, DownloadOutlined, - XOutlined, ClockCircleOutlined, UploadOutlined, + ReloadOutlined, + StarOutlined, + PlayCircleOutlined, } from '@ant-design/icons'; -import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList } from '../api'; +import { gethistory, gethistoryItems, getOAuthList, asyncBatchUploadMaterial, updateFilename, getUploadHistory, getAllOAuthAccountList, getOpenTypeAll, getPreTestList, getDefaultPreTest } from '../api'; const { Search } = Input; const { Text } = Typography; const GeneratedRecord: React.FC = () => { - const [filterType, setFilterType] = useState<'project' | 'creation'>('project'); + const [filterType, setFilterType] = useState<'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate'>('project'); const [filterMedia, setFilterMedia] = useState<'video' | 'image'>('video'); const [recordlist, setRecordList] = useState([]); const [Pagebreak, setPagebreak] = useState({ @@ -72,7 +74,6 @@ const GeneratedRecord: React.FC = () => { const [preTestTemplate, setPreTestTemplate] = useState(''); const [preTestTemplates, setPreTestTemplates] = useState([]); const [preTestTemplatesLoading, setPreTestTemplatesLoading] = useState(false); - // 推送任务历史弹窗相关状态 const [uploadHistoryModalVisible, setUploadHistoryModalVisible] = useState(false); const [uploadHistoryList, setUploadHistoryList] = useState([]); @@ -81,93 +82,11 @@ const GeneratedRecord: React.FC = () => { 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); @@ -203,394 +122,14 @@ const GeneratedRecord: React.FC = () => { 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 [isHovered, setIsHovered] = 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 ( -
{ - setIsHovered(true); - 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) => { - setIsHovered(false); - 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'; - }} - > - 点击预览 -
- )} - - {/* 下载按钮 - hover时显示 */} - {!isSelectionMode && !isExpired && !isError && isLoaded && ( -
{ - e.stopPropagation(); - const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${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); - }} - > - -
{ - e.currentTarget.style.background = 'rgba(0,0,0,0.7)'; - }} - onMouseLeave={(e) => { - e.currentTarget.style.background = 'rgba(0,0,0,0.5)'; - }} - > - -
-
-
- )} - - {/* 选择模式下的选择框 */} - {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 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; + const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; + if (isImage) { + return `${cleanBase}/static/${cleanPath}&w=300&q=50`; + } + return `${cleanBase}/${cleanPath}`; }; // 下载文件 const handleDownload = (item: any) => { @@ -879,9 +418,10 @@ const GeneratedRecord: React.FC = () => { let sourceModel: string; if (item && hasGeneratedResourceId(item)) { sourceModel = 'generated_resources'; - } else { - sourceModel = filterType === 'project' ? 'generation_records' : 'chat_generation_tasks'; } + // else { + // sourceModel = filterType === 'project' ? 'generation_records' : 'chat_generation_tasks'; + // } if (!sourceModelMap.has(sourceModel)) { sourceModelMap.set(sourceModel, []); } @@ -1031,18 +571,22 @@ const GeneratedRecord: React.FC = () => { }; useEffect(() => { setLoading(true); - let parameters = ''; + let historySource = ''; 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}`; + historySource = 'generation_record'; + } else if (filterType === 'hot_opening_replicate') { + historySource = 'hot_opening_replicate'; + } else if (filterType === 'shot_replicate') { + historySource = 'shot_replicate'; + } + let parameters = `?gen_type=${filterMedia}&page=${Pagebreak.page}&page_size=${Pagebreak.pageSize}`; + if (historySource) { + parameters = `?gen_type=${filterMedia}&history_source=${historySource}&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}`; + let parameters = `${selectedDate}?gen_type=${filterMedia}&page=${Pagebreak.page}&page_size=${Pagebreak.pageSize}`; + if (historySource) { + parameters = `${selectedDate}?gen_type=${filterMedia}&history_source=${historySource}&page=${Pagebreak.page}&page_size=${Pagebreak.pageSize}`; } gethistoryItems(parameters).then((res: any) => { const data = Array.isArray(res) ? res : (res?.items || []); @@ -1100,11 +644,17 @@ const GeneratedRecord: React.FC = () => { if (loadingGroups.has(date)) return; setLoadingGroups(prev => new Set([...prev, date])); const addpage = page + 1; - let parameters = ``; + let historySource = ''; 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}`; + historySource = 'generation_record'; + } else if (filterType === 'hot_opening_replicate') { + historySource = 'hot_opening_replicate'; + } else if (filterType === 'shot_replicate') { + historySource = 'shot_replicate'; + } + let parameters = `${time}?gen_type=${filterMedia}&page=${addpage}&page_size=${Pagebreak.pageSize}`; + if (historySource) { + parameters = `${time}?gen_type=${filterMedia}&history_source=${historySource}&page=${addpage}&page_size=${Pagebreak.pageSize}`; } try { const res: any = await gethistoryItems(parameters); @@ -1195,6 +745,46 @@ const GeneratedRecord: React.FC = () => { > 创作记录 + +
@@ -1397,17 +987,171 @@ const GeneratedRecord: React.FC = () => { flexWrap: 'wrap', gap: 8, }}> - {group.items.map((item: any) => ( - isSelectionMode ? handleToggleSelect(getItemResourceId(item)) : handlePreview(item)} - isSelected={selectedItems.has(getItemResourceId(item))} - onToggleSelect={handleToggleSelect} - isSelectionMode={isSelectionMode} - /> - ))} + {group.items.map((item: any) => { + const displayUrl = filterMedia === 'video' && item.videoCoverUrl + ? buildUrl(item.videoCoverUrl, true) + : buildUrl(filterMedia === 'video' ? item.videoUrl : item.imageUrl, true); + const hasCover = filterMedia === 'video' && item.videoCoverUrl; + const showImage = hasCover || filterMedia === 'image'; + const isSelectedItem = selectedItems.has(getItemResourceId(item)); + + return ( +
isSelectionMode ? handleToggleSelect(getItemResourceId(item)) : handlePreview(item)} + onMouseEnter={(e) => { + 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)'; + } + }} + > + {showImage ? ( + 预览 + ) : ( +
+ + 暂无封面 +
+ )} + + {isSelectionMode && ( +
{ + e.stopPropagation(); + handleToggleSelect(getItemResourceId(item)); + }} + > + {isSelectedItem && ( + + + + )} +
+ )} + + {!isSelectionMode && ( + <> +
{ + (e.currentTarget as HTMLElement).style.opacity = '1'; + }} + onMouseLeave={(e) => { + (e.currentTarget as HTMLElement).style.opacity = '0'; + }} + > + 点击预览 +
+
+ +
{ + e.stopPropagation(); + const url = `${import.meta.env.VITE_API_BASE || "http://localhost:8000"}${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); + }} + > + +
+
+
+ + )} + + {isSelectionMode && isSelectedItem && ( +
+ )} +
+ ); + })}
{/* 分组内加载更多 */} {group.total && group.total > group.items.length && ( @@ -1452,520 +1196,6 @@ const GeneratedRecord: React.FC = () => { )}
)} - {/* 推送配置弹窗 */} - { - 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); - }} - onBlur={() => { - const newName = materialFileNames.get(itemId) || item?.fileName || ''; - if (newName.trim()) { - handleUpdateFileName(itemId, newName); - } - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - const newName = materialFileNames.get(itemId) || item?.fileName || ''; - if (newName.trim()) { - handleUpdateFileName(itemId, newName); - } - } - }} - placeholder="输入新名称" - style={{ width: 200, borderRadius: 4 }} - size="small" - /> -
- ); - }); - })()} -
-
- -
-
- 是否开启前测: - setPreTestTemplate(value)} - style={{ width: 200 }} - disabled={isPreTest !== '1'} - loading={preTestTemplatesLoading} - placeholder={isPreTest !== '1' ? '请先开启前测' : '选择前测模板'} - options={preTestTemplates.map((item) => ({ - value: item.id, - label: item.name, - }))} - /> -
-
- { - setAccountTab(key as 'new' | 'history'); - if (key === 'history') { - loadHistoryOAuthList(); - } - }} - items={[ - { - key: 'new', - label: '新增账户', - }, - { - key: 'history', - label: '历史账户', - }, - ]} - /> - {accountTab === 'new' && ( -
- {selectedOauthItems.map((oauthItem, index) => ( -
-
- - 授权账户 {index + 1} - - {selectedOauthItems.length > 1 && ( - - )} -
- setUploadHistoryStatus(value)} @@ -1990,13 +1220,22 @@ const GeneratedRecord: React.FC = () => { ]} allowClear /> - +
+ + +
{ )} + + {/* 推送配置弹窗 */} + { + 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); + }} + onBlur={() => { + const newName = materialFileNames.get(itemId) || item?.fileName || ''; + if (newName.trim()) { + handleUpdateFileName(itemId, newName); + } + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + const newName = materialFileNames.get(itemId) || item?.fileName || ''; + if (newName.trim()) { + handleUpdateFileName(itemId, newName); + } + } + }} + placeholder="输入新名称" + style={{ width: 200, borderRadius: 4 }} + size="small" + /> +
+ ); + }); + })()} +
+
+ +
+
+ 是否开启前测: + setPreTestTemplate(value)} + style={{ width: 200 }} + disabled={isPreTest !== '1'} + loading={preTestTemplatesLoading} + placeholder={isPreTest !== '1' ? '请先开启前测' : '选择前测模板'} + options={preTestTemplates.map((item) => ({ + value: item.id, + label: item.name, + }))} + /> +
+
+ { + setAccountTab(key as 'new' | 'history'); + if (key === 'history') { + loadHistoryOAuthList(); + } + }} + items={[ + { + key: 'new', + label: '新增账户', + }, + { + key: 'history', + label: '历史账户', + }, + ]} + /> + {accountTab === 'new' && ( +
+ {selectedOauthItems.map((oauthItem, index) => ( +
+
+ + 授权账户 {index + 1} + + {selectedOauthItems.length > 1 && ( + + )} +
+
{ + 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 || '-'}, + }, + { + title: '授权用户ID', + dataIndex: 'accountUserid', + key: 'accountUserid', + width: 120, + }, + ]} + 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" + scroll={{ x: 'max-content' }} + onRow={(record) => ({ + onClick: () => { + const id = String(record.id); + const newOauthItems = [...selectedOauthItems]; + newOauthItems[index] = { value: id, label: String(record.accountId)+'-'+(record.accountName || '-') }; + setSelectedOauthItems(newOauthItems); + const newOauthSelectOpens = [...oauthSelectOpens]; + newOauthSelectOpens[index] = false; + setOauthSelectOpens(newOauthSelectOpens); + }, + style: { + cursor: 'pointer', + backgroundColor: oauthItem?.value === String(record.id) ? '#e6f7ff' : undefined, + }, + })} + /> + + )} + open={oauthSelectOpens[index]} + onOpenChange={(open) => { + const newOauthSelectOpens = [...oauthSelectOpens]; + newOauthSelectOpens[index] = open; + setOauthSelectOpens(newOauthSelectOpens); + if (open) { + loadOAuthList(1, oauthPageSize); + } + }} + labelInValue + fieldNames={{ label: 'accountUserid', value: 'id' }} + /> + + 粘贴账户ID(每行一个或用逗号分隔) + + { + const value = e.target.value; + const newAccountIdInputs = [...accountIdInputs]; + newAccountIdInputs[index] = value; + setAccountIdInputs(newAccountIdInputs); + 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 })); + const seen = new Set(); + const finalAccounts = textAccounts.filter(a => { + if (seen.has(a.accountId)) return false; + seen.add(a.accountId); + return true; + }); + const newAccountIdLists = [...accountIdLists]; + newAccountIdLists[index] = finalAccounts; + setAccountIdLists(newAccountIdLists); + }} + placeholder="粘贴账户ID,每行一个或用逗号分隔,例如: + 10001,10002,10003 + 10004" + rows={3} + style={{ borderRadius: 8 }} + /> + + ))} + + + )} + {accountTab === 'history' && ( +
+ {historyOAuthLoading ? ( +
加载中...
+ ) : historyOAuthList.length === 0 ? ( + + ) : ( + item.advertiserId) + .map((item, index) => ({ + key: String(item.advertiserId) || `item-${index}`, + title: String(item.advertiserId), + description: item.advertiserName || '', + ...item, + }))} + targetKeys={selectedHistoryAccounts + .filter(item => item.advertiserId) + .map(item => String(item.advertiserId))} + onChange={(targetKeys) => { + const newSelected = historyOAuthList.filter(item => + item.advertiserId && targetKeys.includes(String(item.advertiserId)) + ); + setSelectedHistoryAccounts(newSelected); + }} + titles={['可选账户列表', '已选账户列表']} + showSearch + filterOption={(inputValue, item) => + String(item.advertiserId).toLowerCase().includes(inputValue.toLowerCase()) || + (item.advertiserName && item.advertiserName.toLowerCase().includes(inputValue.toLowerCase())) + } + > + {({ + direction, + filteredItems, + onItemSelect, + onItemSelectAll, + selectedKeys: listSelectedKeys, + disabled: listDisabled, + }) => { + const columns = [ + { + title: '账户ID', + dataIndex: 'advertiserId', + key: 'advertiserId', + width: 120, + }, + { + title: '账户名称', + dataIndex: 'advertiserName', + key: 'advertiserName', + width: 150, + }, + ]; + const rowSelection = { + getCheckboxProps: () => ({ disabled: listDisabled }), + onChange: (selectedRowKeys: string[]) => { + onItemSelectAll(selectedRowKeys, 'replace'); + }, + selectedRowKeys: listSelectedKeys, + selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE], + }; + return ( +
({ + onClick: () => { + if (itemDisabled || listDisabled) { + return; + } + onItemSelect(key, !listSelectedKeys.includes(key)); + }, + })} + /> + ); + }} + + )} + + )} + {/* 操作按钮 */} +
+ + +
+ + ); }; diff --git a/video-gen-app/src/pages/MaterialListPage.tsx b/video-gen-app/src/pages/MaterialListPage.tsx index 7aca0da1..52eef543 100644 --- a/video-gen-app/src/pages/MaterialListPage.tsx +++ b/video-gen-app/src/pages/MaterialListPage.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Button, Table, Tag, Input, Pagination, Typography, Select, App, Modal } from 'antd'; import { useNavigate } from 'react-router-dom'; import { FolderOpenOutlined, EyeOutlined } from '@ant-design/icons'; -import { getResourcesMaterialList, getPreTestList } from '../api'; +import { getResourcesMaterialList, getPreTestList, submitPreTest, getDefaultPreTest } from '../api'; import PreResultDisplay from '../components/PreResultDisplay'; // 格式化时间 2026-06-12T03:47:28.542988Z -> 2026-06-12 03:47:28 @@ -90,6 +90,9 @@ const MaterialListPage: React.FC = () => { const [total, setTotal] = useState(0); const [selectedRows, setSelectedRows] = useState>(new Set()); const [pushModalOpen, setPushModalOpen] = useState(false); + const [previewModalOpen, setPreviewModalOpen] = useState(false); + const [previewUrl, setPreviewUrl] = useState(''); + const [previewType, setPreviewType] = useState<'image' | 'video'>('image'); const [pushPreTestTemplate, setPushPreTestTemplate] = useState(''); const [pushTemplates, setPushTemplates] = useState([]); const [pushTemplatesLoading, setPushTemplatesLoading] = useState(false); @@ -181,7 +184,11 @@ const MaterialListPage: React.FC = () => { - {/*
+
-
*/} +
{ rowKey="id" bordered={false} scroll={{ x: 'max-content' }} - // rowSelection={{ - // type: 'checkbox', - // selectedRowKeys: Array.from(selectedRows), - // onChange: (keys) => { - // setSelectedRows(new Set(keys as string[])); - // }, - // }} + rowSelection={{ + type: 'checkbox', + selectedRowKeys: Array.from(selectedRows), + onChange: (keys) => { + setSelectedRows(new Set(keys as string[])); + }, + }} />
{
+ + setPreviewModalOpen(false)} + footer={null} + width={600} + style={{ borderRadius: 16 }} + > +
+ {previewType === 'video' ? ( +
+
); }; diff --git a/video-gen-app/src/pages/PreTest.tsx b/video-gen-app/src/pages/PreTest.tsx index 5b7ef746..8dd6f06d 100644 --- a/video-gen-app/src/pages/PreTest.tsx +++ b/video-gen-app/src/pages/PreTest.tsx @@ -163,7 +163,6 @@ const PreTest: React.FC = () => { const loadRecords = async (params?: { page?: number; pageSize?: number; platform?: string }) => { setLoading(true); try { - console.log(params); const page = params?.page ?? currentPage; const size = params?.pageSize ?? pageSize; const platform = params?.platform ?? searchPlatform;