UNPKG

@nutui/nutui-react-taro

Version:

京东风格的轻量级移动端 React 组件库,支持一套代码生成 H5 和小程序

618 lines (617 loc) 23.8 kB
import { _ as __rest } from "./tslib.es6-iWu3F_1J.js"; import React__default, { useState, useImperativeHandle, useRef, useEffect } from "react"; import classNames from "classnames"; import Taro, { uploadFile, getEnv, chooseMedia, chooseImage } from "@tarojs/taro"; import { Link, Del, Failure, Loading, Photograph } from "@nutui/icons-react-taro"; import { B as Button } from "./button.taro-CixO32Hw.js"; import { u as useConfig } from "./configprovider.taro-DpK4IiCE.js"; import { a as isPromise } from "./index-DeNfBlua.js"; import { C as ComponentDefaults } from "./typings-DV9RBfhj.js"; import { u as usePropsValue } from "./use-props-value-SH9krhkx.js"; import { View, Image } from "@tarojs/components"; import { P as Progress } from "./progress.taro-cveY9Y_I.js"; class UploadOptions { constructor() { this.url = ""; this.name = "file"; this.fileType = "image"; this.method = "post"; this.xhrState = 200; this.timeout = 30 * 1e3; this.headers = {}; this.withCredentials = false; } } const UPLOADING = "uploading"; const SUCCESS = "success"; const ERROR = "error"; class Upload { constructor(options) { this.options = options; } upload() { var _a; const { options } = this; const xhr = new XMLHttpRequest(); xhr.timeout = options.timeout; if (xhr.upload) { xhr.upload.addEventListener("progress", (e) => { var _a2; (_a2 = options.onProgress) === null || _a2 === void 0 ? void 0 : _a2.call(options, e, options); }, false); xhr.onreadystatechange = () => { var _a2, _b; if (xhr.readyState === 4) { if (xhr.status === options.xhrState) { (_a2 = options.onSuccess) === null || _a2 === void 0 ? void 0 : _a2.call(options, xhr.responseText, options); } else { (_b = options.onFailure) === null || _b === void 0 ? void 0 : _b.call(options, xhr.responseText, options); } } }; xhr.withCredentials = options.withCredentials; xhr.open(options.method, options.url, true); for (const [key, value] of Object.entries(options.headers)) { xhr.setRequestHeader(key, value); } (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options, options); if (options.beforeXhrUpload) { options.beforeXhrUpload(xhr, options); } else { xhr.send(options.formData); } } else { console.warn("浏览器不支持 XMLHttpRequest"); } } } class UploaderTaro extends Upload { constructor(options) { super(options); } uploadTaro(uploadFile2, env) { var _a; const options = this.options; if (options.beforeXhrUpload) { options.beforeXhrUpload(uploadFile2, options); return; } if (env === "WEB") { this.upload(); } else { const uploadTask = uploadFile2({ url: options.url, filePath: options.taroFilePath, fileType: options.fileType, header: Object.assign({ "Content-Type": "multipart/form-data" }, options.headers), // formData: options.formData, name: options.name, success(response) { var _a2, _b; if (options.xhrState === response.statusCode) { (_a2 = options.onSuccess) === null || _a2 === void 0 ? void 0 : _a2.call(options, response, options); } else { (_b = options.onFailure) === null || _b === void 0 ? void 0 : _b.call(options, response, options); } }, fail(e) { var _a2; (_a2 = options.onFailure) === null || _a2 === void 0 ? void 0 : _a2.call(options, e, options); } }); (_a = options.onStart) === null || _a === void 0 ? void 0 : _a.call(options, options); uploadTask.progress((res) => { var _a2; (_a2 = options.onProgress) === null || _a2 === void 0 ? void 0 : _a2.call(options, res, options); }); } } } const funcInterceptor = (interceptor, { args = [], done, canceled }) => { if (interceptor) { const returnVal = interceptor.apply(null, args); if (isPromise(returnVal)) { returnVal.then((value) => { if (value) { done(value); } else if (canceled) { canceled(); } }).catch(() => { }); } else if (returnVal) { done(); } else if (canceled) { canceled(); } } else { done(); } }; class FileItem { constructor() { this.status = "ready"; this.message = ""; this.uid = (/* @__PURE__ */ new Date()).getTime().toString(); this.percentage = 0; this.formData = {}; } } const chooseFile = (options) => { const shouldBeObjectNative = (options2) => { const isObject2 = options2 !== null && typeof options2 === "object" && !Array.isArray(options2); if (!isObject2) { return { flag: false, msg: "parameter should be Object" }; } return { flag: true, msg: "" }; }; const isObject = shouldBeObjectNative(options); if (!isObject.flag) { const res2 = { errMsg: `taroChooseFile:fail ${isObject.msg}` }; console.error(res2.errMsg); return Promise.reject(res2); } const { count = 1, success, fail, complete, imageId = "taroChooseFile", sourceType = ["album", "camera"] } = options; const res = { tempFilePaths: [], tempFiles: [], errMsg: "" }; const handleSuccess = (data = {}, resolve = Promise.resolve.bind(Promise)) => { if (!data.errMsg) { data.errMsg = "taroChooseFile:ok"; } typeof success === "function" && success(data); typeof complete === "function" && complete(data); return resolve(data); }; const handleFail = (data = {}, reject = Promise.reject.bind(Promise)) => { if (!data.errMsg) { data.errMsg = "taroChooseFile:fail"; } else { data.errMsg = `taroChooseFile:fail ${data.errMsg}`; } console.error(data.errMsg); typeof fail === "function" && fail(data); typeof complete === "function" && complete(data); return reject(data); }; const sourceTypeString = sourceType && sourceType.toString(); const acceptableSourceType = ["user", "environment", "camera"]; if (count && typeof count !== "number") { res.errMsg = `parameter error: count should be number, but got ${typeof count}`; return handleFail(res); } const el = document.getElementById(imageId); if (!el) { res.errMsg = "taroChooseFile:fail element not found"; return handleFail(res); } if (count > 1) { el.setAttribute("multiple", "multiple"); } else { el.removeAttribute("multiple"); } if (acceptableSourceType.indexOf(sourceTypeString) > -1) { el.setAttribute("capture", sourceTypeString); } else { el.removeAttribute("capture"); } return new Promise((resolve) => { if (el) { const evt = new MouseEvent("click", { bubbles: true, cancelable: true, view: window }); el.dispatchEvent(evt); el.onchange = (e) => { const target = e.target; if (target) { const files = target.files || []; const arr = Array.from(files); arr.forEach((item) => { const blob = new Blob([item], { type: item.type }); const url = URL.createObjectURL(blob); res.tempFilePaths.push(url); res.tempFiles.push({ path: url, size: item.size, type: item.type, originalFileObj: item }); }); handleSuccess(res, resolve); target.value = ""; } }; } }); }; const Preview = ({ fileList, previewType, deletable, onDeleteItem, handleItemClick, previewUrl, deleteIcon, children }) => { const renderIcon = (item) => { if (item.status === ERROR) { return item.failIcon || React__default.createElement(Failure, { color: "#fff" }); } return item.loadingIcon || React__default.createElement(Loading, { className: "nut-icon-loading", color: "#fff" }); }; return React__default.createElement(React__default.Fragment, null, fileList.length !== 0 && fileList.map((item, index) => { var _a; return React__default.createElement( "div", { className: `nut-uploader-preview ${previewType}`, key: item.uid }, previewType === "picture" && !children && deletable && React__default.createElement(View, { className: "close", onClick: () => onDeleteItem(item, index) }, deleteIcon), previewType === "picture" && !children && React__default.createElement( "div", { className: "nut-uploader-preview-img" }, item.status === "ready" ? React__default.createElement( "div", { className: "nut-uploader-preview-progress" }, React__default.createElement("div", { className: "nut-uploader-preview-progress-msg" }, item.message) ) : item.status !== "success" && React__default.createElement( "div", { className: "nut-uploader-preview-progress" }, renderIcon(item), React__default.createElement("div", { className: "nut-uploader-preview-progress-msg" }, item.message) ), ((_a = item.type) === null || _a === void 0 ? void 0 : _a.includes("image")) ? React__default.createElement(React__default.Fragment, null, item.url && React__default.createElement(Image, { className: "nut-uploader-preview-img-c", style: { objectFit: "fill" }, mode: "aspectFit", src: item.url, onClick: () => handleItemClick(item, index) })) : React__default.createElement(React__default.Fragment, null, previewUrl ? React__default.createElement(Image, { className: "nut-uploader-preview-img-c", mode: "aspectFit", src: previewUrl, onClick: () => handleItemClick(item, index) }) : React__default.createElement( "div", { className: "nut-uploader-preview-img-file" }, React__default.createElement( "div", { onClick: () => handleItemClick(item, index), className: "nut-uploader-preview-img-file-name" }, React__default.createElement(Link, { color: "#808080" }), React__default.createElement( "span", null, " ", item.name ) ) )), item.status === "success" ? React__default.createElement("div", { className: "tips" }, item.name) : null ), previewType === "list" && React__default.createElement( "div", { className: "nut-uploader-preview-list" }, React__default.createElement( "div", { className: `nut-uploader-preview-img-file-name ${item.status}`, onClick: () => handleItemClick(item, index) }, React__default.createElement(Link, null), React__default.createElement( "span", null, " ", item.name ) ), deletable && React__default.createElement(Del, { color: "#808080", className: "nut-uploader-preview-img-file-del", onClick: () => onDeleteItem(item, index) }), item.status === "uploading" && React__default.createElement(Progress, { percent: item.percentage, color: "linear-gradient(270deg, rgba(18,126,255,1) 0%,rgba(32,147,255,1) 32.815625%,rgba(13,242,204,1) 100%)", showText: false }) ) ); })); }; const defaultProps = Object.assign(Object.assign({}, ComponentDefaults), { url: "", maxCount: 1, sizeType: ["original", "compressed"], sourceType: ["album", "camera"], mediaType: ["image", "video"], camera: "back", uploadIcon: React__default.createElement(Photograph, { size: "20px", color: "#808080" }), deleteIcon: React__default.createElement(Failure, { color: "rgba(0,0,0,0.6)" }), uploadLabel: "", previewType: "picture", fit: "cover", name: "file", accept: "*", disabled: false, autoUpload: true, multiple: false, maxFileSize: Number.MAX_VALUE, data: {}, headers: {}, method: "post", previewUrl: "", xhrState: 200, timeout: 1e3 * 30, preview: true, deletable: true, maxDuration: 10, beforeDelete: (file, files) => { return true; } }); const InternalUploader = (props, ref) => { const { locale } = useConfig(); const _a = Object.assign(Object.assign({}, defaultProps), props), { children, uploadIcon, deleteIcon, uploadLabel, accept, name, camera, defaultValue, value, previewType, fit, disabled, multiple, url, previewUrl, headers, timeout, method, xhrState, data, preview, deletable, maxCount, maxFileSize, mediaType, className, autoUpload, sizeType, sourceType, maxDuration, onStart, onDelete, onChange, onFileItemClick, onProgress, onSuccess, onUpdate, onFailure, onOversize, beforeUpload, beforeXhrUpload, beforeDelete, needChooseFile } = _a, restProps = __rest(_a, ["children", "uploadIcon", "deleteIcon", "uploadLabel", "accept", "name", "camera", "defaultValue", "value", "previewType", "fit", "disabled", "multiple", "url", "previewUrl", "headers", "timeout", "method", "xhrState", "data", "preview", "deletable", "maxCount", "maxFileSize", "mediaType", "className", "autoUpload", "sizeType", "sourceType", "maxDuration", "onStart", "onDelete", "onChange", "onFileItemClick", "onProgress", "onSuccess", "onUpdate", "onFailure", "onOversize", "beforeUpload", "beforeXhrUpload", "beforeDelete", "needChooseFile"]); const [fileList, setFileList] = usePropsValue({ value, defaultValue, finalValue: [], onChange: (v) => { onChange === null || onChange === void 0 ? void 0 : onChange(v); } }); const [uploadQueue, setUploadQueue] = useState([]); const classes = classNames(className, "nut-uploader"); useImperativeHandle(ref, () => ({ submit: () => { Promise.all(uploadQueue).then((res) => { res.forEach((i) => i.uploadTaro(uploadFile, getEnv())); }); }, clear: () => { clearUploadQueue(); } })); const fileListRef = useRef([]); useEffect(() => { fileListRef.current = fileList; }, [fileList]); const clearUploadQueue = (index = -1) => { if (index > -1) { uploadQueue.splice(index, 1); setUploadQueue(uploadQueue); } else { setUploadQueue([]); setFileList([]); } }; const _chooseImage = () => { if (disabled) { return; } if (Taro.getEnv() === "WEB" && needChooseFile) { const el = document.getElementById("taroChooseFile"); if (el) { el === null || el === void 0 ? void 0 : el.setAttribute("accept", accept); } else { const obj = document.createElement("input"); obj.setAttribute("type", "file"); obj.setAttribute("id", "taroChooseFile"); obj.setAttribute("accept", accept); obj.setAttribute("style", "position: fixed; top: -4000px; left: -3000px; z-index: -300;"); document.body.appendChild(obj); } chooseFile({ count: multiple ? maxCount * 1 - fileList.length : 1, // 可以指定是原图还是压缩图,默认二者都有 sizeType, sourceType, success: onChangeImage, fail: (res) => { onFailure && onFailure(res); } }); } else if (["WEAPP", "JD", "WEB"].includes(getEnv()) && chooseMedia) { chooseMedia({ count: multiple ? maxCount * 1 - fileList.length : 1, /** 文件类型 */ mediaType, /** 图片和视频选择的来源 */ sourceType, /** 拍摄视频最长拍摄时间,单位秒。时间范围为 3s 至 30s 之间 */ maxDuration, /** 仅对 mediaType 为 image 时有效,是否压缩所选文件 */ sizeType, /** 仅在 sourceType 为 camera 时生效,使用前置或后置摄像头 */ camera, fail: (res) => { onFailure && onFailure(res); }, success: onChangeMedia }); } else { chooseImage({ count: multiple ? maxCount * 1 - fileList.length : 1, // 可以指定是原图还是压缩图,默认二者都有 sizeType, sourceType, success: onChangeImage, fail: (res) => { onFailure && onFailure(res); } }); } }; const executeUpload = (fileItem, index) => { const uploadOption = new UploadOptions(); uploadOption.name = name; uploadOption.url = url; uploadOption.fileType = fileItem.type; uploadOption.formData = fileItem.formData; uploadOption.timeout = timeout * 1; uploadOption.method = method; uploadOption.xhrState = xhrState; uploadOption.headers = headers; uploadOption.taroFilePath = fileItem.path; uploadOption.beforeXhrUpload = beforeXhrUpload; uploadOption.onStart = (option) => { clearUploadQueue(index); setFileList(fileListRef.current.map((item) => { if (item.uid === fileItem.uid) { item.status = "ready"; item.message = locale.uploader.readyUpload; } return item; })); onStart === null || onStart === void 0 ? void 0 : onStart(option); }; uploadOption.onProgress = (e, option) => { setFileList(fileListRef.current.map((item) => { if (item.uid === fileItem.uid) { item.status = UPLOADING; item.message = locale.uploader.uploading; item.percentage = e.progress; onProgress === null || onProgress === void 0 ? void 0 : onProgress({ e, option, percentage: item.percentage }); } return item; })); }; uploadOption.onSuccess = (responseText, option) => { const list = fileListRef.current.map((item) => { if (item.uid === fileItem.uid) { item.status = SUCCESS; item.message = locale.uploader.success; item.responseText = responseText; } return item; }); setFileList(list); onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess({ responseText, option, files: list }); }; uploadOption.onFailure = (responseText, option) => { const list = fileListRef.current.map((item) => { if (item.uid === fileItem.uid) { item.status = ERROR; item.message = locale.uploader.error; item.responseText = responseText; } return item; }); setFileList(list); onFailure === null || onFailure === void 0 ? void 0 : onFailure({ responseText, option, files: list }); }; const task = new UploaderTaro(uploadOption); if (autoUpload) { task.uploadTaro(uploadFile, getEnv()); } else { uploadQueue.push(new Promise((resolve, reject) => { resolve(task); })); setUploadQueue(uploadQueue); } }; const readFile = (files) => { const results = []; files.forEach((file, index) => { var _a2, _b; let fileType = file.type; const filepath = file.tempFilePath || file.path; const fileItem = new FileItem(); if (file.fileType) { fileType = file.fileType; } else { const imgReg = /\.(png|jpeg|jpg|webp|gif)$/i; if (!fileType && (imgReg.test(filepath) || filepath.includes("data:image"))) { fileType = "image"; } } fileItem.path = filepath; fileItem.name = filepath; fileItem.status = "ready"; fileItem.type = fileType; fileItem.uid = `${fileItem.uid}_${index}`; fileItem.message = autoUpload ? locale.uploader.readyUpload : locale.uploader.waitingUpload; if (getEnv() === "WEB") { const formData = new FormData(); for (const [key, value2] of Object.entries(data)) { formData.append(key, value2); } formData.append(name, file.originalFileObj); fileItem.name = (_a2 = file.originalFileObj) === null || _a2 === void 0 ? void 0 : _a2.name; fileItem.type = (_b = file.originalFileObj) === null || _b === void 0 ? void 0 : _b.type; fileItem.formData = formData; } else { fileItem.formData = data; } if (preview) { fileItem.url = fileType === "video" ? file.thumbTempFilePath : filepath; } executeUpload(fileItem, index); results.push(fileItem); }); setFileList([...fileList, ...results]); }; const filterFiles = (files) => { const maximum = maxCount * 1; const maximize = maxFileSize * 1; const oversizes = new Array(); const filterFile = files.filter((file) => { if (file.size > maximize) { oversizes.push(file); return false; } return true; }); oversizes.length && (onOversize === null || onOversize === void 0 ? void 0 : onOversize(files)); const currentFileLength = filterFile.length + fileList.length; if (currentFileLength > maximum) { filterFile.splice(filterFile.length - (currentFileLength - maximum)); } return filterFile; }; const deleted = (file, index) => { const deletedFileList = fileList.filter((file2, idx) => idx !== index); onDelete === null || onDelete === void 0 ? void 0 : onDelete(file, deletedFileList); setFileList(deletedFileList); }; const onDeleteItem = (file, index) => { clearUploadQueue(index); funcInterceptor(beforeDelete, { args: [file, fileList], done: () => deleted(file, index) }); }; const onChangeMedia = (res) => { const { tempFiles } = res; const _files = filterFiles(tempFiles); if (beforeUpload) { beforeUpload(new Array().slice.call(_files)).then((f) => { const _files2 = filterFiles(new Array().slice.call(f)); if (!_files2.length) res.tempFiles = []; readFile(_files2); }); } else { readFile(_files); } }; const onChangeImage = (res) => { const { tempFiles } = res; const _files = filterFiles(tempFiles); if (beforeUpload) { beforeUpload(new Array().slice.call(_files)).then((f) => { const _files2 = filterFiles(new Array().slice.call(f)); if (!_files2.length) res.tempFiles = []; readFile(_files2); }); } else { readFile(_files); } }; const handleItemClick = (file, index) => { onFileItemClick === null || onFileItemClick === void 0 ? void 0 : onFileItemClick(file, index); }; return React__default.createElement( "div", Object.assign({ className: classes }, restProps), (children || previewType === "list") && React__default.createElement( "div", { className: "nut-uploader-slot" }, React__default.createElement( React__default.Fragment, null, children || React__default.createElement(Button, { nativeType: "button", size: "small", type: "primary" }, locale.uploader.list), Number(maxCount) > fileList.length && React__default.createElement(Button, { nativeType: "button", className: "nut-uploader-input", onClick: _chooseImage }) ) ), React__default.createElement(Preview, { fileList, previewType, deletable, onDeleteItem, handleItemClick, previewUrl, deleteIcon, children }), Number(maxCount) > fileList.length && previewType === "picture" && !children && React__default.createElement( "div", { className: `nut-uploader-upload ${previewType} ${disabled ? "nut-uploader-upload-disabled" : ""}` }, React__default.createElement( "div", { className: "nut-uploader-icon" }, uploadIcon, React__default.createElement("span", { className: "nut-uploader-icon-tip" }, uploadLabel) ), React__default.createElement(Button, { nativeType: "button", className: "nut-uploader-input", onClick: _chooseImage }) ) ); }; const Uploader = React__default.forwardRef(InternalUploader); Uploader.displayName = "NutUploader"; export { Uploader as U };