index.tsx 7.6 KB

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