feat: add download transformFile previewFile actio

pull/1845/head
tangjinzhou 2020-02-23 20:18:20 +08:00
parent 3e8e90da5e
commit 548595959d
19 changed files with 619 additions and 250 deletions

View File

@ -1,5 +1,5 @@
module.exports = {
dev: {
componentName: 'select', // dev components
componentName: 'upload', // dev components
},
};

View File

@ -1,6 +1,7 @@
import classNames from 'classnames';
import uniqBy from 'lodash/uniqBy';
import findIndex from 'lodash/findIndex';
import pick from 'lodash/pick';
import VcUpload from '../vc-upload';
import BaseMixin from '../_util/BaseMixin';
import { getOptionProps, initDefaultProps, hasProp, getListeners } from '../_util/props-util';
@ -66,25 +67,12 @@ export default {
fileList: nextFileList,
});
// fix ie progress
if (!window.FormData) {
if (!window.File || process.env.TEST_IE) {
this.autoUpdateProgress(0, targetItem);
}
},
autoUpdateProgress(_, file) {
const getPercent = genPercentAdd();
let curPercent = 0;
this.clearProgressTimer();
this.progressTimer = setInterval(() => {
curPercent = getPercent(curPercent);
this.onProgress(
{
percent: curPercent * 100,
},
file,
);
}, 200);
},
onSuccess(response, file) {
onSuccess(response, file, xhr) {
this.clearProgressTimer();
try {
if (typeof response === 'string') {
@ -101,6 +89,7 @@ export default {
}
targetItem.status = 'done';
targetItem.response = response;
targetItem.xhr = xhr;
this.onChange({
file: { ...targetItem },
fileList,
@ -140,19 +129,24 @@ export default {
this.$emit('reject', fileList);
},
handleRemove(file) {
const { remove } = this;
const { status } = file;
file.status = 'removed'; // eslint-disable-line
const { remove: onRemove } = this;
const { sFileList: fileList } = this.$data;
Promise.resolve(typeof remove === 'function' ? remove(file) : remove).then(ret => {
Promise.resolve(typeof onRemove === 'function' ? onRemove(file) : onRemove).then(ret => {
// Prevent removing file
if (ret === false) {
file.status = status;
return;
}
const removedFileList = removeFileItem(file, this.sFileList);
const removedFileList = removeFileItem(file, fileList);
if (removedFileList) {
file.status = 'removed'; // eslint-disable-line
if (this.upload) {
this.upload.abort(file);
}
this.onChange({
file,
fileList: removedFileList,
@ -178,14 +172,16 @@ export default {
});
},
reBeforeUpload(file, fileList) {
if (!this.beforeUpload) {
const { beforeUpload } = this.$props;
const { sFileList: stateFileList } = this.$data;
if (!beforeUpload) {
return true;
}
const result = this.beforeUpload(file, fileList);
const result = beforeUpload(file, fileList);
if (result === false) {
this.onChange({
file,
fileList: uniqBy(this.sFileList.concat(fileList.map(fileToObject)), item => item.uid),
fileList: uniqBy(stateFileList.concat(fileList.map(fileToObject)), item => item.uid),
});
return false;
}
@ -197,25 +193,45 @@ export default {
clearProgressTimer() {
clearInterval(this.progressTimer);
},
autoUpdateProgress(_, file) {
const getPercent = genPercentAdd();
let curPercent = 0;
this.clearProgressTimer();
this.progressTimer = setInterval(() => {
curPercent = getPercent(curPercent);
this.onProgress(
{
percent: curPercent * 100,
},
file,
);
}, 200);
},
renderUploadList(locale) {
const { showUploadList = {}, listType } = getOptionProps(this);
const { showRemoveIcon, showPreviewIcon } = showUploadList;
const {
showUploadList = {},
listType,
previewFile,
disabled,
locale: propLocale,
} = getOptionProps(this);
const { showRemoveIcon, showPreviewIcon, showDownloadIcon } = showUploadList;
const { sFileList: fileList } = this.$data;
const uploadListProps = {
props: {
listType,
items: this.sFileList,
showRemoveIcon,
items: fileList,
previewFile,
showRemoveIcon: !disabled && showRemoveIcon,
showPreviewIcon,
locale: { ...locale, ...this.$props.locale },
showDownloadIcon,
locale: { ...locale, ...propLocale },
},
on: {
remove: this.handleManualRemove,
...pick(getListeners(this), ['download', 'preview']), // uploadlist
},
};
const listeners = getListeners(this);
if (listeners.preview) {
uploadListProps.on.preview = listeners.preview;
}
return <UploadList {...uploadListProps} />;
},
},
@ -227,7 +243,7 @@ export default {
type,
disabled,
} = getOptionProps(this);
const { sFileList: fileList, dragState } = this.$data;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('upload', customizePrefixCls);
@ -261,8 +277,8 @@ export default {
if (type === 'drag') {
const dragCls = classNames(prefixCls, {
[`${prefixCls}-drag`]: true,
[`${prefixCls}-drag-uploading`]: this.sFileList.some(file => file.status === 'uploading'),
[`${prefixCls}-drag-hover`]: this.dragState === 'dragover',
[`${prefixCls}-drag-uploading`]: fileList.some(file => file.status === 'uploading'),
[`${prefixCls}-drag-hover`]: dragState === 'dragover',
[`${prefixCls}-disabled`]: disabled,
});
return (
@ -290,7 +306,7 @@ export default {
// Remove id to avoid open by label when trigger is hidden
// https://github.com/ant-design/ant-design/issues/14298
if (!children) {
if (!children || disabled) {
delete vcUploadProps.props.id;
}
@ -302,7 +318,7 @@ export default {
if (listType === 'picture-card') {
return (
<span>
<span class={`${prefixCls}-picture-card-wrapper`}>
{uploadList}
{uploadButton}
</span>

View File

@ -2,51 +2,13 @@ import BaseMixin from '../_util/BaseMixin';
import { getOptionProps, initDefaultProps, getListeners } from '../_util/props-util';
import getTransitionProps from '../_util/getTransitionProps';
import { ConfigConsumerProps } from '../config-provider';
import { previewImage, isImageUrl } from './utils';
import Icon from '../icon';
import Tooltip from '../tooltip';
import Progress from '../progress';
import classNames from 'classnames';
import { UploadListProps } from './interface';
const imageTypes = ['image', 'webp', 'png', 'svg', 'gif', 'jpg', 'jpeg', 'bmp', 'dpg', 'ico'];
// https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL
const previewFile = (file, callback) => {
if (file.type && !imageTypes.includes(file.type)) {
callback('');
}
const reader = new window.FileReader();
reader.onloadend = () => callback(reader.result);
reader.readAsDataURL(file);
};
const extname = url => {
if (!url) {
return '';
}
const temp = url.split('/');
const filename = temp[temp.length - 1];
const filenameWithoutSuffix = filename.split(/#|\?/)[0];
return (/\.[^./\\]*$/.exec(filenameWithoutSuffix) || [''])[0];
};
const isImageUrl = file => {
if (imageTypes.includes(file.type)) {
return true;
}
const url = file.thumbUrl || file.url;
const extension = extname(url);
if (/^data:image\//.test(url) || /(webp|svg|png|gif|jpg|jpeg|bmp|dpg|ico)$/i.test(extension)) {
return true;
} else if (/^data:/.test(url)) {
// other file types of base64
return false;
} else if (extension) {
// other file types which have extension
return false;
}
return true;
};
export default {
name: 'AUploadList',
mixins: [BaseMixin],
@ -57,43 +19,43 @@ export default {
showInfo: false,
},
showRemoveIcon: true,
showDownloadIcon: true,
showPreviewIcon: true,
previewFile: previewImage,
}),
inject: {
configProvider: { default: () => ConfigConsumerProps },
},
updated() {
this.$nextTick(() => {
if (this.listType !== 'picture' && this.listType !== 'picture-card') {
const { listType, items, previewFile } = this.$props;
if (listType !== 'picture' && listType !== 'picture-card') {
return;
}
(this.items || []).forEach(file => {
(items || []).forEach(file => {
if (
typeof document === 'undefined' ||
typeof window === 'undefined' ||
!window.FileReader ||
!window.File ||
!(file.originFileObj instanceof window.File) ||
!(file.originFileObj instanceof File || file.originFileObj instanceof Blob) ||
file.thumbUrl !== undefined
) {
return;
}
/*eslint-disable */
file.thumbUrl = '';
/*eslint -enable */
previewFile(file.originFileObj, previewDataUrl => {
// Need append '' to avoid dead loop
file.thumbUrl = previewDataUrl || '';
/*eslint -enable */
this.$forceUpdate();
});
if (previewFile) {
previewFile(file.originFileObj).then(previewDataUrl => {
// Need append '' to avoid dead loop
file.thumbUrl = previewDataUrl || '';
this.$forceUpdate();
});
}
});
});
},
methods: {
handleClose(file) {
this.$emit('remove', file);
},
handlePreview(file, e) {
const { preview } = getListeners(this);
if (!preview) {
@ -102,6 +64,18 @@ export default {
e.preventDefault();
return this.$emit('preview', file);
},
handleDownload(file) {
const { download } = getListeners(this);
if (typeof download === 'function') {
download(file);
} else if (file.url) {
window.open(file.url);
}
},
handleClose(file) {
this.$emit('remove', file);
},
},
render() {
const {
@ -110,7 +84,9 @@ export default {
listType,
showPreviewIcon,
showRemoveIcon,
showDownloadIcon,
locale,
progressAttr,
} = getOptionProps(this);
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('upload', customizePrefixCls);
@ -126,7 +102,11 @@ export default {
icon = <Icon class={`${prefixCls}-list-item-thumbnail`} type="picture" theme="twoTone" />;
} else {
const thumbnail = isImageUrl(file) ? (
<img src={file.thumbUrl || file.url} alt={file.name} />
<img
src={file.thumbUrl || file.url}
alt={file.name}
class={`${prefixCls}-list-item-image`}
/>
) : (
<Icon type="file" class={`${prefixCls}-list-item-icon`} theme="twoTone" />
);
@ -147,7 +127,7 @@ export default {
if (file.status === 'uploading') {
const progressProps = {
props: {
...this.progressAttr,
...progressAttr,
type: 'line',
percent: file.percent,
},
@ -164,30 +144,64 @@ export default {
const infoUploadingClass = classNames({
[`${prefixCls}-list-item`]: true,
[`${prefixCls}-list-item-${file.status}`]: true,
[`${prefixCls}-list-item-list-type-${listType}`]: true,
});
const linkProps =
typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps;
const preview = file.url ? (
<a
target="_blank"
rel="noopener noreferrer"
class={`${prefixCls}-list-item-name`}
title={file.name}
{...linkProps}
href={file.url}
onClick={e => this.handlePreview(file, e)}
>
{file.name}
</a>
) : (
const removeIcon = showRemoveIcon ? (
<Icon type="delete" title={locale.removeFile} onClick={() => this.handleClose(file)} />
) : null;
const downloadIcon =
showDownloadIcon && file.status === 'done' ? (
<Icon
type="download"
title={locale.downloadFile}
onClick={() => this.handleDownload(file)}
/>
) : null;
const downloadOrDelete = listType !== 'picture-card' && (
<span
class={`${prefixCls}-list-item-name`}
onClick={e => this.handlePreview(file, e)}
title={file.name}
key="download-delete"
class={`${prefixCls}-list-item-card-actions ${listType === 'picture' ? 'picture' : ''}`}
>
{file.name}
{downloadIcon && <a title={locale.downloadFile}>{downloadIcon}</a>}
{removeIcon && <a title={locale.removeFile}>{removeIcon}</a>}
</span>
);
const listItemNameClass = classNames({
[`${prefixCls}-list-item-name`]: true,
[`${prefixCls}-list-item-name-icon-count-${
[downloadIcon, removeIcon].filter(x => x).length
}`]: true,
});
const preview = file.url
? [
<a
target="_blank"
rel="noopener noreferrer"
class={listItemNameClass}
title={file.name}
{...linkProps}
href={file.url}
onClick={e => this.handlePreview(file, e)}
>
{file.name}
</a>,
downloadOrDelete,
]
: [
<span
key="view"
class={`${prefixCls}-list-item-name`}
onClick={e => this.handlePreview(file, e)}
title={file.name}
>
{file.name}
</span>,
downloadOrDelete,
];
const style =
file.url || file.thumbUrl
? undefined
@ -207,55 +221,41 @@ export default {
<Icon type="eye-o" />
</a>
) : null;
const iconProps = {
props: {
type: 'delete',
title: locale.removeFile,
},
on: {
click: () => {
this.handleClose(file);
},
},
};
const iconProps1 = { ...iconProps, ...{ props: { type: 'close' } } };
const removeIcon = showRemoveIcon ? <Icon {...iconProps} /> : null;
const removeIconClose = showRemoveIcon ? <Icon {...iconProps1} /> : null;
const actions =
listType === 'picture-card' && file.status !== 'uploading' ? (
<span class={`${prefixCls}-list-item-actions`}>
{previewIcon}
{removeIcon}
</span>
) : (
removeIconClose
);
const actions = listType === 'picture-card' && file.status !== 'uploading' && (
<span class={`${prefixCls}-list-item-actions`}>
{previewIcon}
{file.status === 'done' && downloadIcon}
{removeIcon}
</span>
);
let message;
if (file.response && typeof file.response === 'string') {
message = file.response;
} else {
message = (file.error && file.error.statusText) || locale.uploadError;
}
const iconAndPreview =
file.status === 'error' ? (
<Tooltip title={message}>
{icon}
{preview}
</Tooltip>
) : (
<span>
{icon}
{preview}
</span>
);
const iconAndPreview = (
<span>
{icon}
{preview}
</span>
);
const transitionProps = getTransitionProps('fade');
return (
const dom = (
<div class={infoUploadingClass} key={file.uid}>
<div class={`${prefixCls}-list-item-info`}>{iconAndPreview}</div>
{actions}
<transition {...transitionProps}>{progress}</transition>
</div>
);
const listContainerNameClass = classNames({
[`${prefixCls}-list-picture-card-container`]: listType === 'picture-card',
});
return (
<div key={file.uid} class={listContainerNameClass}>
{file.status === 'error' ? <Tooltip title={message}>{dom}</Tooltip> : <span>{dom}</span>}
</div>
);
});
const listClassNames = classNames({
[`${prefixCls}-list`]: true,

View File

@ -56,15 +56,15 @@ The return value of function `beforeUpload` can be a Promise to check asynchrono
}
},
beforeUpload(file) {
const isJPG = file.type === 'image/jpeg';
if (!isJPG) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
this.$message.error('You can only upload JPG file!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
this.$message.error('Image must smaller than 2MB!');
}
return isJPG && isLt2M;
return isJpgOrPng && isLt2M;
},
},
};

View File

@ -0,0 +1,46 @@
<cn>
#### 点击上传
经典款式,用户点击按钮弹出文件选择框。
</cn>
<us>
#### Upload by clicking
Classic mode. File selection dialog pops up when upload button is clicked.
</us>
```tpl
<template>
<a-upload
name="file"
:multiple="true"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:headers="headers"
@change="handleChange"
>
<a-button> <a-icon type="upload" /> Click to Upload </a-button>
</a-upload>
</template>
<script>
export default {
data() {
return {
headers: {
authorization: 'authorization-text',
},
};
},
methods: {
handleChange(info) {
if (info.file.status !== 'uploading') {
console.log(info.file, info.fileList);
}
if (info.file.status === 'done') {
this.$message.success(`${info.file.name} file uploaded successfully`);
} else if (info.file.status === 'error') {
this.$message.error(`${info.file.name} file upload failed.`);
}
},
},
};
</script>
```

View File

@ -3,7 +3,6 @@
使用 `fileList` 对列表进行完全控制,可以实现各种自定义功能,以下演示三种情况:
1) 上传列表数量的限制。
2) 读取远程路径并显示链接。
3) 按照服务器返回信息筛选成功上传的文件。
</cn>
<us>
@ -11,7 +10,6 @@
You can gain full control over filelist by configuring `fileList`. You can accomplish all kinds of customed functions. The following shows three circumstances:
1) limit the number of uploaded files.
2) read from response and show file link.
3) filter successfully uploaded files according to response from server.
</us>
```tpl

View File

@ -8,6 +8,8 @@ import Drag from './drag.md';
import PictureStyle from './picture-style.md';
import UploadManually from './upload-manually.md';
import Directory from './directory.md';
import PreviewFile from './preview-file';
import TransformFile from './transform-file';
import CN from '../index.zh-CN.md';
import US from '../index.en-US.md';
@ -52,6 +54,8 @@ export default {
<PictureStyle />
<UploadManually />
<Directory />
<PreviewFile />
<TransformFile />
<api>
<template slot="cn">
<CN />

View File

@ -18,7 +18,7 @@ After users upload picture, the thumbnail will be shown in list. The upload butt
@preview="handlePreview"
@change="handleChange"
>
<div v-if="fileList.length < 3">
<div v-if="fileList.length < 8">
<a-icon type="plus" />
<div class="ant-upload-text">Upload</div>
</div>
@ -29,6 +29,14 @@ After users upload picture, the thumbnail will be shown in list. The upload butt
</div>
</template>
<script>
function getBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});
}
export default {
data() {
return {
@ -37,10 +45,33 @@ After users upload picture, the thumbnail will be shown in list. The upload butt
fileList: [
{
uid: '-1',
name: 'xxx.png',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-2',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-3',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-4',
name: 'image.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-5',
name: 'image.png',
status: 'error',
},
],
};
},
@ -48,8 +79,11 @@ After users upload picture, the thumbnail will be shown in list. The upload butt
handleCancel() {
this.previewVisible = false;
},
handlePreview(file) {
this.previewImage = file.url || file.thumbUrl;
async handlePreview(file) {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
this.previewImage = file.url || file.preview;
this.previewVisible = true;
},
handleChange({ fileList }) {

View File

@ -0,0 +1,39 @@
<cn>
#### 自定义预览
自定义本地预览,用于处理非图片格式文件(例如视频文件)。
</cn>
<us>
#### Customize preview file
Customize local preview. Can handle with non-image format files such as video.
</us>
```tpl
<template>
<div>
<a-upload
listType="picture"
action="//jsonplaceholder.typicode.com/posts/"
:previewFile="previewFile"
>
<a-button> <a-icon type="upload" /> Upload </a-button>
</a-upload>
</div>
</template>
<script>
export default {
methods: {
previewFile(file) {
console.log('Your upload file:', file);
// Your process logic. Here we just mock to the same file
return fetch('https://next.json-generator.com/api/json/get/4ytyBoLK8', {
method: 'POST',
body: file,
})
.then(res => res.json())
.then(({ thumbnail }) => thumbnail);
},
},
};
</script>
```

View File

@ -0,0 +1,48 @@
<cn>
#### 上传前转换文件
使用 `transformFile` 转换上传的文件(例如添加水印)。
</cn>
<us>
#### Transform file before request
Use `transformFile` for transform file before request such as add a watermark.
</us>
```tpl
<template>
<div>
<a-upload
listType="picture"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:transformFile="transformFile"
>
<a-button> <a-icon type="upload" /> Upload </a-button>
</a-upload>
</div>
</template>
<script>
export default {
methods: {
transformFile(file) {
return new Promise(resolve => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const canvas = document.createElement('canvas');
const img = document.createElement('img');
img.src = reader.result;
img.onload = () => {
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
ctx.fillStyle = 'red';
ctx.textBaseline = 'middle';
ctx.fillText('Ant Design', 20, 20);
canvas.toBlob(resolve);
};
};
});
},
},
};
</script>
```

View File

@ -1,33 +1,37 @@
## API
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| accept | File types that can be accepted. See [input accept Attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) | string | - |
| action | Uploading URL | string\|(file) => `Promise` | - |
| directory | support upload whole directory ([caniuse](https://caniuse.com/#feat=input-file-directory)) | boolean | false |
| beforeUpload | Hook function which will be executed before uploading. Uploading will be stopped with `false` or a rejected Promise returned. **Warningthis function is not supported in IE9**。 | (file, fileList) => `boolean | Promise` | - |
| customRequest | override for the default xhr behavior allowing for additional customization and ability to implement your own XMLHttpRequest | Function | - |
| data | Uploading params or function which can return uploading params. | object\|function(file) | - |
| defaultFileList | Default list of files that have been uploaded. | object\[] | - |
| disabled | disable upload button | boolean | false |
| fileList | List of files that have been uploaded (controlled). Here is a common issue [#2423](https://github.com/ant-design/ant-design/issues/2423) when using it | object\[] | - |
| headers | Set request headers, valid above IE10. | object | - |
| listType | Built-in stylesheets, support for three types: `text`, `picture` or `picture-card` | string | 'text' |
| multiple | Whether to support selected multiple file. `IE10+` supported. You can select multiple files with CTRL holding down while multiple is set to be true | boolean | false |
| name | The name of uploading file | string | 'file' |
| showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon` and `showRemoveIcon` individually | Boolean or { showPreviewIcon?: boolean, showRemoveIcon?: boolean } | true |
| supportServerRender | Need to be turned on while the server side is rendering. | boolean | false |
| withCredentials | ajax upload with cookie sent | boolean | false |
| openFileDialogOnClick | click open file dialog | boolean | true |
| remove | A callback function, will be executed when removing file button is clicked, remove event will be prevented when return value is `false` or a Promise which resolve(false) or reject. | Function(file): `boolean | Promise` | - |
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| accept | File types that can be accepted. See [input accept Attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) | string | - | |
| action | Uploading URL | string\|(file) => `Promise` | - | |
| method | http method of upload request | string | 'post' | 1.5.0 |
| directory | support upload whole directory ([caniuse](https://caniuse.com/#feat=input-file-directory)) | boolean | false | |
| beforeUpload | Hook function which will be executed before uploading. Uploading will be stopped with `false` or a rejected Promise returned. **Warningthis function is not supported in IE9**。 | (file, fileList) => `boolean | Promise` | - | |
| customRequest | override for the default xhr behavior allowing for additional customization and ability to implement your own XMLHttpRequest | Function | - | |
| data | Uploading params or function which can return uploading params. | object\|function(file) | - | |
| defaultFileList | Default list of files that have been uploaded. | object\[] | - | |
| disabled | disable upload button | boolean | false | |
| fileList | List of files that have been uploaded (controlled). Here is a common issue [#2423](https://github.com/ant-design/ant-design/issues/2423) when using it | object\[] | - | |
| headers | Set request headers, valid above IE10. | object | - | |
| listType | Built-in stylesheets, support for three types: `text`, `picture` or `picture-card` | string | 'text' | |
| multiple | Whether to support selected multiple file. `IE10+` supported. You can select multiple files with CTRL holding down while multiple is set to be true | boolean | false | |
| name | The name of uploading file | string | 'file' | |
| previewFile | Customize preview file logic | (file: File \| Blob) => Promise<dataURL: string> | - | 1.5.0 |
| showUploadList | Whether to show default upload list, could be an object to specify `showPreviewIcon` and `showRemoveIcon` individually | Boolean or { showPreviewIcon?: boolean, showRemoveIcon?: boolean } | true | |
| supportServerRender | Need to be turned on while the server side is rendering. | boolean | false | |
| withCredentials | ajax upload with cookie sent | boolean | false | |
| openFileDialogOnClick | click open file dialog | boolean | true | |
| remove | A callback function, will be executed when removing file button is clicked, remove event will be prevented when return value is `false` or a Promise which resolve(false) or reject. | Function(file): `boolean | Promise` | - | |
| transformFile   | Customize transform file before request | Function(file): `string | Blob | File | Promise<string | Blob | File>` | - | 1.5.0 |
### events
| Events Name | Description | Arguments |
| --- | --- | --- |
| change | A callback function, can be executed when uploading state is changing. See [change](#change) | Function | - |
| preview | A callback function, will be executed when file link or preview icon is clicked. | Function(file) | - |
| reject | A callback function, will be executed when drop files is not accept. | Function(fileList) | - |
| Events Name | Description | Arguments | Version |
| --- | --- | --- | --- |
| change | A callback function, can be executed when uploading state is changing. See [change](#change) | Function | - | |
| preview | A callback function, will be executed when file link or preview icon is clicked. | Function(file) | - | |
| download | Click the method to download the file, pass the method to perform the method logic, do not pass the default jump to the new TAB. | Function(file): void | Jump to new TAB | 1.5.0 |
| reject | A callback function, will be executed when drop files is not accept. | Function(fileList) | - | |
### change
@ -48,10 +52,11 @@ When uploading state change, it returns:
```js
{
uid: 'uid', // unique identifier, negative is recommend, to prevent interference with internal generated id
name: 'xx.png' // file name
name: 'xx.png', // file name
status: 'done', // optionsuploading, done, error, removed
response: '{"status": "success"}', // response from server
linkProps: '{"download": "image"}', // additional html props of file link
xhr: 'XMLHttpRequest{ ... }', // XMLHttpRequest Header
}
```

View File

@ -1,33 +1,37 @@
## API
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| accept | 接受上传的文件类型, 详见 [input accept Attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) | string | 无 |
| action | 上传的地址 | string\|(file) => `Promise` | 无 |
| directory | 支持上传文件夹([caniuse](https://caniuse.com/#feat=input-file-directory) | boolean | false |
| beforeUpload | 上传文件之前的钩子,参数为上传的文件,若返回 `false` 则停止上传。支持返回一个 Promise 对象Promise 对象 reject 时则停止上传resolve 时开始上传( resolve 传入 `File``Blob` 对象则上传 resolve 传入对象)。**注意IE9 不支持该方法**。 | (file, fileList) => `boolean | Promise` | 无 |
| customRequest | 通过覆盖默认的上传行为,可以自定义自己的上传实现 | Function | 无 |
| data | 上传所需参数或返回上传参数的方法 | object\|(file) => object | 无 |
| defaultFileList | 默认已经上传的文件列表 | object\[] | 无 |
| disabled | 是否禁用 | boolean | false |
| fileList | 已经上传的文件列表(受控) | object\[] | 无 |
| headers | 设置上传的请求头部IE10 以上有效 | object | 无 |
| listType | 上传列表的内建样式,支持三种基本样式 `text`, `picture``picture-card` | string | 'text' |
| multiple | 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件。 | boolean | false |
| name | 发到后台的文件参数名 | string | 'file' |
| showUploadList | 是否展示 uploadList, 可设为一个对象,用于单独设定 showPreviewIcon 和 showRemoveIcon | Boolean or { showPreviewIcon?: boolean, showRemoveIcon?: boolean } | true |
| supportServerRender | 服务端渲染时需要打开这个 | boolean | false |
| withCredentials | 上传请求时是否携带 cookie | boolean | false |
| openFileDialogOnClick | 点击打开文件对话框 | boolean | true |
| remove   | 点击移除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象Promise 对象 resolve(false) 或 reject 时不移除。               | Function(file): `boolean | Promise` | 无   |
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| accept | 接受上传的文件类型, 详见 [input accept Attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) | string | 无 | |
| action | 上传的地址 | string\|(file) => `Promise` | 无 | |
| method | 上传请求的 http method | string | 'post' | 1.5.0 |
| directory | 支持上传文件夹([caniuse](https://caniuse.com/#feat=input-file-directory) | boolean | false | |
| beforeUpload | 上传文件之前的钩子,参数为上传的文件,若返回 `false` 则停止上传。支持返回一个 Promise 对象Promise 对象 reject 时则停止上传resolve 时开始上传( resolve 传入 `File``Blob` 对象则上传 resolve 传入对象)。**注意IE9 不支持该方法**。 | (file, fileList) => `boolean | Promise` | 无 | |
| customRequest | 通过覆盖默认的上传行为,可以自定义自己的上传实现 | Function | 无 | |
| data | 上传所需参数或返回上传参数的方法 | object\|(file) => object | 无 | |
| defaultFileList | 默认已经上传的文件列表 | object\[] | 无 | |
| disabled | 是否禁用 | boolean | false | |
| fileList | 已经上传的文件列表(受控) | object\[] | 无 | |
| headers | 设置上传的请求头部IE10 以上有效 | object | 无 | |
| listType | 上传列表的内建样式,支持三种基本样式 `text`, `picture``picture-card` | string | 'text' | |
| multiple | 是否支持多选文件,`ie10+` 支持。开启后按住 ctrl 可选择多个文件。 | boolean | false | |
| name | 发到后台的文件参数名 | string | 'file' | |
| previewFile | 自定义文件预览逻辑 | (file: File \| Blob) => Promise<dataURL: string> | 无 | 1.5.0 |
| showUploadList | 是否展示 uploadList, 可设为一个对象,用于单独设定 showPreviewIcon 和 showRemoveIcon | Boolean or { showPreviewIcon?: boolean, showRemoveIcon?: boolean } | true | |
| supportServerRender | 服务端渲染时需要打开这个 | boolean | false | |
| withCredentials | 上传请求时是否携带 cookie | boolean | false | |
| openFileDialogOnClick | 点击打开文件对话框 | boolean | true | |
| remove   | 点击移除文件时的回调,返回值为 false 时不移除。支持返回一个 Promise 对象Promise 对象 resolve(false) 或 reject 时不移除。               | Function(file): `boolean | Promise` | 无   | |
| transformFile   | 在上传之前转换文件。支持返回一个 Promise 对象   | Function(file): `string | Blob | File | Promise<string | Blob | File>` | 无   | 1.5.0 |
### 事件
| 事件名称 | 说明 | 回调参数 |
| -------- | -------------------------------------------- | ------------------ |
| change | 上传文件改变时的状态,详见 [change](#change) | Function | 无 |
| preview | 点击文件链接或预览图标时的回调 | Function(file) | 无 |
| reject | 拖拽文件不符合 accept 类型时的回调 | Function(fileList) | 无 |
| 事件名称 | 说明 | 回调参数 | 版本 |
| --- | --- | --- | --- |
| change | 上传文件改变时的状态,详见 [change](#change) | Function | 无 | |
| preview | 点击文件链接或预览图标时的回调 | Function(file) | 无 | |
| download | 点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页。 | Function(file): void | 跳转新标签页 | 1.5.0 |
| reject | 拖拽文件不符合 accept 类型时的回调 | Function(fileList) | 无 | |
### change
@ -48,10 +52,11 @@
```js
{
uid: 'uid', // 文件唯一标识,建议设置为负数,防止和内部产生的 id 冲突
name: 'xx.png' // 文件名
name: 'xx.png', // 文件名
status: 'done', // 状态有uploading done error removed
response: '{"status": "success"}', // 服务端响应内容
linkProps: '{"download": "image"}', // 下载链接额外的 HTML 属性
xhr: 'XMLHttpRequest{ ... }', // XMLHttpRequest Header
}
```

View File

@ -54,6 +54,7 @@ export const ShowUploadListInterface = PropsTypes.shape({
export const UploadLocale = PropsTypes.shape({
uploading: PropsTypes.string,
removeFile: PropsTypes.string,
downloadFile: PropsTypes.string,
uploadError: PropsTypes.string,
previewFile: PropsTypes.string,
}).loose;
@ -66,6 +67,7 @@ export const UploadProps = {
action: PropsTypes.oneOfType([PropsTypes.string, PropsTypes.func]),
directory: PropsTypes.bool,
data: PropsTypes.oneOfType([PropsTypes.object, PropsTypes.func]),
method: PropsTypes.oneOf(['POST', 'PUT', 'post', 'put']),
headers: PropsTypes.object,
showUploadList: PropsTypes.oneOfType([PropsTypes.bool, ShowUploadListInterface]),
multiple: PropsTypes.bool,
@ -86,6 +88,8 @@ export const UploadProps = {
locale: UploadLocale,
height: PropsTypes.number,
id: PropsTypes.string,
previewFile: PropsTypes.func,
transformFile: PropsTypes.func,
};
export const UploadState = {
@ -103,6 +107,8 @@ export const UploadListProps = {
progressAttr: PropsTypes.object,
prefixCls: PropsTypes.string,
showRemoveIcon: PropsTypes.bool,
showDownloadIcon: PropsTypes.bool,
showPreviewIcon: PropsTypes.bool,
locale: UploadLocale,
previewFile: PropsTypes.func,
};

View File

@ -54,3 +54,77 @@ export function removeFileItem(file, fileList) {
}
return removed;
}
// ==================== Default Image Preview ====================
const extname = (url = '') => {
const temp = url.split('/');
const filename = temp[temp.length - 1];
const filenameWithoutSuffix = filename.split(/#|\?/)[0];
return (/\.[^./\\]*$/.exec(filenameWithoutSuffix) || [''])[0];
};
const isImageFileType = type => !!type && type.indexOf('image/') === 0;
export const isImageUrl = file => {
if (isImageFileType(file.type)) {
return true;
}
const url = file.thumbUrl || file.url;
const extension = extname(url);
if (
/^data:image\//.test(url) ||
/(webp|svg|png|gif|jpg|jpeg|jfif|bmp|dpg|ico)$/i.test(extension)
) {
return true;
}
if (/^data:/.test(url)) {
// other file types of base64
return false;
}
if (extension) {
// other file types which have extension
return false;
}
return true;
};
const MEASURE_SIZE = 200;
export function previewImage(file) {
return new Promise(resolve => {
if (!isImageFileType(file.type)) {
resolve('');
return;
}
const canvas = document.createElement('canvas');
canvas.width = MEASURE_SIZE;
canvas.height = MEASURE_SIZE;
canvas.style.cssText = `position: fixed; left: 0; top: 0; width: ${MEASURE_SIZE}px; height: ${MEASURE_SIZE}px; z-index: 9999; display: none;`;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
const img = new Image();
img.onload = () => {
const { width, height } = img;
let drawWidth = MEASURE_SIZE;
let drawHeight = MEASURE_SIZE;
let offsetX = 0;
let offsetY = 0;
if (width < height) {
drawHeight = height * (MEASURE_SIZE / width);
offsetY = -(drawHeight - drawWidth) / 2;
} else {
drawWidth = width * (MEASURE_SIZE / height);
offsetX = -(drawWidth - drawHeight) / 2;
}
ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight);
const dataURL = canvas.toDataURL();
document.body.removeChild(canvas);
resolve(dataURL);
};
img.src = window.URL.createObjectURL(file);
});
}

View File

@ -1,4 +1,4 @@
// rc-upload 2.6.3
// rc-upload 2.9.4
import upload from './src';
export default upload;

View File

@ -28,6 +28,7 @@ const upLoadPropTypes = {
// onProgress: PropTypes.func,
withCredentials: PropTypes.bool,
openFileDialogOnClick: PropTypes.bool,
transformFile: PropTypes.func,
};
const AjaxUploader = {
@ -67,6 +68,7 @@ const AjaxUploader = {
}
},
onFileDrop(e) {
const { multiple } = this.$props;
e.preventDefault();
if (e.type === 'dragover') {
return;
@ -76,21 +78,31 @@ const AjaxUploader = {
attrAccept(_file, this.accept),
);
} else {
const files = partition(Array.prototype.slice.call(e.dataTransfer.files), file =>
let files = partition(Array.prototype.slice.call(e.dataTransfer.files), file =>
attrAccept(file, this.accept),
);
this.uploadFiles(files[0]);
if (files[1].length) {
this.$emit('reject', files[1]);
let successFiles = files[0];
const errorFiles = files[1];
if (multiple === false) {
successFiles = successFiles.slice(0, 1);
}
this.uploadFiles(successFiles);
if (errorFiles.length) {
this.$emit('reject', errorFiles);
}
}
},
uploadFiles(files) {
const postFiles = Array.prototype.slice.call(files);
postFiles.forEach(file => {
file.uid = getUid();
this.upload(file, postFiles);
});
postFiles
.map(file => {
file.uid = getUid();
return file;
})
.forEach(file => {
this.upload(file, postFiles);
});
},
upload(file, fileList) {
if (!this.beforeUpload) {
@ -119,10 +131,10 @@ const AjaxUploader = {
if (!this._isMounted) {
return;
}
let { data } = this.$props;
if (typeof data === 'function') {
data = data(file);
}
const { $props: props } = this;
let { data } = props;
const { transformFile = originFile => originFile } = props;
new Promise(resolve => {
const { action } = this;
if (typeof action === 'function') {
@ -132,26 +144,37 @@ const AjaxUploader = {
}).then(action => {
const { uid } = file;
const request = this.customRequest || defaultRequest;
this.reqs[uid] = request({
action,
filename: this.name,
file,
data,
headers: this.headers,
withCredentials: this.withCredentials,
onProgress: e => {
this.$emit('progress', e, file);
},
onSuccess: (ret, xhr) => {
delete this.reqs[uid];
this.$emit('success', ret, file, xhr);
},
onError: (err, ret) => {
delete this.reqs[uid];
this.$emit('error', err, ret, file);
},
const transform = Promise.resolve(transformFile(file)).catch(e => {
console.error(e); // eslint-disable-line no-console
});
transform.then(transformedFile => {
if (typeof data === 'function') {
data = data(file);
}
const requestOption = {
action,
filename: this.name,
data,
file: transformedFile,
headers: this.headers,
withCredentials: this.withCredentials,
method: props.method || 'post',
onProgress: e => {
this.$emit('progress', e, file);
},
onSuccess: (ret, xhr) => {
delete this.reqs[uid];
this.$emit('success', ret, file, xhr);
},
onError: (err, ret) => {
delete this.reqs[uid];
this.$emit('error', err, ret, file);
},
};
this.reqs[uid] = request(requestOption);
this.$emit('start', file);
});
this.$emit('start', file);
});
},
reset() {
@ -166,13 +189,13 @@ const AjaxUploader = {
if (file && file.uid) {
uid = file.uid;
}
if (reqs[uid]) {
if (reqs[uid] && reqs[uid].abort) {
reqs[uid].abort();
delete reqs[uid];
}
delete reqs[uid];
} else {
Object.keys(reqs).forEach(uid => {
if (reqs[uid]) {
if (reqs[uid] && reqs[uid].abort) {
reqs[uid].abort();
}
@ -201,7 +224,7 @@ const AjaxUploader = {
? {}
: {
click: openFileDialogOnClick ? this.onClick : () => {},
keydown: this.onKeyDown,
keydown: openFileDialogOnClick ? this.onKeyDown : () => {},
drop: this.onFileDrop,
dragover: this.onFileDrop,
};
@ -222,6 +245,7 @@ const AjaxUploader = {
id={$attrs.id}
type="file"
ref="fileInputRef"
onClick={e => e.stopPropagation()} // https://github.com/ant-design/ant-design/issues/19948
key={this.uid}
style={{ display: 'none' }}
accept={accept}

View File

@ -1,8 +1,8 @@
function getError(option, xhr) {
const msg = `cannot post ${option.action} ${xhr.status}'`;
const msg = `cannot ${option.method} ${option.action} ${xhr.status}'`;
const err = new Error(msg);
err.status = xhr.status;
err.method = 'post';
err.method = option.method;
err.url = option.action;
return err;
}
@ -46,7 +46,18 @@ export default function upload(option) {
const formData = new window.FormData();
if (option.data) {
Object.keys(option.data).map(key => {
Object.keys(option.data).forEach(key => {
const value = option.data[key];
// support key-value array data
if (Array.isArray(value)) {
value.forEach(item => {
// { list: [ 11, 22 ] }
// formData.append('list[]', 11);
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, option.data[key]);
});
}
@ -67,7 +78,7 @@ export default function upload(option) {
option.onSuccess(getBody(xhr), xhr);
};
xhr.open('post', option.action, true);
xhr.open(option.method, option.action, true);
// Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
if (option.withCredentials && 'withCredentials' in xhr) {

View File

@ -27,6 +27,20 @@ const traverseFileTree = (files, callback, isAccepted) => {
if (item.isFile) {
item.file(file => {
if (isAccepted(file)) {
// https://github.com/ant-design/ant-design/issues/16426
if (item.fullPath && !file.webkitRelativePath) {
Object.defineProperties(file, {
webkitRelativePath: {
writable: true,
},
});
file.webkitRelativePath = item.fullPath.replace(/^\//, '');
Object.defineProperties(file, {
webkitRelativePath: {
writable: false,
},
});
}
callback([file]);
}
});

49
types/upload.d.ts vendored
View File

@ -4,16 +4,54 @@
import { AntdComponent } from './component';
export interface UploadFile {
uid: string | number;
export interface VcFile extends File {
uid: string;
readonly lastModifiedDate: Date;
readonly webkitRelativePath: string;
}
export interface UploadFile<T = any> {
uid: string;
size: number;
name: string;
fileName?: string;
lastModified?: number;
lastModifiedDate?: Date;
url?: string;
status?: UploadFileStatus;
percent?: number;
thumbUrl?: string;
originFileObj?: File | Blob;
response?: T;
error?: any;
linkProps?: any;
type: string;
xhr?: T;
preview?: string;
}
export interface ShowUploadList {
showRemoveIcon?: boolean;
showPreviewIcon?: boolean;
showDownloadIcon?: boolean;
}
export interface UploadLocale {
uploading?: string;
removeFile?: string;
downloadFile?: string;
uploadError?: string;
previewFile?: string;
}
export type UploadType = 'drag' | 'select';
export type UploadListType = 'text' | 'picture' | 'picture-card';
type PreviewFileHandler = (file: File | Blob) => PromiseLike<string>;
type TransformFileHandler = (
file: VcFile,
) => string | Blob | File | PromiseLike<string | Blob | File>;
export declare class Upload extends AntdComponent {
static Dragger: typeof Upload;
@ -56,6 +94,8 @@ export declare class Upload extends AntdComponent {
*/
data: object | Function;
method?: 'POST' | 'PUT' | 'post' | 'put';
/**
* Default list of files that have been uploaded.
* @type UploadFile[]
@ -137,4 +177,9 @@ export declare class Upload extends AntdComponent {
* @type Function
*/
remove: (file: any) => boolean | Promise<boolean>;
locale?: UploadLocale;
id?: string;
previewFile?: PreviewFileHandler;
transformFile?: TransformFileHandler;
}