@nutui/nutui-react-taro
Version:
京东风格的轻量级移动端 React 组件库,支持一套代码生成 H5 和小程序
618 lines (617 loc) • 23.8 kB
JavaScript
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
};