Browse Source

馆藏管理

shaogen1995 1 năm trước cách đây
mục cha
commit
22fdd9e4f7

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 6333 - 0
public/4dage.js


+ 34 - 0
public/model.html

@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html lang="zh">
+
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <script src="./4dage.js"></script>
+  <title>Document</title>
+  <style>
+    html {
+      overflow: hidden;
+    }
+  </style>
+</head>
+
+<body>
+  <div id="ui"></div>
+  <script>
+    let url = getQueryVariable("m");
+    fdage.embed(url, {
+      // fdage.embed('https://ypbwg.4dage.com' + url, {
+      // fdage.embed('http://192.168.20.61:8064/api' + url, {
+      transparentBackground: true,
+      width: 800,
+      height: 600,
+      autoStart: true,
+      fullFrame: true,
+      pagePreset: false
+    });
+  </script>
+</body>
+
+</html>

+ 147 - 0
src/components/ZupTypes/index.module.scss

@@ -0,0 +1,147 @@
+.ZupTypes {
+  padding-top: 3px;
+
+  :global {
+    .ZTboxTit {
+      margin-left: 10px;
+      font-size: 14px;
+      color: rgb(126, 124, 124);
+      position: relative;
+      top: 4px;
+    }
+
+    .ZTbox {
+      margin-top: 20px;
+      display: flex;
+
+      .ZTbox1 {
+        position: relative;
+        top: 3px;
+        width: 60px;
+
+        &>span {
+          color: #ff4d4f;
+        }
+      }
+
+      .ZTbox2 {
+        width: calc(100% - 60px);
+        margin-left: 5px;
+        display: flex;
+        font-size: 16px;
+        align-items: center;
+
+
+        .ZTbox2Look {
+          margin-left: 20px;
+          cursor: pointer;
+        }
+
+        .ZTbox2Down {
+          margin-left: 15px;
+          color: black;
+        }
+
+        .ZTbox2X {
+          cursor: pointer;
+          margin-left: 15px;
+        }
+      }
+
+
+    }
+
+    // 图片
+    .ZTboxImgMain {
+      margin-top: 20px;
+
+      .ZTboxImgBox {
+        display: flex;
+
+        .ZTbox1 {
+          position: relative;
+          top: 3px;
+          width: 60px;
+
+          &>span {
+            color: #ff4d4f;
+          }
+        }
+
+        .ZTbox1Img {
+          width: calc(100% - 60px);
+          display: flex;
+          flex-wrap: wrap;
+
+          .ZTbox1ImgIcon {
+            margin-right: 20px;
+            color: #a6a6a6;
+            border-radius: 3px;
+            cursor: pointer;
+            font-size: 30px;
+            width: 100px;
+            height: 100px;
+            border: 1px dashed #797979;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+
+
+          }
+
+          .ZTbox1ImgRow {
+            margin-right: 20px;
+            width: 100px;
+            height: 125px;
+            position: relative;
+            margin-bottom: 20px;
+
+            .ZTbox1ImgRowIcon {
+              width: 100%;
+              background-color: rgba(0, 0, 0, 0.6);
+              color: #fff;
+              display: flex;
+              justify-content: space-around;
+              font-size: 16px;
+
+              a {
+                color: #fff;
+              }
+            }
+
+            .ZTbox1ImgRowX {
+              cursor: pointer;
+              position: absolute;
+              right: -10px;
+              top: -10px;
+              z-index: 99;
+              background-color: rgba(0, 0, 0, 0.8);
+              width: 20px;
+              height: 20px;
+              border-radius: 50%;
+              font-size: 16px;
+              color: #fff;
+              display: flex;
+              justify-content: center;
+              align-items: center;
+            }
+          }
+
+        }
+      }
+    }
+
+    .ZcheckTxt {
+      position: relative;
+      overflow: hidden;
+      opacity: 0;
+      transition: top 0.2s;
+      color: #ff4d4f;
+      top: -10px;
+    }
+    .ZcheckTxtAc{
+      top: 2px;
+      opacity: 1;
+    }
+  }
+}

+ 483 - 0
src/components/ZupTypes/index.tsx

@@ -0,0 +1,483 @@
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import styles from "./index.module.scss";
+import { Button, Checkbox, Popconfirm } from "antd";
+import { forwardRef, useImperativeHandle } from "react";
+import { baseURL } from "@/utils/http";
+import {
+  PlusOutlined,
+  CloseCircleOutlined,
+  UploadOutlined,
+  CloseOutlined,
+  DownloadOutlined,
+  EyeOutlined,
+} from "@ant-design/icons";
+import { MessageFu } from "@/utils/message";
+import { API_upFile } from "@/store/action/layout";
+import { fileDomInitialFu } from "@/utils/domShow";
+import store from "@/store";
+import ImageLazy from "../ImageLazy";
+import classNames from "classnames";
+
+export type FileListType = {
+  fileName: string;
+  filePath: string;
+  id: number;
+  type: "model" | "img" | "audio" | "video";
+};
+
+type Props = {
+  ref: any; //当前自己的ref,给父组件调用
+  selecFlag: string; //筛选的数组
+  isLook: boolean; //是不是查看
+  fileCheck: boolean; //有没有点击过确定
+  dirCode: string; //文件的code码
+  myUrl: string; //请求地址
+  modelSize?: number; //模型文件大小限制
+  imgSize?: number; //图片大小限制
+  imgLength?: number; //图片数量限制
+  audioSize?: number; //音频大小限制
+  videoSize?: number; //视频大小限制
+};
+
+function ZupTypes(
+  {
+    selecFlag,
+    isLook,
+    fileCheck,
+    dirCode,
+    myUrl,
+    modelSize = 500,
+    imgSize = 5,
+    imgLength = 9,
+    audioSize = 10,
+    videoSize = 500,
+  }: Props,
+  ref: any
+) {
+  // 筛选数组
+  const typeCheckArr = useMemo(() => {
+    const arr = [
+      { label: "模型", value: "model" },
+      { label: "图片", value: "img" },
+      { label: "音频", value: "audio" },
+      { label: "视频", value: "video" },
+    ];
+    return arr.filter((v) => selecFlag.includes(v.label));
+  }, [selecFlag]);
+
+  // 筛选
+  const [typeCheck, setTypeCheck] = useState<string[]>([]);
+
+  // 上传附件的信息
+  const [fileList, setFileList] = useState({
+    model: {} as FileListType,
+    img: [] as FileListType[],
+    audio: {} as FileListType,
+    video: {} as FileListType,
+  });
+
+  // 附件信息的校验,不满足返回 true
+  const fileCheckFu = useMemo(() => {
+    let flag = false;
+    if (typeCheck.length === 0) flag = true;
+    if (typeCheck.includes("model") && !fileList.model.id) flag = true;
+    if (typeCheck.includes("img") && fileList.img.length === 0) flag = true;
+    if (typeCheck.includes("audio") && !fileList.audio.id) flag = true;
+    if (typeCheck.includes("video") && !fileList.video.id) flag = true;
+    return flag;
+  }, [fileList, typeCheck]);
+
+  // 点击上传附件按钮
+  const myInput = useRef<HTMLInputElement>(null);
+
+  const [fileOneType, setFileOneType] = useState("");
+
+  useEffect(() => {
+    if (fileOneType) myInput.current?.click();
+  }, [fileOneType]);
+
+  const upFileFu = useCallback((type: string) => {
+    setFileOneType("");
+    window.setTimeout(() => {
+      setFileOneType(type);
+    }, 100);
+  }, []);
+
+  // 上传附件的处理函数
+  const handeUpPhoto2 = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0];
+
+        let anType = ["image/jpeg", "image/png", "image/gif"];
+        let anTit1 = "只支持png、jpg、gif和jpeg格式!";
+        let anTit2 = `最大支持${imgSize}M!`;
+        let anSize = imgSize * 1024 * 1024;
+
+        if (fileOneType === "audio") {
+          anType = ["audio/mpeg"];
+          anTit1 = "只支持mp3格式!";
+          anTit2 = `最大支持${audioSize}M!`;
+          anSize = audioSize * 1024 * 1024;
+        } else if (fileOneType === "video") {
+          anType = ["video/mp4"];
+          anTit1 = "只支持mp4格式!";
+          anTit2 = `最大支持${videoSize}M!`;
+          anSize = videoSize * 1024 * 1024;
+        } else if (fileOneType === "model") {
+          anType = [""];
+          anTit1 = "只支持4dage格式!";
+          anTit2 = `最大支持${500}M!`;
+          anSize = modelSize * 1024 * 1024;
+        }
+
+        // 校验格式
+        if (fileOneType !== "model") {
+          if (!anType.includes(filesInfo.type)) {
+            e.target.value = "";
+            return MessageFu.warning(anTit1);
+          }
+        } else {
+          if (!filesInfo.name.includes(".4dage")) {
+            e.target.value = "";
+            return MessageFu.warning(anTit1);
+          }
+        }
+
+        // 校验大小
+        if (filesInfo.size > anSize) {
+          e.target.value = "";
+          return MessageFu.warning(anTit2);
+        }
+        // 创建FormData对象
+        const fd = new FormData();
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append("type", fileOneType);
+        fd.append("dirCode", dirCode);
+        fd.append("file", filesInfo);
+
+        e.target.value = "";
+
+        const res = await API_upFile(fd, myUrl);
+
+        try {
+          if (res.code === 0) {
+            MessageFu.success("上传成功!");
+            if (fileOneType === "img")
+              setFileList({ ...fileList, img: [res.data, ...fileList.img] });
+            else setFileList({ ...fileList, [fileOneType]: res.data });
+          }
+          fileDomInitialFu();
+        } catch (error) {
+          fileDomInitialFu();
+        }
+      }
+    },
+    [
+      audioSize,
+      dirCode,
+      fileList,
+      fileOneType,
+      imgSize,
+      modelSize,
+      myUrl,
+      videoSize,
+    ]
+  );
+
+  // 附件图片的拖动
+  const [dragImg, setDragImg] = useState<any>(null);
+
+  const handleDragOver = useCallback(
+    (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
+      e.dataTransfer.dropEffect = "move";
+    },
+    []
+  );
+
+  const handleDragEnter = useCallback(
+    (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
+      e.dataTransfer.effectAllowed = "move";
+      if (item === dragImg) return;
+      const newItems = [...fileList.img]; //拷贝一份数据进行交换操作。
+      const src = newItems.indexOf(dragImg); //获取数组下标
+      const dst = newItems.indexOf(item);
+      newItems.splice(dst, 0, ...newItems.splice(src, 1)); //交换位置
+      setFileList({ ...fileList, img: newItems });
+    },
+    [dragImg, fileList]
+  );
+
+  // 删除某一张图片
+  const delImgListFu = useCallback(
+    (id: number) => {
+      const newItems = fileList.img.filter((v) => v.id !== id);
+      setFileList({ ...fileList, img: newItems });
+    },
+    [fileList]
+  );
+
+  // 模型 音频 视频 的 dom
+  const resOneDivDom = useCallback(
+    (type: "model" | "audio" | "video") => {
+      const dom = (
+        <div className="ZTbox" hidden={!typeCheck.includes(type)}>
+          <div className="ZTbox1">
+            <span> </span>{" "}
+            {type === "model" ? "模型" : type === "audio" ? "音频" : "视频"}:
+          </div>
+          {fileList[type].id ? (
+            <div className="ZTbox2">
+              <div className="ZTbox2Name">{fileList[type].fileName}</div>
+
+              <div
+                className="ZTbox2Look"
+                onClick={() =>
+                  store.dispatch({
+                    type: "layout/lookDom",
+                    payload: { src: fileList[type].filePath, type },
+                  })
+                }
+              >
+                <EyeOutlined rev={undefined} />
+              </div>
+
+              <a
+                href={baseURL + fileList[type].filePath}
+                download
+                target="_blank"
+                className="ZTbox2Down"
+                rel="noreferrer"
+              >
+                <DownloadOutlined rev={undefined} />
+              </a>
+
+              <Popconfirm
+                title="删除后无法恢复,是否删除?"
+                okText="删除"
+                cancelText="取消"
+                onConfirm={() =>
+                  setFileList({ ...fileList, [type]: {} as FileListType })
+                }
+              >
+                <div className="ZTbox2X">
+                  <CloseCircleOutlined rev={undefined} />
+                </div>
+              </Popconfirm>
+            </div>
+          ) : (
+            <>
+              <Button
+                onClick={() => upFileFu(type)}
+                icon={<UploadOutlined rev={undefined} />}
+              >
+                上传
+              </Button>
+
+              <div className="ZTboxTit">
+                {type === "model"
+                  ? "仅支持4dage格式的模型文件,大小不能超过500M。"
+                  : type === "audio"
+                  ? "仅支持MP3格式的音频文件,大小不得超过10MB。"
+                  : "仅支持MP4格式的视频文件,大小不得超过500MB。"}
+              </div>
+            </>
+          )}
+        </div>
+      );
+      return dom;
+    },
+    [fileList, typeCheck, upFileFu]
+  );
+
+  // ------------让父组件调用的 回显
+  const setFileComFileFu = useCallback((info: any) => {
+    if (info.type) setTypeCheck(info.type.split(","));
+
+    if (info.fileList && info.fileList.length) {
+      const data: FileListType[] = info.fileList;
+      const obj = {
+        model: {} as FileListType,
+        img: [] as FileListType[],
+        audio: {} as FileListType,
+        video: {} as FileListType,
+      };
+
+      data.forEach((v) => {
+        if (v.type === "img") obj.img.push(v);
+        else obj[v.type!] = v;
+      });
+      setFileList(obj);
+    }
+  }, []);
+
+  // --------------让父组件调用的返回 附件 信息
+  const fileComFileResFu = useCallback(() => {
+    const fileIds = [];
+    if (fileList.model.id && typeCheck.includes("model"))
+      fileIds.push(fileList.model.id);
+    if (fileList.audio.id && typeCheck.includes("audio"))
+      fileIds.push(fileList.audio.id);
+    if (fileList.video.id && typeCheck.includes("video"))
+      fileIds.push(fileList.video.id);
+    if (typeCheck.includes("img")) {
+      fileList.img.forEach((v) => {
+        if (v.id) fileIds.push(v.id);
+      });
+    }
+    return {
+      sonType: typeCheck,
+      sonFileIds: fileIds,
+      sonIsOk: fileCheckFu,
+    };
+  }, [
+    fileCheckFu,
+    fileList.audio.id,
+    fileList.img,
+    fileList.model.id,
+    fileList.video.id,
+    typeCheck,
+  ]);
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    setFileComFileFu,
+    fileComFileResFu,
+  }));
+
+  return (
+    <div className={styles.ZupTypes}>
+      <input
+        id="upInput"
+        type="file"
+        accept={
+          fileOneType === "img"
+            ? ".gif,.png,.jpg,.jpeg"
+            : fileOneType === "audio"
+            ? ".mp3"
+            : fileOneType === "model"
+            ? ".4dage"
+            : ".mp4"
+        }
+        ref={myInput}
+        onChange={(e) => handeUpPhoto2(e)}
+      />
+
+      <Checkbox.Group
+        options={typeCheckArr}
+        value={typeCheck}
+        onChange={(e) => setTypeCheck(e as string[])}
+      />
+
+      {/* -----------模型 */}
+      {resOneDivDom("model")}
+
+      {/* -----------图片 */}
+      <div className="ZTboxImgMain" hidden={!typeCheck.includes("img")}>
+        <div className="ZTboxImgBox">
+          <div className="ZTbox1">
+            <span> </span> 图片:
+          </div>
+
+          <div className="ZTbox1Img">
+            <div
+              hidden={!!fileList.img.length && fileList.img.length >= imgLength}
+              className="ZTbox1ImgIcon"
+              onClick={() => upFileFu("img")}
+            >
+              <PlusOutlined rev={undefined} />
+            </div>
+            {fileList.img.map((v) => (
+              <div
+                className="ZTbox1ImgRow"
+                key={v.id}
+                draggable="true"
+                onDragStart={() => setDragImg(v)}
+                onDragOver={(e) => handleDragOver(e, v)}
+                onDragEnter={(e) => handleDragEnter(e, v)}
+                onDragEnd={() => setDragImg(null)}
+              >
+                {v.filePath ? (
+                  <ImageLazy
+                    noLook={true}
+                    width={100}
+                    height={100}
+                    src={v.filePath}
+                  />
+                ) : null}
+                <div className="ZTbox1ImgRowIcon">
+                  <EyeOutlined
+                    onClick={() =>
+                      store.dispatch({
+                        type: "layout/lookBigImg",
+                        payload: {
+                          url: baseURL + v.filePath,
+                          show: true,
+                        },
+                      })
+                    }
+                    rev={undefined}
+                  />
+                  <a
+                    href={baseURL + v.filePath}
+                    download
+                    target="_blank"
+                    rel="noreferrer"
+                  >
+                    <DownloadOutlined rev={undefined} />
+                  </a>
+                </div>
+
+                <Popconfirm
+                  title="删除后无法恢复,是否删除?"
+                  okText="删除"
+                  cancelText="取消"
+                  onConfirm={() => delImgListFu(v.id!)}
+                >
+                  <div className="ZTbox1ImgRowX">
+                    <CloseOutlined rev={undefined} />
+                  </div>
+                </Popconfirm>
+              </div>
+            ))}
+          </div>
+        </div>
+
+        <div className="ZTboxTit">
+          {fileList.img.length && fileList.img.length >= 2 ? (
+            <>
+              按住鼠标可拖动图片调整顺序。
+              <br />
+            </>
+          ) : null}
+          支持png、jpg、gif和jpeg的图片格式;最大支持5M;最多支持9张。
+        </div>
+      </div>
+
+      {/* -----------音频 */}
+      {resOneDivDom("audio")}
+
+      {/* -----------视频 */}
+      {resOneDivDom("video")}
+
+      {/* 最后的提示 */}
+      <div
+        className={classNames(
+          "ZcheckTxt",
+          fileCheck && fileCheckFu ? "ZcheckTxtAc" : ""
+        )}
+      >
+        请最少勾选一个文件类型,并且上传对应的附件!
+      </div>
+    </div>
+  );
+}
+
+export default forwardRef(ZupTypes);

+ 55 - 0
src/pages/A5goods/A5add/index.module.scss

@@ -0,0 +1,55 @@
+.A5add {
+  position: absolute;
+  z-index: 10;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  border-radius: 10px;
+  padding: 20px;
+
+
+  :global {
+    .A5Amain {
+      width: 100%;
+      height: 100%;
+      overflow-y: auto;
+      padding-bottom: 30px;
+      #basic{
+        width: 800px;
+      }
+      .A5link {
+        textarea {
+          min-height: 60px !important;
+        }
+      }
+
+      .formRow {
+        display: flex;
+
+        .formLeft {
+          position: relative;
+          top: 3px;
+          width: 100px;
+          text-align: right;
+
+          &>span {
+            color: #ff4d4f;
+          }
+        }
+
+        .formRight {
+          width: calc(100% - 100px);
+        }
+      }
+
+      .A5Abtn {
+        position: absolute;
+        left: 900px;
+        top: 50%;
+        transform: translateY(-50%);
+      }
+    }
+  }
+}

+ 291 - 0
src/pages/A5goods/A5add/index.tsx

@@ -0,0 +1,291 @@
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import styles from "./index.module.scss";
+import { A5_APIgetInfo, A5_APIsave } from "@/store/action/A5goods";
+import dayjs from "dayjs";
+import {
+  Button,
+  DatePicker,
+  Form,
+  FormInstance,
+  Input,
+  Popconfirm,
+  Radio,
+  Select,
+} from "antd";
+import ZupOne from "@/components/ZupOne";
+import TextArea from "antd/es/input/TextArea";
+import ZupTypes from "@/components/ZupTypes";
+import { MessageFu } from "@/utils/message";
+
+type Props = {
+  addId: number;
+  closeFu: () => void;
+  addTableFu: () => void;
+  editTableFu: () => void;
+};
+
+function A5add({ addId, closeFu, addTableFu, editTableFu }: Props) {
+  const getInfoFu = useCallback(async (id: number) => {
+    const res = await A5_APIgetInfo(id);
+    if (res.code === 0) {
+      const data = res.data.entity;
+
+      FormBoxRef.current?.setFieldsValue({
+        ...data,
+        myTime: dayjs(data.releaseDate),
+        dictLevel: data.dictLevel ? data.dictLevel : null,
+      });
+
+      setDirCode(data.dirCode);
+
+      // 设置封面图
+      ZupOneRef1.current?.setFileComFileFu({
+        fileName: "",
+        filePath: data.thumb,
+      });
+
+      const file = res.data.file;
+
+      // 传给 附件 组件的
+      const sonInfo = {
+        type: data.fileType,
+        fileList: file,
+      };
+      ZupOneRef2.current?.setFileComFileFu(sonInfo);
+    }
+  }, []);
+
+  useEffect(() => {
+    if (addId > 0) {
+      // 编辑
+      getInfoFu(addId);
+    } else {
+      // 新增
+      setDirCode(Date.now() + "");
+      FormBoxRef.current?.setFieldsValue({
+        myTime: dayjs(Date.now()),
+        type: "3D",
+      });
+    }
+  }, [addId, getInfoFu]);
+
+  // 附件 是否 已经点击过确定
+  const [fileCheck, setFileCheck] = useState(false);
+
+  // 文件的code码
+  const [dirCode, setDirCode] = useState("");
+
+  // 表单的ref
+  const FormBoxRef = useRef<FormInstance>(null);
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    setFileCheck(true);
+  }, []);
+
+  //  通过校验点击确定
+  const onFinish = useCallback(
+    async (values: any) => {
+      setFileCheck(true);
+
+      const coverUrl1 = ZupOneRef1.current?.fileComFileResFu();
+      // 没有传 封面图
+      if (!coverUrl1.filePath) return;
+      // 发布日期
+      const releaseDate = dayjs(values.myTime).format("YYYY-MM-DD");
+
+      // 附件组件的 type 数组 和 附件id数组
+      const { sonType, sonFileIds } = ZupOneRef2.current?.fileComFileResFu();
+
+      //  附件组件为必传信息
+      //  if(sonInfo.sonIsOk) return
+
+      const obj = {
+        ...values,
+        id: addId > 0 ? addId : null,
+        releaseDate,
+        thumb: coverUrl1.filePath,
+        dirCode,
+        fileType: sonType ? sonType.join(",") : null,
+        fileIds: sonFileIds ? sonFileIds.join(",") : null,
+      };
+      const res = await A5_APIsave(obj);
+      if (res.code === 0) {
+        MessageFu.success(addId > 0 ? "编辑成功!" : "新增成功!");
+        addId > 0 ? editTableFu() : addTableFu();
+        closeFu();
+      }
+    },
+    [addId, addTableFu, closeFu, dirCode, editTableFu]
+  );
+
+  // 上传附件的ref
+  const ZupOneRef1 = useRef<any>(null);
+  const ZupOneRef2 = useRef<any>(null);
+
+  return (
+    <div className={styles.A5add}>
+      <div className="A5Amain">
+        <Form
+          ref={FormBoxRef}
+          name="basic"
+          labelCol={{ span: 3 }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete="off"
+          scrollToFirstError
+        >
+          <Form.Item
+            label="名称"
+            name="name"
+            rules={[{ required: true, message: "请输入名称!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={20} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="类别"
+            name="type"
+            rules={[{ required: true, message: "请选择类别!" }]}
+          >
+            <Radio.Group>
+              <Radio value="3D">三维文物</Radio>
+              <Radio value="2D">二维文物</Radio>
+            </Radio.Group>
+          </Form.Item>
+
+          {/* 封面 */}
+          <div className="formRow">
+            <div className="formLeft">
+              <span>* </span>
+              封面图:
+            </div>
+            <div className="formRight">
+              <ZupOne
+                ref={ZupOneRef1}
+                isLook={false}
+                fileCheck={fileCheck}
+                size={5}
+                dirCode={dirCode}
+                myUrl="cms/exhibition/upload"
+                format={["image/jpeg", "image/png"]}
+                formatTxt="png、jpg和jpeg"
+                checkTxt="请上传封面图"
+                upTxt="最多1张"
+                myType="thumb"
+              />
+            </div>
+          </div>
+
+          <Form.Item
+            label="发布日期"
+            name="myTime"
+            rules={[{ required: true, message: "请选择发布日期!" }]}
+          >
+            <DatePicker />
+          </Form.Item>
+
+          <Form.Item
+            label="种类"
+            name="dictType"
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={20} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="时代"
+            name="dictAge"
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={10} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="质地"
+            name="dictTexture"
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={10} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item label="级别" name="dictLevel">
+            <Select
+              placeholder="请选择"
+              style={{ width: 400 }}
+              options={[
+                { value: "一级", label: "一级" },
+                { value: "二级", label: "二级" },
+                { value: "三级", label: "三级" },
+              ]}
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="尺寸"
+            name="dictSize"
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={30} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item label="简介" name="description">
+            <TextArea placeholder="请输入内容" showCount maxLength={200} />
+          </Form.Item>
+
+          <Form.Item
+            label="模型链接"
+            name="modelLink"
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <TextArea
+              className="A5link"
+              placeholder="请输入内容"
+              showCount
+              maxLength={200}
+            />
+          </Form.Item>
+
+          {/* 文件类型 */}
+          <div className="formRow">
+            <div className="formLeft">文件类型:</div>
+            <div className="formRight">
+              <ZupTypes
+                ref={ZupOneRef2}
+                // 这个项目没有模型
+                selecFlag="图片/音频/视频"
+                isLook={false}
+                fileCheck={false}
+                dirCode={dirCode}
+                myUrl="cms/goods/upload"
+              />
+            </div>
+          </div>
+
+          {/* 确定和取消按钮 */}
+          <br />
+          <Form.Item className="A5Abtn">
+            <Button type="primary" htmlType="submit">
+              提交
+            </Button>
+            <br />
+            <br />
+            <Popconfirm
+              title="放弃编辑后,信息将不会保存!"
+              okText="放弃"
+              cancelText="取消"
+              onConfirm={closeFu}
+            >
+              <Button>取消</Button>
+            </Popconfirm>
+          </Form.Item>
+        </Form>
+      </div>
+    </div>
+  );
+}
+
+const MemoA5add = React.memo(A5add);
+
+export default MemoA5add;

+ 37 - 3
src/pages/A5goods/index.module.scss

@@ -1,5 +1,39 @@
-.A5goods{
-  :global{
-    
+.A5goods {
+  :global {
+    .A5top {
+      padding: 15px 24px;
+      border-radius: 10px;
+      background-color: #fff;
+      display: flex;
+      justify-content: space-between;
+
+      .A5topRow {
+        display: flex;
+
+        &>div {
+          margin-right: 20px;
+        }
+      }
+    }
+
+    .A5tableBox {
+      border-radius: 10px;
+      overflow: hidden;
+      margin-top: 15px;
+      height: calc(100% - 77px);
+      background-color: #fff;
+
+      .ant-table-body {
+        height: 625px;
+        overflow-y: auto !important;
+        overflow-y: overlay !important;
+
+        .ant-table-row {
+          .ant-table-cell {
+            padding: 10px;
+          }
+        }
+      }
+    }
   }
 }

+ 220 - 5
src/pages/A5goods/index.tsx

@@ -1,12 +1,227 @@
-import React from "react";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
 import styles from "./index.module.scss";
- function A5goods() {
-  
+import { useDispatch, useSelector } from "react-redux";
+import { Button, Input, Popconfirm, Table } from "antd";
+import { A5_APIdel, A5_APIgetList } from "@/store/action/A5goods";
+import { RootState } from "@/store";
+import { A5tableType } from "@/types";
+import ImageLazy from "@/components/ImageLazy";
+import { MessageFu } from "@/utils/message";
+import A5add from "./A5add";
+
+// const optionsArr = [
+//   { value: "", label: "全部" },
+//   { value: "三维文物", label: "三维文物" },
+//   { value: "平面文物", label: "平面文物" },
+//   { value: "油画", label: "油画" },
+//   { value: "电子书", label: "电子书" },
+// ];
+
+function A5goods() {
+  const dispatch = useDispatch();
+
+  const [fromData, setFromData] = useState({
+    pageNum: 1,
+    pageSize: 10,
+    searchKey: "",
+    dictType: "",
+  });
+
+  const getListFu = useCallback(() => {
+    dispatch(A5_APIgetList(fromData));
+  }, [dispatch, fromData]);
+
+  useEffect(() => {
+    getListFu();
+  }, [getListFu]);
+
+  const [inputKey, setInputKey] = useState(1);
+
+  // 标题的输入
+  const timeRef = useRef(-1);
+  const fromKeyChangeFu = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>, key: "searchKey") => {
+      clearTimeout(timeRef.current);
+      timeRef.current = window.setTimeout(() => {
+        setFromData({ ...fromData, [key]: e.target.value, pageNum: 1 });
+      }, 500);
+    },
+    [fromData]
+  );
+
+  // 点击重置
+  const resetSelectFu = useCallback(() => {
+    setInputKey(Date.now());
+    setFromData({
+      pageNum: 1,
+      pageSize: 10,
+      searchKey: "",
+      dictType: "",
+    });
+  }, []);
+
+  // 页码变化
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setFromData({ ...fromData, pageNum, pageSize });
+    },
+    [fromData]
+  );
+
+  const tableInfo = useSelector((state: RootState) => state.A5goods.tableInfo);
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      const res = await A5_APIdel(id);
+      if (res.code === 0) {
+        MessageFu.success("删除成功!");
+        getListFu();
+      }
+    },
+    [getListFu]
+  );
+
+  const columns = useMemo(() => {
+    return [
+      {
+        title: "名称",
+        dataIndex: "name",
+      },
+      {
+        title: "封面",
+        render: (item: A5tableType) => (
+          <div className="tableImgAuto">
+            <ImageLazy width={60} height={60} src={item.thumb} />
+          </div>
+        ),
+      },
+      {
+        title: "种类",
+        render: (item: A5tableType) => item.dictType || "(空)",
+      },
+      {
+        title: "时代",
+        render: (item: A5tableType) => item.dictAge || "(空)",
+      },
+      {
+        title: "级别",
+        render: (item: A5tableType) => item.dictLevel || "(空)",
+      },
+      {
+        title: "尺寸",
+        render: (item: A5tableType) => item.dictSize || "(空)",
+      },
+      {
+        title: "点赞",
+        dataIndex: "pcs",
+      },
+      {
+        title: "发布日期",
+        render: (item: A5tableType) => item.releaseDate || "(空)",
+      },
+      {
+        title: "操作",
+        render: (item: A5tableType) => (
+          <>
+            <Button size="small" type="text" onClick={() => setAddId(item.id)}>
+              编辑
+            </Button>
+
+            <Popconfirm
+              title="删除后无法恢复,是否删除?"
+              okText="删除"
+              cancelText="取消"
+              onConfirm={() => delTableFu(item.id)}
+            >
+              <Button size="small" type="text" danger>
+                删除
+              </Button>
+            </Popconfirm>
+          </>
+        ),
+      },
+    ];
+  }, [delTableFu]);
+
+  // 点击新增和编辑
+  const [addId, setAddId] = useState(0);
+
   return (
     <div className={styles.A5goods}>
-      <h1>A5goods</h1>
+      <div className="pageTitle">馆藏管理</div>
+      {/* 顶部筛选 */}
+      <div className="A5top">
+        <div className="A5topRow">
+          <div>
+            <span>搜索项:</span>
+            <Input
+              key={inputKey}
+              maxLength={20}
+              style={{ width: 300 }}
+              placeholder="请输入名称"
+              allowClear
+              onChange={(e) => fromKeyChangeFu(e, "searchKey")}
+            />
+          </div>
+
+          {/* <div>
+            <span>类别:</span>
+            <Select
+              placeholder="请选择"
+              style={{ width: 160 }}
+              value={fromData.dictType}
+              onChange={(e) =>
+                setFromData({ ...fromData, dictType: e, pageNum: 1 })
+              }
+              options={optionsArr}
+            />
+          </div> */}
+        </div>
+        <div className="A5topRow">
+          <Button onClick={resetSelectFu}>重置</Button>
+          &emsp;&emsp;
+          <Button type="primary" onClick={() => setAddId(-1)}>
+            新增
+          </Button>
+        </div>
+      </div>
+
+      {/* 表格主体 */}
+      <div className="A5tableBox">
+        <Table
+          scroll={{ y: 625 }}
+          dataSource={tableInfo.list}
+          columns={columns}
+          rowKey="id"
+          pagination={{
+            showQuickJumper: true,
+            position: ["bottomCenter"],
+            showSizeChanger: true,
+            current: fromData.pageNum,
+            pageSize: fromData.pageSize,
+            total: tableInfo.total,
+            onChange: paginationChange(),
+          }}
+        />
+      </div>
+      {/* 新增和编辑 */}
+      {addId ? (
+        <A5add
+          addId={addId}
+          closeFu={() => setAddId(0)}
+          addTableFu={() => resetSelectFu()}
+          editTableFu={() => getListFu()}
+        />
+      ) : null}
     </div>
-  )
+  );
 }
 
 const MemoA5goods = React.memo(A5goods);

+ 38 - 0
src/store/action/A5goods.ts

@@ -0,0 +1,38 @@
+import http from "@/utils/http";
+import { AppDispatch } from "..";
+/**
+ * 获取 馆藏管理列表
+ */
+export const A5_APIgetList = (data: any) => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post("cms/goods/pageList", data);
+    if (res.code === 0) {
+      const obj = {
+        list: res.data.records,
+        total: res.data.total,
+      };
+      dispatch({ type: "A5/getList", payload: obj });
+    }
+  };
+};
+
+/**
+ * 删除
+ */
+export const A5_APIdel = (id: number) => {
+  return http.get(`cms/goods/remove/${id}`);
+};
+
+/**
+ * 获取详情
+ */
+export const A5_APIgetInfo = (id: number) => {
+  return http.get(`cms/goods/detail/${id}`);
+};
+
+/**
+ * 新增/编辑
+ */
+export const A5_APIsave = (data: any) => {
+  return http.post("cms/goods/save", data);
+};

+ 28 - 0
src/store/reducer/A5goods.ts

@@ -0,0 +1,28 @@
+import { A5tableType } from "@/types";
+
+// 初始化状态
+const initState = {
+  // 列表数据
+  tableInfo: {
+    list: [] as A5tableType[],
+    total: 0,
+  },
+};
+
+// 定义 action 类型
+type Props = {
+  type: "A5/getList";
+  payload: { list: A5tableType[]; total: number };
+};
+
+// 频道 reducer
+export default function Reducer(state = initState, action: Props) {
+  switch (action.type) {
+    // 获取列表数据
+    case "A5/getList":
+      return { ...state, tableInfo: action.payload };
+
+    default:
+      return state;
+  }
+}

+ 2 - 0
src/store/reducer/index.ts

@@ -6,6 +6,7 @@ import A0Layout from "./layout";
 import A1video from "./A1video";
 import A2poster from "./A2poster";
 import A4exhibit from "./A4exhibit";
+import A5goods from "./A5goods";
 import A7user from "./A7user";
 import A8log from "./A8log";
 
@@ -16,6 +17,7 @@ const rootReducer = combineReducers({
   A1video,
   A2poster,
   A4exhibit,
+  A5goods,
   A7user,
   A8log
 });

+ 22 - 0
src/types/api/A5goods.d.ts

@@ -0,0 +1,22 @@
+export type A5tableType = {
+  createTime: string;
+  creatorId: number;
+  creatorName: string;
+  description: string;
+  dictAge: string;
+  dictLevel: string;
+  dictSize:string
+  dictTexture: string;
+  dictType: string;
+  dirCode: string;
+  fileIds: string;
+  fileType: string;
+  id: number;
+  modelLink: string;
+  name: string;
+  pcs: number;
+  releaseDate: string;
+  thumb: string;
+  type: string;
+  updateTime: string;
+};

+ 1 - 0
src/types/index.d.ts

@@ -1,5 +1,6 @@
 export * from './api/layot'
 export * from './api/A1video'
 export * from './api/A4exhibit'
+export * from './api/A5goods'
 export * from './api/A7user'
 export * from './api/A8log'