import React, { useState } from "react"; import { useDropzone, FileRejection, FileError } from "react-dropzone"; import { StyledProps } from "../_type"; import { upload, ProgressInfo, UploadEventContext } from "./uploadFile"; import { uuid } from "../_util/uuid"; import { injectValue } from "../_util/inject-value"; import { isCallable } from "../_util/is-callable"; import { File } from "./preset/File"; import { Dragger } from "./preset/Dragger"; import { Image } from "./preset/Image"; import { UploadContext } from "./UploadContext"; import { noop } from "../_util/noop"; import { forwardRefWithStatics } from "../_util/forward-ref-with-statics"; export interface UploadProps extends StyledProps { /** * 唤起文件选择的组件 * * 直接传递 ReactNode 将默认在该元素使用点击上传 * * 使用 Render Props 形式可自定义点击/拖拽上传 * * - `open`: 打开文件选择 * - `getDraggerProps(props?)`: 在需要执行拖拽的根元素传递这些 props * - `isDragging`: 是否有文件拖拽至可拖拽区域 * * @docType React.ReactNode | (props: { open: () => void, getDraggerProps: (props?: object) => object, isDragging: boolean }) => React.ReactNode; */ children: | React.ReactNode | ((props: { open: () => void; getDraggerProps: (props?: object) => object; isDragging: boolean; }) => React.ReactNode); /** * 是否支持多选文件 * @default false */ multiple?: boolean; /** * 支持上传文件夹 * @default false */ directory?: boolean; /** * 上传地址 */ action?: string | ((file: File) => Promise); /** * 接受的文件类型 */ accept?: string | string[]; /** * 上传文件大小限制(bytes) */ maxSize?: number; /** * 请求文件参数名 * @default "file" */ name?: string; /** * 设置请求头部 */ headers?: object | ((file: File) => object); /** * 附加请求参数 */ data?: object | ((file: File) => object); /** * 设置请求的 HTTP Method * @default "POST" * @since 2.5.1 */ method?: string; /** * 自定义 XHR send 发送的数据体 * @since 2.6.0 */ send?: (file: File, formData: FormData) => BodyInit; /** * 请求时是否携带 cookie * @default false */ withCredentials?: boolean; /** * 上传文件前调用 * * - `isAccepted`: 当前文件是否符合 `accept` 及 `maxSize` 等限制 * - `reasons`: 当前文件不符合要求的原因 (`^2.5.0` 加入) * * 返回 `true` (Promise `resolve`)执行上传 (`^2.7.4`加入:resolve 传入 File 或 Blob 对象则上传 resolve 传入对象) * * 返回 `false` (Promise `reject`)时不执行后续上传流程 */ beforeUpload?: ( file: File, fileList: File[], isAccepted: boolean, reasons: FileError[] ) => boolean | Promise; /** * 上传开始时回调 */ onStart?: (file: File, context: { xhr: XMLHttpRequest }) => void; /** * 上传进度更新时回调 */ onProgress?: (progress: ProgressInfo, context: UploadEventContext) => void; /** * 上传成功时回调 */ onSuccess?: (result: object | string, context: UploadEventContext) => void; /** * 上传失败时回调 */ onError?: (error: Error, context: UploadEventContext) => void; } function isPromise(p: any): p is Promise { return p && typeof p === "object" && typeof p.then === "function"; } export const Upload = forwardRefWithStatics( function Upload( { action = "", accept, maxSize, multiple = false, directory = false, headers, data, method, send, name = "file", withCredentials, beforeUpload = (_, __, isAccepted) => isAccepted, onStart = noop, onProgress, onSuccess, onError, children, className, ...props }: UploadProps, ref: React.Ref ) { if (directory) { // eslint-disable-next-line no-param-reassign multiple = true; } const [dragging, setDragging] = useState(false); const { getInputProps, getRootProps, open } = useDropzone({ accept, maxSize, multiple, noClick: true, noKeyboard: true, onDrop: uploadFiles, onDragEnter: () => setDragging(true), onDragLeave: () => setDragging(false), }); function uploadFiles( acceptedFiles: File[], fileRejections: FileRejection[] ) { const rejectedFiles = fileRejections.map(r => r.file); const fileList = [...acceptedFiles, ...rejectedFiles]; acceptedFiles.forEach(file => { file["id"] = uuid(); // eslint-disable-line dot-notation uploadFile(file, fileList, true); }); fileRejections.forEach(({ file, errors }) => { file["id"] = uuid(); // eslint-disable-line dot-notation uploadFile(file, fileList, false, errors); }); setDragging(false); } function uploadFile( file: File, fileList: File[], isAccepted: boolean, reasons: FileError[] = [] ) { const couldUpload = beforeUpload(file, fileList, isAccepted, reasons); if (isPromise(couldUpload)) { // support file convert couldUpload .then(convertedFile => request( convertedFile instanceof window.File || convertedFile instanceof window.Blob ? convertedFile : file ) ) .catch(() => null); } else { if (!couldUpload) { return; } setTimeout(() => request(file), 0); } } async function request(file) { let uploadAction; if (typeof action === "function") { try { uploadAction = await action(file); } catch (error) { onError(error, { event: null, xhr: null, file }); return; } } else { uploadAction = action; } const xhr = upload({ action: uploadAction, filename: name, file, data: injectValue(data)(file), headers: injectValue(headers)(file), method, send, withCredentials, onProgress, onSuccess, onError, }); onStart(file, { xhr }); } return ( {isCallable(children) ? ( children({ open, getDraggerProps: getRootProps, isDragging: dragging, }) ) : ( {children} )} ); }, { defaultLabelAlign: "middle", File, Dragger, Image, } ); Upload.displayName = "Upload";