diff --git a/components/components.ts b/components/components.ts index 8db7c37e4..64374607b 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/form/__tests__/__snapshots__/demo.test.js.snap b/components/form/__tests__/__snapshots__/demo.test.js.snap index 6534f8592..d926893d1 100644 --- a/components/form/__tests__/__snapshots__/demo.test.js.snap +++ b/components/form/__tests__/__snapshots__/demo.test.js.snap @@ -1535,8 +1535,10 @@ exports[`renders ./components/form/demo/validate-other.vue correctly 1`] = `
-
-
+
+
+ +
@@ -1550,17 +1552,18 @@ exports[`renders ./components/form/demo/validate-other.vue correctly 1`] = `
-

+

Click or drag file to this area to upload

-

Support for a single or bulk upload.

+

Support for a single or bulk upload.

+
+
+
-
+
-
- - +
diff --git a/components/locale-provider/index.tsx b/components/locale-provider/index.tsx index 8ab541de6..44f864ac6 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/space/__tests__/__snapshots__/demo.test.js.snap b/components/space/__tests__/__snapshots__/demo.test.js.snap index 12f7e81bc..b9a1b8aa9 100644 --- a/components/space/__tests__/__snapshots__/demo.test.js.snap +++ b/components/space/__tests__/__snapshots__/demo.test.js.snap @@ -61,8 +61,10 @@ exports[`renders ./components/space/demo/base.vue correctly 1`] = ` Button
-
-
+
+
+ +
diff --git a/components/style/themes/default.less b/components/style/themes/default.less index a036d2753..4276be61b 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/Dragger.tsx b/components/upload/Dragger.tsx index d3d985eb1..251912a4f 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 6bf14054e..7035ed2e8 100644 --- a/components/upload/Upload.tsx +++ b/components/upload/Upload.tsx @@ -1,86 +1,196 @@ -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, + UploadFile, + UploadChangeParam, + ShowUploadListInterface, + FileType, +} 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, - }; - }, - // 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); - } - }, + 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'); - onSuccess(response, file, xhr) { - this.clearProgressTimer(); + const upload = ref(); + onMounted(() => { + devWarning( + props.fileList !== undefined || attrs.value === undefined, + 'Upload', + '`value` is not a valid prop, do you mean `fileList`?', + ); + + devWarning( + 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 = ( + 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(); + }; + + const mergedBeforeUpload = async (file: FileType, fileListArgs: FileType[]) => { + const { beforeUpload, transformFile } = props; + + let parsedFile: FileType | 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 File; + }; + + 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; + } + + const objectFileList = filteredFileInfoList.map(info => file2Obj(info.file as FileType)); + + // Concat new files with prev files + let newFileList = [...mergedFileList.value]; + + objectFileList.forEach(fileObj => { + // Replace file if exist + newFileList = updateFileList(fileObj, newFileList); + }); + + 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: FileType, xhr: any) => { try { if (typeof response === 'string') { response = JSON.parse(response); @@ -88,255 +198,233 @@ 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: FileType) => { // 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: FileType) => { // 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 => { - // Prevent removing file - if (ret === false) { - return; - } + const nextFileList = updateFileList(targetItem, mergedFileList.value); - 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', + onInternalChange(targetItem, nextFileList); }; - const uploadList = showUploadList ? ( - - ) : null; + const handleRemove = (file: UploadFile) => { + let currentFile: UploadFile; + const mergedRemove = props.onRemove || props.remove; + Promise.resolve(typeof mergedRemove === 'function' ? mergedRemove(file) : mergedRemove).then( + ret => { + // Prevent removing file + if (ret === false) { + return; + } - const children = getSlot(this); + const removedFileList = removeFileItem(file, mergedFileList.value); - 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} -
+ 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); + } + }, ); - } + }; - const uploadButtonCls = classNames(prefixCls, { - [`${prefixCls}-select`]: true, - [`${prefixCls}-select-${listType}`]: true, - [`${prefixCls}-disabled`]: disabled, + const onFileDrop = (e: DragEvent) => { + dragState.value = e.type; + if (e.type === 'drop') { + props.onDrop?.(e); + } + }; + expose({ + onBatchStart, + onSuccess, + onProgress, + onError, + fileList: mergedFileList, + upload, }); - // 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} -
+ const { prefixCls, direction } = useConfigInject('upload', props); + const [locale] = useLocaleReceiver( + 'Upload', + defaultLocale.Upload, + computed(() => props.locale), ); + const renderUploadList = (button?: VueNode) => { + const { + removeIcon, + previewIcon, + downloadIcon, + previewFile, + onPreview, + 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, + }; + delete (rcUploadProps as any).remove; - if (listType === 'picture-card') { + // 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 uploadButtonCls = classNames(prefixCls.value, { + [`${prefixCls.value}-select`]: true, + [`${prefixCls.value}-select-${listType}`]: true, + [`${prefixCls.value}-disabled`]: disabled, + [`${prefixCls.value}-rtl`]: direction.value === 'rtl', + }); + const children = flattenChildren(slots.default?.()); + const uploadButton = ( +
+ +
+ ); + + if (listType === 'picture-card') { + return ( + + {renderUploadList(uploadButton)} + + ); + } return ( - - {uploadList} + {uploadButton} + {renderUploadList()} ); - } - return ( - - {uploadButton} - {uploadList} - - ); + }; }, }); diff --git a/components/upload/UploadList.tsx b/components/upload/UploadList.tsx deleted file mode 100644 index a621f2cd1..000000000 --- a/components/upload/UploadList.tsx +++ /dev/null @@ -1,284 +0,0 @@ -import type { CSSProperties } from 'vue'; -import { defineComponent, inject, nextTick } from 'vue'; -import BaseMixin from '../_util/BaseMixin'; -import { getOptionProps, initDefaultProps } from '../_util/props-util'; -import { - getTransitionProps, - Transition, - getTransitionGroupProps, - TransitionGroup, -} from '../_util/transition'; -import { defaultConfigProvider } from '../config-provider'; -import { previewImage, isImageUrl } from './utils'; -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/FileOutlined'; -import DeleteOutlined from '@ant-design/icons-vue/DeleteOutlined'; -import DownloadOutlined from '@ant-design/icons-vue/DownloadOutlined'; -import EyeOutlined from '@ant-design/icons-vue/EyeOutlined'; -import Tooltip from '../tooltip'; -import Progress from '../progress'; -import classNames from '../_util/classNames'; -import { uploadListProps } from './interface'; - -export default defineComponent({ - name: 'AUploadList', - mixins: [BaseMixin], - props: initDefaultProps(uploadListProps, { - listType: 'text', // or picture - progressAttr: { - strokeWidth: 2, - showInfo: false, - }, - showRemoveIcon: true, - showDownloadIcon: false, - showPreviewIcon: true, - previewFile: previewImage, - }), - setup() { - return { - configProvider: inject('configProvider', defaultConfigProvider), - }; - }, - updated() { - nextTick(() => { - const { listType, items, previewFile } = this.$props; - if (listType !== 'picture' && listType !== 'picture-card') { - return; - } - (items || []).forEach(file => { - if ( - typeof document === 'undefined' || - typeof window === 'undefined' || - !window.FileReader || - !window.File || - !(file.originFileObj instanceof File || file.originFileObj instanceof Blob) || - file.thumbUrl !== undefined - ) { - return; - } - /*eslint-disable */ - file.thumbUrl = ''; - if (previewFile) { - previewFile(file.originFileObj).then(previewDataUrl => { - // Need append '' to avoid dead loop - file.thumbUrl = previewDataUrl || ''; - (this as any).$forceUpdate(); - }); - } - }); - }); - }, - methods: { - handlePreview(file, e) { - const { onPreview } = this.$props; - if (!onPreview) { - return; - } - e.preventDefault(); - return this.$emit('preview', file); - }, - handleDownload(file) { - const { onDownload } = this.$props; - if (typeof onDownload === 'function') { - this.$emit('download', file); - } else if (file.url) { - window.open(file.url); - } - }, - - handleClose(file) { - this.$emit('remove', file); - }, - }, - render() { - const { - prefixCls: customizePrefixCls, - items = [], - listType, - showPreviewIcon, - showRemoveIcon, - showDownloadIcon, - locale, - progressAttr, - } = getOptionProps(this); - const getPrefixCls = this.configProvider.getPrefixCls; - const prefixCls = getPrefixCls('upload', customizePrefixCls); - - const list = items.map(file => { - let progress; - let icon = file.status === 'uploading' ? : ; - - if (listType === 'picture' || listType === 'picture-card') { - if (listType === 'picture-card' && file.status === 'uploading') { - icon =
{locale.uploading}
; - } else if (!file.thumbUrl && !file.url) { - icon = ; - } else { - const thumbnail = isImageUrl(file) ? ( - {file.name} - ) : ( - - ); - icon = ( - this.handlePreview(file, e)} - href={file.url || file.thumbUrl} - target="_blank" - rel="noopener noreferrer" - > - {thumbnail} - - ); - } - } - - if (file.status === 'uploading') { - const progressProps = { - ...progressAttr, - type: 'line', - percent: file.percent, - }; - // show loading icon if upload progress listener is disabled - const loadingProgress = 'percent' in file ? : null; - - progress = ( -
- {loadingProgress} -
- ); - } - const infoUploadingClass = classNames({ - [`${prefixCls}-list-item`]: true, - [`${prefixCls}-list-item-${file.status}`]: true, - [`${prefixCls}-list-item-list-type-${listType}`]: true, - }); - const linkProps = - typeof file.linkProps === 'string' ? JSON.parse(file.linkProps) : file.linkProps; - - const removeIcon = showRemoveIcon ? ( - this.handleClose(file)} /> - ) : null; - const downloadIcon = - showDownloadIcon && file.status === 'done' ? ( - this.handleDownload(file)} /> - ) : null; - const downloadOrDelete = listType !== 'picture-card' && ( - - {downloadIcon && {downloadIcon}} - {removeIcon && {removeIcon}} - - ); - const listItemNameClass = classNames({ - [`${prefixCls}-list-item-name`]: true, - [`${prefixCls}-list-item-name-icon-count-${ - [downloadIcon, removeIcon].filter(x => x).length - }`]: true, - }); - - const preview = file.url - ? [ - this.handlePreview(file, e)} - > - {file.name} - , - downloadOrDelete, - ] - : [ - this.handlePreview(file, e)} - title={file.name} - > - {file.name} - , - downloadOrDelete, - ]; - const style: CSSProperties | undefined = - file.url || file.thumbUrl - ? undefined - : { - pointerEvents: 'none', - opacity: 0.5, - }; - const previewIcon = showPreviewIcon ? ( - this.handlePreview(file, e)} - title={locale.previewFile} - > - - - ) : 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 && file.error.statusText) || locale.uploadError; - } - const iconAndPreview = ( - - {icon} - {preview} - - ); - const transitionProps = getTransitionProps('fade'); - const dom = ( -
-
{iconAndPreview}
- {actions} - {progress} -
- ); - const listContainerNameClass = classNames({ - [`${prefixCls}-list-picture-card-container`]: listType === 'picture-card', - }); - return ( -
- {file.status === 'error' ? {dom} : {dom}} -
- ); - }); - const listClassNames = classNames({ - [`${prefixCls}-list`]: true, - [`${prefixCls}-list-${listType}`]: true, - }); - const animationDirection = listType === 'picture-card' ? 'animate-inline' : 'animate'; - const transitionGroupProps = { - ...getTransitionGroupProps(`${prefixCls}-${animationDirection}`), - class: listClassNames, - }; - return ( - - {list} - - ); - }, -}); diff --git a/components/upload/UploadList/ListItem.tsx b/components/upload/UploadList/ListItem.tsx new file mode 100644 index 000000000..6d492d37d --- /dev/null +++ b/components/upload/UploadList/ListItem.tsx @@ -0,0 +1,286 @@ +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: { 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>, + + 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 = slots.iconRender, + actionIconRender = slots.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/UploadList/index.tsx b/components/upload/UploadList/index.tsx new file mode 100644 index 000000000..6947f061d --- /dev/null +++ b/components/upload/UploadList/index.tsx @@ -0,0 +1,211 @@ +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'; +import listAnimation from './listAnimation'; + +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(() => ({ + ...listAnimation( + `${prefixCls.value}-${props.listType === 'picture-card' ? 'animate-inline' : 'animate'}`, + ), + ...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/UploadList/listAnimation.ts b/components/upload/UploadList/listAnimation.ts new file mode 100644 index 000000000..9c37d266f --- /dev/null +++ b/components/upload/UploadList/listAnimation.ts @@ -0,0 +1,44 @@ +import { addClass, removeClass } from '../../vc-util/Dom/class'; +import { nextTick } from 'vue'; +import type { CSSMotionProps } from '../../_util/transition'; + +const listAnimation = (name): CSSMotionProps => { + return { + name, + appear: true, + css: true, + onBeforeEnter: (node: HTMLDivElement) => { + addClass(node, name); + node.style.height = '0px'; + node.style.opacity = '0'; + }, + onEnter: (node: HTMLDivElement) => { + nextTick(() => { + node.style.height = `${node.scrollHeight}px`; + node.style.opacity = '1'; + }); + }, + onAfterEnter: (node: HTMLDivElement) => { + if (node) removeClass(node, name); + node.style.height = undefined; + node.style.opacity = undefined; + }, + onBeforeLeave: (node: HTMLDivElement) => { + addClass(node, name); + node.style.height = `${node.offsetHeight}px`; + node.style.opacity = undefined; + }, + onLeave: (node: HTMLDivElement) => { + setTimeout(() => { + node.style.height = '0px'; + node.style.opacity = '0'; + }); + }, + onAfterLeave: (node: HTMLDivElement) => { + if (node) removeClass(node, name); + node.style.height = undefined; + node.style.opacity = undefined; + }, + }; +}; +export default listAnimation; diff --git a/components/upload/__tests__/__snapshots__/demo.test.js.snap b/components/upload/__tests__/__snapshots__/demo.test.js.snap index 924cda504..bd98f94f5 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

-
-
-`; - -exports[`renders ./components/upload/demo/fileList.vue correctly 1`] = ` -
+
-
`; -exports[`renders ./components/upload/demo/picture-card.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

+
-
-
image.png - -
- +
+`; + +exports[`renders ./components/upload/demo/fileList.vue correctly 1`] = ` +
+
+
+
+ +
+ +
+`; + +exports[`renders ./components/upload/demo/max-count.vue correctly 1`] = ` +
+
+
+ +
-
-
Upload
-
+ +
+
+ +
+ +
+`; + +exports[`renders ./components/upload/demo/picture-card.vue correctly 1`] = ` +
+ + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+
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/upload-custom-action-icon.vue correctly 1`] = ` +
+
+
+
+
xxx.png
+
+ + +
+
+
+
+
yyy.png
+
+ + +
+
+
+ +
+ + + +
+
+ +
+`; + +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 9ec7b50d1..5a2aa53b8 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 e82a8907e..d6e9da8ea 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 6fe88fca9..52cd2fff1 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 cd68f1a22..66e14fc15 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/basic.vue b/components/upload/demo/basic.vue index 3c9862438..c2fb1f5e0 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. +--- +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/demo/customize-progress-bar.vue b/components/upload/demo/customize-progress-bar.vue new file mode 100644 index 000000000..c3afe5dbe --- /dev/null +++ b/components/upload/demo/customize-progress-bar.vue @@ -0,0 +1,76 @@ + +--- +order: 15 +title: + zh-CN: 自定义进度条样式 + en-US: Customize Progress Bar +--- + +## zh-CN + +使用 `progress` 属性自定义进度条样式。 + +## en-US + +Use `progress` for customize progress bar. + + + + + diff --git a/components/upload/demo/defaultFileList.vue b/components/upload/demo/defaultFileList.vue index 0cb1fab90..c37bc7de0 100644 --- a/components/upload/demo/defaultFileList.vue +++ b/components/upload/demo/defaultFileList.vue @@ -8,11 +8,11 @@ title: ## zh-CN -使用 `defaultFileList` 设置已上传的内容。 +使用 `fileList` 设置已上传的内容。 ## en-US -Use `defaultFileList` for uploaded files when page init. +Use `fileList` for uploaded files when page init.