上传组件封装
/**
* OSS 上传组件
* props = {
* stsUrl: String sts 接口地址,
* accept: String 接受上传的文件类型,
* multiple: Boolean 是否支持多选
* validator: (nowFile, nowFileList) => boolean | { passed: boolean, msg: String } 自定义校验方法,不同于 form 的 rules, 这个验证发生在上传前
* limitedNum: Num 限制的文件数
*
* value: Array 受控的 value 属性,值就是当前的文件列表
* onChange: (newFileList) => {} 组件事件监听
* }
*/
import React, { Component } from 'react';
import { Upload, Modal, Button, Icon, message } from 'antd';
import ImgCrop from 'antd-img-crop';
import OSS from 'ali-oss';
import fetchUtil from '../../utils/fetch-case';
import formatParams from '../../utils/formatParams';
class OSSUpload extends Component {
constructor(props) {
super(props);
this.state = {
stsOptions: null, // 当前文件秘钥对象
value: [], // 文件列表
previewVisible: false, // 图片预览关闭
previewImage: '', // 图片预览地址
};
this.uploadingClients = [];// 上传的客户端
}
// 文件列表
static getDerivedStateFromProps(props) {
if ('value' in props) {
const value = props.value;
return {
value,
};
}
return null;
}
// 提供当前文件列表给form组件
triggerChange = (newValue) => {
const { onChange } = this.props;
if (onChange) {
onChange(newValue);
}
};
// uploading done error都会调用这个函数
onUploadChange = (changeObj) => {
console.log(changeObj, 'changeObj');
const { fileList } = changeObj;// fileList
const tmpFileList = fileList.filter((item) => {
return item.status;// 状态有:uploading done error removed
});
this.setState({
value: [...tmpFileList],
});
this.triggerChange([...tmpFileList]);
};
// 文件上传前的钩子
beforeUpload = (nowFile, nowFileList) => {
console.log(nowFile, nowFileList, '上传前');// nowFile当前上传文件,nowFileList当前上传列表
const { limitedNum, MAX_FILE_SIZE_TEXT, MAX_FILE_SIZE, type, accept } = this.props;
const { value } = this.state;
const alreadyFileListNum = Array.isArray(value) ? value.length : 0;
const nowFileListNum = nowFileList.length;
// 文件数目上限校验(已有个数+当前上传个数)
if (typeof limitedNum === 'number' && (nowFileListNum + alreadyFileListNum) > limitedNum) {
message.error(`上传失败, 原因是:超过最大文件数据上限。最大上传个数为:${limitedNum}`);
return false;
}
// 文件类型判断(accept是否包含当前文件type)
const typeList = accept.split(',').map(m => m = `image/${m.substr(1)}`);
const typeReg = type === 'picture' ? typeList.indexOf(nowFile.type) === -1 : nowFile.type.indexOf('application/') !== 0;
if (typeReg) {
message.error('该文件不是期待的类型');
return false;
}
// 文件大小判断(单位是b)
if (MAX_FILE_SIZE && nowFile.size > MAX_FILE_SIZE) {
message.error(`${nowFile.name} 文件大小超过上限,最大文件大小为: ${MAX_FILE_SIZE_TEXT}`);
return false;
}
// // 自定义校验
// const isPassed = validator(nowFile, nowFileList);
// if (typeof isPassed === 'boolean') {
// if (!isPassed) {
// Modal.error({
// title: '上传失败',
// content: '原因是:不符合文件校验规则。',
// });
// return false;
// }
// } else if (!isPassed.passed) {
// Modal.error({
// title: '上传失败',
// content: `原因是:不符合文件校验规则 -- ${isPassed.msg}`,
// });
// return false;
// }
return true;
};
// 自定义上传操作,上传到ali-oss(获取秘钥)
uploadFile = (customObj) => {
console.log(customObj);
const { file } = customObj;
const self = this;
const { stsUrl, stsUrlType } = self.props;
if (!stsUrl) {
return false;
}
fetchUtil.get(stsUrl, formatParams(stsUrlType), (data) => {
self.setState({
stsOptions: { ...data }, // 图片秘钥
});
self.doOSSUpload(file, { ...data });
}, (msg) => {
self.setState({
stsOptions: null,
});
Modal.error({
title: 'Sorry',
content: `获取上传鉴权失败,原因是:${msg}`,
});
self.onError(msg, null, file);
});
};
// 获取etag
doOSSUpload = (file, stsOptions) => {
const self = this;
const index1 = file.name.lastIndexOf('.');
const index2 = file.name.length;
const realName = `${file.name.substring(0, index1)}${Date.parse(new Date())}${file.name.substring(index1, index2)}`;
console.log(realName, 'realName');// 包含时间戳的文件名
const objectName = `${stsOptions.dir}/${realName}`;
self.uploadingClients.push({
client: new OSS({
region: 'oss-cnnnn-beijing',
accessKeyId: stsOptions.AccessKeyId,
accessKeySecret: stsOptions.AccessKeySecret,
stsToken: stsOptions.SecurityToken,
endpoint: stsOptions.endpoint,
expireTime: stsOptions.Expiration,
bucket: stsOptions.bucket,
cdnDomain: stsOptions.cdnDomain,
}),
// 阿里云 object-name
objectName,
// 阿里云 bucket 下的子目录
dir: stsOptions.dir,
// 枚举类型: -2 -- 初始化, -1 -- 上传中, 0 -- 上传成功, 1 -- 上传失败
status: -1,
// 进度 0 ~ 1
progress: 0,
// 展示信息
msg: '',
// 保存的 url
url: '',
});
// 上传
const uploadingIndex = self.uploadingClients.length - 1;
const uploadingObj = self.uploadingClients[uploadingIndex];
uploadingObj.client.multipartUpload(objectName, file).then((result) => {
console.log(result, 'result');// etag
self.onSuccess({ ...result, dir: stsOptions.dir, realname: realName }, file);
}).catch((e) => {
self.onError(e, null, file);
});
};
// 成功(更新 fileList中当前文件uid/status/path/name)
onSuccess = (response, fileObj) => {
const { stsOptions, value } = this.state;
const path = `https://${stsOptions.cdnDomain}${response.dir}/${encodeURIComponent(response.realname)}`;
const targetUid = fileObj.uid;
const tmpFileList = [...value];
const len = tmpFileList.length;
for (let i = 0; i < len; i += 1) {
if (tmpFileList[i].uid === targetUid) {
tmpFileList[i].status = 'done';
tmpFileList[i].path = path;
tmpFileList[i].objectName = response.name;
break;
}
}
this.setState({
value: tmpFileList,
});
this.triggerChange([...tmpFileList]);
};
/**
* 失败(更新 fileList的status = 'error')
* @param e Error 对象
* @param response 具体响应
* @param fileObj 当前事件对应的文件对象
*/
onError = (e, response, fileObj) => {
const { value } = this.state;
const targetUid = fileObj.uid;
const tmpFileList = [...value];
const len = tmpFileList.length;
for (let i = 0; i < len; i += 1) {
if (tmpFileList[i].uid === targetUid) {
tmpFileList[i].status = 'error';
break;
}
}
this.setState({
value: tmpFileList,
});
this.triggerChange([...tmpFileList]);
// 提示
Modal.error({
title: 'Sorry',
content: `${fileObj.name}上传失败,原因是:${e.message ? e.message : e}`,
});
};
// 移除文件
onRemove = (fileObj) => {
const { handleRemove } = this.props;
this.removeTargetFileFromFileList(fileObj);
if (handleRemove) handleRemove();
};
// 从 state 中的 fileList 中移除指定的 file
removeTargetFileFromFileList = (fileObj) => { // filter当前id
const targetUid = fileObj.uid;
const { value } = this.state;
const tmpFileList = value.filter((item) => {
return item.uid !== targetUid;
});
this.setState({
value: tmpFileList,
});
this.triggerChange([...tmpFileList]);
};
// 文件预览
handlePreview = (file) => {
console.log(file, '预览file');
this.setState({
previewImage: file.url || file.thumbUrl,
previewVisible: true,
});
}
// 预览关闭
handleCancel = () => this.setState({ previewVisible: false })
render() {
const { accept, multiple, type, isEnd, limitedNum, cropSettings } = this.props;
const { value, previewVisible, previewImage } = this.state;
const uploadButton = (
<div>
<Icon type="plus" />
<div className="ant-upload-text">Upload</div>
</div>
);
const optionIcon = isEnd ? { showRemoveIcon: false, showDownloadIcon: false } : { showRemoveIcon: true, showDownloadIcon: false };
return (
<div
style={{ marginBottom: '4px' }}
className="upload-compoment"
>
{type === 'file' ? (// 文件上传
<Upload
accept={accept}
multiple={multiple}
fileList={value}
beforeUpload={this.beforeUpload}
customRequest={this.uploadFile}
onRemove={this.onRemove}
onChange={this.onUploadChange}
>
<Button>
<Icon type="upload" /> 点击浏览文件
</Button>
</Upload>
) : (// 图片裁剪
<div>
{cropSettings ? (
<ImgCrop
{...cropSettings}
resize={false}
beforeCrop={this.beforeUpload}
>
<Upload
listType="picture-card"
accept={accept}
multiple={multiple}
fileList={value}
beforeUpload={this.beforeUpload}
customRequest={this.uploadFile}
onRemove={this.onRemove}
onChange={this.onUploadChange}
onPreview={this.handlePreview}
showUploadList={optionIcon}
>
{(value.length >= limitedNum || isEnd) ? null : uploadButton}
</Upload>
</ImgCrop>
) : (// 图片列表
<Upload
listType="picture-card"// 列表样式
accept={accept}// 文件类型:.jpg,.jpeg,.png
multiple={multiple}// 是否支持多选文件
fileList={value}// 已经上传的文件列表
beforeUpload={this.beforeUpload}// 上传文件之前的钩子
customRequest={this.uploadFile}// 覆盖默认的上传行为
onRemove={this.onRemove}// 点击移除文件时的回调
onChange={this.onUploadChange}// 上传文件改变时的状态
onPreview={this.handlePreview}// 点击文件链接或预览图标时的回调
showUploadList={optionIcon}
>
{(value.length >= limitedNum || isEnd) ? null : uploadButton}
</Upload>
)}
<Modal visible={previewVisible} footer={null} onCancel={this.handleCancel}>
<img alt="example" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
)}
</div>
);
}
}
OSSUpload.defaultProps = {
stsUrl: '',
accept: '*/*',
multiple: false,
validator: () => true,
limitedNum: undefined,
onChange: null,
};
export default OSSUpload;
上传组件调用
import { Form, Modal, message } from 'antd';
import React from 'react';
import OSSUpload from '../../../../components/oss-upload';
import { Interfaces } from '../../../../service/serverConfig';
import FetchUtils from '../../../../utils/fetch-case';
const FormItem = Form.Item;
class PicturesWall extends React.Component {
constructor(props) {
super(props);
this.state = {
urlInfo: [],//图片的回显值
};
}
// 校验
validatorFileList = (rule, value, callback) => {
console.log(value, '校验value');
if (!value || !value[0]) {
// 因为 rule 里面定义了 require 规则,因此这里不需要 callback 内容
callback();
return true;
}
if (value[0].status === 'uploading') {
callback('当前选择的文件尚未上传成功,请稍候');
return false;
}
if (value[0].status === 'error') {
callback('当前选择的文件上传失败,请重新选择文件重试');
return false;
}
callback();
return true;
};
// 提交
subPic = () => {
const { id, uploadType, form, setUploadVisible } = this.props;
const { urlInfo } = this.state;
const picList = form.getFieldValue('picList');
const path = picList.map(item => item.path);// 所有的图片
console.log(path);
// 路径求差集(编辑后新增的图片)
const urlInfoTransferPath = [...urlInfo].map(item => item.path);
const differencePath = new Set([...path].filter(x => urlInfoTransferPath.findIndex(y => y === x) === -1));
const pathArr = [...differencePath];
console.log(differencePath, 'differencePath');
// 删除求差集(编辑后删除的图片)
const urlInfoTransfer = [...urlInfo].map(item => item.uid);
const picListTransfer = [...picList].map(item => item.uid);
const difference = new Set([...urlInfoTransfer].filter(x => picListTransfer.findIndex(y => y === x) === -1));
const delArr = [...difference];
const params = {
bc_id: id,
type: uploadType,
path: urlInfo.length > 0 ? JSON.stringify(pathArr) : JSON.stringify(path),
del_ids: delArr.join(),
};
FetchUtils.post(Interfaces.businessClassUploadLeave, params, (data) => {
setUploadVisible(false);
}, (msg) => {
message.error(msg);
});
}
// 回显(更新fileList和form的值)
componentDidMount () {
const { id, uploadType, form } = this.props;
const params = {
bc_id: id,
type: uploadType,
};
FetchUtils.get(Interfaces.businessClassLeaveList, params, (data) => {
const urlInfoRes = [...data].map((item) => {
return { uid: item.id, status: 'done', url: item.path, path: item.path };
});
form.setFieldsValue({ picList: urlInfoRes });
this.setState({ urlInfo: urlInfoRes });
}, (msg) => {
message.error(msg);
});
}
render () {
const { form, uploadType, setUploadVisible } = this.props;
const { getFieldDecorator } = form;
return (
<Modal
title="添加"
width={610}
visible
onCancel={() => setUploadVisible(false)}
onOk={this.subPic}
okText="提交"
>
<div className="clearfix">
<Form>
<FormItem label={uploadType === 1 ? '上传考勤表' : '上传请假表'}>
{getFieldDecorator('picList', {
initialValue: [],
rules: [{
validator: this.validatorFileList,
}],
})(
<OSSUpload
stsUrl={Interfaces.uploadToken}// 接口
accept=".jpg,.jpeg,.png"
stsUrlType={{ type: 'image' }}// 接口请求参数
limitedNum={2}
type="picture"// 组件类型
MAX_FILE_SIZE={5 * 1024 * 1024}
MAX_FILE_SIZE_TEXT="5M"
multiple
/>,
)}
</FormItem>
<FormItem label="">
<div style={{ color: '#999' }}>图片格式为:png、jpg、jpeg,最多上传50张图片,每张图片最大不超过5MB</div>
</FormItem>
</Form>
</div>
</Modal>
);
}
}
export default Form.create()(PicturesWall);