diff --git a/video-gen-app/src/components/CreationRecordPicker.tsx b/video-gen-app/src/components/CreationRecordPicker.tsx new file mode 100644 index 00000000..3df376b1 --- /dev/null +++ b/video-gen-app/src/components/CreationRecordPicker.tsx @@ -0,0 +1,573 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { Modal, Tabs, Button, Input, Spin, Empty, message } from 'antd'; +import { + FolderOpenOutlined, + FileTextOutlined, + StarOutlined, + PlayCircleOutlined, + UploadOutlined, + VideoCameraOutlined, + PictureOutlined, + DownloadOutlined, + SearchOutlined, +} from '@ant-design/icons'; +import { useNavigate } from 'react-router-dom'; +import { gethistory, gethistoryItems } from '../api'; +import UploadResourceHistoryPanel from './uploadResource/UploadResourceHistoryPanel'; +import { buildImagePreviewUrl } from '../utils/previewUrl'; + +type FilterType = 'project' | 'creation' | 'hot_opening_replicate' | 'shot_replicate' | 'upload_resource'; + +interface CreationRecordPickerProps { + open: boolean; + onClose: () => void; + mediaType?: 'video' | 'image'; + onSelect?: (item: any) => void; +} + +const TAB_ITEMS = [ + { key: 'project', label: 项目记录 }, + { key: 'creation', label: 创作记录 }, + { key: 'hot_opening_replicate', label: 爆款复刻 }, + { key: 'shot_replicate', label: 拆镜复刻 }, + { key: 'upload_resource', label: 历史素材 }, +]; + +const CreationRecordPicker: React.FC = ({ open, onClose, mediaType = 'video', onSelect }) => { + const navigate = useNavigate(); + const [filterType, setFilterType] = useState('project'); + const [filterMedia, setFilterMedia] = useState<'video' | 'image'>(mediaType); + const [recordList, setRecordList] = useState([]); + const [loading, setLoading] = useState(false); + const [loadingGroups, setLoadingGroups] = useState>(new Set()); + const [page, setPage] = useState(1); + const [pageSize] = useState(10); + const [totalDays, setTotalDays] = useState(0); + const [searchKeyword, setSearchKeyword] = useState(''); + const [selectedDate, setSelectedDate] = useState(''); + const [previewItem, setPreviewItem] = useState(null); + const [previewVisible, setPreviewVisible] = useState(false); + const [mediaLoadStatus, setMediaLoadStatus] = useState>(new Map()); + + const buildUrl = useCallback((path: string, isImage: boolean = false): string => { + const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000'; + const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl; + if (isImage) { + return `${cleanBase}${buildImagePreviewUrl(path)}`; + } + return `${cleanBase}${path}`; + }, []); + + const getItemResourceId = (item: any): string => { + return item.generatedResourceId || item.id; + }; + + const isMediaExpired = (url: string): boolean => { + try { + const urlObj = new URL(url, window.location.origin); + const exp = urlObj.searchParams.get('exp'); + if (!exp) return false; + return Math.floor(Date.now() / 1000) > parseInt(exp); + } catch { + return false; + } + }; + + const loadRecordList = useCallback(() => { + if (filterType === 'upload_resource') { + setLoading(false); + setRecordList([]); + return; + } + setLoading(true); + let historySource = ''; + if (filterType === 'project') 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=${page}&page_size=${pageSize}`; + if (historySource) { + parameters = `?gen_type=${filterMedia}&history_source=${historySource}&page=${page}&page_size=${pageSize}`; + } + if (searchKeyword.trim()) { + parameters += `&keyword=${encodeURIComponent(searchKeyword.trim())}`; + } + + if (selectedDate) { + let dateParams = `${selectedDate}?gen_type=${filterMedia}&page=${page}&page_size=${pageSize}`; + if (historySource) { + dateParams = `${selectedDate}?gen_type=${filterMedia}&history_source=${historySource}&page=${page}&page_size=${pageSize}`; + } + if (searchKeyword.trim()) { + dateParams += `&keyword=${encodeURIComponent(searchKeyword.trim())}`; + } + gethistoryItems(dateParams).then((res: any) => { + console.log('[CreationRecordPicker] gethistoryItems response:', res); + const data = Array.isArray(res) ? res : (res?.items || []); + const list = [{ + generatedDate: res?.generated_date || res?.generatedDate || '', + items: data, + total: res?.total ?? data.length, + page: res?.page ?? 1, + }]; + setRecordList(list && list[0].items.length > 0 ? list : []); + setTotalDays(res?.totalDays || res?.total_days || 0); + }).catch((err) => { + console.error('[CreationRecordPicker] gethistoryItems error:', err); + setRecordList([]); + }).finally(() => setLoading(false)); + } else { + const apiCall = historySource || filterType === 'creation' + ? gethistory(parameters) + : gethistory(parameters); + + apiCall.then((res: any) => { + console.log('[CreationRecordPicker] gethistory response:', res); + let data: any[] = []; + if (Array.isArray(res)) { + data = res; + } else if (res?.groups) { + // groups 可能是数组也可能是单个对象 + const groups = Array.isArray(res.groups) ? res.groups : [res.groups]; + data = groups.map((g: any) => ({ + generatedDate: g.generated_date || g.generatedDate || '', + items: g.items || [], + total: g.total ?? 0, + page: g.page ?? 1, + })); + } else if (res?.items) { + // gethistoryItems 格式 + data = [{ + generatedDate: res.generated_date || res.generatedDate || '', + items: res.items, + total: res.total ?? res.items.length, + page: res.page ?? 1, + }]; + } else if (res && typeof res === 'object') { + // 可能是其他格式,尝试包装 + data = [{ + generatedDate: res.generated_date || res.generatedDate || '', + items: [res], + total: res.total ?? 1, + page: 1, + }]; + } + data.forEach((group: any) => { + if (group.page === undefined) group.page = 1; + if (group.total === undefined) group.total = group.items?.length || 0; + }); + setRecordList(page === 1 ? data : (prev: any[]) => [...prev, ...data]); + // 保存 totalDays 用于底部加载更多判断 + setTotalDays(res?.totalDays || res?.total_days || 0); + }).catch((err) => { + console.error('[CreationRecordPicker] gethistory error:', err); + if (page === 1) setRecordList([]); + }).finally(() => setLoading(false)); + } + }, [filterType, filterMedia, page, pageSize, searchKeyword, selectedDate]); + + useEffect(() => { + + + + if (open) { + setRecordList([]); + setMediaLoadStatus(new Map()); + loadRecordList(); + } + }, [open, filterType, filterMedia, page, selectedDate]); + + useEffect(() => { + if (open) { + setFilterMedia(mediaType); + setPage(1); + setSearchKeyword(''); + setSelectedDate(''); + setRecordList([]); + setMediaLoadStatus(new Map()); + } + }, [open, mediaType]); + + const handleItemClick = (item: any) => { + if (onSelect) { + onSelect(item); + onClose(); + } else { + setPreviewItem(item); + setPreviewVisible(true); + } + }; + + const handleClosePreview = () => { + setPreviewVisible(false); + setPreviewItem(null); + }; + + const handleGoEdit = () => { + if (!previewItem) return; + const mediaUrl = previewItem.videoUrl || previewItem.imageUrl || ''; + if (!mediaUrl) { + message.warning('无法获取媒体地址'); + return; + } + if (isMediaExpired(mediaUrl)) { + message.warning('资源已过期,无法编辑'); + return; + } + const type = previewItem.videoUrl ? 'video' : 'image'; + const baseUrl = import.meta.env.VITE_API_BASE || 'http://localhost:8000'; + const fullUrl = mediaUrl.startsWith('http') ? mediaUrl : `${baseUrl}${mediaUrl}`; + const name = previewItem.title || previewItem.id || '导入素材'; + handleClosePreview(); + onClose(); + navigate(`/videoediting?mediaUrl=${encodeURIComponent(fullUrl)}&mediaType=${type}&mediaName=${encodeURIComponent(name)}`); + }; + + const handleSearch = () => { + setPage(1); + loadRecordList(); + }; + + const handleGroupLoadMore = async (time: string, currentPage: number) => { + if (loadingGroups.has(time)) return; + setLoadingGroups(prev => new Set([...prev, time])); + const addpage = currentPage + 1; + let historySource = ''; + if (filterType === 'project') 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=${pageSize}`; + if (historySource) { + parameters = `${time}?gen_type=${filterMedia}&history_source=${historySource}&page=${addpage}&page_size=${pageSize}`; + } + if (searchKeyword.trim()) { + parameters += `&keyword=${encodeURIComponent(searchKeyword.trim())}`; + } + + try { + const res: any = await gethistoryItems(parameters); + const newItems: any[] = res?.items || []; + if (newItems.length > 0) { + setRecordList(prev => prev.map(group => { + if (group.generatedDate === time) { + return { + ...group, + items: [...group.items, ...newItems], + page: addpage, + total: res?.total ?? group.total, + }; + } + return group; + })); + } + } catch (err) { + console.error('[CreationRecordPicker] handleGroupLoadMore error:', err); + } finally { + setLoadingGroups(prev => { + const next = new Set(prev); + next.delete(time); + return next; + }); + } + }; + + const renderItemCard = (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 resourceId = getItemResourceId(item); + + return ( +
handleItemClick(item)} + onMouseEnter={(e) => { + e.currentTarget.style.transform = 'scale(1.05)'; + e.currentTarget.style.boxShadow = '0 4px 16px rgba(0,0,0,0.2)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.transform = 'scale(1)'; + e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)'; + }} + > + {showImage ? ( + 预览 { + if (imgEl && imgEl.complete && mediaLoadStatus.get(resourceId) !== 'loaded') { + setMediaLoadStatus(prev => { + if (prev.get(resourceId) === 'loaded') return prev; + const newMap = new Map(prev); + newMap.set(resourceId, 'loaded'); + return newMap; + }); + } + }} + style={{ + width: '100%', + height: '100%', + objectFit: 'cover', + opacity: mediaLoadStatus.get(resourceId) === 'loaded' ? 1 : 0, + transition: 'opacity 0.3s', + }} + onLoad={() => { + setMediaLoadStatus(prev => { + if (prev.get(resourceId) === 'loaded') return prev; + const newMap = new Map(prev); + newMap.set(resourceId, 'loaded'); + return newMap; + }); + }} + onError={() => { + setMediaLoadStatus(prev => { + if (prev.get(resourceId) === 'loaded') return prev; + const newMap = new Map(prev); + newMap.set(resourceId, 'loaded'); + return newMap; + }); + }} + /> + ) : ( +
+ ); + }; + + return ( + <> + + + 选择创作记录 + + } + open={open} + onCancel={onClose} + width={900} + footer={null} + destroyOnHidden + styles={{ body: { maxHeight: '70vh', overflow: 'auto' } }} + > + {/* 视频/图片切换 */} +
+ + +
+ + {/* Tab 切换 */} + { + setFilterType(key as FilterType); + setPage(1); + setSelectedDate(''); + }} + items={TAB_ITEMS} + size="small" + style={{ marginBottom: 12 }} + /> + + {filterType === 'upload_resource' ? ( + + ) : ( + <> + {/* 搜索栏 */} +
+ setSearchKeyword(e.target.value)} + onPressEnter={handleSearch} + prefix={} + style={{ flex: 1 }} + size="small" + /> + +
+ + {/* 列表 */} + + {recordList.length === 0 && !loading ? ( + + ) : ( +
+ {recordList.map((group: any, gIdx: number) => ( +
+ {group.generatedDate && ( +
+ {group.generatedDate} +
+ )} +
+ {group.items?.map((item: any) => renderItemCard(item))} +
+ {/* 分组内加载更多 */} + {group.total != null && group.total > (group.items?.length || 0) && ( +
+ +
+ )} +
+ ))} + {/* 底部整体加载更多 */} + {recordList.length > 0 && totalDays > recordList.length && ( +
+ +
+ )} +
+ )} +
+ + )} +
+ + {/* 预览弹窗 */} + + + {previewItem?.title || '预览'} + + + } + open={previewVisible} + onCancel={handleClosePreview} + width={800} + footer={ +
+ + + +
+ } + styles={{ body: { height: 400, display: 'flex', flexDirection: 'column', padding: 0 } }} + > + {previewItem && ( +
+ {previewItem.videoUrl ? ( +
+ )} +
+ + ); +}; + +export default CreationRecordPicker;