import { useEffect, useRef, useState } from 'react'; import { uploadFileToOss, type OssMediaType } from '../lib/upload'; import { enqueueUpload } from '../lib/upload-lock'; import { formatChooseImageFailMessage } from '@dukang/weixin-sdk'; import { isWechatEnv, weixinSdk } from '../lib/weixin'; import { toastError, toastSuccess } from '../lib/toast'; type Props = { value?: string[]; onChange?: (urls: string[]) => void; bizType: string; mediaType?: OssMediaType; maxCount?: number; disabled?: boolean; label?: string; /** 系统文件选择器的 accept,默认仅图片 */ accept?: string; /** 计量单位文案,如「张」「个」 */ unit?: string; /** * stack:缩略图 + 下方独立上传按钮(合同等) * grid:九宫格,末尾「+」格上传,无独立大按钮(环境图) */ variant?: 'stack' | 'grid'; }; function isCancelError(msg: string): boolean { return /cancel|取消/i.test(msg); } function isPdf(url: string): boolean { return /\.pdf(\?|$)/i.test(url); } function normalizeUrls(value?: string[]) { return (value ?? []).map((u) => String(u || '').trim()).filter(Boolean); } /** 合伙人端多图批量上传(微信相册可多选) */ export default function MultiOssUploadField({ value, onChange, bizType, mediaType = 'IMAGE', maxCount = 20, disabled, label, accept = 'image/*', unit = '张', variant = 'stack', }: Props) { const inputRef = useRef(null); const pickingRef = useRef(false); const [uploading, setUploading] = useState(false); const [error, setError] = useState(''); const urls = normalizeUrls(value); const urlsRef = useRef(urls); const onChangeRef = useRef(onChange); const remaining = Math.max(0, maxCount - urls.length); const inWechat = isWechatEnv(); const isGrid = variant === 'grid'; useEffect(() => { urlsRef.current = urls; }, [urls]); useEffect(() => { onChangeRef.current = onChange; }, [onChange]); useEffect(() => { if (!inWechat) return; void weixinSdk.init().catch(() => {}); }, [inWechat]); function showUploadError(text: string) { setError(text); toastError(text); } async function uploadFiles(files: File[]) { const current = urlsRef.current; const room = Math.max(0, maxCount - current.length); const picked = files.slice(0, room); if (!picked.length) { showUploadError(`最多 ${maxCount} ${unit}`); return; } setUploading(true); setError(''); const appended: string[] = []; try { for (const file of picked) { if (!file.size) continue; const result = await enqueueUpload(() => uploadFileToOss(file, { bizType, mediaType })); appended.push(result.url); } if (appended.length) { const next = [...urlsRef.current, ...appended]; urlsRef.current = next; onChangeRef.current?.(next); toastSuccess(`已上传 ${appended.length} ${unit}`); } } catch (e) { showUploadError(e instanceof Error ? e.message : '上传失败'); } finally { setUploading(false); if (inputRef.current) inputRef.current.value = ''; } } async function pickWechat() { if (pickingRef.current || uploading || disabled || remaining <= 0) return; pickingRef.current = true; setError(''); try { await weixinSdk.init(); const files = await weixinSdk.chooseImages({ count: Math.min(remaining, 9), sourceType: ['album', 'camera'], }); if (!files?.length) return; await uploadFiles(files); } catch (e) { const msg = e instanceof Error ? e.message : '无法打开相册'; if (isCancelError(msg)) return; const formatted = formatChooseImageFailMessage(msg) || msg; showUploadError(formatted); inputRef.current?.click(); } finally { pickingRef.current = false; } } function openPicker() { if (inWechat) void pickWechat(); else inputRef.current?.click(); } function removeAt(index: number) { if (disabled) return; onChange?.(urls.filter((_, i) => i !== index)); } const thumb = (url: string, index: number) => (
{isPdf(url) ? ( description PDF ) : ( )} {!disabled ? ( ) : null}
); return (
{ const list = Array.from(e.target.files ?? []); if (list.length) void uploadFiles(list); }} /> {isGrid ? (
{urls.map((url, index) => thumb(url, index))} {remaining > 0 && !disabled ? ( ) : null}
) : ( <> {urls.length > 0 ? (
{urls.map((url, index) => thumb(url, index))}
) : null} )} {error ? (

{error}

) : null}
); }