lanxin 4 месяцев назад
Сommit
faaca673ed
100 измененных файлов с 39138 добавлено и 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.ico
  13. 17 0
      public/index.html
  14. 64 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/btn_login.png
  20. BIN
      src/assets/img/latLeftBg.jpg
  21. BIN
      src/assets/img/loading.gif
  22. BIN
      src/assets/img/loginBtn.png
  23. BIN
      src/assets/img/loginInco1.png
  24. BIN
      src/assets/img/loginInco2.png
  25. BIN
      src/assets/img/loginInco3.png
  26. BIN
      src/assets/img/logo.png
  27. BIN
      src/assets/img/logo_login.png
  28. BIN
      src/assets/img/user.png
  29. 258 0
      src/assets/styles/base.css
  30. 412 0
      src/assets/styles/base.less
  31. 21 0
      src/components/AsyncSpinLoding/index.module.scss
  32. 15 0
      src/components/AsyncSpinLoding/index.tsx
  33. 32 0
      src/components/AuthRoute/index.tsx
  34. 51 0
      src/components/ImageLazy/index.module.scss
  35. 63 0
      src/components/ImageLazy/index.tsx
  36. 65 0
      src/components/LookDom/index.module.scss
  37. 46 0
      src/components/LookDom/index.tsx
  38. 29 0
      src/components/Message/index.tsx
  39. 52 0
      src/components/MyPopconfirm.tsx
  40. 330 0
      src/components/MyTable/form.tsx
  41. 18 0
      src/components/MyTable/index.module.scss
  42. 239 0
      src/components/MyTable/index.tsx
  43. 22 0
      src/components/NotFound/index.tsx
  44. 9 0
      src/components/SpinLoding/index.module.scss
  45. 13 0
      src/components/SpinLoding/index.tsx
  46. 21 0
      src/components/SpinLodingSon/index.module.scss
  47. 15 0
      src/components/SpinLodingSon/index.tsx
  48. 43 0
      src/components/UpAsyncLoding/index.module.scss
  49. 38 0
      src/components/UpAsyncLoding/index.tsx
  50. 41 0
      src/components/YtableVideo/index.module.scss
  51. 36 0
      src/components/YtableVideo/index.tsx
  52. 75 0
      src/components/Z3upFiles/data.ts
  53. 152 0
      src/components/Z3upFiles/index.module.scss
  54. 443 0
      src/components/Z3upFiles/index.tsx
  55. 158 0
      src/components/Z3upFilesRef/index.module.scss
  56. 465 0
      src/components/Z3upFilesRef/index.tsx
  57. 195 0
      src/components/ZRichTextOne/index.module.scss
  58. 173 0
      src/components/ZRichTextOne/index.tsx
  59. 101 0
      src/components/ZupOne/index.module.scss
  60. 295 0
      src/components/ZupOne/index.tsx
  61. 42 0
      src/index.tsx
  62. 133 0
      src/pages/Abench/A1statistics/index.module.scss
  63. 718 0
      src/pages/Abench/A1statistics/index.tsx
  64. 35 0
      src/pages/Abench/A2bench/A2editUser/index.module.scss
  65. 120 0
      src/pages/Abench/A2bench/A2editUser/index.tsx
  66. 59 0
      src/pages/Abench/A2bench/A2setStock/index.module.scss
  67. 117 0
      src/pages/Abench/A2bench/A2setStock/index.tsx
  68. 50 0
      src/pages/Abench/A2bench/A2table/index.module.scss
  69. 237 0
      src/pages/Abench/A2bench/A2table/index.tsx
  70. 123 0
      src/pages/Abench/A2bench/index.module.scss
  71. 170 0
      src/pages/Abench/A2bench/index.tsx
  72. 10 0
      src/pages/Abench/A3flow/index.module.scss
  73. 266 0
      src/pages/Abench/A3flow/index.tsx
  74. 10 0
      src/pages/Abench/A4proof/index.module.scss
  75. 220 0
      src/pages/Abench/A4proof/index.tsx
  76. 3 0
      src/pages/Benter/B1collect/B1edit/index.module.scss
  77. 44 0
      src/pages/Benter/B1collect/B1edit/index.tsx
  78. 4 0
      src/pages/Benter/B1collect/index.module.scss
  79. 37 0
      src/pages/Benter/B1collect/index.tsx
  80. 4 0
      src/pages/Benter/B2enterGuan/B2edit/index.module.scss
  81. 45 0
      src/pages/Benter/B2enterGuan/B2edit/index.tsx
  82. 4 0
      src/pages/Benter/B2enterGuan/index.module.scss
  83. 37 0
      src/pages/Benter/B2enterGuan/index.tsx
  84. 4 0
      src/pages/Benter/B3auth/B3edit/index.module.scss
  85. 67 0
      src/pages/Benter/B3auth/B3edit/index.tsx
  86. 32 0
      src/pages/Benter/B3auth/data.ts
  87. 4 0
      src/pages/Benter/B3auth/index.module.scss
  88. 36 0
      src/pages/Benter/B3auth/index.tsx
  89. 4 0
      src/pages/Benter/B4enterCang/B4edit/index.module.scss
  90. 47 0
      src/pages/Benter/B4enterCang/B4edit/index.tsx
  91. 4 0
      src/pages/Benter/B4enterCang/index.module.scss
  92. 37 0
      src/pages/Benter/B4enterCang/index.tsx
  93. 14 0
      src/pages/Cledger/C1ledger/C1Import/index.module.scss
  94. 346 0
      src/pages/Cledger/C1ledger/C1Import/index.tsx
  95. 68 0
      src/pages/Cledger/C1ledger/C1ImportRes/index.module.scss
  96. 233 0
      src/pages/Cledger/C1ledger/C1ImportRes/index.tsx
  97. 68 0
      src/pages/Cledger/C1ledger/C1importZipRes/index.module.scss
  98. 185 0
      src/pages/Cledger/C1ledger/C1importZipRes/index.tsx
  99. 11 0
      src/pages/Cledger/C1ledger/index.module.scss
  100. 0 0
      src/pages/Cledger/C1ledger/index.tsx

+ 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.ico


+ 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.ico" 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>

+ 64 - 0
src/App.tsx

@@ -0,0 +1,64 @@
+// 关于路由
+import React from 'react'
+import { Router, Route, Switch } from 'react-router-dom'
+import history from './utils/history'
+import AuthRoute from './components/AuthRoute'
+import SpinLoding from './components/SpinLoding'
+import AsyncSpinLoding from './components/AsyncSpinLoding'
+import { Image } from 'antd'
+import { useSelector } from 'react-redux'
+import store, { RootState } from './store'
+import UpAsyncLoding from './components/UpAsyncLoding'
+import MessageCom from './components/Message'
+import LookDom from './components/LookDom'
+const Layout = React.lazy(() => import('./pages/Layout'))
+const Login = React.lazy(() => import('./pages/Login'))
+
+export default function App() {
+  // 从仓库中获取查看图片的信息
+  const lookBigImg = useSelector((state: RootState) => state.A0Layout.lookBigImg)
+
+  return (
+    <>
+      {/* 关于路由 */}
+      <Router history={history}>
+        <React.Suspense fallback={<SpinLoding />}>
+          <Switch>
+            {/* 测试页面 */}
+            <Route path='/login' component={Login} />
+            <AuthRoute path='/' component={Layout} />
+          </Switch>
+        </React.Suspense>
+      </Router>
+
+      {/* 发送请求的加载组件 */}
+      <AsyncSpinLoding />
+
+      {/* 所有图片点击预览查看大图 */}
+      {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/btn_login.png


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/logo_login.png


BIN
src/assets/img/user.png


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

@@ -0,0 +1,258 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+html {
+  height: 100%;
+  font-size: 14px;
+}
+/* 主题色 */
+:root {
+  --themeColor: #910000;
+  --loginTxt: #fddb77;
+  --boxBcaColor: rgba(255, 255, 255, 0.6);
+  --txtColor: #910000;
+  --myAntdColor: #333333;
+  --myAntdColor2: #d9bf76;
+}
+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;
+  min-width: 1600px;
+  min-height: 900px;
+  overflow: auto;
+  overflow-y: overlay;
+  /* 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: -65px;
+  left: -18px;
+  padding-left: 40px;
+  color: #333333;
+  letter-spacing: 2px;
+  font-weight: bold;
+}
+.pageTitle::before {
+  position: absolute;
+  right: 0;
+  content: '';
+  font-size: 24px;
+  font-weight: 700;
+  bottom: -14px;
+  width: calc(100% - 40px);
+  height: 4px;
+  background-color: #333333;
+}
+.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: #efefef !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: #333333 !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%;
+}

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

@@ -0,0 +1,412 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+html {
+  height: 100%;
+  font-size: 14px;
+  // user-select: none;
+}
+
+/* 主题色 */
+:root {
+  // 文字颜色
+  --themeColor: #910000;
+  // 登录页颜色
+  --loginTxt: #fddb77;
+  // 一个模块一个模块的背景色
+  --boxBcaColor: rgba(255, 255, 255, 0.6);
+
+  // 这个项目颜色有点杂--字典、组织、标签模块主题色
+  --txtColor: #910000;
+
+  // antd主题色
+  --myAntdColor: #333333;
+
+  // antd主题色2
+  --myAntdColor2: #d9bf76;
+}
+
+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;
+  min-width: 1600px;
+  min-height: 900px;
+  overflow: auto;
+  overflow-y: overlay;
+
+  a {
+    text-decoration: none;
+    color: black;
+    outline: none;
+  }
+
+  // ?的提示
+  .iconHoverTit {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .iconHoverTitTxt {
+    background-color: var(--themeColor);
+    color: #fff;
+    width: 16px;
+    height: 16px;
+    line-height: 16px;
+    text-align: center;
+    font-size: 12px;
+    border-radius: 50%;
+  }
+
+  // a {
+  //   color: var(--themeColor);
+  // }
+
+  /* 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: -65px;
+  left: -18px;
+  padding-left: 40px;
+  color: #333333;
+  letter-spacing: 2px;
+  font-weight: bold;
+
+  &::before {
+    position: absolute;
+    right: 0;
+    content: '';
+    font-size: 24px;
+    font-weight: 700;
+    bottom: -14px;
+    width: calc(100% - 40px);
+    height: 4px;
+    background-color: #333333;
+  }
+}
+
+// 滚动条
+.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: #efefef !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: #333333 !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

+ 330 - 0
src/components/MyTable/form.tsx

@@ -0,0 +1,330 @@
+import React, {
+  forwardRef,
+  useCallback,
+  useEffect,
+  useImperativeHandle,
+  useMemo,
+  useState
+} from 'react'
+import styles from './index.module.scss'
+import { DatePicker, Form, FormInstance, Input, Table, TableProps } from 'antd'
+import ImageLazy from '../ImageLazy'
+import classNames from 'classnames'
+import dayjs from 'dayjs'
+import { myTableTransferSize, resJiLianFu, resTagFu, zhiDiResFu } from '@/utils/dataChange'
+
+interface MyTableProps extends Omit<TableProps, 'onChange'> {
+  yHeight?: number //设置表格的高度
+  list: any //表格数据
+  columnsTemp: any[][] //表格展示
+  total?: number //总数
+  pageNum?: number
+  pageSize?: number
+  pagingInfo?: any | boolean
+  onChange?: (pageNum: number, pageSize: number) => void
+  lastBtn?: any //后面的操作按钮
+  startBtn?: any
+  classKey?: string //一个组件多次使用的时候要传递,分别设置style
+  // 表格简单的合并
+  merge?: { type: string; num: number; loc: 'rowSpan' | 'colSpan' }
+  // 定制化表头
+  myTitle?: { name: string; Com: React.ReactNode }
+  // 为空的定制字段
+  isNull?: string
+  // 设置宽度
+  widthSet?: any
+  rowKey?: string
+  readOnly?: boolean
+  // 没有数据的时候展示
+  emptyText?: boolean
+}
+
+export interface MyTableMethods {
+  form: FormInstance<any>
+}
+
+export const getDesensitizeTxt = (txt: string, frontLen = 3, endLen = 4) => {
+  if (!txt) return txt
+  const totalVisible = frontLen + endLen
+  if (txt.length <= totalVisible) {
+    return txt
+  }
+  const frontStr = txt.substring(0, frontLen)
+  const endStr = endLen > 0 ? txt.substring(txt.length - endLen) : ''
+  const maskStr = '*'.repeat(txt.length - frontLen - endLen)
+
+  return frontStr + maskStr + endStr
+}
+
+const MyTable = forwardRef<MyTableMethods, MyTableProps>(
+  (
+    {
+      yHeight,
+      list,
+      columnsTemp,
+      total,
+      pageNum = 1,
+      pageSize = 10,
+      pagingInfo = {
+        showQuickJumper: true,
+        position: ['bottomCenter'],
+        showSizeChanger: true
+      },
+      onChange,
+      lastBtn = [],
+      startBtn = [],
+      classKey = '',
+      merge,
+      myTitle,
+      isNull = '(空)',
+      widthSet,
+      rowKey = 'id',
+      readOnly,
+      emptyText,
+      ...rest
+    },
+    ref
+  ) => {
+    // 点击操作高亮
+    const [clickAc, setClickAc] = useState(0)
+    const [form] = Form.useForm()
+
+    // 表格内容定制化
+    const tableComObj = useCallback(
+      (key: string, val: string[], id?: any, backFu?: (id: number) => void) => {
+        const obj = {
+          // 超链接打开
+          A: (
+            <a href={val[1]} target='_blank' title={val[1]} rel='noreferrer'>
+              {val[0]}
+            </a>
+          ),
+          // 点击触发事件
+          S: (
+            <span
+              className={classNames('MTclick', clickAc === id ? 'MTclickAc' : '')}
+              onClick={() => {
+                if (id && backFu) {
+                  backFu(id)
+                  setClickAc(id)
+                }
+              }}
+            >
+              {val[1]}
+            </span>
+          )
+        }
+        return Reflect.get(obj, key)
+      },
+      [clickAc]
+    )
+
+    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) =>
+            v[3] && !item[v[2]] ? (
+              <div dangerouslySetInnerHTML={{ __html: v[3] }}></div>
+            ) : item[v[2]] && item[v[2]] !== '0' ? (
+              item[v[2]]
+            ) : (
+              isNull
+            ),
+          txtArr: (item: any) => (
+            <div dangerouslySetInnerHTML={{ __html: (item[v[2]] || []).join('<br/>') }}></div>
+          ),
+          // 日期去掉00:00:00
+          dateRes: (item: any) => {
+            let res = item[v[2]] ? dayjs(item[v[2]]).format('YYYY-MM-DD') : isNull
+            return res
+          },
+          // 多个字段拼接
+          ping: (item: any) => (item[v[2]] || '') + (resJiLianFu(item[v[3]]) || '') || isNull,
+          // 这个模块特有的级联控制
+          txtC: (item: any) =>
+            v[1] === '年代' && item[v[2]] === '其他' ? '其他' : resJiLianFu(item[v[2]]),
+          txtCTag: (item: any) => {
+            // resJiLianFu(item[v[2]], 'tag'),
+            let resTxt = item[v[2]] || isNull
+            if (resTxt) {
+              return resTagFu(resTxt, v[3] || '藏品') || isNull
+            }
+            return resTxt || isNull
+          },
+          // 尺寸
+          size: myTableTransferSize,
+          // 质地
+          zhiDi: zhiDiResFu,
+          img: (item: any) =>
+            v[3] && !item[v[2]] ? (
+              <div dangerouslySetInnerHTML={{ __html: v[3] }}></div>
+            ) : (
+              <div className='tableImgAuto'>
+                <ImageLazy
+                  width={60}
+                  height={60}
+                  srcBig={item.thumbPc || item.filePath}
+                  src={item[v[2]] || item.thumb}
+                  offline={(item[v[2]] || item.thumb || '').includes('http')}
+                />
+              </div>
+            ),
+          // 附件大小
+          fileSize: (item: any) => {
+            if (item[v[2]]) {
+              const resTxt = (item[v[2]] / 1024).toFixed(2)
+              if (resTxt === '0.00') return '0.01'
+              else return resTxt
+            } else return isNull
+          },
+          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]]], item.id, v[5])
+            } else if ((item[v[2]] || '').length >= v[3]) {
+              tempCom = (
+                <span style={{ cursor: 'pointer' }} title={item[v[2]]}>
+                  {tempCom}
+                </span>
+              )
+            }
+
+            return tempCom
+          },
+          input: (item: any) => {
+            return (
+              <Form.Item noStyle name={`${item.id}-${v[2]}`} initialValue={item[v[2]]}>
+                <Input
+                  allowClear
+                  readOnly={readOnly}
+                  maxLength={v[3]?.maxLength}
+                  placeholder={v[3]?.placeholder || '请输入'}
+                />
+              </Form.Item>
+            )
+          },
+          datePicker: (item: any) => {
+            return (
+              <Form.Item noStyle name={`${item.id}-${v[2]}`}>
+                <DatePicker disabled={readOnly} {...(v[3] || {})} />
+              </Form.Item>
+            )
+          },
+          custom: (item: any) => {
+            return (
+              <Form.Item noStyle name={`${item.id}-${v[2]}`} initialValue={item[v[2]]}>
+                {v[3].render(readOnly, item)}
+              </Form.Item>
+            )
+          },
+          desensitize: (item: any) => {
+            const txt = item[v[2]]
+            if (!txt) return isNull
+            const frontLen = v[3]?.frontLen || 3
+            const endLen = v[3]?.endLen || 4
+
+            return getDesensitizeTxt(txt, frontLen, endLen)
+          },
+          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
+          },
+          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, readOnly, tableComObj]
+    )
+
+    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])
+
+    useImperativeHandle(ref, () => ({
+      form
+    }))
+
+    return (
+      <Form form={form} component={false}>
+        <Table
+          className={classNames(
+            `${styles.MyTable} MyTable${classKey}`,
+            emptyText ? styles.MyTableNull : ''
+          )}
+          scroll={{ y: yHeight ? yHeight : '' }}
+          dataSource={list}
+          columns={[...startBtn, ...columns, ...lastBtn]}
+          rowKey={rowKey ? rowKey : 'id'}
+          pagination={
+            pagingInfo
+              ? {
+                  ...pagingInfo,
+                  current: pageNum,
+                  pageSize: pageSize,
+                  total: total,
+                  onChange: paginationChange()
+                }
+              : false
+          }
+          {...rest}
+        />
+      </Form>
+    )
+  }
+)
+
+const MemoMyTable = React.memo(MyTable)
+
+export default MemoMyTable

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

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

@@ -0,0 +1,239 @@
+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'
+import { myTableTransferSize, resJiLianFu, resTagFu, zhiDiResFu } from '@/utils/dataChange'
+
+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
+        },
+        // 多个字段拼接
+        ping: (item: any) => {
+          let jlTxt = resJiLianFu(item[v[3]])
+          jlTxt = jlTxt === isNull ? '' : jlTxt
+          return item[v[2]] && jlTxt ? item[v[2]] + jlTxt : isNull
+        },
+        // 这个模块特有的级联控制
+        txtC: (item: any) => resJiLianFu(item[v[2]]),
+        txtCTag: (item: any) => {
+          // resJiLianFu(item[v[2]], 'tag'),
+          let resTxt = item[v[2]] || isNull
+          if (resTxt) {
+            return resTagFu(resTxt, v[3] || '藏品') || isNull
+          }
+          return resTxt || isNull
+        },
+
+        // 尺寸
+        size: myTableTransferSize,
+        // 质地
+        zhiDi: zhiDiResFu,
+        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

+ 75 - 0
src/components/Z3upFiles/data.ts

@@ -0,0 +1,75 @@
+import store from '@/store'
+import { baseURL } from '@/utils/http'
+
+export type FileListType = {
+  id: number
+  fileName: string
+  filePath: string
+  thumb: string
+  type: 'img' | 'video' | 'doc'
+
+  // 图片类型
+  effect?: string
+}
+
+// 查看 权限 图片 /视频 、音频
+export const authFilesLookFu = (name: string, url: string) => {
+  let flag = false
+
+  const nameRes = name ? name : ''
+
+  // pdf和txt 直接新窗口打开
+  const arr0: ('.pdf' | '.txt')[] = ['.pdf', '.txt']
+  arr0.forEach(v => {
+    if (nameRes.toLowerCase().endsWith(v)) {
+      if (url) window.open(baseURL + url)
+      flag = true
+    }
+  })
+
+  // 图片使用 antd的图片预览组件
+  const arr1 = ['.png', '.jpg', '.jpeg', '.gif']
+  arr1.forEach(v => {
+    if (nameRes.toLowerCase().endsWith(v)) {
+      if (url) {
+        store.dispatch({
+          type: 'layout/lookBigImg',
+
+          payload: {
+            url: baseURL + url,
+            show: true
+          }
+        })
+      }
+
+      flag = true
+    }
+  })
+
+  // 视频和音频 使用自己的封装的组件
+  let type: '' | 'video' | 'audio' = ''
+  const arr2 = ['.mp3', '.wav']
+  arr2.forEach(v => {
+    if (nameRes.toLowerCase().endsWith(v)) {
+      type = 'audio'
+      flag = true
+    }
+  })
+
+  if (nameRes.toLowerCase().endsWith('.mp4')) {
+    type = 'video'
+    flag = true
+  }
+
+  if (type && url) {
+    store.dispatch({
+      type: 'layout/lookDom',
+      payload: {
+        src: url,
+        type
+      }
+    })
+  }
+
+  return flag
+}

+ 152 - 0
src/components/Z3upFiles/index.module.scss

@@ -0,0 +1,152 @@
+.Z3upFiles {
+  :global {
+    a {
+      color: black;
+    }
+    .Z3files {
+      .ZTbox1ImgRow {
+        display: inline-block;
+        margin: 15px 20px 10px 0;
+        width: 100px;
+        height: 125px;
+        position: relative;
+        position: relative;
+
+        // 修复图片闪动问题:确保拖拽手柄有固定尺寸
+        .ZTbox1ImgRowDragHandle {
+          width: 100px;
+          height: 100px;
+          cursor: default;
+
+          &:active {
+            cursor: grabbing;
+          }
+        }
+
+        // 第一张作为封面
+        .ZTbox1ImgRowCover {
+          font-size: 12px;
+          line-height: 22px;
+          position: absolute;
+          left: 0;
+          top: 0;
+          width: 100%;
+          height: 24px;
+          background-color: rgba(0, 0, 0, 0.8);
+          color: #fff;
+          text-align: center;
+          pointer-events: none;
+        }
+
+        .ZTbox1ImgRowIcon {
+          width: 100%;
+          background-color: rgba(0, 0, 0, 0.6);
+          color: #fff;
+          display: flex;
+          justify-content: space-around;
+          font-size: 16px;
+
+          a {
+            color: #fff !important;
+          }
+        }
+
+        .ZTbox1ImgRowX {
+          cursor: pointer;
+          position: absolute;
+          right: -10px;
+          top: -10px;
+          z-index: 99;
+          background-color: rgba(0, 0, 0, 0.8);
+          width: 20px;
+          height: 20px;
+          border-radius: 50%;
+          font-size: 16px;
+          color: #fff;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+        }
+      }
+
+      .Z3filesRow {
+        width: 100%;
+        max-width: 786px;
+        display: flex;
+        align-items: center;
+        padding: 0px 12px;
+        border: 1px solid #d9d9d9;
+        border-radius: 6px;
+        margin: 2px;
+        background: #fff;
+        transition: all 0.3s;
+
+        &:hover {
+          border-color: #40a9ff;
+          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        }
+
+        &.dragging {
+          opacity: 0.5;
+          background: #f0f0f0;
+        }
+
+        &.dragOverlay {
+          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+          transform: rotate(5deg);
+        }
+
+        .dragHandle {
+          cursor: grab;
+          margin-right: 12px;
+          color: #999;
+          padding: 4px;
+
+          &:active {
+            cursor: grabbing;
+          }
+
+          &:hover {
+            color: #40a9ff;
+          }
+        }
+
+        .Z3files1 {
+          flex: 1;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        .Z3files2 {
+          display: flex;
+          align-items: center;
+
+          .anticon {
+            cursor: pointer;
+            color: #666;
+
+            &:hover {
+              color: #40a9ff;
+            }
+          }
+        }
+      }
+    }
+
+    .fileTit {
+      margin-top: 5px;
+      color: #666;
+      font-size: 12px;
+
+      .noUpThumb {
+        display: none;
+
+        &.noUpThumbAc {
+          display: block;
+          color: #ff4d4f;
+        }
+      }
+    }
+  }
+}

+ 443 - 0
src/components/Z3upFiles/index.tsx

@@ -0,0 +1,443 @@
+import React, { useCallback, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { API_upFile } from '@/store/action/layout'
+import { MessageFu } from '@/utils/message'
+import { fileDomInitialFu } from '@/utils/domShow'
+import { Button, Popconfirm } from 'antd'
+import {
+  UploadOutlined,
+  CloseOutlined,
+  DownloadOutlined,
+  EyeOutlined,
+  MenuOutlined
+} from '@ant-design/icons'
+import classNames from 'classnames'
+import { baseURL } from '@/utils/http'
+import { authFilesLookFu, FileListType } from './data'
+import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, closestCenter } from '@dnd-kit/core'
+import {
+  SortableContext,
+  useSortable,
+  verticalListSortingStrategy,
+  arrayMove
+} from '@dnd-kit/sortable'
+import { CSS } from '@dnd-kit/utilities'
+import { useInfo } from '@/pages/Zother/InfoContext'
+import { Typetable } from '@/pages/Zother/data'
+import { fileTypeRes } from '@/utils'
+import ImageLazy from '../ImageLazy'
+import store from '@/store'
+import MyPopconfirm from '../MyPopconfirm'
+
+// ----------------这个组件用于外面一级的附件上传
+
+interface SortableFileItemProps {
+  file: FileListType
+  onDelete: (id: number) => void
+  isLook: boolean
+  index: number
+  disabled?: boolean
+  oneIsCover?: boolean
+}
+// 修复性能问题:移除不必要的状态和事件处理
+const SortableFileItem = React.memo(
+  ({ file, onDelete, isLook, index, disabled, oneIsCover }: SortableFileItemProps) => {
+    const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
+      id: file.id,
+      disabled: disabled || isLook // 修复拖动禁用问题
+    })
+
+    const style = {
+      transform: CSS.Transform.toString(transform),
+      transition,
+      opacity: isDragging ? 0.5 : 1
+    }
+
+    // 修复按钮点击问题:阻止事件冒泡
+    const handleButtonClick = useCallback((e: React.MouseEvent, callback: () => void) => {
+      e.stopPropagation() // 修复按钮点击无效问题:阻止事件冒泡到拖拽元素
+      callback()
+    }, [])
+
+    if (file.type === 'img') {
+      return (
+        <div
+          ref={setNodeRef}
+          style={style}
+          className={classNames('ZTbox1ImgRow', isDragging ? 'dragging' : '')}
+        >
+          {/* 修复按钮点击无效问题:将拖拽手柄单独放置,不覆盖操作按钮 */}
+          <div
+            className='ZTbox1ImgRowDragHandle'
+            {...attributes}
+            {...listeners}
+            style={{ cursor: isLook ? 'default' : 'grab' }}
+          >
+            {file.thumb || file.filePath ? (
+              <ImageLazy noLook={true} width={100} height={100} src={file.thumb || file.filePath} />
+            ) : null}
+          </div>
+
+          {oneIsCover && index === 0 ? <div className='ZTbox1ImgRowCover'>封面</div> : null}
+
+          <div className='ZTbox1ImgRowIcon'>
+            <EyeOutlined
+              onClick={e =>
+                handleButtonClick(e, () =>
+                  store.dispatch({
+                    type: 'layout/lookBigImg',
+                    payload: {
+                      url: baseURL + file.filePath,
+                      show: true
+                    }
+                  })
+                )
+              }
+              rev={undefined}
+            />
+            <a
+              href={baseURL + file.filePath}
+              download
+              target='_blank'
+              rel='noreferrer'
+              onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+            >
+              <DownloadOutlined rev={undefined} />
+            </a>
+          </div>
+
+          {!isLook && (
+            <MyPopconfirm
+              txtK='删除'
+              onConfirm={() => onDelete(file.id)}
+              Dom={
+                <CloseOutlined
+                  className='ZTbox1ImgRowX'
+                  rev={undefined}
+                  onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+                />
+              }
+            />
+          )}
+        </div>
+      )
+    } else {
+      return (
+        <div
+          ref={setNodeRef}
+          style={style}
+          className={classNames('Z3filesRow', isDragging ? 'dragging' : '')}
+        >
+          {!isLook && (
+            <div className='dragHandle' {...attributes} {...listeners}>
+              <MenuOutlined rev={undefined} />
+            </div>
+          )}
+          <div className='Z3files1' title={file.fileName}>
+            {file.fileName}
+          </div>
+          <div className='Z3files2'>
+            {authFilesLookFu(file.fileName, '') ? (
+              <>
+                <EyeOutlined
+                  rev={undefined}
+                  title='查看'
+                  onClick={e =>
+                    handleButtonClick(e, () => authFilesLookFu(file.fileName, file.filePath))
+                  }
+                />
+                &emsp;
+              </>
+            ) : null}
+            <a
+              title='下载'
+              href={baseURL + file.filePath}
+              download={file.fileName}
+              target='_blank'
+              rel='noreferrer'
+              onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+            >
+              <DownloadOutlined rev={undefined} />
+            </a>
+            &emsp;
+            {!isLook && (
+              <Popconfirm
+                title='删除后无法恢复,是否删除?'
+                okText='删除'
+                cancelText='取消'
+                onConfirm={() => onDelete(file.id)}
+                okButtonProps={{ loading: false }}
+              >
+                <CloseOutlined
+                  rev={undefined}
+                  title='删除'
+                  onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+                />
+              </Popconfirm>
+            )}
+          </div>
+        </div>
+      )
+    }
+  }
+)
+
+// ------------------------------------------
+type Props = {
+  dirCode: string
+  myUrl: string
+  isLook?: boolean
+  fileCheck?: boolean
+  fromData?: any
+  tips?: string
+  size?: number
+  maxCount?: number
+  oneIsCover?: boolean
+  // 默认是修改 files 字段。有可能是下面的附件,使用不同的字段
+  filesKey?: 'files' | 'files2'
+}
+
+// 修复类型检查问题:统一使用dnd-kit管理所有拖拽
+function Z3upFiles({
+  isLook = false,
+  fileCheck = false,
+  dirCode,
+  myUrl,
+  fromData,
+  tips = '单个附件不得超过500M',
+  size = 500,
+  maxCount = 999,
+  oneIsCover,
+  filesKey = 'files'
+}: Props) {
+  const { info, setInfoFu } = useInfo()
+
+  // 修复图片闪动问题:分别管理图片和非图片的排序
+  const { imageFiles, otherFiles } = useMemo(() => {
+    const arr = info[filesKey] || []
+    const imgArr = arr.filter(v => v.type === 'img')
+    const otherArr = arr.filter(v => v.type !== 'img')
+    return { imageFiles: imgArr, otherFiles: otherArr }
+  }, [filesKey, info])
+
+  const fileList = useMemo(() => {
+    return [...imageFiles, ...otherFiles]
+  }, [imageFiles, otherFiles])
+
+  // 修复性能问题:使用useCallback优化函数记忆
+  const setFileList = useCallback(
+    (newFiles: FileListType[] | ((prev: FileListType[]) => FileListType[])) => {
+      setInfoFu((prevInfo: Typetable) => {
+        const currentFiles = prevInfo[filesKey] || []
+        const updatedFiles = typeof newFiles === 'function' ? newFiles(currentFiles) : newFiles
+
+        const updatedInfo: Typetable = {
+          ...prevInfo,
+          [filesKey]: updatedFiles
+        }
+        return updatedInfo
+      })
+    },
+    [filesKey, setInfoFu]
+  )
+
+  const [activeId, setActiveId] = useState<number | null>(null)
+  const myInput = useRef<HTMLInputElement>(null)
+
+  // 修复性能问题:使用useMemo缓存文件ID数组
+  const fileIds = useMemo(() => fileList.map(f => f.id), [fileList])
+
+  // 上传多个文件
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (!e.target.files || e.target.files.length === 0) return
+
+      const files = Array.from(e.target.files)
+
+      if (files.length + fileList.length > maxCount)
+        return MessageFu.warning(
+          `最多可上传${maxCount}个文件,当前选中${files.length}个文件,还可上传${maxCount - fileList.length}个文件`
+        )
+
+      // 逐个上传文件
+      for (const file of files) {
+        if (size && file.size > size * 1024 * 1024) {
+          MessageFu.warning(`文件"${file.name}"超过${size}M限制!`)
+          continue
+        }
+        const typeRes = fileTypeRes(file.name)
+        const fd = new FormData()
+        fd.append('type', typeRes)
+        fd.append('dirCode', dirCode)
+        fd.append('isCompress', 'true')
+        fd.append('isDb', 'true')
+        fd.append('moduleId', info.id + '')
+        fd.append('file', file)
+
+        if (fromData) {
+          for (const k in fromData) {
+            if (fromData[k]) fd.append(k, fromData[k])
+          }
+        }
+
+        e.target.value = ''
+
+        try {
+          const res = await API_upFile(fd, myUrl)
+          if (res.code === 0) {
+            MessageFu.success(
+              `上传成功${files.length > 1 ? `(${files.indexOf(file) + 1}/${files.length})` : ''}`
+            )
+            setFileList(prev => [res.data, ...prev])
+            fileDomInitialFu()
+          } else {
+            fileDomInitialFu()
+          }
+        } catch (error) {
+          fileDomInitialFu()
+        }
+      }
+    },
+    [dirCode, fileList.length, fromData, info.id, maxCount, myUrl, setFileList, size]
+  )
+
+  // 修复拖拽逻辑问题:统一使用dnd-kit管理拖拽
+  const handleDragStart = useCallback((event: DragStartEvent) => {
+    setActiveId(Number(event.active.id))
+  }, [])
+
+  // 修复拖拽交换问题:增加类型检查,只有同类型文件才能交换
+  const handleDragEnd = useCallback(
+    (event: DragEndEvent) => {
+      const { active, over } = event
+      setActiveId(null)
+
+      if (over && active.id !== over.id) {
+        const activeFile = fileList.find(item => item.id === active.id)
+        const overFile = fileList.find(item => item.id === over.id)
+
+        // 修复类型检查问题:只有同类型文件才能交换位置
+        if (activeFile && overFile && activeFile.type === overFile.type) {
+          setFileList(items => {
+            const oldIndex = items.findIndex(item => item.id === active.id)
+            const newIndex = items.findIndex(item => item.id === over.id)
+            return arrayMove(items, oldIndex, newIndex)
+          })
+        }
+      }
+    },
+    [fileList, setFileList]
+  )
+
+  // 列表删除某一个文件
+  const delImgListFu = useCallback(
+    (id: number) => {
+      setFileList(prev => prev.filter(v => v.id !== id))
+    },
+    [setFileList]
+  )
+
+  // 获取当前拖拽的文件
+  const activeFile = useMemo(() => {
+    return activeId ? fileList.find(file => file.id === activeId) : null
+  }, [activeId, fileList])
+
+  // 修复拖拽覆盖层显示问题
+  const renderDragOverlay = useCallback(() => {
+    if (!activeFile) return null
+
+    if (activeFile.type === 'img') {
+      return (
+        <div className={classNames('ZTbox1ImgRow', 'dragOverlay')}>
+          {activeFile.thumb || activeFile.filePath ? (
+            <ImageLazy
+              noLook={true}
+              width={100}
+              height={100}
+              src={activeFile.thumb || activeFile.filePath}
+            />
+          ) : null}
+          <div className='ZTbox1ImgRowIcon' style={{ opacity: 0.5 }}>
+            <EyeOutlined rev={undefined} />
+            <DownloadOutlined rev={undefined} />
+          </div>
+        </div>
+      )
+    } else {
+      return (
+        <div className={classNames('Z3filesRow', 'dragOverlay')}>
+          <div className='dragHandle'>
+            <MenuOutlined rev={undefined} />
+          </div>
+          <div className='Z3files1' title={activeFile.fileName}>
+            {activeFile.fileName}
+          </div>
+          <div className='Z3files2'>{/* 拖拽时隐藏操作按钮 */}</div>
+        </div>
+      )
+    }
+  }, [activeFile])
+
+  return (
+    <div className={styles.Z3upFiles}>
+      <input
+        id='upInput'
+        type='file'
+        ref={myInput}
+        onChange={handeUpPhoto}
+        multiple
+        style={{ display: 'none' }}
+      />
+      <div className='Z3Btn'>
+        {!isLook && (
+          <Button
+            onClick={() => myInput.current?.click()}
+            icon={<UploadOutlined rev={undefined} />}
+          >
+            上传文件
+          </Button>
+        )}
+
+        <div className='Z3files'>
+          <DndContext
+            collisionDetection={closestCenter}
+            onDragStart={handleDragStart}
+            onDragEnd={handleDragEnd}
+          >
+            <SortableContext items={fileIds} strategy={verticalListSortingStrategy}>
+              {fileList.map((file, index) => (
+                <SortableFileItem
+                  key={file.id}
+                  file={file}
+                  onDelete={delImgListFu}
+                  isLook={isLook}
+                  index={index}
+                  oneIsCover={oneIsCover}
+                />
+              ))}
+            </SortableContext>
+            <DragOverlay>{renderDragOverlay()}</DragOverlay>
+          </DndContext>
+        </div>
+
+        <div className='fileTit' hidden={isLook}>
+          {tips}
+          ;支持按住Ctrl键选择多个文件上传;按住鼠标拖动图片 / 拖动附件左侧图标 可调整顺序
+          <div
+            className={classNames(
+              'noUpThumb',
+              fileList.length <= 0 && fileCheck ? 'noUpThumbAc' : ''
+            )}
+          >
+            请上传文件!
+          </div>
+        </div>
+      </div>
+      {isLook && fileList.length <= 0 ? (
+        <div style={{ height: 32, lineHeight: '32px' }}>(空)</div>
+      ) : null}
+    </div>
+  )
+}
+
+export default React.memo(Z3upFiles)

+ 158 - 0
src/components/Z3upFilesRef/index.module.scss

@@ -0,0 +1,158 @@
+.Z3upFilesRef {
+  :global {
+    a {
+      color: black;
+    }
+    .Z3Btn {
+      & > span {
+        font-size: 14px;
+        color: #999;
+      }
+    }
+    .Z3files {
+      .ZTbox1ImgRow {
+        display: inline-block;
+        margin: 15px 20px 10px 0;
+        width: 100px;
+        height: 125px;
+        position: relative;
+        position: relative;
+
+        // 修复图片闪动问题:确保拖拽手柄有固定尺寸
+        .ZTbox1ImgRowDragHandle {
+          width: 100px;
+          height: 100px;
+          cursor: grab;
+
+          &:active {
+            cursor: grabbing;
+          }
+        }
+
+        // 第一张作为封面
+        .ZTbox1ImgRowCover {
+          font-size: 12px;
+          line-height: 22px;
+          position: absolute;
+          left: 0;
+          top: 0;
+          width: 100%;
+          height: 24px;
+          background-color: rgba(0, 0, 0, 0.8);
+          color: #fff;
+          text-align: center;
+          pointer-events: none;
+        }
+
+        .ZTbox1ImgRowIcon {
+          width: 100%;
+          background-color: rgba(0, 0, 0, 0.6);
+          color: #fff;
+          display: flex;
+          justify-content: space-around;
+          font-size: 16px;
+
+          a {
+            color: #fff !important;
+          }
+        }
+
+        .ZTbox1ImgRowX {
+          cursor: pointer;
+          position: absolute;
+          right: -10px;
+          top: -10px;
+          z-index: 99;
+          background-color: rgba(0, 0, 0, 0.8);
+          width: 20px;
+          height: 20px;
+          border-radius: 50%;
+          font-size: 16px;
+          color: #fff;
+          display: flex;
+          justify-content: center;
+          align-items: center;
+        }
+      }
+
+      .Z3filesRow {
+        width: 100%;
+        max-width: 786px;
+        display: flex;
+        align-items: center;
+        padding: 0px 12px;
+        border: 1px solid #d9d9d9;
+        border-radius: 6px;
+        margin: 2px;
+        background: #fff;
+        transition: all 0.3s;
+
+        &:hover {
+          border-color: #40a9ff;
+          box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+        }
+
+        &.dragging {
+          opacity: 0.5;
+          background: #f0f0f0;
+        }
+
+        &.dragOverlay {
+          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+          transform: rotate(5deg);
+        }
+
+        .dragHandle {
+          cursor: grab;
+          margin-right: 12px;
+          color: #999;
+          padding: 4px;
+
+          &:active {
+            cursor: grabbing;
+          }
+
+          &:hover {
+            color: #40a9ff;
+          }
+        }
+
+        .Z3files1 {
+          flex: 1;
+          overflow: hidden;
+          text-overflow: ellipsis;
+          white-space: nowrap;
+        }
+
+        .Z3files2 {
+          display: flex;
+          align-items: center;
+
+          .anticon {
+            cursor: pointer;
+            color: #666;
+
+            &:hover {
+              color: #40a9ff;
+            }
+          }
+        }
+      }
+    }
+
+    .fileTit {
+      margin-top: 5px;
+      color: #666;
+      font-size: 12px;
+
+      .noUpThumb {
+        display: none;
+
+        &.noUpThumbAc {
+          display: block;
+          color: #ff4d4f;
+        }
+      }
+    }
+  }
+}

+ 465 - 0
src/components/Z3upFilesRef/index.tsx

@@ -0,0 +1,465 @@
+import React, { useCallback, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { API_upFile } from '@/store/action/layout'
+import { MessageFu } from '@/utils/message'
+import { fileDomInitialFu } from '@/utils/domShow'
+import { Button, Popconfirm } from 'antd'
+import {
+  UploadOutlined,
+  CloseOutlined,
+  DownloadOutlined,
+  EyeOutlined,
+  MenuOutlined
+} from '@ant-design/icons'
+import classNames from 'classnames'
+import { baseURL } from '@/utils/http'
+import { DndContext, DragEndEvent, DragOverlay, DragStartEvent, closestCenter } from '@dnd-kit/core'
+import {
+  SortableContext,
+  useSortable,
+  verticalListSortingStrategy,
+  arrayMove
+} from '@dnd-kit/sortable'
+import { CSS } from '@dnd-kit/utilities'
+import { fileTypeRes } from '@/utils'
+import ImageLazy from '../ImageLazy'
+import store from '@/store'
+import MyPopconfirm from '../MyPopconfirm'
+import { authFilesLookFu, FileListType } from '../Z3upFiles/data'
+import { forwardRef, useImperativeHandle } from 'react'
+
+// ----------------这个组件用于外面一级的附件上传
+
+interface SortableFileItemProps {
+  file: FileListType
+  onDelete: (id: number) => void
+  isLook: boolean
+  index: number
+  disabled?: boolean
+  oneIsCover?: boolean
+}
+// 修复性能问题:移除不必要的状态和事件处理
+const SortableFileItem = React.memo(
+  ({ file, onDelete, isLook, index, disabled, oneIsCover }: SortableFileItemProps) => {
+    const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
+      id: file.id,
+      disabled: disabled || isLook // 修复拖动禁用问题
+    })
+
+    const style = {
+      transform: CSS.Transform.toString(transform),
+      transition,
+      opacity: isDragging ? 0.5 : 1
+    }
+
+    // 修复按钮点击问题:阻止事件冒泡
+    const handleButtonClick = useCallback((e: React.MouseEvent, callback: () => void) => {
+      e.stopPropagation() // 修复按钮点击无效问题:阻止事件冒泡到拖拽元素
+      callback()
+    }, [])
+
+    if (file.type === 'img') {
+      return (
+        <div
+          ref={setNodeRef}
+          style={style}
+          className={classNames('ZTbox1ImgRow', isDragging ? 'dragging' : '')}
+        >
+          {/* 修复按钮点击无效问题:将拖拽手柄单独放置,不覆盖操作按钮 */}
+          <div
+            className='ZTbox1ImgRowDragHandle'
+            {...attributes}
+            {...listeners}
+            style={{ cursor: isLook ? 'default' : 'grab' }}
+          >
+            {file.thumb || file.filePath ? (
+              <ImageLazy noLook={true} width={100} height={100} src={file.thumb || file.filePath} />
+            ) : null}
+          </div>
+
+          {oneIsCover && index === 0 ? <div className='ZTbox1ImgRowCover'>封面</div> : null}
+
+          <div className='ZTbox1ImgRowIcon'>
+            <EyeOutlined
+              onClick={e =>
+                handleButtonClick(e, () =>
+                  store.dispatch({
+                    type: 'layout/lookBigImg',
+                    payload: {
+                      url: baseURL + file.filePath,
+                      show: true
+                    }
+                  })
+                )
+              }
+              rev={undefined}
+            />
+            <a
+              href={baseURL + file.filePath}
+              download
+              target='_blank'
+              rel='noreferrer'
+              onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+            >
+              <DownloadOutlined rev={undefined} />
+            </a>
+          </div>
+
+          {!isLook && (
+            <MyPopconfirm
+              txtK='删除'
+              onConfirm={() => onDelete(file.id)}
+              Dom={
+                <CloseOutlined
+                  className='ZTbox1ImgRowX'
+                  rev={undefined}
+                  onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+                />
+              }
+            />
+          )}
+        </div>
+      )
+    } else {
+      return (
+        <div
+          ref={setNodeRef}
+          style={style}
+          className={classNames('Z3filesRow', isDragging ? 'dragging' : '')}
+        >
+          {!isLook && (
+            <div className='dragHandle' {...attributes} {...listeners}>
+              <MenuOutlined rev={undefined} />
+            </div>
+          )}
+          <div className='Z3files1' title={file.fileName}>
+            {file.fileName}
+          </div>
+          <div className='Z3files2'>
+            {authFilesLookFu(file.fileName, '') ? (
+              <>
+                <EyeOutlined
+                  rev={undefined}
+                  title='查看'
+                  onClick={e =>
+                    handleButtonClick(e, () => authFilesLookFu(file.fileName, file.filePath))
+                  }
+                />
+                &emsp;
+              </>
+            ) : null}
+            <a
+              title='下载'
+              href={baseURL + file.filePath}
+              download={file.fileName}
+              target='_blank'
+              rel='noreferrer'
+              onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+            >
+              <DownloadOutlined rev={undefined} />
+            </a>
+            &emsp;
+            {!isLook && (
+              <Popconfirm
+                title='删除后无法恢复,是否删除?'
+                okText='删除'
+                cancelText='取消'
+                onConfirm={() => onDelete(file.id)}
+                okButtonProps={{ loading: false }}
+              >
+                <CloseOutlined
+                  rev={undefined}
+                  title='删除'
+                  onClick={e => e.stopPropagation()} // 修复按钮点击无效问题
+                />
+              </Popconfirm>
+            )}
+          </div>
+        </div>
+      )
+    }
+  }
+)
+
+// ------------------------------------------
+type Props = {
+  dirCode: string
+  myUrl: string
+  isLook?: boolean
+  fileCheck?: boolean
+  fromData?: any
+  tips?: string
+  size?: number
+  maxCount?: number
+  oneIsCover?: boolean
+  moduleId: number | ''
+  ref: any
+}
+
+// 修复类型检查问题:统一使用dnd-kit管理所有拖拽
+function Z3upFilesRef(
+  {
+    isLook = false,
+    fileCheck = false,
+    dirCode,
+    myUrl,
+    fromData,
+    tips = '单个附件不得超过500M',
+    size = 500,
+    maxCount = 999,
+    oneIsCover,
+    moduleId
+  }: Props,
+  ref: any
+) {
+  const [list, setList] = useState<FileListType[]>([])
+
+  // 修复图片闪动问题:分别管理图片和非图片的排序
+  const { imageFiles, otherFiles } = useMemo(() => {
+    const arr = list || []
+    const imgArr = arr.filter(v => v.type === 'img')
+    const otherArr = arr.filter(v => v.type !== 'img')
+    return { imageFiles: imgArr, otherFiles: otherArr }
+  }, [list])
+
+  const fileList = useMemo(() => {
+    return [...imageFiles, ...otherFiles]
+  }, [imageFiles, otherFiles])
+
+  const [activeId, setActiveId] = useState<number | null>(null)
+  const myInput = useRef<HTMLInputElement>(null)
+
+  // 修复性能问题:使用useMemo缓存文件ID数组
+  const fileIds = useMemo(() => fileList.map(f => f.id), [fileList])
+
+  // 上传多个文件
+  const handeUpPhoto = useCallback(
+    async (e: React.ChangeEvent<HTMLInputElement>) => {
+      if (!e.target.files || e.target.files.length === 0) return
+
+      const files = Array.from(e.target.files)
+
+      if (files.length + fileList.length > maxCount)
+        return MessageFu.warning(
+          `最多可上传${maxCount}个文件,当前选中${files.length}个文件,还可上传${maxCount - fileList.length}个文件`
+        )
+
+      // 逐个上传文件
+      for (const file of files) {
+        if (size && file.size > size * 1024 * 1024) {
+          MessageFu.warning(`文件"${file.name}"超过${size}M限制!`)
+          continue
+        }
+        const typeRes = fileTypeRes(file.name)
+        const fd = new FormData()
+        fd.append('type', typeRes)
+        fd.append('dirCode', dirCode)
+        fd.append('isCompress', 'true')
+        fd.append('isDb', 'true')
+        if (moduleId) {
+          // 后端要求,这个模块不要传moduleId
+          if (!['cms/order/register/son/upload'].includes(myUrl))
+            fd.append('moduleId', moduleId + '')
+        }
+
+        fd.append('file', file)
+
+        if (fromData) {
+          for (const k in fromData) {
+            if (fromData[k]) fd.append(k, fromData[k])
+          }
+        }
+
+        e.target.value = ''
+
+        try {
+          const res = await API_upFile(fd, myUrl)
+          if (res.code === 0) {
+            MessageFu.success(
+              `上传成功${files.length > 1 ? `(${files.indexOf(file) + 1}/${files.length})` : ''}`
+            )
+            setList(prev => [res.data, ...prev])
+            fileDomInitialFu()
+          } else {
+            fileDomInitialFu()
+          }
+        } catch (error) {
+          fileDomInitialFu()
+        }
+      }
+    },
+    [dirCode, fileList.length, fromData, maxCount, moduleId, myUrl, size]
+  )
+
+  // 修复拖拽逻辑问题:统一使用dnd-kit管理拖拽
+  const handleDragStart = useCallback((event: DragStartEvent) => {
+    setActiveId(Number(event.active.id))
+  }, [])
+
+  // 修复拖拽交换问题:增加类型检查,只有同类型文件才能交换
+  const handleDragEnd = useCallback(
+    (event: DragEndEvent) => {
+      const { active, over } = event
+      setActiveId(null)
+
+      if (over && active.id !== over.id) {
+        const activeFile = fileList.find(item => item.id === active.id)
+        const overFile = fileList.find(item => item.id === over.id)
+
+        // 修复类型检查问题:只有同类型文件才能交换位置
+        if (activeFile && overFile && activeFile.type === overFile.type) {
+          setList(items => {
+            const oldIndex = items.findIndex(item => item.id === active.id)
+            const newIndex = items.findIndex(item => item.id === over.id)
+            return arrayMove(items, oldIndex, newIndex)
+          })
+        }
+      }
+    },
+    [fileList]
+  )
+
+  // 列表删除某一个文件
+  const delImgListFu = useCallback(
+    (id: number) => {
+      setList(prev => prev.filter(v => v.id !== id))
+    },
+    [setList]
+  )
+
+  // 获取当前拖拽的文件
+  const activeFile = useMemo(() => {
+    return activeId ? fileList.find(file => file.id === activeId) : null
+  }, [activeId, fileList])
+
+  // 修复拖拽覆盖层显示问题
+  const renderDragOverlay = useCallback(() => {
+    if (!activeFile) return null
+
+    if (activeFile.type === 'img') {
+      return (
+        <div className={classNames('ZTbox1ImgRow', 'dragOverlay')}>
+          {activeFile.thumb || activeFile.filePath ? (
+            <ImageLazy
+              noLook={true}
+              width={100}
+              height={100}
+              src={activeFile.thumb || activeFile.filePath}
+            />
+          ) : null}
+          <div className='ZTbox1ImgRowIcon' style={{ opacity: 0.5 }}>
+            <EyeOutlined rev={undefined} />
+            <DownloadOutlined rev={undefined} />
+          </div>
+        </div>
+      )
+    } else {
+      return (
+        <div className={classNames('Z3filesRow', 'dragOverlay')}>
+          <div className='dragHandle'>
+            <MenuOutlined rev={undefined} />
+          </div>
+          <div className='Z3files1' title={activeFile.fileName}>
+            {activeFile.fileName}
+          </div>
+          <div className='Z3files2'>{/* 拖拽时隐藏操作按钮 */}</div>
+        </div>
+      )
+    }
+  }, [activeFile])
+
+  // 设置数据
+  const sonSetListFu = useCallback((list: FileListType[]) => {
+    setList(list)
+  }, [])
+
+  // 返回数据
+  const sonResListFu = useCallback(() => {
+    const obj = {
+      list: fileList || [],
+      thumb: '',
+      thumbPc: ''
+    }
+    if (oneIsCover && fileList.length) {
+      const findObj = fileList.find(v => v.type === 'img')
+
+      if (findObj) {
+        obj.thumb = findObj.thumb
+        obj.thumbPc = findObj.filePath
+      }
+    }
+    return obj
+  }, [fileList, oneIsCover])
+
+  // 可以让父组件调用子组件的方法
+  useImperativeHandle(ref, () => ({
+    sonSetListFu,
+    sonResListFu
+  }))
+
+  return (
+    <div className={styles.Z3upFilesRef}>
+      <input
+        id='upInput'
+        type='file'
+        ref={myInput}
+        onChange={handeUpPhoto}
+        multiple
+        style={{ display: 'none' }}
+      />
+      <div className='Z3Btn'>
+        {!isLook && (
+          <>
+            <Button
+              onClick={() => myInput.current?.click()}
+              icon={<UploadOutlined rev={undefined} />}
+            >
+              上传文件
+            </Button>
+            &emsp;{oneIsCover ? <span>第一张图片将用作封面</span> : ''}
+          </>
+        )}
+
+        <div className='Z3files'>
+          <DndContext
+            collisionDetection={closestCenter}
+            onDragStart={handleDragStart}
+            onDragEnd={handleDragEnd}
+          >
+            <SortableContext items={fileIds} strategy={verticalListSortingStrategy}>
+              {fileList.map((file, index) => (
+                <SortableFileItem
+                  key={file.id}
+                  file={file}
+                  onDelete={delImgListFu}
+                  isLook={isLook}
+                  index={index}
+                  oneIsCover={oneIsCover}
+                />
+              ))}
+            </SortableContext>
+            <DragOverlay>{renderDragOverlay()}</DragOverlay>
+          </DndContext>
+        </div>
+
+        <div className='fileTit' hidden={isLook}>
+          {tips}
+          ;支持按住Ctrl键选择多个文件上传;按住鼠标拖动图片 / 拖动附件左侧图标 可调整顺序
+          <div
+            className={classNames(
+              'noUpThumb',
+              fileList.length <= 0 && fileCheck ? 'noUpThumbAc' : ''
+            )}
+          >
+            请上传文件!
+          </div>
+        </div>
+      </div>
+      {isLook && fileList.length <= 0 ? (
+        <div style={{ height: 32, lineHeight: '32px' }}>(空)</div>
+      ) : null}
+    </div>
+  )
+}
+
+export default forwardRef(Z3upFilesRef)

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

+ 42 - 0
src/index.tsx

@@ -0,0 +1,42 @@
+// 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: '#910000',
+        colorText: '#333333'
+      },
+      components: {
+        Button: {
+          primaryColor: '#D9BF76'
+        }
+      }
+    }}
+  >
+    <Provider store={store}>
+      <StyleProvider hashPriority='high' transformers={[legacyLogicalPropertiesTransformer]}>
+        <App />
+      </StyleProvider>
+    </Provider>
+  </ConfigProvider>
+)

+ 133 - 0
src/pages/Abench/A1statistics/index.module.scss

@@ -0,0 +1,133 @@
+.A1statistics {
+  padding: 24px;
+
+  .A1export {
+    position: absolute;
+    top: -70px;
+    left: 130px;
+    z-index: 1;
+  }
+
+  :global {
+    .A1_1 {
+      display: flex;
+      margin-bottom: 15px;
+      & > div {
+        width: 18%;
+        height: 150px;
+        border-radius: 6px;
+        border: 1px solid #ccc;
+        background-color: #fff;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        margin-right: 14px;
+        & > div {
+          padding-left: 28px;
+          display: flex;
+          align-items: center;
+          margin-bottom: 20px;
+          img {
+            margin-right: 8px;
+            width: 36px;
+            height: 36px;
+          }
+          h3 {
+            font-size: 16px;
+          }
+        }
+        & > p {
+          padding-left: 55px;
+          & > span {
+            font-size: 34px;
+            font-weight: 700;
+            color: var(--myAntdColor);
+          }
+          & > i {
+            display: inline-block;
+            margin-left: 15px;
+            & > i {
+              margin-left: 5px;
+              font-weight: 700;
+              font-size: 24px;
+              color: var(--myAntdColor);
+            }
+          }
+        }
+      }
+      .A1_1_3 {
+        width: 520px;
+      }
+      .A1_1_4 {
+        margin-right: 0;
+        width: 29%;
+        & > div {
+          position: relative;
+          .A1_1_4_1 {
+            position: absolute;
+            right: 15px;
+            top: 50%;
+            transform: translateY(-50%);
+          }
+        }
+      }
+    }
+
+    .A1_box {
+      width: 100%;
+      height: calc(100% - 160px);
+      display: flex;
+      & > div {
+        padding: 15px;
+        .A1tit {
+          font-weight: 700;
+          font-size: 20px;
+        }
+      }
+      .A1_2 {
+        width: 65%;
+        background-color: #fff;
+        border: 1px solid #ccc;
+        border-radius: 6px;
+        display: flex;
+        justify-content: space-between;
+        .A1_2row {
+          width: 49%;
+          .A1_2ech {
+            width: 100%;
+            height: calc(100% - 20px);
+          }
+        }
+      }
+      .A1_3 {
+        padding: 0;
+        margin-left: 15px;
+        width: calc(35% - 15px);
+        .A1_3row {
+          padding: 15px;
+          background-color: #fff;
+          border: 1px solid #ccc;
+          border-radius: 6px;
+          height: calc(50% - 9px);
+          margin-bottom: 18px;
+          &:nth-of-type(2) {
+            margin-bottom: 0px;
+          }
+          .A1tit2 {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            & > div {
+              font-weight: 700;
+              font-size: 20px;
+            }
+          }
+          .A1_3ech {
+            width: 100%;
+            height: calc(100% - 30px);
+          }
+        }
+      }
+    }
+  }
+}

+ 718 - 0
src/pages/Abench/A1statistics/index.tsx

@@ -0,0 +1,718 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { Button, Select, TreeSelect } from 'antd'
+import { iconUrl } from '@/utils/http'
+import { useDispatch, useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import { E1_APIgetTree } from '@/store/action/Eculture/E1tag'
+import type { TypeI5Tree } from '@/pages/Isystem/I5organization/data'
+import * as xlsx from 'xlsx'
+import * as echarts from 'echarts'
+import {
+  A1_APIworkCount,
+  A1_APIgetDictById,
+  A1_APIgetDataTextureByTagId,
+  A1_APIgetDataTagByTagId,
+  A1_APIgetTotal,
+  A1_APIgetDataTagLevel
+} from '@/store/action/Abench/A1'
+
+// 生成年度选项:2025 到今年
+const currentYear = new Date().getFullYear()
+const yearOptions = Array.from({ length: currentYear - 2025 + 1 }, (_, i) => {
+  const year = 2025 + i
+  return { value: year, label: `${year}年度` }
+})
+
+const DEFAULT_COLORS = ['#c11b2d', '#243220', '#24664b', '#8acfb2', '#806e4c', '#988364', '#972d00']
+
+// 先写一些静态的
+type DictItem = { name: string; id: number }
+type TotalItem = { pcs: number | null; level: string | null }
+
+function A1statistics() {
+  const [yearCountYear, setYearCountYear] = useState(currentYear)
+  const [yearCount, setYearCount] = useState<number | null>(null)
+
+  // 藏品总数、藏品总数量、定级文物数量
+  const [totalList, setTotalList] = useState<TotalItem[] | null>(null)
+
+  // 材质统计
+  const [textureOptions, setTextureOptions] = useState<DictItem[]>([])
+  const [textureTagId, setTextureTagId] = useState<number | null>(null)
+  const [textureData, setTextureData] = useState<{ name: string; value: number }[]>([])
+
+  // 品类统计(使用 cms/tag/getTree 树状结构)
+  const [categoryTagId, setCategoryTagId] = useState<string | null>(null)
+  const [categoryData, setCategoryData] = useState<{ name: string; value: number }[]>([])
+
+  const dispatch = useDispatch()
+  const categoryTreeData = useSelector((state: RootState) => state.E1tag.treeData)
+
+  // 一级/二级分类
+  const [level1Data, setLevel1Data] = useState<{ name: string; value: number }[]>([])
+  const [level2Data, setLevel2Data] = useState<{ name: string; value: number }[]>([])
+
+  const textureInitialized = useRef(false)
+  const categoryInitialized = useRef(false)
+
+  // 将 TypeI5Tree 转为 TreeSelect 的 treeData 格式
+  const categoryTreeSelectData = useMemo(() => {
+    const convert = (nodes: TypeI5Tree[]): { value: string; label: string; children?: any[] }[] =>
+      nodes.map(node => ({
+        value: node.id,
+        label: node.name,
+        children: node.children?.length ? convert(node.children) : undefined
+      }))
+    return convert(categoryTreeData || [])
+  }, [categoryTreeData])
+
+  // 从树中根据 id 查找 name(用于导出)
+  const getCategoryNameById = useCallback(
+    (id: string | null) => {
+      if (!id) return ''
+      const find = (nodes: TypeI5Tree[]): string | undefined => {
+        for (const n of nodes) {
+          if (n.id === id) return n.name
+          if (n.children?.length) {
+            const found = find(n.children)
+            if (found) return found
+          }
+        }
+        return undefined
+      }
+      return find(categoryTreeData || []) ?? ''
+    },
+    [categoryTreeData]
+  )
+
+  // 获取一级/二级分类数据
+  useEffect(() => {
+    const fetchTagLevel = async () => {
+      try {
+        const res = await A1_APIgetDataTagLevel()
+        if (res?.code === 0 && res.data) {
+          const list1 = (res.data.level_1 || []).map((it: { name: string; count: number }) => ({
+            name: it.name,
+            value: it.count
+          }))
+          const list2 = (res.data.level_2 || []).map((it: { name: string; count: number }) => ({
+            name: it.name,
+            value: it.count
+          }))
+          setLevel1Data(list1)
+          setLevel2Data(list2)
+        } else {
+          setLevel1Data([])
+          setLevel2Data([])
+        }
+      } catch {
+        setLevel1Data([])
+        setLevel2Data([])
+      }
+    }
+    fetchTagLevel()
+  }, [])
+
+  // 获取藏品总数统计
+  useEffect(() => {
+    const fetchTotal = async () => {
+      try {
+        const res = await A1_APIgetTotal()
+        if (res?.code === 0 && Array.isArray(res.data)) {
+          setTotalList(res.data as TotalItem[])
+        } else {
+          setTotalList(null)
+        }
+      } catch {
+        setTotalList(null)
+      }
+    }
+    fetchTotal()
+  }, [])
+
+  // 藏品总数、藏品总数量、定级文物按级别统计
+  const totalStats = totalList
+    ? (() => {
+        const totalCount = totalList.length
+        const totalPcs = totalList.reduce((sum, it) => sum + (it.pcs ?? 0), 0)
+        const withLevel = totalList.filter(it => it.level != null && String(it.level).trim() !== '')
+        const level1 = totalList.filter(it => it.level === '一级').length
+        const level2 = totalList.filter(it => it.level === '二级').length
+        const level3 = totalList.filter(it => it.level === '三级').length
+        const level4 = totalList.filter(it => it.level === '一般').length
+        return {
+          totalCount,
+          totalPcs,
+          leveledCount: withLevel.length,
+          level1,
+          level2,
+          level3,
+          level4
+        }
+      })()
+    : null
+
+  // 获取品类树(cms/tag/getTree)
+  useEffect(() => {
+    dispatch(E1_APIgetTree())
+  }, [dispatch])
+
+  // 品类树加载后默认选中第一个
+  useEffect(() => {
+    if (categoryTreeData?.length > 0 && !categoryInitialized.current) {
+      categoryInitialized.current = true
+      setCategoryTagId(categoryTreeData[0].id)
+    }
+  }, [categoryTreeData])
+
+  // 获取筛选字典(材质)
+  useEffect(() => {
+    const fetchDict = async () => {
+      try {
+        const res = await A1_APIgetDictById()
+        if (res?.code === 0 && res.data) {
+          const list3 = res.data.texture || []
+          setTextureOptions(list3)
+          if (list3.length > 0 && !textureInitialized.current) {
+            textureInitialized.current = true
+            setTextureTagId(list3[0].id)
+          }
+        } else {
+          setTextureOptions([])
+        }
+      } catch {
+        setTextureOptions([])
+      }
+    }
+    fetchDict()
+  }, [])
+
+  // 材质 tagId 变化时获取数据
+  useEffect(() => {
+    if (textureTagId == null) {
+      setTextureData([])
+      return
+    }
+    const fetch = async () => {
+      try {
+        const res = await A1_APIgetDataTextureByTagId(textureTagId)
+        if (
+          res?.code === 0 &&
+          res.data &&
+          typeof res.data === 'object' &&
+          !Array.isArray(res.data)
+        ) {
+          setTextureData(
+            Object.entries(res.data as Record<string, number>).map(([name, value]) => ({
+              name,
+              value
+            }))
+          )
+        } else {
+          setTextureData([])
+        }
+      } catch {
+        setTextureData([])
+      }
+    }
+    fetch()
+  }, [textureTagId])
+
+  // 品类 tagId 变化时获取数据
+  useEffect(() => {
+    if (categoryTagId == null) {
+      setCategoryData([])
+      return
+    }
+    const fetch = async () => {
+      try {
+        const res = await A1_APIgetDataTagByTagId(Number(categoryTagId))
+        if (res?.code === 0 && res.data && Array.isArray(res.data)) {
+          setCategoryData(
+            res.data.map(i => ({
+              name: i.name,
+              value: i.count
+            }))
+          )
+        } else {
+          setCategoryData([])
+        }
+      } catch {
+        setCategoryData([])
+      }
+    }
+    fetch()
+  }, [categoryTagId])
+
+  // 获取年度新增产品数量
+  useEffect(() => {
+    const fetchYearCount = async () => {
+      try {
+        const res = await A1_APIworkCount(yearCountYear)
+        if (res?.code === 0) {
+          const count = typeof res.data === 'number' ? res.data : (res.data?.count ?? 0)
+          setYearCount(count)
+        }
+      } catch {
+        setYearCount(null)
+      }
+    }
+    fetchYearCount()
+  }, [yearCountYear])
+
+  // 生成柱状图
+  const initEchFu = useCallback((data: any[], dom: any) => {
+    if (!dom) return
+
+    const myChart = echarts.getInstanceByDom(dom) || echarts.init(dom)
+
+    // 计算总数用于百分比(total 为 0 时避免 NaN)
+    const total = data.reduce((sum, item) => sum + item.value, 0)
+    const pct = (val: number) => (total > 0 ? ((val / total) * 100).toFixed(1) + '%' : '0%')
+    const dataMax = Math.max(...data.map(d => d.value))
+    const yMax = Math.max(5, dataMax)
+    const yInterval = Math.max(1, Math.ceil(yMax / 5))
+
+    const option: echarts.EChartsOption = {
+      tooltip: {
+        trigger: 'axis',
+        axisPointer: {
+          type: 'shadow'
+        },
+        formatter: (params: any) => {
+          const param = params[0]
+          return `
+                  <div style="font-weight: bold;">${param.name}</div>
+                  <div>数量: ${param.value}</div>
+                  <div>占比: ${pct(param.value)}</div>
+                `
+        },
+        backgroundColor: 'rgba(255, 255, 255, 0.95)',
+        borderColor: '#ddd',
+        textStyle: {
+          color: '#000'
+        }
+      },
+      grid: {
+        left: '5%',
+        right: '5%',
+        // 减小底部距离 - 调整这个值可以让柱子更靠近底部
+        bottom: '5%',
+        top: '5%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category',
+        data: data.map(item => item.name),
+        axisLabel: {
+          interval: 0,
+          rotate: 30,
+          fontSize: 12,
+          color: '#000',
+          formatter: (value: string) => {
+            return value.length > 4 ? value.slice(0, 5) + '...' : value
+          },
+          margin: 15 // 调整标签与柱子的距离
+        },
+        axisLine: {
+          lineStyle: {
+            color: '#999'
+          }
+        },
+        axisTick: {
+          show: true,
+          alignWithLabel: true,
+          length: 4
+        },
+        // 调整X轴位置,使其更靠近柱子底部
+        offset: 10
+      },
+      yAxis: {
+        type: 'value',
+        name: '',
+        nameTextStyle: {
+          fontSize: 14,
+          color: '#000',
+          padding: [0, 0, 0, 10]
+        },
+        axisLabel: {
+          fontSize: 12,
+          color: '#000',
+          formatter: (value: number) => {
+            if (value >= 1000) {
+              return `${value / 1000}k`
+            }
+            return value.toString()
+          }
+        },
+        splitLine: {
+          lineStyle: {
+            type: 'dashed',
+            color: '#e0e0e0'
+          }
+        },
+        max: yMax,
+        min: 0,
+        interval: yInterval
+      },
+      series: [
+        {
+          name: '数量',
+          type: 'bar',
+          data: data.map((item, index) => ({
+            value: item.value,
+            percentage: pct(item.value),
+            // 为每个柱子设置独立的渐变
+            itemStyle: {
+              // 关键修改:使用线性渐变实现单个柱子的渐变效果
+              color: new echarts.graphic.LinearGradient(
+                0,
+                0,
+                0,
+                1, // 0,0表示起点,0,1表示终点(垂直方向渐变)
+                [
+                  { offset: 0, color: '#bb1e2e' }, // 顶部颜色
+                  { offset: 1, color: '#29644a' } // 底部颜色(更浅/白色)
+                ]
+              ),
+              borderRadius: [10, 10, 10, 10],
+              // 添加阴影效果增强立体感
+              shadowColor: 'rgba(0, 0, 0, 0.1)',
+              shadowBlur: 4,
+              shadowOffsetY: 2
+            }
+          })),
+          barWidth: '50%', // 稍微调窄柱子
+          // 调整标签位置和样式
+          label: {
+            show: true,
+            position: 'top',
+            formatter: (params: any) => {
+              const itemData = params.data
+              return `${itemData.value}\n${pct(itemData.value)}`
+            },
+            fontSize: 10,
+            fontWeight: 'bold',
+            color: '#000',
+            lineHeight: 16
+          },
+
+          emphasis: {
+            itemStyle: {
+              shadowColor: 'rgba(0, 0, 0, 0.3)',
+              shadowBlur: 10,
+              shadowOffsetY: 3
+            }
+          }
+        }
+      ]
+    }
+
+    myChart.setOption(option)
+  }, [])
+
+  // 生成饼图
+  const binInitFu = useCallback((data: any[], dom: any) => {
+    if (!dom) return
+
+    const myChart = echarts.getInstanceByDom(dom) || echarts.init(dom)
+    if (!data || data.length === 0) {
+      myChart.clear()
+      myChart.setOption({
+        graphic: [
+          {
+            type: 'text',
+            left: 'center',
+            top: 'center',
+            style: { text: '暂无数据', fontSize: 14, fill: '#999', textAlign: 'center' }
+          }
+        ]
+      })
+      return
+    }
+
+    // 有数据时先清除,避免从「暂无数据」切换后 graphic 残留
+    myChart.clear()
+    // 计算总数用于百分比(total 为 0 时避免 NaN)
+    const total = data.reduce((sum, item) => sum + item.value, 0)
+    const pct = (val: number) => (total > 0 ? ((val / total) * 100).toFixed(1) : '0')
+
+    const option = {
+      color: DEFAULT_COLORS, // 设置颜色方案
+      tooltip: {
+        trigger: 'item',
+        formatter: (params: any) => `${params.name}: ${params.value} (${pct(params.value)}%)`
+      },
+      legend: {
+        type: 'scroll',
+        orient: 'vertical',
+        left: '52%',
+        top: 'center',
+        itemGap: 15,
+        textStyle: {
+          fontSize: 12
+        },
+        formatter: function (name: string) {
+          const item = data.find(d => d.name === name)
+          if (item) {
+            return `${name}  ${item.value}  ${pct(item.value)}%`
+          }
+          return name
+        }
+      },
+      series: [
+        {
+          name: '品类分布',
+          type: 'pie',
+          center: ['25%', '50%'], // 设置饼图中心位置
+          radius: ['50%', '90%'],
+          avoidLabelOverlap: false,
+          itemStyle: {
+            borderRadius: 0,
+            borderColor: '#fff',
+            borderWidth: 2
+          },
+          label: {
+            show: false
+          },
+          labelLine: {
+            show: false
+          },
+          emphasis: {
+            itemStyle: {
+              shadowBlur: 10,
+              shadowOffsetX: 0,
+              shadowColor: 'rgba(0, 0, 0, 0.5)'
+            }
+          },
+          data: data.map(item => ({
+            name: item.name,
+            value: item.value
+          }))
+        }
+      ]
+    }
+    myChart.setOption(option)
+  }, [])
+
+  useEffect(() => {
+    initEchFu(level1Data, document.querySelector('#echBox1'))
+  }, [initEchFu, level1Data])
+
+  useEffect(() => {
+    initEchFu(level2Data, document.querySelector('#echBox2'))
+  }, [initEchFu, level2Data])
+
+  useEffect(() => {
+    binInitFu(textureData, document.querySelector('#echBox3'))
+  }, [binInitFu, textureData])
+
+  useEffect(() => {
+    binInitFu(categoryData, document.querySelector('#echBox4'))
+  }, [binInitFu, categoryData])
+
+  // 数据导出
+  const dataExport = useCallback(() => {
+    const wb = xlsx.utils.book_new()
+
+    // 汇总数据
+    const summaryData = [
+      { 指标: '藏品总数', 数值: totalStats?.totalCount ?? '-', 单位: '件/套' },
+      { 指标: '藏品总数量', 数值: totalStats?.totalPcs ?? '-', 单位: '个' },
+      { 指标: '定级文物数量', 数值: totalStats?.leveledCount ?? '-', 单位: '件/套' },
+      { 指标: '一级文物', 数值: totalStats?.level1 ?? 0, 单位: '件/套' },
+      { 指标: '二级文物', 数值: totalStats?.level2 ?? 0, 单位: '件/套' },
+      { 指标: '三级文物', 数值: totalStats?.level3 ?? 0, 单位: '件/套' },
+      { 指标: '一般文物', 数值: totalStats?.level4 ?? 0, 单位: '件/套' },
+      { 指标: `年度新增产品数量(${yearCountYear}年)`, 数值: yearCount ?? '-', 单位: '件/套' }
+    ]
+    const wsSummary = xlsx.utils.json_to_sheet(summaryData)
+    wsSummary['!cols'] = [{ wpx: 180 }, { wpx: 100 }, { wpx: 80 }]
+    xlsx.utils.book_append_sheet(wb, wsSummary, '汇总')
+
+    // 一级分类
+    const level1SheetData = level1Data.map((it, i) => ({
+      序号: i + 1,
+      分类名称: it.name,
+      数量: it.value
+    }))
+    if (level1SheetData.length) {
+      const ws1 = xlsx.utils.json_to_sheet(level1SheetData)
+      ws1['!cols'] = [{ wpx: 60 }, { wpx: 150 }, { wpx: 80 }]
+      xlsx.utils.book_append_sheet(wb, ws1, '一级分类')
+    }
+
+    // 二级分类
+    const level2SheetData = level2Data.map((it, i) => ({
+      序号: i + 1,
+      分类名称: it.name,
+      数量: it.value
+    }))
+    if (level2SheetData.length) {
+      const ws2 = xlsx.utils.json_to_sheet(level2SheetData)
+      ws2['!cols'] = [{ wpx: 60 }, { wpx: 150 }, { wpx: 80 }]
+      xlsx.utils.book_append_sheet(wb, ws2, '二级分类')
+    }
+
+    // 材质统计(当前筛选)
+    const textureSheetData = textureData.map((it, i) => ({
+      序号: i + 1,
+      材质: it.name,
+      数量: it.value
+    }))
+    if (textureSheetData.length) {
+      const ws3 = xlsx.utils.json_to_sheet(textureSheetData)
+      ws3['!cols'] = [{ wpx: 60 }, { wpx: 150 }, { wpx: 80 }]
+      const tagName = (textureOptions.find(t => t.id === textureTagId)?.name ?? '').replace(
+        /[\\/*?:[\]]/g,
+        ''
+      )
+      xlsx.utils.book_append_sheet(wb, ws3, tagName ? `材质_${tagName.slice(0, 20)}` : '材质统计')
+    }
+
+    // 品类统计(当前筛选)
+    const categorySheetData = categoryData.map((it, i) => ({
+      序号: i + 1,
+      品类: it.name,
+      数量: it.value
+    }))
+    if (categorySheetData.length) {
+      const ws4 = xlsx.utils.json_to_sheet(categorySheetData)
+      ws4['!cols'] = [{ wpx: 60 }, { wpx: 150 }, { wpx: 80 }]
+      const tagName = getCategoryNameById(categoryTagId).replace(/[\\/*?:[\]]/g, '')
+      xlsx.utils.book_append_sheet(wb, ws4, tagName ? `品类_${tagName.slice(0, 20)}` : '品类统计')
+    }
+
+    const fileName = `数据统计_${new Date().toISOString().slice(0, 10)}.xlsx`
+    xlsx.writeFile(wb, fileName)
+  }, [
+    totalStats,
+    yearCount,
+    yearCountYear,
+    level1Data,
+    level2Data,
+    textureData,
+    textureTagId,
+    textureOptions,
+    categoryData,
+    categoryTagId,
+    getCategoryNameById
+  ])
+
+  return (
+    <div className={styles.A1statistics}>
+      <div className={styles.A1header}>
+        <div className='pageTitle'>数据统计</div>
+        <Button className={styles.A1export} type='primary' onClick={dataExport}>
+          数据导出
+        </Button>
+      </div>
+
+      <div className='A1_1'>
+        <div>
+          <div>
+            <img src={iconUrl + '/a11.png'} alt='' />
+            <h3>藏品总数</h3>
+          </div>
+          <p>
+            <span>{totalStats?.totalCount ?? 'empty'}</span>(件/套)
+          </p>
+        </div>
+        <div>
+          <div>
+            <img src={iconUrl + '/a22.png'} alt='' />
+            <h3>藏品总数量</h3>
+          </div>
+          <p>
+            <span>{totalStats?.totalPcs ?? 'empty'}</span>(个)
+          </p>
+        </div>
+
+        <div className='A1_1_3'>
+          <div>
+            <img src={iconUrl + '/a33.png'} alt='' />
+            <h3>定级文物数量</h3>
+          </div>
+          <p>
+            <span>{totalStats?.leveledCount ?? 'empty'}</span>(件/套)
+            <i>
+              一级<i>{totalStats?.level1 ?? 0}</i>
+            </i>
+            <i>
+              二级<i>{totalStats?.level2 ?? 0}</i>
+            </i>
+            <i>
+              三级<i>{totalStats?.level3 ?? 0}</i>
+            </i>
+            <i>
+              一般<i>{totalStats?.level4 ?? 0}</i>
+            </i>
+          </p>
+        </div>
+
+        <div className='A1_1_4'>
+          <div>
+            <img src={iconUrl + '/a44.png'} alt='' />
+            <h3>年度新增产品数量</h3>
+            <div className='A1_1_4_1'>
+              <Select value={yearCountYear} onChange={setYearCountYear} options={yearOptions} />
+            </div>
+          </div>
+          <p>
+            <span>{yearCount ?? 'empty'}</span>(件/套)
+          </p>
+        </div>
+      </div>
+
+      <div className='A1_box'>
+        <div className='A1_2'>
+          <div className='A1_2row'>
+            <div className='A1tit'>一级分类</div>
+            <div className='A1_2ech' id='echBox1'></div>
+          </div>
+          <div className='A1_2row'>
+            <div className='A1tit'>二级分类</div>
+            <div className='A1_2ech' id='echBox2'></div>
+          </div>
+        </div>
+        <div className='A1_3'>
+          <div className='A1_3row'>
+            <div className='A1tit2'>
+              <div>材质统计</div>
+              <Select
+                value={textureTagId ?? undefined}
+                onChange={v => setTextureTagId(v ?? null)}
+                options={textureOptions.map(({ id, name }) => ({ value: id, label: name }))}
+                placeholder='请选择'
+                allowClear
+                style={{ width: 200 }}
+              />
+            </div>
+            <div className='A1_3ech' id='echBox3'></div>
+          </div>
+
+          <div className='A1_3row'>
+            <div className='A1tit2'>
+              <div>品类统计</div>
+              <TreeSelect
+                value={categoryTagId ?? undefined}
+                onChange={v => setCategoryTagId(v ?? null)}
+                treeData={categoryTreeSelectData}
+                placeholder='请选择'
+                allowClear
+                style={{ width: 200 }}
+                treeDefaultExpandAll
+              />
+            </div>
+            <div className='A1_3ech' id='echBox4'></div>
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+const MemoA1statistics = React.memo(A1statistics)
+
+export default MemoA1statistics

+ 35 - 0
src/pages/Abench/A2bench/A2editUser/index.module.scss

@@ -0,0 +1,35 @@
+.A2editUser {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+    .ant-modal {
+      width: 800px !important;
+    }
+    .A2Emain {
+      border-top: 1px solid #999999;
+      padding-top: 15px;
+      width: 100%;
+
+      // 封面
+      .formRow {
+        display: flex;
+        margin-bottom: 24px;
+        .formLeft {
+          width: 100px;
+          text-align: right;
+          position: relative;
+          top: 4px;
+          & > span {
+            color: #ff4d4f;
+          }
+        }
+      }
+
+      .A2Ebtn {
+        margin-top: 20px;
+        text-align: center;
+      }
+    }
+  }
+}

+ 120 - 0
src/pages/Abench/A2bench/A2editUser/index.tsx

@@ -0,0 +1,120 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { Button, Input, Modal } from 'antd'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import { MessageFu } from '@/utils/message'
+import { UserTableListType } from '@/pages/Isystem/I7user/data'
+import { A2_APIeditUser } from '@/store/action/Abench/A2bench'
+import ZupOne from '@/components/ZupOne'
+
+type Props = {
+  closeFu: () => void
+  userInfo: UserTableListType
+  succFu: (obj: any) => void
+}
+
+function A2editUser({ closeFu, userInfo, succFu }: Props) {
+  const [realName, setRealName] = useState('')
+
+  // 头像的ref
+  const ZupThumbRef = useRef<any>(null)
+
+  useEffect(() => {
+    setRealName(userInfo.realName)
+    // 设置头像
+    ZupThumbRef.current?.setFileComFileFu({
+      fileName: '',
+      filePath: userInfo.thumb,
+      thumb: userInfo.thumb
+    })
+  }, [userInfo])
+
+  const [fileCheck, setFileCheck] = useState(false)
+
+  const btnOk = useCallback(async () => {
+    setFileCheck(true)
+
+    // 头像
+    const coverUrl1 = ZupThumbRef.current?.fileComFileResFu()
+
+    if (!coverUrl1.thumb) return
+
+    const obj = {
+      id: userInfo.id,
+      realName,
+      thumb: coverUrl1.thumb
+    }
+
+    const res = await A2_APIeditUser(obj)
+
+    if (res.code === 0) {
+      MessageFu.success('编辑成功')
+      succFu(obj)
+      closeFu()
+    }
+  }, [closeFu, realName, succFu, userInfo.id])
+
+  return (
+    <Modal
+      wrapClassName={styles.A2editUser}
+      destroyOnClose
+      open={true}
+      title='个人资料'
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className='A2Emain'>
+        <div className='formRow'>
+          <div className='formLeft'>
+            <span> * </span>真实姓名:
+          </div>
+          <div className='formRight'>
+            <Input
+              style={{ width: 300 }}
+              maxLength={10}
+              showCount
+              placeholder='请输入内容'
+              value={realName}
+              onChange={e => setRealName(e.target.value.replace(/\s+/g, ''))}
+            />
+          </div>
+        </div>
+
+        {/* 封面 */}
+        <div className='formRow'>
+          <div className='formLeft'>
+            <span> * </span> 头像:
+          </div>
+          <div className='formRight'>
+            <ZupOne
+              ref={ZupThumbRef}
+              isLook={false}
+              fileCheck={fileCheck}
+              dirCode='businessEditUser'
+              myUrl='sys/user/upload'
+              format={['image/jpeg', 'image/png']}
+              formatTxt='png、jpg和jpeg'
+              checkTxt='请上传头像!'
+              upTxt='最多1张'
+              myType='thumb'
+              isTouXiang={true}
+            />
+          </div>
+        </div>
+
+        <div className='A2Ebtn'>
+          <Button type='primary' onClick={btnOk}>
+            提交
+          </Button>
+          &emsp;
+          <MyPopconfirm txtK='取消' onConfirm={closeFu} />
+        </div>
+      </div>
+    </Modal>
+  )
+}
+
+const MemoA2editUser = React.memo(A2editUser)
+
+export default MemoA2editUser

+ 59 - 0
src/pages/Abench/A2bench/A2setStock/index.module.scss

@@ -0,0 +1,59 @@
+.A2setStock {
+  :global {
+    .ant-modal-close {
+      display: none;
+    }
+    .ant-modal {
+      width: 800px !important;
+    }
+    .ant-modal-body {
+      border-top: 1px solid #ccc;
+      .A2Smain {
+        margin-top: 20px;
+
+        .Z5eboxrr {
+          width: 100%;
+          .Z5eRow {
+            margin-bottom: 10px;
+            display: flex;
+
+            .Z5eRow1 {
+              width: 70px;
+              font-weight: 700;
+              text-align: right;
+              margin-right: 10px;
+            }
+
+            .Z5eRow2 {
+              width: calc(100% - 80px);
+            }
+          }
+
+          .Z5eErr {
+            margin-bottom: 20px;
+            text-align: center;
+            color: #ff4d4f;
+            opacity: 0;
+            pointer-events: none;
+            transition: all 0.3s;
+            position: relative;
+            top: -10px;
+          }
+
+          .Z5eErrAc {
+            opacity: 1;
+            top: 0;
+          }
+          .Z5eboxrr_1 {
+            margin-top: 5px;
+          }
+        }
+
+        .A2Sbtn {
+          margin-top: 20px;
+          text-align: center;
+        }
+      }
+    }
+  }
+}

+ 117 - 0
src/pages/Abench/A2bench/A2setStock/index.tsx

@@ -0,0 +1,117 @@
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
+import styles from './index.module.scss'
+import { Button, Checkbox, Modal } from 'antd'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import classNmaes from 'classnames'
+import { MessageFu } from '@/utils/message'
+import { A2_APIsetStock } from '@/store/action/Abench/A2bench'
+import { RouterType } from '@/pages/Layout/data'
+
+type Props = {
+  sId: number
+  arr: RouterType
+  closeFu: () => void
+  succFu: () => void
+}
+
+function A2setStock({ arr, closeFu, succFu, sId }: Props) {
+  const [roleArr, setRoleArr] = useState<RouterType>([])
+
+  useEffect(() => {
+    setRoleArr(arr)
+  }, [arr])
+
+  // 多选框变化
+  const onChange = useCallback(
+    (val: boolean, id1: number, id2: number) => {
+      setRoleArr(
+        roleArr.map(v => ({
+          ...v,
+          son:
+            v.id === id1
+              ? v.son.map(c => ({
+                  ...c,
+                  authority: c.id === id2 ? val : c.authority
+                }))
+              : v.son
+        }))
+      )
+    },
+    [roleArr]
+  )
+
+  // 二级选中的数组id集合
+  const checkIds = useMemo(() => {
+    const arr: number[] = []
+    roleArr.forEach(v => {
+      v.son.forEach(c => {
+        if (c.authority) arr.push(c.id)
+      })
+    })
+    return arr
+  }, [roleArr])
+
+  // 点击提交
+  const btnOk = useCallback(async () => {
+    const res = await A2_APIsetStock(sId, JSON.stringify(roleArr))
+    if (res.code === 0) {
+      MessageFu.success('设置成功')
+      succFu()
+      closeFu()
+    }
+  }, [closeFu, roleArr, sId, succFu])
+
+  return (
+    <Modal
+      wrapClassName={styles.A2setStock}
+      open={true}
+      title='常用功能'
+      footer={
+        [] // 设置footer为空,去掉 取消 确定默认按钮
+      }
+    >
+      <div className='A2Smain'>
+        <div className='Z5eboxrr'>
+          {roleArr.map(v => (
+            <div
+              key={v.id}
+              className='Z5eRow'
+              hidden={v.son.length === 0 || (v.son.length === 1 && v.son[0].name === '工作台')}
+            >
+              <div className='Z5eRow1'>{v.name}</div>
+              <div className='Z5eRow2'>
+                {v.son.map(c =>
+                  c.name === '工作台' ? null : (
+                    <Checkbox
+                      key={c.id}
+                      checked={c.authority}
+                      onChange={e => onChange(e.target.checked, v.id, c.id)}
+                    >
+                      {c.name}
+                    </Checkbox>
+                  )
+                )}
+              </div>
+            </div>
+          ))}
+
+          <div className={classNmaes('Z5eErr', checkIds.length <= 0 ? 'Z5eErrAc' : '')}>
+            至少选中一个
+          </div>
+        </div>
+
+        <div className='A2Sbtn'>
+          <Button type='primary' disabled={checkIds.length <= 0} onClick={btnOk}>
+            提交
+          </Button>
+          &emsp;
+          <MyPopconfirm txtK='取消' onConfirm={closeFu} />
+        </div>
+      </div>
+    </Modal>
+  )
+}
+
+const MemoA2setStock = React.memo(A2setStock)
+
+export default MemoA2setStock

+ 50 - 0
src/pages/Abench/A2bench/A2table/index.module.scss

@@ -0,0 +1,50 @@
+.A2table {
+  width: 100%;
+  padding: 20px;
+  background-color: #fff;
+  border-radius: 10px;
+  margin-top: 20px;
+
+  :global {
+    .A2Ttop {
+      display: flex;
+      justify-content: space-between;
+      margin-bottom: 15px;
+
+      .A2TtopLL {
+        display: flex;
+        align-items: center;
+        .ant-btn {
+          margin-right: 15px;
+        }
+        & > div {
+          font-weight: 700;
+          font-size: 18px;
+          position: relative;
+          padding-left: 16px;
+          margin-right: 20px;
+          &::before {
+            content: '';
+            position: absolute;
+            top: 48%;
+            left: 0;
+            transform: translateY(-50%);
+            width: 5px;
+            height: 20px;
+            background-color: var(--themeColor);
+          }
+        }
+      }
+
+      .A2TtopRR {
+        display: flex;
+        & > div {
+          margin-left: 15px;
+        }
+      }
+    }
+    .ant-table-cell {
+      padding: 8px !important;
+    }
+  }
+}

+ 237 - 0
src/pages/Abench/A2bench/A2table/index.tsx

@@ -0,0 +1,237 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import { Button, Cascader, DatePicker, Input, Select } from 'antd'
+import tabLeftArr, { RouterTypeRow } from '@/pages/Layout/data'
+import MyTable from '@/components/MyTable'
+import { MessageFu } from '@/utils/message'
+import { A2_APIgetList } from '@/store/action/Abench/A2bench'
+import { GI5tableType } from '@/pages/ZgoodsInfo/data'
+import { authorityFu, tableListAuditBtnFu } from '@/utils/authority'
+import history from '@/utils/history'
+import { selectObj } from '@/utils/dataChange'
+import { GI5tableC } from '@/utils/tableData'
+import { useSelector } from 'react-redux'
+import store, { RootState } from '@/store'
+import { dictByProResFu } from '@/pages/Isystem/I2dict/data'
+
+const { RangePicker } = DatePicker
+
+const btnArr = [
+  { name: '我发起的', key: '1' },
+  { name: '待我审批', key: '2' }
+]
+
+function A2table() {
+  // 路由信息过滤过来
+  const typePageArr = useMemo(() => {
+    const arr: RouterTypeRow[] = []
+    tabLeftArr.forEach(v1 => {
+      v1.son.forEach(v2 => {
+        if (v2.pageType) arr.push(v2)
+      })
+    })
+    return arr
+  }, [])
+
+  const [formData, setFormData] = useState({} as any)
+
+  // 回调页面的时候 回显参数
+  const A2formData = useSelector((state: RootState) => state.ZformData.A2formData)
+
+  useEffect(() => {
+    setFormData({ ...A2formData })
+  }, [A2formData])
+
+  const getListFu = useCallback(async () => {
+    if (formData.pageNum) {
+      const obj = { ...formData }
+      if (obj.typeDictId && obj.typeDictId.length)
+        obj.typeDictId = obj.typeDictId[obj.typeDictId.length - 1]
+      const res = await A2_APIgetList(obj)
+      if (res.code === 0) {
+        setTableObj({ list: res.data.records || [], total: res.data.total })
+      }
+    }
+  }, [formData])
+
+  useEffect(() => {
+    getListFu()
+  }, [getListFu])
+
+  // 时间选择器改变
+  const timeChange = useCallback(
+    (date: any, dateString: any) => {
+      let startTime = ''
+      let endTime = ''
+      if (dateString[0] && dateString[1]) {
+        startTime = dateString[0] + ' 00:00:00'
+        endTime = dateString[1] + ' 23:59:59'
+      }
+      setFormData({ ...formData, startTime, endTime, pageNum: 1 })
+    },
+    [formData]
+  )
+
+  // 页码变化
+  const paginationChange = useCallback(
+    (pageNum: number, pageSize: number) => {
+      setFormData({ ...formData, pageNum, pageSize })
+    },
+    [formData]
+  )
+
+  // 输入框的输入
+  const timeRef = useRef(-1)
+  const txtChangeFu = useCallback(
+    (e: React.ChangeEvent<HTMLInputElement>, key: 'num') => {
+      clearTimeout(timeRef.current)
+      timeRef.current = window.setTimeout(() => {
+        setFormData({
+          ...formData,
+          [key]: e.target.value.replaceAll("'", ''),
+          pageNum: 1
+        })
+      }, 500)
+    },
+    [formData]
+  )
+
+  const [tableObj, setTableObj] = useState<{ list: GI5tableType[]; total: number }>({
+    list: [],
+    total: 0
+  })
+
+  const btnFu = useCallback(
+    (pageKey: '3' | '4', obj: any, id: number) => {
+      if (obj) {
+        store.dispatch({ type: 'ZF/A2formData', payload: formData })
+        authorityFu(obj.id, obj.name, () => history.push(`${obj.path}_edit/${pageKey}/${id}`))
+      } else MessageFu.warning('业务类型错误')
+    },
+    [formData]
+  )
+
+  const tableLastBtn = useMemo(() => {
+    return [
+      {
+        title: '操作',
+        render: (item: any) => {
+          const obj = typePageArr.find(v => v.pageType === item.type)
+          return (
+            <>
+              <Button size='small' type='text' onClick={() => btnFu('4', obj, item.id)}>
+                查看
+              </Button>
+
+              {tableListAuditBtnFu(item) ? (
+                <Button size='small' type='text' onClick={() => btnFu('3', obj, item.id)}>
+                  审批
+                </Button>
+              ) : null}
+            </>
+          )
+        }
+      }
+    ]
+  }, [btnFu, typePageArr])
+
+  const staBtn = useMemo(() => {
+    return [
+      {
+        title: '业务类型',
+        render: (item: GI5tableType) => {
+          let txt = '(空)'
+          const obj = typePageArr.find(v => v.pageType === item.type)
+          if (obj) txt = obj.name
+          return txt
+        }
+      }
+    ]
+  }, [typePageArr])
+
+  return (
+    <div className={styles.A2table}>
+      <div className='A2Ttop'>
+        <div className='A2TtopLL'>
+          <div>申请管理</div>
+          {btnArr.map(v => (
+            <Button
+              onClick={() => setFormData({ ...formData, auditType: v.key, pageNum: 1 })}
+              key={v.key}
+              type={formData.auditType === v.key ? 'primary' : 'default'}
+            >
+              {v.name}
+            </Button>
+          ))}
+        </div>
+        <div className='A2TtopRR'>
+          <div>
+            <Cascader
+              changeOnSelect
+              allowClear={true}
+              placeholder='业务类型'
+              style={{ width: 200 }}
+              value={formData.typeDictId || null}
+              fieldNames={{ label: 'name', value: 'id', children: 'children' }}
+              onChange={e => setFormData({ ...formData, typeDictId: e })}
+              options={dictByProResFu()}
+            />
+
+            {/* <Select
+              allowClear={true}
+              placeholder='业务类型'
+              style={{ width: 150 }}
+              value={formData.type ? formData.type : null}
+              onChange={e => setFormData({ ...formData, type: e, pageNum: 1 })}
+              options={typePageArr.map(v => ({ value: v.pageType, label: v.name }))}
+            /> */}
+          </div>
+
+          <div>
+            <Input
+              style={{ width: 200 }}
+              placeholder='请输入业务编号'
+              maxLength={30}
+              onChange={e => txtChangeFu(e, 'num')}
+            />
+          </div>
+
+          <div>
+            <RangePicker
+              style={{ width: 260 }}
+              onChange={timeChange}
+              placeholder={['发起日期开始', '发起日期结束']}
+            />
+          </div>
+
+          <div>
+            <Select
+              allowClear={true}
+              placeholder='申请状态'
+              style={{ width: 150 }}
+              value={formData.status ? formData.status : null}
+              onChange={e => setFormData({ ...formData, status: e, pageNum: 1 })}
+              options={selectObj['订单审批状态']}
+            />
+          </div>
+        </div>
+      </div>
+
+      {/* 表格 */}
+      <MyTable
+        list={tableObj.list}
+        columnsTemp={GI5tableC(true)}
+        lastBtn={tableLastBtn}
+        staBtn={staBtn}
+        pageNum={formData.pageNum}
+        pageSize={formData.pageSize}
+        total={tableObj.total}
+        onChange={(pageNum, pageSize) => paginationChange(pageNum, pageSize)}
+      />
+    </div>
+  )
+}
+
+const MemoA2table = React.memo(A2table)
+
+export default MemoA2table

+ 123 - 0
src/pages/Abench/A2bench/index.module.scss

@@ -0,0 +1,123 @@
+.A2bench {
+  display: flex;
+  justify-content: space-between;
+  font-size: 16px;
+  :global {
+    .A2tit {
+      height: 52px;
+      font-weight: 700;
+      font-size: 18px;
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      margin-bottom: 10px;
+      position: relative;
+      padding-left: 16px;
+      &::before {
+        content: '';
+        position: absolute;
+        top: 48%;
+        left: 0;
+        transform: translateY(-50%);
+        width: 5px;
+        height: 20px;
+        background-color: var(--themeColor);
+      }
+    }
+    .A2none {
+      width: 100%;
+      height: 70px;
+      font-weight: 700;
+      font-size: 18px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+    }
+
+    .A2ll {
+      width: 100%;
+      height: 100%;
+      overflow-y: auto;
+
+      .A2ll1 {
+        height: 100px;
+        background-color: #fff;
+        border-radius: 10px;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 0 24px;
+        .A2ll1_1 {
+          display: flex;
+          img {
+            width: 70px;
+            height: 70px;
+            border-radius: 50%;
+          }
+          .A2ll1_1Txt {
+            margin-left: 30px;
+            & > h1 {
+              font-size: 18px;
+            }
+            & > p {
+              margin-top: 20px;
+            }
+          }
+        }
+      }
+
+      .A2ll2 {
+        margin-top: 15px;
+        display: flex;
+        justify-content: space-between;
+
+        .A2ll2_1 {
+          width: 100%;
+          padding: 10px 20px 0px;
+          background-color: #fff;
+          border-radius: 10px;
+        }
+        // .A2ll2_2 {
+        //   width: 500px;
+        //   margin-left: 20px;
+        //   padding: 10px 20px 0px;
+        //   background-color: #fff;
+        //   border-radius: 10px;
+        // }
+
+        .A2ll2RowBox {
+          display: flex;
+          flex-wrap: wrap;
+          align-items: center;
+          .A2ll2Row {
+            cursor: pointer;
+            transition: all 0.3s;
+            margin-right: 20px;
+            margin-bottom: 20px;
+            height: 90px;
+            width: 100px;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            & > img {
+              width: 40px;
+              height: auto;
+              transition: all 0.3s;
+            }
+            & > p {
+              font-size: 14px;
+              margin-top: 10px;
+            }
+            &:hover {
+              color: var(--themeColor);
+              & > img {
+                transform: scale(1.1);
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}

+ 170 - 0
src/pages/Abench/A2bench/index.tsx

@@ -0,0 +1,170 @@
+import React, { useCallback, useEffect, useState } from 'react'
+import styles from './index.module.scss'
+import { useSelector } from 'react-redux'
+import store, { RootState } from '@/store'
+import { Button } from 'antd'
+import { baseURL, iconUrl } from '@/utils/http'
+import baseTouXiangImg from '@/assets/img/user.png'
+import { getTokenInfo, removeTokenInfo, setTokenInfo } from '@/utils/storage'
+import MyPopconfirm from '@/components/MyPopconfirm'
+import history from '@/utils/history'
+import { getUserInfoByIdAPI } from '@/store/action/Isystem/I7user'
+import { RouterType } from '@/pages/Layout/data'
+import A2table from './A2table'
+import A2setStock from './A2setStock'
+import A2editUser from './A2editUser'
+
+const userInfoJting = getTokenInfo().user || {}
+
+function A2bench() {
+  // 设置常用功能
+  const userRolePermissions = useSelector((state: RootState) => state.A0Layout.userRolePermissions)
+  const [stockShow, setStockShow] = useState(false)
+  const [stockList, setStockList] = useState<RouterType>([])
+
+  const getUserInfo = useCallback(async () => {
+    const res = await getUserInfoByIdAPI(userInfoJting.id)
+    if (res.code === 0) {
+      store.dispatch({ type: 'layout/userInfo', payload: res.data })
+      // 设置常用功能
+      if (res.data.permRtf) {
+        setStockList(JSON.parse(res.data.permRtf || '[]'))
+      } else setStockList(userRolePermissions)
+    }
+  }, [userRolePermissions])
+
+  useEffect(() => {
+    getUserInfo()
+  }, [getUserInfo])
+
+  // 获取组织管理列表
+  // const dispatch = useDispatch()
+  // useEffect(() => {
+  //   dispatch(I5_APIgetTree())
+  // }, [dispatch])
+
+  // const { treeData } = useSelector((state: RootState) => state.I5organization)
+
+  const { userInfo } = useSelector((state: RootState) => state.A0Layout)
+
+  // 点击退出登录
+  const loginExit = () => {
+    removeTokenInfo()
+    history.push('/login')
+  }
+
+  // 点击编辑个人资料
+  const [editUser, setEditUser] = useState(false)
+
+  // 更新用户信息
+  const upUserInfoFu = useCallback(
+    (obj: any) => {
+      const userInfoOld = getTokenInfo()
+      setTokenInfo({
+        ...userInfoOld,
+        user: {
+          ...userInfoOld.user,
+          thumb: obj.thumb,
+          realName: obj.realName
+        }
+      })
+      getUserInfo()
+    },
+    [getUserInfo]
+  )
+
+  return (
+    <div className={styles.A2bench}>
+      <div className='pageTitle'>工作台</div>
+      <div className='A2ll'>
+        {/* 顶部盒子 */}
+        <div className='A2ll1'>
+          <div className='A2ll1_1'>
+            <img src={userInfo.thumb ? baseURL + userInfo.thumb : baseTouXiangImg} alt='' />
+            <div className='A2ll1_1Txt'>
+              <h1>{userInfo.realName || userInfo.userName || '匿名'}</h1>
+              <p>
+                {userInfo.userName}
+                {userInfo.deptName ? <>&emsp;{userInfo.deptName}</> : null}
+                &emsp;|&emsp;{userInfo.roleName}
+              </p>
+            </div>
+          </div>
+          <div className='A2ll1_2'>
+            <Button onClick={() => setEditUser(true)}>编辑个人资料</Button>&emsp;
+            <Button onClick={() => store.dispatch({ type: 'layout/passEditShow', payload: true })}>
+              修改密码
+            </Button>
+            &emsp;
+            <MyPopconfirm
+              txtK='退出登录'
+              onConfirm={loginExit}
+              Dom={<Button>退出登录</Button>}
+              loc='bottom'
+            />
+          </div>
+        </div>
+        <div className='A2ll2'>
+          {/* 常用功能*/}
+          <div className='A2ll2_1'>
+            <div className='A2tit'>
+              <div>常用功能</div>
+              <div>
+                <Button onClick={() => setStockShow(true)}>设置</Button>
+              </div>
+            </div>
+            <div className='A2ll2RowBox'>
+              {stockList.map(v1 =>
+                v1.son.map(v2 =>
+                  v2.authority && v2.name !== '工作台' ? (
+                    <div key={v2.id} className='A2ll2Row' onClick={() => history.push(v2.path)}>
+                      <img src={`${iconUrl}/${v2.name}.png`} alt='' />
+                      <p>{v2.name}</p>
+                    </div>
+                  ) : null
+                )
+              )}
+            </div>
+          </div>
+          {/* 最近使用 */}
+          {/* <div className='A2ll2_2'>
+            <div className='A2tit'>最近使用</div>
+            <div className='A2ll2RowBox'>
+              {changGetFu().length ? (
+                changGetFu().map((v, i) => (
+                  <div key={i} className='A2ll2Row' onClick={() => history.push(v.path)}>
+                    <img src={`${baseURL}/baseData/tabImg/${v.name}.png`} alt='' />
+                    <p>{v.name}</p>
+                  </div>
+                ))
+              ) : (
+                <div className='A2none'>暂无信息</div>
+              )}
+            </div>
+          </div> */}
+        </div>
+        {/* 表格 */}
+        <A2table />
+      </div>
+
+      {/* 编辑个人资料 */}
+      {editUser ? (
+        <A2editUser closeFu={() => setEditUser(false)} userInfo={userInfo} succFu={upUserInfoFu} />
+      ) : null}
+
+      {/* 设置常用功能 */}
+      {stockShow ? (
+        <A2setStock
+          sId={userInfoJting.id}
+          arr={stockList}
+          closeFu={() => setStockShow(false)}
+          succFu={getUserInfo}
+        />
+      ) : null}
+    </div>
+  )
+}
+
+const MemoA2bench = React.memo(A2bench)
+
+export default MemoA2bench

+ 10 - 0
src/pages/Abench/A3flow/index.module.scss

@@ -0,0 +1,10 @@
+.A3flow {
+  :global {
+    .TableListToprr {
+      align-items: flex-start;
+    }
+    .ant-table-cell {
+      padding: 8px !important;
+    }
+  }
+}

+ 266 - 0
src/pages/Abench/A3flow/index.tsx

@@ -0,0 +1,266 @@
+import React, { useMemo, useState, useEffect, useCallback } from 'react'
+import styles from './index.module.scss'
+import { selectObj } from '@/utils/dataChange'
+import history from '@/utils/history'
+import { MessageFu } from '@/utils/message'
+import { authorityFu, tableListAuditBtnFu } from '@/utils/authority'
+import TableList from '@/pages/Zother/TableList'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import { API_getGoodsList, API_getGoodsListAll } from '@/store/action/Abench/A3flow'
+import { baseFormData } from '@/pages/Zother/data'
+import { flowCenterTableC } from '@/utils/tableData'
+import { Button, Modal, Radio, Checkbox } from 'antd'
+import { exportExcelFile } from '@/utils/xlsxExport'
+import tabLeftArr from '@/pages/Layout/data'
+import { GI5tableType } from '@/pages/ZgoodsInfo/data'
+import { dictByProResFu } from '@/pages/Isystem/I2dict/data'
+
+const ExportModal = ({
+  open,
+  availableYears,
+  onConfirm,
+  onCancel
+}: {
+  open: boolean
+  availableYears: (number | string)[]
+  onConfirm: (exportType: number, years: (number | string)[]) => void
+  onCancel: () => void
+}) => {
+  const [exportValue, setExportValue] = useState<number>(1)
+  const [selectedYears, setSelectedYears] = useState<(number | string)[]>([])
+
+  const onExportTypeChange = (e: any) => {
+    setExportValue(e.target.value)
+  }
+
+  return (
+    <Modal
+      title='数据导出'
+      open={open}
+      onOk={() => onConfirm(exportValue, selectedYears)}
+      onCancel={onCancel}
+      okText='导出'
+      cancelText='取消'
+    >
+      <span style={{ marginBottom: 16 }}>导出范围:</span>
+
+      <Radio.Group
+        onChange={onExportTypeChange}
+        value={exportValue}
+        options={[
+          { value: 1, label: '所有年份' },
+          { value: 2, label: '按年份' }
+        ]}
+      />
+
+      {exportValue === 2 && (
+        <div style={{ marginTop: 24 }}>
+          <Checkbox.Group
+            style={{ flexDirection: 'column', gap: '5px' }}
+            options={availableYears.map(year => ({
+              label: `${year}年`,
+              value: year
+            }))}
+            value={selectedYears}
+            onChange={checkedValues => setSelectedYears(checkedValues as (number | string)[])}
+          />
+          <div style={{ opacity: '0.5', marginTop: '10px' }}>上述年份为藏品制档日期所在年份</div>
+        </div>
+      )}
+    </Modal>
+  )
+}
+function A3flow() {
+  const tableInfo = useSelector((state: RootState) => state.A3flow.tableInfo)
+  const [allData, setAllData] = useState([])
+  const [isModalOpen, setIsModalOpen] = useState(false)
+  const [availableYears, setAvailableYears] = useState<any>([])
+  const [list, setList] = useState<any[]>([])
+  const showModal = async () => {
+    setIsModalOpen(true)
+    const res = await API_getGoodsListAll()
+    setAllData(res.data.records)
+    //获取年份信息
+    const years = Array.from(
+      new Set(
+        res.data.records
+          .map((item: any) => item.createTime?.slice(0, 4))
+          .filter(Boolean) as string[]
+      )
+    ).sort((a, b) => Number(b) - Number(a))
+    setAvailableYears(years)
+  }
+
+  const dataExport = async (exportType: number = 1, years: (number | string)[] = []) => {
+    // 根据导出范围筛选数据:
+    // exportType === 1:所有年份,不做筛选
+    // exportType === 2:按年份,只保留所选年份的数据
+    const filteredData =
+      exportType === 2 && years.length
+        ? allData.filter((item: any) => {
+            if (!item.createTime) return false
+            const yearStr = item.createTime.slice(0, 4)
+            return years.includes(yearStr)
+          })
+        : allData
+
+    const listData = filteredData.map((item: any) => {
+      let applyTypeText = '(空)'
+      const obj = list.find((v: any) => v.value === item.type)
+      if (obj) applyTypeText = obj.label
+      let applyStatusText = '(空)'
+      const obj2 = selectObj['藏品入库申请状态'].find((v: any) => v.value === item.status)
+      if (obj2) applyStatusText = obj2.label
+      return {
+        申请类型: applyTypeText,
+        申请编号: item.num,
+        发起部门: item.deptName,
+        发起人: item.creatorName,
+        发起日期: item.date,
+        申请状态: applyStatusText
+      }
+    })
+    // console.log(listData)
+    exportExcelFile(listData, [100, 100, 120, 100, 140, 100], '流程中心数据')
+    setIsModalOpen(false)
+  }
+
+  useEffect(() => {
+    const arr: any[] = []
+    tabLeftArr.forEach(v1 => {
+      v1.son.forEach(v2 => {
+        if (v2.pageType) arr.push({ path: v2.path, value: v2.pageType, label: v2.name, id: v2.id })
+      })
+    })
+
+    setList(arr)
+  }, [])
+
+  const staBtn = useMemo(() => {
+    return [
+      {
+        title: '申请类型',
+        render: (item: GI5tableType) => {
+          let txt = '(空)'
+          const obj = list.find(v => v.value === item.type)
+          if (obj) txt = obj.label
+          return txt
+        }
+      }
+    ]
+  }, [list])
+
+  const A3topSearch = useMemo(
+    () => [
+      {
+        type: 'cascader',
+        key: 'typeDictId',
+        placeholder: '业务类型',
+        options: dictByProResFu(),
+        // 可以单选
+        only: true
+      },
+      {
+        type: 'input',
+        key: 'searchKey',
+        placeholder: `请输入申请编号、发起人`
+      },
+      {
+        type: 'time',
+        key: ['startTime', 'endTime'],
+        placeholder: ['发起日期开始', '发起日期结束']
+      },
+      {
+        type: 'select',
+        key: 'status',
+        placeholder: '申请状态',
+        options: selectObj['藏品入库申请状态']
+      }
+    ],
+    []
+  )
+
+  // 故事管理/藏品总账定制右侧内容
+  const storyTableListToprr = ({
+    clickSearch,
+    resetSelectFu
+  }: {
+    clickSearch: () => void
+    resetSelectFu: () => void
+  }) => {
+    return (
+      <>
+        <Button type='primary' onClick={showModal}>
+          数据导出
+        </Button>
+        <ExportModal
+          open={isModalOpen}
+          availableYears={availableYears}
+          onConfirm={dataExport}
+          onCancel={() => setIsModalOpen(false)}
+        />
+        <Button type='primary' onClick={clickSearch}>
+          查询
+        </Button>
+        <Button onClick={resetSelectFu}>重置</Button>
+      </>
+    )
+  }
+
+  const btnFu = useCallback((pageKey: '3' | '4', obj: any, id: number) => {
+    if (obj) {
+      authorityFu(obj.id, obj.label, () => history.push(`${obj.path}_edit/${pageKey}/${id}`))
+    } else MessageFu.warning('申请类型错误')
+  }, [])
+
+  // 故事管理/藏品总账定制右侧操作按钮
+  const storyTableLastBtn = useMemo(() => {
+    return [
+      {
+        title: '操作',
+        render: (item: any) => {
+          const obj = list.find(v => v.value === item.type)
+          return (
+            <>
+              <Button size='small' type='text' onClick={() => btnFu('4', obj, item.id)}>
+                查看
+              </Button>
+
+              {tableListAuditBtnFu(item) ? (
+                <Button size='small' type='text' onClick={() => btnFu('3', obj, item.id)}>
+                  审批
+                </Button>
+              ) : null}
+            </>
+          )
+        }
+      }
+    ]
+  }, [btnFu, list])
+
+  return (
+    <div className={styles.A3flow}>
+      <div className='pageTitle'>流程中心</div>
+
+      <TableList
+        baseFormData={baseFormData}
+        getListAPI={API_getGoodsList}
+        pageKey='A3flow'
+        tableInfo={tableInfo}
+        columnsTemp={flowCenterTableC}
+        rightBtnWidth={240}
+        leftRowWidth={'20%'}
+        yHeight={592}
+        staBtn={staBtn}
+        searchDom={A3topSearch}
+        storyTableListToprr={storyTableListToprr}
+        storyTableLastBtn={storyTableLastBtn}
+      />
+    </div>
+  )
+}
+
+const MemoA3flow = React.memo(A3flow)
+
+export default MemoA3flow

+ 10 - 0
src/pages/Abench/A4proof/index.module.scss

@@ -0,0 +1,10 @@
+.A4proof {
+  :global {
+    .TableListToprr {
+      align-items: flex-start;
+    }
+    .ant-table-cell {
+      padding: 8px !important;
+    }
+  }
+}

+ 220 - 0
src/pages/Abench/A4proof/index.tsx

@@ -0,0 +1,220 @@
+import React, { useMemo, useState, useEffect, useCallback } from 'react'
+import styles from './index.module.scss'
+import history from '@/utils/history'
+import TableList from '@/pages/Zother/TableList'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import { API_getGoodsList, API_download } from '@/store/action/Abench/A4proof'
+import { baseURL } from '@/utils/http'
+import { baseFormData } from '@/pages/Zother/data'
+import { voucherCenterTableC } from '@/utils/tableData'
+import { Button, message } from 'antd'
+import tabLeftArr from '@/pages/Layout/data'
+import { downloadFileByUrl } from '@/utils'
+import { authorityFu } from '@/utils/authority'
+import { MessageFu } from '@/utils/message'
+import { dictByProResFu } from '@/pages/Isystem/I2dict/data'
+
+function A4proof() {
+  const tableInfo = useSelector((state: RootState) => state.A4proof.tableInfo)
+  const [list, setList] = useState<any[]>([])
+  // 跨页参数
+  const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([])
+
+  // 跨页多选
+  const rowSelection = useMemo(
+    () => ({
+      selectedRowKeys,
+      // 这里不直接用 onChange 的 keys 覆盖,避免翻页时丢失其他页勾选
+      onChange: () => {},
+      // 单行勾选/取消
+      onSelect: (record: any, selected: boolean) => {
+        const id = record.id as React.Key
+        setSelectedRowKeys(prev => {
+          if (selected) {
+            if (prev.includes(id)) return prev
+            return [...prev, id]
+          } else {
+            return prev.filter(k => k !== id)
+          }
+        })
+      },
+      // 全选/全不选当前页
+      onSelectAll: (selected: boolean, _selectedRows: any[], changeRows: any[]) => {
+        const changeIds = changeRows.map(r => r.id as React.Key)
+        setSelectedRowKeys(prev => {
+          if (selected) {
+            const set = new Set<React.Key>(prev)
+            changeIds.forEach(id => set.add(id))
+            return Array.from(set)
+          } else {
+            return prev.filter(k => !changeIds.includes(k))
+          }
+        })
+      },
+      preserveSelectedRowKeys: true
+    }),
+    [selectedRowKeys]
+  )
+
+  // 下载方法
+  const downloadFn = async (ids: number | number[]) => {
+    const idArr = Array.isArray(ids) ? ids : [ids]
+    if (!idArr.length) return
+
+    const res: any = await API_download(idArr)
+    if (res && res.code === 0 && res.data) {
+      const path = res.data as string
+      const fullUrl = path.startsWith('http') ? path : `${baseURL}${path}`
+      window.open(fullUrl)
+    }
+  }
+  // 已知路径,单独下载 改为使用downloadFileByUrl
+  // const downloadSingleFn = (path: string, fileName: string) => {
+  //   const downloadUrl = `${baseURL}${path}`
+  //   const link = document.createElement('a')
+  //   link.href = downloadUrl
+  //   link.download = fileName
+  //   document.body.appendChild(link)
+  //   link.click()
+  //   document.body.removeChild(link)
+  // }
+  // 申请类型
+  useEffect(() => {
+    const arr: any[] = []
+    tabLeftArr.forEach(v1 => {
+      v1.son.forEach(v2 => {
+        if (v2.pageType) arr.push({ path: v2.path, value: v2.pageType, label: v2.name, id: v2.id })
+      })
+    })
+
+    setList(arr)
+  }, [])
+
+  // 点击各种操作按钮
+  const btnFu = useCallback((pageKey: '3' | '4', obj: any, id: number) => {
+    if (obj) {
+      authorityFu(obj.id, obj.name, () => history.push(`${obj.path}_edit/${pageKey}/${id}`))
+    } else MessageFu.warning('业务类型错误')
+  }, [])
+
+  const staBtn = useMemo(() => {
+    return [
+      {
+        title: '业务类型',
+        render: (item: any) => {
+          let txt = '(空)'
+          console.log('--------', item.moduleName)
+
+          const obj = list.find(v => v.value === item.moduleName)
+          if (obj) txt = obj.label
+          return txt
+        }
+      }
+    ]
+  }, [list])
+
+  const A4topSearch = useMemo(
+    () => [
+      {
+        type: 'input',
+        key: 'searchKey',
+        placeholder: `请输入凭证名称或业务编号`
+      },
+      {
+        type: 'cascader',
+        key: 'typeDictId',
+        placeholder: '业务类型',
+        options: dictByProResFu(),
+        // 可以单选
+        only: true
+      }
+    ],
+    []
+  )
+
+  // 故事管理/藏品总账定制右侧内容
+  const storyTableListToprr = ({
+    clickSearch,
+    resetSelectFu
+  }: {
+    clickSearch: () => void
+    resetSelectFu: () => void
+  }) => {
+    return (
+      <>
+        <Button
+          type='primary'
+          onClick={() => {
+            const ids = selectedRowKeys.map(key => Number(key)).filter(id => !Number.isNaN(id))
+            if (ids.length === 0) {
+              message.warning('请先选择要下载的凭证')
+              return
+            }
+
+            downloadFn(ids)
+          }}
+        >
+          批量下载
+        </Button>
+        <Button type='primary' onClick={clickSearch}>
+          查询
+        </Button>
+        <Button onClick={resetSelectFu}>重置</Button>
+      </>
+    )
+  }
+
+  // 故事管理/藏品总账定制右侧操作按钮
+  const storyTableLastBtn = useMemo(() => {
+    return [
+      {
+        title: '操作',
+        render: (item: any) => {
+          const obj = list.find(v => v.value === item.moduleName)
+          console.log('obj', obj)
+          return (
+            <>
+              <Button size='small' type='text' onClick={() => btnFu('4', obj, item.moduleId)}>
+                查看
+              </Button>
+              <Button
+                size='small'
+                type='text'
+                onClick={() => downloadFileByUrl(item.filePath, item.fileName)}
+              >
+                下载
+              </Button>
+            </>
+          )
+        }
+      }
+    ]
+  }, [btnFu, list])
+
+  return (
+    <div className={styles.A4proof}>
+      <div className='pageTitle'>凭证中心</div>
+
+      <TableList
+        baseFormData={baseFormData}
+        getListAPI={API_getGoodsList}
+        pageKey='proof'
+        tableInfo={tableInfo}
+        columnsTemp={voucherCenterTableC}
+        rightBtnWidth={240}
+        leftRowWidth={'20%'}
+        yHeight={592}
+        staBtn={staBtn}
+        searchDom={A4topSearch}
+        storyTableListToprr={storyTableListToprr}
+        storyTableLastBtn={storyTableLastBtn}
+        rowSelection={rowSelection}
+      />
+    </div>
+  )
+}
+
+const MemoA4proof = React.memo(A4proof)
+
+export default MemoA4proof

+ 3 - 0
src/pages/Benter/B1collect/B1edit/index.module.scss

@@ -0,0 +1,3 @@
+// .B1edit {
+//   // :global {}
+// }

+ 44 - 0
src/pages/Benter/B1collect/B1edit/index.tsx

@@ -0,0 +1,44 @@
+import React from 'react'
+import styles from './index.module.scss'
+import EditTop from '@/pages/Zother/EditTop'
+import EditBtn from '@/pages/Zother/EditBtn'
+import { InfoProvider } from '@/pages/Zother/InfoContext'
+import { rowArrTemp } from '@/pages/Zother/data'
+import { B1API_obj } from '@/store/action/Benter/B1collect'
+import SonClueList from '@/pages/Zother/SonClueList'
+import FileArchive from '@/pages/Zother/FileArchive'
+
+const rowArr = rowArrTemp('征集')
+
+function B1edit() {
+  return (
+    <InfoProvider>
+      <div className={styles.B1edit} id='editBox'>
+        <div className='editMain'>
+          {/* 顶部 */}
+          <EditTop
+            pageTxt='藏品征集'
+            rowArr={rowArr}
+            APIobj={B1API_obj}
+            fileUpInfo={{ myUrl: 'cms/order/collect/upload', dirCode: 'collect' }}
+          />
+
+          {/* 线索清单 */}
+          <SonClueList
+            fileUpInfo={{ myUrl: 'cms/order/collect/son/upload', dirCode: 'collectClues' }}
+          />
+
+          {/* 附件归档 */}
+          <FileArchive />
+
+          {/* 底部按钮 */}
+          <EditBtn path='/collect' APIobj={B1API_obj} checkListTxt='请添加线索' />
+        </div>
+      </div>
+    </InfoProvider>
+  )
+}
+
+const MemoB1edit = React.memo(B1edit)
+
+export default MemoB1edit

+ 4 - 0
src/pages/Benter/B1collect/index.module.scss

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

+ 37 - 0
src/pages/Benter/B1collect/index.tsx

@@ -0,0 +1,37 @@
+import React from 'react'
+import styles from './index.module.scss'
+import { baseFormData, topSearch } from '@/pages/Zother/data'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import TableList from '@/pages/Zother/TableList'
+import { B1_APIgetList } from '@/store/action/Benter/B1collect'
+import { baseTableC } from '@/utils/tableData'
+
+const B1search = topSearch('征集')
+
+const B1baseFormData = baseFormData()
+
+function B1collect() {
+  // 从仓库拿数据
+  const tableInfo = useSelector((state: RootState) => state.B1collect.tableInfo)
+
+  return (
+    <div className={styles.B1collect}>
+      <div className='pageTitle'>藏品征集</div>
+
+      <TableList
+        baseFormData={B1baseFormData}
+        getListAPI={B1_APIgetList}
+        pageKey='collect'
+        tableInfo={tableInfo}
+        columnsTemp={baseTableC('征集')}
+        yHeight={585}
+        searchDom={B1search}
+      />
+    </div>
+  )
+}
+
+const MemoB1collect = React.memo(B1collect)
+
+export default MemoB1collect

+ 4 - 0
src/pages/Benter/B2enterGuan/B2edit/index.module.scss

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

+ 45 - 0
src/pages/Benter/B2enterGuan/B2edit/index.tsx

@@ -0,0 +1,45 @@
+import React from 'react'
+import styles from './index.module.scss'
+import { rowArrTemp } from '@/pages/Zother/data'
+import { InfoProvider } from '@/pages/Zother/InfoContext'
+import EditTop from '@/pages/Zother/EditTop'
+import { B2_APIgetClueList, B2_APIgoodsSave, B2API_obj } from '@/store/action/Benter/B2enterGuan'
+import EditBtn from '@/pages/Zother/EditBtn'
+import SonGoodsList from '@/pages/Zother/SonGoodsList'
+import FileArchive from '@/pages/Zother/FileArchive'
+
+const rowArr = rowArrTemp('入馆')
+
+function B2edit() {
+  return (
+    <InfoProvider>
+      <div className={styles.B2edit} id='editBox'>
+        <div className='editMain'>
+          {/* 顶部 */}
+          <EditTop
+            pageTxt='藏品入馆'
+            rowArr={rowArr}
+            APIobj={B2API_obj}
+            fileUpInfo={{ myUrl: 'cms/order/house/upload', dirCode: 'enterGuan' }}
+          />
+
+          {/* 藏品清单 */}
+          <SonGoodsList
+            fileUpInfo={{ myUrl: 'cms/order/house/son/upload', dirCode: 'enterGuanGoods' }}
+            selectApi={B2_APIgetClueList}
+            addGoodsApi={B2_APIgoodsSave}
+            isClueSelect={true}
+          />
+          {/* 附件归档 */}
+          <FileArchive />
+          {/* 底部按钮 */}
+          <EditBtn path='/enterGuan' APIobj={B2API_obj} checkListTxt='请添加藏品' />
+        </div>
+      </div>
+    </InfoProvider>
+  )
+}
+
+const MemoB2edit = React.memo(B2edit)
+
+export default MemoB2edit

+ 4 - 0
src/pages/Benter/B2enterGuan/index.module.scss

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

+ 37 - 0
src/pages/Benter/B2enterGuan/index.tsx

@@ -0,0 +1,37 @@
+import React from 'react'
+import styles from './index.module.scss'
+import { baseFormData, topSearch } from '@/pages/Zother/data'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import TableList from '@/pages/Zother/TableList'
+import { B2_APIgetList } from '@/store/action/Benter/B2enterGuan'
+import { baseTableC } from '@/utils/tableData'
+
+const B2search = topSearch('入馆')
+
+const B2baseFormData = baseFormData()
+
+function B2enterGuan() {
+  // 从仓库拿数据
+  const tableInfo = useSelector((state: RootState) => state.B2enterGuan.tableInfo)
+
+  return (
+    <div className={styles.B2enterGuan}>
+      <div className='pageTitle'>藏品入馆</div>
+
+      <TableList
+        baseFormData={B2baseFormData}
+        getListAPI={B2_APIgetList}
+        pageKey='enterGuan'
+        tableInfo={tableInfo}
+        columnsTemp={baseTableC('入馆')}
+        yHeight={585}
+        searchDom={B2search}
+      />
+    </div>
+  )
+}
+
+const MemoB2enterGuan = React.memo(B2enterGuan)
+
+export default MemoB2enterGuan

+ 4 - 0
src/pages/Benter/B3auth/B3edit/index.module.scss

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

+ 67 - 0
src/pages/Benter/B3auth/B3edit/index.tsx

@@ -0,0 +1,67 @@
+import React from 'react'
+import styles from './index.module.scss'
+import { rowArrTemp } from '@/pages/Zother/data'
+import { InfoProvider } from '@/pages/Zother/InfoContext'
+import EditTop from '@/pages/Zother/EditTop'
+import { B3_APIgetClueList, B3API_obj } from '@/store/action/Benter/B3auth'
+import EditBtn from '@/pages/Zother/EditBtn'
+import { B3moreList } from '../data'
+import FileArchive from '@/pages/Zother/FileArchive'
+
+const rowArr = [
+  {
+    name: `鉴定入藏`,
+    type: 'txt'
+  },
+  ...rowArrTemp('入藏', undefined, '藏品鉴定').slice(1)
+]
+
+function B3edit() {
+  return (
+    <InfoProvider>
+      <div className={styles.B3edit} id='editBox'>
+        <div className='editMain'>
+          {/* 顶部 */}
+          <EditTop
+            pageTxt='藏品鉴定'
+            rowArr={rowArr}
+            APIobj={B3API_obj}
+            fileUpInfo={{ myUrl: 'cms/order/auth/upload', dirCode: 'auth' }}
+            // 第二个模块
+            moreDom={{
+              txt: '鉴定报告',
+              API: B3_APIgetClueList,
+              domList: B3moreList,
+              myUrl: 'cms/order/auth/son/upload',
+              dirCode: 'auth'
+            }}
+          />
+
+          {/* 附件归档 */}
+          <FileArchive />
+
+          {/* 底部按钮 */}
+          <EditBtn
+            path='/auth'
+            APIobj={B3API_obj}
+            checkListTxt='请选择关联藏品'
+            isTow={true}
+            verifyBackFu={info => {
+              const res = { flag: false, txt: '' }
+              if (!info.member) {
+                res.flag = true
+                res.txt = '请输入鉴定人员'
+              }
+
+              return res
+            }}
+          />
+        </div>
+      </div>
+    </InfoProvider>
+  )
+}
+
+const MemoB3edit = React.memo(B3edit)
+
+export default MemoB3edit

+ 32 - 0
src/pages/Benter/B3auth/data.ts

@@ -0,0 +1,32 @@
+export const B3moreList = [
+  {
+    name: '鉴定部门',
+    type: 'Input',
+    key: 'unit'
+  },
+  {
+    name: '鉴定人员',
+    must: true,
+    type: 'Input',
+    key: 'member',
+    noNull: true
+  },
+  {
+    name: '鉴定过程',
+    type: 'TextArea',
+    full: true,
+    key: 'course'
+  },
+  {
+    name: '鉴定结论',
+    type: 'TextArea',
+    full: true,
+    key: 'conclusion'
+  },
+  {
+    name: '备注',
+    type: 'TextArea',
+    full: true,
+    key: 'remark'
+  }
+]

+ 4 - 0
src/pages/Benter/B3auth/index.module.scss

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

+ 36 - 0
src/pages/Benter/B3auth/index.tsx

@@ -0,0 +1,36 @@
+import React from 'react'
+import styles from './index.module.scss'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import { B3_APIgetList } from '@/store/action/Benter/B3auth'
+import TableList from '@/pages/Zother/TableList'
+import { baseFormData, topSearch } from '@/pages/Zother/data'
+import { baseTableC } from '@/utils/tableData'
+
+const search = topSearch('鉴定')
+
+const baseFormDataRes = baseFormData()
+
+function B3auth() {
+  // 从仓库拿数据
+  const tableInfo = useSelector((state: RootState) => state.B3auth.tableInfo)
+
+  return (
+    <div className={styles.B3auth}>
+      <div className='pageTitle'>藏品鉴定</div>
+      <TableList
+        baseFormData={baseFormDataRes}
+        getListAPI={B3_APIgetList}
+        pageKey='auth'
+        tableInfo={tableInfo}
+        columnsTemp={baseTableC('鉴定')}
+        yHeight={585}
+        searchDom={search}
+      />
+    </div>
+  )
+}
+
+const MemoB3auth = React.memo(B3auth)
+
+export default MemoB3auth

+ 4 - 0
src/pages/Benter/B4enterCang/B4edit/index.module.scss

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

+ 47 - 0
src/pages/Benter/B4enterCang/B4edit/index.tsx

@@ -0,0 +1,47 @@
+import React from 'react'
+import styles from './index.module.scss'
+import { rowArrTemp } from '@/pages/Zother/data'
+import { InfoProvider } from '@/pages/Zother/InfoContext'
+import EditTop from '@/pages/Zother/EditTop'
+import { B4_APIgetClueList, B4API_obj } from '@/store/action/Benter/B4enterCang'
+import EditBtn from '@/pages/Zother/EditBtn'
+import SonGoodsList from '@/pages/Zother/SonGoodsList'
+import FileArchive from '@/pages/Zother/FileArchive'
+
+const rowArr = rowArrTemp('入藏')
+
+function B4edit() {
+  return (
+    <InfoProvider>
+      <div className={styles.B4edit} id='editBox'>
+        <div className='editMain'>
+          {/* 顶部 */}
+          <EditTop
+            pageTxt='藏品入藏'
+            rowArr={rowArr}
+            APIobj={B4API_obj}
+            fileUpInfo={{ myUrl: 'cms/order/hide/upload', dirCode: 'enterCang' }}
+          />
+
+          {/* 藏品清单 */}
+          <SonGoodsList
+            selectApi={B4_APIgetClueList}
+            isClueSelect={false}
+            btnTxt='从已入馆藏品中添加'
+            addShow={false}
+          />
+
+          {/* 附件归档 */}
+          <FileArchive />
+
+          {/* 底部按钮 */}
+          <EditBtn path='/enterCang' APIobj={B4API_obj} checkListTxt='请添加藏品' />
+        </div>
+      </div>
+    </InfoProvider>
+  )
+}
+
+const MemoB4edit = React.memo(B4edit)
+
+export default MemoB4edit

+ 4 - 0
src/pages/Benter/B4enterCang/index.module.scss

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

+ 37 - 0
src/pages/Benter/B4enterCang/index.tsx

@@ -0,0 +1,37 @@
+import React from 'react'
+import styles from './index.module.scss'
+import { baseFormData, topSearch } from '@/pages/Zother/data'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import TableList from '@/pages/Zother/TableList'
+import { B4_APIgetList } from '@/store/action/Benter/B4enterCang'
+import { baseTableC } from '@/utils/tableData'
+
+const search = topSearch('入藏')
+
+const baseFormDataRes = baseFormData()
+
+function B4enterCang() {
+  // 从仓库拿数据
+  const tableInfo = useSelector((state: RootState) => state.B4enterCang.tableInfo)
+
+  return (
+    <div className={styles.B4enterCang}>
+      <div className='pageTitle'>藏品入藏</div>
+
+      <TableList
+        baseFormData={baseFormDataRes}
+        getListAPI={B4_APIgetList}
+        pageKey='enterCang'
+        tableInfo={tableInfo}
+        columnsTemp={baseTableC('入藏')}
+        yHeight={585}
+        searchDom={search}
+      />
+    </div>
+  )
+}
+
+const MemoB4enterCang = React.memo(B4enterCang)
+
+export default MemoB4enterCang

+ 14 - 0
src/pages/Cledger/C1ledger/C1Import/index.module.scss

@@ -0,0 +1,14 @@
+.C1Import {
+  :global {
+    .TableListToprr {
+      justify-content: space-between !important;
+      .TableListToprrBox {
+        display: flex;
+        gap: 10px;
+      }
+    }
+    .ant-table-cell {
+      padding: 8px !important;
+    }
+  }
+}

+ 346 - 0
src/pages/Cledger/C1ledger/C1Import/index.tsx

@@ -0,0 +1,346 @@
+import React, { useMemo, useRef, useState } from 'react'
+import styles from './index.module.scss'
+import TableList from '@/pages/Zother/TableList'
+import { useSelector } from 'react-redux'
+import { RootState } from '@/store'
+import {
+  API_getImportExcelList,
+  API_getImportZipList,
+  API_uploadImportExcel,
+  API_uploadImportZip,
+  API_getImportResult,
+  API_deleteImportResult,
+  API_getImportExcelResult,
+  API_deleteImportExcelResult,
+  API_getFileLog,
+  API_saveFileLog
+} from '@/store/action/Cledger/C1ledger'
+import { importDataTableC } from '@/utils/tableData'
+import { Button, Radio } from 'antd'
+import { MessageFu } from '@/utils/message'
+import { baseURL } from '@/utils/http'
+import C1ImportRes from '../C1ImportRes'
+import C1ImportZipRes from '../C1importZipRes'
+import { downloadFileByUrl } from '@/utils'
+import DownLog from '@/pages/Zother/DownLog'
+
+const C1ImportBaseFormData = {
+  pageNum: 1,
+  pageSize: 10,
+  searchKey: '',
+  type: ''
+}
+
+function C1Import({ onClose }: { onClose: () => void }) {
+  const importExcelTableInfo = useSelector(
+    (state: RootState) => state.C1ledger.importExcelTableInfo
+  )
+  const importZipTableInfo = useSelector((state: RootState) => state.C1ledger.importZipTableInfo)
+  const [isImportData, setIsImportData] = useState(true)
+  // 是否查看模式(查看历史记录)
+  const [isLook, setIsLook] = useState(false)
+  const fileInputRef = useRef<HTMLInputElement>(null)
+  const clickSearchRef = useRef<() => void>(() => {})
+  // 下载记录参数
+  const [downLogId, setDownLogId] = useState(0)
+  const [uploadData, setUploadData] = useState<{
+    visible: boolean
+    result: any | null
+    errorMsg?: string
+  }>({ visible: false, result: null })
+  // 选择文件并上传--导入数据(excel
+  const handleUploadExcel = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0]
+    if (!file) return
+    const ext = file.name.split('.').pop()?.toLowerCase()
+    if (!['xlsx'].includes(ext || '')) {
+      MessageFu.warning('请选择 Excel 文件(.xlsx 或 .xls)')
+      e.target.value = ''
+      return
+    }
+    const formData = new FormData()
+    formData.append('file', file)
+    formData.append('type', 'doc')
+    formData.append('dirCode', 'importExcel')
+    try {
+      const res: any = await API_uploadImportExcel(formData)
+      e.target.value = ''
+      if (res?.code === 0) {
+        const data = res.data || {}
+        setIsLook(false)
+        setUploadData({
+          visible: true,
+          result: data
+        })
+        MessageFu.success('上传校验完成')
+        clickSearchRef.current?.()
+      } else {
+        setUploadData({
+          visible: false,
+          result: null,
+          errorMsg: res?.msg || '上传失败'
+        })
+      }
+    } catch (err: any) {
+      e.target.value = ''
+      setUploadData({
+        visible: false,
+        result: null,
+        errorMsg: err?.response?.data?.msg || err?.message || '上传失败'
+      })
+    }
+  }
+
+  // 选择文件并上传--导入图片(zip/rar)
+  const handleUploadZip = async (e: React.ChangeEvent<HTMLInputElement>) => {
+    const file = e.target.files?.[0]
+    if (!file) return
+    const ext = file.name.split('.').pop()?.toLowerCase()
+    if (!['zip', 'rar'].includes(ext || '')) {
+      MessageFu.warning('请选择 zip/rar 文件(.zip 或 .rar)')
+      e.target.value = ''
+      return
+    }
+    const formData = new FormData()
+    formData.append('file', file)
+    formData.append('type', 'other')
+    formData.append('dirCode', 'importZip')
+    try {
+      const res: any = await API_uploadImportZip(formData as any)
+      e.target.value = ''
+      if (res?.code === 0) {
+        const data = res.data || {}
+        setIsLook(false)
+        setUploadData({
+          visible: true,
+          result: data
+        })
+        MessageFu.success('上传校验完成')
+        clickSearchRef.current?.()
+      } else {
+        setUploadData({
+          visible: false,
+          result: null,
+          errorMsg: res?.msg || '上传失败'
+        })
+      }
+    } catch (err: any) {
+      e.target.value = ''
+      setUploadData({
+        visible: false,
+        result: null,
+        errorMsg: err?.response?.data?.msg || err?.message || '上传失败'
+      })
+    }
+  }
+
+  // 故事管理/藏品总账定制右侧内容
+  const storyTableListToprr = ({
+    clickSearch,
+    resetSelectFu
+  }: {
+    clickSearch: () => void
+    resetSelectFu: () => void
+  }) => {
+    clickSearchRef.current = resetSelectFu
+    return (
+      <>
+        <div className='TableListToprrBox'>
+          <Button onClick={onClose}>返回</Button>
+          <Radio.Group>
+            <Button
+              value='importData'
+              type={isImportData ? 'primary' : 'default'}
+              onClick={() => setIsImportData(true)}
+            >
+              导入藏品数据
+            </Button>
+            <Button
+              value='importImage'
+              type={isImportData ? 'default' : 'primary'}
+              onClick={() => setIsImportData(false)}
+            >
+              导入藏品图片
+            </Button>
+          </Radio.Group>
+        </div>
+        <div className='TableListToprrBox'>
+          <input
+            ref={fileInputRef}
+            type='file'
+            accept={isImportData ? '.xlsx' : '.zip,.rar'}
+            maxLength={1000}
+            max={isImportData ? 10 * 1024 * 1024 : 500 * 1024 * 1024}
+            style={{ display: 'none' }}
+            onChange={isImportData ? handleUploadExcel : handleUploadZip}
+          />
+          {isImportData ? (
+            <>
+              <div style={{ marginRight: 20 }}>
+                仅支持xlsx格式,文件不得大于10M,单词最多上传1000条
+              </div>
+              <Button
+                type='primary'
+                onClick={() => window.open(`${baseURL}/baseData/ZP_TEMP.xlsx`)}
+              >
+                下载模板
+              </Button>
+              <Button type='primary' onClick={() => fileInputRef.current?.click()}>
+                上传藏品数据
+              </Button>
+            </>
+          ) : (
+            <>
+              <div style={{ marginRight: 20 }}>
+                图片以zip压缩包格式上传,500M以内,请勿嵌套
+                <br />
+                图片命名规则:藏品编号-图片类型(如D00001-A-01)
+              </div>
+              <div style={{ marginRight: 20 }}>
+                图片的格式:jpg/png/bmp/gif
+                <br />
+                图片类型:A:正视图 B:俯视图 C:侧视图 D:全景图 E:局部图 F:底部图
+              </div>
+              <Button type='primary' onClick={() => fileInputRef.current?.click()}>
+                上传压缩包(ZIP,RAR格式)
+              </Button>
+            </>
+          )}
+        </div>
+      </>
+    )
+  }
+
+  // 故事管理/藏品总账定制右侧操作按钮
+  const storyTableLastBtn = useMemo(() => {
+    // 点击查看导入结果
+    const handleGetImportResult = (item: any) => {
+      if (isImportData) {
+        API_getImportExcelResult(item.id).then((res: any) => {
+          setIsLook(true)
+          setUploadData({
+            visible: true,
+            result: {
+              excel: res.data,
+              fileName: item.fileName,
+              pcsSuccess: item.pcsSuccess,
+              pcsError: item.pcsError
+            }
+          })
+        })
+      } else {
+        API_getImportResult(item.id).then((res: any) => {
+          setIsLook(true)
+          setUploadData({
+            visible: true,
+            result: {
+              img: res.data,
+              fileName: item.fileName,
+              pcsSuccess: item.pcsSuccess,
+              pcsError: item.pcsError
+            }
+          })
+        })
+      }
+    }
+
+    // 点击删除导入结果
+    const handleDeleteImportResult = (item: any) => {
+      if (isImportData) {
+        API_deleteImportExcelResult(item.id).then(() => {
+          MessageFu.success('删除成功')
+          clickSearchRef.current?.()
+        })
+      } else {
+        API_deleteImportResult(item.id).then(() => {
+          MessageFu.success('删除成功')
+          clickSearchRef.current?.()
+        })
+      }
+    }
+    return [
+      {
+        title: '操作',
+        render: (item: any) => (
+          <>
+            <Button size='small' type='text' onClick={() => handleGetImportResult(item)}>
+              查看
+            </Button>
+            <Button size='small' type='text' onClick={() => handleDeleteImportResult(item)}>
+              删除记录
+            </Button>
+            <br />
+            <Button
+              size='small'
+              type='text'
+              onClick={() => {
+                downloadFileByUrl(item.filePath, item.fileName)
+                API_saveFileLog({
+                  moduleIds: [item.id],
+                  moduleName: isImportData ? 'Excel导入' : '图片导入'
+                } as any)
+              }}
+            >
+              下载
+            </Button>
+            <Button size='small' type='text' onClick={() => setDownLogId(item.id)}>
+              下载记录
+            </Button>
+          </>
+        )
+      }
+    ]
+  }, [isImportData])
+
+  return (
+    <div className={styles.C1Import}>
+      {uploadData.visible ? (
+        isImportData ? (
+          <C1ImportRes
+            onClose={() => setUploadData({ visible: false, result: null })}
+            tableInfo={uploadData.result}
+            uploadFu={handleUploadExcel}
+            isLook={isLook}
+          />
+        ) : (
+          <C1ImportZipRes
+            onClose={() => setUploadData({ visible: false, result: null })}
+            tableInfo={uploadData.result}
+            uploadFu={handleUploadZip}
+            isLook={isLook}
+          />
+        )
+      ) : (
+        <>
+          <TableList
+            key={isImportData ? 'importExcel' : 'importZip'}
+            baseFormData={C1ImportBaseFormData}
+            getListAPI={isImportData ? API_getImportExcelList : API_getImportZipList}
+            pageKey={isImportData ? 'importExcel' : 'importZip'}
+            tableInfo={isImportData ? importExcelTableInfo : importZipTableInfo}
+            columnsTemp={importDataTableC}
+            rightBtnWidth={'100%'}
+            leftRowWidth={'20%'}
+            yHeight={592}
+            searchDom={[]}
+            storyTableListToprr={storyTableListToprr}
+            storyTableLastBtn={storyTableLastBtn}
+          />
+          {/* 下载记录 */}
+          {downLogId ? (
+            <DownLog
+              id={downLogId}
+              moduleName={isImportData ? 'Excel导入' : '图片导入'}
+              getListAPI={API_getFileLog as any}
+              closeFu={() => setDownLogId(0)}
+            />
+          ) : null}
+        </>
+      )}
+    </div>
+  )
+}
+
+const MemoC1Import = React.memo(C1Import)
+
+export default MemoC1Import

+ 68 - 0
src/pages/Cledger/C1ledger/C1ImportRes/index.module.scss

@@ -0,0 +1,68 @@
+// .B1collect {
+//   :global {
+//   }
+// }
+.TableListTopInfo {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  margin: 15px 15px 5px;
+  .i1 {
+    color: var(--txtColor);
+  }
+}
+
+.TableListTop {
+  border-radius: 10px;
+  padding: 15px 15px 5px;
+  background-color: var(--boxBcaColor);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  :global {
+    .TableListTopll {
+      display: flex;
+      flex-wrap: wrap;
+
+      & > div {
+        padding-right: 10px;
+        margin-bottom: 10px;
+        & > div {
+          width: 100%;
+        }
+        & > input {
+          width: 100%;
+        }
+      }
+    }
+    .TableListToprr {
+      display: flex;
+      flex-wrap: wrap;
+
+      justify-content: space-between;
+      .ant-btn {
+        margin-bottom: 10px;
+      }
+    }
+  }
+}
+
+.TableListTable {
+  border-radius: 10px;
+  overflow: hidden;
+  margin-top: 15px;
+  height: calc(100% - 77px);
+  background-color: var(--boxBcaColor);
+}
+
+.TableListBottom {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  gap: 10px;
+  margin-top: 15px;
+  padding: 15px;
+  background-color: var(--boxBcaColor);
+  border-radius: 10px;
+}

+ 233 - 0
src/pages/Cledger/C1ledger/C1ImportRes/index.tsx

@@ -0,0 +1,233 @@
+import React, { useState, useEffect, useRef, useMemo } from 'react'
+import styles from './index.module.scss'
+import { importDataTableCheckC } from '@/utils/tableData'
+import { selectObj } from '@/utils/dataChange'
+import MyTable from '@/components/MyTable'
+import { Input, Select, Button } from 'antd'
+import {
+  API_addImportExcel,
+  API_addImportExcelRecord,
+  API_saveFileLog
+} from '@/store/action/Cledger/C1ledger'
+import { MessageFu } from '@/utils/message'
+
+type Props = {
+  onClose: () => void
+  tableInfo: any
+  uploadFu: (e: React.ChangeEvent<HTMLInputElement>) => void
+  isLook?: boolean // 是否查看模式
+}
+
+function C1ImportRes({ onClose, tableInfo, uploadFu, isLook = false }: Props) {
+  const staBtn = [
+    {
+      title: '行数',
+      render: (_: any, __: any, index: number) => index + 1
+    }
+  ]
+  const lastBtn = [
+    {
+      title: '失败原因',
+      render: (_: any, record: any) => {
+        const isFail = record.importIsTrue === 0
+        const errArr = record.importErrorMsg ?? record.EError ?? record.eerror ?? []
+        // console.log('errArr', errArr)
+        if (!isFail) return ''
+        return Array.isArray(errArr) ? (
+          <span style={{ whiteSpace: 'pre-line' }}>{errArr.join('\n')}</span>
+        ) : (
+          errArr
+        )
+      }
+    }
+  ]
+  const successCount = useMemo(() => {
+    return (
+      tableInfo.pcsSuccess ?? tableInfo.excel.filter((item: any) => item.importIsTrue === 1).length
+    )
+  }, [tableInfo.excel, tableInfo.pcsSuccess])
+  const errorCount = useMemo(() => {
+    return (
+      tableInfo.pcsError ?? tableInfo.excel.filter((item: any) => item.importIsTrue === 0).length
+    )
+  }, [tableInfo.excel, tableInfo.pcsError])
+  const [tableInfoData, setTableInfoData] = useState(
+    (tableInfo.excel ?? []).map((item: any, i: number) => ({ ...item, _rowKey: `row-${i}` }))
+  )
+  const [searchKey, setSearchKey] = useState('')
+  const [checkResult, setCheckResult] = useState()
+  const [importLoading, setImportLoading] = useState(false)
+  const [pageNum, setPageNum] = useState(1)
+  const [pageSize, setPageSize] = useState(10)
+  const fileInputRef = useRef<HTMLInputElement>(null)
+  // 条件搜索(保留 _rowKey 用于表格 key)
+  useEffect(() => {
+    const filtered = (tableInfo.excel ?? [])
+      .map((item: any, i: number) => ({ ...item, _rowKey: `row-${i}` }))
+      .filter((item: any) => {
+        const matchesSearch =
+          !searchKey || item.num.includes(searchKey) || item.name.includes(searchKey)
+        const matchesCheck =
+          checkResult === undefined ||
+          item.importIsTrue === checkResult ||
+          checkResult === item.importIsTrue
+        return matchesSearch && matchesCheck
+      })
+    setTableInfoData(filtered)
+    setPageNum(1) // 搜索/筛选变化时重置到第一页
+  }, [tableInfo.excel, searchKey, checkResult])
+
+  // 重新上传
+  const reUploadFu = () => {
+    fileInputRef.current?.click()
+    setSearchKey('')
+    setCheckResult(undefined)
+  }
+
+  // 导入校验成功的数据
+  const importSuccessFu = async () => {
+    const successData = tableInfo.excel.filter((item: any) => item.importIsTrue === 1)
+    if (successData.length === 0) {
+      MessageFu.warning('没有校验成功的数据可导入')
+      return
+    }
+    setImportLoading(true)
+    try {
+      // 1. 先添加记录
+      const recordRes: any = await API_addImportExcelRecord({
+        fileName: tableInfo.fileName,
+        filePath: tableInfo.filePath,
+        pcsError: errorCount,
+        pcsSuccess: successCount,
+        pcsTotal: tableInfo.excel.length,
+        type: 'zip'
+      } as any)
+      if (recordRes?.code !== 0) {
+        MessageFu.error(recordRes?.msg || '添加记录失败')
+        return
+      } else {
+        // 2. 再导入数据
+        const importId = recordRes.data.id
+        const res: any = await API_addImportExcel(
+          successData.map((item: any) => {
+            const obj = {
+              ...item,
+              importId,
+              importError: item.importIsTrue === 0 ? 1 : 0
+            }
+            // 过滤掉值为 null 的属性
+            // console.log(
+            //   'obj',
+            //   Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null && v !== 'null'))
+            // )
+            return Object.fromEntries(
+              Object.entries(obj).filter(([, v]) => v != null && v !== 'null')
+            ) as any
+          })
+        )
+        if (res?.code === 0) {
+          // 3. 保存下载记录
+          // await API_saveFileLog({
+          //   moduleIds: [importId],
+          //   moduleName: 'Excel导入'
+          // } as any)
+          MessageFu.success('导入成功')
+          onClose()
+        } else {
+          MessageFu.error(res?.msg || '导入失败')
+        }
+      }
+    } catch (err: any) {
+      MessageFu.error(err?.response?.data?.msg || err?.message || '导入失败')
+    } finally {
+      setImportLoading(false)
+    }
+  }
+
+  const columnsTemp = useMemo(() => {
+    const base = importDataTableCheckC
+    return isLook
+      ? [...base.slice(0, -1), ['txtChange', '校验结果', 'importError', { 1: '失败', 0: '成功' }]]
+      : base
+  }, [isLook])
+
+  return (
+    <div className={styles.C1ImportRes}>
+      <div className={styles.TableListTopInfo}>
+        <h2>{tableInfo.fileName}</h2>
+        <div className='i1'>导入数据条数: {tableInfo.excel.length}</div>
+        <div className='i1'>校验成功条数: {successCount}</div>
+        <div className='i1'>校验失败条数: {errorCount}</div>
+      </div>
+      <div className={styles.TableListTop}>
+        <div
+          className='TableListTopll'
+          style={{
+            width: `100%`
+          }}
+        >
+          <div style={{ width: '20%' }}>
+            <Input
+              placeholder='搜索藏品登记号、藏品名称,不超过30个字'
+              maxLength={30}
+              value={searchKey}
+              onChange={e => setSearchKey(e.target.value)}
+            />
+          </div>
+          <div style={{ width: '20%' }}>
+            <Select
+              allowClear={true}
+              placeholder='校验结果'
+              value={checkResult}
+              onChange={e => setCheckResult(e)}
+              options={selectObj['校验结果']}
+            />
+          </div>
+        </div>
+      </div>
+      <div className={`${styles.TableListTable} table-list-table`}>
+        <MyTable
+          rowKey='_rowKey'
+          list={tableInfoData}
+          columnsTemp={columnsTemp}
+          yHeight={500}
+          staBtn={staBtn}
+          lastBtn={lastBtn}
+          pageNum={pageNum}
+          pageSize={pageSize}
+          total={tableInfoData.length}
+          onChange={(p, s) => {
+            setPageNum(p)
+            setPageSize(s)
+          }}
+        />
+      </div>
+      <div className={styles.TableListBottom}>
+        {!isLook ? (
+          <>
+            <input
+              ref={fileInputRef}
+              type='file'
+              accept='.xlsx'
+              style={{ display: 'none' }}
+              onChange={uploadFu}
+            />
+            <Button type='primary' onClick={reUploadFu}>
+              重新上传表格
+            </Button>
+            <Button type='primary' loading={importLoading} onClick={importSuccessFu}>
+              导入校验成功的数据
+            </Button>
+            <Button onClick={() => onClose()}>取消导入</Button>
+          </>
+        ) : (
+          <Button onClick={() => onClose()}>返回</Button>
+        )}
+      </div>
+    </div>
+  )
+}
+
+const MemoC1ImportRes = React.memo(C1ImportRes)
+
+export default MemoC1ImportRes

+ 68 - 0
src/pages/Cledger/C1ledger/C1importZipRes/index.module.scss

@@ -0,0 +1,68 @@
+// .B1collect {
+//   :global {
+//   }
+// }
+.TableListTopInfo {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  gap: 20px;
+  margin: 15px 15px 5px;
+  .i1 {
+    color: var(--txtColor);
+  }
+}
+
+.TableListTop {
+  border-radius: 10px;
+  padding: 15px 15px 5px;
+  background-color: var(--boxBcaColor);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  :global {
+    .TableListTopll {
+      display: flex;
+      flex-wrap: wrap;
+
+      & > div {
+        padding-right: 10px;
+        margin-bottom: 10px;
+        & > div {
+          width: 100%;
+        }
+        & > input {
+          width: 100%;
+        }
+      }
+    }
+    .TableListToprr {
+      display: flex;
+      flex-wrap: wrap;
+
+      justify-content: space-between;
+      .ant-btn {
+        margin-bottom: 10px;
+      }
+    }
+  }
+}
+
+.TableListTable {
+  border-radius: 10px;
+  overflow: hidden;
+  margin-top: 15px;
+  height: calc(100% - 77px);
+  background-color: var(--boxBcaColor);
+}
+
+.TableListBottom {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  gap: 10px;
+  margin-top: 15px;
+  padding: 15px;
+  background-color: var(--boxBcaColor);
+  border-radius: 10px;
+}

+ 185 - 0
src/pages/Cledger/C1ledger/C1importZipRes/index.tsx

@@ -0,0 +1,185 @@
+import React, { useState, useEffect, useRef, useMemo } from 'react'
+import styles from './index.module.scss'
+import { importZipDataTableCheckC } from '@/utils/tableData'
+import { selectObj } from '@/utils/dataChange'
+import MyTable from '@/components/MyTable'
+import { Input, Select, Button } from 'antd'
+import { API_addImportZip, API_addImportZipRecord } from '@/store/action/Cledger/C1ledger'
+import { MessageFu } from '@/utils/message'
+
+type Props = {
+  onClose: () => void
+  tableInfo: any
+  uploadFu: (e: React.ChangeEvent<HTMLInputElement>) => void
+  isLook?: boolean // 是否查看模式
+}
+
+function C1ImportZipRes({ onClose, tableInfo, uploadFu, isLook = false }: Props) {
+  // console.log('tableInfo', tableInfo)
+  const successCount = useMemo(() => {
+    return tableInfo.pcsSuccess ?? tableInfo.img.filter((item: any) => item.isTrue === 1).length
+  }, [tableInfo.img, tableInfo.pcsSuccess])
+  const errorCount = useMemo(() => {
+    return tableInfo.pcsError ?? tableInfo.img.filter((item: any) => item.isTrue === 0).length
+  }, [tableInfo.img, tableInfo.pcsError])
+  const [tableInfoData, setTableInfoData] = useState(tableInfo.img)
+  const [searchKey, setSearchKey] = useState('')
+  const [checkResult, setCheckResult] = useState()
+  const [importLoading, setImportLoading] = useState(false)
+  const [pageNum, setPageNum] = useState(1)
+  const [pageSize, setPageSize] = useState(10)
+  const fileInputRef = useRef<HTMLInputElement>(null)
+
+  // 条件搜索
+  useEffect(() => {
+    const filtered = tableInfo.img.filter((item: any) => {
+      const matchesSearch =
+        !searchKey || item.num.includes(searchKey) || item.name.includes(searchKey)
+      const matchesCheck = checkResult === undefined || item.isTrue === checkResult
+      return matchesSearch && matchesCheck
+    })
+    setTableInfoData(filtered)
+    setPageNum(1) // 搜索/筛选变化时重置到第一页
+  }, [tableInfo.img, searchKey, checkResult])
+
+  // 重新上传
+  const reUploadFu = () => {
+    fileInputRef.current?.click()
+    setSearchKey('')
+    setCheckResult(undefined)
+  }
+
+  // 导入校验成功的数据:先添加记录,再导入数据
+  const importSuccessFu = async () => {
+    const successData = tableInfo.img.filter((item: any) => item.isTrue === 1)
+    if (successData.length === 0) {
+      MessageFu.warning('没有校验成功的数据可导入')
+      return
+    }
+    setImportLoading(true)
+    try {
+      // 1. 先添加记录
+      const recordRes: any = await API_addImportZipRecord({
+        fileName: tableInfo.fileName,
+        filePath: tableInfo.filePath,
+        pcsError: errorCount,
+        pcsSuccess: successCount,
+        pcsTotal: tableInfo.img.length,
+        type: 'zip'
+      } as any)
+      if (recordRes?.code !== 0) {
+        MessageFu.error(recordRes?.msg || '添加记录失败')
+        return
+      } else {
+        // 2. 再导入数据
+        const importId = recordRes.data.id
+        const payload = successData.map((item: any) => ({
+          description: item.description ?? item.desc ?? '',
+          effect: item.effect ?? '',
+          fileName: item.fileName ?? '',
+          filePath: item.filePath ?? '',
+          id: item.id ?? 0,
+          importError: 0,
+          importId: importId,
+          isTrue: item.isTrue,
+          num: item.num ?? '',
+          type: item.type
+        }))
+        const res: any = await API_addImportZip(payload as any)
+        if (res?.code === 0) {
+          // 3. 保存下载记录
+          // await API_saveFileLog({
+          //   moduleIds: [importId],
+          //   moduleName: '图片导入'
+          // } as any)
+          MessageFu.success('导入成功')
+          onClose()
+        } else {
+          MessageFu.error(res?.msg || '导入失败')
+        }
+      }
+    } catch (err: any) {
+      MessageFu.error(err?.response?.data?.msg || err?.message || '导入失败')
+    } finally {
+      setImportLoading(false)
+    }
+  }
+
+  return (
+    <div className={styles.C1ImportRes}>
+      <div className={styles.TableListTopInfo}>
+        <h2>{tableInfo.fileName}</h2>
+        <div className='i1'>导入数据条数: {tableInfo.img.length}</div>
+        <div className='i1'>校验成功条数: {successCount}</div>
+        <div className='i1'>校验失败条数: {errorCount}</div>
+      </div>
+      <div className={styles.TableListTop}>
+        <div
+          className='TableListTopll'
+          style={{
+            width: `100%`
+          }}
+        >
+          <div style={{ width: '20%' }}>
+            <Input
+              placeholder='搜索藏品登记号、藏品名称,不超过30个字'
+              maxLength={30}
+              value={searchKey}
+              onChange={e => setSearchKey(e.target.value)}
+            />
+          </div>
+          <div style={{ width: '20%' }}>
+            <Select
+              allowClear={true}
+              placeholder='校验结果'
+              value={checkResult}
+              onChange={e => setCheckResult(e)}
+              options={selectObj['校验结果']}
+            />
+          </div>
+        </div>
+      </div>
+      <div className={`${styles.TableListTable} table-list-table`}>
+        <MyTable
+          rowKey='fileName'
+          list={tableInfoData}
+          columnsTemp={importZipDataTableCheckC}
+          yHeight={500}
+          pageNum={pageNum}
+          pageSize={pageSize}
+          total={tableInfoData.length}
+          onChange={(p, s) => {
+            setPageNum(p)
+            setPageSize(s)
+          }}
+        />
+      </div>
+      <div className={styles.TableListBottom}>
+        {!isLook ? (
+          <>
+            <input
+              ref={fileInputRef}
+              type='file'
+              accept='.zip,.rar'
+              style={{ display: 'none' }}
+              onChange={uploadFu}
+            />
+            <Button type='primary' onClick={reUploadFu}>
+              重新上传压缩包
+            </Button>
+            <Button type='primary' loading={importLoading} onClick={importSuccessFu}>
+              导入校验成功的数据
+            </Button>
+            <Button onClick={() => onClose()}>取消导入</Button>
+          </>
+        ) : (
+          <Button onClick={() => onClose()}>返回</Button>
+        )}
+      </div>
+    </div>
+  )
+}
+
+const MemoC1ImportZipRes = React.memo(C1ImportZipRes)
+
+export default MemoC1ImportZipRes

+ 11 - 0
src/pages/Cledger/C1ledger/index.module.scss

@@ -0,0 +1,11 @@
+.C1ledger {
+  :global {
+    .TableListToprr {
+      justify-content: start !important;
+      gap: 0 4%;
+    }
+    .ant-table-cell {
+      padding: 8px !important;
+    }
+  }
+}

+ 0 - 0
src/pages/Cledger/C1ledger/index.tsx


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