import { useState } from 'react' import { Button, Image, Upload, message } from 'antd' import { CloseOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons' import { uploadApi } from '../api' interface ImageUploaderProps { value?: string[] onChange?: (urls: string[]) => void maxCount?: number maxSizeMB?: number /** 压缩后最长边像素,默认 1600 */ maxWidth?: number /** webp 压缩质量 0~1,默认 0.8 */ quality?: number disabled?: boolean } const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp)$/i async function compressToWebp(file: File, maxWidth: number, quality: number): Promise { const bitmap = await createImageBitmap(file) try { const scale = Math.min(1, maxWidth / Math.max(1, bitmap.width)) const width = Math.max(1, Math.round(bitmap.width * scale)) const height = Math.max(1, Math.round(bitmap.height * scale)) const canvas = document.createElement('canvas') canvas.width = width canvas.height = height const ctx = canvas.getContext('2d') if (!ctx) { throw new Error('浏览器不支持图片处理') } ctx.drawImage(bitmap, 0, 0, width, height) const supportsWebp = document.createElement('canvas').toDataURL('image/webp').startsWith('data:image/webp') const blob = await new Promise((resolve) => { canvas.toBlob(resolve, supportsWebp ? 'image/webp' : 'image/jpeg', quality) }) if (!blob) { throw new Error('图片压缩失败') } const name = file.name.replace(IMAGE_EXT_RE, supportsWebp ? '.webp' : '.jpg') return new File([blob], name, { type: blob.type }) } finally { bitmap.close() } } function fileToBase64(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader() reader.readAsDataURL(file) reader.onload = () => resolve(reader.result as string) reader.onerror = (e) => reject(e) }) } /** * 通用图片上传组件:选择后自动压缩并转 webp 再上传,缩略图预览、可删除。 * 受控组件,value 为已上传图片 URL 数组。 */ export default function ImageUploader({ value, onChange, maxCount = 5, maxSizeMB = 5, maxWidth = 1600, quality = 0.8, disabled = false, }: ImageUploaderProps) { const [uploading, setUploading] = useState(false) const urls = value ?? [] const handleUpload = async (file: File) => { if (file.size > maxSizeMB * 1024 * 1024) { message.error(`图片不能超过 ${maxSizeMB}MB`) return Upload.LIST_IGNORE } setUploading(true) try { const compressed = await compressToWebp(file, maxWidth, quality) // 实时生成本地 Base64 作为高优先级预览与兜底 const localBase64 = await fileToBase64(compressed) let finalUrl = localBase64 try { const res = await uploadApi.file(compressed) if (res && res.url) { finalUrl = res.url } } catch (e) { console.warn('后端上传接口网络未通,已使用本地 Base64 进行实时预览:', e) } onChange?.([...urls, finalUrl]) } catch (e) { message.error(e instanceof Error ? e.message : '图片处理失败') } finally { setUploading(false) } return Upload.LIST_IGNORE } const removeUrl = (url: string) => { onChange?.(urls.filter((item) => item !== url)) } return (
{urls.map((url) => (
{!disabled && (
))} {urls.length < maxCount && (
{uploading ? ( ) : ( )} {uploading ? '处理中...' : '上传'}
)}
) }