From b0c3677d501a56ed98a9fa22a809d667aefefa8e Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Sun, 20 Feb 2022 21:48:14 +0800 Subject: [PATCH 1/5] refactor: vc-upload --- components/vc-upload/AjaxUploader.tsx | 313 +++++++++++++++++++++++ components/vc-upload/Upload.tsx | 41 +++ components/vc-upload/attr-accept.ts | 53 ++++ components/vc-upload/index.ts | 7 + components/vc-upload/interface.tsx | 82 ++++++ components/vc-upload/request.ts | 107 ++++++++ components/vc-upload/traverseFileTree.ts | 75 ++++++ components/vc-upload/uid.ts | 7 + 8 files changed, 685 insertions(+) create mode 100644 components/vc-upload/AjaxUploader.tsx create mode 100644 components/vc-upload/Upload.tsx create mode 100644 components/vc-upload/attr-accept.ts create mode 100644 components/vc-upload/index.ts create mode 100644 components/vc-upload/interface.tsx create mode 100644 components/vc-upload/request.ts create mode 100644 components/vc-upload/traverseFileTree.ts create mode 100644 components/vc-upload/uid.ts diff --git a/components/vc-upload/AjaxUploader.tsx b/components/vc-upload/AjaxUploader.tsx new file mode 100644 index 0000000000..f97cad70f4 --- /dev/null +++ b/components/vc-upload/AjaxUploader.tsx @@ -0,0 +1,313 @@ +import defaultRequest from './request'; +import getUid from './uid'; +import attrAccept from './attr-accept'; +import traverseFileTree from './traverseFileTree'; +import type { + RcFile, + UploadProgressEvent, + UploadRequestError, + BeforeUploadFileType, +} from './interface'; +import { uploadProps } from './interface'; +import { defineComponent, onBeforeUnmount, onMounted, ref } from 'vue'; +import type { ChangeEvent } from '../_util/EventInterface'; +import pickAttrs from '../_util/pickAttrs'; + +interface ParsedFileInfo { + origin: RcFile; + action: string; + data: Record; + parsedFile: RcFile; +} +export default defineComponent({ + name: 'AjaxUploader', + inheritAttrs: false, + props: uploadProps(), + setup(props, { slots, attrs, expose }) { + const uid = ref(getUid()); + const reqs: any = {}; + + const fileInput = ref(); + + let isMounted = false; + + /** + * Process file before upload. When all the file is ready, we start upload. + */ + const processFile = async (file: RcFile, fileList: RcFile[]): Promise => { + const { beforeUpload } = props; + + let transformedFile: BeforeUploadFileType | void = file; + if (beforeUpload) { + try { + transformedFile = await beforeUpload(file, fileList); + } catch (e) { + // Rejection will also trade as false + transformedFile = false; + } + if (transformedFile === false) { + return { + origin: file, + parsedFile: null, + action: null, + data: null, + }; + } + } + + // Get latest action + const { action } = props; + let mergedAction: string; + if (typeof action === 'function') { + mergedAction = await action(file); + } else { + mergedAction = action; + } + + // Get latest data + const { data } = props; + let mergedData: Record; + if (typeof data === 'function') { + mergedData = await data(file); + } else { + mergedData = data; + } + + const parsedData = + // string type is from legacy `transformFile`. + // Not sure if this will work since no related test case works with it + (typeof transformedFile === 'object' || typeof transformedFile === 'string') && + transformedFile + ? transformedFile + : file; + + let parsedFile: File; + if (parsedData instanceof File) { + parsedFile = parsedData; + } else { + parsedFile = new File([parsedData], file.name, { type: file.type }); + } + + const mergedParsedFile: RcFile = parsedFile as RcFile; + mergedParsedFile.uid = file.uid; + + return { + origin: file, + data: mergedData, + parsedFile: mergedParsedFile, + action: mergedAction, + }; + }; + + const post = ({ data, origin, action, parsedFile }: ParsedFileInfo) => { + if (!isMounted) { + return; + } + + const { onStart, customRequest, name, headers, withCredentials, method } = props; + + const { uid } = origin; + const request = customRequest || defaultRequest; + + const requestOption = { + action, + filename: name, + data, + file: parsedFile, + headers, + withCredentials, + method: method || 'post', + onProgress: (e: UploadProgressEvent) => { + const { onProgress } = props; + onProgress?.(e, parsedFile); + }, + onSuccess: (ret: any, xhr: XMLHttpRequest) => { + const { onSuccess } = props; + onSuccess?.(ret, parsedFile, xhr); + + delete reqs[uid]; + }, + onError: (err: UploadRequestError, ret: any) => { + const { onError } = props; + onError?.(err, ret, parsedFile); + + delete reqs[uid]; + }, + }; + + onStart(origin); + reqs[uid] = request(requestOption); + }; + + const reset = () => { + uid.value = getUid(); + }; + + const abort = (file?: any) => { + if (file) { + const uid = file.uid ? file.uid : file; + if (reqs[uid] && reqs[uid].abort) { + reqs[uid].abort(); + } + delete reqs[uid]; + } else { + Object.keys(reqs).forEach(uid => { + if (reqs[uid] && reqs[uid].abort) { + reqs[uid].abort(); + } + delete reqs[uid]; + }); + } + }; + + onMounted(() => { + isMounted = true; + }); + + onBeforeUnmount(() => { + isMounted = false; + abort(); + }); + const uploadFiles = (files: File[]) => { + const originFiles = [...files] as RcFile[]; + const postFiles = originFiles.map((file: RcFile & { uid?: string }) => { + // eslint-disable-next-line no-param-reassign + file.uid = getUid(); + return processFile(file, originFiles); + }); + + // Batch upload files + Promise.all(postFiles).then(fileList => { + const { onBatchStart } = props; + + onBatchStart?.(fileList.map(({ origin, parsedFile }) => ({ file: origin, parsedFile }))); + + fileList + .filter(file => file.parsedFile !== null) + .forEach(file => { + post(file); + }); + }); + }; + + const onChange = (e: ChangeEvent) => { + const { accept, directory } = props; + const { files } = e.target as any; + const acceptedFiles = [...files].filter( + (file: RcFile) => !directory || attrAccept(file, accept), + ); + uploadFiles(acceptedFiles); + reset(); + }; + + const onClick = (e: MouseEvent | KeyboardEvent) => { + const el = fileInput.value; + if (!el) { + return; + } + const { onClick } = props; + // TODO + // if (children && (children as any).type === 'button') { + // const parent = el.parentNode as HTMLInputElement; + // parent.focus(); + // parent.querySelector('button').blur(); + // } + el.click(); + if (onClick) { + onClick(e); + } + }; + + const onKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + onClick(e); + } + }; + + const onFileDrop = (e: DragEvent) => { + const { multiple } = props; + + e.preventDefault(); + + if (e.type === 'dragover') { + return; + } + + if (props.directory) { + traverseFileTree( + Array.prototype.slice.call(e.dataTransfer.items), + uploadFiles, + (_file: RcFile) => attrAccept(_file, props.accept), + ); + } else { + let files = [...e.dataTransfer.files].filter((file: RcFile) => + attrAccept(file, props.accept), + ); + + if (multiple === false) { + files = files.slice(0, 1); + } + + uploadFiles(files); + } + }; + expose({ + abort, + }); + return () => { + const { + componentTag: Tag, + prefixCls, + disabled, + id, + multiple, + accept, + capture, + directory, + openFileDialogOnClick, + onMouseenter, + onMouseleave, + ...otherProps + } = props; + const cls = { + [prefixCls]: true, + [`${prefixCls}-disabled`]: disabled, + [attrs.class as string]: !!attrs.class, + }; + // because input don't have directory/webkitdirectory type declaration + const dirProps: any = directory + ? { directory: 'directory', webkitdirectory: 'webkitdirectory' } + : {}; + const events = disabled + ? {} + : { + onClick: openFileDialogOnClick ? onClick : () => {}, + onKeydown: openFileDialogOnClick ? onKeyDown : () => {}, + onMouseenter, + onMouseleave, + onDrop: onFileDrop, + onDragover: onFileDrop, + tabindex: '0', + }; + return ( + + e.stopPropagation()} // https://github.com/ant-design/ant-design/issues/19948 + key={uid.value} + style={{ display: 'none' }} + accept={accept} + {...dirProps} + multiple={multiple} + onChange={onChange} + {...(capture != null ? { capture } : {})} + /> + {slots.default?.()} + + ); + }; + }, +}); diff --git a/components/vc-upload/Upload.tsx b/components/vc-upload/Upload.tsx new file mode 100644 index 0000000000..53119c0875 --- /dev/null +++ b/components/vc-upload/Upload.tsx @@ -0,0 +1,41 @@ +import { defineComponent, ref } from 'vue'; +import { initDefaultProps } from '../_util/props-util'; +import AjaxUpload from './AjaxUploader'; +import type { RcFile } from './interface'; +import { uploadProps } from './interface'; + +function empty() {} + +export default defineComponent({ + name: 'Upload', + inheritAttrs: false, + props: initDefaultProps(uploadProps(), { + componentTag: 'span', + prefixCls: 'rc-upload', + data: {}, + headers: {}, + name: 'file', + multipart: false, + onStart: empty, + onError: empty, + onSuccess: empty, + multiple: false, + beforeUpload: null, + customRequest: null, + withCredentials: false, + openFileDialogOnClick: true, + }), + setup(props, { slots, attrs, expose }) { + const uploader = ref(); + + const abort = (file: RcFile) => { + uploader.value?.abort(file); + }; + expose({ + abort, + }); + return () => { + return ; + }; + }, +}); diff --git a/components/vc-upload/attr-accept.ts b/components/vc-upload/attr-accept.ts new file mode 100644 index 0000000000..f67d293a60 --- /dev/null +++ b/components/vc-upload/attr-accept.ts @@ -0,0 +1,53 @@ +import { warning } from '../vc-util/warning'; +import type { RcFile } from './interface'; + +export default (file: RcFile, acceptedFiles: string | string[]) => { + if (file && acceptedFiles) { + const acceptedFilesArray = Array.isArray(acceptedFiles) + ? acceptedFiles + : acceptedFiles.split(','); + const fileName = file.name || ''; + const mimeType = file.type || ''; + const baseMimeType = mimeType.replace(/\/.*$/, ''); + + return acceptedFilesArray.some(type => { + const validType = type.trim(); + // This is something like */*,* allow all files + if (/^\*(\/\*)?$/.test(type)) { + return true; + } + + // like .jpg, .png + if (validType.charAt(0) === '.') { + const lowerFileName = fileName.toLowerCase(); + const lowerType = validType.toLowerCase(); + + let affixList = [lowerType]; + if (lowerType === '.jpg' || lowerType === '.jpeg') { + affixList = ['.jpg', '.jpeg']; + } + + return affixList.some(affix => lowerFileName.endsWith(affix)); + } + + // This is something like a image/* mime type + if (/\/\*$/.test(validType)) { + return baseMimeType === validType.replace(/\/.*$/, ''); + } + + // Full match + if (mimeType === validType) { + return true; + } + + // Invalidate type should skip + if (/^\w+$/.test(validType)) { + warning(false, `Upload takes an invalidate 'accept' type '${validType}'.Skip for check.`); + return true; + } + + return false; + }); + } + return true; +}; diff --git a/components/vc-upload/index.ts b/components/vc-upload/index.ts new file mode 100644 index 0000000000..3ff01e6774 --- /dev/null +++ b/components/vc-upload/index.ts @@ -0,0 +1,7 @@ +// rc-upload 4.3.3 +import Upload from './Upload'; +import { UploadProps } from './interface'; + +export { UploadProps }; + +export default Upload; diff --git a/components/vc-upload/interface.tsx b/components/vc-upload/interface.tsx new file mode 100644 index 0000000000..fb61edf869 --- /dev/null +++ b/components/vc-upload/interface.tsx @@ -0,0 +1,82 @@ +import type { ExtractPropTypes, PropType } from 'vue'; + +export type BeforeUploadFileType = File | Blob | boolean | string; + +export type Action = string | ((file: RcFile) => string | PromiseLike); + +export const uploadProps = () => { + return { + capture: [Boolean, String] as PropType, + multipart: Boolean, + name: String, + disabled: Boolean, + componentTag: String as PropType, + action: [String, Function] as PropType, + method: String as PropType, + directory: Boolean, + data: [Object, Function] as PropType< + Record | ((file: RcFile | string | Blob) => Record) + >, + headers: Object as PropType, + accept: String, + multiple: Boolean, + onBatchStart: Function as PropType< + (fileList: { file: RcFile; parsedFile: Exclude }[]) => void + >, + onStart: Function as PropType<(file: RcFile) => void>, + onError: Function as PropType< + (error: Error, ret: Record, file: RcFile) => void + >, + onSuccess: Function as PropType< + (response: Record, file: RcFile, xhr: XMLHttpRequest) => void + >, + onProgress: Function as PropType<(event: UploadProgressEvent, file: RcFile) => void>, + beforeUpload: Function as PropType< + ( + file: RcFile, + FileList: RcFile[], + ) => BeforeUploadFileType | Promise + >, + customRequest: Function as PropType<(option: UploadRequestOption) => void>, + withCredentials: Boolean, + openFileDialogOnClick: Boolean, + prefixCls: String, + id: String, + onMouseenter: Function as PropType<(e: MouseEvent) => void>, + onMouseleave: Function as PropType<(e: MouseEvent) => void>, + onClick: Function as PropType<(e: MouseEvent | KeyboardEvent) => void>, + }; +}; + +export type UploadProps = Partial>>; + +export interface UploadProgressEvent extends Partial { + percent?: number; +} + +export type UploadRequestMethod = 'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch'; + +export type UploadRequestHeader = Record; + +export interface UploadRequestError extends Error { + status?: number; + method?: UploadRequestMethod; + url?: string; +} + +export interface UploadRequestOption { + onProgress?: (event: UploadProgressEvent) => void; + onError?: (event: UploadRequestError | ProgressEvent, body?: T) => void; + onSuccess?: (body: T, xhr?: XMLHttpRequest) => void; + data?: Record; + filename?: string; + file: Exclude | RcFile; + withCredentials?: boolean; + action: string; + headers?: UploadRequestHeader; + method: UploadRequestMethod; +} + +export interface RcFile extends File { + uid: string; +} diff --git a/components/vc-upload/request.ts b/components/vc-upload/request.ts new file mode 100644 index 0000000000..898847d0a9 --- /dev/null +++ b/components/vc-upload/request.ts @@ -0,0 +1,107 @@ +import type { UploadRequestOption, UploadRequestError, UploadProgressEvent } from './interface'; + +function getError(option: UploadRequestOption, xhr: XMLHttpRequest) { + const msg = `cannot ${option.method} ${option.action} ${xhr.status}'`; + const err = new Error(msg) as UploadRequestError; + err.status = xhr.status; + err.method = option.method; + err.url = option.action; + return err; +} + +function getBody(xhr: XMLHttpRequest) { + const text = xhr.responseText || xhr.response; + if (!text) { + return text; + } + + try { + return JSON.parse(text); + } catch (e) { + return text; + } +} + +export default function upload(option: UploadRequestOption) { + // eslint-disable-next-line no-undef + const xhr = new XMLHttpRequest(); + + if (option.onProgress && xhr.upload) { + xhr.upload.onprogress = function progress(e: UploadProgressEvent) { + if (e.total > 0) { + e.percent = (e.loaded / e.total) * 100; + } + option.onProgress(e); + }; + } + + // eslint-disable-next-line no-undef + const formData = new FormData(); + + if (option.data) { + 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, value as string | Blob); + }); + } + + // eslint-disable-next-line no-undef + if (option.file instanceof Blob) { + formData.append(option.filename, option.file, (option.file as any).name); + } else { + formData.append(option.filename, option.file); + } + + xhr.onerror = function error(e) { + option.onError(e); + }; + + xhr.onload = function onload() { + // allow success when 2xx status + // see https://github.com/react-component/upload/issues/34 + if (xhr.status < 200 || xhr.status >= 300) { + return option.onError(getError(option, xhr), getBody(xhr)); + } + + return option.onSuccess(getBody(xhr), xhr); + }; + + 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) { + xhr.withCredentials = true; + } + + const headers = option.headers || {}; + + // when set headers['X-Requested-With'] = null , can close default XHR header + // see https://github.com/react-component/upload/issues/33 + if (headers['X-Requested-With'] !== null) { + xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + } + + Object.keys(headers).forEach(h => { + if (headers[h] !== null) { + xhr.setRequestHeader(h, headers[h]); + } + }); + + xhr.send(formData); + + return { + abort() { + xhr.abort(); + }, + }; +} diff --git a/components/vc-upload/traverseFileTree.ts b/components/vc-upload/traverseFileTree.ts new file mode 100644 index 0000000000..15bc72dc2f --- /dev/null +++ b/components/vc-upload/traverseFileTree.ts @@ -0,0 +1,75 @@ +import type { RcFile } from './interface'; + +interface InternalDataTransferItem extends DataTransferItem { + isFile: boolean; + file: (cd: (file: RcFile & { webkitRelativePath?: string }) => void) => void; + createReader: () => any; + fullPath: string; + isDirectory: boolean; + name: string; + path: string; +} + +function loopFiles(item: InternalDataTransferItem, callback) { + const dirReader = item.createReader(); + let fileList = []; + + function sequence() { + dirReader.readEntries((entries: InternalDataTransferItem[]) => { + const entryList = Array.prototype.slice.apply(entries); + fileList = fileList.concat(entryList); + + // Check if all the file has been viewed + const isFinished = !entryList.length; + + if (isFinished) { + callback(fileList); + } else { + sequence(); + } + }); + } + + sequence(); +} + +const traverseFileTree = (files: InternalDataTransferItem[], callback, isAccepted) => { + // eslint-disable-next-line @typescript-eslint/naming-convention + const _traverseFileTree = (item: InternalDataTransferItem, path?: string) => { + // eslint-disable-next-line no-param-reassign + item.path = path || ''; + 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, + }, + }); + // eslint-disable-next-line no-param-reassign + (file as any).webkitRelativePath = item.fullPath.replace(/^\//, ''); + Object.defineProperties(file, { + webkitRelativePath: { + writable: false, + }, + }); + } + callback([file]); + } + }); + } else if (item.isDirectory) { + loopFiles(item, (entries: InternalDataTransferItem[]) => { + entries.forEach(entryItem => { + _traverseFileTree(entryItem, `${path}${item.name}/`); + }); + }); + } + }; + files.forEach(file => { + _traverseFileTree(file.webkitGetAsEntry() as any); + }); +}; + +export default traverseFileTree; diff --git a/components/vc-upload/uid.ts b/components/vc-upload/uid.ts new file mode 100644 index 0000000000..b0f323e95b --- /dev/null +++ b/components/vc-upload/uid.ts @@ -0,0 +1,7 @@ +const now = +new Date(); +let index = 0; + +export default function uid() { + // eslint-disable-next-line no-plusplus + return `vc-upload-${now}-${++index}`; +} From 67f5226cdcfc896d4b5da2d5a0be3aebaf684b0d Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Mon, 21 Feb 2022 22:36:54 +0800 Subject: [PATCH 2/5] refactor: vc-upload --- components/style/themes/default.less | 4 + components/upload copy/Dragger.tsx | 22 + components/upload copy/Upload.tsx | 342 ++++++++++ .../upload copy/UploadList/ListItem.tsx | 287 +++++++++ components/upload copy/UploadList/index.tsx | 261 ++++++++ .../__tests__/__snapshots__/demo.test.js.snap | 126 ++++ .../__snapshots__/uploadlist.test.js.snap | 75 +++ components/upload copy/__tests__/demo.test.js | 3 + components/upload copy/__tests__/mock.js | 14 + components/upload copy/__tests__/requests.js | 11 + .../upload copy/__tests__/upload.test.js | 331 ++++++++++ .../upload copy/__tests__/uploadlist.test.js | 433 +++++++++++++ components/upload copy/demo/avatar.vue | 116 ++++ components/upload copy/demo/basic.vue | 65 ++ .../upload copy/demo/defaultFileList.vue | 70 +++ components/upload copy/demo/directory.vue | 35 ++ components/upload copy/demo/drag.vue | 68 ++ components/upload copy/demo/fileList.vue | 81 +++ components/upload copy/demo/index.vue | 52 ++ components/upload copy/demo/picture-card.vue | 126 ++++ components/upload copy/demo/picture-style.vue | 108 ++++ components/upload copy/demo/preview-file.vue | 58 ++ .../upload copy/demo/transform-file.vue | 66 ++ .../upload copy/demo/upload-manually.vue | 96 +++ components/upload copy/index.en-US.md | 80 +++ components/upload copy/index.tsx | 17 + components/upload copy/index.zh-CN.md | 81 +++ components/upload copy/interface.tsx | 183 ++++++ components/upload copy/style/index.less | 589 ++++++++++++++++++ .../index.ts => upload copy/style/index.tsx} | 1 + components/upload copy/style/rtl.less | 179 ++++++ components/upload copy/utils.tsx | 115 ++++ 32 files changed, 4095 insertions(+) create mode 100644 components/upload copy/Dragger.tsx create mode 100644 components/upload copy/Upload.tsx create mode 100644 components/upload copy/UploadList/ListItem.tsx create mode 100644 components/upload copy/UploadList/index.tsx create mode 100644 components/upload copy/__tests__/__snapshots__/demo.test.js.snap create mode 100644 components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap create mode 100644 components/upload copy/__tests__/demo.test.js create mode 100644 components/upload copy/__tests__/mock.js create mode 100644 components/upload copy/__tests__/requests.js create mode 100644 components/upload copy/__tests__/upload.test.js create mode 100644 components/upload copy/__tests__/uploadlist.test.js create mode 100644 components/upload copy/demo/avatar.vue create mode 100644 components/upload copy/demo/basic.vue create mode 100644 components/upload copy/demo/defaultFileList.vue create mode 100644 components/upload copy/demo/directory.vue create mode 100644 components/upload copy/demo/drag.vue create mode 100644 components/upload copy/demo/fileList.vue create mode 100644 components/upload copy/demo/index.vue create mode 100644 components/upload copy/demo/picture-card.vue create mode 100644 components/upload copy/demo/picture-style.vue create mode 100644 components/upload copy/demo/preview-file.vue create mode 100644 components/upload copy/demo/transform-file.vue create mode 100644 components/upload copy/demo/upload-manually.vue create mode 100644 components/upload copy/index.en-US.md create mode 100644 components/upload copy/index.tsx create mode 100644 components/upload copy/index.zh-CN.md create mode 100755 components/upload copy/interface.tsx create mode 100644 components/upload copy/style/index.less rename components/{upload/style/index.ts => upload copy/style/index.tsx} (82%) create mode 100644 components/upload copy/style/rtl.less create mode 100644 components/upload copy/utils.tsx diff --git a/components/style/themes/default.less b/components/style/themes/default.less index a036d27539..4276be61bf 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -965,6 +965,10 @@ @typography-title-margin-top: 1.2em; @typography-title-margin-bottom: 0.5em; +// Upload +// --- +@upload-actions-color: @text-color-secondary; + // Image // --- @image-size-base: 48px; diff --git a/components/upload copy/Dragger.tsx b/components/upload copy/Dragger.tsx new file mode 100644 index 0000000000..251912a4f8 --- /dev/null +++ b/components/upload copy/Dragger.tsx @@ -0,0 +1,22 @@ +import { defineComponent } from 'vue'; +import Upload from './Upload'; +import { uploadProps } from './interface'; + +export default defineComponent({ + name: 'AUploadDragger', + inheritAttrs: false, + props: uploadProps(), + setup(props, { slots, attrs }) { + return () => { + const { height, ...restProps } = props; + const { style, ...restAttrs } = attrs; + const draggerProps = { + ...restProps, + ...restAttrs, + type: 'drag', + style: { ...(style as any), height: typeof height === 'number' ? `${height}px` : height }, + } as any; + return ; + }; + }, +}); diff --git a/components/upload copy/Upload.tsx b/components/upload copy/Upload.tsx new file mode 100644 index 0000000000..6bf14054e3 --- /dev/null +++ b/components/upload copy/Upload.tsx @@ -0,0 +1,342 @@ +import classNames from '../_util/classNames'; +import uniqBy from 'lodash-es/uniqBy'; +import findIndex from 'lodash-es/findIndex'; +import VcUpload from '../vc-upload'; +import BaseMixin from '../_util/BaseMixin'; +import { getOptionProps, hasProp, getSlot } from '../_util/props-util'; +import initDefaultProps from '../_util/props-util/initDefaultProps'; +import LocaleReceiver from '../locale-provider/LocaleReceiver'; +import defaultLocale from '../locale-provider/default'; +import { defaultConfigProvider } from '../config-provider'; +import Dragger from './Dragger'; +import UploadList from './UploadList'; +import type { UploadFile } from './interface'; +import { uploadProps } from './interface'; +import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from './utils'; +import { defineComponent, inject } from 'vue'; +import { getDataAndAriaProps } from '../_util/util'; +import { useInjectFormItemContext } from '../form/FormItemContext'; + +export default defineComponent({ + name: 'AUpload', + mixins: [BaseMixin], + inheritAttrs: false, + Dragger, + props: initDefaultProps(uploadProps, { + type: 'select', + multiple: false, + action: '', + data: {}, + accept: '', + beforeUpload: T, + showUploadList: true, + listType: 'text', // or pictrue + disabled: false, + supportServerRender: true, + }), + setup() { + const formItemContext = useInjectFormItemContext(); + return { + upload: null, + progressTimer: null, + configProvider: inject('configProvider', defaultConfigProvider), + formItemContext, + }; + }, + // recentUploadStatus: boolean | PromiseLike; + data() { + return { + sFileList: this.fileList || this.defaultFileList || [], + dragState: 'drop', + }; + }, + watch: { + fileList(val) { + this.sFileList = val || []; + }, + }, + beforeUnmount() { + this.clearProgressTimer(); + }, + methods: { + onStart(file) { + const targetItem = fileToObject(file); + targetItem.status = 'uploading'; + const nextFileList = this.sFileList.concat(); + const fileIndex = findIndex(nextFileList, ({ uid }) => uid === targetItem.uid); + if (fileIndex === -1) { + nextFileList.push(targetItem); + } else { + nextFileList[fileIndex] = targetItem; + } + this.handleChange({ + file: targetItem, + fileList: nextFileList, + }); + // fix ie progress + if (!window.File || (typeof process === 'object' && process.env.TEST_IE)) { + this.autoUpdateProgress(0, targetItem); + } + }, + + onSuccess(response, file, xhr) { + this.clearProgressTimer(); + try { + if (typeof response === 'string') { + response = JSON.parse(response); + } + } catch (e) { + /* do nothing */ + } + const fileList = this.sFileList; + const targetItem = getFileItem(file, fileList); + // removed + if (!targetItem) { + return; + } + targetItem.status = 'done'; + targetItem.response = response; + targetItem.xhr = xhr; + this.handleChange({ + file: { ...targetItem }, + fileList, + }); + }, + onProgress(e, file) { + const fileList = this.sFileList; + const targetItem = getFileItem(file, fileList); + // removed + if (!targetItem) { + return; + } + targetItem.percent = e.percent; + this.handleChange({ + event: e, + file: { ...targetItem }, + fileList: this.sFileList, + }); + }, + onError(error, response, file) { + this.clearProgressTimer(); + const fileList = this.sFileList; + const targetItem = getFileItem(file, fileList); + // removed + if (!targetItem) { + return; + } + targetItem.error = error; + targetItem.response = response; + targetItem.status = 'error'; + this.handleChange({ + file: { ...targetItem }, + fileList, + }); + }, + onReject(fileList) { + this.$emit('reject', fileList); + }, + handleRemove(file) { + const { remove: onRemove } = this; + const { sFileList: fileList } = this.$data; + + Promise.resolve(typeof onRemove === 'function' ? onRemove(file) : onRemove).then(ret => { + // Prevent removing file + if (ret === false) { + return; + } + + const removedFileList = removeFileItem(file, fileList); + + if (removedFileList) { + file.status = 'removed'; // eslint-disable-line + + if (this.upload) { + this.upload.abort(file); + } + + this.handleChange({ + file, + fileList: removedFileList, + }); + } + }); + }, + handleManualRemove(file) { + if (this.$refs.uploadRef) { + (this.$refs.uploadRef as any).abort(file); + } + this.handleRemove(file); + }, + handleChange(info) { + if (!hasProp(this, 'fileList')) { + this.setState({ sFileList: info.fileList }); + } + this.$emit('update:fileList', info.fileList); + this.$emit('change', info); + this.formItemContext.onFieldChange(); + }, + onFileDrop(e) { + this.setState({ + dragState: e.type, + }); + }, + reBeforeUpload(file, fileList) { + const { beforeUpload } = this.$props; + const { sFileList: stateFileList } = this.$data; + if (!beforeUpload) { + return true; + } + const result = beforeUpload(file, fileList); + if (result === false) { + this.handleChange({ + file, + fileList: uniqBy( + stateFileList.concat(fileList.map(fileToObject)), + (item: UploadFile) => item.uid, + ), + }); + return false; + } + if (result && result.then) { + return result; + } + return true; + }, + 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, + previewFile, + disabled, + locale: propLocale, + } = getOptionProps(this); + const { showRemoveIcon, showPreviewIcon, showDownloadIcon } = showUploadList; + const { sFileList: fileList } = this.$data; + const { onDownload, onPreview } = this.$props; + const uploadListProps = { + listType, + items: fileList, + previewFile, + showRemoveIcon: !disabled && showRemoveIcon, + showPreviewIcon, + showDownloadIcon, + locale: { ...locale, ...propLocale }, + onRemove: this.handleManualRemove, + onDownload, + onPreview, + }; + return ; + }, + }, + render() { + const { + prefixCls: customizePrefixCls, + showUploadList, + listType, + type, + disabled, + } = getOptionProps(this); + const { sFileList: fileList, dragState } = this.$data; + const { class: className, style } = this.$attrs; + const getPrefixCls = this.configProvider.getPrefixCls; + const prefixCls = getPrefixCls('upload', customizePrefixCls); + + const vcUploadProps = { + ...this.$props, + id: this.$props.id ?? this.formItemContext.id.value, + prefixCls, + beforeUpload: this.reBeforeUpload, + onStart: this.onStart, + onError: this.onError, + onProgress: this.onProgress, + onSuccess: this.onSuccess, + onReject: this.onReject, + ref: 'uploadRef', + }; + + const uploadList = showUploadList ? ( + + ) : null; + + const children = getSlot(this); + + if (type === 'drag') { + const dragCls = classNames(prefixCls, { + [`${prefixCls}-drag`]: true, + [`${prefixCls}-drag-uploading`]: fileList.some((file: any) => file.status === 'uploading'), + [`${prefixCls}-drag-hover`]: dragState === 'dragover', + [`${prefixCls}-disabled`]: disabled, + }); + return ( + +
+ +
{children}
+
+
+ {uploadList} +
+ ); + } + + const uploadButtonCls = classNames(prefixCls, { + [`${prefixCls}-select`]: true, + [`${prefixCls}-select-${listType}`]: true, + [`${prefixCls}-disabled`]: disabled, + }); + + // Remove id to avoid open by label when trigger is hidden + // https://github.com/ant-design/ant-design/issues/14298 + if (!children.length || disabled) { + delete vcUploadProps.id; + } + + const uploadButton = ( +
+ {children} +
+ ); + + if (listType === 'picture-card') { + return ( + + {uploadList} + {uploadButton} + + ); + } + return ( + + {uploadButton} + {uploadList} + + ); + }, +}); diff --git a/components/upload copy/UploadList/ListItem.tsx b/components/upload copy/UploadList/ListItem.tsx new file mode 100644 index 0000000000..63bd7b33aa --- /dev/null +++ b/components/upload copy/UploadList/ListItem.tsx @@ -0,0 +1,287 @@ +import { computed, defineComponent, onBeforeUnmount, onMounted, ref } from 'vue'; +import type { ExtractPropTypes, PropType, CSSProperties } from 'vue'; +import EyeOutlined from '@ant-design/icons-vue/EyeOutlined'; +import DeleteOutlined from '@ant-design/icons-vue/DeleteOutlined'; +import DownloadOutlined from '@ant-design/icons-vue/DownloadOutlined'; +import Tooltip from '../../tooltip'; +import Progress from '../../progress'; + +import type { + ItemRender, + UploadFile, + UploadListProgressProps, + UploadListType, + UploadLocale, +} from '../interface'; +import type { VueNode } from '../../_util/type'; +import useConfigInject from '../../_util/hooks/useConfigInject'; +import Transition, { getTransitionProps } from '../../_util/transition'; +export const listItemProps = () => { + return { + prefixCls: String, + locale: { type: Object as PropType, default: undefined as UploadLocale }, + file: Object as PropType, + items: Array as PropType, + listType: String as PropType, + isImgUrl: Function as PropType<(file: UploadFile) => boolean>, + + showRemoveIcon: Boolean, + showDownloadIcon: Boolean, + showPreviewIcon: Boolean, + removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + + iconRender: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + actionIconRender: Function as PropType< + (opt: { + customIcon: VueNode; + callback: () => void; + prefixCls: string; + title?: string | undefined; + }) => VueNode + >, + itemRender: Function as PropType, + onPreview: Function as PropType<(file: UploadFile, e: Event) => void>, + onClose: Function as PropType<(file: UploadFile) => void>, + onDownload: Function as PropType<(file: UploadFile) => void>, + progress: Object as PropType, + }; +}; + +export type ListItemProps = Partial>>; + +export default defineComponent({ + name: 'ListItem', + inheritAttrs: false, + props: listItemProps(), + setup(props, { slots, attrs }) { + const showProgress = ref(false); + const progressRafRef = ref(); + onMounted(() => { + progressRafRef.value = setTimeout(() => { + showProgress.value = true; + }, 300); + }); + onBeforeUnmount(() => { + clearTimeout(progressRafRef.value); + }); + const { rootPrefixCls } = useConfigInject('upload', props); + const transitionProps = computed(() => getTransitionProps(`${rootPrefixCls.value}-fade`)); + return () => { + const { + prefixCls, + locale, + listType, + file, + items, + progress: progressProps, + iconRender, + actionIconRender, + itemRender = slots.itemRender, + isImgUrl, + showPreviewIcon, + showRemoveIcon, + showDownloadIcon, + previewIcon: customPreviewIcon = slots.previewIcon, + removeIcon: customRemoveIcon = slots.removeIcon, + downloadIcon: customDownloadIcon = slots.downloadIcon, + onPreview, + onDownload, + onClose, + } = props; + const { class: className, style } = attrs; + // This is used for legacy span make scrollHeight the wrong value. + // We will force these to be `display: block` with non `picture-card` + const spanClassName = `${prefixCls}-span`; + + const iconNode = iconRender({ file }); + let icon =
{iconNode}
; + if (listType === 'picture' || listType === 'picture-card') { + if (file.status === 'uploading' || (!file.thumbUrl && !file.url)) { + const uploadingClassName = { + [`${prefixCls}-list-item-thumbnail`]: true, + [`${prefixCls}-list-item-file`]: file.status !== 'uploading', + }; + icon =
{iconNode}
; + } else { + const thumbnail = isImgUrl?.(file) ? ( + {file.name} + ) : ( + iconNode + ); + const aClassName = { + [`${prefixCls}-list-item-thumbnail`]: true, + [`${prefixCls}-list-item-file`]: isImgUrl && !isImgUrl(file), + }; + icon = ( + onPreview(file, e)} + href={file.url || file.thumbUrl} + target="_blank" + rel="noopener noreferrer" + > + {thumbnail} + + ); + } + } + + const infoUploadingClass = { + [`${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 removeIcon = showRemoveIcon + ? actionIconRender({ + customIcon: customRemoveIcon ? customRemoveIcon({ file }) : , + callback: () => onClose(file), + prefixCls, + title: locale.removeFile, + }) + : null; + + const downloadIcon = + showDownloadIcon && file.status === 'done' + ? actionIconRender({ + customIcon: customDownloadIcon ? customDownloadIcon({ file }) : , + callback: () => onDownload(file), + prefixCls, + title: locale.downloadFile, + }) + : null; + const downloadOrDelete = listType !== 'picture-card' && ( + + {downloadIcon} + {removeIcon} + + ); + const listItemNameClass = `${prefixCls}-list-item-name`; + const preview = file.url + ? [ + onPreview(file, e)} + > + {file.name} + , + downloadOrDelete, + ] + : [ + onPreview(file, e)} + title={file.name} + > + {file.name} + , + downloadOrDelete, + ]; + const previewStyle: CSSProperties = { + pointerEvents: 'none', + opacity: 0.5, + }; + const previewIcon = showPreviewIcon ? ( + onPreview(file, e)} + title={locale.previewFile} + > + {customPreviewIcon ? customPreviewIcon({ file }) : } + + ) : null; + + 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?.statusText || file.error?.message || locale.uploadError; + } + const iconAndPreview = ( + + {icon} + {preview} + + ); + + const dom = ( +
+
{iconAndPreview}
+ {actions} + {showProgress.value && ( + +
+ {'percent' in file ? ( + + ) : null} +
+
+ )} +
+ ); + const listContainerNameClass = { + [`${prefixCls}-list-${listType}-container`]: true, + [`${className}`]: !!className, + }; + const item = + file.status === 'error' ? ( + node.parentNode as HTMLElement}> + {dom} + + ) : ( + dom + ); + + return ( +
+ {itemRender + ? itemRender({ + originNode: item, + file, + fileList: items, + actions: { + download: onDownload.bind(null, file), + preview: onPreview.bind(null, file), + remove: onClose.bind(null, file), + }, + }) + : item} +
+ ); + }; + }, +}); diff --git a/components/upload copy/UploadList/index.tsx b/components/upload copy/UploadList/index.tsx new file mode 100644 index 0000000000..89955c2127 --- /dev/null +++ b/components/upload copy/UploadList/index.tsx @@ -0,0 +1,261 @@ +import * as React from 'react'; +import CSSMotion, { CSSMotionList, CSSMotionListProps } from 'rc-motion'; +import classNames from 'classnames'; +import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; +import PaperClipOutlined from '@ant-design/icons/PaperClipOutlined'; +import PictureTwoTone from '@ant-design/icons/PictureTwoTone'; +import FileTwoTone from '@ant-design/icons/FileTwoTone'; +import { cloneElement, isValidElement } from '../../_util/reactNode'; +import { UploadListProps, UploadFile, UploadListType, InternalUploadFile } from '../interface'; +import { previewImage, isImageUrl } from '../utils'; +import collapseMotion from '../../_util/motion'; +import { ConfigContext } from '../../config-provider'; +import Button, { ButtonProps } from '../../button'; +import useForceUpdate from '../../_util/hooks/useForceUpdate'; +import ListItem from './ListItem'; + +const listItemMotion: Partial = { + ...collapseMotion, +}; + +delete listItemMotion.onAppearEnd; +delete listItemMotion.onEnterEnd; +delete listItemMotion.onLeaveEnd; + +const InternalUploadList: React.ForwardRefRenderFunction = ( + { + listType, + previewFile, + onPreview, + onDownload, + onRemove, + locale, + iconRender, + isImageUrl: isImgUrl, + prefixCls: customizePrefixCls, + items = [], + showPreviewIcon, + showRemoveIcon, + showDownloadIcon, + removeIcon, + previewIcon, + downloadIcon, + progress, + appendAction, + itemRender, + }, + ref, +) => { + const forceUpdate = useForceUpdate(); + const [motionAppear, setMotionAppear] = React.useState(false); + + // ============================= Effect ============================= + React.useEffect(() => { + if (listType !== 'picture' && listType !== 'picture-card') { + return; + } + (items || []).forEach((file: InternalUploadFile) => { + if ( + typeof document === 'undefined' || + typeof window === 'undefined' || + !(window as any).FileReader || + !(window as any).File || + !(file.originFileObj instanceof File || (file.originFileObj as Blob) instanceof Blob) || + file.thumbUrl !== undefined + ) { + return; + } + file.thumbUrl = ''; + if (previewFile) { + previewFile(file.originFileObj as File).then((previewDataUrl: string) => { + // Need append '' to avoid dead loop + file.thumbUrl = previewDataUrl || ''; + forceUpdate(); + }); + } + }); + }, [listType, items, previewFile]); + + React.useEffect(() => { + setMotionAppear(true); + }, []); + + // ============================= Events ============================= + const onInternalPreview = (file: UploadFile, e?: React.SyntheticEvent) => { + if (!onPreview) { + return; + } + e?.preventDefault(); + return onPreview(file); + }; + + const onInternalDownload = (file: UploadFile) => { + if (typeof onDownload === 'function') { + onDownload(file); + } else if (file.url) { + window.open(file.url); + } + }; + + const onInternalClose = (file: UploadFile) => { + onRemove?.(file); + }; + + const internalIconRender = (file: UploadFile) => { + if (iconRender) { + return iconRender(file, listType); + } + const isLoading = file.status === 'uploading'; + const fileIcon = isImgUrl && isImgUrl(file) ? : ; + let icon: React.ReactNode = isLoading ? : ; + if (listType === 'picture') { + icon = isLoading ? : fileIcon; + } else if (listType === 'picture-card') { + icon = isLoading ? locale.uploading : fileIcon; + } + return icon; + }; + + const actionIconRender = ( + customIcon: React.ReactNode, + callback: () => void, + prefixCls: string, + title?: string, + ) => { + const btnProps: ButtonProps = { + type: 'text', + size: 'small', + title, + onClick: (e: React.MouseEvent) => { + callback(); + if (isValidElement(customIcon) && customIcon.props.onClick) { + customIcon.props.onClick(e); + } + }, + className: `${prefixCls}-list-item-card-actions-btn`, + }; + if (isValidElement(customIcon)) { + const btnIcon = cloneElement(customIcon, { + ...customIcon.props, + onClick: () => {}, + }); + + return + ); + }; + + // ============================== Ref =============================== + // Test needs + React.useImperativeHandle(ref, () => ({ + handlePreview: onInternalPreview, + handleDownload: onInternalDownload, + })); + + const { getPrefixCls, direction } = React.useContext(ConfigContext); + + // ============================= Render ============================= + const prefixCls = getPrefixCls('upload', customizePrefixCls); + + const listClassNames = classNames({ + [`${prefixCls}-list`]: true, + [`${prefixCls}-list-${listType}`]: true, + [`${prefixCls}-list-rtl`]: direction === 'rtl', + }); + + // >>> Motion config + const motionKeyList = [ + ...items.map(file => ({ + key: file.uid, + file, + })), + ]; + + const animationDirection = listType === 'picture-card' ? 'animate-inline' : 'animate'; + // const transitionName = list.length === 0 ? '' : `${prefixCls}-${animationDirection}`; + + let motionConfig: Omit = { + motionDeadline: 2000, + motionName: `${prefixCls}-${animationDirection}`, + keys: motionKeyList, + motionAppear, + }; + + if (listType !== 'picture-card') { + motionConfig = { + ...listItemMotion, + ...motionConfig, + }; + } + + return ( +
+ + {({ key, file, className: motionClassName, style: motionStyle }) => ( + + )} + + + {/* Append action */} + {appendAction && ( + + {({ className: motionClassName, style: motionStyle }) => + cloneElement(appendAction, oriProps => ({ + className: classNames(oriProps.className, motionClassName), + style: { + ...motionStyle, + ...oriProps.style, + }, + })) + } + + )} +
+ ); +}; + +const UploadList = React.forwardRef(InternalUploadList); + +UploadList.displayName = 'UploadList'; + +UploadList.defaultProps = { + listType: 'text' as UploadListType, // or picture + progress: { + strokeWidth: 2, + showInfo: false, + }, + showRemoveIcon: true, + showDownloadIcon: false, + showPreviewIcon: true, + previewFile: previewImage, + isImageUrl, +}; + +export default UploadList; diff --git a/components/upload copy/__tests__/__snapshots__/demo.test.js.snap b/components/upload copy/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 0000000000..924cda5041 --- /dev/null +++ b/components/upload copy/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,126 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/upload/demo/avatar.vue correctly 1`] = ` +
+
Upload
+
+`; + +exports[`renders ./components/upload/demo/basic.vue correctly 1`] = ` +
+
+`; + +exports[`renders ./components/upload/demo/defaultFileList.vue correctly 1`] = ` +
+
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+
+`; + +exports[`renders ./components/upload/demo/directory.vue correctly 1`] = ` +
+
+`; + +exports[`renders ./components/upload/demo/drag.vue correctly 1`] = ` +

+

Click or drag file to this area to upload

+

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

+
+
+`; + +exports[`renders ./components/upload/demo/fileList.vue correctly 1`] = ` +
+
+
+ + +
+
+`; + +exports[`renders ./components/upload/demo/picture-card.vue correctly 1`] = ` +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
image.png + +
+ + +
+
+
+
+
Upload
+
+ +
+`; + +exports[`renders ./components/upload/demo/picture-style.vue correctly 1`] = ` +
+
+
+ + +
+
+ + +
+


+
+
+ + +
+
+ + +
+
+`; + +exports[`renders ./components/upload/demo/preview-file.vue correctly 1`] = ` +
+
+`; + +exports[`renders ./components/upload/demo/transform-file.vue correctly 1`] = ` +
+
+`; diff --git a/components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap b/components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap new file mode 100644 index 0000000000..9ec7b50d1b --- /dev/null +++ b/components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap @@ -0,0 +1,75 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Upload List handle error 1`] = ` +
foo.png
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Upload List handle error 2`] = ` +
foo.png
+
+
+
+
+
+
+
+
+
+
+
+
+`; + +exports[`Upload List should be uploading when upload a file 1`] = `
`; + +exports[`Upload List should non-image format file preview 1`] = ` +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+`; diff --git a/components/upload copy/__tests__/demo.test.js b/components/upload copy/__tests__/demo.test.js new file mode 100644 index 0000000000..8fd9b81aba --- /dev/null +++ b/components/upload copy/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('upload', { skip: ['upload-manually'] }); diff --git a/components/upload copy/__tests__/mock.js b/components/upload copy/__tests__/mock.js new file mode 100644 index 0000000000..1f2e57c8aa --- /dev/null +++ b/components/upload copy/__tests__/mock.js @@ -0,0 +1,14 @@ +import mock from 'xhr-mock'; + +export function setup() { + mock.setup(); + mock.post('http://upload.com/', (req, res) => { + req.headers({ + 'content-length': 100, + }); + req.body('thisisbody'); + return res; + }); +} + +export const teardown = mock.teardown.bind(mock); diff --git a/components/upload copy/__tests__/requests.js b/components/upload copy/__tests__/requests.js new file mode 100644 index 0000000000..d90ed171ff --- /dev/null +++ b/components/upload copy/__tests__/requests.js @@ -0,0 +1,11 @@ +export const successRequest = ({ onSuccess, file }) => { + setTimeout(() => { + onSuccess(null, file); + }); +}; + +export const errorRequest = ({ onError }) => { + setTimeout(() => { + onError(); + }); +}; diff --git a/components/upload copy/__tests__/upload.test.js b/components/upload copy/__tests__/upload.test.js new file mode 100644 index 0000000000..e82a8907e8 --- /dev/null +++ b/components/upload copy/__tests__/upload.test.js @@ -0,0 +1,331 @@ +import { mount } from '@vue/test-utils'; +import Upload from '..'; +import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from '../utils'; +import PropsTypes from '../../_util/vue-types'; +import { uploadListProps } from '../interface'; +import { setup, teardown } from './mock'; + +uploadListProps.items = PropsTypes.any; + +describe('Upload', () => { + beforeEach(() => setup()); + afterEach(() => teardown()); + it('should get refs inside Upload in componentDidMount', () => { + let ref = null; + const APP = { + mounted() { + ref = this.$refs.input; + }, + render() { + return ( + + + + ); + }, + }; + mount(APP); + expect(ref).toBeDefined(); + }); + + xit('return promise in beforeUpload', done => { + const data = jest.fn(); + const props = { + props: { + action: 'http://upload.com', + beforeUpload: () => new Promise(resolve => setTimeout(() => resolve('success'), 100)), + data, + }, + listeners: { + change: ({ file }) => { + if (file.status !== 'uploading') { + expect(data).toBeCalled(); + done(); + } + }, + }, + slots: { + default: () => , + }, + sync: false, + }; + const wrapper = mount(Upload, props); + setTimeout(() => { + wrapper.findComponent('ajaxUploader').vm.onChange({ + target: { + files: [{ file: 'foo.png' }], + }, + }); + }, 0); + }); + + xit('upload promise return file in beforeUpload', done => { + const data = jest.fn(); + const props = { + action: 'http://upload.com', + beforeUpload: file => + new Promise(resolve => + setTimeout(() => { + const result = file; + result.name = 'test.png'; + resolve(result); + }, 100), + ), + data, + onChange: ({ file }) => { + if (file.status !== 'uploading') { + expect(data).toBeCalled(); + expect(file.name).toEqual('test.png'); + done(); + } + }, + slots: { + default: () => , + }, + sync: false, + }; + + const wrapper = mount(Upload, props); + + setTimeout(() => { + wrapper.find({ name: 'ajaxUploader' }).vm.onChange({ + target: { + files: [{ file: 'foo.png' }], + }, + }); + }, 0); + }); + + xit('should not stop upload when return value of beforeUpload is false', done => { + const data = jest.fn(); + const props = { + action: 'http://upload.com', + beforeUpload: () => false, + data, + onChange: ({ file }) => { + expect(file instanceof File).toBe(true); + expect(data).not.toBeCalled(); + done(); + }, + slots: { + default: () => , + }, + sync: false, + }; + const wrapper = mount(Upload, props); + setTimeout(() => { + const mockFile = new File(['foo'], 'foo.png', { + type: 'image/png', + }); + wrapper.find({ name: 'ajaxUploader' }).vm.onChange({ + target: { + files: [mockFile], + }, + }); + }, 0); + }); + + xit('should increase percent automaticly when call autoUpdateProgress in IE', done => { + let uploadInstance; + let lastPercent = -1; + const props = { + props: { + action: 'http://upload.com', + }, + listeners: { + change: ({ file }) => { + if (file.percent === 0 && file.status === 'uploading') { + // manually call it + uploadInstance.autoUpdateProgress(0, file); + } + if (file.status === 'uploading') { + expect(file.percent).toBeGreaterThan(lastPercent); + lastPercent = file.percent; + } + if (file.status === 'done' || file.status === 'error') { + done(); + } + }, + }, + slots: { + default: '', + }, + sync: false, + }; + const wrapper = mount(Upload, props); + setTimeout(() => { + const mockFile = new File(['foo'], 'foo.png', { + type: 'image/png', + }); + wrapper.find({ name: 'ajaxUploader' }).vm.onChange({ + target: { + files: [mockFile], + }, + }); + uploadInstance = wrapper.vm; + }, 0); + }); + xit('should not stop upload when return value of beforeUpload is not false', done => { + const data = jest.fn(); + const props = { + props: { + action: 'http://upload.com', + beforeUpload() {}, + data, + }, + listeners: { + change: () => { + expect(data).toBeCalled(); + done(); + }, + }, + slots: { + default: '', + }, + sync: false, + }; + + const wrapper = mount(Upload, props); + setTimeout(() => { + const mockFile = new File(['foo'], 'foo.png', { + type: 'image/png', + }); + wrapper.find({ name: 'ajaxUploader' }).vm.onChange({ + target: { + files: [mockFile], + }, + }); + }, 0); + }); + + describe('util', () => { + // https://github.com/react-component/upload/issues/36 + it('should T() return true', () => { + const res = T(); + expect(res).toBe(true); + }); + it('should be able to copy file instance', () => { + const file = new File([], 'aaa.zip'); + const copiedFile = fileToObject(file); + ['uid', 'lastModified', 'lastModifiedDate', 'name', 'size', 'type'].forEach(key => { + expect(key in copiedFile).toBe(true); + }); + }); + it('should be able to progress from 0.1 ', () => { + // 0.1 -> 0.98 + const getPercent = genPercentAdd(); + let curPercent = 0; + curPercent = getPercent(curPercent); + expect(curPercent).toBe(0.1); + }); + + it('should be able to progress to 0.98 ', () => { + // 0.1 -> 0.98 + const getPercent = genPercentAdd(); + let curPercent = 0; + for (let i = 0; i < 500; i += 1) { + curPercent = getPercent(curPercent); + } + expect(parseFloat(curPercent.toFixed(2))).toBe(0.98); + }); + + it('should be able to get fileItem', () => { + const file = { uid: '-1', name: 'item.jpg' }; + const fileList = [ + { + uid: '-1', + name: 'item.jpg', + }, + ]; + const targetItem = getFileItem(file, fileList); + expect(targetItem).toBe(fileList[0]); + }); + + it('should be able to remove fileItem', () => { + const file = { uid: '-1', name: 'item.jpg' }; + const fileList = [ + { + uid: '-1', + name: 'item.jpg', + }, + { + uid: '-2', + name: 'item2.jpg', + }, + ]; + const targetItem = removeFileItem(file, fileList); + expect(targetItem).toEqual(fileList.slice(1)); + }); + + it('should not be able to remove fileItem', () => { + const file = { uid: '-3', name: 'item.jpg' }; + const fileList = [ + { + uid: '-1', + name: 'item.jpg', + }, + { + uid: '-2', + name: 'item2.jpg', + }, + ]; + const targetItem = removeFileItem(file, fileList); + expect(targetItem).toBe(null); + }); + }); + + it('should support linkProps as object', () => { + const fileList = [ + { + uid: '-1', + name: 'foo.png', + status: 'done', + url: 'http://www.baidu.com/xxx.png', + linkProps: { + download: 'image', + rel: 'noopener', + }, + }, + ]; + const props = { + props: { + fileList, + }, + sync: false, + }; + const wrapper = mount(Upload, props); + setTimeout(() => { + const linkNode = wrapper.find('a.ant-upload-list-item-name'); + expect(linkNode.props().download).toBe('image'); + expect(linkNode.props().rel).toBe('noopener'); + }, 0); + }); + + it('should support linkProps as json stringify', () => { + const linkPropsString = JSON.stringify({ + download: 'image', + rel: 'noopener', + }); + const fileList = [ + { + uid: '-1', + name: 'foo.png', + status: 'done', + url: 'http://www.baidu.com/xxx.png', + linkProps: linkPropsString, + }, + ]; + const props = { + props: { + fileList, + }, + sync: false, + }; + const wrapper = mount(Upload, props); + setTimeout(() => { + const linkNode = wrapper.find('a.ant-upload-list-item-name'); + expect(linkNode.props().download).toBe('image'); + expect(linkNode.props().rel).toBe('noopener'); + }, 0); + }); +}); diff --git a/components/upload copy/__tests__/uploadlist.test.js b/components/upload copy/__tests__/uploadlist.test.js new file mode 100644 index 0000000000..6fe88fca9f --- /dev/null +++ b/components/upload copy/__tests__/uploadlist.test.js @@ -0,0 +1,433 @@ +import { mount } from '@vue/test-utils'; +import * as Vue from 'vue'; +import Upload from '..'; +import { errorRequest, successRequest } from './requests'; +import PropsTypes from '../../_util/vue-types'; +import { uploadListProps } from '../interface'; +import { sleep } from '../../../tests/utils'; +import { h } from 'vue'; + +uploadListProps.items = PropsTypes.any; + +const delay = timeout => new Promise(resolve => setTimeout(resolve, timeout)); +const fileList = [ + { + uid: -1, + name: 'xxx.png', + status: 'done', + url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', + thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png', + }, + { + uid: -2, + name: 'yyy.png', + status: 'done', + url: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png', + thumbUrl: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', + }, +]; + +describe('Upload List', () => { + // jsdom not support `createObjectURL` yet. Let's handle this. + const originCreateObjectURL = window.URL.createObjectURL; + window.URL.createObjectURL = jest.fn(() => ''); + const originHTMLCanvasElementGetContext = window.HTMLCanvasElement.prototype.getContext; + window.HTMLCanvasElement.prototype.getContext = jest.fn(() => ''); + // https://github.com/ant-design/ant-design/issues/4653 + afterAll(() => { + window.URL.createObjectURL = originCreateObjectURL; + window.HTMLCanvasElement.prototype.getContext = originHTMLCanvasElementGetContext; + }); + it('should use file.thumbUrl for in priority', done => { + const props = { + props: { + defaultFileList: fileList, + listType: 'picture', + action: '', + }, + slots: { + default: () => h('button', 'upload'), + }, + sync: false, + }; + const wrapper = mount(Upload, props); + Vue.nextTick(() => { + fileList.forEach((file, i) => { + const linkNode = wrapper.findAll('.ant-upload-list-item-thumbnail')[i]; + const imgNode = wrapper.findAll('.ant-upload-list-item-thumbnail img')[i]; + expect(linkNode.attributes().href).toBe(file.url); + expect(imgNode.attributes().src).toBe(file.thumbUrl); + }); + done(); + }); + }); + + // https://github.com/ant-design/ant-design/issues/7269 + it('should remove correct item when uid is 0', done => { + const list = [ + { + uid: 0, + name: 'xxx.png', + status: 'done', + url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', + thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png', + }, + { + uid: 1, + name: 'xxx.png', + status: 'done', + url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png', + thumbUrl: 'https://zos.alipayobjects.com/rmsportal/IQKRngzUuFzJzGzRJXUs.png', + }, + ]; + const props = { + props: { + defaultFileList: list, + action: '', + }, + slots: { + default: () => h('button', 'upload'), + }, + sync: false, + }; + const wrapper = mount(Upload, props); + setTimeout(async () => { + expect(wrapper.findAll('.ant-upload-list-item').length).toBe(2); + wrapper.findAll('.ant-upload-list-item')[0].find('.anticon-delete').trigger('click'); + await delay(400); + // wrapper.update(); + expect(wrapper.findAll('.ant-upload-list-item').length).toBe(1); + done(); + }, 0); + }); + + xit('should be uploading when upload a file', done => { + const props = { + props: { + action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76', + customRequest: successRequest, + onChange: ({ file }) => { + if (file.status === 'uploading') { + expect(wrapper.html()).toMatchSnapshot(); + done(); + } + if (file.status === 'done') { + expect(wrapper.html()).toMatchSnapshot(); + done(); + } + }, + }, + slots: { + default: () => h('button', 'upload'), + }, + sync: false, + }; + const wrapper = mount(Upload, props); + setTimeout(() => { + const mockFile = new File(['foo'], 'foo.png', { + type: 'image/png', + }); + wrapper.findComponent({ name: 'ajaxUploader' }).vm.onChange({ + target: { + files: [mockFile], + }, + }); + }, 0); + }); + + xit('handle error', done => { + const props = { + props: { + action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76', + customRequest: errorRequest, + }, + listeners: { + change: ({ file }) => { + if (file.status !== 'uploading') { + expect(wrapper.html()).toMatchSnapshot(); + done(); + } + }, + }, + slots: { + default: () => h('button', 'upload'), + }, + sync: false, + }; + const wrapper = mount(Upload, props); + setTimeout(() => { + const mockFile = new File(['foo'], 'foo.png', { + type: 'image/png', + }); + wrapper.findComponent({ name: 'ajaxUploader' }).vm.onChange({ + target: { + files: [mockFile], + }, + }); + }, 0); + }); + + xit('does concat filelist when beforeUpload returns false', done => { + const handleChange = jest.fn(); + const props = { + props: { + action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76', + listType: 'picture', + defaultFileList: fileList, + beforeUpload: () => false, + onChange: handleChange, + }, + slots: { + default: () => h('button', 'upload'), + }, + sync: false, + }; + const wrapper = mount(Upload, props); + + setTimeout(() => { + const mockFile = new File(['foo'], 'foo.png', { + type: 'image/png', + }); + wrapper.findComponent({ name: 'ajaxUploader' }).vm.onChange({ + target: { + files: [mockFile], + }, + }); + Vue.nextTick(() => { + expect(wrapper.vm.sFileList.length).toBe(fileList.length + 1); + expect(handleChange.mock.calls[0][0].fileList).toHaveLength(3); + done(); + }); + }, 0); + }); + + // https://github.com/ant-design/ant-design/issues/7762 + // it('work with form validation', (done) => { + // let errors + // const TestForm = { + // methods: { + // handleSubmit () { + // const { validateFields } = this.form + // validateFields((err) => { + // errors = err + // }) + // }, + // }, + // render () { + // const { getFieldDecorator } = this.form + + // return ( + //
+ // + // {getFieldDecorator('file', { + // valuePropname: 'fileList', + // getValueFromEvent: e => e.fileList, + // rules: [ + // { + // required: true, + // validator: (rule, value, callback) => { + // if (!value || value.length === 0) { + // callback('file required') + // } else { + // callback() + // } + // }, + // }, + // ], + // })( + // false} + // > + // + // + // )} + // + //
+ // ) + // }, + // } + + // const App = Form.create()(TestForm) + // console.dir(App) + // const wrapper = mount(() => { + // return + // }) + // setTimeout(async () => { + // wrapper.find(Form).trigger('submit') + // expect(errors.file.errors).toEqual([{ message: 'file required', field: 'file' }]) + + // const mockFile = new File(['foo'], 'foo.png', { + // type: 'image/png', + // }) + // wrapper.findComponent({ name: 'ajaxUploader' }).vm.onChange({ + // target: { + // files: [mockFile], + // }, + // }) + // wrapper.find(Form).trigger('submit') + // expect(errors).toBeNull() + // done() + // }, 0) + // }) + + it('should support onPreview', async () => { + const handlePreview = jest.fn(); + const props = { + props: { + defaultFileList: fileList, + listType: 'picture-card', + action: '', + onPreview: handlePreview, + }, + slots: { + default: () => h('button', 'upload'), + }, + sync: false, + }; + const wrapper = mount(Upload, props); + await sleep(500); + wrapper.findAll('.anticon-eye')[0].trigger('click'); + expect(handlePreview).toBeCalledWith(fileList[0]); + wrapper.findAll('.anticon-eye')[1].trigger('click'); + expect(handlePreview).toBeCalledWith(fileList[1]); + }); + + it('should support onRemove', done => { + const handleRemove = jest.fn(); + const handleChange = jest.fn(); + const props = { + props: { + defaultFileList: fileList, + listType: 'picture-card', + action: '', + remove: handleRemove, + onChange: handleChange, + }, + + slots: { + default: () => h('button', 'upload'), + }, + sync: false, + }; + const wrapper = mount(Upload, props); + jest.setTimeout(300000); + setTimeout(async () => { + wrapper.findAll('.anticon-delete')[0].trigger('click'); + expect(handleRemove).toBeCalledWith(fileList[0]); + wrapper.findAll('.anticon-delete')[1].trigger('click'); + expect(handleRemove).toBeCalledWith(fileList[1]); + await delay(0); + expect(handleChange.mock.calls.length).toBe(2); + done(); + }, 0); + }); + + xit('should generate thumbUrl from file', done => { + const handlePreview = jest.fn(); + const newFileList = [...fileList]; + const newFile = { ...fileList[0], uid: -3, originFileObj: new File([], 'xxx.png') }; + delete newFile.thumbUrl; + newFileList.push(newFile); + const props = { + props: { + defaultFileList: newFileList, + listType: 'picture-card', + action: '', + onPreview: handlePreview, + }, + slots: { + default: () => h('button', 'upload'), + }, + sync: false, + }; + const wrapper = mount(Upload, props); + setTimeout(async () => { + const newFile = { ...fileList[2], uid: -4, originFileObj: new File([], 'xxx.png') }; + newFileList.push(newFile); + wrapper.setProps({ + defaultFileList: [...newFileList], + }); + await delay(200); + expect(wrapper.vm.sFileList[2].thumbUrl).not.toBe(undefined); + done(); + }, 1000); + }); + + it('should non-image format file preview', done => { + const list = [ + { + name: 'not-image', + status: 'done', + uid: -3, + url: 'https://cdn.xxx.com/aaa.zip', + thumbUrl: 'data:application/zip;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk', + originFileObj: new File([], 'aaa.zip'), + }, + { + name: 'image', + status: 'done', + uid: -4, + url: 'https://cdn.xxx.com/aaa', + }, + { + name: 'not-image', + status: 'done', + uid: -5, + url: 'https://cdn.xxx.com/aaa.xx', + }, + { + name: 'not-image', + status: 'done', + uid: -6, + url: 'https://cdn.xxx.com/aaa.png/xx.xx', + }, + { + name: 'image', + status: 'done', + uid: -7, + url: 'https://cdn.xxx.com/xx.xx/aaa.png', + }, + { + name: 'image', + status: 'done', + uid: -8, + url: 'https://cdn.xxx.com/xx.xx/aaa.png', + thumbUrl: 'data:image/png;base64,UEsDBAoAAAAAADYZYkwAAAAAAAAAAAAAAAAdAAk', + }, + { + name: 'image', + status: 'done', + uid: -9, + url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=123', + }, + { + name: 'image', + status: 'done', + uid: -10, + url: 'https://cdn.xxx.com/xx.xx/aaa.png#anchor', + }, + { + name: 'image', + status: 'done', + uid: -11, + url: 'https://cdn.xxx.com/xx.xx/aaa.png?query=some.query.with.dot', + }, + ]; + const props = { + props: { + defaultFileList: list, + listType: 'picture', + action: '', + }, + slots: { + default: () => h('button', 'upload'), + }, + sync: false, + }; + const wrapper = mount(Upload, props); + Vue.nextTick(() => { + expect(wrapper.html()).toMatchSnapshot(); + done(); + }); + }); +}); diff --git a/components/upload copy/demo/avatar.vue b/components/upload copy/demo/avatar.vue new file mode 100644 index 0000000000..cd68f1a221 --- /dev/null +++ b/components/upload copy/demo/avatar.vue @@ -0,0 +1,116 @@ + +--- +order: 1 +title: + zh-CN: 用户头像 + en-US: Avatar +--- + +## zh-CN + +点击上传用户头像,并使用 `beforeUpload` 限制用户上传的图片格式和大小。 + +> `beforeUpload` 的返回值可以是一个 Promise 以支持异步处理,如服务端校验等:[示例](http://react-component.github.io/upload/examples/beforeUpload.html)。 + +## en-US + +Click to upload user's avatar, and validate size and format of picture with `beforeUpload`. + +> The return value of function `beforeUpload` can be a Promise to check asynchronously. [demo](http://react-component.github.io/upload/examples/beforeUpload.html) + + + + + diff --git a/components/upload copy/demo/basic.vue b/components/upload copy/demo/basic.vue new file mode 100644 index 0000000000..3c9862438f --- /dev/null +++ b/components/upload copy/demo/basic.vue @@ -0,0 +1,65 @@ + +--- +order: 0 +title: + zh-CN: 点击上传 + en-US: Upload by clicking +--- + +## zh-CN + +经典款式,用户点击按钮弹出文件选择框。 + +## en-US + +Classic mode. File selection dialog pops up when upload button is clicked. + + + + diff --git a/components/upload copy/demo/defaultFileList.vue b/components/upload copy/demo/defaultFileList.vue new file mode 100644 index 0000000000..0cb1fab908 --- /dev/null +++ b/components/upload copy/demo/defaultFileList.vue @@ -0,0 +1,70 @@ + +--- +order: 2 +title: + zh-CN: 已上传的文件列表 + en-US: Default Files +--- + +## zh-CN + +使用 `defaultFileList` 设置已上传的内容。 + +## en-US + +Use `defaultFileList` for uploaded files when page init. + + + + diff --git a/components/upload copy/demo/directory.vue b/components/upload copy/demo/directory.vue new file mode 100644 index 0000000000..feaacb0781 --- /dev/null +++ b/components/upload copy/demo/directory.vue @@ -0,0 +1,35 @@ + +--- +order: 8 +title: + zh-CN: 文件夹上传 + en-US: Upload directory +--- + +## zh-CN + +支持上传一个文件夹里的所有文件。 + +## en-US + +You can select and upload a whole directory. + + + + diff --git a/components/upload copy/demo/drag.vue b/components/upload copy/demo/drag.vue new file mode 100644 index 0000000000..6bf30241ab --- /dev/null +++ b/components/upload copy/demo/drag.vue @@ -0,0 +1,68 @@ + +--- +order: 5 +title: + zh-CN: 拖拽上传 + en-US: Drag and Drop +--- + +## zh-CN + +把文件拖入指定区域,完成上传,同样支持点击上传。 + +设置 `multiple` 后,在 `IE10+` 可以一次上传多个文件。 + +## en-US + +You can drag files to a specific area, to upload. Alternatively, you can also upload by selecting. + +We can upload serveral files at once in modern browsers by giving the input the `multiple` attribute. + + + + diff --git a/components/upload copy/demo/fileList.vue b/components/upload copy/demo/fileList.vue new file mode 100644 index 0000000000..963343540f --- /dev/null +++ b/components/upload copy/demo/fileList.vue @@ -0,0 +1,81 @@ + +--- +order: 4 +title: + zh-CN: 完全控制的上传列表 + en-US: Complete control over file list +--- + +## zh-CN + +使用 `fileList` 对列表进行完全控制,可以实现各种自定义功能,以下演示二种情况: + +1. 上传列表数量的限制。 + +2. 读取远程路径并显示链接。 + +## en-US + +You can gain full control over filelist by configuring `fileList`. You can accomplish all kinds of customed functions. The following shows two circumstances: + +1. limit the number of uploaded files. + +2. read from response and show file link. + + + + diff --git a/components/upload copy/demo/index.vue b/components/upload copy/demo/index.vue new file mode 100644 index 0000000000..b1ae4e92e4 --- /dev/null +++ b/components/upload copy/demo/index.vue @@ -0,0 +1,52 @@ + + diff --git a/components/upload copy/demo/picture-card.vue b/components/upload copy/demo/picture-card.vue new file mode 100644 index 0000000000..34fdd976da --- /dev/null +++ b/components/upload copy/demo/picture-card.vue @@ -0,0 +1,126 @@ + +--- +order: 3 +title: + zh-CN: 照片墙 + en-US: Pictures Wall +--- + +## zh-CN + +用户可以上传图片并在列表中显示缩略图。当上传照片数到达限制后,上传按钮消失。 + +## en-US + +After users upload picture, the thumbnail will be shown in list. The upload button will disappear when count meets limitation. + + + + + diff --git a/components/upload copy/demo/picture-style.vue b/components/upload copy/demo/picture-style.vue new file mode 100644 index 0000000000..9ad5b92a92 --- /dev/null +++ b/components/upload copy/demo/picture-style.vue @@ -0,0 +1,108 @@ + +--- +order: 6 +title: + zh-CN: 图片列表样式 + en-US: Pictures with list style +--- + +## zh-CN + +上传文件为图片,可展示本地缩略图。`IE8/9` 不支持浏览器本地缩略图展示([Ref](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL)),可以写 `thumbUrl` 属性来代替。 + +## en-US + +If uploaded file is a picture, the thumbnail can be shown. `IE8/9` do not support local thumbnail show. Please use `thumbUrl` instead. + + + + + + diff --git a/components/upload copy/demo/preview-file.vue b/components/upload copy/demo/preview-file.vue new file mode 100644 index 0000000000..9f2ceb2fbe --- /dev/null +++ b/components/upload copy/demo/preview-file.vue @@ -0,0 +1,58 @@ + +--- +order: 9 +title: + zh-CN: 自定义预览 + en-US: Customize preview file +--- + +## zh-CN + +自定义本地预览,用于处理非图片格式文件(例如视频文件)。 + +## en-US + +Customize local preview. Can handle with non-image format files such as video. + + + + diff --git a/components/upload copy/demo/transform-file.vue b/components/upload copy/demo/transform-file.vue new file mode 100644 index 0000000000..857a6b89ae --- /dev/null +++ b/components/upload copy/demo/transform-file.vue @@ -0,0 +1,66 @@ + +--- +order: 10 +title: + zh-CN: 上传前转换文件 + en-US: Transform file before request +--- + +## zh-CN + +使用 `beforeUpload` 转换上传的文件(例如添加水印)。 + +## en-US + +Use `beforeUpload` for transform file before request such as add a watermark. + + + + diff --git a/components/upload copy/demo/upload-manually.vue b/components/upload copy/demo/upload-manually.vue new file mode 100644 index 0000000000..d84c67de83 --- /dev/null +++ b/components/upload copy/demo/upload-manually.vue @@ -0,0 +1,96 @@ + +--- +order: 7 +title: + zh-CN: 手动上传 + en-US: Upload manually +--- + +## zh-CN + +`beforeUpload` 返回 `false` 后,手动上传文件。 + +## en-US + +Upload files manually after `beforeUpload` returns `false`. + + + + diff --git a/components/upload copy/index.en-US.md b/components/upload copy/index.en-US.md new file mode 100644 index 0000000000..84500cf848 --- /dev/null +++ b/components/upload copy/index.en-US.md @@ -0,0 +1,80 @@ +--- +category: Components +type: Data Entry +title: Upload +cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg +--- + +Upload file by selecting or dragging. + +## When To Use + +Uploading is the process of publishing information (web pages, text, pictures, video, etc.) to a remote server via a web page or upload tool. + +- When you need to upload one or more files. +- When you need to show the process of uploading. +- When you need to upload files by dragging and dropping. + +## API + +| 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) | - | | +| 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 | 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 + +> The function will be called when uploading is in progress, completed or failed + +When uploading state change, it returns: + +```jsx +{ + file: { /* ... */ }, + fileList: [ /* ... */ ], + event: { /* ... */ }, +} +``` + +1. `file` File object for the current operation. + + ```jsx + { + uid: 'uid', // unique identifier, negative is recommend, to prevent interference with internal generated id + 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 + } + ``` + +2. `fileList` current list of files +3. `event` response from server, including uploading progress, supported by advanced browsers. diff --git a/components/upload copy/index.tsx b/components/upload copy/index.tsx new file mode 100644 index 0000000000..ba1b8b09ac --- /dev/null +++ b/components/upload copy/index.tsx @@ -0,0 +1,17 @@ +import type { App } from 'vue'; +import Upload from './Upload'; +import Dragger from './Dragger'; + +export type { UploadProps, UploadListProps, UploadChangeParam } from './interface'; + +/* istanbul ignore next */ +export const UploadDragger = Dragger; + +export default Object.assign(Upload, { + Dragger, + install(app: App) { + app.component(Upload.name, Upload); + app.component(Dragger.name, Dragger); + return app; + }, +}); diff --git a/components/upload copy/index.zh-CN.md b/components/upload copy/index.zh-CN.md new file mode 100644 index 0000000000..73db3ef2ce --- /dev/null +++ b/components/upload copy/index.zh-CN.md @@ -0,0 +1,81 @@ +--- +category: Components +subtitle: 上传 +type: 数据录入 +title: Upload +cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg +--- + +文件选择上传和拖拽上传控件。 + +## 何时使用 + +上传是将信息(网页、文字、图片、视频等)通过网页或者上传工具发布到远程服务器上的过程。 + +- 当需要上传一个或一些文件时。 +- 当需要展现上传的进度时。 +- 当需要使用拖拽交互时。 + +## API + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 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 | 无 | | +| 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) | 无 | | +| download | 点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页。 | Function(file): void | 跳转新标签页 | 1.5.0 | +| reject | 拖拽文件不符合 accept 类型时的回调 | Function(fileList) | 无 | | + +### change + +> 上传中、完成、失败都会调用这个函数。 + +文件状态改变的回调,返回为: + +```jsx +{ + file: { /* ... */ }, + fileList: [ /* ... */ ], + event: { /* ... */ }, +} +``` + +1. `file` 当前操作的文件对象。 + + ```jsx + { + uid: 'uid', // 文件唯一标识,建议设置为负数,防止和内部产生的 id 冲突 + name: 'xx.png', // 文件名 + status: 'done', // 状态有:uploading done error removed + response: '{"status": "success"}', // 服务端响应内容 + linkProps: '{"download": "image"}', // 下载链接额外的 HTML 属性 + xhr: 'XMLHttpRequest{ ... }', // XMLHttpRequest Header + } + ``` + +2. `fileList` 当前的文件列表。 +3. `event` 上传中的服务端响应内容,包含了上传进度等信息,高级浏览器支持。 diff --git a/components/upload copy/interface.tsx b/components/upload copy/interface.tsx new file mode 100755 index 0000000000..91e78958f8 --- /dev/null +++ b/components/upload copy/interface.tsx @@ -0,0 +1,183 @@ +import type { + RcFile as OriRcFile, + UploadRequestOption as RcCustomRequestOptions, +} from '../vc-upload/interface'; +import type { ProgressProps } from '../progress'; +import type { VueNode } from '../_util/type'; +import type { ExtractPropTypes, PropType } from 'vue'; + +export interface RcFile extends OriRcFile { + readonly lastModifiedDate: Date; +} + +export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed'; + +export interface HttpRequestHeader { + [key: string]: 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?: RcFile; + response?: T; + error?: any; + linkProps?: any; + type?: string; + xhr?: T; + preview?: string; +} + +export interface InternalUploadFile extends UploadFile { + originFileObj: RcFile; +} + +export interface UploadChangeParam { + // https://github.com/ant-design/ant-design/issues/14420 + file: T; + fileList: UploadFile[]; + event?: { percent: number }; +} + +// export interface ShowUploadListInterface { +// showRemoveIcon?: boolean; +// showPreviewIcon?: boolean; +// showDownloadIcon?: boolean; +// removeIcon?: VueNode | ((file: UploadFile) => VueNode); +// downloadIcon?: VueNode | ((file: UploadFile) => VueNode); +// previewIcon?: VueNode | ((file: UploadFile) => VueNode); +// } + +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'; +export type UploadListProgressProps = Omit; + +export type ItemRender = (opt: { + originNode: VueNode; + file: UploadFile; + fileList: Array>; + actions: { + download: () => void; + preview: () => void; + remove: () => void; + }; +}) => VueNode; + +type PreviewFileHandler = (file: File | Blob) => PromiseLike; +type TransformFileHandler = ( + file: RcFile, +) => string | Blob | File | PromiseLike; +type BeforeUploadValueType = void | boolean | string | Blob | File; + +function uploadProps() { + return { + capture: [Boolean, String] as PropType, + type: String as PropType, + name: String, + defaultFileList: Array as PropType>>, + fileList: Array as PropType>>, + action: [String, Function] as PropType< + string | ((file: RcFile) => string) | ((file: RcFile) => PromiseLike) + >, + directory: Boolean, + data: [Object, Function] as PropType< + | Record + | ((file: UploadFile) => Record | Promise>) + >, + method: String as PropType<'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch'>, + headers: Object as PropType, + showUploadList: Boolean, + multiple: Boolean, + accept: String, + beforeUpload: Function as PropType< + (file: RcFile, FileList: RcFile[]) => BeforeUploadValueType | Promise + >, + onChange: Function as PropType<(info: UploadChangeParam) => void>, + onDrop: Function as PropType<(event: DragEvent) => void>, + listType: String as PropType, + onPreview: Function as PropType<(file: UploadFile) => void>, + onDownload: Function as PropType<(file: UploadFile) => void>, + onRemove: Function as PropType< + (file: UploadFile) => void | boolean | Promise + >, + supportServerRender: Boolean, + disabled: Boolean, + prefixCls: String, + customRequest: Function as PropType<(options: RcCustomRequestOptions) => void>, + withCredentials: Boolean, + openFileDialogOnClick: Boolean, + locale: Object as PropType, + id: String, + previewFile: Function as PropType, + /** @deprecated Please use `beforeUpload` directly */ + transformFile: Function as PropType, + iconRender: Function as PropType< + (opt: { file: UploadFile; listType?: UploadListType }) => VueNode + >, + isImageUrl: Function as PropType<(file: UploadFile) => boolean>, + progress: Object as PropType, + itemRender: Function as PropType>, + /** Config max count of `fileList`. Will replace current one when `maxCount` is 1 */ + maxCount: Number, + height: [Number, String], + + showRemoveIcon: Boolean, + showDownloadIcon: Boolean, + showPreviewIcon: Boolean, + removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + }; +} + +export type UploadProps = Partial>>; + +export interface UploadState { + fileList: UploadFile[]; + dragState: string; +} + +function uploadListProps() { + return { + listType: String as PropType, + onPreview: Function as PropType<(file: UploadFile) => void>, + onDownload: Function as PropType<(file: UploadFile) => void>, + onRemove: Function as PropType<(file: UploadFile) => void | boolean>, + items: Array as PropType>>, + progress: Object as PropType, + prefixCls: String as PropType, + showRemoveIcon: Boolean, + showDownloadIcon: Boolean, + showPreviewIcon: Boolean, + removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, + locale: Object as PropType, + previewFile: Function as PropType, + iconRender: Function as PropType< + (opt: { file: UploadFile; listType?: UploadListType }) => VueNode + >, + isImageUrl: Function as PropType<(file: UploadFile) => boolean>, + appendAction: Function as PropType<() => VueNode>, + itemRender: Function as PropType>, + }; +} + +export type UploadListProps = Partial>>; +export { uploadProps, uploadListProps }; diff --git a/components/upload copy/style/index.less b/components/upload copy/style/index.less new file mode 100644 index 0000000000..49ba7f211b --- /dev/null +++ b/components/upload copy/style/index.less @@ -0,0 +1,589 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; + +@upload-prefix-cls: ~'@{ant-prefix}-upload'; +@upload-item: ~'@{ant-prefix}-upload-list-item'; +@upload-picture-card-size: 104px; +@upload-picture-card-border-style: @border-style-base; + +.@{upload-prefix-cls} { + .reset-component(); + + outline: 0; + + p { + margin: 0; + } + + &-btn { + display: block; + width: 100%; + outline: none; + } + + input[type='file'] { + cursor: pointer; + } + + &&-select { + display: inline-block; + } + + &&-disabled { + cursor: not-allowed; + } + + &&-select-picture-card { + width: @upload-picture-card-size; + height: @upload-picture-card-size; + margin-right: 8px; + margin-bottom: 8px; + text-align: center; + vertical-align: top; + background-color: @background-color-light; + border: @border-width-base dashed @border-color-base; + border-radius: @border-radius-base; + cursor: pointer; + transition: border-color 0.3s; + + > .@{upload-prefix-cls} { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + text-align: center; + } + + &:hover { + border-color: @primary-color; + .@{upload-prefix-cls}-disabled& { + border-color: @border-color-base; + } + } + } + + &&-drag { + position: relative; + width: 100%; + height: 100%; + text-align: center; + background: @background-color-light; + border: @border-width-base dashed @border-color-base; + border-radius: @border-radius-base; + cursor: pointer; + transition: border-color 0.3s; + + .@{upload-prefix-cls} { + padding: @padding-md 0; + } + + &.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) { + border-color: @primary-7; + } + + &.@{upload-prefix-cls}-disabled { + cursor: not-allowed; + } + + .@{upload-prefix-cls}-btn { + display: table; + height: 100%; + } + + .@{upload-prefix-cls}-drag-container { + display: table-cell; + vertical-align: middle; + } + + &:not(.@{upload-prefix-cls}-disabled):hover { + border-color: @primary-5; + } + + p.@{upload-prefix-cls}-drag-icon { + .@{iconfont-css-prefix} { + color: @primary-5; + font-size: 48px; + } + + margin-bottom: 20px; + } + p.@{upload-prefix-cls}-text { + margin: 0 0 4px; + color: @heading-color; + font-size: @font-size-lg; + } + p.@{upload-prefix-cls}-hint { + color: @text-color-secondary; + font-size: @font-size-base; + } + + .@{iconfont-css-prefix}-plus { + color: @disabled-color; + font-size: 30px; + transition: all 0.3s; + + &:hover { + color: @text-color-secondary; + } + } + &:hover .@{iconfont-css-prefix}-plus { + color: @text-color-secondary; + } + } + + &-picture-card-wrapper { + .clearfix(); + + display: inline-block; + width: 100%; + } +} + +.@{upload-prefix-cls}-list { + .reset-component(); + .clearfix(); + line-height: @line-height-base; + + // ============================ Item ============================ + &-item { + position: relative; + height: @line-height-base * @font-size-base; + margin-top: @margin-xs; + font-size: @font-size-base; + + &-name { + display: inline-block; + width: 100%; + padding-left: @font-size-base + 8px; + overflow: hidden; + line-height: @line-height-base; + white-space: nowrap; + text-overflow: ellipsis; + } + + &-card-actions { + position: absolute; + right: 0; + + &-btn { + opacity: 0; + } + &-btn.@{ant-prefix}-btn-sm { + height: 20px; + line-height: 1; + } + + &.picture { + top: 22px; + line-height: 0; + } + + &-btn:focus, + &.picture &-btn { + opacity: 1; + } + + .@{iconfont-css-prefix} { + color: @upload-actions-color; + } + } + + &-info { + height: 100%; + padding: 0 4px; + transition: background-color 0.3s; + + > span { + display: block; + width: 100%; + height: 100%; + } + + .@{iconfont-css-prefix}-loading, + .@{upload-prefix-cls}-text-icon { + .@{iconfont-css-prefix} { + position: absolute; + top: (@font-size-base / 2) - 2px; + color: @text-color-secondary; + font-size: @font-size-base; + } + } + } + + .@{iconfont-css-prefix}-close { + position: absolute; + top: 6px; + right: 4px; + color: @text-color-secondary; + font-size: 10px; + line-height: 0; + cursor: pointer; + opacity: 0; + transition: all 0.3s; + + &:hover { + color: @text-color; + } + } + + &:hover &-info { + background-color: @item-hover-bg; + } + + &:hover .@{iconfont-css-prefix}-close { + opacity: 1; + } + + &:hover &-card-actions-btn { + opacity: 1; + } + + &-error, + &-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix}, + &-error &-name { + color: @error-color; + } + + &-error &-card-actions { + .@{iconfont-css-prefix} { + color: @error-color; + } + + &-btn { + opacity: 1; + } + } + + &-progress { + position: absolute; + bottom: -12px; + width: 100%; + padding-left: @font-size-base + 12px; + font-size: @font-size-base; + line-height: 0; + } + } + + // =================== Picture & Picture Card =================== + &-picture, + &-picture-card { + .@{upload-item} { + position: relative; + height: 66px; + padding: @padding-xs; + border: @border-width-base @upload-picture-card-border-style @border-color-base; + border-radius: @border-radius-base; + + &:hover { + background: transparent; + } + + &-error { + border-color: @error-color; + } + } + + .@{upload-item}-info { + padding: 0; + } + + .@{upload-item}:hover .@{upload-item}-info { + background: transparent; + } + + .@{upload-item}-uploading { + border-style: dashed; + } + + .@{upload-item}-thumbnail { + width: 48px; + height: 48px; + line-height: 60px; + text-align: center; + opacity: 0.8; + + .@{iconfont-css-prefix} { + font-size: 26px; + } + } + + // Adjust the color of the error icon : https://github.com/ant-design/ant-design/pull/24160 + .@{upload-item}-error .@{upload-item}-thumbnail { + .@{iconfont-css-prefix} { + svg path { + &[fill='#e6f7ff'] { + fill: @error-color-deprecated-bg; + } + + &[fill='#1890ff'] { + fill: @error-color; + } + } + } + } + + .@{upload-item}-icon { + position: absolute; + top: 50%; + left: 50%; + font-size: 26px; + transform: translate(-50%, -50%); + + .@{iconfont-css-prefix} { + font-size: 26px; + } + } + + .@{upload-item}-image { + max-width: 100%; + } + + .@{upload-item}-thumbnail img { + display: block; + width: 48px; + height: 48px; + overflow: hidden; + } + + .@{upload-item}-name { + display: inline-block; + box-sizing: border-box; + max-width: 100%; + margin: 0 0 0 8px; + padding-right: 8px; + padding-left: 48px; + overflow: hidden; + line-height: 44px; + white-space: nowrap; + text-overflow: ellipsis; + transition: all 0.3s; + } + + .@{upload-item}-uploading .@{upload-item}-name { + margin-bottom: 12px; + } + + .@{upload-item}-progress { + bottom: 14px; + width: ~'calc(100% - 24px)'; + margin-top: 0; + padding-left: 56px; + } + + .@{iconfont-css-prefix}-close { + position: absolute; + top: 8px; + right: 8px; + line-height: 1; + opacity: 1; + } + } + + // ======================== Picture Card ======================== + &-picture-card { + &-container { + display: inline-block; + width: @upload-picture-card-size; + height: @upload-picture-card-size; + margin: 0 @margin-xs @margin-xs 0; + vertical-align: top; + } + + &.@{upload-prefix-cls}-list::after { + display: none; + } + + .@{upload-item} { + height: 100%; + margin: 0; + } + + .@{upload-item}-info { + position: relative; + height: 100%; + overflow: hidden; + + &::before { + position: absolute; + z-index: 1; + width: 100%; + height: 100%; + background-color: fade(@black, 50%); + opacity: 0; + transition: all 0.3s; + content: ' '; + } + } + + .@{upload-item}:hover .@{upload-item}-info::before { + opacity: 1; + } + + .@{upload-item}-actions { + position: absolute; + top: 50%; + left: 50%; + z-index: 10; + white-space: nowrap; + transform: translate(-50%, -50%); + opacity: 0; + transition: all 0.3s; + + .@{iconfont-css-prefix}-eye, + .@{iconfont-css-prefix}-download, + .@{iconfont-css-prefix}-delete { + z-index: 10; + width: 16px; + margin: 0 4px; + color: @text-color-dark; + font-size: 16px; + cursor: pointer; + transition: all 0.3s; + + &:hover { + color: @text-color-inverse; + } + } + } + + .@{upload-item}-info:hover + .@{upload-item}-actions, + .@{upload-item}-actions:hover { + opacity: 1; + } + + .@{upload-item}-thumbnail, + .@{upload-item}-thumbnail img { + position: static; + display: block; + width: 100%; + height: 100%; + object-fit: contain; + } + + .@{upload-item}-name { + display: none; + margin: 8px 0 0; + padding: 0; + line-height: @line-height-base; + text-align: center; + } + + .@{upload-item}-file + .@{upload-item}-name { + position: absolute; + bottom: 10px; + display: block; + } + + .@{upload-item}-uploading { + &.@{upload-item} { + background-color: @background-color-light; + } + + .@{upload-item}-info { + height: auto; + + &::before, + .@{iconfont-css-prefix}-eye, + .@{iconfont-css-prefix}-delete { + display: none; + } + } + } + + .@{upload-item}-progress { + bottom: 32px; + width: calc(100% - 14px); + padding-left: 0; + } + } + + // ======================= Picture & Text ======================= + &-text, + &-picture { + &-container { + transition: opacity @animation-duration-slow, height @animation-duration-slow; + + &::before { + display: table; + width: 0; + height: 0; + content: ''; + } + + // Don't know why span here, just stretch it + .@{upload-prefix-cls}-span { + display: block; + flex: auto; + } + } + + // text & picture no need this additional element. + // But it used for picture-card, let's keep it. + .@{upload-prefix-cls}-span { + display: flex; + align-items: center; + + > * { + flex: none; + } + } + + .@{upload-item}-name { + flex: auto; + margin: 0; + padding: 0 @padding-xs; + } + + .@{upload-item}-card-actions { + position: static; + } + } + + // ============================ Text ============================ + &-text { + .@{upload-prefix-cls}-text-icon { + .@{iconfont-css-prefix} { + position: static; + } + } + } + + // =========================== Motion =========================== + .@{upload-prefix-cls}-animate-inline-appear, + .@{upload-prefix-cls}-animate-inline-enter, + .@{upload-prefix-cls}-animate-inline-leave { + animation-duration: @animation-duration-slow; + animation-fill-mode: @ease-in-out-circ; + } + + .@{upload-prefix-cls}-animate-inline-appear, + .@{upload-prefix-cls}-animate-inline-enter { + animation-name: uploadAnimateInlineIn; + } + + .@{upload-prefix-cls}-animate-inline-leave { + animation-name: uploadAnimateInlineOut; + } +} + +@keyframes uploadAnimateInlineIn { + from { + width: 0; + height: 0; + margin: 0; + padding: 0; + opacity: 0; + } +} + +@keyframes uploadAnimateInlineOut { + to { + width: 0; + height: 0; + margin: 0; + padding: 0; + opacity: 0; + } +} + +@import './rtl'; diff --git a/components/upload/style/index.ts b/components/upload copy/style/index.tsx similarity index 82% rename from components/upload/style/index.ts rename to components/upload copy/style/index.tsx index b8fd70a86a..582def0ccd 100644 --- a/components/upload/style/index.ts +++ b/components/upload copy/style/index.tsx @@ -2,5 +2,6 @@ import '../../style/index.less'; import './index.less'; // style dependencies +import '../../button/style'; import '../../progress/style'; import '../../tooltip/style'; diff --git a/components/upload copy/style/rtl.less b/components/upload copy/style/rtl.less new file mode 100644 index 0000000000..0dd9836eff --- /dev/null +++ b/components/upload copy/style/rtl.less @@ -0,0 +1,179 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; + +@upload-prefix-cls: ~'@{ant-prefix}-upload'; +@upload-item: ~'@{ant-prefix}-upload-list-item'; + +.@{upload-prefix-cls} { + &-rtl { + direction: rtl; + } + + &&-select-picture-card { + .@{upload-prefix-cls}-rtl& { + margin-right: auto; + margin-left: 8px; + } + } +} + +.@{upload-prefix-cls}-list { + &-rtl { + direction: rtl; + } + + &-item-list-type-text { + &:hover { + .@{upload-prefix-cls}-list-item-name-icon-count-1 { + .@{upload-prefix-cls}-list-rtl & { + padding-right: 22px; + padding-left: 14px; + } + } + .@{upload-prefix-cls}-list-item-name-icon-count-2 { + .@{upload-prefix-cls}-list-rtl & { + padding-right: 22px; + padding-left: 28px; + } + } + } + } + + &-item { + &-name { + .@{upload-prefix-cls}-list-rtl & { + padding-right: @font-size-base + 8px; + padding-left: 0; + } + } + + &-name-icon-count-1 { + .@{upload-prefix-cls}-list-rtl & { + padding-left: 14px; + } + } + + &-card-actions { + .@{upload-prefix-cls}-list-rtl & { + right: auto; + left: 0; + } + .@{iconfont-css-prefix} { + .@{upload-prefix-cls}-list-rtl & { + padding-right: 0; + padding-left: 5px; + } + } + } + + &-info { + .@{upload-prefix-cls}-list-rtl & { + padding: 0 4px 0 12px; + } + } + + .@{iconfont-css-prefix}-close { + .@{upload-prefix-cls}-list-rtl & { + right: auto; + left: 4px; + } + } + + &-error &-card-actions { + .@{iconfont-css-prefix} { + .@{upload-prefix-cls}-list-rtl & { + padding-right: 0; + padding-left: 5px; + } + } + } + + &-progress { + .@{upload-prefix-cls}-list-rtl & { + padding-right: @font-size-base + 12px; + padding-left: 0; + } + } + } + + &-picture, + &-picture-card { + .@{upload-item}-info { + padding: 0; + } + + .@{upload-item}-thumbnail { + .@{upload-prefix-cls}-list-rtl& { + right: 8px; + left: auto; + } + } + + .@{upload-item}-icon { + .@{upload-prefix-cls}-list-rtl& { + right: 50%; + left: auto; + transform: translate(50%, -50%); + } + } + + .@{upload-item}-name { + .@{upload-prefix-cls}-list-rtl& { + margin: 0 8px 0 0; + padding-right: 48px; + padding-left: 8px; + } + } + + .@{upload-item}-name-icon-count-1 { + .@{upload-prefix-cls}-list-rtl& { + padding-right: 48px; + padding-left: 18px; + } + } + + .@{upload-item}-name-icon-count-2 { + .@{upload-prefix-cls}-list-rtl& { + padding-right: 48px; + padding-left: 36px; + } + } + + .@{upload-item}-progress { + .@{upload-prefix-cls}-list-rtl& { + padding-right: 0; + padding-left: 0; + } + } + + .@{iconfont-css-prefix}-close { + .@{upload-prefix-cls}-list-rtl& { + right: auto; + left: 8px; + } + } + } + + &-picture-card { + &-container { + .@{upload-prefix-cls}-list-rtl & { + margin: 0 0 @margin-xs @margin-xs; + } + } + + .@{upload-item}-actions { + .@{upload-prefix-cls}-list-rtl& { + right: 50%; + left: auto; + transform: translate(50%, -50%); + } + } + + .@{upload-item}-file + .@{upload-item}-name { + .@{upload-prefix-cls}-list-rtl& { + margin: 8px 0 0; + padding: 0; + } + } + } +} diff --git a/components/upload copy/utils.tsx b/components/upload copy/utils.tsx new file mode 100644 index 0000000000..3f605723a2 --- /dev/null +++ b/components/upload copy/utils.tsx @@ -0,0 +1,115 @@ +import type { RcFile, UploadFile, InternalUploadFile } from './interface'; + +export function file2Obj(file: RcFile): InternalUploadFile { + return { + ...file, + lastModified: file.lastModified, + lastModifiedDate: file.lastModifiedDate, + name: file.name, + size: file.size, + type: file.type, + uid: file.uid, + percent: 0, + originFileObj: file, + }; +} + +/** Upload fileList. Replace file if exist or just push into it. */ +export function updateFileList(file: UploadFile, fileList: UploadFile[]) { + const nextFileList = [...fileList]; + const fileIndex = nextFileList.findIndex(({ uid }: UploadFile) => uid === file.uid); + if (fileIndex === -1) { + nextFileList.push(file); + } else { + nextFileList[fileIndex] = file; + } + return nextFileList; +} + +export function getFileItem(file: RcFile, fileList: UploadFile[]) { + const matchKey = file.uid !== undefined ? 'uid' : 'name'; + return fileList.filter(item => item[matchKey] === file[matchKey])[0]; +} + +export function removeFileItem(file: UploadFile, fileList: UploadFile[]) { + const matchKey = file.uid !== undefined ? 'uid' : 'name'; + const removed = fileList.filter(item => item[matchKey] !== file[matchKey]); + if (removed.length === fileList.length) { + return null; + } + 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: string): boolean => type.indexOf('image/') === 0; + +export const isImageUrl = (file: UploadFile): boolean => { + if (file.type && !file.thumbUrl) { + return isImageFileType(file.type); + } + const url: string = (file.thumbUrl || file.url || '') as string; + 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: File | Blob): Promise { + return new Promise(resolve => { + if (!file.type || !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); + }); +} From f0f970c37bc887bd810bdd70fcbd0bef34afeabc Mon Sep 17 00:00:00 2001 From: tangjinzhou <415800467@qq.com> Date: Tue, 22 Feb 2022 18:20:19 +0800 Subject: [PATCH 3/5] refactor: upload --- components/components.ts | 2 +- components/locale-provider/index.tsx | 3 +- components/upload copy/Dragger.tsx | 22 - components/upload copy/UploadList/index.tsx | 261 -------- components/upload copy/index.tsx | 17 - components/upload copy/interface.tsx | 183 ----- components/upload old/Dragger.tsx | 22 + .../{upload copy => upload old}/Upload.tsx | 0 .../{upload => upload old}/UploadList.tsx | 0 .../__tests__/__snapshots__/demo.test.js.snap | 0 .../__snapshots__/uploadlist.test.js.snap | 0 .../__tests__/demo.test.js | 0 .../__tests__/mock.js | 0 .../__tests__/requests.js | 0 .../__tests__/upload.test.js | 0 .../__tests__/uploadlist.test.js | 0 .../demo/avatar.vue | 0 .../demo/basic.vue | 0 .../demo/defaultFileList.vue | 0 .../demo/directory.vue | 0 .../{upload copy => upload old}/demo/drag.vue | 0 .../demo/fileList.vue | 0 .../demo/index.vue | 0 .../demo/picture-card.vue | 0 .../demo/picture-style.vue | 0 .../demo/preview-file.vue | 0 .../demo/transform-file.vue | 0 .../demo/upload-manually.vue | 0 .../index.en-US.md | 0 components/upload old/index.tsx | 21 + .../index.zh-CN.md | 0 components/upload old/interface.tsx | 116 ++++ .../style/index.less | 259 +++----- components/{upload => upload old}/utils.jsx | 0 components/upload/Dragger.tsx | 26 +- components/upload/Upload.tsx | 627 ++++++++++-------- .../UploadList/ListItem.tsx | 10 +- components/upload/UploadList/index.tsx | 206 ++++++ components/upload/demo/basic.vue | 1 - components/upload/demo/picture-card.vue | 25 +- components/upload/index.tsx | 27 +- components/upload/interface.tsx | 218 +++--- components/upload/style/index.less | 259 +++++--- .../{upload copy => upload}/style/index.tsx | 0 .../{upload copy => upload}/style/rtl.less | 0 components/{upload copy => upload}/utils.tsx | 0 components/vc-upload/index.js | 4 - components/vc-upload/index.ts | 4 +- components/vc-upload/interface.tsx | 12 +- components/vc-upload/src/AjaxUploader.jsx | 262 -------- components/vc-upload/src/IframeUploader.jsx | 281 -------- components/vc-upload/src/Upload.jsx | 97 --- components/vc-upload/src/attr-accept.js | 26 - components/vc-upload/src/index.js | 4 - components/vc-upload/src/request.js | 108 --- components/vc-upload/src/traverseFileTree.js | 60 -- components/vc-upload/src/uid.js | 6 - site/debugger/index.tsx | 2 +- 58 files changed, 1177 insertions(+), 1994 deletions(-) delete mode 100644 components/upload copy/Dragger.tsx delete mode 100644 components/upload copy/UploadList/index.tsx delete mode 100644 components/upload copy/index.tsx delete mode 100755 components/upload copy/interface.tsx create mode 100644 components/upload old/Dragger.tsx rename components/{upload copy => upload old}/Upload.tsx (100%) rename components/{upload => upload old}/UploadList.tsx (100%) rename components/{upload copy => upload old}/__tests__/__snapshots__/demo.test.js.snap (100%) rename components/{upload copy => upload old}/__tests__/__snapshots__/uploadlist.test.js.snap (100%) rename components/{upload copy => upload old}/__tests__/demo.test.js (100%) rename components/{upload copy => upload old}/__tests__/mock.js (100%) rename components/{upload copy => upload old}/__tests__/requests.js (100%) rename components/{upload copy => upload old}/__tests__/upload.test.js (100%) rename components/{upload copy => upload old}/__tests__/uploadlist.test.js (100%) rename components/{upload copy => upload old}/demo/avatar.vue (100%) rename components/{upload copy => upload old}/demo/basic.vue (100%) rename components/{upload copy => upload old}/demo/defaultFileList.vue (100%) rename components/{upload copy => upload old}/demo/directory.vue (100%) rename components/{upload copy => upload old}/demo/drag.vue (100%) rename components/{upload copy => upload old}/demo/fileList.vue (100%) rename components/{upload copy => upload old}/demo/index.vue (100%) rename components/{upload copy => upload old}/demo/picture-card.vue (100%) rename components/{upload copy => upload old}/demo/picture-style.vue (100%) rename components/{upload copy => upload old}/demo/preview-file.vue (100%) rename components/{upload copy => upload old}/demo/transform-file.vue (100%) rename components/{upload copy => upload old}/demo/upload-manually.vue (100%) rename components/{upload copy => upload old}/index.en-US.md (100%) create mode 100644 components/upload old/index.tsx rename components/{upload copy => upload old}/index.zh-CN.md (100%) create mode 100755 components/upload old/interface.tsx rename components/{upload copy => upload old}/style/index.less (71%) rename components/{upload => upload old}/utils.jsx (100%) rename components/{upload copy => upload}/UploadList/ListItem.tsx (97%) create mode 100644 components/upload/UploadList/index.tsx rename components/{upload copy => upload}/style/index.tsx (100%) rename components/{upload copy => upload}/style/rtl.less (100%) rename components/{upload copy => upload}/utils.tsx (100%) delete mode 100644 components/vc-upload/index.js delete mode 100644 components/vc-upload/src/AjaxUploader.jsx delete mode 100644 components/vc-upload/src/IframeUploader.jsx delete mode 100644 components/vc-upload/src/Upload.jsx delete mode 100644 components/vc-upload/src/attr-accept.js delete mode 100644 components/vc-upload/src/index.js delete mode 100644 components/vc-upload/src/request.js delete mode 100644 components/vc-upload/src/traverseFileTree.js delete mode 100644 components/vc-upload/src/uid.js diff --git a/components/components.ts b/components/components.ts index 8db7c37e45..64374607b7 100644 --- a/components/components.ts +++ b/components/components.ts @@ -224,7 +224,7 @@ export { TypographyTitle, } from './typography'; -export type { UploadProps, UploadListProps, UploadChangeParam } from './upload'; +export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from './upload'; export { default as Upload, UploadDragger } from './upload'; diff --git a/components/locale-provider/index.tsx b/components/locale-provider/index.tsx index 8ab541de6c..44f864ac6e 100644 --- a/components/locale-provider/index.tsx +++ b/components/locale-provider/index.tsx @@ -9,6 +9,7 @@ import type { TransferLocale } from '../transfer'; import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker'; import type { PaginationLocale } from '../pagination/Pagination'; import type { TableLocale } from '../table/interface'; +import type { UploadLocale } from '../upload/interface'; interface TransferLocaleForEmpty { description: string; @@ -18,7 +19,6 @@ export interface Locale { Pagination?: PaginationLocale; Table?: TableLocale; Popconfirm?: Record; - Upload?: Record; Form?: { optional?: string; defaultValidateMessages: ValidateMessages; @@ -32,6 +32,7 @@ export interface Locale { Modal?: ModalLocale; Transfer?: Partial; Select?: Record; + Upload?: UploadLocale; Empty?: TransferLocaleForEmpty; global?: Record; PageHeader?: { back: string }; diff --git a/components/upload copy/Dragger.tsx b/components/upload copy/Dragger.tsx deleted file mode 100644 index 251912a4f8..0000000000 --- a/components/upload copy/Dragger.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { defineComponent } from 'vue'; -import Upload from './Upload'; -import { uploadProps } from './interface'; - -export default defineComponent({ - name: 'AUploadDragger', - inheritAttrs: false, - props: uploadProps(), - setup(props, { slots, attrs }) { - return () => { - const { height, ...restProps } = props; - const { style, ...restAttrs } = attrs; - const draggerProps = { - ...restProps, - ...restAttrs, - type: 'drag', - style: { ...(style as any), height: typeof height === 'number' ? `${height}px` : height }, - } as any; - return ; - }; - }, -}); diff --git a/components/upload copy/UploadList/index.tsx b/components/upload copy/UploadList/index.tsx deleted file mode 100644 index 89955c2127..0000000000 --- a/components/upload copy/UploadList/index.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import * as React from 'react'; -import CSSMotion, { CSSMotionList, CSSMotionListProps } from 'rc-motion'; -import classNames from 'classnames'; -import LoadingOutlined from '@ant-design/icons/LoadingOutlined'; -import PaperClipOutlined from '@ant-design/icons/PaperClipOutlined'; -import PictureTwoTone from '@ant-design/icons/PictureTwoTone'; -import FileTwoTone from '@ant-design/icons/FileTwoTone'; -import { cloneElement, isValidElement } from '../../_util/reactNode'; -import { UploadListProps, UploadFile, UploadListType, InternalUploadFile } from '../interface'; -import { previewImage, isImageUrl } from '../utils'; -import collapseMotion from '../../_util/motion'; -import { ConfigContext } from '../../config-provider'; -import Button, { ButtonProps } from '../../button'; -import useForceUpdate from '../../_util/hooks/useForceUpdate'; -import ListItem from './ListItem'; - -const listItemMotion: Partial = { - ...collapseMotion, -}; - -delete listItemMotion.onAppearEnd; -delete listItemMotion.onEnterEnd; -delete listItemMotion.onLeaveEnd; - -const InternalUploadList: React.ForwardRefRenderFunction = ( - { - listType, - previewFile, - onPreview, - onDownload, - onRemove, - locale, - iconRender, - isImageUrl: isImgUrl, - prefixCls: customizePrefixCls, - items = [], - showPreviewIcon, - showRemoveIcon, - showDownloadIcon, - removeIcon, - previewIcon, - downloadIcon, - progress, - appendAction, - itemRender, - }, - ref, -) => { - const forceUpdate = useForceUpdate(); - const [motionAppear, setMotionAppear] = React.useState(false); - - // ============================= Effect ============================= - React.useEffect(() => { - if (listType !== 'picture' && listType !== 'picture-card') { - return; - } - (items || []).forEach((file: InternalUploadFile) => { - if ( - typeof document === 'undefined' || - typeof window === 'undefined' || - !(window as any).FileReader || - !(window as any).File || - !(file.originFileObj instanceof File || (file.originFileObj as Blob) instanceof Blob) || - file.thumbUrl !== undefined - ) { - return; - } - file.thumbUrl = ''; - if (previewFile) { - previewFile(file.originFileObj as File).then((previewDataUrl: string) => { - // Need append '' to avoid dead loop - file.thumbUrl = previewDataUrl || ''; - forceUpdate(); - }); - } - }); - }, [listType, items, previewFile]); - - React.useEffect(() => { - setMotionAppear(true); - }, []); - - // ============================= Events ============================= - const onInternalPreview = (file: UploadFile, e?: React.SyntheticEvent) => { - if (!onPreview) { - return; - } - e?.preventDefault(); - return onPreview(file); - }; - - const onInternalDownload = (file: UploadFile) => { - if (typeof onDownload === 'function') { - onDownload(file); - } else if (file.url) { - window.open(file.url); - } - }; - - const onInternalClose = (file: UploadFile) => { - onRemove?.(file); - }; - - const internalIconRender = (file: UploadFile) => { - if (iconRender) { - return iconRender(file, listType); - } - const isLoading = file.status === 'uploading'; - const fileIcon = isImgUrl && isImgUrl(file) ? : ; - let icon: React.ReactNode = isLoading ? : ; - if (listType === 'picture') { - icon = isLoading ? : fileIcon; - } else if (listType === 'picture-card') { - icon = isLoading ? locale.uploading : fileIcon; - } - return icon; - }; - - const actionIconRender = ( - customIcon: React.ReactNode, - callback: () => void, - prefixCls: string, - title?: string, - ) => { - const btnProps: ButtonProps = { - type: 'text', - size: 'small', - title, - onClick: (e: React.MouseEvent) => { - callback(); - if (isValidElement(customIcon) && customIcon.props.onClick) { - customIcon.props.onClick(e); - } - }, - className: `${prefixCls}-list-item-card-actions-btn`, - }; - if (isValidElement(customIcon)) { - const btnIcon = cloneElement(customIcon, { - ...customIcon.props, - onClick: () => {}, - }); - - return - ); - }; - - // ============================== Ref =============================== - // Test needs - React.useImperativeHandle(ref, () => ({ - handlePreview: onInternalPreview, - handleDownload: onInternalDownload, - })); - - const { getPrefixCls, direction } = React.useContext(ConfigContext); - - // ============================= Render ============================= - const prefixCls = getPrefixCls('upload', customizePrefixCls); - - const listClassNames = classNames({ - [`${prefixCls}-list`]: true, - [`${prefixCls}-list-${listType}`]: true, - [`${prefixCls}-list-rtl`]: direction === 'rtl', - }); - - // >>> Motion config - const motionKeyList = [ - ...items.map(file => ({ - key: file.uid, - file, - })), - ]; - - const animationDirection = listType === 'picture-card' ? 'animate-inline' : 'animate'; - // const transitionName = list.length === 0 ? '' : `${prefixCls}-${animationDirection}`; - - let motionConfig: Omit = { - motionDeadline: 2000, - motionName: `${prefixCls}-${animationDirection}`, - keys: motionKeyList, - motionAppear, - }; - - if (listType !== 'picture-card') { - motionConfig = { - ...listItemMotion, - ...motionConfig, - }; - } - - return ( -
- - {({ key, file, className: motionClassName, style: motionStyle }) => ( - - )} - - - {/* Append action */} - {appendAction && ( - - {({ className: motionClassName, style: motionStyle }) => - cloneElement(appendAction, oriProps => ({ - className: classNames(oriProps.className, motionClassName), - style: { - ...motionStyle, - ...oriProps.style, - }, - })) - } - - )} -
- ); -}; - -const UploadList = React.forwardRef(InternalUploadList); - -UploadList.displayName = 'UploadList'; - -UploadList.defaultProps = { - listType: 'text' as UploadListType, // or picture - progress: { - strokeWidth: 2, - showInfo: false, - }, - showRemoveIcon: true, - showDownloadIcon: false, - showPreviewIcon: true, - previewFile: previewImage, - isImageUrl, -}; - -export default UploadList; diff --git a/components/upload copy/index.tsx b/components/upload copy/index.tsx deleted file mode 100644 index ba1b8b09ac..0000000000 --- a/components/upload copy/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { App } from 'vue'; -import Upload from './Upload'; -import Dragger from './Dragger'; - -export type { UploadProps, UploadListProps, UploadChangeParam } from './interface'; - -/* istanbul ignore next */ -export const UploadDragger = Dragger; - -export default Object.assign(Upload, { - Dragger, - install(app: App) { - app.component(Upload.name, Upload); - app.component(Dragger.name, Dragger); - return app; - }, -}); diff --git a/components/upload copy/interface.tsx b/components/upload copy/interface.tsx deleted file mode 100755 index 91e78958f8..0000000000 --- a/components/upload copy/interface.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import type { - RcFile as OriRcFile, - UploadRequestOption as RcCustomRequestOptions, -} from '../vc-upload/interface'; -import type { ProgressProps } from '../progress'; -import type { VueNode } from '../_util/type'; -import type { ExtractPropTypes, PropType } from 'vue'; - -export interface RcFile extends OriRcFile { - readonly lastModifiedDate: Date; -} - -export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed'; - -export interface HttpRequestHeader { - [key: string]: 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?: RcFile; - response?: T; - error?: any; - linkProps?: any; - type?: string; - xhr?: T; - preview?: string; -} - -export interface InternalUploadFile extends UploadFile { - originFileObj: RcFile; -} - -export interface UploadChangeParam { - // https://github.com/ant-design/ant-design/issues/14420 - file: T; - fileList: UploadFile[]; - event?: { percent: number }; -} - -// export interface ShowUploadListInterface { -// showRemoveIcon?: boolean; -// showPreviewIcon?: boolean; -// showDownloadIcon?: boolean; -// removeIcon?: VueNode | ((file: UploadFile) => VueNode); -// downloadIcon?: VueNode | ((file: UploadFile) => VueNode); -// previewIcon?: VueNode | ((file: UploadFile) => VueNode); -// } - -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'; -export type UploadListProgressProps = Omit; - -export type ItemRender = (opt: { - originNode: VueNode; - file: UploadFile; - fileList: Array>; - actions: { - download: () => void; - preview: () => void; - remove: () => void; - }; -}) => VueNode; - -type PreviewFileHandler = (file: File | Blob) => PromiseLike; -type TransformFileHandler = ( - file: RcFile, -) => string | Blob | File | PromiseLike; -type BeforeUploadValueType = void | boolean | string | Blob | File; - -function uploadProps() { - return { - capture: [Boolean, String] as PropType, - type: String as PropType, - name: String, - defaultFileList: Array as PropType>>, - fileList: Array as PropType>>, - action: [String, Function] as PropType< - string | ((file: RcFile) => string) | ((file: RcFile) => PromiseLike) - >, - directory: Boolean, - data: [Object, Function] as PropType< - | Record - | ((file: UploadFile) => Record | Promise>) - >, - method: String as PropType<'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch'>, - headers: Object as PropType, - showUploadList: Boolean, - multiple: Boolean, - accept: String, - beforeUpload: Function as PropType< - (file: RcFile, FileList: RcFile[]) => BeforeUploadValueType | Promise - >, - onChange: Function as PropType<(info: UploadChangeParam) => void>, - onDrop: Function as PropType<(event: DragEvent) => void>, - listType: String as PropType, - onPreview: Function as PropType<(file: UploadFile) => void>, - onDownload: Function as PropType<(file: UploadFile) => void>, - onRemove: Function as PropType< - (file: UploadFile) => void | boolean | Promise - >, - supportServerRender: Boolean, - disabled: Boolean, - prefixCls: String, - customRequest: Function as PropType<(options: RcCustomRequestOptions) => void>, - withCredentials: Boolean, - openFileDialogOnClick: Boolean, - locale: Object as PropType, - id: String, - previewFile: Function as PropType, - /** @deprecated Please use `beforeUpload` directly */ - transformFile: Function as PropType, - iconRender: Function as PropType< - (opt: { file: UploadFile; listType?: UploadListType }) => VueNode - >, - isImageUrl: Function as PropType<(file: UploadFile) => boolean>, - progress: Object as PropType, - itemRender: Function as PropType>, - /** Config max count of `fileList`. Will replace current one when `maxCount` is 1 */ - maxCount: Number, - height: [Number, String], - - showRemoveIcon: Boolean, - showDownloadIcon: Boolean, - showPreviewIcon: Boolean, - removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, - downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, - previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, - }; -} - -export type UploadProps = Partial>>; - -export interface UploadState { - fileList: UploadFile[]; - dragState: string; -} - -function uploadListProps() { - return { - listType: String as PropType, - onPreview: Function as PropType<(file: UploadFile) => void>, - onDownload: Function as PropType<(file: UploadFile) => void>, - onRemove: Function as PropType<(file: UploadFile) => void | boolean>, - items: Array as PropType>>, - progress: Object as PropType, - prefixCls: String as PropType, - showRemoveIcon: Boolean, - showDownloadIcon: Boolean, - showPreviewIcon: Boolean, - removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, - downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, - previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, - locale: Object as PropType, - previewFile: Function as PropType, - iconRender: Function as PropType< - (opt: { file: UploadFile; listType?: UploadListType }) => VueNode - >, - isImageUrl: Function as PropType<(file: UploadFile) => boolean>, - appendAction: Function as PropType<() => VueNode>, - itemRender: Function as PropType>, - }; -} - -export type UploadListProps = Partial>>; -export { uploadProps, uploadListProps }; diff --git a/components/upload old/Dragger.tsx b/components/upload old/Dragger.tsx new file mode 100644 index 0000000000..d3d985eb15 --- /dev/null +++ b/components/upload old/Dragger.tsx @@ -0,0 +1,22 @@ +import { defineComponent } from 'vue'; +import { getOptionProps, getSlot } from '../_util/props-util'; +import Upload from './Upload'; +import { uploadProps } from './interface'; + +export default defineComponent({ + name: 'AUploadDragger', + inheritAttrs: false, + props: uploadProps, + render() { + const props = getOptionProps(this); + const { height, ...restProps } = props; + const { style, ...restAttrs } = this.$attrs; + const draggerProps = { + ...restProps, + ...restAttrs, + type: 'drag', + style: { ...(style as any), height }, + } as any; + return {getSlot(this)}; + }, +}); diff --git a/components/upload copy/Upload.tsx b/components/upload old/Upload.tsx similarity index 100% rename from components/upload copy/Upload.tsx rename to components/upload old/Upload.tsx diff --git a/components/upload/UploadList.tsx b/components/upload old/UploadList.tsx similarity index 100% rename from components/upload/UploadList.tsx rename to components/upload old/UploadList.tsx diff --git a/components/upload copy/__tests__/__snapshots__/demo.test.js.snap b/components/upload old/__tests__/__snapshots__/demo.test.js.snap similarity index 100% rename from components/upload copy/__tests__/__snapshots__/demo.test.js.snap rename to components/upload old/__tests__/__snapshots__/demo.test.js.snap diff --git a/components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap b/components/upload old/__tests__/__snapshots__/uploadlist.test.js.snap similarity index 100% rename from components/upload copy/__tests__/__snapshots__/uploadlist.test.js.snap rename to components/upload old/__tests__/__snapshots__/uploadlist.test.js.snap diff --git a/components/upload copy/__tests__/demo.test.js b/components/upload old/__tests__/demo.test.js similarity index 100% rename from components/upload copy/__tests__/demo.test.js rename to components/upload old/__tests__/demo.test.js diff --git a/components/upload copy/__tests__/mock.js b/components/upload old/__tests__/mock.js similarity index 100% rename from components/upload copy/__tests__/mock.js rename to components/upload old/__tests__/mock.js diff --git a/components/upload copy/__tests__/requests.js b/components/upload old/__tests__/requests.js similarity index 100% rename from components/upload copy/__tests__/requests.js rename to components/upload old/__tests__/requests.js diff --git a/components/upload copy/__tests__/upload.test.js b/components/upload old/__tests__/upload.test.js similarity index 100% rename from components/upload copy/__tests__/upload.test.js rename to components/upload old/__tests__/upload.test.js diff --git a/components/upload copy/__tests__/uploadlist.test.js b/components/upload old/__tests__/uploadlist.test.js similarity index 100% rename from components/upload copy/__tests__/uploadlist.test.js rename to components/upload old/__tests__/uploadlist.test.js diff --git a/components/upload copy/demo/avatar.vue b/components/upload old/demo/avatar.vue similarity index 100% rename from components/upload copy/demo/avatar.vue rename to components/upload old/demo/avatar.vue diff --git a/components/upload copy/demo/basic.vue b/components/upload old/demo/basic.vue similarity index 100% rename from components/upload copy/demo/basic.vue rename to components/upload old/demo/basic.vue diff --git a/components/upload copy/demo/defaultFileList.vue b/components/upload old/demo/defaultFileList.vue similarity index 100% rename from components/upload copy/demo/defaultFileList.vue rename to components/upload old/demo/defaultFileList.vue diff --git a/components/upload copy/demo/directory.vue b/components/upload old/demo/directory.vue similarity index 100% rename from components/upload copy/demo/directory.vue rename to components/upload old/demo/directory.vue diff --git a/components/upload copy/demo/drag.vue b/components/upload old/demo/drag.vue similarity index 100% rename from components/upload copy/demo/drag.vue rename to components/upload old/demo/drag.vue diff --git a/components/upload copy/demo/fileList.vue b/components/upload old/demo/fileList.vue similarity index 100% rename from components/upload copy/demo/fileList.vue rename to components/upload old/demo/fileList.vue diff --git a/components/upload copy/demo/index.vue b/components/upload old/demo/index.vue similarity index 100% rename from components/upload copy/demo/index.vue rename to components/upload old/demo/index.vue diff --git a/components/upload copy/demo/picture-card.vue b/components/upload old/demo/picture-card.vue similarity index 100% rename from components/upload copy/demo/picture-card.vue rename to components/upload old/demo/picture-card.vue diff --git a/components/upload copy/demo/picture-style.vue b/components/upload old/demo/picture-style.vue similarity index 100% rename from components/upload copy/demo/picture-style.vue rename to components/upload old/demo/picture-style.vue diff --git a/components/upload copy/demo/preview-file.vue b/components/upload old/demo/preview-file.vue similarity index 100% rename from components/upload copy/demo/preview-file.vue rename to components/upload old/demo/preview-file.vue diff --git a/components/upload copy/demo/transform-file.vue b/components/upload old/demo/transform-file.vue similarity index 100% rename from components/upload copy/demo/transform-file.vue rename to components/upload old/demo/transform-file.vue diff --git a/components/upload copy/demo/upload-manually.vue b/components/upload old/demo/upload-manually.vue similarity index 100% rename from components/upload copy/demo/upload-manually.vue rename to components/upload old/demo/upload-manually.vue diff --git a/components/upload copy/index.en-US.md b/components/upload old/index.en-US.md similarity index 100% rename from components/upload copy/index.en-US.md rename to components/upload old/index.en-US.md diff --git a/components/upload old/index.tsx b/components/upload old/index.tsx new file mode 100644 index 0000000000..a29ac4d3c2 --- /dev/null +++ b/components/upload old/index.tsx @@ -0,0 +1,21 @@ +import type { App, Plugin } from 'vue'; +import Upload from './Upload'; +import Dragger from './Dragger'; + +export type { UploadProps, UploadListProps, UploadChangeParam } from './interface'; + +Upload.Dragger = Dragger; + +/* istanbul ignore next */ +Upload.install = function (app: App) { + app.component(Upload.name, Upload); + app.component(Dragger.name, Dragger); + return app; +}; + +export const UploadDragger = Dragger; + +export default Upload as typeof Upload & + Plugin & { + readonly Dragger: typeof Dragger; + }; diff --git a/components/upload copy/index.zh-CN.md b/components/upload old/index.zh-CN.md similarity index 100% rename from components/upload copy/index.zh-CN.md rename to components/upload old/index.zh-CN.md diff --git a/components/upload old/interface.tsx b/components/upload old/interface.tsx new file mode 100755 index 0000000000..d95add1834 --- /dev/null +++ b/components/upload old/interface.tsx @@ -0,0 +1,116 @@ +import type { ExtractPropTypes, PropType } from 'vue'; +import { tuple } from '../_util/type'; +import PropsTypes from '../_util/vue-types'; + +export const UploadFileStatus = PropsTypes.oneOf( + tuple('error', 'success', 'done', 'uploading', 'removed'), +); + +export interface HttpRequestHeader { + [key: string]: string; +} + +export interface VcFile extends File { + uid: string; + readonly lastModifiedDate: Date; + readonly webkitRelativePath: string; +} + +export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed'; +export interface UploadFile { + uid: string; + size?: number; + name: string; + fileName?: string; + lastModified?: number; + lastModifiedDate?: Date; + url?: string; + status?: UploadFileStatus; + percent?: number; + thumbUrl?: string; + originFileObj?: any; + response?: T; + error?: any; + linkProps?: any; + type?: string; + xhr?: T; + preview?: string; +} + +export interface UploadChangeParam { + file: T; + fileList: UploadFile[]; + event?: { percent: number }; +} + +export const ShowUploadListInterface = PropsTypes.shape({ + showRemoveIcon: PropsTypes.looseBool, + showPreviewIcon: PropsTypes.looseBool, +}).loose; + +export interface UploadLocale { + uploading?: string; + removeFile?: string; + downloadFile?: string; + uploadError?: string; + previewFile?: string; +} + +export const uploadProps = { + type: PropsTypes.oneOf(tuple('drag', 'select')), + name: PropsTypes.string, + defaultFileList: { type: Array as PropType }, + fileList: { type: Array as PropType }, + action: PropsTypes.oneOfType([PropsTypes.string, PropsTypes.func]), + directory: PropsTypes.looseBool, + data: PropsTypes.oneOfType([PropsTypes.object, PropsTypes.func]), + method: PropsTypes.oneOf(tuple('POST', 'PUT', 'PATCH', 'post', 'put', 'patch')), + headers: PropsTypes.object, + showUploadList: PropsTypes.oneOfType([PropsTypes.looseBool, ShowUploadListInterface]), + multiple: PropsTypes.looseBool, + accept: PropsTypes.string, + beforeUpload: PropsTypes.func, + listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')), + // className: PropsTypes.string, + remove: PropsTypes.func, + supportServerRender: PropsTypes.looseBool, + // style: PropsTypes.object, + disabled: PropsTypes.looseBool, + prefixCls: PropsTypes.string, + customRequest: PropsTypes.func, + withCredentials: PropsTypes.looseBool, + openFileDialogOnClick: PropsTypes.looseBool, + locale: { type: Object as PropType }, + height: PropsTypes.number, + id: PropsTypes.string, + previewFile: PropsTypes.func, + transformFile: PropsTypes.func, + onChange: { type: Function as PropType<(info: UploadChangeParam) => void> }, + onPreview: { type: Function as PropType<(file: UploadFile) => void> }, + onRemove: { + type: Function as PropType<(file: UploadFile) => void | boolean | Promise>, + }, + onDownload: { type: Function as PropType<(file: UploadFile) => void> }, + 'onUpdate:fileList': { type: Function as PropType<(files: UploadFile[]) => void> }, +}; + +export type UploadProps = Partial>; +export const uploadListProps = { + listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')), + // items: PropsTypes.arrayOf(UploadFile), + items: { type: Array as PropType }, + progressAttr: PropsTypes.object, + prefixCls: PropsTypes.string, + showRemoveIcon: PropsTypes.looseBool, + showDownloadIcon: PropsTypes.looseBool, + showPreviewIcon: PropsTypes.looseBool, + locale: { type: Object as PropType }, + previewFile: PropsTypes.func, + onPreview: { type: Function as PropType<(file: UploadFile) => void> }, + onRemove: { + type: Function as PropType<(file: UploadFile) => void | boolean>, + }, + onDownload: { type: Function as PropType<(file: UploadFile) => void> }, +}; + +export type UploadListProps = Partial>; diff --git a/components/upload copy/style/index.less b/components/upload old/style/index.less similarity index 71% rename from components/upload copy/style/index.less rename to components/upload old/style/index.less index 49ba7f211b..17f4154eb3 100644 --- a/components/upload copy/style/index.less +++ b/components/upload old/style/index.less @@ -34,6 +34,8 @@ } &&-select-picture-card { + display: table; + float: left; width: @upload-picture-card-size; height: @upload-picture-card-size; margin-right: 8px; @@ -44,21 +46,19 @@ border: @border-width-base dashed @border-color-base; border-radius: @border-radius-base; cursor: pointer; - transition: border-color 0.3s; + transition: border-color 0.3s ease; > .@{upload-prefix-cls} { - display: flex; - align-items: center; - justify-content: center; + display: table-cell; + width: 100%; height: 100%; + padding: 8px; text-align: center; + vertical-align: middle; } &:hover { border-color: @primary-color; - .@{upload-prefix-cls}-disabled& { - border-color: @border-color-base; - } } } @@ -74,7 +74,7 @@ transition: border-color 0.3s; .@{upload-prefix-cls} { - padding: @padding-md 0; + padding: 16px 0; } &.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) { @@ -116,12 +116,10 @@ color: @text-color-secondary; font-size: @font-size-base; } - .@{iconfont-css-prefix}-plus { color: @disabled-color; font-size: 30px; transition: all 0.3s; - &:hover { color: @text-color-secondary; } @@ -142,55 +140,52 @@ .@{upload-prefix-cls}-list { .reset-component(); .clearfix(); - line-height: @line-height-base; - - // ============================ Item ============================ + &-item-list-type-text { + &:hover { + .@{upload-prefix-cls}-list-item-name-icon-count-1 { + padding-right: 14px; + } + .@{upload-prefix-cls}-list-item-name-icon-count-2 { + padding-right: 28px; + } + } + } &-item { position: relative; - height: @line-height-base * @font-size-base; - margin-top: @margin-xs; + height: 22px; + margin-top: 8px; font-size: @font-size-base; - &-name { display: inline-block; width: 100%; padding-left: @font-size-base + 8px; overflow: hidden; - line-height: @line-height-base; white-space: nowrap; text-overflow: ellipsis; } + &-name-icon-count-1 { + padding-right: 14px; + } + &-card-actions { position: absolute; right: 0; - - &-btn { - opacity: 0; - } - &-btn.@{ant-prefix}-btn-sm { - height: 20px; - line-height: 1; - } - + opacity: 0; &.picture { - top: 22px; - line-height: 0; - } - - &-btn:focus, - &.picture &-btn { + top: 25px; + line-height: 1; opacity: 1; } - - .@{iconfont-css-prefix} { - color: @upload-actions-color; + .anticon { + padding-right: 6px; + color: rgba(0, 0, 0, 0.45); } } &-info { height: 100%; - padding: 0 4px; + padding: 0 12px 0 4px; transition: background-color 0.3s; > span { @@ -200,27 +195,25 @@ } .@{iconfont-css-prefix}-loading, - .@{upload-prefix-cls}-text-icon { - .@{iconfont-css-prefix} { - position: absolute; - top: (@font-size-base / 2) - 2px; - color: @text-color-secondary; - font-size: @font-size-base; - } + .@{iconfont-css-prefix}-paper-clip { + position: absolute; + top: (@font-size-base / 2) - 2px; + color: @text-color-secondary; + font-size: @font-size-base; } } .@{iconfont-css-prefix}-close { + .iconfont-size-under-12px(10px); + position: absolute; top: 6px; right: 4px; color: @text-color-secondary; - font-size: 10px; line-height: 0; cursor: pointer; opacity: 0; transition: all 0.3s; - &:hover { color: @text-color; } @@ -234,24 +227,21 @@ opacity: 1; } - &:hover &-card-actions-btn { + &:hover &-card-actions { opacity: 1; } &-error, - &-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix}, + &-error .@{iconfont-css-prefix}-paper-clip, &-error &-name { color: @error-color; } &-error &-card-actions { - .@{iconfont-css-prefix} { + .anticon { color: @error-color; } - - &-btn { - opacity: 1; - } + opacity: 1; } &-progress { @@ -264,20 +254,17 @@ } } - // =================== Picture & Picture Card =================== &-picture, &-picture-card { .@{upload-item} { position: relative; height: 66px; - padding: @padding-xs; + padding: 8px; border: @border-width-base @upload-picture-card-border-style @border-color-base; border-radius: @border-radius-base; - &:hover { background: transparent; } - &-error { border-color: @error-color; } @@ -296,30 +283,15 @@ } .@{upload-item}-thumbnail { + position: absolute; + top: 8px; + left: 8px; width: 48px; height: 48px; - line-height: 60px; + font-size: 26px; + line-height: 54px; text-align: center; opacity: 0.8; - - .@{iconfont-css-prefix} { - font-size: 26px; - } - } - - // Adjust the color of the error icon : https://github.com/ant-design/ant-design/pull/24160 - .@{upload-item}-error .@{upload-item}-thumbnail { - .@{iconfont-css-prefix} { - svg path { - &[fill='#e6f7ff'] { - fill: @error-color-deprecated-bg; - } - - &[fill='#1890ff'] { - fill: @error-color; - } - } - } } .@{upload-item}-icon { @@ -328,10 +300,6 @@ left: 50%; font-size: 26px; transform: translate(-50%, -50%); - - .@{iconfont-css-prefix} { - font-size: 26px; - } } .@{upload-item}-image { @@ -359,8 +327,16 @@ transition: all 0.3s; } + .@{upload-item}-name-icon-count-1 { + padding-right: 18px; + } + + .@{upload-item}-name-icon-count-2 { + padding-right: 36px; + } + .@{upload-item}-uploading .@{upload-item}-name { - margin-bottom: 12px; + line-height: 28px; } .@{upload-item}-progress { @@ -379,23 +355,21 @@ } } - // ======================== Picture Card ======================== &-picture-card { + &.@{upload-prefix-cls}-list::after { + display: none; + } &-container { - display: inline-block; + float: left; width: @upload-picture-card-size; height: @upload-picture-card-size; - margin: 0 @margin-xs @margin-xs 0; - vertical-align: top; - } - - &.@{upload-prefix-cls}-list::after { - display: none; + margin: 0 8px 8px 0; } - .@{upload-item} { - height: 100%; - margin: 0; + float: left; + width: @upload-picture-card-size; + height: @upload-picture-card-size; + margin: 0 8px 8px 0; } .@{upload-item}-info { @@ -439,7 +413,6 @@ font-size: 16px; cursor: pointer; transition: all 0.3s; - &:hover { color: @text-color-inverse; } @@ -457,7 +430,7 @@ display: block; width: 100%; height: 100%; - object-fit: contain; + object-fit: cover; } .@{upload-item}-name { @@ -468,7 +441,7 @@ text-align: center; } - .@{upload-item}-file + .@{upload-item}-name { + .anticon-picture + .@{upload-item}-name { position: absolute; bottom: 10px; display: block; @@ -481,82 +454,46 @@ .@{upload-item}-info { height: auto; - &::before, - .@{iconfont-css-prefix}-eye, + .@{iconfont-css-prefix}-eye-o, .@{iconfont-css-prefix}-delete { display: none; } } + + &-text { + margin-top: 18px; + color: @text-color-secondary; + } } .@{upload-item}-progress { bottom: 32px; - width: calc(100% - 14px); padding-left: 0; } } - // ======================= Picture & Text ======================= - &-text, - &-picture { - &-container { - transition: opacity @animation-duration-slow, height @animation-duration-slow; - - &::before { - display: table; - width: 0; - height: 0; - content: ''; - } - - // Don't know why span here, just stretch it - .@{upload-prefix-cls}-span { - display: block; - flex: auto; - } - } - - // text & picture no need this additional element. - // But it used for picture-card, let's keep it. - .@{upload-prefix-cls}-span { - display: flex; - align-items: center; - - > * { - flex: none; - } - } - - .@{upload-item}-name { - flex: auto; - margin: 0; - padding: 0 @padding-xs; - } - - .@{upload-item}-card-actions { - position: static; - } - } - - // ============================ Text ============================ - &-text { - .@{upload-prefix-cls}-text-icon { - .@{iconfont-css-prefix} { - position: static; - } - } + .@{upload-prefix-cls}-success-icon { + color: @success-color; + font-weight: bold; } - // =========================== Motion =========================== - .@{upload-prefix-cls}-animate-inline-appear, + .@{upload-prefix-cls}-animate-enter, + .@{upload-prefix-cls}-animate-leave, .@{upload-prefix-cls}-animate-inline-enter, .@{upload-prefix-cls}-animate-inline-leave { - animation-duration: @animation-duration-slow; + animation-duration: 0.3s; animation-fill-mode: @ease-in-out-circ; } - .@{upload-prefix-cls}-animate-inline-appear, + .@{upload-prefix-cls}-animate-enter { + animation-name: uploadAnimateIn; + } + + .@{upload-prefix-cls}-animate-leave { + animation-name: uploadAnimateOut; + } + .@{upload-prefix-cls}-animate-inline-enter { animation-name: uploadAnimateInlineIn; } @@ -566,6 +503,24 @@ } } +@keyframes uploadAnimateIn { + from { + height: 0; + margin: 0; + padding: 0; + opacity: 0; + } +} + +@keyframes uploadAnimateOut { + to { + height: 0; + margin: 0; + padding: 0; + opacity: 0; + } +} + @keyframes uploadAnimateInlineIn { from { width: 0; @@ -585,5 +540,3 @@ opacity: 0; } } - -@import './rtl'; diff --git a/components/upload/utils.jsx b/components/upload old/utils.jsx similarity index 100% rename from components/upload/utils.jsx rename to components/upload old/utils.jsx diff --git a/components/upload/Dragger.tsx b/components/upload/Dragger.tsx index d3d985eb15..251912a4f8 100644 --- a/components/upload/Dragger.tsx +++ b/components/upload/Dragger.tsx @@ -1,22 +1,22 @@ import { defineComponent } from 'vue'; -import { getOptionProps, getSlot } from '../_util/props-util'; import Upload from './Upload'; import { uploadProps } from './interface'; export default defineComponent({ name: 'AUploadDragger', inheritAttrs: false, - props: uploadProps, - render() { - const props = getOptionProps(this); - const { height, ...restProps } = props; - const { style, ...restAttrs } = this.$attrs; - const draggerProps = { - ...restProps, - ...restAttrs, - type: 'drag', - style: { ...(style as any), height }, - } as any; - return {getSlot(this)}; + props: uploadProps(), + setup(props, { slots, attrs }) { + return () => { + const { height, ...restProps } = props; + const { style, ...restAttrs } = attrs; + const draggerProps = { + ...restProps, + ...restAttrs, + type: 'drag', + style: { ...(style as any), height: typeof height === 'number' ? `${height}px` : height }, + } as any; + return ; + }; }, }); diff --git a/components/upload/Upload.tsx b/components/upload/Upload.tsx index 6bf14054e3..3ba5d4aa87 100644 --- a/components/upload/Upload.tsx +++ b/components/upload/Upload.tsx @@ -1,86 +1,191 @@ -import classNames from '../_util/classNames'; -import uniqBy from 'lodash-es/uniqBy'; -import findIndex from 'lodash-es/findIndex'; +import type { UploadProps as RcUploadProps } from '../vc-upload'; import VcUpload from '../vc-upload'; -import BaseMixin from '../_util/BaseMixin'; -import { getOptionProps, hasProp, getSlot } from '../_util/props-util'; -import initDefaultProps from '../_util/props-util/initDefaultProps'; -import LocaleReceiver from '../locale-provider/LocaleReceiver'; -import defaultLocale from '../locale-provider/default'; -import { defaultConfigProvider } from '../config-provider'; -import Dragger from './Dragger'; import UploadList from './UploadList'; -import type { UploadFile } from './interface'; +import type { + UploadType, + UploadListType, + RcFile, + UploadFile, + UploadChangeParam, + ShowUploadListInterface, +} from './interface'; import { uploadProps } from './interface'; -import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from './utils'; -import { defineComponent, inject } from 'vue'; -import { getDataAndAriaProps } from '../_util/util'; -import { useInjectFormItemContext } from '../form/FormItemContext'; +import { file2Obj, getFileItem, removeFileItem, updateFileList } from './utils'; +import { useLocaleReceiver } from '../locale-provider/LocaleReceiver'; +import defaultLocale from '../locale/default'; +import { computed, defineComponent, onMounted, ref, toRef } from 'vue'; +import { flattenChildren, initDefaultProps } from '../_util/props-util'; +import useMergedState from '../_util/hooks/useMergedState'; +import devWarning from '../vc-util/devWarning'; +import useConfigInject from '../_util/hooks/useConfigInject'; +import type { VueNode } from '../_util/type'; +import classNames from '../_util/classNames'; +import { useInjectFormItemContext } from '../form'; + +export const LIST_IGNORE = `__LIST_IGNORE_${Date.now()}__`; export default defineComponent({ name: 'AUpload', - mixins: [BaseMixin], inheritAttrs: false, - Dragger, - props: initDefaultProps(uploadProps, { - type: 'select', + props: initDefaultProps(uploadProps(), { + type: 'select' as UploadType, multiple: false, action: '', data: {}, accept: '', - beforeUpload: T, showUploadList: true, - listType: 'text', // or pictrue + listType: 'text' as UploadListType, // or picture disabled: false, supportServerRender: true, }), - setup() { + setup(props, { slots, attrs, expose }) { const formItemContext = useInjectFormItemContext(); - return { - upload: null, - progressTimer: null, - configProvider: inject('configProvider', defaultConfigProvider), - formItemContext, + const [mergedFileList, setMergedFileList] = useMergedState(props.defaultFileList || [], { + value: toRef(props, 'fileList'), + postState: list => { + const timestamp = Date.now(); + return (list ?? []).map((file, index) => { + if (!file.uid && !Object.isFrozen(file)) { + file.uid = `__AUTO__${timestamp}_${index}__`; + } + return file; + }); + }, + }); + const dragState = ref('drop'); + + const upload = ref(); + onMounted(() => { + devWarning( + 'fileList' in props || !('value' in props), + 'Upload', + '`value` is not a valid prop, do you mean `fileList`?', + ); + + devWarning( + !('transformFile' in props), + 'Upload', + '`transformFile` is deprecated. Please use `beforeUpload` directly.', + ); + }); + + const onInternalChange = ( + file: UploadFile, + changedFileList: UploadFile[], + event?: { percent: number }, + ) => { + let cloneList = [...changedFileList]; + + // Cut to match count + if (props.maxCount === 1) { + cloneList = cloneList.slice(-1); + } else if (props.maxCount) { + cloneList = cloneList.slice(0, props.maxCount); + } + + setMergedFileList(cloneList); + + const changeInfo: UploadChangeParam = { + file: file as UploadFile, + fileList: cloneList, + }; + + if (event) { + changeInfo.event = event; + } + props['onUpdate:fileList']?.(changeInfo.fileList); + props.onChange?.(changeInfo); + formItemContext.onFieldChange(); }; - }, - // recentUploadStatus: boolean | PromiseLike; - data() { - return { - sFileList: this.fileList || this.defaultFileList || [], - dragState: 'drop', + + const mergedBeforeUpload = async (file: RcFile, fileListArgs: RcFile[]) => { + const { beforeUpload, transformFile } = props; + + let parsedFile: File | Blob | string = file; + if (beforeUpload) { + const result = await beforeUpload(file, fileListArgs); + + if (result === false) { + return false; + } + + // Hack for LIST_IGNORE, we add additional info to remove from the list + delete (file as any)[LIST_IGNORE]; + if ((result as any) === LIST_IGNORE) { + Object.defineProperty(file, LIST_IGNORE, { + value: true, + configurable: true, + }); + return false; + } + + if (typeof result === 'object' && result) { + parsedFile = result as File; + } + } + + if (transformFile) { + parsedFile = await transformFile(parsedFile as any); + } + + return parsedFile as RcFile; }; - }, - watch: { - fileList(val) { - this.sFileList = val || []; - }, - }, - beforeUnmount() { - this.clearProgressTimer(); - }, - methods: { - onStart(file) { - const targetItem = fileToObject(file); - targetItem.status = 'uploading'; - const nextFileList = this.sFileList.concat(); - const fileIndex = findIndex(nextFileList, ({ uid }) => uid === targetItem.uid); - if (fileIndex === -1) { - nextFileList.push(targetItem); - } else { - nextFileList[fileIndex] = targetItem; + + const onBatchStart: RcUploadProps['onBatchStart'] = batchFileInfoList => { + // Skip file which marked as `LIST_IGNORE`, these file will not add to file list + const filteredFileInfoList = batchFileInfoList.filter( + info => !(info.file as any)[LIST_IGNORE], + ); + + // Nothing to do since no file need upload + if (!filteredFileInfoList.length) { + return; } - this.handleChange({ - file: targetItem, - fileList: nextFileList, + + const objectFileList = filteredFileInfoList.map(info => file2Obj(info.file as RcFile)); + + // Concat new files with prev files + let newFileList = [...mergedFileList.value]; + + objectFileList.forEach(fileObj => { + // Replace file if exist + newFileList = updateFileList(fileObj, newFileList); }); - // fix ie progress - if (!window.File || (typeof process === 'object' && process.env.TEST_IE)) { - this.autoUpdateProgress(0, targetItem); - } - }, - onSuccess(response, file, xhr) { - this.clearProgressTimer(); + objectFileList.forEach((fileObj, index) => { + // Repeat trigger `onChange` event for compatible + let triggerFileObj: UploadFile = fileObj; + + if (!filteredFileInfoList[index].parsedFile) { + // `beforeUpload` return false + const { originFileObj } = fileObj; + let clone; + + try { + clone = new File([originFileObj], originFileObj.name, { + type: originFileObj.type, + }) as any as UploadFile; + } catch (e) { + clone = new Blob([originFileObj], { + type: originFileObj.type, + }) as any as UploadFile; + clone.name = originFileObj.name; + clone.lastModifiedDate = new Date(); + clone.lastModified = new Date().getTime(); + } + + clone.uid = fileObj.uid; + triggerFileObj = clone; + } else { + // Inject `uploading` status + fileObj.status = 'uploading'; + } + + onInternalChange(triggerFileObj, newFileList); + }); + }; + + const onSuccess = (response: any, file: RcFile, xhr: any) => { try { if (typeof response === 'string') { response = JSON.parse(response); @@ -88,255 +193,231 @@ export default defineComponent({ } catch (e) { /* do nothing */ } - const fileList = this.sFileList; - const targetItem = getFileItem(file, fileList); + // removed - if (!targetItem) { + if (!getFileItem(file, mergedFileList.value)) { return; } + + const targetItem = file2Obj(file); targetItem.status = 'done'; + targetItem.percent = 100; targetItem.response = response; targetItem.xhr = xhr; - this.handleChange({ - file: { ...targetItem }, - fileList, - }); - }, - onProgress(e, file) { - const fileList = this.sFileList; - const targetItem = getFileItem(file, fileList); + + const nextFileList = updateFileList(targetItem, mergedFileList.value); + + onInternalChange(targetItem, nextFileList); + }; + + const onProgress = (e: { percent: number }, file: RcFile) => { // removed - if (!targetItem) { + if (!getFileItem(file, mergedFileList.value)) { return; } + + const targetItem = file2Obj(file); + targetItem.status = 'uploading'; targetItem.percent = e.percent; - this.handleChange({ - event: e, - file: { ...targetItem }, - fileList: this.sFileList, - }); - }, - onError(error, response, file) { - this.clearProgressTimer(); - const fileList = this.sFileList; - const targetItem = getFileItem(file, fileList); + + const nextFileList = updateFileList(targetItem, mergedFileList.value); + + onInternalChange(targetItem, nextFileList, e); + }; + + const onError = (error: Error, response: any, file: RcFile) => { // removed - if (!targetItem) { + if (!getFileItem(file, mergedFileList.value)) { return; } + + const targetItem = file2Obj(file); targetItem.error = error; targetItem.response = response; targetItem.status = 'error'; - this.handleChange({ - file: { ...targetItem }, - fileList, - }); - }, - onReject(fileList) { - this.$emit('reject', fileList); - }, - handleRemove(file) { - const { remove: onRemove } = this; - const { sFileList: fileList } = this.$data; - - Promise.resolve(typeof onRemove === 'function' ? onRemove(file) : onRemove).then(ret => { + + const nextFileList = updateFileList(targetItem, mergedFileList.value); + + onInternalChange(targetItem, nextFileList); + }; + + const handleRemove = (file: UploadFile) => { + let currentFile: UploadFile; + Promise.resolve( + typeof props.onRemove === 'function' ? props.onRemove(file) : props.onRemove, + ).then(ret => { // Prevent removing file if (ret === false) { return; } - const removedFileList = removeFileItem(file, fileList); + const removedFileList = removeFileItem(file, mergedFileList.value); if (removedFileList) { - file.status = 'removed'; // eslint-disable-line - - if (this.upload) { - this.upload.abort(file); - } - - this.handleChange({ - file, - fileList: removedFileList, + currentFile = { ...file, status: 'removed' }; + mergedFileList.value?.forEach(item => { + const matchKey = currentFile.uid !== undefined ? 'uid' : 'name'; + if (item[matchKey] === currentFile[matchKey] && !Object.isFrozen(item)) { + item.status = 'removed'; + } }); + upload.value?.abort(currentFile); + + onInternalChange(currentFile, removedFileList); } }); - }, - handleManualRemove(file) { - if (this.$refs.uploadRef) { - (this.$refs.uploadRef as any).abort(file); - } - this.handleRemove(file); - }, - handleChange(info) { - if (!hasProp(this, 'fileList')) { - this.setState({ sFileList: info.fileList }); - } - this.$emit('update:fileList', info.fileList); - this.$emit('change', info); - this.formItemContext.onFieldChange(); - }, - onFileDrop(e) { - this.setState({ - dragState: e.type, - }); - }, - reBeforeUpload(file, fileList) { - const { beforeUpload } = this.$props; - const { sFileList: stateFileList } = this.$data; - if (!beforeUpload) { - return true; - } - const result = beforeUpload(file, fileList); - if (result === false) { - this.handleChange({ - file, - fileList: uniqBy( - stateFileList.concat(fileList.map(fileToObject)), - (item: UploadFile) => item.uid, - ), - }); - return false; - } - if (result && result.then) { - return result; + }; + + const onFileDrop = (e: DragEvent) => { + dragState.value = e.type; + if (e.type === 'drop') { + props.onDrop?.(e); } - return true; - }, - 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) { + }; + expose({ + onBatchStart, + onSuccess, + onProgress, + onError, + fileList: mergedFileList, + upload, + }); + + const { prefixCls, direction } = useConfigInject('upload', props); + const [locale] = useLocaleReceiver( + 'Upload', + defaultLocale.Upload, + computed(() => props.locale), + ); + const renderUploadList = (button?: VueNode) => { const { - showUploadList = {}, - listType, - previewFile, - disabled, - locale: propLocale, - } = getOptionProps(this); - const { showRemoveIcon, showPreviewIcon, showDownloadIcon } = showUploadList; - const { sFileList: fileList } = this.$data; - const { onDownload, onPreview } = this.$props; - const uploadListProps = { - listType, - items: fileList, + removeIcon, + previewIcon, + downloadIcon, previewFile, - showRemoveIcon: !disabled && showRemoveIcon, - showPreviewIcon, - showDownloadIcon, - locale: { ...locale, ...propLocale }, - onRemove: this.handleManualRemove, - onDownload, onPreview, - }; - return ; - }, - }, - render() { - const { - prefixCls: customizePrefixCls, - showUploadList, - listType, - type, - disabled, - } = getOptionProps(this); - const { sFileList: fileList, dragState } = this.$data; - const { class: className, style } = this.$attrs; - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('upload', customizePrefixCls); - - const vcUploadProps = { - ...this.$props, - id: this.$props.id ?? this.formItemContext.id.value, - prefixCls, - beforeUpload: this.reBeforeUpload, - onStart: this.onStart, - onError: this.onError, - onProgress: this.onProgress, - onSuccess: this.onSuccess, - onReject: this.onReject, - ref: 'uploadRef', + onDownload, + disabled, + isImageUrl, + progress, + itemRender, + iconRender, + showUploadList, + } = props; + const { showDownloadIcon, showPreviewIcon, showRemoveIcon } = + typeof showUploadList === 'boolean' ? ({} as ShowUploadListInterface) : showUploadList; + return showUploadList ? ( + button }} + /> + ) : ( + button + ); }; + return () => { + const { listType, disabled, type } = props; + const rcUploadProps = { + onBatchStart, + onError, + onProgress, + onSuccess, + ...(props as RcUploadProps), + id: props.id ?? formItemContext.id.value, + prefixCls: prefixCls.value, + beforeUpload: mergedBeforeUpload, + onChange: undefined, + }; + + // Remove id to avoid open by label when trigger is hidden + // !children: https://github.com/ant-design/ant-design/issues/14298 + // disabled: https://github.com/ant-design/ant-design/issues/16478 + // https://github.com/ant-design/ant-design/issues/24197 + if (!slots.default || disabled) { + delete rcUploadProps.id; + } + if (type === 'drag') { + const dragCls = classNames( + prefixCls.value, + { + [`${prefixCls.value}-drag`]: true, + [`${prefixCls.value}-drag-uploading`]: mergedFileList.value.some( + file => file.status === 'uploading', + ), + [`${prefixCls.value}-drag-hover`]: dragState.value === 'dragover', + [`${prefixCls.value}-disabled`]: disabled, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }, + attrs.class, + ); + return ( + +
+ +
{slots.default?.()}
+
+
+ {renderUploadList()} +
+ ); + } - const uploadList = showUploadList ? ( - - ) : null; - - const children = getSlot(this); - - if (type === 'drag') { - const dragCls = classNames(prefixCls, { - [`${prefixCls}-drag`]: true, - [`${prefixCls}-drag-uploading`]: fileList.some((file: any) => file.status === 'uploading'), - [`${prefixCls}-drag-hover`]: dragState === 'dragover', - [`${prefixCls}-disabled`]: disabled, + const uploadButtonCls = classNames(prefixCls.value, { + [`${prefixCls.value}-select`]: true, + [`${prefixCls.value}-select-${listType}`]: true, + [`${prefixCls.value}-disabled`]: disabled, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', }); - return ( - -
- -
{children}
-
-
- {uploadList} -
+ const children = flattenChildren(slots.default?.()); + const uploadButton = ( +
+ +
); - } - - const uploadButtonCls = classNames(prefixCls, { - [`${prefixCls}-select`]: true, - [`${prefixCls}-select-${listType}`]: true, - [`${prefixCls}-disabled`]: disabled, - }); - // Remove id to avoid open by label when trigger is hidden - // https://github.com/ant-design/ant-design/issues/14298 - if (!children.length || disabled) { - delete vcUploadProps.id; - } - - const uploadButton = ( -
- {children} -
- ); - - if (listType === 'picture-card') { + if (listType === 'picture-card') { + return ( + + {renderUploadList(uploadButton)} + + ); + } return ( - - {uploadList} + {uploadButton} + {renderUploadList()} ); - } - return ( - - {uploadButton} - {uploadList} - - ); + }; }, }); diff --git a/components/upload copy/UploadList/ListItem.tsx b/components/upload/UploadList/ListItem.tsx similarity index 97% rename from components/upload copy/UploadList/ListItem.tsx rename to components/upload/UploadList/ListItem.tsx index 63bd7b33aa..4e51a8184e 100644 --- a/components/upload copy/UploadList/ListItem.tsx +++ b/components/upload/UploadList/ListItem.tsx @@ -25,9 +25,9 @@ export const listItemProps = () => { listType: String as PropType, isImgUrl: Function as PropType<(file: UploadFile) => boolean>, - showRemoveIcon: Boolean, - showDownloadIcon: Boolean, - showPreviewIcon: Boolean, + showRemoveIcon: { type: Boolean, default: undefined }, + showDownloadIcon: { type: Boolean, default: undefined }, + showPreviewIcon: { type: Boolean, default: undefined }, removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>, @@ -76,8 +76,8 @@ export default defineComponent({ file, items, progress: progressProps, - iconRender, - actionIconRender, + iconRender = slots.iconRender, + actionIconRender = slots.actionIconRender, itemRender = slots.itemRender, isImgUrl, showPreviewIcon, diff --git a/components/upload/UploadList/index.tsx b/components/upload/UploadList/index.tsx new file mode 100644 index 0000000000..206636c53e --- /dev/null +++ b/components/upload/UploadList/index.tsx @@ -0,0 +1,206 @@ +import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; +import PaperClipOutlined from '@ant-design/icons-vue/PaperClipOutlined'; +import PictureTwoTone from '@ant-design/icons-vue/PictureTwoTone'; +import FileTwoTone from '@ant-design/icons-vue/FileTwoTone'; +import type { UploadListType, InternalUploadFile, UploadFile } from '../interface'; +import { uploadListProps } from '../interface'; +import { previewImage, isImageUrl } from '../utils'; +import type { ButtonProps } from '../../button'; +import Button from '../../button'; +import ListItem from './ListItem'; +import type { HTMLAttributes } from 'vue'; +import { computed, defineComponent, getCurrentInstance, onMounted, ref, watchEffect } from 'vue'; +import { initDefaultProps, isValidElement } from '../../_util/props-util'; +import type { VueNode } from '../../_util/type'; +import useConfigInject from '../../_util/hooks/useConfigInject'; +import { getTransitionGroupProps, TransitionGroup } from '../../_util/transition'; + +const HackSlot = (_, { slots }) => { + return slots.default?.()[0]; +}; +export default defineComponent({ + name: 'AUploadList', + props: initDefaultProps(uploadListProps(), { + listType: 'text' as UploadListType, // or picture + progress: { + strokeWidth: 2, + showInfo: false, + }, + showRemoveIcon: true, + showDownloadIcon: false, + showPreviewIcon: true, + previewFile: previewImage, + isImageUrl, + items: [], + }), + setup(props, { slots, expose }) { + const motionAppear = ref(false); + const instance = getCurrentInstance(); + onMounted(() => { + motionAppear.value == true; + }); + watchEffect(() => { + if (props.listType !== 'picture' && props.listType !== 'picture-card') { + return; + } + (props.items || []).forEach((file: InternalUploadFile) => { + if ( + typeof document === 'undefined' || + typeof window === 'undefined' || + !(window as any).FileReader || + !(window as any).File || + !(file.originFileObj instanceof File || (file.originFileObj as Blob) instanceof Blob) || + file.thumbUrl !== undefined + ) { + return; + } + file.thumbUrl = ''; + if (props.previewFile) { + props.previewFile(file.originFileObj as File).then((previewDataUrl: string) => { + // Need append '' to avoid dead loop + file.thumbUrl = previewDataUrl || ''; + instance.update(); + }); + } + }); + }); + + // ============================= Events ============================= + const onInternalPreview = (file: UploadFile, e?: Event) => { + if (!props.onPreview) { + return; + } + e?.preventDefault(); + return props.onPreview(file); + }; + + const onInternalDownload = (file: UploadFile) => { + if (typeof props.onDownload === 'function') { + props.onDownload(file); + } else if (file.url) { + window.open(file.url); + } + }; + + const onInternalClose = (file: UploadFile) => { + props.onRemove?.(file); + }; + + const internalIconRender = ({ file }: { file: UploadFile }) => { + const iconRender = props.iconRender || slots.iconRender; + if (iconRender) { + return iconRender({ file, listType: props.listType }); + } + const isLoading = file.status === 'uploading'; + const fileIcon = + props.isImageUrl && props.isImageUrl(file) ? : ; + let icon: VueNode = isLoading ? : ; + if (props.listType === 'picture') { + icon = isLoading ? : fileIcon; + } else if (props.listType === 'picture-card') { + icon = isLoading ? props.locale.uploading : fileIcon; + } + return icon; + }; + + const actionIconRender = (opt: { + customIcon: VueNode; + callback: () => void; + prefixCls: string; + title?: string; + }) => { + const { customIcon, callback, prefixCls, title } = opt; + const btnProps: ButtonProps & HTMLAttributes = { + type: 'text', + size: 'small', + title, + onClick: () => { + callback(); + }, + class: `${prefixCls}-list-item-card-actions-btn`, + }; + if (isValidElement(customIcon)) { + return + ); + }; + + expose({ + handlePreview: onInternalPreview, + handleDownload: onInternalDownload, + }); + + const { prefixCls, direction } = useConfigInject('upload', props); + + const listClassNames = computed(() => ({ + [`${prefixCls.value}-list`]: true, + [`${prefixCls.value}-list-${props.listType}`]: true, + [`${prefixCls.value}-list-rtl`]: direction.value === 'rtl', + })); + const transitionGroupProps = computed(() => ({ + ...getTransitionGroupProps( + `${prefixCls.value}-${props.listType === 'picture-card' ? 'animate-inline' : 'animate'}`, + ), + class: listClassNames.value, + appear: motionAppear.value, + })); + return () => { + const { + listType, + locale, + isImageUrl: isImgUrl, + items = [], + showPreviewIcon, + showRemoveIcon, + showDownloadIcon, + removeIcon, + previewIcon, + downloadIcon, + progress, + appendAction = slots.appendAction, + itemRender, + } = props; + const appendActionDom = appendAction?.()[0]; + return ( + + {items.map(file => { + const { uid: key } = file; + return ( + + ); + })} + {isValidElement(appendActionDom) ? ( + {appendActionDom} + ) : null} + + ); + }; + }, +}); diff --git a/components/upload/demo/basic.vue b/components/upload/demo/basic.vue index 3c9862438f..c2fb1f5e0c 100644 --- a/components/upload/demo/basic.vue +++ b/components/upload/demo/basic.vue @@ -19,7 +19,6 @@ Classic mode. File selection dialog pops up when upload button is clicked.
-
Upload
+
Upload
- + example @@ -36,7 +36,7 @@ After users upload picture, the thumbnail will be shown in list. The upload butt - diff --git a/components/upload old/demo/directory.vue b/components/upload old/demo/directory.vue deleted file mode 100644 index feaacb0781..0000000000 --- a/components/upload old/demo/directory.vue +++ /dev/null @@ -1,35 +0,0 @@ - ---- -order: 8 -title: - zh-CN: 文件夹上传 - en-US: Upload directory ---- - -## zh-CN - -支持上传一个文件夹里的所有文件。 - -## en-US - -You can select and upload a whole directory. - - - - diff --git a/components/upload old/demo/drag.vue b/components/upload old/demo/drag.vue deleted file mode 100644 index 6bf30241ab..0000000000 --- a/components/upload old/demo/drag.vue +++ /dev/null @@ -1,68 +0,0 @@ - ---- -order: 5 -title: - zh-CN: 拖拽上传 - en-US: Drag and Drop ---- - -## zh-CN - -把文件拖入指定区域,完成上传,同样支持点击上传。 - -设置 `multiple` 后,在 `IE10+` 可以一次上传多个文件。 - -## en-US - -You can drag files to a specific area, to upload. Alternatively, you can also upload by selecting. - -We can upload serveral files at once in modern browsers by giving the input the `multiple` attribute. - - - - diff --git a/components/upload old/demo/fileList.vue b/components/upload old/demo/fileList.vue deleted file mode 100644 index 963343540f..0000000000 --- a/components/upload old/demo/fileList.vue +++ /dev/null @@ -1,81 +0,0 @@ - ---- -order: 4 -title: - zh-CN: 完全控制的上传列表 - en-US: Complete control over file list ---- - -## zh-CN - -使用 `fileList` 对列表进行完全控制,可以实现各种自定义功能,以下演示二种情况: - -1. 上传列表数量的限制。 - -2. 读取远程路径并显示链接。 - -## en-US - -You can gain full control over filelist by configuring `fileList`. You can accomplish all kinds of customed functions. The following shows two circumstances: - -1. limit the number of uploaded files. - -2. read from response and show file link. - - - - diff --git a/components/upload old/demo/index.vue b/components/upload old/demo/index.vue deleted file mode 100644 index b1ae4e92e4..0000000000 --- a/components/upload old/demo/index.vue +++ /dev/null @@ -1,52 +0,0 @@ - - diff --git a/components/upload old/demo/picture-card.vue b/components/upload old/demo/picture-card.vue deleted file mode 100644 index 34fdd976da..0000000000 --- a/components/upload old/demo/picture-card.vue +++ /dev/null @@ -1,126 +0,0 @@ - ---- -order: 3 -title: - zh-CN: 照片墙 - en-US: Pictures Wall ---- - -## zh-CN - -用户可以上传图片并在列表中显示缩略图。当上传照片数到达限制后,上传按钮消失。 - -## en-US - -After users upload picture, the thumbnail will be shown in list. The upload button will disappear when count meets limitation. - - - - - diff --git a/components/upload old/demo/picture-style.vue b/components/upload old/demo/picture-style.vue deleted file mode 100644 index 9ad5b92a92..0000000000 --- a/components/upload old/demo/picture-style.vue +++ /dev/null @@ -1,108 +0,0 @@ - ---- -order: 6 -title: - zh-CN: 图片列表样式 - en-US: Pictures with list style ---- - -## zh-CN - -上传文件为图片,可展示本地缩略图。`IE8/9` 不支持浏览器本地缩略图展示([Ref](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL)),可以写 `thumbUrl` 属性来代替。 - -## en-US - -If uploaded file is a picture, the thumbnail can be shown. `IE8/9` do not support local thumbnail show. Please use `thumbUrl` instead. - - - - - - diff --git a/components/upload old/demo/preview-file.vue b/components/upload old/demo/preview-file.vue deleted file mode 100644 index 9f2ceb2fbe..0000000000 --- a/components/upload old/demo/preview-file.vue +++ /dev/null @@ -1,58 +0,0 @@ - ---- -order: 9 -title: - zh-CN: 自定义预览 - en-US: Customize preview file ---- - -## zh-CN - -自定义本地预览,用于处理非图片格式文件(例如视频文件)。 - -## en-US - -Customize local preview. Can handle with non-image format files such as video. - - - - diff --git a/components/upload old/demo/transform-file.vue b/components/upload old/demo/transform-file.vue deleted file mode 100644 index 857a6b89ae..0000000000 --- a/components/upload old/demo/transform-file.vue +++ /dev/null @@ -1,66 +0,0 @@ - ---- -order: 10 -title: - zh-CN: 上传前转换文件 - en-US: Transform file before request ---- - -## zh-CN - -使用 `beforeUpload` 转换上传的文件(例如添加水印)。 - -## en-US - -Use `beforeUpload` for transform file before request such as add a watermark. - - - - diff --git a/components/upload old/demo/upload-manually.vue b/components/upload old/demo/upload-manually.vue deleted file mode 100644 index d84c67de83..0000000000 --- a/components/upload old/demo/upload-manually.vue +++ /dev/null @@ -1,96 +0,0 @@ - ---- -order: 7 -title: - zh-CN: 手动上传 - en-US: Upload manually ---- - -## zh-CN - -`beforeUpload` 返回 `false` 后,手动上传文件。 - -## en-US - -Upload files manually after `beforeUpload` returns `false`. - - - - diff --git a/components/upload old/index.en-US.md b/components/upload old/index.en-US.md deleted file mode 100644 index 84500cf848..0000000000 --- a/components/upload old/index.en-US.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -category: Components -type: Data Entry -title: Upload -cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg ---- - -Upload file by selecting or dragging. - -## When To Use - -Uploading is the process of publishing information (web pages, text, pictures, video, etc.) to a remote server via a web page or upload tool. - -- When you need to upload one or more files. -- When you need to show the process of uploading. -- When you need to upload files by dragging and dropping. - -## API - -| 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) | - | | -| 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 | 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 - -> The function will be called when uploading is in progress, completed or failed - -When uploading state change, it returns: - -```jsx -{ - file: { /* ... */ }, - fileList: [ /* ... */ ], - event: { /* ... */ }, -} -``` - -1. `file` File object for the current operation. - - ```jsx - { - uid: 'uid', // unique identifier, negative is recommend, to prevent interference with internal generated id - 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 - } - ``` - -2. `fileList` current list of files -3. `event` response from server, including uploading progress, supported by advanced browsers. diff --git a/components/upload old/index.tsx b/components/upload old/index.tsx deleted file mode 100644 index a29ac4d3c2..0000000000 --- a/components/upload old/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import type { App, Plugin } from 'vue'; -import Upload from './Upload'; -import Dragger from './Dragger'; - -export type { UploadProps, UploadListProps, UploadChangeParam } from './interface'; - -Upload.Dragger = Dragger; - -/* istanbul ignore next */ -Upload.install = function (app: App) { - app.component(Upload.name, Upload); - app.component(Dragger.name, Dragger); - return app; -}; - -export const UploadDragger = Dragger; - -export default Upload as typeof Upload & - Plugin & { - readonly Dragger: typeof Dragger; - }; diff --git a/components/upload old/index.zh-CN.md b/components/upload old/index.zh-CN.md deleted file mode 100644 index 73db3ef2ce..0000000000 --- a/components/upload old/index.zh-CN.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -category: Components -subtitle: 上传 -type: 数据录入 -title: Upload -cover: https://gw.alipayobjects.com/zos/alicdn/QaeBt_ZMg/Upload.svg ---- - -文件选择上传和拖拽上传控件。 - -## 何时使用 - -上传是将信息(网页、文字、图片、视频等)通过网页或者上传工具发布到远程服务器上的过程。 - -- 当需要上传一个或一些文件时。 -- 当需要展现上传的进度时。 -- 当需要使用拖拽交互时。 - -## API - -| 参数 | 说明 | 类型 | 默认值 | 版本 | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| 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 | 无 | | -| 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) | 无 | | -| download | 点击下载文件时的回调,如果没有指定,则默认跳转到文件 url 对应的标签页。 | Function(file): void | 跳转新标签页 | 1.5.0 | -| reject | 拖拽文件不符合 accept 类型时的回调 | Function(fileList) | 无 | | - -### change - -> 上传中、完成、失败都会调用这个函数。 - -文件状态改变的回调,返回为: - -```jsx -{ - file: { /* ... */ }, - fileList: [ /* ... */ ], - event: { /* ... */ }, -} -``` - -1. `file` 当前操作的文件对象。 - - ```jsx - { - uid: 'uid', // 文件唯一标识,建议设置为负数,防止和内部产生的 id 冲突 - name: 'xx.png', // 文件名 - status: 'done', // 状态有:uploading done error removed - response: '{"status": "success"}', // 服务端响应内容 - linkProps: '{"download": "image"}', // 下载链接额外的 HTML 属性 - xhr: 'XMLHttpRequest{ ... }', // XMLHttpRequest Header - } - ``` - -2. `fileList` 当前的文件列表。 -3. `event` 上传中的服务端响应内容,包含了上传进度等信息,高级浏览器支持。 diff --git a/components/upload old/interface.tsx b/components/upload old/interface.tsx deleted file mode 100755 index d95add1834..0000000000 --- a/components/upload old/interface.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import type { ExtractPropTypes, PropType } from 'vue'; -import { tuple } from '../_util/type'; -import PropsTypes from '../_util/vue-types'; - -export const UploadFileStatus = PropsTypes.oneOf( - tuple('error', 'success', 'done', 'uploading', 'removed'), -); - -export interface HttpRequestHeader { - [key: string]: string; -} - -export interface VcFile extends File { - uid: string; - readonly lastModifiedDate: Date; - readonly webkitRelativePath: string; -} - -export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed'; -export interface UploadFile { - uid: string; - size?: number; - name: string; - fileName?: string; - lastModified?: number; - lastModifiedDate?: Date; - url?: string; - status?: UploadFileStatus; - percent?: number; - thumbUrl?: string; - originFileObj?: any; - response?: T; - error?: any; - linkProps?: any; - type?: string; - xhr?: T; - preview?: string; -} - -export interface UploadChangeParam { - file: T; - fileList: UploadFile[]; - event?: { percent: number }; -} - -export const ShowUploadListInterface = PropsTypes.shape({ - showRemoveIcon: PropsTypes.looseBool, - showPreviewIcon: PropsTypes.looseBool, -}).loose; - -export interface UploadLocale { - uploading?: string; - removeFile?: string; - downloadFile?: string; - uploadError?: string; - previewFile?: string; -} - -export const uploadProps = { - type: PropsTypes.oneOf(tuple('drag', 'select')), - name: PropsTypes.string, - defaultFileList: { type: Array as PropType }, - fileList: { type: Array as PropType }, - action: PropsTypes.oneOfType([PropsTypes.string, PropsTypes.func]), - directory: PropsTypes.looseBool, - data: PropsTypes.oneOfType([PropsTypes.object, PropsTypes.func]), - method: PropsTypes.oneOf(tuple('POST', 'PUT', 'PATCH', 'post', 'put', 'patch')), - headers: PropsTypes.object, - showUploadList: PropsTypes.oneOfType([PropsTypes.looseBool, ShowUploadListInterface]), - multiple: PropsTypes.looseBool, - accept: PropsTypes.string, - beforeUpload: PropsTypes.func, - listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')), - // className: PropsTypes.string, - remove: PropsTypes.func, - supportServerRender: PropsTypes.looseBool, - // style: PropsTypes.object, - disabled: PropsTypes.looseBool, - prefixCls: PropsTypes.string, - customRequest: PropsTypes.func, - withCredentials: PropsTypes.looseBool, - openFileDialogOnClick: PropsTypes.looseBool, - locale: { type: Object as PropType }, - height: PropsTypes.number, - id: PropsTypes.string, - previewFile: PropsTypes.func, - transformFile: PropsTypes.func, - onChange: { type: Function as PropType<(info: UploadChangeParam) => void> }, - onPreview: { type: Function as PropType<(file: UploadFile) => void> }, - onRemove: { - type: Function as PropType<(file: UploadFile) => void | boolean | Promise>, - }, - onDownload: { type: Function as PropType<(file: UploadFile) => void> }, - 'onUpdate:fileList': { type: Function as PropType<(files: UploadFile[]) => void> }, -}; - -export type UploadProps = Partial>; -export const uploadListProps = { - listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')), - // items: PropsTypes.arrayOf(UploadFile), - items: { type: Array as PropType }, - progressAttr: PropsTypes.object, - prefixCls: PropsTypes.string, - showRemoveIcon: PropsTypes.looseBool, - showDownloadIcon: PropsTypes.looseBool, - showPreviewIcon: PropsTypes.looseBool, - locale: { type: Object as PropType }, - previewFile: PropsTypes.func, - onPreview: { type: Function as PropType<(file: UploadFile) => void> }, - onRemove: { - type: Function as PropType<(file: UploadFile) => void | boolean>, - }, - onDownload: { type: Function as PropType<(file: UploadFile) => void> }, -}; - -export type UploadListProps = Partial>; diff --git a/components/upload old/style/index.less b/components/upload old/style/index.less deleted file mode 100644 index 17f4154eb3..0000000000 --- a/components/upload old/style/index.less +++ /dev/null @@ -1,542 +0,0 @@ -@import '../../style/themes/index'; -@import '../../style/mixins/index'; - -@upload-prefix-cls: ~'@{ant-prefix}-upload'; -@upload-item: ~'@{ant-prefix}-upload-list-item'; -@upload-picture-card-size: 104px; -@upload-picture-card-border-style: @border-style-base; - -.@{upload-prefix-cls} { - .reset-component(); - - outline: 0; - - p { - margin: 0; - } - - &-btn { - display: block; - width: 100%; - outline: none; - } - - input[type='file'] { - cursor: pointer; - } - - &&-select { - display: inline-block; - } - - &&-disabled { - cursor: not-allowed; - } - - &&-select-picture-card { - display: table; - float: left; - width: @upload-picture-card-size; - height: @upload-picture-card-size; - margin-right: 8px; - margin-bottom: 8px; - text-align: center; - vertical-align: top; - background-color: @background-color-light; - border: @border-width-base dashed @border-color-base; - border-radius: @border-radius-base; - cursor: pointer; - transition: border-color 0.3s ease; - - > .@{upload-prefix-cls} { - display: table-cell; - width: 100%; - height: 100%; - padding: 8px; - text-align: center; - vertical-align: middle; - } - - &:hover { - border-color: @primary-color; - } - } - - &&-drag { - position: relative; - width: 100%; - height: 100%; - text-align: center; - background: @background-color-light; - border: @border-width-base dashed @border-color-base; - border-radius: @border-radius-base; - cursor: pointer; - transition: border-color 0.3s; - - .@{upload-prefix-cls} { - padding: 16px 0; - } - - &.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) { - border-color: @primary-7; - } - - &.@{upload-prefix-cls}-disabled { - cursor: not-allowed; - } - - .@{upload-prefix-cls}-btn { - display: table; - height: 100%; - } - - .@{upload-prefix-cls}-drag-container { - display: table-cell; - vertical-align: middle; - } - - &:not(.@{upload-prefix-cls}-disabled):hover { - border-color: @primary-5; - } - - p.@{upload-prefix-cls}-drag-icon { - .@{iconfont-css-prefix} { - color: @primary-5; - font-size: 48px; - } - - margin-bottom: 20px; - } - p.@{upload-prefix-cls}-text { - margin: 0 0 4px; - color: @heading-color; - font-size: @font-size-lg; - } - p.@{upload-prefix-cls}-hint { - color: @text-color-secondary; - font-size: @font-size-base; - } - .@{iconfont-css-prefix}-plus { - color: @disabled-color; - font-size: 30px; - transition: all 0.3s; - &:hover { - color: @text-color-secondary; - } - } - &:hover .@{iconfont-css-prefix}-plus { - color: @text-color-secondary; - } - } - - &-picture-card-wrapper { - .clearfix(); - - display: inline-block; - width: 100%; - } -} - -.@{upload-prefix-cls}-list { - .reset-component(); - .clearfix(); - &-item-list-type-text { - &:hover { - .@{upload-prefix-cls}-list-item-name-icon-count-1 { - padding-right: 14px; - } - .@{upload-prefix-cls}-list-item-name-icon-count-2 { - padding-right: 28px; - } - } - } - &-item { - position: relative; - height: 22px; - margin-top: 8px; - font-size: @font-size-base; - &-name { - display: inline-block; - width: 100%; - padding-left: @font-size-base + 8px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - &-name-icon-count-1 { - padding-right: 14px; - } - - &-card-actions { - position: absolute; - right: 0; - opacity: 0; - &.picture { - top: 25px; - line-height: 1; - opacity: 1; - } - .anticon { - padding-right: 6px; - color: rgba(0, 0, 0, 0.45); - } - } - - &-info { - height: 100%; - padding: 0 12px 0 4px; - transition: background-color 0.3s; - - > span { - display: block; - width: 100%; - height: 100%; - } - - .@{iconfont-css-prefix}-loading, - .@{iconfont-css-prefix}-paper-clip { - position: absolute; - top: (@font-size-base / 2) - 2px; - color: @text-color-secondary; - font-size: @font-size-base; - } - } - - .@{iconfont-css-prefix}-close { - .iconfont-size-under-12px(10px); - - position: absolute; - top: 6px; - right: 4px; - color: @text-color-secondary; - line-height: 0; - cursor: pointer; - opacity: 0; - transition: all 0.3s; - &:hover { - color: @text-color; - } - } - - &:hover &-info { - background-color: @item-hover-bg; - } - - &:hover .@{iconfont-css-prefix}-close { - opacity: 1; - } - - &:hover &-card-actions { - opacity: 1; - } - - &-error, - &-error .@{iconfont-css-prefix}-paper-clip, - &-error &-name { - color: @error-color; - } - - &-error &-card-actions { - .anticon { - color: @error-color; - } - opacity: 1; - } - - &-progress { - position: absolute; - bottom: -12px; - width: 100%; - padding-left: @font-size-base + 12px; - font-size: @font-size-base; - line-height: 0; - } - } - - &-picture, - &-picture-card { - .@{upload-item} { - position: relative; - height: 66px; - padding: 8px; - border: @border-width-base @upload-picture-card-border-style @border-color-base; - border-radius: @border-radius-base; - &:hover { - background: transparent; - } - &-error { - border-color: @error-color; - } - } - - .@{upload-item}-info { - padding: 0; - } - - .@{upload-item}:hover .@{upload-item}-info { - background: transparent; - } - - .@{upload-item}-uploading { - border-style: dashed; - } - - .@{upload-item}-thumbnail { - position: absolute; - top: 8px; - left: 8px; - width: 48px; - height: 48px; - font-size: 26px; - line-height: 54px; - text-align: center; - opacity: 0.8; - } - - .@{upload-item}-icon { - position: absolute; - top: 50%; - left: 50%; - font-size: 26px; - transform: translate(-50%, -50%); - } - - .@{upload-item}-image { - max-width: 100%; - } - - .@{upload-item}-thumbnail img { - display: block; - width: 48px; - height: 48px; - overflow: hidden; - } - - .@{upload-item}-name { - display: inline-block; - box-sizing: border-box; - max-width: 100%; - margin: 0 0 0 8px; - padding-right: 8px; - padding-left: 48px; - overflow: hidden; - line-height: 44px; - white-space: nowrap; - text-overflow: ellipsis; - transition: all 0.3s; - } - - .@{upload-item}-name-icon-count-1 { - padding-right: 18px; - } - - .@{upload-item}-name-icon-count-2 { - padding-right: 36px; - } - - .@{upload-item}-uploading .@{upload-item}-name { - line-height: 28px; - } - - .@{upload-item}-progress { - bottom: 14px; - width: ~'calc(100% - 24px)'; - margin-top: 0; - padding-left: 56px; - } - - .@{iconfont-css-prefix}-close { - position: absolute; - top: 8px; - right: 8px; - line-height: 1; - opacity: 1; - } - } - - &-picture-card { - &.@{upload-prefix-cls}-list::after { - display: none; - } - &-container { - float: left; - width: @upload-picture-card-size; - height: @upload-picture-card-size; - margin: 0 8px 8px 0; - } - .@{upload-item} { - float: left; - width: @upload-picture-card-size; - height: @upload-picture-card-size; - margin: 0 8px 8px 0; - } - - .@{upload-item}-info { - position: relative; - height: 100%; - overflow: hidden; - - &::before { - position: absolute; - z-index: 1; - width: 100%; - height: 100%; - background-color: fade(@black, 50%); - opacity: 0; - transition: all 0.3s; - content: ' '; - } - } - - .@{upload-item}:hover .@{upload-item}-info::before { - opacity: 1; - } - - .@{upload-item}-actions { - position: absolute; - top: 50%; - left: 50%; - z-index: 10; - white-space: nowrap; - transform: translate(-50%, -50%); - opacity: 0; - transition: all 0.3s; - - .@{iconfont-css-prefix}-eye, - .@{iconfont-css-prefix}-download, - .@{iconfont-css-prefix}-delete { - z-index: 10; - width: 16px; - margin: 0 4px; - color: @text-color-dark; - font-size: 16px; - cursor: pointer; - transition: all 0.3s; - &:hover { - color: @text-color-inverse; - } - } - } - - .@{upload-item}-info:hover + .@{upload-item}-actions, - .@{upload-item}-actions:hover { - opacity: 1; - } - - .@{upload-item}-thumbnail, - .@{upload-item}-thumbnail img { - position: static; - display: block; - width: 100%; - height: 100%; - object-fit: cover; - } - - .@{upload-item}-name { - display: none; - margin: 8px 0 0; - padding: 0; - line-height: @line-height-base; - text-align: center; - } - - .anticon-picture + .@{upload-item}-name { - position: absolute; - bottom: 10px; - display: block; - } - - .@{upload-item}-uploading { - &.@{upload-item} { - background-color: @background-color-light; - } - - .@{upload-item}-info { - height: auto; - &::before, - .@{iconfont-css-prefix}-eye-o, - .@{iconfont-css-prefix}-delete { - display: none; - } - } - - &-text { - margin-top: 18px; - color: @text-color-secondary; - } - } - - .@{upload-item}-progress { - bottom: 32px; - padding-left: 0; - } - } - - .@{upload-prefix-cls}-success-icon { - color: @success-color; - font-weight: bold; - } - - .@{upload-prefix-cls}-animate-enter, - .@{upload-prefix-cls}-animate-leave, - .@{upload-prefix-cls}-animate-inline-enter, - .@{upload-prefix-cls}-animate-inline-leave { - animation-duration: 0.3s; - animation-fill-mode: @ease-in-out-circ; - } - - .@{upload-prefix-cls}-animate-enter { - animation-name: uploadAnimateIn; - } - - .@{upload-prefix-cls}-animate-leave { - animation-name: uploadAnimateOut; - } - - .@{upload-prefix-cls}-animate-inline-enter { - animation-name: uploadAnimateInlineIn; - } - - .@{upload-prefix-cls}-animate-inline-leave { - animation-name: uploadAnimateInlineOut; - } -} - -@keyframes uploadAnimateIn { - from { - height: 0; - margin: 0; - padding: 0; - opacity: 0; - } -} - -@keyframes uploadAnimateOut { - to { - height: 0; - margin: 0; - padding: 0; - opacity: 0; - } -} - -@keyframes uploadAnimateInlineIn { - from { - width: 0; - height: 0; - margin: 0; - padding: 0; - opacity: 0; - } -} - -@keyframes uploadAnimateInlineOut { - to { - width: 0; - height: 0; - margin: 0; - padding: 0; - opacity: 0; - } -} diff --git a/components/upload old/utils.jsx b/components/upload old/utils.jsx deleted file mode 100644 index 2ee74403e8..0000000000 --- a/components/upload old/utils.jsx +++ /dev/null @@ -1,130 +0,0 @@ -export function T() { - return true; -} - -// Fix IE file.status problem -// via coping a new Object -export function fileToObject(file) { - return { - ...file, - lastModified: file.lastModified, - lastModifiedDate: file.lastModifiedDate, - name: file.name, - size: file.size, - type: file.type, - uid: file.uid, - percent: 0, - originFileObj: file, - }; -} - -/** - * 生成Progress percent: 0.1 -> 0.98 - * - for ie - */ -export function genPercentAdd() { - let k = 0.1; - const i = 0.01; - const end = 0.98; - return function (s) { - let start = s; - if (start >= end) { - return start; - } - - start += k; - k = k - i; - if (k < 0.001) { - k = 0.001; - } - return start; - }; -} - -export function getFileItem(file, fileList) { - const matchKey = file.uid !== undefined ? 'uid' : 'name'; - return fileList.filter(item => item[matchKey] === file[matchKey])[0]; -} - -export function removeFileItem(file, fileList) { - const matchKey = file.uid !== undefined ? 'uid' : 'name'; - const removed = fileList.filter(item => item[matchKey] !== file[matchKey]); - if (removed.length === fileList.length) { - return null; - } - 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/upload/Upload.tsx b/components/upload/Upload.tsx index 3ba5d4aa87..7035ed2e85 100644 --- a/components/upload/Upload.tsx +++ b/components/upload/Upload.tsx @@ -4,10 +4,10 @@ import UploadList from './UploadList'; import type { UploadType, UploadListType, - RcFile, UploadFile, UploadChangeParam, ShowUploadListInterface, + FileType, } from './interface'; import { uploadProps } from './interface'; import { file2Obj, getFileItem, removeFileItem, updateFileList } from './utils'; @@ -57,16 +57,21 @@ export default defineComponent({ const upload = ref(); onMounted(() => { devWarning( - 'fileList' in props || !('value' in props), + props.fileList !== undefined || attrs.value === undefined, 'Upload', '`value` is not a valid prop, do you mean `fileList`?', ); devWarning( - !('transformFile' in props), + props.transformFile === undefined, 'Upload', '`transformFile` is deprecated. Please use `beforeUpload` directly.', ); + devWarning( + props.remove === undefined, + 'Upload', + '`remove` props is deprecated. Please use `remove` event.', + ); }); const onInternalChange = ( @@ -98,10 +103,10 @@ export default defineComponent({ formItemContext.onFieldChange(); }; - const mergedBeforeUpload = async (file: RcFile, fileListArgs: RcFile[]) => { + const mergedBeforeUpload = async (file: FileType, fileListArgs: FileType[]) => { const { beforeUpload, transformFile } = props; - let parsedFile: File | Blob | string = file; + let parsedFile: FileType | Blob | string = file; if (beforeUpload) { const result = await beforeUpload(file, fileListArgs); @@ -128,7 +133,7 @@ export default defineComponent({ parsedFile = await transformFile(parsedFile as any); } - return parsedFile as RcFile; + return parsedFile as File; }; const onBatchStart: RcUploadProps['onBatchStart'] = batchFileInfoList => { @@ -142,7 +147,7 @@ export default defineComponent({ return; } - const objectFileList = filteredFileInfoList.map(info => file2Obj(info.file as RcFile)); + const objectFileList = filteredFileInfoList.map(info => file2Obj(info.file as FileType)); // Concat new files with prev files let newFileList = [...mergedFileList.value]; @@ -185,7 +190,7 @@ export default defineComponent({ }); }; - const onSuccess = (response: any, file: RcFile, xhr: any) => { + const onSuccess = (response: any, file: FileType, xhr: any) => { try { if (typeof response === 'string') { response = JSON.parse(response); @@ -210,7 +215,7 @@ export default defineComponent({ onInternalChange(targetItem, nextFileList); }; - const onProgress = (e: { percent: number }, file: RcFile) => { + const onProgress = (e: { percent: number }, file: FileType) => { // removed if (!getFileItem(file, mergedFileList.value)) { return; @@ -225,7 +230,7 @@ export default defineComponent({ onInternalChange(targetItem, nextFileList, e); }; - const onError = (error: Error, response: any, file: RcFile) => { + const onError = (error: Error, response: any, file: FileType) => { // removed if (!getFileItem(file, mergedFileList.value)) { return; @@ -243,29 +248,30 @@ export default defineComponent({ const handleRemove = (file: UploadFile) => { let currentFile: UploadFile; - Promise.resolve( - typeof props.onRemove === 'function' ? props.onRemove(file) : props.onRemove, - ).then(ret => { - // Prevent removing file - if (ret === false) { - return; - } + const mergedRemove = props.onRemove || props.remove; + Promise.resolve(typeof mergedRemove === 'function' ? mergedRemove(file) : mergedRemove).then( + ret => { + // Prevent removing file + if (ret === false) { + return; + } - const removedFileList = removeFileItem(file, mergedFileList.value); + const removedFileList = removeFileItem(file, mergedFileList.value); - if (removedFileList) { - currentFile = { ...file, status: 'removed' }; - mergedFileList.value?.forEach(item => { - const matchKey = currentFile.uid !== undefined ? 'uid' : 'name'; - if (item[matchKey] === currentFile[matchKey] && !Object.isFrozen(item)) { - item.status = 'removed'; - } - }); - upload.value?.abort(currentFile); + if (removedFileList) { + currentFile = { ...file, status: 'removed' }; + mergedFileList.value?.forEach(item => { + const matchKey = currentFile.uid !== undefined ? 'uid' : 'name'; + if (item[matchKey] === currentFile[matchKey] && !Object.isFrozen(item)) { + item.status = 'removed'; + } + }); + upload.value?.abort(currentFile); - onInternalChange(currentFile, removedFileList); - } - }); + onInternalChange(currentFile, removedFileList); + } + }, + ); }; const onFileDrop = (e: DragEvent) => { @@ -344,6 +350,7 @@ export default defineComponent({ beforeUpload: mergedBeforeUpload, onChange: undefined, }; + delete (rcUploadProps as any).remove; // Remove id to avoid open by label when trigger is hidden // !children: https://github.com/ant-design/ant-design/issues/14298 diff --git a/components/upload/UploadList/ListItem.tsx b/components/upload/UploadList/ListItem.tsx index 4e51a8184e..6d492d37dd 100644 --- a/components/upload/UploadList/ListItem.tsx +++ b/components/upload/UploadList/ListItem.tsx @@ -148,7 +148,6 @@ export default defineComponent({ title: locale.removeFile, }) : null; - const downloadIcon = showDownloadIcon && file.status === 'done' ? actionIconRender({ diff --git a/components/upload/UploadList/index.tsx b/components/upload/UploadList/index.tsx index bcc19773a0..6947f061df 100644 --- a/components/upload/UploadList/index.tsx +++ b/components/upload/UploadList/index.tsx @@ -189,13 +189,14 @@ export default defineComponent({ onPreview={onInternalPreview} onDownload={onInternalDownload} onClose={onInternalClose} + removeIcon={removeIcon} + previewIcon={previewIcon} + downloadIcon={downloadIcon} + itemRender={itemRender} v-slots={{ - removeIcon, - previewIcon, - downloadIcon, + ...slots, iconRender: internalIconRender, actionIconRender, - itemRender, }} /> ); diff --git a/components/upload/__tests__/__snapshots__/demo.test.js.snap b/components/upload/__tests__/__snapshots__/demo.test.js.snap index 924cda5041..bd98f94f5a 100644 --- a/components/upload/__tests__/__snapshots__/demo.test.js.snap +++ b/components/upload/__tests__/__snapshots__/demo.test.js.snap @@ -1,126 +1,305 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`renders ./components/upload/demo/avatar.vue correctly 1`] = ` -
+
Upload
`; exports[`renders ./components/upload/demo/basic.vue correctly 1`] = ` -
-
+
+
+ +
+`; + +exports[`renders ./components/upload/demo/custom-render.vue correctly 1`] = ` +
+
+
+
+
xxx.png
+ + + + + +
+
+
+
+
yyy.png
+ + + + + +
+
+
+
+
zzz.png
+ + + + + +
+
+ +
+`; + +exports[`renders ./components/upload/demo/customize-progress-bar.vue correctly 1`] = ` +
+
+ +
`; exports[`renders ./components/upload/demo/defaultFileList.vue correctly 1`] = ` -
+
-
+
+
+ + + +
+
+
+
+ -
-
- - -
-
+
+
+
- - - + + +
+
+
`; exports[`renders ./components/upload/demo/directory.vue correctly 1`] = ` -
-
+
+
+ +
`; exports[`renders ./components/upload/demo/drag.vue correctly 1`] = ` -

+

Click or drag file to this area to upload

-

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

-
-
+

Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files

+
+ +
`; exports[`renders ./components/upload/demo/fileList.vue correctly 1`] = ` -
+
-
- - -
+
+
+ + + +
+
+
`; -exports[`renders ./components/upload/demo/picture-card.vue correctly 1`] = ` -
- - -
-
- +exports[`renders ./components/upload/demo/max-count.vue correctly 1`] = ` +
+
+
+ +
+
+
+
+
- -
+`; + +exports[`renders ./components/upload/demo/picture-card.vue correctly 1`] = ` +
+
+
- -
-
image.png - -
- +
+ +
+
+
+ + + +
-
-
Upload
-
+
+
+ + + +
+
+
+
+
Uploading...
image.png
+ + +
+
+
+ +
+
image.png +
+
+ + +
+
+
+
Upload
+
+
`; exports[`renders ./components/upload/demo/picture-style.vue correctly 1`] = ` -
+
-
+
+
+ + + +
+
+
+
+ + + +
+
+


+
+
+
+ + + +
+
+
+
+ + + +
+
-
- - -
-


+`; + +exports[`renders ./components/upload/demo/preview-file.vue correctly 1`] = ` +
- -
- -
+`; + +exports[`renders ./components/upload/demo/transform-file.vue correctly 1`] = ` +
+
+
`; -exports[`renders ./components/upload/demo/preview-file.vue correctly 1`] = ` -
-
+exports[`renders ./components/upload/demo/upload-custom-action-icon.vue correctly 1`] = ` +
+
+
+
+
xxx.png
+
+ + +
+
+
+
+
yyy.png
+
+ + +
+
+
+ +
+ + + +
+
+ +
`; -exports[`renders ./components/upload/demo/transform-file.vue correctly 1`] = ` -
-
+exports[`renders ./components/upload/demo/upload-png-only.vue correctly 1`] = ` +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+ +
+ + + +
+
+ +
`; diff --git a/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap b/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap index 9ec7b50d1b..5a2aa53b85 100644 --- a/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap +++ b/components/upload/__tests__/__snapshots__/uploadlist.test.js.snap @@ -35,41 +35,71 @@ exports[`Upload List handle error 2`] = ` exports[`Upload List should be uploading when upload a file 1`] = `
`; exports[`Upload List should non-image format file preview 1`] = ` -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
`; diff --git a/components/upload/__tests__/upload.test.js b/components/upload/__tests__/upload.test.js index e82a8907e8..d6e9da8ea3 100644 --- a/components/upload/__tests__/upload.test.js +++ b/components/upload/__tests__/upload.test.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils'; import Upload from '..'; -import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from '../utils'; +import { getFileItem, removeFileItem } from '../utils'; import PropsTypes from '../../_util/vue-types'; import { uploadListProps } from '../interface'; import { setup, teardown } from './mock'; @@ -199,36 +199,6 @@ describe('Upload', () => { }); describe('util', () => { - // https://github.com/react-component/upload/issues/36 - it('should T() return true', () => { - const res = T(); - expect(res).toBe(true); - }); - it('should be able to copy file instance', () => { - const file = new File([], 'aaa.zip'); - const copiedFile = fileToObject(file); - ['uid', 'lastModified', 'lastModifiedDate', 'name', 'size', 'type'].forEach(key => { - expect(key in copiedFile).toBe(true); - }); - }); - it('should be able to progress from 0.1 ', () => { - // 0.1 -> 0.98 - const getPercent = genPercentAdd(); - let curPercent = 0; - curPercent = getPercent(curPercent); - expect(curPercent).toBe(0.1); - }); - - it('should be able to progress to 0.98 ', () => { - // 0.1 -> 0.98 - const getPercent = genPercentAdd(); - let curPercent = 0; - for (let i = 0; i < 500; i += 1) { - curPercent = getPercent(curPercent); - } - expect(parseFloat(curPercent.toFixed(2))).toBe(0.98); - }); - it('should be able to get fileItem', () => { const file = { uid: '-1', name: 'item.jpg' }; const fileList = [ diff --git a/components/upload/__tests__/uploadlist.test.js b/components/upload/__tests__/uploadlist.test.js index 6fe88fca9f..52cd2fff1a 100644 --- a/components/upload/__tests__/uploadlist.test.js +++ b/components/upload/__tests__/uploadlist.test.js @@ -300,7 +300,7 @@ describe('Upload List', () => { defaultFileList: fileList, listType: 'picture-card', action: '', - remove: handleRemove, + onRemove: handleRemove, onChange: handleChange, }, diff --git a/components/upload/demo/avatar.vue b/components/upload/demo/avatar.vue index cd68f1a221..66e14fc15c 100644 --- a/components/upload/demo/avatar.vue +++ b/components/upload/demo/avatar.vue @@ -10,7 +10,7 @@ title: 点击上传用户头像,并使用 `beforeUpload` 限制用户上传的图片格式和大小。 -> `beforeUpload` 的返回值可以是一个 Promise 以支持异步处理,如服务端校验等:[示例](http://react-component.github.io/upload/examples/beforeUpload.html)。 +> `beforeUpload` 的返回值可以是一个 Promise 以支持异步处理,如服务端校验等:可参考react版本[示例](http://react-component.github.io/upload/examples/beforeUpload.html)。 ## en-US diff --git a/components/upload/demo/custom-render.vue b/components/upload/demo/custom-render.vue new file mode 100644 index 0000000000..0a77bf3367 --- /dev/null +++ b/components/upload/demo/custom-render.vue @@ -0,0 +1,93 @@ + +--- +order: 0 +title: + zh-CN: 自定义上传列表 + en-US: Custom Render +--- + +## zh-CN + +使用 `itemRender` 插槽进行完全自定义列表 + +## en-US + +Custom render by using `itemRender` slot. + + + + diff --git a/components/upload old/demo/basic.vue b/components/upload/demo/customize-progress-bar.vue similarity index 69% rename from components/upload old/demo/basic.vue rename to components/upload/demo/customize-progress-bar.vue index 3c9862438f..c3afe5dbe8 100644 --- a/components/upload old/demo/basic.vue +++ b/components/upload/demo/customize-progress-bar.vue @@ -1,27 +1,28 @@ --- -order: 0 +order: 15 title: - zh-CN: 点击上传 - en-US: Upload by clicking + zh-CN: 自定义进度条样式 + en-US: Customize Progress Bar --- ## zh-CN -经典款式,用户点击按钮弹出文件选择框。 +使用 `progress` 属性自定义进度条样式。 ## en-US -Classic mode. File selection dialog pops up when upload button is clicked. +Use `progress` for customize progress bar. +