refactor: upload
parent
67f5226cdc
commit
f0f970c37b
|
@ -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';
|
||||
|
||||
|
|
|
@ -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<string, any>;
|
||||
Upload?: Record<string, any>;
|
||||
Form?: {
|
||||
optional?: string;
|
||||
defaultValidateMessages: ValidateMessages;
|
||||
|
@ -32,6 +32,7 @@ export interface Locale {
|
|||
Modal?: ModalLocale;
|
||||
Transfer?: Partial<TransferLocale>;
|
||||
Select?: Record<string, any>;
|
||||
Upload?: UploadLocale;
|
||||
Empty?: TransferLocaleForEmpty;
|
||||
global?: Record<string, any>;
|
||||
PageHeader?: { back: string };
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import Upload from './Upload';
|
||||
import { uploadProps } from './interface';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AUploadDragger',
|
||||
inheritAttrs: false,
|
||||
props: uploadProps(),
|
||||
setup(props, { slots, attrs }) {
|
||||
return () => {
|
||||
const { height, ...restProps } = props;
|
||||
const { style, ...restAttrs } = attrs;
|
||||
const draggerProps = {
|
||||
...restProps,
|
||||
...restAttrs,
|
||||
type: 'drag',
|
||||
style: { ...(style as any), height: typeof height === 'number' ? `${height}px` : height },
|
||||
} as any;
|
||||
return <Upload {...draggerProps} v-slots={slots}></Upload>;
|
||||
};
|
||||
},
|
||||
});
|
|
@ -1,261 +0,0 @@
|
|||
import * as React from 'react';
|
||||
import CSSMotion, { CSSMotionList, CSSMotionListProps } from 'rc-motion';
|
||||
import classNames from 'classnames';
|
||||
import LoadingOutlined from '@ant-design/icons/LoadingOutlined';
|
||||
import PaperClipOutlined from '@ant-design/icons/PaperClipOutlined';
|
||||
import PictureTwoTone from '@ant-design/icons/PictureTwoTone';
|
||||
import FileTwoTone from '@ant-design/icons/FileTwoTone';
|
||||
import { cloneElement, isValidElement } from '../../_util/reactNode';
|
||||
import { UploadListProps, UploadFile, UploadListType, InternalUploadFile } from '../interface';
|
||||
import { previewImage, isImageUrl } from '../utils';
|
||||
import collapseMotion from '../../_util/motion';
|
||||
import { ConfigContext } from '../../config-provider';
|
||||
import Button, { ButtonProps } from '../../button';
|
||||
import useForceUpdate from '../../_util/hooks/useForceUpdate';
|
||||
import ListItem from './ListItem';
|
||||
|
||||
const listItemMotion: Partial<CSSMotionListProps> = {
|
||||
...collapseMotion,
|
||||
};
|
||||
|
||||
delete listItemMotion.onAppearEnd;
|
||||
delete listItemMotion.onEnterEnd;
|
||||
delete listItemMotion.onLeaveEnd;
|
||||
|
||||
const InternalUploadList: React.ForwardRefRenderFunction<unknown, UploadListProps> = (
|
||||
{
|
||||
listType,
|
||||
previewFile,
|
||||
onPreview,
|
||||
onDownload,
|
||||
onRemove,
|
||||
locale,
|
||||
iconRender,
|
||||
isImageUrl: isImgUrl,
|
||||
prefixCls: customizePrefixCls,
|
||||
items = [],
|
||||
showPreviewIcon,
|
||||
showRemoveIcon,
|
||||
showDownloadIcon,
|
||||
removeIcon,
|
||||
previewIcon,
|
||||
downloadIcon,
|
||||
progress,
|
||||
appendAction,
|
||||
itemRender,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const [motionAppear, setMotionAppear] = React.useState(false);
|
||||
|
||||
// ============================= Effect =============================
|
||||
React.useEffect(() => {
|
||||
if (listType !== 'picture' && listType !== 'picture-card') {
|
||||
return;
|
||||
}
|
||||
(items || []).forEach((file: InternalUploadFile) => {
|
||||
if (
|
||||
typeof document === 'undefined' ||
|
||||
typeof window === 'undefined' ||
|
||||
!(window as any).FileReader ||
|
||||
!(window as any).File ||
|
||||
!(file.originFileObj instanceof File || (file.originFileObj as Blob) instanceof Blob) ||
|
||||
file.thumbUrl !== undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
file.thumbUrl = '';
|
||||
if (previewFile) {
|
||||
previewFile(file.originFileObj as File).then((previewDataUrl: string) => {
|
||||
// Need append '' to avoid dead loop
|
||||
file.thumbUrl = previewDataUrl || '';
|
||||
forceUpdate();
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [listType, items, previewFile]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setMotionAppear(true);
|
||||
}, []);
|
||||
|
||||
// ============================= Events =============================
|
||||
const onInternalPreview = (file: UploadFile, e?: React.SyntheticEvent<HTMLElement>) => {
|
||||
if (!onPreview) {
|
||||
return;
|
||||
}
|
||||
e?.preventDefault();
|
||||
return onPreview(file);
|
||||
};
|
||||
|
||||
const onInternalDownload = (file: UploadFile) => {
|
||||
if (typeof onDownload === 'function') {
|
||||
onDownload(file);
|
||||
} else if (file.url) {
|
||||
window.open(file.url);
|
||||
}
|
||||
};
|
||||
|
||||
const onInternalClose = (file: UploadFile) => {
|
||||
onRemove?.(file);
|
||||
};
|
||||
|
||||
const internalIconRender = (file: UploadFile) => {
|
||||
if (iconRender) {
|
||||
return iconRender(file, listType);
|
||||
}
|
||||
const isLoading = file.status === 'uploading';
|
||||
const fileIcon = isImgUrl && isImgUrl(file) ? <PictureTwoTone /> : <FileTwoTone />;
|
||||
let icon: React.ReactNode = isLoading ? <LoadingOutlined /> : <PaperClipOutlined />;
|
||||
if (listType === 'picture') {
|
||||
icon = isLoading ? <LoadingOutlined /> : fileIcon;
|
||||
} else if (listType === 'picture-card') {
|
||||
icon = isLoading ? locale.uploading : fileIcon;
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
const actionIconRender = (
|
||||
customIcon: React.ReactNode,
|
||||
callback: () => void,
|
||||
prefixCls: string,
|
||||
title?: string,
|
||||
) => {
|
||||
const btnProps: ButtonProps = {
|
||||
type: 'text',
|
||||
size: 'small',
|
||||
title,
|
||||
onClick: (e: React.MouseEvent<HTMLElement>) => {
|
||||
callback();
|
||||
if (isValidElement(customIcon) && customIcon.props.onClick) {
|
||||
customIcon.props.onClick(e);
|
||||
}
|
||||
},
|
||||
className: `${prefixCls}-list-item-card-actions-btn`,
|
||||
};
|
||||
if (isValidElement(customIcon)) {
|
||||
const btnIcon = cloneElement(customIcon, {
|
||||
...customIcon.props,
|
||||
onClick: () => {},
|
||||
});
|
||||
|
||||
return <Button {...btnProps} icon={btnIcon} />;
|
||||
}
|
||||
return (
|
||||
<Button {...btnProps}>
|
||||
<span>{customIcon}</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
// ============================== Ref ===============================
|
||||
// Test needs
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
handlePreview: onInternalPreview,
|
||||
handleDownload: onInternalDownload,
|
||||
}));
|
||||
|
||||
const { getPrefixCls, direction } = React.useContext(ConfigContext);
|
||||
|
||||
// ============================= Render =============================
|
||||
const prefixCls = getPrefixCls('upload', customizePrefixCls);
|
||||
|
||||
const listClassNames = classNames({
|
||||
[`${prefixCls}-list`]: true,
|
||||
[`${prefixCls}-list-${listType}`]: true,
|
||||
[`${prefixCls}-list-rtl`]: direction === 'rtl',
|
||||
});
|
||||
|
||||
// >>> Motion config
|
||||
const motionKeyList = [
|
||||
...items.map(file => ({
|
||||
key: file.uid,
|
||||
file,
|
||||
})),
|
||||
];
|
||||
|
||||
const animationDirection = listType === 'picture-card' ? 'animate-inline' : 'animate';
|
||||
// const transitionName = list.length === 0 ? '' : `${prefixCls}-${animationDirection}`;
|
||||
|
||||
let motionConfig: Omit<CSSMotionListProps, 'onVisibleChanged'> = {
|
||||
motionDeadline: 2000,
|
||||
motionName: `${prefixCls}-${animationDirection}`,
|
||||
keys: motionKeyList,
|
||||
motionAppear,
|
||||
};
|
||||
|
||||
if (listType !== 'picture-card') {
|
||||
motionConfig = {
|
||||
...listItemMotion,
|
||||
...motionConfig,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={listClassNames}>
|
||||
<CSSMotionList {...motionConfig} component={false}>
|
||||
{({ key, file, className: motionClassName, style: motionStyle }) => (
|
||||
<ListItem
|
||||
key={key}
|
||||
locale={locale}
|
||||
prefixCls={prefixCls}
|
||||
className={motionClassName}
|
||||
style={motionStyle}
|
||||
file={file}
|
||||
items={items}
|
||||
progress={progress}
|
||||
listType={listType}
|
||||
isImgUrl={isImgUrl}
|
||||
showPreviewIcon={showPreviewIcon}
|
||||
showRemoveIcon={showRemoveIcon}
|
||||
showDownloadIcon={showDownloadIcon}
|
||||
removeIcon={removeIcon}
|
||||
previewIcon={previewIcon}
|
||||
downloadIcon={downloadIcon}
|
||||
iconRender={internalIconRender}
|
||||
actionIconRender={actionIconRender}
|
||||
itemRender={itemRender}
|
||||
onPreview={onInternalPreview}
|
||||
onDownload={onInternalDownload}
|
||||
onClose={onInternalClose}
|
||||
/>
|
||||
)}
|
||||
</CSSMotionList>
|
||||
|
||||
{/* Append action */}
|
||||
{appendAction && (
|
||||
<CSSMotion {...motionConfig}>
|
||||
{({ className: motionClassName, style: motionStyle }) =>
|
||||
cloneElement(appendAction, oriProps => ({
|
||||
className: classNames(oriProps.className, motionClassName),
|
||||
style: {
|
||||
...motionStyle,
|
||||
...oriProps.style,
|
||||
},
|
||||
}))
|
||||
}
|
||||
</CSSMotion>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UploadList = React.forwardRef<unknown, UploadListProps>(InternalUploadList);
|
||||
|
||||
UploadList.displayName = 'UploadList';
|
||||
|
||||
UploadList.defaultProps = {
|
||||
listType: 'text' as UploadListType, // or picture
|
||||
progress: {
|
||||
strokeWidth: 2,
|
||||
showInfo: false,
|
||||
},
|
||||
showRemoveIcon: true,
|
||||
showDownloadIcon: false,
|
||||
showPreviewIcon: true,
|
||||
previewFile: previewImage,
|
||||
isImageUrl,
|
||||
};
|
||||
|
||||
export default UploadList;
|
|
@ -1,17 +0,0 @@
|
|||
import type { App } from 'vue';
|
||||
import Upload from './Upload';
|
||||
import Dragger from './Dragger';
|
||||
|
||||
export type { UploadProps, UploadListProps, UploadChangeParam } from './interface';
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const UploadDragger = Dragger;
|
||||
|
||||
export default Object.assign(Upload, {
|
||||
Dragger,
|
||||
install(app: App) {
|
||||
app.component(Upload.name, Upload);
|
||||
app.component(Dragger.name, Dragger);
|
||||
return app;
|
||||
},
|
||||
});
|
|
@ -1,183 +0,0 @@
|
|||
import type {
|
||||
RcFile as OriRcFile,
|
||||
UploadRequestOption as RcCustomRequestOptions,
|
||||
} from '../vc-upload/interface';
|
||||
import type { ProgressProps } from '../progress';
|
||||
import type { VueNode } from '../_util/type';
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
|
||||
export interface RcFile extends OriRcFile {
|
||||
readonly lastModifiedDate: Date;
|
||||
}
|
||||
|
||||
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
|
||||
|
||||
export interface HttpRequestHeader {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface UploadFile<T = any> {
|
||||
uid: string;
|
||||
size?: number;
|
||||
name: string;
|
||||
fileName?: string;
|
||||
lastModified?: number;
|
||||
lastModifiedDate?: Date;
|
||||
url?: string;
|
||||
status?: UploadFileStatus;
|
||||
percent?: number;
|
||||
thumbUrl?: string;
|
||||
originFileObj?: RcFile;
|
||||
response?: T;
|
||||
error?: any;
|
||||
linkProps?: any;
|
||||
type?: string;
|
||||
xhr?: T;
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
export interface InternalUploadFile<T = any> extends UploadFile<T> {
|
||||
originFileObj: RcFile;
|
||||
}
|
||||
|
||||
export interface UploadChangeParam<T = UploadFile> {
|
||||
// https://github.com/ant-design/ant-design/issues/14420
|
||||
file: T;
|
||||
fileList: UploadFile[];
|
||||
event?: { percent: number };
|
||||
}
|
||||
|
||||
// export interface ShowUploadListInterface {
|
||||
// showRemoveIcon?: boolean;
|
||||
// showPreviewIcon?: boolean;
|
||||
// showDownloadIcon?: boolean;
|
||||
// removeIcon?: VueNode | ((file: UploadFile) => VueNode);
|
||||
// downloadIcon?: VueNode | ((file: UploadFile) => VueNode);
|
||||
// previewIcon?: VueNode | ((file: UploadFile) => VueNode);
|
||||
// }
|
||||
|
||||
export interface UploadLocale {
|
||||
uploading?: string;
|
||||
removeFile?: string;
|
||||
downloadFile?: string;
|
||||
uploadError?: string;
|
||||
previewFile?: string;
|
||||
}
|
||||
|
||||
export type UploadType = 'drag' | 'select';
|
||||
export type UploadListType = 'text' | 'picture' | 'picture-card';
|
||||
export type UploadListProgressProps = Omit<ProgressProps, 'percent' | 'type'>;
|
||||
|
||||
export type ItemRender<T = any> = (opt: {
|
||||
originNode: VueNode;
|
||||
file: UploadFile;
|
||||
fileList: Array<UploadFile<T>>;
|
||||
actions: {
|
||||
download: () => void;
|
||||
preview: () => void;
|
||||
remove: () => void;
|
||||
};
|
||||
}) => VueNode;
|
||||
|
||||
type PreviewFileHandler = (file: File | Blob) => PromiseLike<string>;
|
||||
type TransformFileHandler = (
|
||||
file: RcFile,
|
||||
) => string | Blob | File | PromiseLike<string | Blob | File>;
|
||||
type BeforeUploadValueType = void | boolean | string | Blob | File;
|
||||
|
||||
function uploadProps<T = any>() {
|
||||
return {
|
||||
capture: [Boolean, String] as PropType<boolean | 'user' | 'environment'>,
|
||||
type: String as PropType<UploadType>,
|
||||
name: String,
|
||||
defaultFileList: Array as PropType<Array<UploadFile<T>>>,
|
||||
fileList: Array as PropType<Array<UploadFile<T>>>,
|
||||
action: [String, Function] as PropType<
|
||||
string | ((file: RcFile) => string) | ((file: RcFile) => PromiseLike<string>)
|
||||
>,
|
||||
directory: Boolean,
|
||||
data: [Object, Function] as PropType<
|
||||
| Record<string, unknown>
|
||||
| ((file: UploadFile<T>) => Record<string, unknown> | Promise<Record<string, unknown>>)
|
||||
>,
|
||||
method: String as PropType<'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch'>,
|
||||
headers: Object as PropType<HttpRequestHeader>,
|
||||
showUploadList: Boolean,
|
||||
multiple: Boolean,
|
||||
accept: String,
|
||||
beforeUpload: Function as PropType<
|
||||
(file: RcFile, FileList: RcFile[]) => BeforeUploadValueType | Promise<BeforeUploadValueType>
|
||||
>,
|
||||
onChange: Function as PropType<(info: UploadChangeParam<T>) => void>,
|
||||
onDrop: Function as PropType<(event: DragEvent) => void>,
|
||||
listType: String as PropType<UploadListType>,
|
||||
onPreview: Function as PropType<(file: UploadFile<T>) => void>,
|
||||
onDownload: Function as PropType<(file: UploadFile<T>) => void>,
|
||||
onRemove: Function as PropType<
|
||||
(file: UploadFile<T>) => void | boolean | Promise<void | boolean>
|
||||
>,
|
||||
supportServerRender: Boolean,
|
||||
disabled: Boolean,
|
||||
prefixCls: String,
|
||||
customRequest: Function as PropType<(options: RcCustomRequestOptions) => void>,
|
||||
withCredentials: Boolean,
|
||||
openFileDialogOnClick: Boolean,
|
||||
locale: Object as PropType<UploadLocale>,
|
||||
id: String,
|
||||
previewFile: Function as PropType<PreviewFileHandler>,
|
||||
/** @deprecated Please use `beforeUpload` directly */
|
||||
transformFile: Function as PropType<TransformFileHandler>,
|
||||
iconRender: Function as PropType<
|
||||
(opt: { file: UploadFile<T>; listType?: UploadListType }) => VueNode
|
||||
>,
|
||||
isImageUrl: Function as PropType<(file: UploadFile) => boolean>,
|
||||
progress: Object as PropType<UploadListProgressProps>,
|
||||
itemRender: Function as PropType<ItemRender<T>>,
|
||||
/** Config max count of `fileList`. Will replace current one when `maxCount` is 1 */
|
||||
maxCount: Number,
|
||||
height: [Number, String],
|
||||
|
||||
showRemoveIcon: Boolean,
|
||||
showDownloadIcon: Boolean,
|
||||
showPreviewIcon: Boolean,
|
||||
removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
};
|
||||
}
|
||||
|
||||
export type UploadProps = Partial<ExtractPropTypes<ReturnType<typeof uploadProps>>>;
|
||||
|
||||
export interface UploadState<T = any> {
|
||||
fileList: UploadFile<T>[];
|
||||
dragState: string;
|
||||
}
|
||||
|
||||
function uploadListProps<T = any>() {
|
||||
return {
|
||||
listType: String as PropType<UploadListType>,
|
||||
onPreview: Function as PropType<(file: UploadFile<T>) => void>,
|
||||
onDownload: Function as PropType<(file: UploadFile<T>) => void>,
|
||||
onRemove: Function as PropType<(file: UploadFile<T>) => void | boolean>,
|
||||
items: Array as PropType<Array<UploadFile<T>>>,
|
||||
progress: Object as PropType<UploadListProgressProps>,
|
||||
prefixCls: String as PropType<string>,
|
||||
showRemoveIcon: Boolean,
|
||||
showDownloadIcon: Boolean,
|
||||
showPreviewIcon: Boolean,
|
||||
removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
locale: Object as PropType<UploadLocale>,
|
||||
previewFile: Function as PropType<PreviewFileHandler>,
|
||||
iconRender: Function as PropType<
|
||||
(opt: { file: UploadFile<T>; listType?: UploadListType }) => VueNode
|
||||
>,
|
||||
isImageUrl: Function as PropType<(file: UploadFile) => boolean>,
|
||||
appendAction: Function as PropType<() => VueNode>,
|
||||
itemRender: Function as PropType<ItemRender<T>>,
|
||||
};
|
||||
}
|
||||
|
||||
export type UploadListProps = Partial<ExtractPropTypes<ReturnType<typeof uploadListProps>>>;
|
||||
export { uploadProps, uploadListProps };
|
|
@ -0,0 +1,22 @@
|
|||
import { defineComponent } from 'vue';
|
||||
import { getOptionProps, getSlot } from '../_util/props-util';
|
||||
import Upload from './Upload';
|
||||
import { uploadProps } from './interface';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AUploadDragger',
|
||||
inheritAttrs: false,
|
||||
props: uploadProps,
|
||||
render() {
|
||||
const props = getOptionProps(this);
|
||||
const { height, ...restProps } = props;
|
||||
const { style, ...restAttrs } = this.$attrs;
|
||||
const draggerProps = {
|
||||
...restProps,
|
||||
...restAttrs,
|
||||
type: 'drag',
|
||||
style: { ...(style as any), height },
|
||||
} as any;
|
||||
return <Upload {...draggerProps}>{getSlot(this)}</Upload>;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
import type { App, Plugin } from 'vue';
|
||||
import Upload from './Upload';
|
||||
import Dragger from './Dragger';
|
||||
|
||||
export type { UploadProps, UploadListProps, UploadChangeParam } from './interface';
|
||||
|
||||
Upload.Dragger = Dragger;
|
||||
|
||||
/* istanbul ignore next */
|
||||
Upload.install = function (app: App) {
|
||||
app.component(Upload.name, Upload);
|
||||
app.component(Dragger.name, Dragger);
|
||||
return app;
|
||||
};
|
||||
|
||||
export const UploadDragger = Dragger;
|
||||
|
||||
export default Upload as typeof Upload &
|
||||
Plugin & {
|
||||
readonly Dragger: typeof Dragger;
|
||||
};
|
|
@ -0,0 +1,116 @@
|
|||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import { tuple } from '../_util/type';
|
||||
import PropsTypes from '../_util/vue-types';
|
||||
|
||||
export const UploadFileStatus = PropsTypes.oneOf(
|
||||
tuple('error', 'success', 'done', 'uploading', 'removed'),
|
||||
);
|
||||
|
||||
export interface HttpRequestHeader {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface VcFile extends File {
|
||||
uid: string;
|
||||
readonly lastModifiedDate: Date;
|
||||
readonly webkitRelativePath: string;
|
||||
}
|
||||
|
||||
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
|
||||
export interface UploadFile<T = any> {
|
||||
uid: string;
|
||||
size?: number;
|
||||
name: string;
|
||||
fileName?: string;
|
||||
lastModified?: number;
|
||||
lastModifiedDate?: Date;
|
||||
url?: string;
|
||||
status?: UploadFileStatus;
|
||||
percent?: number;
|
||||
thumbUrl?: string;
|
||||
originFileObj?: any;
|
||||
response?: T;
|
||||
error?: any;
|
||||
linkProps?: any;
|
||||
type?: string;
|
||||
xhr?: T;
|
||||
preview?: string;
|
||||
}
|
||||
|
||||
export interface UploadChangeParam<T extends object = UploadFile> {
|
||||
file: T;
|
||||
fileList: UploadFile[];
|
||||
event?: { percent: number };
|
||||
}
|
||||
|
||||
export const ShowUploadListInterface = PropsTypes.shape({
|
||||
showRemoveIcon: PropsTypes.looseBool,
|
||||
showPreviewIcon: PropsTypes.looseBool,
|
||||
}).loose;
|
||||
|
||||
export interface UploadLocale {
|
||||
uploading?: string;
|
||||
removeFile?: string;
|
||||
downloadFile?: string;
|
||||
uploadError?: string;
|
||||
previewFile?: string;
|
||||
}
|
||||
|
||||
export const uploadProps = {
|
||||
type: PropsTypes.oneOf(tuple('drag', 'select')),
|
||||
name: PropsTypes.string,
|
||||
defaultFileList: { type: Array as PropType<UploadFile[]> },
|
||||
fileList: { type: Array as PropType<UploadFile[]> },
|
||||
action: PropsTypes.oneOfType([PropsTypes.string, PropsTypes.func]),
|
||||
directory: PropsTypes.looseBool,
|
||||
data: PropsTypes.oneOfType([PropsTypes.object, PropsTypes.func]),
|
||||
method: PropsTypes.oneOf(tuple('POST', 'PUT', 'PATCH', 'post', 'put', 'patch')),
|
||||
headers: PropsTypes.object,
|
||||
showUploadList: PropsTypes.oneOfType([PropsTypes.looseBool, ShowUploadListInterface]),
|
||||
multiple: PropsTypes.looseBool,
|
||||
accept: PropsTypes.string,
|
||||
beforeUpload: PropsTypes.func,
|
||||
listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')),
|
||||
// className: PropsTypes.string,
|
||||
remove: PropsTypes.func,
|
||||
supportServerRender: PropsTypes.looseBool,
|
||||
// style: PropsTypes.object,
|
||||
disabled: PropsTypes.looseBool,
|
||||
prefixCls: PropsTypes.string,
|
||||
customRequest: PropsTypes.func,
|
||||
withCredentials: PropsTypes.looseBool,
|
||||
openFileDialogOnClick: PropsTypes.looseBool,
|
||||
locale: { type: Object as PropType<UploadLocale> },
|
||||
height: PropsTypes.number,
|
||||
id: PropsTypes.string,
|
||||
previewFile: PropsTypes.func,
|
||||
transformFile: PropsTypes.func,
|
||||
onChange: { type: Function as PropType<(info: UploadChangeParam) => void> },
|
||||
onPreview: { type: Function as PropType<(file: UploadFile) => void> },
|
||||
onRemove: {
|
||||
type: Function as PropType<(file: UploadFile) => void | boolean | Promise<void | boolean>>,
|
||||
},
|
||||
onDownload: { type: Function as PropType<(file: UploadFile) => void> },
|
||||
'onUpdate:fileList': { type: Function as PropType<(files: UploadFile[]) => void> },
|
||||
};
|
||||
|
||||
export type UploadProps = Partial<ExtractPropTypes<typeof uploadProps>>;
|
||||
export const uploadListProps = {
|
||||
listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')),
|
||||
// items: PropsTypes.arrayOf(UploadFile),
|
||||
items: { type: Array as PropType<UploadFile[]> },
|
||||
progressAttr: PropsTypes.object,
|
||||
prefixCls: PropsTypes.string,
|
||||
showRemoveIcon: PropsTypes.looseBool,
|
||||
showDownloadIcon: PropsTypes.looseBool,
|
||||
showPreviewIcon: PropsTypes.looseBool,
|
||||
locale: { type: Object as PropType<UploadLocale> },
|
||||
previewFile: PropsTypes.func,
|
||||
onPreview: { type: Function as PropType<(file: UploadFile) => void> },
|
||||
onRemove: {
|
||||
type: Function as PropType<(file: UploadFile) => void | boolean>,
|
||||
},
|
||||
onDownload: { type: Function as PropType<(file: UploadFile) => void> },
|
||||
};
|
||||
|
||||
export type UploadListProps = Partial<ExtractPropTypes<typeof uploadListProps>>;
|
|
@ -34,6 +34,8 @@
|
|||
}
|
||||
|
||||
&&-select-picture-card {
|
||||
display: table;
|
||||
float: left;
|
||||
width: @upload-picture-card-size;
|
||||
height: @upload-picture-card-size;
|
||||
margin-right: 8px;
|
||||
|
@ -44,21 +46,19 @@
|
|||
border: @border-width-base dashed @border-color-base;
|
||||
border-radius: @border-radius-base;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s;
|
||||
transition: border-color 0.3s ease;
|
||||
|
||||
> .@{upload-prefix-cls} {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: @primary-color;
|
||||
.@{upload-prefix-cls}-disabled& {
|
||||
border-color: @border-color-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@
|
|||
transition: border-color 0.3s;
|
||||
|
||||
.@{upload-prefix-cls} {
|
||||
padding: @padding-md 0;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
&.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
|
||||
|
@ -116,12 +116,10 @@
|
|||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
.@{iconfont-css-prefix}-plus {
|
||||
color: @disabled-color;
|
||||
font-size: 30px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
|
@ -142,55 +140,52 @@
|
|||
.@{upload-prefix-cls}-list {
|
||||
.reset-component();
|
||||
.clearfix();
|
||||
line-height: @line-height-base;
|
||||
|
||||
// ============================ Item ============================
|
||||
&-item-list-type-text {
|
||||
&:hover {
|
||||
.@{upload-prefix-cls}-list-item-name-icon-count-1 {
|
||||
padding-right: 14px;
|
||||
}
|
||||
.@{upload-prefix-cls}-list-item-name-icon-count-2 {
|
||||
padding-right: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-item {
|
||||
position: relative;
|
||||
height: @line-height-base * @font-size-base;
|
||||
margin-top: @margin-xs;
|
||||
height: 22px;
|
||||
margin-top: 8px;
|
||||
font-size: @font-size-base;
|
||||
|
||||
&-name {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding-left: @font-size-base + 8px;
|
||||
overflow: hidden;
|
||||
line-height: @line-height-base;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&-name-icon-count-1 {
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
&-card-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
&-btn {
|
||||
opacity: 0;
|
||||
}
|
||||
&-btn.@{ant-prefix}-btn-sm {
|
||||
height: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&.picture {
|
||||
top: 22px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
&-btn:focus,
|
||||
&.picture &-btn {
|
||||
top: 25px;
|
||||
line-height: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
color: @upload-actions-color;
|
||||
.anticon {
|
||||
padding-right: 6px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
height: 100%;
|
||||
padding: 0 4px;
|
||||
padding: 0 12px 0 4px;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
> span {
|
||||
|
@ -200,27 +195,25 @@
|
|||
}
|
||||
|
||||
.@{iconfont-css-prefix}-loading,
|
||||
.@{upload-prefix-cls}-text-icon {
|
||||
.@{iconfont-css-prefix} {
|
||||
.@{iconfont-css-prefix}-paper-clip {
|
||||
position: absolute;
|
||||
top: (@font-size-base / 2) - 2px;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{iconfont-css-prefix}-close {
|
||||
.iconfont-size-under-12px(10px);
|
||||
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 4px;
|
||||
color: @text-color-secondary;
|
||||
font-size: 10px;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: @text-color;
|
||||
}
|
||||
|
@ -234,25 +227,22 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover &-card-actions-btn {
|
||||
&:hover &-card-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-error,
|
||||
&-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix},
|
||||
&-error .@{iconfont-css-prefix}-paper-clip,
|
||||
&-error &-name {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&-error &-card-actions {
|
||||
.@{iconfont-css-prefix} {
|
||||
.anticon {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-progress {
|
||||
position: absolute;
|
||||
|
@ -264,20 +254,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
// =================== Picture & Picture Card ===================
|
||||
&-picture,
|
||||
&-picture-card {
|
||||
.@{upload-item} {
|
||||
position: relative;
|
||||
height: 66px;
|
||||
padding: @padding-xs;
|
||||
padding: 8px;
|
||||
border: @border-width-base @upload-picture-card-border-style @border-color-base;
|
||||
border-radius: @border-radius-base;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&-error {
|
||||
border-color: @error-color;
|
||||
}
|
||||
|
@ -296,30 +283,15 @@
|
|||
}
|
||||
|
||||
.@{upload-item}-thumbnail {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
line-height: 60px;
|
||||
font-size: 26px;
|
||||
line-height: 54px;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the color of the error icon : https://github.com/ant-design/ant-design/pull/24160
|
||||
.@{upload-item}-error .@{upload-item}-thumbnail {
|
||||
.@{iconfont-css-prefix} {
|
||||
svg path {
|
||||
&[fill='#e6f7ff'] {
|
||||
fill: @error-color-deprecated-bg;
|
||||
}
|
||||
|
||||
&[fill='#1890ff'] {
|
||||
fill: @error-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{upload-item}-icon {
|
||||
|
@ -328,10 +300,6 @@
|
|||
left: 50%;
|
||||
font-size: 26px;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.@{upload-item}-image {
|
||||
|
@ -359,8 +327,16 @@
|
|||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.@{upload-item}-name-icon-count-1 {
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.@{upload-item}-name-icon-count-2 {
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.@{upload-item}-uploading .@{upload-item}-name {
|
||||
margin-bottom: 12px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.@{upload-item}-progress {
|
||||
|
@ -379,23 +355,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
// ======================== Picture Card ========================
|
||||
&-picture-card {
|
||||
&-container {
|
||||
display: inline-block;
|
||||
width: @upload-picture-card-size;
|
||||
height: @upload-picture-card-size;
|
||||
margin: 0 @margin-xs @margin-xs 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&.@{upload-prefix-cls}-list::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-container {
|
||||
float: left;
|
||||
width: @upload-picture-card-size;
|
||||
height: @upload-picture-card-size;
|
||||
margin: 0 8px 8px 0;
|
||||
}
|
||||
.@{upload-item} {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
float: left;
|
||||
width: @upload-picture-card-size;
|
||||
height: @upload-picture-card-size;
|
||||
margin: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.@{upload-item}-info {
|
||||
|
@ -439,7 +413,6 @@
|
|||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: @text-color-inverse;
|
||||
}
|
||||
|
@ -457,7 +430,7 @@
|
|||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.@{upload-item}-name {
|
||||
|
@ -468,7 +441,7 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.@{upload-item}-file + .@{upload-item}-name {
|
||||
.anticon-picture + .@{upload-item}-name {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
display: block;
|
||||
|
@ -481,82 +454,46 @@
|
|||
|
||||
.@{upload-item}-info {
|
||||
height: auto;
|
||||
|
||||
&::before,
|
||||
.@{iconfont-css-prefix}-eye,
|
||||
.@{iconfont-css-prefix}-eye-o,
|
||||
.@{iconfont-css-prefix}-delete {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-top: 18px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.@{upload-item}-progress {
|
||||
bottom: 32px;
|
||||
width: calc(100% - 14px);
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================= Picture & Text =======================
|
||||
&-text,
|
||||
&-picture {
|
||||
&-container {
|
||||
transition: opacity @animation-duration-slow, height @animation-duration-slow;
|
||||
|
||||
&::before {
|
||||
display: table;
|
||||
width: 0;
|
||||
height: 0;
|
||||
content: '';
|
||||
.@{upload-prefix-cls}-success-icon {
|
||||
color: @success-color;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Don't know why span here, just stretch it
|
||||
.@{upload-prefix-cls}-span {
|
||||
display: block;
|
||||
flex: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// text & picture no need this additional element.
|
||||
// But it used for picture-card, let's keep it.
|
||||
.@{upload-prefix-cls}-span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.@{upload-item}-name {
|
||||
flex: auto;
|
||||
margin: 0;
|
||||
padding: 0 @padding-xs;
|
||||
}
|
||||
|
||||
.@{upload-item}-card-actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Text ============================
|
||||
&-text {
|
||||
.@{upload-prefix-cls}-text-icon {
|
||||
.@{iconfont-css-prefix} {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================== Motion ===========================
|
||||
.@{upload-prefix-cls}-animate-inline-appear,
|
||||
.@{upload-prefix-cls}-animate-enter,
|
||||
.@{upload-prefix-cls}-animate-leave,
|
||||
.@{upload-prefix-cls}-animate-inline-enter,
|
||||
.@{upload-prefix-cls}-animate-inline-leave {
|
||||
animation-duration: @animation-duration-slow;
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: @ease-in-out-circ;
|
||||
}
|
||||
|
||||
.@{upload-prefix-cls}-animate-inline-appear,
|
||||
.@{upload-prefix-cls}-animate-enter {
|
||||
animation-name: uploadAnimateIn;
|
||||
}
|
||||
|
||||
.@{upload-prefix-cls}-animate-leave {
|
||||
animation-name: uploadAnimateOut;
|
||||
}
|
||||
|
||||
.@{upload-prefix-cls}-animate-inline-enter {
|
||||
animation-name: uploadAnimateInlineIn;
|
||||
}
|
||||
|
@ -566,6 +503,24 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes uploadAnimateIn {
|
||||
from {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes uploadAnimateOut {
|
||||
to {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes uploadAnimateInlineIn {
|
||||
from {
|
||||
width: 0;
|
||||
|
@ -585,5 +540,3 @@
|
|||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@import './rtl';
|
|
@ -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);
|
||||
props: uploadProps(),
|
||||
setup(props, { slots, attrs }) {
|
||||
return () => {
|
||||
const { height, ...restProps } = props;
|
||||
const { style, ...restAttrs } = this.$attrs;
|
||||
const { style, ...restAttrs } = attrs;
|
||||
const draggerProps = {
|
||||
...restProps,
|
||||
...restAttrs,
|
||||
type: 'drag',
|
||||
style: { ...(style as any), height },
|
||||
style: { ...(style as any), height: typeof height === 'number' ? `${height}px` : height },
|
||||
} as any;
|
||||
return <Upload {...draggerProps}>{getSlot(this)}</Upload>;
|
||||
return <Upload {...draggerProps} v-slots={slots}></Upload>;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,86 +1,191 @@
|
|||
import classNames from '../_util/classNames';
|
||||
import uniqBy from 'lodash-es/uniqBy';
|
||||
import findIndex from 'lodash-es/findIndex';
|
||||
import type { UploadProps as RcUploadProps } from '../vc-upload';
|
||||
import VcUpload from '../vc-upload';
|
||||
import BaseMixin from '../_util/BaseMixin';
|
||||
import { getOptionProps, hasProp, getSlot } from '../_util/props-util';
|
||||
import initDefaultProps from '../_util/props-util/initDefaultProps';
|
||||
import LocaleReceiver from '../locale-provider/LocaleReceiver';
|
||||
import defaultLocale from '../locale-provider/default';
|
||||
import { defaultConfigProvider } from '../config-provider';
|
||||
import Dragger from './Dragger';
|
||||
import UploadList from './UploadList';
|
||||
import type { UploadFile } from './interface';
|
||||
import type {
|
||||
UploadType,
|
||||
UploadListType,
|
||||
RcFile,
|
||||
UploadFile,
|
||||
UploadChangeParam,
|
||||
ShowUploadListInterface,
|
||||
} from './interface';
|
||||
import { uploadProps } from './interface';
|
||||
import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from './utils';
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import { getDataAndAriaProps } from '../_util/util';
|
||||
import { useInjectFormItemContext } from '../form/FormItemContext';
|
||||
import { file2Obj, getFileItem, removeFileItem, updateFileList } from './utils';
|
||||
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
|
||||
import defaultLocale from '../locale/default';
|
||||
import { computed, defineComponent, onMounted, ref, toRef } from 'vue';
|
||||
import { flattenChildren, initDefaultProps } from '../_util/props-util';
|
||||
import useMergedState from '../_util/hooks/useMergedState';
|
||||
import devWarning from '../vc-util/devWarning';
|
||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||
import type { VueNode } from '../_util/type';
|
||||
import classNames from '../_util/classNames';
|
||||
import { useInjectFormItemContext } from '../form';
|
||||
|
||||
export const LIST_IGNORE = `__LIST_IGNORE_${Date.now()}__`;
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AUpload',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
Dragger,
|
||||
props: initDefaultProps(uploadProps, {
|
||||
type: 'select',
|
||||
props: initDefaultProps(uploadProps(), {
|
||||
type: 'select' as UploadType,
|
||||
multiple: false,
|
||||
action: '',
|
||||
data: {},
|
||||
accept: '',
|
||||
beforeUpload: T,
|
||||
showUploadList: true,
|
||||
listType: 'text', // or pictrue
|
||||
listType: 'text' as UploadListType, // or picture
|
||||
disabled: false,
|
||||
supportServerRender: true,
|
||||
}),
|
||||
setup() {
|
||||
setup(props, { slots, attrs, expose }) {
|
||||
const formItemContext = useInjectFormItemContext();
|
||||
return {
|
||||
upload: null,
|
||||
progressTimer: null,
|
||||
configProvider: inject('configProvider', defaultConfigProvider),
|
||||
formItemContext,
|
||||
};
|
||||
},
|
||||
// recentUploadStatus: boolean | PromiseLike<any>;
|
||||
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;
|
||||
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}__`;
|
||||
}
|
||||
this.handleChange({
|
||||
file: targetItem,
|
||||
fileList: nextFileList,
|
||||
return file;
|
||||
});
|
||||
// fix ie progress
|
||||
if (!window.File || (typeof process === 'object' && process.env.TEST_IE)) {
|
||||
this.autoUpdateProgress(0, targetItem);
|
||||
}
|
||||
},
|
||||
});
|
||||
const dragState = ref('drop');
|
||||
|
||||
onSuccess(response, file, xhr) {
|
||||
this.clearProgressTimer();
|
||||
const upload = ref();
|
||||
onMounted(() => {
|
||||
devWarning(
|
||||
'fileList' in props || !('value' in props),
|
||||
'Upload',
|
||||
'`value` is not a valid prop, do you mean `fileList`?',
|
||||
);
|
||||
|
||||
devWarning(
|
||||
!('transformFile' in props),
|
||||
'Upload',
|
||||
'`transformFile` is deprecated. Please use `beforeUpload` directly.',
|
||||
);
|
||||
});
|
||||
|
||||
const onInternalChange = (
|
||||
file: UploadFile,
|
||||
changedFileList: UploadFile[],
|
||||
event?: { percent: number },
|
||||
) => {
|
||||
let cloneList = [...changedFileList];
|
||||
|
||||
// Cut to match count
|
||||
if (props.maxCount === 1) {
|
||||
cloneList = cloneList.slice(-1);
|
||||
} else if (props.maxCount) {
|
||||
cloneList = cloneList.slice(0, props.maxCount);
|
||||
}
|
||||
|
||||
setMergedFileList(cloneList);
|
||||
|
||||
const changeInfo: UploadChangeParam<UploadFile> = {
|
||||
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: RcFile, fileListArgs: RcFile[]) => {
|
||||
const { beforeUpload, transformFile } = props;
|
||||
|
||||
let parsedFile: File | Blob | string = file;
|
||||
if (beforeUpload) {
|
||||
const result = await beforeUpload(file, fileListArgs);
|
||||
|
||||
if (result === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hack for LIST_IGNORE, we add additional info to remove from the list
|
||||
delete (file as any)[LIST_IGNORE];
|
||||
if ((result as any) === LIST_IGNORE) {
|
||||
Object.defineProperty(file, LIST_IGNORE, {
|
||||
value: true,
|
||||
configurable: true,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof result === 'object' && result) {
|
||||
parsedFile = result as File;
|
||||
}
|
||||
}
|
||||
|
||||
if (transformFile) {
|
||||
parsedFile = await transformFile(parsedFile as any);
|
||||
}
|
||||
|
||||
return parsedFile as RcFile;
|
||||
};
|
||||
|
||||
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 RcFile));
|
||||
|
||||
// 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: RcFile, xhr: any) => {
|
||||
try {
|
||||
if (typeof response === 'string') {
|
||||
response = JSON.parse(response);
|
||||
|
@ -88,255 +193,231 @@ export default defineComponent({
|
|||
} catch (e) {
|
||||
/* do nothing */
|
||||
}
|
||||
const fileList = this.sFileList;
|
||||
const targetItem = getFileItem(file, fileList);
|
||||
|
||||
// removed
|
||||
if (!targetItem) {
|
||||
if (!getFileItem(file, mergedFileList.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetItem = file2Obj(file);
|
||||
targetItem.status = 'done';
|
||||
targetItem.percent = 100;
|
||||
targetItem.response = response;
|
||||
targetItem.xhr = xhr;
|
||||
this.handleChange({
|
||||
file: { ...targetItem },
|
||||
fileList,
|
||||
});
|
||||
},
|
||||
onProgress(e, file) {
|
||||
const fileList = this.sFileList;
|
||||
const targetItem = getFileItem(file, fileList);
|
||||
|
||||
const nextFileList = updateFileList(targetItem, mergedFileList.value);
|
||||
|
||||
onInternalChange(targetItem, nextFileList);
|
||||
};
|
||||
|
||||
const onProgress = (e: { percent: number }, file: RcFile) => {
|
||||
// removed
|
||||
if (!targetItem) {
|
||||
if (!getFileItem(file, mergedFileList.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetItem = file2Obj(file);
|
||||
targetItem.status = 'uploading';
|
||||
targetItem.percent = e.percent;
|
||||
this.handleChange({
|
||||
event: e,
|
||||
file: { ...targetItem },
|
||||
fileList: this.sFileList,
|
||||
});
|
||||
},
|
||||
onError(error, response, file) {
|
||||
this.clearProgressTimer();
|
||||
const fileList = this.sFileList;
|
||||
const targetItem = getFileItem(file, fileList);
|
||||
|
||||
const nextFileList = updateFileList(targetItem, mergedFileList.value);
|
||||
|
||||
onInternalChange(targetItem, nextFileList, e);
|
||||
};
|
||||
|
||||
const onError = (error: Error, response: any, file: RcFile) => {
|
||||
// removed
|
||||
if (!targetItem) {
|
||||
if (!getFileItem(file, mergedFileList.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetItem = file2Obj(file);
|
||||
targetItem.error = error;
|
||||
targetItem.response = response;
|
||||
targetItem.status = 'error';
|
||||
this.handleChange({
|
||||
file: { ...targetItem },
|
||||
fileList,
|
||||
});
|
||||
},
|
||||
onReject(fileList) {
|
||||
this.$emit('reject', fileList);
|
||||
},
|
||||
handleRemove(file) {
|
||||
const { remove: onRemove } = this;
|
||||
const { sFileList: fileList } = this.$data;
|
||||
|
||||
Promise.resolve(typeof onRemove === 'function' ? onRemove(file) : onRemove).then(ret => {
|
||||
const nextFileList = updateFileList(targetItem, mergedFileList.value);
|
||||
|
||||
onInternalChange(targetItem, nextFileList);
|
||||
};
|
||||
|
||||
const handleRemove = (file: UploadFile) => {
|
||||
let currentFile: UploadFile;
|
||||
Promise.resolve(
|
||||
typeof props.onRemove === 'function' ? props.onRemove(file) : props.onRemove,
|
||||
).then(ret => {
|
||||
// Prevent removing file
|
||||
if (ret === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const removedFileList = removeFileItem(file, fileList);
|
||||
const removedFileList = removeFileItem(file, mergedFileList.value);
|
||||
|
||||
if (removedFileList) {
|
||||
file.status = 'removed'; // eslint-disable-line
|
||||
currentFile = { ...file, status: 'removed' };
|
||||
mergedFileList.value?.forEach(item => {
|
||||
const matchKey = currentFile.uid !== undefined ? 'uid' : 'name';
|
||||
if (item[matchKey] === currentFile[matchKey] && !Object.isFrozen(item)) {
|
||||
item.status = 'removed';
|
||||
}
|
||||
});
|
||||
upload.value?.abort(currentFile);
|
||||
|
||||
if (this.upload) {
|
||||
this.upload.abort(file);
|
||||
onInternalChange(currentFile, removedFileList);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.handleChange({
|
||||
file,
|
||||
fileList: removedFileList,
|
||||
const onFileDrop = (e: DragEvent) => {
|
||||
dragState.value = e.type;
|
||||
if (e.type === 'drop') {
|
||||
props.onDrop?.(e);
|
||||
}
|
||||
};
|
||||
expose({
|
||||
onBatchStart,
|
||||
onSuccess,
|
||||
onProgress,
|
||||
onError,
|
||||
fileList: mergedFileList,
|
||||
upload,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
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,
|
||||
|
||||
const { prefixCls, direction } = useConfigInject('upload', props);
|
||||
const [locale] = useLocaleReceiver(
|
||||
'Upload',
|
||||
defaultLocale.Upload,
|
||||
computed(() => props.locale),
|
||||
);
|
||||
}, 200);
|
||||
},
|
||||
renderUploadList(locale) {
|
||||
const renderUploadList = (button?: VueNode) => {
|
||||
const {
|
||||
showUploadList = {},
|
||||
listType,
|
||||
removeIcon,
|
||||
previewIcon,
|
||||
downloadIcon,
|
||||
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 <UploadList {...uploadListProps} />;
|
||||
},
|
||||
},
|
||||
render() {
|
||||
const {
|
||||
prefixCls: customizePrefixCls,
|
||||
showUploadList,
|
||||
listType,
|
||||
type,
|
||||
onDownload,
|
||||
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',
|
||||
isImageUrl,
|
||||
progress,
|
||||
itemRender,
|
||||
iconRender,
|
||||
showUploadList,
|
||||
} = props;
|
||||
const { showDownloadIcon, showPreviewIcon, showRemoveIcon } =
|
||||
typeof showUploadList === 'boolean' ? ({} as ShowUploadListInterface) : showUploadList;
|
||||
return showUploadList ? (
|
||||
<UploadList
|
||||
listType={props.listType}
|
||||
items={mergedFileList.value}
|
||||
previewFile={previewFile}
|
||||
onPreview={onPreview}
|
||||
onDownload={onDownload}
|
||||
onRemove={handleRemove}
|
||||
showRemoveIcon={!disabled && showRemoveIcon}
|
||||
showPreviewIcon={showPreviewIcon}
|
||||
showDownloadIcon={showDownloadIcon}
|
||||
removeIcon={removeIcon}
|
||||
previewIcon={previewIcon}
|
||||
downloadIcon={downloadIcon}
|
||||
iconRender={iconRender}
|
||||
locale={locale.value}
|
||||
isImageUrl={isImageUrl}
|
||||
progress={progress}
|
||||
itemRender={itemRender}
|
||||
v-slots={{ ...slots, appendAction: () => 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,
|
||||
};
|
||||
|
||||
const uploadList = showUploadList ? (
|
||||
<LocaleReceiver
|
||||
componentName="Upload"
|
||||
defaultLocale={defaultLocale.Upload}
|
||||
children={this.renderUploadList}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const children = getSlot(this);
|
||||
|
||||
// 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, {
|
||||
[`${prefixCls}-drag`]: true,
|
||||
[`${prefixCls}-drag-uploading`]: fileList.some((file: any) => file.status === 'uploading'),
|
||||
[`${prefixCls}-drag-hover`]: dragState === 'dragover',
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
});
|
||||
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 (
|
||||
<span class={className} {...getDataAndAriaProps(this.$attrs)}>
|
||||
<span>
|
||||
<div
|
||||
class={dragCls}
|
||||
onDrop={this.onFileDrop}
|
||||
onDragover={this.onFileDrop}
|
||||
onDragleave={this.onFileDrop}
|
||||
style={style}
|
||||
onDrop={onFileDrop}
|
||||
onDragover={onFileDrop}
|
||||
onDragleave={onFileDrop}
|
||||
style={attrs.style}
|
||||
>
|
||||
<VcUpload {...vcUploadProps} class={`${prefixCls}-btn`}>
|
||||
<div class={`${prefixCls}-drag-container`}>{children}</div>
|
||||
<VcUpload
|
||||
{...rcUploadProps}
|
||||
ref={upload}
|
||||
class={`${prefixCls.value}-btn`}
|
||||
v-slots={slots}
|
||||
>
|
||||
<div class={`${prefixCls}-drag-container`}>{slots.default?.()}</div>
|
||||
</VcUpload>
|
||||
</div>
|
||||
{uploadList}
|
||||
{renderUploadList()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const uploadButtonCls = classNames(prefixCls, {
|
||||
[`${prefixCls}-select`]: true,
|
||||
[`${prefixCls}-select-${listType}`]: true,
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
const uploadButtonCls = classNames(prefixCls.value, {
|
||||
[`${prefixCls.value}-select`]: true,
|
||||
[`${prefixCls.value}-select-${listType}`]: true,
|
||||
[`${prefixCls.value}-disabled`]: disabled,
|
||||
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
|
||||
});
|
||||
|
||||
// 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 children = flattenChildren(slots.default?.());
|
||||
const uploadButton = (
|
||||
<div class={uploadButtonCls} style={children.length ? undefined : { display: 'none' }}>
|
||||
<VcUpload {...vcUploadProps}>{children}</VcUpload>
|
||||
<div
|
||||
class={uploadButtonCls}
|
||||
style={children && children.length ? undefined : { display: 'none' }}
|
||||
>
|
||||
<VcUpload {...rcUploadProps} ref={upload} v-slots={slots} />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (listType === 'picture-card') {
|
||||
return (
|
||||
<span class={classNames(`${prefixCls}-picture-card-wrapper`, className)}>
|
||||
{uploadList}
|
||||
{uploadButton}
|
||||
<span class={classNames(`${prefixCls.value}-picture-card-wrapper`, attrs.class)}>
|
||||
{renderUploadList(uploadButton)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span class={className}>
|
||||
<span class={attrs.class}>
|
||||
{uploadButton}
|
||||
{uploadList}
|
||||
{renderUploadList()}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -25,9 +25,9 @@ export const listItemProps = () => {
|
|||
listType: String as PropType<UploadListType>,
|
||||
isImgUrl: Function as PropType<(file: UploadFile) => boolean>,
|
||||
|
||||
showRemoveIcon: Boolean,
|
||||
showDownloadIcon: Boolean,
|
||||
showPreviewIcon: Boolean,
|
||||
showRemoveIcon: { type: Boolean, default: undefined },
|
||||
showDownloadIcon: { type: Boolean, default: undefined },
|
||||
showPreviewIcon: { type: Boolean, default: undefined },
|
||||
removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
|
@ -76,8 +76,8 @@ export default defineComponent({
|
|||
file,
|
||||
items,
|
||||
progress: progressProps,
|
||||
iconRender,
|
||||
actionIconRender,
|
||||
iconRender = slots.iconRender,
|
||||
actionIconRender = slots.actionIconRender,
|
||||
itemRender = slots.itemRender,
|
||||
isImgUrl,
|
||||
showPreviewIcon,
|
|
@ -0,0 +1,206 @@
|
|||
import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined';
|
||||
import PaperClipOutlined from '@ant-design/icons-vue/PaperClipOutlined';
|
||||
import PictureTwoTone from '@ant-design/icons-vue/PictureTwoTone';
|
||||
import FileTwoTone from '@ant-design/icons-vue/FileTwoTone';
|
||||
import type { UploadListType, InternalUploadFile, UploadFile } from '../interface';
|
||||
import { uploadListProps } from '../interface';
|
||||
import { previewImage, isImageUrl } from '../utils';
|
||||
import type { ButtonProps } from '../../button';
|
||||
import Button from '../../button';
|
||||
import ListItem from './ListItem';
|
||||
import type { HTMLAttributes } from 'vue';
|
||||
import { computed, defineComponent, getCurrentInstance, onMounted, ref, watchEffect } from 'vue';
|
||||
import { initDefaultProps, isValidElement } from '../../_util/props-util';
|
||||
import type { VueNode } from '../../_util/type';
|
||||
import useConfigInject from '../../_util/hooks/useConfigInject';
|
||||
import { getTransitionGroupProps, TransitionGroup } from '../../_util/transition';
|
||||
|
||||
const HackSlot = (_, { slots }) => {
|
||||
return slots.default?.()[0];
|
||||
};
|
||||
export default defineComponent({
|
||||
name: 'AUploadList',
|
||||
props: initDefaultProps(uploadListProps(), {
|
||||
listType: 'text' as UploadListType, // or picture
|
||||
progress: {
|
||||
strokeWidth: 2,
|
||||
showInfo: false,
|
||||
},
|
||||
showRemoveIcon: true,
|
||||
showDownloadIcon: false,
|
||||
showPreviewIcon: true,
|
||||
previewFile: previewImage,
|
||||
isImageUrl,
|
||||
items: [],
|
||||
}),
|
||||
setup(props, { slots, expose }) {
|
||||
const motionAppear = ref(false);
|
||||
const instance = getCurrentInstance();
|
||||
onMounted(() => {
|
||||
motionAppear.value == true;
|
||||
});
|
||||
watchEffect(() => {
|
||||
if (props.listType !== 'picture' && props.listType !== 'picture-card') {
|
||||
return;
|
||||
}
|
||||
(props.items || []).forEach((file: InternalUploadFile) => {
|
||||
if (
|
||||
typeof document === 'undefined' ||
|
||||
typeof window === 'undefined' ||
|
||||
!(window as any).FileReader ||
|
||||
!(window as any).File ||
|
||||
!(file.originFileObj instanceof File || (file.originFileObj as Blob) instanceof Blob) ||
|
||||
file.thumbUrl !== undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
file.thumbUrl = '';
|
||||
if (props.previewFile) {
|
||||
props.previewFile(file.originFileObj as File).then((previewDataUrl: string) => {
|
||||
// Need append '' to avoid dead loop
|
||||
file.thumbUrl = previewDataUrl || '';
|
||||
instance.update();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ============================= Events =============================
|
||||
const onInternalPreview = (file: UploadFile, e?: Event) => {
|
||||
if (!props.onPreview) {
|
||||
return;
|
||||
}
|
||||
e?.preventDefault();
|
||||
return props.onPreview(file);
|
||||
};
|
||||
|
||||
const onInternalDownload = (file: UploadFile) => {
|
||||
if (typeof props.onDownload === 'function') {
|
||||
props.onDownload(file);
|
||||
} else if (file.url) {
|
||||
window.open(file.url);
|
||||
}
|
||||
};
|
||||
|
||||
const onInternalClose = (file: UploadFile) => {
|
||||
props.onRemove?.(file);
|
||||
};
|
||||
|
||||
const internalIconRender = ({ file }: { file: UploadFile }) => {
|
||||
const iconRender = props.iconRender || slots.iconRender;
|
||||
if (iconRender) {
|
||||
return iconRender({ file, listType: props.listType });
|
||||
}
|
||||
const isLoading = file.status === 'uploading';
|
||||
const fileIcon =
|
||||
props.isImageUrl && props.isImageUrl(file) ? <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-card-actions-btn`,
|
||||
};
|
||||
if (isValidElement(customIcon)) {
|
||||
return <Button {...btnProps} v-slots={{ icon: () => customIcon }} />;
|
||||
}
|
||||
return (
|
||||
<Button {...btnProps}>
|
||||
<span>{customIcon}</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
expose({
|
||||
handlePreview: onInternalPreview,
|
||||
handleDownload: onInternalDownload,
|
||||
});
|
||||
|
||||
const { prefixCls, direction } = useConfigInject('upload', props);
|
||||
|
||||
const listClassNames = computed(() => ({
|
||||
[`${prefixCls.value}-list`]: true,
|
||||
[`${prefixCls.value}-list-${props.listType}`]: true,
|
||||
[`${prefixCls.value}-list-rtl`]: direction.value === 'rtl',
|
||||
}));
|
||||
const transitionGroupProps = computed(() => ({
|
||||
...getTransitionGroupProps(
|
||||
`${prefixCls.value}-${props.listType === 'picture-card' ? 'animate-inline' : 'animate'}`,
|
||||
),
|
||||
class: listClassNames.value,
|
||||
appear: motionAppear.value,
|
||||
}));
|
||||
return () => {
|
||||
const {
|
||||
listType,
|
||||
locale,
|
||||
isImageUrl: isImgUrl,
|
||||
items = [],
|
||||
showPreviewIcon,
|
||||
showRemoveIcon,
|
||||
showDownloadIcon,
|
||||
removeIcon,
|
||||
previewIcon,
|
||||
downloadIcon,
|
||||
progress,
|
||||
appendAction = slots.appendAction,
|
||||
itemRender,
|
||||
} = props;
|
||||
const appendActionDom = appendAction?.()[0];
|
||||
return (
|
||||
<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}
|
||||
v-slots={{
|
||||
removeIcon,
|
||||
previewIcon,
|
||||
downloadIcon,
|
||||
iconRender: internalIconRender,
|
||||
actionIconRender,
|
||||
itemRender,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{isValidElement(appendActionDom) ? (
|
||||
<HackSlot key="__ant_upload_appendAction">{appendActionDom}</HackSlot>
|
||||
) : null}
|
||||
</TransitionGroup>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
|
@ -19,7 +19,6 @@ Classic mode. File selection dialog pops up when upload button is clicked.
|
|||
<a-upload
|
||||
v-model:file-list="fileList"
|
||||
name="file"
|
||||
:multiple="true"
|
||||
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
|
||||
:headers="headers"
|
||||
@change="handleChange"
|
||||
|
|
|
@ -25,10 +25,10 @@ After users upload picture, the thumbnail will be shown in list. The upload butt
|
|||
>
|
||||
<div v-if="fileList.length < 8">
|
||||
<plus-outlined />
|
||||
<div class="ant-upload-text">Upload</div>
|
||||
<div style="margin-top: 8px">Upload</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
|
||||
<a-modal :visible="previewVisible" :title="previewTitle" :footer="null" @cancel="handleCancel">
|
||||
<img alt="example" style="width: 100%" :src="previewImage" />
|
||||
</a-modal>
|
||||
</div>
|
||||
|
@ -36,7 +36,7 @@ After users upload picture, the thumbnail will be shown in list. The upload butt
|
|||
<script lang="ts">
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { defineComponent, ref } from 'vue';
|
||||
import type { UploadChangeParam, UploadProps } from 'ant-design-vue';
|
||||
import type { UploadProps } from 'ant-design-vue';
|
||||
|
||||
function getBase64(file: File) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -52,8 +52,9 @@ export default defineComponent({
|
|||
PlusOutlined,
|
||||
},
|
||||
setup() {
|
||||
const previewVisible = ref<boolean>(false);
|
||||
const previewImage = ref<string | undefined>('');
|
||||
const previewVisible = ref(false);
|
||||
const previewImage = ref('');
|
||||
const previewTitle = ref('');
|
||||
|
||||
const fileList = ref<UploadProps['fileList']>([
|
||||
{
|
||||
|
@ -80,6 +81,13 @@ export default defineComponent({
|
|||
status: 'done',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
},
|
||||
{
|
||||
uid: '-xxx',
|
||||
percent: 50,
|
||||
name: 'image.png',
|
||||
status: 'uploading',
|
||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
||||
},
|
||||
{
|
||||
uid: '-5',
|
||||
name: 'image.png',
|
||||
|
@ -89,6 +97,7 @@ export default defineComponent({
|
|||
|
||||
const handleCancel = () => {
|
||||
previewVisible.value = false;
|
||||
previewTitle.value = '';
|
||||
};
|
||||
const handlePreview = async (file: UploadProps['fileList'][number]) => {
|
||||
if (!file.url && !file.preview) {
|
||||
|
@ -96,9 +105,7 @@ export default defineComponent({
|
|||
}
|
||||
previewImage.value = file.url || file.preview;
|
||||
previewVisible.value = true;
|
||||
};
|
||||
const handleChange = ({ fileList: newFileList }: UploadChangeParam) => {
|
||||
fileList.value = newFileList;
|
||||
previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1);
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -107,7 +114,7 @@ export default defineComponent({
|
|||
fileList,
|
||||
handleCancel,
|
||||
handlePreview,
|
||||
handleChange,
|
||||
previewTitle,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import type { App, Plugin } from 'vue';
|
||||
import Upload from './Upload';
|
||||
import type { App } from 'vue';
|
||||
import Upload, { LIST_IGNORE } from './Upload';
|
||||
import Dragger from './Dragger';
|
||||
|
||||
export type { UploadProps, UploadListProps, UploadChangeParam } from './interface';
|
||||
|
||||
Upload.Dragger = Dragger;
|
||||
export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from './interface';
|
||||
|
||||
/* istanbul ignore next */
|
||||
Upload.install = function (app: App) {
|
||||
export const UploadDragger = Dragger;
|
||||
|
||||
export default Object.assign(Upload, {
|
||||
Dragger,
|
||||
LIST_IGNORE,
|
||||
install(app: App) {
|
||||
app.component(Upload.name, Upload);
|
||||
app.component(Dragger.name, Dragger);
|
||||
return app;
|
||||
};
|
||||
|
||||
export const UploadDragger = Dragger;
|
||||
|
||||
export default Upload as typeof Upload &
|
||||
Plugin & {
|
||||
readonly Dragger: typeof Dragger;
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import type {
|
||||
RcFile as OriRcFile,
|
||||
UploadRequestOption as RcCustomRequestOptions,
|
||||
} from '../vc-upload/interface';
|
||||
import type { ProgressProps } from '../progress';
|
||||
import type { VueNode } from '../_util/type';
|
||||
import type { ExtractPropTypes, PropType } from 'vue';
|
||||
import { tuple } from '../_util/type';
|
||||
import PropsTypes from '../_util/vue-types';
|
||||
|
||||
export const UploadFileStatus = PropsTypes.oneOf(
|
||||
tuple('error', 'success', 'done', 'uploading', 'removed'),
|
||||
);
|
||||
export interface RcFile extends OriRcFile {
|
||||
readonly lastModifiedDate: Date;
|
||||
}
|
||||
|
||||
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
|
||||
|
||||
export interface HttpRequestHeader {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface VcFile extends File {
|
||||
uid: string;
|
||||
readonly lastModifiedDate: Date;
|
||||
readonly webkitRelativePath: string;
|
||||
}
|
||||
|
||||
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
|
||||
export interface UploadFile<T = any> {
|
||||
uid: string;
|
||||
size?: number;
|
||||
|
@ -28,7 +27,7 @@ export interface UploadFile<T = any> {
|
|||
status?: UploadFileStatus;
|
||||
percent?: number;
|
||||
thumbUrl?: string;
|
||||
originFileObj?: any;
|
||||
originFileObj?: RcFile;
|
||||
response?: T;
|
||||
error?: any;
|
||||
linkProps?: any;
|
||||
|
@ -37,17 +36,23 @@ export interface UploadFile<T = any> {
|
|||
preview?: string;
|
||||
}
|
||||
|
||||
export interface UploadChangeParam<T extends object = UploadFile> {
|
||||
export interface InternalUploadFile<T = any> extends UploadFile<T> {
|
||||
originFileObj: RcFile;
|
||||
}
|
||||
|
||||
export interface ShowUploadListInterface {
|
||||
showRemoveIcon?: boolean;
|
||||
showPreviewIcon?: boolean;
|
||||
showDownloadIcon?: boolean;
|
||||
}
|
||||
|
||||
export interface UploadChangeParam<T = UploadFile> {
|
||||
// https://github.com/ant-design/ant-design/issues/14420
|
||||
file: T;
|
||||
fileList: UploadFile[];
|
||||
event?: { percent: number };
|
||||
}
|
||||
|
||||
export const ShowUploadListInterface = PropsTypes.shape({
|
||||
showRemoveIcon: PropsTypes.looseBool,
|
||||
showPreviewIcon: PropsTypes.looseBool,
|
||||
}).loose;
|
||||
|
||||
export interface UploadLocale {
|
||||
uploading?: string;
|
||||
removeFile?: string;
|
||||
|
@ -56,61 +61,120 @@ export interface UploadLocale {
|
|||
previewFile?: string;
|
||||
}
|
||||
|
||||
export const uploadProps = {
|
||||
type: PropsTypes.oneOf(tuple('drag', 'select')),
|
||||
name: PropsTypes.string,
|
||||
defaultFileList: { type: Array as PropType<UploadFile[]> },
|
||||
fileList: { type: Array as PropType<UploadFile[]> },
|
||||
action: PropsTypes.oneOfType([PropsTypes.string, PropsTypes.func]),
|
||||
directory: PropsTypes.looseBool,
|
||||
data: PropsTypes.oneOfType([PropsTypes.object, PropsTypes.func]),
|
||||
method: PropsTypes.oneOf(tuple('POST', 'PUT', 'PATCH', 'post', 'put', 'patch')),
|
||||
headers: PropsTypes.object,
|
||||
showUploadList: PropsTypes.oneOfType([PropsTypes.looseBool, ShowUploadListInterface]),
|
||||
multiple: PropsTypes.looseBool,
|
||||
accept: PropsTypes.string,
|
||||
beforeUpload: PropsTypes.func,
|
||||
listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')),
|
||||
// className: PropsTypes.string,
|
||||
remove: PropsTypes.func,
|
||||
supportServerRender: PropsTypes.looseBool,
|
||||
// style: PropsTypes.object,
|
||||
disabled: PropsTypes.looseBool,
|
||||
prefixCls: PropsTypes.string,
|
||||
customRequest: PropsTypes.func,
|
||||
withCredentials: PropsTypes.looseBool,
|
||||
openFileDialogOnClick: PropsTypes.looseBool,
|
||||
locale: { type: Object as PropType<UploadLocale> },
|
||||
height: PropsTypes.number,
|
||||
id: PropsTypes.string,
|
||||
previewFile: PropsTypes.func,
|
||||
transformFile: PropsTypes.func,
|
||||
onChange: { type: Function as PropType<(info: UploadChangeParam) => void> },
|
||||
onPreview: { type: Function as PropType<(file: UploadFile) => void> },
|
||||
onRemove: {
|
||||
type: Function as PropType<(file: UploadFile) => void | boolean | Promise<void | boolean>>,
|
||||
},
|
||||
onDownload: { type: Function as PropType<(file: UploadFile) => void> },
|
||||
'onUpdate:fileList': { type: Function as PropType<(files: UploadFile[]) => void> },
|
||||
};
|
||||
export type UploadType = 'drag' | 'select';
|
||||
export type UploadListType = 'text' | 'picture' | 'picture-card';
|
||||
export type UploadListProgressProps = Omit<ProgressProps, 'percent' | 'type'>;
|
||||
|
||||
export type UploadProps = Partial<ExtractPropTypes<typeof uploadProps>>;
|
||||
export const uploadListProps = {
|
||||
listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')),
|
||||
// items: PropsTypes.arrayOf(UploadFile),
|
||||
items: { type: Array as PropType<UploadFile[]> },
|
||||
progressAttr: PropsTypes.object,
|
||||
prefixCls: PropsTypes.string,
|
||||
showRemoveIcon: PropsTypes.looseBool,
|
||||
showDownloadIcon: PropsTypes.looseBool,
|
||||
showPreviewIcon: PropsTypes.looseBool,
|
||||
locale: { type: Object as PropType<UploadLocale> },
|
||||
previewFile: PropsTypes.func,
|
||||
onPreview: { type: Function as PropType<(file: UploadFile) => void> },
|
||||
onRemove: {
|
||||
type: Function as PropType<(file: UploadFile) => void | boolean>,
|
||||
},
|
||||
onDownload: { type: Function as PropType<(file: UploadFile) => void> },
|
||||
};
|
||||
export type ItemRender<T = any> = (opt: {
|
||||
originNode: VueNode;
|
||||
file: UploadFile;
|
||||
fileList: Array<UploadFile<T>>;
|
||||
actions: {
|
||||
download: () => void;
|
||||
preview: () => void;
|
||||
remove: () => void;
|
||||
};
|
||||
}) => VueNode;
|
||||
|
||||
export type UploadListProps = Partial<ExtractPropTypes<typeof uploadListProps>>;
|
||||
type PreviewFileHandler = (file: File | Blob) => PromiseLike<string>;
|
||||
type TransformFileHandler = (
|
||||
file: RcFile,
|
||||
) => string | Blob | File | PromiseLike<string | Blob | File>;
|
||||
type BeforeUploadValueType = void | boolean | string | Blob | File;
|
||||
|
||||
function uploadProps<T = any>() {
|
||||
return {
|
||||
capture: [Boolean, String] as PropType<boolean | 'user' | 'environment'>,
|
||||
type: String as PropType<UploadType>,
|
||||
name: String,
|
||||
defaultFileList: Array as PropType<Array<UploadFile<T>>>,
|
||||
fileList: Array as PropType<Array<UploadFile<T>>>,
|
||||
action: [String, Function] as PropType<
|
||||
string | ((file: RcFile) => string) | ((file: RcFile) => PromiseLike<string>)
|
||||
>,
|
||||
directory: { type: Boolean, default: undefined },
|
||||
data: [Object, Function] as PropType<
|
||||
| Record<string, unknown>
|
||||
| ((file: UploadFile<T>) => Record<string, unknown> | Promise<Record<string, unknown>>)
|
||||
>,
|
||||
method: String as PropType<'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch'>,
|
||||
headers: Object as PropType<HttpRequestHeader>,
|
||||
showUploadList: {
|
||||
type: [Boolean, Object] as PropType<boolean | ShowUploadListInterface>,
|
||||
default: undefined as boolean | ShowUploadListInterface,
|
||||
},
|
||||
multiple: { type: Boolean, default: undefined },
|
||||
accept: String,
|
||||
beforeUpload: Function as PropType<
|
||||
(file: RcFile, FileList: RcFile[]) => BeforeUploadValueType | Promise<BeforeUploadValueType>
|
||||
>,
|
||||
onChange: Function as PropType<(info: UploadChangeParam<T>) => void>,
|
||||
'onUpdate:fileList': Function as PropType<(fileList: UploadChangeParam<T>['fileList']) => void>,
|
||||
onDrop: Function as PropType<(event: DragEvent) => void>,
|
||||
listType: String as PropType<UploadListType>,
|
||||
onPreview: Function as PropType<(file: UploadFile<T>) => void>,
|
||||
onDownload: Function as PropType<(file: UploadFile<T>) => void>,
|
||||
onRemove: Function as PropType<
|
||||
(file: UploadFile<T>) => void | boolean | Promise<void | boolean>
|
||||
>,
|
||||
supportServerRender: { type: Boolean, default: undefined },
|
||||
disabled: { type: Boolean, default: undefined },
|
||||
prefixCls: String,
|
||||
customRequest: Function as PropType<(options: RcCustomRequestOptions) => void>,
|
||||
withCredentials: { type: Boolean, default: undefined },
|
||||
openFileDialogOnClick: { type: Boolean, default: undefined },
|
||||
locale: { type: Object as PropType<UploadLocale>, default: undefined as UploadLocale },
|
||||
id: String,
|
||||
previewFile: Function as PropType<PreviewFileHandler>,
|
||||
/** @deprecated Please use `beforeUpload` directly */
|
||||
transformFile: Function as PropType<TransformFileHandler>,
|
||||
iconRender: Function as PropType<
|
||||
(opt: { file: UploadFile<T>; listType?: UploadListType }) => VueNode
|
||||
>,
|
||||
isImageUrl: Function as PropType<(file: UploadFile) => boolean>,
|
||||
progress: Object as PropType<UploadListProgressProps>,
|
||||
itemRender: Function as PropType<ItemRender<T>>,
|
||||
/** Config max count of `fileList`. Will replace current one when `maxCount` is 1 */
|
||||
maxCount: Number,
|
||||
height: [Number, String],
|
||||
removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||
};
|
||||
}
|
||||
|
||||
export type UploadProps = Partial<ExtractPropTypes<ReturnType<typeof uploadProps>>>;
|
||||
|
||||
export interface UploadState<T = any> {
|
||||
fileList: UploadFile<T>[];
|
||||
dragState: string;
|
||||
}
|
||||
|
||||
function uploadListProps<T = any>() {
|
||||
return {
|
||||
listType: String as PropType<UploadListType>,
|
||||
onPreview: Function as PropType<(file: UploadFile<T>) => void>,
|
||||
onDownload: Function as PropType<(file: UploadFile<T>) => void>,
|
||||
onRemove: Function as PropType<(file: UploadFile<T>) => void | boolean>,
|
||||
items: Array as PropType<Array<UploadFile<T>>>,
|
||||
progress: Object as PropType<UploadListProgressProps>,
|
||||
prefixCls: String as PropType<string>,
|
||||
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>,
|
||||
locale: { type: Object as PropType<UploadLocale>, default: undefined as UploadLocale },
|
||||
previewFile: Function as PropType<PreviewFileHandler>,
|
||||
iconRender: Function as PropType<
|
||||
(opt: { file: UploadFile<T>; listType?: UploadListType }) => VueNode
|
||||
>,
|
||||
isImageUrl: Function as PropType<(file: UploadFile) => boolean>,
|
||||
appendAction: Function as PropType<() => VueNode>,
|
||||
itemRender: Function as PropType<ItemRender<T>>,
|
||||
};
|
||||
}
|
||||
|
||||
export type UploadListProps = Partial<ExtractPropTypes<ReturnType<typeof uploadListProps>>>;
|
||||
export { uploadProps, uploadListProps };
|
||||
|
|
|
@ -34,8 +34,6 @@
|
|||
}
|
||||
|
||||
&&-select-picture-card {
|
||||
display: table;
|
||||
float: left;
|
||||
width: @upload-picture-card-size;
|
||||
height: @upload-picture-card-size;
|
||||
margin-right: 8px;
|
||||
|
@ -46,19 +44,21 @@
|
|||
border: @border-width-base dashed @border-color-base;
|
||||
border-radius: @border-radius-base;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s ease;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
> .@{upload-prefix-cls} {
|
||||
display: table-cell;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: @primary-color;
|
||||
.@{upload-prefix-cls}-disabled& {
|
||||
border-color: @border-color-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -74,7 +74,7 @@
|
|||
transition: border-color 0.3s;
|
||||
|
||||
.@{upload-prefix-cls} {
|
||||
padding: 16px 0;
|
||||
padding: @padding-md 0;
|
||||
}
|
||||
|
||||
&.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
|
||||
|
@ -116,10 +116,12 @@
|
|||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
.@{iconfont-css-prefix}-plus {
|
||||
color: @disabled-color;
|
||||
font-size: 30px;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
|
@ -140,52 +142,55 @@
|
|||
.@{upload-prefix-cls}-list {
|
||||
.reset-component();
|
||||
.clearfix();
|
||||
&-item-list-type-text {
|
||||
&:hover {
|
||||
.@{upload-prefix-cls}-list-item-name-icon-count-1 {
|
||||
padding-right: 14px;
|
||||
}
|
||||
.@{upload-prefix-cls}-list-item-name-icon-count-2 {
|
||||
padding-right: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
line-height: @line-height-base;
|
||||
|
||||
// ============================ Item ============================
|
||||
&-item {
|
||||
position: relative;
|
||||
height: 22px;
|
||||
margin-top: 8px;
|
||||
height: @line-height-base * @font-size-base;
|
||||
margin-top: @margin-xs;
|
||||
font-size: @font-size-base;
|
||||
|
||||
&-name {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
padding-left: @font-size-base + 8px;
|
||||
overflow: hidden;
|
||||
line-height: @line-height-base;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&-name-icon-count-1 {
|
||||
padding-right: 14px;
|
||||
}
|
||||
|
||||
&-card-actions {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
&-btn {
|
||||
opacity: 0;
|
||||
&.picture {
|
||||
top: 25px;
|
||||
}
|
||||
&-btn.@{ant-prefix}-btn-sm {
|
||||
height: 20px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&.picture {
|
||||
top: 22px;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
&-btn:focus,
|
||||
&.picture &-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
.anticon {
|
||||
padding-right: 6px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
color: @upload-actions-color;
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
height: 100%;
|
||||
padding: 0 12px 0 4px;
|
||||
padding: 0 4px;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
> span {
|
||||
|
@ -195,25 +200,27 @@
|
|||
}
|
||||
|
||||
.@{iconfont-css-prefix}-loading,
|
||||
.@{iconfont-css-prefix}-paper-clip {
|
||||
.@{upload-prefix-cls}-text-icon {
|
||||
.@{iconfont-css-prefix} {
|
||||
position: absolute;
|
||||
top: (@font-size-base / 2) - 2px;
|
||||
color: @text-color-secondary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{iconfont-css-prefix}-close {
|
||||
.iconfont-size-under-12px(10px);
|
||||
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 4px;
|
||||
color: @text-color-secondary;
|
||||
font-size: 10px;
|
||||
line-height: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: @text-color;
|
||||
}
|
||||
|
@ -227,22 +234,25 @@
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover &-card-actions {
|
||||
&:hover &-card-actions-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-error,
|
||||
&-error .@{iconfont-css-prefix}-paper-clip,
|
||||
&-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix},
|
||||
&-error &-name {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&-error &-card-actions {
|
||||
.anticon {
|
||||
.@{iconfont-css-prefix} {
|
||||
color: @error-color;
|
||||
}
|
||||
|
||||
&-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-progress {
|
||||
position: absolute;
|
||||
|
@ -254,17 +264,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
// =================== Picture & Picture Card ===================
|
||||
&-picture,
|
||||
&-picture-card {
|
||||
.@{upload-item} {
|
||||
position: relative;
|
||||
height: 66px;
|
||||
padding: 8px;
|
||||
padding: @padding-xs;
|
||||
border: @border-width-base @upload-picture-card-border-style @border-color-base;
|
||||
border-radius: @border-radius-base;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&-error {
|
||||
border-color: @error-color;
|
||||
}
|
||||
|
@ -283,15 +296,30 @@
|
|||
}
|
||||
|
||||
.@{upload-item}-thumbnail {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 26px;
|
||||
line-height: 54px;
|
||||
line-height: 60px;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the color of the error icon : https://github.com/ant-design/ant-design/pull/24160
|
||||
.@{upload-item}-error .@{upload-item}-thumbnail {
|
||||
.@{iconfont-css-prefix} {
|
||||
svg path {
|
||||
&[fill='#e6f7ff'] {
|
||||
fill: @error-color-deprecated-bg;
|
||||
}
|
||||
|
||||
&[fill='#1890ff'] {
|
||||
fill: @error-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{upload-item}-icon {
|
||||
|
@ -300,6 +328,10 @@
|
|||
left: 50%;
|
||||
font-size: 26px;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.@{iconfont-css-prefix} {
|
||||
font-size: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.@{upload-item}-image {
|
||||
|
@ -327,16 +359,8 @@
|
|||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.@{upload-item}-name-icon-count-1 {
|
||||
padding-right: 18px;
|
||||
}
|
||||
|
||||
.@{upload-item}-name-icon-count-2 {
|
||||
padding-right: 36px;
|
||||
}
|
||||
|
||||
.@{upload-item}-uploading .@{upload-item}-name {
|
||||
line-height: 28px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.@{upload-item}-progress {
|
||||
|
@ -355,21 +379,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
// ======================== Picture Card ========================
|
||||
&-picture-card {
|
||||
&-container {
|
||||
display: inline-block;
|
||||
width: @upload-picture-card-size;
|
||||
height: @upload-picture-card-size;
|
||||
margin: 0 @margin-xs @margin-xs 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
&.@{upload-prefix-cls}-list::after {
|
||||
display: none;
|
||||
}
|
||||
&-container {
|
||||
float: left;
|
||||
width: @upload-picture-card-size;
|
||||
height: @upload-picture-card-size;
|
||||
margin: 0 8px 8px 0;
|
||||
}
|
||||
|
||||
.@{upload-item} {
|
||||
float: left;
|
||||
width: @upload-picture-card-size;
|
||||
height: @upload-picture-card-size;
|
||||
margin: 0 8px 8px 0;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.@{upload-item}-info {
|
||||
|
@ -413,6 +439,7 @@
|
|||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: @text-color-inverse;
|
||||
}
|
||||
|
@ -430,7 +457,7 @@
|
|||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.@{upload-item}-name {
|
||||
|
@ -441,7 +468,7 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.anticon-picture + .@{upload-item}-name {
|
||||
.@{upload-item}-file + .@{upload-item}-name {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
display: block;
|
||||
|
@ -454,46 +481,82 @@
|
|||
|
||||
.@{upload-item}-info {
|
||||
height: auto;
|
||||
|
||||
&::before,
|
||||
.@{iconfont-css-prefix}-eye-o,
|
||||
.@{iconfont-css-prefix}-eye,
|
||||
.@{iconfont-css-prefix}-delete {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-top: 18px;
|
||||
color: @text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.@{upload-item}-progress {
|
||||
bottom: 32px;
|
||||
width: calc(100% - 14px);
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.@{upload-prefix-cls}-success-icon {
|
||||
color: @success-color;
|
||||
font-weight: bold;
|
||||
// ======================= Picture & Text =======================
|
||||
&-text,
|
||||
&-picture {
|
||||
&-container {
|
||||
transition: opacity @animation-duration-slow, height @animation-duration-slow;
|
||||
|
||||
&::before {
|
||||
display: table;
|
||||
width: 0;
|
||||
height: 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.@{upload-prefix-cls}-animate-enter,
|
||||
.@{upload-prefix-cls}-animate-leave,
|
||||
// Don't know why span here, just stretch it
|
||||
.@{upload-prefix-cls}-span {
|
||||
display: block;
|
||||
flex: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// text & picture no need this additional element.
|
||||
// But it used for picture-card, let's keep it.
|
||||
.@{upload-prefix-cls}-span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.@{upload-item}-name {
|
||||
flex: auto;
|
||||
margin: 0;
|
||||
padding: 0 @padding-xs;
|
||||
}
|
||||
|
||||
.@{upload-item}-card-actions {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================ Text ============================
|
||||
&-text {
|
||||
.@{upload-prefix-cls}-text-icon {
|
||||
.@{iconfont-css-prefix} {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================== Motion ===========================
|
||||
.@{upload-prefix-cls}-animate-inline-appear,
|
||||
.@{upload-prefix-cls}-animate-inline-enter,
|
||||
.@{upload-prefix-cls}-animate-inline-leave {
|
||||
animation-duration: 0.3s;
|
||||
animation-duration: @animation-duration-slow;
|
||||
animation-fill-mode: @ease-in-out-circ;
|
||||
}
|
||||
|
||||
.@{upload-prefix-cls}-animate-enter {
|
||||
animation-name: uploadAnimateIn;
|
||||
}
|
||||
|
||||
.@{upload-prefix-cls}-animate-leave {
|
||||
animation-name: uploadAnimateOut;
|
||||
}
|
||||
|
||||
.@{upload-prefix-cls}-animate-inline-appear,
|
||||
.@{upload-prefix-cls}-animate-inline-enter {
|
||||
animation-name: uploadAnimateInlineIn;
|
||||
}
|
||||
|
@ -503,24 +566,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes uploadAnimateIn {
|
||||
from {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes uploadAnimateOut {
|
||||
to {
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes uploadAnimateInlineIn {
|
||||
from {
|
||||
width: 0;
|
||||
|
@ -540,3 +585,5 @@
|
|||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@import './rtl';
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
// rc-upload 2.9.4
|
||||
import upload from './src';
|
||||
|
||||
export default upload;
|
|
@ -1,7 +1,7 @@
|
|||
// rc-upload 4.3.3
|
||||
import Upload from './Upload';
|
||||
import { UploadProps } from './interface';
|
||||
import type { UploadProps } from './interface';
|
||||
|
||||
export { UploadProps };
|
||||
export type { UploadProps };
|
||||
|
||||
export default Upload;
|
||||
|
|
|
@ -7,19 +7,19 @@ export type Action = string | ((file: RcFile) => string | PromiseLike<string>);
|
|||
export const uploadProps = () => {
|
||||
return {
|
||||
capture: [Boolean, String] as PropType<boolean | 'user' | 'environment'>,
|
||||
multipart: Boolean,
|
||||
multipart: { type: Boolean, default: undefined },
|
||||
name: String,
|
||||
disabled: Boolean,
|
||||
disabled: { type: Boolean, default: undefined },
|
||||
componentTag: String as PropType<any>,
|
||||
action: [String, Function] as PropType<Action>,
|
||||
method: String as PropType<UploadRequestMethod>,
|
||||
directory: Boolean,
|
||||
directory: { type: Boolean, default: undefined },
|
||||
data: [Object, Function] as PropType<
|
||||
Record<string, unknown> | ((file: RcFile | string | Blob) => Record<string, unknown>)
|
||||
>,
|
||||
headers: Object as PropType<UploadRequestHeader>,
|
||||
accept: String,
|
||||
multiple: Boolean,
|
||||
multiple: { type: Boolean, default: undefined },
|
||||
onBatchStart: Function as PropType<
|
||||
(fileList: { file: RcFile; parsedFile: Exclude<BeforeUploadFileType, boolean> }[]) => void
|
||||
>,
|
||||
|
@ -38,8 +38,8 @@ export const uploadProps = () => {
|
|||
) => BeforeUploadFileType | Promise<void | BeforeUploadFileType>
|
||||
>,
|
||||
customRequest: Function as PropType<(option: UploadRequestOption) => void>,
|
||||
withCredentials: Boolean,
|
||||
openFileDialogOnClick: Boolean,
|
||||
withCredentials: { type: Boolean, default: undefined },
|
||||
openFileDialogOnClick: { type: Boolean, default: undefined },
|
||||
prefixCls: String,
|
||||
id: String,
|
||||
onMouseenter: Function as PropType<(e: MouseEvent) => void>,
|
||||
|
|
|
@ -1,262 +0,0 @@
|
|||
import PropTypes from '../../_util/vue-types';
|
||||
import BaseMixin from '../../_util/BaseMixin';
|
||||
import partition from 'lodash-es/partition';
|
||||
import classNames from '../../_util/classNames';
|
||||
import defaultRequest from './request';
|
||||
import getUid from './uid';
|
||||
import attrAccept from './attr-accept';
|
||||
import traverseFileTree from './traverseFileTree';
|
||||
import { getSlot } from '../../_util/props-util';
|
||||
|
||||
const upLoadPropTypes = {
|
||||
componentTag: PropTypes.string,
|
||||
// style: PropTypes.object,
|
||||
prefixCls: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
// className: PropTypes.string,
|
||||
multiple: PropTypes.looseBool,
|
||||
directory: PropTypes.looseBool,
|
||||
disabled: PropTypes.looseBool,
|
||||
accept: PropTypes.string,
|
||||
// children: PropTypes.any,
|
||||
// onStart: PropTypes.func,
|
||||
data: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
|
||||
action: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
headers: PropTypes.object,
|
||||
beforeUpload: PropTypes.func,
|
||||
customRequest: PropTypes.func,
|
||||
// onProgress: PropTypes.func,
|
||||
withCredentials: PropTypes.looseBool,
|
||||
openFileDialogOnClick: PropTypes.looseBool,
|
||||
transformFile: PropTypes.func,
|
||||
method: PropTypes.string,
|
||||
};
|
||||
|
||||
const AjaxUploader = {
|
||||
inheritAttrs: false,
|
||||
name: 'ajaxUploader',
|
||||
mixins: [BaseMixin],
|
||||
props: upLoadPropTypes,
|
||||
data() {
|
||||
this.reqs = {};
|
||||
return {
|
||||
uid: getUid(),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this._isMounted = true;
|
||||
},
|
||||
beforeUnmount() {
|
||||
this._isMounted = false;
|
||||
this.abort();
|
||||
},
|
||||
methods: {
|
||||
onChange(e) {
|
||||
const files = e.target.files;
|
||||
this.uploadFiles(files);
|
||||
this.reset();
|
||||
},
|
||||
onClick() {
|
||||
const el = this.$refs.fileInputRef;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
el.click();
|
||||
},
|
||||
onKeyDown(e) {
|
||||
if (e.key === 'Enter') {
|
||||
this.onClick();
|
||||
}
|
||||
},
|
||||
onFileDrop(e) {
|
||||
const { multiple } = this.$props;
|
||||
e.preventDefault();
|
||||
if (e.type === 'dragover') {
|
||||
return;
|
||||
}
|
||||
if (this.directory) {
|
||||
traverseFileTree(e.dataTransfer.items, this.uploadFiles, _file =>
|
||||
attrAccept(_file, this.accept),
|
||||
);
|
||||
} else {
|
||||
let files = partition(Array.prototype.slice.call(e.dataTransfer.files), file =>
|
||||
attrAccept(file, this.accept),
|
||||
);
|
||||
let successFiles = files[0];
|
||||
const errorFiles = files[1];
|
||||
if (multiple === false) {
|
||||
successFiles = successFiles.slice(0, 1);
|
||||
}
|
||||
this.uploadFiles(successFiles);
|
||||
|
||||
if (errorFiles.length) {
|
||||
this.__emit('reject', errorFiles);
|
||||
}
|
||||
}
|
||||
},
|
||||
uploadFiles(files) {
|
||||
const postFiles = Array.prototype.slice.call(files);
|
||||
postFiles
|
||||
.map(file => {
|
||||
file.uid = getUid();
|
||||
return file;
|
||||
})
|
||||
.forEach(file => {
|
||||
this.upload(file, postFiles);
|
||||
});
|
||||
},
|
||||
upload(file, fileList) {
|
||||
if (!this.beforeUpload) {
|
||||
// always async in case use react state to keep fileList
|
||||
return setTimeout(() => this.post(file), 0);
|
||||
}
|
||||
|
||||
const before = this.beforeUpload(file, fileList);
|
||||
if (before && before.then) {
|
||||
before
|
||||
.then(processedFile => {
|
||||
const processedFileType = Object.prototype.toString.call(processedFile);
|
||||
if (processedFileType === '[object File]' || processedFileType === '[object Blob]') {
|
||||
return this.post(processedFile);
|
||||
}
|
||||
return this.post(file);
|
||||
})
|
||||
.catch(e => {
|
||||
console && console.log(e); // eslint-disable-line
|
||||
});
|
||||
} else if (before !== false) {
|
||||
setTimeout(() => this.post(file), 0);
|
||||
}
|
||||
},
|
||||
post(file) {
|
||||
if (!this._isMounted) {
|
||||
return;
|
||||
}
|
||||
const { $props: props } = this;
|
||||
let { data } = props;
|
||||
const { transformFile = originFile => originFile } = props;
|
||||
|
||||
new Promise(resolve => {
|
||||
const { action } = this;
|
||||
if (typeof action === 'function') {
|
||||
return resolve(action(file));
|
||||
}
|
||||
resolve(action);
|
||||
}).then(action => {
|
||||
const { uid } = file;
|
||||
const request = this.customRequest || defaultRequest;
|
||||
const transform = Promise.resolve(transformFile(file)).catch(e => {
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
});
|
||||
transform.then(transformedFile => {
|
||||
if (typeof data === 'function') {
|
||||
data = data(file);
|
||||
}
|
||||
|
||||
const requestOption = {
|
||||
action,
|
||||
filename: this.name,
|
||||
data,
|
||||
file: transformedFile,
|
||||
headers: this.headers,
|
||||
withCredentials: this.withCredentials,
|
||||
method: props.method || 'post',
|
||||
onProgress: e => {
|
||||
this.__emit('progress', e, file);
|
||||
},
|
||||
onSuccess: (ret, xhr) => {
|
||||
delete this.reqs[uid];
|
||||
this.__emit('success', ret, file, xhr);
|
||||
},
|
||||
onError: (err, ret) => {
|
||||
delete this.reqs[uid];
|
||||
this.__emit('error', err, ret, file);
|
||||
},
|
||||
};
|
||||
this.reqs[uid] = request(requestOption);
|
||||
this.__emit('start', file);
|
||||
});
|
||||
});
|
||||
},
|
||||
reset() {
|
||||
this.setState({
|
||||
uid: getUid(),
|
||||
});
|
||||
},
|
||||
abort(file) {
|
||||
const { reqs } = this;
|
||||
if (file) {
|
||||
let uid = file;
|
||||
if (file && file.uid) {
|
||||
uid = file.uid;
|
||||
}
|
||||
if (reqs[uid] && reqs[uid].abort) {
|
||||
reqs[uid].abort();
|
||||
}
|
||||
delete reqs[uid];
|
||||
} else {
|
||||
Object.keys(reqs).forEach(uid => {
|
||||
if (reqs[uid] && reqs[uid].abort) {
|
||||
reqs[uid].abort();
|
||||
}
|
||||
|
||||
delete reqs[uid];
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
const { $props, $attrs } = this;
|
||||
const {
|
||||
componentTag: Tag,
|
||||
prefixCls,
|
||||
disabled,
|
||||
multiple,
|
||||
accept,
|
||||
directory,
|
||||
openFileDialogOnClick,
|
||||
} = $props;
|
||||
const { class: className, style, id } = $attrs;
|
||||
const cls = classNames({
|
||||
[prefixCls]: true,
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
[className]: className,
|
||||
});
|
||||
const events = disabled
|
||||
? {}
|
||||
: {
|
||||
onClick: openFileDialogOnClick ? this.onClick : () => {},
|
||||
onKeydown: openFileDialogOnClick ? this.onKeyDown : () => {},
|
||||
onDrop: this.onFileDrop,
|
||||
onDragover: this.onFileDrop,
|
||||
};
|
||||
const tagProps = {
|
||||
...events,
|
||||
role: 'button',
|
||||
tabindex: disabled ? null : '0',
|
||||
class: cls,
|
||||
style,
|
||||
};
|
||||
return (
|
||||
<Tag {...tagProps}>
|
||||
<input
|
||||
id={id}
|
||||
type="file"
|
||||
ref="fileInputRef"
|
||||
onClick={e => e.stopPropagation()} // https://github.com/ant-design/ant-design/issues/19948
|
||||
key={this.uid}
|
||||
style={{ display: 'none' }}
|
||||
accept={accept}
|
||||
directory={directory ? 'directory' : null}
|
||||
webkitdirectory={directory ? 'webkitdirectory' : null}
|
||||
multiple={multiple}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
{getSlot(this)}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default AjaxUploader;
|
|
@ -1,281 +0,0 @@
|
|||
import PropTypes from '../../_util/vue-types';
|
||||
import BaseMixin from '../../_util/BaseMixin';
|
||||
import classNames from '../../_util/classNames';
|
||||
import getUid from './uid';
|
||||
import warning from '../../_util/warning';
|
||||
import { getSlot, findDOMNode } from '../../_util/props-util';
|
||||
|
||||
const IFRAME_STYLE = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
opacity: 0,
|
||||
filter: 'alpha(opacity=0)',
|
||||
left: 0,
|
||||
zIndex: 9999,
|
||||
};
|
||||
|
||||
// diferent from AjaxUpload, can only upload on at one time, serial seriously
|
||||
const IframeUploader = {
|
||||
name: 'IframeUploader',
|
||||
mixins: [BaseMixin],
|
||||
props: {
|
||||
componentTag: PropTypes.string,
|
||||
// style: PropTypes.object,
|
||||
disabled: PropTypes.looseBool,
|
||||
prefixCls: PropTypes.string,
|
||||
// className: PropTypes.string,
|
||||
accept: PropTypes.string,
|
||||
// onStart: PropTypes.func,
|
||||
multiple: PropTypes.looseBool,
|
||||
// children: PropTypes.any,
|
||||
data: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
|
||||
action: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
name: PropTypes.string,
|
||||
},
|
||||
data() {
|
||||
this.file = {};
|
||||
return {
|
||||
uploading: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onLoad() {
|
||||
if (!this.uploading) {
|
||||
return;
|
||||
}
|
||||
const { file } = this;
|
||||
let response;
|
||||
try {
|
||||
const doc = this.getIframeDocument();
|
||||
const script = doc.getElementsByTagName('script')[0];
|
||||
if (script && script.parentNode === doc.body) {
|
||||
doc.body.removeChild(script);
|
||||
}
|
||||
response = doc.body.innerHTML;
|
||||
this.__emit('success', response, file);
|
||||
} catch (err) {
|
||||
warning(
|
||||
false,
|
||||
'cross domain error for Upload. Maybe server should return document.domain script. see Note from https://github.com/react-component/upload',
|
||||
);
|
||||
response = 'cross-domain';
|
||||
this.__emit('error', err, null, file);
|
||||
}
|
||||
this.endUpload();
|
||||
},
|
||||
onChange() {
|
||||
const target = this.getFormInputNode();
|
||||
// ie8/9 don't support FileList Object
|
||||
// http://stackoverflow.com/questions/12830058/ie8-input-type-file-get-files
|
||||
const file = (this.file = {
|
||||
uid: getUid(),
|
||||
name:
|
||||
target.value &&
|
||||
target.value.substring(target.value.lastIndexOf('\\') + 1, target.value.length),
|
||||
});
|
||||
this.startUpload();
|
||||
const { $props: props } = this;
|
||||
if (!props.beforeUpload) {
|
||||
return this.post(file);
|
||||
}
|
||||
const before = props.beforeUpload(file);
|
||||
if (before && before.then) {
|
||||
before.then(
|
||||
() => {
|
||||
this.post(file);
|
||||
},
|
||||
() => {
|
||||
this.endUpload();
|
||||
},
|
||||
);
|
||||
} else if (before !== false) {
|
||||
this.post(file);
|
||||
} else {
|
||||
this.endUpload();
|
||||
}
|
||||
},
|
||||
getIframeNode() {
|
||||
return this.$refs.iframeRef;
|
||||
},
|
||||
getIframeDocument() {
|
||||
return this.getIframeNode().contentDocument;
|
||||
},
|
||||
getFormNode() {
|
||||
return this.getIframeDocument().getElementById('form');
|
||||
},
|
||||
getFormInputNode() {
|
||||
return this.getIframeDocument().getElementById('input');
|
||||
},
|
||||
getFormDataNode() {
|
||||
return this.getIframeDocument().getElementById('data');
|
||||
},
|
||||
getFileForMultiple(file) {
|
||||
return this.multiple ? [file] : file;
|
||||
},
|
||||
getIframeHTML(domain) {
|
||||
let domainScript = '';
|
||||
let domainInput = '';
|
||||
if (domain) {
|
||||
const script = 'script';
|
||||
domainScript = `<${script}>document.domain="${domain}";</${script}>`;
|
||||
domainInput = `<input name="_documentDomain" value="${domain}" />`;
|
||||
}
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<style>
|
||||
body,html {padding:0;margin:0;border:0;overflow:hidden;}
|
||||
</style>
|
||||
${domainScript}
|
||||
</head>
|
||||
<body>
|
||||
<form method="post"
|
||||
encType="multipart/form-data"
|
||||
action="" id="form"
|
||||
style="display:block;height:9999px;position:relative;overflow:hidden;">
|
||||
<input id="input" type="file"
|
||||
name="${this.name}"
|
||||
style="position:absolute;top:0;right:0;height:9999px;font-size:9999px;cursor:pointer;"/>
|
||||
${domainInput}
|
||||
<span id="data"></span>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
},
|
||||
initIframeSrc() {
|
||||
if (this.domain) {
|
||||
this.getIframeNode().src = `javascript:void((function(){
|
||||
var d = document;
|
||||
d.open();
|
||||
d.domain='${this.domain}';
|
||||
d.write('');
|
||||
d.close();
|
||||
})())`;
|
||||
}
|
||||
},
|
||||
initIframe() {
|
||||
const iframeNode = this.getIframeNode();
|
||||
let win = iframeNode.contentWindow;
|
||||
let doc;
|
||||
this.domain = this.domain || '';
|
||||
this.initIframeSrc();
|
||||
try {
|
||||
doc = win.document;
|
||||
} catch (e) {
|
||||
this.domain = document.domain;
|
||||
this.initIframeSrc();
|
||||
win = iframeNode.contentWindow;
|
||||
doc = win.document;
|
||||
}
|
||||
doc.open('text/html', 'replace');
|
||||
doc.write(this.getIframeHTML(this.domain));
|
||||
doc.close();
|
||||
this.getFormInputNode().onchange = this.onChange;
|
||||
},
|
||||
endUpload() {
|
||||
if (this.uploading) {
|
||||
this.file = {};
|
||||
// hack avoid batch
|
||||
this.uploading = false;
|
||||
this.setState({
|
||||
uploading: false,
|
||||
});
|
||||
this.initIframe();
|
||||
}
|
||||
},
|
||||
startUpload() {
|
||||
if (!this.uploading) {
|
||||
this.uploading = true;
|
||||
this.setState({
|
||||
uploading: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
updateIframeWH() {
|
||||
const rootNode = findDOMNode(this);
|
||||
const iframeNode = this.getIframeNode();
|
||||
iframeNode.style.height = `${rootNode.offsetHeight}px`;
|
||||
iframeNode.style.width = `${rootNode.offsetWidth}px`;
|
||||
},
|
||||
abort(file) {
|
||||
if (file) {
|
||||
let uid = file;
|
||||
if (file && file.uid) {
|
||||
uid = file.uid;
|
||||
}
|
||||
if (uid === this.file.uid) {
|
||||
this.endUpload();
|
||||
}
|
||||
} else {
|
||||
this.endUpload();
|
||||
}
|
||||
},
|
||||
post(file) {
|
||||
const formNode = this.getFormNode();
|
||||
const dataSpan = this.getFormDataNode();
|
||||
let { data } = this.$props;
|
||||
if (typeof data === 'function') {
|
||||
data = data(file);
|
||||
}
|
||||
const inputs = document.createDocumentFragment();
|
||||
for (const key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
const input = document.createElement('input');
|
||||
input.setAttribute('name', key);
|
||||
input.value = data[key];
|
||||
inputs.appendChild(input);
|
||||
}
|
||||
}
|
||||
dataSpan.appendChild(inputs);
|
||||
new Promise(resolve => {
|
||||
const { action } = this;
|
||||
if (typeof action === 'function') {
|
||||
return resolve(action(file));
|
||||
}
|
||||
resolve(action);
|
||||
}).then(action => {
|
||||
formNode.setAttribute('action', action);
|
||||
formNode.submit();
|
||||
dataSpan.innerHTML = '';
|
||||
this.__emit('start', file);
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.updateIframeWH();
|
||||
this.initIframe();
|
||||
});
|
||||
},
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
this.updateIframeWH();
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
const { componentTag: Tag, disabled, prefixCls } = this.$props;
|
||||
const { class: className, style } = this.$attrs;
|
||||
const iframeStyle = {
|
||||
...IFRAME_STYLE,
|
||||
display: this.uploading || disabled ? 'none' : '',
|
||||
};
|
||||
const cls = classNames({
|
||||
[prefixCls]: true,
|
||||
[`${prefixCls}-disabled`]: disabled,
|
||||
[className]: className,
|
||||
});
|
||||
|
||||
return (
|
||||
<Tag class={cls} style={{ position: 'relative', zIndex: 0, ...style }}>
|
||||
<iframe ref="iframeRef" onLoad={this.onLoad} style={iframeStyle} />
|
||||
{getSlot(this)}
|
||||
</Tag>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default IframeUploader;
|
|
@ -1,97 +0,0 @@
|
|||
import PropTypes from '../../_util/vue-types';
|
||||
import { initDefaultProps, getSlot } from '../../_util/props-util';
|
||||
import BaseMixin from '../../_util/BaseMixin';
|
||||
import AjaxUpload from './AjaxUploader';
|
||||
import IframeUpload from './IframeUploader';
|
||||
import { defineComponent, nextTick } from 'vue';
|
||||
|
||||
function empty() {}
|
||||
|
||||
const uploadProps = {
|
||||
componentTag: PropTypes.string,
|
||||
prefixCls: PropTypes.string,
|
||||
action: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
name: PropTypes.string,
|
||||
multipart: PropTypes.looseBool,
|
||||
directory: PropTypes.looseBool,
|
||||
onError: PropTypes.func,
|
||||
onSuccess: PropTypes.func,
|
||||
onProgress: PropTypes.func,
|
||||
onStart: PropTypes.func,
|
||||
data: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
|
||||
headers: PropTypes.object,
|
||||
accept: PropTypes.string,
|
||||
multiple: PropTypes.looseBool,
|
||||
disabled: PropTypes.looseBool,
|
||||
beforeUpload: PropTypes.func,
|
||||
customRequest: PropTypes.func,
|
||||
onReady: PropTypes.func,
|
||||
withCredentials: PropTypes.looseBool,
|
||||
supportServerRender: PropTypes.looseBool,
|
||||
openFileDialogOnClick: PropTypes.looseBool,
|
||||
method: PropTypes.string,
|
||||
};
|
||||
export default defineComponent({
|
||||
name: 'Upload',
|
||||
mixins: [BaseMixin],
|
||||
inheritAttrs: false,
|
||||
props: initDefaultProps(uploadProps, {
|
||||
componentTag: 'span',
|
||||
prefixCls: 'rc-upload',
|
||||
data: {},
|
||||
headers: {},
|
||||
name: 'file',
|
||||
multipart: false,
|
||||
onReady: empty,
|
||||
onStart: empty,
|
||||
onError: empty,
|
||||
onSuccess: empty,
|
||||
supportServerRender: false,
|
||||
multiple: false,
|
||||
beforeUpload: empty,
|
||||
withCredentials: false,
|
||||
openFileDialogOnClick: true,
|
||||
}),
|
||||
data() {
|
||||
this.Component = null;
|
||||
return {
|
||||
// Component: null, // 组件作为响应式数据,性能比较低,采用强制刷新
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
if (this.supportServerRender) {
|
||||
this.Component = this.getComponent();
|
||||
this.$forceUpdate();
|
||||
nextTick(() => {
|
||||
this.__emit('ready');
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
getComponent() {
|
||||
return typeof File !== 'undefined' ? AjaxUpload : IframeUpload;
|
||||
},
|
||||
abort(file) {
|
||||
this.$refs.uploaderRef.abort(file);
|
||||
},
|
||||
},
|
||||
|
||||
render() {
|
||||
const componentProps = {
|
||||
...this.$props,
|
||||
ref: 'uploaderRef',
|
||||
...this.$attrs,
|
||||
};
|
||||
if (this.supportServerRender) {
|
||||
const ComponentUploader = this.Component;
|
||||
if (ComponentUploader) {
|
||||
return <ComponentUploader {...componentProps}>{getSlot(this)}</ComponentUploader>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const ComponentUploader = this.getComponent();
|
||||
return <ComponentUploader {...componentProps}>{getSlot(this)}</ComponentUploader>;
|
||||
},
|
||||
});
|
|
@ -1,26 +0,0 @@
|
|||
function endsWith(str, suffix) {
|
||||
return str.indexOf(suffix, str.length - suffix.length) !== -1;
|
||||
}
|
||||
|
||||
export default (file, acceptedFiles) => {
|
||||
if (file && acceptedFiles) {
|
||||
const acceptedFilesArray = Array.isArray(acceptedFiles)
|
||||
? acceptedFiles
|
||||
: acceptedFiles.split(',');
|
||||
const fileName = file.name || '';
|
||||
const mimeType = file.type || '';
|
||||
const baseMimeType = mimeType.replace(/\/.*$/, '');
|
||||
|
||||
return acceptedFilesArray.some(type => {
|
||||
const validType = type.trim();
|
||||
if (validType.charAt(0) === '.') {
|
||||
return endsWith(fileName.toLowerCase(), validType.toLowerCase());
|
||||
} else if (/\/\*$/.test(validType)) {
|
||||
// This is something like a image/* mime type
|
||||
return baseMimeType === validType.replace(/\/.*$/, '');
|
||||
}
|
||||
return mimeType === validType;
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
|
@ -1,4 +0,0 @@
|
|||
// export this package's api
|
||||
import Upload from './Upload';
|
||||
|
||||
export default Upload;
|
|
@ -1,108 +0,0 @@
|
|||
function getError(option, xhr) {
|
||||
const msg = `cannot ${option.method} ${option.action} ${xhr.status}'`;
|
||||
const err = new Error(msg);
|
||||
err.status = xhr.status;
|
||||
err.method = option.method;
|
||||
err.url = option.action;
|
||||
return err;
|
||||
}
|
||||
|
||||
function getBody(xhr) {
|
||||
const text = xhr.responseText || xhr.response;
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch (e) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
// option {
|
||||
// onProgress: (event: { percent: number }): void,
|
||||
// onError: (event: Error, body?: Object): void,
|
||||
// onSuccess: (body: Object): void,
|
||||
// data: Object,
|
||||
// filename: String,
|
||||
// file: File,
|
||||
// withCredentials: Boolean,
|
||||
// action: String,
|
||||
// headers: Object,
|
||||
// }
|
||||
export default function upload(option) {
|
||||
const xhr = new window.XMLHttpRequest();
|
||||
|
||||
if (option.onProgress && xhr.upload) {
|
||||
xhr.upload.onprogress = function progress(e) {
|
||||
if (e.total > 0) {
|
||||
e.percent = (e.loaded / e.total) * 100;
|
||||
}
|
||||
option.onProgress(e);
|
||||
};
|
||||
}
|
||||
|
||||
const formData = new window.FormData();
|
||||
|
||||
if (option.data) {
|
||||
Object.keys(option.data).forEach(key => {
|
||||
const value = option.data[key];
|
||||
// support key-value array data
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(item => {
|
||||
// { list: [ 11, 22 ] }
|
||||
// formData.append('list[]', 11);
|
||||
formData.append(`${key}[]`, item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append(key, option.data[key]);
|
||||
});
|
||||
}
|
||||
|
||||
formData.append(option.filename, option.file);
|
||||
|
||||
xhr.onerror = function error(e) {
|
||||
option.onError(e);
|
||||
};
|
||||
|
||||
xhr.onload = function onload() {
|
||||
// allow success when 2xx status
|
||||
// see https://github.com/react-component/upload/issues/34
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
return option.onError(getError(option, xhr), getBody(xhr));
|
||||
}
|
||||
|
||||
option.onSuccess(getBody(xhr), xhr);
|
||||
};
|
||||
|
||||
xhr.open(option.method, option.action, true);
|
||||
|
||||
// Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179
|
||||
if (option.withCredentials && 'withCredentials' in xhr) {
|
||||
xhr.withCredentials = true;
|
||||
}
|
||||
|
||||
const headers = option.headers || {};
|
||||
|
||||
// when set headers['X-Requested-With'] = null , can close default XHR header
|
||||
// see https://github.com/react-component/upload/issues/33
|
||||
if (headers['X-Requested-With'] !== null) {
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
}
|
||||
|
||||
for (const h in headers) {
|
||||
if (headers.hasOwnProperty(h) && headers[h] !== null) {
|
||||
xhr.setRequestHeader(h, headers[h]);
|
||||
}
|
||||
}
|
||||
xhr.send(formData);
|
||||
|
||||
return {
|
||||
abort() {
|
||||
xhr.abort();
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
function loopFiles(item, callback) {
|
||||
const dirReader = item.createReader();
|
||||
let fileList = [];
|
||||
|
||||
function sequence() {
|
||||
dirReader.readEntries(entries => {
|
||||
const entryList = Array.prototype.slice.apply(entries);
|
||||
fileList = fileList.concat(entryList);
|
||||
|
||||
// Check if all the file has been viewed
|
||||
const isFinished = !entryList.length;
|
||||
|
||||
if (isFinished) {
|
||||
callback(fileList);
|
||||
} else {
|
||||
sequence();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sequence();
|
||||
}
|
||||
|
||||
const traverseFileTree = (files, callback, isAccepted) => {
|
||||
const _traverseFileTree = (item, path) => {
|
||||
path = path || '';
|
||||
if (item.isFile) {
|
||||
item.file(file => {
|
||||
if (isAccepted(file)) {
|
||||
// https://github.com/ant-design/ant-design/issues/16426
|
||||
if (item.fullPath && !file.webkitRelativePath) {
|
||||
Object.defineProperties(file, {
|
||||
webkitRelativePath: {
|
||||
writable: true,
|
||||
},
|
||||
});
|
||||
file.webkitRelativePath = item.fullPath.replace(/^\//, '');
|
||||
Object.defineProperties(file, {
|
||||
webkitRelativePath: {
|
||||
writable: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
callback([file]);
|
||||
}
|
||||
});
|
||||
} else if (item.isDirectory) {
|
||||
loopFiles(item, entries => {
|
||||
entries.forEach(entryItem => {
|
||||
_traverseFileTree(entryItem, `${path}${item.name}/`);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
for (const file of files) {
|
||||
_traverseFileTree(file.webkitGetAsEntry());
|
||||
}
|
||||
};
|
||||
|
||||
export default traverseFileTree;
|
|
@ -1,6 +0,0 @@
|
|||
const now = +new Date();
|
||||
let index = 0;
|
||||
|
||||
export default function uid() {
|
||||
return `vc-upload-${now}-${++index}`;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
// debugger tsx
|
||||
import Demo from '../../components/form/demo/normal-login.vue';
|
||||
import Demo from '../../components/upload/demo/defaultFileList.vue';
|
||||
// import Demo from './demo/demo.vue';
|
||||
|
||||
export default {
|
||||
|
|
Loading…
Reference in New Issue