shaogen1995 2 месяцев назад
Сommit
a040f31675
100 измененных файлов с 36740 добавлено и 0 удалено
  1. 12 0
      .editorconfig
  2. 3 0
      .env
  3. 23 0
      .gitignore
  4. BIN
      .nvmrc
  5. 11 0
      .prettierrc.js
  6. 39 0
      .vscode/settings.json
  7. 10 0
      README.md
  8. 10 0
      config-overrides.js
  9. 31293 0
      package-lock.json
  10. 74 0
      package.json
  11. 8 0
      path.tsconfig.json
  12. BIN
      public/favicon.png
  13. 17 0
      public/index.html
  14. 124 0
      src/App.tsx
  15. BIN
      src/assets/img/IMGerror.png
  16. BIN
      src/assets/img/Union.png
  17. BIN
      src/assets/img/UnionNo.png
  18. BIN
      src/assets/img/bg.jpg
  19. BIN
      src/assets/img/latLeftBg.jpg
  20. BIN
      src/assets/img/loading.gif
  21. BIN
      src/assets/img/loginBtn.png
  22. BIN
      src/assets/img/loginInco1.png
  23. BIN
      src/assets/img/loginInco2.png
  24. BIN
      src/assets/img/loginInco3.png
  25. BIN
      src/assets/img/logo.png
  26. BIN
      src/assets/img/user.png
  27. 255 0
      src/assets/styles/base.css
  28. 407 0
      src/assets/styles/base.less
  29. 21 0
      src/components/AsyncSpinLoding/index.module.scss
  30. 15 0
      src/components/AsyncSpinLoding/index.tsx
  31. 32 0
      src/components/AuthRoute/index.tsx
  32. 51 0
      src/components/ImageLazy/index.module.scss
  33. 63 0
      src/components/ImageLazy/index.tsx
  34. 65 0
      src/components/LookDom/index.module.scss
  35. 46 0
      src/components/LookDom/index.tsx
  36. 29 0
      src/components/Message/index.tsx
  37. 52 0
      src/components/MyPopconfirm.tsx
  38. 18 0
      src/components/MyTable/index.module.scss
  39. 217 0
      src/components/MyTable/index.tsx
  40. 22 0
      src/components/NotFound/index.tsx
  41. 9 0
      src/components/SpinLoding/index.module.scss
  42. 13 0
      src/components/SpinLoding/index.tsx
  43. 21 0
      src/components/SpinLodingSon/index.module.scss
  44. 15 0
      src/components/SpinLodingSon/index.tsx
  45. 43 0
      src/components/UpAsyncLoding/index.module.scss
  46. 38 0
      src/components/UpAsyncLoding/index.tsx
  47. 41 0
      src/components/YtableVideo/index.module.scss
  48. 36 0
      src/components/YtableVideo/index.tsx
  49. 195 0
      src/components/ZRichTextOne/index.module.scss
  50. 173 0
      src/components/ZRichTextOne/index.tsx
  51. 101 0
      src/components/ZupOne/index.module.scss
  52. 295 0
      src/components/ZupOne/index.tsx
  53. 36 0
      src/index.tsx
  54. 4 0
      src/pages/Aresource/A1overview/index.module.scss
  55. 13 0
      src/pages/Aresource/A1overview/index.tsx
  56. 4 0
      src/pages/Aresource/A2resFile/index.module.scss
  57. 13 0
      src/pages/Aresource/A2resFile/index.tsx
  58. 4 0
      src/pages/Aresource/A3resTag/index.module.scss
  59. 13 0
      src/pages/Aresource/A3resTag/index.tsx
  60. 12 0
      src/pages/Layout/PassEdit/index.module.scss
  61. 140 0
      src/pages/Layout/PassEdit/index.tsx
  62. 138 0
      src/pages/Layout/data.ts
  63. 226 0
      src/pages/Layout/index.module.scss
  64. 316 0
      src/pages/Layout/index.tsx
  65. 165 0
      src/pages/Login/index.module.scss
  66. 135 0
      src/pages/Login/index.tsx
  67. 4 0
      src/pages/Zsystem/Z1sysSet/index.module.scss
  68. 13 0
      src/pages/Zsystem/Z1sysSet/index.tsx
  69. 226 0
      src/pages/Zsystem/Z2dict/Z2add.tsx
  70. 68 0
      src/pages/Zsystem/Z2dict/data.ts
  71. 205 0
      src/pages/Zsystem/Z2dict/index.module.scss
  72. 287 0
      src/pages/Zsystem/Z2dict/index.tsx
  73. 4 0
      src/pages/Zsystem/Z3ipSet/index.module.scss
  74. 13 0
      src/pages/Zsystem/Z3ipSet/index.tsx
  75. 4 0
      src/pages/Zsystem/Z4numSet/index.module.scss
  76. 13 0
      src/pages/Zsystem/Z4numSet/index.tsx
  77. 4 0
      src/pages/Zsystem/Z5flowSet/index.module.scss
  78. 13 0
      src/pages/Zsystem/Z5flowSet/index.tsx
  79. 4 0
      src/pages/Zsystem/Z6roleSet/index.module.scss
  80. 13 0
      src/pages/Zsystem/Z6roleSet/index.tsx
  81. 4 0
      src/pages/Zsystem/Z7userSet/index.module.scss
  82. 13 0
      src/pages/Zsystem/Z7userSet/index.tsx
  83. 4 0
      src/pages/Zsystem/Z8sysLog/index.module.scss
  84. 13 0
      src/pages/Zsystem/Z8sysLog/index.tsx
  85. 5 0
      src/pages/初始化组件/index.module.scss
  86. 14 0
      src/pages/初始化组件/index.tsx
  87. 42 0
      src/store/action/Z2dict.ts
  88. 63 0
      src/store/action/layout.ts
  89. 20 0
      src/store/index.ts
  90. 24 0
      src/store/reducer/Z2dict.ts
  91. 15 0
      src/store/reducer/index.ts
  92. 87 0
      src/store/reducer/layout.ts
  93. 9 0
      src/types/declaration.d.ts
  94. 35 0
      src/utils/domShow.ts
  95. 38 0
      src/utils/history.ts
  96. 115 0
      src/utils/http.ts
  97. 65 0
      src/utils/index.ts
  98. 50 0
      src/utils/message.ts
  99. 137 0
      src/utils/pass.ts
  100. 0 0
      src/utils/storage.ts

+ 12 - 0
.editorconfig

@@ -0,0 +1,12 @@
+root = true # 控制配置文件 .editorconfig 是否生效的字段
+ 
+[**] # 匹配全部文件
+indent_style = space # 缩进风格,可选space|tab
+indent_size = 2 # 缩进的空格数
+charset = utf-8 # 设置字符集
+trim_trailing_whitespace = true # 删除一行中的前后空格
+insert_final_newline = true # 设为true表示使文件以一个空白行结尾
+end_of_line = lf
+ 
+[**.md] # 匹配md文件
+trim_trailing_whitespace = false

+ 3 - 0
.env

@@ -0,0 +1,3 @@
+# .env.production
+GENERATE_SOURCEMAP = false
+# 关闭映射

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


+ 11 - 0
.prettierrc.js

@@ -0,0 +1,11 @@
+module.exports = {
+  printWidth: 100, // 一行的字符数,如果超过会进行换行
+  tabWidth: 2, // 一个tab代表几个空格数,默认就是2
+  useTabs: false, // 是否启用tab取代空格符缩进,.editorconfig设置空格缩进,所以设置为false
+  semi: false, // 行尾是否使用分号,默认为true
+  singleQuote: true, // 字符串是否使用单引号
+  trailingComma: "none", // 对象或数组末尾是否添加逗号 none| es5| all
+  jsxSingleQuote: true, // 在jsx里是否使用单引号,你看着办
+  bracketSpacing: true, // 对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
+  arrowParens: "avoid", // 箭头函数如果只有一个参数则省略括号
+};

+ 39 - 0
.vscode/settings.json

@@ -0,0 +1,39 @@
+{
+  "search.exclude": {
+    "/node_modules": true,
+    "dist": true,
+    "pnpm-lock.sh": true
+  },
+  "editor.formatOnSave": true,
+  "[javascript]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[javascriptreact]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[typescript]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[typescriptreact]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[json]": {
+    "editor.defaultFormatter": "vscode.json-language-features"
+  },
+  "[html]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[markdown]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[css]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[less]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "[scss]": {
+    "editor.defaultFormatter": "esbenp.prettier-vscode"
+  },
+  "liveServer.settings.port": 5502
+}

+ 10 - 0
README.md

@@ -0,0 +1,10 @@
+1.使用 yarn。 npm 有问题
+node 版本:24.12.0(如果 npm run build 报错,使用 22.18.0)
+
+2.测试后端文档地址:https://sit-qingdaobeer.4dage.com/api/doc.html#/home
+
+3.测试堡垒机位置:/Default/腾讯云/项目节点/腾讯云-四维时代-项目测试服务器-111.230.233.212/data/data/museum_shandong_qingdao_pijiu_storage_data/backstage
+
+4:测试网址:https://sit-qingdaobeer.4dage.com/backstage
+
+5:蓝湖地址:https://lanhuapp.com/web/#/item/project/product?tid=de3e5e3e-a489-4b19-862a-7c87ce113467&pid=bee8cc26-6a17-4b77-8e80-45b03b05465a&image_id=a4256a90-52f3-41bc-acb3-6ae1344fa1c2&docId=a4256a90-52f3-41bc-acb3-6ae1344fa1c2&docType=axure&versionId=77cd75fb-b6af-43d6-b5e2-11d3ac67d9b5&pageId=2fe0d9a1b6a144c1b199ed14c162aa98&parentId=8f795cc5-2462-4553-acc4-02267adcff9a&share_type=quickShare

+ 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


+ 74 - 0
package.json

@@ -0,0 +1,74 @@
+{
+  "name": "demo2",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@ant-design/cssinjs": "^1.5.6",
+    "@dnd-kit/core": "^6.3.1",
+    "@dnd-kit/sortable": "^10.0.0",
+    "@dnd-kit/utilities": "^3.2.2",
+    "@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",
+    "echarts": "^6.0.0",
+    "echarts-for-react": "^3.0.6",
+    "file-saver": "^2.0.5",
+    "js-base64": "^3.7.3",
+    "js-export-excel": "^1.1.4",
+    "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",
+    "xlsx": "^0.18.5"
+  },
+  "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",
+    "@types/xlsx": "^0.0.36",
+    "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.png


+ 17 - 0
public/index.html

@@ -0,0 +1,17 @@
+<!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.png" type="image/x-icon" />
+
+    <title>潮学研究院图书数字展示平台-后台管理</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+  </body>
+</html>

+ 124 - 0
src/App.tsx

@@ -0,0 +1,124 @@
+// 关于路由
+import React, { useCallback, useEffect, useRef } 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'))
+
+let tempW = document.documentElement.clientWidth
+
+let tempH = document.documentElement.clientHeight
+
+let tempMax = tempW >= tempH ? tempW : tempH
+let tempMin = tempW >= tempH ? tempH : tempW
+
+const pageBi = Math.round(Number((tempMax / tempMin).toFixed(2)))
+
+// 设计图按照 1920 X 960 来开发
+const planSize = {
+  width: 1920,
+  height: Math.round(Number((1920 / pageBi).toFixed(0)))
+}
+
+export default function App() {
+  const rootRef = useRef<any>(null)
+
+  const timeRef = useRef(0)
+
+  const pageFullChangeFu = useCallback((num: number = 200) => {
+    clearTimeout(timeRef.current)
+    timeRef.current = window.setTimeout(() => {
+      let width = window.visualViewport
+        ? window.visualViewport.width
+        : document.documentElement.clientWidth
+      let height = window.visualViewport
+        ? window.visualViewport.height
+        : document.documentElement.clientHeight
+
+      if (width >= height) {
+        document.querySelector('body')!.style.overflow = 'hidden'
+
+        // if (tempMax - width > 100) return
+
+        const sizeW = width / planSize.width
+        let sizeH = height / planSize.height
+
+        let moveX = (planSize.width - width) / 2
+        let moveY = (planSize.height - height) / 2
+
+        if (width >= planSize.width) moveX = 0
+        rootRef.current.style.left = '0'
+        rootRef.current.style.transform = `translate3d(${-moveX}px,${-moveY}px,0px) scale(${sizeW},${sizeH}) rotate(0deg)`
+        rootRef.current.style.transformOrigin = 'center'
+      } else {
+        document.querySelector('body')!.style.overflow = 'auto'
+        rootRef.current.style.transform = 'none'
+      }
+    }, num)
+  }, [])
+
+  useEffect(() => {
+    rootRef.current = document.querySelector('#root')
+    rootRef.current.style.width = planSize.width + 'px'
+    rootRef.current.style.height = planSize.height + 'px'
+
+    pageFullChangeFu(0)
+    window.addEventListener('resize', () => pageFullChangeFu(), false)
+  }, [pageFullChangeFu])
+
+  // 从仓库中获取查看图片的信息
+  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 />
+
+      {/* 所有图片点击预览查看大图 */}
+      {lookBigImg.show ? (
+        <Image
+          preview={{
+            visible: lookBigImg.show,
+            src: lookBigImg.url,
+            onVisibleChange: value => {
+              // 清除仓库信息
+              store.dispatch({
+                type: 'layout/lookBigImg',
+                payload: { url: '', show: false }
+              })
+            }
+          }}
+        />
+      ) : null}
+
+      {/* 上传附件的进度条元素 */}
+      <UpAsyncLoding />
+
+      {/* 查看视频音频 */}
+      <LookDom />
+
+      {/* antd 轻提示 ---兼容360浏览器 */}
+      <MessageCom />
+    </>
+  )
+}

BIN
src/assets/img/IMGerror.png


BIN
src/assets/img/Union.png


BIN
src/assets/img/UnionNo.png


BIN
src/assets/img/bg.jpg


BIN
src/assets/img/latLeftBg.jpg


BIN
src/assets/img/loading.gif


BIN
src/assets/img/loginBtn.png


BIN
src/assets/img/loginInco1.png


BIN
src/assets/img/loginInco2.png


BIN
src/assets/img/loginInco3.png


BIN
src/assets/img/logo.png


BIN
src/assets/img/user.png


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

@@ -0,0 +1,255 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+html {
+  height: 100%;
+  font-size: 14px;
+}
+/* 主题色 */
+:root {
+  --themeColor: #002c15;
+  --loginTxt: #fddb77;
+  --boxBcaColor: #f8f8f8;
+  --txtColor: #4c7f7a;
+  --myAntdColor: #ab925b;
+}
+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: var(--themeColor) !important;
+}
+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;
+}
+/* 找不到页面 */
+.noFindPage {
+  opacity: 0;
+  transition: opacity 0.5s;
+}
+.noFindPage .ant-result-title {
+  display: none;
+}
+/* 兼容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;
+  position: relative;
+  overflow: hidden;
+  /* 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 .tableImgAuto {
+  display: flex;
+  justify-content: center;
+}
+#root .ant-image {
+  display: none;
+}
+#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;
+}
+#upInputAudio {
+  display: none;
+}
+.pageTitle {
+  font-size: 18px;
+  position: absolute;
+  z-index: 11;
+  top: -72px;
+  left: -18px;
+  padding-left: 40px;
+  color: #fff;
+  text-shadow: 2px 2px 2px black;
+  letter-spacing: 2px;
+}
+.pageTitle::before {
+  position: absolute;
+  left: -7px;
+  content: '';
+  font-size: 24px;
+  font-weight: 700;
+  top: 0px;
+  width: 2px;
+  height: 26px;
+  background-color: #596754;
+}
+.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;
+}
+.ant-image-preview-operations {
+  background-color: rgba(0, 0, 0, 0.8) !important;
+}
+.ant-table-body {
+  background-color: var(--boxBcaColor) !important;
+}
+/* antd表格居中 */
+.ant-table-cell {
+  text-align: center !important;
+}
+.ant-table-header .ant-table-cell {
+  color: var(--themeColor) !important;
+  background-color: #d6e6df !important;
+}
+.ant-table-body .ant-table-cell {
+  border-color: #ccc !important;
+}
+#editBox {
+  font-size: 16px;
+  background-color: var(--boxBcaColor);
+  border-radius: 10px;
+}
+#editBox .editMain {
+  width: 100%;
+  height: calc(100% - 60px);
+  overflow-y: auto;
+}
+/* 普通文字按钮的颜色 */
+.ant-btn-text {
+  color: var(--myAntdColor) !important;
+}
+.ant-btn-text:disabled {
+  cursor: not-allowed !important;
+  color: rgba(0, 0, 0, 0.25) !important;
+}
+/* 按钮的危险颜色 */
+.ant-btn-text.ant-btn-dangerous {
+  color: #ff4d4d !important;
+}
+.ant-dropdown-menu-title-content {
+  text-align: center;
+}
+.tablelookVideo {
+  display: flex;
+  justify-content: center;
+  position: relative;
+}
+.tablelookVideo .tablelookBox {
+  cursor: pointer;
+  transition: opacity 0.3s;
+  opacity: 0;
+  z-index: 10;
+  position: absolute;
+  top: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  width: 60px;
+  height: 60px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 18px;
+  color: #fff;
+  background-color: rgba(0, 0, 0, 0.6);
+}
+.tablelookVideo .tablelookBox > div {
+  font-size: 14px;
+}
+.tablelookVideo .tablelookBox:hover {
+  opacity: 1;
+}
+.tablelookVideo video {
+  width: 60px;
+  height: 60px;
+  object-fit: cover;
+}
+.ant-table-wrapper .ant-table-selection,
+.ant-table-wrapper .ant-checkbox-wrapper {
+  width: 50px !important;
+}
+.ant-table-wrapper .ant-table-selection .ant-checkbox-inner,
+.ant-table-wrapper .ant-checkbox-wrapper .ant-checkbox-inner {
+  width: 24px !important;
+  height: 24px !important;
+}
+.ant-table-wrapper .ant-table-selection .ant-checkbox-inner:after,
+.ant-table-wrapper .ant-checkbox-wrapper .ant-checkbox-inner:after {
+  left: 35%;
+}
+.ant-table-wrapper .ant-table-tbody .ant-checkbox-wrapper {
+  position: relative;
+  left: -2px;
+}
+.ant-table-wrapper .ant-table-thead .ant-checkbox-indeterminate .ant-checkbox-inner:after {
+  width: 15px;
+  height: 15px;
+  left: 50%;
+}

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

@@ -0,0 +1,407 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+html {
+  height: 100%;
+  font-size: 14px;
+  // user-select: none;
+}
+
+/* 主题色 */
+:root {
+  // 文字颜色
+  --themeColor: #002c15;
+  // 登录页颜色
+  --loginTxt: #fddb77;
+  // 一个模块一个模块的背景色
+  --boxBcaColor: #f8f8f8;
+
+  // 这个项目颜色有点杂--字典、组织、标签模块主题色
+  --txtColor: #4c7f7a;
+
+  // antd主题色
+  --myAntdColor: #ab925b;
+}
+
+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: var(--themeColor) !important;
+}
+
+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;
+}
+
+/* 找不到页面 */
+.noFindPage {
+  opacity: 0;
+  transition: opacity 0.5s;
+  .ant-result-title {
+    display: none;
+  }
+}
+
+/* 兼容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;
+  position: relative;
+  overflow: hidden;
+
+  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);
+  // }
+
+  /* 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;
+  }
+
+  // 树型 表格 定制化
+  #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;
+}
+
+#upInputAudio {
+  display: none;
+}
+
+// 页面标题
+.pageTitle {
+  font-size: 18px;
+  position: absolute;
+  z-index: 11;
+  top: -72px;
+  left: -18px;
+  padding-left: 40px;
+  color: #fff;
+  text-shadow: 2px 2px 2px black;
+  letter-spacing: 2px;
+
+  &::before {
+    position: absolute;
+    left: -7px;
+    content: '';
+    font-size: 24px;
+    font-weight: 700;
+    top: 0px;
+    width: 2px;
+    height: 26px;
+    background-color: #596754;
+  }
+}
+
+// 滚动条
+.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;
+}
+.ant-image-preview-operations {
+  background-color: rgba(0, 0, 0, 0.8) !important;
+}
+
+// --------关于表格
+.ant-table-body {
+  background-color: var(--boxBcaColor) !important;
+}
+/* antd表格居中 */
+.ant-table-cell {
+  text-align: center !important;
+}
+
+.ant-table-header {
+  .ant-table-cell {
+    color: var(--themeColor) !important;
+    background-color: #d6e6df !important;
+  }
+}
+
+.ant-table-body {
+  .ant-table-cell {
+    border-color: #ccc !important;
+  }
+}
+
+// .ant-table-placeholder {
+//   background-color: transparent !important;
+//   .ant-empty-description {
+//     color: var(--themeColor) !important;
+//   }
+//   ellipse {
+//     fill: var(--boxBcaColor) !important;
+//   }
+//   path {
+//     fill: transparent !important;
+//   }
+//   g {
+//     stroke: var(--themeColor) !important;
+//   }
+// }
+#editBox {
+  font-size: 16px;
+  background-color: var(--boxBcaColor);
+  border-radius: 10px;
+  .editMain {
+    width: 100%;
+    height: calc(100% - 60px);
+    overflow-y: auto;
+  }
+}
+
+/* 普通文字按钮的颜色 */
+.ant-btn-text {
+  color: var(--myAntdColor) !important;
+}
+
+.ant-btn-text:disabled {
+  cursor: not-allowed !important;
+  color: rgba(0, 0, 0, 0.25) !important;
+}
+
+/* 按钮的危险颜色 */
+.ant-btn-text.ant-btn-dangerous {
+  color: #ff4d4d !important;
+}
+
+// 抽屉文字居中
+.ant-dropdown-menu-title-content {
+  text-align: center;
+}
+
+// ----------表格中的视频查看
+.tablelookVideo {
+  display: flex;
+  justify-content: center;
+  position: relative;
+
+  .tablelookBox {
+    cursor: pointer;
+    transition: opacity 0.3s;
+    opacity: 0;
+    z-index: 10;
+    position: absolute;
+    top: 0;
+    left: 50%;
+    transform: translateX(-50%);
+    width: 60px;
+    height: 60px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    font-size: 18px;
+    color: #fff;
+    background-color: rgba(0, 0, 0, 0.6);
+
+    & > div {
+      font-size: 14px;
+    }
+    &:hover {
+      opacity: 1;
+    }
+  }
+  video {
+    width: 60px;
+    height: 60px;
+    object-fit: cover;
+  }
+}
+
+// -----------表格中的多选框
+.ant-table-wrapper {
+  .ant-table-selection,
+  .ant-checkbox-wrapper {
+    width: 50px !important;
+
+    .ant-checkbox-inner {
+      width: 24px !important;
+      height: 24px !important;
+    }
+    .ant-checkbox-inner:after {
+      left: 35%;
+    }
+  }
+  .ant-table-tbody .ant-checkbox-wrapper {
+    position: relative;
+    left: -2px;
+  }
+
+  .ant-table-thead .ant-checkbox-indeterminate {
+    .ant-checkbox-inner:after {
+      width: 15px;
+      height: 15px;
+      left: 50%;
+    }
+  }
+}

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

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

@@ -0,0 +1,63 @@
+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
+  srcBig?: string
+  noLook?: boolean
+  offline?: boolean
+}
+
+function ImageLazy({ width = 100, height = 100, src, srcBig, 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 + (srcBig ? srcBig : src), show: true }
+    })
+  }, [offline, src, srcBig])
+
+  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%;
+      }
+    }
+  }
+}

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

@@ -0,0 +1,46 @@
+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}`} title='model'></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;

+ 52 - 0
src/components/MyPopconfirm.tsx

@@ -0,0 +1,52 @@
+import React, { useMemo } from 'react'
+import { Button, Popconfirm } from 'antd'
+import { defaultPassWord } from '@/utils/http'
+
+type Props = {
+  txtK: '删除' | '删除2' | '取消' | '返回' | '重置密码' | '退出登录' | '重新提交' | '撤回'
+  onConfirm: () => void
+  Dom?: React.ReactNode
+  loc?: 'bottom'
+  disabled?: boolean
+}
+
+function MyPopconfirm({ txtK, onConfirm, Dom, loc, disabled }: Props) {
+  const txt = useMemo(() => {
+    const obj = {
+      删除: ['删除后无法恢复,是否删除?', '删除'],
+      删除2: ['确定删除吗?', '确定'],
+      取消: ['放弃编辑后,信息将不会保存!', '放弃'],
+      返回: ['放弃编辑后,信息将不会保存!', '放弃'],
+      重置密码: [`密码重制后为${defaultPassWord},是否重置?`, '重置'],
+      退出登录: ['确定退出吗?', '确定'],
+      重新提交: ['确定重新提交吗?', '确定'],
+      撤回: ['确定撤回吗?', '确定']
+    }
+    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 disabled={disabled} size='small' type='text' danger={!disabled}>
+          {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;
+          }
+        }
+      }
+    }
+  }
+}

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

@@ -0,0 +1,217 @@
+import React, { useCallback, useEffect, useMemo } from 'react'
+import styles from './index.module.scss'
+import { Table } from 'antd'
+import ImageLazy from '../ImageLazy'
+import dayjs from 'dayjs'
+
+type Props = {
+  yHeight?: number //设置表格的高度
+  list: any //表格数据
+  columnsTemp: any[][] //表格展示
+  total?: number //总数
+  pageNum?: number
+  pageSize?: number
+  pagingInfo?: any | boolean
+  onChange?: (pageNum: number, pageSize: number) => void
+  staBtn?: any
+  lastBtn?: any
+  classKey?: string //一个组件多次使用的时候要传递,分别设置style
+  // 表格简单的合并
+  merge?: { type: string; num: number; loc: 'rowSpan' | 'colSpan' }
+  // 定制化表头
+  myTitle?: { name: string; Com: React.ReactNode }
+  // 为空的定制字段
+  isNull?: string
+  // 设置宽度
+  widthSet?: any
+  rowKey?: string
+  // antd Table rowSelection 配置,支持跨页多选等
+  rowSelection?: any
+}
+
+// 表格内容定制化
+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 = [],
+  staBtn = [],
+  classKey = '',
+  merge,
+  myTitle,
+  isNull = '(空)',
+  widthSet,
+  rowKey,
+  rowSelection
+}: 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,
+        adiutTxt: (item: any) => {
+          let txt1 = item.status === 1 ? '同意' : item.status === 2 ? '不同意' : '待处理'
+          if (item.isUse === 0) txt1 = '(空)'
+          const txt2 = item.isAuto === 1 ? '(自动)' : ''
+          return txt1 + txt2
+        },
+        // 日期去掉00:00:00
+        dateRes: (item: any) => {
+          let res = item[v[2]] ? dayjs(item[v[2]]).format('YYYY-MM-DD') : isNull
+          return res
+        },
+        img: (item: any) => (
+          <div className='tableImgAuto'>
+            <ImageLazy
+              width={60}
+              height={60}
+              src={item[v[2]] || item.thumb || item.thumbPc}
+              srcBig={item.thumbPc || item.thumb}
+            />
+          </div>
+        ),
+
+        select: (item: any) => {
+          let txt = isNull
+          const data = v[3]
+          const id = item[v[2]]
+          const obj = data.find((v: any) => v.value === id)
+          if (obj) txt = obj.label
+          return txt
+        },
+        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]] && item[v[2]].length >= v[3]) {
+            tempCom = (
+              <span style={{ cursor: 'pointer' }} title={item[v[2]]}>
+                {tempCom}
+              </span>
+            )
+          }
+
+          return tempCom
+        },
+
+        text2: (item: any) => {
+          return (
+            <div
+              style={{ textAlign: item[v[2]] ? 'left' : 'center' }}
+              dangerouslySetInnerHTML={{ __html: item[v[2]] || isNull }}
+            ></div>
+          )
+        },
+
+        custom: (item: any) => v[2]?.(item) || isNull,
+        sizeNum: (item: any) => {
+          let txt = isNull
+          const size = item[v[2]] || ''
+          if (size) txt = (size / v[3]).toFixed(2)
+          return txt
+        },
+        connectTxt: (item: any) => {
+          const txt1 = v[2] ? item[v[2]] || isNull : isNull
+          const txt2 = v[3] ? item[v[3]] || isNull : isNull
+          return !item[v[2]] && !item[v[3]] ? isNull : `${txt1}-${txt2}`
+        }
+      }
+
+      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),
+      width: widthSet && Reflect.get(widthSet, v[2]) ? Reflect.get(widthSet, v[2]) : 'auto',
+      onCell:
+        merge && v.includes(merge.type)
+          ? // {rowSpan:3}
+            (item: any, index: number) => ({
+              rowSpan: index === 0 ? merge.num : 0
+            })
+          : ''
+    }))
+
+    return arr
+  }, [columnsTemp, dataChangeFu, merge, myTitle, widthSet])
+
+  return (
+    <Table
+      className={`${styles.MyTable} MyTable${classKey}`}
+      scroll={{ y: yHeight ? yHeight : '' }}
+      dataSource={list}
+      columns={[...staBtn, ...columns, ...lastBtn]}
+      rowKey={rowKey ? rowKey : 'id'}
+      rowSelection={rowSelection}
+      pagination={
+        pagingInfo
+          ? {
+              ...pagingInfo,
+              current: pageNum,
+              pageSize: pageSize,
+              total: total,
+              onChange: paginationChange()
+            }
+          : false
+      }
+    />
+  )
+}
+
+const MemoMyTable = React.memo(MyTable)
+
+export default MemoMyTable

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

@@ -0,0 +1,22 @@
+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>
+  )
+}

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

@@ -0,0 +1,9 @@
+.SpinLoding {
+  position: relative;
+  z-index: 9999;
+  width: 100%;
+  height: 100%;
+  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;

+ 21 - 0
src/components/SpinLodingSon/index.module.scss

@@ -0,0 +1,21 @@
+.SpinLodingSon {
+  width: 100%;
+  height: 100%;
+  // background-color: #fff;
+  border-radius: 10px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  :global {
+    .SpinLodingSon2 {
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+  }
+}

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

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

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

+ 41 - 0
src/components/YtableVideo/index.module.scss

@@ -0,0 +1,41 @@
+.YtableVideo {
+  display: flex;
+  justify-content: center;
+  :global {
+    .TvideoBox {
+      cursor: pointer;
+      width: 60px;
+      height: 60px;
+      position: relative;
+      .TvideoBoxLook {
+        position: absolute;
+        z-index: 10;
+        opacity: 0;
+        transition: opacity 0.3s;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        font-size: 14px;
+        color: #fff;
+        background-color: rgba(0, 0, 0, 0.6);
+        .anticon-eye {
+          font-size: 18px;
+        }
+      }
+      video {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+      }
+      &:hover {
+        .TvideoBoxLook {
+          opacity: 1;
+        }
+      }
+    }
+  }
+}

+ 36 - 0
src/components/YtableVideo/index.tsx

@@ -0,0 +1,36 @@
+import React from 'react'
+import styles from './index.module.scss'
+import store from '@/store'
+import { EyeOutlined } from '@ant-design/icons'
+import { baseURL } from '@/utils/http'
+
+type Props = {
+  src: string
+}
+
+function YtableVideo({ src }: Props) {
+  return (
+    <div className={styles.YtableVideo}>
+      <div className='TvideoBox'>
+        <div
+          className='TvideoBoxLook'
+          onClick={() =>
+            store.dispatch({
+              type: 'layout/lookDom',
+              payload: { src: baseURL + src, type: 'video', flag: true }
+            })
+          }
+        >
+          <EyeOutlined />
+          &nbsp;
+          <div>预览</div>
+        </div>
+        <video src={baseURL + src}></video>
+      </div>
+    </div>
+  )
+}
+
+const MemoYtableVideo = React.memo(YtableVideo)
+
+export default MemoYtableVideo

+ 195 - 0
src/components/ZRichTextOne/index.module.scss

@@ -0,0 +1,195 @@
+.ZRichTextOne {
+  width: 1000px;
+
+  :global {
+    // 正文
+    .formRightZW {
+      width: 700px;
+      top: -3px;
+      position: relative;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      height: 32px;
+
+      .formRightZWRR {
+        display: flex;
+      }
+    }
+
+    // 从查看进来
+    .formRightZWLook {
+      .ant-checkbox-wrapper {
+        pointer-events: none;
+      }
+    }
+
+    .txtBox {
+      width: 100%;
+      position: relative;
+
+      a {
+        color: #fff !important;
+      }
+
+      // 隐藏媒体功能
+      .control-item.media {
+        display: none;
+      }
+
+      button {
+        &:nth-of-type(7) {
+          display: none !important;
+        }
+        &:nth-of-type(8) {
+          display: none !important;
+        }
+        &:nth-of-type(18) {
+          display: none !important;
+        }
+        &:nth-of-type(19) {
+          display: none !important;
+        }
+      }
+
+      .bf-container {
+        height: 100%;
+      }
+
+      .bf-content {
+        height: 400px;
+        padding-bottom: 0px;
+      }
+
+      .bf-controlbar {
+        position: relative;
+
+        .upImgBox {
+          position: absolute;
+          bottom: 13px;
+          right: 15px;
+          cursor: pointer;
+          color: var(--themeColor);
+          // display: none;
+        }
+
+        .upImgBoxNo {
+          display: none;
+        }
+      }
+
+      .zztxtRow {
+        margin-bottom: 20px;
+        border: 1px solid #ccc;
+
+        .zztxtRow1 {
+          padding: 0 20px;
+          height: 40px;
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          background-color: #e8e8e8;
+
+          .zztxtRow1_1 {
+            display: flex;
+            align-items: center;
+
+            .zztxtRow1_1_1 {
+              font-weight: 700;
+              font-size: 16px;
+              margin-right: 20px;
+            }
+
+            .zztxtRow1_1_2 {
+              display: flex;
+              align-items: center;
+              width: 580px;
+              height: 32px;
+            }
+          }
+        }
+
+        .zztxtRow1_2 {
+          display: flex;
+          align-items: center;
+
+          .anticon {
+            cursor: pointer;
+            font-size: 18px;
+          }
+
+          .zztxtRow1_2Icon {
+            position: relative;
+            top: 2px;
+          }
+
+          .zztxtRow1_2IconNo {
+            pointer-events: none;
+            opacity: 0.2;
+          }
+        }
+      }
+
+      .zztxtRowErr {
+        border-color: #ff4d4f;
+      }
+    }
+
+    // 从查看进来
+    .txtBoxLook {
+      .button-remove {
+        display: none !important;
+      }
+      .bf-controlbar {
+        pointer-events: auto !important;
+        display: flex;
+        justify-content: flex-end;
+        button {
+          display: none;
+          &:last-child {
+            display: inline-block;
+          }
+        }
+        div {
+          display: none;
+        }
+        .separator-line {
+          display: none;
+        }
+      }
+    }
+
+    .noUpThumb {
+      position: relative;
+      overflow: hidden;
+      opacity: 0;
+      transition: top 0.2s;
+      color: #ff4d4f;
+      top: -20px;
+    }
+
+    .noUpThumbAc {
+      top: -10px;
+      opacity: 1;
+    }
+
+    .bf-media .bf-image {
+      float: initial !important;
+      display: block;
+      margin: 0px auto;
+      text-align: center;
+
+      // 不让拖动放大缩小图片(会报错)
+      .bf-csize-icon {
+        display: none !important;
+      }
+
+      img {
+        max-width: 500px;
+        max-height: 300px;
+      }
+    }
+
+    // .bf-video-wrap
+  }
+}

+ 173 - 0
src/components/ZRichTextOne/index.tsx

@@ -0,0 +1,173 @@
+import React, { useCallback, 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'
+import { Button } from 'antd'
+
+type Props = {
+  check: boolean //表单校验,为fasle表示不校验
+  isLook: boolean //是否是查看进来
+  ref: any //当前自己的ref,给父组件调用
+  myUrl: string //上传的api地址
+  dirCode: string
+  moduleId: number | ''
+  titTxt?: string
+}
+
+function ZRichTextOne(
+  { check, isLook, myUrl, titTxt = '请输入内容', dirCode, moduleId }: Props,
+  ref: any
+) {
+  const [section, setSection] = useState(BraftEditor.createEditorState(''))
+
+  // 判断 富文本是否为空
+  const isTxtFlag = useMemo(() => {
+    let flag = false
+
+    // 不是按章节发布,检查第一个富文本
+    const txt = section.toText()
+    const txtHtml = section.toHTML()
+    const txtRes = txt.replaceAll('\n', '').replaceAll(' ', '')
+    if (!txtRes && !txtHtml.includes('class="media-wrap')) flag = true
+
+    return flag
+  }, [section])
+
+  const myInput = useRef<HTMLInputElement>(null)
+
+  // 上传图片、视频
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (e.target.files) {
+        // 拿到files信息
+        const filesInfo = e.target.files[0]
+
+        let type = ['image/jpeg', 'image/png', 'video/mp4']
+        let size = 5
+        let txt = '图片只支持png、jpg和jpeg格式!'
+        let txt2 = '图片最大支持5M!'
+
+        // 校验格式
+        if (!type.includes(filesInfo.type)) {
+          e.target.value = ''
+          return MessageFu.warning(txt)
+        }
+
+        // 校验大小
+        if (filesInfo.size > size * 1024 * 1024) {
+          e.target.value = ''
+          return MessageFu.warning(txt2)
+        }
+
+        // 创建FormData对象
+        const fd = new FormData()
+        // 把files添加进FormData对象(‘photo’为后端需要的字段)
+        fd.append('type', 'img')
+        fd.append('dirCode', dirCode)
+        fd.append('isCompress', 'true')
+        console.log(moduleId)
+        if (moduleId) {
+          fd.append('moduleId', moduleId + '')
+        }
+
+        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(section, [
+              {
+                type: 'IMAGE',
+                url: baseURL + (res.data.thumb || res.data.thumbPc)
+              }
+            ])
+
+            setSection(newTxt)
+          }
+          fileDomInitialFu()
+        } catch (error) {
+          fileDomInitialFu()
+        }
+      }
+    },
+    [dirCode, moduleId, myUrl, section]
+  )
+
+  // 让父组件调用的 回显 富文本
+  const ritxtShowFu = useCallback((val: any) => {
+    if (val) setSection(BraftEditor.createEditorState(val))
+  }, [])
+
+  // 让父组件调用的返回 富文本信息 和 表单校验 isTxtFlag为ture表示未通过校验
+  const fatherBtnOkFu = useCallback(() => {
+    return { val: section.toHTML(), flag: isTxtFlag }
+  }, [isTxtFlag, section])
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    ritxtShowFu,
+    fatherBtnOkFu
+  }))
+
+  return (
+    <div className={styles.ZRichTextOne} id='ZRichTextOne'>
+      <input
+        id='upInput'
+        type='file'
+        accept='.png,.jpg,.jpeg,.mp4'
+        ref={myInput}
+        onChange={e => handeUpPhoto(e)}
+      />
+
+      <div className={classNames('formRightZW', isLook ? 'formRightZWLook' : '')}>
+        <div className='formRightZWRR'>
+          <div hidden={isLook}>
+            <Button
+              onClick={() => {
+                myInput.current?.click()
+              }}
+            >
+              上传图片
+            </Button>
+          </div>
+        </div>
+      </div>
+
+      <div className={classNames('txtBox', isLook ? 'txtBoxLook' : '')}>
+        <div className={classNames('zztxtRow')}>
+          {/* 主体 */}
+          <BraftEditor
+            readOnly={isLook}
+            placeholder={isLook ? '(空)' : '请输入内容'}
+            value={section}
+            onChange={e => setSection(e)}
+            imageControls={['remove']}
+          />
+        </div>
+      </div>
+      <div className={classNames('noUpThumb', check && isTxtFlag ? 'noUpThumbAc' : '')}>
+        {titTxt}
+      </div>
+    </div>
+  )
+}
+
+export default forwardRef(ZRichTextOne)

+ 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, 0.6);
+        color: #fff;
+        display: flex;
+        justify-content: space-around;
+
+        & > a {
+          color: #fff !important;
+        }
+
+        font-size: 16px;
+      }
+    }
+
+    .file_imgYuan {
+      #ImageLazy {
+        border-radius: 50%;
+        overflow: hidden;
+      }
+    }
+
+    .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 0.2s;
+      color: #ff4d4f;
+      top: -10px;
+    }
+
+    .noUpThumbAc {
+      top: 0;
+      opacity: 1;
+    }
+  }
+}

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

@@ -0,0 +1,295 @@
+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' | 'epub'
+
+// 这个组件 只处理 上传 一张图片或者 视频 音频 模型 pdf 的情况
+
+type Props = {
+  fileCheck: boolean //有没有点击过确定
+  dirCode: string //文件的code码
+  myUrl: string //请求地址
+  format: string[] //上传格式 ["image/jpeg", "image/png"] ["video/mp4"] ,application/pdf
+  formatTxt: string //上传图片提示
+  checkTxt: string
+  upTxt: string
+  myType: MyTypeType
+  isLook?: boolean //是不是查看
+  fromData?: any
+  ref: any //当前自己的ref,给父组件调用
+  isTouXiang?: boolean //圆形头像展示
+}
+
+function ZupOne(
+  {
+    fileCheck,
+    dirCode,
+    myUrl,
+    format,
+    formatTxt,
+    checkTxt,
+    upTxt,
+    myType,
+    isLook = false,
+    fromData,
+    isTouXiang
+  }: Props,
+  ref: any
+) {
+  const [fileUrl, setFileUrl] = useState({
+    fileName: '',
+    filePath: '',
+    thumb: '' //压缩图
+  })
+
+  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 (myType === 'epub') {
+          if (!filesInfo.name.endsWith('.epub')) {
+            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', 'epub'].includes(myTypeRes)) myTypeRes = 'doc'
+        fd.append('type', myTypeRes === 'thumb' ? 'img' : myTypeRes)
+        fd.append('dirCode', dirCode)
+        fd.append('file', filesInfo)
+
+        if (fromData) {
+          for (const k in fromData) {
+            if (fromData[k]) fd.append(k, fromData[k])
+          }
+        }
+
+        // 开启压缩图片
+        fd.append('isCompress', 'true')
+
+        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]
+  )
+
+  // 让父组件调用的 回显 附件 地址
+  const setFileComFileFu = useCallback(
+    (valObj: { fileName: string; filePath: string; thumb: 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'
+    else if (myType === 'epub') accept = '.epub'
+    return accept
+  }, [myType])
+
+  // 点击 预览(除了图片)
+  const lookFileNoImgFu = useCallback(
+    (type: MyTypeType) => {
+      if (type === 'pdf' || type === 'thumb') {
+        // 新窗口打开
+        window.open(baseURL + fileUrl.filePath)
+      } else if (type !== 'epub') {
+        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={classNames('file_img', isTouXiang ? 'file_imgYuan' : '')}
+          hidden={fileUrl.filePath === ''}
+        >
+          {fileUrl ? (
+            <ImageLazy
+              width={100}
+              height={100}
+              srcBig={fileUrl.filePath}
+              src={fileUrl.thumb}
+              noLook
+            />
+          ) : null}
+
+          {/* 删除 */}
+          <div className='file_closeBox' hidden={isLook}>
+            <MyPopconfirm
+              txtK='删除'
+              onConfirm={() => setFileUrl({ fileName: '', filePath: '', thumb: '' })}
+              Dom={<CloseOutlined rev={undefined} />}
+            />
+          </div>
+
+          {/* 预览 下载 */}
+          <div className='file_lookBox' hidden={isTouXiang}>
+            <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 || myType === 'epub'}
+            onClick={() => lookFileNoImgFu(myType)}
+          >
+            <EyeOutlined rev={undefined} />
+          </div>
+          {/* 视频下载 */}
+          <a
+            href={baseURL + fileUrl.filePath}
+            download
+            target='_blank'
+            className='clearCover'
+            rel='noreferrer'
+          >
+            <DownloadOutlined rev={undefined} />
+          </a>
+          {/* 视频删除 */}
+
+          {isLook ? null : (
+            <MyPopconfirm
+              txtK='删除'
+              onConfirm={() => setFileUrl({ fileName: '', filePath: '', thumb: '' })}
+              Dom={<CloseOutlined className='clearCover' rev={undefined} />}
+            />
+          )}
+        </div>
+      ) : null}
+
+      <div className='fileBoxRow_r_tit' hidden={isLook}>
+        格式要求:支持{formatTxt}格式。{upTxt}
+        <br />
+        <div
+          className={classNames('noUpThumb', !fileUrl.filePath && fileCheck ? 'noUpThumbAc' : '')}
+        >
+          {checkTxt}
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default forwardRef(ZupOne)

+ 36 - 0
src/index.tsx

@@ -0,0 +1,36 @@
+// import 'default-passive-events';
+import '@/assets/styles/base.css'
+
+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: '#ab925b'
+      }
+    }}
+  >
+    <Provider store={store}>
+      <StyleProvider hashPriority='high' transformers={[legacyLogicalPropertiesTransformer]}>
+        <App />
+      </StyleProvider>
+    </Provider>
+  </ConfigProvider>
+)

+ 4 - 0
src/pages/Aresource/A1overview/index.module.scss

@@ -0,0 +1,4 @@
+.A1overview {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Aresource/A1overview/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function A1overview() {
+  return (
+    <div className={styles.A1overview}>
+      <h1>A1overview</h1>
+    </div>
+  )
+}
+
+const MemoA1overview = React.memo(A1overview)
+
+export default MemoA1overview

+ 4 - 0
src/pages/Aresource/A2resFile/index.module.scss

@@ -0,0 +1,4 @@
+.A2resFile {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Aresource/A2resFile/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function A2resFile() {
+  return (
+    <div className={styles.A2resFile}>
+      <h1>A2resFile</h1>
+    </div>
+  )
+}
+
+const MemoA2resFile = React.memo(A2resFile)
+
+export default MemoA2resFile

+ 4 - 0
src/pages/Aresource/A3resTag/index.module.scss

@@ -0,0 +1,4 @@
+.A3resTag {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Aresource/A3resTag/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function A3resTag() {
+  return (
+    <div className={styles.A3resTag}>
+      <h1>A3resTag</h1>
+    </div>
+  )
+}
+
+const MemoA3resTag = React.memo(A3resTag)
+
+export default MemoA3resTag

+ 12 - 0
src/pages/Layout/PassEdit/index.module.scss

@@ -0,0 +1,12 @@
+.PassEdit {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+
+    .ant-modal-body {
+      border-top: 1px solid #ccc;
+      padding-top: 20px !important;
+    }
+  }
+}

+ 140 - 0
src/pages/Layout/PassEdit/index.tsx

@@ -0,0 +1,140 @@
+import React, { useCallback, useEffect, useMemo, useRef } from 'react'
+import styles from './index.module.scss'
+import { Button, Form, FormInstance, Input, Modal } from 'antd'
+import { MessageFu } from '@/utils/message'
+import { Base64 } from 'js-base64'
+import encodeStr from '@/utils/pass'
+import { passWordEditAPI, passWordEditAPI2 } from '@/store/action/layout'
+import store from '@/store'
+import { loginExit } from '../data'
+import { setTokenInfo } from '@/utils/storage'
+import history from '@/utils/history'
+import { defaultPassWord } from '@/utils/http'
+
+type Props = {
+  userTokenInfo?: any
+}
+
+function PassEdit({ userTokenInfo }: Props) {
+  // 设置表单ref
+  const FormBoxRef = useRef<FormInstance>(null)
+
+  // 是否是从登录页面进来的,需求强制修改默认密码
+  const flag = useMemo(() => {
+    let flag = false
+    if (userTokenInfo && userTokenInfo.token) flag = true
+    return flag
+  }, [userTokenInfo])
+
+  useEffect(() => {
+    if (flag) {
+      FormBoxRef.current?.setFieldsValue({
+        oldPassword: defaultPassWord
+      })
+    }
+  }, [flag])
+
+  // 拿到新密码的输入框的值
+  const oldPasswordValue = useRef('')
+
+  const checkPassWord = (rule: any, value: any = '') => {
+    if (value !== oldPasswordValue.current) return Promise.reject('新密码不一致!')
+    else return Promise.resolve(value)
+  }
+
+  const onFinish = useCallback(
+    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 token = flag ? userTokenInfo.token : undefined
+      const res: any = flag ? await passWordEditAPI(obj, token) : await passWordEditAPI2(obj)
+      if (res.code === 0) {
+        store.dispatch({ type: 'layout/passEditShow', payload: false })
+        if (flag) {
+          MessageFu.success('登录成功')
+          // 用户信息存到本地
+          setTokenInfo(userTokenInfo)
+          history.replace('/')
+        } else {
+          MessageFu.success('修改成功')
+          loginExit()
+        }
+      }
+    },
+    [flag, userTokenInfo]
+  )
+
+  return (
+    <Modal
+      wrapClassName={styles.PassEdit}
+      destroyOnClose
+      open={true}
+      title={flag ? '为保障账号安全,请修改默认密码' : '修改密码'}
+      // onCancel={() => store.dispatch({ type: 'layout/passEditShow', payload: false })}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <Form
+        ref={FormBoxRef}
+        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} disabled={flag} />
+        </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: 5, span: 16 }}>
+          <Button onClick={() => store.dispatch({ type: 'layout/passEditShow', payload: false })}>
+            {flag ? '取消登录' : '取消'}
+          </Button>
+          &emsp;
+          <Button type='primary' htmlType='submit'>
+            {flag ? '确定修改并进入系统' : '确定'}
+          </Button>
+        </Form.Item>
+      </Form>
+    </Modal>
+  )
+}
+
+const MemoPassEdit = React.memo(PassEdit)
+
+export default MemoPassEdit

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

@@ -0,0 +1,138 @@
+import history from '@/utils/history'
+import { removeTokenInfo } from '@/utils/storage'
+import React from 'react'
+
+// 点击退出登录
+export const loginExit = () => {
+  removeTokenInfo()
+  history.replace('/login')
+}
+
+export type LookDomType = {
+  src: string
+  type: 'video' | 'audio' | 'model' | ''
+  flag?: boolean
+}
+
+export type RouterTypeRow = {
+  id: number
+  name: string
+  path: string
+  pathLast?: string
+  Com: React.LazyExoticComponent<React.MemoExoticComponent<() => JSX.Element>>
+  pageType?: string
+  authority?: boolean
+}
+
+export type RouterType = {
+  id: number
+  name: string
+  show: boolean
+  son: RouterTypeRow[]
+}[]
+
+const tabLeftArr: RouterType = [
+  {
+    id: 1,
+    name: '资源管理',
+    show: false,
+    son: [
+      {
+        id: 110,
+        name: '资源总览',
+        path: '/',
+        Com: React.lazy(() => import('../Aresource/A1overview'))
+      },
+      {
+        id: 120,
+        name: '资源附件',
+        path: '/resFile',
+        Com: React.lazy(() => import('../Aresource/A2resFile'))
+      },
+      {
+        id: 130,
+        name: '标签管理',
+        path: '/resTag',
+        Com: React.lazy(() => import('../Aresource/A3resTag'))
+      }
+    ]
+  },
+
+  {
+    id: 9,
+    name: '系统管理',
+    show: false,
+    son: [
+      {
+        id: 709,
+        name: '资源库管理',
+        path: '/sysSet',
+        Com: React.lazy(() => import('../Zsystem/Z1sysSet'))
+      },
+      {
+        id: 710,
+        name: '数据字典',
+        path: '/dict',
+        Com: React.lazy(() => import('../Zsystem/Z2dict'))
+      },
+      {
+        id: 720,
+        name: '编号设置',
+        path: '/ipSet',
+        Com: React.lazy(() => import('../Zsystem/Z3ipSet'))
+      },
+      {
+        id: 730,
+        name: '编号设置',
+        path: '/numSet',
+        Com: React.lazy(() => import('../Zsystem/Z4numSet'))
+      },
+      {
+        id: 740,
+        name: '流程设置',
+        path: '/flowSet',
+        Com: React.lazy(() => import('../Zsystem/Z5flowSet'))
+      },
+      {
+        id: 750,
+        name: '角色管理',
+        path: '/roleSet',
+        Com: React.lazy(() => import('../Zsystem/Z6roleSet'))
+      },
+      {
+        id: 760,
+        name: '用户管理',
+        path: '/userSet',
+        Com: React.lazy(() => import('../Zsystem/Z7userSet'))
+      },
+
+      {
+        id: 770,
+        name: '系统日志',
+        path: '/sysLog',
+        Com: React.lazy(() => import('../Zsystem/Z8sysLog'))
+      }
+      // 不需要 高亮的 详情页
+      // {
+      //   id: 9901,
+      //   name: '藏品详情',
+      //   path: '/goodsLook/:id',
+      //   pathLast: '/goodsLook',
+      //   Com: React.lazy(() => import('../ZgoodsInfo'))
+      // }
+    ]
+  }
+]
+
+export default tabLeftArr
+
+// 里面的页面,不是左边的tab栏
+// 这里的id要和tab栏的id相同,不然没有权限进入页面
+export const routerSon: RouterTypeRow[] = [
+  // {
+  //   id: 210,
+  //   name: '藏品征集-详情页',
+  //   path: '/collect_edit/:key/:id',
+  //   Com: React.lazy(() => import('../Benter/B1collect/B1edit'))
+  // },
+]

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

@@ -0,0 +1,226 @@
+.Layout {
+  width: 100%;
+  height: 100%;
+  display: flex;
+
+  :global {
+    .layoutLeft {
+      position: relative;
+      width: 220px;
+      height: 100%;
+
+      .layoutLeftTop {
+        height: 70px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        background-color: #243220;
+
+        .logo {
+          width: 150px;
+          position: relative;
+          top: -5px;
+        }
+      }
+
+      .layoutLeftMain {
+        height: calc(100% - 70px);
+        overflow-y: auto;
+        padding-bottom: 10px;
+        padding: 30px 0 0;
+        background-image: url('../../assets/img/latLeftBg.jpg');
+        background-size: 100% 100%;
+
+        &::-webkit-scrollbar-thumb {
+          background: var(--myAntdColor);
+        }
+
+        .layoutLRowBox {
+          margin-bottom: 20px;
+          &:last-child {
+            margin-bottom: 50px;
+          }
+          .layoutLRowBoxTxt {
+            padding-left: 60px;
+            cursor: pointer;
+            margin: 0px auto;
+            width: 200px;
+            font-size: 16px;
+            height: 48px;
+            line-height: 48px;
+            color: #fff;
+            background-image: url('../../assets/img/UnionNo.png');
+            background-size: 100% 100%;
+            position: relative;
+            font-weight: 700;
+            // opacity: .8;
+            & > span {
+              position: absolute;
+              right: 24px;
+              display: inline-block;
+              margin-left: 20px;
+              transition: all 0.3s;
+            }
+            .layoutLRowBoxTxtIcon {
+              transform: rotate(180deg);
+            }
+          }
+          .layoutLRowBoxTxtShow {
+            background-image: url('../../assets/img/Union.png');
+          }
+          .layoutLRowBoxTxtShowAc {
+            color: var(--myAntdColor);
+          }
+
+          .layoutLRowBoxRow {
+            padding-left: 60px;
+            width: 200px;
+            margin: 3px auto;
+            opacity: 0.8;
+            color: #fff;
+            cursor: pointer;
+            transition: all 0.3s;
+            font-size: 16px;
+            height: 40px;
+            line-height: 40px;
+            background-color: #24664b;
+            overflow: hidden;
+            position: relative;
+
+            &:hover {
+              opacity: 1;
+              background-color: var(--myAntdColor);
+            }
+            & > span {
+              position: absolute;
+              right: 24px;
+              display: inline-block;
+              transition: all 0.3s;
+              font-size: 14px;
+            }
+          }
+          .layoutLRowBoxRowHide {
+            height: 0;
+            margin: 0 auto;
+          }
+
+          .active {
+            opacity: 1;
+            background-color: var(--myAntdColor);
+          }
+        }
+      }
+    }
+
+    .layoutRight {
+      width: calc(100% - 220px);
+      height: 100%;
+      overflow: hidden;
+
+      .layoutRightTop {
+        height: 70px;
+        position: relative;
+        background-color: #243220;
+
+        .user {
+          position: absolute;
+          right: -150px;
+          top: 0;
+          height: 100%;
+          display: flex;
+          align-items: center;
+          font-size: 16px;
+          color: black;
+          transition: all 0.3s;
+
+          .userNameBox {
+            cursor: pointer;
+            // background: url('../../assets/img/user.png') no-repeat left center;
+            // background-size: 40px 40px;
+            // padding-left: 46px;
+            height: 60px;
+            display: flex;
+            align-items: center;
+            color: #fff;
+            img {
+              border-radius: 50%;
+              width: 40px;
+              height: 40px;
+              margin-right: 20px;
+            }
+          }
+
+          .userInco {
+            margin-left: 10px;
+            color: #fff;
+          }
+
+          .userInco1 {
+            // display: none;
+            opacity: 0 !important;
+          }
+
+          .userSet {
+            position: relative;
+            top: 10px;
+            margin-left: 40px;
+            width: 140px;
+            opacity: 1;
+            height: 74px;
+
+            & > div {
+              box-shadow: 1px 1px 4px 4px #ccc;
+              border-radius: 10px;
+              overflow: hidden;
+
+              & > span {
+                cursor: pointer;
+                background-color: #fff;
+                display: block;
+                width: 100%;
+                text-align: center;
+                height: 35px;
+                line-height: 35px;
+
+                &:hover {
+                  color: var(--themeColor);
+                }
+              }
+            }
+          }
+        }
+
+        .userShow {
+          right: 20px;
+        }
+      }
+
+      .layoutRightMain {
+        padding: 24px 24px 20px;
+        height: calc(100% - 70px);
+        background-color: #ebeded;
+
+        .mainBoxR {
+          width: 100%;
+          height: 100%;
+          // overflow: hidden;
+          position: relative;
+
+          & > div {
+            width: 100%;
+            height: 100%;
+            position: relative;
+            // padding: 15px;
+            border: 1px solid #d1d3d3;
+            background-color: var(--boxBcaColor);
+            border-radius: 10px;
+
+            // background-color: #fff;
+            // border-radius: 10px;
+            // padding: 20px;
+          }
+        }
+      }
+    }
+  }
+}

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

@@ -0,0 +1,316 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import { CaretRightOutlined } from '@ant-design/icons'
+import styles from './index.module.scss'
+import { Route, Switch, useLocation } from 'react-router-dom'
+import AuthRoute from '@/components/AuthRoute'
+import history from '@/utils/history'
+import { getTokenInfo } from '@/utils/storage'
+import NotFound from '@/components/NotFound'
+import classNames from 'classnames'
+import tabLeftArr, { loginExit, routerSon, RouterType, RouterTypeRow } from './data'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import store, { RootState } from '@/store'
+import SpinLodingSon from '@/components/SpinLodingSon'
+import { DownOutlined, RightOutlined } from '@ant-design/icons'
+import { useSelector } from 'react-redux'
+import { baseURL } from '@/utils/http'
+import baseTouXiangImg from '@/assets/img/user.png'
+import PassEdit from './PassEdit'
+
+function Layout() {
+  const [loding, setLoding] = useState(false)
+
+  // 获取字典值  // 获取标签
+  // const dispatch = useDispatch()
+
+  const getListFu = useCallback(() => {
+    //  待完善
+    setLoding(true)
+  }, [])
+
+  useEffect(() => {
+    // 用户信息存到仓库
+    store.dispatch({ type: 'layout/userInfo', payload: (getTokenInfo() || {}).user })
+    getListFu()
+  }, [getListFu])
+  // 获取用户信息
+  const { userInfo, passEditShow } = useSelector((state: RootState) => state.A0Layout)
+
+  // 当前路径选中的左侧菜单
+  const location = useLocation()
+  const sroolRef = useRef<HTMLDivElement>(null)
+  const sroolTimeRef = useRef(-1)
+
+  const [acPageName, setAcPageName] = useState(['', ''])
+
+  useEffect(() => {
+    if (loding) {
+      const arr = location.pathname.split('/')
+      let pathTemp = '/'
+      if (arr[1]) pathTemp = '/' + arr[1]
+
+      // 滚动到中间
+      clearTimeout(sroolTimeRef.current)
+      if (pathTemp !== '/goodsLook') {
+        sroolTimeRef.current = window.setTimeout(() => {
+          if (sroolRef.current) {
+            const dom = document.querySelector('.layoutLeftMain') as HTMLDivElement
+            if (dom) {
+              dom.scrollTo({
+                top: sroolRef.current.offsetTop - 350,
+                behavior: 'smooth'
+              })
+            }
+          }
+        }, 200)
+      }
+      const pathRes = pathTemp.split('_')[0]
+
+      tabLeftArr.forEach(v1 => {
+        v1.son.forEach(v2 => {
+          if (v2.path === pathRes) setAcPageName([v1.name, v2.name])
+          else if (pathRes === '/goodsLook') setAcPageName(['', ''])
+        })
+      })
+    }
+  }, [location, loding])
+
+  const [routerSonRes, setRouterSonRes] = useState<RouterTypeRow[]>([])
+
+  // 获取用户权限信息
+  const getUserAuthFu = useCallback(async () => {
+    const userInfo = getTokenInfo().user
+
+    // 待完善
+    let isOkIdArr: number[] = [9901, 709, 710, 720, 730, 740, 750, 760, 770]
+
+    const roleArr: any[] = []
+
+    roleArr.forEach(v1 => {
+      if (v1.children) {
+        v1.children.forEach((v2: any) => {
+          if (v2.authority) isOkIdArr.push(v2.id)
+          if (v2.children) {
+            v2.children.forEach((v3: any) => {
+              if (v3.authority) isOkIdArr.push(v3.id)
+            })
+          }
+        })
+      }
+    })
+
+    // 详情页,也需要看下有没有页面权限
+    setRouterSonRes(routerSon.filter(v => isOkIdArr.includes(v.id)))
+
+    // 是管理员
+    if (userInfo.isAdmin === 1) {
+      ;[750].forEach(v => {
+        isOkIdArr.push(v)
+      })
+    } else if (isOkIdArr.includes(750)) isOkIdArr = isOkIdArr.filter(v => v !== 750)
+
+    // 页面权限id,存到仓库
+    store.dispatch({ type: 'layout/authorityIds', payload: isOkIdArr })
+
+    const tempArr: RouterTypeRow[] = []
+
+    // 权限数据存到仓库
+    const roleArrStoreArr: RouterType = []
+
+    tabLeftArr.forEach(v1 => {
+      if (v1.son && v1.son[0]) {
+        const obj = {
+          ...v1,
+          son: [] as RouterTypeRow[]
+        }
+
+        v1.son.forEach(v2 => {
+          if (isOkIdArr.includes(v2.id)) {
+            tempArr.push(v2)
+            // 过滤掉 藏品详情 页
+            if (v2.id < 9901) obj.son.push({ ...v2, authority: true })
+          }
+        })
+        roleArrStoreArr.push(obj)
+      }
+    })
+
+    // 权限数据存到仓库
+    store.dispatch({ type: 'layout/userRolePermissions', payload: roleArrStoreArr })
+
+    setRouterCom(tempArr)
+
+    // 如果当前页面没有权限了,跳转有权限的第一个页面
+    const urlAll = window.location.hash
+    const isNowPath = urlAll.replace('#', '')
+    const pathArr = tempArr.map(v => v.pathLast || v.path)
+
+    const routerSonArr = routerSon.map(v => v.path)
+
+    const lastArr = [...pathArr, ...routerSonArr]
+
+    let flagPush = true
+
+    lastArr.forEach(v => {
+      if (v === '/') {
+        if (isNowPath === '/') flagPush = false
+      } else {
+        if (isNowPath.includes(v)) flagPush = false
+      }
+    })
+
+    if (flagPush) history.push(pathArr[0])
+
+    const pathRes = isNowPath.split('_')[0]
+
+    const resList = tabLeftArr.map(v => ({
+      ...v,
+      show: v.son.some(v => v.path === (flagPush ? pathArr[0] : pathRes)),
+      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((item: RouterTypeRow) => {
+    history.push(item.path)
+  }, [])
+
+  // 点击用户 出来 退出登录 修改密码
+  const [isUserBtnShow, setIsUserBtnShow] = useState(false)
+
+  return (
+    <div className={styles.Layout}>
+      {/* 左边 */}
+      <div className='layoutLeft'>
+        <div className='layoutLeftTop'>
+          <img className='logo' src={require('@/assets/img/logo.png')} alt='' />
+        </div>
+        {/* 左边主体 */}
+        <div className='layoutLeftMain mySorrl'>
+          {list.map(v => (
+            <div
+              className={classNames('layoutLRowBox')}
+              key={v.id}
+              hidden={!v.son.length || (v.son.length === 1 && v.son[0].name === '藏品详情')}
+            >
+              <div
+                className={classNames(
+                  'layoutLRowBoxTxt',
+                  v.show ? 'layoutLRowBoxTxtShow' : '',
+                  acPageName[0] === v.name ? 'layoutLRowBoxTxtShowAc' : ''
+                )}
+                onClick={() => {
+                  setList(
+                    list.map(c => ({
+                      ...c,
+                      show: c.id === v.id ? !c.show : c.show
+                    }))
+                  )
+                }}
+              >
+                {v.name}
+                <span className={classNames(v.show ? 'layoutLRowBoxTxtIcon' : '')}>
+                  <DownOutlined />
+                </span>
+              </div>
+              {v.son.map(v2 => (
+                <div
+                  key={v2.id}
+                  hidden={v2.id >= 9901}
+                  className={classNames(
+                    'layoutLRowBoxRow',
+                    acPageName[1] === v2.name ? 'active' : '',
+                    v.show ? '' : 'layoutLRowBoxRowHide'
+                  )}
+                  ref={acPageName[1] === v2.name ? sroolRef : null}
+                  onClick={() => pathCutFu(v2)}
+                >
+                  {v2.name}
+                  <span>
+                    <RightOutlined />
+                  </span>
+                </div>
+              ))}
+            </div>
+          ))}
+        </div>
+      </div>
+      {/* 右边 */}
+      <div className='layoutRight'>
+        <div className='layoutRightTop'>
+          {/* 用户相关 */}
+          <div
+            className={classNames('user', isUserBtnShow ? 'userShow' : '')}
+            onMouseLeave={() => setIsUserBtnShow(false)}
+          >
+            <div className='userNameBox' onClick={() => setIsUserBtnShow(true)}>
+              <img src={userInfo.thumb ? baseURL + userInfo.thumb : baseTouXiangImg} alt='' />
+
+              {userInfo.realName || userInfo.userName || '匿名'}
+
+              <div className='userInco userInco2'>
+                <CaretRightOutlined />
+              </div>
+            </div>
+
+            <div className='userSet'>
+              <div>
+                <span
+                  onClick={() => store.dispatch({ type: 'layout/passEditShow', payload: true })}
+                >
+                  修改密码
+                </span>
+                <MyPopconfirm txtK='退出登录' onConfirm={loginExit} Dom='退出登录' loc='bottom' />
+              </div>
+            </div>
+          </div>
+        </div>
+        {/* 右边主体 */}
+        <div className='layoutRightMain'>
+          {/* 二级路由页面 */}
+          {loding ? (
+            <div className='mainBoxR'>
+              <React.Suspense fallback={<SpinLodingSon />}>
+                <Switch>
+                  {RouterCom.map(v => (
+                    <AuthRoute key={v.id} exact path={v.path} component={v.Com} />
+                  ))}
+
+                  {/* 非tab栏页面 */}
+                  {routerSonRes.map(v => (
+                    <AuthRoute key={v.id} exact path={v.path} component={v.Com} />
+                  ))}
+
+                  <Route path='*' component={NotFound} />
+                </Switch>
+              </React.Suspense>
+            </div>
+          ) : null}
+        </div>
+      </div>
+
+      {/* 点击修改密码打开的对话框 */}
+      {passEditShow ? <PassEdit /> : null}
+    </div>
+  )
+}
+
+// 使用 React.memo 来优化组件,避免组件的无效更新,类似 类组件里面的PureComponent
+const MemoLayout = React.memo(Layout)
+export default MemoLayout

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

@@ -0,0 +1,165 @@
+.Login {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  padding: 8%;
+  background-image: url('../../assets/img/bg.jpg');
+  background-size: 100% 100%;
+  position: relative;
+
+  :global {
+    .LogoImg {
+      position: absolute;
+      top: 60px;
+      left: 50px;
+    }
+
+    .mainRight {
+      padding-bottom: 45px;
+      position: absolute;
+      right: 6%;
+      top: 28%;
+      border-radius: 6px;
+      width: 400px;
+      display: flex;
+      flex-direction: column;
+      justify-content: center;
+      align-items: center;
+      color: #fff;
+      background-color: rgba(255, 255, 255, 0.8);
+      backdrop-filter: blur(6px);
+      border-radius: 10px;
+      overflow: hidden;
+
+      & > h3 {
+        width: 100%;
+        height: 75px;
+        text-align: center;
+        line-height: 75px;
+        font-size: 22px;
+        letter-spacing: 2px;
+        background-color: #24664b;
+      }
+
+      .inputBox {
+        width: 100%;
+
+        input::-webkit-input-placeholder {
+          /* WebKit browsers */
+          color: #999;
+        }
+
+        input:-moz-placeholder {
+          /* Mozilla Firefox 4 to 18 */
+          color: #999;
+        }
+
+        input::-moz-placeholder {
+          /* Mozilla Firefox 19+ */
+          color: #999;
+        }
+
+        input:-ms-input-placeholder {
+          /* Internet Explorer 10+ */
+          color: #999;
+        }
+
+        .inputBoxRow {
+          width: 300px;
+          margin: 30px auto;
+          position: relative;
+          .logIcon {
+            position: absolute;
+            top: 50%;
+            left: 3px;
+            transform: translateY(-50%);
+          }
+        }
+
+        .inputBoxRow2 {
+          position: relative;
+
+          .loginCode {
+            z-index: 110;
+            cursor: pointer;
+            position: absolute;
+            top: 50%;
+            right: 10px;
+            transform: translateY(-50%);
+          }
+
+          .ant-input {
+            background-color: transparent !important;
+            padding-left: 40px !important;
+          }
+        }
+
+        .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: #24664b !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,
+        .ant-input-affix-wrapper {
+          background-color: transparent;
+          padding: 0 20px;
+          width: 100%;
+          height: 60px;
+          border: none;
+          border-bottom: 1px solid #24664b;
+          border-radius: 0px;
+          color: #24664b;
+
+          .ant-input {
+            background-color: transparent;
+
+            width: 100%;
+            height: 60px;
+            color: #24664b;
+          }
+        }
+
+        .ant-input,
+        .ant-input-affix-wrapper-focused {
+          box-shadow: none;
+        }
+
+        .loginBtn {
+          cursor: pointer;
+          font-size: 20px;
+          width: 300px;
+          height: 48px;
+          margin: 0 auto;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          background-color: #24664b;
+          color: #fff;
+          transition: 'opacity' 0.3;
+          & > img {
+            display: block;
+            position: relative;
+            top: 1px;
+            margin-right: 10px;
+          }
+          &:hover {
+            opacity: 0.8;
+          }
+        }
+      }
+    }
+  }
+}

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

@@ -0,0 +1,135 @@
+import styles from './index.module.scss'
+
+import { Input } from 'antd'
+import { useCallback, useEffect, useRef, 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 store, { RootState } from '@/store'
+import PassEdit from '../Layout/PassEdit'
+import { useSelector } from 'react-redux'
+import { defaultPassWord } from '@/utils/http'
+
+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>('')
+
+  // 键盘按下回车事件
+  const keyUpEntFu = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    if (e.key === 'Enter') loginClickFu()
+  }
+
+  const userTokenInfoRef = useRef<any>({})
+
+  // 点击登录
+  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) {
+      userTokenInfoRef.current = { ...res.data, token: res.data.satoken }
+
+      // 检查密码是不是默认密码,是的话给提示
+      if (passWord === defaultPassWord) {
+        MessageFu.warning('为保障账号安全,请修改默认密码')
+        setCode('')
+        LoginGetCodeFu()
+        store.dispatch({ type: 'layout/passEditShow', payload: true })
+      } else {
+        MessageFu.success('登录成功')
+        // 用户信息存到本地
+        setTokenInfo(userTokenInfoRef.current)
+        const urlAll = window.location.href
+        if (urlAll.includes('?back=')) {
+          const url = urlAll.split('?back=')[1]
+          if (url) history.replace(`/${url}`)
+          else history.replace('/')
+        } else history.replace('/')
+      }
+    } else if (res.code === 3014) {
+      MessageFu.warning('账号不存在或密码错误,请联系管理员!')
+    } else if (res.code === -1 && res.msg === '验证码有误') {
+      LoginGetCodeFu()
+    }
+  }, [LoginGetCodeFu, code, passWord, userName])
+
+  const { passEditShow } = useSelector((state: RootState) => state.A0Layout)
+
+  return (
+    <div className={styles.Login}>
+      <div className='mainRight'>
+        <h3>青岛啤酒博物馆藏品管理系统</h3>
+
+        {/* 账号密码输入框 */}
+        <div className='inputBox'>
+          <div className='inputBoxRow inputBoxRow2'>
+            <img className='logIcon' src={require('@/assets/img/loginInco1.png')} alt='' />
+            <Input
+              onKeyUp={e => keyUpEntFu(e)}
+              value={userName}
+              onChange={e => setUserName(e.target.value.trim())}
+              placeholder='请输入账号'
+              maxLength={15}
+            />
+          </div>
+          <div className='inputBoxRow'>
+            <img className='logIcon' src={require('@/assets/img/loginInco2.png')} alt='' />
+
+            <Input.Password
+              onKeyUp={e => keyUpEntFu(e)}
+              value={passWord}
+              onChange={e => setPassWord(e.target.value.trim())}
+              placeholder='请输入密码'
+              maxLength={20}
+            />
+          </div>
+          <div className='inputBoxRow inputBoxRow2'>
+            <img className='logIcon' src={require('@/assets/img/loginInco3.png')} alt='' />
+
+            <Input
+              onKeyUp={e => keyUpEntFu(e)}
+              value={code}
+              onChange={e => setCode(e.target.value.trim())}
+              placeholder='请输入验证码'
+              maxLength={4}
+            />
+            {codeImg ? (
+              <img onClick={LoginGetCodeFu} className='loginCode' src={codeImg} alt='' />
+            ) : null}
+          </div>
+          {/* 登录按钮 */}
+          <div className='loginBtn' onClick={loginClickFu}>
+            <img src={require('@/assets/img/loginBtn.png')} alt='' />登 录
+          </div>
+        </div>
+      </div>
+      {passEditShow ? <PassEdit userTokenInfo={userTokenInfoRef.current} /> : null}
+    </div>
+  )
+}

+ 4 - 0
src/pages/Zsystem/Z1sysSet/index.module.scss

@@ -0,0 +1,4 @@
+.Z1sysSet {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Zsystem/Z1sysSet/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function Z1sysSet() {
+  return (
+    <div className={styles.Z1sysSet}>
+      <h1>Z1sysSet</h1>
+    </div>
+  )
+}
+
+const MemoZ1sysSet = React.memo(Z1sysSet)
+
+export default MemoZ1sysSet

+ 226 - 0
src/pages/Zsystem/Z2dict/Z2add.tsx

@@ -0,0 +1,226 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { Button, Cascader, Form, FormInstance, Input, InputNumber, Modal } from 'antd'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import { MessageFu } from '@/utils/message'
+import TextArea from 'antd/es/input/TextArea'
+import classNames from 'classnames'
+import { Z2AddInfoType, Z2dataType } from './data'
+import { Z2_APIgetInfo, Z2_APIsave } from '@/store/action/Z2dict'
+
+type Props = {
+  addInfo: Z2AddInfoType
+  addFu: () => void
+  closeFu: () => void
+}
+
+function Z2add({ addInfo, addFu, closeFu }: Props) {
+  const treeData = useSelector((state: RootState) => state.Z2dict.dictAll)
+
+  // 级联选择器改变的时候 筛选当前级联的 信息出来
+  const [acCardInfo, setAcCardInfo] = useState({} as Z2dataType)
+  const [parentIdArr, setParentIdArr] = useState<string[] | null>(null)
+
+  useEffect(() => {
+    setAcCardInfo(addInfo.acInfo)
+
+    let ids: string[] | null = addInfo.acInfo.id ? [addInfo.acInfo.id] : null
+
+    if (addInfo.acInfo.ancestor) ids = [...addInfo.acInfo.ancestor.split(',')]
+
+    let idsRes = ids
+    if (ids && ids.length >= 1 && addInfo.txt === '编辑') {
+      ids.pop()
+    }
+
+    if (idsRes) idsRes = idsRes.filter((v, i) => v !== '0')
+
+    setParentIdArr(idsRes)
+  }, [addInfo.acInfo, addInfo.txt])
+
+  const cardChange = useCallback((aa: any, bb: any) => {
+    setParentIdArr(aa)
+
+    if (bb && bb.length) setAcCardInfo(bb[bb.length - 1])
+    else setAcCardInfo({} as Z2dataType)
+  }, [])
+
+  const getInfoFu = useCallback(async (id: string) => {
+    const res = await Z2_APIgetInfo(id)
+    if (res.code === 0) {
+      FormBoxRef.current?.setFieldsValue({
+        ...res.data
+      })
+    }
+  }, [])
+  useEffect(() => {
+    if (addInfo.txt === '编辑') getInfoFu(addInfo.id)
+    else {
+      FormBoxRef.current?.setFieldsValue({
+        sort: 999
+      })
+    }
+  }, [addInfo.id, addInfo.txt, getInfoFu])
+
+  // 设置表单初始数据(区分编辑和新增)
+  const FormBoxRef = useRef<FormInstance>(null)
+
+  // 没有通过校验
+  const onFinishFailed = useCallback(() => {
+    // return MessageFu.warning("有表单不符号规则!");
+  }, [])
+
+  // 一些模块只能增加到二级字典
+  const isTowFlag = useMemo(() => {
+    let flag = false
+    if (parentIdArr && parentIdArr.length === 2 && 1 + 1 !== 2) {
+      flag = true
+    }
+    return flag
+  }, [parentIdArr])
+
+  // 通过校验点击确定
+  const onFinish = useCallback(
+    async (values: any) => {
+      if (!parentIdArr || parentIdArr.length === 0) return MessageFu.warning('请选择上级名称')
+
+      let ancestor = ''
+      let parentId: string | null = null
+
+      if (isTowFlag) MessageFu.warning('该字典模块最多支持二级!')
+
+      // if (1 + 1 === 2) {
+      //   console.log('-----', parentIdArr)
+
+      //   return
+      // }
+
+      if (parentIdArr && parentIdArr.length >= 5 && addInfo.txt === '新增')
+        return MessageFu.warning('最多支持五级!')
+
+      if (acCardInfo) parentId = addInfo.txt === '编辑' ? acCardInfo.parentId : acCardInfo.id
+      if (parentIdArr && parentId) ancestor = parentIdArr.filter(v => v !== addInfo.id).join(',')
+
+      let level = 1
+      if (parentIdArr) {
+        level = addInfo.txt === '新增' ? acCardInfo.level + 1 : acCardInfo.level
+      }
+
+      const obj = {
+        ...values,
+        id: addInfo.id === '-1' ? null : addInfo.id,
+        ancestor,
+        level,
+        parentId
+      }
+      // console.log(123, obj)
+      // if (1 + 1 === 2) {
+      //   return
+      // }
+
+      const res = await Z2_APIsave(obj)
+
+      if (res.code === 0) {
+        MessageFu.success(addInfo.txt + '成功!')
+        addFu()
+        closeFu()
+      }
+    },
+    [acCardInfo, addFu, addInfo.id, addInfo.txt, closeFu, isTowFlag, parentIdArr]
+  )
+
+  return (
+    <Modal
+      wrapClassName={styles.Z2add}
+      open={true}
+      title={addInfo.txt}
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className='Z2aMain'>
+        <Form
+          scrollToFirstError={true}
+          ref={FormBoxRef}
+          name='basic'
+          labelCol={{ span: 3 }}
+          onFinish={onFinish}
+          onFinishFailed={onFinishFailed}
+          autoComplete='off'
+        >
+          <div className='fromRow'>
+            <div className='fromRowll'>
+              <span>* </span> 上级名称:
+            </div>
+            <div className='fromRowrr'>
+              <Cascader
+                style={{ width: 658 }}
+                disabled={addInfo.txt === '编辑'}
+                changeOnSelect
+                fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+                options={treeData}
+                placeholder={addInfo.txt === '编辑' ? '空' : '请选择'}
+                value={parentIdArr ? [...parentIdArr] : []}
+                onChange={cardChange}
+              />
+            </div>
+          </div>
+
+          <Form.Item
+            label='字典名称'
+            name='name'
+            rules={[{ required: true, message: '请输入字典名称' }]}
+            getValueFromEvent={e => e.target.value.replace(/\s+/g, '')}
+          >
+            <Input maxLength={20} showCount placeholder='请输入内容' />
+          </Form.Item>
+
+          <Form.Item
+            label='字典编号'
+            name='num'
+            rules={[{ required: true, message: '请输入字典编号' }]}
+            getValueFromEvent={e => e.target.value.replace(/\s+/g, '')}
+          >
+            <Input maxLength={20} showCount placeholder='请输入内容' />
+          </Form.Item>
+
+          <Form.Item label='字典说明' name='intro'>
+            <TextArea maxLength={500} showCount placeholder='请输入内容' />
+          </Form.Item>
+
+          <div className='fromRow2'>
+            <Form.Item
+              label='排序值'
+              name='sort'
+              rules={[{ required: true, message: '请输入排序值!' }]}
+            >
+              <InputNumber min={1} max={99999} precision={0} placeholder='请输入' />
+            </Form.Item>
+            <div className='fromRowTit'>
+              请输入1~99999的数字。数字越小,排序越靠前。数字相同时,更新发布的内容排在前面
+            </div>
+          </div>
+
+          {/* 确定和取消按钮 */}
+          <br />
+          <Form.Item className='Z2aTitBox'>
+            <Button type='primary' htmlType='submit' disabled={isTowFlag}>
+              提交
+            </Button>
+            &emsp;
+            <MyPopconfirm txtK='取消' onConfirm={closeFu} />
+            <div className={classNames('Z2aTit', isTowFlag ? 'Z2aTitShow' : '')}>
+              该字典模块最多支持二级
+            </div>
+          </Form.Item>
+        </Form>
+      </div>
+    </Modal>
+  )
+}
+
+const MemoZ2add = React.memo(Z2add)
+
+export default MemoZ2add

+ 68 - 0
src/pages/Zsystem/Z2dict/data.ts

@@ -0,0 +1,68 @@
+export type Z2dataType = {
+  id: string
+  parentId: string
+  sort: number
+  name: string
+  num: string
+  intro: string
+  ancestor: string
+  level: number
+  description: string
+  children?: Z2dataType[]
+}
+
+export type Z2AddInfoType = {
+  id: string
+  txt: '新增' | '编辑'
+  acInfo: Z2dataType
+}
+
+// -------------------树结构的搜索过滤-------------------
+export const filterTreeByName = (tree: Z2dataType[], searchTemp: string): Z2dataType[] => {
+  const searchKey = searchTemp.toUpperCase()
+
+  const dfs = (node: Z2dataType): Z2dataType | null => {
+    // 先递归处理子节点(深度优先)
+    const filteredChildren = (node.children?.map(dfs).filter(Boolean) as Z2dataType[]) || []
+
+    // 判断当前节点是否匹配或子节点有匹配项
+
+    const txt = node.name.toUpperCase() + (node.num || '').toUpperCase()
+
+    const isSelfMatch = txt.includes(searchKey)
+
+    // console.log('pppppppp', isSelfMatch, searchKey, node.num)
+
+    const hasChildMatch = filteredChildren.length > 0
+
+    if (isSelfMatch || hasChildMatch) {
+      return {
+        ...node,
+        children: hasChildMatch ? filteredChildren : undefined
+      }
+    }
+    return null
+  }
+
+  return tree.map(dfs).filter(Boolean) as Z2dataType[]
+}
+
+// 通过id获取 树的obj
+export const treeResIdFu = (list: any, id: string) => {
+  // 每次进来使用find遍历一次
+  let res = list.find((item: any) => item.id === id)
+
+  if (res) {
+    return res
+  } else {
+    for (let i = 0; i < list.length; i++) {
+      if (list[i].children instanceof Array && list[i].children.length > 0) {
+        res = treeResIdFu(list[i].children, id)
+
+        if (res) return res
+      }
+    }
+
+    return null
+  }
+}

+ 205 - 0
src/pages/Zsystem/Z2dict/index.module.scss

@@ -0,0 +1,205 @@
+.Z2dict {
+  display: flex;
+  :global {
+    .Z2son {
+      width: 60%;
+      margin-right: 20px;
+      background-color: var(--boxBcaColor);
+      border-radius: 10px;
+      padding: 24px;
+    }
+
+    .Z2ll {
+      .Z2ll1 {
+        margin-bottom: 20px;
+      }
+      .Z2ll2 {
+        height: calc(100% - 52px);
+        overflow-y: auto;
+        .site-tree-search-value {
+          color: red;
+          font-weight: 700;
+        }
+        .ant-tree {
+          background-color: var(--boxBcaColor);
+          min-height: 100%;
+          padding: 10px;
+        }
+        .Z2m1llNone {
+          width: 70%;
+          height: 80%;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          color: var(--themeColor);
+          font-size: 20px;
+          letter-spacing: 4px;
+          font-weight: 700;
+        }
+
+        .NODATA {
+          display: flex;
+          flex-direction: column;
+          justify-content: center;
+          align-items: center;
+          color: #666666;
+          font-size: 16px;
+          height: 100%;
+          // img {
+          //   width: 150px;
+          // }
+          & > p {
+            margin-bottom: 10px;
+          }
+          div {
+            width: 80%;
+            margin-top: 8%;
+            display: flex;
+            justify-content: space-around;
+          }
+        }
+        .ant-tree-title span {
+          font-size: 18px;
+        }
+
+        .ant-tree-node-selected {
+          padding: 4px 8px !important;
+          background-color: var(--txtColor) !important;
+          .ant-tree-title span {
+            color: #fff !important;
+          }
+        }
+      }
+    }
+
+    .Z2rr {
+      width: calc(40% - 20px);
+      overflow-y: auto;
+      margin-right: 0;
+      .Z2mr1 {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        font-weight: 700;
+        color: var(--themeColor);
+        font-size: 24px;
+        .Z2mr1Tit {
+          color: #999;
+          font-size: 14px;
+        }
+      }
+      .Z2mr2 {
+        margin: 20px 0 40px;
+        display: flex;
+        .Z2mr2Btn {
+          cursor: pointer;
+          margin-right: 15px;
+          width: 130px;
+          height: 40px;
+          line-height: 38px;
+          text-align: center;
+          color: #fff;
+          background-color: var(--txtColor);
+          border-radius: 4px;
+          font-size: 18px;
+          letter-spacing: 2px;
+          border: 1px solid transparent;
+          transition: all 0.3s;
+          &:hover {
+            transform: scale(1.1);
+          }
+        }
+        .Z2mr2Btn2 {
+          color: #ff4d4d;
+          background-color: #fff;
+          border: 1px solid #ccc;
+        }
+        .Z2mr2BtnNo {
+          color: #fff;
+          background-color: #ccc;
+          pointer-events: none;
+        }
+      }
+
+      .Z2mr3 {
+        display: flex;
+        font-size: 20px;
+        margin-bottom: 30px;
+        .Z2mr3ll {
+          width: 100px;
+          font-weight: 700;
+        }
+        .Z2mr3rr {
+          width: calc(100% - 100px);
+          word-wrap: break-word;
+        }
+      }
+    }
+  }
+}
+
+// 新增弹窗页面
+.Z2add {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+
+    .ant-modal {
+      width: 800px !important;
+    }
+
+    .ant-modal-body {
+      border-top: 1px solid #ccc;
+    }
+
+    .Z2aMain {
+      padding-top: 15px;
+      .fromRow2 {
+        position: relative;
+
+        .fromRowTit {
+          position: absolute;
+          left: 200px;
+          top: 5px;
+          color: #999;
+          font-size: 12px;
+        }
+      }
+      .fromRow {
+        display: flex;
+        align-items: center;
+        margin-bottom: 24px;
+        .fromRowll {
+          width: 94px;
+          text-align: right;
+          position: relative;
+          & > span {
+            color: #ff4d4f;
+          }
+        }
+        .fromRowrr {
+          width: calc(100% - 94px);
+        }
+      }
+      .Z2aTitBox {
+        position: relative;
+        text-align: center;
+        .Z2aTit {
+          opacity: 0;
+          pointer-events: none;
+          transition: all 0.3s;
+          position: absolute;
+          bottom: -40px;
+          left: 50%;
+          transform: translateX(-50%);
+          color: #ff4d4f;
+        }
+        .Z2aTitShow {
+          opacity: 1;
+          bottom: -26px;
+        }
+      }
+    }
+  }
+}

+ 287 - 0
src/pages/Zsystem/Z2dict/index.tsx

@@ -0,0 +1,287 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { useDispatch, useSelector } from 'react-redux'
+import { Z2_APIdel, Z2_APIgetDict } from '@/store/action/Z2dict'
+import { RootState } from '@/store'
+import { filterTreeByName, treeResIdFu, Z2AddInfoType, Z2dataType } from './data'
+import { Button, Input, Tree, TreeDataNode } from 'antd'
+import { MessageFu } from '@/utils/message'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import classNames from 'classnames'
+import Z2add from './Z2add'
+
+function Z2dict() {
+  const [loding, setLoding] = useState(false)
+
+  const dispatch = useDispatch()
+
+  const getInfoFu = useCallback(
+    (flag: boolean) => {
+      // 默认选中第一个为true
+      dispatch(
+        Z2_APIgetDict(data => {
+          setLoding(true)
+          if (flag && data && data.length) setAcShu(data[0].id)
+        })
+      )
+    },
+    [dispatch]
+  )
+
+  useEffect(() => {
+    getInfoFu(true)
+  }, [getInfoFu])
+
+  const { dictAll } = useSelector((state: RootState) => state.Z2dict)
+
+  // 点击重置
+  const resetFu = useCallback(
+    (flag: boolean) => {
+      // 重置 flag为true
+      // 新增和编辑 为false
+      getInfoFu(flag)
+
+      setValue('')
+      setValue2('')
+    },
+    [getInfoFu]
+  )
+  // 当前选中的树节点ID
+  const [acShu, setAcShu] = useState('')
+
+  // 点击树节点
+  const onSelect = (id: any) => {
+    // console.log('点击树节点', id)
+    if (id[0]) setAcShu(id[0])
+  }
+
+  const [value, setValue] = useState('')
+  const [value2, setValue2] = useState('')
+
+  const timeRef = useRef(-1)
+  const valueChange = useCallback((val: string) => {
+    setValue(val.trim())
+    clearTimeout(timeRef.current)
+    timeRef.current = window.setTimeout(() => {
+      setValue2(val.trim())
+    }, 500)
+  }, [])
+
+  // value变化的时候获取所有dom 设置隐藏
+  const treeDataTemp = useMemo(() => {
+    if (value2) {
+      return filterTreeByName(dictAll, value2)
+    } else return dictAll
+  }, [dictAll, value2])
+
+  // 搜索高亮
+  const treeData = useMemo(() => {
+    const loop = (dataTemp: any[]): TreeDataNode[] => {
+      const data = dataTemp || []
+
+      return data.map(item => {
+        const strTitle = ((item.num ? item.num + ' ' : '') + item.name) as string
+        const strTitleD = strTitle.toUpperCase()
+
+        const valueD = value.toUpperCase()
+        const index = strTitleD.indexOf(valueD)
+
+        const beforeStr = strTitle.substring(0, index)
+        const afterStr = strTitle.slice(index + value.length)
+        const title =
+          index > -1 ? (
+            <span key={item.id}>
+              {beforeStr}
+              <span className='site-tree-search-value'>{value}</span>
+              {afterStr}
+            </span>
+          ) : (
+            <span key={item.id}>{strTitle}</span>
+          )
+        if (item.children) {
+          return { title, key: item.id, children: loop(item.children) }
+        }
+        return {
+          title,
+          key: item.id
+        }
+      })
+    }
+
+    return loop(treeDataTemp)
+  }, [treeDataTemp, value])
+
+  // 右侧信息
+  const rightData = useMemo(() => {
+    let obj = {} as Z2dataType
+    if (treeDataTemp && treeDataTemp.length && acShu) obj = treeResIdFu(treeDataTemp, acShu)
+
+    return obj || {}
+  }, [acShu, treeDataTemp])
+
+  // 点击新增和编辑
+  const [addInfo, setAddInfo] = useState({} as Z2AddInfoType)
+
+  const addSonFu = useCallback(
+    (id: string) => {
+      setAddInfo({
+        id,
+        txt: id === '-1' ? '新增' : '编辑',
+        acInfo: rightData
+      })
+    },
+    [rightData]
+  )
+
+  // 点击删除
+  const delTree = useCallback(
+    async (id: string) => {
+      const res = await Z2_APIdel(id)
+      if (res.code === 0) {
+        MessageFu.success('删除成功!')
+        resetFu(true)
+      }
+    },
+    [resetFu]
+  )
+  return (
+    <div className={styles.Z2dict}>
+      <div className='pageTitle'>数据字典</div>
+
+      {/* 左侧 */}
+      <div className='Z2son Z2ll'>
+        <div className='Z2ll1'>
+          <Input
+            style={{ width: 300 }}
+            placeholder='请输入字典名称/编号'
+            maxLength={30}
+            showCount
+            value={value}
+            onChange={e => valueChange(e.target.value)}
+            allowClear
+          />
+        </div>
+
+        <div className='Z2ll2'>
+          {treeDataTemp && treeDataTemp.length ? (
+            <Tree
+              // 默认全部展开
+              defaultExpandAll={true}
+              // 数据
+              treeData={treeData}
+              // 自定义字段
+              // fieldNames={{ title: 'name', key: 'id', children: 'children' }}
+              // 选中
+              selectedKeys={[acShu]}
+              // 点击
+              onSelect={onSelect}
+            />
+          ) : null}
+
+          {loding && treeDataTemp.length === 0 ? (
+            <div className='NODATA'>
+              <p>暂无相关搜索结果,请更换关键字搜索</p>
+            </div>
+          ) : null}
+        </div>
+      </div>
+
+      {/* 右侧 */}
+      <div className='Z2son Z2rr'>
+        {rightData.id ? (
+          <>
+            <div className='Z2mr1'>
+              <div>
+                操作
+                <span className='Z2mr1Tit'>
+                  {rightData.parentId === '0' ? '(一级字典不可编辑)' : ''}
+                </span>
+              </div>
+
+              <Button
+                size='large'
+                type='primary'
+                onClick={() =>
+                  setAddInfo({
+                    id: '-1',
+                    txt: '新增',
+                    acInfo: rightData
+                  })
+                }
+              >
+                新增
+              </Button>
+            </div>
+            <div className='Z2mr2'>
+              <div
+                onClick={() => addSonFu(rightData.id)}
+                className={classNames('Z2mr2Btn', rightData.parentId === '0' ? 'Z2mr2BtnNo' : '')}
+              >
+                编辑
+              </div>
+              &emsp;
+              <MyPopconfirm
+                txtK='删除'
+                onConfirm={() => delTree(rightData.id)}
+                disabled={rightData.parentId === '0'}
+                Dom={
+                  <div
+                    className={classNames(
+                      'Z2mr2Btn Z2mr2Btn2',
+                      rightData.parentId === '0' ? 'Z2mr2BtnNo' : ''
+                    )}
+                  >
+                    删除
+                  </div>
+                }
+              />
+            </div>
+
+            {/* <div className='Z2mr3'>
+                <div className='Z2mr3ll'>id:</div>
+                <div className='Z2mr3rr'>{rightData.id}</div>
+              </div> */}
+
+            <div className='Z2mr3'>
+              <div className='Z2mr3ll'>字典名称:</div>
+              <div className='Z2mr3rr'>{rightData.name}</div>
+            </div>
+
+            <div className='Z2mr3'>
+              <div className='Z2mr3ll'>字典编号:</div>
+              <div className='Z2mr3rr'>{rightData.num}</div>
+            </div>
+
+            <div className='Z2mr3'>
+              <div className='Z2mr3ll'>层级:</div>
+              <div className='Z2mr3rr'>{rightData.level}</div>
+            </div>
+
+            <div className='Z2mr3'>
+              <div className='Z2mr3ll'>排序值:</div>
+              <div className='Z2mr3rr'>{rightData.sort}</div>
+            </div>
+
+            <div className='Z2mr3'>
+              <div className='Z2mr3ll'>字典说明:</div>
+              <div className='Z2mr3rr'>{rightData.intro || '(空)'}</div>
+            </div>
+          </>
+        ) : null}
+      </div>
+
+      {/* 新增/编辑页面 */}
+      {addInfo.id ? (
+        <Z2add
+          addInfo={addInfo}
+          addFu={() => resetFu(false)}
+          closeFu={() => setAddInfo({} as Z2AddInfoType)}
+        />
+      ) : null}
+    </div>
+  )
+}
+
+const MemoZ2dict = React.memo(Z2dict)
+
+export default MemoZ2dict

+ 4 - 0
src/pages/Zsystem/Z3ipSet/index.module.scss

@@ -0,0 +1,4 @@
+.Z3ipSet {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Zsystem/Z3ipSet/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function Z3ipSet() {
+  return (
+    <div className={styles.Z3ipSet}>
+      <h1>Z3ipSet</h1>
+    </div>
+  )
+}
+
+const MemoZ3ipSet = React.memo(Z3ipSet)
+
+export default MemoZ3ipSet

+ 4 - 0
src/pages/Zsystem/Z4numSet/index.module.scss

@@ -0,0 +1,4 @@
+.Z4numSet {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Zsystem/Z4numSet/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function Z4numSet() {
+  return (
+    <div className={styles.Z4numSet}>
+      <h1>Z4numSet</h1>
+    </div>
+  )
+}
+
+const MemoZ4numSet = React.memo(Z4numSet)
+
+export default MemoZ4numSet

+ 4 - 0
src/pages/Zsystem/Z5flowSet/index.module.scss

@@ -0,0 +1,4 @@
+.Z5flowSet {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Zsystem/Z5flowSet/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function Z5flowSet() {
+  return (
+    <div className={styles.Z5flowSet}>
+      <h1>Z5flowSet</h1>
+    </div>
+  )
+}
+
+const MemoZ5flowSet = React.memo(Z5flowSet)
+
+export default MemoZ5flowSet

+ 4 - 0
src/pages/Zsystem/Z6roleSet/index.module.scss

@@ -0,0 +1,4 @@
+.Z6roleSet {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Zsystem/Z6roleSet/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function Z6roleSet() {
+  return (
+    <div className={styles.Z6roleSet}>
+      <h1>Z6roleSet</h1>
+    </div>
+  )
+}
+
+const MemoZ6roleSet = React.memo(Z6roleSet)
+
+export default MemoZ6roleSet

+ 4 - 0
src/pages/Zsystem/Z7userSet/index.module.scss

@@ -0,0 +1,4 @@
+.Z7userSet {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Zsystem/Z7userSet/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function Z7userSet() {
+  return (
+    <div className={styles.Z7userSet}>
+      <h1>Z7userSet</h1>
+    </div>
+  )
+}
+
+const MemoZ7userSet = React.memo(Z7userSet)
+
+export default MemoZ7userSet

+ 4 - 0
src/pages/Zsystem/Z8sysLog/index.module.scss

@@ -0,0 +1,4 @@
+.Z8sysLog {
+  :global {
+  }
+}

+ 13 - 0
src/pages/Zsystem/Z8sysLog/index.tsx

@@ -0,0 +1,13 @@
+import React from 'react'
+import styles from './index.module.scss'
+function Z8sysLog() {
+  return (
+    <div className={styles.Z8sysLog}>
+      <h1>Z8sysLog</h1>
+    </div>
+  )
+}
+
+const MemoZ8sysLog = React.memo(Z8sysLog)
+
+export default MemoZ8sysLog

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

+ 42 - 0
src/store/action/Z2dict.ts

@@ -0,0 +1,42 @@
+import { Z2dataType } from '@/pages/Zsystem/Z2dict/data'
+import { AppDispatch } from '@/store'
+import http from '@/utils/http'
+/**
+ * 获取对应表格列表(传id就是列表,从第三级开始)
+ */
+export const Z2_APIgetDict = (backFu?: (data: Z2dataType[]) => void, urlTemp?: string): any => {
+  return async (dispatch: AppDispatch) => {
+    let url = urlTemp || 'cms/dict/getTree'
+    const res = await http.get(url)
+    if (res.code === 0) {
+      dispatch({ type: 'Z2/getDictAll', payload: res.data || [] })
+      backFu && backFu(res.data || [])
+    }
+  }
+}
+
+// 根据指定id返回树结构
+export const Z2_APIgetDictZhi = (id: string) => {
+  return http.get(`cms/dict/getTree?parentId=${id}`)
+}
+
+/**
+ * 获取详情
+ */
+export const Z2_APIgetInfo = (id: string) => {
+  return http.get(`cms/dict/detail/${id}`)
+}
+
+/**
+ * 新增、编辑
+ */
+export const Z2_APIsave = (data: any) => {
+  return http.post('cms/dict/save', { ...data, type: 'dict' })
+}
+
+/**
+ * 删除
+ */
+export const Z2_APIdel = (id: string) => {
+  return http.get(`cms/dict/remove/${id}`)
+}

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

@@ -0,0 +1,63 @@
+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, token?: string) => {
+  const config: any = {}
+  if (token) {
+    config.headers = { satoken: token }
+  }
+  return http.post('sys/user/updatePwd', { ...data }, config)
+}
+
+export const passWordEditAPI2 = (data: any) => {
+  return http.post('sys/user/updatePwd', { ...data })
+}
+
+/**
+ * 获取验证码
+ */
+export const API_LoginGetCode = () => {
+  return http.get('admin/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 }
+      })
+    })
+  })
+}
+
+// 直接封装api通用方法
+export const APIbase = (fa: 'get' | 'post', url: string, can?: any) => {
+  if (fa === 'get') return http.get(url)
+  else return http.post(url, can)
+}

+ 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

+ 24 - 0
src/store/reducer/Z2dict.ts

@@ -0,0 +1,24 @@
+import { Z2dataType } from '@/pages/Zsystem/Z2dict/data'
+
+// 初始化状态
+const initState = {
+  // 所有字典数据
+  dictAll: [] as Z2dataType[]
+}
+
+// 定义 action 类型
+type Props = {
+  type: 'Z2/getDictAll'
+  payload: Z2dataType[]
+}
+
+// reducer
+export default function Reducer(state = initState, action: Props) {
+  switch (action.type) {
+    // 获取字典数据
+    case 'Z2/getDictAll':
+      return { ...state, dictAll: action.payload }
+    default:
+      return state
+  }
+}

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

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

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

@@ -0,0 +1,87 @@
+import { LookDomType, RouterType } from '@/pages/Layout/data'
+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
+  },
+  // 页面权限id集合
+  authorityIds: [] as number[],
+
+  // 用户工作台常用功能
+  userRolePermissions: [] as RouterType,
+  // 用户信息
+  userInfo: {} as any,
+  // 修改密码
+  passEditShow: false
+}
+
+// 定义 action 类型
+type LayoutActionType =
+  | { type: 'layout/lookBigImg'; payload: { url: string; show: boolean } }
+  | { type: 'layout/lookDom'; payload: LookDomType }
+  | { type: 'layout/message'; payload: MessageType }
+  | { type: 'layout/authorityIds'; payload: number[] }
+  | { type: 'layout/userRolePermissions'; payload: RouterType }
+  | { type: 'layout/userInfo'; payload: any }
+  | { type: 'layout/passEditShow'; payload: boolean }
+  | {
+      type: 'layout/closeUpFile'
+      payload: {
+        fu: () => void
+        state: boolean
+      }
+    }
+
+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 }
+    // 页面权限id集合
+    case 'layout/authorityIds':
+      return { ...state, authorityIds: action.payload }
+    // 用户工作台常用功能
+    case 'layout/userRolePermissions':
+      return { ...state, userRolePermissions: action.payload }
+    // 设置用户信息
+    case 'layout/userInfo':
+      return { ...state, userInfo: action.payload }
+    // 修改密码
+    case 'layout/passEditShow':
+      return { ...state, passEditShow: action.payload }
+
+    default:
+      return state
+  }
+}

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

@@ -0,0 +1,9 @@
+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'
+declare module 'file-saver'

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

+ 38 - 0
src/utils/history.ts

@@ -0,0 +1,38 @@
+import { createHashHistory } from 'history'
+const history = createHashHistory()
+export default history
+
+// 权限失效,回登录页,带上回跳路径
+export const loginOutFu = () => {
+  const urlAll = window.location.href
+  const urlArr = urlAll.split('/#/')
+
+  if (!urlAll.includes('/login')) {
+    if (urlArr[1]) {
+      history.replace(`/login?back=${urlArr[1]}`)
+    } else history.replace('/login')
+  }
+}
+
+// 新窗口打开页面
+export const openLink = (path: string) => {
+  const urlAll = window.location.href
+  const qian = urlAll.split('/#/')[0]
+  window.open(`${qian}/#${path}`, '_blank')
+}
+
+// 监听路由
+let routerLength = 0
+history.listen((_: any, listener: any) => {
+  if (listener === 'PUSH') {
+    routerLength += 1
+  } else if (listener === 'POP') {
+    if (routerLength >= 1) routerLength -= 1
+  }
+})
+
+// 点击返回
+export const backPageFu = (path: string) => {
+  if (routerLength) history.go(-1)
+  else history.push(path)
+}

+ 115 - 0
src/utils/http.ts

@@ -0,0 +1,115 @@
+import axios from 'axios'
+import { loginOutFu } from './history'
+import { getTokenInfo, removeTokenInfo } from './storage'
+import store from '@/store'
+import { MessageFu } from './message'
+import { domShowFu } from './domShow'
+
+// 初始默认密码
+export const defaultPassWord = 'Aa147852'
+
+export const envFlag = process.env.NODE_ENV === 'development'
+
+// const baseUrlTemp = 'xxxxxxxxxxxxxx' // 测试环境
+const baseUrlTemp = 'http://192.168.20.61:8118' // 线下环境
+
+const baseFlag = baseUrlTemp.includes('https://')
+
+// 工作台图标/其他静态图 前缀地址
+export const iconUrl = envFlag
+  ? `https://sit-qingdaobeer.4dage.com/backstage/tabImg`
+  : '/backstage/tabImg'
+
+// 请求基地址
+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: 1000 * 60 * 20
+})
+
+let axajInd = 0
+
+// 请求拦截器
+http.interceptors.request.use(
+  function (config: any) {
+    // 发请求前打开加载提示
+    domShowFu('#AsyncSpinLoding', true)
+
+    axajInd++
+
+    const { token } = getTokenInfo()
+    if (token) config.headers.satoken = token
+    return config
+  },
+  function (err) {
+    return Promise.reject(err)
+  }
+)
+
+let timeId = -1
+
+// 响应拦截器
+http.interceptors.response.use(
+  function (response) {
+    // 请求回来的关闭加载提示
+    if (axajInd >= 1) axajInd--
+
+    if (axajInd === 0) {
+      domShowFu('#AsyncSpinLoding', false)
+    }
+    if ([401, 403].includes(response.data.code)) {
+      removeTokenInfo()
+      loginOutFu()
+      clearTimeout(timeId)
+      timeId = window.setTimeout(() => {
+        MessageFu.warning(response.data.code === 401 ? '登录超时,请重新登录' : response.data.msg)
+      }, 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)
+    if (axajInd >= 1) axajInd--
+    timeId = window.setTimeout(() => {
+      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 ([401, 403].includes(err.response.data.code)) {
+            removeTokenInfo()
+            loginOutFu()
+          }
+        } else MessageFu.error('响应错误,请联系管理员!')
+      }
+    }, 100)
+
+    return Promise.reject(err)
+  }
+)
+
+// 导出 axios 实例
+export default http

+ 65 - 0
src/utils/index.ts

@@ -0,0 +1,65 @@
+import { baseURL } from './http'
+
+// 上传文件自动归类
+type FileObjType = {
+  [key: string]: string[]
+}
+export const fileImgArr = [
+  'jpg',
+  'jpeg',
+  'png',
+  'gif',
+  'bmp',
+  'tiff',
+  'webp',
+  'svg',
+  'psd',
+  'ai',
+  'ico'
+]
+export const fileVideoArr = ['mp4', 'mov', 'avi', 'mkv', 'flv', 'wmv', 'mpeg', 'webm']
+
+const fileObj: FileObjType = {
+  img: fileImgArr,
+  video: fileVideoArr,
+  model: ['4dage', 'obj', 'stl', 'fbx', 'gltf', '3ds', 'blend', 'dae', 'step'],
+  audio: ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a'],
+  doc: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'csv', 'md', 'html']
+  // 'other':[]
+}
+
+export const fileTypeRes = (fileName: string) => {
+  const txtArr = fileName.split('.')
+  const txt = txtArr[txtArr.length - 1]
+
+  let type = 'other'
+
+  for (const k in fileObj) {
+    if (fileObj[k].includes(txt.toLowerCase())) type = k
+  }
+  return type
+}
+
+// 简化版本:通过URL下载同域文件
+export const downloadFileByUrl = async (fileUrl: string, fileName?: string, back?: () => void) => {
+  const a = document.createElement('a')
+  a.href = baseURL + fileUrl
+  a.target = '_blank'
+
+  // 设置下载文件名
+  if (fileName) {
+    a.download = fileName
+  } else {
+    // 自动从URL提取文件名
+    const urlParts = fileUrl.split('/')
+    const originalName = urlParts[urlParts.length - 1]
+    a.download = originalName || 'download'
+  }
+
+  // 触发下载
+  a.style.display = 'none'
+  document.body.appendChild(a)
+  a.click()
+  document.body.removeChild(a)
+  if (back) back()
+}

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

+ 137 - 0
src/utils/pass.ts

@@ -0,0 +1,137 @@
+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
+}
+
+const decodeOne = (str: string) => {
+  const str1 = str.substring(str.length - 10, str.length - 8)
+  const str2 = str.substring(8, str.length - 18)
+  return str1 + str2
+}
+
+const decodeStr = (encoded: string | [string, string]) => {
+  if (Array.isArray(encoded)) {
+    return [decodeOne(encoded[0]), decodeOne(encoded[1])]
+  }
+  return decodeOne(encoded)
+}
+
+/** 判断是否为 encodeStr 加密后的字符串(长度>=26) */
+const isEncoded = (str: string) => str && str.length >= 26
+
+/** 解密或返回原值(兼容未加密的旧数据) */
+export const decodeIfEncoded = (str: string): string => (isEncoded(str) ? decodeOne(str) : str)
+
+/** 脱敏:papers 保留前2位、后2位 */
+export const maskPapers = (str: string) => {
+  if (!str) return str
+  if (str.length <= 4) return str
+  return str.substring(0, 2) + '*'.repeat(str.length - 4) + str.substring(str.length - 2)
+}
+
+/** 脱敏:phone 保留前3位、后4位 */
+export const maskPhone = (str: string) => {
+  if (!str) return str
+  if (str.length <= 7) return str
+  return str.substring(0, 3) + '*'.repeat(str.length - 7) + str.substring(str.length - 4)
+}
+
+/** 列表显示:papers 解密后脱敏 */
+export const getStaffDisplayPapers = (val: string) => maskPapers(decodeIfEncoded(val || ''))
+
+/** 列表显示:phone 解密后脱敏 */
+export const getStaffDisplayPhone = (val: string) => maskPhone(decodeIfEncoded(val || ''))
+
+export default encodeStr
+export { decodeStr }

+ 0 - 0
src/utils/storage.ts


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