shaogen1995 преди 1 година
ревизия
e3c6353074
променени са 93 файла, в които са добавени 47927 реда и са изтрити 0 реда
  1. 23 0
      .gitignore
  2. 2 0
      README.md
  3. 10 0
      config-overrides.js
  4. 31293 0
      package-lock.json
  5. 65 0
      package.json
  6. 8 0
      path.tsconfig.json
  7. BIN
      public/favicon.ico
  8. 43 0
      public/index.html
  9. 65 0
      src/App.tsx
  10. BIN
      src/assets/img/IMGerror.png
  11. BIN
      src/assets/img/loading.gif
  12. BIN
      src/assets/img/loginBac.jpg
  13. BIN
      src/assets/img/logo.png
  14. BIN
      src/assets/img/user.png
  15. 206 0
      src/assets/styles/base.css
  16. 283 0
      src/assets/styles/base.less
  17. 21 0
      src/components/AsyncSpinLoding/index.module.scss
  18. 15 0
      src/components/AsyncSpinLoding/index.tsx
  19. 32 0
      src/components/AuthRoute/index.tsx
  20. 51 0
      src/components/ImageLazy/index.module.scss
  21. 68 0
      src/components/ImageLazy/index.tsx
  22. 65 0
      src/components/LookDom/index.module.scss
  23. 52 0
      src/components/LookDom/index.tsx
  24. 29 0
      src/components/Message/index.tsx
  25. 46 0
      src/components/MyPopconfirm.tsx
  26. 18 0
      src/components/MyTable/index.module.scss
  27. 161 0
      src/components/MyTable/index.tsx
  28. 26 0
      src/components/NotFound/index.tsx
  29. 10 0
      src/components/SpinLoding/index.module.scss
  30. 13 0
      src/components/SpinLoding/index.tsx
  31. 43 0
      src/components/UpAsyncLoding/index.module.scss
  32. 38 0
      src/components/UpAsyncLoding/index.tsx
  33. 82 0
      src/components/ZRichText/index.module.scss
  34. 202 0
      src/components/ZRichText/index.tsx
  35. 101 0
      src/components/ZupOne/index.module.scss
  36. 282 0
      src/components/ZupOne/index.tsx
  37. 148 0
      src/components/ZupTypes/index.module.scss
  38. 481 0
      src/components/ZupTypes/index.tsx
  39. 92 0
      src/components/ZupVideos/index.module.scss
  40. 205 0
      src/components/ZupVideos/index.tsx
  41. 41 0
      src/index.tsx
  42. 5 0
      src/pages/A1event/index.module.scss
  43. 14 0
      src/pages/A1event/index.tsx
  44. 5 0
      src/pages/A2exhibition/index.module.scss
  45. 14 0
      src/pages/A2exhibition/index.tsx
  46. 5 0
      src/pages/A3holding/index.module.scss
  47. 14 0
      src/pages/A3holding/index.tsx
  48. 5 0
      src/pages/A4study/index.module.scss
  49. 14 0
      src/pages/A4study/index.tsx
  50. 5 0
      src/pages/A5publish/index.module.scss
  51. 14 0
      src/pages/A5publish/index.tsx
  52. 5 0
      src/pages/A6home/index.module.scss
  53. 14 0
      src/pages/A6home/index.tsx
  54. 5 0
      src/pages/A7poster/index.module.scss
  55. 14 0
      src/pages/A7poster/index.tsx
  56. 5 0
      src/pages/A8dict/index.module.scss
  57. 14 0
      src/pages/A8dict/index.tsx
  58. 80 0
      src/pages/Layout/data.ts
  59. 182 0
      src/pages/Layout/index.module.scss
  60. 296 0
      src/pages/Layout/index.tsx
  61. 150 0
      src/pages/Login/index.module.scss
  62. 134 0
      src/pages/Login/index.tsx
  63. 19 0
      src/pages/Z1user/UserAdd/index.module.scss
  64. 127 0
      src/pages/Z1user/UserAdd/index.tsx
  65. 152 0
      src/pages/Z1user/Z1auth.tsx
  66. 78 0
      src/pages/Z1user/index.module.scss
  67. 227 0
      src/pages/Z1user/index.tsx
  68. 25 0
      src/pages/Z2log/index.module.scss
  69. 100 0
      src/pages/Z2log/index.tsx
  70. 5 0
      src/pages/初始化组件/index.module.scss
  71. 14 0
      src/pages/初始化组件/index.tsx
  72. 68 0
      src/store/action/Z1user.ts
  73. 17 0
      src/store/action/Z2log.ts
  74. 49 0
      src/store/action/layout.ts
  75. 20 0
      src/store/index.ts
  76. 28 0
      src/store/reducer/Z1user.ts
  77. 27 0
      src/store/reducer/Z2log.ts
  78. 17 0
      src/store/reducer/index.ts
  79. 65 0
      src/store/reducer/layout.ts
  80. 31 0
      src/types/api/Z1user.d.ts
  81. 11 0
      src/types/api/Z2log.d.ts
  82. 25 0
      src/types/api/layot.d.ts
  83. 8 0
      src/types/declaration.d.ts
  84. 3 0
      src/types/index.d.ts
  85. 35 0
      src/utils/domShow.ts
  86. 3 0
      src/utils/history.ts
  87. 110 0
      src/utils/http.ts
  88. 50 0
      src/utils/message.ts
  89. 100 0
      src/utils/pass.ts
  90. 40 0
      src/utils/storage.ts
  91. 122 0
      src/utils/tableData.ts
  92. 27 0
      tsconfig.json
  93. 11010 0
      yarn.lock

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

+ 2 - 0
README.md

@@ -0,0 +1,2 @@
+1.使用 yarn。 npm有问题
+

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

Файловите разлики са ограничени, защото са твърде много
+ 31293 - 0
package-lock.json


+ 65 - 0
package.json

@@ -0,0 +1,65 @@
+{
+  "name": "demo2",
+  "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.8.3",
+    "antd-mobile": "^5.30.0",
+    "axios": "^1.1.3",
+    "braft-editor": "^2.3.9",
+    "braft-utils": "^3.0.12",
+    "dayjs": "^1.11.10",
+    "js-base64": "^3.7.3",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-redux": "^8.0.4",
+    "react-router-dom": "5.3",
+    "react-scripts": "5.0.1",
+    "react-sortablejs": "^6.1.4",
+    "redux": "^4.2.0",
+    "redux-devtools-extension": "^2.13.9",
+    "redux-thunk": "^2.4.1",
+    "sass": "^1.55.0",
+    "typescript": "^4.8.4",
+    "web-vitals": "^2.1.4"
+  },
+  "scripts": {
+    "dev": "react-app-rewired start",
+    "build": "react-app-rewired build",
+    "test": "react-app-rewired test",
+    "eject": "react-scripts eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  },
+  "devDependencies": {
+    "@types/history": "^5.0.0",
+    "@types/react-router-dom": "^5.3.3",
+    "customize-cra": "^1.0.0",
+    "react-app-rewired": "^2.2.1"
+  },
+  "homepage": "."
+}

+ 8 - 0
path.tsconfig.json

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

BIN
public/favicon.ico


+ 43 - 0
public/index.html

@@ -0,0 +1,43 @@
+<!DOCTYPE html>
+<html lang="zh">
+  <head>
+    <meta charset="utf-8" />
+    <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" />
+    <link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
+    <!--
+      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 MessageCom from "./components/Message";
+import LookDom from "./components/LookDom";
+const Layout = React.lazy(() => import("./pages/Layout"));
+const Login = React.lazy(() => import("./pages/Login"));
+
+export default function App() {
+  // 从仓库中获取查看图片的信息
+  const lookBigImg = useSelector(
+    (state: RootState) => state.A0Layout.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 />
+
+      {/* 查看视频音频 */}
+      <LookDom />
+
+      {/* antd 轻提示 ---兼容360浏览器 */}
+      <MessageCom />
+    </>
+  );
+}

BIN
src/assets/img/IMGerror.png


BIN
src/assets/img/loading.gif


BIN
src/assets/img/loginBac.jpg


BIN
src/assets/img/logo.png


BIN
src/assets/img/user.png


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

@@ -0,0 +1,206 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+html {
+  height: 100%;
+  font-size: 14px;
+}
+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;
+}
+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;
+  min-height: 100px !important;
+}
+/* 主题色 */
+:root {
+  --themeColor: #CC1424;
+  --themeColor2: #272727;
+}
+/* 找不到页面 */
+.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);
+}
+.ant-popconfirm {
+  width: 240px;
+}
+#root {
+  width: 100vw;
+  height: 100vh;
+  min-width: 1600px;
+  min-height: 900px;
+  overflow: auto;
+  overflow-y: overlay;
+  /* 普通文字按钮的颜色 */
+  /* 按钮的危险颜色 */
+  /* antd分页器样式 */
+  /* 表格的图片居中 */
+  /* antd图片预览组件 */
+  /* antd表格居中 */
+}
+#root a {
+  text-decoration: none;
+  color: black;
+  outline: none;
+}
+#root .iconHoverTit {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+#root .iconHoverTitTxt {
+  background-color: var(--themeColor);
+  color: #fff;
+  width: 16px;
+  height: 16px;
+  line-height: 16px;
+  text-align: center;
+  font-size: 12px;
+  border-radius: 50%;
+}
+#root .ant-btn-text {
+  color: var(--themeColor);
+}
+#root .ant-btn-text:disabled {
+  cursor: not-allowed;
+  color: rgba(0, 0, 0, 0.25);
+}
+#root .ant-btn-text.ant-btn-dangerous {
+  color: #ff4d4d;
+}
+#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;
+}
+#root #A2Table3 .ant-table-row-expand-icon {
+  background-color: var(--themeColor);
+  color: #fff;
+}
+#root #A2Table3 .ant-table-cell-with-append {
+  display: flex;
+  justify-content: flex-start;
+}
+[hidden] {
+  display: none !important;
+}
+#upInput {
+  display: none;
+}
+#upInput2 {
+  display: none;
+}
+.pageTitle {
+  font-size: 18px;
+  font-weight: 700;
+  position: absolute;
+  z-index: 11;
+  top: -56px;
+  left: -18px;
+  padding-left: 40px;
+}
+.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;
+}

+ 283 - 0
src/assets/styles/base.less

@@ -0,0 +1,283 @@
+* {
+  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;
+}
+
+
+
+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;
+  min-height: 100px !important;
+}
+
+/* 主题色 */
+:root {
+  --themeColor: #CC1424;
+  --themeColor2: #272727;
+}
+
+
+
+
+
+/* 找不到页面 */
+.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);
+}
+
+// 气泡框闪烁问题
+.ant-popconfirm{
+  width: 240px;
+}
+
+// 重置antd样式
+#root {
+  width: 100vw;
+  height: 100vh;
+  min-width: 1600px;
+  min-height: 900px;
+  overflow: auto;
+  overflow-y: overlay;
+
+  a {
+    text-decoration: none;
+    color: black;
+    outline: none;
+  }
+
+  // ?的提示
+  .iconHoverTit {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .iconHoverTitTxt {
+    background-color: var(--themeColor);
+    color: #fff;
+    width: 16px;
+    height: 16px;
+    line-height: 16px;
+    text-align: center;
+    font-size: 12px;
+    border-radius: 50%;
+  }
+
+  // a {
+  //   color: var(--themeColor);
+  // }
+
+  /* 普通文字按钮的颜色 */
+  .ant-btn-text {
+    color: var(--themeColor);
+  }
+
+  .ant-btn-text:disabled {
+    cursor: not-allowed;
+    color: rgba(0, 0, 0, 0.25);
+  }
+
+  /* 按钮的危险颜色 */
+  .ant-btn-text.ant-btn-dangerous {
+    color: #ff4d4d;
+  }
+
+  /* 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;
+  }
+
+
+  // 树型 表格 定制化
+  #A2Table3 {
+    .ant-table-row-expand-icon {
+      background-color: var(--themeColor);
+      color: #fff;
+    }
+
+    .ant-table-cell-with-append {
+      display: flex;
+      justify-content: flex-start;
+    }
+  }
+
+
+}
+
+
+
+[hidden] {
+  display: none !important;
+}
+
+
+
+#upInput {
+  display: none;
+}
+
+#upInput2 {
+  display: none;
+}
+
+// 页面标题
+.pageTitle {
+  font-size: 18px;
+  font-weight: 700;
+  position: absolute;
+  z-index: 11;
+  top: -56px;
+  left: -18px;
+  padding-left: 40px;
+
+  &::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;
+}

+ 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",
+              }}
+            />
+          );
+        }
+      }}
+    />
+  );
+}

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

@@ -0,0 +1,51 @@
+.ImageLazy {
+  position: relative;
+
+  :global {
+    .lazyBox {
+      width: 100%;
+      height: 100%;
+      position: relative;
+
+      .adm-image {
+        width: 100%;
+        height: 100%;
+
+        img {
+          width: 100%;
+          height: 100%;
+        }
+      }
+
+      .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;
+        }
+      }
+    }
+  }
+
+}

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

@@ -0,0 +1,68 @@
+import React, { useCallback, useState } from "react";
+import styles from "./index.module.scss";
+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";
+import { Image } from "antd-mobile";
+
+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 [lookImg, setLookImg] = useState(false);
+
+  // 图片加载完成
+  const onLoad = useCallback(() => {
+    setLookImg(true);
+  }, []);
+
+  // 点击预览图片
+  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">
+        <Image
+          lazy
+          onLoad={onLoad}
+          src={src ? (offline ? src : baseURL + src) : ""}
+          placeholder={<img src={imgLoding} alt="" />}
+          fallback={<img src={imgErr} alt="" />}
+          fit="cover"
+        />
+
+        {/* 图片预览 */}
+        {noLook || !lookImg ? null : (
+          <div className="lookImg" onClick={lookBigImg}>
+            <EyeOutlined rev={undefined} />
+            &nbsp;
+            <div>预览</div>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+}
+
+const MemoImageLazy = React.memo(ImageLazy);
+
+export default MemoImageLazy;

+ 65 - 0
src/components/LookDom/index.module.scss

@@ -0,0 +1,65 @@
+.LookDom {
+  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;
+      z-index: 10;
+    }
+
+    .viedoBox {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 800px;
+      height: 500px;
+
+      video {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+    .audioBox {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      width: 500px;
+      height: 60px;
+
+      audio {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+    .modelBox {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+
+      iframe {
+        width: 100%;
+        height: 100%;
+      }
+    }
+  }
+}

+ 52 - 0
src/components/LookDom/index.tsx

@@ -0,0 +1,52 @@
+/* eslint-disable jsx-a11y/iframe-has-title */
+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 LookDom() {
+  const { src, type, flag } = useSelector(
+    (state: RootState) => state.A0Layout.lookDom
+  );
+  return (
+    <div
+      className={styles.LookDom}
+      style={src ? { opacity: 1, pointerEvents: "auto" } : {}}
+    >
+      {src ? (
+        <>
+          {type === "video" ? (
+            <div className="viedoBox">
+              <video autoPlay controls src={flag ? src : baseURL + src}></video>
+            </div>
+          ) : type === "audio" ? (
+            <div className="audioBox">
+              <audio autoPlay controls src={flag ? src : baseURL + src}></audio>
+            </div>
+          ) : (
+            <div className="modelBox">
+              <iframe src={`model.html?m=${src}`}></iframe>
+            </div>
+          )}
+
+          <div
+            className="close"
+            onClick={() =>
+              store.dispatch({
+                type: "layout/lookDom",
+                payload: { src: "", type: "", flag: false },
+              })
+            }
+          >
+            <CloseCircleOutlined rev={undefined} />
+          </div>
+        </>
+      ) : null}
+    </div>
+  );
+}
+
+const MemoLookDom = React.memo(LookDom);
+
+export default MemoLookDom;

+ 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.A0Layout.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;

+ 46 - 0
src/components/MyPopconfirm.tsx

@@ -0,0 +1,46 @@
+import React, { useMemo } from "react";
+import { Button, Popconfirm } from "antd";
+
+type Props = {
+  txtK: "删除" | "取消" | "重置密码" | "退出登录";
+  onConfirm: () => void;
+  Dom?: React.ReactNode;
+  loc?: "bottom";
+};
+
+function MyPopconfirm({ txtK, onConfirm, Dom, loc }: Props) {
+  const txt = useMemo(() => {
+    const obj = {
+      删除: ["删除后无法恢复,是否删除?", "删除"],
+      取消: ["放弃编辑后,信息将不会保存!", "放弃"],
+      重置密码: ["密码重制后为123456,是否重置?", "重置"],
+      退出登录: ["确定退出吗?", "确定"],
+    };
+    return Reflect.get(obj, txtK) || ["", ""];
+  }, [txtK]);
+
+  return (
+    <Popconfirm
+      placement={loc}
+      title={txt[0]}
+      okText={txt[1]}
+      cancelText="取消"
+      onConfirm={onConfirm}
+      okButtonProps={{ loading: false }}
+    >
+      {Dom ? (
+        Dom
+      ) : txtK === "删除" ? (
+        <Button size="small" type="text" danger>
+          {txtK}
+        </Button>
+      ) : (
+        <Button>{txtK}</Button>
+      )}
+    </Popconfirm>
+  );
+}
+
+const MemoMyPopconfirm = React.memo(MyPopconfirm);
+
+export default MemoMyPopconfirm;

+ 18 - 0
src/components/MyTable/index.module.scss

@@ -0,0 +1,18 @@
+.MyTable {
+  :global {
+    .ant-table-body {
+      overflow-y: auto !important;
+      overflow-y: overlay !important;
+
+      .ant-table-row {
+        .ant-table-cell {
+          padding: 10px;
+
+          a {
+            color: var(--themeColor) !important;
+          }
+        }
+      }
+    }
+  }
+}

+ 161 - 0
src/components/MyTable/index.tsx

@@ -0,0 +1,161 @@
+import React, { useCallback, useEffect, useMemo } from "react";
+import styles from "./index.module.scss";
+import { Table } from "antd";
+import ImageLazy from "../ImageLazy";
+
+type Props = {
+  yHeight?: number; //设置表格的高度
+  list: any; //表格数据
+  columnsTemp: any[][]; //表格展示
+  total?: number; //总数
+  pageNum?: number;
+  pageSize?: number;
+  pagingInfo?: any | boolean;
+  onChange?: (pageNum: number, pageSize: number) => void;
+  lastBtn?: any;
+  classKey?: string; //一个组件多次使用的时候要传递,分别设置style
+  // 表格简单的合并
+  merge?: { type: string; num: number; loc: "rowSpan" | "colSpan" };
+  // 定制化表头
+  myTitle?: { name: string; Com: React.ReactNode };
+  // 为空的定制字段
+  isNull?: string;
+};
+
+// 表格内容定制化
+const tableComObj = (key: string, val: string[]) => {
+  const obj = {
+    A: (
+      <a href={val[1]} target="_blank" title={val[1]} rel="noreferrer">
+        {val[0]}
+      </a>
+    ),
+  };
+  return Reflect.get(obj, key);
+};
+
+function MyTable({
+  yHeight,
+  list,
+  columnsTemp,
+  total,
+  pageNum = 1,
+  pageSize = 10,
+  pagingInfo = {
+    showQuickJumper: true,
+    position: ["bottomCenter"],
+    showSizeChanger: true,
+  },
+  onChange,
+  lastBtn = [],
+  classKey = "",
+  merge,
+  myTitle,
+  isNull = "(空)",
+}: Props) {
+  useEffect(() => {
+    const dom = document.querySelector(
+      `.MyTable${classKey} .ant-table-body`
+    ) as HTMLDivElement;
+
+    if (dom && yHeight) dom.style.height = yHeight + "px";
+  }, [classKey, yHeight]);
+
+  // 页码变化
+  const paginationChange = useCallback(
+    () => (pageNum: number, pageSize: number) => {
+      if (onChange) {
+        onChange(pageNum, pageSize);
+      }
+    },
+    [onChange]
+  );
+
+  const dataChangeFu = useCallback(
+    (v: any) => {
+      /**
+       * index:序号
+       * txt:正常数据
+       * img:图片
+       * txtChange:判断显示不同字段
+       * text:文字比较多的情况
+       */
+
+      const obj = {
+        index: (_: any, __: any, index: number) =>
+          index + 1 + (pageNum - 1) * pageSize,
+        txt: (item: any) => item[v[2]] || isNull,
+        img: (item: any) => (
+          <div className="tableImgAuto">
+            <ImageLazy width={60} height={60} src={item.thumb} />
+          </div>
+        ),
+        txtChange: (item: any) =>
+          Reflect.get(v[3], item[v[2]]) || v[4] || isNull,
+        text: (item: any) => {
+          let tempCom: any = item[v[2]] || isNull;
+
+          if (tempCom.length >= v[3]) {
+            tempCom = tempCom.substring(0, v[3]) + "...";
+          }
+
+          if (v[4]) {
+            tempCom = tableComObj(v[4], [tempCom, item[v[2]]]);
+          } else if (item[v[2]].length >= v[3]) {
+            tempCom = (
+              <span style={{ cursor: "pointer" }} title={item[v[2]]}>
+                {tempCom}
+              </span>
+            );
+          }
+
+          return tempCom;
+        },
+      };
+
+      return Reflect.get(obj, v[0]);
+    },
+    [isNull, pageNum, pageSize]
+  );
+
+  const columns = useMemo(() => {
+    const arr: any = columnsTemp.map((v: any) => ({
+      title: myTitle && v.includes(myTitle.name) ? myTitle.Com : v[1],
+      render: dataChangeFu(v),
+      onCell:
+        merge && v.includes(merge.type)
+          ? // {rowSpan:3}
+            (item: any, index: number) => ({
+              rowSpan: index === 0 ? merge.num : 0,
+            })
+          : "",
+    }));
+
+    return arr;
+  }, [columnsTemp, dataChangeFu, merge, myTitle]);
+
+  return (
+    <Table
+      className={`${styles.MyTable} MyTable${classKey}`}
+      scroll={{ y: yHeight ? yHeight : "" }}
+      dataSource={list}
+      columns={[...columns, ...lastBtn]}
+      rowKey="id"
+      pagination={
+        pagingInfo
+          ? {
+              ...pagingInfo,
+              current: pageNum,
+              pageSize: pageSize,
+              total: total,
+              onChange: paginationChange(),
+            }
+          : false
+      }
+    />
+  );
+}
+
+const MemoMyTable = React.memo(MyTable);
+
+export default MemoMyTable;

+ 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";
+    }, 500);
+    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.A0Layout.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;

+ 82 - 0
src/components/ZRichText/index.module.scss

@@ -0,0 +1,82 @@
+.ZRichText {
+  width: 1000px;
+  height: 100%;
+
+  :global {
+    .txtBox {
+      width: 100%;
+      height: 100%;
+      border: 1px solid #ccc;
+
+      a{
+        color: #fff !important;
+      }
+
+      // 隐藏媒体功能
+      .control-item.media {
+        display: none;
+      }
+
+      .bf-container {
+        height:100%;
+      }
+
+      .bf-content {
+        height: calc(100% - 92px);
+        padding-bottom: 0px;
+      }
+
+
+
+      .bf-controlbar {
+        position: relative;
+
+        .upImgBox {
+          position: absolute;
+          bottom: 13px;
+          right: 15px;
+          cursor: pointer;
+          color: var(--themeColor);
+          // display: none;
+        }
+
+        .upImgBoxNo {
+          display: none;
+        }
+
+      }
+    }
+
+    .noUpThumb {
+      position: relative;
+      overflow: hidden;
+      opacity: 0;
+      transition: top .2s;
+      color: #ff4d4f;
+      top: -10px;
+    }
+
+    .noUpThumbAc {
+      top: 0;
+      opacity: 1;
+    }
+
+    .bf-media .bf-image {
+      float: initial !important;
+      display: block;
+      margin: 10px auto;
+      text-align: center;
+
+      // 不让拖动放大缩小图片(会报错)
+      .bf-csize-icon {
+        display: none !important;
+      }
+
+      img {
+        max-width: 500px;
+        max-height: 300px;
+      }
+    }
+
+  }
+}

+ 202 - 0
src/components/ZRichText/index.tsx

@@ -0,0 +1,202 @@
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import styles from "./index.module.scss";
+
+// 引入编辑器组件
+
+// 安装---npm install braft-editor --save --force
+// npm install braft-utils --save --force
+import { ContentUtils } from "braft-utils";
+import BraftEditor from "braft-editor";
+// 引入编辑器样式
+import "braft-editor/dist/index.css";
+
+import classNames from "classnames";
+import { MessageFu } from "@/utils/message";
+import { fileDomInitialFu } from "@/utils/domShow";
+import { baseURL } from "@/utils/http";
+
+import { forwardRef, useImperativeHandle } from "react";
+import { API_upFile } from "@/store/action/layout";
+
+type Props = {
+  check: boolean; //表单校验,为fasle表示不校验
+  dirCode: string; //文件的code码
+  isLook: boolean; //是否是查看进来
+  ref: any; //当前自己的ref,给父组件调用
+  myUrl: string; //上传的api地址
+  full?: boolean;
+};
+
+function ZRichText({ check, dirCode, isLook, myUrl, full }: Props, ref: any) {
+  // 添加 上传 图片的dom
+  useEffect(() => {
+    setTimeout(() => {
+      const dom = document.querySelector(".bf-controlbar")!;
+      const div = document.createElement("div");
+      div.className = "upImgBox";
+      // div.title = "上传图片";
+      div.innerHTML = "上传图片";
+      div.onclick = async () => {
+        myInput.current?.click();
+      };
+      dom.appendChild(div);
+    }, 20);
+
+    // 监听 富文本 的 class 变化,在全屏的时候会 富文本会添加上 fullscreen 的类
+    // 修复顶部样式冲突问题
+
+    const editorDom = document.querySelector(".bf-container") as HTMLDivElement;
+
+    const observer = new MutationObserver(() => {
+      // console.log("change");
+      const dom = document.querySelector(".layoutRightTop") as HTMLDivElement;
+
+      if (editorDom.className.includes("fullscreen")) dom.style.zIndex = "-1";
+      else dom.style.zIndex = "100";
+    });
+
+    observer.observe(editorDom, {
+      attributes: true,
+    });
+
+    // 销毁监听
+    return () => {
+      observer.disconnect();
+    };
+  }, []);
+
+  useEffect(() => {
+    const controlbarDom = document.querySelectorAll(".txtBox .bf-controlbar ");
+    const contentDom = document.querySelectorAll(".txtBox .bf-content ");
+    if (controlbarDom) {
+      controlbarDom.forEach((v: any) => {
+        v.style.display = isLook ? "none" : "block";
+      });
+      contentDom.forEach((v: any) => {
+        v.style.height = isLook ? "100%" : "";
+      });
+    }
+  }, [isLook]);
+
+  // 编辑器文本
+  const [editorValue, setEditorValue] = useState(
+    // 初始内容
+    BraftEditor.createEditorState("")
+  );
+
+  // 判断 富文本是否为空
+  const isTxtFlag = useMemo(() => {
+    const txt: string = editorValue.toHTML();
+    if (
+      txt.replaceAll("<p>", "").replaceAll("</p>", "").replaceAll(" ", "") ===
+      ""
+    ) {
+      return true;
+    } else return false;
+  }, [editorValue]);
+
+  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("只支持png、jpg和jpeg格式!");
+        }
+        // 校验大小
+        if (filesInfo.size > 5 * 1024 * 1024) {
+          e.target.value = "";
+          return MessageFu.warning("最大支持5M!");
+        }
+        // 创建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 API_upFile(fd, myUrl);
+          if (res.code === 0) {
+            MessageFu.success("上传成功!");
+            // 在光标位置插入图片
+            const newTxt = ContentUtils.insertMedias(editorValue, [
+              {
+                type: "IMAGE",
+                url: baseURL + res.data.filePath,
+              },
+            ]);
+
+            setEditorValue(newTxt);
+          }
+          fileDomInitialFu();
+        } catch (error) {
+          fileDomInitialFu();
+        }
+      }
+    },
+    [dirCode, editorValue, myUrl]
+  );
+
+  // 让父组件调用的 回显 富文本
+  const ritxtShowFu = useCallback((val: string) => {
+    setEditorValue(BraftEditor.createEditorState(val));
+  }, []);
+
+  // 让父组件调用的返回 富文本信息 和 表单校验 isTxtFlag为ture表示未通过校验
+  const fatherBtnOkFu = useCallback(() => {
+    return { val: editorValue.toHTML(), flag: isTxtFlag };
+  }, [editorValue, isTxtFlag]);
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    ritxtShowFu,
+    fatherBtnOkFu,
+  }));
+
+  return (
+    <div className={styles.ZRichText} style={{ width: full ? "100%" : "" }}>
+      <input
+        id="upInput"
+        type="file"
+        accept=".png,.jpg,.jpeg"
+        ref={myInput}
+        onChange={(e) => handeUpPhoto(e)}
+      />
+
+      <div className="txtBox">
+        <BraftEditor
+          readOnly={isLook}
+          placeholder="请输入内容"
+          value={editorValue}
+          onChange={(e) => setEditorValue(e)}
+          imageControls={["remove"]}
+        />
+      </div>
+      <div
+        className={classNames(
+          "noUpThumb",
+          check && isTxtFlag ? "noUpThumbAc" : ""
+        )}
+      >
+        请输入正文!
+      </div>
+    </div>
+  );
+}
+
+export default forwardRef(ZRichText);

+ 101 - 0
src/components/ZupOne/index.module.scss

@@ -0,0 +1,101 @@
+.ZupOne {
+  width: 100%;
+  height: 100%;
+  position: relative;
+
+  :global {
+
+    .file_upIcon {
+      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;
+
+
+    }
+
+    .file_img {
+      width: 100px;
+      height: 126px;
+      position: relative;
+      margin-top: 10px;
+
+      .file_closeBox {
+        position: absolute;
+        right: -10px;
+        top: -10px;
+        z-index: 99;
+        background-color: rgba(0, 0, 0, 0.8);
+        width: 20px;
+        height: 20px;
+        border-radius: 50%;
+        font-size: 16px;
+        color: #fff;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+      }
+
+
+
+      .file_lookBox {
+        width: 100%;
+        background-color: rgba(0, 0, 0, .6);
+        color: #fff;
+        display: flex;
+        justify-content: space-around;
+
+        &>a {
+          color: #fff !important;
+        }
+
+        font-size: 16px;
+      }
+    }
+
+    .fileInfo {
+      display: flex;
+      align-items: center;
+      font-size: 16px;
+
+      .clearCover {
+        margin-left: 20px;
+        cursor: pointer;
+        font-size: 16px;
+      }
+
+      &>a {
+        color: black;
+      }
+    }
+
+    .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;
+    }
+
+    .noUpThumbAc {
+      top: 0;
+      opacity: 1;
+    }
+  }
+}

+ 282 - 0
src/components/ZupOne/index.tsx

@@ -0,0 +1,282 @@
+import React, { useCallback, useMemo, useRef, useState } from "react";
+import styles from "./index.module.scss";
+import ImageLazy from "@/components/ImageLazy";
+import {
+  PlusOutlined,
+  EyeOutlined,
+  CloseOutlined,
+  DownloadOutlined,
+  UploadOutlined,
+} from "@ant-design/icons";
+import store from "@/store";
+import { baseURL } from "@/utils/http";
+import classNames from "classnames";
+import { Button } from "antd";
+import { MessageFu } from "@/utils/message";
+import { fileDomInitialFu } from "@/utils/domShow";
+import { API_upFile } from "@/store/action/layout";
+import { forwardRef, useImperativeHandle } from "react";
+import MyPopconfirm from "../MyPopconfirm";
+
+type MyTypeType = "thumb" | "video" | "audio" | "model" | "pdf";
+
+// 这个组件 只处理 上传 一张图片或者 视频 音频 模型 pdf 的情况
+
+type Props = {
+  fileCheck: boolean; //有没有点击过确定
+  size: number; //上传附件大小(M)
+  dirCode: string; //文件的code码
+  myUrl: string; //请求地址
+  format: string[]; //上传格式 ["image/jpeg", "image/png"] ["video/mp4"]
+  formatTxt: string; //上传图片提示
+  checkTxt: string;
+  upTxt: string;
+  myType: MyTypeType;
+  isLook?: boolean; //是不是查看
+  fromData?: any;
+  ref: any; //当前自己的ref,给父组件调用
+};
+
+function ZupOne(
+  {
+    fileCheck,
+    size,
+    dirCode,
+    myUrl,
+    format,
+    formatTxt,
+    checkTxt,
+    upTxt,
+    myType,
+    isLook = false,
+    fromData,
+  }: Props,
+  ref: any
+) {
+  const [fileUrl, setFileUrl] = useState({
+    fileName: "",
+    filePath: "",
+  });
+
+  const myInput = useRef<HTMLInputElement>(null);
+
+  // 上传封面图
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0];
+        // console.log("-----", filesInfo.type);
+
+        // 校验格式
+        const type = format;
+
+        if (myType === "pdf") {
+          if (!filesInfo.type.includes("pdf")) {
+            e.target.value = "";
+            return MessageFu.warning(`只支持${formatTxt}格式!`);
+          }
+        } else {
+          if (!type.includes(filesInfo.type)) {
+            e.target.value = "";
+            return MessageFu.warning(`只支持${formatTxt}格式!`);
+          }
+        }
+
+        // 校验大小
+        if (filesInfo.size > size * 1024 * 1024) {
+          e.target.value = "";
+          return MessageFu.warning(`最大支持${size}M!`);
+        }
+        // 创建FormData对象
+        const fd = new FormData();
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        let myTypeRes: string = myType;
+        if (["pdf"].includes(myTypeRes)) myTypeRes = "doc";
+        fd.append("type", myTypeRes);
+        fd.append("dirCode", dirCode);
+        fd.append("file", filesInfo);
+
+        if (fromData) {
+          for (const k in fromData) {
+            if (fromData[k]) fd.append(k, fromData[k]);
+          }
+        }
+
+        e.target.value = "";
+
+        try {
+          const res = await API_upFile(fd, myUrl);
+          if (res.code === 0) {
+            MessageFu.success("上传成功!");
+            setFileUrl(res.data);
+          }
+          fileDomInitialFu();
+        } catch (error) {
+          fileDomInitialFu();
+        }
+      }
+    },
+    [dirCode, format, formatTxt, fromData, myType, myUrl, size]
+  );
+
+  // 让父组件调用的 回显 附件 地址
+  const setFileComFileFu = useCallback(
+    (valObj: { fileName: string; filePath: string }) => {
+      setFileUrl(valObj);
+    },
+    []
+  );
+
+  // 让父组件调用的返回 附件 名字和路径
+  const fileComFileResFu = useCallback(() => {
+    return fileUrl;
+  }, [fileUrl]);
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    setFileComFileFu,
+    fileComFileResFu,
+  }));
+
+  const acceptRes = useMemo(() => {
+    let accept = ".png,.jpg,.jpeg";
+    if (myType === "video") accept = ".mp4";
+    else if (myType === "audio") accept = ".mp3";
+    else if (myType === "model") accept = ".4dage";
+    else if (myType === "pdf") accept = ".pdf";
+    return accept;
+  }, [myType]);
+
+  // 点击 预览(除了图片)
+  const lookFileNoImgFu = useCallback(
+    (type: MyTypeType) => {
+      if (type === "pdf" || type === "thumb") {
+        // 新窗口打开
+        window.open(baseURL + fileUrl.filePath);
+      } else {
+        store.dispatch({
+          type: "layout/lookDom",
+          payload: { src: fileUrl.filePath, type },
+        });
+      }
+
+      if (type === "pdf") {
+      } else {
+      }
+    },
+    [fileUrl.filePath]
+  );
+
+  return (
+    <div className={styles.ZupOne}>
+      <input
+        id="upInput"
+        type="file"
+        accept={acceptRes}
+        ref={myInput}
+        onChange={(e) => handeUpPhoto(e)}
+      />
+      {myType === "thumb" ? (
+        <div
+          hidden={fileUrl.filePath !== ""}
+          className="file_upIcon"
+          onClick={() => myInput.current?.click()}
+        >
+          <PlusOutlined rev={undefined} />
+        </div>
+      ) : (
+        <Button
+          hidden={fileUrl.filePath !== ""}
+          onClick={() => myInput.current?.click()}
+          icon={<UploadOutlined rev={undefined} />}
+        >
+          上传
+        </Button>
+      )}
+
+      {/* 为图片的情况-------------- */}
+      {myType === "thumb" ? (
+        <div className="file_img" hidden={fileUrl.filePath === ""}>
+          {fileUrl ? (
+            <ImageLazy width={100} height={100} src={fileUrl.filePath} noLook />
+          ) : null}
+
+          {/* 删除 */}
+          <div className="file_closeBox" hidden={isLook}>
+            <MyPopconfirm
+              txtK="删除"
+              onConfirm={() => setFileUrl({ fileName: "", filePath: "" })}
+              Dom={<CloseOutlined rev={undefined} />}
+            />
+          </div>
+
+          {/* 预览 下载 */}
+          <div className="file_lookBox">
+            <EyeOutlined
+              onClick={() =>
+                store.dispatch({
+                  type: "layout/lookBigImg",
+                  payload: { url: baseURL + fileUrl.filePath, show: true },
+                })
+              }
+              rev={undefined}
+            />
+            <a
+              href={baseURL + fileUrl.filePath}
+              download
+              target="_blank"
+              rel="noreferrer"
+            >
+              <DownloadOutlined rev={undefined} />
+            </a>
+          </div>
+        </div>
+      ) : fileUrl.filePath ? (
+        <div className="fileInfo">
+          <div className="upSuccTxt">{fileUrl.fileName}</div>
+          {/* 视频预览 */}
+          <div
+            className="clearCover"
+            hidden={!fileUrl.filePath}
+            onClick={() => lookFileNoImgFu(myType)}
+          >
+            <EyeOutlined rev={undefined} />
+          </div>
+          {/* 视频下载 */}
+          <a
+            href={baseURL + fileUrl.filePath}
+            download
+            target="_blank"
+            className="clearCover"
+            rel="noreferrer"
+          >
+            <DownloadOutlined rev={undefined} />
+          </a>
+          {/* 视频删除 */}
+
+          <MyPopconfirm
+            txtK="删除"
+            onConfirm={() => setFileUrl({ fileName: "", filePath: "" })}
+            Dom={<CloseOutlined className="clearCover" rev={undefined} />}
+          />
+        </div>
+      ) : null}
+
+      <div className="fileBoxRow_r_tit" hidden={isLook}>
+        格式要求:支持{formatTxt}格式;最大支持{size}M。{upTxt}
+        <br />
+        <div
+          className={classNames(
+            "noUpThumb",
+            !fileUrl.filePath && fileCheck ? "noUpThumbAc" : ""
+          )}
+        >
+          {checkTxt}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+export default forwardRef(ZupOne);

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

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

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

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

+ 92 - 0
src/components/ZupVideos/index.module.scss

@@ -0,0 +1,92 @@
+.ZupVideos {
+  width: 100%;
+
+  :global {
+
+    .zVmain {
+      width: 100%;
+      display: flex;
+      flex-wrap: wrap;
+
+      .file_upIcon {
+        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;
+        margin-right: 100px;
+      }
+
+      .zVRow {
+        margin-right: 30px;
+        margin-bottom: 20px;
+
+        .zVRow1 {
+          display: flex;
+
+          .zVRow1V {
+            width: 150px;
+            height: 100px;
+            border: 1px dashed #797979;
+            border-radius: 3px;
+
+            video {
+              width: 100%;
+              height: 100%;
+            }
+          }
+
+          .zVRow1I {
+            margin-left: 4px;
+            display: flex;
+            flex-direction: column;
+            justify-content: space-around;
+            align-items: center;
+          }
+        }
+
+        .zVRow2 {
+          margin-top: 3px;
+          font-size: 14px;
+          text-align: center;
+          width: 170px;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+      }
+
+    }
+
+
+
+    .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;
+    }
+
+    .noUpThumbAc {
+      top: 0;
+      opacity: 1;
+    }
+  }
+}

+ 205 - 0
src/components/ZupVideos/index.tsx

@@ -0,0 +1,205 @@
+import React, { useCallback, useRef, useState } from "react";
+import styles from "./index.module.scss";
+import { forwardRef, useImperativeHandle } from "react";
+import { FileListType } from "../ZupTypes";
+import { MessageFu } from "@/utils/message";
+import { API_upFile } from "@/store/action/layout";
+import { fileDomInitialFu } from "@/utils/domShow";
+import {
+  PlusOutlined,
+  EyeOutlined,
+  CloseOutlined,
+  DownloadOutlined,
+} from "@ant-design/icons";
+import classNames from "classnames";
+import { baseURL } from "@/utils/http";
+import store from "@/store";
+import MyPopconfirm from "../MyPopconfirm";
+
+type Props = {
+  size: number; //视频大小
+  fileNum: number; //数量限制
+  dirCode: string; //文件的code码
+  myUrl: string; //请求地址
+  fileCheck?: boolean; //是否检验
+  format?: string[]; //上传格式 默认["video/mp4"]
+  formatTxt?: string; //上传提示
+  checkTxt?: string;
+  upTxt?: string;
+  isLook?: boolean; //是不是查看
+  fromData?: any;
+  ref: any; //当前自己的ref,给父组件调用
+};
+
+function ZupVideos(
+  {
+    size,
+    fileNum,
+    dirCode,
+    myUrl,
+    fileCheck = false,
+    format = ["video/mp4"],
+    formatTxt = ".mp4",
+    checkTxt = "请上传视频!",
+    upTxt = "",
+    isLook = false,
+    fromData,
+  }: Props,
+  ref: any
+) {
+  const [fileList, setFileList] = useState<FileListType[]>([]);
+
+  // 上传
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0];
+        // console.log("-----", filesInfo.type);
+
+        // 校验格式
+        if (!format.includes(filesInfo.type)) {
+          e.target.value = "";
+          return MessageFu.warning(`只支持${formatTxt}格式!`);
+        }
+
+        // 校验大小
+        if (filesInfo.size > size * 1024 * 1024) {
+          e.target.value = "";
+          return MessageFu.warning(`最大支持${size}M!`);
+        }
+        // 创建FormData对象
+        const fd = new FormData();
+
+        fd.append("type", "video");
+        fd.append("isDb", "true");
+        fd.append("dirCode", dirCode);
+        fd.append("file", filesInfo);
+
+        if (fromData) {
+          for (const k in fromData) {
+            if (fromData[k]) fd.append(k, fromData[k]);
+          }
+        }
+
+        e.target.value = "";
+
+        try {
+          const res = await API_upFile(fd, myUrl);
+          if (res.code === 0) {
+            MessageFu.success("上传成功!");
+            setFileList([...fileList, res.data]);
+            // console.log(res);
+          }
+          fileDomInitialFu();
+        } catch (error) {
+          fileDomInitialFu();
+        }
+      }
+    },
+    [dirCode, fileList, format, formatTxt, fromData, myUrl, size]
+  );
+
+  // 让父组件调用的 回显 附件 地址
+  const setFileComFileFu = useCallback((valList: FileListType[]) => {
+    setFileList(valList);
+  }, []);
+
+  // 让父组件调用的返回 附件 名字和路径
+  const fileComFileResFu = useCallback(() => {
+    return fileList;
+  }, [fileList]);
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    setFileComFileFu,
+    fileComFileResFu,
+  }));
+
+  const myInput = useRef<HTMLInputElement>(null);
+
+  // 点击预览
+  const lookFileFu = useCallback((file: string) => {
+    store.dispatch({
+      type: "layout/lookDom",
+      payload: { src: file, type: "video" },
+    });
+  }, []);
+
+  return (
+    <div className={styles.ZupVideos}>
+      <input
+        id="upInput"
+        type="file"
+        accept={formatTxt}
+        ref={myInput}
+        onChange={(e) => handeUpPhoto(e)}
+      />
+
+      <div hidden={!(isLook && fileList.length <= 0)}>(空)</div>
+
+      <div className="zVmain">
+        <div
+          hidden={fileList.length >= fileNum || isLook}
+          className="file_upIcon"
+          onClick={() => myInput.current?.click()}
+        >
+          <PlusOutlined />
+        </div>
+
+        {fileList.map((v) => (
+          <div className="zVRow" key={v.id}>
+            <div className="zVRow1">
+              <div className="zVRow1V" title={v.fileName}>
+                <video src={baseURL + v.filePath}></video>
+              </div>
+              <div className="zVRow1I">
+                {/* 视频预览 */}
+                <EyeOutlined onClick={() => lookFileFu(v.filePath)} />
+                {/* 视频下载 */}
+                <a
+                  href={baseURL + v.filePath}
+                  download
+                  target="_blank"
+                  rel="noreferrer"
+                >
+                  <DownloadOutlined />
+                </a>
+                {/* 视频删除 */}
+
+                {isLook ? null : (
+                  <MyPopconfirm
+                    txtK="删除"
+                    onConfirm={() =>
+                      setFileList(fileList.filter((c) => c.id !== v.id))
+                    }
+                    Dom={<CloseOutlined />}
+                  />
+                )}
+              </div>
+            </div>
+            <div className="zVRow2" title={v.fileName}>
+              {v.fileName}
+            </div>
+          </div>
+        ))}
+      </div>
+
+      <div className="fileBoxRow_r_tit" hidden={isLook}>
+        格式要求:仅支持{formatTxt.replaceAll(".", "")}格式,最大支持{size}M
+        {upTxt}
+        <br />
+        <div
+          className={classNames(
+            "noUpThumb",
+            fileList.length <= 0 && fileCheck ? "noUpThumbAc" : ""
+          )}
+        >
+          {checkTxt}
+        </div>
+      </div>
+    </div>
+  );
+}
+
+export default forwardRef(ZupVideos);

+ 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: " #CC1424",
+      },
+    }}
+  >
+    <Provider store={store}>
+      <StyleProvider
+        hashPriority="high"
+        transformers={[legacyLogicalPropertiesTransformer]}
+      >
+        <App />
+      </StyleProvider>
+    </Provider>
+  </ConfigProvider>
+);

+ 5 - 0
src/pages/A1event/index.module.scss

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

+ 14 - 0
src/pages/A1event/index.tsx

@@ -0,0 +1,14 @@
+import React from "react";
+import styles from "./index.module.scss";
+ function A1event() {
+  
+  return (
+    <div className={styles.A1event}>
+       <div className="pageTitle">事件管理</div>
+    </div>
+  )
+}
+
+const MemoA1event = React.memo(A1event);
+
+export default MemoA1event;

+ 5 - 0
src/pages/A2exhibition/index.module.scss

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

+ 14 - 0
src/pages/A2exhibition/index.tsx

@@ -0,0 +1,14 @@
+import React from "react";
+import styles from "./index.module.scss";
+ function A2exhibition() {
+  
+  return (
+    <div className={styles.A2exhibition}>
+       <div className="pageTitle">展览管理</div>
+    </div>
+  )
+}
+
+const MemoA2exhibition = React.memo(A2exhibition);
+
+export default MemoA2exhibition;

+ 5 - 0
src/pages/A3holding/index.module.scss

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

+ 14 - 0
src/pages/A3holding/index.tsx

@@ -0,0 +1,14 @@
+import React from "react";
+import styles from "./index.module.scss";
+ function A3holding() {
+  
+  return (
+    <div className={styles.A3holding}>
+       <div className="pageTitle">馆藏管理</div>
+    </div>
+  )
+}
+
+const MemoA3holding = React.memo(A3holding);
+
+export default MemoA3holding;

+ 5 - 0
src/pages/A4study/index.module.scss

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

+ 14 - 0
src/pages/A4study/index.tsx

@@ -0,0 +1,14 @@
+import React from "react";
+import styles from "./index.module.scss";
+ function A4study() {
+  
+  return (
+    <div className={styles.A4study}>
+       <div className="pageTitle">学习参与管理</div>
+    </div>
+  )
+}
+
+const MemoA4study = React.memo(A4study);
+
+export default MemoA4study;

+ 5 - 0
src/pages/A5publish/index.module.scss

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

+ 14 - 0
src/pages/A5publish/index.tsx

@@ -0,0 +1,14 @@
+import React from "react";
+import styles from "./index.module.scss";
+ function A5publish() {
+  
+  return (
+    <div className={styles.A5publish}>
+       <div className="pageTitle">出版物管理</div>
+    </div>
+  )
+}
+
+const MemoA5publish = React.memo(A5publish);
+
+export default MemoA5publish;

+ 5 - 0
src/pages/A6home/index.module.scss

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

+ 14 - 0
src/pages/A6home/index.tsx

@@ -0,0 +1,14 @@
+import React from "react";
+import styles from "./index.module.scss";
+ function A6home() {
+  
+  return (
+    <div className={styles.A6home}>
+       <div className="pageTitle">首页管理</div>
+    </div>
+  )
+}
+
+const MemoA6home = React.memo(A6home);
+
+export default MemoA6home;

+ 5 - 0
src/pages/A7poster/index.module.scss

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

+ 14 - 0
src/pages/A7poster/index.tsx

@@ -0,0 +1,14 @@
+import React from "react";
+import styles from "./index.module.scss";
+ function A7poster() {
+  
+  return (
+    <div className={styles.A7poster}>
+       <div className="pageTitle">海报管理</div>
+    </div>
+  )
+}
+
+const MemoA7poster = React.memo(A7poster);
+
+export default MemoA7poster;

+ 5 - 0
src/pages/A8dict/index.module.scss

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

+ 14 - 0
src/pages/A8dict/index.tsx

@@ -0,0 +1,14 @@
+import React from "react";
+import styles from "./index.module.scss";
+ function A8dict() {
+  
+  return (
+    <div className={styles.A8dict}>
+       <div className="pageTitle">字典管理</div>
+    </div>
+  )
+}
+
+const MemoA8dict = React.memo(A8dict);
+
+export default MemoA8dict;

+ 80 - 0
src/pages/Layout/data.ts

@@ -0,0 +1,80 @@
+import { RouterType } from "@/types";
+import React from "react";
+
+const tabLeftArr: RouterType = [
+  {
+    id: 1,
+    name: "内容发布",
+    son: [
+      {
+        id: 200,
+        name: "事件管理",
+        path: "/",
+        Com: React.lazy(() => import("../A1event")),
+      },
+      {
+        id: 300,
+        name: "展览管理",
+        path: "/exhibition",
+        Com: React.lazy(() => import("../A2exhibition")),
+      },
+      {
+        id: 400,
+        name: "馆藏管理",
+        path: "/holding",
+        Com: React.lazy(() => import("../A3holding")),
+      },
+      {
+        id: 500,
+        name: "学习参与管理",
+        path: "/study",
+        Com: React.lazy(() => import("../A4study")),
+      },
+      {
+        id: 900,
+        name: "出版物管理",
+        path: "/publish",
+        Com: React.lazy(() => import("../A5publish")),
+      },
+      {
+        id: 600,
+        name: "首页管理",
+        path: "/home",
+        Com: React.lazy(() => import("../A6home")),
+      },
+      {
+        id: 700,
+        name: "海报管理",
+        path: "/poster",
+        Com: React.lazy(() => import("../A7poster")),
+      },
+      {
+        id: 800,
+        name: "字典管理",
+        path: "/dict",
+        Com: React.lazy(() => import("../A8dict")),
+      },
+    ],
+  },
+
+  {
+    id: 4,
+    name: "系统设置",
+    son: [
+      {
+        id: 2100,
+        name: "用户管理",
+        path: "/user",
+        Com: React.lazy(() => import("../Z1user")),
+      },
+      {
+        id: 2200,
+        name: "系统日志",
+        path: "/log",
+        Com: React.lazy(() => import("../Z2log")),
+      },
+    ],
+  },
+];
+
+export default tabLeftArr;

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

@@ -0,0 +1,182 @@
+.Layout {
+  width: 100%;
+  height: 100%;
+  display: flex;
+
+  :global {
+
+    .layoutLeft {
+      position: relative;
+      width: 220px;
+      height: 100%;
+      background-color: var(--themeColor2);
+
+
+      .layoutLeftTop {
+        text-align: center;
+        padding: 20px 0px 20px;
+
+        &>img {
+          width: 208px;
+        }
+      }
+
+      .layoutLeftMain {
+        padding-top: 30px;
+        .layoutLRowBox {
+          .layoutLRowBoxTxt {
+            font-size: 18px;
+            font-weight: 700;
+            height: 50px;
+            line-height: 50px;
+            padding-left: 40px;
+            color: #fff;
+            display: none;
+            // opacity: .8;
+          }
+
+          .layoutLRowBoxRow {
+            opacity: .6;
+            color: #fff;
+            text-align: center;
+            cursor: pointer;
+
+            font-size: 16px;
+            height: 40px;
+            line-height: 40px;
+            margin-bottom: 25px;
+
+            &:hover {
+              background-color: var(--themeColor);
+              opacity: 1;
+            }
+          }
+
+          .active {
+            opacity: 1;
+            pointer-events: none;
+            background-color: var(--themeColor);
+          }
+        }
+
+
+
+
+
+      }
+
+
+    }
+
+    .layoutRight {
+      width: calc(100% - 220px);
+      height: 100%;
+
+
+      .layoutRightTop {
+        height: 60px;
+        display: flex;
+        justify-content: flex-end;
+        position: relative;
+        z-index: 1000;
+
+        .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: black;
+
+          .userInco {
+            margin-left: 10px;
+            color: black;
+          }
+
+          .userInco1 {
+            display: none;
+          }
+
+          .userSet {
+            width: 140px;
+            opacity: 0;
+            pointer-events: none;
+            transition: bottom .3s;
+            height: 120px;
+            position: absolute;
+            left: 50%;
+            transform: translateX(-50%);
+            bottom: -80px;
+
+            // color: rgb(226, 223, 223);
+            &>div {
+              box-shadow: 1px 1px 4px 4px #ccc;
+              margin-top: 15px;
+              border-radius: 10px;
+              overflow: hidden;
+
+              &>span {
+                background-color: #fff;
+                display: block;
+                width: 100%;
+                height: 50px;
+                line-height: 50px;
+                text-align: center;
+
+                &:hover {
+                  color: var(--themeColor);
+                }
+              }
+            }
+
+          }
+
+          &:hover {
+            .userSet {
+              opacity: 1;
+              pointer-events: auto;
+              bottom: -120px;
+            }
+
+            .userInco1 {
+              display: block;
+            }
+
+            .userInco2 {
+              display: none;
+            }
+
+
+          }
+        }
+      }
+
+      .layoutRightMain {
+        height: calc(100% - 60px);
+        padding: 15px;
+        background-color: #ecedf1;
+
+        .mainBoxR {
+          width: 100%;
+          height: 100%;
+          // overflow: hidden;
+          position: relative;
+
+          &>div {
+            width: 100%;
+            height: 100%;
+          }
+        }
+      }
+
+    }
+
+
+
+  }
+}

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

@@ -0,0 +1,296 @@
+import React, { useCallback, useEffect, 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 } from "antd";
+import { Base64 } from "js-base64";
+import encodeStr from "@/utils/pass";
+import { passWordEditAPI } from "@/store/action/layout";
+import { getTokenInfo, removeTokenInfo } from "@/utils/storage";
+import { MessageFu } from "@/utils/message";
+import logoImg from "@/assets/img/logo.png";
+import NotFound from "@/components/NotFound";
+
+import { RouterType, RouterTypeRow } from "@/types";
+import tabLeftArr from "./data";
+import MyPopconfirm from "@/components/MyPopconfirm";
+import { Z1_APIgetAuthByUserId } from "@/store/action/Z1user";
+import { UserListType } from "../Z1user/Z1auth";
+
+function Layout() {
+  // 当前路径选中的左侧菜单
+  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 getUserAuthFu = useCallback(async () => {
+    const userInfo = getTokenInfo().user;
+
+    const res = await Z1_APIgetAuthByUserId(userInfo.id);
+    if (res.code === 0) {
+      // 这里后台返回的一层的数据。懒得改逻辑,手动修改成2级的数据
+      res.data = [
+        {
+          id: Date.now(),
+          name: "",
+          children: res.data,
+        },
+      ];
+
+      const isOkIdArr: number[] = [
+        // 101, 102, 103, 104, 105, 106, 107, 108
+      ];
+      const tempList: UserListType[] = res.data || [];
+      // console.log(123, tempList);
+      tempList.forEach((v) => {
+        if (v.children) {
+          v.children.forEach((c) => {
+            if (c.authority) isOkIdArr.push(c.id);
+          });
+        }
+      });
+      // 是管理员
+      if (userInfo.isAdmin === 1) {
+        isOkIdArr.push(2100);
+        isOkIdArr.push(2200);
+      }
+
+      const tempArr: RouterTypeRow = [];
+
+      tabLeftArr.forEach((v1) => {
+        if (v1.son && v1.son[0]) {
+          v1.son.forEach((v2) => {
+            if (isOkIdArr.includes(v2.id)) {
+              tempArr.push(v2);
+            }
+          });
+        }
+      });
+
+      setRouterCom(tempArr);
+
+      // 如果当前页面没有权限了,跳转有权限的第一个页面
+      const urlAll = window.location.hash;
+      const isNowPath = urlAll.replace("#", "");
+      const pathArr = tempArr.map((v) => v.path);
+      if (!pathArr.includes(isNowPath)) {
+        history.push(pathArr[0]);
+      }
+
+      const resList = tabLeftArr.map((v) => ({
+        ...v,
+        son: v.son.filter((c) => isOkIdArr.includes(c.id)),
+      }));
+
+      setList(resList);
+    }
+  }, []);
+
+  useEffect(() => {
+    getUserAuthFu();
+  }, [getUserAuthFu]);
+
+  // 左侧菜单 信息
+  const [list, setList] = useState([] as RouterType);
+
+  // 路由信息(过滤之后的)
+  const [RouterCom, setRouterCom] = useState<RouterTypeRow>([]);
+
+  // useEffect(() => {
+  //   console.log(123, list);
+  // }, [list]);
+
+  // 点击跳转
+  const pathCutFu = useCallback((path: string) => {
+    history.push(path);
+  }, []);
+
+  // 修改密码相关
+  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");
+  };
+
+  return (
+    <div className={styles.Layout}>
+      {/* 左边 */}
+      <div className="layoutLeft">
+        <div className="layoutLeftTop">
+          <img src={logoImg} alt="" />
+        </div>
+        {/* 左边主体 */}
+        <div className="layoutLeftMain">
+          {list.map((v) => (
+            <div
+              className={classNames("layoutLRowBox")}
+              key={v.id}
+              hidden={!v.son.length}
+            >
+              {/* 这个项目没有一级目录 */}
+              <div className="layoutLRowBoxTxt">{v.name}</div>
+              {v.son.map((v2) => (
+                <div
+                  key={v2.id}
+                  className={classNames(
+                    "layoutLRowBoxRow",
+                    path === v2.path ? "active" : ""
+                  )}
+                  onClick={() => pathCutFu(v2.path)}
+                >
+                  {v2.name}
+                </div>
+              ))}
+            </div>
+          ))}
+        </div>
+      </div>
+      {/* 右边 */}
+      <div className="layoutRight">
+        <div className="layoutRightTop">
+          {/* 用户相关 */}
+          <div className="user">
+            {getTokenInfo().user.realName ||
+              getTokenInfo().user.userName ||
+              "匿名"}
+            <div className="userInco userInco1">
+              <CaretUpOutlined rev={undefined} />
+            </div>
+            <div className="userInco userInco2">
+              <CaretDownOutlined rev={undefined} />
+            </div>
+            <div className="userSet">
+              <div>
+                <span onClick={() => setOpen(true)}>修改密码</span>
+                <MyPopconfirm
+                  txtK="退出登录"
+                  onConfirm={loginExit}
+                  Dom="退出登录"
+                  loc="bottom"
+                />
+              </div>
+            </div>
+          </div>
+        </div>
+        {/* 右边主体 */}
+        <div className="layoutRightMain">
+          {/* 二级路由页面 */}
+          <div className="mainBoxR">
+            <React.Suspense fallback={<SpinLoding />}>
+              <Switch>
+                {RouterCom.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
+          scrollToFirstError={true}
+          name="basic"
+          labelCol={{ span: 5 }}
+          wrapperCol={{ span: 16 }}
+          onFinish={onFinish}
+          autoComplete="off"
+        >
+          <Form.Item
+            label="旧密码"
+            name="oldPassword"
+            rules={[{ required: true, message: "不能为空!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input.Password maxLength={20} />
+          </Form.Item>
+
+          <Form.Item
+            label="新密码"
+            name="newPassword"
+            rules={[
+              { required: true, message: "不能为空!" },
+              { min: 6, max: 15, message: "密码长度为6-15个字符!" },
+            ]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input.Password
+              maxLength={15}
+              onChange={(e) => (oldPasswordValue.current = e.target.value)}
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="确定新密码"
+            name="checkPass"
+            rules={[{ validator: checkPassWord }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <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;

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

@@ -0,0 +1,150 @@
+.Login {
+  width: 100%;
+  height: 100%;
+  background-image: url("../../assets/img/loginBac.jpg");
+  background-size: 100% 100%;
+
+  :global {
+
+
+    .mainRight {
+      position: absolute;
+      right: 0px;
+      top: 0;
+      width: 530px;
+      height: 100%;
+      background-color: rgba(0, 0, 0, 0.70);
+      backdrop-filter: blur(6px);
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+
+        .LogoImg{
+          position: absolute;
+          top: 90px;
+          left: 80px;
+          &>img{
+            width: 300px;
+          }
+        }
+
+        .inputBox {
+          margin: 40px 0 80px;
+          width: 100%;
+
+          input::-webkit-input-placeholder {
+            /* WebKit browsers */
+            color: rgba(255,255,255,.6);
+          }
+
+          input:-moz-placeholder {
+            /* Mozilla Firefox 4 to 18 */
+            color: rgba(255,255,255,.6);
+          }
+
+          input::-moz-placeholder {
+            /* Mozilla Firefox 19+ */
+            color: rgba(255,255,255,.6);
+          }
+
+          input:-ms-input-placeholder {
+            /* Internet Explorer 10+ */
+            color: rgba(255,255,255,.6);
+          }
+
+
+          .inputBoxRow {
+            width: 370px;
+            margin: 30px auto;
+
+            .ant-input-suffix .ant-input-password-icon {
+              color: #fff;
+              font-size: 22px;
+            }
+          }
+
+          .inputBoxRow2 {
+            position: relative;
+
+            .loginCode {
+              z-index: 110;
+              cursor: pointer;
+              position: absolute;
+              top: 50%;
+              right: 0;
+              transform: translateY(-50%);
+            }
+          }
+
+          .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: #fff !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 #fff;
+            border-radius: 0;
+            color: #fff;
+
+            .ant-input {
+              background-color: transparent;
+              width: 100%;
+              height: 60px;
+              color: #fff;
+            }
+          }
+
+          .ant-input-affix-wrapper-focused {
+            box-shadow: none
+          }
+        }
+
+        .loginBtn {
+
+          .ant-btn {
+            color: #fff;
+            background-color: var(--themeColor);
+            border-radius: 25px;
+            font-size:18px;
+            width: 375px;
+            height: 50px;
+          }
+        }
+
+    }
+  }
+}

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

@@ -0,0 +1,134 @@
+import styles from "./index.module.scss";
+
+import { Input, Button } from "antd";
+import { UserOutlined, LockOutlined, NumberOutlined } from "@ant-design/icons";
+import { useCallback, useEffect, 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 { API_LoginGetCode, userLoginAPI } from "@/store/action/layout";
+import LogoImg from "@/assets/img/logo.png";
+
+export default function Login() {
+  // 获取验证码
+  const LoginGetCodeFu = useCallback(async () => {
+    const res: any = await API_LoginGetCode();
+    const reader = new FileReader();
+    reader.readAsDataURL(res);
+    reader.onload = () => {
+      setCodeImg(reader.result);
+    };
+  }, []);
+
+  useEffect(() => {
+    LoginGetCodeFu();
+  }, [LoginGetCodeFu]);
+
+  // 账号密码 - 验证码
+  const [userName, setUserName] = useState("");
+  const [passWord, setPassWord] = useState("");
+  const [code, setCode] = useState<any>("");
+  const [codeImg, setCodeImg] = useState<any>("");
+
+  useEffect(() => {
+    //进入登录页 重置 权限信息为空
+  }, []);
+
+  // 键盘按下回车事件
+  const keyUpEntFu = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    if (e.key === "Enter") loginClickFu();
+  };
+  // 点击登录
+  const loginClickFu = useCallback(async () => {
+    // 非空判断
+    if (userName === "") return MessageFu.warning("请输入账号!");
+    else if (passWord === "") return MessageFu.warning("请输入密码!");
+    const obj = {
+      userName,
+      passWord: encodeStr(Base64.encode(passWord)),
+      randCode: code,
+    };
+    const res: any = await userLoginAPI(obj);
+    if (res.code === 0) {
+      MessageFu.success("登录成功");
+
+      // 检查密码是不是默认密码,是的话给提示
+      if (passWord === "123456") {
+        window.setTimeout(() => {
+          MessageFu.warning("您的密码还是默认密码,请尽快修改!");
+        }, 1000);
+      }
+
+      // 用户信息存到本地
+      setTokenInfo(res.data);
+      history.push("/");
+    } else if (res.code === 3014) {
+      MessageFu.warning("账号不存在或密码错误,请联系管理员!");
+    } else if (res.code === -1 && res.msg === "验证码有误") {
+      LoginGetCodeFu();
+    }
+  }, [LoginGetCodeFu, code, passWord, userName]);
+
+  return (
+    <div className={styles.Login}>
+      <div className="mainRight">
+
+        {/* logg */}
+        <div className="LogoImg">
+        <img src={LogoImg}  alt="" />
+        </div>
+
+          {/* 账号密码输入框 */}
+          <div className="inputBox">
+            <div className="inputBoxRow">
+              <Input
+                onKeyUp={(e) => keyUpEntFu(e)}
+                value={userName}
+                onChange={(e) => setUserName(e.target.value.trim())}
+                prefix={<UserOutlined rev={undefined} />}
+                placeholder="请输入账号"
+                maxLength={15}
+              />
+            </div>
+            <div className="inputBoxRow">
+              <Input.Password
+                onKeyUp={(e) => keyUpEntFu(e)}
+                value={passWord}
+                onChange={(e) => setPassWord(e.target.value.trim())}
+                prefix={<LockOutlined rev={undefined} />}
+                placeholder="请输入密码"
+                maxLength={20}
+              />
+            </div>
+            <div className="inputBoxRow inputBoxRow2">
+              <Input
+                onKeyUp={(e) => keyUpEntFu(e)}
+                value={code}
+                onChange={(e) => setCode(e.target.value.trim())}
+                prefix={<NumberOutlined rev={undefined} />}
+                placeholder="请输入验证码"
+                maxLength={5}
+              />
+              {codeImg ? (
+                <img
+                  onClick={LoginGetCodeFu}
+                  className="loginCode"
+                  src={codeImg}
+                  alt=""
+                />
+              ) : null}
+            </div>
+          </div>
+
+          {/* 登录按钮 */}
+          <div className="loginBtn">
+            <Button type="primary" size="large" onClick={loginClickFu}>
+              登 录
+            </Button>
+          </div>
+      </div>
+    </div>
+  );
+}

+ 19 - 0
src/pages/Z1user/UserAdd/index.module.scss

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

+ 127 - 0
src/pages/Z1user/UserAdd/index.tsx

@@ -0,0 +1,127 @@
+import { getUserInfoByIdAPI, userSaveAPI } from "@/store/action/Z1user";
+import { SaveUserType } from "@/types";
+import { MessageFu } from "@/utils/message";
+import { Button, Form, FormInstance, Input, Modal } from "antd";
+import React, { useCallback, useEffect, useRef } from "react";
+import styles from "./index.module.scss";
+import MyPopconfirm from "@/components/MyPopconfirm";
+
+type Props = {
+  id: any;
+  closePage: () => void;
+  upTableList: () => void;
+  addTableList: () => void;
+};
+
+function UserAdd({ id, closePage, upTableList, addTableList }: Props) {
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<FormInstance>(null);
+
+  const getInfoInAPIFu = useCallback(async (id: number) => {
+    const res = await getUserInfoByIdAPI(id);
+    FormBoxRef.current?.setFieldsValue(res.data);
+    // console.log("是编辑,在这里发请求拿数据", res);
+  }, []);
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    // return MessageFu.warning("有表单不符号规则!");
+  }, []);
+
+  useEffect(() => {
+    if (id) getInfoInAPIFu(id);
+    else {
+      FormBoxRef.current?.setFieldsValue({});
+    }
+  }, [getInfoInAPIFu, id]);
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (values: any) => {
+      const obj: SaveUserType = {
+        ...values,
+        id: id ? id : null,
+        // 角色id写死 2=》普通用户
+        roleId: 2,
+      };
+
+      const res: any = await userSaveAPI(obj);
+
+      if (res.code === 0) {
+        MessageFu.success(id ? "编辑成功!" : "新增成功!");
+        if (id) upTableList();
+        else addTableList();
+
+        closePage();
+      }
+      // console.log("通过校验,点击确定");
+    },
+    [addTableList, closePage, id, upTableList]
+  );
+
+  return (
+    <Modal
+      wrapClassName={styles.userAdd}
+      destroyOnClose
+      open={true}
+      title={id ? "编辑用户" : "新增用户"}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className="userAddMain">
+        <Form
+          scrollToFirstError={true}
+          ref={FormBoxRef}
+          name="basic"
+          labelCol={{ span: 5 }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete="off"
+        >
+          <Form.Item
+            label="账号名"
+            name="userName"
+            rules={[
+              { required: true, message: "请输入账号名!" },
+              { min: 6, message: "最少6个字!" },
+            ]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input
+              disabled={id}
+              maxLength={15}
+              showCount
+              placeholder="请输入内容"
+            />
+          </Form.Item>
+
+          <Form.Item
+            label="真实姓名"
+            name="realName"
+            // rules={[{ required: true, message: "请输入真实姓名!" }]}
+            getValueFromEvent={(e) => e.target.value.replace(/\s+/g, "")}
+          >
+            <Input maxLength={8} showCount placeholder="请输入内容" />
+          </Form.Item>
+
+          {id ? null : <div className="passTit">* 默认密码 123456</div>}
+
+          {/* 确定和取消按钮 */}
+          <br />
+          <Form.Item wrapperCol={{ offset: 9, span: 16 }}>
+            <Button type="primary" htmlType="submit">
+              提交
+            </Button>
+            &emsp;
+            <MyPopconfirm txtK="取消" onConfirm={closePage} />
+          </Form.Item>
+        </Form>
+      </div>
+    </Modal>
+  );
+}
+
+const MemoUserAdd = React.memo(UserAdd);
+
+export default MemoUserAdd;

+ 152 - 0
src/pages/Z1user/Z1auth.tsx

@@ -0,0 +1,152 @@
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import styles from "./index.module.scss";
+import { Button, Checkbox, Modal } from "antd";
+import MyPopconfirm from "@/components/MyPopconfirm";
+import { Z1_APIgetAuthByUserId, Z1_APIsetAuth } from "@/store/action/Z1user";
+import classNmaes from "classnames";
+import { MessageFu } from "@/utils/message";
+
+export type UserListType = {
+  id: number;
+  name: string;
+  children: {
+    id: number;
+    name: string;
+    authority: boolean;
+  }[];
+};
+
+type Props = {
+  authInfo: { id: number; name: string };
+  closeFu: () => void;
+};
+
+function Z1auth({ authInfo, closeFu }: Props) {
+  const getAuthByUserIdFu = useCallback(async () => {
+    const res = await Z1_APIgetAuthByUserId(authInfo.id);
+
+    if (res.code === 0) {
+      // 这里后台返回的一层的数据。懒得改逻辑,手动修改成2级的数据
+
+      setList([
+        {
+          id: Date.now(),
+          name: "",
+          children: res.data,
+        },
+      ]);
+
+      // setList(res.data)
+    }
+  }, [authInfo.id]);
+
+  useEffect(() => {
+    getAuthByUserIdFu();
+  }, [getAuthByUserIdFu]);
+
+  const [list, setList] = useState<UserListType[]>([]);
+
+  // 多选框变化
+  const onChange = useCallback(
+    (val: boolean, id1: number, id2: number) => {
+      setList(
+        list.map((v) => ({
+          ...v,
+          children:
+            v.id === id1
+              ? v.children.map((c) => ({
+                  ...c,
+                  authority: c.id === id2 ? val : c.authority,
+                }))
+              : v.children,
+        }))
+      );
+    },
+    [list]
+  );
+
+  // 二级选中的数组id集合
+  const checkIds = useMemo(() => {
+    const arr: number[] = [];
+    list.forEach((v) => {
+      if (v.children) {
+        v.children.forEach((c) => {
+          if (c.authority) arr.push(c.id);
+        });
+      }
+    });
+    return arr;
+  }, [list]);
+
+  // 点击确定
+  const btnOkFu = useCallback(async () => {
+    const obj = {
+      userId: authInfo.id,
+      resources: checkIds,
+    };
+
+    const res = await Z1_APIsetAuth(obj);
+
+    if (res.code === 0) {
+      MessageFu.success("授权成功!");
+      closeFu();
+    }
+  }, [authInfo.id, checkIds, closeFu]);
+
+  return (
+    <Modal
+      wrapClassName={styles.Z1auth}
+      open={true}
+      title={`${authInfo.name} - 权限管理`}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className="Z1aEmain">
+        {list.map((v) => (
+          <div key={v.id} className="Z1aRow">
+            {/* 隐藏一级数据 */}
+            {/* <div className="Z1aRow1">{v.name}</div> */}
+            <div className="Z1aRow2">
+              {v.children.map((c) => (
+                <div key={c.id}>
+                  <Checkbox
+                    checked={c.authority}
+                    onChange={(e) => onChange(e.target.checked, v.id, c.id)}
+                  >
+                    {c.name}
+                  </Checkbox>
+                </div>
+              ))}
+            </div>
+          </div>
+        ))}
+
+        <div
+          className={classNmaes(
+            "Z1aErr",
+            checkIds.length <= 0 ? "Z1aErrAc" : ""
+          )}
+        >
+          至少选中一个
+        </div>
+
+        <div className="Z1aEbtn">
+          <Button
+            type="primary"
+            onClick={btnOkFu}
+            disabled={checkIds.length <= 0}
+          >
+            提交
+          </Button>
+          &emsp;
+          <MyPopconfirm txtK="取消" onConfirm={closeFu} />
+        </div>
+      </div>
+    </Modal>
+  );
+}
+
+const MemoZ1auth = React.memo(Z1auth);
+
+export default MemoZ1auth;

+ 78 - 0
src/pages/Z1user/index.module.scss

@@ -0,0 +1,78 @@
+.Z1user {
+  :global {
+    .selectBox {
+      border-radius: 10px;
+      padding: 15px 24px;
+      background-color: #fff;
+      display: flex;
+      justify-content: space-between;
+    }
+
+    .tableBox {
+      border-radius: 10px;
+      overflow: hidden;
+      margin-top: 15px;
+      height: calc(100% - 77px);
+      background-color: #fff;
+    }
+  }
+}
+
+// 授权
+.Z1auth {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+
+    .ant-modal {
+      width: 800px !important;
+    }
+
+    .ant-modal-body {
+      border-top: 1px solid #ccc;
+    }
+
+    .Z1aEmain {
+      padding-top: 15px;
+
+      .Z1aRow {
+        margin-bottom: 10px;
+        // display: flex;
+
+        // 隐藏一级数据
+        // .Z1aRow1 {
+        //   width: 90px;
+        //   font-weight: 700;
+        // }
+
+        .Z1aRow2 {
+          width: calc(100% - 90px);
+          &>div{
+            margin-bottom: 15px;
+          }
+        }
+      }
+
+      .Z1aErr {
+        margin-bottom: 20px;
+        text-align: center;
+        color: #ff4d4f;
+        opacity: 0;
+        pointer-events: none;
+        transition: all .3s;
+        position: relative;
+        top: -10px;
+      }
+
+      .Z1aErrAc {
+        opacity: 1;
+        top: 0;
+      }
+
+      .Z1aEbtn {
+        text-align: center;
+      }
+    }
+  }
+}

+ 227 - 0
src/pages/Z1user/index.tsx

@@ -0,0 +1,227 @@
+import { RootState } from "@/store";
+import {
+  getUserListAPI,
+  userPassResetAPI,
+  userRemoveAPI,
+} from "@/store/action/Z1user";
+import { UserTableAPIType, UserTableListType } from "@/types";
+import { MessageFu } from "@/utils/message";
+import { Input, Button } from "antd";
+import React, {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useDispatch, useSelector } from "react-redux";
+import styles from "./index.module.scss";
+import UserAdd from "./UserAdd";
+import MyTable from "@/components/MyTable";
+import { Z1tableC } from "@/utils/tableData";
+import MyPopconfirm from "@/components/MyPopconfirm";
+import Z1auth from "./Z1auth";
+
+function Z1user() {
+  const dispatch = useDispatch();
+
+  // 顶部筛选
+  const [fromData, setFromData] = useState<UserTableAPIType>({
+    pageNum: 1,
+    pageSize: 10,
+    searchKey: "",
+  });
+
+  // 封装发送请求的函数
+
+  const getList = useCallback(async () => {
+    dispatch(getUserListAPI(fromData));
+  }, [dispatch, fromData]);
+
+  useEffect(() => {
+    getList();
+  }, [getList]);
+
+  const timeRef = useRef(-1);
+  // 用户名
+  const txtChangeFu = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>, key: "searchKey") => {
+      clearTimeout(timeRef.current);
+      timeRef.current = window.setTimeout(() => {
+        setFromData({
+          ...fromData,
+          [key]: e.target.value,
+          pageNum: 1,
+        });
+      }, 500);
+    },
+    [fromData]
+  );
+
+  // 点击重置
+  const [inputKey, setInputKey] = useState(1);
+  const resetSelectFu = useCallback(() => {
+    // 把2个输入框和时间选择器清空
+    setInputKey(Date.now());
+    setFromData({
+      pageNum: 1,
+      pageSize: 10,
+      searchKey: "",
+    });
+  }, []);
+
+  // 从仓库中获取表格数据
+  const tableInfo = useSelector((state: RootState) => state.Z1user.tableInfo);
+
+  // 点击删除
+  const delTableFu = useCallback(
+    async (id: number) => {
+      const res: any = await userRemoveAPI(id);
+      if (res.code === 0) {
+        MessageFu.success("删除成功!");
+        getList();
+      }
+    },
+    [getList]
+  );
+
+  // 点击重置密码
+  const resetPassFu = useCallback(async (id: number) => {
+    const res: any = await userPassResetAPI(id);
+    if (res.code === 0) MessageFu.success("重置成功!");
+  }, []);
+
+  // 0------------点击新增或者编辑出来的页面
+  const [editPageShow, setEditPageShow] = useState(false);
+  const editId = useRef(0);
+
+  const openEditPageFu = useCallback(
+    (id: number) => {
+      if (id === 0 && tableInfo.list.length >= 20)
+        return MessageFu.warning("最多支持20个用户!");
+
+      editId.current = id;
+      setEditPageShow(true);
+    },
+    [tableInfo.list.length]
+  );
+
+  const tableLastBtn = useMemo(() => {
+    return [
+      {
+        title: "操作",
+        render: (item: UserTableListType) => {
+          return item.isAdmin === 1 ? (
+            "-"
+          ) : (
+            <>
+              <MyPopconfirm
+                txtK="重置密码"
+                onConfirm={() => resetPassFu(item.id)}
+                Dom={
+                  <Button size="small" type="text">
+                    重置密码
+                  </Button>
+                }
+              />
+              <Button
+                size="small"
+                type="text"
+                onClick={() =>
+                  setAuthInfo({ id: item.id, name: item.userName })
+                }
+              >
+                权限管理
+              </Button>
+              <Button
+                size="small"
+                type="text"
+                onClick={() => openEditPageFu(item.id)}
+              >
+                编辑
+              </Button>
+              <MyPopconfirm txtK="删除" onConfirm={() => delTableFu(item.id)} />
+            </>
+          );
+        },
+      },
+    ];
+  }, [delTableFu, openEditPageFu, resetPassFu]);
+
+  // 授权管理
+  const [authInfo, setAuthInfo] = useState({ id: 0, name: "" });
+
+  return (
+    <div className={styles.Z1user}>
+      <div className="pageTitle">用户管理</div>
+      <div className="userTop">
+        <div className="selectBox">
+          <div className="selectBoxRow">
+            <span>搜索项:</span>
+            <Input
+              key={inputKey}
+              maxLength={10}
+              showCount
+              style={{ width: 300 }}
+              placeholder="请输入用户名"
+              allowClear
+              onChange={(e) => txtChangeFu(e, "searchKey")}
+            />
+          </div>
+
+          <div className="selectBoxRow">
+            &emsp;&emsp;<Button onClick={resetSelectFu}>重置</Button>
+            &emsp;&emsp;
+            <Button
+              type="primary"
+              onClick={() => {
+                if (tableInfo.total >= 20)
+                  return MessageFu.warning("最多20个账号!");
+                openEditPageFu(0);
+              }}
+            >
+              新增
+            </Button>
+          </div>
+        </div>
+      </div>
+      {/* 表格主体 */}
+      <div className="tableBox">
+        <MyTable
+          yHeight={630}
+          list={tableInfo.list}
+          columnsTemp={Z1tableC}
+          lastBtn={tableLastBtn}
+          pageNum={fromData.pageNum}
+          pageSize={fromData.pageSize}
+          total={tableInfo.total}
+          onChange={(pageNum, pageSize) =>
+            setFromData({ ...fromData, pageNum, pageSize })
+          }
+        />
+      </div>
+
+      {/* 点击新增或者编辑 */}
+      {editPageShow ? (
+        <UserAdd
+          id={editId.current}
+          closePage={() => setEditPageShow(false)}
+          upTableList={getList}
+          addTableList={resetSelectFu}
+        />
+      ) : null}
+
+      {/* 点击授权 */}
+      {authInfo.id ? (
+        <Z1auth
+          authInfo={authInfo}
+          closeFu={() => setAuthInfo({ id: 0, name: "" })}
+        />
+      ) : null}
+    </div>
+  );
+}
+
+const MemoZ1user = React.memo(Z1user);
+
+export default MemoZ1user;

+ 25 - 0
src/pages/Z2log/index.module.scss

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

+ 100 - 0
src/pages/Z2log/index.tsx

@@ -0,0 +1,100 @@
+import { RootState } from "@/store";
+import { getLogListAPI } from "@/store/action/Z2log";
+import { Input, DatePicker } from "antd";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { useDispatch, useSelector } from "react-redux";
+
+import styles from "./index.module.scss";
+import MyTable from "@/components/MyTable";
+import { Z2tableC } from "@/utils/tableData";
+
+const { RangePicker } = DatePicker;
+
+function Z2log() {
+  const dispatch = useDispatch();
+
+  // 筛选和分页
+  const [fromData, setFromData] = useState({
+    searchKey: "",
+    pageSize: 10,
+    pageNum: 1,
+    startTime: "",
+    endTime: "",
+  });
+
+  // 账号的输入
+  const nameTime = useRef(-1);
+  const nameChange = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>) => {
+      clearTimeout(nameTime.current);
+      nameTime.current = window.setTimeout(() => {
+        setFromData({ ...fromData, searchKey: e.target.value, pageNum: 1 });
+      }, 500);
+    },
+    [fromData]
+  );
+  // 时间选择器改变
+  const timeChange = useCallback(
+    (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";
+      }
+      setFromData({ ...fromData, startTime, endTime, pageNum: 1 });
+    },
+    [fromData]
+  );
+
+  useEffect(() => {
+    dispatch(getLogListAPI(fromData));
+  }, [dispatch, fromData]);
+
+  // ---------关于表格
+  const tableInfo = useSelector((state: RootState) => state.Z2log.tableInfo);
+
+  return (
+    <div className={styles.Z2log}>
+      <div className="pageTitle">操作日志</div>
+      <div className="logTop">
+        <div className="tableSelectBox">
+          <div className="row">
+            <span>账号:</span>
+            <Input
+              maxLength={10}
+              showCount
+              style={{ width: 300 }}
+              placeholder="请输入账号"
+              allowClear
+              onChange={(e) => nameChange(e)}
+            />
+          </div>
+          <div className="row">
+            <span>操作日期:</span>
+            <RangePicker onChange={timeChange} />
+          </div>
+        </div>
+      </div>
+
+      {/* 表格主体 */}
+      <div className="tableMain">
+        <MyTable
+          yHeight={630}
+          list={tableInfo.list}
+          columnsTemp={Z2tableC}
+          pageNum={fromData.pageNum}
+          pageSize={fromData.pageSize}
+          total={tableInfo.total}
+          onChange={(pageNum, pageSize) =>
+            setFromData({ ...fromData, pageNum, pageSize })
+          }
+        />
+      </div>
+    </div>
+  );
+}
+
+const MemoZ2log = React.memo(Z2log);
+
+export default MemoZ2log;

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

+ 68 - 0
src/store/action/Z1user.ts

@@ -0,0 +1,68 @@
+import { SaveUserType, UserTableAPIType } from "@/types";
+import http from "@/utils/http";
+import { AppDispatch } from "..";
+/**
+ * 获取用户管理表格列表
+ */
+export const getUserListAPI = (data: UserTableAPIType): any => {
+  return async (dispatch: AppDispatch) => {
+    const res = await http.post("sys/user/list", data);
+    if (res.code === 0) {
+      const obj = {
+        list: res.data.records,
+        total: res.data.total,
+      };
+
+      dispatch({ type: "Z1/getList", payload: obj });
+    }
+  };
+};
+
+/**
+ * 删除用户
+ */
+export const userRemoveAPI = (id: number) => {
+  return http.get(`sys/user/removes/${id}`);
+};
+
+/**
+ * 重置密码
+ */
+export const userPassResetAPI = (id: number) => {
+  return http.get(`sys/user/resetPass/${id}`);
+};
+
+/**
+ * 新增/修改用户信息
+ */
+export const userSaveAPI = (data: SaveUserType) => {
+  return http.post("sys/user/save", data);
+};
+
+/**
+ * 通过id获取角色详情
+ */
+export const getUserInfoByIdAPI = (id: number) => {
+  return http.get(`sys/user/detail/${id}`);
+};
+
+/**
+ * 角色授权-获取
+ */
+export const Z1_APIgetAuthByUserId = (userId: number) => {
+  return http.get(`sys/user/perm/getUserTree/${userId}`);
+};
+
+/**
+ * 角色授权-设置
+ */
+export const Z1_APIsetAuth = (data: any) => {
+  return http.post("sys/user/perm/auth", data);
+};
+
+/**
+ * 角色授权-获取 初始页面获取
+ */
+export const Z1_APIgetAuthBase = () => {
+  return http.get("sys/user/perm/getTree");
+};

+ 17 - 0
src/store/action/Z2log.ts

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

+ 49 - 0
src/store/action/layout.ts

@@ -0,0 +1,49 @@
+import { domShowFu, progressDomFu } from "@/utils/domShow";
+import http from "@/utils/http";
+import axios from "axios";
+import store 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 API_LoginGetCode = () => {
+  return http.get("show/getRandCode", { responseType: "blob" });
+};
+
+const CancelToken = axios.CancelToken;
+/**
+ * 上传封面图和附件
+ */
+export const API_upFile = (data: any, url: string) => {
+  domShowFu("#UpAsyncLoding", true);
+
+  return http.post(url, 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 },
+      });
+    }),
+  });
+};

+ 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

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

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

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

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

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

@@ -0,0 +1,17 @@
+// 导入合并reducer的依赖
+import { combineReducers } from "redux";
+
+// 导入 登录 模块的 reducer
+import A0Layout from "./layout";
+import Z1user from "./Z1user";
+import Z2log from "./Z2log";
+
+// 合并 reducer
+const rootReducer = combineReducers({
+  A0Layout,
+  Z1user,
+  Z2log,
+});
+
+// 默认导出
+export default rootReducer;

+ 65 - 0
src/store/reducer/layout.ts

@@ -0,0 +1,65 @@
+import { LookDomType } from "@/types";
+import { MessageType } from "@/utils/message";
+
+// 初始化状态
+const initState = {
+  // 所有图片点击预览查看大图
+  lookBigImg: {
+    url: "",
+    show: false,
+  },
+  // 查看视频、音频、模型
+  lookDom: {
+    src: "",
+    type: "",
+  } as LookDomType,
+
+  // antd轻提示(兼容360浏览器)
+  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/lookDom"; payload: LookDomType }
+  | { type: "layout/message"; payload: MessageType }
+  | {
+      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/lookDom":
+      return { ...state, lookDom: action.payload };
+
+    // antd轻提示(兼容360浏览器)
+    case "layout/message":
+      return { ...state, message: action.payload };
+    // 上传文件点击取消
+    case "layout/closeUpFile":
+      return { ...state, closeUpFile: action.payload };
+    default:
+      return state;
+  }
+}

+ 31 - 0
src/types/api/Z1user.d.ts

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

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

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

+ 25 - 0
src/types/api/layot.d.ts

@@ -0,0 +1,25 @@
+export type LookDomType = {
+  src: string;
+  type: "video" | "audio" | "model" | "";
+  flag?: boolean;
+};
+
+export type RouterTypeRow = {
+  id: number;
+  name: string;
+  path: string;
+  Com: React.LazyExoticComponent<React.MemoExoticComponent<() => JSX.Element>>;
+}[];
+
+export type RouterType = {
+  id: number;
+  name: string;
+  son: RouterTypeRow;
+}[];
+
+export type FileImgListType = {
+  id: number;
+  fileName: string;
+  filePath: string;
+  type: "img" | "video" | "doc";
+};

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

@@ -0,0 +1,8 @@
+declare module "history";
+declare module "*.scss";
+declare module "*.png";
+declare module "*.jpg";
+declare module "*.gif";
+declare module "*.svg";
+declare module "js-export-excel";
+declare module 'braft-utils';

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

@@ -0,0 +1,3 @@
+export * from './api/layot'
+export * from './api/Z1user'
+export * from './api/Z2log'

+ 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().A0Layout.closeUpFile.state)
+      store.dispatch({
+        type: "layout/closeUpFile",
+        payload: { fu: () => {}, state: false },
+      });
+  }, 200);
+};

+ 3 - 0
src/utils/history.ts

@@ -0,0 +1,3 @@
+import { createHashHistory  } from 'history'
+const history = createHashHistory()
+export default history

+ 110 - 0
src/utils/http.ts

@@ -0,0 +1,110 @@
+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";
+
+const envFlag = process.env.NODE_ENV === "development";
+
+// const baseUrlTemp = "xxxxxxxxxxx"; // 测试环境
+const baseUrlTemp = "http://192.168.20.61:8070"; // 线下环境
+
+const baseFlag = baseUrlTemp.includes("https://");
+
+// 请求基地址
+export const baseURL = envFlag
+  ? `${baseUrlTemp}${baseFlag ? "" : "/api/"}`
+  : "";
+
+// 处理  类型“AxiosResponse<any, any>”上不存在属性“code”
+declare module "axios" {
+  interface AxiosResponse {
+    code: number;
+    timestamp: string;
+    // 这里追加你的参数
+  }
+}
+
+// 创建 axios 实例
+const http = axios.create({
+  baseURL: `${baseURL}${baseFlag ? "/api/" : ""}`,
+  timeout: 10000,
+});
+
+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) {
+      removeTokenInfo();
+      history.push("/login");
+      clearTimeout(timeId);
+      timeId = window.setTimeout(() => {
+        MessageFu.warning("登录失效!");
+      }, 200);
+    } else if (response.data.code === 0) {
+      // MessageFu.success(response.data.msg);
+    } else if (response.data.code !== 3014)
+      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().A0Layout.closeUpFile.state)
+          MessageFu.warning("取消上传!");
+        else MessageFu.error("网络繁忙,请稍后重试!");
+      } else {
+        if (
+          err.response &&
+          err.response.data &&
+          err.response.data.msg &&
+          err.response.data.msg.length < 30
+        ) {
+          MessageFu.error(err.response.data.msg);
+          // 没有权限
+          if (err.response.data.code === 5003) {
+            removeTokenInfo();
+            history.push("/login");
+          }
+        } 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;

+ 40 - 0
src/utils/storage.ts

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

+ 122 - 0
src/utils/tableData.ts

@@ -0,0 +1,122 @@
+/**
+ * index:序号
+ * txt:正常数据
+ * img:图片
+ * txtChange:判断显示不同字段
+ * text:文字比较多的情况、获取使用其他标签(没有的需要自己配置)
+ */
+// export const A1tableCFu = (type: "video" | "poster") => {
+//   return [
+//     ["index", "序号"],
+//     ["txt", "标题", "name"],
+//     ["img", type === "video" ? "视频封面" : "海报", "thumb"],
+//     ["txtChange", "自动播放", "display", { 0: "否", 1: "是" }],
+//     ["text", "创建日期",'description', 50,A],
+//   ];
+// };
+
+export const A2tableC = [
+  ["txt", "标题", "name"],
+  ["img", "封面", "thumb"],
+  ["txt", "发布日期", "publishDate"],
+  ["txt", "所属栏目", "dictName"],
+  ["text", "摘要", "description", 50],
+];
+
+export const A3tableC = [
+  ["txt", "标题", "name"],
+  ["img", "封面", "thumb"],
+  ["txt", "发布日期", "publishDate"],
+  ["text", "链接", "link", 50, "A"],
+];
+
+export const A4tableC = [
+  ["txt", "标题", "name"],
+  ["img", "封面", "thumb"],
+  ["txt", "发布日期", "publishDate"],
+  ["txt", "所属栏目", "dictName"],
+  ["txt", "质地", "texture"],
+  ["txt", "年代", "age"],
+  ["text", "简介", "description", 50],
+];
+
+export const A5tableC = [
+  ["txt", "标题", "name"],
+  ["img", "封面", "thumb"],
+  ["txt", "发布日期", "publishDate"],
+  ["txt", "展览类型", "type"],
+  ["text", "摘要", "description", 50],
+];
+
+export const A6tableC = [
+  ["txt", "标题", "name"],
+  ["img", "封面", "thumb"],
+  ["txt", "发布日期", "publishDate"],
+  ["txt", "所属栏目", "dictName"],
+  ["txtChange", "需要预约", "isNeed", { 1: "是", 0: "否" }],
+];
+
+export const A7tableC = [
+  ["txt", "标题", "name"],
+  ["img", "封面", "thumb"],
+  ["txt", "发布日期", "publishDate"],
+];
+
+export const B1tableC = [
+  ["txt", "申请时间", "createTime"],
+  ["txtChange", "验证状态", "status", { 0: "未验证", 1: "已验证" }],
+  ["txt", "姓名", "name"],
+  ["txt", "联系电话", "phone"],
+  ["txt", "人数", "pcs"],
+  ["txt", "预约日期", "bookDate"],
+  ["txt", "预约时段", "time"],
+];
+
+export const B2tableC = [
+  ["txt", "申请时间", "createTime"],
+  ["txt", "姓名", "name"],
+  ["txt", "活动名称", "title"],
+  ["txt", "联系电话", "phone"],
+  ["txt", "身份证", "identity"],
+  ["text", "预约说明", "description", 50],
+];
+
+export const B3tableC = [
+  ["txt", "申请时间", "createTime"],
+  ["txt", "姓名", "name"],
+  ["txt", "联系电话", "phone"],
+  ["txt", "身份证", "identity"],
+  ["text", "预约说明", "description", 50],
+];
+
+export const C1tableC = [
+  ["txt", "时段", "time"],
+  ["txt", "周一", "monday"],
+  ["txt", "周二", "tuesday"],
+  ["txt", "周三", "wednesday"],
+  ["txt", "周四", "thursday"],
+  ["txt", "周五", "friday"],
+  ["txt", "周六", "saturday"],
+  ["txt", "周日", "sunday"],
+];
+
+export const Z0tableC = [
+  ["txt", "栏目名称", "name"],
+  ["text", "说明", "rtf", 50],
+];
+
+export const Z1tableC = [
+  ["txt", "用户名", "userName"],
+  ["txtChange", "角色", "isAdmin", { 1: "管理员", 0: "普通成员" }],
+  ["txt", "真实姓名", "realName"],
+  ["txt", "创建日期", "createTime"],
+];
+
+export const Z2tableC = [
+  ["index", "序号"],
+  ["txt", "账号", "userName"],
+  ["txt", "操作日期", "createTime"],
+  ["txt", "IP记录", "ip"],
+  ["txt", "操作模块", "type"],
+  ["txt", "操作事件", "description"],
+];

+ 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"
+  ]
+}

Файловите разлики са ограничени, защото са твърде много
+ 11010 - 0
yarn.lock