import type { UploadProps as RcUploadProps } from '../vc-upload'; import VcUpload from '../vc-upload'; import UploadList from './UploadList'; import type { UploadType, UploadListType, UploadFile, UploadChangeParam, ShowUploadListInterface, FileType, } from './interface'; import { uploadProps } from './interface'; import { file2Obj, getFileItem, removeFileItem, updateFileList } from './utils'; import { useLocaleReceiver } from '../locale-provider/LocaleReceiver'; import defaultLocale from '../locale/default'; import type { CSSProperties } from 'vue'; 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({ compatConfig: { MODE: 3 }, name: 'AUpload', inheritAttrs: false, props: initDefaultProps(uploadProps(), { type: 'select' as UploadType, multiple: false, action: '', data: {}, accept: '', showUploadList: true, listType: 'text' as UploadListType, // or picture disabled: false, supportServerRender: true, }), setup(props, { slots, attrs, expose }) { const formItemContext = useInjectFormItemContext(); 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( 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); } } catch (e) { /* do nothing */ } // removed if (!getFileItem(file, mergedFileList.value)) { return; } const targetItem = file2Obj(file); targetItem.status = 'done'; targetItem.percent = 100; targetItem.response = response; targetItem.xhr = xhr; const nextFileList = updateFileList(targetItem, mergedFileList.value); onInternalChange(targetItem, nextFileList); }; const onProgress = (e: { percent: number }, file: FileType) => { // removed if (!getFileItem(file, mergedFileList.value)) { return; } const targetItem = file2Obj(file); targetItem.status = 'uploading'; targetItem.percent = e.percent; const nextFileList = updateFileList(targetItem, mergedFileList.value); onInternalChange(targetItem, nextFileList, e); }; const onError = (error: Error, response: any, file: FileType) => { // removed if (!getFileItem(file, mergedFileList.value)) { return; } const targetItem = file2Obj(file); targetItem.error = error; targetItem.response = response; targetItem.status = 'error'; const nextFileList = updateFileList(targetItem, mergedFileList.value); onInternalChange(targetItem, nextFileList); }; 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 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); onInternalChange(currentFile, removedFileList); } }, ); }; const onFileDrop = (e: DragEvent) => { dragState.value = e.type; if (e.type === 'drop') { props.onDrop?.(e); } }; 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, buttonVisible?: boolean) => { 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?.() ); }; return () => { const { listType, disabled, type } = props; const { class: className, style: styleName, ...transAttrs } = attrs; const rcUploadProps = { onBatchStart, onError, onProgress, onSuccess, ...transAttrs, ...(props as RcUploadProps), id: props.id ?? formItemContext.id.value, prefixCls: prefixCls.value, 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 // 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 renderUploadButton = (uploadButtonStyle?: CSSProperties) => (
); if (listType === 'picture-card') { return ( {renderUploadList(renderUploadButton, !!(children && children.length))} ); } return ( {renderUploadButton(children && children.length ? undefined : { display: 'none' })} {renderUploadList()} ); }; }, });