2020-09-07 17:48:39 +08:00

583 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { func, obj } from '../util';
import Icon from '../icon';
import Base from './base';
import Uploader from './runtime/index';
import html5Uploader from './runtime/html5-uploader';
import List from './list';
import { fileToObject, getFileItem, errorCode } from './util';
const { noop } = func;
/**
* Upload
*/
class Upload extends Base {
static displayName = 'Upload';
static propTypes = {
...html5Uploader.propTypes,
...List.propTypes,
/**
* 样式前缀
*/
prefix: PropTypes.string.isRequired,
/**
* 上传的地址
*/
action: PropTypes.string,
/**
* 文件列表
*/
value: PropTypes.array,
/**
* 默认文件列表
*/
defaultValue: PropTypes.array,
/**
* 上传按钮形状
*/
shape: PropTypes.oneOf(['card']),
/**
* 上传列表的样式
* @enumdesc 文字, 图文, 卡片
*/
listType: PropTypes.oneOf(['text', 'image', 'card']),
list: PropTypes.any,
/**
* 文件名字段
*/
name: PropTypes.string,
/**
* 上传额外传参
*/
data: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
/**
* 数据格式化函数,配合自定义 action 使用,参数为服务器的响应数据,详见 [formatter](#formater)
* @param {Object} response 返回
* @param {File} file 文件对象
*/
formatter: PropTypes.func,
/**
* 最大文件上传个数
*/
limit: PropTypes.number,
/**
* 设置上传超时,单位ms
*/
timeout: PropTypes.number,
/**
* 可选参数,是否支持拖拽上传,`ie10+` 支持。
*/
dragable: PropTypes.bool,
closable: PropTypes.bool,
/**
* 可选参数,是否本地预览
*/
useDataURL: PropTypes.bool,
/**
* 可选参数,是否禁用上传功能
*/
disabled: PropTypes.bool,
/**
* 选择文件回调
*/
onSelect: PropTypes.func,
/**
* 上传中
*/
onProgress: PropTypes.func,
/**
* 上传文件改变时的状态
* @param {Object} info 文件事件对象
*/
onChange: PropTypes.func,
/**
* 可选参数,上传成功回调函数,参数为请求下响应信息以及文件
* @param {Object} file 文件
* @param {Array<Object>} value 值
*/
onSuccess: PropTypes.func,
/**
* 可选参数, 用于校验文件,afterSelect仅在 autoUpload=false 的时候生效,autoUpload=true时,可以使用beforeUpload完全可以替代该功能.
* @param {Object} file
* @returns {Boolean} 返回false会阻止上传,其他则表示正常
*/
afterSelect: PropTypes.func,
/**
* 移除文件回调函数
* @param {Object} file 文件
* @returns {Boolean|Promise} 返回 false、Promise.resolve(false)、 Promise.reject() 将阻止文件删除
*/
onRemove: PropTypes.func,
/**
* 可选参数,上传失败回调函数,参数为上传失败的信息、响应信息以及文件
* @param {Object} file 出错的文件
* @param {Array} value 当前值
*/
onError: PropTypes.func,
/**
* 可选参数, 详见 [beforeUpload](#beforeUpload)
* @param {Object} file 所有文件
* @param {Object} options 参数
* @returns {Boolean|Object|Promise} 返回值作用见demo
*/
beforeUpload: PropTypes.func,
/**
* 放文件
*/
onDrop: PropTypes.func,
/**
* 自定义class
*/
className: PropTypes.string,
/**
* 自定义内联样式
*/
style: PropTypes.object,
/**
* 子元素
*/
children: PropTypes.node,
/**
* 自动上传
*/
autoUpload: PropTypes.bool,
/**
* 自定义上传方法
* @param {Object} option
* @return {Object} object with abort method
*/
request: PropTypes.func,
/**
* 透传给Progress props
*/
progressProps: PropTypes.object,
rtl: PropTypes.bool,
/**
* 是否为预览态
*/
isPreview: PropTypes.bool,
/**
* 预览态模式下渲染的内容
* @param {number} value 评分值
*/
renderPreview: PropTypes.func,
};
static defaultProps = {
...html5Uploader.defaultProps,
prefix: 'next-',
limit: Infinity,
autoUpload: true,
closable: true,
onSelect: noop,
onProgress: noop,
onChange: noop,
onSuccess: noop,
onRemove: noop,
onError: noop,
onDrop: noop,
beforeUpload: noop,
afterSelect: noop,
};
constructor(props) {
super(props);
let value;
if ('value' in props) {
value = props.value;
} else {
value = props.defaultValue;
}
this.state = {
value: typeof value === 'undefined' ? [] : [].concat(value),
};
this.uploading = false;
}
componentWillReceiveProps(nextProps) {
if ('value' in nextProps && !this.uploading) {
this.setState({
value:
typeof nextProps.value === 'undefined'
? []
: [].concat(nextProps.value),
});
}
}
onSelect = files => {
const { autoUpload, afterSelect, onSelect, limit } = this.props;
// 总数
const total = this.state.value.length + files.length;
// 差额
const less = limit - this.state.value.length;
if (less <= 0) {
// 差额不足 则不上传
return;
}
const fileList = files.map(file => {
const objFile = fileToObject(file);
objFile.state = 'selected';
return objFile;
});
// 默认全量上传
let uploadFiles = fileList;
let discardFiles = [];
if (total > limit) {
// 全量上传总数会超过limit 但是 还有差额
uploadFiles = fileList.slice(0, less);
discardFiles = fileList.slice(less);
}
const value = this.state.value.concat(fileList);
/* eslint-disable-next */
this.state.value = value;
if (autoUpload) {
this.uploadFiles(uploadFiles);
}
onSelect(uploadFiles, value);
discardFiles.forEach(file => {
// 丢弃的文件
const err = new Error(errorCode.EXCEED_LIMIT);
err.code = errorCode.EXCEED_LIMIT;
this.onError(err, null, file);
});
if (!autoUpload) {
uploadFiles.forEach(file => {
const isPassed = afterSelect(file);
func.promiseCall(isPassed, func.noop, error => {
this.onError(error, null, file); // TODO: handle error message
});
});
this.onChange(value, uploadFiles);
}
};
onDrop = files => {
this.onSelect(files);
this.props.onDrop(files);
};
/**
* 对外暴露API, 添加文件
* @param files
*/
selectFiles(files) {
const filesArr = files.length
? Array.prototype.slice.call(files)
: [files];
this.onSelect(filesArr);
}
uploadFiles(files) {
// NOTE: drag上传当鼠标松开的时候回执行 onDrop但此时onChange还没出发所以 value=[], 必须提前标识上传中
this.uploading = true;
const fileList = files
.filter(file => {
if (file.state === 'selected') {
file.state = 'uploading';
return true;
}
return false;
})
.map(file => {
return file.originFileObj;
});
fileList.length && this.uploaderRef.startUpload(fileList);
}
/**
* 对外暴露api控制文件上传
*/
startUpload() {
this.uploadFiles(this.state.value);
}
replaceFiles(old, current) {
const targetItem = getFileItem(old, this.state.value);
if (!targetItem) {
return;
}
current.uid = old.uid;
targetItem.originFileObj = current;
}
isUploading() {
return this.uploading;
}
onProgress = (e, file) => {
this.uploading = true;
const { value } = this.state;
const targetItem = getFileItem(file, value);
if (!targetItem) {
return;
}
Object.assign(targetItem, {
state: 'uploading',
percent: e.percent,
});
this.setState({
value,
});
this.props.onProgress(value, targetItem);
};
onSuccess = (response, file) => {
this.uploading = false;
const { formatter } = this.props;
if (formatter) {
response = formatter(response, file);
}
try {
if (typeof response === 'string') {
response = JSON.parse(response);
}
} catch (e) {
e.code = errorCode.RESPONSE_FAIL;
return this.onError(e, response, file);
}
if (response.success === false) {
const err = new Error(response.message || errorCode.RESPONSE_FAIL);
err.code = errorCode.RESPONSE_FAIL;
return this.onError(err, response, file);
}
const { value } = this.state;
const targetItem = getFileItem(file, value);
if (!targetItem) {
return;
}
Object.assign(targetItem, {
state: 'done',
response,
url: response.url,
downloadURL: response.downloadURL || response.url, // 下载地址(可选)
});
if (!this.props.useDataURL) {
targetItem.imgURL = response.imgURL || response.url; // 缩略图地址(可选)
}
this.onChange(value, targetItem);
this.props.onSuccess(targetItem, value);
};
onError = (err, response, file) => {
this.uploading = false;
const { value } = this.state;
const targetItem = getFileItem(file, value);
if (!targetItem) {
return;
}
Object.assign(targetItem, {
state: 'error',
error: err,
response,
});
this.onChange(value, targetItem);
this.props.onError(targetItem, value);
};
/**
* 删除文件
* @param {File} file
* @return {void}
*/
removeFile = file => {
file.state = 'removed';
this.uploaderRef.abort(file); // 删除组件时调用组件的 `abort` 方法中断上传
const fileList = this.state.value;
const targetItem = getFileItem(file, fileList);
const index = fileList.indexOf(targetItem);
if (index !== -1) {
fileList.splice(index, 1);
this.onChange(fileList, targetItem);
}
};
/**
* 取消上传
* @param {File} file
* @return {void}
*/
abort = file => {
const fileList = this.state.value;
const targetItem = getFileItem(file, fileList);
const index = fileList.indexOf(targetItem);
if (index !== -1) {
fileList.splice(index, 1);
this.onChange(fileList, targetItem);
}
this.uploaderRef.abort(file); // 取消上传时调用组件的 `abort` 方法中断上传
};
onChange = (value, file) => {
this.setState({
value,
});
this.props.onChange(value, file);
};
render() {
const {
listType,
prefix,
dragable,
shape,
className,
style,
useDataURL,
disabled,
limit,
closable,
beforeUpload,
readonly,
onRemove,
onCancel,
onPreview,
list,
extraRender,
progressProps,
rtl,
isPreview,
renderPreview,
...others
} = this.props;
const cls = classNames({
[`${prefix}upload`]: true,
[`${prefix}upload-dragable`]: dragable,
[`${prefix}disabled`]: disabled,
[`${prefix}readonly`]: readonly,
[className]: className,
});
const isExceedLimit = this.state.value.length >= limit;
const innerCls = classNames({
[`${prefix}upload-inner`]: true,
[`${prefix}hidden`]: isExceedLimit,
});
let { children } = this.props;
if (shape === 'card') {
const cardCls = classNames({
[`${prefix}upload-card`]: true,
[`${prefix}disabled`]: disabled,
});
children = (
<div className={cardCls}>
<Icon type="add" size="large" />
<div
tabIndex="0"
role="button"
className={`${prefix}upload-text`}
>
{children}
</div>
</div>
);
}
if (isPreview) {
if (typeof renderPreview === 'function') {
const previewCls = classNames({
[`${prefix}form-preview`]: true,
[className]: !!className,
});
return (
<div style={style} className={previewCls}>
{renderPreview(this.state.value, this.props)}
</div>
);
}
if (listType) {
return (
<List
isPreview
listType={listType}
style={style}
className={className}
value={this.state.value}
/>
);
}
return null;
}
// disabled 状态下把 remove函数替换成禁止 remove的函数
const onRemoveFunc = disabled ? func.prevent : onRemove;
const otherAttributes = obj.pickAttrsWith(this.props, 'data-');
return (
<div className={cls} style={style} {...otherAttributes}>
<Uploader
{...others}
beforeUpload={beforeUpload}
dragable={dragable}
disabled={disabled || isExceedLimit}
className={innerCls}
onSelect={this.onSelect}
onDrop={this.onDrop}
onProgress={this.onProgress}
onSuccess={this.onSuccess}
onError={this.onError}
ref={this.saveUploaderRef}
>
{children}
</Uploader>
{listType || list ? (
<List
useDataURL={useDataURL}
uploader={this}
listType={listType}
value={this.state.value}
closable={closable}
onRemove={onRemoveFunc}
progressProps={progressProps}
onCancel={onCancel}
onPreview={onPreview}
extraRender={extraRender}
rtl={rtl}
/>
) : null}
</div>
);
}
}
export default Upload;