index.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import { Button, Modal, Upload, message } from "antd";
  2. import type { UploadRequestOption as RcCustomRequestOptions } from "rc-upload/lib/interface";
  3. import {
  4. CloudUploadOutlined,
  5. PlusOutlined,
  6. UploadOutlined,
  7. } from "@ant-design/icons";
  8. import { updateFileList } from "antd/es/upload/utils";
  9. import { FC, useContext, useEffect, useMemo, useRef, useState } from "react";
  10. import { requestByPost, getBaseURL } from "@dage/service";
  11. import { RcFile, UploadFile, UploadProps } from "antd/es/upload";
  12. import {
  13. DageFileAPIResponseType,
  14. DageUploadProps,
  15. DageUploadType,
  16. } from "./types";
  17. import {
  18. AntdDragger,
  19. AntdDraggerText,
  20. AntdUpload,
  21. UploadContainer,
  22. UploadTips,
  23. } from "./style";
  24. import { DageUploadContext } from "./context";
  25. import { DageUploadItemActions } from "./ItemActions";
  26. import { getImageSize } from "./utils";
  27. const getBase64 = (file: RcFile): Promise<string> =>
  28. new Promise((resolve, reject) => {
  29. const reader = new FileReader();
  30. reader.readAsDataURL(file);
  31. reader.onload = () => resolve(reader.result as string);
  32. reader.onerror = (error) => reject(error);
  33. });
  34. export const DageUpload: FC<DageUploadProps> = ({
  35. action,
  36. value,
  37. dType = DageUploadType.IMG,
  38. maxCount = 9,
  39. maxSize = 5,
  40. tips,
  41. disabled,
  42. name = "file",
  43. downloadErrorMessage = "下载失败",
  44. onChange,
  45. ...rest
  46. }) => {
  47. const context = useContext(DageUploadContext);
  48. // 内部维护 mergeFileList
  49. // 修复 reat18 受控模式下批量更新导致状态异常
  50. const _mergedFileList = useRef<
  51. (UploadFile<any> | Readonly<UploadFile<any>>)[]
  52. >([]);
  53. const [previewOpen, setPreviewOpen] = useState(false);
  54. const [previewImage, setPreviewImage] = useState("");
  55. const [previewTitle, setPreviewTitle] = useState("");
  56. const uploadListType = useMemo(() => {
  57. switch (dType) {
  58. case DageUploadType.IMG:
  59. return "picture-card";
  60. default:
  61. return "text";
  62. }
  63. }, [dType]);
  64. const isPictureCard = uploadListType === "picture-card";
  65. // 可选文件类型
  66. const accept = useMemo(() => {
  67. switch (dType) {
  68. case DageUploadType.IMG:
  69. return ".jpg,.jpeg,.png,.gif";
  70. case DageUploadType.MOBILE_MODEL:
  71. return ".zip";
  72. case DageUploadType.MODEL:
  73. return ".4dage";
  74. case DageUploadType.VIDEO:
  75. return ".mp4";
  76. case DageUploadType.AUDIO:
  77. return ".mp3";
  78. default:
  79. return "*";
  80. }
  81. }, [dType]);
  82. useEffect(() => {
  83. if (!value) return;
  84. _mergedFileList.current = value;
  85. }, [value]);
  86. const beforeUpload = (file: RcFile) => {
  87. let passFileType = false;
  88. // 校验文件类型
  89. const fileName = file.name.toLowerCase();
  90. const fileExtension = fileName.substring(fileName.lastIndexOf("."));
  91. passFileType = accept.split(",").includes(fileExtension);
  92. if (!passFileType) {
  93. message.error(tips || "选择的文件类型不正确!");
  94. return Upload.LIST_IGNORE;
  95. }
  96. // 校验文件大小
  97. const isLtM = file.size / 1024 / 1024 < maxSize;
  98. if (!isLtM) {
  99. message.error(`最大支持 ${maxSize}M!`);
  100. return Upload.LIST_IGNORE;
  101. }
  102. context?.handleUploadingFileNum("add");
  103. return true;
  104. };
  105. const onUpload = (option: RcCustomRequestOptions) => {
  106. const formData = new FormData();
  107. const headers = option.headers || {};
  108. formData.append("type", dType);
  109. if (option.file instanceof Blob) {
  110. formData.append(name, option.file, (option.file as any).name);
  111. } else {
  112. formData.append(name, option.file);
  113. }
  114. option.data &&
  115. Object.keys(option.data).forEach((key) => {
  116. const value = option.data?.[key];
  117. if (Array.isArray(value)) {
  118. value.forEach((item) => {
  119. formData.append(`${key}[]`, item);
  120. });
  121. return;
  122. }
  123. formData.append(key, value as string | Blob);
  124. });
  125. requestByPost<DageFileAPIResponseType>(action, formData, {
  126. headers,
  127. })
  128. .then((data) => {
  129. option.onSuccess?.(data);
  130. })
  131. .catch((err) => {
  132. context?.handleUploadingFileNum("uploaded");
  133. option.onError?.(err);
  134. });
  135. };
  136. const handleChange: UploadProps["onChange"] = async ({
  137. fileList: newFileList,
  138. file,
  139. }) => {
  140. if (file.status === "done") {
  141. if (typeof file.response !== "object" || !file.response.fileName) {
  142. // 上传失败
  143. file.status = "error";
  144. } else if (isPictureCard && file.originFileObj) {
  145. // 如果是图片获取图片宽高
  146. try {
  147. // @ts-ignore
  148. file.imgAttrs = await getImageSize(file.originFileObj);
  149. // todo: 缩略图并发上传有时候会丢失
  150. file.thumbUrl = getBaseURL() + file.response.filePath;
  151. } catch (err) {
  152. // 图片加载失败
  153. file.status = "error";
  154. }
  155. }
  156. context?.handleUploadingFileNum("uploaded");
  157. }
  158. if (_mergedFileList.current.length > newFileList.length) {
  159. _mergedFileList.current = newFileList;
  160. } else {
  161. _mergedFileList.current = updateFileList(file, _mergedFileList.current);
  162. }
  163. onChange?.(
  164. _mergedFileList.current.map((i) => ({
  165. ...i,
  166. dType,
  167. })),
  168. {
  169. ...file,
  170. dType,
  171. }
  172. );
  173. };
  174. const handleCancel = () => setPreviewOpen(false);
  175. const handlePreview = async (file: UploadFile) => {
  176. if (!isPictureCard) return;
  177. if (!file.url && !file.preview) {
  178. file.preview = await getBase64(file.originFileObj as RcFile);
  179. }
  180. setPreviewImage(file.url || (file.preview as string));
  181. setPreviewOpen(true);
  182. setPreviewTitle(
  183. file.name || file.url!.substring(file.url!.lastIndexOf("/") + 1)
  184. );
  185. };
  186. const props: UploadProps = {
  187. fileList: value,
  188. withCredentials: true,
  189. customRequest: onUpload,
  190. listType: uploadListType,
  191. maxCount,
  192. accept,
  193. disabled,
  194. itemRender: (node, file, fileList, actions) => (
  195. <DageUploadItemActions
  196. isPictureCard={isPictureCard}
  197. disabled={disabled}
  198. file={file}
  199. actions={actions}
  200. downloadErrorMessage={downloadErrorMessage}
  201. >
  202. {node}
  203. </DageUploadItemActions>
  204. ),
  205. showUploadList: {
  206. showPreviewIcon: false,
  207. showDownloadIcon: false,
  208. showRemoveIcon: false,
  209. },
  210. multiple: maxCount > 1,
  211. onPreview: handlePreview,
  212. beforeUpload: beforeUpload,
  213. onChange: handleChange,
  214. ...rest,
  215. };
  216. return (
  217. <UploadContainer>
  218. {[DageUploadType.MODEL, DageUploadType.MOBILE_MODEL].includes(dType) ? (
  219. <AntdDragger style={{ padding: "0 24px", width: "320px" }} {...props}>
  220. <CloudUploadOutlined style={{ fontSize: "40px", color: "#1677ff" }} />
  221. <AntdDraggerText>
  222. 将文件拖到此处,或<span>点击上传</span>
  223. </AntdDraggerText>
  224. <UploadTips className="dage-upload__tips">{tips}</UploadTips>
  225. </AntdDragger>
  226. ) : (
  227. <>
  228. <AntdUpload {...props}>
  229. {isPictureCard ? (
  230. !disabled &&
  231. (!value || value.length < maxCount) && <PlusOutlined />
  232. ) : (
  233. <Button disabled={disabled} icon={<UploadOutlined />}>
  234. 上传
  235. </Button>
  236. )}
  237. </AntdUpload>
  238. {!!tips && (
  239. <UploadTips
  240. style={{ marginTop: isPictureCard ? 0 : "8px" }}
  241. className="dage-upload__tips"
  242. >
  243. {tips}
  244. </UploadTips>
  245. )}
  246. </>
  247. )}
  248. <Modal
  249. open={previewOpen}
  250. title={previewTitle}
  251. footer={null}
  252. onCancel={handleCancel}
  253. >
  254. <img alt="example" style={{ width: "100%" }} src={previewImage} />
  255. </Modal>
  256. </UploadContainer>
  257. );
  258. };
  259. export * from "./types";
  260. export * from "./context";