mirror of
https://gitee.com/ant-design-vue/ant-design-vue.git
synced 2024-12-02 03:58:05 +08:00
255 lines
7.7 KiB
Vue
255 lines
7.7 KiB
Vue
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 { 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 {
|
|
triggerRef,
|
|
watch,
|
|
computed,
|
|
defineComponent,
|
|
onMounted,
|
|
shallowRef,
|
|
watchEffect,
|
|
} from 'vue';
|
|
import { filterEmpty, initDefaultProps, isValidElement } from '../../_util/props-util';
|
|
import type { VueNode } from '../../_util/type';
|
|
import useConfigInject from '../../config-provider/hooks/useConfigInject';
|
|
import { getTransitionGroupProps, TransitionGroup } from '../../_util/transition';
|
|
import collapseMotion from '../../_util/collapseMotion';
|
|
|
|
const HackSlot = (_, { slots }) => {
|
|
return filterEmpty(slots.default?.())[0];
|
|
};
|
|
|
|
export default defineComponent({
|
|
compatConfig: { MODE: 3 },
|
|
name: 'AUploadList',
|
|
props: initDefaultProps(uploadListProps(), {
|
|
listType: 'text', // or picture
|
|
progress: {
|
|
strokeWidth: 2,
|
|
showInfo: false,
|
|
},
|
|
showRemoveIcon: true,
|
|
showDownloadIcon: false,
|
|
showPreviewIcon: true,
|
|
previewFile: previewImage,
|
|
isImageUrl,
|
|
items: [],
|
|
appendActionVisible: true,
|
|
}),
|
|
setup(props, { slots, expose }) {
|
|
const motionAppear = shallowRef(false);
|
|
onMounted(() => {
|
|
motionAppear.value == true;
|
|
});
|
|
const mergedItems = shallowRef([]);
|
|
watch(
|
|
() => props.items,
|
|
(val = []) => {
|
|
mergedItems.value = val.slice();
|
|
},
|
|
{
|
|
immediate: true,
|
|
deep: true,
|
|
},
|
|
);
|
|
watchEffect(() => {
|
|
if (props.listType !== 'picture' && props.listType !== 'picture-card') {
|
|
return;
|
|
}
|
|
let hasUpdate = false;
|
|
(props.items || []).forEach((file: InternalUploadFile, index) => {
|
|
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
|
|
const thumbUrl = previewDataUrl || '';
|
|
if (thumbUrl !== file.thumbUrl) {
|
|
mergedItems.value[index].thumbUrl = thumbUrl;
|
|
hasUpdate = true;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
if (hasUpdate) {
|
|
triggerRef(mergedItems);
|
|
}
|
|
});
|
|
|
|
// ============================= 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) ? <PictureTwoTone /> : <FileTwoTone />;
|
|
let icon: VueNode = isLoading ? <LoadingOutlined /> : <PaperClipOutlined />;
|
|
if (props.listType === 'picture') {
|
|
icon = isLoading ? <LoadingOutlined /> : 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-action`,
|
|
};
|
|
if (isValidElement(customIcon)) {
|
|
return <Button {...btnProps} v-slots={{ icon: () => customIcon }} />;
|
|
}
|
|
return (
|
|
<Button {...btnProps}>
|
|
<span>{customIcon}</span>
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
expose({
|
|
handlePreview: onInternalPreview,
|
|
handleDownload: onInternalDownload,
|
|
});
|
|
|
|
const { prefixCls, rootPrefixCls } = useConfigInject('upload', props);
|
|
|
|
const listClassNames = computed(() => ({
|
|
[`${prefixCls.value}-list`]: true,
|
|
[`${prefixCls.value}-list-${props.listType}`]: true,
|
|
}));
|
|
const transitionGroupProps = computed(() => {
|
|
const motion = {
|
|
...collapseMotion(`${rootPrefixCls.value}-motion-collapse`),
|
|
};
|
|
delete motion.onAfterAppear;
|
|
delete motion.onAfterEnter;
|
|
delete motion.onAfterLeave;
|
|
const motionConfig = {
|
|
...getTransitionGroupProps(
|
|
`${prefixCls.value}-${props.listType === 'picture-card' ? 'animate-inline' : 'animate'}`,
|
|
),
|
|
class: listClassNames.value,
|
|
appear: motionAppear.value,
|
|
};
|
|
return props.listType !== 'picture-card'
|
|
? {
|
|
...motion,
|
|
...motionConfig,
|
|
}
|
|
: motionConfig;
|
|
});
|
|
return () => {
|
|
const {
|
|
listType,
|
|
locale,
|
|
isImageUrl: isImgUrl,
|
|
showPreviewIcon,
|
|
showRemoveIcon,
|
|
showDownloadIcon,
|
|
removeIcon,
|
|
previewIcon,
|
|
downloadIcon,
|
|
progress,
|
|
appendAction,
|
|
itemRender,
|
|
appendActionVisible,
|
|
} = props;
|
|
const appendActionDom = appendAction?.();
|
|
const items = mergedItems.value;
|
|
return (
|
|
<TransitionGroup {...transitionGroupProps.value} tag="div">
|
|
{items.map(file => {
|
|
const { uid: key } = file;
|
|
return (
|
|
<ListItem
|
|
key={key}
|
|
locale={locale}
|
|
prefixCls={prefixCls.value}
|
|
file={file}
|
|
items={items}
|
|
progress={progress}
|
|
listType={listType}
|
|
isImgUrl={isImgUrl}
|
|
showPreviewIcon={showPreviewIcon}
|
|
showRemoveIcon={showRemoveIcon}
|
|
showDownloadIcon={showDownloadIcon}
|
|
onPreview={onInternalPreview}
|
|
onDownload={onInternalDownload}
|
|
onClose={onInternalClose}
|
|
removeIcon={removeIcon}
|
|
previewIcon={previewIcon}
|
|
downloadIcon={downloadIcon}
|
|
itemRender={itemRender}
|
|
v-slots={{
|
|
...slots,
|
|
iconRender: internalIconRender,
|
|
actionIconRender,
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
{appendAction ? (
|
|
<HackSlot
|
|
key="__ant_upload_appendAction"
|
|
v-show={!!appendActionVisible}
|
|
v-slots={{ default: () => appendActionDom }}
|
|
></HackSlot>
|
|
) : null}
|
|
</TransitionGroup>
|
|
);
|
|
};
|
|
},
|
|
});
|