refactor: upload

refactor-upload
tangjinzhou 2022-02-22 18:20:19 +08:00
parent 67f5226cdc
commit f0f970c37b
58 changed files with 1183 additions and 2000 deletions

View File

@ -224,7 +224,7 @@ export {
TypographyTitle,
} from './typography';
export type { UploadProps, UploadListProps, UploadChangeParam } from './upload';
export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from './upload';
export { default as Upload, UploadDragger } from './upload';

View File

@ -9,6 +9,7 @@ import type { TransferLocale } from '../transfer';
import type { PickerLocale as DatePickerLocale } from '../date-picker/generatePicker';
import type { PaginationLocale } from '../pagination/Pagination';
import type { TableLocale } from '../table/interface';
import type { UploadLocale } from '../upload/interface';
interface TransferLocaleForEmpty {
description: string;
@ -18,7 +19,6 @@ export interface Locale {
Pagination?: PaginationLocale;
Table?: TableLocale;
Popconfirm?: Record<string, any>;
Upload?: Record<string, any>;
Form?: {
optional?: string;
defaultValidateMessages: ValidateMessages;
@ -32,6 +32,7 @@ export interface Locale {
Modal?: ModalLocale;
Transfer?: Partial<TransferLocale>;
Select?: Record<string, any>;
Upload?: UploadLocale;
Empty?: TransferLocaleForEmpty;
global?: Record<string, any>;
PageHeader?: { back: string };

View File

@ -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>;
};
},
});

View File

@ -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;

View File

@ -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;
},
});

View File

@ -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 };

View File

@ -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>;
},
});

View File

@ -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;
};

View File

@ -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>>;

View File

@ -34,6 +34,8 @@
}
&&-select-picture-card {
display: table;
float: left;
width: @upload-picture-card-size;
height: @upload-picture-card-size;
margin-right: 8px;
@ -44,21 +46,19 @@
border: @border-width-base dashed @border-color-base;
border-radius: @border-radius-base;
cursor: pointer;
transition: border-color 0.3s;
transition: border-color 0.3s ease;
> .@{upload-prefix-cls} {
display: flex;
align-items: center;
justify-content: center;
display: table-cell;
width: 100%;
height: 100%;
padding: 8px;
text-align: center;
vertical-align: middle;
}
&:hover {
border-color: @primary-color;
.@{upload-prefix-cls}-disabled& {
border-color: @border-color-base;
}
}
}
@ -74,7 +74,7 @@
transition: border-color 0.3s;
.@{upload-prefix-cls} {
padding: @padding-md 0;
padding: 16px 0;
}
&.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
@ -116,12 +116,10 @@
color: @text-color-secondary;
font-size: @font-size-base;
}
.@{iconfont-css-prefix}-plus {
color: @disabled-color;
font-size: 30px;
transition: all 0.3s;
&:hover {
color: @text-color-secondary;
}
@ -142,55 +140,52 @@
.@{upload-prefix-cls}-list {
.reset-component();
.clearfix();
line-height: @line-height-base;
// ============================ Item ============================
&-item-list-type-text {
&:hover {
.@{upload-prefix-cls}-list-item-name-icon-count-1 {
padding-right: 14px;
}
.@{upload-prefix-cls}-list-item-name-icon-count-2 {
padding-right: 28px;
}
}
}
&-item {
position: relative;
height: @line-height-base * @font-size-base;
margin-top: @margin-xs;
height: 22px;
margin-top: 8px;
font-size: @font-size-base;
&-name {
display: inline-block;
width: 100%;
padding-left: @font-size-base + 8px;
overflow: hidden;
line-height: @line-height-base;
white-space: nowrap;
text-overflow: ellipsis;
}
&-name-icon-count-1 {
padding-right: 14px;
}
&-card-actions {
position: absolute;
right: 0;
&-btn {
opacity: 0;
}
&-btn.@{ant-prefix}-btn-sm {
height: 20px;
line-height: 1;
}
opacity: 0;
&.picture {
top: 22px;
line-height: 0;
}
&-btn:focus,
&.picture &-btn {
top: 25px;
line-height: 1;
opacity: 1;
}
.@{iconfont-css-prefix} {
color: @upload-actions-color;
.anticon {
padding-right: 6px;
color: rgba(0, 0, 0, 0.45);
}
}
&-info {
height: 100%;
padding: 0 4px;
padding: 0 12px 0 4px;
transition: background-color 0.3s;
> span {
@ -200,27 +195,25 @@
}
.@{iconfont-css-prefix}-loading,
.@{upload-prefix-cls}-text-icon {
.@{iconfont-css-prefix} {
position: absolute;
top: (@font-size-base / 2) - 2px;
color: @text-color-secondary;
font-size: @font-size-base;
}
.@{iconfont-css-prefix}-paper-clip {
position: absolute;
top: (@font-size-base / 2) - 2px;
color: @text-color-secondary;
font-size: @font-size-base;
}
}
.@{iconfont-css-prefix}-close {
.iconfont-size-under-12px(10px);
position: absolute;
top: 6px;
right: 4px;
color: @text-color-secondary;
font-size: 10px;
line-height: 0;
cursor: pointer;
opacity: 0;
transition: all 0.3s;
&:hover {
color: @text-color;
}
@ -234,24 +227,21 @@
opacity: 1;
}
&:hover &-card-actions-btn {
&:hover &-card-actions {
opacity: 1;
}
&-error,
&-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix},
&-error .@{iconfont-css-prefix}-paper-clip,
&-error &-name {
color: @error-color;
}
&-error &-card-actions {
.@{iconfont-css-prefix} {
.anticon {
color: @error-color;
}
&-btn {
opacity: 1;
}
opacity: 1;
}
&-progress {
@ -264,20 +254,17 @@
}
}
// =================== Picture & Picture Card ===================
&-picture,
&-picture-card {
.@{upload-item} {
position: relative;
height: 66px;
padding: @padding-xs;
padding: 8px;
border: @border-width-base @upload-picture-card-border-style @border-color-base;
border-radius: @border-radius-base;
&:hover {
background: transparent;
}
&-error {
border-color: @error-color;
}
@ -296,30 +283,15 @@
}
.@{upload-item}-thumbnail {
position: absolute;
top: 8px;
left: 8px;
width: 48px;
height: 48px;
line-height: 60px;
font-size: 26px;
line-height: 54px;
text-align: center;
opacity: 0.8;
.@{iconfont-css-prefix} {
font-size: 26px;
}
}
// Adjust the color of the error icon : https://github.com/ant-design/ant-design/pull/24160
.@{upload-item}-error .@{upload-item}-thumbnail {
.@{iconfont-css-prefix} {
svg path {
&[fill='#e6f7ff'] {
fill: @error-color-deprecated-bg;
}
&[fill='#1890ff'] {
fill: @error-color;
}
}
}
}
.@{upload-item}-icon {
@ -328,10 +300,6 @@
left: 50%;
font-size: 26px;
transform: translate(-50%, -50%);
.@{iconfont-css-prefix} {
font-size: 26px;
}
}
.@{upload-item}-image {
@ -359,8 +327,16 @@
transition: all 0.3s;
}
.@{upload-item}-name-icon-count-1 {
padding-right: 18px;
}
.@{upload-item}-name-icon-count-2 {
padding-right: 36px;
}
.@{upload-item}-uploading .@{upload-item}-name {
margin-bottom: 12px;
line-height: 28px;
}
.@{upload-item}-progress {
@ -379,23 +355,21 @@
}
}
// ======================== Picture Card ========================
&-picture-card {
&-container {
display: inline-block;
width: @upload-picture-card-size;
height: @upload-picture-card-size;
margin: 0 @margin-xs @margin-xs 0;
vertical-align: top;
}
&.@{upload-prefix-cls}-list::after {
display: none;
}
&-container {
float: left;
width: @upload-picture-card-size;
height: @upload-picture-card-size;
margin: 0 8px 8px 0;
}
.@{upload-item} {
height: 100%;
margin: 0;
float: left;
width: @upload-picture-card-size;
height: @upload-picture-card-size;
margin: 0 8px 8px 0;
}
.@{upload-item}-info {
@ -439,7 +413,6 @@
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: @text-color-inverse;
}
@ -457,7 +430,7 @@
display: block;
width: 100%;
height: 100%;
object-fit: contain;
object-fit: cover;
}
.@{upload-item}-name {
@ -468,7 +441,7 @@
text-align: center;
}
.@{upload-item}-file + .@{upload-item}-name {
.anticon-picture + .@{upload-item}-name {
position: absolute;
bottom: 10px;
display: block;
@ -481,82 +454,46 @@
.@{upload-item}-info {
height: auto;
&::before,
.@{iconfont-css-prefix}-eye,
.@{iconfont-css-prefix}-eye-o,
.@{iconfont-css-prefix}-delete {
display: none;
}
}
&-text {
margin-top: 18px;
color: @text-color-secondary;
}
}
.@{upload-item}-progress {
bottom: 32px;
width: calc(100% - 14px);
padding-left: 0;
}
}
// ======================= Picture & Text =======================
&-text,
&-picture {
&-container {
transition: opacity @animation-duration-slow, height @animation-duration-slow;
&::before {
display: table;
width: 0;
height: 0;
content: '';
}
// Don't know why span here, just stretch it
.@{upload-prefix-cls}-span {
display: block;
flex: auto;
}
}
// text & picture no need this additional element.
// But it used for picture-card, let's keep it.
.@{upload-prefix-cls}-span {
display: flex;
align-items: center;
> * {
flex: none;
}
}
.@{upload-item}-name {
flex: auto;
margin: 0;
padding: 0 @padding-xs;
}
.@{upload-item}-card-actions {
position: static;
}
.@{upload-prefix-cls}-success-icon {
color: @success-color;
font-weight: bold;
}
// ============================ Text ============================
&-text {
.@{upload-prefix-cls}-text-icon {
.@{iconfont-css-prefix} {
position: static;
}
}
}
// =========================== Motion ===========================
.@{upload-prefix-cls}-animate-inline-appear,
.@{upload-prefix-cls}-animate-enter,
.@{upload-prefix-cls}-animate-leave,
.@{upload-prefix-cls}-animate-inline-enter,
.@{upload-prefix-cls}-animate-inline-leave {
animation-duration: @animation-duration-slow;
animation-duration: 0.3s;
animation-fill-mode: @ease-in-out-circ;
}
.@{upload-prefix-cls}-animate-inline-appear,
.@{upload-prefix-cls}-animate-enter {
animation-name: uploadAnimateIn;
}
.@{upload-prefix-cls}-animate-leave {
animation-name: uploadAnimateOut;
}
.@{upload-prefix-cls}-animate-inline-enter {
animation-name: uploadAnimateInlineIn;
}
@ -566,6 +503,24 @@
}
}
@keyframes uploadAnimateIn {
from {
height: 0;
margin: 0;
padding: 0;
opacity: 0;
}
}
@keyframes uploadAnimateOut {
to {
height: 0;
margin: 0;
padding: 0;
opacity: 0;
}
}
@keyframes uploadAnimateInlineIn {
from {
width: 0;
@ -585,5 +540,3 @@
opacity: 0;
}
}
@import './rtl';

View File

@ -1,22 +1,22 @@
import { defineComponent } from 'vue';
import { getOptionProps, getSlot } from '../_util/props-util';
import Upload from './Upload';
import { uploadProps } from './interface';
export default defineComponent({
name: 'AUploadDragger',
inheritAttrs: false,
props: uploadProps,
render() {
const props = getOptionProps(this);
const { height, ...restProps } = props;
const { style, ...restAttrs } = this.$attrs;
const draggerProps = {
...restProps,
...restAttrs,
type: 'drag',
style: { ...(style as any), height },
} as any;
return <Upload {...draggerProps}>{getSlot(this)}</Upload>;
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>;
};
},
});

View File

@ -1,86 +1,191 @@
import classNames from '../_util/classNames';
import uniqBy from 'lodash-es/uniqBy';
import findIndex from 'lodash-es/findIndex';
import type { UploadProps as RcUploadProps } from '../vc-upload';
import VcUpload from '../vc-upload';
import BaseMixin from '../_util/BaseMixin';
import { getOptionProps, hasProp, getSlot } from '../_util/props-util';
import initDefaultProps from '../_util/props-util/initDefaultProps';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale-provider/default';
import { defaultConfigProvider } from '../config-provider';
import Dragger from './Dragger';
import UploadList from './UploadList';
import type { UploadFile } from './interface';
import type {
UploadType,
UploadListType,
RcFile,
UploadFile,
UploadChangeParam,
ShowUploadListInterface,
} from './interface';
import { uploadProps } from './interface';
import { T, fileToObject, genPercentAdd, getFileItem, removeFileItem } from './utils';
import { defineComponent, inject } from 'vue';
import { getDataAndAriaProps } from '../_util/util';
import { useInjectFormItemContext } from '../form/FormItemContext';
import { file2Obj, getFileItem, removeFileItem, updateFileList } from './utils';
import { useLocaleReceiver } from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale/default';
import { computed, defineComponent, onMounted, ref, toRef } from 'vue';
import { flattenChildren, initDefaultProps } from '../_util/props-util';
import useMergedState from '../_util/hooks/useMergedState';
import devWarning from '../vc-util/devWarning';
import useConfigInject from '../_util/hooks/useConfigInject';
import type { VueNode } from '../_util/type';
import classNames from '../_util/classNames';
import { useInjectFormItemContext } from '../form';
export const LIST_IGNORE = `__LIST_IGNORE_${Date.now()}__`;
export default defineComponent({
name: 'AUpload',
mixins: [BaseMixin],
inheritAttrs: false,
Dragger,
props: initDefaultProps(uploadProps, {
type: 'select',
props: initDefaultProps(uploadProps(), {
type: 'select' as UploadType,
multiple: false,
action: '',
data: {},
accept: '',
beforeUpload: T,
showUploadList: true,
listType: 'text', // or pictrue
listType: 'text' as UploadListType, // or picture
disabled: false,
supportServerRender: true,
}),
setup() {
setup(props, { slots, attrs, expose }) {
const formItemContext = useInjectFormItemContext();
return {
upload: null,
progressTimer: null,
configProvider: inject('configProvider', defaultConfigProvider),
formItemContext,
};
},
// recentUploadStatus: boolean | PromiseLike<any>;
data() {
return {
sFileList: this.fileList || this.defaultFileList || [],
dragState: 'drop',
};
},
watch: {
fileList(val) {
this.sFileList = val || [];
},
},
beforeUnmount() {
this.clearProgressTimer();
},
methods: {
onStart(file) {
const targetItem = fileToObject(file);
targetItem.status = 'uploading';
const nextFileList = this.sFileList.concat();
const fileIndex = findIndex(nextFileList, ({ uid }) => uid === targetItem.uid);
if (fileIndex === -1) {
nextFileList.push(targetItem);
} else {
nextFileList[fileIndex] = targetItem;
}
this.handleChange({
file: targetItem,
fileList: nextFileList,
});
// fix ie progress
if (!window.File || (typeof process === 'object' && process.env.TEST_IE)) {
this.autoUpdateProgress(0, targetItem);
}
},
const [mergedFileList, setMergedFileList] = useMergedState(props.defaultFileList || [], {
value: toRef(props, 'fileList'),
postState: list => {
const timestamp = Date.now();
return (list ?? []).map((file, index) => {
if (!file.uid && !Object.isFrozen(file)) {
file.uid = `__AUTO__${timestamp}_${index}__`;
}
return file;
});
},
});
const dragState = ref('drop');
onSuccess(response, file, xhr) {
this.clearProgressTimer();
const upload = ref();
onMounted(() => {
devWarning(
'fileList' in props || !('value' in props),
'Upload',
'`value` is not a valid prop, do you mean `fileList`?',
);
devWarning(
!('transformFile' in props),
'Upload',
'`transformFile` is deprecated. Please use `beforeUpload` directly.',
);
});
const onInternalChange = (
file: UploadFile,
changedFileList: UploadFile[],
event?: { percent: number },
) => {
let cloneList = [...changedFileList];
// Cut to match count
if (props.maxCount === 1) {
cloneList = cloneList.slice(-1);
} else if (props.maxCount) {
cloneList = cloneList.slice(0, props.maxCount);
}
setMergedFileList(cloneList);
const changeInfo: UploadChangeParam<UploadFile> = {
file: file as UploadFile,
fileList: cloneList,
};
if (event) {
changeInfo.event = event;
}
props['onUpdate:fileList']?.(changeInfo.fileList);
props.onChange?.(changeInfo);
formItemContext.onFieldChange();
};
const mergedBeforeUpload = async (file: RcFile, fileListArgs: RcFile[]) => {
const { beforeUpload, transformFile } = props;
let parsedFile: File | Blob | string = file;
if (beforeUpload) {
const result = await beforeUpload(file, fileListArgs);
if (result === false) {
return false;
}
// Hack for LIST_IGNORE, we add additional info to remove from the list
delete (file as any)[LIST_IGNORE];
if ((result as any) === LIST_IGNORE) {
Object.defineProperty(file, LIST_IGNORE, {
value: true,
configurable: true,
});
return false;
}
if (typeof result === 'object' && result) {
parsedFile = result as File;
}
}
if (transformFile) {
parsedFile = await transformFile(parsedFile as any);
}
return parsedFile as RcFile;
};
const onBatchStart: RcUploadProps['onBatchStart'] = batchFileInfoList => {
// Skip file which marked as `LIST_IGNORE`, these file will not add to file list
const filteredFileInfoList = batchFileInfoList.filter(
info => !(info.file as any)[LIST_IGNORE],
);
// Nothing to do since no file need upload
if (!filteredFileInfoList.length) {
return;
}
const objectFileList = filteredFileInfoList.map(info => file2Obj(info.file as RcFile));
// Concat new files with prev files
let newFileList = [...mergedFileList.value];
objectFileList.forEach(fileObj => {
// Replace file if exist
newFileList = updateFileList(fileObj, newFileList);
});
objectFileList.forEach((fileObj, index) => {
// Repeat trigger `onChange` event for compatible
let triggerFileObj: UploadFile = fileObj;
if (!filteredFileInfoList[index].parsedFile) {
// `beforeUpload` return false
const { originFileObj } = fileObj;
let clone;
try {
clone = new File([originFileObj], originFileObj.name, {
type: originFileObj.type,
}) as any as UploadFile;
} catch (e) {
clone = new Blob([originFileObj], {
type: originFileObj.type,
}) as any as UploadFile;
clone.name = originFileObj.name;
clone.lastModifiedDate = new Date();
clone.lastModified = new Date().getTime();
}
clone.uid = fileObj.uid;
triggerFileObj = clone;
} else {
// Inject `uploading` status
fileObj.status = 'uploading';
}
onInternalChange(triggerFileObj, newFileList);
});
};
const onSuccess = (response: any, file: RcFile, xhr: any) => {
try {
if (typeof response === 'string') {
response = JSON.parse(response);
@ -88,255 +193,231 @@ export default defineComponent({
} catch (e) {
/* do nothing */
}
const fileList = this.sFileList;
const targetItem = getFileItem(file, fileList);
// removed
if (!targetItem) {
if (!getFileItem(file, mergedFileList.value)) {
return;
}
const targetItem = file2Obj(file);
targetItem.status = 'done';
targetItem.percent = 100;
targetItem.response = response;
targetItem.xhr = xhr;
this.handleChange({
file: { ...targetItem },
fileList,
});
},
onProgress(e, file) {
const fileList = this.sFileList;
const targetItem = getFileItem(file, fileList);
const nextFileList = updateFileList(targetItem, mergedFileList.value);
onInternalChange(targetItem, nextFileList);
};
const onProgress = (e: { percent: number }, file: RcFile) => {
// removed
if (!targetItem) {
if (!getFileItem(file, mergedFileList.value)) {
return;
}
const targetItem = file2Obj(file);
targetItem.status = 'uploading';
targetItem.percent = e.percent;
this.handleChange({
event: e,
file: { ...targetItem },
fileList: this.sFileList,
});
},
onError(error, response, file) {
this.clearProgressTimer();
const fileList = this.sFileList;
const targetItem = getFileItem(file, fileList);
const nextFileList = updateFileList(targetItem, mergedFileList.value);
onInternalChange(targetItem, nextFileList, e);
};
const onError = (error: Error, response: any, file: RcFile) => {
// removed
if (!targetItem) {
if (!getFileItem(file, mergedFileList.value)) {
return;
}
const targetItem = file2Obj(file);
targetItem.error = error;
targetItem.response = response;
targetItem.status = 'error';
this.handleChange({
file: { ...targetItem },
fileList,
});
},
onReject(fileList) {
this.$emit('reject', fileList);
},
handleRemove(file) {
const { remove: onRemove } = this;
const { sFileList: fileList } = this.$data;
Promise.resolve(typeof onRemove === 'function' ? onRemove(file) : onRemove).then(ret => {
const nextFileList = updateFileList(targetItem, mergedFileList.value);
onInternalChange(targetItem, nextFileList);
};
const handleRemove = (file: UploadFile) => {
let currentFile: UploadFile;
Promise.resolve(
typeof props.onRemove === 'function' ? props.onRemove(file) : props.onRemove,
).then(ret => {
// Prevent removing file
if (ret === false) {
return;
}
const removedFileList = removeFileItem(file, fileList);
const removedFileList = removeFileItem(file, mergedFileList.value);
if (removedFileList) {
file.status = 'removed'; // eslint-disable-line
if (this.upload) {
this.upload.abort(file);
}
this.handleChange({
file,
fileList: removedFileList,
currentFile = { ...file, status: 'removed' };
mergedFileList.value?.forEach(item => {
const matchKey = currentFile.uid !== undefined ? 'uid' : 'name';
if (item[matchKey] === currentFile[matchKey] && !Object.isFrozen(item)) {
item.status = 'removed';
}
});
upload.value?.abort(currentFile);
onInternalChange(currentFile, removedFileList);
}
});
},
handleManualRemove(file) {
if (this.$refs.uploadRef) {
(this.$refs.uploadRef as any).abort(file);
}
this.handleRemove(file);
},
handleChange(info) {
if (!hasProp(this, 'fileList')) {
this.setState({ sFileList: info.fileList });
}
this.$emit('update:fileList', info.fileList);
this.$emit('change', info);
this.formItemContext.onFieldChange();
},
onFileDrop(e) {
this.setState({
dragState: e.type,
});
},
reBeforeUpload(file, fileList) {
const { beforeUpload } = this.$props;
const { sFileList: stateFileList } = this.$data;
if (!beforeUpload) {
return true;
}
const result = beforeUpload(file, fileList);
if (result === false) {
this.handleChange({
file,
fileList: uniqBy(
stateFileList.concat(fileList.map(fileToObject)),
(item: UploadFile) => item.uid,
),
});
return false;
}
if (result && result.then) {
return result;
}
return true;
},
clearProgressTimer() {
clearInterval(this.progressTimer);
},
autoUpdateProgress(_, file) {
const getPercent = genPercentAdd();
let curPercent = 0;
this.clearProgressTimer();
this.progressTimer = setInterval(() => {
curPercent = getPercent(curPercent);
this.onProgress(
{
percent: curPercent * 100,
},
file,
);
}, 200);
},
renderUploadList(locale) {
const {
showUploadList = {},
listType,
previewFile,
disabled,
locale: propLocale,
} = getOptionProps(this);
const { showRemoveIcon, showPreviewIcon, showDownloadIcon } = showUploadList;
const { sFileList: fileList } = this.$data;
const { onDownload, onPreview } = this.$props;
const uploadListProps = {
listType,
items: fileList,
previewFile,
showRemoveIcon: !disabled && showRemoveIcon,
showPreviewIcon,
showDownloadIcon,
locale: { ...locale, ...propLocale },
onRemove: this.handleManualRemove,
onDownload,
onPreview,
};
return <UploadList {...uploadListProps} />;
},
},
render() {
const {
prefixCls: customizePrefixCls,
showUploadList,
listType,
type,
disabled,
} = getOptionProps(this);
const { sFileList: fileList, dragState } = this.$data;
const { class: className, style } = this.$attrs;
const getPrefixCls = this.configProvider.getPrefixCls;
const prefixCls = getPrefixCls('upload', customizePrefixCls);
const vcUploadProps = {
...this.$props,
id: this.$props.id ?? this.formItemContext.id.value,
prefixCls,
beforeUpload: this.reBeforeUpload,
onStart: this.onStart,
onError: this.onError,
onProgress: this.onProgress,
onSuccess: this.onSuccess,
onReject: this.onReject,
ref: 'uploadRef',
};
const uploadList = showUploadList ? (
<LocaleReceiver
componentName="Upload"
defaultLocale={defaultLocale.Upload}
children={this.renderUploadList}
/>
) : null;
const children = getSlot(this);
if (type === 'drag') {
const dragCls = classNames(prefixCls, {
[`${prefixCls}-drag`]: true,
[`${prefixCls}-drag-uploading`]: fileList.some((file: any) => file.status === 'uploading'),
[`${prefixCls}-drag-hover`]: dragState === 'dragover',
[`${prefixCls}-disabled`]: disabled,
});
return (
<span class={className} {...getDataAndAriaProps(this.$attrs)}>
<div
class={dragCls}
onDrop={this.onFileDrop}
onDragover={this.onFileDrop}
onDragleave={this.onFileDrop}
style={style}
>
<VcUpload {...vcUploadProps} class={`${prefixCls}-btn`}>
<div class={`${prefixCls}-drag-container`}>{children}</div>
</VcUpload>
</div>
{uploadList}
</span>
);
}
const uploadButtonCls = classNames(prefixCls, {
[`${prefixCls}-select`]: true,
[`${prefixCls}-select-${listType}`]: true,
[`${prefixCls}-disabled`]: disabled,
const onFileDrop = (e: DragEvent) => {
dragState.value = e.type;
if (e.type === 'drop') {
props.onDrop?.(e);
}
};
expose({
onBatchStart,
onSuccess,
onProgress,
onError,
fileList: mergedFileList,
upload,
});
// Remove id to avoid open by label when trigger is hidden
// https://github.com/ant-design/ant-design/issues/14298
if (!children.length || disabled) {
delete vcUploadProps.id;
}
const uploadButton = (
<div class={uploadButtonCls} style={children.length ? undefined : { display: 'none' }}>
<VcUpload {...vcUploadProps}>{children}</VcUpload>
</div>
const { prefixCls, direction } = useConfigInject('upload', props);
const [locale] = useLocaleReceiver(
'Upload',
defaultLocale.Upload,
computed(() => props.locale),
);
const renderUploadList = (button?: VueNode) => {
const {
removeIcon,
previewIcon,
downloadIcon,
previewFile,
onPreview,
onDownload,
disabled,
isImageUrl,
progress,
itemRender,
iconRender,
showUploadList,
} = props;
const { showDownloadIcon, showPreviewIcon, showRemoveIcon } =
typeof showUploadList === 'boolean' ? ({} as ShowUploadListInterface) : showUploadList;
return showUploadList ? (
<UploadList
listType={props.listType}
items={mergedFileList.value}
previewFile={previewFile}
onPreview={onPreview}
onDownload={onDownload}
onRemove={handleRemove}
showRemoveIcon={!disabled && showRemoveIcon}
showPreviewIcon={showPreviewIcon}
showDownloadIcon={showDownloadIcon}
removeIcon={removeIcon}
previewIcon={previewIcon}
downloadIcon={downloadIcon}
iconRender={iconRender}
locale={locale.value}
isImageUrl={isImageUrl}
progress={progress}
itemRender={itemRender}
v-slots={{ ...slots, appendAction: () => button }}
/>
) : (
button
);
};
return () => {
const { listType, disabled, type } = props;
const rcUploadProps = {
onBatchStart,
onError,
onProgress,
onSuccess,
...(props as RcUploadProps),
id: props.id ?? formItemContext.id.value,
prefixCls: prefixCls.value,
beforeUpload: mergedBeforeUpload,
onChange: undefined,
};
if (listType === 'picture-card') {
// Remove id to avoid open by label when trigger is hidden
// !children: https://github.com/ant-design/ant-design/issues/14298
// disabled: https://github.com/ant-design/ant-design/issues/16478
// https://github.com/ant-design/ant-design/issues/24197
if (!slots.default || disabled) {
delete rcUploadProps.id;
}
if (type === 'drag') {
const dragCls = classNames(
prefixCls.value,
{
[`${prefixCls.value}-drag`]: true,
[`${prefixCls.value}-drag-uploading`]: mergedFileList.value.some(
file => file.status === 'uploading',
),
[`${prefixCls.value}-drag-hover`]: dragState.value === 'dragover',
[`${prefixCls.value}-disabled`]: disabled,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
},
attrs.class,
);
return (
<span>
<div
class={dragCls}
onDrop={onFileDrop}
onDragover={onFileDrop}
onDragleave={onFileDrop}
style={attrs.style}
>
<VcUpload
{...rcUploadProps}
ref={upload}
class={`${prefixCls.value}-btn`}
v-slots={slots}
>
<div class={`${prefixCls}-drag-container`}>{slots.default?.()}</div>
</VcUpload>
</div>
{renderUploadList()}
</span>
);
}
const uploadButtonCls = classNames(prefixCls.value, {
[`${prefixCls.value}-select`]: true,
[`${prefixCls.value}-select-${listType}`]: true,
[`${prefixCls.value}-disabled`]: disabled,
[`${prefixCls.value}-rtl`]: direction.value === 'rtl',
});
const children = flattenChildren(slots.default?.());
const uploadButton = (
<div
class={uploadButtonCls}
style={children && children.length ? undefined : { display: 'none' }}
>
<VcUpload {...rcUploadProps} ref={upload} v-slots={slots} />
</div>
);
if (listType === 'picture-card') {
return (
<span class={classNames(`${prefixCls.value}-picture-card-wrapper`, attrs.class)}>
{renderUploadList(uploadButton)}
</span>
);
}
return (
<span class={classNames(`${prefixCls}-picture-card-wrapper`, className)}>
{uploadList}
<span class={attrs.class}>
{uploadButton}
{renderUploadList()}
</span>
);
}
return (
<span class={className}>
{uploadButton}
{uploadList}
</span>
);
};
},
});

View File

@ -25,9 +25,9 @@ export const listItemProps = () => {
listType: String as PropType<UploadListType>,
isImgUrl: Function as PropType<(file: UploadFile) => boolean>,
showRemoveIcon: Boolean,
showDownloadIcon: Boolean,
showPreviewIcon: Boolean,
showRemoveIcon: { type: Boolean, default: undefined },
showDownloadIcon: { type: Boolean, default: undefined },
showPreviewIcon: { type: Boolean, default: undefined },
removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
@ -76,8 +76,8 @@ export default defineComponent({
file,
items,
progress: progressProps,
iconRender,
actionIconRender,
iconRender = slots.iconRender,
actionIconRender = slots.actionIconRender,
itemRender = slots.itemRender,
isImgUrl,
showPreviewIcon,

View File

@ -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>
);
};
},
});

View File

@ -19,7 +19,6 @@ Classic mode. File selection dialog pops up when upload button is clicked.
<a-upload
v-model:file-list="fileList"
name="file"
:multiple="true"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:headers="headers"
@change="handleChange"

View File

@ -25,10 +25,10 @@ After users upload picture, the thumbnail will be shown in list. The upload butt
>
<div v-if="fileList.length < 8">
<plus-outlined />
<div class="ant-upload-text">Upload</div>
<div style="margin-top: 8px">Upload</div>
</div>
</a-upload>
<a-modal :visible="previewVisible" :footer="null" @cancel="handleCancel">
<a-modal :visible="previewVisible" :title="previewTitle" :footer="null" @cancel="handleCancel">
<img alt="example" style="width: 100%" :src="previewImage" />
</a-modal>
</div>
@ -36,7 +36,7 @@ After users upload picture, the thumbnail will be shown in list. The upload butt
<script lang="ts">
import { PlusOutlined } from '@ant-design/icons-vue';
import { defineComponent, ref } from 'vue';
import type { UploadChangeParam, UploadProps } from 'ant-design-vue';
import type { UploadProps } from 'ant-design-vue';
function getBase64(file: File) {
return new Promise((resolve, reject) => {
@ -52,8 +52,9 @@ export default defineComponent({
PlusOutlined,
},
setup() {
const previewVisible = ref<boolean>(false);
const previewImage = ref<string | undefined>('');
const previewVisible = ref(false);
const previewImage = ref('');
const previewTitle = ref('');
const fileList = ref<UploadProps['fileList']>([
{
@ -80,6 +81,13 @@ export default defineComponent({
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-xxx',
percent: 50,
name: 'image.png',
status: 'uploading',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
{
uid: '-5',
name: 'image.png',
@ -89,6 +97,7 @@ export default defineComponent({
const handleCancel = () => {
previewVisible.value = false;
previewTitle.value = '';
};
const handlePreview = async (file: UploadProps['fileList'][number]) => {
if (!file.url && !file.preview) {
@ -96,9 +105,7 @@ export default defineComponent({
}
previewImage.value = file.url || file.preview;
previewVisible.value = true;
};
const handleChange = ({ fileList: newFileList }: UploadChangeParam) => {
fileList.value = newFileList;
previewTitle.value = file.name || file.url.substring(file.url.lastIndexOf('/') + 1);
};
return {
@ -107,7 +114,7 @@ export default defineComponent({
fileList,
handleCancel,
handlePreview,
handleChange,
previewTitle,
};
},
});

View File

@ -1,21 +1,18 @@
import type { App, Plugin } from 'vue';
import Upload from './Upload';
import type { App } from 'vue';
import Upload, { LIST_IGNORE } from './Upload';
import Dragger from './Dragger';
export type { UploadProps, UploadListProps, UploadChangeParam } from './interface';
Upload.Dragger = Dragger;
export type { UploadProps, UploadListProps, UploadChangeParam, UploadFile } from './interface';
/* istanbul ignore next */
Upload.install = function (app: App) {
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;
};
export default Object.assign(Upload, {
Dragger,
LIST_IGNORE,
install(app: App) {
app.component(Upload.name, Upload);
app.component(Dragger.name, Dragger);
return app;
},
});

View File

@ -1,22 +1,21 @@
import type {
RcFile as OriRcFile,
UploadRequestOption as RcCustomRequestOptions,
} from '../vc-upload/interface';
import type { ProgressProps } from '../progress';
import type { VueNode } from '../_util/type';
import type { ExtractPropTypes, PropType } from 'vue';
import { tuple } from '../_util/type';
import PropsTypes from '../_util/vue-types';
export const UploadFileStatus = PropsTypes.oneOf(
tuple('error', 'success', 'done', 'uploading', 'removed'),
);
export interface RcFile extends OriRcFile {
readonly lastModifiedDate: Date;
}
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
export interface HttpRequestHeader {
[key: string]: string;
}
export interface VcFile extends File {
uid: string;
readonly lastModifiedDate: Date;
readonly webkitRelativePath: string;
}
export type UploadFileStatus = 'error' | 'success' | 'done' | 'uploading' | 'removed';
export interface UploadFile<T = any> {
uid: string;
size?: number;
@ -28,7 +27,7 @@ export interface UploadFile<T = any> {
status?: UploadFileStatus;
percent?: number;
thumbUrl?: string;
originFileObj?: any;
originFileObj?: RcFile;
response?: T;
error?: any;
linkProps?: any;
@ -37,17 +36,23 @@ export interface UploadFile<T = any> {
preview?: string;
}
export interface UploadChangeParam<T extends object = UploadFile> {
export interface InternalUploadFile<T = any> extends UploadFile<T> {
originFileObj: RcFile;
}
export interface ShowUploadListInterface {
showRemoveIcon?: boolean;
showPreviewIcon?: boolean;
showDownloadIcon?: boolean;
}
export interface UploadChangeParam<T = UploadFile> {
// https://github.com/ant-design/ant-design/issues/14420
file: T;
fileList: UploadFile[];
event?: { percent: number };
}
export const ShowUploadListInterface = PropsTypes.shape({
showRemoveIcon: PropsTypes.looseBool,
showPreviewIcon: PropsTypes.looseBool,
}).loose;
export interface UploadLocale {
uploading?: string;
removeFile?: string;
@ -56,61 +61,120 @@ export interface UploadLocale {
previewFile?: string;
}
export const uploadProps = {
type: PropsTypes.oneOf(tuple('drag', 'select')),
name: PropsTypes.string,
defaultFileList: { type: Array as PropType<UploadFile[]> },
fileList: { type: Array as PropType<UploadFile[]> },
action: PropsTypes.oneOfType([PropsTypes.string, PropsTypes.func]),
directory: PropsTypes.looseBool,
data: PropsTypes.oneOfType([PropsTypes.object, PropsTypes.func]),
method: PropsTypes.oneOf(tuple('POST', 'PUT', 'PATCH', 'post', 'put', 'patch')),
headers: PropsTypes.object,
showUploadList: PropsTypes.oneOfType([PropsTypes.looseBool, ShowUploadListInterface]),
multiple: PropsTypes.looseBool,
accept: PropsTypes.string,
beforeUpload: PropsTypes.func,
listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')),
// className: PropsTypes.string,
remove: PropsTypes.func,
supportServerRender: PropsTypes.looseBool,
// style: PropsTypes.object,
disabled: PropsTypes.looseBool,
prefixCls: PropsTypes.string,
customRequest: PropsTypes.func,
withCredentials: PropsTypes.looseBool,
openFileDialogOnClick: PropsTypes.looseBool,
locale: { type: Object as PropType<UploadLocale> },
height: PropsTypes.number,
id: PropsTypes.string,
previewFile: PropsTypes.func,
transformFile: PropsTypes.func,
onChange: { type: Function as PropType<(info: UploadChangeParam) => void> },
onPreview: { type: Function as PropType<(file: UploadFile) => void> },
onRemove: {
type: Function as PropType<(file: UploadFile) => void | boolean | Promise<void | boolean>>,
},
onDownload: { type: Function as PropType<(file: UploadFile) => void> },
'onUpdate:fileList': { type: Function as PropType<(files: UploadFile[]) => void> },
};
export type UploadType = 'drag' | 'select';
export type UploadListType = 'text' | 'picture' | 'picture-card';
export type UploadListProgressProps = Omit<ProgressProps, 'percent' | 'type'>;
export type UploadProps = Partial<ExtractPropTypes<typeof uploadProps>>;
export const uploadListProps = {
listType: PropsTypes.oneOf(tuple('text', 'picture', 'picture-card')),
// items: PropsTypes.arrayOf(UploadFile),
items: { type: Array as PropType<UploadFile[]> },
progressAttr: PropsTypes.object,
prefixCls: PropsTypes.string,
showRemoveIcon: PropsTypes.looseBool,
showDownloadIcon: PropsTypes.looseBool,
showPreviewIcon: PropsTypes.looseBool,
locale: { type: Object as PropType<UploadLocale> },
previewFile: PropsTypes.func,
onPreview: { type: Function as PropType<(file: UploadFile) => void> },
onRemove: {
type: Function as PropType<(file: UploadFile) => void | boolean>,
},
onDownload: { type: Function as PropType<(file: UploadFile) => void> },
};
export type ItemRender<T = any> = (opt: {
originNode: VueNode;
file: UploadFile;
fileList: Array<UploadFile<T>>;
actions: {
download: () => void;
preview: () => void;
remove: () => void;
};
}) => VueNode;
export type UploadListProps = Partial<ExtractPropTypes<typeof uploadListProps>>;
type PreviewFileHandler = (file: File | Blob) => PromiseLike<string>;
type TransformFileHandler = (
file: RcFile,
) => string | Blob | File | PromiseLike<string | Blob | File>;
type BeforeUploadValueType = void | boolean | string | Blob | File;
function uploadProps<T = any>() {
return {
capture: [Boolean, String] as PropType<boolean | 'user' | 'environment'>,
type: String as PropType<UploadType>,
name: String,
defaultFileList: Array as PropType<Array<UploadFile<T>>>,
fileList: Array as PropType<Array<UploadFile<T>>>,
action: [String, Function] as PropType<
string | ((file: RcFile) => string) | ((file: RcFile) => PromiseLike<string>)
>,
directory: { type: Boolean, default: undefined },
data: [Object, Function] as PropType<
| Record<string, unknown>
| ((file: UploadFile<T>) => Record<string, unknown> | Promise<Record<string, unknown>>)
>,
method: String as PropType<'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch'>,
headers: Object as PropType<HttpRequestHeader>,
showUploadList: {
type: [Boolean, Object] as PropType<boolean | ShowUploadListInterface>,
default: undefined as boolean | ShowUploadListInterface,
},
multiple: { type: Boolean, default: undefined },
accept: String,
beforeUpload: Function as PropType<
(file: RcFile, FileList: RcFile[]) => BeforeUploadValueType | Promise<BeforeUploadValueType>
>,
onChange: Function as PropType<(info: UploadChangeParam<T>) => void>,
'onUpdate:fileList': Function as PropType<(fileList: UploadChangeParam<T>['fileList']) => void>,
onDrop: Function as PropType<(event: DragEvent) => void>,
listType: String as PropType<UploadListType>,
onPreview: Function as PropType<(file: UploadFile<T>) => void>,
onDownload: Function as PropType<(file: UploadFile<T>) => void>,
onRemove: Function as PropType<
(file: UploadFile<T>) => void | boolean | Promise<void | boolean>
>,
supportServerRender: { type: Boolean, default: undefined },
disabled: { type: Boolean, default: undefined },
prefixCls: String,
customRequest: Function as PropType<(options: RcCustomRequestOptions) => void>,
withCredentials: { type: Boolean, default: undefined },
openFileDialogOnClick: { type: Boolean, default: undefined },
locale: { type: Object as PropType<UploadLocale>, default: undefined as UploadLocale },
id: String,
previewFile: Function as PropType<PreviewFileHandler>,
/** @deprecated Please use `beforeUpload` directly */
transformFile: Function as PropType<TransformFileHandler>,
iconRender: Function as PropType<
(opt: { file: UploadFile<T>; listType?: UploadListType }) => VueNode
>,
isImageUrl: Function as PropType<(file: UploadFile) => boolean>,
progress: Object as PropType<UploadListProgressProps>,
itemRender: Function as PropType<ItemRender<T>>,
/** Config max count of `fileList`. Will replace current one when `maxCount` is 1 */
maxCount: Number,
height: [Number, String],
removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
};
}
export type UploadProps = Partial<ExtractPropTypes<ReturnType<typeof uploadProps>>>;
export interface UploadState<T = any> {
fileList: UploadFile<T>[];
dragState: string;
}
function uploadListProps<T = any>() {
return {
listType: String as PropType<UploadListType>,
onPreview: Function as PropType<(file: UploadFile<T>) => void>,
onDownload: Function as PropType<(file: UploadFile<T>) => void>,
onRemove: Function as PropType<(file: UploadFile<T>) => void | boolean>,
items: Array as PropType<Array<UploadFile<T>>>,
progress: Object as PropType<UploadListProgressProps>,
prefixCls: String as PropType<string>,
showRemoveIcon: { type: Boolean, default: undefined },
showDownloadIcon: { type: Boolean, default: undefined },
showPreviewIcon: { type: Boolean, default: undefined },
removeIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
downloadIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
previewIcon: Function as PropType<(opt: { file: UploadFile }) => VueNode>,
locale: { type: Object as PropType<UploadLocale>, default: undefined as UploadLocale },
previewFile: Function as PropType<PreviewFileHandler>,
iconRender: Function as PropType<
(opt: { file: UploadFile<T>; listType?: UploadListType }) => VueNode
>,
isImageUrl: Function as PropType<(file: UploadFile) => boolean>,
appendAction: Function as PropType<() => VueNode>,
itemRender: Function as PropType<ItemRender<T>>,
};
}
export type UploadListProps = Partial<ExtractPropTypes<ReturnType<typeof uploadListProps>>>;
export { uploadProps, uploadListProps };

View File

@ -34,8 +34,6 @@
}
&&-select-picture-card {
display: table;
float: left;
width: @upload-picture-card-size;
height: @upload-picture-card-size;
margin-right: 8px;
@ -46,19 +44,21 @@
border: @border-width-base dashed @border-color-base;
border-radius: @border-radius-base;
cursor: pointer;
transition: border-color 0.3s ease;
transition: border-color 0.3s;
> .@{upload-prefix-cls} {
display: table-cell;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 8px;
text-align: center;
vertical-align: middle;
}
&:hover {
border-color: @primary-color;
.@{upload-prefix-cls}-disabled& {
border-color: @border-color-base;
}
}
}
@ -74,7 +74,7 @@
transition: border-color 0.3s;
.@{upload-prefix-cls} {
padding: 16px 0;
padding: @padding-md 0;
}
&.@{upload-prefix-cls}-drag-hover:not(.@{upload-prefix-cls}-disabled) {
@ -116,10 +116,12 @@
color: @text-color-secondary;
font-size: @font-size-base;
}
.@{iconfont-css-prefix}-plus {
color: @disabled-color;
font-size: 30px;
transition: all 0.3s;
&:hover {
color: @text-color-secondary;
}
@ -140,52 +142,55 @@
.@{upload-prefix-cls}-list {
.reset-component();
.clearfix();
&-item-list-type-text {
&:hover {
.@{upload-prefix-cls}-list-item-name-icon-count-1 {
padding-right: 14px;
}
.@{upload-prefix-cls}-list-item-name-icon-count-2 {
padding-right: 28px;
}
}
}
line-height: @line-height-base;
// ============================ Item ============================
&-item {
position: relative;
height: 22px;
margin-top: 8px;
height: @line-height-base * @font-size-base;
margin-top: @margin-xs;
font-size: @font-size-base;
&-name {
display: inline-block;
width: 100%;
padding-left: @font-size-base + 8px;
overflow: hidden;
line-height: @line-height-base;
white-space: nowrap;
text-overflow: ellipsis;
}
&-name-icon-count-1 {
padding-right: 14px;
}
&-card-actions {
position: absolute;
right: 0;
opacity: 0;
&.picture {
top: 25px;
&-btn {
opacity: 0;
}
&-btn.@{ant-prefix}-btn-sm {
height: 20px;
line-height: 1;
}
&.picture {
top: 22px;
line-height: 0;
}
&-btn:focus,
&.picture &-btn {
opacity: 1;
}
.anticon {
padding-right: 6px;
color: rgba(0, 0, 0, 0.45);
.@{iconfont-css-prefix} {
color: @upload-actions-color;
}
}
&-info {
height: 100%;
padding: 0 12px 0 4px;
padding: 0 4px;
transition: background-color 0.3s;
> span {
@ -195,25 +200,27 @@
}
.@{iconfont-css-prefix}-loading,
.@{iconfont-css-prefix}-paper-clip {
position: absolute;
top: (@font-size-base / 2) - 2px;
color: @text-color-secondary;
font-size: @font-size-base;
.@{upload-prefix-cls}-text-icon {
.@{iconfont-css-prefix} {
position: absolute;
top: (@font-size-base / 2) - 2px;
color: @text-color-secondary;
font-size: @font-size-base;
}
}
}
.@{iconfont-css-prefix}-close {
.iconfont-size-under-12px(10px);
position: absolute;
top: 6px;
right: 4px;
color: @text-color-secondary;
font-size: 10px;
line-height: 0;
cursor: pointer;
opacity: 0;
transition: all 0.3s;
&:hover {
color: @text-color;
}
@ -227,21 +234,24 @@
opacity: 1;
}
&:hover &-card-actions {
&:hover &-card-actions-btn {
opacity: 1;
}
&-error,
&-error .@{iconfont-css-prefix}-paper-clip,
&-error .@{upload-prefix-cls}-text-icon > .@{iconfont-css-prefix},
&-error &-name {
color: @error-color;
}
&-error &-card-actions {
.anticon {
.@{iconfont-css-prefix} {
color: @error-color;
}
opacity: 1;
&-btn {
opacity: 1;
}
}
&-progress {
@ -254,17 +264,20 @@
}
}
// =================== Picture & Picture Card ===================
&-picture,
&-picture-card {
.@{upload-item} {
position: relative;
height: 66px;
padding: 8px;
padding: @padding-xs;
border: @border-width-base @upload-picture-card-border-style @border-color-base;
border-radius: @border-radius-base;
&:hover {
background: transparent;
}
&-error {
border-color: @error-color;
}
@ -283,15 +296,30 @@
}
.@{upload-item}-thumbnail {
position: absolute;
top: 8px;
left: 8px;
width: 48px;
height: 48px;
font-size: 26px;
line-height: 54px;
line-height: 60px;
text-align: center;
opacity: 0.8;
.@{iconfont-css-prefix} {
font-size: 26px;
}
}
// Adjust the color of the error icon : https://github.com/ant-design/ant-design/pull/24160
.@{upload-item}-error .@{upload-item}-thumbnail {
.@{iconfont-css-prefix} {
svg path {
&[fill='#e6f7ff'] {
fill: @error-color-deprecated-bg;
}
&[fill='#1890ff'] {
fill: @error-color;
}
}
}
}
.@{upload-item}-icon {
@ -300,6 +328,10 @@
left: 50%;
font-size: 26px;
transform: translate(-50%, -50%);
.@{iconfont-css-prefix} {
font-size: 26px;
}
}
.@{upload-item}-image {
@ -327,16 +359,8 @@
transition: all 0.3s;
}
.@{upload-item}-name-icon-count-1 {
padding-right: 18px;
}
.@{upload-item}-name-icon-count-2 {
padding-right: 36px;
}
.@{upload-item}-uploading .@{upload-item}-name {
line-height: 28px;
margin-bottom: 12px;
}
.@{upload-item}-progress {
@ -355,21 +379,23 @@
}
}
// ======================== Picture Card ========================
&-picture-card {
&-container {
display: inline-block;
width: @upload-picture-card-size;
height: @upload-picture-card-size;
margin: 0 @margin-xs @margin-xs 0;
vertical-align: top;
}
&.@{upload-prefix-cls}-list::after {
display: none;
}
&-container {
float: left;
width: @upload-picture-card-size;
height: @upload-picture-card-size;
margin: 0 8px 8px 0;
}
.@{upload-item} {
float: left;
width: @upload-picture-card-size;
height: @upload-picture-card-size;
margin: 0 8px 8px 0;
height: 100%;
margin: 0;
}
.@{upload-item}-info {
@ -413,6 +439,7 @@
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
&:hover {
color: @text-color-inverse;
}
@ -430,7 +457,7 @@
display: block;
width: 100%;
height: 100%;
object-fit: cover;
object-fit: contain;
}
.@{upload-item}-name {
@ -441,7 +468,7 @@
text-align: center;
}
.anticon-picture + .@{upload-item}-name {
.@{upload-item}-file + .@{upload-item}-name {
position: absolute;
bottom: 10px;
display: block;
@ -454,46 +481,82 @@
.@{upload-item}-info {
height: auto;
&::before,
.@{iconfont-css-prefix}-eye-o,
.@{iconfont-css-prefix}-eye,
.@{iconfont-css-prefix}-delete {
display: none;
}
}
&-text {
margin-top: 18px;
color: @text-color-secondary;
}
}
.@{upload-item}-progress {
bottom: 32px;
width: calc(100% - 14px);
padding-left: 0;
}
}
.@{upload-prefix-cls}-success-icon {
color: @success-color;
font-weight: bold;
// ======================= Picture & Text =======================
&-text,
&-picture {
&-container {
transition: opacity @animation-duration-slow, height @animation-duration-slow;
&::before {
display: table;
width: 0;
height: 0;
content: '';
}
// Don't know why span here, just stretch it
.@{upload-prefix-cls}-span {
display: block;
flex: auto;
}
}
// text & picture no need this additional element.
// But it used for picture-card, let's keep it.
.@{upload-prefix-cls}-span {
display: flex;
align-items: center;
> * {
flex: none;
}
}
.@{upload-item}-name {
flex: auto;
margin: 0;
padding: 0 @padding-xs;
}
.@{upload-item}-card-actions {
position: static;
}
}
.@{upload-prefix-cls}-animate-enter,
.@{upload-prefix-cls}-animate-leave,
// ============================ Text ============================
&-text {
.@{upload-prefix-cls}-text-icon {
.@{iconfont-css-prefix} {
position: static;
}
}
}
// =========================== Motion ===========================
.@{upload-prefix-cls}-animate-inline-appear,
.@{upload-prefix-cls}-animate-inline-enter,
.@{upload-prefix-cls}-animate-inline-leave {
animation-duration: 0.3s;
animation-duration: @animation-duration-slow;
animation-fill-mode: @ease-in-out-circ;
}
.@{upload-prefix-cls}-animate-enter {
animation-name: uploadAnimateIn;
}
.@{upload-prefix-cls}-animate-leave {
animation-name: uploadAnimateOut;
}
.@{upload-prefix-cls}-animate-inline-appear,
.@{upload-prefix-cls}-animate-inline-enter {
animation-name: uploadAnimateInlineIn;
}
@ -503,24 +566,6 @@
}
}
@keyframes uploadAnimateIn {
from {
height: 0;
margin: 0;
padding: 0;
opacity: 0;
}
}
@keyframes uploadAnimateOut {
to {
height: 0;
margin: 0;
padding: 0;
opacity: 0;
}
}
@keyframes uploadAnimateInlineIn {
from {
width: 0;
@ -540,3 +585,5 @@
opacity: 0;
}
}
@import './rtl';

View File

@ -1,4 +0,0 @@
// rc-upload 2.9.4
import upload from './src';
export default upload;

View File

@ -1,7 +1,7 @@
// rc-upload 4.3.3
import Upload from './Upload';
import { UploadProps } from './interface';
import type { UploadProps } from './interface';
export { UploadProps };
export type { UploadProps };
export default Upload;

View File

@ -7,19 +7,19 @@ export type Action = string | ((file: RcFile) => string | PromiseLike<string>);
export const uploadProps = () => {
return {
capture: [Boolean, String] as PropType<boolean | 'user' | 'environment'>,
multipart: Boolean,
multipart: { type: Boolean, default: undefined },
name: String,
disabled: Boolean,
disabled: { type: Boolean, default: undefined },
componentTag: String as PropType<any>,
action: [String, Function] as PropType<Action>,
method: String as PropType<UploadRequestMethod>,
directory: Boolean,
directory: { type: Boolean, default: undefined },
data: [Object, Function] as PropType<
Record<string, unknown> | ((file: RcFile | string | Blob) => Record<string, unknown>)
>,
headers: Object as PropType<UploadRequestHeader>,
accept: String,
multiple: Boolean,
multiple: { type: Boolean, default: undefined },
onBatchStart: Function as PropType<
(fileList: { file: RcFile; parsedFile: Exclude<BeforeUploadFileType, boolean> }[]) => void
>,
@ -38,8 +38,8 @@ export const uploadProps = () => {
) => BeforeUploadFileType | Promise<void | BeforeUploadFileType>
>,
customRequest: Function as PropType<(option: UploadRequestOption) => void>,
withCredentials: Boolean,
openFileDialogOnClick: Boolean,
withCredentials: { type: Boolean, default: undefined },
openFileDialogOnClick: { type: Boolean, default: undefined },
prefixCls: String,
id: String,
onMouseenter: Function as PropType<(e: MouseEvent) => void>,

View File

@ -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;

View File

@ -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;

View File

@ -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>;
},
});

View File

@ -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;
};

View File

@ -1,4 +0,0 @@
// export this package's api
import Upload from './Upload';
export default Upload;

View File

@ -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();
},
};
}

View File

@ -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;

View File

@ -1,6 +0,0 @@
const now = +new Date();
let index = 0;
export default function uid() {
return `vc-upload-${now}-${++index}`;
}

View File

@ -1,5 +1,5 @@
// debugger tsx
import Demo from '../../components/form/demo/normal-login.vue';
import Demo from '../../components/upload/demo/defaultFileList.vue';
// import Demo from './demo/demo.vue';
export default {