refactor: upload
parent
67f5226cdc
commit
f0f970c37b
|
@ -224,7 +224,7 @@ export {
|
||||||
TypographyTitle,
|
TypographyTitle,
|
||||||
} from './typography';
|
} from './typography';
|
||||||
|
|
||||||
export type { UploadProps, UploadListProps, UploadChangeParam } from './upload';
|
export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from './upload';
|
||||||
|
|
||||||
export { default as Upload, UploadDragger } 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 { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker';
|
||||||
import type { PaginationLocale } from '../pagination/Pagination';
|
import type { PaginationLocale } from '../pagination/Pagination';
|
||||||
import type { TableLocale } from '../table/interface';
|
import type { TableLocale } from '../table/interface';
|
||||||
|
import type { UploadLocale } from '../upload/interface';
|
||||||
|
|
||||||
interface TransferLocaleForEmpty {
|
interface TransferLocaleForEmpty {
|
||||||
description: string;
|
description: string;
|
||||||
|
@ -18,7 +19,6 @@ export interface Locale {
|
||||||
Pagination?: PaginationLocale;
|
Pagination?: PaginationLocale;
|
||||||
Table?: TableLocale;
|
Table?: TableLocale;
|
||||||
Popconfirm?: Record<string, any>;
|
Popconfirm?: Record<string, any>;
|
||||||
Upload?: Record<string, any>;
|
|
||||||
Form?: {
|
Form?: {
|
||||||
optional?: string;
|
optional?: string;
|
||||||
defaultValidateMessages: ValidateMessages;
|
defaultValidateMessages: ValidateMessages;
|
||||||
|
@ -32,6 +32,7 @@ export interface Locale {
|
||||||
Modal?: ModalLocale;
|
Modal?: ModalLocale;
|
||||||
Transfer?: Partial<TransferLocale>;
|
Transfer?: Partial<TransferLocale>;
|
||||||
Select?: Record<string, any>;
|
Select?: Record<string, any>;
|
||||||
|
Upload?: UploadLocale;
|
||||||
Empty?: TransferLocaleForEmpty;
|
Empty?: TransferLocaleForEmpty;
|
||||||
global?: Record<string, any>;
|
global?: Record<string, any>;
|
||||||
PageHeader?: { back: string };
|
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 {
|
&&-select-picture-card {
|
||||||
|
display: table;
|
||||||
|
float: left;
|
||||||
width: @upload-picture-card-size;
|
width: @upload-picture-card-size;
|
||||||
height: @upload-picture-card-size;
|
height: @upload-picture-card-size;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
@ -44,21 +46,19 @@
|
||||||
border: @border-width-base dashed @border-color-base;
|
border: @border-width-base dashed @border-color-base;
|
||||||
border-radius: @border-radius-base;
|
border-radius: @border-radius-base;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.3s;
|
transition: border-color 0.3s ease;
|
||||||
|
|
||||||
> .@{upload-prefix-cls} {
|
> .@{upload-prefix-cls} {
|
||||||
display: flex;
|
display: table-cell;
|
||||||
align-items: center;
|
width: 100%;
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
padding: 8px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: @primary-color;
|
border-color: @primary-color;
|
||||||
.@{upload-prefix-cls}-disabled& {
|
|
||||||
border-color: @border-color-base;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
transition: border-color 0.3s;
|
transition: border-color 0.3s;
|
||||||
|
|
||||||
.@{upload-prefix-cls} {
|
.@{upload-prefix-cls} {
|
||||||
padding: @padding-md 0;
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
|
&.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
|
||||||
|
@ -116,12 +116,10 @@
|
||||||
color: @text-color-secondary;
|
color: @text-color-secondary;
|
||||||
font-size: @font-size-base;
|
font-size: @font-size-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{iconfont-css-prefix}-plus {
|
.@{iconfont-css-prefix}-plus {
|
||||||
color: @disabled-color;
|
color: @disabled-color;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: @text-color-secondary;
|
color: @text-color-secondary;
|
||||||
}
|
}
|
||||||
|
@ -142,55 +140,52 @@
|
||||||
.@{upload-prefix-cls}-list {
|
.@{upload-prefix-cls}-list {
|
||||||
.reset-component();
|
.reset-component();
|
||||||
.clearfix();
|
.clearfix();
|
||||||
line-height: @line-height-base;
|
&-item-list-type-text {
|
||||||
|
&:hover {
|
||||||
// ============================ Item ============================
|
.@{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 {
|
&-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: @line-height-base * @font-size-base;
|
height: 22px;
|
||||||
margin-top: @margin-xs;
|
margin-top: 8px;
|
||||||
font-size: @font-size-base;
|
font-size: @font-size-base;
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: @font-size-base + 8px;
|
padding-left: @font-size-base + 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
line-height: @line-height-base;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-name-icon-count-1 {
|
||||||
|
padding-right: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
&-card-actions {
|
&-card-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
&-btn {
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
|
||||||
&-btn.@{ant-prefix}-btn-sm {
|
|
||||||
height: 20px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.picture {
|
&.picture {
|
||||||
top: 22px;
|
top: 25px;
|
||||||
line-height: 0;
|
line-height: 1;
|
||||||
}
|
|
||||||
|
|
||||||
&-btn:focus,
|
|
||||||
&.picture &-btn {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
.anticon {
|
||||||
.@{iconfont-css-prefix} {
|
padding-right: 6px;
|
||||||
color: @upload-actions-color;
|
color: rgba(0, 0, 0, 0.45);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-info {
|
&-info {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 4px;
|
padding: 0 12px 0 4px;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
|
@ -200,27 +195,25 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{iconfont-css-prefix}-loading,
|
.@{iconfont-css-prefix}-loading,
|
||||||
.@{upload-prefix-cls}-text-icon {
|
.@{iconfont-css-prefix}-paper-clip {
|
||||||
.@{iconfont-css-prefix} {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: (@font-size-base / 2) - 2px;
|
top: (@font-size-base / 2) - 2px;
|
||||||
color: @text-color-secondary;
|
color: @text-color-secondary;
|
||||||
font-size: @font-size-base;
|
font-size: @font-size-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.@{iconfont-css-prefix}-close {
|
.@{iconfont-css-prefix}-close {
|
||||||
|
.iconfont-size-under-12px(10px);
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
color: @text-color-secondary;
|
color: @text-color-secondary;
|
||||||
font-size: 10px;
|
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: @text-color;
|
color: @text-color;
|
||||||
}
|
}
|
||||||
|
@ -234,25 +227,22 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover &-card-actions-btn {
|
&:hover &-card-actions {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-error,
|
&-error,
|
||||||
&-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix},
|
&-error .@{iconfont-css-prefix}-paper-clip,
|
||||||
&-error &-name {
|
&-error &-name {
|
||||||
color: @error-color;
|
color: @error-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-error &-card-actions {
|
&-error &-card-actions {
|
||||||
.@{iconfont-css-prefix} {
|
.anticon {
|
||||||
color: @error-color;
|
color: @error-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-btn {
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&-progress {
|
&-progress {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -264,20 +254,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =================== Picture & Picture Card ===================
|
|
||||||
&-picture,
|
&-picture,
|
||||||
&-picture-card {
|
&-picture-card {
|
||||||
.@{upload-item} {
|
.@{upload-item} {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 66px;
|
height: 66px;
|
||||||
padding: @padding-xs;
|
padding: 8px;
|
||||||
border: @border-width-base @upload-picture-card-border-style @border-color-base;
|
border: @border-width-base @upload-picture-card-border-style @border-color-base;
|
||||||
border-radius: @border-radius-base;
|
border-radius: @border-radius-base;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-error {
|
&-error {
|
||||||
border-color: @error-color;
|
border-color: @error-color;
|
||||||
}
|
}
|
||||||
|
@ -296,30 +283,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-thumbnail {
|
.@{upload-item}-thumbnail {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
line-height: 60px;
|
font-size: 26px;
|
||||||
|
line-height: 54px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.8;
|
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 {
|
.@{upload-item}-icon {
|
||||||
|
@ -328,10 +300,6 @@
|
||||||
left: 50%;
|
left: 50%;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
.@{iconfont-css-prefix} {
|
|
||||||
font-size: 26px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-image {
|
.@{upload-item}-image {
|
||||||
|
@ -359,8 +327,16 @@
|
||||||
transition: all 0.3s;
|
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 {
|
.@{upload-item}-uploading .@{upload-item}-name {
|
||||||
margin-bottom: 12px;
|
line-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-progress {
|
.@{upload-item}-progress {
|
||||||
|
@ -379,23 +355,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================== Picture Card ========================
|
|
||||||
&-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 {
|
&.@{upload-prefix-cls}-list::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
&-container {
|
||||||
|
float: left;
|
||||||
|
width: @upload-picture-card-size;
|
||||||
|
height: @upload-picture-card-size;
|
||||||
|
margin: 0 8px 8px 0;
|
||||||
|
}
|
||||||
.@{upload-item} {
|
.@{upload-item} {
|
||||||
height: 100%;
|
float: left;
|
||||||
margin: 0;
|
width: @upload-picture-card-size;
|
||||||
|
height: @upload-picture-card-size;
|
||||||
|
margin: 0 8px 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-info {
|
.@{upload-item}-info {
|
||||||
|
@ -439,7 +413,6 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: @text-color-inverse;
|
color: @text-color-inverse;
|
||||||
}
|
}
|
||||||
|
@ -457,7 +430,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-name {
|
.@{upload-item}-name {
|
||||||
|
@ -468,7 +441,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-file + .@{upload-item}-name {
|
.anticon-picture + .@{upload-item}-name {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -481,82 +454,46 @@
|
||||||
|
|
||||||
.@{upload-item}-info {
|
.@{upload-item}-info {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
&::before,
|
&::before,
|
||||||
.@{iconfont-css-prefix}-eye,
|
.@{iconfont-css-prefix}-eye-o,
|
||||||
.@{iconfont-css-prefix}-delete {
|
.@{iconfont-css-prefix}-delete {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-text {
|
||||||
|
margin-top: 18px;
|
||||||
|
color: @text-color-secondary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-progress {
|
.@{upload-item}-progress {
|
||||||
bottom: 32px;
|
bottom: 32px;
|
||||||
width: calc(100% - 14px);
|
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================= Picture & Text =======================
|
.@{upload-prefix-cls}-success-icon {
|
||||||
&-text,
|
color: @success-color;
|
||||||
&-picture {
|
font-weight: bold;
|
||||||
&-container {
|
|
||||||
transition: opacity @animation-duration-slow, height @animation-duration-slow;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
display: table;
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
content: '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't know why span here, just stretch it
|
.@{upload-prefix-cls}-animate-enter,
|
||||||
.@{upload-prefix-cls}-span {
|
.@{upload-prefix-cls}-animate-leave,
|
||||||
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-enter,
|
||||||
.@{upload-prefix-cls}-animate-inline-leave {
|
.@{upload-prefix-cls}-animate-inline-leave {
|
||||||
animation-duration: @animation-duration-slow;
|
animation-duration: 0.3s;
|
||||||
animation-fill-mode: @ease-in-out-circ;
|
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 {
|
.@{upload-prefix-cls}-animate-inline-enter {
|
||||||
animation-name: uploadAnimateInlineIn;
|
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 {
|
@keyframes uploadAnimateInlineIn {
|
||||||
from {
|
from {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
@ -585,5 +540,3 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@import './rtl';
|
|
|
@ -1,22 +1,22 @@
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import { getOptionProps, getSlot } from '../_util/props-util';
|
|
||||||
import Upload from './Upload';
|
import Upload from './Upload';
|
||||||
import { uploadProps } from './interface';
|
import { uploadProps } from './interface';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'AUploadDragger',
|
name: 'AUploadDragger',
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: uploadProps,
|
props: uploadProps(),
|
||||||
render() {
|
setup(props, { slots, attrs }) {
|
||||||
const props = getOptionProps(this);
|
return () => {
|
||||||
const { height, ...restProps } = props;
|
const { height, ...restProps } = props;
|
||||||
const { style, ...restAttrs } = this.$attrs;
|
const { style, ...restAttrs } = attrs;
|
||||||
const draggerProps = {
|
const draggerProps = {
|
||||||
...restProps,
|
...restProps,
|
||||||
...restAttrs,
|
...restAttrs,
|
||||||
type: 'drag',
|
type: 'drag',
|
||||||
style: { ...(style as any), height },
|
style: { ...(style as any), height: typeof height === 'number' ? `${height}px` : height },
|
||||||
} as any;
|
} 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 type { UploadProps as RcUploadProps } from '../vc-upload';
|
||||||
import uniqBy from 'lodash-es/uniqBy';
|
|
||||||
import findIndex from 'lodash-es/findIndex';
|
|
||||||
import VcUpload 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 UploadList from './UploadList';
|
||||||
import type { UploadFile } from './interface';
|
import type {
|
||||||
|
UploadType,
|
||||||
|
UploadListType,
|
||||||
|
RcFile,
|
||||||
|
UploadFile,
|
||||||
|
UploadChangeParam,
|
||||||
|
ShowUploadListInterface,
|
||||||
|
} from './interface';
|
||||||
import { uploadProps } from './interface';
|
import { uploadProps } from './interface';
|
||||||
import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from './utils';
|
import { file2Obj, getFileItem, removeFileItem, updateFileList } from './utils';
|
||||||
import { defineComponent, inject } from 'vue';
|
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
|
||||||
import { getDataAndAriaProps } from '../_util/util';
|
import defaultLocale from '../locale/default';
|
||||||
import { useInjectFormItemContext } from '../form/FormItemContext';
|
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({
|
export default defineComponent({
|
||||||
name: 'AUpload',
|
name: 'AUpload',
|
||||||
mixins: [BaseMixin],
|
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
Dragger,
|
props: initDefaultProps(uploadProps(), {
|
||||||
props: initDefaultProps(uploadProps, {
|
type: 'select' as UploadType,
|
||||||
type: 'select',
|
|
||||||
multiple: false,
|
multiple: false,
|
||||||
action: '',
|
action: '',
|
||||||
data: {},
|
data: {},
|
||||||
accept: '',
|
accept: '',
|
||||||
beforeUpload: T,
|
|
||||||
showUploadList: true,
|
showUploadList: true,
|
||||||
listType: 'text', // or pictrue
|
listType: 'text' as UploadListType, // or picture
|
||||||
disabled: false,
|
disabled: false,
|
||||||
supportServerRender: true,
|
supportServerRender: true,
|
||||||
}),
|
}),
|
||||||
setup() {
|
setup(props, { slots, attrs, expose }) {
|
||||||
const formItemContext = useInjectFormItemContext();
|
const formItemContext = useInjectFormItemContext();
|
||||||
return {
|
const [mergedFileList, setMergedFileList] = useMergedState(props.defaultFileList || [], {
|
||||||
upload: null,
|
value: toRef(props, 'fileList'),
|
||||||
progressTimer: null,
|
postState: list => {
|
||||||
configProvider: inject('configProvider', defaultConfigProvider),
|
const timestamp = Date.now();
|
||||||
formItemContext,
|
return (list ?? []).map((file, index) => {
|
||||||
};
|
if (!file.uid && !Object.isFrozen(file)) {
|
||||||
},
|
file.uid = `__AUTO__${timestamp}_${index}__`;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
this.handleChange({
|
return file;
|
||||||
file: targetItem,
|
|
||||||
fileList: nextFileList,
|
|
||||||
});
|
});
|
||||||
// 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) {
|
const upload = ref();
|
||||||
this.clearProgressTimer();
|
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 {
|
try {
|
||||||
if (typeof response === 'string') {
|
if (typeof response === 'string') {
|
||||||
response = JSON.parse(response);
|
response = JSON.parse(response);
|
||||||
|
@ -88,255 +193,231 @@ export default defineComponent({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* do nothing */
|
/* do nothing */
|
||||||
}
|
}
|
||||||
const fileList = this.sFileList;
|
|
||||||
const targetItem = getFileItem(file, fileList);
|
|
||||||
// removed
|
// removed
|
||||||
if (!targetItem) {
|
if (!getFileItem(file, mergedFileList.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetItem = file2Obj(file);
|
||||||
targetItem.status = 'done';
|
targetItem.status = 'done';
|
||||||
|
targetItem.percent = 100;
|
||||||
targetItem.response = response;
|
targetItem.response = response;
|
||||||
targetItem.xhr = xhr;
|
targetItem.xhr = xhr;
|
||||||
this.handleChange({
|
|
||||||
file: { ...targetItem },
|
const nextFileList = updateFileList(targetItem, mergedFileList.value);
|
||||||
fileList,
|
|
||||||
});
|
onInternalChange(targetItem, nextFileList);
|
||||||
},
|
};
|
||||||
onProgress(e, file) {
|
|
||||||
const fileList = this.sFileList;
|
const onProgress = (e: { percent: number }, file: RcFile) => {
|
||||||
const targetItem = getFileItem(file, fileList);
|
|
||||||
// removed
|
// removed
|
||||||
if (!targetItem) {
|
if (!getFileItem(file, mergedFileList.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetItem = file2Obj(file);
|
||||||
|
targetItem.status = 'uploading';
|
||||||
targetItem.percent = e.percent;
|
targetItem.percent = e.percent;
|
||||||
this.handleChange({
|
|
||||||
event: e,
|
const nextFileList = updateFileList(targetItem, mergedFileList.value);
|
||||||
file: { ...targetItem },
|
|
||||||
fileList: this.sFileList,
|
onInternalChange(targetItem, nextFileList, e);
|
||||||
});
|
};
|
||||||
},
|
|
||||||
onError(error, response, file) {
|
const onError = (error: Error, response: any, file: RcFile) => {
|
||||||
this.clearProgressTimer();
|
|
||||||
const fileList = this.sFileList;
|
|
||||||
const targetItem = getFileItem(file, fileList);
|
|
||||||
// removed
|
// removed
|
||||||
if (!targetItem) {
|
if (!getFileItem(file, mergedFileList.value)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const targetItem = file2Obj(file);
|
||||||
targetItem.error = error;
|
targetItem.error = error;
|
||||||
targetItem.response = response;
|
targetItem.response = response;
|
||||||
targetItem.status = 'error';
|
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
|
// Prevent removing file
|
||||||
if (ret === false) {
|
if (ret === false) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const removedFileList = removeFileItem(file, fileList);
|
const removedFileList = removeFileItem(file, mergedFileList.value);
|
||||||
|
|
||||||
if (removedFileList) {
|
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) {
|
onInternalChange(currentFile, removedFileList);
|
||||||
this.upload.abort(file);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
this.handleChange({
|
const onFileDrop = (e: DragEvent) => {
|
||||||
file,
|
dragState.value = e.type;
|
||||||
fileList: removedFileList,
|
if (e.type === 'drop') {
|
||||||
|
props.onDrop?.(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
expose({
|
||||||
|
onBatchStart,
|
||||||
|
onSuccess,
|
||||||
|
onProgress,
|
||||||
|
onError,
|
||||||
|
fileList: mergedFileList,
|
||||||
|
upload,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
const { prefixCls, direction } = useConfigInject('upload', props);
|
||||||
},
|
const [locale] = useLocaleReceiver(
|
||||||
handleManualRemove(file) {
|
'Upload',
|
||||||
if (this.$refs.uploadRef) {
|
defaultLocale.Upload,
|
||||||
(this.$refs.uploadRef as any).abort(file);
|
computed(() => props.locale),
|
||||||
}
|
|
||||||
this.handleRemove(file);
|
|
||||||
},
|
|
||||||
handleChange(info) {
|
|
||||||
if (!hasProp(this, 'fileList')) {
|
|
||||||
this.setState({ sFileList: info.fileList });
|
|
||||||
}
|
|
||||||
this.$emit('update:fileList', info.fileList);
|
|
||||||
this.$emit('change', info);
|
|
||||||
this.formItemContext.onFieldChange();
|
|
||||||
},
|
|
||||||
onFileDrop(e) {
|
|
||||||
this.setState({
|
|
||||||
dragState: e.type,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
reBeforeUpload(file, fileList) {
|
|
||||||
const { beforeUpload } = this.$props;
|
|
||||||
const { sFileList: stateFileList } = this.$data;
|
|
||||||
if (!beforeUpload) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const result = beforeUpload(file, fileList);
|
|
||||||
if (result === false) {
|
|
||||||
this.handleChange({
|
|
||||||
file,
|
|
||||||
fileList: uniqBy(
|
|
||||||
stateFileList.concat(fileList.map(fileToObject)),
|
|
||||||
(item: UploadFile) => item.uid,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (result && result.then) {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
clearProgressTimer() {
|
|
||||||
clearInterval(this.progressTimer);
|
|
||||||
},
|
|
||||||
autoUpdateProgress(_, file) {
|
|
||||||
const getPercent = genPercentAdd();
|
|
||||||
let curPercent = 0;
|
|
||||||
this.clearProgressTimer();
|
|
||||||
this.progressTimer = setInterval(() => {
|
|
||||||
curPercent = getPercent(curPercent);
|
|
||||||
this.onProgress(
|
|
||||||
{
|
|
||||||
percent: curPercent * 100,
|
|
||||||
},
|
|
||||||
file,
|
|
||||||
);
|
);
|
||||||
}, 200);
|
const renderUploadList = (button?: VueNode) => {
|
||||||
},
|
|
||||||
renderUploadList(locale) {
|
|
||||||
const {
|
const {
|
||||||
showUploadList = {},
|
removeIcon,
|
||||||
listType,
|
previewIcon,
|
||||||
|
downloadIcon,
|
||||||
previewFile,
|
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,
|
onPreview,
|
||||||
};
|
onDownload,
|
||||||
return <UploadList {...uploadListProps} />;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
prefixCls: customizePrefixCls,
|
|
||||||
showUploadList,
|
|
||||||
listType,
|
|
||||||
type,
|
|
||||||
disabled,
|
disabled,
|
||||||
} = getOptionProps(this);
|
isImageUrl,
|
||||||
const { sFileList: fileList, dragState } = this.$data;
|
progress,
|
||||||
const { class: className, style } = this.$attrs;
|
itemRender,
|
||||||
const getPrefixCls = this.configProvider.getPrefixCls;
|
iconRender,
|
||||||
const prefixCls = getPrefixCls('upload', customizePrefixCls);
|
showUploadList,
|
||||||
|
} = props;
|
||||||
const vcUploadProps = {
|
const { showDownloadIcon, showPreviewIcon, showRemoveIcon } =
|
||||||
...this.$props,
|
typeof showUploadList === 'boolean' ? ({} as ShowUploadListInterface) : showUploadList;
|
||||||
id: this.$props.id ?? this.formItemContext.id.value,
|
return showUploadList ? (
|
||||||
prefixCls,
|
<UploadList
|
||||||
beforeUpload: this.reBeforeUpload,
|
listType={props.listType}
|
||||||
onStart: this.onStart,
|
items={mergedFileList.value}
|
||||||
onError: this.onError,
|
previewFile={previewFile}
|
||||||
onProgress: this.onProgress,
|
onPreview={onPreview}
|
||||||
onSuccess: this.onSuccess,
|
onDownload={onDownload}
|
||||||
onReject: this.onReject,
|
onRemove={handleRemove}
|
||||||
ref: 'uploadRef',
|
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 ? (
|
// Remove id to avoid open by label when trigger is hidden
|
||||||
<LocaleReceiver
|
// !children: https://github.com/ant-design/ant-design/issues/14298
|
||||||
componentName="Upload"
|
// disabled: https://github.com/ant-design/ant-design/issues/16478
|
||||||
defaultLocale={defaultLocale.Upload}
|
// https://github.com/ant-design/ant-design/issues/24197
|
||||||
children={this.renderUploadList}
|
if (!slots.default || disabled) {
|
||||||
/>
|
delete rcUploadProps.id;
|
||||||
) : null;
|
}
|
||||||
|
|
||||||
const children = getSlot(this);
|
|
||||||
|
|
||||||
if (type === 'drag') {
|
if (type === 'drag') {
|
||||||
const dragCls = classNames(prefixCls, {
|
const dragCls = classNames(
|
||||||
[`${prefixCls}-drag`]: true,
|
prefixCls.value,
|
||||||
[`${prefixCls}-drag-uploading`]: fileList.some((file: any) => file.status === 'uploading'),
|
{
|
||||||
[`${prefixCls}-drag-hover`]: dragState === 'dragover',
|
[`${prefixCls.value}-drag`]: true,
|
||||||
[`${prefixCls}-disabled`]: disabled,
|
[`${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 (
|
return (
|
||||||
<span class={className} {...getDataAndAriaProps(this.$attrs)}>
|
<span>
|
||||||
<div
|
<div
|
||||||
class={dragCls}
|
class={dragCls}
|
||||||
onDrop={this.onFileDrop}
|
onDrop={onFileDrop}
|
||||||
onDragover={this.onFileDrop}
|
onDragover={onFileDrop}
|
||||||
onDragleave={this.onFileDrop}
|
onDragleave={onFileDrop}
|
||||||
style={style}
|
style={attrs.style}
|
||||||
>
|
>
|
||||||
<VcUpload {...vcUploadProps} class={`${prefixCls}-btn`}>
|
<VcUpload
|
||||||
<div class={`${prefixCls}-drag-container`}>{children}</div>
|
{...rcUploadProps}
|
||||||
|
ref={upload}
|
||||||
|
class={`${prefixCls.value}-btn`}
|
||||||
|
v-slots={slots}
|
||||||
|
>
|
||||||
|
<div class={`${prefixCls}-drag-container`}>{slots.default?.()}</div>
|
||||||
</VcUpload>
|
</VcUpload>
|
||||||
</div>
|
</div>
|
||||||
{uploadList}
|
{renderUploadList()}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadButtonCls = classNames(prefixCls, {
|
const uploadButtonCls = classNames(prefixCls.value, {
|
||||||
[`${prefixCls}-select`]: true,
|
[`${prefixCls.value}-select`]: true,
|
||||||
[`${prefixCls}-select-${listType}`]: true,
|
[`${prefixCls.value}-select-${listType}`]: true,
|
||||||
[`${prefixCls}-disabled`]: disabled,
|
[`${prefixCls.value}-disabled`]: disabled,
|
||||||
|
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
|
||||||
});
|
});
|
||||||
|
const children = flattenChildren(slots.default?.());
|
||||||
// Remove id to avoid open by label when trigger is hidden
|
|
||||||
// https://github.com/ant-design/ant-design/issues/14298
|
|
||||||
if (!children.length || disabled) {
|
|
||||||
delete vcUploadProps.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadButton = (
|
const uploadButton = (
|
||||||
<div class={uploadButtonCls} style={children.length ? undefined : { display: 'none' }}>
|
<div
|
||||||
<VcUpload {...vcUploadProps}>{children}</VcUpload>
|
class={uploadButtonCls}
|
||||||
|
style={children && children.length ? undefined : { display: 'none' }}
|
||||||
|
>
|
||||||
|
<VcUpload {...rcUploadProps} ref={upload} v-slots={slots} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (listType === 'picture-card') {
|
if (listType === 'picture-card') {
|
||||||
return (
|
return (
|
||||||
<span class={classNames(`${prefixCls}-picture-card-wrapper`, className)}>
|
<span class={classNames(`${prefixCls.value}-picture-card-wrapper`, attrs.class)}>
|
||||||
{uploadList}
|
{renderUploadList(uploadButton)}
|
||||||
{uploadButton}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span class={className}>
|
<span class={attrs.class}>
|
||||||
{uploadButton}
|
{uploadButton}
|
||||||
{uploadList}
|
{renderUploadList()}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,9 +25,9 @@ export const listItemProps = () => {
|
||||||
listType: String as PropType<UploadListType>,
|
listType: String as PropType<UploadListType>,
|
||||||
isImgUrl: Function as PropType<(file: UploadFile) => boolean>,
|
isImgUrl: Function as PropType<(file: UploadFile) => boolean>,
|
||||||
|
|
||||||
showRemoveIcon: Boolean,
|
showRemoveIcon: { type: Boolean, default: undefined },
|
||||||
showDownloadIcon: Boolean,
|
showDownloadIcon: { type: Boolean, default: undefined },
|
||||||
showPreviewIcon: Boolean,
|
showPreviewIcon: { type: Boolean, default: undefined },
|
||||||
removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||||
downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||||
previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
|
||||||
|
@ -76,8 +76,8 @@ export default defineComponent({
|
||||||
file,
|
file,
|
||||||
items,
|
items,
|
||||||
progress: progressProps,
|
progress: progressProps,
|
||||||
iconRender,
|
iconRender = slots.iconRender,
|
||||||
actionIconRender,
|
actionIconRender = slots.actionIconRender,
|
||||||
itemRender = slots.itemRender,
|
itemRender = slots.itemRender,
|
||||||
isImgUrl,
|
isImgUrl,
|
||||||
showPreviewIcon,
|
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
|
<a-upload
|
||||||
v-model:file-list="fileList"
|
v-model:file-list="fileList"
|
||||||
name="file"
|
name="file"
|
||||||
:multiple="true"
|
|
||||||
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
|
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
|
||||||
:headers="headers"
|
:headers="headers"
|
||||||
@change="handleChange"
|
@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">
|
<div v-if="fileList.length < 8">
|
||||||
<plus-outlined />
|
<plus-outlined />
|
||||||
<div class="ant-upload-text">Upload</div>
|
<div style="margin-top: 8px">Upload</div>
|
||||||
</div>
|
</div>
|
||||||
</a-upload>
|
</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" />
|
<img alt="example" style="width: 100%" :src="previewImage" />
|
||||||
</a-modal>
|
</a-modal>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@ After users upload picture, the thumbnail will be shown in list. The upload butt
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||||
import { defineComponent, ref } from '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) {
|
function getBase64(file: File) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
@ -52,8 +52,9 @@ export default defineComponent({
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const previewVisible = ref<boolean>(false);
|
const previewVisible = ref(false);
|
||||||
const previewImage = ref<string | undefined>('');
|
const previewImage = ref('');
|
||||||
|
const previewTitle = ref('');
|
||||||
|
|
||||||
const fileList = ref<UploadProps['fileList']>([
|
const fileList = ref<UploadProps['fileList']>([
|
||||||
{
|
{
|
||||||
|
@ -80,6 +81,13 @@ export default defineComponent({
|
||||||
status: 'done',
|
status: 'done',
|
||||||
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
|
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',
|
uid: '-5',
|
||||||
name: 'image.png',
|
name: 'image.png',
|
||||||
|
@ -89,6 +97,7 @@ export default defineComponent({
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
previewVisible.value = false;
|
previewVisible.value = false;
|
||||||
|
previewTitle.value = '';
|
||||||
};
|
};
|
||||||
const handlePreview = async (file: UploadProps['fileList'][number]) => {
|
const handlePreview = async (file: UploadProps['fileList'][number]) => {
|
||||||
if (!file.url && !file.preview) {
|
if (!file.url && !file.preview) {
|
||||||
|
@ -96,9 +105,7 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
previewImage.value = file.url || file.preview;
|
previewImage.value = file.url || file.preview;
|
||||||
previewVisible.value = true;
|
previewVisible.value = true;
|
||||||
};
|
previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1);
|
||||||
const handleChange = ({ fileList: newFileList }: UploadChangeParam) => {
|
|
||||||
fileList.value = newFileList;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -107,7 +114,7 @@ export default defineComponent({
|
||||||
fileList,
|
fileList,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
handlePreview,
|
handlePreview,
|
||||||
handleChange,
|
previewTitle,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,21 +1,18 @@
|
||||||
import type { App, Plugin } from 'vue';
|
import type { App } from 'vue';
|
||||||
import Upload from './Upload';
|
import Upload, { LIST_IGNORE } from './Upload';
|
||||||
import Dragger from './Dragger';
|
import Dragger from './Dragger';
|
||||||
|
|
||||||
export type { UploadProps, UploadListProps, UploadChangeParam } from './interface';
|
export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from './interface';
|
||||||
|
|
||||||
Upload.Dragger = Dragger;
|
|
||||||
|
|
||||||
/* istanbul ignore next */
|
/* 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(Upload.name, Upload);
|
||||||
app.component(Dragger.name, Dragger);
|
app.component(Dragger.name, Dragger);
|
||||||
return app;
|
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 type { ExtractPropTypes, PropType } from 'vue';
|
||||||
import { tuple } from '../_util/type';
|
|
||||||
import PropsTypes from '../_util/vue-types';
|
|
||||||
|
|
||||||
export const UploadFileStatus = PropsTypes.oneOf(
|
export interface RcFile extends OriRcFile {
|
||||||
tuple('error', 'success', 'done', 'uploading', 'removed'),
|
readonly lastModifiedDate: Date;
|
||||||
);
|
}
|
||||||
|
|
||||||
|
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
|
||||||
|
|
||||||
export interface HttpRequestHeader {
|
export interface HttpRequestHeader {
|
||||||
[key: string]: string;
|
[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> {
|
export interface UploadFile<T = any> {
|
||||||
uid: string;
|
uid: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
@ -28,7 +27,7 @@ export interface UploadFile<T = any> {
|
||||||
status?: UploadFileStatus;
|
status?: UploadFileStatus;
|
||||||
percent?: number;
|
percent?: number;
|
||||||
thumbUrl?: string;
|
thumbUrl?: string;
|
||||||
originFileObj?: any;
|
originFileObj?: RcFile;
|
||||||
response?: T;
|
response?: T;
|
||||||
error?: any;
|
error?: any;
|
||||||
linkProps?: any;
|
linkProps?: any;
|
||||||
|
@ -37,17 +36,23 @@ export interface UploadFile<T = any> {
|
||||||
preview?: string;
|
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;
|
file: T;
|
||||||
fileList: UploadFile[];
|
fileList: UploadFile[];
|
||||||
event?: { percent: number };
|
event?: { percent: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShowUploadListInterface = PropsTypes.shape({
|
|
||||||
showRemoveIcon: PropsTypes.looseBool,
|
|
||||||
showPreviewIcon: PropsTypes.looseBool,
|
|
||||||
}).loose;
|
|
||||||
|
|
||||||
export interface UploadLocale {
|
export interface UploadLocale {
|
||||||
uploading?: string;
|
uploading?: string;
|
||||||
removeFile?: string;
|
removeFile?: string;
|
||||||
|
@ -56,61 +61,120 @@ export interface UploadLocale {
|
||||||
previewFile?: string;
|
previewFile?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const uploadProps = {
|
export type UploadType = 'drag' | 'select';
|
||||||
type: PropsTypes.oneOf(tuple('drag', 'select')),
|
export type UploadListType = 'text' | 'picture' | 'picture-card';
|
||||||
name: PropsTypes.string,
|
export type UploadListProgressProps = Omit<ProgressProps, 'percent' | 'type'>;
|
||||||
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 type ItemRender<T = any> = (opt: {
|
||||||
export const uploadListProps = {
|
originNode: VueNode;
|
||||||
listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')),
|
file: UploadFile;
|
||||||
// items: PropsTypes.arrayOf(UploadFile),
|
fileList: Array<UploadFile<T>>;
|
||||||
items: { type: Array as PropType<UploadFile[]> },
|
actions: {
|
||||||
progressAttr: PropsTypes.object,
|
download: () => void;
|
||||||
prefixCls: PropsTypes.string,
|
preview: () => void;
|
||||||
showRemoveIcon: PropsTypes.looseBool,
|
remove: () => void;
|
||||||
showDownloadIcon: PropsTypes.looseBool,
|
};
|
||||||
showPreviewIcon: PropsTypes.looseBool,
|
}) => VueNode;
|
||||||
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>>;
|
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 {
|
&&-select-picture-card {
|
||||||
display: table;
|
|
||||||
float: left;
|
|
||||||
width: @upload-picture-card-size;
|
width: @upload-picture-card-size;
|
||||||
height: @upload-picture-card-size;
|
height: @upload-picture-card-size;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
@ -46,19 +44,21 @@
|
||||||
border: @border-width-base dashed @border-color-base;
|
border: @border-width-base dashed @border-color-base;
|
||||||
border-radius: @border-radius-base;
|
border-radius: @border-radius-base;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.3s ease;
|
transition: border-color 0.3s;
|
||||||
|
|
||||||
> .@{upload-prefix-cls} {
|
> .@{upload-prefix-cls} {
|
||||||
display: table-cell;
|
display: flex;
|
||||||
width: 100%;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 8px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
vertical-align: middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: @primary-color;
|
border-color: @primary-color;
|
||||||
|
.@{upload-prefix-cls}-disabled& {
|
||||||
|
border-color: @border-color-base;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
transition: border-color 0.3s;
|
transition: border-color 0.3s;
|
||||||
|
|
||||||
.@{upload-prefix-cls} {
|
.@{upload-prefix-cls} {
|
||||||
padding: 16px 0;
|
padding: @padding-md 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
|
&.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
|
||||||
|
@ -116,10 +116,12 @@
|
||||||
color: @text-color-secondary;
|
color: @text-color-secondary;
|
||||||
font-size: @font-size-base;
|
font-size: @font-size-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{iconfont-css-prefix}-plus {
|
.@{iconfont-css-prefix}-plus {
|
||||||
color: @disabled-color;
|
color: @disabled-color;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: @text-color-secondary;
|
color: @text-color-secondary;
|
||||||
}
|
}
|
||||||
|
@ -140,52 +142,55 @@
|
||||||
.@{upload-prefix-cls}-list {
|
.@{upload-prefix-cls}-list {
|
||||||
.reset-component();
|
.reset-component();
|
||||||
.clearfix();
|
.clearfix();
|
||||||
&-item-list-type-text {
|
line-height: @line-height-base;
|
||||||
&:hover {
|
|
||||||
.@{upload-prefix-cls}-list-item-name-icon-count-1 {
|
// ============================ Item ============================
|
||||||
padding-right: 14px;
|
|
||||||
}
|
|
||||||
.@{upload-prefix-cls}-list-item-name-icon-count-2 {
|
|
||||||
padding-right: 28px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&-item {
|
&-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 22px;
|
height: @line-height-base * @font-size-base;
|
||||||
margin-top: 8px;
|
margin-top: @margin-xs;
|
||||||
font-size: @font-size-base;
|
font-size: @font-size-base;
|
||||||
|
|
||||||
&-name {
|
&-name {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-left: @font-size-base + 8px;
|
padding-left: @font-size-base + 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
line-height: @line-height-base;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-name-icon-count-1 {
|
|
||||||
padding-right: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-card-actions {
|
&-card-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|
||||||
|
&-btn {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
&.picture {
|
}
|
||||||
top: 25px;
|
&-btn.@{ant-prefix}-btn-sm {
|
||||||
|
height: 20px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.picture {
|
||||||
|
top: 22px;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-btn:focus,
|
||||||
|
&.picture &-btn {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
.anticon {
|
|
||||||
padding-right: 6px;
|
.@{iconfont-css-prefix} {
|
||||||
color: rgba(0, 0, 0, 0.45);
|
color: @upload-actions-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-info {
|
&-info {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0 12px 0 4px;
|
padding: 0 4px;
|
||||||
transition: background-color 0.3s;
|
transition: background-color 0.3s;
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
|
@ -195,25 +200,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{iconfont-css-prefix}-loading,
|
.@{iconfont-css-prefix}-loading,
|
||||||
.@{iconfont-css-prefix}-paper-clip {
|
.@{upload-prefix-cls}-text-icon {
|
||||||
|
.@{iconfont-css-prefix} {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: (@font-size-base / 2) - 2px;
|
top: (@font-size-base / 2) - 2px;
|
||||||
color: @text-color-secondary;
|
color: @text-color-secondary;
|
||||||
font-size: @font-size-base;
|
font-size: @font-size-base;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.@{iconfont-css-prefix}-close {
|
.@{iconfont-css-prefix}-close {
|
||||||
.iconfont-size-under-12px(10px);
|
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 6px;
|
top: 6px;
|
||||||
right: 4px;
|
right: 4px;
|
||||||
color: @text-color-secondary;
|
color: @text-color-secondary;
|
||||||
|
font-size: 10px;
|
||||||
line-height: 0;
|
line-height: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: @text-color;
|
color: @text-color;
|
||||||
}
|
}
|
||||||
|
@ -227,22 +234,25 @@
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover &-card-actions {
|
&:hover &-card-actions-btn {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-error,
|
&-error,
|
||||||
&-error .@{iconfont-css-prefix}-paper-clip,
|
&-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix},
|
||||||
&-error &-name {
|
&-error &-name {
|
||||||
color: @error-color;
|
color: @error-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-error &-card-actions {
|
&-error &-card-actions {
|
||||||
.anticon {
|
.@{iconfont-css-prefix} {
|
||||||
color: @error-color;
|
color: @error-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&-btn {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&-progress {
|
&-progress {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -254,17 +264,20 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =================== Picture & Picture Card ===================
|
||||||
&-picture,
|
&-picture,
|
||||||
&-picture-card {
|
&-picture-card {
|
||||||
.@{upload-item} {
|
.@{upload-item} {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 66px;
|
height: 66px;
|
||||||
padding: 8px;
|
padding: @padding-xs;
|
||||||
border: @border-width-base @upload-picture-card-border-style @border-color-base;
|
border: @border-width-base @upload-picture-card-border-style @border-color-base;
|
||||||
border-radius: @border-radius-base;
|
border-radius: @border-radius-base;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-error {
|
&-error {
|
||||||
border-color: @error-color;
|
border-color: @error-color;
|
||||||
}
|
}
|
||||||
|
@ -283,15 +296,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-thumbnail {
|
.@{upload-item}-thumbnail {
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
font-size: 26px;
|
line-height: 60px;
|
||||||
line-height: 54px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.8;
|
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 {
|
.@{upload-item}-icon {
|
||||||
|
@ -300,6 +328,10 @@
|
||||||
left: 50%;
|
left: 50%;
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
|
|
||||||
|
.@{iconfont-css-prefix} {
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-image {
|
.@{upload-item}-image {
|
||||||
|
@ -327,16 +359,8 @@
|
||||||
transition: all 0.3s;
|
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 {
|
.@{upload-item}-uploading .@{upload-item}-name {
|
||||||
line-height: 28px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-progress {
|
.@{upload-item}-progress {
|
||||||
|
@ -355,21 +379,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== Picture Card ========================
|
||||||
&-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 {
|
&.@{upload-prefix-cls}-list::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
&-container {
|
|
||||||
float: left;
|
|
||||||
width: @upload-picture-card-size;
|
|
||||||
height: @upload-picture-card-size;
|
|
||||||
margin: 0 8px 8px 0;
|
|
||||||
}
|
|
||||||
.@{upload-item} {
|
.@{upload-item} {
|
||||||
float: left;
|
height: 100%;
|
||||||
width: @upload-picture-card-size;
|
margin: 0;
|
||||||
height: @upload-picture-card-size;
|
|
||||||
margin: 0 8px 8px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-info {
|
.@{upload-item}-info {
|
||||||
|
@ -413,6 +439,7 @@
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: @text-color-inverse;
|
color: @text-color-inverse;
|
||||||
}
|
}
|
||||||
|
@ -430,7 +457,7 @@
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-name {
|
.@{upload-item}-name {
|
||||||
|
@ -441,7 +468,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.anticon-picture + .@{upload-item}-name {
|
.@{upload-item}-file + .@{upload-item}-name {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -454,46 +481,82 @@
|
||||||
|
|
||||||
.@{upload-item}-info {
|
.@{upload-item}-info {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
||||||
&::before,
|
&::before,
|
||||||
.@{iconfont-css-prefix}-eye-o,
|
.@{iconfont-css-prefix}-eye,
|
||||||
.@{iconfont-css-prefix}-delete {
|
.@{iconfont-css-prefix}-delete {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-text {
|
|
||||||
margin-top: 18px;
|
|
||||||
color: @text-color-secondary;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-item}-progress {
|
.@{upload-item}-progress {
|
||||||
bottom: 32px;
|
bottom: 32px;
|
||||||
|
width: calc(100% - 14px);
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-prefix-cls}-success-icon {
|
// ======================= Picture & Text =======================
|
||||||
color: @success-color;
|
&-text,
|
||||||
font-weight: bold;
|
&-picture {
|
||||||
|
&-container {
|
||||||
|
transition: opacity @animation-duration-slow, height @animation-duration-slow;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: table;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
content: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-prefix-cls}-animate-enter,
|
// Don't know why span here, just stretch it
|
||||||
.@{upload-prefix-cls}-animate-leave,
|
.@{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-enter,
|
||||||
.@{upload-prefix-cls}-animate-inline-leave {
|
.@{upload-prefix-cls}-animate-inline-leave {
|
||||||
animation-duration: 0.3s;
|
animation-duration: @animation-duration-slow;
|
||||||
animation-fill-mode: @ease-in-out-circ;
|
animation-fill-mode: @ease-in-out-circ;
|
||||||
}
|
}
|
||||||
|
|
||||||
.@{upload-prefix-cls}-animate-enter {
|
.@{upload-prefix-cls}-animate-inline-appear,
|
||||||
animation-name: uploadAnimateIn;
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{upload-prefix-cls}-animate-leave {
|
|
||||||
animation-name: uploadAnimateOut;
|
|
||||||
}
|
|
||||||
|
|
||||||
.@{upload-prefix-cls}-animate-inline-enter {
|
.@{upload-prefix-cls}-animate-inline-enter {
|
||||||
animation-name: uploadAnimateInlineIn;
|
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 {
|
@keyframes uploadAnimateInlineIn {
|
||||||
from {
|
from {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
@ -540,3 +585,5 @@
|
||||||
opacity: 0;
|
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
|
// rc-upload 4.3.3
|
||||||
import Upload from './Upload';
|
import Upload from './Upload';
|
||||||
import { UploadProps } from './interface';
|
import type { UploadProps } from './interface';
|
||||||
|
|
||||||
export { UploadProps };
|
export type { UploadProps };
|
||||||
|
|
||||||
export default Upload;
|
export default Upload;
|
||||||
|
|
|
@ -7,19 +7,19 @@ export type Action = string | ((file: RcFile) => string | PromiseLike<string>);
|
||||||
export const uploadProps = () => {
|
export const uploadProps = () => {
|
||||||
return {
|
return {
|
||||||
capture: [Boolean, String] as PropType<boolean | 'user' | 'environment'>,
|
capture: [Boolean, String] as PropType<boolean | 'user' | 'environment'>,
|
||||||
multipart: Boolean,
|
multipart: { type: Boolean, default: undefined },
|
||||||
name: String,
|
name: String,
|
||||||
disabled: Boolean,
|
disabled: { type: Boolean, default: undefined },
|
||||||
componentTag: String as PropType<any>,
|
componentTag: String as PropType<any>,
|
||||||
action: [String, Function] as PropType<Action>,
|
action: [String, Function] as PropType<Action>,
|
||||||
method: String as PropType<UploadRequestMethod>,
|
method: String as PropType<UploadRequestMethod>,
|
||||||
directory: Boolean,
|
directory: { type: Boolean, default: undefined },
|
||||||
data: [Object, Function] as PropType<
|
data: [Object, Function] as PropType<
|
||||||
Record<string, unknown> | ((file: RcFile | string | Blob) => Record<string, unknown>)
|
Record<string, unknown> | ((file: RcFile | string | Blob) => Record<string, unknown>)
|
||||||
>,
|
>,
|
||||||
headers: Object as PropType<UploadRequestHeader>,
|
headers: Object as PropType<UploadRequestHeader>,
|
||||||
accept: String,
|
accept: String,
|
||||||
multiple: Boolean,
|
multiple: { type: Boolean, default: undefined },
|
||||||
onBatchStart: Function as PropType<
|
onBatchStart: Function as PropType<
|
||||||
(fileList: { file: RcFile; parsedFile: Exclude<BeforeUploadFileType, boolean> }[]) => void
|
(fileList: { file: RcFile; parsedFile: Exclude<BeforeUploadFileType, boolean> }[]) => void
|
||||||
>,
|
>,
|
||||||
|
@ -38,8 +38,8 @@ export const uploadProps = () => {
|
||||||
) => BeforeUploadFileType | Promise<void | BeforeUploadFileType>
|
) => BeforeUploadFileType | Promise<void | BeforeUploadFileType>
|
||||||
>,
|
>,
|
||||||
customRequest: Function as PropType<(option: UploadRequestOption) => void>,
|
customRequest: Function as PropType<(option: UploadRequestOption) => void>,
|
||||||
withCredentials: Boolean,
|
withCredentials: { type: Boolean, default: undefined },
|
||||||
openFileDialogOnClick: Boolean,
|
openFileDialogOnClick: { type: Boolean, default: undefined },
|
||||||
prefixCls: String,
|
prefixCls: String,
|
||||||
id: String,
|
id: String,
|
||||||
onMouseenter: Function as PropType<(e: MouseEvent) => void>,
|
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
|
// 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';
|
// import Demo from './demo/demo.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
Loading…
Reference in New Issue