From 548595959d886bb30a7574160fee4eb3ca06b641 Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sun, 23 Feb 2020 20:18:20 +0800 Subject: [PATCH] feat: add download transformFile previewFile actio --- build/config.js | 2 +- components/upload/Upload.jsx | 94 ++++---- components/upload/UploadList.jsx | 214 +++++++++---------- components/upload/demo/avatar.md | 6 +- components/upload/demo/basic copy 3.md | 46 ++++ components/upload/demo/fileList.md | 2 - components/upload/demo/index.vue | 4 + components/upload/demo/picture-card.md | 42 +++- components/upload/demo/preview-file.md | 39 ++++ components/upload/demo/transform-file.md | 48 +++++ components/upload/index.en-US.md | 57 ++--- components/upload/index.zh-CN.md | 57 ++--- components/upload/interface.jsx | 6 + components/upload/utils.jsx | 74 +++++++ components/vc-upload/index.js | 2 +- components/vc-upload/src/AjaxUploader.jsx | 94 +++++--- components/vc-upload/src/request.js | 19 +- components/vc-upload/src/traverseFileTree.js | 14 ++ types/upload.d.ts | 49 ++++- 19 files changed, 619 insertions(+), 250 deletions(-) create mode 100644 components/upload/demo/basic copy 3.md create mode 100644 components/upload/demo/preview-file.md create mode 100644 components/upload/demo/transform-file.md diff --git a/build/config.js b/build/config.js index b9f68ea9a..fbee0138a 100644 --- a/build/config.js +++ b/build/config.js @@ -1,5 +1,5 @@ module.exports = { dev: { - componentName: 'select', // dev components + componentName: 'upload', // dev components }, }; diff --git a/components/upload/Upload.jsx b/components/upload/Upload.jsx index f0e831cf7..528cd039e 100644 --- a/components/upload/Upload.jsx +++ b/components/upload/Upload.jsx @@ -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 ; }, }, @@ -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 ( - + {uploadList} {uploadButton} diff --git a/components/upload/UploadList.jsx b/components/upload/UploadList.jsx index 43bc727d8..17b6707cf 100644 --- a/components/upload/UploadList.jsx +++ b/components/upload/UploadList.jsx @@ -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 = ; } else { const thumbnail = isImageUrl(file) ? ( - + ) : ( ); @@ -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 ? ( - this.handlePreview(file, e)} - > - {file.name} - - ) : ( + + const removeIcon = showRemoveIcon ? ( + this.handleClose(file)} /> + ) : null; + const downloadIcon = + showDownloadIcon && file.status === 'done' ? ( + this.handleDownload(file)} + /> + ) : null; + const downloadOrDelete = listType !== 'picture-card' && ( this.handlePreview(file, e)} - title={file.name} + key="download-delete" + class={`${prefixCls}-list-item-card-actions ${listType === 'picture' ? 'picture' : ''}`} > - {file.name} + {downloadIcon && {downloadIcon}} + {removeIcon && {removeIcon}} ); + 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 + ? [ + this.handlePreview(file, e)} + > + {file.name} + , + downloadOrDelete, + ] + : [ + this.handlePreview(file, e)} + title={file.name} + > + {file.name} + , + downloadOrDelete, + ]; const style = file.url || file.thumbUrl ? undefined @@ -207,55 +221,41 @@ export default { ) : null; - const iconProps = { - props: { - type: 'delete', - title: locale.removeFile, - }, - on: { - click: () => { - this.handleClose(file); - }, - }, - }; - const iconProps1 = { ...iconProps, ...{ props: { type: 'close' } } }; - const removeIcon = showRemoveIcon ? : null; - const removeIconClose = showRemoveIcon ? : null; - const actions = - listType === 'picture-card' && file.status !== 'uploading' ? ( - - {previewIcon} - {removeIcon} - - ) : ( - removeIconClose - ); + const actions = listType === 'picture-card' && file.status !== 'uploading' && ( + + {previewIcon} + {file.status === 'done' && downloadIcon} + {removeIcon} + + ); 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' ? ( - - {icon} - {preview} - - ) : ( - - {icon} - {preview} - - ); + const iconAndPreview = ( + + {icon} + {preview} + + ); const transitionProps = getTransitionProps('fade'); - return ( + const dom = ( {iconAndPreview} {actions} {progress} ); + const listContainerNameClass = classNames({ + [`${prefixCls}-list-picture-card-container`]: listType === 'picture-card', + }); + return ( + + {file.status === 'error' ? {dom} : {dom}} + + ); }); const listClassNames = classNames({ [`${prefixCls}-list`]: true, diff --git a/components/upload/demo/avatar.md b/components/upload/demo/avatar.md index 423d0b17f..863a92b81 100644 --- a/components/upload/demo/avatar.md +++ b/components/upload/demo/avatar.md @@ -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; }, }, }; diff --git a/components/upload/demo/basic copy 3.md b/components/upload/demo/basic copy 3.md new file mode 100644 index 000000000..9d6e62b36 --- /dev/null +++ b/components/upload/demo/basic copy 3.md @@ -0,0 +1,46 @@ + +#### 点击上传 +经典款式,用户点击按钮弹出文件选择框。 + + + +#### Upload by clicking +Classic mode. File selection dialog pops up when upload button is clicked. + + +```tpl + + + Click to Upload + + + +``` diff --git a/components/upload/demo/fileList.md b/components/upload/demo/fileList.md index e6a46486f..c29a47db5 100644 --- a/components/upload/demo/fileList.md +++ b/components/upload/demo/fileList.md @@ -3,7 +3,6 @@ 使用 `fileList` 对列表进行完全控制,可以实现各种自定义功能,以下演示三种情况: 1) 上传列表数量的限制。 2) 读取远程路径并显示链接。 -3) 按照服务器返回信息筛选成功上传的文件。 @@ -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. ```tpl diff --git a/components/upload/demo/index.vue b/components/upload/demo/index.vue index dc32a0006..e852f5e4b 100644 --- a/components/upload/demo/index.vue +++ b/components/upload/demo/index.vue @@ -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 { + + diff --git a/components/upload/demo/picture-card.md b/components/upload/demo/picture-card.md index be833544a..7e5a94a60 100644 --- a/components/upload/demo/picture-card.md +++ b/components/upload/demo/picture-card.md @@ -18,7 +18,7 @@ After users upload picture, the thumbnail will be shown in list. The upload butt @preview="handlePreview" @change="handleChange" > - + Upload @@ -29,6 +29,14 @@ After users upload picture, the thumbnail will be shown in list. The upload butt +``` diff --git a/components/upload/demo/transform-file.md b/components/upload/demo/transform-file.md new file mode 100644 index 000000000..159864d99 --- /dev/null +++ b/components/upload/demo/transform-file.md @@ -0,0 +1,48 @@ + +#### 上传前转换文件 +使用 `transformFile` 转换上传的文件(例如添加水印)。 + + + +#### Transform file before request +Use `transformFile` for transform file before request such as add a watermark. + + +```tpl + + + + Upload + + + + +``` diff --git a/components/upload/index.en-US.md b/components/upload/index.en-US.md index 44c23ddfa..76f42b0d6 100644 --- a/components/upload/index.en-US.md +++ b/components/upload/index.en-US.md @@ -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. **Warning:this 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. **Warning:this 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 | - | 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` | - | 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', // options:uploading, done, error, removed response: '{"status": "success"}', // response from server linkProps: '{"download": "image"}', // additional html props of file link + xhr: 'XMLHttpRequest{ ... }', // XMLHttpRequest Header } ``` diff --git a/components/upload/index.zh-CN.md b/components/upload/index.zh-CN.md index b2bcd17e7..7d4c7d670 100644 --- a/components/upload/index.zh-CN.md +++ b/components/upload/index.zh-CN.md @@ -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 | 无 | 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` | 无 | 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 } ``` diff --git a/components/upload/interface.jsx b/components/upload/interface.jsx index 12c3e9932..080fd2cdd 100755 --- a/components/upload/interface.jsx +++ b/components/upload/interface.jsx @@ -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, }; diff --git a/components/upload/utils.jsx b/components/upload/utils.jsx index e46a1a92c..3642ce9fb 100644 --- a/components/upload/utils.jsx +++ b/components/upload/utils.jsx @@ -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); + }); +} diff --git a/components/vc-upload/index.js b/components/vc-upload/index.js index 7b5aa8555..764dba626 100644 --- a/components/vc-upload/index.js +++ b/components/vc-upload/index.js @@ -1,4 +1,4 @@ -// rc-upload 2.6.3 +// rc-upload 2.9.4 import upload from './src'; export default upload; diff --git a/components/vc-upload/src/AjaxUploader.jsx b/components/vc-upload/src/AjaxUploader.jsx index c6013d895..12100745e 100644 --- a/components/vc-upload/src/AjaxUploader.jsx +++ b/components/vc-upload/src/AjaxUploader.jsx @@ -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} diff --git a/components/vc-upload/src/request.js b/components/vc-upload/src/request.js index db16c0c2c..bb6107acb 100644 --- a/components/vc-upload/src/request.js +++ b/components/vc-upload/src/request.js @@ -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) { diff --git a/components/vc-upload/src/traverseFileTree.js b/components/vc-upload/src/traverseFileTree.js index f4193b0b0..3fa9c8869 100644 --- a/components/vc-upload/src/traverseFileTree.js +++ b/components/vc-upload/src/traverseFileTree.js @@ -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]); } }); diff --git a/types/upload.d.ts b/types/upload.d.ts index 0e4cdf24e..656169e82 100644 --- a/types/upload.d.ts +++ b/types/upload.d.ts @@ -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 { + 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; +type TransformFileHandler = ( + file: VcFile, +) => string | Blob | File | PromiseLike; + 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; + + locale?: UploadLocale; + id?: string; + previewFile?: PreviewFileHandler; + transformFile?: TransformFileHandler; }