shaogen1995 2 лет назад
Сommit
3ff71ab943
100 измененных файлов с 36792 добавлено и 0 удалено
  1. 23 0
      .gitignore
  2. 46 0
      README.md
  3. 10 0
      config-overrides.js
  4. 30152 0
      package-lock.json
  5. 63 0
      package.json
  6. 8 0
      path.tsconfig.json
  7. BIN
      public/favicon.ico
  8. 43 0
      public/index.html
  9. 62 0
      src/App.tsx
  10. BIN
      src/assets/img/IMGerror.png
  11. BIN
      src/assets/img/activeLL.png
  12. BIN
      src/assets/img/bg.jpg
  13. BIN
      src/assets/img/inco1.png
  14. BIN
      src/assets/img/inco1Ac.png
  15. BIN
      src/assets/img/inco2.png
  16. BIN
      src/assets/img/inco2Ac.png
  17. BIN
      src/assets/img/inco3.png
  18. BIN
      src/assets/img/inco3Ac.png
  19. BIN
      src/assets/img/inco4.png
  20. BIN
      src/assets/img/inco4Ac.png
  21. BIN
      src/assets/img/layoutLeftMain.jpg
  22. BIN
      src/assets/img/layoutTop.jpg
  23. BIN
      src/assets/img/loading.gif
  24. BIN
      src/assets/img/loginLeft.png
  25. BIN
      src/assets/img/loginRight.png
  26. BIN
      src/assets/img/top.jpg
  27. BIN
      src/assets/img/user.png
  28. 184 0
      src/assets/styles/base.css
  29. 26 0
      src/components/AsyncSpinLoding/index.module.scss
  30. 29 0
      src/components/AsyncSpinLoding/index.tsx
  31. 32 0
      src/components/AuthRoute/index.tsx
  32. 37 0
      src/components/ImageLazy/index.module.scss
  33. 88 0
      src/components/ImageLazy/index.tsx
  34. 26 0
      src/components/NotFound/index.tsx
  35. 10 0
      src/components/SpinLoding/index.module.scss
  36. 13 0
      src/components/SpinLoding/index.tsx
  37. 36 0
      src/components/UpAsyncLoding/index.module.scss
  38. 15 0
      src/components/UpAsyncLoding/index.tsx
  39. 52 0
      src/components/VideoLook/index.module.scss
  40. 37 0
      src/components/VideoLook/index.tsx
  41. 34 0
      src/components/VideoLookDom/index.module.scss
  42. 33 0
      src/components/VideoLookDom/index.tsx
  43. 30 0
      src/index.tsx
  44. 252 0
      src/pages/Exhibit/Edit.tsx
  45. 80 0
      src/pages/Exhibit/index.css
  46. 97 0
      src/pages/Exhibit/index.less
  47. 10 0
      src/pages/Exhibit/index.module.scss
  48. 121 0
      src/pages/Exhibit/index.tsx
  49. 40 0
      src/pages/Goods/index.module.scss
  50. 460 0
      src/pages/Goods/index.tsx
  51. 180 0
      src/pages/GoodsAdd/index.module.scss
  52. 669 0
      src/pages/GoodsAdd/index.tsx
  53. 197 0
      src/pages/Layout/index.module.scss
  54. 305 0
      src/pages/Layout/index.tsx
  55. 23 0
      src/pages/Log/index.module.scss
  56. 133 0
      src/pages/Log/index.tsx
  57. 128 0
      src/pages/Login/index.module.scss
  58. 92 0
      src/pages/Login/index.tsx
  59. 49 0
      src/pages/Role/RoleAdd/index.css
  60. 66 0
      src/pages/Role/RoleAdd/index.less
  61. 170 0
      src/pages/Role/RoleAdd/index.tsx
  62. 21 0
      src/pages/Role/index.module.scss
  63. 237 0
      src/pages/Role/index.tsx
  64. 13 0
      src/pages/User/UserAdd/index.css
  65. 17 0
      src/pages/User/UserAdd/index.less
  66. 145 0
      src/pages/User/UserAdd/index.tsx
  67. 31 0
      src/pages/User/index.module.scss
  68. 358 0
      src/pages/User/index.tsx
  69. 87 0
      src/pages/Wall/WallAdd/index.css
  70. 109 0
      src/pages/Wall/WallAdd/index.less
  71. 313 0
      src/pages/Wall/WallAdd/index.tsx
  72. 17 0
      src/pages/Wall/WallTable/index.module.scss
  73. 212 0
      src/pages/Wall/WallTable/index.tsx
  74. 60 0
      src/pages/Wall/index.module.scss
  75. 196 0
      src/pages/Wall/index.tsx
  76. 48 0
      src/store/action/exhibit.ts
  77. 63 0
      src/store/action/goods.ts
  78. 15 0
      src/store/action/log.ts
  79. 40 0
      src/store/action/login.ts
  80. 56 0
      src/store/action/role.ts
  81. 64 0
      src/store/action/user.ts
  82. 83 0
      src/store/action/wall.ts
  83. 20 0
      src/store/index.ts
  84. 27 0
      src/store/reducer/exhibit.ts
  85. 30 0
      src/store/reducer/goods.ts
  86. 25 0
      src/store/reducer/index.ts
  87. 25 0
      src/store/reducer/log.ts
  88. 57 0
      src/store/reducer/login.ts
  89. 27 0
      src/store/reducer/role.ts
  90. 44 0
      src/store/reducer/user.ts
  91. 21 0
      src/store/reducer/wall.ts
  92. 13 0
      src/types/api/exhibit.d.ts
  93. 21 0
      src/types/api/goods.d.ts
  94. 11 0
      src/types/api/log.d.ts
  95. 27 0
      src/types/api/login.d.ts
  96. 30 0
      src/types/api/role.d.ts
  97. 35 0
      src/types/api/user.d.ts
  98. 28 0
      src/types/api/wall.d.ts
  99. 5 0
      src/types/declaration.d.ts
  100. 0 0
      src/types/index.d.ts

+ 23 - 0
.gitignore

@@ -0,0 +1,23 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# production
+/build
+
+# misc
+.DS_Store
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*

+ 46 - 0
README.md

@@ -0,0 +1,46 @@
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `npm start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
+
+The page will reload if you make edits.\
+You will also see any lint errors in the console.
+
+### `npm test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `npm run build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `npm run eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
+
+If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
+
+You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).

+ 10 - 0
config-overrides.js

@@ -0,0 +1,10 @@
+const path = require('path')
+const { override, addWebpackAlias } = require('customize-cra')
+
+// 添加 @ 别名
+const webpackAlias = addWebpackAlias({
+  '@': path.resolve(__dirname, 'src'),
+})
+
+// 导出要进行覆盖的 webpack 配置
+module.exports = override(webpackAlias)

Разница между файлами не показана из-за своего большого размера
+ 30152 - 0
package-lock.json


+ 63 - 0
package.json

@@ -0,0 +1,63 @@
+{
+  "name": "demo",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@testing-library/jest-dom": "^5.16.5",
+    "@testing-library/react": "^13.4.0",
+    "@testing-library/user-event": "^13.5.0",
+    "@types/jest": "^27.5.2",
+    "@types/node": "^16.18.3",
+    "@types/react": "^18.0.24",
+    "@types/react-dom": "^18.0.8",
+    "antd": "^5.0.4",
+    "axios": "^1.1.3",
+    "dayjs": "^1.11.7",
+    "echarts": "^5.4.0",
+    "js-base64": "^3.7.3",
+    "js-export-excel": "^1.1.4",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-lazyimg-component": "^1.0.1",
+    "react-redux": "^8.0.4",
+    "react-router-dom": "5.3",
+    "react-scripts": "5.0.1",
+    "redux": "^4.2.0",
+    "redux-devtools-extension": "^2.13.9",
+    "redux-thunk": "^2.4.1",
+    "sass": "^1.55.0",
+    "typescript": "^4.8.4",
+    "web-vitals": "^2.1.4"
+  },
+  "scripts": {
+    "dev": "react-app-rewired start",
+    "build": "react-app-rewired build",
+    "test": "react-app-rewired test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "devDependencies": {
+    "@types/history": "^5.0.0",
+    "@types/react-router-dom": "^5.3.3",
+    "customize-cra": "^1.0.0",
+    "react-app-rewired": "^2.2.1"
+  },
+  "homepage": "."
+}

+ 8 - 0
path.tsconfig.json

@@ -0,0 +1,8 @@
+{
+    "compilerOptions": {
+      "baseUrl": "./",
+      "paths": {
+        "@/*": ["src/*"]
+      }
+    }
+  }

BIN
public/favicon.ico


+ 43 - 0
public/index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="zh">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#000000" />
+    <meta
+      name="description"
+      content="Web site created using create-react-app"
+    />
+    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
+    <!--
+      manifest.json provides metadata used when your web app is installed on a
+      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
+    -->
+
+    <!--
+      Notice the use of %PUBLIC_URL% in the tags above.
+      It will be replaced with the URL of the `public` folder during the build.
+      Only files inside the `public` folder can be referenced from the HTML.
+
+      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
+      work correctly both with client-side routing and a non-root public URL.
+      Learn how to configure a non-root public URL by running `npm run build`.
+    -->
+    <title>管理后台</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+    <!--
+      This HTML file is a template.
+      If you open it directly in the browser, you will see an empty page.
+
+      You can add webfonts, meta tags, or analytics to this file.
+      The build step will place the bundled scripts into the <body> tag.
+
+      To begin the development, run `npm start` or `yarn start`.
+      To create a production bundle, use `npm run build` or `yarn build`.
+    -->
+  </body>
+</html>

+ 62 - 0
src/App.tsx

@@ -0,0 +1,62 @@
+import "@/assets/styles/base.css";
+// 关于路由
+import React from "react";
+import { Router, Route, Switch } from "react-router-dom";
+import history from "./utils/history";
+import AuthRoute from "./components/AuthRoute";
+import SpinLoding from "./components/SpinLoding";
+import AsyncSpinLoding from "./components/AsyncSpinLoding";
+import { Image } from "antd";
+import { useDispatch, useSelector } from "react-redux";
+import { RootState } from "./store";
+import UpAsyncLoding from "./components/UpAsyncLoding";
+import VideoLookDom from "./components/VideoLookDom";
+const Layout = React.lazy(() => import("./pages/Layout"));
+const Login = React.lazy(() => import("./pages/Login"));
+
+export default function App() {
+  const dispatch = useDispatch();
+
+  // 从仓库中获取查看图片的信息
+  const lookBigImg = useSelector(
+    (state: RootState) => state.loginStore.lookBigImg
+  );
+
+  return (
+    <>
+      {/* 关于路由 */}
+      <Router history={history}>
+        <React.Suspense fallback={<SpinLoding />}>
+          <Switch>
+            <Route path="/login" component={Login} />
+            <AuthRoute path="/" component={Layout} />
+          </Switch>
+        </React.Suspense>
+      </Router>
+
+      {/* 发送请求的加载组件 */}
+      <AsyncSpinLoding />
+
+      {/* 所有图片点击预览查看大图 */}
+      <Image
+        preview={{
+          visible: lookBigImg.show,
+          src: lookBigImg.url,
+          onVisibleChange: (value) => {
+            // 清除仓库信息
+            dispatch({
+              type: "login/lookBigImg",
+              payload: { url: "", show: false },
+            });
+          },
+        }}
+      />
+
+      {/* 上传附件的进度条元素 */}
+      <UpAsyncLoding />
+
+      {/* 点击预览视频组件 */}
+      <VideoLookDom />
+    </>
+  );
+}

BIN
src/assets/img/IMGerror.png


BIN
src/assets/img/activeLL.png


BIN
src/assets/img/bg.jpg


BIN
src/assets/img/inco1.png


BIN
src/assets/img/inco1Ac.png


BIN
src/assets/img/inco2.png


BIN
src/assets/img/inco2Ac.png


BIN
src/assets/img/inco3.png


BIN
src/assets/img/inco3Ac.png


BIN
src/assets/img/inco4.png


BIN
src/assets/img/inco4Ac.png


BIN
src/assets/img/layoutLeftMain.jpg


BIN
src/assets/img/layoutTop.jpg


BIN
src/assets/img/loading.gif


BIN
src/assets/img/loginLeft.png


BIN
src/assets/img/loginRight.png


BIN
src/assets/img/top.jpg


BIN
src/assets/img/user.png


+ 184 - 0
src/assets/styles/base.css

@@ -0,0 +1,184 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+html {
+  height: 100%;
+  font-size: 14px;
+  user-select: none;
+}
+
+body {
+  font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif;
+  height: 100%;
+  color: #7e8293;
+}
+
+a {
+  text-decoration: none;
+  color: #7e8293;
+  outline: none;
+}
+
+i {
+  font-style: normal;
+}
+
+img {
+  max-width: 100%;
+  max-height: 100%;
+  vertical-align: middle;
+}
+
+ul {
+  list-style: none;
+}
+
+#root {
+  width: 100vw;
+  height: 100vh;
+  min-width: 1600px;
+  min-height: 900px;
+  overflow-y: auto;
+
+}
+
+body {
+  overflow-y: overlay;
+}
+
+/* 文本域取消下拉 */
+textarea {
+  resize: none !important;
+}
+
+/* 主题色 */
+:root {
+  --themeColor: #b34831
+}
+
+a {
+  color: var(--themeColor);
+}
+
+
+/* 普通按钮的颜色 */
+.ant-btn-text {
+  color: #409eff;
+}
+
+/* 按钮的危险颜色 */
+.ant-btn-text.ant-btn-dangerous {
+  color: var(--themeColor);
+}
+
+/* antd分页器样式 */
+.ant-pagination .ant-pagination-item {
+  border-radius: 50%;
+  border: 1px solid #999;
+  background-color: transparent !important;
+}
+
+.ant-pagination .ant-pagination-item-active {
+  background-color: var(--themeColor) !important;
+}
+
+
+.ant-pagination .ant-pagination-item-active a {
+  color: #fff !important;
+}
+
+.ant-pagination .ant-pagination-item:hover {
+  background-color: var(--themeColor) !important;
+}
+
+.ant-pagination .ant-pagination-item:hover a {
+  color: #fff !important;
+}
+
+.ant-pagination-prev {
+  border-radius: 50% !important;
+  border: 1px solid #999;
+}
+
+.ant-pagination-prev:hover {
+  background-color: var(--themeColor);
+}
+
+.ant-pagination-prev:hover button {
+  color: #fff;
+}
+
+
+
+.ant-pagination-next {
+  border-radius: 50% !important;
+  border: 1px solid #999;
+}
+
+
+.ant-pagination-next:hover {
+  background-color: var(--themeColor);
+}
+
+.ant-pagination-next:hover button {
+  color: #fff;
+}
+
+.ant-pagination-disabled {
+  border: 1px solid #ccc;
+}
+
+.ant-pagination-disabled:hover {
+  background-color: transparent;
+}
+
+img {
+  object-fit: cover;
+}
+
+/* 表格的图片居中 */
+.tableImgAuto {
+  display: flex;
+  justify-content: center;
+}
+
+[hidden] {
+  display: none !important;
+}
+
+
+/* 找不到页面 */
+.noFindPage {
+  opacity: 0;
+  transition: opacity .5s;
+}
+
+/* antd图片预览组件 */
+.ant-image {
+  display: none;
+}
+
+/* antd表格居中 */
+
+.ant-table-cell {
+  text-align: center !important;
+}
+
+/* 页面的顶部title */
+.pageTitlt {
+  font-size: 20px;
+  font-weight: 700;
+  color: var(--themeColor);
+}
+
+
+#upInput {
+  display: none;
+}
+
+#upInput2 {
+  display: none;
+}

+ 26 - 0
src/components/AsyncSpinLoding/index.module.scss

@@ -0,0 +1,26 @@
+.AsyncSpinLoding {
+  opacity: 0;
+  pointer-events: none;
+  transition: all .5s;
+  position: fixed;
+  z-index: 9998;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  // background-color: rgba(0, 0, 0, .6);
+  background-color: transparent;
+  :global{
+    .ant-spin-spinning{
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%,-50%);
+    }
+  }
+}
+
+.AsyncSpinLodingShow {
+  opacity: 1;
+  pointer-events: auto;
+}

+ 29 - 0
src/components/AsyncSpinLoding/index.tsx

@@ -0,0 +1,29 @@
+import styles from "./index.module.scss";
+import { Spin } from "antd";
+import React from "react";
+import classNames from "classnames";
+import { useSelector } from "react-redux";
+import { RootState } from "@/store";
+
+function AsyncSpinLoding() {
+  // 从仓库中获取查看图片的信息
+  const asyncLoding = useSelector(
+    (state: RootState) => state.loginStore.asyncLoding
+  );
+
+  return (
+    <div
+      id="AsyncSpinLoding"
+      className={classNames(
+        styles.AsyncSpinLoding,
+        asyncLoding ? styles.AsyncSpinLodingShow : ""
+      )}
+    >
+      <Spin size="large" />
+    </div>
+  );
+}
+
+const MemoAsyncSpinLoding = React.memo(AsyncSpinLoding);
+
+export default MemoAsyncSpinLoding;

+ 32 - 0
src/components/AuthRoute/index.tsx

@@ -0,0 +1,32 @@
+import { hasToken } from "@//utils/storage";
+import { message } from "antd";
+import React from "react";
+import { Redirect, Route } from "react-router-dom";
+
+type AtahType = {
+  path: string;
+  component: React.FC;
+  [x: string]: any;
+};
+
+export default function AuthRoute({ path, component: Com, ...rest }: AtahType) {
+  return (
+    <Route
+      path={path}
+      {...rest}
+      render={() => {
+        if (hasToken()) return <Com />;
+        else {
+          message.warning("登录失效!");
+          return (
+            <Redirect
+              to={{
+                pathname: "/login",
+              }}
+            />
+          );
+        }
+      }}
+    />
+  );
+}

+ 37 - 0
src/components/ImageLazy/index.module.scss

@@ -0,0 +1,37 @@
+.ImageLazy{
+  position: relative;
+  :global{
+    .lazyBox{
+      width: 100%;
+      height: 100%;
+      position: relative;
+      .lookImg{
+        cursor: pointer;
+        transition: opacity .3s;
+        opacity: 0;
+        pointer-events: none;
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        font-size: 18px;
+        color: #fff;
+        background-color: rgba(0,0,0,.6);
+        &>div{
+          font-size: 14px;
+        }
+      }
+      &:hover{
+        .lookImg{
+          opacity: 1;
+          pointer-events: auto;
+        }
+      }
+    }
+  }
+
+}

+ 88 - 0
src/components/ImageLazy/index.tsx

@@ -0,0 +1,88 @@
+import React, { useCallback, useEffect, useState } from "react";
+import styles from "./index.module.scss";
+import Lazyimg from "react-lazyimg-component";
+import { baseURL } from "@/utils/http";
+import imgLoding from "@/assets/img/loading.gif";
+import imgErr from "@/assets/img/IMGerror.png";
+import { EyeOutlined } from "@ant-design/icons";
+import { useDispatch } from "react-redux";
+
+type Props = {
+  width?: number;
+  height?: number;
+  src: string;
+  noLook?: boolean;
+};
+
+function ImageLazy({ width = 100, height = 100, src, noLook }: Props) {
+  const dispatch = useDispatch();
+
+  // 图片占位符
+  const [placeholderUrl, setPlaceholderUrl] = useState(
+    src ? imgLoding : imgErr
+  );
+
+  // 默认不能预览图片,加载成功之后能预览
+  const [lookImg, setLookImg] = useState(false);
+
+  useEffect(() => {
+    if (src) {
+      // 进页面查看图片的加载情况
+      // 创建一个img标签
+      const imgDom = document.createElement("img");
+      imgDom.src = baseURL + src;
+
+      // 不管图片加载成功或者失败,都删除掉,提高性能
+      // 图片加载成功
+      imgDom.onload = function () {
+        setLookImg(true);
+        imgDom.remove();
+      };
+      // 图片加载失败
+      imgDom.onerror = function () {
+        setPlaceholderUrl(imgErr);
+        imgDom.remove();
+      };
+
+      return () => {
+        // 离开页面也删掉掉元素
+        imgDom.remove();
+      };
+    }
+  }, [src]);
+
+  // 点击预览图片
+  const lookBigImg = useCallback(() => {
+    dispatch({
+      type: "login/lookBigImg",
+      payload: { url: baseURL + src, show: true },
+    });
+  }, [dispatch, src]);
+
+  return (
+    <div className={styles.ImageLazy} style={{ width: width, height: height }}>
+      <div className="lazyBox">
+        <Lazyimg
+          src={src ? baseURL + src : ""}
+          width={width}
+          height={height}
+          placeholder={placeholderUrl}
+          alt=""
+        />
+
+        {/* 图片预览 */}
+        {noLook || !lookImg ? null : (
+          <div className="lookImg" onClick={lookBigImg}>
+            <EyeOutlined />
+            &nbsp;
+            <div>预览</div>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+const MemoImageLazy = React.memo(ImageLazy);
+
+export default MemoImageLazy;

+ 26 - 0
src/components/NotFound/index.tsx

@@ -0,0 +1,26 @@
+import { Result } from "antd";
+import { useEffect, useRef } from "react";
+
+export default function NotFound() {
+  const timeRef = useRef(-1);
+
+  useEffect(() => {
+    timeRef.current = window.setTimeout(() => {
+      const dom: any = document.querySelector(".noFindPage");
+      dom.style.opacity = 1;
+    }, 200);
+    return () => {
+      clearTimeout(timeRef.current);
+    };
+  }, []);
+
+  return (
+    <div className="noFindPage">
+      <Result
+        status="404"
+        title="404"
+        subTitle="页面找不到或没有权限,请联系管理员!"
+      />
+    </div>
+  );
+}

+ 10 - 0
src/components/SpinLoding/index.module.scss

@@ -0,0 +1,10 @@
+.SpinLoding {
+  position: relative;
+  z-index: 9999;
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}

+ 13 - 0
src/components/SpinLoding/index.tsx

@@ -0,0 +1,13 @@
+import styles from "./index.module.scss";
+import { Spin } from "antd";
+import React from "react";
+function SpinLoding() {
+  return (
+    <div className={styles.SpinLoding}>
+      <Spin size='large'/>
+    </div>
+  );
+}
+const MemoSpinLoding = React.memo(SpinLoding);
+
+export default MemoSpinLoding;

+ 36 - 0
src/components/UpAsyncLoding/index.module.scss

@@ -0,0 +1,36 @@
+.UpAsyncLoding {
+  opacity: 0;
+  pointer-events: none;
+  position: fixed;
+  z-index: 9997;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, .4);
+
+  :global {
+    .progressBox {
+      position: absolute;
+      top: 60%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 500px;
+      height: 6px;
+      border-radius: 3px;
+      border: 1px solid var(--themeColor);
+      overflow: hidden;
+
+      #progress {
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 0%;
+        height: 100%;
+        background-color: var(--themeColor);
+      }
+
+    }
+
+  }
+}

+ 15 - 0
src/components/UpAsyncLoding/index.tsx

@@ -0,0 +1,15 @@
+import React from "react";
+import styles from "./index.module.scss";
+function UpAsyncLoding() {
+  return (
+    <div id="UpAsyncLoding" className={styles.UpAsyncLoding}>
+      <div className="progressBox">
+        <div id="progress"></div>
+      </div>
+    </div>
+  );
+}
+
+const MemoUpAsyncLoding = React.memo(UpAsyncLoding);
+
+export default MemoUpAsyncLoding;

+ 52 - 0
src/components/VideoLook/index.module.scss

@@ -0,0 +1,52 @@
+.VideoLook {
+  :global {
+    .videoLookBox {
+      cursor: pointer;
+      width: 100%;
+      height: 100%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+      .videoCover {
+        width: 100%;
+        height: 100%;
+        position: relative;
+
+        video {
+          width: 100%;
+          height: 100%;
+        }
+
+        .videoInco {
+          cursor: pointer;
+          transition: opacity .3s;
+          opacity: 0;
+          pointer-events: none;
+          position: absolute;
+          top: 0;
+          left: 0;
+          width: 100%;
+          height: 100%;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          font-size: 18px;
+          color: #fff;
+          background-color: rgba(0, 0, 0, .6);
+
+          &>div {
+            font-size: 14px;
+          }
+        }
+
+        &:hover {
+          .videoInco {
+            opacity: 1;
+            pointer-events: auto;
+          }
+        }
+      }
+    }
+  }
+}

+ 37 - 0
src/components/VideoLook/index.tsx

@@ -0,0 +1,37 @@
+import React from "react";
+import { PlayCircleOutlined } from "@ant-design/icons";
+import styles from "./index.module.scss";
+import { useDispatch } from "react-redux";
+import { baseURL } from "@/utils/http";
+
+type Props = {
+  width?: number;
+  height?: number;
+  src: string;
+};
+
+function VideoLook({ src, width = 100, height = 100 }: Props) {
+  const dispatch = useDispatch();
+
+  return (
+    <div className={styles.VideoLook} style={{ width, height }}>
+      <div
+        className="videoLookBox"
+        onClick={() => dispatch({ type: "login/lookVideo", payload: src })}
+      >
+        <div className="videoCover">
+          <video src={baseURL + src}></video>
+          <div className="videoInco">
+            <PlayCircleOutlined />
+            &nbsp;
+            <div>预览</div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+const MemoVideoLook = React.memo(VideoLook);
+
+export default MemoVideoLook;

+ 34 - 0
src/components/VideoLookDom/index.module.scss

@@ -0,0 +1,34 @@
+.VideoLookDom{
+  transition: opacity .3s;
+  position: fixed;
+  z-index: 9991;
+  opacity: 0;
+  pointer-events: none;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background-color: rgba(0,0,0,.6);
+  :global{
+    .close{
+      color: #fff;
+      position: absolute;
+      right: 70px;
+      top: 70px;
+      font-size: 30px;
+      cursor: pointer;
+    }
+    .viedoBox{
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%,-50%);
+      width: 800px;
+      height: 500px;
+      video{
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+}

+ 33 - 0
src/components/VideoLookDom/index.tsx

@@ -0,0 +1,33 @@
+import React from "react";
+import { CloseCircleOutlined } from "@ant-design/icons";
+import styles from "./index.module.scss";
+import { useDispatch, useSelector } from "react-redux";
+import { RootState } from "@/store";
+import { baseURL } from "@/utils/http";
+function VideoLookDom() {
+  const videoSrc = useSelector((state: RootState) => state.loginStore.videoSrc);
+  const dispatch = useDispatch();
+  return (
+    <div
+      className={styles.VideoLookDom}
+      style={videoSrc ? { opacity: 1, pointerEvents: "auto" } : {}}
+    >
+      <div className="viedoBox">
+        {videoSrc ? (
+          <video autoPlay controls src={baseURL + videoSrc}></video>
+        ) : null}
+      </div>
+
+      <div
+        className="close"
+        onClick={() => dispatch({ type: "login/lookVideo", payload: "" })}
+      >
+        <CloseCircleOutlined />
+      </div>
+    </div>
+  );
+}
+
+const MemoVideoLookDom = React.memo(VideoLookDom);
+
+export default MemoVideoLookDom;

+ 30 - 0
src/index.tsx

@@ -0,0 +1,30 @@
+// import 'default-passive-events';
+
+import App from "./App";
+import store from "./store/index";
+
+import { Provider } from "react-redux";
+import { createRoot } from "react-dom/client";
+
+import { ConfigProvider } from "antd";
+// import dayjs from "dayjs";
+import "dayjs/locale/zh-cn";
+import locale from "antd/locale/zh_CN";
+
+const container = document.getElementById("root") as HTMLElement;
+const root = createRoot(container);
+
+root.render(
+  <ConfigProvider
+    locale={locale}
+    theme={{
+      token: {
+        colorPrimary: "#b34831",
+      },
+    }}
+  >
+    <Provider store={store}>
+      <App />
+    </Provider>
+  </ConfigProvider>
+);

+ 252 - 0
src/pages/Exhibit/Edit.tsx

@@ -0,0 +1,252 @@
+import ImageLazy from "@/components/ImageLazy";
+import {
+  getExhibitInfoAPI,
+  exhibitUploadAPI,
+  exhibitionSaveAPI,
+} from "@/store/action/exhibit";
+import { ExhibitTableType } from "@/types/api/exhibit";
+import { Button, Form, Input, message, Modal, Popconfirm } from "antd";
+import TextArea from "antd/es/input/TextArea";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { PlusOutlined, CloseCircleOutlined } from "@ant-design/icons";
+import classNames from "classnames";
+import "./index.css";
+
+type Props = {
+  id: number;
+  closeFu: () => void;
+  upList: () => void;
+};
+
+// 上传附件的进度条
+const UpAsyncLodingDom: any = document.querySelector("#UpAsyncLoding");
+const progressDom: any = document.querySelector("#progress");
+
+function ExhibitEdit({ id, closeFu, upList }: Props) {
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<any>({});
+
+  // 通过id获取详情
+  const getInfoByIdFu = useCallback(async (id: number) => {
+    const res = await getExhibitInfoAPI(id);
+    // 回显表单信息
+    FormBoxRef.current.setFieldsValue(res.data);
+    // 回显展馆封面
+    setCover(res.data.thumb)
+  }, []);
+
+  useEffect(() => {
+    getInfoByIdFu(id);
+  }, [getInfoByIdFu, id]);
+
+  // 封面图
+  const [coverCheck, setCoverCheck] = useState(false);
+  const [cover, setCover] = useState("");
+
+  const myInput = useRef<HTMLInputElement>(null);
+
+  // 上传封面图
+  const handeUpPhoto = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files) {
+      // 拿到files信息
+      const filesInfo = e.target.files[0];
+      // 校验格式
+      const type = ["image/jpeg", "image/png"];
+      if (!type.includes(filesInfo.type)) {
+        e.target.value = "";
+        return message.warning("只支持jpg、png格式!");
+      }
+      // 校验大小
+      if (filesInfo.size > 20 * 1024 * 1024) {
+        e.target.value = "";
+        return message.warning("最大支持20M!");
+      }
+      // 创建FormData对象
+      const fd = new FormData();
+      // 把files添加进FormData对象(‘photo’为后端需要的字段)
+      fd.append("file", filesInfo);
+
+      e.target.value = "";
+
+      const res: any = await exhibitUploadAPI(fd);
+      if (res.code === 0) {
+        message.success("上传成功!");
+        setCover(res.data.filePath);
+      }
+      UpAsyncLodingDom.style.opacity = 0;
+      progressDom.style.width = "0%";
+    }
+  };
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    setCoverCheck(true);
+    // return message.warning("有表单不符号规则!");
+  }, []);
+
+  // 通过校验-点击确定
+  const onFinish = useCallback(
+    async (values: ExhibitTableType) => {
+      console.log("----通过校验", values);
+      setCoverCheck(true);
+      if (cover === "") return;
+
+      const obj: ExhibitTableType = {
+        ...values,
+        id: id,
+        thumb: cover,
+      };
+
+      const res = await exhibitionSaveAPI(obj);
+
+      if (res.code === 0) {
+        message.success("编辑成功!");
+        // 更新列表
+        upList();
+        // 关闭弹窗
+        closeFu();
+      }
+    },
+    [closeFu, cover, id, upList]
+  );
+
+  return (
+    <Modal
+      wrapClassName="ExhibitEdit"
+      destroyOnClose
+      open={true}
+      title="编辑展馆"
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className="ExhibitEditMain">
+        <Form
+          ref={FormBoxRef}
+          name="basic"
+          labelCol={{ span: 3 }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete="off"
+        >
+          <Form.Item className="noneFoucs" label="展馆名称" name="name">
+            <Input />
+          </Form.Item>
+
+          <Form.Item
+            getValueFromEvent={(e) => e.target.value.trim()}
+            label="展馆简介"
+            name="description"
+          >
+            <TextArea
+              rows={4}
+              placeholder="请输入内容"
+              showCount
+              maxLength={300}
+            />
+          </Form.Item>
+
+          {/* -----上传封面图片 */}
+          <div className="upThumbBox">
+            <input
+              id="upInput"
+              type="file"
+              accept=".png,.jpg,.jpeg"
+              ref={myInput}
+              onChange={(e) => handeUpPhoto(e)}
+            />
+
+            <div className="label">
+              <span>*</span> 展馆封面:
+            </div>
+            <div className="fileBoxRow_r">
+              <div
+                hidden={cover !== ""}
+                className="fileBoxRow_up"
+                onClick={() => myInput.current?.click()}
+              >
+                <PlusOutlined />
+              </div>
+              <div className="fileBoxRow_r_img" hidden={cover === ""}>
+                {cover ? (
+                  <ImageLazy width={100} height={100} src={cover} />
+                ) : null}
+
+                <Popconfirm
+                  title="确定删除吗?"
+                  okText="确定"
+                  cancelText="取消"
+                  onConfirm={() => setCover("")}
+                >
+                  <div className="clearCover">
+                    <CloseCircleOutlined />
+                  </div>
+                </Popconfirm>
+              </div>
+              <div className="fileBoxRow_r_tit">
+                格式要求:支持png、jpg和jpeg的图片格式;最大支持20M。
+                <br />
+                <div
+                  className={classNames(
+                    "noUpThumb",
+                    !cover && coverCheck ? "noUpThumbAc" : ""
+                  )}
+                >
+                  请上传展馆封面!
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <Form.Item
+            label="联系电话"
+            name="phone"
+            rules={[{ required: true, message: "请输入联系电话!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={30} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="展馆地址"
+            name="address"
+            rules={[{ required: true, message: "请输入展馆地址!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={30} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="开放时间"
+            name="openTime"
+            rules={[{ required: true, message: "请输入开放时间!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={30} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          {/* 确定和取消按钮 */}
+          <br />
+          <Form.Item wrapperCol={{ offset: 11, span: 16 }}>
+            <Button type="primary" htmlType="submit">
+              提交
+            </Button>
+            &emsp;
+            <Popconfirm
+              title="放弃编辑后,信息将不会保存!"
+              okText="放弃"
+              cancelText="取消"
+              onConfirm={closeFu}
+            >
+              <Button>取消</Button>
+            </Popconfirm>
+          </Form.Item>
+        </Form>
+      </div>
+    </Modal>
+  );
+}
+
+const MemoExhibitEdit = React.memo(ExhibitEdit);
+
+export default MemoExhibitEdit;

+ 80 - 0
src/pages/Exhibit/index.css

@@ -0,0 +1,80 @@
+.ExhibitEdit .ant-modal-close {
+  display: none;
+}
+.ExhibitEdit .ant-modal {
+  width: 800px !important;
+}
+.ExhibitEdit .ExhibitEditMain {
+  padding-right: 100px;
+  border-top: 1px solid #999999;
+  padding-top: 15px;
+  width: 100%;
+}
+.ExhibitEdit .ExhibitEditMain .noneFoucs .ant-form-item-row {
+  pointer-events: none;
+}
+.ExhibitEdit .ExhibitEditMain .noneFoucs .ant-form-item-row .ant-input {
+  border: none;
+}
+.ExhibitEdit .ExhibitEditMain .upThumbBox {
+  margin-top: -20px;
+  display: flex;
+  margin-bottom: 10px;
+}
+.ExhibitEdit .ExhibitEditMain .upThumbBox .label {
+  width: 81.5px;
+}
+.ExhibitEdit .ExhibitEditMain .upThumbBox .label > span {
+  position: relative;
+  top: 2px;
+  color: #ff4d4f;
+}
+.ExhibitEdit .ExhibitEditMain .fileBoxRow_r {
+  position: relative;
+}
+.ExhibitEdit .ExhibitEditMain .fileBoxRow_r .fileBoxRow_up {
+  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;
+}
+.ExhibitEdit .ExhibitEditMain .fileBoxRow_r .fileBoxRow_r_img {
+  width: 100px;
+  height: 100px;
+  position: relative;
+}
+.ExhibitEdit .ExhibitEditMain .fileBoxRow_r .fileBoxRow_r_img .clearCover {
+  cursor: pointer;
+  z-index: 10;
+  position: absolute;
+  width: 50px;
+  height: 50px;
+  top: 50%;
+  transform: translateY(-50%);
+  right: -50px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 24px;
+}
+.ExhibitEdit .ExhibitEditMain .fileBoxRow_r .fileBoxRow_r_tit {
+  height: 46px;
+  margin-top: 5px;
+  font-size: 14px;
+  color: #7e7c7c;
+}
+.ExhibitEdit .ExhibitEditMain .fileBoxRow_r .fileBoxRow_r_tit .noUpThumb {
+  overflow: hidden;
+  height: 0px;
+  transition: height 0.2s;
+  color: #ff4d4f;
+}
+.ExhibitEdit .ExhibitEditMain .fileBoxRow_r .fileBoxRow_r_tit .noUpThumbAc {
+  height: 24px;
+}

+ 97 - 0
src/pages/Exhibit/index.less

@@ -0,0 +1,97 @@
+.ExhibitEdit {
+  .ant-modal-close {
+    display: none;
+  }
+
+  .ant-modal {
+    width: 800px !important;
+  }
+
+  .ExhibitEditMain {
+    padding-right: 100px;
+    border-top: 1px solid #999999;
+    padding-top: 15px;
+    width: 100%;
+
+    .noneFoucs {
+      .ant-form-item-row {
+
+        pointer-events: none;
+
+        .ant-input {
+          border: none;
+        }
+      }
+    }
+
+    .upThumbBox{
+      margin-top: -20px;
+      display: flex;
+      margin-bottom: 10px;
+      .label{
+        width: 81.5px;
+        &>span{
+          position: relative;
+          top: 2px;
+          color: #ff4d4f;
+        }
+      }
+    }
+
+    .fileBoxRow_r {
+      position: relative;
+
+      .fileBoxRow_up {
+        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;
+
+
+      }
+
+      .fileBoxRow_r_img {
+        width: 100px;
+        height: 100px;
+        position: relative;
+
+        .clearCover {
+          cursor: pointer;
+          z-index: 10;
+          position: absolute;
+          width: 50px;
+          height: 50px;
+          top: 50%;
+          transform: translateY(-50%);
+          right: -50px;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          font-size: 24px;
+        }
+      }
+
+      .fileBoxRow_r_tit {
+        height: 46px;
+        margin-top: 5px;
+        font-size: 14px;
+        color: rgb(126, 124, 124);
+        .noUpThumb{
+          overflow: hidden;
+          height: 0px;
+          transition: height .2s;
+          color: #ff4d4f;
+        }
+        .noUpThumbAc{
+          height: 24px;
+        }
+      }
+    }
+  }
+}

+ 10 - 0
src/pages/Exhibit/index.module.scss

@@ -0,0 +1,10 @@
+.Exhibit {
+  :global {
+    .tableBox{
+      background-color: #fff;
+      border-radius: 10px;
+      margin-top: 15px;
+      height: calc(100% - 40px);
+    }
+  }
+}

+ 121 - 0
src/pages/Exhibit/index.tsx

@@ -0,0 +1,121 @@
+import ImageLazy from "@/components/ImageLazy";
+import { RootState } from "@/store";
+import { getExhibitListAPI } from "@/store/action/exhibit";
+import { ExhibitTableType } from "@/types/api/exhibit";
+import { Button, Table } from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useDispatch, useSelector } from "react-redux";
+import ExhibitEdit from "./Edit";
+import styles from "./index.module.scss";
+
+function Exhibit() {
+  const dispatch = useDispatch();
+
+  useEffect(() => {
+    dispatch(getExhibitListAPI());
+  }, [dispatch]);
+
+  // 从仓库中获取列表数据
+  const results = useSelector((state: RootState) => state.ExhibitReducer.list);
+
+  // 点击编辑
+  const editTableFu = useCallback((id: number) => {
+    editIdRef.current = id;
+    setOpen(true);
+  }, []);
+
+  const columns = useMemo(() => {
+    return [
+      {
+        width: 100,
+        title: "序号",
+        render: (text: any, record: any, index: any) => index + 1,
+      },
+      {
+        title: "展馆名称",
+        dataIndex: "name",
+      },
+
+      {
+        title: "展馆简介",
+        render: (item: ExhibitTableType) =>
+          item.description ? item.description : "(空)",
+      },
+      {
+        title: "展馆封面",
+        render: (item: ExhibitTableType) => (
+          <div className="tableImgAuto">
+            <ImageLazy width={80} height={80} src={item.thumb} />
+          </div>
+        ),
+      },
+      {
+        title: "联系电话",
+        dataIndex: "phone",
+      },
+      {
+        title: "展馆地址",
+        dataIndex: "address",
+      },
+      {
+        title: "开放时间",
+        dataIndex: "openTime",
+      },
+      {
+        title: "操作",
+        render: (item: ExhibitTableType, _: any, index: any) => (
+          <>
+            <Button
+              size="small"
+              type="text"
+              onClick={() => editTableFu(item.id)}
+            >
+              编辑
+            </Button>
+          </>
+        ),
+      },
+    ];
+  }, [editTableFu]);
+
+  // 点击编辑打开弹窗
+  const [open, setOpen] = useState(false);
+
+  const editIdRef = useRef(0);
+
+  return (
+    <div className={styles.Exhibit}>
+      <div className="pageTitlt">展馆管理</div>
+      {/* 表格主体 */}
+      <div className="tableBox">
+        <Table
+          size="small"
+          scroll={{ y: 700 }}
+          dataSource={results}
+          columns={columns}
+          rowKey="id"
+          pagination={false}
+        />
+      </div>
+
+      {/* 点击编辑打开的页面 */}
+      {open ? (
+        <ExhibitEdit
+          id={editIdRef.current}
+          closeFu={() => setOpen(false)}
+          upList={() => dispatch(getExhibitListAPI())}
+        />
+      ) : null}
+    </div>
+  );
+}
+
+const MemoExhibit = React.memo(Exhibit);
+
+export default MemoExhibit;

+ 40 - 0
src/pages/Goods/index.module.scss

@@ -0,0 +1,40 @@
+.Goods {
+  position: relative;
+
+  :global {
+    .searTopBox {
+      padding: 15px 20px 0;
+      background-color: #fff;
+      border-radius: 10px;
+      flex-wrap: wrap;
+      margin-top: 15px;
+
+      .searTopBoxSon {
+        width: 1250px;
+        position: relative;
+        display: flex;
+        align-items: center;
+
+        .row {
+          margin-right: 20px;
+          margin-bottom: 15px;
+        }
+
+        .rowBtn {
+          right: 20px;
+          top: 0px;
+          position: absolute;
+        }
+      }
+
+    }
+
+    .tableBox {
+      margin-top: 15px;
+      padding: 15px 15px 0;
+      height: calc(100% - 165px);
+      background-color: #fff;
+      border-radius: 10px;
+    }
+  }
+}

+ 460 - 0
src/pages/Goods/index.tsx

@@ -0,0 +1,460 @@
+import ImageLazy from "@/components/ImageLazy";
+import { RootState } from "@/store";
+import { getExhibitListAPI } from "@/store/action/exhibit";
+import {
+  getGoodsList,
+  goodsDisplayAPI,
+  goodsRemoveAPI,
+} from "@/store/action/goods";
+import { GoodsTableSearch } from "@/types";
+import { typeChangeObj } from "@/utils/changeData";
+import {
+  Input,
+  Select,
+  DatePicker,
+  Button,
+  Table,
+  Switch,
+  Popconfirm,
+  message,
+} from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useDispatch, useSelector } from "react-redux";
+import GoodsAdd from "../GoodsAdd";
+import styles from "./index.module.scss";
+
+const { RangePicker } = DatePicker;
+
+function Goods() {
+  const dispatch = useDispatch();
+
+  const pageNumRef = useRef(1);
+  const pagePageRef = useRef(10);
+
+  // 进来页面也要获取展馆的表格信息,用于所属展馆的下拉框
+  useEffect(() => {
+    dispatch(getExhibitListAPI());
+  }, [dispatch]);
+
+  const ExhibitList = useSelector(
+    (state: RootState) => state.ExhibitReducer.list
+  );
+
+  const ExhibitTableList = useMemo(() => {
+    const data = ExhibitList.map((v) => ({
+      value: v.id as any,
+      label: v.name,
+    }));
+    data.unshift({ value: "", label: "全部" });
+    return data;
+  }, [ExhibitList]);
+
+  // 0------------点击新增或者编辑出来的页面
+  const [editPageShow, setEditPageShow] = useState(false);
+  const editId = useRef(0);
+
+  const openEditPageFu = useCallback((id: number) => {
+    editId.current = id;
+    setEditPageShow(true);
+  }, []);
+
+  // 从仓库获取下拉列表数据
+  const dictList = useSelector((state: RootState) => state.loginStore.dictList);
+
+  // 从仓库获取表格列表信息
+  const tableInfo = useSelector(
+    (state: RootState) => state.goodsReducer.tableInfo
+  );
+
+  // 顶部筛选
+  const [tableSelect, setTableSelect] = useState<GoodsTableSearch>({
+    searchKey: "",
+    exhibitionId: "",
+    dictAge: "",
+    dictTexture: "",
+    dictLevel: "",
+    dictSource: "",
+    startTime: "",
+    endTime: "",
+    display: -1,
+
+    pageSize: 10,
+    pageNum: 1,
+  });
+
+  // 封装发送请求的函数
+
+  const getList = useCallback(async () => {
+    const data = {
+      ...tableSelect,
+      display: tableSelect.display === -1 ? null : tableSelect.display,
+
+      pageNum: pageNumRef.current,
+    };
+    dispatch(getGoodsList(data));
+  }, [dispatch, tableSelect]);
+
+  // 当前页码统一
+  useEffect(() => {
+    pageNumRef.current = tableSelect.pageNum;
+    pagePageRef.current = tableSelect.pageSize;
+  }, [tableSelect.pageNum, tableSelect.pageSize]);
+
+  // 防止返回的时候发送了2次请求来对应页码
+
+  const getListRef = useRef(-1);
+
+  useEffect(() => {
+    clearTimeout(getListRef.current);
+    getListRef.current = window.setTimeout(() => {
+      getList();
+    }, 100);
+  }, [getList, tableSelect]);
+
+  // 名称的输入
+  const nameTime = useRef(-1);
+  const nameChange = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      clearTimeout(nameTime.current);
+      nameTime.current = window.setTimeout(() => {
+        setTableSelect({
+          ...tableSelect,
+          searchKey: e.target.value,
+          pageNum: 1,
+        });
+      }, 500);
+    },
+    [tableSelect]
+  );
+
+  // 时间选择器改变
+  const timeChange = (date: any, dateString: any) => {
+    let startTime = "";
+    let endTime = "";
+    if (dateString[0] && dateString[1]) {
+      startTime = dateString[0] + " 00:00:00";
+      endTime = dateString[1] + " 23:59:59";
+    }
+    setTableSelect({ ...tableSelect, startTime, endTime, pageNum: 1 });
+  };
+
+  // 点击重置
+  const [inputKey, setInputKey] = useState(1);
+  const resetSelectFu = useCallback(() => {
+    // 把2个输入框和时间选择器清空
+    setInputKey(Date.now());
+    setTableSelect({
+      searchKey: "",
+      exhibitionId: "",
+      dictAge: "",
+      dictTexture: "",
+      dictLevel: "",
+      dictSource: "",
+      startTime: "",
+      endTime: "",
+      display: -1,
+
+      pageSize: 10,
+      pageNum: 1,
+    });
+  }, []);
+
+  // 关于表格的数据
+
+  // 页码变化
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      pageNumRef.current = pageNum;
+      pagePageRef.current = pageSize;
+      setTableSelect({ ...tableSelect, pageNum, pageSize });
+    },
+    [tableSelect]
+  );
+
+  // 切换表格中的启用停用状态
+  const isEnabledClickFu = useCallback(
+    async (val: boolean, id: number) => {
+      const isDisable = val ? 1 : 0;
+      const res: any = await goodsDisplayAPI(id, isDisable);
+      if (res.code === 0) getList();
+    },
+    [getList]
+  );
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      const res: any = await goodsRemoveAPI(id);
+      if (res.code === 0) {
+        message.success("删除成功!");
+        getList();
+      }
+    },
+    [getList]
+  );
+
+  const columns = useMemo(() => {
+    return [
+      // {
+      //   width: 80,
+      //   title: "序号",
+      //   render: (text: any, record: any, index: any) =>
+      //     index + 1 + (pageNumRef.current - 1) * pagePageRef.current,
+      // },
+      {
+        title: "标题",
+        dataIndex: "name",
+      },
+      {
+        title: "所属展馆",
+        dataIndex: "num",
+      },
+      {
+        title: "年代",
+        dataIndex: "dictAge",
+      },
+      {
+        title: "类别",
+        dataIndex: "dictTexture",
+      },
+
+      {
+        title: "级别",
+        dataIndex: "dictLevel",
+      },
+      {
+        title: "来源",
+        dataIndex: "dictSource",
+      },
+      {
+        title: "简介",
+        render: (item: GoodsTableSearch) =>
+          item.description ? (
+            item.description.length >= 20 ? (
+              <span style={{ cursor: "pointer" }} title={item.description}>
+                {item.description.substring(0, 20) + "..."}
+              </span>
+            ) : (
+              item.description
+            )
+          ) : (
+            "(空)"
+          ),
+      },
+      {
+        title: "封面图",
+        render: (item: GoodsTableSearch) => (
+          <div className="tableImgAuto">
+            <ImageLazy width={80} height={80} src={item.thumb!} />
+          </div>
+        ),
+      },
+      {
+        title: "类型",
+        render: (item: GoodsTableSearch) => typeChangeObj[item.type!],
+      },
+      {
+        title: "最近编辑时间",
+        dataIndex: "updateTime",
+      },
+      {
+        title: "展示状态",
+        render: (item: GoodsTableSearch) => (
+          <Switch
+            checkedChildren="启用"
+            unCheckedChildren="停用"
+            checked={item.display === 1}
+            onChange={(val) => isEnabledClickFu(val, item.id!)}
+          />
+        ),
+      },
+
+      {
+        title: "操作",
+        render: (item: GoodsTableSearch) => (
+          <>
+            <Button
+              size="small"
+              type="text"
+              onClick={() => openEditPageFu(item.id!)}
+            >
+              编辑
+            </Button>
+            <Popconfirm
+              title="删除后无法恢复,是否删除?"
+              okText="删除"
+              cancelText="取消"
+              onConfirm={() => delTableFu(item.id!)}
+            >
+              <Button size="small" type="text" danger>
+                删除
+              </Button>
+            </Popconfirm>
+          </>
+        ),
+      },
+    ];
+  }, [delTableFu, isEnabledClickFu, openEditPageFu]);
+
+  return (
+    <div className={styles.Goods}>
+      <div className="pageTitlt">馆藏管理</div>
+
+      <div className="searTopBox">
+        <div className="searTopBoxSon">
+          <div className="row">
+            <span>名称:</span>
+            <Input
+              key={inputKey}
+              maxLength={10}
+              style={{ width: 150 }}
+              placeholder="请输入关键字"
+              allowClear
+              onChange={(e) => nameChange(e)}
+            />
+          </div>
+          {ExhibitTableList.length > 1 ? (
+            <div className="row">
+              <span>所属展馆:</span>
+              <Select
+                placeholder="请选择"
+                style={{ width: 200 }}
+                value={tableSelect.exhibitionId}
+                onChange={(e) =>
+                  setTableSelect({
+                    ...tableSelect,
+                    exhibitionId: e,
+                    pageNum: 1,
+                  })
+                }
+                options={ExhibitTableList}
+              />
+            </div>
+          ) : null}
+
+          <div className="row">
+            <span>年代:</span>
+            <Select
+              placeholder="请选择"
+              style={{ width: 150 }}
+              value={tableSelect.dictAge}
+              onChange={(e) =>
+                setTableSelect({ ...tableSelect, dictAge: e, pageNum: 1 })
+              }
+              options={dictList["age"]}
+            />
+          </div>
+
+          <div className="row">
+            <span>类别:</span>
+            <Select
+              placeholder="请选择"
+              style={{ width: 150 }}
+              value={tableSelect.dictTexture}
+              onChange={(e) =>
+                setTableSelect({ ...tableSelect, dictTexture: e, pageNum: 1 })
+              }
+              options={dictList["texture"]}
+            />
+          </div>
+
+          <div className="row">
+            <span>级别:</span>
+            <Select
+              placeholder="请选择"
+              style={{ width: 100 }}
+              value={tableSelect.dictLevel}
+              onChange={(e) =>
+                setTableSelect({ ...tableSelect, dictLevel: e, pageNum: 1 })
+              }
+              options={dictList["level"]}
+            />
+          </div>
+
+          <div className="row">
+            <span>来源:</span>
+            <Select
+              placeholder="请选择"
+              style={{ width: 100 }}
+              value={tableSelect.dictSource}
+              onChange={(e) =>
+                setTableSelect({ ...tableSelect, dictSource: e, pageNum: 1 })
+              }
+              options={dictList["source"]}
+            />
+          </div>
+        </div>
+        <div className="searTopBoxSon">
+          <div className="row">
+            <span>最近编辑日期:</span>
+            <RangePicker key={inputKey} onChange={timeChange} />
+          </div>
+          <div className="row">
+            <span>展示状态:</span>
+            <Select
+              placeholder="请选择"
+              style={{ width: 100 }}
+              value={tableSelect.display}
+              onChange={(e) =>
+                setTableSelect({ ...tableSelect, display: e, pageNum: 1 })
+              }
+              options={[
+                { value: -1, label: "全部" },
+                { value: 1, label: "开启" },
+                { value: 0, label: "关闭" },
+              ]}
+            />
+          </div>
+
+          <div className="rowBtn">
+            <Button onClick={resetSelectFu}>重置</Button>
+            &emsp;&emsp;&emsp;&emsp;
+            <Button type="primary" onClick={() => openEditPageFu(0)}>
+              新增
+            </Button>
+          </div>
+        </div>
+      </div>
+
+      {/* 表格主体 */}
+      <div className="tableBox">
+        <Table
+          scroll={{ y: 480 }}
+          dataSource={tableInfo.list}
+          columns={columns}
+          rowKey="id"
+          pagination={{
+            showQuickJumper: true,
+            position: ["bottomCenter"],
+            showSizeChanger: true,
+            current: tableSelect.pageNum,
+            pageSize: tableSelect.pageSize,
+            total: tableInfo.total,
+            onChange: paginationChange(),
+          }}
+        />
+      </div>
+
+      {/* 点击新增或者编辑出来的页面 */}
+      {editPageShow ? (
+        <GoodsAdd
+          id={editId.current}
+          closePage={() => setEditPageShow(false)}
+          upTableList={getList}
+          addTableList={resetSelectFu}
+          ExhibitList={ExhibitList}
+        />
+      ) : null}
+    </div>
+  );
+}
+
+const MemoGoods = React.memo(Goods);
+
+export default MemoGoods;

+ 180 - 0
src/pages/GoodsAdd/index.module.scss

@@ -0,0 +1,180 @@
+.GoodsAdd {
+  position: absolute;
+  width: 100%;
+  height: 101%;
+  top: 0;
+  left: 0;
+  z-index: 10;
+  padding-right: 15px;
+  background-color: #faf0e4;
+
+
+  :global {
+    .formBox {
+      margin-top: 15px;
+      background-color: #fff;
+      border-radius: 10px;
+
+      padding: 20px 0 10px;
+      width: 100%;
+      height: calc(100% - 50px);
+      overflow-y: auto;
+
+      .formBoxSon {
+        width: 800px;
+
+        .upImgBox {
+          display: flex;
+          flex-wrap: wrap;
+
+          &>div {
+            margin: 0 15px 15px 0;
+          }
+
+          .fileBoxRow_r_img {
+            position: relative;
+
+            .clearCover {
+              right: -10px;
+              top: -10px;
+              transform: translate(0, 0);
+              background-color: rgba(0, 0, 0, .8);
+              width: 20px;
+              height: 20px;
+              border-radius: 50%;
+              font-size: 16px;
+              color: #fff;
+              // left: 50%;
+              // right: auto;
+              // top: auto;
+              // bottom: -26px;
+              // transform: translate(-50%,0);
+              // width: 20px;
+              // height: 20px;
+              // font-size: 18px;
+              // color: var(--themeColor);
+            }
+          }
+        }
+
+
+        .fileBoxRow_up {
+          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;
+        }
+
+        .fileBoxRow_r_img {
+          width: 100px;
+          height: 100px;
+          position: relative;
+
+          .clearCover {
+            cursor: pointer;
+            z-index: 10;
+            position: absolute;
+            width: 50px;
+            height: 50px;
+            top: 50%;
+            transform: translateY(-50%);
+            right: -50px;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            font-size: 24px;
+          }
+        }
+
+        .fileBoxRow_tit {
+          width: 133.33px;
+          text-align: right;
+          position: relative;
+
+          &::before {
+            content: '*';
+            position: absolute;
+            right: 60px;
+            top: 2px;
+            color: #ff4d4f;
+            z-index: 10;
+          }
+        }
+
+        .formRow {
+          margin-top: -28px;
+          display: flex;
+        }
+
+        .formRow2 {
+          margin-top: 10px;
+          display: flex;
+
+          .fileBoxRow_tit {
+            &::before {
+              right: 74px;
+            }
+          }
+        }
+
+        .upFileBox {
+          margin-top: 15px;
+          padding-left: 133.33px;
+
+          .fileBoxRow_r_tit {
+            padding-left: 0;
+          }
+
+          .fileRowBox {
+            display: flex;
+            align-items: center;
+            margin-top: 8px;
+
+            .clearCover {
+              cursor: pointer;
+              font-size: 20px;
+              margin-left: 24px;
+            }
+          }
+        }
+
+        .fileBoxRow_r_tit {
+          padding-left: 133.33px;
+          margin-top: 8px;
+          font-size: 14px;
+          color: rgb(126, 124, 124);
+        }
+
+
+      }
+    }
+
+    .formBox::-webkit-scrollbar {
+      /*滚动条整体样式*/
+      width: 5px;
+      /*高宽分别对应横竖滚动条的尺寸*/
+      height: 1px;
+    }
+
+    .formBox::-webkit-scrollbar-thumb {
+      /*滚动条里面小方块*/
+      border-radius: 10px;
+      -webkit-box-shadow: inset 0 0 5px transparent;
+      background: var(--themeColor);
+    }
+
+    .formBox::-webkit-scrollbar-track {
+      /*滚动条里面轨道*/
+      -webkit-box-shadow: inset 0 0 5px transparent;
+      border-radius: 10px;
+      background: transparent;
+    }
+
+  }
+}

+ 669 - 0
src/pages/GoodsAdd/index.tsx

@@ -0,0 +1,669 @@
+import { RootState } from "@/store";
+import { GoodsTableSearch } from "@/types";
+import { Button, Form, Input, message, Popconfirm, Radio, Select } from "antd";
+import TextArea from "antd/es/input/TextArea";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import {
+  PlusOutlined,
+  CloseCircleOutlined,
+  UploadOutlined,
+  PlayCircleOutlined,
+  CloseOutlined
+} from "@ant-design/icons";
+import { useDispatch, useSelector } from "react-redux";
+import styles from "./index.module.scss";
+import ImageLazy from "@/components/ImageLazy";
+import {
+  goodsDetailById,
+  goodsSaveAPI,
+  goodsUploadAPI,
+} from "@/store/action/goods";
+import { baseURL } from "@/utils/http";
+import { ExhibitTableType } from "@/types/api/exhibit";
+
+// 上传附件的进度条
+const UpAsyncLodingDom: any = document.querySelector("#UpAsyncLoding");
+const progressDom: any = document.querySelector("#progress");
+
+type Props = {
+  id: any;
+  closePage: () => void;
+  upTableList: () => void;
+  addTableList: () => void;
+  ExhibitList: ExhibitTableType[];
+};
+
+function GoodsAdd({
+  id,
+  closePage,
+  upTableList,
+  addTableList,
+  ExhibitList,
+}: Props) {
+  const dispatch = useDispatch();
+
+  // 从仓库获取下拉列表数据
+  const dictList = useSelector((state: RootState) => state.loginStore.dictList);
+
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<any>({});
+
+  const getInfoInAPIFu = useCallback(async (id: number) => {
+    const res = await goodsDetailById(id);
+    const data: GoodsTableSearch = res.data;
+    FormBoxRef.current.setFieldsValue(data);
+
+    setCover(data.thumb!);
+    setType(data.type!);
+
+    // if (data.type === "model") {
+    //   setModelFile({ fileName: data.fileName!, filePath: data.filePath! });
+    // } else if (data.type === "img")
+    //   setImgFile({ fileName: data.fileName!, filePath: data.filePath! });
+    // else if (data.type === "audio")
+    //   setAudioFile({ fileName: data.fileName!, filePath: data.filePath! });
+    // else if (data.type === "video")
+    //   setVideoFile({ fileName: data.fileName!, filePath: data.filePath! });
+
+    console.log("是编辑,在这里发请求拿数据", res);
+  }, []);
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    // return message.warning("有表单不符号规则!");
+  }, []);
+
+  useEffect(() => {
+    if (id) getInfoInAPIFu(id);
+    else {
+      setDirCode(Date.now() + "");
+      FormBoxRef.current.setFieldsValue({
+        topic: 1,
+      });
+    }
+  }, [getInfoInAPIFu, id]);
+
+  // 上传的inputRef
+  const myInput = useRef<HTMLInputElement>(null);
+  const myInput2 = useRef<HTMLInputElement>(null);
+
+  // 封面图片
+  const [cover, setCover] = useState("");
+
+  // 文件类型
+  const [type, setType] = useState<"model" | "img" | "audio" | "video">(
+    "model"
+  );
+
+  // 文件的dirCode码
+  const [dirCode, setDirCode] = useState("");
+
+  // 模型的链接输入
+  const [modelFile, setModelFile] = useState({
+    fileName: "",
+    filePath: "",
+    id: "",
+  });
+
+  // 图片的附件信息
+  const [imgFile, setImgFile] = useState<
+    {
+      fileName: string;
+      filePath: string;
+      id: string;
+    }[]
+  >([]);
+
+  // 删除某一张图片
+  const delImgListFu = useCallback(
+    (id: string) => {
+      setImgFile(imgFile.filter((v) => v.id !== id));
+    },
+    [imgFile]
+  );
+
+  // 音频的附件信息
+  const [audioFile, setAudioFile] = useState({
+    fileName: "",
+    filePath: "",
+    id: "",
+  });
+
+  // 视频的附件信息
+  const [videoFile, setVideoFile] = useState({
+    fileName: "",
+    filePath: "",
+    id: "",
+  });
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (values: GoodsTableSearch) => {
+      if (cover === "") return message.warning("请上传封面图!");
+      if (type === "model" && modelFile.filePath === "")
+        return message.warning("请输上传模型附件!");
+      if (type === "img" && imgFile.length === 0)
+        return message.warning("请上传图片附件!");
+      if (type === "audio" && audioFile.filePath === "")
+        return message.warning("请上传音频附件!");
+      if (type === "video" && videoFile.filePath === "")
+        return message.warning("请上传视频附件!");
+
+      let fileIds = "";
+
+      if (type === "model") fileIds = modelFile.id;
+      else if (type === "img") fileIds = imgFile.map((v) => v.id).join(",");
+      else if (type === "audio") fileIds = audioFile.id;
+      else if (type === "video") fileIds = videoFile.id;
+
+      const obj = {
+        ...values,
+        id: id ? id : null,
+        thumb: cover,
+        type: type,
+        fileIds,
+        dirCode: dirCode,
+      };
+
+      const res: any = await goodsSaveAPI(obj);
+
+      if (res.code === 0) {
+        message.success(id ? "编辑成功!" : "新增成功!");
+        if (id) upTableList();
+        else addTableList();
+
+        closePage();
+      }
+
+      console.log("通过校验,点击确定", res);
+    },
+    [
+      addTableList,
+      audioFile,
+      closePage,
+      cover,
+      dirCode,
+      id,
+      imgFile,
+      modelFile,
+      type,
+      upTableList,
+      videoFile,
+    ]
+  );
+
+  // 上传封面图
+  const handeUpPhoto = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    if (e.target.files) {
+      // 拿到files信息
+      const filesInfo = e.target.files[0];
+      // 校验格式
+      const type = ["image/jpeg", "image/png"];
+      if (!type.includes(filesInfo.type)) {
+        e.target.value = "";
+        return message.warning("只支持jpg、png格式!");
+      }
+      // 校验大小
+      if (filesInfo.size > 20 * 1024 * 1024) {
+        e.target.value = "";
+        return message.warning("最大支持20M!");
+      }
+      // 创建FormData对象
+      const fd = new FormData();
+      // 把files添加进FormData对象(‘photo’为后端需要的字段)
+      fd.append("type", "thumb");
+      fd.append("dirCode", dirCode);
+      fd.append("file", filesInfo);
+
+      e.target.value = "";
+
+      const res: any = await goodsUploadAPI(fd);
+      if (res.code === 0) {
+        message.success("上传成功!");
+        setCover(res.data.filePath);
+      }
+      UpAsyncLodingDom.style.opacity = 0;
+      progressDom.style.width = "0%";
+    }
+  };
+
+  // 上传附件
+  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 = "最大支持20M!";
+        let anSize = 20 * 1024 * 1024;
+
+        if (type === "audio") {
+          anType = ["audio/mpeg"];
+          anTit1 = "只支持mp3格式!";
+          anTit2 = "最大支持10M!";
+          anSize = 10 * 1024 * 1024;
+        } else if (type === "video") {
+          anType = ["video/mp4"];
+          anTit1 = "只支持mp4格式!";
+          anTit2 = "最大支持500M!";
+          anSize = 500 * 1024 * 1024;
+        } else if (type === "model") {
+          anType = [""];
+          anTit1 = "只支持4dage格式!";
+          anTit2 = "最大支持500M!";
+          anSize = 500 * 1024 * 1024;
+        }
+
+        // 校验格式
+        if (type !== "model") {
+          if (!anType.includes(filesInfo.type)) {
+            e.target.value = "";
+            return message.warning(anTit1);
+          }
+        } else {
+          if (!filesInfo.name.includes(".4dage")) {
+            e.target.value = "";
+            return message.warning(anTit1);
+          }
+        }
+
+        // 校验大小
+        if (filesInfo.size > anSize) {
+          e.target.value = "";
+          return message.warning(anTit2);
+        }
+        // 创建FormData对象
+        const fd = new FormData();
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append("type", type);
+        fd.append("file", filesInfo);
+
+        e.target.value = "";
+
+        const res: any = await goodsUploadAPI(fd);
+        if (res.code === 0) {
+          message.success("上传成功!");
+          if (type === "img") setImgFile([res.data, ...imgFile]);
+          else if (type === "audio") setAudioFile(res.data);
+          else if (type === "video") setVideoFile(res.data);
+          else if (type === "model") setModelFile(res.data);
+        }
+        UpAsyncLodingDom.style.opacity = 0;
+        progressDom.style.width = "0%";
+      }
+    },
+    [imgFile, type]
+  );
+
+  return (
+    <div className={styles.GoodsAdd}>
+      <div className="pageTitlt">{id ? "编辑" : "新增"}馆藏</div>
+      <div className="formBox">
+        <div className="formBoxSon">
+          <Form
+            ref={FormBoxRef}
+            name="basic"
+            labelCol={{ span: 4 }}
+            onFinish={onFinish}
+            onFinishFailed={onFinishFailed}
+            autoComplete="off"
+          >
+            <Form.Item
+              label="名称"
+              name="name"
+              rules={[{ required: true, message: "请输入名称!" }]}
+              getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+            >
+              <Input maxLength={50} showCount placeholder="请输入内容" />
+            </Form.Item>
+
+            <Form.Item
+              label="所属展馆"
+              name="exhibitionId"
+              rules={[{ required: true, message: "请选择所属展馆!" }]}
+            >
+              <Select
+                placeholder="请选择"
+                style={{ width: 400 }}
+                options={ExhibitList.map((v) => ({
+                  value: v.id,
+                  label: v.name,
+                }))}
+              />
+            </Form.Item>
+
+            <Form.Item
+              label="年代"
+              name="dictAge"
+              rules={[{ required: true, message: "请选择年代!" }]}
+            >
+              <Select
+                placeholder="请选择"
+                style={{ width: 400 }}
+                options={dictList["age"].slice(1)}
+              />
+            </Form.Item>
+
+            <Form.Item
+              label="类别"
+              name="dictTexture"
+              rules={[{ required: true, message: "请选择类别!" }]}
+            >
+              <Select
+                placeholder="请选择"
+                style={{ width: 400 }}
+                options={dictList["texture"].slice(1)}
+              />
+            </Form.Item>
+
+            <Form.Item
+              label="级别"
+              name="dictLevel"
+              rules={[{ required: true, message: "请选择级别!" }]}
+            >
+              <Select
+                placeholder="请选择"
+                style={{ width: 400 }}
+                options={dictList["level"].slice(1)}
+              />
+            </Form.Item>
+
+            <Form.Item
+              label="来源"
+              name="dictSource"
+              rules={[{ required: true, message: "请选择来源!" }]}
+            >
+              <Select
+                placeholder="请选择"
+                style={{ width: 400 }}
+                options={dictList["source"].slice(1)}
+              />
+            </Form.Item>
+
+            <Form.Item label="简历" name="description">
+              <TextArea
+                rows={4}
+                placeholder="请输入内容"
+                showCount
+                maxLength={500}
+              />
+            </Form.Item>
+            <input
+              id="upInput"
+              type="file"
+              accept=".png,.jpg,.jpeg"
+              ref={myInput}
+              onChange={(e) => handeUpPhoto(e)}
+            />
+            <input
+              id="upInput2"
+              type="file"
+              accept={
+                type === "img"
+                  ? ".gif,.png,.jpg,.jpeg"
+                  : type === "audio"
+                  ? ".mp3"
+                  : type === "model"
+                  ? ".4dage"
+                  : ".mp4"
+              }
+              ref={myInput2}
+              onChange={(e) => handeUpPhoto2(e)}
+            />
+
+            {/* 封面图片 */}
+
+            <div className="formRow">
+              <div className="fileBoxRow_tit">封面图:</div>
+              <div
+                hidden={cover !== ""}
+                className="fileBoxRow_up"
+                onClick={() => myInput.current?.click()}
+              >
+                <PlusOutlined />
+              </div>
+              <div className="fileBoxRow_r_img" hidden={cover === ""}>
+                {cover ? (
+                  <ImageLazy width={100} height={100} src={cover} />
+                ) : null}
+
+                <Popconfirm
+                  title="删除后无法恢复,是否删除?"
+                  okText="删除"
+                  cancelText="取消"
+                  onConfirm={() => setCover("")}
+                >
+                  <div className="clearCover">
+                    <CloseCircleOutlined />
+                  </div>
+                </Popconfirm>
+              </div>
+            </div>
+            <div className="fileBoxRow_r_tit">
+              格式要求:支持png、jpg和jpeg的图片格式;最大支持20M。
+            </div>
+
+            {/* 文件类型 */}
+            <div className="formRow2">
+              <div className="fileBoxRow_tit">文件类型:</div>
+              <Radio.Group
+                onChange={(e) => setType(e.target.value)}
+                value={type}
+              >
+                <Radio value="model">模型</Radio>
+                <Radio value="img">图片</Radio>
+                <Radio value="audio">音频</Radio>
+                <Radio value="video">视频</Radio>
+              </Radio.Group>
+            </div>
+
+            {/* 模型的文件上传 */}
+            <div className="upFileBox">
+              <div hidden={type !== "model"}>
+                <Button
+                  hidden={!!modelFile.filePath}
+                  onClick={() => myInput2.current?.click()}
+                  icon={<UploadOutlined />}
+                >
+                  上传
+                </Button>
+
+                <div className="donUpFileBox">
+                  <div className="fileRowBox">
+                    <a
+                      href={baseURL + modelFile.filePath}
+                      download
+                      target="_blank"
+                      className="upSuccTxt"
+                      rel="noreferrer"
+                    >
+                      {modelFile.fileName}
+                    </a>
+
+                    <Popconfirm
+                      title="删除后无法恢复,是否删除?"
+                      okText="删除"
+                      cancelText="取消"
+                      onConfirm={() =>
+                        setModelFile({ fileName: "", filePath: "", id: "" })
+                      }
+                    >
+                      <div className="clearCover" hidden={!modelFile.fileName}>
+                        <CloseCircleOutlined />
+                      </div>
+                    </Popconfirm>
+                  </div>
+
+                  <div className="fileBoxRow_r_tit">
+                    仅支持4dage格式的模型文件,大小不能超过500M。
+                  </div>
+                </div>
+              </div>
+
+              {/* 上传附件图片 */}
+              <div hidden={type !== "img"}>
+                <div className="upImgBox">
+                  <div
+                    hidden={imgFile.length >= 9}
+                    className="fileBoxRow_up"
+                    onClick={() => myInput2.current?.click()}
+                  >
+                    <PlusOutlined />
+                  </div>
+                  {imgFile.map((v) => (
+                    <div className="fileBoxRow_r_img" key={v.id} >
+                      {v.filePath ? (
+                        <ImageLazy width={100} height={100} src={v.filePath} />
+                      ) : null}
+
+                      <Popconfirm
+                        title="删除后无法恢复,是否删除?"
+                        okText="删除"
+                        cancelText="取消"
+                        onConfirm={() => delImgListFu(v.id)}
+                      >
+                        <div className="clearCover">
+                        <CloseOutlined />
+                        </div>
+                      </Popconfirm>
+                    </div>
+                  ))}
+                </div>
+
+                <div className="fileBoxRow_r_tit">
+                  支持png、jpg、gif和jpeg的图片格式;最大支持20M;最多支持9张。
+                </div>
+              </div>
+
+              {/* 上传音频和视频 */}
+              <div hidden={type === "model" || type === "img"}>
+                <Button
+                  hidden={
+                    (type === "audio" && !!audioFile.filePath) ||
+                    (type === "video" && !!videoFile.filePath)
+                  }
+                  onClick={() => myInput2.current?.click()}
+                  icon={<UploadOutlined />}
+                >
+                  上传
+                </Button>
+
+                {type === "audio" ? (
+                  <div className="donUpFileBox">
+                    <div className="fileRowBox">
+                      <a
+                        href={baseURL + audioFile.filePath}
+                        download
+                        target="_blank"
+                        className="upSuccTxt"
+                        rel="noreferrer"
+                      >
+                        {audioFile.fileName}
+                      </a>
+
+                      <Popconfirm
+                        title="删除后无法恢复,是否删除?"
+                        okText="删除"
+                        cancelText="取消"
+                        onConfirm={() =>
+                          setAudioFile({ fileName: "", filePath: "", id: "" })
+                        }
+                      >
+                        <div
+                          className="clearCover"
+                          hidden={!audioFile.fileName}
+                        >
+                          <CloseCircleOutlined />
+                        </div>
+                      </Popconfirm>
+                    </div>
+
+                    <div className="fileBoxRow_r_tit">
+                      仅支持MP3格式的音频文件,大小不得超过10MB。
+                    </div>
+                  </div>
+                ) : type === "video" ? (
+                  <div className="donUpFileBox">
+                    <div className="fileRowBox">
+                      <div className="upSuccTxt">{videoFile.fileName}</div>
+                      <div
+                        className="clearCover"
+                        hidden={!videoFile.fileName}
+                        onClick={() =>
+                          dispatch({
+                            type: "login/lookVideo",
+                            payload: videoFile.filePath,
+                          })
+                        }
+                      >
+                        <PlayCircleOutlined />
+                      </div>
+
+                      <Popconfirm
+                        title="删除后无法恢复,是否删除?"
+                        okText="删除"
+                        cancelText="取消"
+                        onConfirm={() =>
+                          setVideoFile({ fileName: "", filePath: "", id: "" })
+                        }
+                      >
+                        <div
+                          className="clearCover"
+                          hidden={!videoFile.fileName}
+                        >
+                          <CloseCircleOutlined />
+                        </div>
+                      </Popconfirm>
+                    </div>
+
+                    <div className="fileBoxRow_r_tit">
+                      仅支持MP4格式的视频文件,大小不得超过500MB。
+                    </div>
+                  </div>
+                ) : null}
+              </div>
+            </div>
+            <br />
+            <Form.Item
+              label="展示状态"
+              name="display"
+              rules={[{ required: true, message: "请选择来源!" }]}
+            >
+              <Select
+                placeholder="请选择"
+                style={{ width: 400 }}
+                options={[
+                  { value: 1, label: "展示" },
+                  { value: 0, label: "不展示" },
+                ]}
+              />
+            </Form.Item>
+
+            {/* 确定和取消按钮 */}
+            <br />
+            <Form.Item wrapperCol={{ offset: 10, span: 16 }}>
+              <Button type="primary" htmlType="submit">
+                提交
+              </Button>
+              &emsp;
+              <Popconfirm
+                title="放弃编辑后,信息将不会保存!"
+                okText="放弃"
+                cancelText="取消"
+                onConfirm={closePage}
+              >
+                <Button>取消</Button>
+              </Popconfirm>
+            </Form.Item>
+          </Form>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+const MemoGoodsAdd = React.memo(GoodsAdd);
+
+export default MemoGoodsAdd;

+ 197 - 0
src/pages/Layout/index.module.scss

@@ -0,0 +1,197 @@
+.Layout {
+  width: 100%;
+  height: 100%;
+  display: flex;
+
+  :global {
+
+    .layoutLeft {
+      position: relative;
+      z-index: 10;
+      width: 220px; 
+      height: 100%;
+      box-shadow: 0px 0px 5px 3px;
+
+      .layoutLeftTop {
+        height: 60px;
+        background-color: var(--themeColor);
+        padding: 7px 0 0 20px;
+
+        &>h3 {
+          color: #fff;
+        }
+      }
+
+      .layoutLeftMain {
+        height: calc(100% - 60px);
+        padding: 20px;
+        background-image: url('../../assets/img/layoutLeftMain.jpg');
+        background-size: 100% 100%;
+
+        .mainBoxL2Row {
+          padding-left: 60px;
+          cursor: pointer;
+          height: 50px;
+          line-height: 50px;
+          font-size: 16px;
+          position: relative;
+          margin-bottom: 15px;
+
+          .tabImg {
+            z-index: 3;
+            position: absolute;
+            top: 50%;
+            left: 20px;
+            transform: translateY(-50%);
+            width: 20px;
+          }
+
+          .tabImgAc {
+            z-index: 3;
+            position: absolute;
+            top: 50%;
+            left: 20px;
+            transform: translateY(-50%);
+            width: 20px;
+            display: none;
+          }
+
+          &:hover {
+            color: #fff;
+            background-image: url('../../assets/img/activeLL.png');
+            background-size: 100% 100%;
+
+            .tabImgAc {
+              display: block;
+            }
+          }
+        }
+
+
+
+        .active {
+          color: #fff;
+          pointer-events: none;
+          background-image: url('../../assets/img/activeLL.png');
+          background-size: 100% 100%;
+
+          .tabImgAc {
+            display: block;
+          }
+        }
+      }
+
+
+    }
+
+    .layoutRight {
+      width: calc(100% - 220px);
+      height: 100%;
+ 
+
+      .layoutRightTop {
+        height: 60px;
+        background-image: url('../../assets/img/layoutTop.jpg');
+        background-size: 100% 100%;
+        display: flex;
+        justify-content: flex-end;
+        position: relative;
+        z-index: 10;
+
+        .user {
+          padding-right: 40px;
+          display: flex;
+          align-items: center;
+          padding-left: 55px;
+          cursor: pointer;
+          position: relative;
+          background: url('../../assets/img/user.png') no-repeat left center;
+          background-size: 40px 40px;
+          font-size: 16px;
+          color: #fff;
+
+          .userInco {
+            margin-left: 10px;
+            color: #fff;
+          }
+
+          .userInco1 {
+            display: none;
+          }
+
+          .userSet {
+            border-radius: 10px;
+            overflow: hidden;
+            width: 140px;
+            opacity: 0;
+            pointer-events: none;
+            transition: bottom .3s;
+            height: 120px;
+            position: absolute;
+            left: 50%;
+            transform: translateX(-50%);
+            bottom: -80px;
+            padding-top: 20px;
+            color: rgb(226, 223, 223);
+
+            &>span {
+              background-color: var(--themeColor);
+              display: block;
+              width: 100%;
+              height: 50px;
+              line-height: 50px;
+              text-align: center;
+
+              &:first-child {
+                border-radius: 10px 10px 0 0;
+              }
+
+              &:hover {
+                color: #fff;
+              }
+            }
+          }
+
+          &:hover {
+            .userSet {
+              opacity: 1;
+              pointer-events: auto;
+              bottom: -110px;
+            }
+
+            .userInco1 {
+              display: block;
+            }
+
+            .userInco2 {
+              display: none;
+            }
+
+
+          }
+        }
+      }
+
+      .layoutRightMain {
+        height: calc(100% - 60px);
+        .mainBoxR {
+          width: 100%;
+          height: 100%;
+          background-image: url('../../assets/img/bg.jpg');
+          background-size: 100% 100%;
+          overflow: hidden;
+          padding:24px 30px;
+          
+          &>div {
+            width: 100%;
+            height: 100%;
+          }
+        }
+      }
+
+    }
+
+
+
+  }
+}

+ 305 - 0
src/pages/Layout/index.tsx

@@ -0,0 +1,305 @@
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { CaretUpOutlined, CaretDownOutlined } from "@ant-design/icons";
+import styles from "./index.module.scss";
+import SpinLoding from "@/components/SpinLoding";
+import { Route, Switch, useLocation } from "react-router-dom";
+import AuthRoute from "@/components/AuthRoute";
+import NotFound from "@/components/NotFound";
+import classNames from "classnames";
+import history from "@/utils/history";
+import { Button, Form, Input, message, Modal, Popconfirm } from "antd";
+import { Base64 } from "js-base64";
+import encodeStr from "@/utils/pass";
+import { getDictListAPI, passWordEditAPI } from "@/store/action/login";
+import { getTokenInfo, removeTokenInfo } from "@/utils/storage";
+import { useDispatch, useSelector } from "react-redux";
+import { RootState } from "@/store";
+import inco1 from "@/assets/img/inco1.png";
+import inco2 from "@/assets/img/inco2.png";
+import inco3 from "@/assets/img/inco3.png";
+import inco4 from "@/assets/img/inco4.png";
+import inco1Ac from "@/assets/img/inco1Ac.png";
+import inco2Ac from "@/assets/img/inco2Ac.png";
+import inco3Ac from "@/assets/img/inco3Ac.png";
+import inco4Ac from "@/assets/img/inco4Ac.png";
+
+function Layout() {
+  const dispatch = useDispatch();
+
+  const listTemp = useMemo(() => {
+    return [
+      {
+        id: 100,
+        name: "展馆管理",
+        path: "/",
+        done: true,
+        Com: React.lazy(() => import("../Exhibit")),
+        inco: inco1,
+        incoAc: inco1Ac,
+      },
+      {
+        id: 200,
+        name: "馆藏管理",
+        path: "/goods",
+        done: true,
+        Com: React.lazy(() => import("../Goods")),
+        inco: inco2,
+        incoAc: inco2Ac,
+      },
+    ];
+  }, []);
+
+  // 从仓库中获取页面权限数据
+  const authPageArr = useSelector(
+    (state: RootState) => state.loginStore.authPageArr
+  );
+
+  // 是超级管理员
+  useEffect(() => {
+    const userInfo = getTokenInfo().user;
+    if (userInfo.isAdmin === 1) {
+      listTemp.push(
+        {
+          id: 300,
+          name: "用户管理",
+          path: "/user",
+          done: true,
+          Com: React.lazy(() => import("../User")),
+          inco: inco3,
+          incoAc: inco3Ac,
+        },
+
+        {
+          id: 400,
+          name: "操作日志",
+          path: "/log",
+          done: true,
+          Com: React.lazy(() => import("../Log")),
+          inco: inco4,
+          incoAc: inco4Ac,
+        }
+      );
+    }
+  }, [listTemp]);
+
+  // 权限的数据和页面判断
+  useEffect(() => {
+    // authPageArr.forEach((v) => {
+    //   if (v.authority) {
+    //     listTemp.forEach((v2) => {
+    //       if (v.id === v2.id) v2.done = true;
+    //     });
+    //   }
+    // });
+    // const newList = listTemp.filter((v) => v.done);
+    // setList(newList);
+  }, [authPageArr, listTemp]);
+
+  const [list, setList] = useState(listTemp);
+
+  // 进页面看看第一个页面有权限的是哪一个
+  useEffect(() => {
+    const userInfo = getTokenInfo().user;
+    if (userInfo.isAdmin !== 1) {
+      if (list[0] && list[0].id !== 100) history.replace(list[0].path);
+    }
+  }, [list]);
+
+  // 进页面获取所有下拉信息和权限信息
+  useEffect(() => {
+    dispatch(getDictListAPI());
+    // dispatch(getPermissionsAPI());
+  }, [dispatch]);
+
+  // 点击跳转
+  const pathCutFu = useCallback((path: string) => {
+    history.push(path);
+  }, []);
+
+  // 当前路径选中的左侧菜单
+  const location = useLocation();
+  const [path, setPath] = useState("");
+
+  useEffect(() => {
+    const arr = location.pathname.split("/");
+    let pathTemp = "/";
+    if (arr[1]) pathTemp = "/" + arr[1];
+
+    setPath(pathTemp);
+  }, [location]);
+
+  const userInfo = useMemo(() => {
+    return getTokenInfo().user;
+  }, []);
+
+  // 修改密码相关
+  const [open, setOpen] = useState(false);
+
+  // 拿到新密码的输入框的值
+  const oldPasswordValue = useRef("");
+
+  const checkPassWord = (rule: any, value: any = "") => {
+    if (value !== oldPasswordValue.current)
+      return Promise.reject("新旧密码不一致!");
+    else return Promise.resolve(value);
+  };
+
+  const onFinish = async (values: any) => {
+    // 通过校验之后发送请求
+    if (values.oldPassword === values.newPassword)
+      return message.warning("新旧密码不能相同!");
+    const obj = {
+      oldPassword: encodeStr(Base64.encode(values.oldPassword)),
+      newPassword: encodeStr(Base64.encode(values.newPassword)),
+    };
+    const res: any = await passWordEditAPI(obj);
+    if (res.code === 0) {
+      message.success("修改成功!");
+      loginExit();
+    }
+  };
+
+  // 点击退出登录
+  const loginExit = () => {
+    removeTokenInfo();
+    history.push("/login");
+  };
+
+  return (
+    <div className={styles.Layout}>
+      {/* 左边 */}
+      <div className="layoutLeft">
+        <div className="layoutLeftTop">
+          <h3>
+            中医药文化宣传教育基地
+            <br />
+            线上展馆管理后台
+          </h3>
+        </div>
+        {/* 左边主体 */}
+        <div className="layoutLeftMain">
+          {list.map((v) => (
+            <div
+              key={v.id}
+              onClick={() => pathCutFu(v.path)}
+              className={classNames(
+                "mainBoxL2Row",
+                v.path === path ? "active" : ""
+              )}
+            >
+              <img className="tabImg" src={v.inco} alt="" />
+              <img className="tabImgAc" src={v.incoAc} alt="" />
+              <div className="txt">{v.name}</div>
+            </div>
+          ))}
+        </div>
+      </div>
+      {/* 右边 */}
+      <div className="layoutRight">
+        <div className="layoutRightTop">
+          {/* 用户相关 */}
+          <div className="user">
+            {userInfo.realName}
+            <div className="userInco userInco1">
+              <CaretUpOutlined />
+            </div>
+            <div className="userInco userInco2">
+              <CaretDownOutlined />
+            </div>
+            <div className="userSet">
+              <span onClick={() => setOpen(true)}>修改密码</span>
+              <Popconfirm
+                placement="bottom"
+                title="确定退出吗?"
+                okText="确定"
+                cancelText="取消"
+                onConfirm={loginExit}
+              >
+                退出登录
+              </Popconfirm>
+            </div>
+          </div>
+        </div>
+        {/* 右边主体 */}
+        <div className="layoutRightMain">
+          {/* 二级路由页面 */}
+          <div className="mainBoxR">
+            <React.Suspense fallback={<SpinLoding />}>
+              <Switch>
+                {list.map((v) => (
+                  <AuthRoute key={v.id} exact path={v.path} component={v.Com} />
+                ))}
+
+                <Route path="*" component={NotFound} />
+              </Switch>
+            </React.Suspense>
+          </div>
+        </div>
+      </div>
+
+      {/* 点击修改密码打开的对话框 */}
+      <Modal
+        destroyOnClose
+        open={open}
+        title="修改密码"
+        onCancel={() => setOpen(false)}
+        footer={
+          [] // 设置footer为空,去掉 取消 确定默认按钮
+        }
+      >
+        <Form
+          name="basic"
+          labelCol={{ span: 5 }}
+          wrapperCol={{ span: 16 }}
+          onFinish={onFinish}
+          autoComplete="off"
+        >
+          <Form.Item
+            label="旧密码"
+            name="oldPassword"
+            rules={[{ required: true, message: "不能为空!" }]}
+          >
+            <Input.Password maxLength={15} />
+          </Form.Item>
+
+          <Form.Item
+            label="新密码"
+            name="newPassword"
+            rules={[{ required: true, message: "不能为空!" }]}
+          >
+            <Input.Password
+              maxLength={15}
+              onChange={(e) => (oldPasswordValue.current = e.target.value)}
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="确定新密码"
+            name="checkPass"
+            rules={[{ validator: checkPassWord }]}
+          >
+            <Input.Password maxLength={15} />
+          </Form.Item>
+
+          <Form.Item wrapperCol={{ offset: 14, span: 16 }}>
+            <Button onClick={() => setOpen(false)}>取消</Button>
+            &emsp;
+            <Button type="primary" htmlType="submit">
+              确定
+            </Button>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+}
+
+// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
+const MemoLayout = React.memo(Layout);
+export default MemoLayout;

+ 23 - 0
src/pages/Log/index.module.scss

@@ -0,0 +1,23 @@
+.Log{
+  :global{
+    .logTop{
+      height: 100px;
+      border-radius: 10px;
+      background-color: #fff;
+      .tableSelectBox{
+        padding: 8px 24px 0;
+        display: flex;
+        align-items: center;
+        .row{
+          margin-right: 20px;
+        }
+      }
+    }
+    .tableMain{
+      border-radius: 10px;
+      margin-top: 20px;
+      height: calc(100% - 117px);
+      background-color: #fff;
+    }
+  }
+}

+ 133 - 0
src/pages/Log/index.tsx

@@ -0,0 +1,133 @@
+import { RootState } from "@/store";
+import { getLogListAPI } from "@/store/action/log";
+import { Input, DatePicker, Table } from "antd";
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import styles from "./index.module.scss";
+
+const { RangePicker } = DatePicker;
+
+function Log() {
+  const dispatch = useDispatch();
+
+  const pageNumRef = useRef(1);
+  const pagePageRef = useRef(10);
+  // 筛选和分页
+  const [tableSelect, setTableSelect] = useState({
+    searchKey: "",
+    pageSize: 10,
+    pageNum: 1,
+    startTime: "",
+    endTime: "",
+  });
+
+  // 账号的输入
+  const nameTime = useRef(-1);
+  const nameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+    clearTimeout(nameTime.current);
+    nameTime.current = window.setTimeout(() => {
+      setTableSelect({ ...tableSelect, searchKey: e.target.value, pageNum: 1 });
+    }, 500);
+  };
+  // 时间选择器改变
+  const timeChange = (date: any, dateString: any) => {
+    let startTime = "";
+    let endTime = "";
+    if (dateString[0] && dateString[1]) {
+      startTime = dateString[0] + " 00:00:00";
+      endTime = dateString[1] + " 23:59:59";
+    }
+    setTableSelect({ ...tableSelect, startTime, endTime, pageNum: 1 });
+  };
+
+  useEffect(() => {
+    pageNumRef.current = tableSelect.pageNum;
+    pagePageRef.current = tableSelect.pageSize;
+    dispatch(getLogListAPI(tableSelect));
+  }, [dispatch, tableSelect]);
+
+  // ---------关于表格
+
+  // 页码变化
+  const paginationChange = (pageNum: number, pageSize: number) => {
+    pageNumRef.current = pageNum;
+    pagePageRef.current = pageSize;
+    setTableSelect({ ...tableSelect, pageNum, pageSize });
+  };
+
+  const results = useSelector((state: RootState) => state.logReducer.tableInfo);
+
+  const columns = useMemo(() => {
+    return [
+      {
+        title: "序号",
+        render: (text: any, record: any, index: any) =>
+          index + 1 + (pageNumRef.current - 1) * pagePageRef.current,
+      },
+      {
+        title: "操作者",
+        dataIndex: "userName",
+      },
+      {
+        title: "操作日期",
+        dataIndex: "createTime",
+      },
+      {
+        title: "IP记录",
+        dataIndex: "ip",
+      },
+      {
+        title: "操作记录",
+        dataIndex: "type",
+      },
+    ];
+  }, []);
+
+  return (
+    <div className={styles.Log}>
+      <div className="logTop">
+        <div className="pageTitlt">操作日志</div>
+        <div className="tableSelectBox">
+          <div className="row">
+            <span>账号:</span>
+            <Input
+              maxLength={10}
+              style={{ width: 150 }}
+              placeholder="请输入"
+              allowClear
+              onChange={(e) => nameChange(e)}
+            />
+          </div>
+          <div className="row">
+            <span>操作日期:</span>
+            <RangePicker onChange={timeChange} />
+          </div>
+        </div>
+      </div>
+
+      {/* 表格主体 */}
+      <div className="tableMain">
+        <Table
+          scroll={{ y: 570 }}
+          dataSource={results.list}
+          columns={columns}
+          rowKey="id"
+          pagination={{
+            showQuickJumper: true,
+            position: ["bottomCenter"],
+            showSizeChanger: true,
+            current: tableSelect.pageNum,
+            pageSize: tableSelect.pageSize,
+            total: results.total,
+            onChange: paginationChange,
+          }}
+        />
+      </div>
+    </div>
+  );
+}
+
+const MemoLog = React.memo(Log);
+
+export default MemoLog;

+ 128 - 0
src/pages/Login/index.module.scss

@@ -0,0 +1,128 @@
+.Login {
+  width: 100%;
+  height: 100%;
+  background-image: url('../../assets/img/bg.jpg');
+  background-size: cover;
+  position: relative;
+
+  :global {
+    .loginTop{
+      height: 60px;
+      background-image: url('../../assets/img/top.jpg');
+      background-size: 100% 100%;
+      padding: 7px 0 0 20px;
+      color: #fff;
+    }
+    .main {
+      border-radius: 6px;
+      position: absolute;
+      top: 52%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 1266px;
+      height: 694px;
+      .mainLeft{
+        width: 50%;
+        height: 100%;
+        background-image: url('../../assets/img/loginLeft.png');
+        background-size: 100% 100%;
+        position: relative;
+        .mainLeftTitle{
+          font-size: 20px;
+          letter-spacing: 5px;
+          position: absolute;
+          left: 50%;
+          bottom: 60px;
+          transform: translateX(-50%);
+          color: #fff;
+        }
+      }
+      .mainRight {
+        position: absolute;
+        right: 8px;
+        top: 10%;
+        width: 50%;
+        height: 80%;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        background-image: url('../../assets/img/loginRight.png');
+        background-size: 100% 100%;
+
+        &>h1 {
+          letter-spacing: 6px;
+          font-size: 30px;
+          font-weight: 600;
+        }
+
+        .inputBox {
+          width: 100%;
+
+          .inputBoxRow {
+            width: 400px;
+            margin: 50px auto;
+
+            .ant-input-suffix .ant-input-password-icon {
+              font-size: 22px;
+            }
+          }
+
+          .ant-input-prefix {
+            margin-right: 10px;
+
+            .anticon {
+              padding-right: 10px;
+              width: 36px;
+              height: 36px;
+
+              svg {
+                width: 100%;
+                height: 100%;
+              }
+            }
+          }
+
+          .ant-input {
+            font-size: 18px;
+            width: 45%;
+            height: 60px;
+            line-height: 60px;
+            background-clip: content-box;
+          }
+
+          input:-webkit-autofill {
+            background-image: none;
+            -webkit-box-shadow: 0 0 0px 1000px transparent inset !important; //填充阴影,可以用来遮住背景色
+            background-color: transparent;
+            transition: background-color 50000s ease-in-out 0s; //背景色透明  生效时长  过渡效果  启用时延迟的时间
+
+          }
+
+          .ant-input-affix-wrapper {
+            background-color: transparent;
+            padding: 0 11px;
+            width: 100%;
+            height: 60px;
+
+            .ant-input {
+              background-color: transparent;
+              width: 100%;
+              height: 60px;
+            }
+          }
+        }
+
+        .loginBtn {
+
+          .ant-btn {
+            font-size: 24px;
+            width: 400px;
+            height: 60px;
+          }
+        }
+      }
+
+    }
+  }
+}

+ 92 - 0
src/pages/Login/index.tsx

@@ -0,0 +1,92 @@
+import styles from "./index.module.scss";
+
+import { Input, Button, message } from "antd";
+import { UserOutlined, LockOutlined } from "@ant-design/icons";
+import { useEffect, useState } from "react";
+import { Base64 } from "js-base64";
+import encodeStr from "@/utils/pass";
+import { userLoginAPI } from "@/store/action/login";
+import { setTokenInfo } from "@/utils/storage";
+import history from "@/utils/history";
+import { useDispatch } from "react-redux";
+
+export default function Login() {
+  const dispatch = useDispatch();
+
+  // 进登录页面把权限的信息初始化,防止登录成功之后进到首页,数据渲染问题
+  useEffect(() => {
+    dispatch({ type: "login/setAuthPageArr", payload: [] });
+  }, [dispatch]);
+
+  // 账号密码
+  const [userName, setUserName] = useState("");
+  const [passWord, setPassWord] = useState("");
+
+  // 键盘按下回车事件
+  const keyUpEntFu = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    if (e.key === "Enter") loginClickFu();
+  };
+  // 点击登录
+  const loginClickFu = async () => {
+    // 非空判断
+    if (userName === "") return message.warning("请输入用户名!");
+    else if (passWord === "") return message.warning("请输入密码!");
+    const obj = {
+      userName,
+      passWord: encodeStr(Base64.encode(passWord)),
+    };
+    const res: any = await userLoginAPI(obj);
+    if (res.code === 0) {
+      message.success("登录成功");
+      // 用户信息存到本地
+      setTokenInfo(res.data);
+      history.push("/");
+    }
+  };
+
+  return (
+    <div className={styles.Login}>
+      <div className="loginTop">
+        <h3>中医药文化宣传教育基地<br/>线上展馆管理后台</h3>
+      </div>
+      <div className="main">
+        <div className="mainLeft">
+          <div className="mainLeftTitle">江苏省中医药管理局</div>
+        </div>
+        <div className="mainRight">
+          <h1>欢迎登录</h1>
+          {/* 账号密码输入框 */}
+          <div className="inputBox">
+            <div className="inputBoxRow">
+              <Input
+                onKeyUp={(e) => keyUpEntFu(e)}
+                value={userName}
+                onChange={(e) => setUserName(e.target.value.trim())}
+                prefix={<UserOutlined />}
+                placeholder="请输入用户名"
+                maxLength={15}
+              />
+            </div>
+            <div className="inputBoxRow">
+              <Input.Password
+                onKeyUp={(e) => keyUpEntFu(e)}
+                value={passWord}
+                onChange={(e) => setPassWord(e.target.value.trim())}
+                prefix={<LockOutlined />}
+                placeholder="请输入用户密码"
+                maxLength={15}
+              />
+            </div>
+          </div>
+
+          {/* 登录按钮 */}
+          <div className="loginBtn">
+            <Button type="primary" size="large" onClick={loginClickFu}>
+              登 录
+            </Button>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 49 - 0
src/pages/Role/RoleAdd/index.css

@@ -0,0 +1,49 @@
+.roleAdd .ant-modal-close {
+  display: none;
+}
+.roleAdd .roleAddMain {
+  border-top: 1px solid #999999;
+  padding-top: 15px;
+  width: 100%;
+}
+.roleAdd .roleAddMain .row {
+  margin-bottom: 20px;
+  position: relative;
+  display: flex;
+  padding: 0 24px 0 0px;
+  text-align: right;
+}
+.roleAdd .roleAddMain .row .rowSpan {
+  display: inline-block;
+  width: 80px;
+  line-height: 32px;
+}
+.roleAdd .roleAddMain .row .bs::before {
+  content: '*';
+  position: absolute;
+  top: 2px;
+  left: 2px;
+  z-index: 10;
+  color: #ff4d4f;
+}
+.roleAdd .roleAddMain .row .inputBox {
+  width: calc(100% - 90px);
+}
+.roleAdd .roleAddMain .row .inputBoxCheck {
+  width: 155px;
+}
+.roleAdd .roleAddMain .row .inputBoxCheck .rowCheck {
+  display: block;
+  display: flex;
+  margin-left: 4px;
+  margin-top: 5px;
+}
+.roleAdd .roleAddMain .rowThree {
+  margin-top: -26px;
+}
+.roleAdd .roleAddMain .roleAddButton {
+  text-align: center;
+}
+.roleAdd .lookRole .row {
+  pointer-events: none;
+}

+ 66 - 0
src/pages/Role/RoleAdd/index.less

@@ -0,0 +1,66 @@
+.roleAdd {
+  .ant-modal-close {
+    display: none;
+  }
+
+  .roleAddMain {
+    border-top: 1px solid #999999;
+    padding-top: 15px;
+    width: 100%;
+
+    .row {
+      margin-bottom: 20px;
+      position: relative;
+      display: flex;
+      padding: 0 24px 0 0px;
+      text-align: right;
+
+      .rowSpan {
+        display: inline-block;
+        width: 80px;
+        line-height: 32px;
+      }
+
+      .bs {
+        &::before {
+          content: '*';
+          position: absolute;
+          top: 2px;
+          left: 2px;
+          z-index: 10;
+          color: #ff4d4f;
+        }
+      }
+
+      .inputBox {
+        width: calc(100% - 90px);
+
+      }
+
+      .inputBoxCheck {
+        width: 155px;
+
+        .rowCheck {
+          display: block;
+          display: flex;
+          margin-left: 4px;
+          margin-top: 5px;
+        }
+      }
+    }
+
+    .rowThree {
+      margin-top: -26px;
+    }
+
+    .roleAddButton {
+      text-align: center;
+    }
+  }
+
+  .lookRole {
+    .row {
+      pointer-events: none;
+    }
+  }
+}

+ 170 - 0
src/pages/Role/RoleAdd/index.tsx

@@ -0,0 +1,170 @@
+import { getRoleInfoByIdAPI, roleSaveAPI } from "@/store/action/role";
+
+import { Button, Checkbox, Input, message, Modal, Popconfirm } from "antd";
+import React, { useCallback, useEffect, useState } from "react";
+import classNames from "classnames";
+import "./index.css";
+import { AddRoleType, PermissionsAPIType, RoleTableType } from "@/types";
+const { TextArea } = Input;
+
+type Props = {
+  id: any;
+  closePage: () => void;
+  upTableList: () => void;
+  addTableList: () => void;
+};
+
+function RoleAdd({ id, closePage, upTableList, addTableList }: Props) {
+  // 角色名称
+  const [roleName, setRoleName] = useState("");
+
+  // 角色描述
+  const [roleDesc, setRoleDesc] = useState("");
+
+  // 页面权限的选择
+  const [list, setList] = useState<PermissionsAPIType[]>([
+    { id: 100, name: "热度统计", authority: true },
+    { id: 200, name: "万物墙管理", authority: true },
+    { id: 300, name: "馆藏管理", authority: true },
+  ]);
+
+  const checkChangeFu = useCallback(
+    (checked: boolean, id: number) => {
+      const newList = list.map((v) => {
+        return {
+          ...v,
+          authority: v.id === id ? checked : v.authority,
+        };
+      });
+      setList(newList);
+    },
+    [list]
+  );
+
+  const getRoleInfoByIdFu = useCallback(async (id: number) => {
+    const res = await getRoleInfoByIdAPI(id);
+    const info: RoleTableType = res.data.role;
+    const authPageArr: PermissionsAPIType[] = res.data.permission;
+    setRoleName(info.roleName);
+    setRoleDesc(info.roleDesc);
+    setList(authPageArr);
+  }, []);
+  // 如果是编辑
+  useEffect(() => {
+    if (id) getRoleInfoByIdFu(id);
+  }, [getRoleInfoByIdFu, id]);
+
+  // 点击提交
+  const btnOkFu = useCallback(async () => {
+    if (roleName === "") return message.warning("请输入角色名称!");
+    const obj: AddRoleType = {
+      id: id ? id : null,
+      resources: list.filter((v) => v.authority).map((v) => v.id),
+      roleDesc: roleDesc,
+      roleName: roleName,
+    };
+    const res: any = await roleSaveAPI(obj);
+
+    if (res.code === 0) {
+      message.success(id ? "编辑成功!" : "新增成功!");
+      closePage();
+      if (id) addTableList();
+      else upTableList();
+    }
+  }, [addTableList, closePage, id, list, roleDesc, roleName, upTableList]);
+
+  return (
+    <Modal
+      wrapClassName="roleAdd"
+      destroyOnClose
+      open={true}
+      title={id ? (id === 1 ? "查看角色" : "编辑角色") : "新增角色"}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className={classNames("roleAddMain", id === 1 ? "lookRole" : "")}>
+        <div className="row">
+          <span className="bs rowSpan">角色名称:</span>
+          <div className="inputBox">
+            <Input
+              maxLength={8}
+              value={roleName}
+              onChange={(e) => setRoleName(e.target.value.replace(/\s+/g, ""))}
+              showCount
+              placeholder="请输入内容"
+            />
+          </div>
+        </div>
+
+        <div className="row">
+          <span className="rowSpan">角色描述:</span>
+          <div className="inputBox">
+            <TextArea
+              rows={4}
+              placeholder="请输入内容"
+              maxLength={100}
+              showCount
+              value={roleDesc}
+              onChange={(e) => setRoleDesc(e.target.value.replace(/\s+/g, ""))}
+            />
+          </div>
+        </div>
+
+        <div className="row rowThree">
+          <span className="rowSpan">权限设置:</span>
+          <div className="inputBox inputBoxCheck">
+            {list.map((v) => (
+              <Checkbox
+                className="rowCheck"
+                key={v.id}
+                checked={v.authority}
+                onChange={(e) => checkChangeFu(e.target.checked, v.id)}
+              >
+                {v.name}
+              </Checkbox>
+            ))}
+            {id === 1 ? (
+              <>
+                <Checkbox className="rowCheck" defaultChecked={true}>
+                  用户管理
+                </Checkbox>
+                <Checkbox className="rowCheck" defaultChecked={true}>
+                  角色管理
+                </Checkbox>
+                <Checkbox className="rowCheck" defaultChecked={true}>
+                  操作日志
+                </Checkbox>
+              </>
+            ) : null}
+          </div>
+        </div>
+
+        <div className="roleAddButton">
+          {id !== 1 ? (
+            <>
+              <Button type="primary" onClick={btnOkFu}>
+                提交
+              </Button>
+              &emsp;
+              <Popconfirm
+                title="放弃编辑后,信息将不会保存!"
+                okText="放弃"
+                cancelText="取消"
+                onConfirm={closePage}
+              >
+                <Button>取消</Button>
+              </Popconfirm>
+            </>
+          ) : (
+            <Button onClick={closePage}>关闭</Button>
+          )}
+        </div>
+      </div>
+    </Modal>
+  );
+}
+
+const MemoRoleAdd = React.memo(RoleAdd);
+
+export default MemoRoleAdd;

+ 21 - 0
src/pages/Role/index.module.scss

@@ -0,0 +1,21 @@
+.Role{
+  :global{
+    .roleTop{
+      height: 95px;
+      border-radius: 10px;
+      background-color: #fff;
+      .searchTop{
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 5px 40px 5px 24px;
+      }
+    }
+    .roleMain{
+      margin-top: 15px;
+      height: calc(100% - 105px);
+      border-radius: 10px;
+      background-color: #fff;
+    }
+  }
+}

+ 237 - 0
src/pages/Role/index.tsx

@@ -0,0 +1,237 @@
+import { RootState } from "@/store";
+import {
+  getRoleListAPI,
+  roleDisplayAPI,
+  roleRemoveAPI,
+} from "@/store/action/role";
+import { RoleTableType } from "@/types";
+import { Button, Input, message, Popconfirm, Switch, Table } from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useDispatch, useSelector } from "react-redux";
+import styles from "./index.module.scss";
+import RoleAdd from "./RoleAdd";
+function Role() {
+  const dispatch = useDispatch();
+
+  // 顶部筛选
+
+  type TableListType = {
+    pageNum: number;
+    pageSize: number;
+    searchKey: string;
+  };
+
+  const [tableSelect, setTableSelect] = useState<TableListType>({
+    pageNum: 1,
+    pageSize: 10,
+    searchKey: "",
+  });
+
+  useEffect(() => {
+    dispatch(getRoleListAPI(tableSelect));
+  }, [dispatch, tableSelect]);
+
+  // 角色名的输入
+  const nameTime = useRef(-1);
+  const nameChange = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      clearTimeout(nameTime.current);
+      nameTime.current = window.setTimeout(() => {
+        setTableSelect({
+          ...tableSelect,
+          searchKey: e.target.value,
+          pageNum: 1,
+        });
+      }, 500);
+    },
+    [tableSelect]
+  );
+
+  // 角色名的重置
+  const [inputKey, setInputKey] = useState(1);
+  const resetSelectFu = useCallback(() => {
+    // 把2个输入框和时间选择器清空
+    setInputKey(Date.now());
+    setTableSelect({
+      pageNum: 1,
+      pageSize: 10,
+      searchKey: "",
+    });
+  }, []);
+
+  // 切换表格中的启用停用状态
+  const isEnabledClickFu = useCallback(
+    async (val: boolean, id: number) => {
+      const isDisable = val ? 1 : 0;
+      const res: any = await roleDisplayAPI(id, isDisable);
+      if (res.code === 0) dispatch(getRoleListAPI(tableSelect));
+    },
+    [dispatch, tableSelect]
+  );
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      const res: any = await roleRemoveAPI(id);
+      if (res.code === 0) {
+        message.success("删除成功!");
+        dispatch(getRoleListAPI(tableSelect));
+      }
+    },
+    [dispatch, tableSelect]
+  );
+
+  // 点击新增和编辑
+  const [editPageShow, setEditPageShow] = useState(false);
+  const editId = useRef(0);
+  const openEditPageFu = useCallback((id: number) => {
+    editId.current = id;
+    setEditPageShow(true);
+  }, []);
+
+  // 从仓库中获取表格数据
+  const tableInfo = useSelector(
+    (state: RootState) => state.RoleReducer.tableInfo
+  );
+
+  // 页码变化
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setTableSelect({ ...tableSelect, pageNum, pageSize });
+    },
+    [tableSelect]
+  );
+
+  const columns = useMemo(() => {
+    return [
+      // {
+      //   width: 80,
+      //   title: "序号",
+      //   render: (text: any, record: any, index: any) =>
+      //     index + 1 + (pageNumRef.current - 1) * pagePageRef.current,
+      // },
+      {
+        title: "角色名称",
+        dataIndex: "roleName",
+      },
+      {
+        title: "角色描述",
+        render: (item: RoleTableType) =>
+          item.roleDesc ? item.roleDesc : "(空)",
+      },
+      {
+        title: "成员数量",
+        dataIndex: "count",
+      },
+      {
+        title: "最后编辑时间",
+        dataIndex: "updateTime",
+      },
+
+      {
+        title: "启用状态",
+        render: (item: RoleTableType) => (
+          <Switch
+            disabled={item.roleKey === "sys_admin"}
+            checkedChildren="启用"
+            unCheckedChildren="停用"
+            checked={item.isEnabled === 1}
+            onChange={(val) => isEnabledClickFu(val, item.id!)}
+          />
+        ),
+      },
+
+      {
+        title: "操作",
+        render: (item: RoleTableType) => {
+          return (
+            <>
+              <Button
+                size="small"
+                type="text"
+                onClick={() => openEditPageFu(item.id!)}
+              >
+                {item.roleKey === "sys_admin" ? "查看" : "编辑"}
+              </Button>
+              {item.roleKey !== "sys_admin" ? (
+                <Popconfirm
+                  title="删除后无法恢复,是否删除?"
+                  okText="删除"
+                  cancelText="取消"
+                  onConfirm={() => delTableFu(item.id!)}
+                >
+                  <Button size="small" type="text" danger>
+                    删除
+                  </Button>
+                </Popconfirm>
+              ) : null}
+            </>
+          );
+        },
+      },
+    ];
+  }, [delTableFu, isEnabledClickFu, openEditPageFu]);
+
+  return (
+    <div className={styles.Role}>
+      <div className="roleTop">
+        <div className="pageTitlt">角色管理</div>
+        <div className="searchTop">
+          <div className="row">
+            <span>角色名:</span>
+            <Input
+              key={inputKey}
+              maxLength={8}
+              style={{ width: 200 }}
+              placeholder="请输入"
+              allowClear
+              onChange={(e) => nameChange(e)}
+            />
+          </div>
+          <div className="row">
+            <Button type="primary" onClick={() => openEditPageFu(0)}>
+              新增
+            </Button>
+          </div>
+        </div>
+      </div>
+
+      <div className="roleMain">
+        <Table
+          scroll={{ y: 575 }}
+          dataSource={tableInfo.list}
+          columns={columns}
+          rowKey="id"
+          pagination={{
+            showQuickJumper: true,
+            position: ["bottomCenter"],
+            showSizeChanger: true,
+            current: tableSelect.pageNum,
+            pageSize: tableSelect.pageSize,
+            total: tableInfo.total,
+            onChange: paginationChange(),
+          }}
+        />
+      </div>
+      {/* 点击新增或者编辑 */}
+      {editPageShow ? (
+        <RoleAdd
+          id={editId.current}
+          closePage={() => setEditPageShow(false)}
+          upTableList={() => dispatch(getRoleListAPI(tableSelect))}
+          addTableList={resetSelectFu}
+        />
+      ) : null}
+    </div>
+  );
+}
+
+const MemoRole = React.memo(Role);
+
+export default MemoRole;

+ 13 - 0
src/pages/User/UserAdd/index.css

@@ -0,0 +1,13 @@
+.userAdd .ant-modal-close {
+  display: none;
+}
+.userAdd .userAddMain {
+  border-top: 1px solid #999999;
+  padding-top: 15px;
+  width: 100%;
+}
+.userAdd .userAddMain .passTit {
+  color: #ff4d4f;
+  font-size: 14px;
+  padding-left: 98px;
+}

+ 17 - 0
src/pages/User/UserAdd/index.less

@@ -0,0 +1,17 @@
+.userAdd {
+  .ant-modal-close {
+    display: none;
+  }
+
+  .userAddMain {
+    border-top: 1px solid #999999;
+    padding-top: 15px;
+    width: 100%;
+
+    .passTit {
+      color: #ff4d4f;
+      font-size: 14px;
+      padding-left: 98px;
+    }
+  }
+}

+ 145 - 0
src/pages/User/UserAdd/index.tsx

@@ -0,0 +1,145 @@
+import { RootState } from "@/store";
+import { getUserInfoByIdAPI, userSaveAPI } from "@/store/action/user";
+import { SaveUserType } from "@/types";
+import { Button, Form, Input, message, Modal, Popconfirm, Select } from "antd";
+import React, { useCallback, useEffect, useRef } from "react";
+import { useSelector } from "react-redux";
+import "./index.css";
+
+type Props = {
+  id: any;
+  closePage: () => void;
+  upTableList: () => void;
+  addTableList: () => void;
+};
+
+function UserAdd({ id, closePage, upTableList, addTableList }: Props) {
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<any>({});
+
+  const getInfoInAPIFu = useCallback(async (id: number) => {
+    const res =await getUserInfoByIdAPI(id)
+    FormBoxRef.current.setFieldsValue(res.data)
+    console.log("是编辑,在这里发请求拿数据",res);
+  }, []);
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    // return message.warning("有表单不符号规则!");
+  }, []);
+
+  useEffect(() => {
+    if (id) getInfoInAPIFu(id);
+    else {
+      FormBoxRef.current.setFieldsValue({});
+    }
+  }, [getInfoInAPIFu, id]);
+
+  // 从仓库获取角色下拉列表信息
+  const roleList = useSelector(
+    (state: RootState) => state.userReducer.roleList
+  );
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (values: any) => {
+      const obj: SaveUserType = {
+        ...values,
+        id: id ? id : null,
+      };
+
+      const res: any = await userSaveAPI(obj);
+
+      if (res.code === 0) {
+        message.success(id ? "编辑成功!" : "新增成功!");
+        if (id) upTableList();
+        else addTableList();
+
+        closePage();
+      }
+      console.log("通过校验,点击确定");
+    },
+    [addTableList, closePage, id, upTableList]
+  );
+
+  return (
+    <Modal
+      wrapClassName="userAdd"
+      destroyOnClose
+      open={true}
+      title={id ? "编辑用户" : "新增用户"}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className="userAddMain">
+        <Form
+          ref={FormBoxRef}
+          name="basic"
+          labelCol={{ span: 5 }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete="off"
+        >
+          <Form.Item
+            label="账号名"
+            name="userName"
+            rules={[{ required: true, message: "请输入账号名!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input disabled={id} maxLength={15} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="用户昵称"
+            name="nickName"
+            rules={[{ required: true, message: "请输入用户昵称!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={8} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="用户角色"
+            name="roleId"
+            rules={[{ required: true, message: "请选择角色!" }]}
+          >
+            <Select placeholder="请选择" options={roleList} />
+          </Form.Item>
+
+          <Form.Item
+            label="真实姓名"
+            name="realName"
+            rules={[{ required: true, message: "请输入真实姓名!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={8} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          {id ? null : <div className="passTit">* 默认密码 123456</div>}
+
+          {/* 确定和取消按钮 */}
+          <br />
+          <Form.Item wrapperCol={{ offset: 9, span: 16 }}>
+            <Button type="primary" htmlType="submit">
+              提交
+            </Button>
+            &emsp;
+            <Popconfirm
+              title="放弃编辑后,信息将不会保存!"
+              okText="放弃"
+              cancelText="取消"
+              onConfirm={closePage}
+            >
+              <Button>取消</Button>
+            </Popconfirm>
+          </Form.Item>
+        </Form>
+      </div>
+    </Modal>
+  );
+}
+
+const MemoUserAdd = React.memo(UserAdd);
+
+export default MemoUserAdd;

+ 31 - 0
src/pages/User/index.module.scss

@@ -0,0 +1,31 @@
+.User {
+  :global {
+    .userTop {
+      height: 100px;
+      border-radius: 10px;
+      background-color: #fff;
+
+      .selectBox {
+        padding: 4px 24px 5px;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+
+        .row {
+          margin-right: 20px;
+        }
+
+        .selectBoxL {
+          display: flex;
+          align-items: center;
+        }
+      }
+    }
+    .tableBox{
+      margin-top: 15px;
+      height: calc(100% - 110px);
+      background-color: #fff;
+      border-radius: 10px;
+    }
+  }
+}

+ 358 - 0
src/pages/User/index.tsx

@@ -0,0 +1,358 @@
+import { RootState } from "@/store";
+import {
+  getUserListAPI,
+  getUserRoleAPI,
+  userDisplayAPI,
+  userPassResetAPI,
+  userRemoveAPI,
+} from "@/store/action/user";
+import { UserTableAPIType, UserTableListType } from "@/types";
+import {
+  Input,
+  DatePicker,
+  Button,
+  Table,
+  Switch,
+  Popconfirm,
+  message,
+} from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useDispatch, useSelector } from "react-redux";
+import styles from "./index.module.scss";
+import UserAdd from "./UserAdd";
+const { RangePicker } = DatePicker;
+
+function User() {
+  const dispatch = useDispatch();
+
+  const pageNumRef = useRef(1);
+  const pagePageRef = useRef(10);
+
+  // 顶部筛选
+  const [tableSelect, setTableSelect] = useState<UserTableAPIType>({
+    startTime: "",
+    endTime: "",
+    nickName: "",
+    pageNum: 1,
+    pageSize: 10,
+    realName: "",
+    searchKey: "",
+  });
+
+  // 进来用户管理页面获取角色的下拉列表
+  useEffect(() => {
+    dispatch(getUserRoleAPI());
+  }, [dispatch]);
+
+  // 封装发送请求的函数
+
+  const getList = useCallback(async () => {
+    const data = {
+      ...tableSelect,
+      pageNum: pageNumRef.current,
+    };
+    dispatch(getUserListAPI(data));
+  }, [dispatch, tableSelect]);
+
+  // 当前页码统一
+  useEffect(() => {
+    pageNumRef.current = tableSelect.pageNum;
+    pagePageRef.current = tableSelect.pageSize;
+  }, [tableSelect.pageNum, tableSelect.pageSize]);
+
+  // 防止发送了2次请求来对应页码
+
+  const getListRef = useRef(-1);
+
+  useEffect(() => {
+    clearTimeout(getListRef.current);
+    getListRef.current = window.setTimeout(() => {
+      getList();
+    }, 100);
+  }, [getList, tableSelect]);
+
+  // 用户昵称的输入
+  const nameTime = useRef(-1);
+  const nameChange = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      clearTimeout(nameTime.current);
+      nameTime.current = window.setTimeout(() => {
+        setTableSelect({
+          ...tableSelect,
+          nickName: e.target.value,
+          pageNum: 1,
+        });
+      }, 500);
+    },
+    [tableSelect]
+  );
+
+  // 真实姓名的输入
+  const realNameTime = useRef(-1);
+  const realNameChange = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      clearTimeout(realNameTime.current);
+      realNameTime.current = window.setTimeout(() => {
+        setTableSelect({
+          ...tableSelect,
+          realName: e.target.value,
+          pageNum: 1,
+        });
+      }, 500);
+    },
+    [tableSelect]
+  );
+
+  // 时间选择器改变
+  const timeChange = (date: any, dateString: any) => {
+    let startTime = "";
+    let endTime = "";
+    if (dateString[0] && dateString[1]) {
+      startTime = dateString[0] + " 00:00:00";
+      endTime = dateString[1] + " 23:59:59";
+    }
+    setTableSelect({ ...tableSelect, startTime, endTime, pageNum: 1 });
+  };
+
+  // 点击重置
+  const [inputKey, setInputKey] = useState(1);
+  const resetSelectFu = useCallback(() => {
+    // 把2个输入框和时间选择器清空
+    setInputKey(Date.now());
+    setTableSelect({
+      startTime: "",
+      endTime: "",
+      nickName: "",
+      pageNum: 1,
+      pageSize: 10,
+      realName: "",
+      searchKey: "",
+    });
+  }, []);
+
+  // 从仓库中获取表格数据
+  const tableInfo = useSelector(
+    (state: RootState) => state.userReducer.tableInfo
+  );
+
+  // 页码变化
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      pageNumRef.current = pageNum;
+      pagePageRef.current = pageSize;
+      setTableSelect({ ...tableSelect, pageNum, pageSize });
+    },
+    [tableSelect]
+  );
+
+  // 切换表格中的启用停用状态
+  const isEnabledClickFu = useCallback(
+    async (val: boolean, id: number) => {
+      const isDisable = val ? 1 : 0;
+      const res: any = await userDisplayAPI(id, isDisable);
+      if (res.code === 0) getList();
+    },
+    [getList]
+  );
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      const res: any = await userRemoveAPI(id);
+      if (res.code === 0) {
+        message.success("删除成功!");
+        getList();
+      }
+    },
+    [getList]
+  );
+
+  // 点击重置密码
+  const resetPassFu = useCallback(async (id: number) => {
+    const res: any = await userPassResetAPI(id);
+    if (res.code === 0) message.success("重置成功!");
+  }, []);
+
+  // 0------------点击新增或者编辑出来的页面
+  const [editPageShow, setEditPageShow] = useState(false);
+  const editId = useRef(0);
+
+  const openEditPageFu = useCallback(
+    (id: number) => {
+      if (id === 0 && tableInfo.list.length >= 30)
+        return message.warning("最多支持30个用户!");
+
+      editId.current = id;
+      setEditPageShow(true);
+    },
+    [tableInfo.list.length]
+  );
+
+  const columns = useMemo(() => {
+    return [
+      // {
+      //   width: 80,
+      //   title: "序号",
+      //   render: (text: any, record: any, index: any) =>
+      //     index + 1 + (pageNumRef.current - 1) * pagePageRef.current,
+      // },
+      {
+        title: "账号名",
+        dataIndex: "userName",
+      },
+      {
+        title: "用户昵称",
+        dataIndex: "nickName",
+      },
+      {
+        title: "用户角色",
+        dataIndex: "roleName",
+      },
+      {
+        title: "真实姓名",
+        dataIndex: "realName",
+      },
+      {
+        title: "注册时间",
+        dataIndex: "createTime",
+      },
+
+      {
+        title: "启用状态",
+        render: (item: UserTableListType) => (
+          <Switch
+            disabled={item.isAdmin === 1}
+            checkedChildren="启用"
+            unCheckedChildren="停用"
+            checked={item.isEnabled === 1}
+            onChange={(val) => isEnabledClickFu(val, item.id!)}
+          />
+        ),
+      },
+
+      {
+        title: "操作",
+        render: (item: UserTableListType) => {
+          return item.isAdmin === 1 ? (
+            "-"
+          ) : (
+            <>
+              <Popconfirm
+                title="密码重制后为123456,是否重置?"
+                okText="重置"
+                cancelText="取消"
+                onConfirm={() => resetPassFu(item.id!)}
+              >
+                <Button size="small" type="text">
+                  重置密码
+                </Button>
+              </Popconfirm>
+
+              <Button
+                size="small"
+                type="text"
+                onClick={() => openEditPageFu(item.id!)}
+              >
+                编辑
+              </Button>
+              <Popconfirm
+                title="删除后无法恢复,是否删除?"
+                okText="删除"
+                cancelText="取消"
+                onConfirm={() => delTableFu(item.id!)}
+              >
+                <Button size="small" type="text" danger>
+                  删除
+                </Button>
+              </Popconfirm>
+            </>
+          );
+        },
+      },
+    ];
+  }, [delTableFu, isEnabledClickFu, openEditPageFu, resetPassFu]);
+
+  return (
+    <div className={styles.User}>
+      <div className="userTop">
+        <div className="pageTitlt">用户管理</div>
+        <div className="selectBox">
+          <div className="selectBoxL">
+            <div className="row">
+              <span>用户昵称:</span>
+              <Input
+                key={inputKey}
+                maxLength={8}
+                style={{ width: 150 }}
+                placeholder="请输入"
+                allowClear
+                onChange={(e) => nameChange(e)}
+              />
+            </div>
+
+            <div className="row">
+              <span>真实姓名:</span>
+              <Input
+                key={inputKey}
+                maxLength={8}
+                style={{ width: 150 }}
+                placeholder="请输入"
+                allowClear
+                onChange={(e) => realNameChange(e)}
+              />
+            </div>
+
+            <div className="row">
+              <span>注册日期:</span>
+              <RangePicker key={inputKey} onChange={timeChange} />
+            </div>
+          </div>
+          <div className="selectBoxR">
+            <Button onClick={resetSelectFu}>重置</Button>&emsp;&emsp;
+            <Button type="primary" onClick={() => openEditPageFu(0)}>
+              新增
+            </Button>
+          </div>
+        </div>
+      </div>
+      {/* 表格主体 */}
+      <div className="tableBox">
+        <Table
+          scroll={{ y: 575 }}
+          dataSource={tableInfo.list}
+          columns={columns}
+          rowKey="id"
+          pagination={{
+            showQuickJumper: true,
+            position: ["bottomCenter"],
+            // showSizeChanger: true,
+            current: tableSelect.pageNum,
+            pageSize: tableSelect.pageSize,
+            total: tableInfo.total,
+            onChange: paginationChange(),
+          }}
+        />
+      </div>
+
+      {/* 点击新增或者编辑 */}
+      {editPageShow ? (
+        <UserAdd
+          id={editId.current}
+          closePage={() => setEditPageShow(false)}
+          upTableList={getList}
+          addTableList={resetSelectFu}
+        />
+      ) : null}
+    </div>
+  );
+}
+
+const MemoUser = React.memo(User);
+
+export default MemoUser;

+ 87 - 0
src/pages/Wall/WallAdd/index.css

@@ -0,0 +1,87 @@
+.wallAdd .ant-modal-close {
+  display: none;
+}
+.wallAdd .wallAddMain {
+  border-top: 1px solid #999999;
+  padding-top: 15px;
+  width: 100%;
+}
+.wallAdd .wallAddMain .fileBoxRow_up {
+  cursor: pointer;
+  font-size: 30px;
+  width: 100px;
+  height: 100px;
+  background-color: #fafafa;
+  border: 1px dashed #ccc;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.wallAdd .wallAddMain .fileBoxRow_r_img {
+  width: 120px;
+  height: 120px;
+  position: relative;
+}
+.wallAdd .wallAddMain .fileBoxRow_r_img .clearCover {
+  cursor: pointer;
+  z-index: 10;
+  position: absolute;
+  width: 50px;
+  height: 50px;
+  top: 50%;
+  transform: translateY(-50%);
+  right: -50px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 24px;
+}
+.wallAdd .wallAddMain .fileBoxRow_r_tit {
+  margin-top: 5px;
+  font-size: 14px;
+  color: #7e7c7c;
+}
+.wallAdd .wallAddMain .row {
+  position: relative;
+  margin-bottom: 15px;
+  display: flex;
+}
+.wallAdd .wallAddMain .row .lable {
+  line-height: 30px;
+  width: 54px;
+  text-align: right;
+  font-weight: 700;
+}
+.wallAdd .wallAddMain .row .bs {
+  position: absolute;
+  top: 7px;
+  left: 0px;
+  z-index: 10;
+  color: #ff4d4f;
+}
+.wallAdd .wallAddMain .upBox .upVideoSucc {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  margin: 10px 30px 10px 0;
+}
+.wallAdd .wallAddMain .upBox .upVideoSucc .upVideoSuccTxt {
+  max-width: 280px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+.wallAdd .wallAddMain .upBox .upVideoSucc > div {
+  margin-right: 20px;
+}
+.wallAdd .wallAddMain .upBox .upVideoSucc .lookVideoIncoBox {
+  cursor: pointer;
+  font-size: 20px;
+}
+.wallAdd .wallAddMain .upBox .upVideoSucc .clearCover {
+  cursor: pointer;
+  font-size: 20px;
+}
+.wallAdd .wallAddBtn {
+  text-align: center;
+}

+ 109 - 0
src/pages/Wall/WallAdd/index.less

@@ -0,0 +1,109 @@
+.wallAdd {
+  .ant-modal-close {
+    display: none;
+  }
+
+  .wallAddMain {
+    border-top: 1px solid #999999;
+    padding-top: 15px;
+    width: 100%;
+
+
+
+    .fileBoxRow_up {
+      cursor: pointer;
+      font-size: 30px;
+      width: 100px;
+      height: 100px;
+      background-color: #fafafa;
+      border: 1px dashed #ccc;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+
+    }
+
+    .fileBoxRow_r_img {
+      width: 120px;
+      height: 120px;
+      position: relative;
+
+      .clearCover {
+        cursor: pointer;
+        z-index: 10;
+        position: absolute;
+        width: 50px;
+        height: 50px;
+        top: 50%;
+        transform: translateY(-50%);
+        right: -50px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        font-size: 24px;
+      }
+    }
+
+    .fileBoxRow_r_tit {
+      margin-top: 5px;
+      font-size: 14px;
+      color: rgb(126, 124, 124);
+    }
+
+    .row {
+      position: relative;
+      margin-bottom: 15px;
+      display: flex;
+
+      .lable {
+        line-height: 30px;
+        width: 54px;
+        text-align: right;
+        font-weight: 700;
+      }
+
+      .bs {
+        position: absolute;
+        top: 7px;
+        left: 0px;
+        z-index: 10;
+        color: #ff4d4f;
+      }
+    }
+
+    .upBox {
+      .upVideoSucc {
+        display: flex;
+        flex-wrap: wrap;
+        align-items: center;
+        margin: 10px 30px 10px 0;
+
+        .upVideoSuccTxt {
+          max-width: 280px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        &>div {
+          margin-right: 20px;
+        }
+
+        .lookVideoIncoBox {
+          cursor: pointer;
+          font-size: 20px;
+        }
+
+        .clearCover {
+          cursor: pointer;
+          font-size: 20px;
+        }
+      }
+    }
+  }
+
+  .wallAddBtn {
+    text-align: center;
+  }
+}

+ 313 - 0
src/pages/Wall/WallAdd/index.tsx

@@ -0,0 +1,313 @@
+import { Button, Input, message, Modal, Popconfirm, Select } from "antd";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import {
+  PlusOutlined,
+  CloseCircleOutlined,
+  UploadOutlined,
+  PlayCircleOutlined,
+} from "@ant-design/icons";
+import "./index.css";
+import ImageLazy from "@/components/ImageLazy";
+import {
+  getWallDetailByIdAPI,
+  wallUploadAPI,
+  wallUpSaveAPI,
+} from "@/store/action/wall";
+import { WallUpSaveAPI } from "@/types";
+import { useDispatch } from "react-redux";
+
+type Props = {
+  id: number;
+  closeFu: () => void;
+  upList: () => void;
+};
+
+// 上传附件的进度条
+const UpAsyncLodingDom: any = document.querySelector("#UpAsyncLoding");
+const progressDom: any = document.querySelector("#progress");
+
+function WallAdd({ id, closeFu, upList }: Props) {
+  const dispatch = useDispatch();
+
+  // 上传的inputRef
+  const myInput = useRef<HTMLInputElement>(null);
+
+  // 类型
+  const [type, setType] = useState<"img" | "video">("img");
+
+  // 名称
+  const [name, setName] = useState("");
+
+  // 图片
+  const [cover, setCover] = useState({ fileName: "", filePath: "" });
+
+  // 视频
+  const [video, setVideo] = useState({ fileName: "", filePath: "" });
+
+  // 通过id获取详情
+  const getInfoById = useCallback(async () => {
+    const res = await getWallDetailByIdAPI(id);
+
+    setType(res.data.type);
+
+    setName(res.data.name);
+
+    if (res.data.type === "img")
+      setCover({ fileName: res.data.fileName, filePath: res.data.filePath });
+    else setVideo({ fileName: res.data.fileName, filePath: res.data.filePath });
+  }, [id]);
+  // 进来看看是不是编辑
+  useEffect(() => {
+    if (id) getInfoById();
+  }, [getInfoById, id]);
+
+  // 上传图片或者视频
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0];
+
+        let typeArr = [] as string[];
+        let tit1 = "";
+        let fileSize = 20 * 1024 * 1024;
+        let tit2 = "最大支持20M!";
+
+        if (type === "img") {
+          typeArr = ["image/jpeg", "image/png", "image/gif"];
+          tit1 = "只支持jpg、png、gif格式!";
+        } else {
+          tit2 = "最大支持500M!";
+          fileSize = 500 * 1024 * 1024;
+          typeArr = ["video/mp4"];
+          tit1 = "只支持mp4格式!";
+        }
+
+        // 校验格式
+        if (!typeArr.includes(filesInfo.type)) {
+          e.target.value = "";
+          return message.warning(tit1);
+        }
+
+        // 校验大小
+        if (filesInfo.size > fileSize) {
+          e.target.value = "";
+          return message.warning(tit2);
+        }
+
+        // 创建FormData对象
+        const fd = new FormData();
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append("type", type);
+        fd.append("file", filesInfo);
+        e.target.value = "";
+
+        const res: any = await wallUploadAPI(fd);
+        if (res.code === 0) {
+          message.success("上传成功!");
+
+          // 上传的是图片
+          if (type === "img") setCover(res.data);
+          else setVideo(res.data);
+        }
+        UpAsyncLodingDom.style.opacity = 0;
+        progressDom.style.width = "0%";
+      }
+    },
+    [type]
+  );
+
+  // 点击确定
+  const btnOkFu = useCallback(async () => {
+    if (name === "") return message.warning("请输入名称!");
+
+    if (
+      (type === "img" && cover.fileName === "") ||
+      (type === "video" && video.fileName === "")
+    )
+      return message.warning("请上传附件!");
+
+    const obj: WallUpSaveAPI = {
+      fileName: type === "img" ? cover.fileName : video.fileName,
+      filePath: type === "img" ? cover.filePath : video.filePath,
+      id: id ? id : null,
+      name: name,
+      type: type,
+    };
+
+    const res: any = await wallUpSaveAPI(obj);
+
+    if (res.code === 0) {
+      message.success(`${id ? "编辑" : "新增"}成功!`);
+
+      upList();
+      closeFu();
+    }
+  }, [
+    closeFu,
+    cover.fileName,
+    cover.filePath,
+    id,
+    name,
+    type,
+    upList,
+    video.fileName,
+    video.filePath,
+  ]);
+
+  return (
+    <Modal
+      wrapClassName="wallAdd"
+      destroyOnClose
+      open={true}
+      title={id ? "编辑" : "新增"}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      {/* 主要内容 */}
+      <div className="wallAddMain">
+        <div className="row">
+          <div className="bs">*</div>
+          <div className="lable">类型:</div>
+          <Select
+            value={type}
+            onChange={(val) => setType(val)}
+            style={{ width: 100 }}
+            options={[
+              { value: "img", label: "图片" },
+              { value: "video", label: "视频" },
+            ]}
+          />
+        </div>
+
+        <div className="row">
+          <div className="bs">*</div>
+          <div className="lable">名称:</div>
+          <Input
+            maxLength={15}
+            showCount
+            style={{ width: 300 }}
+            placeholder="请输入"
+            value={name}
+            onChange={(e) => setName(e.target.value.trim())}
+          />
+        </div>
+
+        <div className="row">
+          <div className="bs">*</div>
+          <div className="lable">附件:</div>
+
+          <div className="upBox">
+            <input
+              id="upInput"
+              type="file"
+              accept={type === "img" ? ".png,.jpg,.gif,.jpeg" : ".mp4"}
+              ref={myInput}
+              onChange={(e) => handeUpPhoto(e)}
+            />
+            {type === "img" ? (
+              // 图片上传
+              <>
+                <div
+                  hidden={cover.fileName !== ""}
+                  className="fileBoxRow_up"
+                  onClick={() => myInput.current?.click()}
+                >
+                  <PlusOutlined />
+                </div>
+                <div
+                  className="fileBoxRow_r_img"
+                  hidden={cover.fileName === ""}
+                >
+                  {cover.filePath ? (
+                    <ImageLazy width={120} height={120} src={cover.filePath} />
+                  ) : null}
+
+                  <Popconfirm
+                    title="删除后无法恢复,是否删除?"
+                    okText="删除"
+                    cancelText="取消"
+                    onConfirm={() => setCover({ fileName: "", filePath: "" })}
+                  >
+                    <div className="clearCover">
+                      <CloseCircleOutlined />
+                    </div>
+                  </Popconfirm>
+                </div>
+              </>
+            ) : (
+              // 视频上传
+              <>
+                <div className="upVideo">
+                  <Button
+                    onClick={() => myInput.current?.click()}
+                    icon={<UploadOutlined />}
+                  >
+                    上传
+                  </Button>
+                </div>
+                {/* 视频上传成功之后的信息 */}
+                <div className="upVideoSucc">
+                  <div className="upVideoSuccTxt">{video.fileName}</div>
+                  <div
+                    className="lookVideoIncoBox"
+                    hidden={!video.fileName}
+                    onClick={() =>
+                      dispatch({
+                        type: "login/lookVideo",
+                        payload: video.filePath,
+                      })
+                    }
+                  >
+                    <PlayCircleOutlined />
+                  </div>
+                  <Popconfirm
+                    title="删除后无法恢复,是否删除?"
+                    okText="删除"
+                    cancelText="取消"
+                    onConfirm={() => setVideo({ fileName: "", filePath: "" })}
+                  >
+                    <div className="clearCover" hidden={!video.fileName}>
+                      <CloseCircleOutlined />
+                    </div>
+                  </Popconfirm>
+                </div>
+              </>
+            )}
+
+            {/* 图片上传提示 */}
+            <div className="fileBoxRow_r_tit" hidden={type !== "img"}>
+              图片格式:支持png、jpg、gif和jpeg的图片格式;最大支持20M。
+            </div>
+
+            {/* 视频上传提示 */}
+            <div className="fileBoxRow_r_tit" hidden={type !== "video"}>
+              视频格式:仅支持MP4格式的视频文件,大小不得超过500MB。
+            </div>
+          </div>
+        </div>
+      </div>
+
+      {/* 确定按钮 */}
+      <div className="wallAddBtn">
+        <Button type="primary" onClick={btnOkFu}>
+          提交
+        </Button>
+        &emsp;
+        <Popconfirm
+          title="放弃编辑后,信息将不会保存!"
+          okText="放弃"
+          cancelText="取消"
+          onConfirm={closeFu}
+        >
+          <Button>取消</Button>
+        </Popconfirm>
+      </div>
+    </Modal>
+  );
+}
+
+const MemoWallAdd = React.memo(WallAdd);
+
+export default MemoWallAdd;

+ 17 - 0
src/pages/Wall/WallTable/index.module.scss

@@ -0,0 +1,17 @@
+.WallTable{
+  width: 1200px;
+  height: 100%;
+  :global{
+    .title{
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      font-size: 16px;
+      font-weight: 700;
+      width: 100%;
+    }
+    .table{
+      margin-top: 15px;
+    }
+  }
+}

+ 212 - 0
src/pages/Wall/WallTable/index.tsx

@@ -0,0 +1,212 @@
+import ImageLazy from "@/components/ImageLazy";
+import VideoLook from "@/components/VideoLook";
+import { RootState } from "@/store";
+import {
+  awllDisplayAPI,
+  getWallTableListAPI,
+  wallRemoveAPI,
+  wallSortAPI,
+} from "@/store/action/wall";
+import { WallTableList } from "@/types";
+import { Button, message, Popconfirm, Switch, Table } from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useDispatch, useSelector } from "react-redux";
+import WallAdd from "../WallAdd";
+import styles from "./index.module.scss";
+function WallTable() {
+  const dispatch = useDispatch();
+
+  useEffect(() => {
+    dispatch(getWallTableListAPI());
+  }, [dispatch]);
+
+  // 切换表格中的启用停用状态
+  const isEnabledClickFu = useCallback(
+    async (val: boolean, id: number) => {
+      const isDisable = val ? 1 : 0;
+      const res: any = await awllDisplayAPI(id, isDisable);
+      if (res.code === 0) dispatch(getWallTableListAPI());
+    },
+    [dispatch]
+  );
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      const res: any = await wallRemoveAPI(id);
+      if (res.code === 0) {
+        message.success("删除成功!");
+        dispatch(getWallTableListAPI());
+      }
+    },
+    [dispatch]
+  );
+
+  // 点击上移或者下移
+
+  const srotFu = useCallback(
+    async (id: number, broId: number) => {
+      const res: any = await wallSortAPI(id, broId);
+      if (res.code === 0) {
+        message.success("操作成功!");
+        dispatch(getWallTableListAPI());
+      }
+    },
+    [dispatch]
+  );
+
+  // 从仓库中获取列表数据
+  const results = useSelector((state: RootState) => state.wallReducer.list);
+
+  // 点击新增或者编辑
+  const [open, setOpen] = useState(false);
+
+  const editId = useRef(0);
+
+  const editTableFu = useCallback(
+    (id: number) => {
+      if (id === 0 && results.length >= 20)
+        return message.warning("最多支持上传20个内容!");
+
+      editId.current = id;
+      setOpen(true);
+    },
+    [results.length]
+  );
+
+  const columns = useMemo(() => {
+    return [
+      // {
+      //   width: 80,
+      //   title: "序号",
+      //   render: (text: any, record: any, index: any) => index + 1,
+      // },
+      {
+        width: 100,
+        title: "内容类型",
+        render: (item: WallTableList) =>
+          item.type === "img" ? "图片" : "视频",
+      },
+      {
+        title: "附件名称",
+        dataIndex: "fileName",
+      },
+      {
+        width: 100,
+        title: "附件预览",
+        render: (item: WallTableList) => {
+          if (item.type === "img")
+            return (
+              <div className="tableImgAuto">
+                <ImageLazy width={80} height={80} src={item.filePath} />
+              </div>
+            );
+          else
+            return (
+              <div className="tableImgAuto">
+                <VideoLook width={80} height={80} src={item.filePath} />
+              </div>
+            );
+        },
+      },
+      {
+        title: "名称",
+        dataIndex: "name",
+      },
+      {
+        width: 100,
+        title: "展示状态",
+        render: (item: WallTableList) => (
+          <Switch
+            checkedChildren="启用"
+            unCheckedChildren="停用"
+            checked={item.display === 1}
+            onChange={(val) => isEnabledClickFu(val, item.id)}
+          />
+        ),
+      },
+      {
+        width: 200,
+        title: "操作",
+        render: (item: WallTableList, _: any, index: any) => (
+          <>
+            <Button
+              size="small"
+              type="text"
+              disabled={index === 0}
+              onClick={() => srotFu(item.id, results[index - 1].id)}
+            >
+              上移
+            </Button>
+            <Button
+              size="small"
+              type="text"
+              disabled={index === results.length - 1}
+              onClick={() => srotFu(item.id, results[index + 1].id)}
+            >
+              下移
+            </Button>
+            <Button
+              size="small"
+              type="text"
+              onClick={() => editTableFu(item.id)}
+            >
+              编辑
+            </Button>
+            <Popconfirm
+              title="删除后无法恢复,是否删除?"
+              okText="删除"
+              cancelText="取消"
+              onConfirm={() => delTableFu(item.id)}
+            >
+              <Button size="small" type="text" danger>
+                删除
+              </Button>
+            </Popconfirm>
+          </>
+        ),
+      },
+    ];
+  }, [delTableFu, editTableFu, isEnabledClickFu, results, srotFu]);
+
+  return (
+    <div className={styles.WallTable}>
+      <div className="title">
+        <div className="txt">内容管理</div>
+        <div className="titleButton">
+          <Button type="primary" onClick={() => editTableFu(0)}>
+            新增
+          </Button>
+        </div>
+      </div>
+      <div className="table">
+        <Table
+          size="small"
+          scroll={{ y: 428 }}
+          dataSource={results}
+          columns={columns}
+          rowKey="id"
+          pagination={false}
+        />
+      </div>
+      {/* 点击新增或者编辑 */}
+      {open ? (
+        <WallAdd
+          id={editId.current}
+          closeFu={() => setOpen(false)}
+          upList={() => dispatch(getWallTableListAPI())}
+        />
+      ) : null}
+    </div>
+  );
+}
+
+const MemoWallTable = React.memo(WallTable);
+
+export default MemoWallTable;

+ 60 - 0
src/pages/Wall/index.module.scss

@@ -0,0 +1,60 @@
+.Wall {
+  :global {
+    .wallBox1 {
+      width: 100%;
+      height: 170px;
+      margin: 15px 0;
+      border-radius: 10px;
+      background-color: #fff;
+      padding: 10px 15px 0;
+
+      .wallTit {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        font-size: 16px;
+        font-weight: 700;
+        width: 400px;
+
+        .wallLeft {
+          display: flex;
+          align-items: center;
+        }
+
+        .hotTitleInco {
+          cursor: pointer;
+          position: relative;
+          z-index: 10;
+          width: 16px;
+          height: 16px;
+          border-radius: 50%;
+          background-color: #696969;
+          margin-left: 8px;
+          text-align: center;
+          line-height: 16px;
+          color: #fff;
+          font-size: 12px;
+
+        }
+      }
+
+      .autoPlay {
+        display: flex;
+        align-items: center;
+        margin-top: 20px;
+
+        .bs {
+          margin: 0 8px;
+        }
+      }
+    }
+
+    .wallBox2 {
+      width: 100%;
+      height: calc(100% - 235px);
+      border-radius: 10px;
+      background-color: #fff;
+      padding: 10px 15px 0;
+    }
+  }
+}

+ 196 - 0
src/pages/Wall/index.tsx

@@ -0,0 +1,196 @@
+import {
+  Button,
+  Radio,
+  Tooltip,
+  DatePicker,
+  Select,
+  message,
+  Popconfirm,
+} from "antd";
+import React, { useCallback, useEffect, useState } from "react";
+import styles from "./index.module.scss";
+import { ExclamationOutlined } from "@ant-design/icons";
+import dayjs from "dayjs";
+import { editWallAutoApi, getWallAutoApi } from "@/store/action/wall";
+import { EditWallAutoApi } from "@/types";
+import WallTable from "./WallTable";
+const { RangePicker } = DatePicker;
+function Wall() {
+  // 获取自动播放信息
+  const getWallAutoApiFu = useCallback(async () => {
+    const res = await getWallAutoApi();
+    setValue0(res.data.isAuto);
+    setTime0(res.data.startTime + "," + res.data.endTime);
+    setMove0(res.data.type);
+  }, []);
+
+  useEffect(() => {
+    getWallAutoApiFu();
+  }, [getWallAutoApiFu]);
+
+  // 关于自动播放的修改
+  const [state, setState] = useState(false);
+  // 自动轮播开启关闭的展示
+  const [value0, setValue0] = useState(-1);
+
+  // 自动轮播开启关闭的修改
+  const [value1, setValue1] = useState(0);
+
+  // 自动轮播时间的展示
+  const [time0, setTime0] = useState(" , ");
+
+  // 自动轮播时间的修改
+  const [time1, setTime1] = useState<string[]>([]);
+
+  // 自动轮播动画的展示
+  const [move0, setMove0] = useState("");
+
+  // 自动轮播动画的修改
+  const [move1, setMove1] = useState("");
+
+  // 点击修改
+  const editAutoPlay = useCallback(() => {
+    setValue1(value0);
+    setTime1(time0.split(","));
+    setMove1(move0);
+    setState(true);
+  }, [move0, time0, value0]);
+
+  // 时间选择器改变
+  const timeChange = (date: any, dateString: any) => {
+    setTime1(dateString);
+  };
+
+  // 自动播放点击取消
+  const btnX = useCallback(() => {
+    setState(false);
+  }, []);
+
+  // 自动播放点击确定
+
+  const btnOk = useCallback(async () => {
+    const obj = {
+      endTime: time1[1],
+      startTime: time1[0],
+      isAuto: value1,
+      type: move1,
+    } as EditWallAutoApi;
+    const res: any = await editWallAutoApi(obj);
+    if (res.code === 0) {
+      message.success("修改成功!");
+      getWallAutoApiFu();
+    }
+    btnX();
+  }, [btnX, getWallAutoApiFu, move1, time1, value1]);
+
+  return (
+    <div className={styles.Wall}>
+      <div className="pageTitlt">万物墙管理</div>
+
+      {/* 第一个盒子 */}
+      <div className="wallBox1">
+        {/* 自动播放 */}
+        <div className="wallTit">
+          <div className="wallLeft">
+            自动播放
+            <div className="hotTitleInco">
+              <Tooltip title="当超过15秒未操作时,将按顺序自动播放下列内容">
+                <div className="hotTitleInco1">
+                  <ExclamationOutlined />
+                </div>
+              </Tooltip>
+            </div>
+          </div>
+
+          <div className="awllTitButton">
+            {state ? (
+              <>
+                <Button type="primary" onClick={btnOk}>
+                  确定
+                </Button>
+                &emsp;
+                <Popconfirm
+                  title="放弃编辑后,信息将不会保存!"
+                  okText="放弃"
+                  cancelText="取消"
+                  onConfirm={btnX}
+                >
+                  <Button>取消</Button>
+                </Popconfirm>
+              </>
+            ) : (
+              <Button type="primary" onClick={editAutoPlay}>
+                修改
+              </Button>
+            )}
+          </div>
+        </div>
+
+        <div className="autoPlay">
+          <div>自动轮播:</div>&emsp;&emsp;
+          {state ? (
+            <>
+              <Radio.Group
+                onChange={(e) => setValue1(e.target.value)}
+                value={value1}
+              >
+                <Radio value={0}>关闭</Radio>
+                <Radio value={1}>开启</Radio>
+              </Radio.Group>
+              &emsp;
+              <RangePicker
+                allowClear={false}
+                defaultValue={[
+                  dayjs(time1[0], "YYYY/MM/DD"),
+                  dayjs(time1[1], "YYYY/MM/DD"),
+                ]}
+                onChange={timeChange}
+              />
+            </>
+          ) : (
+            <>
+              <div>{value0 ? "开启" : "关闭"}</div>
+              <div className="bs"> | </div>
+              <div>
+                {time0.split(",")[0]} 至 {time0.split(",")[1]}
+              </div>
+            </>
+          )}
+        </div>
+
+        <div className="autoPlay">
+          <div>动画设置:</div>&emsp;&emsp;
+          {state ? (
+            <>
+              <Select
+                style={{ width: 150 }}
+                value={move1}
+                onChange={(e) => setMove1(e)}
+                options={[
+                  { value: "随机", label: "随机" },
+                  { value: "飞入", label: "飞入" },
+                  { value: "劈裂", label: "劈裂" },
+                  { value: "百叶窗", label: "百叶窗" },
+                  { value: "落叶式", label: "落叶式" },
+                  { value: "规则图形路径", label: "规则图形路径" },
+                  { value: "圆形展开", label: "圆形展开" },
+                ]}
+              />
+            </>
+          ) : (
+            <div>{move0}</div>
+          )}
+        </div>
+      </div>
+
+      {/* 第二个盒子 */}
+      <div className="wallBox2">
+        <WallTable />
+      </div>
+    </div>
+  );
+}
+
+const MemoWall = React.memo(Wall);
+
+export default MemoWall;

+ 48 - 0
src/store/action/exhibit.ts

@@ -0,0 +1,48 @@
+import { ExhibitTableType } from "@/types/api/exhibit";
+import http from "@/utils/http";
+import { AppDispatch } from "..";
+/**
+ * 获取列表
+ */
+export const getExhibitListAPI = () => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.get("cms/exhibition/getList");
+    const list: ExhibitTableType[] = res.data;
+
+    dispatch({ type: "exhibit/getList", payload: list });
+  };
+};
+
+/**
+ * 通过id获取详情
+ */
+export const getExhibitInfoAPI = (id: number) => {
+  return http.get(`cms/exhibition/detail/${id}`);
+};
+
+// 上传附件的进度条
+const UpAsyncLodingDom: any = document.querySelector("#UpAsyncLoding");
+const progressDom: any = document.querySelector("#progress");
+
+/**
+ * 上传封面图和附件
+ */
+export const exhibitUploadAPI = (data: any) => {
+  UpAsyncLodingDom.style.opacity = 1;
+
+  return http.post("cms/exhibition/upload", data, {
+    timeout:30000,
+    // 显示进度条
+    onUploadProgress: (e: any) => {
+      const complete = (e.loaded / e.total) * 100 || 0;
+      progressDom.style.width = complete + "%";
+    },
+  });
+};
+
+/**
+ * 编辑展馆
+ */
+export const exhibitionSaveAPI = (data: ExhibitTableType) => {
+  return http.post("cms/exhibition/save",data);
+};

+ 63 - 0
src/store/action/goods.ts

@@ -0,0 +1,63 @@
+import { GoodsTableSearch } from "@/types";
+import http from "@/utils/http";
+import { AppDispatch } from "..";
+/**
+ * 获取列表数据
+ */
+export const getGoodsList = (data: any) => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post("cms/goods/pageList", data);
+    dispatch({
+      type: "goods/getList",
+      payload: { list: res.data.records, total: res.data.total },
+    });
+  };
+};
+
+// 上传附件的进度条
+const UpAsyncLodingDom: any = document.querySelector("#UpAsyncLoding");
+const progressDom: any = document.querySelector("#progress");
+
+/**
+ * 上传封面图和附件
+ */
+export const goodsUploadAPI = (data: any) => {
+  UpAsyncLodingDom.style.opacity = 1;
+
+  return http.post("cms/goods/upload", data, {
+    timeout:30000,
+    // 显示进度条
+    onUploadProgress: (e: any) => {
+      const complete = (e.loaded / e.total) * 100 || 0;
+      progressDom.style.width = complete + "%";
+    },
+  });
+};
+
+/**
+ * 新增编辑藏品
+ */
+export const goodsSaveAPI = (data: GoodsTableSearch) => {
+  return http.post("cms/goods/save", data);
+};
+
+/**
+ * 删除藏品
+ */
+export const goodsRemoveAPI = (id: number) => {
+  return http.get(`cms/goods/remove/${id}`);
+};
+
+/**
+ * 内容-是否显示
+ */
+export const goodsDisplayAPI = (id: number, display: number) => {
+  return http.get(`cms/goods/display/${id}/${display}`);
+};
+
+/**
+ * 通过id获取详情
+ */
+export const goodsDetailById = (id: number) => {
+  return http.get(`cms/goods/detail/${id}`);
+};

+ 15 - 0
src/store/action/log.ts

@@ -0,0 +1,15 @@
+import http from "@/utils/http";
+import { AppDispatch } from "..";
+/**
+ * 获取用户管理表格列表
+ */
+export const getLogListAPI = (data: any) => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post("sys/log/list", data);
+    const obj = {
+      list: res.data.records,
+      total: res.data.total,
+    };
+    dispatch({ type: "log/getList", payload: obj });
+  };
+};

+ 40 - 0
src/store/action/login.ts

@@ -0,0 +1,40 @@
+// 导入总仓库和自己声明的 AppDispatch
+// import store, { AppDispatch } from ".."
+import { DictListTypeAPI, DictListTypeObj } from "@/types";
+import http from "@/utils/http";
+import { AppDispatch } from "..";
+
+/**
+ * 用户登录接口
+ */
+export const userLoginAPI = (data: any) => {
+  return http.post("admin/login", { ...data });
+};
+
+/**
+ * 修改密码接口
+ */
+export const passWordEditAPI = (data: any) => {
+  return http.post("sys/user/updatePwd", { ...data });
+};
+
+/**
+ * 获取下拉框数据
+ */
+export const getDictListAPI = () => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.get("cms/dict/getList");
+    const list: DictListTypeAPI[] = res.data;
+    const obj = {
+      age: [{ label: "全部", value: "", type: "age" }],
+      texture: [{ label: "全部", value: "", type: "texture" }],
+      level: [{ label: "全部", value: "", type: "level" }],
+      source: [{ label: "全部", value: "", type: "source" }],
+    } as DictListTypeObj;
+    list.forEach((v) => {
+      obj[v.type].push({ label: v.name, value: v.name, type: v.type });
+    });
+
+    dispatch({ type: "login/getDictList", payload: obj });
+  };
+};

+ 56 - 0
src/store/action/role.ts

@@ -0,0 +1,56 @@
+import { AddRoleType } from "@/types";
+import http from "@/utils/http";
+import { AppDispatch } from "..";
+/**
+ * 获取角色表格列表数据
+ */
+export const getRoleListAPI = (data: any) => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post("sys/role/listCountPage", data);
+
+    const obj = {
+      list: res.data.records,
+      total: res.data.total,
+    };
+
+    dispatch({ type: "Role/getList", payload: obj });
+  };
+};
+
+/**
+ * 删除角色
+ */
+export const roleRemoveAPI = (id: number) => {
+  return http.get(`sys/role/remove/${id}`);
+};
+
+/**
+ * 用户-是否显示
+ */
+export const roleDisplayAPI = (id: number, display: number) => {
+  return http.get(`sys/role/editStatus/${id}/${display}`);
+};
+
+/**
+ * 获取用户的权限信息
+ */
+export const getPermissionsAPI = () => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.get("sys/resource/getTreePermissions");
+    dispatch({ type: "login/setAuthPageArr", payload: res.data });
+  };
+};
+
+/**
+ * 新增或修改角色
+ */
+export const roleSaveAPI = (data: AddRoleType) => {
+  return http.post("sys/role/save", data);
+};
+
+/**
+ * 通过id获取角色详情
+ */
+export const getRoleInfoByIdAPI = (id: number) => {
+  return http.get(`sys/role/detail/${id}`);
+};

+ 64 - 0
src/store/action/user.ts

@@ -0,0 +1,64 @@
+import { RoleTableType, SaveUserType, UserTableAPIType } from "@/types";
+import http from "@/utils/http";
+import { AppDispatch } from "..";
+/**
+ * 获取用户管理表格列表
+ */
+export const getUserListAPI = (data: UserTableAPIType) => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post("sys/user/list", data);
+    const obj = {
+      list: res.data.records,
+      total: res.data.total,
+    };
+
+    dispatch({ type: "user/getList", payload: obj });
+  };
+};
+
+/**
+ * 获取用户管理-角色列表
+ */
+export const getUserRoleAPI = () => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.get("sys/user/getRole");
+    const data: RoleTableType[] = res.data;
+    const newData = data.map((v) => ({ label: v.roleName, value: v.id }));
+    dispatch({ type: "user/getRoleList", payload: newData });
+  };
+};
+
+/**
+ * 用户-是否显示
+ */
+export const userDisplayAPI = (id: number, display: number) => {
+  return http.get(`sys/user/editStatus/${id}/${display}`);
+};
+
+/**
+ * 删除用户
+ */
+export const userRemoveAPI = (id: number) => {
+  return http.get(`sys/user/removes/${id}`);
+};
+
+/**
+ * 重置密码
+ */
+export const userPassResetAPI = (id: number) => {
+  return http.get(`sys/user/resetPass/${id}`);
+};
+
+/**
+ * 新增/修改用户信息
+ */
+export const userSaveAPI = (data: SaveUserType) => {
+  return http.post("sys/user/save", data);
+};
+
+/**
+ * 通过id获取角色详情
+ */
+export const getUserInfoByIdAPI = (id: number) => {
+  return http.get(`sys/user/detail/${id}`);
+};

+ 83 - 0
src/store/action/wall.ts

@@ -0,0 +1,83 @@
+import { EditWallAutoApi, WallUpSaveAPI } from "@/types";
+import http from "@/utils/http";
+import { AppDispatch } from "..";
+
+/**
+ * 获取万物墙自动播放数据
+ */
+export const getWallAutoApi = () => {
+  return http.get("cms/wall/getInfo");
+};
+
+/**
+ * 修改万物墙自动播放数据
+ */
+export const editWallAutoApi = (data: EditWallAutoApi) => {
+  return http.post("cms/wall/editInfo", data);
+};
+
+/**
+ * 获取列表数据
+ */
+export const getWallTableListAPI = () => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.get("cms/wall/list");
+    dispatch({ type: "wall/getList", payload: res.data });
+  };
+};
+
+/**
+ * 内容-是否显示
+ */
+export const awllDisplayAPI = (id: number, display: number) => {
+  return http.get(`cms/wall/display/${id}/${display}`);
+};
+
+/**
+ * 万物墙上传图片和视频
+ */
+
+// 上传附件的进度条
+const UpAsyncLodingDom: any = document.querySelector("#UpAsyncLoding");
+const progressDom: any = document.querySelector("#progress");
+
+export const wallUploadAPI = (data: any) => {
+  UpAsyncLodingDom.style.opacity = 1;
+
+  return http.post("cms/wall/upload", data, {
+    timeout:30000,
+    // 显示进度条
+    onUploadProgress: (e: any) => {
+      const complete = (e.loaded / e.total) * 100 || 0;
+      progressDom.style.width = complete + "%";
+    },
+  });
+};
+
+/**
+ * 内容-新增|编辑
+ */
+export const wallUpSaveAPI = (data: WallUpSaveAPI) => {
+  return http.post("cms/wall/save", data);
+};
+
+/**
+ * 内容-排序
+ */
+export const wallSortAPI = (id1: number, id2: number) => {
+  return http.get(`cms/wall/sort/${id1}/${id2}`);
+};
+
+/**
+ * 内容-删除
+ */
+export const wallRemoveAPI = (id: number) => {
+  return http.get(`cms/wall/remove/${id}`);
+};
+
+/**
+ * 内容-详情
+ */
+export const getWallDetailByIdAPI = (id: number) => {
+  return http.get(`cms/wall/detail/${id}`);
+};

+ 20 - 0
src/store/index.ts

@@ -0,0 +1,20 @@
+// 导入 redux
+import { applyMiddleware, legacy_createStore as createStore } from 'redux'
+// 导入自己封装的  rootReducer 
+import rootReducer from './reducer'
+// 导入调试工具和 异步的 redux(用来发送异步请求)
+// 调试工具需要下载谷歌 扩展程序 我用的是 Redux DevTools 3.0.17
+import { composeWithDevTools } from 'redux-devtools-extension'
+import thunk from 'redux-thunk'
+
+// 创建仓库实例
+const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))
+
+// 声明 RootState,在使用仓库的时候用来使用
+export type RootState = ReturnType<typeof store.getState>
+
+// 声明 AppDispatch,在异步请求的时候来使用
+export type AppDispatch = typeof store.dispatch
+
+// 导出仓库实例
+export default store

+ 27 - 0
src/store/reducer/exhibit.ts

@@ -0,0 +1,27 @@
+import { ExhibitTableType } from "@/types/api/exhibit";
+
+// 初始化状态
+const initState = {
+  // 列表数据
+  list: [] as ExhibitTableType[],
+};
+
+// 定义 action 类型
+type ExhibitActionType = {
+  type: "exhibit/getList";
+  payload: ExhibitTableType[];
+};
+
+// 频道 reducer
+export default function ExhibitReducer(
+  state = initState,
+  action: ExhibitActionType
+) {
+  switch (action.type) {
+    // 获取列表数据
+    case "exhibit/getList":
+      return { ...state, list: action.payload };
+    default:
+      return state;
+  }
+}

+ 30 - 0
src/store/reducer/goods.ts

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

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

@@ -0,0 +1,25 @@
+// 导入合并reducer的依赖
+import { combineReducers } from 'redux'
+import goodsReducer from './goods'
+import ExhibitReducer from './exhibit'
+import logReducer from './log'
+
+// 导入 登录 模块的 reducer
+import loginReducer from './login'
+import RoleReducer from './role'
+import userReducer from './user'
+import wallReducer from './wall'
+
+// 合并 reducer
+const rootReducer = combineReducers({
+  loginStore: loginReducer,
+  wallReducer:wallReducer,
+  goodsReducer:goodsReducer,
+  userReducer:userReducer,
+  logReducer:logReducer,
+  RoleReducer:RoleReducer,
+  ExhibitReducer:ExhibitReducer
+})
+
+// 默认导出
+export default rootReducer

+ 25 - 0
src/store/reducer/log.ts

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

+ 57 - 0
src/store/reducer/login.ts

@@ -0,0 +1,57 @@
+import {  PermissionsAPIType } from "@/types";
+
+// 初始化状态
+const initState = {
+  // 所有图片点击预览查看大图
+  lookBigImg: {
+    url: "",
+    show: false,
+  },
+  // 加载请求的loding
+  asyncLoding: false,
+  // 视频的src
+  videoSrc: "",
+  // 所有的下拉框数据
+  dictList: {
+    age: [],
+    texture: [],
+    level: [],
+    source: [],
+  } as any,
+  // 有关权限的信息
+  authPageArr: [] as PermissionsAPIType[],
+};
+
+// 定义 action 类型
+type LoginActionType =
+  | { type: "login/lookBigImg"; payload: { url: string; show: boolean } }
+  | { type: "login/asyncLoding"; payload: boolean }
+  | { type: "login/lookVideo"; payload: string }
+  | { type: "login/getDictList"; payload: any }
+  | { type: "login/setAuthPageArr"; payload: PermissionsAPIType[] };
+
+// 频道 reducer
+export default function loginReducer(
+  state = initState,
+  action: LoginActionType
+) {
+  switch (action.type) {
+    // 所有图片点击预览查看大图
+    case "login/lookBigImg":
+      return { ...state, lookBigImg: action.payload };
+    // 加载请求的loding
+    case "login/asyncLoding":
+      return { ...state, asyncLoding: action.payload };
+    // 查看视频
+    case "login/lookVideo":
+      return { ...state, videoSrc: action.payload };
+    // 所有的下拉框数据
+    case "login/getDictList":
+      return { ...state, dictList: action.payload };
+    // 有关权限的信息
+    case "login/setAuthPageArr":
+      return { ...state, authPageArr: action.payload };
+    default:
+      return state;
+  }
+}

+ 27 - 0
src/store/reducer/role.ts

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

+ 44 - 0
src/store/reducer/user.ts

@@ -0,0 +1,44 @@
+import { UserTableListType } from "@/types";
+
+// 初始化状态
+const initState = {
+  // 列表数据
+  tableInfo: {
+    list: [] as UserTableListType[],
+    total: 0,
+  },
+  // 角色列表数据
+  roleList: [] as {
+    label: string;
+    value: number;
+  }[],
+};
+
+// 定义 action 类型
+type UserActionType =
+  | {
+      type: "user/getList";
+      payload: { list: UserTableListType[]; total: number };
+    }
+  | {
+      type: "user/getRoleList";
+      payload: {
+        label: string;
+        value: number;
+      }[];
+    };
+
+// 频道 reducer
+export default function userReducer(state = initState, action: UserActionType) {
+  switch (action.type) {
+    // 获取列表数据
+    case "user/getList":
+      return { ...state, tableInfo: action.payload };
+    // 获取角色列表数据
+    case "user/getRoleList":
+      return { ...state, roleList: action.payload };
+
+    default:
+      return state;
+  }
+}

+ 21 - 0
src/store/reducer/wall.ts

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

+ 13 - 0
src/types/api/exhibit.d.ts

@@ -0,0 +1,13 @@
+export type ExhibitTableType = {
+  address: string;
+  createTime: string;
+  creatorId: null;
+  creatorName: string;
+  description: string;
+  id: number;
+  name: string;
+  openTime: string;
+  phone: string;
+  thumb: string;
+  updateTime: string;
+};

+ 21 - 0
src/types/api/goods.d.ts

@@ -0,0 +1,21 @@
+export type GoodsTableSearch = {
+  dictTexture: string;
+  dictAge: string;
+  dictLevel: string;
+  dictSource: string;
+  startTime: string;
+  endTime: string;
+  display: -1 | 0 | 1;
+  pageSize: number;
+  pageNum: number;
+  description?: string;
+  dirCode?: string;
+  fileName?: string;
+  filePath?: string;
+  id?: number;
+  thumb?: string;
+  type?: "model" | "img" | "audio" | "video";
+
+  searchKey: string;
+  exhibitionId: string;
+};

+ 11 - 0
src/types/api/log.d.ts

@@ -0,0 +1,11 @@
+type LogTableType = {
+  createTime: string;
+  creatorId: null;
+  creatorName: string;
+  description: string;
+  id: number;
+  ip: string;
+  type: string;
+  updateTime: null;
+  userName: string;
+}

+ 27 - 0
src/types/api/login.d.ts

@@ -0,0 +1,27 @@
+export type DictListType = {
+  label: string;
+  value: string;
+  type: "age" | "texture" | "level" | "source";
+};
+
+export type DictListTypeObj = {
+  age: DictListType[];
+  texture: DictListType[];
+  level: DictListType[];
+  source: DictListType[];
+};
+
+
+export type DictListTypeAPI ={
+  createTime: null;
+  creatorId: null;
+  creatorName: string;
+  display: number;
+  id: string;
+  name: string;
+  parentId: null;
+  sort: null;
+  type: "age" | "texture" | "level" | "source";
+  updateTime: null;
+}
+

+ 30 - 0
src/types/api/role.d.ts

@@ -0,0 +1,30 @@
+export type RoleTableType = {
+  count: number;
+  createTime: string;
+  creatorId: null;
+  creatorName: string;
+  id: number;
+  isEnabled: number;
+  roleDesc: string;
+  roleKey: string;
+  roleName: string;
+  sort: string;
+  updateTime: string;
+};
+
+
+export type PermissionsAPIType = {
+  authority: boolean;
+  children?: null;
+  id: number;
+  name: string;
+  parentId?: null;
+  resourceType?: string;
+}
+
+export type AddRoleType ={
+  id:number|null
+  roleName:string
+  roleDesc:string
+  resources:number[]
+}

+ 35 - 0
src/types/api/user.d.ts

@@ -0,0 +1,35 @@
+export type UserTableAPIType={
+  startTime:string
+  endTime:string
+  nickName:string
+  pageNum:number
+  pageSize:number
+  realName:string
+  searchKey:string
+}
+
+export type UserTableListType={
+  createTime: string;
+  creatorId: null;
+  creatorName: string;
+  id: number;
+  isAdmin: number;
+  isEnabled: number;
+  nickName: string;
+  phone: string;
+  realName: string;
+  roleId: null;
+  roleName: string;
+  sex: string;
+  thumb: string;
+  updateTime: string;
+  userName: string;
+}
+
+export type SaveUserType ={
+  id:number|null
+  userName:string
+  nickName:string
+  roleId:number
+  realName:string
+}

+ 28 - 0
src/types/api/wall.d.ts

@@ -0,0 +1,28 @@
+export type EditWallAutoApi = {
+  endTime: string;
+  isAuto: number;
+  startTime: string;
+  type: string;
+};
+
+export type WallTableList = {
+  createTime: string;
+  creatorId: number;
+  creatorName: string;
+  display: number;
+  fileName: string;
+  filePath: string;
+  id: number;
+  name: string;
+  sort: number;
+  type: 'img'|'video';
+  updateTime: string;
+};
+
+export type WallUpSaveAPI ={
+  fileName: string;
+  filePath: string;
+  id: number|null;
+  name: string;
+  type: string;
+}

+ 5 - 0
src/types/declaration.d.ts

@@ -0,0 +1,5 @@
+declare module "history";
+declare module "*.scss";
+declare module "*.png";
+declare module "*.gif";
+declare module "js-export-excel";

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


Некоторые файлы не были показаны из-за большого количества измененных файлов