import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { AudioOutlined, CheckOutlined, PictureOutlined, ReloadOutlined, SearchOutlined, VideoCameraOutlined } from '@ant-design/icons'; import { Button, Empty, Input, message, Modal, Pagination, Select, Space, Spin, Tag, Typography } from 'antd'; import { getUploadResourceHistoryItems, getUploadResourceHistory } from '../../api'; import type { UploadResourceHistoryItem } from '../../types'; import CustomAudioPlayer from './CustomAudioPlayer'; const { Text } = Typography; type MediaTypeFilter = '' | 'image' | 'video' | 'audio'; interface UploadResourceHistoryPickerProps { open: boolean; onClose: () => void; onSelect: (items: UploadResourceHistoryItem[]) => void; allowedTypes?: Array<'image' | 'video' | 'audio'>; selectedIds?: string[]; title?: string; maxImageCount?: number; maxVideoCount?: number; usedImageCount?: number; usedVideoCount?: number; maxVideoDuration?: number; usedVideoDuration?: number; maxAudioCount?: number; usedAudioCount?: number; maxAudioDuration?: number; usedAudioDuration?: number; } const buildPreviewUrl = (url: string) => { if (!url) return ''; if (/^(https?:|data:|blob:)/i.test(url)) return url; const base = (import.meta.env.VITE_API_BASE || '').replace(/\/$/, ''); return `${base}${url.startsWith('/') ? '' : '/'}${url}`; }; const typeIcon = (type: string) => { if (type === 'video') return ; if (type === 'audio') return ; return ; }; const typeLabel = (type: string) => { if (type === 'video') return '视频'; if (type === 'audio') return '音频'; return '图片'; }; const UploadResourceHistoryPicker: React.FC = ({ open, onClose, onSelect, allowedTypes, selectedIds = [], title = '选择历史上传素材', maxImageCount, maxVideoCount, usedImageCount, usedVideoCount, maxVideoDuration, usedVideoDuration, maxAudioCount, usedAudioCount, maxAudioDuration, usedAudioDuration, }) => { const [resourceType, setResourceType] = useState(''); const [keyword, setKeyword] = useState(''); const [groupPage, setGroupPage] = useState(1); const [groupPageSize, setGroupPageSize] = useState(10); const [itemPage, setItemPage] = useState(1); const [itemPageSize, setItemPageSize] = useState(10); const [loading, setLoading] = useState(false); const [loadingGroups, setLoadingGroups] = useState(false); const [groups, setGroups] = useState([]); const [totalDays, setTotalDays] = useState(0); const [selectedDate, setSelectedDate] = useState(''); const [items, setItems] = useState([]); const [total, setTotal] = useState(0); const [checkedMap, setCheckedMap] = useState>(new Map()); const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]); const effectiveAllowedTypes = useMemo(() => new Set(allowedTypes || ['image', 'video', 'audio']), [JSON.stringify(allowedTypes)]); const selectedImages = useMemo(() => Array.from(checkedMap.values()).filter(item => item.resourceType === 'image').length, [checkedMap]); const selectedVideos = useMemo(() => Array.from(checkedMap.values()).filter(item => item.resourceType === 'video').length, [checkedMap]); const selectedVideoDuration = useMemo(() => Array.from(checkedMap.values()) .filter(item => item.resourceType === 'video') .reduce((sum, item) => sum + (item.durationSeconds || 0), 0), [checkedMap]); const selectedAudios = useMemo(() => Array.from(checkedMap.values()).filter(item => item.resourceType === 'audio').length, [checkedMap]); const selectedAudioDuration = useMemo(() => Array.from(checkedMap.values()) .filter(item => item.resourceType === 'audio') .reduce((sum, item) => sum + (item.durationSeconds || 0), 0), [checkedMap]); const maxAvailableImages = useMemo(() => maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : Infinity, [maxImageCount, usedImageCount]); const maxAvailableVideos = useMemo(() => maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : Infinity, [maxVideoCount, usedVideoCount]); const maxAvailableDuration = useMemo(() => maxVideoDuration !== undefined && usedVideoDuration !== undefined ? maxVideoDuration - usedVideoDuration : Infinity, [maxVideoDuration, usedVideoDuration]); const maxAvailableAudios = useMemo(() => maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : Infinity, [maxAudioCount, usedAudioCount]); const maxAvailableAudioDuration = useMemo(() => maxAudioDuration !== undefined && usedAudioDuration !== undefined ? maxAudioDuration - usedAudioDuration : Infinity, [maxAudioDuration, usedAudioDuration]); const imageExceeded = useMemo(() => selectedImages > maxAvailableImages, [selectedImages, maxAvailableImages]); const videoExceeded = useMemo(() => selectedVideos > maxAvailableVideos, [selectedVideos, maxAvailableVideos]); const durationExceeded = useMemo(() => selectedVideoDuration > maxAvailableDuration, [selectedVideoDuration, maxAvailableDuration]); const audioExceeded = useMemo(() => selectedAudios > maxAvailableAudios, [selectedAudios, maxAvailableAudios]); const audioDurationExceeded = useMemo(() => selectedAudioDuration > maxAvailableAudioDuration, [selectedAudioDuration, maxAvailableAudioDuration]); const loadGroups = useCallback(async () => { if (!open) return; setLoadingGroups(true); try { const res = await getUploadResourceHistory({ resourceType: resourceType || undefined, keyword: keyword || undefined, page: groupPage, pageSize: groupPageSize, scene: 'picker', }); if (groupPage === 1) { setGroups(res.groups || []); } else { setGroups((prev) => [...prev, ...(res.groups || [])]); } setTotalDays(res.totalDays || 0); } catch (error: any) { message.error(error?.message || '加载历史上传素材失败'); if (groupPage === 1) { setGroups([]); } setTotalDays(0); } finally { setLoadingGroups(false); } }, [effectiveAllowedTypes, keyword, open, groupPage, groupPageSize, resourceType]); const loadItems = useCallback(async (date: string) => { setLoading(true); try { const res = await getUploadResourceHistoryItems(date, { resourceType: resourceType || undefined, keyword: keyword || undefined, page: itemPage, pageSize: 8, scene: 'picker', }); const filtered = (res.items || []).filter((item) => effectiveAllowedTypes.has(item.resourceType)); setItems(filtered); setTotal(res.total || 0); } catch (error: any) { message.error(error?.message || '加载历史上传素材失败'); setItems([]); setTotal(0); } finally { setLoading(false); } }, [effectiveAllowedTypes, keyword, itemPage, itemPageSize, resourceType]); useEffect(() => { if (open) loadGroups(); }, [open, groupPage, loadGroups]); useEffect(() => { if (selectedDate) { setItemPage(1); } }, [selectedDate]); useEffect(() => { if (selectedDate) { loadItems(selectedDate); } }, [selectedDate, itemPage, loadItems]); useEffect(() => { if (open) setCheckedMap(new Map()); }, [open]); const toggle = (item: UploadResourceHistoryItem) => { if (selectedIdSet.has(item.id)) { message.warning('该素材已经在参考内容中'); return; } setCheckedMap((prev) => { const next = new Map(prev); if (next.has(item.id)) next.delete(item.id); else next.set(item.id, item); return next; }); }; const confirm = () => { const selected = Array.from(checkedMap.values()); if (selected.length === 0) { message.warning('请先选择历史素材'); return; } onSelect(selected); setCheckedMap(new Map()); onClose(); }; const typeOptions = useMemo(() => { const all = [ { label: '全部', value: '' }, { label: '图片', value: 'image' }, { label: '视频', value: 'video' }, { label: '音频', value: 'audio' }, ]; return all.filter((opt) => !opt.value || effectiveAllowedTypes.has(opt.value as any)); }, [effectiveAllowedTypes]); return ( 取消, , ]} >
} placeholder="搜索文件名" style={{ width: 220 }} onChange={(e) => setKeyword(e.target.value)} onPressEnter={() => { setGroupPage(1); loadGroups(); }} />
还可选取图片 {selectedImages}/{maxImageCount !== undefined && usedImageCount !== undefined ? maxImageCount - usedImageCount : '-'} 张 视频还可选取 {selectedVideos}/{maxVideoCount !== undefined && usedVideoCount !== undefined ? maxVideoCount - usedVideoCount : '-'} 个({selectedVideoDuration.toFixed(1)}/{maxVideoDuration !== undefined && usedVideoDuration !== undefined ? (maxVideoDuration - usedVideoDuration).toFixed(1) : '-'}秒) {maxAudioCount !== undefined && ( 音频还可选取 {selectedAudios}/{maxAudioCount !== undefined && usedAudioCount !== undefined ? maxAudioCount - usedAudioCount : '-'} 个({selectedAudioDuration.toFixed(1)}/{maxAudioDuration !== undefined && usedAudioDuration !== undefined ? (maxAudioDuration - usedAudioDuration).toFixed(1) : '-'}秒) )}
日期列表 {groups.length === 0 ? ( ) : (
{groups.map((group) => (
setSelectedDate(group.generatedDate)} style={{ padding: '10px 12px', borderRadius: 10, marginBottom: 6, cursor: 'pointer', border: selectedDate === group.generatedDate ? '1px solid #8b5cf6' : '1px solid transparent', background: selectedDate === group.generatedDate ? '#f5f3ff' : '#fff', }} >
{group.generatedDate}
共 {group.total} 个素材
))} {totalDays > groups.length && (
)}
)}
{!selectedDate ? ( ) : items.length === 0 ? ( ) : ( <>
{items.map((item) => { const checked = checkedMap.has(item.id); const disabled = selectedIdSet.has(item.id); const preview = buildPreviewUrl(item.previewUrl || item.displayUrl || item.resourceUrl); return (
!disabled && toggle(item)} style={{ position: 'relative', borderRadius: 14, border: checked ? '2px solid #8b5cf6' : '1px solid #e2e8f0', background: disabled ? '#f8fafc' : '#fff', opacity: disabled ? 0.55 : 1, cursor: disabled ? 'not-allowed' : 'pointer', overflow: 'hidden', boxShadow: checked ? '0 10px 24px rgba(139, 92, 246, 0.18)' : '0 6px 18px rgba(15,23,42,0.06)', }} >
{item.resourceType === 'image' ? ( {item.fileName ) : item.resourceType === 'video' ? (
{typeLabel(item.resourceType)} {item.durationSeconds ? {Number(item.durationSeconds).toFixed(1)}s : null} {item.fileName || item.id} {item.moduleLabel || '普通上传'}
{checked && (
)} {disabled &&
已选
}
); })}
{ setItemPage(nextPage); setItemPageSize(nextSize); }} />
)}
); }; export default UploadResourceHistoryPicker;