概述
超过100MB的情况,阿里云直接上传会报错,调研了一下阿里云的文件上传功能并进行了以下相关实践
上传文件
主要分为:简单上传、分片上传、断点续传
- 简单上传
- File对象、Blob数据以及OSS Buffer上传到OSS
- 弊端:图片不支持20MB以上,文件不支持100MB以上
- 分片上传
- 上传大于100 MB且小于48.8 TB
- 可计算上传进度
- 断点续传
- 文件上传过程中因网络异常或程序崩溃导致文件上传失败时,可继续上传未完成的部分
上传流程设计
-
初始化一个对象存储文件进度
-
初始化一个数组存储上传的文件数组
-
每次上传初始化一个文件选择器
效果预览
组件开发
-
准备工作
-
- 使用RAM用户,获取上传凭证
- 设置跨域资源共享(CORS)
- 具体操作,请参见设置跨域资源共享。
- 通过浏览器直接访问OSS时,CORS配置规则要求如下:
- 来源:设置精准域名(例如
www.aliyun.com
)或带有通配符星号(*)的域名(例如*.aliyun.com
)。 - 允许Methods:请根据实际使用场景,选择不同的Methods。例如分片上传时,设置为PUT;删除文件时,设置为DELETE。
- 允许Headers:设置为
*
。 - 暴露Headers:设置为
ETag
、x-oss-request-id
和x-oss-version-id
。
安装阿里云支持
yarn add ali-oss
具体代码实现
1、基础组件及获取鉴权
import { getBatchImageZoomUrl, getOSSData } from "@/common/dataSource/oss";
import { Button, Progress, Upload } from "antd";
import { useEffect, useState, useRef } from 'react'
import './OssUploadFile.less'
import numeral from 'numeral'
const OSS = require('ali-oss');
const defaultImage = 'https://image.baidu.com/search/index?ct=201326592&z=undefined&tn=baiduimage&ipn=d&word=%E6%96%87%E4%BB%B6&step_word=&ie=utf-8&in=&cl=2&lm=-1&st=undefined&hd=undefined&latest=undefined©right=undefined&cs=3382622322,308361337&os=2045226791,2317509516&simid=3382622322,308361337&pn=124&di=7108135681917976577&ln=1948&fr=&fmq=1658197673394_R&fm=&ic=undefined&s=undefined&se=&sme=&tab=0&width=undefined&height=undefined&face=undefined&is=0,0&istype=0&ist=&jit=&bdtype=0&spn=0&pi=0&gsm=5a&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%253A%252F%252Fimg.zcool.cn%252Fcommunity%252F010a005544672f0000019ae9eee026.jpg%25402o.jpg%26refer%3Dhttp%253A%252F%252Fimg.zcool.cn%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Dauto%3Fsec%3D1660789581%26t%3Dca4d615c293ec7590ca4bd9dc84792d5&rpstart=0&rpnum=0&adpicid=0&nojc=undefined&tt=1&dyTabStr=MCwzLDYsNSwyLDEsNCw4LDcsOQ%3D%3D';
enum UploadMethodType {
SECOND = 'second',
MULTIPART = 'multipart'
}
enum StatusType {
PENDING = 'pending',
DONE = 'done',
}
type ShowListType = {
name: string,
url: string,
key: string,
type: UploadMethodType,
status: StatusType }
function OssUploadFile() {
const fileInputRef = useRef(null);
const [showList, setShowList] = useState<ShowListType[]>([])
const [uploadPercentMap, setUploadPercentMap] = useState({})
const [oSSData, setOSSData] = useState({})
async function initOssClient() {
const info = await getOSSData()
setOSSData(info)
};
function commonUpload(file) {
}
function multipart(file) {
}
function uploadOss() {
// status.innerText = 'Uploading';
// 获取STS Token
if (!oSSData?.accessId) {
return
}
const { files } = fileDom;
const fileList = Array.from(files);
fileList.forEach(file => {
// 如果文件大学小于分片大小,使用普通上传,否则使用分片上传
if (file.size / 1024 / 1024 < 100) {
commonUpload(file);
} else {
multipart(file)
}
});
}
useEffect(() => {
initOssClient()
}, [])
function openFile() {
fileInputRef.current?.click()
}
return (
<div className="oss-upload-file-container">
<input type="file" id='fileDom' multiple onChange={uploadOss} ref={fileInputRef} style={{ display: "none" }} />
<Button onClick={openFile}>上传文件</Button>
<Upload />
<div className="show-list">
{showList?.map(one => {
const percent = numeral(uploadPercentMap[one.key] * 100).format('0.0')
return <div key={one.key} className='file-item'>
{!one.url && one.type === UploadMethodType.MULTIPART ? <Progress percent={percent} width={80} type="circle" /> : <img src={one.url} key={one.url || defaultImage} width={80} height={80} />}
</div>
})}
</div>
</div>
)
}
export default OssUploadFile
2、文件格式校验及文件唯一标示生成
// 上传前的检查及file格式化处理
const beforeUpload = async (file: any) => {
let limitFormat = { number: limit, size: 'MB', type: '图片' };
let isLt2M = limit && file.size / 1024 / 1024 < limit;
// 文件处理
if (file.type?.split('/')?.[0] !== 'image') {
isLt2M = fileLimit && file.size / 1024 / 1024 < fileLimit;
limitFormat = { number: fileLimit, size: 'MB', type: '文件' };
}
// 大小转换
if (limitFormat.number && limitFormat.number > 1024) {
limitFormat = {
number: Number(`${limitFormat.number / 1024}`?.split('.')?.[0]),
size: 'GB',
type: limitFormat.type,
};
}
// 文件大小判断
if (!isLt2M) {
message.error(
`${file.name} ${limitFormat.type}大小应小于 ${limitFormat?.number}${limitFormat.size}!`,
);
return 'LIST_IGNORE';
}
const expire = Number(oSSData.expire) - 60 || 0; // 减去60 秒 留出缓冲时间
if (expire * 1000 < Date.now()) {
await initOssClient();
}
// 文件个数判断
if (fileNum && showList.length + uploadRef.current >= fileNum) {
message.error(`${file.name}上传失败,超过最大限制${fileNum}个`);
return 'LIST_IGNORE';
}
// 唯一值设置
uploadRef.current += 1;
const suffix = file.name?.slice(file.name.lastIndexOf('.'));
const filename = `${Date.now()}${uploadRef.current}${suffix}`;
file.key = oSSData.dir + filename; // eslint-disable-line
// 上传类型判断
if (file.size / 1024 / 1024 < 10) {
commonUpload(file);
} else {
multipart(file);
}
};
3、简单上传
function commonUpload(file) {
// 建立阿里云连接
const ossClient = new OSS({
region: 'oss-cn-beijing',
accessKeyId: oSSData?.accessId,
accessKeySecret: oSSData?.accessKey,
bucket: oSSData?.bucketName,
});
// 新增待上传文件
file.status = StatusTypeEnum.PENDING;
const fileKey = file.key;
showOssUploadFileListRef.current = [
...showOssUploadFileListRef.current,
{
...file,
key: fileKey,
name: file.name,
size: file.size,
type: UploadMethodType.SECOND,
},
];
setShowList(showOssUploadFileListRef.current);
return ossClient
.put(fileKey, file)
.then((result) => {
// 更新上传文件地址
getBatchImageZoomUrl({ picNames: result.name, proportion: 10 }).then((urls) => {
message.success('上传成功');
uploadEnd && uploadEnd();
showOssUploadFileListRef.current = showOssUploadFileListRef.current.map((fileItem) => {
if (fileItem.key === fileKey) {
return {
...fileItem,
status: StatusTypeEnum?.DONE,
url: urls?.[0] || defaultImage,
};
} else {
return fileItem;
}
});
setShowList(showOssUploadFileListRef.current);
uploadRef.current -= 1;
props?.onChange?.(showOssUploadFileListRef.current);
});
})
.catch(() => {
uploadEnd && uploadEnd();
message.error('上传失败~');
removeImg(fileKey);
uploadRef.current -= 1;
});
}
4、分片上传
// 分片上传
function multipart(file) {
// 建立阿里云连接
const ossClient = new OSS({
region: 'oss-cn-beijing',
accessKeyId: oSSData?.accessId,
accessKeySecret: oSSData?.accessKey,
bucket: oSSData?.bucketName,
});
const fileKey = file.key;
// 新增待上传文件
file.status = StatusTypeEnum.PENDING;
showOssUploadFileListRef.current = [
...showOssUploadFileListRef.current,
{
...file,
key: fileKey,
name: file.name,
size: file.size,
type: UploadMethodType.MULTIPART,
},
];
setShowList(showOssUploadFileListRef.current);
return ossClient
.multipartUpload(fileKey, file, {
// 获取分片上传进度、断点和返回值。
progress: (p, cpt, res) => {
uploadPercentRef.current = { ...uploadPercentRef.current, [fileKey]: p };
setUploadPercentMap(uploadPercentRef.current);
},
// 设置并发上传的分片数量。
parallel: 4,
// 设置分片大小。默认值为1 MB,最小值为100 KB。
partSize: 1024 * 1024 * 5,
// headers,
// 自定义元数据,通过HeadObject接口可以获取Object的元数据。
meta: { year: 2020, people: 'test' },
mime: 'text/plain',
})
.then((result) => {
// 更新上传文件地址
getBatchImageZoomUrl({ picNames: result.name, proportion: 30 }).then((urls) => {
showOssUploadFileListRef.current = showOssUploadFileListRef.current.map((fileItem) => {
if (fileItem.key === fileKey) {
return {
...fileItem,
status: StatusTypeEnum?.DONE,
url: urls?.[0] || defaultImage,
};
} else {
return fileItem;
}
});
setShowList(showOssUploadFileListRef.current);
uploadRef.current -= 1;
props?.onChange?.(showOssUploadFileListRef.current);
});
})
.catch((err) => {
message.error('上传失败~');
removeImg(fileKey);
uploadRef.current -= 1;
});
}
5、自定义上传
- 隐藏原始file,新增自定义按钮控制原始file的dom 行为
<div id="fileDomList" style={{ display: 'none' }}>
{fileDomList?.map((one) => (
<input
type="file"
accept={accept}
disabled={uploadDisabled}
id={one}
key={one}
onChange={uploadOss}
multiple
/>
))}
</div>
<Space>
<span onClick={openFile}>{uploadDom}</span>
{addonAfter && <Typography.Text type="secondary">
{addonAfter}</Typography.Text>}
</Space>
- 避免选择文件重复处理,每次点击dom按钮,新增一个file的选择器
function openFile() {
const fileDom = fileDomList?.[fileDomList.length - 1];
document.getElementById(fileDom)?.click();
setOpenFileDom(fileDom);
setFileDomList([...fileDomList, `fileDom_${fileDomList.length}`]);
}
拓展一:Form支持
根据form可以直接获取上传文件的数据
- 支持外部onChang操作
- 支持 value值变化更新
<Form.Item name="workMaterialInfoVOList" label="回执文件" required>
<OssUploadFile addonAfter={`图片大小不超过15MB, 文件大小不超过4GB, 支持格式:${accept}`}
accept={accept}
/>
</Form.Item>
拓展二:自定义文件展示
- 根据函数
listRenderFunc
对外暴露 文件列表showList
和进度对象uploadPercentMap
,可根据具体业务需求处理不用的样式
{listRenderFunc ? (
listRenderFunc(showList, uploadPercentMap)
) : (
<div className="show-list">
{showList?.map((one) => {
const percent = numeral(uploadPercentMap[one.key] * 100).format('0.0');
return (
<div key={one.key} className="file-item-box">
<div className="file-item">
<div className="operate-action">
{one.status === StatusTypeEnum.DONE ? (
<DeleteOutlined className="operate-icon" onClick={() => removeImg(one.key)} />
) : null}
</div>
{!one.url && one.type === UploadMethodType.MULTIPART ? (
<span>
<Progress percent={percent} width={80} type="circle" />
</span>
) : (
<span className="img-container">
<img
id={`js-img-${one.key}`}
src={one.url}
key={one.url || defaultImage}
width={80}
height={80}
/>
</span>
)}
</div>
<div>
<Typography.Text ellipsis={{ tooltip: one.name }} style={{ width: 100 }}>
{one.name}
</Typography.Text>
</div>
</div>
);
})}
</div>
)}
拓展三:手动删除
可手动删除已上传的文件
<div className="file-item">
<div className="operate-action">
{one.status === StatusTypeEnum.DONE ? (
<DeleteOutlined className="operate-icon" onClick={() => removeImg(one.key)} />
) : null}
</div>
{!one.url && one.type === UploadMethodType.MULTIPART ? (
<span>
<Progress percent={percent} width={80} type="circle" />
</span>
) : (
<span className="img-container">
<img
id={`js-img-${one.key}`}
src={one.url}
key={one.url || defaultImage}
width={80}
height={80}
/>
</span>
)}
</div>
- Js
// 删除
function removeImg(fileKey: string) {
showOssUploadFileListRef.current = showOssUploadFileListRef.current.filter(
(fileItem) => fileItem.key !== fileKey,
);
setShowList(showOssUploadFileListRef.current);
props?.onChange?.(showOssUploadFileListRef.current);
}
优化鉴权加密
- 获取鉴权的时候增加AES解密,安装解密依赖
yarn add crypto-js
- 解密
// 解密AES
function decrypted(encryptedBase64Str: string) {
const encryptedkey = CryptoJS.enc.Utf8.parse(ACCESS_KEY_PASS);
const decryptedData = CryptoJS.AES.decrypt(encryptedBase64Str, encryptedkey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return decryptedData.toString(CryptoJS.enc.Utf8);
}
// 获取权限
async function initOssClient() {
const info = await getOSSData();
setOSSData({
...info,
accessKey: decrypted(info.accessKey),
});
}
API
参数 | 说明 | 类型 | 默认值 |
---|---|---|---|
accept | 文件上传格式 | string | '.png, .jpg, .jpeg' |
limit | 图片上传大小 | number | 15 |
fileLimit | 文件上传大小 | number | 1024 * 4 |
fileNum | 上传最大个数 | number | 99 |
listRenderFunc | 自定义文件列表 | (showList: ShowListType[], uploadPercentMap: object) => React.ReactNode; | |
onChange | 外部回调 | (value: any, name?: any) => void; | |
value | 外部回调 | ShowListType[]; | |
uploadDom | 自定义上传按钮 | React.ReactNode | < Button >上传文件</ Button > |
addonAfter | 说明信息 | any | 图片大小不超过15MB, 文件大小不超过4GB |
showName | 是否展示文件名称 | boolean | false |
fileDomId | 上传文件dom的id | string | fileDom |
踩坑
-
在多个文件上传时会存在数据和渲染不一致的行为,体现在当前操作的文件不能获取到最新的数据
- 解决方案:新增了一个全部变量,保证数据的及时性,根据setState去更新dom渲染
参考文献
[ OSS Browser.js SDK ]:用于阿里云服务的上传文档,包含简单上传、分片上传、断点上传等
[ crypto-js ]:CryptoJS (crypto.js) 为 JavaScript 提供了各种各样的加密算法在线尝试
完整代码
import { getBatchImageZoomUrl, getOSSData } from '@/common/dataSource/oss';
import { Button, message, Progress, Space, Typography } from 'antd';
import React, { useEffect, useState, useRef } from 'react';
import './OssUploadFile.less';
import numeral from 'numeral';
import { DeleteOutlined } from '@ant-design/icons';
import CryptoJS from 'crypto-js';
import { isNodeEnv } from '@sentry/utils';
const OSS = require('ali-oss');
const defaultImage = 'https://image.baidu.com/search/index?ct=201326592&z=undefined&tn=baiduimage&ipn=d&word=%E6%96%87%E4%BB%B6&step_word=&ie=utf-8&in=&cl=2&lm=-1&st=undefined&hd=undefined&latest=undefined©right=undefined&cs=3382622322,308361337&os=2045226791,2317509516&simid=3382622322,308361337&pn=124&di=7108135681917976577&ln=1948&fr=&fmq=1658197673394_R&fm=&ic=undefined&s=undefined&se=&sme=&tab=0&width=undefined&height=undefined&face=undefined&is=0,0&istype=0&ist=&jit=&bdtype=0&spn=0&pi=0&gsm=5a&objurl=https%3A%2F%2Fgimg2.baidu.com%2Fimage_search%2Fsrc%3Dhttp%253A%252F%252Fimg.zcool.cn%252Fcommunity%252F010a005544672f0000019ae9eee026.jpg%25402o.jpg%26refer%3Dhttp%253A%252F%252Fimg.zcool.cn%26app%3D2002%26size%3Df9999%2C10000%26q%3Da80%26n%3D0%26g%3D0n%26fmt%3Dauto%3Fsec%3D1660789581%26t%3Dca4d615c293ec7590ca4bd9dc84792d5&rpstart=0&rpnum=0&adpicid=0&nojc=undefined&tt=1&dyTabStr=MCwzLDYsNSwyLDEsNCw4LDcsOQ%3D%3D';
enum UploadMethodType {
SECOND = 'second',
MULTIPART = 'multipart',
}
enum StatusTypeEnum {
PENDING = 'pending',
DONE = 'done',
}
type ShowListType = {
name: string;
url: string;
key: string;
type: UploadMethodType;
status: StatusTypeEnum;
};
type OssUploadFileType = {
accept?: string;
limit?: number; // 上传图片大小
fileLimit?: number; // 上传文件大小
fileNum?: number; // 上传文件个数
listRenderFunc?: (showList: ShowListType[], uploadPercentMap: object) => React.ReactNode; // 列表的数据
onChange?: (value: any, name?: any) => void;
value?: ShowListType[];
uploadDom?: React.ReactNode;
addonAfter?: any;
showName: boolean;
fileDomId?: string;
uploadStart?: () => void;
uploadEnd?: () => void;
uploadDisabled?: boolean;
};
type OSSDataType = {
accessId: string;
accessKey: string;
bucketName: string;
dir: string;
expire: string;
host: string;
policy: string;
signature: string;
};
const ACCESS_KEY_PASS = '用作密码转换的key,前后端一致即可';
const OssUploadFile: React.FC<OssUploadFileType> = (props) => {
const [showList, setShowList] = useState<ShowListType[]>([]);
const [uploadPercentMap, setUploadPercentMap] = useState({});
const [oSSData, setOSSData] = useState<Partial<OSSDataType>>({});
const [fileDomList, setFileDomList] = useState([props?.fileDomId]);
const uploadRef = useRef(0);
const openFileDomRef = useRef('');
const uploadPercentRef = useRef({});
const showOssUploadFileListRef = useRef<ShowListType[]>([]);
const {
limit,
fileNum,
fileLimit,
accept,
listRenderFunc,
uploadDom,
addonAfter,
uploadStart,
uploadEnd,
uploadDisabled,
} = props;
// 解密AES
function decrypted(encryptedBase64Str: string) {
const encryptedkey = CryptoJS.enc.Utf8.parse(ACCESS_KEY_PASS);
const decryptedData = CryptoJS.AES.decrypt(encryptedBase64Str, encryptedkey, {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7,
});
return decryptedData.toString(CryptoJS.enc.Utf8);
}
async function initOssClient() {
const info = await getOSSData();
setOSSData({
...info,
accessKey: decrypted(info.accessKey),
});
}
useEffect(() => {
if (props?.value) {
setShowList(props.value);
showOssUploadFileListRef.current = props.value;
}
}, [props.value]);
// 删除
function removeImg(fileKey: string) {
showOssUploadFileListRef.current = showOssUploadFileListRef.current.filter(
(fileItem) => fileItem.key !== fileKey,
);
setShowList(showOssUploadFileListRef.current);
props?.onChange?.(showOssUploadFileListRef.current);
}
function commonUpload(file) {
// 建立阿里云连接
const ossClient = new OSS({
region: 'oss-cn-beijing',
accessKeyId: oSSData?.accessId,
accessKeySecret: oSSData?.accessKey,
bucket: oSSData?.bucketName,
});
// 新增待上传文件
file.status = StatusTypeEnum.PENDING;
const fileKey = file.key;
showOssUploadFileListRef.current = [
...showOssUploadFileListRef.current,
{
...file,
key: fileKey,
name: file.name,
size: file.size,
type: UploadMethodType.SECOND,
},
];
setShowList(showOssUploadFileListRef.current);
return ossClient
.put(fileKey, file)
.then((result) => {
// 更新上传文件地址
getBatchImageZoomUrl({ picNames: result.name, proportion: 10 }).then((urls) => {
message.success('上传成功');
uploadEnd && uploadEnd();
showOssUploadFileListRef.current = showOssUploadFileListRef.current.map((fileItem) => {
if (fileItem.key === fileKey) {
return {
...fileItem,
status: StatusTypeEnum?.DONE,
url: urls?.[0] || defaultImage,
};
} else {
return fileItem;
}
});
setShowList(showOssUploadFileListRef.current);
uploadRef.current -= 1;
props?.onChange?.(showOssUploadFileListRef.current);
});
})
.catch(() => {
uploadEnd && uploadEnd();
message.error('上传失败~');
removeImg(fileKey);
uploadRef.current -= 1;
});
}
// 分片上传
function multipart(file) {
// 建立阿里云连接
const ossClient = new OSS({
region: 'oss-cn-beijing',
accessKeyId: oSSData?.accessId,
accessKeySecret: oSSData?.accessKey,
bucket: oSSData?.bucketName,
});
const fileKey = file.key;
// 新增待上传文件
file.status = StatusTypeEnum.PENDING;
showOssUploadFileListRef.current = [
...showOssUploadFileListRef.current,
{
...file,
key: fileKey,
name: file.name,
size: file.size,
type: UploadMethodType.MULTIPART,
},
];
setShowList(showOssUploadFileListRef.current);
return ossClient
.multipartUpload(fileKey, file, {
// 获取分片上传进度、断点和返回值。
progress: (p, cpt, res) => {
uploadPercentRef.current = { ...uploadPercentRef.current, [fileKey]: p };
setUploadPercentMap(uploadPercentRef.current);
},
// 设置并发上传的分片数量。
parallel: 4,
// 设置分片大小。默认值为1 MB,最小值为100 KB。
partSize: 1024 * 1024 * 5,
// headers,
// 自定义元数据,通过HeadObject接口可以获取Object的元数据。
meta: { year: 2020, people: 'test' },
mime: 'text/plain',
})
.then((result) => {
// 更新上传文件地址
getBatchImageZoomUrl({ picNames: result.name, proportion: 30 }).then((urls) => {
showOssUploadFileListRef.current = showOssUploadFileListRef.current.map((fileItem) => {
if (fileItem.key === fileKey) {
return {
...fileItem,
status: StatusTypeEnum?.DONE,
url: urls?.[0] || defaultImage,
};
} else {
return fileItem;
}
});
setShowList(showOssUploadFileListRef.current);
uploadRef.current -= 1;
props?.onChange?.(showOssUploadFileListRef.current);
});
})
.catch((err) => {
message.error('上传失败~');
removeImg(fileKey);
uploadRef.current -= 1;
});
}
// 上传前的检查及file格式化处理
const beforeUpload = async (file: any) => {
let limitFormat = { number: limit, size: 'MB', type: '图片' };
let isLt2M = limit && file.size / 1024 / 1024 < limit;
// 文件处理
if (file.type?.split('/')?.[0] !== 'image') {
isLt2M = fileLimit && file.size / 1024 / 1024 < fileLimit;
limitFormat = { number: fileLimit, size: 'MB', type: '文件' };
}
// 大小转换
if (limitFormat.number && limitFormat.number > 1024) {
limitFormat = {
number: Number(`${limitFormat.number / 1024}`?.split('.')?.[0]),
size: 'GB',
type: limitFormat.type,
};
}
if (!isLt2M) {
message.error(
`${file.name} ${limitFormat.type}大小应小于 ${limitFormat?.number}${limitFormat.size}!`,
);
return 'LIST_IGNORE';
}
const expire = Number(oSSData.expire) - 60 || 0; // 减去60 秒 留出缓冲时间
if (expire * 1000 < Date.now()) {
await initOssClient();
}
if (fileNum && showList.length + uploadRef.current >= fileNum) {
message.error(`${file.name}上传失败,超过最大限制${fileNum}个`);
return 'LIST_IGNORE';
}
uploadRef.current += 1;
const suffix = file.name?.slice(file.name.lastIndexOf('.'));
const filename = `${Date.now()}${uploadRef.current}${suffix}`;
file.key = oSSData.dir + filename; // eslint-disable-line
if (file.size / 1024 / 1024 < 10) {
commonUpload(file);
} else {
multipart(file);
}
};
function uploadOss() {
if (!oSSData?.accessId) {
return;
}
const { files } = document.getElementById(openFileDomRef.current);
const fileList = Array.from(files);
uploadStart && uploadStart();
message.success('开始上传...');
for (let i = 0; i < fileList.length; i++) {
beforeUpload(fileList[i]);
}
}
useEffect(() => {
initOssClient();
}, []);
function openFile() {
const fileDom = fileDomList?.[fileDomList.length - 1];
document.getElementById(fileDom)?.click();
openFileDomRef.current = fileDom;
setFileDomList([...fileDomList, `fileDom_${fileDomList.length}`]);
}
return (
<div className="oss-upload-file-container">
<div id="fileDomList" style={{ display: 'none' }}>
{fileDomList?.map((one) => (
<input
type="file"
accept={accept}
disabled={uploadDisabled}
id={one}
key={one}
onChange={uploadOss}
multiple
/>
))}
</div>
<Space>
<span onClick={openFile}>{uploadDom}</span>
{addonAfter && <Typography.Text type="secondary">{addonAfter}</Typography.Text>}
</Space>
{listRenderFunc ? (
listRenderFunc(showList, uploadPercentMap)
) : (
<div className="show-list">
{showList?.map((one) => {
const percent = numeral(uploadPercentMap[one.key] * 100).format('0.0');
return (
<div key={one.key} className="file-item-box">
<div className="file-item">
<div className="operate-action">
{one.status === StatusTypeEnum.DONE ? (
<DeleteOutlined className="operate-icon" onClick={() => removeImg(one.key)} />
) : null}
</div>
{!one.url && one.type === UploadMethodType.MULTIPART ? (
<span>
<Progress percent={percent} width={80} type="circle" />
</span>
) : (
<span className="img-container">
<img
id={`js-img-${one.key}`}
src={one.url}
key={one.url || defaultImage}
width={80}
height={80}
/>
</span>
)}
</div>
<div>
<Typography.Text ellipsis={{ tooltip: one.name }} style={{ width: 100 }}>
{one.name}
</Typography.Text>
</div>
</div>
);
})}
</div>
)}
</div>
);
};
OssUploadFile.defaultProps = {
accept: '.png, .jpg, .jpeg',
limit: 15,
fileLimit: 1024 * 4,
fileNum: 99,
uploadDom: <Button>上传文件</Button>,
addonAfter: `图片大小不超过15MB, 文件大小不超过4GB`,
showName: false,
fileDomId: 'fileDom',
};
export default OssUploadFile;