index.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. import React, {
  2. useCallback,
  3. useEffect,
  4. useMemo,
  5. useRef,
  6. useState,
  7. } from "react";
  8. import styles from "./index.module.scss";
  9. import { Button, Checkbox, Input, Modal } from "antd";
  10. import { forwardRef, useImperativeHandle } from "react";
  11. import { baseURL } from "@/utils/http";
  12. import {
  13. PlusOutlined,
  14. CloseCircleOutlined,
  15. UploadOutlined,
  16. CloseOutlined,
  17. DownloadOutlined,
  18. EyeOutlined,
  19. } from "@ant-design/icons";
  20. import { MessageFu } from "@/utils/message";
  21. import { API_upFile } from "@/store/action/layout";
  22. import { fileDomInitialFu } from "@/utils/domShow";
  23. import store from "@/store";
  24. import ImageLazy from "../ImageLazy";
  25. import classNames from "classnames";
  26. import MyPopconfirm from "../MyPopconfirm";
  27. // import { A2_APIchangeImgName } from "@/store/action/A2exhibition";
  28. export type FileListType = {
  29. fileName: string;
  30. thumb?: string;
  31. filePath: string;
  32. id: number;
  33. type: "model" | "img" | "audio" | "video";
  34. imgName: string;
  35. };
  36. type Props = {
  37. ref: any; //当前自己的ref,给父组件调用
  38. selecFlag: string; //筛选的字符串 模型/图片/音频/视频
  39. fileCheck: boolean; //有没有点击过确定
  40. dirCode: string; //文件的code码
  41. myUrl: string; //请求地址
  42. isLook?: boolean; //是不是查看
  43. modelSize?: number; //模型文件大小限制
  44. imgSize?: number; //图片大小限制
  45. imgLength?: number; //图片数量限制
  46. audioSize?: number; //音频大小限制
  47. videoSize?: number; //视频大小限制
  48. videoTit?: string; //视频上传的提示语
  49. isTypeShow?: boolean; //默认就选中(只有一个类型的时候)
  50. isUpName?: boolean; //是否能修改图片名字
  51. lastImgTxt?: string; //加载最后面的上传提示
  52. oneIsCover?: boolean; //是否将第一张作为封面
  53. };
  54. function ZupTypes(
  55. {
  56. selecFlag,
  57. fileCheck,
  58. dirCode,
  59. myUrl,
  60. isLook = false,
  61. modelSize = 500,
  62. imgSize = 5,
  63. imgLength = 9,
  64. audioSize = 10,
  65. videoSize = 500,
  66. videoTit = "",
  67. isTypeShow = false,
  68. isUpName = false,
  69. lastImgTxt = "",
  70. oneIsCover = false,
  71. }: Props,
  72. ref: any
  73. ) {
  74. // 筛选
  75. const [typeCheck, setTypeCheck] = useState<string[]>([]);
  76. // 筛选数组
  77. const typeCheckArr = useMemo(() => {
  78. const arr = [
  79. { label: "模型", value: "model" },
  80. { label: "图片", value: "img" },
  81. { label: "音频", value: "audio" },
  82. { label: "视频", value: "video" },
  83. ];
  84. const arrRes = arr.filter((v) => selecFlag.includes(v.label));
  85. if (arrRes.length <= 1 && isTypeShow) {
  86. setTypeCheck([arrRes[0].value]);
  87. // 默认就选中(只有一个类型的时候)
  88. }
  89. return arrRes;
  90. }, [isTypeShow, selecFlag]);
  91. // 上传附件的信息
  92. const [fileList, setFileList] = useState({
  93. model: {} as FileListType,
  94. img: [] as FileListType[],
  95. audio: {} as FileListType,
  96. video: {} as FileListType,
  97. });
  98. // 附件信息的校验,不满足返回 true
  99. const fileCheckFu = useMemo(() => {
  100. let flag = false;
  101. if (typeCheck.length === 0) flag = true;
  102. if (typeCheck.includes("model") && !fileList.model.id) flag = true;
  103. if (typeCheck.includes("img") && fileList.img.length === 0) flag = true;
  104. if (typeCheck.includes("audio") && !fileList.audio.id) flag = true;
  105. if (typeCheck.includes("video") && !fileList.video.id) flag = true;
  106. return flag;
  107. }, [fileList, typeCheck]);
  108. // 点击上传附件按钮
  109. const myInput = useRef<HTMLInputElement>(null);
  110. const [fileOneType, setFileOneType] = useState("");
  111. useEffect(() => {
  112. if (fileOneType) myInput.current?.click();
  113. }, [fileOneType]);
  114. const upFileFu = useCallback((type: string) => {
  115. setFileOneType("");
  116. window.setTimeout(() => {
  117. setFileOneType(type);
  118. }, 100);
  119. }, []);
  120. // 上传附件的处理函数
  121. const handeUpPhoto2 = useCallback(
  122. async (e: React.ChangeEvent<HTMLInputElement>) => {
  123. if (e.target.files) {
  124. // 拿到files信息
  125. const filesInfo = e.target.files[0];
  126. let anType = ["image/jpeg", "image/png"];
  127. let anTit1 = "只支持png、jpg格式!";
  128. let anTit2 = `最大支持${imgSize}M!`;
  129. let anSize = imgSize * 1024 * 1024;
  130. if (fileOneType === "audio") {
  131. anType = ["audio/mpeg"];
  132. anTit1 = "只支持mp3格式!";
  133. anTit2 = `最大支持${audioSize}M!`;
  134. anSize = audioSize * 1024 * 1024;
  135. } else if (fileOneType === "video") {
  136. anType = ["video/mp4"];
  137. anTit1 = "只支持mp4格式!";
  138. anTit2 = `最大支持${videoSize}M!`;
  139. anSize = videoSize * 1024 * 1024;
  140. } else if (fileOneType === "model") {
  141. anType = [""];
  142. anTit1 = "只支持4dage格式!";
  143. anTit2 = `最大支持${modelSize}M!`;
  144. anSize = modelSize * 1024 * 1024;
  145. }
  146. // 校验格式
  147. if (fileOneType !== "model") {
  148. if (!anType.includes(filesInfo.type)) {
  149. e.target.value = "";
  150. return MessageFu.warning(anTit1);
  151. }
  152. } else {
  153. if (!filesInfo.name.includes(".4dage")) {
  154. e.target.value = "";
  155. return MessageFu.warning(anTit1);
  156. }
  157. }
  158. // 校验大小
  159. if (filesInfo.size > anSize) {
  160. e.target.value = "";
  161. return MessageFu.warning(anTit2);
  162. }
  163. // 创建FormData对象
  164. const fd = new FormData();
  165. // 把files添加进FormData对象(‘photo’为后端需要的字段)
  166. fd.append("type", fileOneType);
  167. fd.append("dirCode", dirCode);
  168. fd.append("isDb", "true");
  169. //初始图片 fileName为:未命名
  170. if (isUpName) {
  171. fd.append("isDefaultName", "false");
  172. }
  173. fd.append("file", filesInfo);
  174. if (fileOneType === "img" && filesInfo.size > 1 * 1024 * 1024) {
  175. // 开启压缩图片
  176. fd.append("isCompress", "true");
  177. }
  178. e.target.value = "";
  179. const res = await API_upFile(fd, myUrl);
  180. try {
  181. if (res.code === 0) {
  182. MessageFu.success("上传成功!");
  183. if (fileOneType === "img")
  184. setFileList({
  185. ...fileList,
  186. img: [{ ...res.data, imgName: "未命名" }, ...fileList.img],
  187. });
  188. else setFileList({ ...fileList, [fileOneType]: res.data });
  189. }
  190. fileDomInitialFu();
  191. } catch (error) {
  192. fileDomInitialFu();
  193. }
  194. }
  195. },
  196. [
  197. audioSize,
  198. dirCode,
  199. fileList,
  200. fileOneType,
  201. imgSize,
  202. isUpName,
  203. modelSize,
  204. myUrl,
  205. videoSize,
  206. ]
  207. );
  208. // 附件图片的拖动
  209. const [dragImg, setDragImg] = useState<any>(null);
  210. const handleDragOver = useCallback(
  211. (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
  212. if (isLook) return;
  213. e.dataTransfer.dropEffect = "move";
  214. },
  215. [isLook]
  216. );
  217. const handleDragEnter = useCallback(
  218. (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
  219. if (isLook) return;
  220. e.dataTransfer.effectAllowed = "move";
  221. if (item === dragImg) return;
  222. const newItems = [...fileList.img]; //拷贝一份数据进行交换操作。
  223. const src = newItems.indexOf(dragImg); //获取数组下标
  224. const dst = newItems.indexOf(item);
  225. newItems.splice(dst, 0, ...newItems.splice(src, 1)); //交换位置
  226. setFileList({ ...fileList, img: newItems });
  227. },
  228. [dragImg, fileList, isLook]
  229. );
  230. // 删除某一张图片
  231. const delImgListFu = useCallback(
  232. (id: number) => {
  233. const newItems = fileList.img.filter((v) => v.id !== id);
  234. setFileList({ ...fileList, img: newItems });
  235. },
  236. [fileList]
  237. );
  238. // 模型 音频 视频 的 dom
  239. const resOneDivDom = useCallback(
  240. (type: "model" | "audio" | "video") => {
  241. const dom = (
  242. <div className="ZTbox" hidden={!typeCheck.includes(type)}>
  243. <div className="ZTbox1">
  244. <span> </span>
  245. {type === "model" ? "模型" : type === "audio" ? "音频" : "视频"}:
  246. </div>
  247. {fileList[type].id ? (
  248. <div className="ZTbox2">
  249. <div className="ZTbox2Name">{fileList[type].fileName}</div>
  250. <div
  251. className="ZTbox2Look"
  252. onClick={() =>
  253. store.dispatch({
  254. type: "layout/lookDom",
  255. payload: { src: fileList[type].filePath, type },
  256. })
  257. }
  258. >
  259. <EyeOutlined rev={undefined} />
  260. </div>
  261. <a
  262. href={baseURL + fileList[type].filePath}
  263. download
  264. target="_blank"
  265. className="ZTbox2Down"
  266. rel="noreferrer"
  267. >
  268. <DownloadOutlined rev={undefined} />
  269. </a>
  270. <MyPopconfirm
  271. txtK="删除"
  272. onConfirm={() =>
  273. setFileList({ ...fileList, [type]: {} as FileListType })
  274. }
  275. Dom={
  276. <CloseCircleOutlined className="ZTbox2X" rev={undefined} />
  277. }
  278. />
  279. </div>
  280. ) : (
  281. <>
  282. <Button
  283. onClick={() => upFileFu(type)}
  284. icon={<UploadOutlined rev={undefined} />}
  285. >
  286. 上传
  287. </Button>
  288. <div className="ZTboxTit">
  289. {type === "model"
  290. ? `仅支持4dage格式的模型文件,大小不能超过${modelSize}M。`
  291. : type === "audio"
  292. ? `仅支持mp3格式的音频文件,大小不得超过${audioSize}MB。`
  293. : `仅支持mp4格式的视频文件,大小不得超过${videoSize}MB。${videoTit}`}
  294. </div>
  295. </>
  296. )}
  297. </div>
  298. );
  299. return dom;
  300. },
  301. [audioSize, fileList, modelSize, typeCheck, upFileFu, videoSize, videoTit]
  302. );
  303. // ------------让父组件调用的 回显
  304. const setFileComFileFu = useCallback((info: any) => {
  305. if (info.type) setTypeCheck(info.type.split(","));
  306. if (info.fileList && info.fileList.length) {
  307. const data: FileListType[] = info.fileList;
  308. const obj = {
  309. model: {} as FileListType,
  310. img: [] as FileListType[],
  311. audio: {} as FileListType,
  312. video: {} as FileListType,
  313. };
  314. data.forEach((v) => {
  315. if (v.type === "img") {
  316. obj.img.push({ ...v, imgName: v.fileName });
  317. } else obj[v.type!] = v;
  318. });
  319. setFileList(obj);
  320. }
  321. }, []);
  322. // --------------让父组件调用的返回 附件 信息
  323. const fileComFileResFu = useCallback(() => {
  324. let coverUrl = "";
  325. const fileIds = [];
  326. if (fileList.model.id && typeCheck.includes("model"))
  327. fileIds.push(fileList.model.id);
  328. if (fileList.audio.id && typeCheck.includes("audio"))
  329. fileIds.push(fileList.audio.id);
  330. if (fileList.video.id && typeCheck.includes("video"))
  331. fileIds.push(fileList.video.id);
  332. if (typeCheck.includes("img")) {
  333. fileList.img.forEach((v, i) => {
  334. if (v.id) {
  335. fileIds.push(v.id);
  336. if (oneIsCover && i === 0) {
  337. // 返回 第一张图的url 作为封面
  338. coverUrl = v.thumb || v.filePath;
  339. }
  340. }
  341. });
  342. }
  343. return {
  344. sonType: typeCheck,
  345. sonFileIds: fileIds,
  346. sonIsOk: fileCheckFu,
  347. coverUrl,
  348. };
  349. }, [
  350. fileCheckFu,
  351. fileList.audio.id,
  352. fileList.img,
  353. fileList.model.id,
  354. fileList.video.id,
  355. oneIsCover,
  356. typeCheck,
  357. ]);
  358. // 可以让父组件调用子组件的方法
  359. useImperativeHandle(ref, () => ({
  360. setFileComFileFu,
  361. fileComFileResFu,
  362. }));
  363. // 修改图片名称
  364. const [isNameChange, setIsNameChange] = useState({
  365. id: 0,
  366. oldName: "",
  367. newName: "",
  368. });
  369. // 关闭弹窗
  370. const isNameChangeXFu = useCallback(() => {
  371. setIsNameChange({ id: 0, oldName: "", newName: "" });
  372. }, []);
  373. // 点击图片名字-出来弹窗
  374. const isNameChangeFu = useCallback(
  375. (item: FileListType) => {
  376. if (isLook) return;
  377. setIsNameChange({ id: item.id, oldName: item.imgName, newName: "" });
  378. },
  379. [isLook]
  380. );
  381. // 修改完这点击 确定修改
  382. const isNameChangeOkFu = useCallback(async () => {
  383. // if (!isNameChange.newName) return MessageFu.warning("图片名不能为空!");
  384. // const res = await A2_APIchangeImgName({
  385. // id: isNameChange.id,
  386. // fileName: isNameChange.newName,
  387. // });
  388. // if (res.code === 0) {
  389. // MessageFu.success("修改图片名成功!");
  390. // setFileList({
  391. // ...fileList,
  392. // img: fileList.img.map((v) => ({
  393. // ...v,
  394. // imgName: v.id === isNameChange.id ? isNameChange.newName : v.imgName,
  395. // })),
  396. // });
  397. // isNameChangeXFu();
  398. // }
  399. }, []);
  400. //
  401. return (
  402. <div
  403. className={classNames(styles.ZupTypes, isLook ? styles.ZupTypesLook : "")}
  404. >
  405. <input
  406. id="upInput"
  407. type="file"
  408. accept={
  409. fileOneType === "img"
  410. ? ".png,.jpg,.jpeg"
  411. : fileOneType === "audio"
  412. ? ".mp3"
  413. : fileOneType === "model"
  414. ? ".4dage"
  415. : ".mp4"
  416. }
  417. ref={myInput}
  418. onChange={(e) => handeUpPhoto2(e)}
  419. />
  420. <div hidden={isTypeShow}>
  421. <Checkbox.Group
  422. options={typeCheckArr}
  423. value={typeCheck}
  424. onChange={(e) => setTypeCheck(e as string[])}
  425. />
  426. </div>
  427. {/* -----------模型 */}
  428. {resOneDivDom("model")}
  429. {/* -----------图片 */}
  430. <div className="ZTboxImgMain" hidden={!typeCheck.includes("img")}>
  431. <div className="ZTboxImgBox">
  432. <div className="ZTbox1" hidden={isTypeShow}>
  433. <span> </span> 图片:
  434. </div>
  435. <div
  436. className="ZTbox1Img"
  437. style={{ width: isTypeShow ? "100%" : "" }}
  438. >
  439. <div
  440. hidden={
  441. (!!fileList.img.length && fileList.img.length >= imgLength) ||
  442. isLook
  443. }
  444. className="ZTbox1ImgIcon"
  445. onClick={() => upFileFu("img")}
  446. >
  447. <PlusOutlined rev={undefined} />
  448. </div>
  449. {fileList.img.map((v, i) => (
  450. <div
  451. className="ZTbox1ImgRow"
  452. key={v.id}
  453. draggable="true"
  454. onDragStart={() => setDragImg(v)}
  455. onDragOver={(e) => handleDragOver(e, v)}
  456. onDragEnter={(e) => handleDragEnter(e, v)}
  457. onDragEnd={() => setDragImg(null)}
  458. >
  459. {v.thumb || v.filePath ? (
  460. <ImageLazy
  461. noLook={true}
  462. width={100}
  463. height={100}
  464. src={v.thumb || v.filePath}
  465. />
  466. ) : null}
  467. {oneIsCover && i === 0 ? (
  468. <div className="ZTbox1ImgRowCover">封面</div>
  469. ) : null}
  470. {/* 修改图片名字 */}
  471. {isUpName ? (
  472. <div
  473. title={v.imgName}
  474. className="ZTbox1ImgRowName"
  475. onClick={() => isNameChangeFu(v)}
  476. >
  477. {v.imgName}
  478. </div>
  479. ) : null}
  480. <div className="ZTbox1ImgRowIcon">
  481. <EyeOutlined
  482. onClick={() =>
  483. store.dispatch({
  484. type: "layout/lookBigImg",
  485. payload: {
  486. url: baseURL + v.filePath,
  487. show: true,
  488. },
  489. })
  490. }
  491. rev={undefined}
  492. />
  493. <a
  494. href={baseURL + v.filePath}
  495. download
  496. target="_blank"
  497. rel="noreferrer"
  498. >
  499. <DownloadOutlined rev={undefined} />
  500. </a>
  501. </div>
  502. <MyPopconfirm
  503. txtK="删除"
  504. onConfirm={() => delImgListFu(v.id!)}
  505. Dom={
  506. <CloseOutlined className="ZTbox1ImgRowX" rev={undefined} />
  507. }
  508. />
  509. </div>
  510. ))}
  511. </div>
  512. </div>
  513. <div className="ZTboxTit" hidden={isLook}>
  514. {fileList.img.length && fileList.img.length >= 2 ? (
  515. <>
  516. 按住鼠标可拖动图片调整顺序。
  517. <br />
  518. </>
  519. ) : null}
  520. 支持png、jpg的图片格式;最大支持5M;最多支持{imgLength}张。
  521. {lastImgTxt}
  522. </div>
  523. </div>
  524. {/* -----------音频 */}
  525. {resOneDivDom("audio")}
  526. {/* -----------视频 */}
  527. {resOneDivDom("video")}
  528. {/* 最后的提示 */}
  529. <div
  530. className={classNames(
  531. "ZcheckTxt",
  532. fileCheck && fileCheckFu ? "ZcheckTxtAc" : ""
  533. )}
  534. >
  535. 请最少上传一张图片!
  536. </div>
  537. {/* 点击修改名字出来的弹窗 */}
  538. {isNameChange.id ? (
  539. <Modal
  540. wrapClassName={styles.ZupTypesMo}
  541. open={true}
  542. title="修改展品图片名称"
  543. footer={
  544. [] // 设置footer为空,去掉 取消 确定默认按钮
  545. }
  546. >
  547. <br />
  548. <div className="ZupTypesMoRow">
  549. <strong>当前名:</strong>
  550. {isNameChange.oldName}
  551. </div>
  552. <div className="ZupTypesMoRow">
  553. <br />
  554. <strong>修改为:</strong>
  555. <Input
  556. style={{ width: 400 }}
  557. placeholder="请输入图片名"
  558. maxLength={50}
  559. showCount
  560. value={isNameChange.newName}
  561. onChange={(e) => {
  562. setIsNameChange({
  563. ...isNameChange,
  564. newName: e.target.value.replace(/\s+/g, ""),
  565. });
  566. }}
  567. />
  568. </div>
  569. <div className="ZupTypesMoBtn">
  570. <Button onClick={isNameChangeXFu}>取消</Button>
  571. &emsp;
  572. <Button type="primary" onClick={isNameChangeOkFu}>
  573. 修改
  574. </Button>
  575. </div>
  576. </Modal>
  577. ) : null}
  578. </div>
  579. );
  580. }
  581. export default forwardRef(ZupTypes);