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 (
-
-
- {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) ? (
-
- ) : (
-
- );
- 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) ? (
+
+ ) : (
+ 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