2025-11-01 15:21:51 +08:00
|
|
|
|
import { PlusOutlined, UploadOutlined, VideoCameraAddOutlined } from "@ant-design/icons";
|
2025-10-22 14:43:42 +08:00
|
|
|
|
import { Upload as AntUpload, Button, message, Modal } from "antd";
|
|
|
|
|
|
import { useState } from "react";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 文件上传组件
|
|
|
|
|
|
*/
|
|
|
|
|
|
const Upload = (props) => {
|
|
|
|
|
|
const {
|
|
|
|
|
|
value = [],
|
|
|
|
|
|
onChange,
|
|
|
|
|
|
onPreview,
|
2025-11-07 08:52:15 +08:00
|
|
|
|
onRemove,
|
|
|
|
|
|
onGetRemoveFile,
|
|
|
|
|
|
beforeUpload,
|
2025-11-01 15:21:51 +08:00
|
|
|
|
maxCount: externalMaxCount,
|
|
|
|
|
|
listType: externalListType,
|
|
|
|
|
|
accept: externalAccept,
|
2025-10-22 14:43:42 +08:00
|
|
|
|
ratio = "",
|
|
|
|
|
|
showTip = true,
|
|
|
|
|
|
multiple = true,
|
2025-11-01 15:21:51 +08:00
|
|
|
|
size: externalSize,
|
2025-10-22 14:43:42 +08:00
|
|
|
|
tipContent,
|
2025-11-01 15:21:51 +08:00
|
|
|
|
uploadButtonText: externalUploadButtonText,
|
|
|
|
|
|
fileType: externalFileType,
|
2025-10-22 14:43:42 +08:00
|
|
|
|
...restProps
|
|
|
|
|
|
} = props;
|
|
|
|
|
|
|
|
|
|
|
|
const [previewVisible, setPreviewVisible] = useState(false);
|
|
|
|
|
|
const [previewImage, setPreviewImage] = useState("");
|
|
|
|
|
|
|
2025-11-01 15:21:51 +08:00
|
|
|
|
// 预设的文件格式
|
|
|
|
|
|
const imageAccept = ".jpg,.jpeg,.png";
|
|
|
|
|
|
const documentAccept = ".pdf,.doc,.docx";
|
|
|
|
|
|
const videoAccept = ".mp4";
|
|
|
|
|
|
|
|
|
|
|
|
// 根据accept自动判断文件类型
|
|
|
|
|
|
const getAutoFileType = () => {
|
|
|
|
|
|
if (externalAccept) {
|
|
|
|
|
|
if (externalAccept === "*")
|
|
|
|
|
|
return "document";
|
|
|
|
|
|
const acceptList = externalAccept.split(",");
|
|
|
|
|
|
if (acceptList.some(format => videoAccept.split(",").includes(format)))
|
|
|
|
|
|
return "video";
|
|
|
|
|
|
if (acceptList.some(format => documentAccept.split(",").includes(format)))
|
|
|
|
|
|
return "document";
|
|
|
|
|
|
if (acceptList.some(format => imageAccept.split(",").includes(format)))
|
|
|
|
|
|
return "image";
|
|
|
|
|
|
return "document";
|
|
|
|
|
|
}
|
|
|
|
|
|
return "image";
|
|
|
|
|
|
};
|
|
|
|
|
|
const fileType = externalFileType || getAutoFileType();
|
|
|
|
|
|
|
|
|
|
|
|
// 文件类型判断
|
|
|
|
|
|
const isImageType = fileType === "image";
|
|
|
|
|
|
const isVideoType = fileType === "video";
|
|
|
|
|
|
const isDocumentType = fileType === "document";
|
|
|
|
|
|
|
|
|
|
|
|
// 获取listType
|
|
|
|
|
|
const getListType = () => {
|
|
|
|
|
|
if (externalListType)
|
|
|
|
|
|
return externalListType;
|
|
|
|
|
|
if (externalAccept === "*")
|
|
|
|
|
|
return "text";
|
|
|
|
|
|
if (fileType === "image")
|
|
|
|
|
|
return "picture-card";
|
|
|
|
|
|
return "text";
|
|
|
|
|
|
};
|
|
|
|
|
|
const listType = getListType();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取文件格式
|
|
|
|
|
|
const getAccept = () => {
|
|
|
|
|
|
if (externalAccept)
|
|
|
|
|
|
return externalAccept === "*" ? "" : externalAccept;
|
|
|
|
|
|
if (isImageType)
|
|
|
|
|
|
return imageAccept;
|
|
|
|
|
|
if (isVideoType)
|
|
|
|
|
|
return videoAccept;
|
|
|
|
|
|
if (isDocumentType)
|
|
|
|
|
|
return documentAccept;
|
|
|
|
|
|
return imageAccept;
|
|
|
|
|
|
};
|
|
|
|
|
|
const accept = getAccept();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取默认上传数量
|
|
|
|
|
|
const getMaxCount = () => {
|
|
|
|
|
|
if (externalMaxCount)
|
|
|
|
|
|
return externalMaxCount;
|
|
|
|
|
|
if (isVideoType)
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
if (isImageType)
|
|
|
|
|
|
return 4;
|
|
|
|
|
|
if (isDocumentType)
|
|
|
|
|
|
return 4;
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
};
|
|
|
|
|
|
const maxCount = getMaxCount();
|
|
|
|
|
|
|
|
|
|
|
|
// 获取默认文件大小
|
|
|
|
|
|
const getSize = () => {
|
|
|
|
|
|
if (externalSize)
|
|
|
|
|
|
return externalSize;
|
|
|
|
|
|
if (isVideoType)
|
|
|
|
|
|
return 100;
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
};
|
|
|
|
|
|
const size = getSize();
|
|
|
|
|
|
|
|
|
|
|
|
// 上传按钮文字
|
|
|
|
|
|
const uploadButtonText = externalUploadButtonText || (isVideoType ? "上传视频" : "上传附件");
|
|
|
|
|
|
|
|
|
|
|
|
// 文件格式提示
|
|
|
|
|
|
const acceptTip = accept.replace(/\./g, "").split(",").join("、");
|
|
|
|
|
|
|
2025-10-22 14:43:42 +08:00
|
|
|
|
// 生成提示信息
|
|
|
|
|
|
const getTipText = () => {
|
|
|
|
|
|
if (tipContent)
|
|
|
|
|
|
return tipContent;
|
|
|
|
|
|
|
|
|
|
|
|
const tips = [
|
|
|
|
|
|
`最多上传${maxCount}个文件`,
|
|
|
|
|
|
accept
|
2025-11-01 15:21:51 +08:00
|
|
|
|
? `并且只能上传${acceptTip}格式的文件`
|
2025-10-22 14:43:42 +08:00
|
|
|
|
: "可以上传任意格式的文件",
|
|
|
|
|
|
size ? `文件大小不能超过${size}M` : "",
|
|
|
|
|
|
ratio ? `只能上传${ratio}分辨率的图片` : "",
|
|
|
|
|
|
].filter(Boolean);
|
|
|
|
|
|
|
|
|
|
|
|
return `${tips.join(",")}。`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-07 08:52:15 +08:00
|
|
|
|
const handleBeforeUpload = (file, fileList) => {
|
|
|
|
|
|
if (beforeUpload)
|
|
|
|
|
|
return beforeUpload(file, fileList)
|
2025-10-22 14:43:42 +08:00
|
|
|
|
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)) {
|
2025-11-01 15:21:51 +08:00
|
|
|
|
message.warning(`只能上传${acceptTip}格式的文件`);
|
2025-10-22 14:43:42 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-11-07 08:52:15 +08:00
|
|
|
|
// 删除文件
|
|
|
|
|
|
const handleRemove = (file) => {
|
|
|
|
|
|
if (file.originFileObj)
|
|
|
|
|
|
onGetRemoveFile?.(file);
|
|
|
|
|
|
return onRemove?.(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 14:43:42 +08:00
|
|
|
|
// 预览文件
|
|
|
|
|
|
const handlePreview = (file) => {
|
2025-11-01 15:21:51 +08:00
|
|
|
|
if (isImageType) {
|
2025-10-22 14:43:42 +08:00
|
|
|
|
setPreviewImage(file.url || file.thumbUrl);
|
|
|
|
|
|
setPreviewVisible(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
onPreview?.(file);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 关闭预览
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
setPreviewVisible(false);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 上传按钮
|
|
|
|
|
|
const uploadButton
|
2025-11-01 15:21:51 +08:00
|
|
|
|
= isImageType
|
2025-10-22 14:43:42 +08:00
|
|
|
|
? (
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<PlusOutlined style={{ fontSize: 32 }} />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
: (
|
2025-11-01 15:21:51 +08:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={isVideoType ? <VideoCameraAddOutlined /> : <UploadOutlined />}
|
|
|
|
|
|
>
|
|
|
|
|
|
{uploadButtonText}
|
|
|
|
|
|
</Button>
|
2025-10-22 14:43:42 +08:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<AntUpload
|
|
|
|
|
|
fileList={value}
|
|
|
|
|
|
multiple={multiple}
|
|
|
|
|
|
maxCount={maxCount}
|
|
|
|
|
|
listType={listType}
|
|
|
|
|
|
accept={accept}
|
|
|
|
|
|
onChange={handleChange}
|
|
|
|
|
|
onPreview={handlePreview}
|
2025-11-07 08:52:15 +08:00
|
|
|
|
onRemove={handleRemove}
|
2025-10-22 14:43:42 +08:00
|
|
|
|
beforeUpload={handleBeforeUpload}
|
|
|
|
|
|
{...restProps}
|
|
|
|
|
|
>
|
|
|
|
|
|
{value.length >= maxCount ? null : uploadButton}
|
|
|
|
|
|
</AntUpload>
|
|
|
|
|
|
{
|
|
|
|
|
|
showTip
|
2025-11-01 15:21:51 +08:00
|
|
|
|
? (getTipText()) && (
|
2025-10-22 14:43:42 +08:00
|
|
|
|
<div style={{ marginTop: 10, color: "#ff4d4f" }}>
|
2025-11-01 15:21:51 +08:00
|
|
|
|
{getTipText()}
|
2025-10-22 14:43:42 +08:00
|
|
|
|
</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;
|