shaogen1995 2 年之前
當前提交
0592065624
共有 98 個文件被更改,包括 36931 次插入0 次删除
  1. 23 0
      管理后台/.gitignore
  2. 46 0
      管理后台/README.md
  3. 10 0
      管理后台/config-overrides.js
  4. 29967 0
      管理后台/package-lock.json
  5. 65 0
      管理后台/package.json
  6. 8 0
      管理后台/path.tsconfig.json
  7. 二進制
      管理后台/public/favicon.ico
  8. 43 0
      管理后台/public/index.html
  9. 65 0
      管理后台/src/App.tsx
  10. 二進制
      管理后台/src/assets/img/IMGerror.png
  11. 二進制
      管理后台/src/assets/img/bg.jpg
  12. 二進制
      管理后台/src/assets/img/inco1.png
  13. 二進制
      管理后台/src/assets/img/inco1Ac.png
  14. 二進制
      管理后台/src/assets/img/inco2.png
  15. 二進制
      管理后台/src/assets/img/inco2Ac.png
  16. 二進制
      管理后台/src/assets/img/inco3.png
  17. 二進制
      管理后台/src/assets/img/inco3Ac.png
  18. 二進制
      管理后台/src/assets/img/inco4.png
  19. 二進制
      管理后台/src/assets/img/inco4Ac.png
  20. 二進制
      管理后台/src/assets/img/inco5.png
  21. 二進制
      管理后台/src/assets/img/inco5Ac.png
  22. 二進制
      管理后台/src/assets/img/layoutLeftMain.jpg
  23. 二進制
      管理后台/src/assets/img/loading.gif
  24. 二進制
      管理后台/src/assets/img/loginBox.jpg
  25. 二進制
      管理后台/src/assets/img/logo.png
  26. 二進制
      管理后台/src/assets/img/logo2.png
  27. 二進制
      管理后台/src/assets/img/top.jpg
  28. 二進制
      管理后台/src/assets/img/user.png
  29. 196 0
      管理后台/src/assets/styles/base.css
  30. 254 0
      管理后台/src/assets/styles/base.less
  31. 21 0
      管理后台/src/components/AsyncSpinLoding/index.module.scss
  32. 15 0
      管理后台/src/components/AsyncSpinLoding/index.tsx
  33. 32 0
      管理后台/src/components/AuthRoute/index.tsx
  34. 37 0
      管理后台/src/components/ImageLazy/index.module.scss
  35. 93 0
      管理后台/src/components/ImageLazy/index.tsx
  36. 29 0
      管理后台/src/components/Message/index.tsx
  37. 26 0
      管理后台/src/components/NotFound/index.tsx
  38. 10 0
      管理后台/src/components/SpinLoding/index.module.scss
  39. 13 0
      管理后台/src/components/SpinLoding/index.tsx
  40. 43 0
      管理后台/src/components/UpAsyncLoding/index.module.scss
  41. 38 0
      管理后台/src/components/UpAsyncLoding/index.tsx
  42. 52 0
      管理后台/src/components/VideoLook/index.module.scss
  43. 37 0
      管理后台/src/components/VideoLook/index.tsx
  44. 34 0
      管理后台/src/components/VideoLookDom/index.module.scss
  45. 36 0
      管理后台/src/components/VideoLookDom/index.tsx
  46. 41 0
      管理后台/src/index.tsx
  47. 116 0
      管理后台/src/pages/A1Hot/index.module.scss
  48. 314 0
      管理后台/src/pages/A1Hot/index.tsx
  49. 181 0
      管理后台/src/pages/A2News/NewsAdd/index.module.scss
  50. 530 0
      管理后台/src/pages/A2News/NewsAdd/index.tsx
  51. 49 0
      管理后台/src/pages/A2News/NewsTable/index.module.scss
  52. 245 0
      管理后台/src/pages/A2News/NewsTable/index.tsx
  53. 20 0
      管理后台/src/pages/A2News/index.module.scss
  54. 181 0
      管理后台/src/pages/A2News/index.tsx
  55. 198 0
      管理后台/src/pages/A3Goods/GoodsAdd/index.module.scss
  56. 745 0
      管理后台/src/pages/A3Goods/GoodsAdd/index.tsx
  57. 43 0
      管理后台/src/pages/A3Goods/index.module.scss
  58. 311 0
      管理后台/src/pages/A3Goods/index.tsx
  59. 139 0
      管理后台/src/pages/A4Venue/VenueEdit/index.module.scss
  60. 317 0
      管理后台/src/pages/A4Venue/VenueEdit/index.tsx
  61. 49 0
      管理后台/src/pages/A4Venue/VenueTable/index.module.scss
  62. 203 0
      管理后台/src/pages/A4Venue/VenueTable/index.tsx
  63. 20 0
      管理后台/src/pages/A4Venue/index.module.scss
  64. 122 0
      管理后台/src/pages/A4Venue/index.tsx
  65. 34 0
      管理后台/src/pages/A5Guest/index.module.scss
  66. 183 0
      管理后台/src/pages/A5Guest/index.tsx
  67. 194 0
      管理后台/src/pages/Layout/index.module.scss
  68. 275 0
      管理后台/src/pages/Layout/index.tsx
  69. 142 0
      管理后台/src/pages/Login/index.module.scss
  70. 82 0
      管理后台/src/pages/Login/index.tsx
  71. 5 0
      管理后台/src/pages/初始化组件/index.module.scss
  72. 14 0
      管理后台/src/pages/初始化组件/index.tsx
  73. 31 0
      管理后台/src/store/action/A1Hot.ts
  74. 72 0
      管理后台/src/store/action/A2News.ts
  75. 71 0
      管理后台/src/store/action/A3Goods.ts
  76. 65 0
      管理后台/src/store/action/A4Venue.ts
  77. 22 0
      管理后台/src/store/action/A5Guest.ts
  78. 39 0
      管理后台/src/store/action/layout.ts
  79. 20 0
      管理后台/src/store/index.ts
  80. 29 0
      管理后台/src/store/reducer/A2News.ts
  81. 30 0
      管理后台/src/store/reducer/A3Goods.ts
  82. 21 0
      管理后台/src/store/reducer/A4Venue.ts
  83. 19 0
      管理后台/src/store/reducer/index.ts
  84. 70 0
      管理后台/src/store/reducer/layout.ts
  85. 8 0
      管理后台/src/types/api/A1Hot.d.ts
  86. 41 0
      管理后台/src/types/api/A2News.d.ts
  87. 33 0
      管理后台/src/types/api/A3Goods.d.ts
  88. 22 0
      管理后台/src/types/api/A4Venue.d.ts
  89. 24 0
      管理后台/src/types/api/layot.d.ts
  90. 6 0
      管理后台/src/types/declaration.d.ts
  91. 6 0
      管理后台/src/types/index.d.ts
  92. 35 0
      管理后台/src/utils/domShow.ts
  93. 17 0
      管理后台/src/utils/history.ts
  94. 93 0
      管理后台/src/utils/http.ts
  95. 50 0
      管理后台/src/utils/message.ts
  96. 100 0
      管理后台/src/utils/pass.ts
  97. 34 0
      管理后台/src/utils/storage.ts
  98. 27 0
      管理后台/tsconfig.json

+ 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)

文件差異過大導致無法顯示
+ 29967 - 0
管理后台/package-lock.json


+ 65 - 0
管理后台/package.json

@@ -0,0 +1,65 @@
+{
+  "name": "demo",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@ant-design/cssinjs": "^1.5.6",
+    "@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",
+    "react": "^18.2.0",
+    "react-dnd": "^16.0.1",
+    "react-dnd-html5-backend": "^16.0.1",
+    "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/*"]
+      }
+    }
+  }

二進制
管理后台/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>

+ 65 - 0
管理后台/src/App.tsx

@@ -0,0 +1,65 @@
+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 { useSelector } from "react-redux";
+import store, { RootState } from "./store";
+import UpAsyncLoding from "./components/UpAsyncLoding";
+import VideoLookDom from "./components/VideoLookDom";
+import MessageCom from "./components/Message";
+const Layout = React.lazy(() => import("./pages/Layout"));
+const Login = React.lazy(() => import("./pages/Login"));
+
+export default function App() {
+  // 从仓库中获取查看图片的信息
+  const lookBigImg = useSelector(
+    (state: RootState) => state.layoutStore.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) => {
+            // 清除仓库信息
+            store.dispatch({
+              type: "layout/lookBigImg",
+              payload: { url: "", show: false },
+            });
+          },
+        }}
+      />
+
+      {/* 上传附件的进度条元素 */}
+      <UpAsyncLoding />
+
+      {/* 点击预览视频组件 */}
+      <VideoLookDom />
+
+      {/* antd 轻提示 ---兼容360浏览器 */}
+      <MessageCom />
+    </>
+  );
+}

二進制
管理后台/src/assets/img/IMGerror.png


二進制
管理后台/src/assets/img/bg.jpg


二進制
管理后台/src/assets/img/inco1.png


二進制
管理后台/src/assets/img/inco1Ac.png


二進制
管理后台/src/assets/img/inco2.png


二進制
管理后台/src/assets/img/inco2Ac.png


二進制
管理后台/src/assets/img/inco3.png


二進制
管理后台/src/assets/img/inco3Ac.png


二進制
管理后台/src/assets/img/inco4.png


二進制
管理后台/src/assets/img/inco4Ac.png


二進制
管理后台/src/assets/img/inco5.png


二進制
管理后台/src/assets/img/inco5Ac.png


二進制
管理后台/src/assets/img/layoutLeftMain.jpg


二進制
管理后台/src/assets/img/loading.gif


二進制
管理后台/src/assets/img/loginBox.jpg


二進制
管理后台/src/assets/img/logo.png


二進制
管理后台/src/assets/img/logo2.png


二進制
管理后台/src/assets/img/top.jpg


二進制
管理后台/src/assets/img/user.png


+ 196 - 0
管理后台/src/assets/styles/base.css

@@ -0,0 +1,196 @@
+* {
+  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: black;
+}
+a {
+  text-decoration: none;
+  color: black;
+  outline: none;
+}
+i {
+  font-style: normal;
+}
+img {
+  max-width: 100%;
+  max-height: 100%;
+  vertical-align: middle;
+  object-fit: cover;
+}
+ul {
+  list-style: none;
+}
+body {
+  overflow: auto;
+  overflow-y: overlay;
+}
+/* 文本域取消下拉 */
+textarea {
+  resize: none !important;
+}
+/* 主题色 */
+:root {
+  --themeColor: #9d6b39;
+}
+/* 找不到页面 */
+.noFindPage {
+  opacity: 0;
+  transition: opacity 0.5s;
+}
+/* 兼容360浏览器的下拉框 */
+.ant-select-selector {
+  position: relative;
+  background-color: #ffffff;
+  border: 1px solid #d9d9d9;
+  transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+#root {
+  width: 100vw;
+  height: 100vh;
+  min-width: 1600px;
+  min-height: 900px;
+  overflow: auto;
+  overflow-y: overlay;
+  /* 普通文字按钮的颜色 */
+  /* 按钮的危险颜色 */
+  /* antd分页器样式 */
+  /* 表格的图片居中 */
+  /* antd图片预览组件 */
+  /* antd表格居中 */
+}
+#root a {
+  color: var(--themeColor);
+}
+#root .ant-btn-text {
+  color: #99b8dd;
+}
+#root .ant-btn-text:disabled {
+  cursor: not-allowed;
+  color: rgba(0, 0, 0, 0.25);
+}
+#root .ant-btn-text.ant-btn-dangerous {
+  color: var(--themeColor);
+}
+#root .ant-pagination .ant-pagination-item {
+  border-radius: 50%;
+  border: 1px solid #999;
+  background-color: transparent !important;
+}
+#root .ant-pagination .ant-pagination-item-active {
+  background-color: var(--themeColor) !important;
+}
+#root .ant-pagination .ant-pagination-item-active a {
+  color: #fff !important;
+}
+#root .ant-pagination .ant-pagination-item:hover {
+  background-color: var(--themeColor) !important;
+}
+#root .ant-pagination .ant-pagination-item:hover a {
+  color: #fff !important;
+}
+#root .ant-pagination-prev {
+  border-radius: 50% !important;
+  border: 1px solid #999;
+}
+#root .ant-pagination-prev:hover {
+  background-color: var(--themeColor);
+}
+#root .ant-pagination-prev:hover button {
+  color: #fff;
+}
+#root .ant-pagination-next {
+  border-radius: 50% !important;
+  border: 1px solid #999;
+}
+#root .ant-pagination-next:hover {
+  background-color: var(--themeColor);
+}
+#root .ant-pagination-next:hover button {
+  color: #fff;
+}
+#root .ant-pagination-disabled {
+  border: 1px solid #ccc;
+}
+#root .ant-pagination-disabled:hover {
+  background-color: transparent;
+}
+#root .tableImgAuto {
+  display: flex;
+  justify-content: center;
+}
+#root .ant-image {
+  display: none;
+}
+#root .ant-table-cell {
+  text-align: center !important;
+}
+[hidden] {
+  display: none !important;
+}
+#upInput {
+  display: none;
+}
+#upInput2 {
+  display: none;
+}
+.pageTitle {
+  font-size: 20px;
+  font-weight: 700;
+  position: relative;
+  padding-left: 40px;
+  height: 50px;
+  line-height: 50px;
+  background-color: #fff;
+  border-radius: 10px;
+}
+.pageTitle::before {
+  position: absolute;
+  left: 20px;
+  top: 50%;
+  transform: translateY(-50%);
+  content: '';
+  width: 6px;
+  height: 20px;
+  background-color: var(--themeColor);
+}
+.mySorrl::-webkit-scrollbar {
+  /*滚动条整体样式*/
+  width: 5px;
+  /*高宽分别对应横竖滚动条的尺寸*/
+  height: 1px;
+}
+.mySorrl::-webkit-scrollbar-thumb {
+  /*滚动条里面小方块*/
+  border-radius: 10px;
+  -webkit-box-shadow: inset 0 0 5px transparent;
+  background: var(--themeColor);
+}
+.mySorrl::-webkit-scrollbar-track {
+  /*滚动条里面轨道*/
+  -webkit-box-shadow: inset 0 0 5px transparent;
+  border-radius: 10px;
+  background: transparent;
+}
+.myCoverBox {
+  pointer-events: none;
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  z-index: 10;
+  background-color: rgba(0, 0, 0, 0.6);
+  color: #fff;
+  height: 30px;
+  line-height: 30px;
+  text-align: center;
+}

+ 254 - 0
管理后台/src/assets/styles/base.less

@@ -0,0 +1,254 @@
+* {
+  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: black;
+}
+
+a {
+  text-decoration: none;
+  color: black;
+  outline: none;
+}
+
+i {
+  font-style: normal;
+}
+
+img {
+  max-width: 100%;
+  max-height: 100%;
+  vertical-align: middle;
+  object-fit: cover;
+}
+
+ul {
+  list-style: none;
+}
+
+body {
+  overflow: auto;
+  overflow-y: overlay;
+}
+
+/* 文本域取消下拉 */
+textarea {
+  resize: none !important;
+}
+
+/* 主题色 */
+:root {
+  --themeColor: #9d6b39;
+}
+
+
+
+
+
+/* 找不到页面 */
+.noFindPage {
+  opacity: 0;
+  transition: opacity .5s;
+}
+
+
+/* 兼容360浏览器的下拉框 */
+.ant-select-selector {
+  position: relative;
+  background-color: #ffffff;
+  border: 1px solid #d9d9d9;
+  transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
+}
+
+
+// 重置antd样式
+#root {
+  width: 100vw;
+  height: 100vh;
+  min-width: 1600px;
+  min-height: 900px;
+  overflow: auto;
+  overflow-y: overlay;
+
+  a {
+    color: var(--themeColor);
+  }
+
+  /* 普通文字按钮的颜色 */
+  .ant-btn-text {
+    color: #99b8dd;
+  }
+
+  .ant-btn-text:disabled {
+    cursor: not-allowed;
+    color: rgba(0, 0, 0, 0.25);
+  }
+
+  /* 按钮的危险颜色 */
+  .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;
+  }
+
+
+  /* 表格的图片居中 */
+  .tableImgAuto {
+    display: flex;
+    justify-content: center;
+  }
+
+  /* antd图片预览组件 */
+  .ant-image {
+    display: none;
+  }
+
+  /* antd表格居中 */
+
+  .ant-table-cell {
+    text-align: center !important;
+  }
+}
+
+
+
+[hidden] {
+  display: none !important;
+}
+
+
+
+#upInput {
+  display: none;
+}
+
+#upInput2 {
+  display: none;
+}
+
+// 页面标题
+.pageTitle{
+  font-size: 20px;
+  font-weight: 700;
+  position: relative;
+  padding-left: 40px;
+  height: 50px;
+  line-height: 50px;
+  background-color: #fff;
+  border-radius: 10px;
+  &::before {
+    position: absolute;
+    left: 20px;
+    top: 50%;
+    transform: translateY(-50%);
+    content: '';
+    width: 6px;
+    height: 20px;
+    background-color: var(--themeColor);
+  }
+}
+
+// 滚动条
+.mySorrl::-webkit-scrollbar {
+  /*滚动条整体样式*/
+  width: 5px;
+  /*高宽分别对应横竖滚动条的尺寸*/
+  height: 1px;
+}
+
+.mySorrl::-webkit-scrollbar-thumb {
+  /*滚动条里面小方块*/
+  border-radius: 10px;
+  -webkit-box-shadow: inset 0 0 5px transparent;
+  background: var(--themeColor);
+}
+
+.mySorrl::-webkit-scrollbar-track {
+  /*滚动条里面轨道*/
+  -webkit-box-shadow: inset 0 0 5px transparent;
+  border-radius: 10px;
+  background: transparent;
+}
+.myCoverBox{
+  pointer-events: none;
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  z-index: 10;
+  background-color: rgba(0, 0, 0, 0.6);
+  color: #fff;
+  height: 30px;
+  line-height: 30px;
+  text-align: center;
+}

+ 21 - 0
管理后台/src/components/AsyncSpinLoding/index.module.scss

@@ -0,0 +1,21 @@
+.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%);
+    }
+  }
+}

+ 15 - 0
管理后台/src/components/AsyncSpinLoding/index.tsx

@@ -0,0 +1,15 @@
+import styles from "./index.module.scss";
+import { Spin } from "antd";
+import React from "react";
+
+function AsyncSpinLoding() {
+  return (
+    <div id="AsyncSpinLoding" className={styles.AsyncSpinLoding}>
+      <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 { MessageFu } from "@/utils/message";
+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 {
+          MessageFu.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;
+        }
+      }
+    }
+  }
+
+}

+ 93 - 0
管理后台/src/components/ImageLazy/index.tsx

@@ -0,0 +1,93 @@
+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 store from "@/store";
+
+type Props = {
+  width?: number | string;
+  height?: number | string;
+  src: string;
+  noLook?: boolean;
+  offline?: boolean;
+};
+
+function ImageLazy({
+  width = 100,
+  height = 100,
+  src,
+  noLook,
+  offline = false,
+}: Props) {
+  // 图片占位符
+  const [placeholderUrl, setPlaceholderUrl] = useState(
+    src ? imgLoding : imgErr
+  );
+
+  // 默认不能预览图片,加载成功之后能预览
+  const [lookImg, setLookImg] = useState(false);
+
+  useEffect(() => {
+    if (src) {
+      // 进页面查看图片的加载情况
+      // 创建一个img标签
+      const imgDom = document.createElement("img");
+      imgDom.src = offline ? src : baseURL + src;
+
+      // 不管图片加载成功或者失败,都删除掉,提高性能
+      // 图片加载成功
+      imgDom.onload = function () {
+        setLookImg(true);
+        imgDom.remove();
+      };
+      // 图片加载失败
+      imgDom.onerror = function () {
+        setPlaceholderUrl(imgErr);
+        imgDom.remove();
+      };
+
+      return () => {
+        // 离开页面也删掉掉元素
+        imgDom.remove();
+      };
+    }
+  }, [offline, src]);
+
+  // 点击预览图片
+  const lookBigImg = useCallback(() => {
+    store.dispatch({
+      type: "layout/lookBigImg",
+      payload: { url: offline ? src : baseURL + src, show: true },
+    });
+  }, [offline, src]);
+
+  return (
+    <div className={styles.ImageLazy} style={{ width: width, height: height }}>
+      <div className="lazyBox">
+        <Lazyimg
+          src={src ? (offline ? 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;

+ 29 - 0
管理后台/src/components/Message/index.tsx

@@ -0,0 +1,29 @@
+import React, { useEffect } from "react";
+import { message } from "antd";
+import { useSelector } from "react-redux";
+import { RootState } from "@/store";
+
+function MessageCom() {
+  // 从仓库中获取 antd 轻提示信息
+  const messageReducerInfo = useSelector(
+    (state: RootState) => state.layoutStore.message
+  );
+
+  const [messageApi, contextHolder] = message.useMessage();
+
+  useEffect(() => {
+    if (messageReducerInfo.txt) {
+      messageApi.open({
+        type: messageReducerInfo.type,
+        content: messageReducerInfo.txt,
+        duration: messageReducerInfo.duration,
+      });
+    }
+  }, [messageApi, messageReducerInfo]);
+
+  return <>{contextHolder}</>;
+}
+
+const MemoMessage = React.memo(MessageCom);
+
+export default MemoMessage;

+ 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: HTMLDivElement = document.querySelector(".noFindPage")!;
+      dom.style.opacity = "1";
+    }, 300);
+    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;

+ 43 - 0
管理后台/src/components/UpAsyncLoding/index.module.scss

@@ -0,0 +1,43 @@
+.UpAsyncLoding {
+  opacity: 0;
+  pointer-events: none;
+  position: fixed;
+  z-index: 10000;
+  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);
+      }
+
+    }
+
+    .closeUpBtn {
+      position: absolute;
+      top: 70%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+    }
+
+  }
+}

+ 38 - 0
管理后台/src/components/UpAsyncLoding/index.tsx

@@ -0,0 +1,38 @@
+import store, { RootState } from "@/store";
+import { Button } from "antd";
+import React, { useCallback } from "react";
+import { useSelector } from "react-redux";
+import styles from "./index.module.scss";
+function UpAsyncLoding() {
+  // 从仓库中获取取消上传的函数
+  const closeUpFile = useSelector(
+    (state: RootState) => state.layoutStore.closeUpFile
+  );
+
+  const btnClose = useCallback(() => {
+    closeUpFile.fu();
+
+    setTimeout(() => {
+      store.dispatch({
+        type: "layout/closeUpFile",
+        payload: { fu: () => {}, state: false },
+      });
+    }, 200);
+  }, [closeUpFile]);
+
+  return (
+    <div id="UpAsyncLoding" className={styles.UpAsyncLoding}>
+      <div className="progressBox">
+        <div id="progress"></div>
+      </div>
+      {/* 手动取消上传按钮 */}
+      <div className="closeUpBtn">
+        <Button onClick={btnClose}>取消上传</Button>
+      </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 { baseURL } from "@/utils/http";
+import store from "@/store";
+
+type Props = {
+  width?: number;
+  height?: number;
+  src: string;
+};
+
+function VideoLook({ src, width = 100, height = 100 }: Props) {
+  return (
+    <div className={styles.VideoLook} style={{ width, height }}>
+      <div
+        className="videoLookBox"
+        onClick={() =>
+          store.dispatch({ type: "layout/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%;
+      }
+    }
+  }
+}

+ 36 - 0
管理后台/src/components/VideoLookDom/index.tsx

@@ -0,0 +1,36 @@
+import React from "react";
+import { CloseCircleOutlined } from "@ant-design/icons";
+import styles from "./index.module.scss";
+import { useSelector } from "react-redux";
+import store, { RootState } from "@/store";
+import { baseURL } from "@/utils/http";
+function VideoLookDom() {
+  const videoSrc = useSelector(
+    (state: RootState) => state.layoutStore.videoSrc
+  );
+  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={() =>
+          store.dispatch({ type: "layout/lookVideo", payload: "" })
+        }
+      >
+        <CloseCircleOutlined />
+      </div>
+    </div>
+  );
+}
+
+const MemoVideoLookDom = React.memo(VideoLookDom);
+
+export default MemoVideoLookDom;

+ 41 - 0
管理后台/src/index.tsx

@@ -0,0 +1,41 @@
+// 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";
+
+// 兼容360浏览器
+import {
+  StyleProvider,
+  legacyLogicalPropertiesTransformer,
+} from "@ant-design/cssinjs";
+
+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: "#9d6b39",
+      },
+    }}
+  >
+    <Provider store={store}>
+      <StyleProvider
+        hashPriority="high"
+        transformers={[legacyLogicalPropertiesTransformer]}
+      >
+        <App />
+      </StyleProvider>
+    </Provider>
+  </ConfigProvider>
+);

+ 116 - 0
管理后台/src/pages/A1Hot/index.module.scss

@@ -0,0 +1,116 @@
+.Hot {
+
+  :global {
+
+    .hotTopBox {
+      background-color: #fff;
+      border-radius: 10px;
+      height: 50px;
+      position: relative;
+
+      .hotTopTime {
+        position: absolute;
+        left: 180px;
+        top: 0;
+        padding: 10px 20px;
+        height: 50px;
+        display: flex;
+        align-items: center;
+      }
+    }
+
+    .hotMainBox {
+      width: 100%;
+      height: calc(100% - 70px);
+
+      .noneInfo {
+        width: 100%;
+        height: calc(100% - 32px);
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+
+
+      .hotMain {
+        margin-top: 20px;
+        width: 100%;
+        height: 100%;
+        display: flex;
+
+        .hotBoxTit {
+          font-size: 18px;
+          position: relative;
+
+          .hotBoxTitRight {
+            position: absolute;
+            right: 20px;
+            top: 50%;
+            transform: translateY(-50%);
+          }
+
+
+        }
+
+        &>div {
+          padding: 10px 20px;
+          height: 100%;
+          border-radius: 10px;
+          background-color: #fff;
+        }
+
+        .hotBox1 {
+          width: 300px;
+          position: relative;
+
+          #echarts1 {
+            width: 100%;
+            height: calc(100% - 30px);
+          }
+        }
+
+        .hotBox2 {
+          width: calc(50% - 160px);
+          margin: 0 10px;
+          padding: 10px 0 10px 20px;
+        }
+
+        .hotBox3 {
+          width: calc(50% - 160px);
+          padding: 10px 0 10px 20px;
+        }
+
+
+        .hotListBox {
+          width: 100%;
+          height: calc(100% - 50px);
+          margin-top: 20px;
+          overflow-y: auto;
+          overflow-y: overlay;
+
+          .row {
+            width: calc(100% - 20px);
+
+            height: 60px;
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            padding: 0 30px 0 15px;
+            border-radius: 5px;
+            border: 1px solid #dddddd;
+            margin-bottom: 15px;
+
+            .rowLeft {
+              width: calc(100% - 100px);
+              overflow: hidden;
+              text-overflow: ellipsis;
+              white-space: nowrap;
+            }
+          }
+        }
+
+      }
+    }
+
+  }
+}

+ 314 - 0
管理后台/src/pages/A1Hot/index.tsx

@@ -0,0 +1,314 @@
+import { Empty, Select } from "antd";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import styles from "./index.module.scss";
+
+import * as echarts from "echarts/core";
+import {
+  ToolboxComponent,
+  LegendComponent,
+  TooltipComponent,
+} from "echarts/components";
+import { PieChart } from "echarts/charts";
+import { LabelLayout } from "echarts/features";
+import { CanvasRenderer } from "echarts/renderers";
+import {
+  getHotExhibitionAPI,
+  getHotGoodsAPI,
+  getHotModuleAPI,
+} from "@/store/action/A1Hot";
+import { HotData1Type, HotSelectTimeType, HotSelectType } from "@/types";
+
+echarts.use([
+  ToolboxComponent,
+  TooltipComponent,
+  LegendComponent,
+  PieChart,
+  CanvasRenderer,
+  LabelLayout,
+]);
+
+function Hot() {
+  // 日期下拉框
+  const [select1, setSelect1] = useState<HotSelectTimeType>(7);
+  const select1Ref = useRef<HotSelectTimeType>(7);
+
+  // 热门场馆下拉
+  const [select2, setSelect2] = useState<HotSelectType>("visit");
+  const select2Ref = useRef<HotSelectType>("");
+
+  // 热门馆藏下拉
+  const [select3, setSelect3] = useState<HotSelectType>("visit");
+  const select3Ref = useRef<HotSelectType>("");
+
+  // 获取饼图的函数
+  const getEchartsFu = useCallback(
+    (
+      data: {
+        value: number;
+        name: string;
+      }[]
+    ) => {
+      const chartDom = document.getElementById("echarts1")!;
+      const myChart = echarts.init(chartDom);
+      const option = {
+        tooltip: {
+          trigger: "item",
+        },
+        legend: {
+          orient: "vertical",
+          left: "center",
+          bottom: "12%",
+          data,
+          // 设置间距
+          itemGap: 20,
+          formatter: (name: string) => {
+            let resName = "";
+            data.forEach((v) => {
+              if (name === v.name) {
+                if (name.length > 8)
+                  resName = name.slice(0, 8) + "... " + v.value;
+                else resName = name + " " + v.value;
+              }
+            });
+
+            return resName;
+          },
+        },
+        series: [
+          {
+            name: "",
+            type: "pie",
+            radius: [60, 100],
+            center: ["50%", "30%"],
+            // 设置圆角
+            itemStyle: {
+              borderRadius: 6,
+              borderColor: "#fff",
+              borderWidth: 1,
+            },
+            label: {
+              show: false,
+              position: "center",
+            },
+            emphasis: {
+              label: {
+                show: false,
+              },
+            },
+            labelLine: {
+              show: false,
+            },
+
+            data,
+          },
+        ],
+      };
+      option && myChart.setOption(option);
+    },
+    []
+  );
+
+  // 时间改变的时候 获取3个数据
+  const getInfoAPIFu1 = useCallback(async () => {
+    const res1 = await getHotModuleAPI(select1Ref.current);
+    const data1 = res1.data.map((v: HotData1Type) => {
+      return {
+        value: v.pcs,
+        name: v.groupKey,
+        icon: "circle",
+      };
+    });
+    // console.log("1获取饼图数据", select1Ref.current);
+    getEchartsFu(data1);
+
+    // -------------
+    // console.log(
+    //   "2获取场馆数据",
+    //   select1Ref.current,
+    //   select2Ref.current ? select2Ref.current : "visit"
+    // );
+
+    const res2 = await getHotExhibitionAPI(
+      select1Ref.current,
+      select2Ref.current ? select2Ref.current : "visit"
+    );
+    const data2: HotData1Type[] = res2.data;
+
+    setList1(data2);
+
+    // ---------------
+
+    // console.log(
+    //   "3获取馆藏数据",
+    //   select1Ref.current,
+    //   select3Ref.current ? select3Ref.current : "visit"
+    // );
+
+    const res3 = await getHotGoodsAPI(
+      select1Ref.current,
+      select3Ref.current ? select3Ref.current : "visit"
+    );
+    const data3: HotData1Type[] = res3.data;
+
+    setList2(data3);
+
+    // 每次变化馆藏盒子滚动到顶部
+    const dom: HTMLDivElement = document.querySelector("#goodsSroolBox")!;
+    if (dom) dom.scrollTop = 0;
+  }, [getEchartsFu]);
+
+  // 获取热门场馆数据
+  const [list1, setList1] = useState<HotData1Type[]>([]);
+
+  const getInfoAPIFu2 = useCallback(async () => {
+    // console.log("2获取场馆数据", select1Ref.current, select2Ref.current);
+    const res = await getHotExhibitionAPI(
+      select1Ref.current,
+      select2Ref.current ? select2Ref.current : "visit"
+    );
+    const data: HotData1Type[] = res.data;
+
+    setList1(data);
+  }, []);
+
+  // 获取馆藏数据
+
+  const [list2, setList2] = useState<HotData1Type[]>([]);
+
+  const getInfoAPIFu3 = useCallback(async () => {
+    // console.log("3获取馆藏数据", select1Ref.current, select3Ref.current);
+    const res = await getHotGoodsAPI(
+      select1Ref.current,
+      select3Ref.current ? select3Ref.current : "visit"
+    );
+    const data: HotData1Type[] = res.data;
+
+    setList2(data);
+  }, []);
+
+  // 改变时间选择发送3个请求
+  useEffect(() => {
+    select1Ref.current = select1;
+    getInfoAPIFu1();
+  }, [getInfoAPIFu1, select1]);
+
+  // 热门场馆改变
+  useEffect(() => {
+    // 第一次的时候不发送请求
+    if (!select2Ref.current) select2Ref.current = select2;
+    else {
+      select2Ref.current = select2;
+      getInfoAPIFu2();
+    }
+  }, [getInfoAPIFu2, select2]);
+
+  // 热门馆藏的改变
+  useEffect(() => {
+    // 第一次的时候不发送请求
+    if (!select3Ref.current) select3Ref.current = select3;
+    else {
+      select3Ref.current = select3;
+      getInfoAPIFu3();
+    }
+    // 每次变化滚动到顶部
+    const dom: HTMLDivElement = document.querySelector("#goodsSroolBox")!;
+    if (dom) dom.scrollTop = 0;
+  }, [getInfoAPIFu3, select3]);
+
+  return (
+    <div className={styles.Hot}>
+      <div className="hotTopBox">
+        <div className="pageTitle">热度统计</div>
+        <div className="hotTopTime">
+          <Select
+            value={select1}
+            style={{ width: 100 }}
+            onChange={(e) => setSelect1(e)}
+            options={[
+              { value: 1, label: "今日" },
+              { value: 7, label: "近七天" },
+              { value: 30, label: "近三十天" },
+              { value: "", label: "全部" },
+            ]}
+          />
+        </div>
+      </div>
+
+      <div className="hotMainBox">
+        <div className="hotMain">
+          <div className="hotBox1">
+            <div className="hotBoxTit">热门板块</div>
+            {/* 第一个echarts盒子 */}
+            <div id="echarts1"></div>
+          </div>
+          <div className="hotBox2">
+            <div className="hotBoxTit">
+              热门场馆
+              <div className="hotBoxTitRight">
+                <Select
+                  value={select2}
+                  style={{ width: 100 }}
+                  onChange={(e) => setSelect2(e)}
+                  options={[
+                    { value: "visit", label: "按浏览" },
+                    { value: "star", label: "按点赞" },
+                  ]}
+                />
+              </div>
+            </div>
+            {/* 场馆数据 */}
+            <div className="hotListBox mySorrl">
+              {list1.map((v) => (
+                <div className="row" key={v.groupKey}>
+                  <div className="rowLeft" title={v.groupKey}>
+                    {v.groupKey}
+                  </div>
+                  <div className="rowRight">{v.pcs}</div>
+                </div>
+              ))}
+            </div>
+          </div>
+          <div className="hotBox3">
+            <div className="hotBoxTit">
+              热门馆藏
+              <div className="hotBoxTitRight">
+                <Select
+                  value={select3}
+                  style={{ width: 100 }}
+                  onChange={(e) => setSelect3(e)}
+                  options={[
+                    { value: "visit", label: "按浏览" },
+                    { value: "star", label: "按点赞" },
+                  ]}
+                />
+              </div>
+            </div>
+            {/* 馆藏数据 */}
+            <div className="hotListBox mySorrl" id="goodsSroolBox">
+              {list2.length ? (
+                list2.slice(0, 10).map((v) => (
+                  <div className="row" key={v.groupKey}>
+                    <div className="rowLeft" title={v.groupKey}>
+                      {v.groupKey}
+                    </div>
+                    <div className="rowRight">{v.pcs}</div>
+                  </div>
+                ))
+              ) : (
+                <div className="noneInfo">
+                  <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
+                </div>
+              )}
+
+              {}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+const MemoHot = React.memo(Hot);
+
+export default MemoHot;

+ 181 - 0
管理后台/src/pages/A2News/NewsAdd/index.module.scss

@@ -0,0 +1,181 @@
+.NewsAdd {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  border-radius: 10px;
+  z-index: 20;
+
+  :global {
+    .formBox {
+      width: 1200px;
+      margin-top: 15px;
+      height: calc(100% - 75px);
+      padding-right: 400px;
+      overflow-y: auto;
+
+
+      // 多张附件图片上传
+
+
+      .myformBox {
+        display: flex;
+        margin-bottom: 10px;
+        margin-top: 40px;
+
+        .ant-btn-default {
+          width: 100px;
+        }
+
+        .label {
+          width: 100px;
+          text-align: right;
+
+          &>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);
+
+
+          }
+
+          .upImgBox {
+            display: flex;
+            flex-wrap: wrap;
+            max-width: 700px;
+
+            &>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;
+              }
+            }
+          }
+
+        }
+
+      }
+
+      // 视频和单个图片上传
+      .myformBox2 {
+        align-items: center;
+        height: 32px;
+
+        .label {
+          height: 32px;
+          line-height: 32px;
+        }
+
+        .fileTit {
+          margin-left: 20px;
+          font-size: 14px;
+          color: rgb(126, 124, 124);
+        }
+
+        .fileInfo {
+          height: 32px;
+          line-height: 32px;
+          display: flex;
+          font-size: 16px;
+
+          .clearCover {
+            margin-left: 20px;
+            cursor: pointer;
+            font-size: 18px;
+          }
+
+        }
+
+
+      }
+
+      .myformBox3{
+        margin-bottom: -20px;
+      }
+
+    }
+
+    .noUpThumb {
+      position: relative;
+      overflow: hidden;
+      opacity: 0;
+      transition: top .2s;
+      color: #ff4d4f;
+      top: -10px;
+    }
+
+    .noUpThumb2 {
+      position: relative;
+      padding-left: 100px;
+      margin-bottom: 12px;
+    }
+
+    .noUpThumbAc {
+      top: 0;
+      opacity: 1;
+    }
+
+  }
+}

+ 530 - 0
管理后台/src/pages/A2News/NewsAdd/index.tsx

@@ -0,0 +1,530 @@
+import {
+  getNewsDetailAPI,
+  newSaveAPI,
+  newsUploadAPI,
+} from "@/store/action/A2News";
+import { fileDomInitialFu } from "@/utils/domShow";
+import { MessageFu } from "@/utils/message";
+import {
+  Button,
+  DatePicker,
+  Form,
+  FormInstance,
+  Input,
+  Popconfirm,
+  Select,
+} from "antd";
+import TextArea from "antd/es/input/TextArea";
+import dayjs from "dayjs";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import styles from "./index.module.scss";
+import {
+  PlusOutlined,
+  CloseOutlined,
+  PlayCircleOutlined,
+  CloseCircleOutlined,
+  UploadOutlined,
+} from "@ant-design/icons";
+import ImageLazy from "@/components/ImageLazy";
+import classNames from "classnames";
+import store from "@/store";
+import { ImgListType, NewsSaveApiType } from "@/types";
+
+type Props = {
+  id: number;
+  tableType: "news" | "video" | "img";
+  closePageFu: () => void;
+  editTableFu: () => void;
+  addTableFu: () => void;
+};
+
+function NewsAdd({
+  id,
+  closePageFu,
+  editTableFu,
+  addTableFu,
+  tableType,
+}: Props) {
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<FormInstance>(null);
+
+  // 通过id获取详情,回显数据
+  const getInfoFu = useCallback(
+    async (id: number) => {
+      const res = await getNewsDetailAPI(id);
+      const data = res.data.entity;
+      const fileList = res.data.file;
+      setDirCode(data.dirCode);
+
+      FormBoxRef.current?.setFieldsValue({
+        ...data,
+        newsDay: dayjs(data.newsDay, "YYYY-MM-DD"),
+      });
+      if (tableType === "news") setImgList(fileList);
+      else setFileOne(fileList[0]);
+    },
+    [tableType]
+  );
+
+  useEffect(() => {
+    if (id > 0) getInfoFu(id);
+    else {
+      setDirCode(Date.now() + "");
+      FormBoxRef.current?.setFieldsValue({
+        display: 1,
+        // 默认新闻日期为当天
+        newsDay:
+          tableType === "news"
+            ? dayjs(dayjs(new Date()).format("YYYY-MM-DD"), "YYYY-MM-DD")
+            : "",
+      });
+    }
+  }, [getInfoFu, id, tableType]);
+
+  const myInput = useRef<HTMLInputElement>(null);
+
+  // 文件的dirCode码
+  const [dirCode, setDirCode] = useState("");
+
+  // 多张图片附件
+  const [imgList, setImgList] = useState<ImgListType[]>([]);
+
+  // 单个视频或者图片上传
+  const [fileOne, setFileOne] = useState<ImgListType>({
+    fileName: "",
+    filePath: "",
+    id: 0,
+  });
+
+  // 上传附件的处理函数
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0];
+
+        let anType = ["image/jpeg", "image/png"];
+        let anTit1 = "只支持png、jpg和jpeg格式!";
+        let anTit2 = "最大支持20M!";
+        let anSize = 20 * 1024 * 1024;
+
+        if (tableType === "video") {
+          anType = ["video/mp4"];
+          anTit1 = "只支持mp4格式!";
+          anTit2 = "最大支持500M!";
+          anSize = 500 * 1024 * 1024;
+        }
+
+        // 校验格式
+        if (!anType.includes(filesInfo.type)) {
+          e.target.value = "";
+          return MessageFu.warning(anTit1);
+        }
+
+        // 校验大小
+        if (filesInfo.size > anSize) {
+          e.target.value = "";
+          return MessageFu.warning(anTit2);
+        }
+        // 创建FormData对象
+        const fd = new FormData();
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append(
+          "type",
+          tableType === "video"
+            ? "video"
+            : tableType === "img"
+            ? "thumb"
+            : "img"
+        );
+        fd.append("dirCode", dirCode);
+        fd.append("file", filesInfo);
+
+        e.target.value = "";
+
+        try {
+          const res = await newsUploadAPI(fd);
+
+          if (res.code === 0) {
+            MessageFu.success("上传成功!");
+            if (tableType === "news") setImgList([...imgList, res.data]);
+            else setFileOne(res.data);
+          }
+          fileDomInitialFu();
+        } catch (error) {
+          fileDomInitialFu();
+        }
+      }
+    },
+    [dirCode, imgList, tableType]
+  );
+
+  // 附件图片的拖动
+  const [dragImg, setDragImg] = useState<any>(null);
+
+  const handleDragEnter = useCallback(
+    (e: React.DragEvent<HTMLDivElement>, item: ImgListType) => {
+      e.dataTransfer.effectAllowed = "move";
+      if (item === dragImg) return;
+      const newItems = [...imgList]; //拷贝一份数据进行交换操作。
+      const src = newItems.indexOf(dragImg); //获取数组下标
+      const dst = newItems.indexOf(item);
+      newItems.splice(dst, 0, ...newItems.splice(src, 1)); //交换位置
+      setImgList(newItems);
+    },
+    [dragImg, imgList]
+  );
+
+  // 删除某一张图片
+  const delImgListFu = useCallback(
+    (id: number) => {
+      const newItems = imgList.filter((v) => v.id !== id);
+      setImgList(newItems);
+    },
+    [imgList]
+  );
+
+  const [typeOk, setTypeOk] = useState(false);
+
+  // 附件信息的校验
+  const fileCheckFu = useMemo(() => {
+    let flag = false;
+    if (tableType !== "news" && !fileOne.filePath) flag = true;
+    if (tableType === "news") flag = false;
+    return flag;
+  }, [fileOne.filePath, tableType]);
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    setTypeOk(true);
+    // return MessageFu.warning("有表单不符号规则!");
+  }, []);
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (values: NewsSaveApiType) => {
+      setTypeOk(true);
+
+      if (fileCheckFu) return;
+
+      let thumb = "";
+      if (tableType === "news") thumb = imgList[0] ? imgList[0].filePath : "";
+      else thumb = fileOne.filePath;
+      const obj = {
+        ...values,
+        id: id > 0 ? id : null,
+        dirCode,
+        newsDay: values.newsDay
+          ? dayjs(values.newsDay).format("YYYY-MM-DD")
+          : "",
+        thumb,
+        fileIds:
+          tableType === "news"
+            ? imgList.map((v) => v.id).join(",")
+            : fileOne.id + "",
+        type: tableType,
+      };
+      const res = await newSaveAPI(obj);
+      if (res.code === 0) {
+        if (id > 0) editTableFu();
+        else addTableFu();
+        MessageFu.success(id > 0 ? "编辑成功!" : "新增成功!");
+        closePageFu();
+      }
+    },
+    [
+      addTableFu,
+      closePageFu,
+      dirCode,
+      editTableFu,
+      fileCheckFu,
+      fileOne,
+      id,
+      imgList,
+      tableType,
+    ]
+  );
+
+  return (
+    <div className={styles.NewsAdd}>
+      <div className="pageTitle">{id > 0 ? "编辑资讯" : "新增资讯"}</div>
+      <div className="formBox mySorrl">
+        <Form
+          ref={FormBoxRef}
+          name="basic"
+          labelCol={{ span: 3 }}
+          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={20} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <input
+            id="upInput2"
+            type="file"
+            accept={tableType === "video" ? ".mp4" : ".png,.jpg,.jpeg"}
+            ref={myInput}
+            onChange={(e) => handeUpPhoto(e)}
+          />
+
+          {tableType === "news" ? (
+            <>
+              <Form.Item
+                label="新闻日期"
+                name="newsDay"
+                rules={[{ required: true, message: "请选择新闻日期!" }]}
+              >
+                <DatePicker />
+              </Form.Item>
+              <Form.Item
+                label="正文"
+                name="description"
+                rules={[
+                  { required: true, message: "请输入正文!" },
+                  {
+                    validator: (rule, value) => {
+                      if (value) {
+                        const txt = value
+                          .replaceAll(" ", "")
+                          .replaceAll("\n", "");
+                        return txt === ""
+                          ? Promise.reject("请输入有效字符!")
+                          : Promise.resolve();
+                      } else return Promise.resolve();
+                    },
+                  },
+                ]}
+                // getValueFromEvent={(e) => e.target.value.trim()}
+              >
+                <TextArea
+                  rows={8}
+                  placeholder="请输入内容"
+                  showCount
+                  maxLength={1000}
+                />
+              </Form.Item>
+
+              {/* 上传附件图片 */}
+              <div className="myformBox">
+                <div className="label">图片:</div>
+                <>
+                  <div className="fileBoxRow_r">
+                    <div className="upImgBox">
+                      <div
+                        hidden={imgList.length >= 9}
+                        className="fileBoxRow_up"
+                        onClick={() => myInput.current?.click()}
+                      >
+                        <PlusOutlined />
+                      </div>
+                      {imgList.map((v, i) => (
+                        <div
+                          className="fileBoxRow_r_img"
+                          key={v.id}
+                          draggable="true"
+                          onDragStart={() => setDragImg(v)}
+                          onDragOver={(e) => {
+                            e.preventDefault();
+                            e.dataTransfer.dropEffect = "move";
+                          }}
+                          onDragEnter={(e) => handleDragEnter(e, v)}
+                          onDragEnd={() => setDragImg(null)}
+                        >
+                          {i === 0 ? (
+                            <div className="myCoverBox">封面</div>
+                          ) : null}
+                          {v.filePath ? (
+                            <ImageLazy
+                              noLook={dragImg ? true : false}
+                              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="fileTit">
+                      {imgList.length >= 2 ? (
+                        <>
+                          按住鼠标可拖动图片调整顺序,第一张为展示端封面。
+                          <br />
+                        </>
+                      ) : null}
+                      支持png、jpg和jpeg的图片格式;最大支持20M;最多支持9张。
+                    </div>
+                  </div>
+                </>
+              </div>
+            </>
+          ) : tableType === "video" ? (
+            // 上传视频
+            <>
+              {/* -----------视频上传 */}
+              <div className="myformBox myformBox2">
+                <div className="label">
+                  <span>*</span> 视频:
+                </div>
+                {fileOne.id ? (
+                  <div className="fileInfo">
+                    <div className="upSuccTxt">{fileOne.fileName}</div>
+                    <div
+                      className="clearCover"
+                      hidden={!fileOne.filePath}
+                      onClick={() =>
+                        store.dispatch({
+                          type: "layout/lookVideo",
+                          payload: fileOne.filePath!,
+                        })
+                      }
+                    >
+                      <PlayCircleOutlined />
+                    </div>
+
+                    <Popconfirm
+                      title="删除后无法恢复,是否删除?"
+                      okText="删除"
+                      cancelText="取消"
+                      onConfirm={() =>
+                        setFileOne({ fileName: "", filePath: "", id: 0 })
+                      }
+                    >
+                      <div className="clearCover">
+                        <CloseCircleOutlined />
+                      </div>
+                    </Popconfirm>
+                  </div>
+                ) : (
+                  <>
+                    <Button
+                      onClick={() => myInput.current?.click()}
+                      icon={<UploadOutlined />}
+                    >
+                      上传
+                    </Button>
+
+                    <div className="fileTit">
+                      仅支持MP4格式的视频文件,大小不得超过500MB。
+                    </div>
+                  </>
+                )}
+              </div>
+            </>
+          ) : (
+            <>
+              {/* 上传单张图片 */}
+              <div className="myformBox myformBox3">
+                <div className="label">
+                  <span>*</span> 图片:
+                </div>
+                <div className="fileBoxRow_r">
+                  <div
+                    hidden={!!fileOne.filePath}
+                    className="fileBoxRow_up"
+                    onClick={() => myInput.current?.click()}
+                  >
+                    <PlusOutlined />
+                  </div>
+                  <div className="fileBoxRow_r_img" hidden={!fileOne.filePath}>
+                    {fileOne.filePath ? (
+                      <ImageLazy
+                        width={100}
+                        height={100}
+                        src={fileOne.filePath}
+                      />
+                    ) : null}
+
+                    <Popconfirm
+                      title="删除后无法恢复,是否删除?"
+                      okText="删除"
+                      cancelText="取消"
+                      onConfirm={() =>
+                        setFileOne({ fileName: "", filePath: "", id: 0 })
+                      }
+                    >
+                      <div className="clearCover">
+                        <CloseCircleOutlined />
+                      </div>
+                    </Popconfirm>
+                  </div>
+                  <div className="fileBoxRow_r_tit">
+                    支持png、jpg和jpeg的图片格式;最大支持20M。
+                  </div>
+                </div>
+              </div>
+            </>
+          )}
+
+          <div
+            className={classNames(
+              "noUpThumb noUpThumb2",
+              fileCheckFu && typeOk ? "noUpThumbAc" : ""
+            )}
+          >
+            请上传附件!
+          </div>
+
+          <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={closePageFu}
+            >
+              <Button>取消</Button>
+            </Popconfirm>
+          </Form.Item>
+        </Form>
+      </div>
+    </div>
+  );
+}
+
+const MemoNewsAdd = React.memo(NewsAdd);
+
+export default MemoNewsAdd;

+ 49 - 0
管理后台/src/pages/A2News/NewsTable/index.module.scss

@@ -0,0 +1,49 @@
+.NewsTable {
+  border-radius: 10px;
+  overflow: hidden;
+  margin-top: 20px;
+  height: calc(100% - 130px);
+  background-color: #fff;
+
+  :global {
+    .ant-table-body {
+      height: 615px;
+      overflow-y: auto !important;
+      overflow-y: overlay !important;
+
+      .ant-table-row {
+        .ant-table-cell {
+          padding: 8px;
+        }
+      }
+    }
+
+    .incoTitle {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+      .hotTitleInco1 {
+        cursor: pointer;
+        width: 16px;
+        height: 16px;
+        border-radius: 50%;
+        background-color: #696969;
+        margin-left: 8px;
+        text-align: center;
+        line-height: 16px;
+        color: #fff;
+        font-size: 12px;
+      }
+    }
+
+    // 表头拖拽样式
+    .drop-over-downward td {
+      border-bottom: 2px dashed var(--themeColor) !important;
+    }
+
+    .drop-over-upward td {
+      border-top: 2px dashed var(--themeColor) !important;
+    }
+  }
+}

+ 245 - 0
管理后台/src/pages/A2News/NewsTable/index.tsx

@@ -0,0 +1,245 @@
+import ImageLazy from "@/components/ImageLazy";
+import VideoLook from "@/components/VideoLook";
+import { RootState } from "@/store";
+import { NewsTableType } from "@/types";
+import { Button, Popconfirm, Table, Tooltip } from "antd";
+import React, { useCallback, useEffect, useMemo, useRef } from "react";
+import { useSelector } from "react-redux";
+import { ExclamationOutlined } from "@ant-design/icons";
+import styles from "./index.module.scss";
+
+// 表格拖动排序-----------------
+import { DndProvider, useDrag, useDrop } from "react-dnd";
+import { HTML5Backend } from "react-dnd-html5-backend";
+import { newRemoveAPI, newSortApi } from "@/store/action/A2News";
+import { MessageFu } from "@/utils/message";
+
+type Porps = {
+  tableType: string;
+  editInfoFu: (id: number) => void;
+  delInfoFu: () => void;
+};
+
+function NewsTable({ tableType, editInfoFu, delInfoFu }: Porps) {
+  // 有关表格数据
+  const { tableInfo: results, sroolNum } = useSelector(
+    (state: RootState) => state.newsReducer
+  );
+
+  useEffect(() => {
+    if (sroolNum > 0) {
+      const dom: any = document.querySelector("#NewsTable .ant-table-body");
+      dom.scrollTop = sroolNum;
+    }
+  }, [sroolNum]);
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      const res = await newRemoveAPI(id);
+      if (res.code === 0) {
+        MessageFu.success("删除成功!");
+        delInfoFu();
+      }
+    },
+    [delInfoFu]
+  );
+
+  const columns = useMemo(() => {
+    return [
+      {
+        title: () => (
+          <div className="incoTitle">
+            序号
+            {results.length >= 2 ? (
+              <Tooltip title="按住鼠标可拖动表格调整顺序">
+                <div className="hotTitleInco1">
+                  <ExclamationOutlined />
+                </div>
+              </Tooltip>
+            ) : null}
+          </div>
+        ),
+        width: 100,
+        render: (text: any, item: any, index: number) => index + 1,
+      },
+      {
+        title: "标题",
+        dataIndex: "name",
+      },
+      {
+        title:
+          tableType === "news"
+            ? "正文"
+            : tableType === "video"
+            ? "视频"
+            : "图片",
+        render: (item: NewsTableType) => {
+          if (tableType === "news") {
+            return item.description.length > 35 ? (
+              <span style={{ cursor: "pointer" }} title={item.description}>
+                {item.description.substring(0, 35) + "..."}
+              </span>
+            ) : (
+              item.description
+            );
+          } else if (tableType === "video") {
+            return (
+              <div className="tableImgAuto">
+                {item.thumb ? (
+                  <VideoLook src={item.thumb} width={60} height={60} />
+                ) : null}
+              </div>
+            );
+          } else {
+            return (
+              <div className="tableImgAuto">
+                {item.thumb ? (
+                  <ImageLazy width={60} height={60} src={item.thumb} />
+                ) : null}
+              </div>
+            );
+          }
+        },
+      },
+      {
+        title: "最近编辑时间",
+        dataIndex: "updateTime",
+      },
+      {
+        title: "展示状态",
+        render: (item: NewsTableType) =>
+          item.display === 1 ? "展示" : "不展示",
+      },
+      {
+        title: "操作",
+        render: (item: NewsTableType) => (
+          <>
+            <Button
+              size="small"
+              type="text"
+              onClick={() => editInfoFu(item.id)}
+            >
+              编辑
+            </Button>
+            <Popconfirm
+              title="删除后无法恢复,是否删除?"
+              okText="删除"
+              cancelText="取消"
+              onConfirm={() => delTableFu(item.id!)}
+            >
+              <Button size="small" type="text" danger>
+                删除
+              </Button>
+            </Popconfirm>
+          </>
+        ),
+      },
+    ];
+  }, [delTableFu, editInfoFu, results.length, tableType]);
+
+  // 表格拖动排序-----------------
+  interface DraggableBodyRowProps
+    extends React.HTMLAttributes<HTMLTableRowElement> {
+    index: number;
+    moveRow: (dragIndex: number, hoverIndex: number) => void;
+  }
+
+  const type = "DraggableBodyRow";
+
+  const DraggableBodyRow = useCallback(
+    ({
+      index,
+      moveRow,
+      className,
+      style,
+      ...restProps
+    }: DraggableBodyRowProps) => {
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const ref = useRef<HTMLTableRowElement>(null);
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const [{ isOver, dropClassName }, drop] = useDrop({
+        accept: type,
+        collect: (monitor) => {
+          const { index: dragIndex } = monitor.getItem() || {};
+          if (dragIndex === index) {
+            return {};
+          }
+          return {
+            isOver: monitor.isOver(),
+            dropClassName:
+              dragIndex < index ? " drop-over-downward" : " drop-over-upward",
+          };
+        },
+        drop: (item: { index: number }) => {
+          moveRow(item.index, index);
+        },
+      });
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const [, drag] = useDrag({
+        type,
+        item: { index },
+        collect: (monitor) => ({
+          isDragging: monitor.isDragging(),
+        }),
+      });
+      drop(drag(ref));
+
+      return (
+        <tr
+          ref={ref}
+          className={`${className}${isOver ? dropClassName : ""}`}
+          style={{ cursor: "move", ...style }}
+          {...restProps}
+        />
+      );
+    },
+    []
+  );
+
+  const components = {
+    body: {
+      row: DraggableBodyRow,
+    },
+  };
+
+  const moveRow = useCallback(
+    async (dragIndex: number, hoverIndex: number) => {
+      if (dragIndex === hoverIndex) return;
+      // 交互位置-之前的id
+      const beforeId = results[dragIndex].id;
+      const afterId = results[hoverIndex].id;
+
+      const res = await newSortApi(beforeId, afterId);
+
+      if (res.code === 0) delInfoFu();
+    },
+    [delInfoFu, results]
+  );
+
+  return (
+    <div className={styles.NewsTable} id="NewsTable">
+      <DndProvider backend={HTML5Backend}>
+        <Table
+          scroll={{ y: 615 }}
+          dataSource={results}
+          columns={columns}
+          rowKey="id"
+          pagination={false}
+          components={components}
+          onRow={(_, index) => {
+            const attr = {
+              index,
+              moveRow,
+            };
+            return attr as React.HTMLAttributes<any>;
+          }}
+        />
+      </DndProvider>
+    </div>
+  );
+}
+
+const MemoNewsTable = React.memo(NewsTable);
+
+export default MemoNewsTable;

+ 20 - 0
管理后台/src/pages/A2News/index.module.scss

@@ -0,0 +1,20 @@
+.News {
+  position: relative;
+  :global {
+    .newTop {
+      background-color: #fff;
+      border-radius: 10px;
+
+      .searchBox {
+        position: relative;
+        padding: 10px 20px 20px;
+        display: flex;
+        align-items: center;
+
+        .row {
+          margin-right: 50px;
+        }
+      }
+    }
+  }
+}

+ 181 - 0
管理后台/src/pages/A2News/index.tsx

@@ -0,0 +1,181 @@
+import store, { RootState } from "@/store";
+import { getNewsListAPI } from "@/store/action/A2News";
+import { NewsButtonType, NewsTableApiType } from "@/types";
+import { MessageFu } from "@/utils/message";
+import { Button, Input, Select } from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useDispatch, useSelector } from "react-redux";
+import styles from "./index.module.scss";
+import NewsAdd from "./NewsAdd";
+import NewsTable from "./NewsTable";
+function News() {
+  const dispatch = useDispatch();
+
+  // 顶部筛选的数据
+  const [tableSelect, setTableSelect] = useState<NewsTableApiType>({
+    type: "news",
+    searchKey: "",
+    display: -1,
+    pageSize: 50,
+    pageNum: 1,
+  });
+
+  const buttonData = useMemo<NewsButtonType[]>(() => {
+    return [
+      { id: 1, name: "新闻", type: "news" },
+      { id: 2, name: "视频", type: "video" },
+      { id: 3, name: "图片", type: "img" },
+    ];
+  }, []);
+
+  const getTableInfoFu = useCallback(() => {
+    const obj = {
+      ...tableSelect,
+      display: tableSelect.display === -1 ? null : tableSelect.display,
+    };
+    dispatch(getNewsListAPI(obj));
+  }, [dispatch, tableSelect]);
+
+  useEffect(() => {
+    getTableInfoFu();
+  }, [getTableInfoFu]);
+
+  // 名称的输入
+  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(() => {
+    // 新增成功把表格滚动到顶部
+    store.dispatch({ type: "news/setSroolNum", payload: 0 });
+
+    // 把2个输入框和时间选择器清空
+    setInputKey(Date.now());
+    setTableSelect({
+      type: tableSelect.type,
+      searchKey: "",
+      display: -1,
+      pageSize: 50,
+      pageNum: 1,
+    });
+  }, [tableSelect.type]);
+
+  // 新增或者编辑的id
+  const [editId, setEditId] = useState(0);
+
+  // 有关表格数据
+  const results = useSelector(
+    (state: RootState) => state.newsReducer.tableInfo
+  );
+
+  // 点击新增或者编辑
+  const addInfoFu = useCallback(
+    (id: number) => {
+      if (id === -1 && results.length >= 50)
+        return MessageFu.warning("最多可录入50条信息!");
+
+      const dom: HTMLDivElement = document.querySelector(
+        "#NewsTable .ant-table-body"
+      )!;
+      // 设置当前表格滚动位置
+      store.dispatch({ type: "news/setSroolNum", payload: dom.scrollTop });
+
+      setEditId(id);
+    },
+    [results.length]
+  );
+
+  return (
+    <div className={styles.News}>
+      <div className="newTop">
+        <div className="pageTitle">资讯管理</div>
+        <div className="searchBox">
+          <div className="row">
+            {buttonData.map((v) => (
+              <Button
+                key={v.id}
+                onClick={() => setTableSelect({ ...tableSelect, type: v.type })}
+                type={tableSelect.type === v.type ? "primary" : "default"}
+              >
+                {v.name}
+              </Button>
+            ))}
+          </div>
+          <div className="row">
+            <span>标题或正文:</span>
+            <Input
+              key={inputKey}
+              maxLength={20}
+              style={{ width: 200 }}
+              placeholder="请输入关键字"
+              allowClear
+              onChange={(e) => nameChange(e)}
+            />
+          </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="row">
+            <Button onClick={resetSelectFu}>重置</Button>
+            &emsp;&emsp;&emsp;&emsp;
+            <Button type="primary" onClick={() => addInfoFu(-1)}>
+              新增
+            </Button>
+          </div>
+        </div>
+      </div>
+      {/* 新增和编辑 和 表格主体 (拖动有冲突,所以只能同时渲染一个)*/}
+      {editId ? (
+        <NewsAdd
+          id={editId}
+          tableType={tableSelect.type}
+          closePageFu={() => setEditId(0)}
+          editTableFu={getTableInfoFu}
+          addTableFu={resetSelectFu}
+        />
+      ) : (
+        <NewsTable
+          tableType={tableSelect.type}
+          editInfoFu={(id) => addInfoFu(id)}
+          delInfoFu={getTableInfoFu}
+        />
+      )}
+    </div>
+  );
+}
+
+const MemoNews = React.memo(News);
+
+export default MemoNews;

+ 198 - 0
管理后台/src/pages/A3Goods/GoodsAdd/index.module.scss

@@ -0,0 +1,198 @@
+.GoodsAdd {
+  position: absolute;
+  z-index: 20;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+
+  border-radius: 10px;
+  background-color: #fff;
+
+  :global {
+    .main {
+      margin-top: 5px;
+      width: 1200px;
+      padding-right: 400px;
+      padding-left:15px;
+      height: calc(100% - 66px);
+      overflow-y: auto;
+      overflow-y: overlay;
+      .myformBox0 {
+        margin-top: 0px;
+      }
+
+      .myformBox {
+        .ant-btn-default {
+          width: 100px;
+        }
+
+        display: flex;
+        margin-bottom: 20px;
+
+        .label {
+          width: 100px;
+          text-align: right;
+
+          &>span {
+            position: relative;
+            top: 2px;
+            color: #ff4d4f;
+          }
+        }
+
+      }
+
+      .myformBox2 {
+        align-items: center;
+        height: 32px;
+
+        .label {
+          height: 32px;
+          line-height: 32px;
+        }
+
+        .fileTit {
+          margin-left: 20px;
+          font-size: 14px;
+          color: rgb(126, 124, 124);
+        }
+
+        .fileInfo {
+          height: 32px;
+          line-height: 32px;
+          display: flex;
+          font-size: 16px;
+
+          .clearCover {
+            margin-left: 20px;
+            cursor: pointer;
+            font-size: 18px;
+          }
+
+        }
+
+
+      }
+
+      .myformBox3 {
+        margin-top: 20px;
+
+        .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;
+            }
+          }
+        }
+
+        .fileTit {
+          font-size: 14px;
+          color: rgb(126, 124, 124);
+        }
+      }
+
+      .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 {
+        position: relative;
+        overflow: hidden;
+        opacity: 0;
+        transition: top .2s;
+        color: #ff4d4f;
+        top: -10px;
+      }
+
+      .noUpThumb2 {
+        margin-top: -10px;
+        position: relative;
+        padding-left: 100px;
+        margin-bottom: 12px;
+      }
+
+      .noUpThumbAc {
+        top: 0;
+        opacity: 1;
+      }
+
+      .myformBox {
+        .laseFormRight {
+          .laseFormRightRow {
+            display: flex;
+            align-items: center;
+          }
+
+          .laseFormRightTit {
+            font-size: 14px;
+            color: rgb(126, 124, 124);
+          }
+        }
+      }
+    }
+  }
+}

+ 745 - 0
管理后台/src/pages/A3Goods/GoodsAdd/index.tsx

@@ -0,0 +1,745 @@
+import store, { RootState } from "@/store";
+import {
+  Button,
+  Checkbox,
+  Form,
+  FormInstance,
+  Input,
+  Popconfirm,
+  Select,
+} from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useSelector } from "react-redux";
+import TextArea from "antd/es/input/TextArea";
+import styles from "./index.module.scss";
+import { MessageFu } from "@/utils/message";
+import ImageLazy from "@/components/ImageLazy";
+import classNames from "classnames";
+import {
+  PlusOutlined,
+  CloseCircleOutlined,
+  UploadOutlined,
+  CloseOutlined,
+  PlayCircleOutlined,
+} from "@ant-design/icons";
+import {
+  getGoodsInfoByIdAPI,
+  getGoodsSaveAPI,
+  goodsUploadAPI,
+} from "@/store/action/A3Goods";
+import { baseURL } from "@/utils/http";
+import { FileListType, GoodsTableType } from "@/types";
+import { fileDomInitialFu } from "@/utils/domShow";
+
+type Props = {
+  id: number;
+  closeMoalFu: () => void;
+  addListFu: () => void;
+  editListFu: () => void;
+};
+
+function GoodsAdd({ id, closeMoalFu, addListFu, editListFu }: Props) {
+  // 上传附件的信息
+  const [fileList, setFileList] = useState({
+    model: {} as FileListType,
+    img: [] as FileListType[],
+    audio: {} as FileListType,
+    video: {} as FileListType,
+  });
+
+  // 表单的ref
+  const FormBoxRef = useRef<FormInstance>(null);
+
+  // 文件的dirCode码
+  const [dirCode, setDirCode] = useState("");
+
+  const getInfoInAPIFu = useCallback(async (id: number) => {
+    const res = await getGoodsInfoByIdAPI(id);
+    FormBoxRef.current?.setFieldsValue(res.data.entity);
+    setCover(res.data.entity.thumb);
+
+    if (res.data.entity.type) setTypeCheck(res.data.entity.type.split(","));
+
+    const data: FileListType[] = res.data.file;
+    const obj = {
+      model: {} as FileListType,
+      img: [] as FileListType[],
+      audio: {} as FileListType,
+      video: {} as FileListType,
+    };
+
+    data.forEach((v) => {
+      if (v.type === "img") obj.img.push(v);
+      else obj[v.type!] = v;
+    });
+    setFileList(obj);
+    setDirCode(res.data.entity.dirCode);
+  }, []);
+
+  useEffect(() => {
+    if (id > 0) getInfoInAPIFu(id);
+    else {
+      setDirCode(Date.now() + "");
+      FormBoxRef.current?.setFieldsValue({
+        display: 1,
+      });
+    }
+  }, [getInfoInAPIFu, id]);
+
+  // 从仓库获取下拉列表数据
+  const dictList = useSelector(
+    (state: RootState) => state.layoutStore.dictList
+  );
+
+  // 上传封面图的ref
+  const [coverCheck, setCoverCheck] = useState(false);
+  const [cover, setCover] = useState("");
+  const myInput = useRef<HTMLInputElement>(null);
+
+  // 上传封面图
+  const handeUpPhoto = useCallback(
+    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 MessageFu.warning("只支持jpg、png格式!");
+        }
+        // 校验大小
+        if (filesInfo.size > 20 * 1024 * 1024) {
+          e.target.value = "";
+          return MessageFu.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 = "";
+
+        try {
+          const res = await goodsUploadAPI(fd);
+          if (res.code === 0) {
+            MessageFu.success("上传成功!");
+            setCover(res.data.filePath);
+          }
+          fileDomInitialFu();
+        } catch (error) {
+          fileDomInitialFu();
+        }
+      }
+    },
+    [dirCode]
+  );
+
+  // 选中附件类型
+  const [typeCheck, setTypeCheck] = useState<any>([]);
+  const [typeOk, setTypeOk] = useState(false);
+
+  const typeCheckArr = useMemo(() => {
+    return [
+      { label: "模型", value: "model" },
+      { label: "图片", value: "img" },
+      { label: "音频", value: "audio" },
+      { label: "视频", value: "video" },
+    ];
+  }, []);
+
+  // 附件信息的校验
+
+  const fileCheckFu = useMemo(() => {
+    let flag = false;
+    if (typeCheck.length === 0) flag = true;
+    if (typeCheck.includes("model") && !fileList.model.id) flag = true;
+    if (typeCheck.includes("img") && fileList.img.length === 0) flag = true;
+    if (typeCheck.includes("audio") && !fileList.audio.id) flag = true;
+    if (typeCheck.includes("video") && !fileList.video.id) flag = true;
+    return flag;
+  }, [fileList, typeCheck]);
+
+  // 点击上传附件按钮
+  const myInput2 = useRef<HTMLInputElement>(null);
+
+  const [fileOneType, setFileOneType] = useState("");
+
+  useEffect(() => {
+    if (fileOneType) myInput2.current?.click();
+  }, [fileOneType]);
+
+  const upFileFu = useCallback((type: string) => {
+    setFileOneType("");
+    window.setTimeout(() => {
+      setFileOneType(type);
+    }, 100);
+  }, []);
+
+  // 上传附件的处理函数
+  const handeUpPhoto2 = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0];
+
+        let anType = ["image/jpeg", "image/png", "image/gif"];
+        let anTit1 = "只支持png、jpg、gif和jpeg格式!";
+        let anTit2 = "最大支持20M!";
+        let anSize = 20 * 1024 * 1024;
+
+        if (fileOneType === "audio") {
+          anType = ["audio/mpeg"];
+          anTit1 = "只支持mp3格式!";
+          anTit2 = "最大支持10M!";
+          anSize = 10 * 1024 * 1024;
+        } else if (fileOneType === "video") {
+          anType = ["video/mp4"];
+          anTit1 = "只支持mp4格式!";
+          anTit2 = "最大支持500M!";
+          anSize = 500 * 1024 * 1024;
+        } else if (fileOneType === "model") {
+          anType = [""];
+          anTit1 = "只支持4dage格式!";
+          anTit2 = "最大支持500M!";
+          anSize = 500 * 1024 * 1024;
+        }
+
+        // 校验格式
+        if (fileOneType !== "model") {
+          if (!anType.includes(filesInfo.type)) {
+            e.target.value = "";
+            return MessageFu.warning(anTit1);
+          }
+        } else {
+          if (!filesInfo.name.includes(".4dage")) {
+            e.target.value = "";
+            return MessageFu.warning(anTit1);
+          }
+        }
+
+        // 校验大小
+        if (filesInfo.size > anSize) {
+          e.target.value = "";
+          return MessageFu.warning(anTit2);
+        }
+        // 创建FormData对象
+        const fd = new FormData();
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append("type", fileOneType);
+        fd.append("dirCode", dirCode);
+        fd.append("file", filesInfo);
+
+        e.target.value = "";
+
+        try {
+          const res = await goodsUploadAPI(fd);
+
+          if (res.code === 0) {
+            MessageFu.success("上传成功!");
+            if (fileOneType === "img")
+              setFileList({ ...fileList, img: [...fileList.img, res.data] });
+            else setFileList({ ...fileList, [fileOneType]: res.data });
+          }
+          fileDomInitialFu();
+        } catch (error) {
+          fileDomInitialFu();
+        }
+      }
+    },
+    [dirCode, fileList, fileOneType]
+  );
+
+  // 附件图片的拖动
+  const [dragImg, setDragImg] = useState<any>(null);
+
+  const handleDragEnter = useCallback(
+    (e: React.DragEvent<HTMLDivElement>, item: FileListType) => {
+      e.dataTransfer.effectAllowed = "move";
+      if (item === dragImg) return;
+      const newItems = [...fileList.img]; //拷贝一份数据进行交换操作。
+      const src = newItems.indexOf(dragImg); //获取数组下标
+      const dst = newItems.indexOf(item);
+      newItems.splice(dst, 0, ...newItems.splice(src, 1)); //交换位置
+      setFileList({ ...fileList, img: newItems });
+    },
+    [dragImg, fileList]
+  );
+
+  // 删除某一张图片
+  const delImgListFu = useCallback(
+    (id: number) => {
+      const newItems = fileList.img.filter((v) => v.id !== id);
+      setFileList({ ...fileList, img: newItems });
+    },
+    [fileList]
+  );
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    setCoverCheck(true);
+    setTypeOk(true);
+    return MessageFu.warning("有表单不符号规则!");
+  }, []);
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (value: GoodsTableType) => {
+      console.log("通过校验,点击确定");
+      setCoverCheck(true);
+      setTypeOk(true);
+      if (typeCheck.length === 0 || cover === "" || fileCheckFu)
+        return MessageFu.warning("有表单不符号规则!");
+
+      const fileIds = [];
+      if (fileList.model.id && typeCheck.includes("model"))
+        fileIds.push(fileList.model.id);
+      if (fileList.audio.id && typeCheck.includes("audio"))
+        fileIds.push(fileList.audio.id);
+      if (fileList.video.id && typeCheck.includes("video"))
+        fileIds.push(fileList.video.id);
+      if (typeCheck.includes("img")) {
+        fileList.img.forEach((v) => {
+          if (v.id) fileIds.push(v.id);
+        });
+      }
+
+      const obj = {
+        ...value,
+        id: id > 0 ? id : null,
+        dirCode,
+        fileIds: fileIds.join(","),
+        thumb: cover,
+        type: typeCheck.join(","),
+      } as GoodsTableType;
+      const res = await getGoodsSaveAPI(obj);
+      if (res.code === 0) {
+        MessageFu.success(id > 0 ? "编辑成功!" : "新增成功!");
+        closeMoalFu();
+        if (id > 0) editListFu();
+        else addListFu();
+      }
+    },
+    [
+      typeCheck,
+      cover,
+      fileCheckFu,
+      fileList.model.id,
+      fileList.audio.id,
+      fileList.video.id,
+      fileList.img,
+      id,
+      dirCode,
+      closeMoalFu,
+      editListFu,
+      addListFu,
+    ]
+  );
+
+  return (
+    <div className={styles.GoodsAdd}>
+      <div className="pageTitle">{id > 0 ? "编辑馆藏" : "新增馆藏"}</div>
+      <div className="main mySorrl">
+        <Form
+          ref={FormBoxRef}
+          name="basic"
+          labelCol={{ span: 3 }}
+          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={25} showCount placeholder="请输入内容" />
+          </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="dictAge"
+            rules={[{ required: true, message: "请选择年代!" }]}
+          >
+            <Select
+              placeholder="请选择"
+              style={{ width: 400 }}
+              options={dictList["age"].slice(1)}
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="简介"
+            name="description"
+            // getValueFromEvent={(e) => e.target.value.trim()}
+          >
+            <TextArea
+              rows={4}
+              placeholder="请输入内容"
+              showCount
+              maxLength={200}
+            />
+          </Form.Item>
+
+          {/* -----上传封面图片 */}
+          <div className="myformBox myformBox0">
+            <input
+              id="upInput"
+              type="file"
+              accept=".png,.jpg,.jpeg"
+              ref={myInput}
+              onChange={(e) => handeUpPhoto(e)}
+            />
+            <input
+              id="upInput2"
+              type="file"
+              accept={
+                fileOneType === "img"
+                  ? ".gif,.png,.jpg,.jpeg"
+                  : fileOneType === "audio"
+                  ? ".mp3"
+                  : fileOneType === "model"
+                  ? ".4dage"
+                  : ".mp4"
+              }
+              ref={myInput2}
+              onChange={(e) => handeUpPhoto2(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>
+
+          {/* 选中文件类型和上传附件 */}
+          <div className="myformBox">
+            <div className="label">
+              <span>*</span> 文件类型:
+            </div>
+            <div className="myformBoxR">
+              <Checkbox.Group
+                options={typeCheckArr}
+                value={typeCheck}
+                onChange={(e) => setTypeCheck(e)}
+                // onChange={(e) => console.log(e)}
+              />
+            </div>
+          </div>
+
+          {/* -----------模型上传 */}
+          <div
+            className="myformBox myformBox2"
+            hidden={!typeCheck.includes("model")}
+          >
+            <div className="label">
+              <span>*</span> 模型:
+            </div>
+            {fileList.model.id ? (
+              <div className="fileInfo">
+                <a
+                  href={baseURL + fileList.model.filePath}
+                  download
+                  target="_blank"
+                  className="upSuccTxt"
+                  rel="noreferrer"
+                >
+                  {fileList.model.fileName}
+                </a>
+
+                <Popconfirm
+                  title="删除后无法恢复,是否删除?"
+                  okText="删除"
+                  cancelText="取消"
+                  onConfirm={() => setFileList({ ...fileList, model: {} })}
+                >
+                  <div className="clearCover">
+                    <CloseCircleOutlined />
+                  </div>
+                </Popconfirm>
+              </div>
+            ) : (
+              <>
+                <Button
+                  onClick={() => upFileFu("model")}
+                  icon={<UploadOutlined />}
+                >
+                  上传
+                </Button>
+
+                <div className="fileTit">
+                  仅支持4dage格式的模型文件,大小不能超过500M。
+                </div>
+              </>
+            )}
+          </div>
+
+          {/* -----------图片上传 */}
+          <div
+            className="myformBox myformBox3"
+            hidden={!typeCheck.includes("img")}
+          >
+            <div className="label">
+              <span>*</span> 图片:
+            </div>
+            <>
+              <div className="fileBoxRow_r">
+                <div className="upImgBox">
+                  <div
+                    hidden={!!fileList.img.length && fileList.img.length >= 9}
+                    className="fileBoxRow_up"
+                    onClick={() => upFileFu("img")}
+                  >
+                    <PlusOutlined />
+                  </div>
+                  {fileList.img.map((v, i) => (
+                    <div
+                      className="fileBoxRow_r_img"
+                      key={v.id}
+                      draggable="true"
+                      onDragStart={() => setDragImg(v)}
+                      onDragOver={(e) => {
+                        e.dataTransfer.dropEffect = "move";
+                        e.preventDefault();
+                      }}
+                      onDragEnter={(e) => handleDragEnter(e, v)}
+                      onDragEnd={() => setDragImg(null)}
+                    >
+                      {v.filePath ? (
+                        <ImageLazy
+                          noLook={dragImg ? true : false}
+                          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="fileTit">
+                  {fileList.img.length && fileList.img.length >= 2 ? (
+                    <>
+                      按住鼠标可拖动图片调整顺序。
+                      <br />
+                    </>
+                  ) : null}
+                  支持png、jpg、gif和jpeg的图片格式;最大支持20M;最多支持9张。
+                </div>
+              </div>
+            </>
+          </div>
+
+          {/* -----------音频上传 */}
+          <div
+            className="myformBox myformBox2"
+            hidden={!typeCheck.includes("audio")}
+          >
+            <div className="label">
+              <span>*</span> 音频:
+            </div>
+            {fileList.audio.id ? (
+              <div className="fileInfo">
+                <a
+                  href={baseURL + fileList.audio.filePath}
+                  download
+                  target="_blank"
+                  className="upSuccTxt"
+                  rel="noreferrer"
+                >
+                  {fileList.audio.fileName}
+                </a>
+
+                <Popconfirm
+                  title="删除后无法恢复,是否删除?"
+                  okText="删除"
+                  cancelText="取消"
+                  onConfirm={() => setFileList({ ...fileList, audio: {} })}
+                >
+                  <div className="clearCover">
+                    <CloseCircleOutlined />
+                  </div>
+                </Popconfirm>
+              </div>
+            ) : (
+              <>
+                <Button
+                  onClick={() => upFileFu("audio")}
+                  icon={<UploadOutlined />}
+                >
+                  上传
+                </Button>
+
+                <div className="fileTit">
+                  仅支持MP3格式的音频文件,大小不得超过10MB。
+                </div>
+              </>
+            )}
+          </div>
+
+          {/* -----------视频上传 */}
+          <div
+            className="myformBox myformBox2"
+            hidden={!typeCheck.includes("video")}
+          >
+            <div className="label">
+              <span>*</span> 视频:
+            </div>
+            {fileList.video.id ? (
+              <div className="fileInfo">
+                <div className="upSuccTxt">{fileList.video.fileName}</div>
+                <div
+                  className="clearCover"
+                  hidden={!fileList.video.filePath}
+                  onClick={() =>
+                    store.dispatch({
+                      type: "layout/lookVideo",
+                      payload: fileList.video.filePath!,
+                    })
+                  }
+                >
+                  <PlayCircleOutlined />
+                </div>
+
+                <Popconfirm
+                  title="删除后无法恢复,是否删除?"
+                  okText="删除"
+                  cancelText="取消"
+                  onConfirm={() => setFileList({ ...fileList, video: {} })}
+                >
+                  <div className="clearCover">
+                    <CloseCircleOutlined />
+                  </div>
+                </Popconfirm>
+              </div>
+            ) : (
+              <>
+                <Button
+                  onClick={() => upFileFu("video")}
+                  icon={<UploadOutlined />}
+                >
+                  上传
+                </Button>
+
+                <div className="fileTit">
+                  仅支持MP4格式的视频文件,大小不得超过500MB。
+                </div>
+              </>
+            )}
+          </div>
+
+          <div
+            className={classNames(
+              "noUpThumb noUpThumb2",
+              fileCheckFu && typeOk ? "noUpThumbAc" : ""
+            )}
+          >
+            请至少选择一个文件类型,并上传对应附件!
+          </div>
+
+          <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: 9, span: 16 }}>
+            <Button type="primary" htmlType="submit">
+              提交
+            </Button>
+            &emsp;
+            <Popconfirm
+              title="放弃编辑后,信息将不会保存!"
+              okText="放弃"
+              cancelText="取消"
+              onConfirm={closeMoalFu}
+            >
+              <Button>取消</Button>
+            </Popconfirm>
+          </Form.Item>
+        </Form>
+      </div>
+    </div>
+  );
+}
+
+const MemoGoodsAdd = React.memo(GoodsAdd);
+
+export default MemoGoodsAdd;

+ 43 - 0
管理后台/src/pages/A3Goods/index.module.scss

@@ -0,0 +1,43 @@
+.Goods {
+  position: relative;
+  :global {
+    .goodsTop {
+      border-radius: 10px;
+      background-color: #fff;
+    }
+
+    .searchBox {
+      padding: 10px 15px 0;
+      display: flex;
+      flex-wrap: wrap;
+
+      .searchRow {
+        margin-bottom: 20px;
+        margin-right: 20px;
+      }
+      .searchRow2{
+        margin-right: 0;
+      }
+    }
+
+    .tableBox {
+      border-radius: 10px;
+      overflow: hidden;
+      margin-top: 20px;
+      height: calc(100% - 128px);
+      background-color: #fff;
+
+      .ant-table-body {
+        height: 560px;
+        overflow-y: auto !important;
+        overflow-y: overlay !important;
+
+        .ant-table-row {
+          .ant-table-cell {
+            padding: 8px;
+          }
+        }
+      }
+    }
+  }
+}

+ 311 - 0
管理后台/src/pages/A3Goods/index.tsx

@@ -0,0 +1,311 @@
+import { RootState } from "@/store";
+import { getGoodsListAPI, goodsRemoveAPI } from "@/store/action/A3Goods";
+import { MessageFu } from "@/utils/message";
+import { Input, Select, DatePicker, Button, Table, Popconfirm } from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useDispatch, useSelector } from "react-redux";
+import styles from "./index.module.scss";
+import ImageLazy from "@/components/ImageLazy";
+import { GoodsTableType } from "@/types";
+import GoodsAdd from "./GoodsAdd";
+
+const { RangePicker } = DatePicker;
+function Goods() {
+  const dispatch = useDispatch();
+
+  // 从仓库获取下拉列表数据
+  const dictList = useSelector(
+    (state: RootState) => state.layoutStore.dictList
+  );
+
+  // 从仓库获取表格列表信息
+  const tableInfo = useSelector(
+    (state: RootState) => state.goodsReducer.tableInfo
+  );
+
+  // 顶部筛选
+  const [tableSelect, setTableSelect] = useState({
+    searchKey: "",
+    dictTexture: "",
+    dictAge: "",
+    startTime: "",
+    endTime: "",
+    display: -1,
+    pageSize: 10,
+    pageNum: 1,
+  });
+
+  // 封装发送请求的函数
+
+  const getList = useCallback(async () => {
+    const data = {
+      ...tableSelect,
+      display: tableSelect.display === -1 ? null : tableSelect.display,
+    };
+    dispatch(getGoodsListAPI(data));
+  }, [dispatch, tableSelect]);
+
+  useEffect(() => {
+    getList();
+  }, [getList]);
+
+  // 名称的输入
+  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: "",
+      dictTexture: "",
+      dictAge: "",
+      startTime: "",
+      endTime: "",
+      display: -1,
+      pageSize: 10,
+      pageNum: 1,
+    });
+  }, []);
+
+  // ----------关于表格的数据
+
+  // 页码变化
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      setTableSelect({ ...tableSelect, pageNum, pageSize });
+    },
+    [tableSelect]
+  );
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      const res = await goodsRemoveAPI(id);
+      if (res.code === 0) {
+        MessageFu.success("删除成功!");
+        getList();
+      }
+    },
+    [getList]
+  );
+
+  const columns = useMemo(() => {
+    return [
+      {
+        title: "名称",
+        dataIndex: "name",
+      },
+
+      {
+        title: "类别",
+        dataIndex: "dictTexture",
+      },
+      {
+        title: "年代",
+        dataIndex: "dictAge",
+      },
+      {
+        title: "简介",
+        render: (item: GoodsTableType) =>
+          item.description ? (
+            item.description.length >= 25 ? (
+              <span style={{ cursor: "pointer" }} title={item.description}>
+                {item.description.substring(0, 25) + "..."}
+              </span>
+            ) : (
+              item.description
+            )
+          ) : (
+            "(空)"
+          ),
+      },
+      {
+        title: "图片",
+        render: (item: GoodsTableType) => (
+          <div className="tableImgAuto">
+            <ImageLazy width={60} height={60} src={item.thumb!} />
+          </div>
+        ),
+      },
+      {
+        title: "最近编辑时间",
+        dataIndex: "updateTime",
+      },
+
+      {
+        title: "展示状态",
+        render: (item: GoodsTableType) =>
+          item.display === 1 ? "展示" : "不展示",
+      },
+
+      {
+        title: "操作",
+        render: (item: GoodsTableType) => (
+          <>
+            <Button size="small" type="text" onClick={() => setAddId(item.id!)}>
+              编辑
+            </Button>
+            <Popconfirm
+              title="删除后无法恢复,是否删除?"
+              okText="删除"
+              cancelText="取消"
+              onConfirm={() => delTableFu(item.id!)}
+            >
+              <Button size="small" type="text" danger>
+                删除
+              </Button>
+            </Popconfirm>
+          </>
+        ),
+      },
+    ];
+  }, [delTableFu]);
+
+  // 新增或者编辑的弹窗
+  const [addId, setAddId] = useState(0);
+
+  return (
+    <div className={styles.Goods}>
+      <div className="goodsTop">
+        <div className="pageTitle">馆藏管理</div>
+        {/* 搜索信息 */}
+        <div className="searchBox">
+          <div className="searchRow">
+            <span>名称:</span>
+            <Input
+              key={inputKey}
+              maxLength={25}
+              style={{ width: 140 }}
+              placeholder="请输入关键字"
+              allowClear
+              onChange={(e) => nameChange(e)}
+            />
+          </div>
+
+          <div className="searchRow">
+            <span>类别:</span>
+            <Select
+              placeholder="请选择"
+              style={{ width: 140 }}
+              value={tableSelect.dictTexture}
+              onChange={(e) =>
+                setTableSelect({ ...tableSelect, dictTexture: e, pageNum: 1 })
+              }
+              options={dictList["texture"]}
+            />
+          </div>
+
+          <div className="searchRow">
+            <span>年代:</span>
+            <Select
+              placeholder="请选择"
+              style={{ width: 140 }}
+              value={tableSelect.dictAge}
+              onChange={(e) =>
+                setTableSelect({ ...tableSelect, dictAge: e, pageNum: 1 })
+              }
+              options={dictList["age"]}
+            />
+          </div>
+
+          <div className="searchRow">
+            <span>最近编辑日期:</span>
+            <RangePicker
+              style={{ width: 230 }}
+              key={inputKey}
+              onChange={timeChange}
+            />
+          </div>
+
+          <div className="searchRow">
+            <span>展示状态:</span>
+            <Select
+              placeholder="请选择"
+              style={{ width: 80 }}
+              value={tableSelect.display}
+              onChange={(e) =>
+                setTableSelect({ ...tableSelect, display: e, pageNum: 1 })
+              }
+              options={[
+                { value: -1, label: "全部" },
+                { value: 1, label: "开启" },
+                { value: 0, label: "关闭" },
+              ]}
+            />
+          </div>
+
+          <div className="searchRow searchRow2">
+            <Button onClick={resetSelectFu}>重置</Button>&emsp;&emsp;
+            <Button type="primary" onClick={() => setAddId(-1)}>
+              新增
+            </Button>
+          </div>
+        </div>
+      </div>
+      {/* 表格主体 */}
+      <div className="tableBox">
+        <Table
+          scroll={{ y: 560 }}
+          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>
+
+      {/* 新增和编辑的弹窗 */}
+      {addId !== 0 ? (
+        <GoodsAdd
+          id={addId}
+          closeMoalFu={() => setAddId(0)}
+          addListFu={resetSelectFu}
+          editListFu={getList}
+        />
+      ) : null}
+    </div>
+  );
+}
+
+const MemoGoods = React.memo(Goods);
+
+export default MemoGoods;

+ 139 - 0
管理后台/src/pages/A4Venue/VenueEdit/index.module.scss

@@ -0,0 +1,139 @@
+.VenueEdit {
+  position: absolute;
+  z-index: 20;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  border-radius: 10px;
+
+  :global {
+    .formBox {
+      margin-top: 10px;
+      width: 1200px;
+      height: calc(100% - 70px);
+      padding-right: 400px;
+      overflow-y: auto;
+
+      // 多张附件图片上传
+      .myformBox {
+        display: flex;
+        margin-bottom: 10px;
+        margin-top: 40px;
+
+        .ant-btn-default {
+          width: 100px;
+        }
+
+        .label {
+          width: 100px;
+          text-align: right;
+
+          &>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);
+
+
+          }
+
+          .upImgBox {
+            display: flex;
+            flex-wrap: wrap;
+            max-width: 700px;
+
+            &>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;
+              }
+            }
+          }
+
+        }
+
+      }
+
+      .noUpThumb {
+        position: relative;
+        overflow: hidden;
+        opacity: 0;
+        transition: top .2s;
+        color: #ff4d4f;
+        top: -10px;
+      }
+
+      .noUpThumb2 {
+        position: relative;
+        padding-left: 100px;
+        margin-bottom: 12px;
+      }
+
+      .noUpThumbAc {
+        top: 0;
+        opacity: 1;
+      }
+    }
+  }
+}

+ 317 - 0
管理后台/src/pages/A4Venue/VenueEdit/index.tsx

@@ -0,0 +1,317 @@
+import {
+  getVenueDetailAPI,
+  setVenueSaveApi,
+  venueUploadAPI,
+} from "@/store/action/A4Venue";
+import { ImgListType, VenueTableType } from "@/types";
+import { fileDomInitialFu } from "@/utils/domShow";
+import { MessageFu } from "@/utils/message";
+import { Button, Form, FormInstance, Input, Popconfirm } from "antd";
+import { PlusOutlined, CloseOutlined } from "@ant-design/icons";
+import TextArea from "antd/es/input/TextArea";
+import classNames from "classnames";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import styles from "./index.module.scss";
+import ImageLazy from "@/components/ImageLazy";
+
+type Props = {
+  id: number;
+  closePageFu: () => void;
+  editTableFu: () => void;
+};
+
+function VenueEdit({ id, closePageFu, editTableFu }: Props) {
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<FormInstance>(null);
+
+  // 文件的dirCode码
+  const [dirCode, setDirCode] = useState("");
+
+  const myInput = useRef<HTMLInputElement>(null);
+
+  // 通过id获取详情,回显数据
+  const getInfoFu = useCallback(async (id: number) => {
+    const res = await getVenueDetailAPI(id);
+    const data: VenueTableType = res.data.entity;
+    const fileList: ImgListType[] = res.data.file;
+    setDirCode(data.dirCode);
+    FormBoxRef.current?.setFieldsValue(data);
+    setImgList(fileList);
+  }, []);
+
+  useEffect(() => {
+    if (id) getInfoFu(id);
+    else {
+      setDirCode(Date.now() + "");
+    }
+  }, [getInfoFu, id]);
+
+  // 多张图片附件
+  const [imgList, setImgList] = useState<ImgListType[]>([]);
+
+  // 上传附件的处理函数
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0];
+
+        let anType = ["image/jpeg", "image/png"];
+        let anTit1 = "只支持png、jpg和jpeg格式!";
+        let anTit2 = "最大支持20M!";
+        let anSize = 20 * 1024 * 1024;
+
+        // 校验格式
+        if (!anType.includes(filesInfo.type)) {
+          e.target.value = "";
+          return MessageFu.warning(anTit1);
+        }
+
+        // 校验大小
+        if (filesInfo.size > anSize) {
+          e.target.value = "";
+          return MessageFu.warning(anTit2);
+        }
+        // 创建FormData对象
+        const fd = new FormData();
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append("type", "img");
+        fd.append("dirCode", dirCode);
+        fd.append("file", filesInfo);
+
+        e.target.value = "";
+
+        try {
+          const res = await venueUploadAPI(fd);
+
+          if (res.code === 0) {
+            MessageFu.success("上传成功!");
+            setImgList([...imgList, res.data]);
+          }
+          fileDomInitialFu();
+        } catch (error) {
+          fileDomInitialFu();
+        }
+      }
+    },
+    [dirCode, imgList]
+  );
+
+  // 附件图片的拖动
+  const [dragImg, setDragImg] = useState<any>(null);
+
+  const handleDragEnter = useCallback(
+    (e: React.DragEvent<HTMLDivElement>, item: ImgListType) => {
+      e.dataTransfer.effectAllowed = "move";
+      if (item === dragImg) return;
+      const newItems = [...imgList]; //拷贝一份数据进行交换操作。
+      const src = newItems.indexOf(dragImg); //获取数组下标
+      const dst = newItems.indexOf(item);
+      newItems.splice(dst, 0, ...newItems.splice(src, 1)); //交换位置
+      setImgList(newItems);
+    },
+    [dragImg, imgList]
+  );
+
+  // 删除某一张图片
+  const delImgListFu = useCallback(
+    (id: number) => {
+      const newItems = imgList.filter((v) => v.id !== id);
+      setImgList(newItems);
+    },
+    [imgList]
+  );
+
+  const [typeOk, setTypeOk] = useState(false);
+
+  // 附件信息的校验
+  const fileCheckFu = useMemo(() => {
+    let flag = false;
+    if (imgList.length === 0) flag = true;
+    return flag;
+  }, [imgList.length]);
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    setTypeOk(true);
+    // return MessageFu.warning("有表单不符号规则!");
+  }, []);
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (values: VenueTableType) => {
+      setTypeOk(true);
+
+      if (fileCheckFu) return;
+
+      const obj = {
+        ...values,
+        id: id > 0 ? id : null,
+        dirCode,
+        fileIds: imgList.map((v) => v.id).join(","),
+        thumb: imgList[0].filePath,
+      };
+      // console.log(obj);
+      const res = await setVenueSaveApi(obj);
+      if (res.code === 0) {
+        MessageFu.success("编辑成功!");
+        editTableFu();
+        closePageFu();
+      }
+    },
+    [closePageFu, dirCode, editTableFu, fileCheckFu, id, imgList]
+  );
+
+  return (
+    <div className={styles.VenueEdit}>
+      <div className="pageTitle">编辑场馆</div>
+      <div className="formBox mySorrl">
+        <Form
+          ref={FormBoxRef}
+          name="basic"
+          labelCol={{ span: 3 }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete="off"
+        >
+          <Form.Item
+            label="名称"
+            name="name"
+            rules={[{ required: true, message: "请输入标题!" }]}
+          >
+            <Input disabled />
+          </Form.Item>
+
+          <Form.Item
+            label="位置"
+            name="location"
+            rules={[{ required: true, message: "请输入位置!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={8} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          <Form.Item
+            label="简介"
+            name="description"
+            // getValueFromEvent={(e) => e.target.value.trim()}
+          >
+            <TextArea
+              rows={5}
+              placeholder="请输入内容"
+              showCount
+              maxLength={200}
+            />
+          </Form.Item>
+
+          {/* 上传附件图片 */}
+          <div className="myformBox">
+            <input
+              id="upInput2"
+              type="file"
+              accept=".png,.jpg,.jpeg"
+              ref={myInput}
+              onChange={(e) => handeUpPhoto(e)}
+            />
+
+            <div className="label">
+              <span>*</span> 图片:
+            </div>
+            <>
+              <div className="fileBoxRow_r">
+                <div className="upImgBox">
+                  <div
+                    hidden={imgList.length >= 9}
+                    className="fileBoxRow_up"
+                    onClick={() => myInput.current?.click()}
+                  >
+                    <PlusOutlined />
+                  </div>
+                  {imgList.map((v, i) => (
+                    <div
+                      className="fileBoxRow_r_img"
+                      key={v.id}
+                      draggable="true"
+                      onDragStart={() => setDragImg(v)}
+                      onDragOver={(e) => {
+                        e.dataTransfer.dropEffect = "move";
+                        e.preventDefault();
+                      }}
+                      onDragEnter={(e) => handleDragEnter(e, v)}
+                      onDragEnd={() => setDragImg(null)}
+                    >
+                      {i === 0 ? <div className="myCoverBox">封面</div> : null}
+                      {v.filePath ? (
+                        <ImageLazy
+                          noLook={dragImg ? true : false}
+                          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="fileTit">
+                  {imgList.length >= 2 ? (
+                    <>
+                      按住鼠标可拖动图片调整顺序,第一张为展示端封面。
+                      <br />
+                    </>
+                  ) : null}
+                  支持png、jpg和jpeg的图片格式;最大支持20M;最多支持9张。
+                </div>
+              </div>
+            </>
+          </div>
+          <div
+            className={classNames(
+              "noUpThumb noUpThumb2",
+              fileCheckFu && typeOk ? "noUpThumbAc" : ""
+            )}
+          >
+            请上传图片!
+          </div>
+
+          {/* 确定和取消按钮 */}
+          <br />
+          <Form.Item wrapperCol={{ offset: 10, span: 16 }}>
+            <Button type="primary" htmlType="submit">
+              提交
+            </Button>
+            &emsp;
+            <Popconfirm
+              title="放弃编辑后,信息将不会保存!"
+              okText="放弃"
+              cancelText="取消"
+              onConfirm={closePageFu}
+            >
+              <Button>取消</Button>
+            </Popconfirm>
+          </Form.Item>
+        </Form>
+      </div>
+    </div>
+  );
+}
+
+const MemoVenueEdit = React.memo(VenueEdit);
+
+export default MemoVenueEdit;

+ 49 - 0
管理后台/src/pages/A4Venue/VenueTable/index.module.scss

@@ -0,0 +1,49 @@
+.VenueTable {
+  border-radius: 10px;
+  overflow: hidden;
+  margin-top: 20px;
+  height: calc(100% - 130px);
+  background-color: #fff;
+
+  :global {
+    .ant-table-body {
+      height: 615px;
+      overflow-y: auto !important;
+      overflow-y: overlay !important;
+
+      .ant-table-row {
+        .ant-table-cell {
+          padding: 8px;
+        }
+      }
+    }
+
+    .incoTitle {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+
+      .hotTitleInco1 {
+        cursor: pointer;
+        width: 16px;
+        height: 16px;
+        border-radius: 50%;
+        background-color: #696969;
+        margin-left: 8px;
+        text-align: center;
+        line-height: 16px;
+        color: #fff;
+        font-size: 12px;
+      }
+    }
+
+    // 表头拖拽样式
+    .drop-over-downward td {
+      border-bottom: 2px dashed var(--themeColor) !important;
+    }
+
+    .drop-over-upward td {
+      border-top: 2px dashed var(--themeColor) !important;
+    }
+  }
+}

+ 203 - 0
管理后台/src/pages/A4Venue/VenueTable/index.tsx

@@ -0,0 +1,203 @@
+import ImageLazy from "@/components/ImageLazy";
+import { RootState } from "@/store";
+import { VenueTableType } from "@/types";
+import { Button, Table, Tooltip } from "antd";
+import React, { useCallback, useMemo, useRef } from "react";
+import { useSelector } from "react-redux";
+import { ExclamationOutlined } from "@ant-design/icons";
+import styles from "./index.module.scss";
+
+// 表格拖动排序-----------------
+import { DndProvider, useDrag, useDrop } from "react-dnd";
+import { HTML5Backend } from "react-dnd-html5-backend";
+import { venueSortApi } from "@/store/action/A4Venue";
+
+type Porps = {
+  editInfoFu: (id: number) => void;
+  delInfoFu:()=>void
+};
+
+function VenueTable({ editInfoFu,delInfoFu }: Porps) {
+  // 有关表格数据
+  const results = useSelector(
+    (state: RootState) => state.venueReducer.tableInfo
+  );
+
+  const columns = useMemo(() => {
+    return [
+      {
+        title: () => (
+          <div className="incoTitle">
+            序号
+            {results.length >= 2 ? (
+              <Tooltip title="按住鼠标可拖动表格调整顺序">
+                <div className="hotTitleInco1">
+                  <ExclamationOutlined />
+                </div>
+              </Tooltip>
+            ) : null}
+          </div>
+        ),
+        width: 100,
+        render: (text: any, item: any, index: number) => index + 1,
+      },
+      {
+        title: "名称",
+        dataIndex: "name",
+      },
+      {
+        title: "位置",
+        render: (item: VenueTableType) =>
+          item.location ? item.location : "(空)",
+      },
+      {
+        title: "简介",
+        render: (item: VenueTableType) =>
+          item.description ? (
+            item.description.length >= 25 ? (
+              <span style={{ cursor: "pointer" }} title={item.description}>
+                {item.description.substring(0, 25) + "..."}
+              </span>
+            ) : (
+              item.description
+            )
+          ) : (
+            "(空)"
+          ),
+      },
+      {
+        title: "图片",
+        render: (item: VenueTableType) => (
+          <div className="tableImgAuto">
+            <ImageLazy width={60} height={60} src={item.thumb} />
+          </div>
+        ),
+      },
+
+      {
+        title: "最近编辑时间",
+        dataIndex: "updateTime",
+      },
+
+      {
+        title: "操作",
+        render: (item: VenueTableType) => (
+          <>
+            <Button
+              size="small"
+              type="text"
+              onClick={() => editInfoFu(item.id!)}
+            >
+              编辑
+            </Button>
+          </>
+        ),
+      },
+    ];
+  }, [editInfoFu, results.length]);
+
+  // 表格拖动排序-----------------
+  interface DraggableBodyRowProps
+    extends React.HTMLAttributes<HTMLTableRowElement> {
+    index: number;
+    moveRow: (dragIndex: number, hoverIndex: number) => void;
+  }
+
+  const type = "DraggableBodyRow";
+
+  const DraggableBodyRow = useCallback(
+    ({
+      index,
+      moveRow,
+      className,
+      style,
+      ...restProps
+    }: DraggableBodyRowProps) => {
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const ref = useRef<HTMLTableRowElement>(null);
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const [{ isOver, dropClassName }, drop] = useDrop({
+        accept: type,
+        collect: (monitor) => {
+          const { index: dragIndex } = monitor.getItem() || {};
+          if (dragIndex === index) {
+            return {};
+          }
+          return {
+            isOver: monitor.isOver(),
+            dropClassName:
+              dragIndex < index ? " drop-over-downward" : " drop-over-upward",
+          };
+        },
+        drop: (item: { index: number }) => {
+          moveRow(item.index, index);
+        },
+      });
+      // eslint-disable-next-line react-hooks/rules-of-hooks
+      const [, drag] = useDrag({
+        type,
+        item: { index },
+        collect: (monitor) => ({
+          isDragging: monitor.isDragging(),
+        }),
+      });
+      drop(drag(ref));
+
+      return (
+        <tr
+          ref={ref}
+          className={`${className}${isOver ? dropClassName : ""}`}
+          style={{ cursor: "move", ...style }}
+          {...restProps}
+        />
+      );
+    },
+    []
+  );
+
+  const components = {
+    body: {
+      row: DraggableBodyRow,
+    },
+  };
+
+  const moveRow = useCallback(
+    async (dragIndex: number, hoverIndex: number) => {
+      if (dragIndex === hoverIndex) return;
+      // 交互位置-之前的id
+      const beforeId = results[dragIndex].id;
+      const afterId = results[hoverIndex].id;
+
+      const res = await venueSortApi(beforeId!, afterId!);
+
+      if (res.code === 0) delInfoFu()
+    },
+    [delInfoFu, results]
+  );
+
+  return (
+    <div className={styles.VenueTable}>
+      <DndProvider backend={HTML5Backend}>
+        <Table
+          scroll={{ y: 615 }}
+          dataSource={results}
+          columns={columns}
+          rowKey="id"
+          pagination={false}
+          components={components}
+          onRow={(_, index) => {
+            const attr = {
+              index,
+              moveRow,
+            };
+            return attr as React.HTMLAttributes<any>;
+          }}
+        />
+      </DndProvider>
+    </div>
+  );
+}
+
+const MemoVenueTable = React.memo(VenueTable);
+
+export default MemoVenueTable;

+ 20 - 0
管理后台/src/pages/A4Venue/index.module.scss

@@ -0,0 +1,20 @@
+.Venue{
+  position: relative;
+  :global{
+    .venueTop {
+      background-color: #fff;
+      border-radius: 10px;
+
+      .searchBox {
+        position: relative;
+        padding: 10px 20px 20px;
+        display: flex;
+        align-items: center;
+
+        .row {
+          margin-right: 50px;
+        }
+      }
+    }
+  }
+}

+ 122 - 0
管理后台/src/pages/A4Venue/index.tsx

@@ -0,0 +1,122 @@
+import { getVenueListAPI } from "@/store/action/A4Venue";
+import { Button, DatePicker, Input } from "antd";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { useDispatch } from "react-redux";
+import styles from "./index.module.scss";
+import VenueEdit from "./VenueEdit";
+import VenueTable from "./VenueTable";
+const { RangePicker } = DatePicker;
+
+function Venue() {
+  const dispatch = useDispatch();
+  // 顶部筛选的数据
+  const [tableSelect, setTableSelect] = useState({
+    searchKey: "",
+    startTime: "",
+    endTime: "",
+    pageSize: 50,
+    pageNum: 1,
+  });
+
+  useEffect(() => {
+    dispatch(getVenueListAPI(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 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: "",
+      startTime: "",
+      endTime: "",
+      pageSize: 50,
+      pageNum: 1,
+    });
+  }, []);
+
+  const addInfoFu = useCallback((id: number) => {
+    setEditId(id);
+  }, []);
+
+  const [editId, setEditId] = useState(0);
+
+  return (
+    <div className={styles.Venue}>
+      <div className="venueTop">
+        <div className="pageTitle">场馆管理</div>
+        <div className="searchBox">
+          <div className="row">
+            <span>名称:</span>
+            <Input
+              key={inputKey}
+              maxLength={20}
+              style={{ width: 200 }}
+              placeholder="请输入关键字"
+              allowClear
+              onChange={(e) => nameChange(e)}
+            />
+          </div>
+          <div className="row">
+            <span>最近编辑日期:</span>
+            <RangePicker
+              style={{ width: 230 }}
+              key={inputKey}
+              onChange={timeChange}
+            />
+          </div>
+          <div className="row">
+            &emsp;&emsp;&emsp;&emsp;
+            <Button onClick={resetSelectFu}>重置</Button>
+          </div>
+        </div>
+      </div>
+
+      {/* 新增和编辑 和 表格主体 (拖动有冲突,所以只能同时渲染一个)*/}
+      {editId ? (
+        <VenueEdit
+          id={editId}
+          closePageFu={() => setEditId(0)}
+          editTableFu={() => dispatch(getVenueListAPI(tableSelect))}
+        />
+      ) : (
+        <VenueTable
+          editInfoFu={(id) => addInfoFu(id)}
+          delInfoFu={() => dispatch(getVenueListAPI(tableSelect))}
+        />
+      )}
+    </div>
+  );
+}
+
+const MemoVenue = React.memo(Venue);
+
+export default MemoVenue;

+ 34 - 0
管理后台/src/pages/A5Guest/index.module.scss

@@ -0,0 +1,34 @@
+.Guest {
+  :global {
+    .guetsMain {
+      margin-top: 20px;
+      width: 100%;
+      height: calc(100% - 70px);
+      overflow-y: auto;
+
+      .guetsMainBox {
+        padding: 15px 30px 30px;
+        background-color: #fff;
+        border-radius: 10px;
+        min-height: 200px;
+
+        &:nth-of-type(1) {
+          margin-bottom: 20px;
+        }
+
+        .tit {
+          font-size: 16px;
+          .time{
+            font-size: 14px;
+            color: #999;
+          }
+        }
+        .txt{
+          margin-top: 20px;
+          font-size: 14px;
+          line-height: 30px;
+        }
+      }
+    }
+  }
+}

+ 183 - 0
管理后台/src/pages/A5Guest/index.tsx

@@ -0,0 +1,183 @@
+import {
+  getGuestInfoAPI,
+  setGuestGuideAPI,
+  setGuestNoticeAPI,
+} from "@/store/action/A5Guest";
+import { MessageFu } from "@/utils/message";
+import { Button, Popconfirm } from "antd";
+import TextArea from "antd/es/input/TextArea";
+import dayjs from "dayjs";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import styles from "./index.module.scss";
+function Guest() {
+  const [edit1, setEdit1] = useState({
+    show: false,
+    txt: "",
+  });
+  const [value1, setValue1] = useState({
+    txt: "",
+    time: "",
+  });
+  const [edit2, setEdit2] = useState({
+    show: false,
+    txt: "",
+  });
+  const [value2, setValue2] = useState({
+    txt: "",
+    time: "",
+  });
+
+  // 进页面获取信息
+  const getInfoFu = useCallback(async () => {
+    const res = await getGuestInfoAPI();
+    setValue1(JSON.parse(res.data[0].content))
+    setValue2(JSON.parse(res.data[1].content))
+  }, []);
+
+  useEffect(() => {
+    getInfoFu();
+  }, [getInfoFu]);
+
+  const txt1 = useMemo(() => {
+    if (value1.txt.replaceAll(" ", "").replaceAll("\n", "") === "") return "";
+    return value1.txt.replaceAll(" ", "&emsp;").replaceAll("\n", "<br/>");
+  }, [value1.txt]);
+
+  const txt2 = useMemo(() => {
+    if (value2.txt.replaceAll(" ", "").replaceAll("\n", "") === "") return "";
+    return value2.txt.replaceAll(" ", "&emsp;").replaceAll("\n", "<br/>");
+  }, [value2.txt]);
+
+  const editFu = useCallback(
+    (val: number) => {
+      if (val === 1) setEdit1({ show: true, txt: value1.txt });
+      else setEdit2({ show: true, txt: value2.txt });
+    },
+    [value1.txt, value2.txt]
+  );
+
+  const btnOk = useCallback(
+    async (val: number) => {
+      const data = {
+        txt: val === 1 ? edit1.txt : edit2.txt,
+        time: dayjs(new Date()).format("YYYY-MM-DD HH:mm"),
+      };
+
+      const res =
+        val === 1
+          ? await setGuestGuideAPI(JSON.stringify(data))
+          : await setGuestNoticeAPI(JSON.stringify(data));
+
+      if (res.code === 0) {
+        val === 1
+          ? setEdit1({ txt: "", show: false })
+          : setEdit2({ txt: "", show: false });
+        MessageFu.success("修改成功!");
+        getInfoFu()
+      }
+    },
+    [edit1.txt, edit2.txt, getInfoFu]
+  );
+
+  return (
+    <div className={styles.Guest}>
+      <div className="pageTitle">游客服务</div>
+      <div className="guetsMain mySorrl">
+        <div className="guetsMainBox">
+          <div className="tit">
+            参观指南:&emsp;
+            {edit1.show ? (
+              <>
+                <Button type="primary" onClick={() => btnOk(1)}>
+                  保存
+                </Button>
+                &emsp;
+                <Popconfirm
+                  title="放弃编辑后,信息将不会保存!"
+                  okText="放弃"
+                  cancelText="取消"
+                  onConfirm={() => setEdit1({ ...edit1, show: false })}
+                >
+                  <Button>取消</Button>
+                </Popconfirm>
+                <br />
+                <br />
+                <TextArea
+                  value={edit1.txt}
+                  onChange={(e) =>
+                    setEdit1({ show: true, txt: e.target.value })
+                  }
+                  autoSize
+                  placeholder="请输入内容"
+                  showCount
+                  maxLength={500}
+                />
+              </>
+            ) : (
+              <>
+                <Button type="primary" onClick={() => editFu(1)}>
+                  修改
+                </Button>
+                &emsp;&emsp;
+                <span className="time">最近编辑时间:{value1.time}</span>
+                <div
+                  className="txt"
+                  dangerouslySetInnerHTML={{ __html: txt1 ? txt1 : "(空)" }}
+                ></div>
+              </>
+            )}
+          </div>
+        </div>
+        <div className="guetsMainBox">
+          <div className="tit">
+            参观须知:&emsp;
+            {edit2.show ? (
+              <>
+                <Button type="primary" onClick={() => btnOk(2)}>
+                  保存
+                </Button>
+                &emsp;
+                <Popconfirm
+                  title="放弃编辑后,信息将不会保存!"
+                  okText="放弃"
+                  cancelText="取消"
+                  onConfirm={() => setEdit2({ ...edit2, show: false })}
+                >
+                  <Button>取消</Button>
+                </Popconfirm>
+                <br />
+                <br />
+                <TextArea
+                  value={edit2.txt}
+                  onChange={(e) =>
+                    setEdit2({ show: true, txt: e.target.value })
+                  }
+                  autoSize
+                  placeholder="请输入内容"
+                  showCount
+                  maxLength={500}
+                />
+              </>
+            ) : (
+              <>
+                <Button type="primary" onClick={() => editFu(2)}>
+                  修改
+                </Button>
+                &emsp;&emsp;
+                <span className="time">最近编辑时间:{value2.time}</span>
+                <div
+                  className="txt"
+                  dangerouslySetInnerHTML={{ __html: txt2 ? txt2 : "(空)" }}
+                ></div>
+              </>
+            )}
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+const MemoGuest = React.memo(Guest);
+
+export default MemoGuest;

+ 194 - 0
管理后台/src/pages/Layout/index.module.scss

@@ -0,0 +1,194 @@
+.Layout {
+  width: 100%;
+  height: 100%;
+  display: flex;
+
+  :global {
+
+    .layoutLeft {
+      position: relative;
+      z-index: 30;
+      width: 220px;
+      height: 100%;
+      box-shadow: 0px 0px 5px 3px;
+      background-image: url('../../assets/img/layoutLeftMain.jpg');
+      background-size: 100% 100%;
+
+      .layoutLeftTop {
+        height: 60px;
+        background-color: var(--themeColor);
+        padding: 10px;
+        padding-left: 20px;
+      }
+
+      .layoutLeftMain {
+        height: calc(100% - 60px);
+        padding: 20px;
+
+
+        .mainBoxL2Row {
+          padding-left: 60px;
+          cursor: pointer;
+          height: 50px;
+          line-height: 50px;
+          font-size: 16px;
+          position: relative;
+          margin-top: 25px;
+          border-radius: 8px;
+          color: var(--themeColor);
+
+          .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-color: var(--themeColor);
+
+            .tabImgAc {
+              display: block;
+            }
+          }
+        }
+
+
+
+        .active {
+          color: #fff;
+          pointer-events: none;
+          background-color: var(--themeColor);
+          .tabImgAc {
+            display: block;
+          }
+        }
+      }
+
+
+    }
+
+    .layoutRight {
+      width: calc(100% - 220px);
+      height: 100%;
+
+
+      .layoutRightTop {
+        height: 60px;
+        background-color: var(--themeColor);
+        display: flex;
+        justify-content: flex-end;
+        position: relative;
+        z-index: 30;
+
+        .user {
+          margin-right: 40px;
+          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);
+        padding: 15px 30px 20px;
+        background-image: url('../../assets/img/loginBox.jpg');
+        background-size: 100% 100%;
+
+        .mainBoxR {
+          width: 100%;
+          height: 100%;
+          overflow: hidden;
+          &>div {
+            width: 100%;
+            height: 100%;
+          }
+        }
+      }
+
+    }
+
+
+
+  }
+}

+ 275 - 0
管理后台/src/pages/Layout/index.tsx

@@ -0,0 +1,275 @@
+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 classNames from "classnames";
+import history from "@/utils/history";
+import { Button, Form, Input, Modal, Popconfirm } from "antd";
+import { Base64 } from "js-base64";
+import encodeStr from "@/utils/pass";
+import { getDictListAPI, passWordEditAPI } from "@/store/action/layout";
+import { getTokenInfo, removeTokenInfo } from "@/utils/storage";
+import { useDispatch } from "react-redux";
+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 inco5 from "@/assets/img/inco5.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";
+import inco5Ac from "@/assets/img/inco5Ac.png";
+import logonImg from "@/assets/img/logo2.png";
+import { MessageFu } from "@/utils/message";
+
+const NotFound = React.lazy(() => import("@/components/NotFound"));
+
+function Layout() {
+  const dispatch = useDispatch();
+
+  const list = useMemo(() => {
+    return [
+      {
+        id: 100,
+        name: "热度统计",
+        path: "/",
+        Com: React.lazy(() => import("../A1Hot")),
+        inco: inco1,
+        incoAc: inco1Ac,
+      },
+      {
+        id: 200,
+        name: "资讯管理",
+        path: "/news",
+        Com: React.lazy(() => import("../A2News")),
+        inco: inco2,
+        incoAc: inco2Ac,
+      },
+      {
+        id: 300,
+        name: "馆藏管理",
+        path: "/goods",
+        Com: React.lazy(() => import("../A3Goods")),
+        inco: inco3,
+        incoAc: inco3Ac,
+      },
+      {
+        id: 400,
+        name: "场馆管理",
+        path: "/venue",
+        Com: React.lazy(() => import("../A4Venue")),
+        inco: inco4,
+        incoAc: inco4Ac,
+      },
+      {
+        id: 500,
+        name: "游客服务",
+        path: "/guest",
+        Com: React.lazy(() => import("../A5Guest")),
+        inco: inco5,
+        incoAc: inco5Ac,
+      },
+    ];
+  }, []);
+
+  // 进页面获取所有下拉信息
+  useEffect(() => {
+    dispatch(getDictListAPI());
+  }, [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 MessageFu.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) {
+      MessageFu.success("修改成功!");
+      loginExit();
+    }
+  };
+
+  // 点击退出登录
+  const loginExit = () => {
+    removeTokenInfo();
+    // history.push("/login");
+    // 跳到前台页面
+    window.location.href = "/web/index.html";
+  };
+
+  return (
+    <div className={styles.Layout}>
+      {/* 左边 */}
+      <div className="layoutLeft">
+        <div className="layoutLeftTop">
+          <img src={logonImg} alt="" />
+        </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: "不能为空!" },
+              { min: 6, max: 15, message: "密码长度为6-15个字符!" },
+            ]}
+          >
+            <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;

+ 142 - 0
管理后台/src/pages/Login/index.module.scss

@@ -0,0 +1,142 @@
+.Login {
+  width: 100%;
+  height: 100%;
+  background-image: url('../../assets/img/bg.jpg');
+  background-size: cover;
+  position: relative;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background-color: rgba(0, 0, 0, .4);
+  }
+
+  :global {
+
+    .main {
+      border-top: 3px solid rgba(200, 185, 146, .8);
+      border-radius: 6px;
+      position: absolute;
+      top: 50%;
+      right: 240px;
+      transform: translateY(-50%);
+      width: 530px;
+      height: 720px;
+      padding-top: 60px;
+      text-align: center;
+      background-image: url('../../assets/img/loginBox.jpg');
+      background-size: 100% 100%;
+
+      .logo {
+        margin: 30px auto 0;
+        width: 375px;
+        height: 120px;
+      }
+
+
+      .inputBox {
+        width: 100%;
+
+        input::-webkit-input-placeholder {
+          /* WebKit browsers */
+          color: rgba(157, 107, 57, .5);
+        }
+
+        input:-moz-placeholder {
+          /* Mozilla Firefox 4 to 18 */
+          color: rgba(157, 107, 57, .5);
+        }
+
+        input::-moz-placeholder {
+          /* Mozilla Firefox 19+ */
+          color: rgba(157, 107, 57, .5);
+        }
+
+        input:-ms-input-placeholder {
+          /* Internet Explorer 10+ */
+          color: rgba(157, 107, 57, .5);
+        }
+
+
+        .inputBoxRow {
+          width: 370px;
+          margin: 50px auto;
+
+          .ant-input-suffix .ant-input-password-icon {
+            color: var(--themeColor);
+            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 {
+          font-size: 18px !important;
+          -webkit-text-fill-color: var(--themeColor) !important;
+          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;
+          border: none;
+          border-bottom: 1px solid var(--themeColor);
+          border-radius: 0;
+          color: var(--themeColor);
+
+          .ant-input {
+            background-color: transparent;
+            width: 100%;
+            height: 60px;
+          }
+        }
+
+        .ant-input-affix-wrapper-focused {
+          box-shadow: none
+        }
+      }
+
+      .loginBtn {
+        margin-top: 100px;
+        .ant-btn {
+          border-radius: 25px;
+          font-size: 24px;
+          width: 375px;
+          height: 50px;
+        }
+      }
+
+    }
+  }
+}

+ 82 - 0
管理后台/src/pages/Login/index.tsx

@@ -0,0 +1,82 @@
+import styles from "./index.module.scss";
+
+import { Input, Button } from "antd";
+import { UserOutlined, LockOutlined } from "@ant-design/icons";
+import { useState } from "react";
+import { Base64 } from "js-base64";
+import encodeStr from "@/utils/pass";
+import { setTokenInfo } from "@/utils/storage";
+import history from "@/utils/history";
+import { MessageFu } from "@/utils/message";
+import { userLoginAPI } from "@/store/action/layout";
+import logoImg from "@/assets/img/logo.png";
+
+export default function Login() {
+  // 账号密码
+  const [userName, setUserName] = useState("admin");
+  const [passWord, setPassWord] = useState("");
+
+  // 键盘按下回车事件
+  const keyUpEntFu = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    if (e.key === "Enter") loginClickFu();
+  };
+  // 点击登录
+  const loginClickFu = async () => {
+    // 非空判断
+    if (userName === "") return MessageFu.warning("请输入用户名!");
+    else if (passWord === "") return MessageFu.warning("请输入密码!");
+    const obj = {
+      userName,
+      passWord: encodeStr(Base64.encode(passWord)),
+    };
+    const res: any = await userLoginAPI(obj);
+    if (res.code === 0) {
+      MessageFu.success("登录成功");
+      // 用户信息存到本地
+      setTokenInfo(res.data);
+      history.push("/");
+    } else if (res.code === 3014)
+      MessageFu.warning("用户名不存在或密码错误,请联系管理员!");
+  };
+
+  return (
+    <div className={styles.Login}>
+      <div className="main">
+        <div className="logo">
+          <img src={logoImg} alt="" />
+        </div>
+        {/* 账号密码输入框 */}
+        <div className="inputBox">
+          <div className="inputBoxRow">
+            <Input
+              disabled
+              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>
+  );
+}

+ 5 - 0
管理后台/src/pages/初始化组件/index.module.scss

@@ -0,0 +1,5 @@
+.AAAAA{
+  :global{
+    
+  }
+}

+ 14 - 0
管理后台/src/pages/初始化组件/index.tsx

@@ -0,0 +1,14 @@
+import React from "react";
+import styles from "./index.module.scss";
+ function AAAAA() {
+  
+  return (
+    <div className={styles.AAAAA}>
+      <h1>AAAAA</h1>
+    </div>
+  )
+}
+
+const MemoAAAAA = React.memo(AAAAA);
+
+export default MemoAAAAA;

+ 31 - 0
管理后台/src/store/action/A1Hot.ts

@@ -0,0 +1,31 @@
+import { HotSelectTimeType, HotSelectType } from "@/types";
+import http from "@/utils/http";
+
+/**
+ * 统计热门板块
+ */
+export const getHotModuleAPI = (dayScope: HotSelectTimeType) => {
+  return http.get(`cms/report/ranking/module?dayScope=${dayScope}`);
+};
+
+/**
+ * 统计-热门场馆
+ */
+export const getHotExhibitionAPI = (
+  dayScope: HotSelectTimeType,
+  type: HotSelectType
+) => {
+  return http.get(
+    `cms/report/ranking/exhibition?dayScope=${dayScope}&type=${type}`
+  );
+};
+
+/**
+ * 统计-热门场馆
+ */
+export const getHotGoodsAPI = (
+  dayScope: HotSelectTimeType,
+  type: HotSelectType
+) => {
+  return http.get(`cms/report/ranking/goods?dayScope=${dayScope}&type=${type}`);
+};

+ 72 - 0
管理后台/src/store/action/A2News.ts

@@ -0,0 +1,72 @@
+import { NewsSaveApiType, NewsTableApiType } from "@/types";
+import { domShowFu, progressDomFu } from "@/utils/domShow";
+import http from "@/utils/http";
+import axios from "axios";
+import store, { AppDispatch } from "..";
+
+const CancelToken = axios.CancelToken;
+/**
+ * 上传封面图和附件
+ */
+export const newsUploadAPI = (data: any) => {
+  domShowFu("#UpAsyncLoding", true);
+
+  return http.post("cms/news/upload", data, {
+    timeout: 0,
+    // 显示进度条
+    onUploadProgress: (e: any) => {
+      const complete = (e.loaded / e.total) * 100 || 0;
+      progressDomFu(complete + "%");
+    },
+    // 取消上传
+    cancelToken: new CancelToken(function executor(c) {
+      store.dispatch({
+        type: "layout/closeUpFile",
+        payload: { fu: c, state: true },
+      });
+    }),
+  });
+};
+
+/**
+ * 新增|编辑
+ */
+export const newSaveAPI = (data: NewsSaveApiType) => {
+  return http.post("cms/news/save", data);
+};
+
+/**
+ * 获取列表数据
+ */
+export const getNewsListAPI = (data: NewsTableApiType) => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post("cms/news/getList", data);
+    if (res.code === 0) {
+      dispatch({
+        type: "news/getList",
+        payload: res.data,
+      });
+    }
+  };
+};
+
+/**
+ * 删除
+ */
+export const newRemoveAPI = (id: number) => {
+  return http.get(`cms/news/remove/${id}`);
+};
+
+/**
+ * 详情
+ */
+export const getNewsDetailAPI = (id: number) => {
+  return http.get(`cms/news/detail/${id}`);
+};
+
+/**
+ * 拖动排序
+ */
+export const newSortApi = (id1: number, id2: number) => {
+  return http.get(`cms/news/sort/${id1}/${id2}`);
+};

+ 71 - 0
管理后台/src/store/action/A3Goods.ts

@@ -0,0 +1,71 @@
+import { GoodsTableType } from "@/types";
+import { domShowFu, progressDomFu } from "@/utils/domShow";
+import http from "@/utils/http";
+import axios from "axios";
+import store, { AppDispatch } from "..";
+/**
+ * 获取列表数据
+ */
+export const getGoodsListAPI = (data: any) => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post("cms/goods/pageList", data);
+    if (res.code === 0) {
+      dispatch({
+        type: "goods/getList",
+        payload: { list: res.data.records, total: res.data.total },
+      });
+    }
+  };
+};
+
+/**
+ * 内容-是否显示
+ */
+export const goodsDisplayAPI = (id: number, display: number) => {
+  return http.get(`cms/goods/display/${id}/${display}`);
+};
+
+/**
+ * 删除藏品
+ */
+export const goodsRemoveAPI = (id: number) => {
+  return http.get(`cms/goods/remove/${id}`);
+};
+
+const CancelToken = axios.CancelToken;
+/**
+ * 上传封面图和附件
+ */
+export const goodsUploadAPI = (data: any) => {
+  domShowFu("#UpAsyncLoding", true);
+
+  return http.post("cms/goods/upload", data, {
+    timeout: 0,
+    // 显示进度条
+    onUploadProgress: (e: any) => {
+      const complete = (e.loaded / e.total) * 100 || 0;
+      progressDomFu(complete + "%");
+    },
+    // 取消上传
+    cancelToken: new CancelToken(function executor(c) {
+      store.dispatch({
+        type: "layout/closeUpFile",
+        payload: { fu: c, state: true },
+      });
+    }),
+  });
+};
+
+/**
+ * 新增|编辑
+ */
+export const getGoodsSaveAPI = (data: GoodsTableType) => {
+  return http.post("cms/goods/save", data);
+};
+
+/**
+ * 通过id获取详情
+ */
+export const getGoodsInfoByIdAPI = (id: number) => {
+  return http.get(`cms/goods/detail/${id}`);
+};

+ 65 - 0
管理后台/src/store/action/A4Venue.ts

@@ -0,0 +1,65 @@
+import { VenueTableApiType, VenueTableType } from "@/types";
+import { domShowFu, progressDomFu } from "@/utils/domShow";
+import http from "@/utils/http";
+import axios from "axios";
+import store, { AppDispatch } from "..";
+
+const CancelToken = axios.CancelToken;
+/**
+ * 上传封面图和附件
+ */
+export const venueUploadAPI = (data: any) => {
+  domShowFu("#UpAsyncLoding", true);
+
+  return http.post("cms/exhibition/upload", data, {
+    timeout: 0,
+    // 显示进度条
+    onUploadProgress: (e: any) => {
+      const complete = (e.loaded / e.total) * 100 || 0;
+      progressDomFu(complete + "%");
+    },
+    // 取消上传
+    cancelToken: new CancelToken(function executor(c) {
+      store.dispatch({
+        type: "layout/closeUpFile",
+        payload: { fu: c, state: true },
+      });
+    }),
+  });
+};
+
+/**
+ * 获取列表数据
+ */
+export const getVenueListAPI = (data: VenueTableApiType) => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post("cms/exhibition/getList", data);
+    if (res.code === 0) {
+      dispatch({
+        type: "venue/getList",
+        payload: res.data,
+      });
+    }
+  };
+};
+
+/**
+ * 详情
+ */
+export const getVenueDetailAPI = (id: number) => {
+  return http.get(`cms/exhibition/detail/${id}`);
+};
+
+/**
+ * 拖动排序
+ */
+export const venueSortApi = (id1: number, id2: number) => {
+  return http.get(`cms/exhibition/sort/${id1}/${id2}`);
+};
+
+/**
+ * 新增|编辑
+ */
+export const setVenueSaveApi = (data: VenueTableType) => {
+  return http.post("cms/exhibition/save", data);
+};

+ 22 - 0
管理后台/src/store/action/A5Guest.ts

@@ -0,0 +1,22 @@
+import http from "@/utils/http";
+
+/**
+ * 配置-获取
+ */
+export const getGuestInfoAPI = () => {
+  return http.get("cms/visitor/getConfig");
+};
+
+/**
+ * 修改参观指南
+ */
+export const setGuestGuideAPI = (data: string) => {
+  return http.post("cms/visitor/setConfigByGuide", { content: data });
+};
+
+/**
+ * 参观须知-修改
+ */
+export const setGuestNoticeAPI = (data: string) => {
+  return http.post("cms/visitor/setConfigByNotice", { content: data });
+};

+ 39 - 0
管理后台/src/store/action/layout.ts

@@ -0,0 +1,39 @@
+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");
+    if (res.code === 0) {
+      const list: DictListTypeAPI[] = res.data;
+      const obj = {
+        age: [{ label: "全部", value: "", type: "age" }],
+        texture: [{ label: "全部", value: "", type: "texture" }],
+      } as DictListTypeObj;
+      list.forEach((v) => {
+        if (obj[v.type])
+          obj[v.type].push({ label: v.name, value: v.name, type: v.type });
+      });
+
+      dispatch({ type: "layout/getDictList", payload: obj });
+    }
+  };
+};

+ 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

+ 29 - 0
管理后台/src/store/reducer/A2News.ts

@@ -0,0 +1,29 @@
+import { NewsTableType } from "@/types";
+
+// 初始化状态
+const initState = {
+  // 表格数据
+  tableInfo: [] as NewsTableType[],
+
+  // 表格的滚动位置
+  sroolNum: 0,
+};
+
+// 定义 action 类型
+type NewsActionType =
+  | { type: "news/getList"; payload: NewsTableType[] }
+  | { type: "news/setSroolNum"; payload: number };
+// 频道 reducer
+export default function newsReducer(state = initState, action: NewsActionType) {
+  switch (action.type) {
+    // 表格数据
+    case "news/getList":
+      return { ...state, tableInfo: action.payload };
+    // 表格的滚动位置
+    case "news/setSroolNum":
+      return { ...state, sroolNum: action.payload };
+
+    default:
+      return state;
+  }
+}

+ 30 - 0
管理后台/src/store/reducer/A3Goods.ts

@@ -0,0 +1,30 @@
+import { GoodsTableType } from "@/types";
+
+// 初始化状态
+const initState = {
+  // 列表数据
+  tableInfo: {
+    list: [] as GoodsTableType[],
+    total: 0,
+  },
+};
+
+// 定义 action 类型
+type GoodsActionType = {
+  type: "goods/getList";
+  payload: { list: GoodsTableType[]; 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;
+  }
+}

+ 21 - 0
管理后台/src/store/reducer/A4Venue.ts

@@ -0,0 +1,21 @@
+import { VenueTableType } from "@/types";
+
+// 初始化状态
+const initState = {
+  // 表格数据
+  tableInfo: [] as VenueTableType[],
+};
+
+// 定义 action 类型
+type VenueActionType = { type: "venue/getList"; payload: VenueTableType[] };
+
+// 频道 reducer
+export default function venueReducer(state = initState, action: VenueActionType) {
+  switch (action.type) {
+    // 表格数据
+    case "venue/getList":
+      return { ...state, tableInfo: action.payload };
+    default:
+      return state;
+  }
+}

+ 19 - 0
管理后台/src/store/reducer/index.ts

@@ -0,0 +1,19 @@
+// 导入合并reducer的依赖
+import { combineReducers } from 'redux'
+import newsReducer from './A2News'
+import goodsReducer from './A3Goods'
+import venueReducer from './A4Venue'
+
+// 导入 登录 模块的 reducer
+import layoutReducer from './layout'
+
+// 合并 reducer
+const rootReducer = combineReducers({
+  layoutStore: layoutReducer,
+  newsReducer:newsReducer,
+  goodsReducer:goodsReducer,
+  venueReducer:venueReducer
+})
+
+// 默认导出
+export default rootReducer

+ 70 - 0
管理后台/src/store/reducer/layout.ts

@@ -0,0 +1,70 @@
+import { DictListTypeObj } from "@/types";
+import { MessageType } from "@/utils/message";
+
+// 初始化状态
+const initState = {
+  // 所有图片点击预览查看大图
+  lookBigImg: {
+    url: "",
+    show: false,
+  },
+  // 视频的src
+  videoSrc: "",
+
+  // 所有的下拉框数据
+  dictList: {
+    age: [],
+    texture: [],
+  } as DictListTypeObj,
+
+  message: {
+    txt: "",
+    type: "info",
+    duration: 3,
+  } as MessageType,
+  // 上传文件点击取消
+  closeUpFile: {
+    fu: () => {},
+    state: false,
+  },
+};
+
+// 定义 action 类型
+type LayoutActionType =
+  | { type: "layout/lookBigImg"; payload: { url: string; show: boolean } }
+  | { type: "layout/lookVideo"; payload: string }
+  | { type: "layout/message"; payload: MessageType }
+  | { type: "layout/getDictList"; payload: DictListTypeObj }
+  | {
+      type: "layout/closeUpFile";
+      payload: {
+        fu: () => void;
+        state: boolean;
+      };
+    };
+
+// 频道 reducer
+export default function layoutReducer(
+  state = initState,
+  action: LayoutActionType
+) {
+  switch (action.type) {
+    // 所有图片点击预览查看大图
+    case "layout/lookBigImg":
+      return { ...state, lookBigImg: action.payload };
+    // 查看视频
+    case "layout/lookVideo":
+      return { ...state, videoSrc: action.payload };
+    // 所有的下拉框数据
+    case "layout/getDictList":
+      return { ...state, dictList: action.payload };
+    // antd轻提示(兼容360浏览器)
+    case "layout/message":
+      return { ...state, message: action.payload };
+    // 上传文件点击取消
+    case "layout/closeUpFile":
+      return { ...state, closeUpFile: action.payload };
+    default:
+      return state;
+  }
+}

+ 8 - 0
管理后台/src/types/api/A1Hot.d.ts

@@ -0,0 +1,8 @@
+export type HotSelectType = "visit" | "star" | "";
+
+export type HotSelectTimeType = 1 | 7 | 30 | "";
+
+export type HotData1Type = {
+  pcs: number;
+  groupKey: string;
+};

+ 41 - 0
管理后台/src/types/api/A2News.d.ts

@@ -0,0 +1,41 @@
+export type NewsTableType = {
+	createTime: string;
+	creatorId: number;
+	creatorName: string;
+	description: string;
+	dirCode: string;
+	display: number;
+	fileIds: string;
+	id: number;
+	name: string;
+	newsDay: string;
+	thumb: string;
+	type: string;
+	updateTime: string;
+}
+
+export type NewsSaveApiType = {
+  description?: string;
+  dirCode: string;
+  display: number;
+  fileIds: string;
+  id: number | null;
+  name: string;
+  newsDay: string;
+  thumb: string;
+  type: "news" | "video" | "img";
+};
+
+export type NewsTableApiType = {
+  type: "news" | "video" | "img";
+  searchKey: string;
+  display: number|null;
+  pageSize: number;
+  pageNum: number;
+};
+
+export type NewsButtonType = {
+  id: number;
+  name: string;
+  type: "news" | "video" | "img";
+};

+ 33 - 0
管理后台/src/types/api/A3Goods.d.ts

@@ -0,0 +1,33 @@
+export type GoodsTableType = {
+  createTime: string;
+  creatorId: number;
+  creatorName: string;
+  description: string;
+  dictAge: string;
+  dictLevel: string;
+  dictSource: string;
+  dictTexture: string;
+  dirCode: string;
+  display: number;
+  fileIds: string;
+  id: number;
+  isBarrage: number;
+  name: string;
+  num: string;
+  thumb: string;
+  topic: string;
+  type: string;
+  updateTime: string;
+  tagType?:string
+  tagCountry?:string
+};
+
+export type FileListType = {
+  fileName?: string;
+  filePath?: string;
+  id?: number;
+  isFrame?: boolean;
+  done?: boolean;
+  type?: "model" | "img" | "audio" | "video";
+};
+

+ 22 - 0
管理后台/src/types/api/A4Venue.d.ts

@@ -0,0 +1,22 @@
+export type VenueTableType = {
+  createTime: string;
+  creatorId?: any;
+  creatorName: string;
+  description: string;
+  dirCode: string;
+  fileIds: string;
+  id: number|null;
+  location: string;
+  name: string;
+  sort?: any;
+  thumb: string;
+  updateTime: string;
+};
+
+export type VenueTableApiType = {
+  searchKey: string;
+  startTime: string;
+  endTime: string;
+  pageSize: number;
+  pageNum: number;
+};

+ 24 - 0
管理后台/src/types/api/layot.d.ts

@@ -0,0 +1,24 @@
+
+export type DictListTypeObj = {
+  age: DictListType[];
+  texture: DictListType[];
+};
+
+export type DictListTypeAPI = {
+  createTime: null;
+  creatorId: null;
+  creatorName: string;
+  display: number;
+  id: string;
+  name: string;
+  parentId: null;
+  sort: null;
+  type: "age" | "texture" 
+  updateTime: null;
+};
+
+export type ImgListType = {
+  fileName: string;
+  filePath: string;
+  id: number;
+};

+ 6 - 0
管理后台/src/types/declaration.d.ts

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

+ 6 - 0
管理后台/src/types/index.d.ts

@@ -0,0 +1,6 @@
+export * from './api/layot'
+export * from './api/A1Hot'
+
+export * from './api/A2News'
+export * from './api/A3Goods'
+export * from './api/A4Venue'

+ 35 - 0
管理后台/src/utils/domShow.ts

@@ -0,0 +1,35 @@
+import store from "@/store";
+
+// 加载和上传的盒子的显示隐藏
+export const domShowFu = (ele: string, val: boolean) => {
+  const dom: HTMLDivElement = document.querySelector(ele)!;
+  if (val) {
+    dom.style.opacity = "1";
+    dom.style.pointerEvents = "auto";
+  } else {
+    dom.style.opacity = "0";
+    dom.style.pointerEvents = "none";
+  }
+};
+
+// 上传附件的进度条
+let progressDom: HTMLDivElement = document.querySelector("#progress")!;
+export const progressDomFu = (val: string) => {
+  if (!progressDom) progressDom = document.querySelector("#progress")!;
+  progressDom.style.width = val;
+};
+
+// 上传附件的dom操作
+export const fileDomInitialFu = () => {
+  // 隐藏进度条的dom
+  domShowFu("#UpAsyncLoding", false);
+  progressDomFu("0%");
+  // 初始化 上传附件 的状态
+  setTimeout(() => {
+    if (store.getState().layoutStore.closeUpFile.state)
+      store.dispatch({
+        type: "layout/closeUpFile",
+        payload: { fu: () => {}, state: false },
+      });
+  }, 200);
+};

+ 17 - 0
管理后台/src/utils/history.ts

@@ -0,0 +1,17 @@
+import { createHashHistory  } from 'history'
+const history = createHashHistory()
+export default history
+
+export const urlParameter = (data: string) => {
+  if (data) {
+    const query = data.substring(data.indexOf("?") + 1);
+    const arr = query.split("&");
+    const params = {} as any;
+    arr.forEach((v) => {
+      const key = v.substring(0, v.indexOf("="));
+      const val = v.substring(v.indexOf("=") + 1);
+      params[key] = val;
+    });
+    return params;
+  } else return {};
+};

+ 93 - 0
管理后台/src/utils/http.ts

@@ -0,0 +1,93 @@
+import axios from "axios";
+import history from "./history";
+import { getTokenInfo, removeTokenInfo } from "./storage";
+import store from "@/store";
+import { MessageFu } from "./message";
+import { domShowFu } from "./domShow";
+// 请求基地址
+export const baseURL =
+  // 线下的图片地址需要加上/api/
+  // process.env.NODE_ENV === "development"
+  //   ? "http://192.168.20.55:8042/api/"
+  //   : "";
+  process.env.NODE_ENV === "development" ? "https://bengbubwg.4dage.com" : "";
+
+// 处理  类型“AxiosResponse<any, any>”上不存在属性“code”
+declare module "axios" {
+  interface AxiosResponse {
+    code: number;
+    // 这里追加你的参数
+  }
+}
+
+// 创建 axios 实例
+const http = axios.create({
+  // --------线下的地址不用加/api/
+  // baseURL: baseURL,
+
+  // --------打包或线上环境接口需要加上api/
+  baseURL: baseURL + "/api/",
+  timeout: 5000,
+});
+
+let axajInd = 0;
+
+// 请求拦截器
+http.interceptors.request.use(
+  function (config: any) {
+    // 发请求前打开加载提示
+    domShowFu("#AsyncSpinLoding", true);
+
+    axajInd++;
+
+    const { token } = getTokenInfo();
+    if (token) config.headers.token = token;
+    return config;
+  },
+  function (err) {
+    return Promise.reject(err);
+  }
+);
+
+let timeId = -1;
+
+// 响应拦截器
+http.interceptors.response.use(
+  function (response) {
+    // 请求回来的关闭加载提示
+    axajInd--;
+    if (axajInd === 0) {
+      domShowFu("#AsyncSpinLoding", false);
+    }
+    if (response.data.code === 5001 || response.data.code === 5002) {
+      clearTimeout(timeId);
+      timeId = window.setTimeout(() => {
+        removeTokenInfo();
+        MessageFu.warning("登录失效!");
+        history.push("/login");
+      }, 200);
+    } else if (response.data.code === 0) {
+      // MessageFu.success(response.data.msg);
+    } else MessageFu.warning(response.data.msg);
+
+    return response.data;
+  },
+  async function (err) {
+    clearTimeout(timeId);
+    timeId = window.setTimeout(() => {
+      axajInd = 0;
+      domShowFu("#AsyncSpinLoding", false);
+      // 如果因为网络原因,response没有,给提示消息
+      if (!err.response) {
+        if (store.getState().layoutStore.closeUpFile.state)
+          MessageFu.warning("取消上传!");
+        else MessageFu.error("网络繁忙,请稍后重试!");
+      } else MessageFu.error("响应错误,请联系管理员!");
+    }, 100);
+
+    return Promise.reject(err);
+  }
+);
+
+// 导出 axios 实例
+export default http;

+ 50 - 0
管理后台/src/utils/message.ts

@@ -0,0 +1,50 @@
+import store from "@/store";
+
+export type MessageType = {
+  txt: string;
+  type: "info" | "success" | "error" | "warning";
+  duration: number;
+};
+
+export const MessageFu = {
+  info: (txt: string, duration?: number) => {
+    store.dispatch({
+      type: "layout/message",
+      payload: {
+        txt,
+        type: "info",
+        duration: duration === undefined ? 3 : duration,
+      },
+    });
+  },
+  success: (txt: string, duration?: number) => {
+    store.dispatch({
+      type: "layout/message",
+      payload: {
+        txt,
+        type: "success",
+        duration: duration === undefined ? 3 : duration,
+      },
+    });
+  },
+  error: (txt: string, duration?: number) => {
+    store.dispatch({
+      type: "layout/message",
+      payload: {
+        txt,
+        type: "error",
+        duration: duration === undefined ? 3 : duration,
+      },
+    });
+  },
+  warning: (txt: string, duration?: number) => {
+    store.dispatch({
+      type: "layout/message",
+      payload: {
+        txt,
+        type: "warning",
+        duration: duration === undefined ? 3 : duration,
+      },
+    });
+  },
+};

+ 100 - 0
管理后台/src/utils/pass.ts

@@ -0,0 +1,100 @@
+function randomWord(randomFlag: boolean, min: number, max: number = 15) {
+  let str = "";
+  let range = min;
+  const arr = [
+    "0",
+    "1",
+    "2",
+    "3",
+    "4",
+    "5",
+    "6",
+    "7",
+    "8",
+    "9",
+    "a",
+    "b",
+    "c",
+    "d",
+    "e",
+    "f",
+    "g",
+    "h",
+    "i",
+    "j",
+    "k",
+    "l",
+    "m",
+    "n",
+    "o",
+    "p",
+    "q",
+    "r",
+    "s",
+    "t",
+    "u",
+    "v",
+    "w",
+    "x",
+    "y",
+    "z",
+    "A",
+    "B",
+    "C",
+    "D",
+    "E",
+    "F",
+    "G",
+    "H",
+    "I",
+    "J",
+    "K",
+    "L",
+    "M",
+    "N",
+    "O",
+    "P",
+    "Q",
+    "R",
+    "S",
+    "T",
+    "U",
+    "V",
+    "W",
+    "X",
+    "Y",
+    "Z",
+  ];
+  // 随机产生
+  if (randomFlag) {
+    range = Math.round(Math.random() * (max - min)) + min;
+  }
+  for (var i = 0; i < range; i++) {
+    const pos = Math.round(Math.random() * (arr.length - 1));
+    str += arr[pos];
+  }
+  return str;
+}
+
+const encodeStr = (str: string, strv = "") => {
+  const NUM = 2;
+  const front = randomWord(false, 8);
+  const middle = randomWord(false, 8);
+  const end = randomWord(false, 8);
+
+  const str1 = str.substring(0, NUM);
+  const str2 = str.substring(NUM);
+
+  if (strv) {
+    const strv1 = strv.substring(0, NUM);
+    const strv2 = strv.substring(NUM);
+    return [
+      front + str2 + middle + str1 + end,
+      front + strv2 + middle + strv1 + end,
+    ];
+  }
+
+  return front + str2 + middle + str1 + end;
+};
+
+export default encodeStr;

+ 34 - 0
管理后台/src/utils/storage.ts

@@ -0,0 +1,34 @@
+// ------------------------------------token的本地存储------------------------------------
+
+// 用户 Token 的本地缓存键名,自己定义
+const TOKEN_KEY = 'BBSBWG_HT_USER_INFO'
+
+/**
+ * 从本地缓存中获取 Token 信息
+ */
+export const getTokenInfo = (): any => {
+  return JSON.parse(localStorage.getItem(TOKEN_KEY) || '{}')
+}
+
+/**
+ * 将 Token 信息存入缓存
+ * @param {Object} tokenInfo 从后端获取到的 Token 信息
+ */
+export const setTokenInfo = (tokenInfo: any): void => {
+  localStorage.setItem(TOKEN_KEY, JSON.stringify(tokenInfo))
+}
+
+/**
+ * 删除本地缓存中的 Token 信息
+ */
+export const removeTokenInfo = (): void => {
+  localStorage.removeItem(TOKEN_KEY)
+}
+
+/**
+ * 判断本地缓存中是否存在 Token 信息
+ */
+export const hasToken = (): boolean => {
+  return Boolean(getTokenInfo().token)
+}
+

+ 27 - 0
管理后台/tsconfig.json

@@ -0,0 +1,27 @@
+{
+  "extends": "./path.tsconfig.json",
+  "compilerOptions": {
+    "target": "es5",
+    "lib": [
+      "dom",
+      "dom.iterable",
+      "esnext"
+    ],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noFallthroughCasesInSwitch": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "noEmit": true,
+    "jsx": "react-jsx"
+  },
+  "include": [
+    "src"
+  ]
+}