import { useEffect, useRef, useState } from 'react'; import { Button, Image, Space, Typography, Upload, message } from 'antd'; import { UploadOutlined, DeleteOutlined, FilePdfOutlined } from '@ant-design/icons'; import type { UploadProps } from 'antd'; import { uploadFileToOss, type OssMediaType } from '../lib/upload'; type Props = { value?: string[]; onChange?: (urls: string[]) => void; bizType: string; mediaType?: OssMediaType; /** 最多张数;不传则不限制 */ maxCount?: number; tip?: string; accept?: string; /** 上传按钮文案,默认「批量上传图片」 */ buttonText?: string; }; function normalizeUrls(value?: string[]) { return (value ?? []).map((u) => String(u || '').trim()).filter(Boolean); } function isPdf(url: string) { return /\.pdf(\?|$)/i.test(url); } /** * 多图批量上传(一次可选多张),用于套餐图 / 环境照 / 商品详情图等。 * Form.Item 直接绑定 string[]。 */ export default function MultiImageUpload({ value, onChange, bizType, mediaType = 'IMAGE', maxCount, tip, accept = 'image/*', buttonText = '批量上传图片', }: Props) { const urls = normalizeUrls(value); const urlsRef = useRef(urls); const onChangeRef = useRef(onChange); const [uploading, setUploading] = useState(false); const batchBuf = useRef([]); const batchTimer = useRef | null>(null); const uploadChain = useRef(Promise.resolve()); useEffect(() => { urlsRef.current = urls; }, [urls]); useEffect(() => { onChangeRef.current = onChange; }, [onChange]); useEffect(() => { return () => { if (batchTimer.current) clearTimeout(batchTimer.current); }; }, []); const remaining = maxCount != null ? Math.max(0, maxCount - urls.length) : Number.POSITIVE_INFINITY; const canAdd = remaining > 0; async function uploadBatch(files: File[]) { const current = urlsRef.current; const room = maxCount != null ? Math.max(0, maxCount - current.length) : files.length; const picked = files.slice(0, room); if (!picked.length) { message.warning(maxCount != null ? `最多 ${maxCount} 张` : '无法上传'); return; } if (files.length > picked.length) { message.warning(`已达上限,仅上传前 ${picked.length} 张`); } setUploading(true); const appended: string[] = []; let fail = 0; try { for (const file of picked) { try { const result = await uploadFileToOss(file, { bizType, mediaType }); appended.push(result.url); } catch { fail += 1; } } if (appended.length) { // 始终基于最新列表追加,避免并行上传互相覆盖 const next = [...urlsRef.current, ...appended]; urlsRef.current = next; onChangeRef.current?.(next); message.success(`成功上传 ${appended.length} 张${fail ? `,失败 ${fail} 张` : ''}`); } else if (fail) { message.error('上传失败'); } } finally { setUploading(false); } } function enqueueUploadBatch(files: File[]) { uploadChain.current = uploadChain.current .then(() => uploadBatch(files)) .catch(() => undefined); } function flushBatch() { if (batchTimer.current) { clearTimeout(batchTimer.current); batchTimer.current = null; } if (!batchBuf.current.length) return; const files = [...batchBuf.current]; batchBuf.current = []; enqueueUploadBatch(files); } const beforeUpload: UploadProps['beforeUpload'] = (file) => { batchBuf.current.push(file as File); // 多选时 beforeUpload 可能逐文件触发;短防抖合并成一次批量 if (batchTimer.current) clearTimeout(batchTimer.current); batchTimer.current = setTimeout(() => { flushBatch(); }, 80); return false; }; function removeAt(index: number) { const next = urlsRef.current.filter((_, i) => i !== index); urlsRef.current = next; onChangeRef.current?.(next); } return ( {tip ?? (maxCount != null ? `最多 ${maxCount} 张,支持一次选择多张批量上传` : '支持一次选择多张批量上传')} {maxCount != null ? `(已选 ${urls.length}/${maxCount})` : urls.length ? `(已选 ${urls.length})` : ''} {urls.length > 0 ? ( {urls.map((url, index) => (
{isPdf(url) ? ( PDF ) : ( )}
))}
) : null}
); }