AntDesign upload 多图可拖拽排序 Demo
需要安装依赖
yarn add react-dnd-html5-backend@14.1.0
yarn add react-dnd@14.0.5
yarn add immutability-helper@3.1.1
直接上代码
DragSortingUpload.tsx
import React, { useState, useCallback } from 'react';
import { Upload, Tooltip } from 'antd';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import update from 'immutability-helper';
import { PlusOutlined } from '@ant-design/icons';
import { getBase64 } from '@/utils';
import uploadRequest from '@/utils/uploadRequest';
import Modal from 'antd/lib/modal/Modal';
import { applyToken } from '../../services/ant-design-pro/qiniu';
import './ManyUpload.less';
const type = 'DragableUploadList';
const DragableUploadListItem = ({ originNode, moveRow, file, fileList }: any) => {
const ref = React.useRef(null);
const index = fileList.indexOf(file);
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor: any) => {
const { index: dragIndex } = monitor.getItem() || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName: dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
const errorNode = <Tooltip title="Upload Error">{originNode.props.children}</Tooltip>;
return (
<div
ref={ref}
className={`ant-upload-draggable-list-item ${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', height: '100%' }}
>
{file.status === 'error' ? errorNode : originNode}
</div>
);
};
const uploadButton = (
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload</div>
</div>
);
const DragSortingUpload = () => {
const [previewVisible, setPreviewVisible] = React.useState(false);
const [previewImage, setPreviewImage] = React.useState('');
const [previewTitle, setPreviewTitle] = React.useState('');
const [fileList, setFileList] = useState<any>([
{
uid: '-1',
name: 'image1.png',
status: 'done',
url: 'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
},
]);
// 自定义上传 由于antDesign使用的是 RC.upload 组件 https://github.com/react-component/upload
const customRequest = async (options: any) => {
const { data } = await applyToken();
options.action = data.url;
options.data['token'] = data.token
// 复制了RC.upload request的上传方法 https://github.com/react-component/upload/blob/master/src/request.ts
uploadRequest(options);
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
const dragRow = fileList[dragIndex];
setFileList(
update(fileList, {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragRow],
],
}),
);
},
[fileList],
);
const handleCancel = () => setPreviewVisible(false);
const handlePreview = async (file: any) => {
if (!file.url && !file.preview) {
file.preview = await getBase64(file.originFileObj);
}
setPreviewVisible(true);
setPreviewImage(file.url || file.preview);
setPreviewTitle(file.name || file.url.substring(file.url.lastIndexOf('/') + 1));
};
return (
<div className="many-upload">
<DndProvider backend={HTML5Backend}>
<Upload
multiple
customRequest={customRequest}
fileList={fileList}
onPreview={handlePreview}
listType="picture-card"
onChange={(e) => {
// console.log("change?", e);
setFileList(e.fileList);
}}
onRemove={(e: any) => {
const arr = fileList.filter((v: any) => e.uid !== v.uid);
setFileList(arr);
}}
itemRender={(originNode, file, currFileList) => (
<DragableUploadListItem
originNode={originNode}
file={file}
fileList={currFileList}
moveRow={moveRow}
/>
)}
>
{fileList.length >= 8 ? null : uploadButton}
</Upload>
</DndProvider>
<Modal visible={previewVisible} title={previewTitle} footer={null} onCancel={handleCancel}>
<img alt="preview" style={{ width: '100%' }} src={previewImage} />
</Modal>
</div>
);
};
export default DragSortingUpload;
此文件是复制 RC.upload 的 request.ts
@/utils/uploadRequest.ts
import type { UploadRequestOption, UploadRequestError, UploadProgressEvent } from './interface';
function getError(option: UploadRequestOption, xhr: XMLHttpRequest) {
const msg = `cannot ${option.method} ${option.action} ${xhr.status}'`;
const err = new Error(msg) as UploadRequestError;
err.status = xhr.status;
err.method = option.method;
err.url = option.action;
return err;
}
function getBody(xhr: XMLHttpRequest) {
const text = xhr.responseText || xhr.response;
if (!text) {
return text;
}
try {
return JSON.parse(text);
} catch (e) {
return text;
}
}
export default function upload(option: UploadRequestOption) {
// eslint-disable-next-line no-undef
const xhr = new XMLHttpRequest();
if (option.onProgress && xhr.upload) {
xhr.upload.onprogress = function progress(e: UploadProgressEvent) {
if (e.total > 0) {
e.percent = (e.loaded / e.total) * 100;
}
option.onProgress(e);
};
}
// eslint-disable-next-line no-undef
const formData = new 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, value as string | Blob);
});
}
// eslint-disable-next-line no-undef
if (option.file instanceof Blob) {
formData.append(option.filename, option.file, (option.file as any).name);
} else {
formData.append(option.filename, option.file);
}
xhr.onerror = function error(e) {
option.onError && 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));
}
return 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');
}
Object.keys(headers).forEach(h => {
if (headers[h] !== null) {
xhr.setRequestHeader(h, headers[h]);
}
});
xhr.send(formData);
return {
abort() {
xhr.abort();
},
};
}
引用 RC.upload 的 interface.ts
interface.ts
import type * as React from 'react';
export type BeforeUploadFileType = File | Blob | boolean | string;
export type Action = string | ((file: RcFile) => string | PromiseLike<string>);
export interface UploadProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onError' | 'onProgress'> {
name?: string;
style?: React.CSSProperties;
className?: string;
disabled?: boolean;
component?: React.JSXElementConstructor<any>;
action?: Action;
method?: UploadRequestMethod;
directory?: boolean;
data?: Record<string, unknown> | ((file: RcFile | string | Blob) => Record<string, unknown>);
headers?: UploadRequestHeader;
accept?: string;
multiple?: boolean;
onBatchStart?: (
fileList: { file: RcFile; parsedFile: Exclude<BeforeUploadFileType, boolean> }[],
) => void;
onStart?: (file: RcFile) => void;
onError?: (error: Error, ret: Record<string, unknown>, file: RcFile) => void;
onSuccess?: (response: Record<string, unknown>, file: RcFile, xhr: XMLHttpRequest) => void;
onProgress?: (event: UploadProgressEvent, file: RcFile) => void;
beforeUpload?: (
file: RcFile,
FileList: RcFile[],
) => BeforeUploadFileType | Promise<void | BeforeUploadFileType>;
customRequest?: (option: UploadRequestOption) => void;
withCredentials?: boolean;
openFileDialogOnClick?: boolean;
prefixCls?: string;
id?: string;
onMouseEnter?: (e: React.MouseEvent<HTMLDivElement>) => void;
onMouseLeave?: (e: React.MouseEvent<HTMLDivElement>) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => void;
}
export interface UploadProgressEvent extends Partial<ProgressEvent> {
percent?: number;
loaded: number
total: number
}
export type UploadRequestMethod = 'POST' | 'PUT' | 'PATCH' | 'post' | 'put' | 'patch';
export type UploadRequestHeader = Record<string, string>;
export interface UploadRequestError extends Error {
status?: number;
method?: UploadRequestMethod;
url?: string;
}
export interface UploadRequestOption<T = any> {
onProgress: (event: UploadProgressEvent) => void;
onError: (event: UploadRequestError | ProgressEvent, body?: T) => void;
onSuccess: (body: T, xhr?: XMLHttpRequest) => void;
data: Record<string, unknown>;
filename: string;
file: Exclude<BeforeUploadFileType, File | boolean> | RcFile;
withCredentials: boolean;
action: string;
headers?: UploadRequestHeader;
method: UploadRequestMethod;
}
export interface RcFile extends File {
uid: string;
}