| 
									
										
										
										
											2025-10-22 14:43:42 +08:00
										 |  |  |  | import { PlusOutlined } from "@ant-design/icons"; | 
					
						
							|  |  |  |  | import { Upload as AntUpload, Button, message, Modal } from "antd"; | 
					
						
							|  |  |  |  | import { useState } from "react"; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | /** | 
					
						
							|  |  |  |  |  * 文件上传组件 | 
					
						
							|  |  |  |  |  */ | 
					
						
							|  |  |  |  | const Upload = (props) => { | 
					
						
							|  |  |  |  |   const { | 
					
						
							|  |  |  |  |     value = [], | 
					
						
							|  |  |  |  |     onChange, | 
					
						
							|  |  |  |  |     onPreview, | 
					
						
							|  |  |  |  |     maxCount = 1, | 
					
						
							| 
									
										
										
										
											2025-10-30 11:27:04 +08:00
										 |  |  |  |     listType = "picture-card", | 
					
						
							|  |  |  |  |     accept = ["picture-card", "picture-circle", "picture"].includes(listType) ? ".jpg,.jpeg,.png" : "", | 
					
						
							| 
									
										
										
										
											2025-10-22 14:43:42 +08:00
										 |  |  |  |     ratio = "", | 
					
						
							|  |  |  |  |     showTip = true, | 
					
						
							|  |  |  |  |     multiple = true, | 
					
						
							|  |  |  |  |     size = 0, | 
					
						
							|  |  |  |  |     tipContent, | 
					
						
							|  |  |  |  |     uploadButtonText = "点击选择文件上传", | 
					
						
							| 
									
										
										
										
											2025-10-29 16:26:08 +08:00
										 |  |  |  |     formValues, | 
					
						
							| 
									
										
										
										
											2025-10-22 14:43:42 +08:00
										 |  |  |  |     ...restProps | 
					
						
							|  |  |  |  |   } = props; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   const [previewVisible, setPreviewVisible] = useState(false); | 
					
						
							|  |  |  |  |   const [previewImage, setPreviewImage] = useState(""); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 生成提示信息
 | 
					
						
							|  |  |  |  |   const getTipText = () => { | 
					
						
							|  |  |  |  |     if (tipContent) | 
					
						
							|  |  |  |  |       return tipContent; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     const tips = [ | 
					
						
							|  |  |  |  |       `最多上传${maxCount}个文件`, | 
					
						
							|  |  |  |  |       accept | 
					
						
							|  |  |  |  |         ? `并且只能上传${accept | 
					
						
							|  |  |  |  |           .replace(/\./g, "") | 
					
						
							|  |  |  |  |           .split(",") | 
					
						
							|  |  |  |  |           .join("、")}格式的文件`
 | 
					
						
							|  |  |  |  |         : "可以上传任意格式的文件", | 
					
						
							|  |  |  |  |       size ? `文件大小不能超过${size}M` : "", | 
					
						
							|  |  |  |  |       ratio ? `只能上传${ratio}分辨率的图片` : "", | 
					
						
							|  |  |  |  |     ].filter(Boolean); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     return `${tips.join(",")}。`; | 
					
						
							|  |  |  |  |   }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   const handleBeforeUpload = () => { | 
					
						
							|  |  |  |  |     return false; | 
					
						
							|  |  |  |  |   }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 文件状态改变
 | 
					
						
							|  |  |  |  |   const handleChange = ({ file, fileList }) => { | 
					
						
							|  |  |  |  |     const acceptList = accept ? accept.split(",") : []; | 
					
						
							|  |  |  |  |     const ratioArr = ratio ? ratio.split("*") : []; | 
					
						
							|  |  |  |  |     const suffix = file.name.substring( | 
					
						
							|  |  |  |  |       file.name.lastIndexOf("."), | 
					
						
							|  |  |  |  |       file.name.length, | 
					
						
							|  |  |  |  |     ); | 
					
						
							|  |  |  |  |     const maxSize = size * 1024 * 1024; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 验证文件格式
 | 
					
						
							|  |  |  |  |     if (acceptList.length > 0 && !acceptList.includes(suffix)) { | 
					
						
							|  |  |  |  |       message.warning(`只能上传${accept}格式的文件`); | 
					
						
							|  |  |  |  |       return; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 验证文件大小
 | 
					
						
							|  |  |  |  |     if (maxSize && file.size > maxSize) { | 
					
						
							|  |  |  |  |       message.warning(`文件大小不能超过${size}M`); | 
					
						
							|  |  |  |  |       return; | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |     // 验证图片分辨率
 | 
					
						
							|  |  |  |  |     if (ratioArr.length === 2 && file.type?.startsWith("image/")) { | 
					
						
							| 
									
										
										
										
											2025-10-28 14:08:33 +08:00
										 |  |  |  |       const validateImageResolution = (imageUrl) => { | 
					
						
							|  |  |  |  |         const img = new Image(); | 
					
						
							|  |  |  |  |         img.onload = () => { | 
					
						
							|  |  |  |  |           if (img.width !== +ratioArr[0] || img.height !== +ratioArr[1]) { | 
					
						
							|  |  |  |  |             message.warning(`只能上传${ratio}分辨率的图片`); | 
					
						
							|  |  |  |  |             const filtered = fileList.filter(item => item.uid !== file.uid); | 
					
						
							|  |  |  |  |             onChange?.(filtered); | 
					
						
							|  |  |  |  |             return; | 
					
						
							|  |  |  |  |           } | 
					
						
							|  |  |  |  |           onChange?.(fileList); | 
					
						
							|  |  |  |  |         }; | 
					
						
							|  |  |  |  |         img.src = imageUrl; | 
					
						
							| 
									
										
										
										
											2025-10-22 14:43:42 +08:00
										 |  |  |  |       }; | 
					
						
							| 
									
										
										
										
											2025-10-28 14:08:33 +08:00
										 |  |  |  | 
 | 
					
						
							|  |  |  |  |       // 如果有现成的URL则直接使用,否则使用FileReader读取本地文件
 | 
					
						
							|  |  |  |  |       if (file.url) { | 
					
						
							|  |  |  |  |         validateImageResolution(file.url); | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |       else { | 
					
						
							|  |  |  |  |         const reader = new FileReader(); | 
					
						
							|  |  |  |  |         reader.onload = (e) => { | 
					
						
							|  |  |  |  |           validateImageResolution(e.target.result); | 
					
						
							|  |  |  |  |         }; | 
					
						
							|  |  |  |  |         reader.readAsDataURL(file); | 
					
						
							|  |  |  |  |       } | 
					
						
							| 
									
										
										
										
											2025-10-22 14:43:42 +08:00
										 |  |  |  |     } | 
					
						
							|  |  |  |  |     else { | 
					
						
							|  |  |  |  |       onChange?.(fileList); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |   }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 预览文件
 | 
					
						
							|  |  |  |  |   const handlePreview = (file) => { | 
					
						
							|  |  |  |  |     if (["picture-card", "picture-circle", "picture"].includes(listType)) { | 
					
						
							|  |  |  |  |       setPreviewImage(file.url || file.thumbUrl); | 
					
						
							|  |  |  |  |       setPreviewVisible(true); | 
					
						
							|  |  |  |  |     } | 
					
						
							|  |  |  |  |     onPreview?.(file); | 
					
						
							|  |  |  |  |   }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 关闭预览
 | 
					
						
							|  |  |  |  |   const handleCancel = () => { | 
					
						
							|  |  |  |  |     setPreviewVisible(false); | 
					
						
							|  |  |  |  |   }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   // 上传按钮
 | 
					
						
							|  |  |  |  |   const uploadButton | 
					
						
							|  |  |  |  |     = ["picture-card", "picture-circle", "picture"].includes(listType) | 
					
						
							|  |  |  |  |       ? ( | 
					
						
							|  |  |  |  |           <div> | 
					
						
							|  |  |  |  |             <PlusOutlined style={{ fontSize: 32 }} /> | 
					
						
							|  |  |  |  |           </div> | 
					
						
							|  |  |  |  |         ) | 
					
						
							|  |  |  |  |       : ( | 
					
						
							|  |  |  |  |           <Button type="primary">{uploadButtonText}</Button> | 
					
						
							|  |  |  |  |         ); | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  |   return ( | 
					
						
							|  |  |  |  |     <> | 
					
						
							|  |  |  |  |       <AntUpload | 
					
						
							|  |  |  |  |         fileList={value} | 
					
						
							|  |  |  |  |         multiple={multiple} | 
					
						
							|  |  |  |  |         maxCount={maxCount} | 
					
						
							|  |  |  |  |         listType={listType} | 
					
						
							|  |  |  |  |         accept={accept} | 
					
						
							|  |  |  |  |         onChange={handleChange} | 
					
						
							|  |  |  |  |         onPreview={handlePreview} | 
					
						
							|  |  |  |  |         beforeUpload={handleBeforeUpload} | 
					
						
							|  |  |  |  |         {...restProps} | 
					
						
							|  |  |  |  |       > | 
					
						
							|  |  |  |  |         {value.length >= maxCount ? null : uploadButton} | 
					
						
							|  |  |  |  |       </AntUpload> | 
					
						
							|  |  |  |  |       { | 
					
						
							|  |  |  |  |         showTip | 
					
						
							|  |  |  |  |           ? (tipContent || getTipText()) && ( | 
					
						
							|  |  |  |  |               <div style={{ marginTop: 10, color: "#ff4d4f" }}> | 
					
						
							|  |  |  |  |                 {tipContent || getTipText()} | 
					
						
							|  |  |  |  |               </div> | 
					
						
							|  |  |  |  |             ) | 
					
						
							|  |  |  |  |           : null | 
					
						
							|  |  |  |  |       } | 
					
						
							|  |  |  |  |       <Modal | 
					
						
							|  |  |  |  |         open={previewVisible} | 
					
						
							|  |  |  |  |         title="查看图片" | 
					
						
							|  |  |  |  |         footer={null} | 
					
						
							|  |  |  |  |         onCancel={handleCancel} | 
					
						
							|  |  |  |  |       > | 
					
						
							|  |  |  |  |         <img | 
					
						
							|  |  |  |  |           alt="preview" | 
					
						
							|  |  |  |  |           style={{ width: "100%", objectFit: "scale-down" }} | 
					
						
							|  |  |  |  |           src={previewImage} | 
					
						
							|  |  |  |  |         /> | 
					
						
							|  |  |  |  |       </Modal> | 
					
						
							|  |  |  |  |     </> | 
					
						
							|  |  |  |  |   ); | 
					
						
							|  |  |  |  | }; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | Upload.displayName = "Upload"; | 
					
						
							|  |  |  |  | 
 | 
					
						
							|  |  |  |  | export default Upload; |