import { useRef, useState } from 'react'; import { useInputBarStore } from '../store/inputBar'; import { showToast } from './Toast'; import { ImageLightbox } from './ImageLightbox'; import { tosThumb } from '../lib/api'; import styles from './UniversalUpload.module.css'; const Spinner = () => ( ); const ErrorIcon = () => ( ! ); const MAX_IMAGE_SIZE = 30 * 1024 * 1024; // 30MB per API doc const MAX_VIDEO_SIZE = 50 * 1024 * 1024; // 50MB per API doc const MAX_AUDIO_SIZE = 15 * 1024 * 1024; // 15MB per API doc const THUMB_H = 80; // matches --thumbnail-size const THUMB_W = THUMB_H * 3 / 4; // 60px (aspect-ratio 3:4) const PEEK = 12; // visible width per stacked card beyond the first const AudioIcon = () => ( ); export function UniversalUpload() { const references = useInputBarStore((s) => s.references); const addReferences = useInputBarStore((s) => s.addReferences); const removeReference = useInputBarStore((s) => s.removeReference); const retryUpload = useInputBarStore((s) => s.retryUpload); const fileInputRef = useRef(null); const [expanded, setExpanded] = useState(false); const [badgeHover, setBadgeHover] = useState(false); const [lightboxSrc, setLightboxSrc] = useState(null); const handleTrigger = () => { fileInputRef.current?.click(); }; const handleFileChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); if (!files.length) return; const valid: File[] = []; for (const f of files) { // Format validation if (f.type.startsWith('video/') && f.type !== 'video/mp4' && f.type !== 'video/quicktime') { showToast('仅支持 MP4 和 MOV 格式的视频'); continue; } if (f.type.startsWith('audio/') && f.type !== 'audio/mpeg' && f.type !== 'audio/wav') { showToast('仅支持 MP3 和 WAV 格式的音频'); continue; } // Size validation let limit: number; let limitLabel: string; if (f.type.startsWith('video/')) { limit = MAX_VIDEO_SIZE; limitLabel = '视频文件不能超过50MB'; } else if (f.type.startsWith('audio/')) { limit = MAX_AUDIO_SIZE; limitLabel = '音频文件不能超过15MB'; } else { limit = MAX_IMAGE_SIZE; limitLabel = '图片文件不能超过30MB'; } if (f.size > limit) { showToast(limitLabel); } else { valid.push(f); } } if (!valid.length) { e.target.value = ''; return; } addReferences(valid); e.target.value = ''; }; const hasFiles = references.length > 0; const count = references.length; // Check if all type slots are full const counts = { image: 0, video: 0, audio: 0 }; for (const ref of references) counts[ref.type]++; const allFull = counts.image >= 9 && counts.video >= 3 && counts.audio >= 3; // Collapsed stack visual width const stackWidth = THUMB_W + Math.max(0, count - 1) * PEEK; return (
{/* Empty state */} {!hasFiles && (
参考内容
)} {/* Thumbnails — thumbRow always absolute, hover to expand */} {hasFiles && ( <>
setExpanded(true)} onMouseLeave={() => setExpanded(false)} > {references.map((ref, i) => (
{ref.type === 'video' ? (
{ref.label}
))} {/* Add more button (expanded state only) */} {expanded && !allFull && (
{ e.stopPropagation(); handleTrigger(); }} >
上传参考内容
)}
{/* "+" badge — outside thumbRow, position based on stack width */} {!expanded && !allFull && (
{ e.stopPropagation(); handleTrigger(); }} onMouseEnter={() => setBadgeHover(true)} onMouseLeave={() => setBadgeHover(false)} > + {badgeHover && (
上传参考内容
)}
)} )} setLightboxSrc(null)} />
); }