Explorar el Código

Merge branch 'master' of http://39.97.59.228:8013/hrx/mky-vent-base

lxh hace 3 días
padre
commit
68b0916932
Se han modificado 88 ficheros con 9452 adiciones y 4081 borrados
  1. 3 8
      .editorconfig
  2. 1 1
      .env.development
  3. 7 3
      build/vite/plugin/styleImport.ts
  4. 32 16
      index.html
  5. 17 1
      package.json
  6. 344 338
      pnpm-lock.yaml
  7. 22 10
      prettier.config.js
  8. 2 2
      public/js/config.js
  9. 6 3
      src/api/sys/modelFile.ts
  10. 0 3
      src/assets/images/ventAI/chatModalBlue/2-10.svg
  11. 0 3
      src/assets/images/ventAI/chatModalBlue/2-11.svg
  12. BIN
      src/assets/images/ventAI/chatModalBlue/2-13.png
  13. BIN
      src/assets/images/ventAI/chatModalBlue/2-15.png
  14. BIN
      src/assets/images/ventAI/chatModalBlue/2-9.png
  15. 3 0
      src/assets/images/ventAI/chatModalBlue/3-1.svg
  16. 6 0
      src/assets/images/ventAI/chatModalBlue/3-10.svg
  17. 3 0
      src/assets/images/ventAI/chatModalBlue/3-11.svg
  18. 3 0
      src/assets/images/ventAI/chatModalBlue/3-12.svg
  19. 3 0
      src/assets/images/ventAI/chatModalBlue/3-2.svg
  20. 1 0
      src/assets/images/ventAI/chatModalBlue/3-3.svg
  21. 1 0
      src/assets/images/ventAI/chatModalBlue/3-4.svg
  22. 1 0
      src/assets/images/ventAI/chatModalBlue/3-5.svg
  23. 1 0
      src/assets/images/ventAI/chatModalBlue/3-6.svg
  24. 3 0
      src/assets/images/ventAI/chatModalBlue/3-9.svg
  25. BIN
      src/assets/images/ventAI/chatModalBlue/fullscreen/1-1.png
  26. BIN
      src/assets/images/ventAI/chatModalBlue/fullscreen/1-2.png
  27. BIN
      src/assets/images/ventAI/chatModalBlue/fullscreen/1-3.png
  28. BIN
      src/assets/images/ventAI/chatModalBlue/fullscreen/1-4.png
  29. BIN
      src/assets/images/ventAI/chatModalBlue/fullscreen/1-5.png
  30. BIN
      src/assets/images/ventAI/chatModalBlue/fullscreen/1-6.png
  31. BIN
      src/assets/images/ventAI/chatModalBlue/fullscreen/1-7.png
  32. BIN
      src/assets/images/ventAI/chatModalBlue/fullscreen/1-8.png
  33. 36 0
      src/assets/images/ventAI/chatModalBlue/fullscreen/1-9.svg
  34. 3 0
      src/assets/images/ventAI/dataPicker/AI-close.svg
  35. 3 0
      src/assets/images/ventAI/dataPicker/AI-open.svg
  36. 21 4
      src/design/themify/deepblue.less
  37. 21 4
      src/design/themify/default.less
  38. 0 410
      src/hooks/core/useThree copy.ts
  39. 84 110
      src/hooks/vent/useThree.ts
  40. 251 245
      src/layouts/default/header/components/VoiceBroadcast.vue
  41. 238 232
      src/layouts/default/header/components/VoiceBroadcastGsd.vue
  42. 20 0
      src/layouts/default/header/index.vue
  43. 19 1
      src/layouts/default/index.vue
  44. 10 3
      src/layouts/default/sider/bottomSideder.vue
  45. 10 3
      src/layouts/default/sider/bottomSideder3.vue
  46. 9 1
      src/qiankun/apps.ts
  47. 4 0
      src/store/modules/permission.ts
  48. 1 0
      src/store/modules/vent.ts
  49. 5 11
      src/utils/threejs/useThree.ts
  50. 49 44
      src/views/system/role/components/RoleDrawer.vue
  51. 147 147
      src/views/system/role/components/RolePermissionDrawer.vue
  52. 223 148
      src/views/system/role/index.vue
  53. 170 161
      src/views/vent/deviceManager/comment/DeviceModal.vue
  54. 9 8
      src/views/vent/deviceManager/comment/NormalTable.vue
  55. 1 0
      src/views/vent/deviceManager/deviceTable/device.api.ts
  56. 1 1
      src/views/vent/home/billboard/billboard.api.ts
  57. 377 377
      src/views/vent/home/configurable/belt/belt-new.vue
  58. 103 19
      src/views/vent/home/configurable/firedoor/components/fireDoorBoard.vue
  59. 62 16
      src/views/vent/home/configurable/firedoor/threejs/firedoor.threejs.ts
  60. 0 1
      src/views/vent/monitorManager/compressor/components/nitrogenHome.vue
  61. 12 9
      src/views/vent/monitorManager/compressor/index.vue
  62. 1 0
      src/views/vent/monitorManager/deviceMonitor/components/device/device.data.ts
  63. 24 25
      src/views/vent/monitorManager/deviceMonitor/components/device/modal/fiber.modal.sw.vue
  64. 24 25
      src/views/vent/monitorManager/deviceMonitor/components/device/modal/fiber.modal.yjl.vue
  65. 2927 0
      src/views/vent/monitorManager/fireDoorMonitor/components/fireDoorSVG.vue
  66. 61 0
      src/views/vent/monitorManager/gasPumpMonitor/gasPump.data.bet.ts
  67. 364 0
      src/views/vent/monitorManager/gateMonitor/components/GateShrinkSVG.vue
  68. 384 0
      src/views/vent/monitorManager/gateMonitor/components/GateYeYaSVG.vue
  69. 524 524
      src/views/vent/monitorManager/gateMonitor/index.vue
  70. 767 804
      src/views/vent/monitorManager/mainFanMonitor/index.vue
  71. 10 5
      src/views/vent/monitorManager/mainFanMonitor/main.data.ts
  72. 2 2
      src/views/vent/monitorManager/nitrogen/nitrogen.data.1.ts
  73. 5 5
      src/views/vent/monitorManager/warningMonitor/warning.api.ts
  74. 396 0
      src/views/vent/monitorManager/windowMonitor/components/windowDoorBltSVG.vue
  75. 136 0
      src/views/vent/packageUpload/components/DeployHistory.vue
  76. 289 0
      src/views/vent/packageUpload/components/PackageUpload.vue
  77. 26 269
      src/views/vent/packageUpload/index.vue
  78. 7 0
      src/views/vent/packageUpload/package.api.ts
  79. 7 7
      src/views/vent/performance/fileDetail/fileDetail.api.ts
  80. 10 11
      src/views/vent/reportManager/reportManager.api.ts
  81. 1 1
      src/views/ventAI/dataPicker/index.ts
  82. 34 0
      src/views/ventAI/dataPicker/useDataPicker.ts
  83. 84 0
      src/views/ventAI/manageAssistent/api.ts
  84. 86 9
      src/views/ventAI/manageAssistent/components/AiAssistantModal.vue
  85. 791 11
      src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue
  86. 12 18
      src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue
  87. 108 21
      src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue
  88. 24 1
      src/views/ventAI/manageAssistent/components/chatModal/utils.ts

+ 3 - 8
.editorconfig

@@ -1,14 +1,9 @@
 root = true
 
 [*]
-charset=utf-8
-end_of_line=lf
-insert_final_newline=true
-indent_style=space
-indent_size=2
-max_line_length = 100
-
-[*.{yml,yaml,json}]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
 indent_style = space
 indent_size = 2
 

+ 1 - 1
.env.development

@@ -32,5 +32,5 @@ VITE_GLOB_API_URL_PREFIX=
 # VITE_APP_SUB_APP = [["micro-vent-3dModal", "//192.168.183.154:8091/", "micro-vent-3dModal"], ["micro-need-air", "//192.168.183.88:8093/", "micro-need-air"], ["micro-fire-front", "//localhost:8097/", "fire-Micro"]]
 # VITE_APP_SUB_APP = [["micro-vent-3dModal", "//192.168.1.16:8091/", "micro-vent-3dModal"], ["micro-need-air", "//192.168.183.88:8093/", "micro-need-air"], ["micro-fire-front", "//localhost:8097/", "fire-Micro"]]
 # VITE_APP_SUB_APP = [["micro-vent-3dModal", "//182.92.126.35:8091", "micro-vent-3dModal"],["micro-vent-2dModal", "//localhost:8088", "micro-vent-2dModal"],["micro-vent-doc", "//localhost:5173", "micro-vent-doc"],["micro-need-air", "//182.92.126.35:8093", "micro-need-air"], ["micro-fire-front", "//182.92.126.35:8097", "fire-Micro"]]
-VITE_APP_SUB_APP = [["micro-vent-3dModal", "//192.168.1.16:8091", "micro-vent-3dModal"],["micro-vent-2dModal", "//localhost:8088", "micro-vent-2dModal"],["micro-vent-doc", "//localhost:5173", "micro-vent-doc"],["micro-need-air", "//182.92.126.35:8093", "micro-need-air"], ["micro-fire-front", "//182.92.126.35:8097", "fire-Micro"]]
+VITE_APP_SUB_APP = [["micro-vent-3dModal", "//192.168.1.23:8091", "micro-vent-3dModal"],["micro-vent-2dModal", "//localhost:8088", "micro-vent-2dModal"],["micro-vent-doc", "//localhost:5173", "micro-vent-doc"],["micro-need-air", "//192.168.1.21:8099", "micro-need-air"], ["micro-fire-front", "//182.92.126.35:8097", "fire-Micro"]]
 # VITE_APP_SUB_APP = [["micro-vent-3dModal", "//localhost:8091/"], ["micro-need-air", "//localhost:8099/"], ["micro-fire-front", "//localhost:8090/"]]

+ 7 - 3
build/vite/plugin/styleImport.ts

@@ -58,6 +58,7 @@ export function configStyleImportPlugin(_isBuild: boolean) {
             'input-password': 'input',
             'input-search': 'input',
             'input-group': 'input',
+            'textarea': 'input',
             'radio-group': 'radio',
             'checkbox-group': 'checkbox',
             'layout-sider': 'layout',
@@ -69,12 +70,15 @@ export function configStyleImportPlugin(_isBuild: boolean) {
             'image-preview-group': 'image',
             'time-range-picker': 'time-picker',
           };
-
+          // 处理textarea组件的样式引入问题
+          if (name === 'textarea') {
+            return `ant-design-vue/es/input/style/index`;
+          }
           return ignoreList.includes(name)
             ? ''
             : replaceList.hasOwnProperty(name)
-            ? `ant-design-vue/es/${replaceList[name]}/style/index`
-            : `ant-design-vue/es/${name}/style/index`;
+              ? `ant-design-vue/es/${replaceList[name]}/style/index`
+              : `ant-design-vue/es/${name}/style/index`;
         },
       },
     ],

+ 32 - 16
index.html

@@ -1,4 +1,4 @@
-<!DOCTYPE html>
+<!doctype html>
 <html lang="zh_CN" id="htmlRoot">
   <head>
     <meta charset="UTF-8" />
@@ -25,9 +25,9 @@
         }
         const remoteUrl = window.location.hostname === 'localhost' ? '182.92.126.35' : window.location.hostname;
         var script = document.createElement('script');
-          script.type = 'text/javascript';
-          script.src = 'http://'+remoteUrl+':9050/web-apps/apps/api/documents/api.js';
-          document.head.appendChild(script);
+        script.type = 'text/javascript';
+        script.src = 'http://' + remoteUrl + ':9050/web-apps/apps/api/documents/api.js';
+        document.head.appendChild(script);
       })();
     </script>
     <div id="app">
@@ -43,8 +43,8 @@
           background-color: none;
           background-image: url('./src/assets/images/themify/deepblue/loading-bg.png');
         }
-        body{
-          background-color: var(--vent-base-color)
+        body {
+          background-color: var(--vent-base-color);
         }
         .app-loading {
           display: flex;
@@ -273,35 +273,51 @@
         @keyframes before {
           0% {
             width: 0.5em;
-            box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75), -1em 0.5em rgba(111, 202, 220, 0.75);
+            box-shadow:
+              1em -0.5em rgba(225, 20, 98, 0.75),
+              -1em 0.5em rgba(111, 202, 220, 0.75);
           }
           35% {
             width: 2.5em;
-            box-shadow: 0 -0.5em rgba(225, 20, 98, 0.75), 0 0.5em rgba(111, 202, 220, 0.75);
+            box-shadow:
+              0 -0.5em rgba(225, 20, 98, 0.75),
+              0 0.5em rgba(111, 202, 220, 0.75);
           }
           70% {
             width: 0.5em;
-            box-shadow: -1em -0.5em rgba(225, 20, 98, 0.75), 1em 0.5em rgba(111, 202, 220, 0.75);
+            box-shadow:
+              -1em -0.5em rgba(225, 20, 98, 0.75),
+              1em 0.5em rgba(111, 202, 220, 0.75);
           }
           100% {
-            box-shadow: 1em -0.5em rgba(225, 20, 98, 0.75), -1em 0.5em rgba(111, 202, 220, 0.75);
+            box-shadow:
+              1em -0.5em rgba(225, 20, 98, 0.75),
+              -1em 0.5em rgba(111, 202, 220, 0.75);
           }
         }
         @keyframes after {
           0% {
             height: 0.5em;
-            box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75), -0.5em -1em rgba(233, 169, 32, 0.75);
+            box-shadow:
+              0.5em 1em rgba(61, 184, 143, 0.75),
+              -0.5em -1em rgba(233, 169, 32, 0.75);
           }
           35% {
             height: 2.5em;
-            box-shadow: 0.5em 0 rgba(61, 184, 143, 0.75), -0.5em 0 rgba(233, 169, 32, 0.75);
+            box-shadow:
+              0.5em 0 rgba(61, 184, 143, 0.75),
+              -0.5em 0 rgba(233, 169, 32, 0.75);
           }
           70% {
             height: 0.5em;
-            box-shadow: 0.5em -1em rgba(61, 184, 143, 0.75), -0.5em 1em rgba(233, 169, 32, 0.75);
+            box-shadow:
+              0.5em -1em rgba(61, 184, 143, 0.75),
+              -0.5em 1em rgba(233, 169, 32, 0.75);
           }
           100% {
-            box-shadow: 0.5em 1em rgba(61, 184, 143, 0.75), -0.5em -1em rgba(233, 169, 32, 0.75);
+            box-shadow:
+              0.5em 1em rgba(61, 184, 143, 0.75),
+              -0.5em -1em rgba(233, 169, 32, 0.75);
           }
         }
       </style>
@@ -323,11 +339,11 @@
             <div class="child-common child9"></div>
             <div class="child-common child10"></div>
           </div>
-          <div class="app-loading-title"><%= title %></div>
+          <div class="app-loading-title">正在加载</div>
         </div>
       </div>
     </div>
-    
+
     <script type="module" src="/src/main.ts"></script>
     <script>
       // var _hmt = _hmt || [];

+ 17 - 1
package.json

@@ -19,7 +19,8 @@
     "gen:icon": "esno ./build/generate/icon/index.ts",
     "batch:prettier": "prettier --write  \"./src/**/*.{js,json,tsx,css,less,scss,vue,html,md}\"",
     "upgrade:log": "conventional-changelog -p angular -i CHANGELOG.md -s",
-    "husky:install": "husky install"
+    "husky:install": "husky install",
+    "prepare": "husky install"
   },
   "dependencies": {
     "@ant-design/colors": "^7.0.0",
@@ -59,6 +60,7 @@
     "html2canvas": "^1.4.1",
     "katex": "^0.16.25",
     "lodash-es": "^4.17.21",
+    "magic-string": "^1.1.0",
     "marked": "^17.0.1",
     "mky-svg": "^1.0.2",
     "nprogress": "^0.2.0",
@@ -191,6 +193,20 @@
   "engines": {
     "node": "^12 || >=14"
   },
+  "lint-staged": {
+    "*.{js,jsx,ts,tsx,vue}": [
+      "prettier --write",
+      "eslint --fix",
+      "stylelint --fix"
+    ],
+    "*.{css,less,scss}": [
+      "prettier --write",
+      "stylelint --fix"
+    ],
+    "*.{html,json,md}": [
+      "prettier --write"
+    ]
+  },
   "vite": {
     "optimizeDeps": {
       "include": [

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 344 - 338
pnpm-lock.yaml


+ 22 - 10
prettier.config.js

@@ -1,20 +1,32 @@
 module.exports = {
+  // 行宽
   printWidth: 150,
+  // 缩进
   tabWidth: 2,
   useTabs: false,
-  semi: true, //语句末尾使用分号
-  vueIndentScriptAndStyle: true,
-  singleQuote: true, // 使用单引号
+  // 分号
+  semi: true,
+  // 引号
+  singleQuote: true,
+  jsxSingleQuote: false,
   quoteProps: 'as-needed',
-  bracketSpacing: true,
+  // 尾逗号
   trailingComma: 'es5',
-  jsxBracketSameLine: false,
-  jsxSingleQuote: false,
+  // 括号空格
+  bracketSpacing: true,
+  bracketSameLine: false,
+  // 箭头函数括号
   arrowParens: 'always',
-  insertPragma: false,
-  requirePragma: false,
-  proseWrap: 'never',
+  // Vue
+  vueIndentScriptAndStyle: true,
+  // 换行符 — 与 .editorconfig 保持统一
+  endOfLine: 'lf',
+  // HTML 空白
   htmlWhitespaceSensitivity: 'strict',
-  endOfLine: 'auto',
+  // Markdown
+  proseWrap: 'never',
+  // 范围 / pragma
   rangeStart: 0,
+  insertPragma: false,
+  requirePragma: false,
 };

+ 2 - 2
public/js/config.js

@@ -17,11 +17,11 @@ const VENT_PARAM = {
   modalText: '煤科通安(北京)智控科技有限公司研制', //国家能源集团沙吉海煤矿 //三维模型电子屏底部文字配置
   productionCrlPlatformUrl: '', // 神东要求主扇的控制功能改回跳转至生产管控平台,这里填写上生产管控地址后(例如填写:"https://www.baidu.com/"),就可以跳转了
   safetyCrlPlatformUrl: '', // 神东要求安全监控跳转至安全监控管控平台,这里填写上生产管控地址后(例如填写:"https://www.baidu.com/"),就可以跳转了
-  gasControlMock: true, // 项目关于瓦斯自主调控,是否模拟演示, true: 真实调控; false 模拟调控
   historyIsMultiple: false, // 设备历史数据是否支持多选
   isShowQy: true, // 是否显示气压
   is2DModel: false, // 是否尽最大可能使用2D模型
   isLoginByCode: false, // 登录页面是否有验证码
   loadText: '智能通风管控系统',
-  fanSwitchoverSimulation: true, // 不停风倒机模拟功能: true=启用模拟演示, false=使用实际控制
+  gasControlMock: false, // 项目关于瓦斯自主调控,是否模拟演示, true: 真实调控; false 模拟调控
+  fanSwitchoverSimulation: false, // 不停风倒机模拟功能: true=启用模拟演示, false=使用实际控制
 }

+ 6 - 3
src/api/sys/modelFile.ts

@@ -5,7 +5,10 @@ enum Api {
 }
 
 export const getAllModelFileJsonApi = () => {
-  return defHttp.get<Record<string, { fileName: string; filePath: string }[]>>({
-    url: Api.getAllModelFileJson,
-  });
+  return defHttp.get<Record<string, { fileName: string; filePath: string }[]>>(
+    {
+      url: Api.getAllModelFileJson,
+    },
+    { errorMessageMode: 'none' }
+  );
 };

+ 0 - 3
src/assets/images/ventAI/chatModalBlue/2-10.svg

@@ -1,3 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="16.836" height="18.704" viewBox="0 0 16.836 18.704">
-  <path id="路径_58030" data-name="路径 58030" d="M143.9,102.337a.935.935,0,1,1,0,1.871H128.936a.935.935,0,1,1,0-1.871H143.9ZM136.419,85.5a.935.935,0,0,1,.935.935v11.3l4.48-4.48a.935.935,0,0,1,1.4,1.234l-.079.088-6.081,6.085a.935.935,0,0,1-1.235.079l-.088-.078L129.678,94.6a.935.935,0,0,1,1.233-1.4l.088.078,4.484,4.475V86.439A.935.935,0,0,1,136.419,85.5Z" transform="translate(-128 -85.504)" fill="#1889ed"/>
-</svg>

+ 0 - 3
src/assets/images/ventAI/chatModalBlue/2-11.svg

@@ -1,3 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="18.843" height="18.705" viewBox="0 0 18.843 18.705">
-  <path id="路径_58031" data-name="路径 58031" d="M65.722,76.638H70.4l-5.141,5.139a.974.974,0,0,0,1.378,1.378l5.221-5.221v4.639a.975.975,0,0,0,1.95,0V75.661a.974.974,0,0,0-.975-.974h-7.11a.976.976,0,0,0,0,1.952ZM75.5,73.7h7.111a.974.974,0,0,0,0-1.949H77.939l5.141-5.141A.974.974,0,0,0,81.7,65.234L76.48,70.454V65.817a.975.975,0,1,0-1.95,0v6.911a.974.974,0,0,0,.975.974Z" transform="translate(-64.747 -64.842)" fill="#1889ed"/>
-</svg>

BIN
src/assets/images/ventAI/chatModalBlue/2-13.png


BIN
src/assets/images/ventAI/chatModalBlue/2-15.png


BIN
src/assets/images/ventAI/chatModalBlue/2-9.png


+ 3 - 0
src/assets/images/ventAI/chatModalBlue/3-1.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="7.789" height="11.035" viewBox="0 0 7.789 11.035">
+  <path id="路径_58148" data-name="路径 58148" d="M281.877,153.942v6.426h-1.3v-6.426l-1.363,1.363-.909-.974,2.921-2.921,2.921,2.921-.909.909Zm3.245-3.31h-7.789v-1.3h7.789Z" transform="translate(-277.333 -149.334)" fill="#fff"/>
+</svg>

+ 6 - 0
src/assets/images/ventAI/chatModalBlue/3-10.svg

@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="12.214" height="14.196" viewBox="0 0 12.214 14.196">
+  <g id="组_16859" data-name="组 16859" transform="translate(-470.331 -148.095)">
+    <path id="路径_58158" data-name="路径 58158" d="M107.97,34.133l.592.418a9.788,9.788,0,0,0,1.887.978,7.033,7.033,0,0,0,2.147.457l.981.014v.461l0,5.06a4.353,4.353,0,0,1-.607,2.114,7.535,7.535,0,0,1-1.433,1.791,8.152,8.152,0,0,1-1.786,1.278,4.065,4.065,0,0,1-1.78.512,4,4,0,0,1-1.77-.5,8.147,8.147,0,0,1-1.8-1.286,5.675,5.675,0,0,1-2.039-3.9V36l.981-.013a6.782,6.782,0,0,0,2.151-.458,9.667,9.667,0,0,0,1.889-.979Zm0,1.151-.025.02a10.972,10.972,0,0,1-2.088,1.089,7.738,7.738,0,0,1-2.5.524l-.041,0v4.6a4.779,4.779,0,0,0,1.745,3.229,7.115,7.115,0,0,0,1.573,1.127,3.163,3.163,0,0,0,1.335.4,5.108,5.108,0,0,0,2.906-1.531,4.773,4.773,0,0,0,1.744-3.228l0-4.6-.037,0a7.848,7.848,0,0,1-2.5-.524A10.9,10.9,0,0,1,108,35.305l-.027-.02Z" transform="translate(368.465 114.574)" fill="#cdeeff" stroke="rgba(0,0,0,0)" stroke-miterlimit="10" stroke-width="1" opacity="0.5"/>
+    <path id="路径_58159" data-name="路径 58159" d="M64.294,89.988h-.531a.242.242,0,0,0-.191.092L60.44,94.048l-1.5-1.9a.243.243,0,0,0-.191-.092h-.531a.061.061,0,0,0-.048.1l2.081,2.636a.243.243,0,0,0,.382,0l3.71-4.7a.06.06,0,0,0-.048-.1Z" transform="translate(415.182 62.815)" fill="#cdeeff" stroke="rgba(0,0,0,0)" stroke-miterlimit="10" stroke-width="1" opacity="0.5"/>
+  </g>
+</svg>

+ 3 - 0
src/assets/images/ventAI/chatModalBlue/3-11.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="11.228" height="13.501" viewBox="0 0 11.228 13.501">
+  <path id="路径_58160" data-name="路径 58160" d="M135.387,42.667a.568.568,0,0,1,.568.568v.284h1.7a.568.568,0,0,1,.568.568V54.6a.568.568,0,0,1-.568.568h-9.092A.568.568,0,0,1,128,54.6V44.087a.568.568,0,0,1,.568-.568h1.7v-.284a.568.568,0,0,1,.568-.568h4.546Zm-5.114,1.7h-1.421v9.944h8.524V44.371h-1.421v.284a.568.568,0,0,1-.568.568h-4.546a.568.568,0,0,1-.568-.568Zm2.7,5.967a.426.426,0,1,1,0,.852H130.7a.426.426,0,0,1,0-.852Zm2.557-2.557a.426.426,0,0,1,0,.852H130.7a.426.426,0,0,1,0-.852Zm-.426-4.262h-3.978v.852H135.1v-.852Z" transform="translate(-127.5 -42.167)" fill="#cdeeff" stroke="rgba(0,0,0,0)" stroke-miterlimit="10" stroke-width="1" opacity="0.5"/>
+</svg>

+ 3 - 0
src/assets/images/ventAI/chatModalBlue/3-12.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="11.214" height="13.083" viewBox="0 0 11.214 13.083">
+  <path id="路径_58157" data-name="路径 58157" d="M107.97,34.133l.592.418a9.788,9.788,0,0,0,1.887.978,7.033,7.033,0,0,0,2.147.457l.981.014v.461l0,5.06a4.353,4.353,0,0,1-.607,2.114,7.535,7.535,0,0,1-1.433,1.791,8.152,8.152,0,0,1-1.786,1.278,4.065,4.065,0,0,1-1.78.512,4,4,0,0,1-1.77-.5,8.147,8.147,0,0,1-1.8-1.286,5.675,5.675,0,0,1-2.039-3.9V36l.981-.013a6.782,6.782,0,0,0,2.151-.458,9.667,9.667,0,0,0,1.889-.979Zm0,1.151-.025.02a10.972,10.972,0,0,1-2.088,1.089,7.738,7.738,0,0,1-2.5.524l-.041,0v4.6a4.779,4.779,0,0,0,1.745,3.229,7.115,7.115,0,0,0,1.573,1.127,3.163,3.163,0,0,0,1.335.4,5.108,5.108,0,0,0,2.906-1.531,4.773,4.773,0,0,0,1.744-3.228l0-4.6-.037,0a7.848,7.848,0,0,1-2.5-.524A10.9,10.9,0,0,1,108,35.305l-.027-.02Zm0,6.8a.467.467,0,1,1-.467.467.467.467,0,0,1,.467-.467Zm0-4.209a.467.467,0,0,1,.467.467v2.8a.467.467,0,1,1-.935,0v-2.8a.467.467,0,0,1,.467-.467Z" transform="translate(-102.366 -34.133)" fill="#fff"/>
+</svg>

+ 3 - 0
src/assets/images/ventAI/chatModalBlue/3-2.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="10.407" height="11.447" viewBox="0 0 10.407 11.447">
+  <path id="路径_58147" data-name="路径 58147" d="M114.805,65.757h-.52v8.325a1.041,1.041,0,0,1-1.041,1.041H107a1.041,1.041,0,0,1-1.041-1.041V65.757h-.52a.52.52,0,0,1,0-1.041h3.122a1.041,1.041,0,0,1,1.041-1.041h1.041a1.041,1.041,0,0,1,1.041,1.041h3.122a.52.52,0,0,1,0,1.041Zm-1.561,0H107v8.325h6.244ZM109.082,66.8a.52.52,0,0,1,.52.52v5.2a.52.52,0,1,1-1.041,0v-5.2A.52.52,0,0,1,109.082,66.8Zm2.081,0a.52.52,0,0,1,.52.52v5.2a.52.52,0,1,1-1.041,0v-5.2A.52.52,0,0,1,111.163,66.8Z" transform="translate(-104.919 -63.676)" fill="#fff"/>
+</svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/3-3.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785310549625" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11695" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M924.672 126.976q36.864 36.864 54.784 82.432t17.92 93.696-17.92 93.696-54.784 82.432l-392.192 389.12q-36.864 36.864-90.624 61.44t-113.664 28.672-122.368-16.384-115.712-73.728q-52.224-52.224-72.704-113.152t-16.384-121.344 28.16-113.664 60.928-90.112l348.16-345.088q9.216-9.216 27.136-4.608t27.136 13.824q8.192 9.216 13.312 27.136t-4.096 27.136l-347.136 344.064q-27.648 27.648-46.08 64.512t-21.504 78.848 12.288 84.992 55.296 82.944q35.84 35.84 79.36 50.688t86.528 12.288 81.92-18.944 66.56-44.032l391.168-388.096q27.648-27.648 39.424-57.344t11.264-58.88-13.824-56.832-36.864-51.2q-44.032-43.008-98.816-40.448t-110.08 57.856l-353.28 351.232q-23.552 23.552-23.04 52.224t18.944 47.104q22.528 22.528 51.712 18.432t47.616-22.528l320.512-318.464q9.216-9.216 27.136-4.608t27.136 13.824 14.336 27.136-4.096 27.136l-321.536 318.464q-36.864 36.864-70.656 51.2t-63.488 12.8-55.296-15.872-47.104-34.816q-17.408-16.384-31.232-41.984t-15.872-56.32 10.752-65.536 49.664-70.656q18.432-18.432 32.768-33.792 12.288-13.312 23.04-23.552t11.776-11.264l285.696-284.672q36.864-36.864 80.384-57.856t88.576-24.064 88.576 12.288 80.384 52.224z" p-id="11696" fill="#ffffff"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/3-4.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785309784811" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7437" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 938.652444c-235.264 0-426.666667-191.402667-426.666667-426.666666s191.402667-426.666667 426.666667-426.666667 426.666667 191.402667 426.666667 426.666667c0 55.552-10.538667 109.639111-31.317334 160.739555a42.709333 42.709333 0 0 1-55.608889 23.438223 42.695111 42.695111 0 0 1-23.452444-55.608889A339.256889 339.256889 0 0 0 853.333333 511.985778c0-188.202667-153.130667-341.333333-341.333333-341.333334s-341.333333 153.130667-341.333333 341.333334 153.130667 341.333333 341.333333 341.333333c53.333333 0 104.391111-11.946667 151.736889-35.484444a42.666667 42.666667 0 0 1 38.001778 76.416A422.684444 422.684444 0 0 1 512 938.652444" fill="#ffffff" p-id="7438"></path><path d="M512 369.763556c-78.421333 0-142.222222 63.800889-142.222222 142.222222s63.800889 142.222222 142.222222 142.222222a142.222222 142.222222 0 0 0 142.222222-142.222222c0-78.421333-63.800889-142.222222-142.222222-142.222222m0 369.777777c-125.468444 0-227.555556-102.087111-227.555556-227.555555s102.087111-227.555556 227.555556-227.555556 227.555556 102.087111 227.555556 227.555556a227.498667 227.498667 0 0 1-227.555556 227.555555" fill="#ffffff" p-id="7439"></path><path d="M765.368889 782.791111c-7.822222 0-15.431111-0.768-22.755556-2.275555C689.749333 769.592889 654.222222 722.488889 654.222222 663.324444V327.096889a42.666667 42.666667 0 1 1 85.333334 0v336.213333c0 10.993778 2.631111 29.980444 20.337777 33.649778 19.569778 3.982222 49.322667-12.202667 68.750223-57.230222a42.680889 42.680889 0 0 1 78.364444 33.806222c-33.393778 77.312-91.562667 109.269333-141.653333 109.269333" fill="#ffffff" p-id="7440"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/3-5.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785309973161" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9438" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M643.7888 573.1328v-336.896c0-33.8944-27.5456-61.44-61.44-61.44H180.5312c-33.8944 0-61.44 27.5456-61.44 61.44v336.9984c0 33.8944 27.5456 61.44 61.44 61.44H230.4v54.8864c0 11.5712 6.4512 22.1184 16.7936 27.3408 4.4032 2.2528 9.216 3.3792 13.9264 3.3792 6.3488 0 12.6976-1.9456 18.0224-5.8368l109.9776-79.872h193.2288c33.8944 0 61.44-27.5456 61.44-61.44z m-257.9456 20.48c-6.5536 0-12.8 2.048-18.0224 5.8368L271.36 669.4912v-45.056c0-16.896-13.824-30.72-30.72-30.72h-60.1088c-11.264 0-20.48-9.216-20.48-20.48V236.2368c0-11.264 9.216-20.48 20.48-20.48h401.8176c11.264 0 20.48 9.216 20.48 20.48v336.9984c0 11.264-9.216 20.48-20.48 20.48H385.8432z" p-id="9439" fill="#ffffff"></path><path d="M843.4688 308.9408H708.7104c-11.264 0-20.48 9.216-20.48 20.48s9.216 20.48 20.48 20.48h134.7584c11.264 0 20.48 9.216 20.48 20.48v338.1248c0 11.264-9.216 20.48-20.48 20.48h-80.4864c-16.896 0-30.72 13.824-30.72 30.72v38.912l-96.0512-68.1984c-5.2224-3.6864-11.3664-5.632-17.8176-5.632h-95.3344c-11.264 0-20.48-9.216-20.48-20.48v-19.456c0-11.264-9.216-20.48-20.48-20.48s-20.48 9.216-20.48 20.48v19.456c0 33.8944 27.5456 61.44 61.44 61.44h92.0576l109.568 77.824c5.3248 3.7888 11.5712 5.7344 17.8176 5.7344 4.8128 0 9.6256-1.1264 14.1312-3.4816 10.24-5.3248 16.5888-15.7696 16.5888-27.3408V770.048h70.2464c33.8944 0 61.44-27.5456 61.44-61.44V370.3808c0-33.8944-27.5456-61.44-61.44-61.44z" p-id="9440" fill="#ffffff"></path></svg>

+ 1 - 0
src/assets/images/ventAI/chatModalBlue/3-6.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785310039627" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="10668" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M804.571429 73.142857H219.428571c-80.457143 0-146.285714 65.828571-146.285714 146.285714v585.142858c0 80.457143 65.828571 146.285714 146.285714 146.285714h585.142858c80.457143 0 146.285714-65.828571 146.285714-146.285714V219.428571c0-80.457143-65.828571-146.285714-146.285714-146.285714z m73.142857 731.428572c0 40.228571-32.914286 73.142857-73.142857 73.142857H219.428571c-40.228571 0-73.142857-32.914286-73.142857-73.142857V219.428571c0-40.228571 32.914286-73.142857 73.142857-73.142857h585.142858c40.228571 0 73.142857 32.914286 73.142857 73.142857v585.142858z m-512-73.142858h292.571428V658.285714H365.714286v73.142857zM219.428571 658.285714h73.142858V585.142857H219.428571v73.142857z m0-365.714285v73.142857h73.142858V292.571429H219.428571z m146.285715 219.428571H292.571429v73.142857h73.142857V512zM292.571429 365.714286v73.142857h73.142857V365.714286H292.571429z m146.285714 73.142857H365.714286v73.142857h73.142857V438.857143z" fill="#ffffff" p-id="10669"></path></svg>

+ 3 - 0
src/assets/images/ventAI/chatModalBlue/3-9.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="12.177" height="13.499" viewBox="0 0 12.177 13.499">
+  <path id="路径_58155" data-name="路径 58155" d="M137.832,66.594a1.374,1.374,0,0,0-.453.076v-.878a1.339,1.339,0,0,0-1.346-1.329,1.356,1.356,0,0,0-.6.138,1.349,1.349,0,0,0-2.425.04,1.427,1.427,0,0,0-.691-.178,1.487,1.487,0,0,0-1.465,1.5V69.12l-.215-.518-.009-.018a1.311,1.311,0,0,0-.76-.684,1.35,1.35,0,0,0-1.681,1.82l1.66,3.97.012.021a7.869,7.869,0,0,0,1.7,2.21,1.849,1.849,0,0,0,1.2.443h3.062a2.52,2.52,0,0,0,1.7-.841,4.718,4.718,0,0,0,1.118-1.805,12.561,12.561,0,0,0,.531-3.98V67.905a1.331,1.331,0,0,0-1.347-1.311Zm.452,3.142a11.725,11.725,0,0,1-.485,3.695,3.859,3.859,0,0,1-.9,1.457,1.694,1.694,0,0,1-1.067.581h-3.062a.951.951,0,0,1-.62-.227,6.958,6.958,0,0,1-1.49-1.941l-1.644-3.932-.008-.018a.444.444,0,0,1,.224-.585.457.457,0,0,1,.349-.02.422.422,0,0,1,.242.212l.9,2.172a.534.534,0,0,0,1.029-.206V65.968a.569.569,0,1,1,1.136-.077v3.727a.447.447,0,0,0,.895.009v-.909a.458.458,0,0,0,0-.065V65.968c0-.04,0-.081,0-.121V65.2a.452.452,0,0,1,.9,0v4.368a.453.453,0,0,0,.435.456.447.447,0,0,0,.46-.447V65.793a.452.452,0,0,1,.9,0v4.4a.453.453,0,0,0,.435.456.447.447,0,0,0,.46-.447v-2.3a.454.454,0,0,1,.9,0v1.831Z" transform="translate(-127.502 -63.364)" fill="#cdeeff" stroke="rgba(0,0,0,0)" stroke-miterlimit="10" stroke-width="1" opacity="0.5"/>
+</svg>

BIN
src/assets/images/ventAI/chatModalBlue/fullscreen/1-1.png


BIN
src/assets/images/ventAI/chatModalBlue/fullscreen/1-2.png


BIN
src/assets/images/ventAI/chatModalBlue/fullscreen/1-3.png


BIN
src/assets/images/ventAI/chatModalBlue/fullscreen/1-4.png


BIN
src/assets/images/ventAI/chatModalBlue/fullscreen/1-5.png


BIN
src/assets/images/ventAI/chatModalBlue/fullscreen/1-6.png


BIN
src/assets/images/ventAI/chatModalBlue/fullscreen/1-7.png


BIN
src/assets/images/ventAI/chatModalBlue/fullscreen/1-8.png


+ 36 - 0
src/assets/images/ventAI/chatModalBlue/fullscreen/1-9.svg

@@ -0,0 +1,36 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="31.028" height="37.013" viewBox="0 0 31.028 37.013">
+  <defs>
+    <filter id="矩形_3967" x="0" y="12.479" width="13.717" height="12.055" filterUnits="userSpaceOnUse">
+      <feOffset input="SourceAlpha"/>
+      <feGaussianBlur stdDeviation="1.5" result="blur"/>
+      <feFlood flood-color="#73ccff" flood-opacity="0.702"/>
+      <feComposite operator="in" in2="blur"/>
+      <feComposite in="SourceGraphic"/>
+    </filter>
+    <filter id="路径_45164" x="3.566" y="0" width="27.462" height="37.013" filterUnits="userSpaceOnUse">
+      <feOffset input="SourceAlpha"/>
+      <feGaussianBlur stdDeviation="3" result="blur-2"/>
+      <feFlood flood-color="#00b4ff" flood-opacity="0.502"/>
+      <feComposite operator="in" in2="blur-2"/>
+      <feComposite in="SourceGraphic"/>
+    </filter>
+    <filter id="路径_45150" x="4.717" y="9.354" width="13.854" height="18.307" filterUnits="userSpaceOnUse">
+      <feOffset input="SourceAlpha"/>
+      <feGaussianBlur stdDeviation="1.5" result="blur-3"/>
+      <feFlood flood-color="#73ccff" flood-opacity="0.702"/>
+      <feComposite operator="in" in2="blur-3"/>
+      <feComposite in="SourceGraphic"/>
+    </filter>
+  </defs>
+  <g id="组_16884" data-name="组 16884" transform="translate(-1426.704 -273.629)">
+    <g transform="matrix(1, 0, 0, 1, 1426.7, 273.63)" filter="url(#矩形_3967)">
+      <rect id="矩形_3967-2" data-name="矩形 3967" width="4.717" height="3.054" transform="translate(4.5 16.98)" fill="#fff"/>
+    </g>
+    <g transform="matrix(1, 0, 0, 1, 1426.7, 273.63)" filter="url(#路径_45164)">
+      <path id="路径_45164-2" data-name="路径 45164" d="M257.2,0V5.09l4.4,4.417-4.4,4.417v5.09l9.462-9.507Z" transform="translate(-244.63 9)" fill="#3ba3dc"/>
+    </g>
+    <g transform="matrix(1, 0, 0, 1, 1426.7, 273.63)" filter="url(#路径_45150)">
+      <path id="路径_45150-2" data-name="路径 45150" d="M185.274,4.653l-4.855,4.653V0Z" transform="translate(-171.2 13.85)" fill="#fff"/>
+    </g>
+  </g>
+</svg>

+ 3 - 0
src/assets/images/ventAI/dataPicker/AI-close.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="110.862" height="99.776" viewBox="0 0 110.862 99.776">
+  <path id="路径_58058" data-name="路径 58058" d="M146.307,148.685a11.086,11.086,0,1,0-11.086,0v12.572H104.734a5.543,5.543,0,0,0-5.543,5.543v55.431a5.543,5.543,0,0,0,5.543,5.543h72.06a5.543,5.543,0,0,0,5.543-5.543V166.8a5.543,5.543,0,0,0-5.543-5.543H146.307Zm-22.172,29.2a5.543,5.543,0,1,1-5.543,5.543A5.543,5.543,0,0,1,124.135,177.886Zm33.259,0a5.543,5.543,0,1,1-5.543,5.543A5.543,5.543,0,0,1,157.394,177.886ZM124.135,205.6a5.543,5.543,0,0,1,5.543-5.543h22.172a5.543,5.543,0,1,1,0,11.086H129.678A5.543,5.543,0,0,1,124.135,205.6ZM90.876,177.886a5.543,5.543,0,0,1,5.543,5.543V205.6a5.543,5.543,0,1,1-11.086,0V183.429a5.543,5.543,0,0,1,5.543-5.543Zm99.776,0a5.543,5.543,0,0,1,5.543,5.543V205.6a5.543,5.543,0,1,1-11.086,0V183.429a5.543,5.543,0,0,1,5.543-5.543Z" transform="translate(-85.333 -127.998)" fill="#6a7680"/>
+</svg>

+ 3 - 0
src/assets/images/ventAI/dataPicker/AI-open.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="110.862" height="99.776" viewBox="0 0 110.862 99.776">
+  <path id="路径_58053" data-name="路径 58053" d="M146.307,148.685a11.086,11.086,0,1,0-11.086,0v12.572H104.734a5.543,5.543,0,0,0-5.543,5.543v55.431a5.543,5.543,0,0,0,5.543,5.543h72.06a5.543,5.543,0,0,0,5.543-5.543V166.8a5.543,5.543,0,0,0-5.543-5.543H146.307Zm-22.172,29.2a5.543,5.543,0,1,1-5.543,5.543A5.543,5.543,0,0,1,124.135,177.886Zm33.259,0a5.543,5.543,0,1,1-5.543,5.543A5.543,5.543,0,0,1,157.394,177.886ZM124.135,205.6a5.543,5.543,0,0,1,5.543-5.543h22.172a5.543,5.543,0,1,1,0,11.086H129.678A5.543,5.543,0,0,1,124.135,205.6ZM90.876,177.886a5.543,5.543,0,0,1,5.543,5.543V205.6a5.543,5.543,0,1,1-11.086,0V183.429a5.543,5.543,0,0,1,5.543-5.543Zm99.776,0a5.543,5.543,0,0,1,5.543,5.543V205.6a5.543,5.543,0,1,1-11.086,0V183.429a5.543,5.543,0,0,1,5.543-5.543Z" transform="translate(-85.333 -127.998)" fill="#00baff"/>
+</svg>

+ 21 - 4
src/design/themify/deepblue.less

@@ -89,12 +89,12 @@ html[data-theme='deepblue'] {
   --img-chat-send-icon: url('/src/assets/images/ventAI/chatModalBlue/2-6.svg');
   --img-chat-close-bg: url('/src/assets/images/ventAI/chatModalBlue/2-7.png');
   --img-chat-download-icon: url('/src/assets/images/ventAI/chatModalBlue/2-8.png');
-  --img-chat-ai-avatar: url('/src/assets/images/ventAI/chatModalBlue/2-9.png');
-  --img-chat-upload-icon: url('/src/assets/images/ventAI/chatModalBlue/2-10.svg');
-  --img-chat-close-icon: url('/src/assets/images/ventAI/chatModalBlue/2-11.svg');
   --img-chat-input-bg: url('/src/assets/images/ventAI/chatModalBlue/2-13.png');
-  --img-chat-collapse-bg: url('/src/assets/images/ventAI/chatModalBlue/2-15.png');
   --img-chat-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-7.svg');
+  --img-chat-popup-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-3.svg');
+  --img-chat-popup-mention-icon: url('/src/assets/images/ventAI/chatModalBlue/3-4.svg');
+  --img-chat-popup-session-icon: url('/src/assets/images/ventAI/chatModalBlue/3-5.svg');
+  --img-chat-popup-command-icon: url('/src/assets/images/ventAI/chatModalBlue/3-6.svg');
   --img-chat-scroll-bottom: url('/src/assets/images/ventAI/chatModalBlue/3-8.png');
   --img-chat-arrow-icon: url('/src/assets/images/ventAI/chatModalBlue/4-2.svg');
   --img-chat-copy-icon: url('/src/assets/images/ventAI/chatModalBlue/4-3.svg');
@@ -102,4 +102,21 @@ html[data-theme='deepblue'] {
   --img-chat-download-icon-btn: url('/src/assets/images/ventAI/chatModalBlue/4-5.svg');
   --img-chat-preview-icon: url('/src/assets/images/ventAI/chatModalBlue/4-6.svg');
   --img-chat-welcome-img: url('/src/assets/images/ventAI/chatModalBlue/5-1.png');
+  --img-chat-pin-icon: url('/src/assets/images/ventAI/chatModalBlue/3-1.svg');
+  --img-chat-delete-icon: url('/src/assets/images/ventAI/chatModalBlue/3-2.svg');
+  --img-chat-mode-confirm-icon: url('/src/assets/images/ventAI/chatModalBlue/3-9.svg');
+  --img-chat-mode-auto-icon: url('/src/assets/images/ventAI/chatModalBlue/3-10.svg');
+  --img-chat-mode-plan-icon: url('/src/assets/images/ventAI/chatModalBlue/3-11.svg');
+  --img-chat-mode-full-icon: url('/src/assets/images/ventAI/chatModalBlue/3-12.svg');
+
+  // 全屏模式图片
+  --img-chat-fs-header-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-1.png');
+  --img-chat-fs-modal-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-2.png');
+  --img-chat-fs-task-panel-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-3.png');
+  --img-chat-fs-panel-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-4.png');
+  --img-chat-fs-collapse-btn-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-5.png');
+  --img-chat-fs-options-item-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-6.png');
+  --img-chat-fs-options-item-active-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-7.png');
+  --img-chat-fs-header-title-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-8.png');
+  --img-chat-fs-header-title-icon: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-9.svg');
 }

+ 21 - 4
src/design/themify/default.less

@@ -95,12 +95,12 @@ html {
   --img-chat-copy-success-icon: url('/src/assets/images/ventAI/chatModalBlue/2-5.svg');
   --img-chat-send-icon: url('/src/assets/images/ventAI/chatModalBlue/2-6.svg');
   --img-chat-download-icon: url('/src/assets/images/ventAI/chatModalBlue/2-8.png');
-  --img-chat-ai-avatar: url('/src/assets/images/ventAI/chatModalBlue/2-9.png');
-  --img-chat-upload-icon: url('/src/assets/images/ventAI/chatModalBlue/2-10.svg');
-  --img-chat-close-icon: url('/src/assets/images/ventAI/chatModalBlue/2-11.svg');
   --img-chat-input-bg: url('/src/assets/images/ventAI/chatModalBlue/1-7.svg');
-  --img-chat-collapse-bg: url('/src/assets/images/ventAI/chatModalBlue/2-15.png');
   --img-chat-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-7.svg');
+  --img-chat-popup-attach-icon: url('/src/assets/images/ventAI/chatModalBlue/3-3.svg');
+  --img-chat-popup-mention-icon: url('/src/assets/images/ventAI/chatModalBlue/3-4.svg');
+  --img-chat-popup-session-icon: url('/src/assets/images/ventAI/chatModalBlue/3-5.svg');
+  --img-chat-popup-command-icon: url('/src/assets/images/ventAI/chatModalBlue/3-6.svg');
   --img-chat-scroll-bottom: url('/src/assets/images/ventAI/chatModalBlue/3-8.png');
   --img-chat-arrow-icon: url('/src/assets/images/ventAI/chatModalBlue/4-2.svg');
   --img-chat-copy-icon: url('/src/assets/images/ventAI/chatModalBlue/4-3.svg');
@@ -108,4 +108,21 @@ html {
   --img-chat-download-icon-btn: url('/src/assets/images/ventAI/chatModalBlue/4-5.svg');
   --img-chat-preview-icon: url('/src/assets/images/ventAI/chatModalBlue/4-6.svg');
   --img-chat-welcome-img: url('/src/assets/images/ventAI/chatModalBlue/5-1.png');
+  --img-chat-pin-icon: url('/src/assets/images/ventAI/chatModalBlue/3-1.svg');
+  --img-chat-delete-icon: url('/src/assets/images/ventAI/chatModalBlue/3-2.svg');
+  --img-chat-mode-confirm-icon: url('/src/assets/images/ventAI/chatModalBlue/3-9.svg');
+  --img-chat-mode-auto-icon: url('/src/assets/images/ventAI/chatModalBlue/3-10.svg');
+  --img-chat-mode-plan-icon: url('/src/assets/images/ventAI/chatModalBlue/3-11.svg');
+  --img-chat-mode-full-icon: url('/src/assets/images/ventAI/chatModalBlue/3-12.svg');
+
+  // 全屏模式图片
+  --img-chat-fs-header-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-1.png');
+  --img-chat-fs-modal-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-2.png');
+  --img-chat-fs-task-panel-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-3.png');
+  --img-chat-fs-panel-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-4.png');
+  --img-chat-fs-collapse-btn-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-5.png');
+  --img-chat-fs-options-item-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-6.png');
+  --img-chat-fs-options-item-active-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-7.png');
+  --img-chat-fs-header-title-bg: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-8.png');
+  --img-chat-fs-header-title-icon: url('/src/assets/images/ventAI/chatModalBlue/fullscreen/1-9.svg');
 }

+ 0 - 410
src/hooks/core/useThree copy.ts

@@ -1,410 +0,0 @@
-import * as THREE from 'three';
-import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';
-// 导入轨道控制器
-import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
-
-import gsap from 'gsap';
-
-import ResourceTracker from '/@/utils/threejs/ResourceTracker';
-const resourceTracker = new ResourceTracker();
-const track = resourceTracker.track.bind(resourceTracker);
-
-class UseThree {
-  container: HTMLCanvasElement;
-  camera: THREE.PerspectiveCamera | null = null;
-  scene: THREE.Scene | null = null;
-  renderer: THREE.WebGLRenderer | null = null;
-  orbitControls: OrbitControls | null = null;
-  giftLoader: THREE.Object3D | null = null;
-  animationMixer: THREE.AnimationMixer | null = null;
-  animationAction: THREE.AnimationAction | null = null;
-  clock: THREE.Clock = new THREE.Clock(); // 计时器
-  timeoutId: NodeJS.Timeout | null = null;
-  animationId: number = 0;
-
-  constructor(selector) {
-    this.animationId = 0;
-    this.container = document.querySelector(selector);
-    //初始化
-    this.init();
-    this.animate();
-    window.addEventListener('resize', this.resizeRenderer.bind(this));
-    // 添加滚动事件,鼠标滚动模型执行动画
-    // window.addEventListener('wheel', this.wheelRenderer.bind(this));
-  }
-  init() {
-    // 初始化场景
-    this.initScene();
-    // 初始化环境光
-    this.initLight();
-    // 初始化相机
-    this.initCamera();
-    //初始化渲染器
-    this.initRenderer();
-    // 初始化控制器
-    this.initControles();
-  }
-
-  initScene() {
-    this.scene = new THREE.Scene();
-    const axesHelper = new THREE.AxesHelper(5);
-    // this.scene.add(axesHelper);
-  }
-  initLight() {
-    const light = new THREE.AmbientLight(0xfafafa, 1);
-    light.position.set(0, 200, 200);
-    (this.scene as THREE.Scene).add(light);
-  }
-  initCamera() {
-    this.camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
-    this.camera.position.set(0, 0.2, 0.3);
-  }
-  initRenderer() {
-    this.renderer = new THREE.WebGLRenderer({ antialias: true });
-    // 设置屏幕像素比
-    this.renderer.setPixelRatio(window.devicePixelRatio);
-    // 设置渲染的尺寸
-    this.renderer.setSize(window.innerWidth, window.innerHeight);
-    // 色调映射
-    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
-    // 曝光程度
-    this.renderer.toneMappingExposure = 3;
-    this.container.appendChild(this.renderer.domElement);
-  }
-
-  initControles() {
-    this.orbitControls = new OrbitControls(this.camera as THREE.Camera, this.renderer?.domElement);
-  }
-
-  test() {
-    //
-    const planeGeometry = new THREE.PlaneGeometry(1, 1);
-    const meshStandardMaterial = new THREE.MeshStandardMaterial();
-    const mesh = new THREE.Mesh(planeGeometry, meshStandardMaterial);
-    mesh.position.set(0, -0.2, 0);
-    mesh.rotation.z = Math.PI / 4;
-    this.scene?.add(mesh);
-  }
-
-  setGLTFModel(modalName) {
-    const a = new Date().getTime() / 1000;
-    return new Promise(async (resolve, reject) => {
-      try {
-        const db = window['CustomDB'];
-        const modalArr = await db.modal.where('modalName').equals(modalName).toArray();
-        if (modalArr.length > 0) {
-          const modalValue = modalArr[0].modalVal;
-          new THREE.ObjectLoader().parse(modalValue, (e) => {
-            const group = e.children[0];
-
-            const cityMaterial = new THREE.MeshBasicMaterial({
-              color: new THREE.Color(0x0c016f),
-            });
-            group?.traverse((item: THREE.Mesh) => {
-              if (item.type === 'Mesh') {
-                item.material = cityMaterial;
-                this.setMaterial(item);
-              }
-            });
-            this.scene?.add(e.children[0]);
-            console.log(new Date().getTime() / 1000 - a + ' s');
-            resolve(this.scene);
-          });
-        }
-      } catch (error) {
-        reject('加载模型出错');
-      }
-    });
-  }
-
-  setMaterial(obj) {
-    obj.geometry.computeBoundingBox();
-    const { max, min } = obj.geometry.boundingBox;
-    const distance = max.y - min.y + 2;
-
-    obj.material.onBeforeCompile = (shader) => {
-      shader.uniforms.uDistance = {
-        value: distance,
-      };
-      shader.uniforms.uTopColor = {
-        value: new THREE.Color(0xaaaeff),
-      };
-      this.addGrad(shader);
-      this.addRadiusSpread(shader);
-      this.addLineSpread(shader);
-      this.addToSpread(shader);
-    };
-  }
-  addGrad(shader) {
-    shader.vertexShader = shader.vertexShader.replace(
-      '#include <common>',
-      `
-
-      #include <common>
-      varying vec3 vPosition;
-    `
-    );
-    shader.vertexShader = shader.vertexShader.replace(
-      '#include <fog_vertex>',
-      `
-
-      #include <fog_vertex>
-      vPosition = position;
-     `
-    );
-    shader.fragmentShader = shader.fragmentShader.replace(
-      '#include <common>',
-      `
-      #include <common>
-      uniform vec3 uTopColor;
-      uniform float uDistance;
-      varying vec3 vPosition;
-  
-      `
-    );
-
-    shader.fragmentShader = shader.fragmentShader.replace(
-      '#include <dithering_fragment>',
-      `
-      #include <dithering_fragment>
-
-      vec4 distGradColor = gl_FragColor;
-      // 设置混合的百分比
-      float gradMix = (vPosition.y + (uDistance / 2.0)) / uDistance / 5.0;
-      // 设置混合颜色
-      vec3 colorMix = mix(distGradColor.xyz, uTopColor, gradMix);
-      gl_FragColor = vec4(colorMix, 1.0);
-      //#end#
-      `
-    );
-  }
-
-  addRadiusSpread(shader) {
-    // 设置扩散中心店
-    shader.uniforms.uRadiusSpreadCenter = {
-      value: new THREE.Vector2(0, 0),
-    };
-    // 扩散时间
-    shader.uniforms.uRadiusSpreadTime = {
-      value: 0,
-    };
-
-    shader.uniforms.uRadiusSpreadWidth = {
-      value: 20,
-    };
-    shader.fragmentShader = shader.fragmentShader.replace(
-      '#include <common>',
-      `
-      #include <common>
-      uniform vec2 uRadiusSpreadCenter;
-      uniform float uRadiusSpreadTime;
-      uniform float uRadiusSpreadWidth;
-
-      `
-    );
-
-    shader.fragmentShader = shader.fragmentShader.replace(
-      '//#end#',
-      `
-      float spreadRadius = distance(vPosition.xz + vec2(-720, -550), uRadiusSpreadCenter);
-      // 扩散范围
-      float spreadIndex = -(spreadRadius - uRadiusSpreadTime) * (spreadRadius - uRadiusSpreadTime ) + uRadiusSpreadWidth;
-      if(spreadIndex > 0.0){
-        gl_FragColor = mix(gl_FragColor, vec4(1.0, 1.0, 1.0, 1.0), spreadIndex / uRadiusSpreadWidth );
-      }
-      //#end#
-      `
-    );
-    gsap.to(shader.uniforms.uRadiusSpreadTime, {
-      value: 250,
-      duration: 1,
-      ease: 'none',
-      repeat: -1,
-    });
-  }
-
-  addLineSpread(shader) {
-    // 设置扩散中心店
-    shader.uniforms.uLineSpreadCenter = {
-      value: new THREE.Vector2(0, 0),
-    };
-    // 扩散时间
-    shader.uniforms.uLineSpreadTime = {
-      value: 0,
-    };
-
-    shader.uniforms.uLineSpreadWidth = {
-      value: 20,
-    };
-    shader.fragmentShader = shader.fragmentShader.replace(
-      '#include <common>',
-      `
-      #include <common>
-      uniform vec2 uLineSpreadCenter;
-      uniform float uLineSpreadTime;
-      uniform float uLineSpreadWidth;
-
-      `
-    );
-
-    shader.fragmentShader = shader.fragmentShader.replace(
-      '//#end#',
-      `
-      float spreadLine = vPosition.x - 400.0;
-      // 扩散范围
-      float spreadLineIndex = -(spreadLine - uLineSpreadTime * 5.0) * (spreadLine - uLineSpreadTime * 5.0) + uLineSpreadWidth;
-      if(spreadLineIndex > 0.0){
-        gl_FragColor = mix(gl_FragColor, vec4(1.0, 1.0, 1.0, 1.0), spreadLineIndex / uLineSpreadWidth );
-      }
-      //#end#
-      `
-    );
-    gsap.to(shader.uniforms.uLineSpreadTime, {
-      value: 150,
-      duration: 2,
-      ease: 'none',
-      repeat: -1,
-    });
-  }
-
-  addToSpread(shader) {
-    // 设置扩散中心店
-    shader.uniforms.uTopSpreadCenter = {
-      value: new THREE.Vector2(0, 0),
-    };
-    // 扩散时间
-    shader.uniforms.uTopSpreadTime = {
-      value: 0,
-    };
-
-    shader.uniforms.uTopSpreadWidth = {
-      value: 20,
-    };
-    shader.fragmentShader = shader.fragmentShader.replace(
-      '#include <common>',
-      `
-      #include <common>
-      uniform vec2 uTopSpreadCenter;
-      uniform float uTopSpreadTime;
-      uniform float uTopSpreadWidth;
-
-      `
-    );
-
-    shader.fragmentShader = shader.fragmentShader.replace(
-      '//#end#',
-      `
-      float spreadTopLine = vPosition.y * 2.0;
-      // 扩散范围uTopSpreadWidth
-      float spreadTopIndex = -(spreadTopLine - uTopSpreadTime) * (spreadTopLine - uTopSpreadTime) + uTopSpreadWidth;
-      if(spreadTopIndex > 0.0){
-        gl_FragColor = mix(gl_FragColor, vec4(0.0, 1.0, 1.0, 1.0), spreadTopIndex / uTopSpreadWidth / uTopSpreadTime * 2.0);
-      }
-      //#end#
-      `
-    );
-    gsap.to(shader.uniforms.uTopSpreadTime, {
-      value: 200,
-      duration: 5,
-      ease: 'none',
-      repeat: -1,
-    });
-  }
-
-  addLine(geometry) {
-    const edges = new THREE.EdgesGeometry(geometry);
-    const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xff0000 }));
-    (this.scene as THREE.Scene).add(line);
-    // console.log(line);
-  }
-
-  /* 场景环境背景 */
-  setEnvMap(hdr) {
-    new RGBELoader().setPath('/public/model/hdr/').load(hdr + '.hdr', (texture) => {
-      texture.mapping = THREE.EquirectangularReflectionMapping;
-      (this.scene as THREE.Scene).background = texture;
-      (this.scene as THREE.Scene).environment = texture;
-    });
-  }
-
-  render() {
-    const delta = this.clock.getDelta();
-    this.renderer?.render(this.scene as THREE.Object3D, this.camera as THREE.Camera);
-    this.animationMixer?.update(delta);
-  }
-
-  animate() {
-    // this.renderer?.setAnimationLoop(this.render.bind(this));
-    if (this.animationId != -1) {
-      this.render();
-      this.animationId = requestAnimationFrame(this.animate.bind(this));
-    }
-  }
-  resizeRenderer() {
-    // 更新相机比例
-    (this.camera as THREE.PerspectiveCamera).aspect = window.innerWidth / window.innerHeight;
-    // 刷新相机矩阵
-    this.camera?.updateProjectionMatrix();
-    // 设置场景尺寸
-    this.renderer?.setSize(window.innerWidth, window.innerHeight);
-  }
-  wheelRenderer(e: WheelEvent) {
-    const timeScale = e.deltaY > 0 ? 1 : -1;
-    this.animationAction?.setEffectiveTimeScale(timeScale);
-    (this.animationAction as THREE.AnimationAction).paused = false;
-    this.animationAction?.play();
-    if (this.timeoutId) {
-      clearTimeout(this.timeoutId);
-    }
-    this.timeoutId = setTimeout(() => {
-      this.animationAction?.halt(0.5);
-    }, 500);
-  }
-  clearScene() {
-    this.scene?.children.forEach((obj: any) => {
-      if (obj.type === 'Group') {
-        obj.traverse(function (item: any) {
-          if (item.type === 'Mesh') {
-            item.geometry.dispose();
-            item.material.dispose();
-            !!item.clear && item.clear();
-          }
-        });
-      } else if (obj.material) {
-        obj.material.dispose();
-      } else if (obj.geometry) {
-        obj.geometry.dispose();
-      }
-      this.scene?.remove(obj);
-      !!obj.clear ?? obj.clear();
-      obj = null;
-    });
-    const gl = this.renderer?.domElement.getContext('webgl');
-    gl && gl?.getExtension('WEBGL_lose_context')?.loseContext();
-
-    this.renderer?.forceContextLoss();
-    this.renderer?.dispose();
-    this.camera = null;
-    this.orbitControls = null;
-    this.renderer.domElement = null;
-    this.renderer = null;
-
-    // css3dRender.domElement = null;
-    // css3dRender = null;
-    // model3D.innerHTML = '';
-    // css3D.innerHTML = '';
-    // model3D = null
-    // css3D = null
-
-    !!this.scene?.clear ?? this.scene?.clear();
-    cancelAnimationFrame(this.animationId);
-    this.animationId = -1;
-    // this.stats = null
-    // scene = null
-
-    THREE.Cache.clear();
-    console.log('3D环境已清理干净');
-  }
-}
-
-export default UseThree;

+ 84 - 110
src/hooks/vent/useThree.ts

@@ -166,7 +166,6 @@ class UseThree {
 
   setGLTFModel(modalNames, group = null, isBlender = false) {
     window['startTime'] = new Date().getTime();
-    const modelStore = useModelStore();
     return new Promise(async (resolve, reject) => {
       try {
         const gltfLoader = new GLTFLoader();
@@ -186,15 +185,12 @@ class UseThree {
             try {
               // 解析模型
               const modalNameStr = modalNameArr[i];
-              const data = modelStore.modelArr.get(modalNameStr) || null;
               let modalValue;
-              if (!data) {
-                const modalArr = await db.modal.where('modalName').equals(modalNameStr).toArray();
-                if (modalArr.length > 0) {
-                  modalValue = modalArr[0].modalVal;
-                }
-              } else {
-                modalValue = data.modalVal;
+              const modalArr = await db.modal.where('modalName').equals(modalNameStr).toArray();
+              if (modalArr.length > 0 && modalArr[0].modalVal.byteLength > 0) {
+                modalValue = modalArr[0].modalVal;
+              } else if (modalArr.length > 0) {
+                db.modal.delete(modalArr[0].id);
               }
               if (modalValue) {
                 gltfLoader.parse(
@@ -202,7 +198,6 @@ class UseThree {
                   '/model/glft/',
                   (gltf) => {
                     let object: THREE.Object3D = gltf.scene;
-                    // setModalCenter(object);
                     object.traverse((obj) => {
                       if (obj instanceof THREE.Mesh) {
                         obj.material.emissiveIntensity = 1;
@@ -233,35 +228,98 @@ class UseThree {
                         // obj.renderOrder = 1;
                       }
                     });
-
-                    if (object.children.length == 0) {
+                    if (isBlender) {
                       object = object.children[0];
                     }
                     object.animations = gltf.animations;
                     object.name = modalNameStr;
-                    group?.add(object);
+                    if (group) group.add(object);
                     childResolve(object);
                   },
                   (err) => {
                     console.log(err);
+                    childResolve(null);
                   }
                 );
               } else {
                 // 开启线程下载
-                gltfLoader.load(`${baseApiUrl}/sys/common/static/webfile/${modalNameStr}.glb`, async (glft) => {
-                  if (glft) {
-                    const object = glft.scene;
-                    object.name = modalNameStr;
-                    if (glft.animations.length > 0) {
-                      object.animations = glft.animations;
-                    }
-                    group?.add(object);
-                    childResolve(object);
-                    const modalArr = await db.modal.where('modalName').equals(modalNameStr).toArray();
-                    if (modalArr.length < 1) {
-                      saveModel(modalNameStr, `webfile/${modalNameStr}.glb`, 1);
+
+                const result = (await getList({ fileName: modalNameStr })) || [];
+                const file = result['records'][0];
+                debugger;
+                console.log('需要开启线程下载模型资源。。。。。');
+
+                // 如果是常村时
+                const token = getToken();
+                fetch(`${baseApiUrl}/sys/common/static/webfile/${modalNameStr}.glb`, {
+                  method: 'GET',
+                  cache: 'no-cache',
+                  headers: {
+                    'Content-Type': 'application/force-download',
+                    Authorization: token,
+                    'X-Access-Token': token,
+                    token: token,
+                  },
+                }).then(async (data) => {
+                  const arrayBuffer = await data.arrayBuffer();
+                  // 将资源存放到db
+                  const fileName = modalNameStr;
+                  const model = {
+                    modalName: fileName,
+                    modalVal: arrayBuffer,
+                    versionStr: fileName,
+                  };
+                  db.open();
+                  await db.modal.add(model);
+                  gltfLoader.parse(
+                    arrayBuffer,
+                    '/model/glft/',
+                    (gltf) => {
+                      let object: THREE.Object3D = gltf.scene;
+                      // setModalCenter(object);
+                      object.traverse((obj) => {
+                        if (obj instanceof THREE.Mesh) {
+                          obj.material.emissiveIntensity = 1;
+                          obj.material.emissiveMap = obj.material.map;
+                          obj.material.blending = THREE.CustomBlending;
+
+                          if (obj.material.opacity < 1) {
+                            obj.material.transparent = true;
+                          }
+                          if (obj.material.map) {
+                            obj.material.map.colorSpace = THREE.SRGBColorSpace;
+                            obj.material.map.flipY = false;
+                            obj.material.map.anisotropy = 1;
+                          }
+                          if (obj.material.emissiveMap) {
+                            obj.material.emissiveMap.colorSpace = THREE.SRGBColorSpace;
+                            obj.material.emissiveMap.flipY = false;
+                          }
+
+                          if (obj.material.map || obj.material.emissiveMap) {
+                            obj.material.needsUpdate = true;
+                          }
+
+                          // if (envMap) {
+                          //   obj.material.envMap = envMap;
+                          //   obj.material.envMapIntensity = 1;
+                          // }
+                          // obj.renderOrder = 1;
+                        }
+                      });
+                      if (isBlender) {
+                        object = object.children[0];
+                      }
+                      object.animations = gltf.animations;
+                      object.name = modalNameStr;
+                      if (group) group.add(object);
+                      childResolve(object);
+                    },
+                    (err) => {
+                      console.log(err);
+                      childResolve(null);
                     }
-                  }
+                  );
                 });
               }
             } catch (error) {
@@ -280,90 +338,6 @@ class UseThree {
     });
   }
 
-  // setFBXModel(modalNames, group = null) {
-  //   window['startTime'] = new Date().getTime();
-  //   const modelStore = useModelStore();
-  //   return new Promise(async (resolve, reject) => {
-  //     try {
-  //       const fbxLoader = new FBXLoader();
-
-  //       fbxLoader.setPath('/model/fbx/');
-
-  //       const db = window['CustomDB'];
-  //       const resolvePromise: Promise<any>[] = [];
-
-  //       let modalNameArr = Object.prototype.toString.call(modalNames) === '[object Array]' ? modalNames : [modalNames];
-  //       let len = modalNameArr.length;
-
-  //       for (let i = 0; i < len; i++) {
-  //         resolvePromise[i] = new Promise(async (childResolve, reject) => {
-  //           try {
-  //             // 解析模型
-  //             let modalValue;
-  //             const modalNameStr = modalNameArr[i];
-  //             let data = modelStore.modelArr.get(modalNameStr) || null;
-  //             if (!data) {
-  //               const modalArr = await db.modal.where('modalName').equals(modalNameStr).toArray();
-  //               if (modalArr.length > 0) modalValue = modalArr[0].modalVal;
-  //             } else {
-  //               modalValue = data.modalVal;
-  //             }
-  //             if (modalValue) {
-  //               const object = fbxLoader.parse(modalValue, '/model/fbx/');
-  //               // const object = fbx.scene;
-  //               // setModalCenter(object);
-  //               if (object) {
-  //                 object.traverse((obj) => {
-  //                   if (obj instanceof THREE.Mesh) {
-  //                     obj.material.emissiveIntensity = 1;
-  //                     obj.material.emissiveMap = obj.material.map;
-  //                     obj.material.blending = THREE.CustomBlending;
-  //                     if (obj.material.opacity < 1) {
-  //                       obj.material.transparent = true;
-  //                     }
-  //                     if (obj.material.map) {
-  //                       obj.material.map.encoding = THREE.sRGBEncoding;
-  //                       obj.material.map.flipY = false;
-  //                       obj.material.map.anisotropy = 1;
-  //                     }
-  //                     if (obj.material.emissiveMap) {
-  //                       obj.material.emissiveMap.encoding = THREE.sRGBEncoding;
-  //                       obj.material.emissiveMap.flipY = false;
-  //                     }
-
-  //                     if (obj.material.map || obj.material.emissiveMap) {
-  //                       obj.material.needsUpdate = true;
-  //                     }
-
-  //                     // if (envMap) {
-  //                     //   obj.material.envMap = envMap;
-  //                     //   obj.material.envMapIntensity = 1;
-  //                     // }
-  //                     // obj.renderOrder = 1;
-  //                   }
-  //                 });
-  //                 object.animations = object.animations;
-  //                 object.name = modalNameStr;
-  //                 group?.add(object);
-  //                 childResolve(object);
-  //               }
-  //             }
-  //           } catch (error) {
-  //             console.log(error);
-  //             reject();
-  //           }
-  //         });
-  //       }
-
-  //       Promise.all(resolvePromise).then((objects) => {
-  //         resolve(objects);
-  //       });
-  //     } catch (error) {
-  //       reject('加载模型出错');
-  //     }
-  //   });
-  // }
-
   setTestPlane() {
     const plane = new THREE.Mesh(new THREE.PlaneGeometry(10, 10), new THREE.MeshPhongMaterial({ color: 0x999999, specular: 0x101010 }));
     plane.rotation.x = -Math.PI / 2;

+ 251 - 245
src/layouts/default/header/components/VoiceBroadcast.vue

@@ -16,6 +16,7 @@
         <div class="badge-box">
           <ClearOutlined style="font-size: 20px; color: #fff" @click="clearInfo" />
           <SoundOutlined :class="{ 'no-play': !isBroad }" style="font-size: 20px; color: #fff" @click="handleBroad" />
+          <CloseOutlined style="font-size: 20px; color: #fff" @click="closeWarningBroad" />
         </div>
       </div>
       <div class="broadcast-context">
@@ -46,292 +47,297 @@
   </div>
 </template>
 <script lang="ts">
-  import { Tooltip, Badge } from 'ant-design-vue';
-  import { SoundOutlined, ClearOutlined, BellOutlined, WarningOutlined } from '@ant-design/icons-vue';
-  import Icon from '/@/components/Icon';
-  import { defineComponent, ref, unref, onMounted, nextTick } from 'vue';
-  import { defHttp } from '/@/utils/http/axios';
-  import { useRouter } from 'vue-router';
-  import { connectWebSocket, onWebSocket } from '/@/hooks/web/useWebSocket';
-  import { getToken } from '/@/utils/auth';
-  import { useUserStore } from '/@/store/modules/user';
-  import { useGlobSetting } from '/@/hooks/setting';
-  import SpeakVoice from './notify/speakVoice';
-  import { useDrag } from '@/hooks/event/useDrag';
-  import dayjs from 'dayjs';
-  export default defineComponent({
-    name: 'VoiceBroadcast',
-    components: { Icon, Tooltip, Badge, SoundOutlined, ClearOutlined, BellOutlined, WarningOutlined },
+import { Tooltip, Badge } from 'ant-design-vue';
+import { SoundOutlined, ClearOutlined, BellOutlined, WarningOutlined, CloseOutlined } from '@ant-design/icons-vue';
+import Icon from '/@/components/Icon';
+import { defineComponent, ref, unref, onMounted, nextTick } from 'vue';
+import { defHttp } from '/@/utils/http/axios';
+import { useRouter } from 'vue-router';
+import { connectWebSocket, onWebSocket } from '/@/hooks/web/useWebSocket';
+import { getToken } from '/@/utils/auth';
+import { useUserStore } from '/@/store/modules/user';
+import { useGlobSetting } from '/@/hooks/setting';
+import SpeakVoice from './notify/speakVoice';
+import { useDrag } from '@/hooks/event/useDrag';
+import dayjs from 'dayjs';
+export default defineComponent({
+  name: 'VoiceBroadcast',
+  components: { Icon, Tooltip, Badge, SoundOutlined, ClearOutlined, BellOutlined, WarningOutlined, CloseOutlined },
 
-    setup() {
-      // debugger;
-      let speakVoice;
-      const userStore = useUserStore();
-      const glob = useGlobSetting();
-      const router = useRouter();
-      const list = (params) => defHttp.get({ url: '/safety/ventanalyAlarmLog/list', params });
-      const cleanAlarm = () => defHttp.get({ url: '/safety/ventanalyAlarmLog/cleanAlarmLog' });
-      const activeKey = ref(0);
-      const isShowWarningBroad = ref(false);
-      const isBroad = ref(true);
-      const isWarningDot = ref(false);
-      const broadcastList = ref([]);
-      function showWarningBroad() {
-        isShowWarningBroad.value = !isShowWarningBroad.value;
-        if (isShowWarningBroad.value) {
-          toSelectList(0);
-          nextTick(() => {
-            const dom = document.getElementById('VoiceBroadcast');
-            if (dom) useDrag(dom);
-          });
-        }
-      }
-      function handleBroad() {
-        isBroad.value = !isBroad.value;
+  setup() {
+    // debugger;
+    let speakVoice;
+    const userStore = useUserStore();
+    const glob = useGlobSetting();
+    const router = useRouter();
+    const list = (params) => defHttp.get({ url: '/safety/ventanalyAlarmLog/list', params });
+    const cleanAlarm = () => defHttp.get({ url: '/safety/ventanalyAlarmLog/cleanAlarmLog' });
+    const activeKey = ref(0);
+    const isShowWarningBroad = ref(false);
+    const isBroad = ref(true);
+    const isWarningDot = ref(false);
+    const broadcastList = ref([]);
+    function showWarningBroad() {
+      isShowWarningBroad.value = !isShowWarningBroad.value;
+      if (isShowWarningBroad.value) {
+        toSelectList(0);
+        nextTick(() => {
+          const dom = document.getElementById('VoiceBroadcast');
+          if (dom) useDrag(dom);
+        });
       }
+    }
+    function handleBroad() {
+      isBroad.value = !isBroad.value;
+    }
+    //点击关闭预警信息弹窗
+    function closeWarningBroad() {
+      isShowWarningBroad.value = false;
+    }
 
-      async function clearInfo() {
-        await cleanAlarm();
-        const res = await list({ pageSize: 20, devicetype: '', sort: 'createTime', delFlag: 0, nwartype_end: '999' });
-        broadcastList.value = res['records'];
-      }
+    async function clearInfo() {
+      await cleanAlarm();
+      const res = await list({ pageSize: 20, devicetype: '', sort: 'createTime', delFlag: 0, nwartype_end: '999' });
+      broadcastList.value = res['records'];
+    }
 
-      async function toSelectList(key) {
-        activeKey.value = key;
-        const res = await list({
-          pageSize: 20,
-          devicetype: '',
-          isok: key == 1 ? 0 : key == 2 ? 1 : null,
-          sort: 'createTime',
-          delFlag: 0,
-          nwartype_end: '999',
-        });
-        broadcastList.value = res['records'];
-        const isHasWarning = broadcastList.value.findIndex((item) => !item['isok']);
-        if (key !== 2) isWarningDot.value = isHasWarning > -1 ? true : false;
-      }
+    async function toSelectList(key) {
+      activeKey.value = key;
+      const res = await list({
+        pageSize: 20,
+        devicetype: '',
+        isok: key == 1 ? 0 : key == 2 ? 1 : null,
+        sort: 'createTime',
+        delFlag: 0,
+        nwartype_end: '999',
+      });
+      broadcastList.value = res['records'];
+      const isHasWarning = broadcastList.value.findIndex((item) => !item['isok']);
+      if (key !== 2) isWarningDot.value = isHasWarning > -1 ? true : false;
+    }
 
-      async function toMore() {
-        await router.push({ path: '/monitorChannel/device-monitor/warningHistory/home' });
-        showWarningBroad();
-      }
+    async function toMore() {
+      await router.push({ path: '/monitorChannel/device-monitor/warningHistory/home' });
+      showWarningBroad();
+    }
 
-      // 初始化 WebSocket
-      function initWebSocket() {
-        let token = getToken();
-        //将登录token生成一个短的标识
-        // let wsClientId = md5(token);
-        // let userId = unref(userStore.getUserInfo).id + '_' + wsClientId;
-        let userId = unref(userStore.getUserInfo).id + '?token=' + token;
-        console.log(userStore.getUserInfo, 'userId---');
-        // WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https
-        // let url = glob.wsUrl?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
-        let url = 'http://' + window.location.hostname + ':9999'?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
-        console.log(url, 'url------');
-        connectWebSocket(url);
-        onWebSocket(onWebSocketMessage);
-      }
+    // 初始化 WebSocket
+    function initWebSocket() {
+      let token = getToken();
+      //将登录token生成一个短的标识
+      // let wsClientId = md5(token);
+      // let userId = unref(userStore.getUserInfo).id + '_' + wsClientId;
+      let userId = unref(userStore.getUserInfo).id + '?token=' + token;
+      console.log(userStore.getUserInfo, 'userId---');
+      // WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https
+      // let url = glob.wsUrl?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
+      let url = 'http://' + window.location.hostname + ':9999'?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
+      console.log(url, 'url------');
+      connectWebSocket(url);
+      onWebSocket(onWebSocketMessage);
+    }
 
-      async function onWebSocketMessage(data) {
-        console.log('WebSocket 监测消息--------------》', data);
-        if (data.topic === 'warn' || data.cmd === 'user') {
-          if (isBroad.value) {
-            await speakVoice.getSpeechCnVoices();
-            const messageText = data['warndata'];
-            speakVoice.handleReply(messageText);
-            const time = dayjs().format('YYYY-MM-DD HH:mm:ss');
-            console.log(time + '语音播报开始报警------>', data);
-          }
-          if (!isShowWarningBroad.value) {
-            isWarningDot.value = true;
-          } else {
-            isWarningDot.value = false;
-          }
-          setTimeout(() => {
-            if (isShowWarningBroad.value) {
-              toSelectList(0);
-            }
-          }, 0);
+    async function onWebSocketMessage(data) {
+      console.log('WebSocket 监测消息--------------》', data);
+      if (data.topic === 'warn' || data.cmd === 'user') {
+        if (isBroad.value) {
+          await speakVoice.getSpeechCnVoices();
+          const messageText = data['warndata'];
+          speakVoice.handleReply(messageText);
+          const time = dayjs().format('YYYY-MM-DD HH:mm:ss');
+          console.log(time + '语音播报开始报警------>', data);
+        }
+        if (!isShowWarningBroad.value) {
+          isWarningDot.value = true;
+        } else {
+          isWarningDot.value = false;
         }
+        setTimeout(() => {
+          if (isShowWarningBroad.value) {
+            toSelectList(0);
+          }
+        }, 0);
       }
-      onMounted(() => {
-        speakVoice = new SpeakVoice();
-        nextTick(async () => {
-          initWebSocket();
-          await toSelectList(1);
-        });
-        window.speechSynthesis.onvoiceschanged = () => {
-          console.log('语音列表已更新');
-        };
+    }
+    onMounted(() => {
+      speakVoice = new SpeakVoice();
+      nextTick(async () => {
+        initWebSocket();
+        await toSelectList(1);
       });
-
-      return {
-        showWarningBroad,
-        isShowWarningBroad,
-        activeKey,
-        toSelectList,
-        broadcastList,
-        toMore,
-        isBroad,
-        handleBroad,
-        clearInfo,
-        isWarningDot,
+      window.speechSynthesis.onvoiceschanged = () => {
+        console.log('语音列表已更新');
       };
-    },
-  });
+    });
+
+    return {
+      showWarningBroad,
+      isShowWarningBroad,
+      activeKey,
+      toSelectList,
+      broadcastList,
+      toMore,
+      isBroad,
+      handleBroad,
+      clearInfo,
+      closeWarningBroad,
+      isWarningDot,
+    };
+  },
+});
 </script>
 <style lang="less" scoped>
-  .btn {
-    line-height: 30px;
-    margin-right: 10px;
-    cursor: pointer;
-    display: flex;
-  }
+.btn {
+  line-height: 30px;
+  margin-right: 10px;
+  cursor: pointer;
+  display: flex;
+}
 
-  .no-play {
-    position: relative;
-    &::after {
-      position: absolute;
-      width: 70%;
-      height: 100%;
-      content: '';
-      left: 15%;
-      top: 0;
-      background: linear-gradient(
-        to bottom left,
-        transparent 0%,
-        transparent calc(50% - 1px),
-        #ffffff 50%,
-        transparent calc(50% + 1px),
-        transparent 100%
-      );
-    }
+.no-play {
+  position: relative;
+  &::after {
+    position: absolute;
+    width: 70%;
+    height: 100%;
+    content: '';
+    left: 15%;
+    top: 0;
+    background: linear-gradient(
+      to bottom left,
+      transparent 0%,
+      transparent calc(50% - 1px),
+      #ffffff 50%,
+      transparent calc(50% + 1px),
+      transparent 100%
+    );
   }
+}
 
-  .broadcast {
-    width: 500px;
-    height: 350px;
-    border-radius: 4px;
-    position: fixed;
-    top: 57px;
-    right: 2px;
-    background-color: rgb(255, 255, 255);
-    background: url('../../../../assets/images/warn-dialog-bg.png') no-repeat center;
-    background-size: 100% 100%;
-    z-index: 9999999;
-    color: #fff;
+.broadcast {
+  width: 500px;
+  height: 350px;
+  border-radius: 4px;
+  position: fixed;
+  top: 57px;
+  right: 2px;
+  background-color: rgb(255, 255, 255);
+  background: url('../../../../assets/images/warn-dialog-bg.png') no-repeat center;
+  background-size: 100% 100%;
+  z-index: 9999999;
+  color: #fff;
 
-    .title {
-      height: 32px;
-      padding: 0 20px;
+  .title {
+    height: 32px;
+    padding: 0 20px;
 
-      :deep(.ant-badge:not(.ant-badge-status)) {
-        margin-right: 40px !important;
-      }
+    :deep(.ant-badge:not(.ant-badge-status)) {
+      margin-right: 40px !important;
+    }
 
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 5px;
+
+    .message-title {
+      font-size: 18px;
+      padding-top: 10px;
+    }
+
+    .badge-box {
       display: flex;
       align-items: center;
-      justify-content: space-between;
-      margin-bottom: 5px;
+      padding-top: 10px;
+      .badge-title {
+        display: inline-block;
+        width: 62px;
+        line-height: 32px;
+        background-color: #2174f0;
+        border-radius: 26px;
+        text-align: center;
+        color: #fff;
+        padding-bottom: 2px;
+      }
+    }
+  }
 
-      .message-title {
-        font-size: 18px;
-        padding-top: 10px;
+  .broadcast-context {
+    .context-tab {
+      display: flex;
+      padding: 20px;
+      .context-tab-item {
+        line-height: 24px;
+        background-color: #6b6b6b;
+        border-radius: 24px;
+        text-align: center;
+        padding: 0 10px;
+        color: #fff;
+        margin: 5px;
+        cursor: pointer;
+        font-size: 14px;
       }
 
-      .badge-box {
-        display: flex;
-        align-items: center;
-        padding-top: 10px;
-        .badge-title {
-          display: inline-block;
-          width: 62px;
-          line-height: 32px;
-          background-color: #2174f0;
-          border-radius: 26px;
-          text-align: center;
-          color: #fff;
-          padding-bottom: 2px;
-        }
+      .context-tab-item-active {
+        background-color: #2174f0;
       }
     }
 
-    .broadcast-context {
-      .context-tab {
-        display: flex;
-        padding: 20px;
-        .context-tab-item {
-          line-height: 24px;
-          background-color: #6b6b6b;
-          border-radius: 24px;
-          text-align: center;
-          padding: 0 10px;
-          color: #fff;
-          margin: 5px;
-          cursor: pointer;
-          font-size: 14px;
-        }
+    .context-box {
+      flex: 1;
+      padding: 0 10px;
+      height: 200px;
+      overflow-y: auto;
 
-        .context-tab-item-active {
-          background-color: #2174f0;
-        }
+      .no-context {
+        display: flex;
+        justify-content: center;
+        padding-top: 30px;
+        font-size: 16px;
       }
 
-      .context-box {
-        flex: 1;
-        padding: 0 10px;
-        height: 200px;
-        overflow-y: auto;
-
-        .no-context {
-          display: flex;
-          justify-content: center;
-          padding-top: 30px;
-          font-size: 16px;
-        }
+      .context-detail {
+        display: flex;
+        justify-content: space-between;
+        line-height: 24px;
+        padding: 0px 16px;
 
-        .context-detail {
+        div {
           display: flex;
-          justify-content: space-between;
-          line-height: 24px;
-          padding: 0px 16px;
+          justify-content: flex-start;
 
-          div {
-            display: flex;
-            justify-content: flex-start;
-
-            &:nth-child(1) {
-              width: 44%;
-            }
+          &:nth-child(1) {
+            width: 44%;
+          }
 
-            &:nth-child(2) {
-              width: 40%;
-            }
+          &:nth-child(2) {
+            width: 40%;
+          }
 
-            &:nth-child(3) {
-              width: 25%;
-            }
+          &:nth-child(3) {
+            width: 25%;
+          }
 
-            &:nth-child(4) {
-              width: 15%;
-            }
+          &:nth-child(4) {
+            width: 15%;
           }
         }
       }
-      .more {
-        position: absolute;
-        left: 24px;
-        bottom: 10px;
-        cursor: pointer;
+    }
+    .more {
+      position: absolute;
+      left: 24px;
+      bottom: 10px;
+      cursor: pointer;
 
-        &:hover {
-          color: #2174f0;
-        }
+      &:hover {
+        color: #2174f0;
       }
     }
   }
-  :deep(.zxm-badge-count) {
-    width: 8px;
-    height: 8px;
-  }
+}
+:deep(.zxm-badge-count) {
+  width: 8px;
+  height: 8px;
+}
 </style>

+ 238 - 232
src/layouts/default/header/components/VoiceBroadcastGsd.vue

@@ -12,6 +12,7 @@
         <div class="badge-box">
           <ClearOutlined style="font-size: 20px; color: #fff" @click="clearInfo" />
           <SoundOutlined :class="{ 'no-play': !isBroad }" style="font-size: 20px; color: #fff" @click="handleBroad" />
+          <CloseOutlined style="font-size: 20px; color: #fff" @click="closeWarningBroad" />
         </div>
       </div>
       <div class="broadcast-context">
@@ -34,279 +35,284 @@
   </div>
 </template>
 <script lang="ts">
-  import { SoundOutlined, ClearOutlined, BellOutlined, WarningOutlined } from '@ant-design/icons-vue';
-  import { defineComponent, ref, unref, onMounted, nextTick, computed } from 'vue';
-  import { useRouter } from 'vue-router';
-  import { connectWebSocket, onWebSocket } from '/@/hooks/web/useWebSocket';
-  import { getToken } from '/@/utils/auth';
-  import { useUserStore } from '/@/store/modules/user';
-  import { useGlobSetting } from '/@/hooks/setting';
-  import SpeakVoice from './notify/speakVoice';
+import { SoundOutlined, ClearOutlined, BellOutlined, WarningOutlined, CloseOutlined } from '@ant-design/icons-vue';
+import { defineComponent, ref, unref, onMounted, nextTick, computed } from 'vue';
+import { useRouter } from 'vue-router';
+import { connectWebSocket, onWebSocket } from '/@/hooks/web/useWebSocket';
+import { getToken } from '/@/utils/auth';
+import { useUserStore } from '/@/store/modules/user';
+import { useGlobSetting } from '/@/hooks/setting';
+import SpeakVoice from './notify/speakVoice';
 
-  import { useDrag } from '@/hooks/event/useDrag';
-  export default defineComponent({
-    name: 'VoiceBroadcast',
-    components: { SoundOutlined, ClearOutlined, BellOutlined, WarningOutlined },
+import { useDrag } from '@/hooks/event/useDrag';
+export default defineComponent({
+  name: 'VoiceBroadcast',
+  components: { SoundOutlined, ClearOutlined, BellOutlined, WarningOutlined, CloseOutlined },
 
-    setup() {
-      let speakVoice;
-      let broadcastList = ref<any[]>([]);
-      const userStore = useUserStore();
-      const glob = useGlobSetting();
-      const router = useRouter();
-      const activeKey = ref(0);
-      const isShowWarningBroad = ref(false);
-      const isBroad = ref(false);
-      const isWarningDot = ref(false);
+  setup() {
+    let speakVoice;
+    let broadcastList = ref<any[]>([]);
+    const userStore = useUserStore();
+    const glob = useGlobSetting();
+    const router = useRouter();
+    const activeKey = ref(0);
+    const isShowWarningBroad = ref(false);
+    const isBroad = ref(false);
+    const isWarningDot = ref(false);
 
-      //点击切换预警信息弹窗显示
-      function showWarningBroad() {
-        isShowWarningBroad.value = !isShowWarningBroad.value;
-        if (isShowWarningBroad.value) {
-          nextTick(() => {
-            const dom = document.getElementById('VoiceBroadcast');
-            if (dom) useDrag(dom);
-          });
-        }
-      }
-      function handleBroad() {
-        isBroad.value = !isBroad.value;
-      }
-      async function clearInfo() {
-        broadcastList.value = [];
-        localStorage.removeItem('messageArr');
+    //点击切换预警信息弹窗显示
+    function showWarningBroad() {
+      isShowWarningBroad.value = !isShowWarningBroad.value;
+      if (isShowWarningBroad.value) {
+        nextTick(() => {
+          const dom = document.getElementById('VoiceBroadcast');
+          if (dom) useDrag(dom);
+        });
       }
+    }
+    //点击关闭预警信息弹窗
+    function closeWarningBroad() {
+      isShowWarningBroad.value = false;
+    }
+    function handleBroad() {
+      isBroad.value = !isBroad.value;
+    }
+    async function clearInfo() {
+      broadcastList.value = [];
+      localStorage.removeItem('messageArr');
+    }
 
-      //点击跳转预警历史详情
-      async function toMore() {
-        await router.push({ path: '/monitorChannel/device-monitor/warningHistory/company/home' });
-        showWarningBroad();
-      }
+    //点击跳转预警历史详情
+    async function toMore() {
+      await router.push({ path: '/monitorChannel/device-monitor/warningHistory/company/home' });
+      showWarningBroad();
+    }
 
-      // 初始化 WebSocket
-      function initWebSocket() {
-        let token = getToken();
-        //将登录token生成一个短的标识
-        // let wsClientId = md5(token);
-        // let userId = unref(userStore.getUserInfo).id + '_' + wsClientId;
-        let userId = unref(userStore.getUserInfo).id + '?token=' + token;
-        // WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https
-        let url = 'http://' + window.location.hostname + ':9999'?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
-        connectWebSocket(url);
-        onWebSocket(onWebSocketMessage);
-      }
+    // 初始化 WebSocket
+    function initWebSocket() {
+      let token = getToken();
+      //将登录token生成一个短的标识
+      // let wsClientId = md5(token);
+      // let userId = unref(userStore.getUserInfo).id + '_' + wsClientId;
+      let userId = unref(userStore.getUserInfo).id + '?token=' + token;
+      // WebSocket与普通的请求所用协议有所不同,ws等同于http,wss等同于https
+      let url = 'http://' + window.location.hostname + ':9999'?.replace('https://', 'wss://').replace('http://', 'ws://') + '/websocket/' + userId;
+      connectWebSocket(url);
+      onWebSocket(onWebSocketMessage);
+    }
 
-      async function onWebSocketMessage(data) {
-        console.log('WebSocket 监测消息--------------》', data);
-        if (data.topic === 'warn') {
-          const messageText = data['warndata'];
-          const msgList = [...broadcastList.value];
-          if (msgList.length <= 20) {
-            msgList.push({ label: messageText });
-            localStorage.removeItem('messageArr');
-            localStorage.setItem('messageArr', msgList as any);
-          } else {
-            msgList.shift();
-            msgList.push({ label: messageText });
-            localStorage.removeItem('messageArr');
-            localStorage.setItem('messageArr', msgList as any);
-          }
-          broadcastList.value = msgList;
-          if (isBroad.value) {
-            await speakVoice.getSpeechCnVoices();
-            speakVoice.handleReply(messageText);
-            const time = dayjs().format('YYYY-MM-DD HH:mm:ss');
-            console.log(time + '语音播报开始报警------>', data);
-          }
-          //   const messageText = '这是一个测试';
-          if (!isShowWarningBroad.value) {
-            isWarningDot.value = true;
-          } else {
-            isWarningDot.value = false;
-          }
+    async function onWebSocketMessage(data) {
+      console.log('WebSocket 监测消息--------------》', data);
+      if (data.topic === 'warn') {
+        const messageText = data['warndata'];
+        const msgList = [...broadcastList.value];
+        if (msgList.length <= 20) {
+          msgList.push({ label: messageText });
+          localStorage.removeItem('messageArr');
+          localStorage.setItem('messageArr', msgList as any);
+        } else {
+          msgList.shift();
+          msgList.push({ label: messageText });
+          localStorage.removeItem('messageArr');
+          localStorage.setItem('messageArr', msgList as any);
+        }
+        broadcastList.value = msgList;
+        if (isBroad.value) {
+          await speakVoice.getSpeechCnVoices();
+          speakVoice.handleReply(messageText);
+          const time = dayjs().format('YYYY-MM-DD HH:mm:ss');
+          console.log(time + '语音播报开始报警------>', data);
+        }
+        //   const messageText = '这是一个测试';
+        if (!isShowWarningBroad.value) {
+          isWarningDot.value = true;
+        } else {
+          isWarningDot.value = false;
         }
       }
-      onMounted(() => {
-        speakVoice = new SpeakVoice();
-        nextTick(async () => {
-          initWebSocket();
-        });
-        window.speechSynthesis.onvoiceschanged = () => {
-          console.log('语音列表已更新');
-        };
+    }
+    onMounted(() => {
+      speakVoice = new SpeakVoice();
+      nextTick(async () => {
+        initWebSocket();
       });
-
-      return {
-        showWarningBroad,
-        isShowWarningBroad,
-        activeKey,
-        broadcastList,
-        toMore,
-        clearInfo,
-        isWarningDot,
-        handleBroad,
-        isBroad,
+      window.speechSynthesis.onvoiceschanged = () => {
+        console.log('语音列表已更新');
       };
-    },
-  });
+    });
+
+    return {
+      showWarningBroad,
+      isShowWarningBroad,
+      activeKey,
+      broadcastList,
+      toMore,
+      clearInfo,
+      isWarningDot,
+      handleBroad,
+      closeWarningBroad,
+      isBroad,
+    };
+  },
+});
 </script>
 <style lang="less" scoped>
-  .btn {
-    line-height: 30px;
-    margin-right: 10px;
-    cursor: pointer;
-    display: flex;
-  }
+.btn {
+  line-height: 30px;
+  margin-right: 10px;
+  cursor: pointer;
+  display: flex;
+}
 
-  .no-play {
-    position: relative;
-    &::after {
-      position: absolute;
-      width: 70%;
-      height: 100%;
-      content: '';
-      left: 15%;
-      top: 0;
-      background: linear-gradient(
-        to bottom left,
-        transparent 0%,
-        transparent calc(50% - 1px),
-        #ffffff 50%,
-        transparent calc(50% + 1px),
-        transparent 100%
-      );
-    }
+.no-play {
+  position: relative;
+  &::after {
+    position: absolute;
+    width: 70%;
+    height: 100%;
+    content: '';
+    left: 15%;
+    top: 0;
+    background: linear-gradient(
+      to bottom left,
+      transparent 0%,
+      transparent calc(50% - 1px),
+      #ffffff 50%,
+      transparent calc(50% + 1px),
+      transparent 100%
+    );
   }
+}
 
-  .broadcast {
-    width: 500px;
-    height: 350px;
-    border-radius: 4px;
-    position: fixed;
-    top: 50px;
-    right: 20px;
-    background-color: rgb(255, 255, 255);
-    background: url('../../../../assets/images/warn-dialog-bg.png') no-repeat center;
-    background-size: 100% 100%;
-    z-index: 9999999;
-    color: #fff;
+.broadcast {
+  width: 500px;
+  height: 350px;
+  border-radius: 4px;
+  position: fixed;
+  top: 50px;
+  right: 20px;
+  background-color: rgb(255, 255, 255);
+  background: url('../../../../assets/images/warn-dialog-bg.png') no-repeat center;
+  background-size: 100% 100%;
+  z-index: 9999999;
+  color: #fff;
 
-    .title {
-      height: 32px;
-      padding: 0 20px;
+  .title {
+    height: 32px;
+    padding: 0 20px;
 
-      :deep(.ant-badge:not(.ant-badge-status)) {
-        margin-right: 40px !important;
-      }
+    :deep(.ant-badge:not(.ant-badge-status)) {
+      margin-right: 40px !important;
+    }
 
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      margin-bottom: 5px;
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    margin-bottom: 5px;
 
-      .message-title {
-        font-size: 18px;
-        padding-top: 10px;
-      }
+    .message-title {
+      font-size: 18px;
+      padding-top: 10px;
+    }
 
-      .badge-box {
-        display: flex;
-        align-items: center;
-        padding-top: 10px;
+    .badge-box {
+      display: flex;
+      align-items: center;
+      padding-top: 10px;
 
-        .badge-title {
-          display: inline-block;
-          width: 62px;
-          line-height: 32px;
-          background-color: #2174f0;
-          border-radius: 26px;
-          text-align: center;
-          color: #fff;
-          padding-bottom: 2px;
-        }
+      .badge-title {
+        display: inline-block;
+        width: 62px;
+        line-height: 32px;
+        background-color: #2174f0;
+        border-radius: 26px;
+        text-align: center;
+        color: #fff;
+        padding-bottom: 2px;
       }
     }
+  }
 
-    .broadcast-context {
-      .context-tab {
-        display: flex;
-        padding: 20px;
+  .broadcast-context {
+    .context-tab {
+      display: flex;
+      padding: 20px;
 
-        .context-tab-item {
-          line-height: 24px;
-          background-color: #6b6b6b;
-          border-radius: 24px;
-          text-align: center;
-          padding: 0 10px;
-          color: #fff;
-          margin: 5px;
-          cursor: pointer;
-          font-size: 14px;
-        }
+      .context-tab-item {
+        line-height: 24px;
+        background-color: #6b6b6b;
+        border-radius: 24px;
+        text-align: center;
+        padding: 0 10px;
+        color: #fff;
+        margin: 5px;
+        cursor: pointer;
+        font-size: 14px;
+      }
 
-        .context-tab-item-active {
-          background-color: #2174f0;
-        }
+      .context-tab-item-active {
+        background-color: #2174f0;
       }
+    }
 
-      .context-box {
-        flex: 1;
-        padding: 0 10px;
-        height: 200px;
-        overflow-y: auto;
+    .context-box {
+      flex: 1;
+      padding: 0 10px;
+      height: 200px;
+      overflow-y: auto;
 
-        .no-context {
-          display: flex;
-          justify-content: center;
-          padding-top: 30px;
-          font-size: 16px;
-        }
+      .no-context {
+        display: flex;
+        justify-content: center;
+        padding-top: 30px;
+        font-size: 16px;
+      }
 
-        .context-detail {
-          width: 100%;
-          display: flex;
-          justify-content: space-between;
-          line-height: 24px;
-          padding: 0px 16px;
+      .context-detail {
+        width: 100%;
+        display: flex;
+        justify-content: space-between;
+        line-height: 24px;
+        padding: 0px 16px;
 
-          // div {
-          //     display: flex;
-          //     justify-content: flex-start;
+        // div {
+        //     display: flex;
+        //     justify-content: flex-start;
 
-          //     &:nth-child(1) {
-          //         width: 44%;
-          //     }
+        //     &:nth-child(1) {
+        //         width: 44%;
+        //     }
 
-          //     &:nth-child(2) {
-          //         width: 40%;
-          //     }
+        //     &:nth-child(2) {
+        //         width: 40%;
+        //     }
 
-          //     &:nth-child(3) {
-          //         width: 25%;
-          //     }
+        //     &:nth-child(3) {
+        //         width: 25%;
+        //     }
 
-          //     &:nth-child(4) {
-          //         width: 15%;
-          //     }
-          // }
-        }
+        //     &:nth-child(4) {
+        //         width: 15%;
+        //     }
+        // }
       }
+    }
 
-      .more {
-        position: absolute;
-        left: 24px;
-        bottom: 10px;
-        cursor: pointer;
+    .more {
+      position: absolute;
+      left: 24px;
+      bottom: 10px;
+      cursor: pointer;
 
-        &:hover {
-          color: #2174f0;
-        }
+      &:hover {
+        color: #2174f0;
       }
     }
   }
+}
 
-  :deep(.zxm-badge-count) {
-    width: 8px;
-    height: 8px;
-  }
+:deep(.zxm-badge-count) {
+  width: 8px;
+  height: 8px;
+}
 </style>

+ 20 - 0
src/layouts/default/header/index.vue

@@ -62,6 +62,7 @@
         <a-radio :value="true">快速模式</a-radio>
         <a-radio :value="false">特效模式</a-radio>
       </a-radio-group> -->
+
       <div v-if="hasPermission('show:modalChange')" @click="changeModalType" style="width: 92px; color: #fff; cursor: pointer">
         <div v-if="programModalType == true"> <span class="icon">⚡</span>快速模式</div>
         <div v-if="programModalType == false"> <span class="icon">✨</span>特效模式</div>
@@ -69,6 +70,13 @@
       <WeatherBroadcast v-if="sysOrgCode != 'sdmtjtgsd' && isShowQy && portValue != '8062' && hasPermission('show:weather')" />
       <VoiceBroadcast v-if="sysOrgCode != 'sdmtjtgsd' && portValue != '8062'" />
       <AIChat v-if="hasPermission('show:AIChat')" />
+      <div style="width: 24px; height: 24px; cursor: pointer; user-select: none; margin: 0 3px">
+        <img
+          :src="dataPickerHoverEnabled ? AIOpenIcon : AICloseIcon"
+          :title="dataPickerHoverEnabled ? '关闭AI解读' : '开启AI解读'"
+          @click="toggleDataPickerHover"
+        />
+      </div>
       <VoiceBroadcastGsd v-if="sysOrgCode == 'sdmtjtgsd'" />
       <UserDropDown v-if="showUserDropdown" :theme="getHeaderTheme" />
       <LoginSelect ref="loginSelectRef" @success="loginSelectOk" />
@@ -116,6 +124,9 @@
   import { usePermission } from '/@/hooks/web/usePermission';
   import { useAppStore } from '/@/store/modules/app';
   import { router } from '/@/router';
+  import { dataPickerHoverEnabled, setDataPickerHoverEnabled } from '/@/views/ventAI/dataPicker';
+  import AIOpenIcon from '/@/assets/images/ventAI/dataPicker/AI-open.svg';
+  import AICloseIcon from '/@/assets/images/ventAI/dataPicker/AI-close.svg';
 
   export default defineComponent({
     name: 'LayoutHeader',
@@ -252,6 +263,10 @@
         window.location.reload();
       };
 
+      const toggleDataPickerHover = () => {
+        setDataPickerHoverEnabled(!dataPickerHoverEnabled.value);
+      };
+
       onMounted(() => {
         showLoginSelect();
       });
@@ -294,6 +309,10 @@
         isShowQy,
         programModalType,
         changeModalType,
+        dataPickerHoverEnabled,
+        toggleDataPickerHover,
+        AIOpenIcon,
+        AICloseIcon,
       };
     },
   });
@@ -371,5 +390,6 @@
     display: flex;
     align-items: center;
     height: 48px;
+    width: max-content;
   }
 </style>

+ 19 - 1
src/layouts/default/index.vue

@@ -14,7 +14,12 @@
     </Layout>
 
     <!-- AI智能助手悬浮按钮(支持拖动,固定右侧) -->
-    <div class="ai-assistant-float" :style="{ right: aiBtnPosition.right, bottom: aiBtnPosition.bottom }" @mousedown="handleDragStart">
+    <div
+      v-if="hasPermission('show:ventAI')"
+      class="ai-assistant-float"
+      :style="{ right: aiBtnPosition.right, bottom: aiBtnPosition.bottom }"
+      @mousedown="handleDragStart"
+    >
       <div class="ai-float-icon" @click="openAiAssistant"></div>
     </div>
 
@@ -40,6 +45,7 @@
   import { useAppStore } from '/@/store/modules/app';
 
   import { useAppInject } from '/@/hooks/web/useAppInject';
+  import { usePermission } from '/@/hooks/web/usePermission';
   import { antPrefixCls } from '/@/settings/designSetting';
 
   export default defineComponent({
@@ -58,6 +64,7 @@
       const { prefixCls } = useDesign('default-layout');
       const { getIsMobile } = useAppInject();
       const { getShowSidebar, getIsMixSidebar, getShowMenu } = useMenuSetting();
+      const { hasPermission } = usePermission();
 
       // Create a lock screen monitor
       const lockEvents = useLockPage();
@@ -141,6 +148,16 @@
         const handleDragEnd = () => {
           if (dragged) {
             isDragging.value = false;
+            // 拖拽结束后阻止随后触发的 click 事件
+            const iconEl = document.querySelector('.ai-float-icon');
+            if (iconEl) {
+              const preventClick = (e: MouseEvent) => {
+                e.stopPropagation();
+                e.preventDefault();
+                iconEl.removeEventListener('click', preventClick, true);
+              };
+              iconEl.addEventListener('click', preventClick, true);
+            }
           }
           document.removeEventListener('mousemove', handleDragMove);
           document.removeEventListener('mouseup', handleDragEnd);
@@ -157,6 +174,7 @@
         getIsMixSidebar,
         layoutClass,
         lockEvents,
+        hasPermission,
         aiAssistantVisible,
         aiBtnPosition,
         openAiAssistant,

+ 10 - 3
src/layouts/default/sider/bottomSideder.vue

@@ -148,11 +148,18 @@
           }
         };
 
-        const onUp = (ev: MouseEvent) => {
+        const onUp = () => {
           if (dragged) {
             isDragging.value = false;
-            ev.preventDefault();
-            ev.stopPropagation();
+            const iconEl = document.querySelector('.menu-show-icon .icon');
+            if (iconEl) {
+              const preventClick = (e: MouseEvent) => {
+                e.stopPropagation();
+                e.preventDefault();
+                iconEl.removeEventListener('click', preventClick, true);
+              };
+              iconEl.addEventListener('click', preventClick, true);
+            }
           }
           document.removeEventListener('mousemove', onMove);
           document.removeEventListener('mouseup', onUp);

+ 10 - 3
src/layouts/default/sider/bottomSideder3.vue

@@ -135,11 +135,18 @@
           }
         };
 
-        const onUp = (ev: MouseEvent) => {
+        const onUp = () => {
           if (dragged) {
             isDragging.value = false;
-            ev.preventDefault();
-            ev.stopPropagation();
+            const iconEl = document.querySelector('.menu-show-icon .icon');
+            if (iconEl) {
+              const preventClick = (e: MouseEvent) => {
+                e.stopPropagation();
+                e.preventDefault();
+                iconEl.removeEventListener('click', preventClick, true);
+              };
+              iconEl.addEventListener('click', preventClick, true);
+            }
           }
           document.removeEventListener('mousemove', onMove);
           document.removeEventListener('mouseup', onUp);

+ 9 - 1
src/qiankun/apps.ts

@@ -35,6 +35,14 @@ for (const key in import.meta.env) {
       _apps.push(obj);
     });
   }
-  prefetchApps([..._apps]);
+  // prefetchApps([..._apps]);
+  // 延迟预取子应用资源到浏览器空闲时,避免启动时与首屏关键请求竞争带宽
+  const schedulePrefetch = window.requestIdleCallback || ((cb) => setTimeout(cb, 200));
+  schedulePrefetch(
+    () => {
+      prefetchApps([..._apps]);
+    },
+    { timeout: 3000 } as IdleRequestOptions
+  );
 }
 export const apps = _apps;

+ 4 - 0
src/store/modules/permission.ts

@@ -24,6 +24,7 @@ import { useMessage } from '/@/hooks/web/useMessage';
 import { PageEnum } from '/@/enums/pageEnum';
 // import { cloneDeep } from 'lodash-es';
 import { useGlobSetting } from '/@/hooks/setting';
+import { useVentStore } from '/@/store/modules/vent';
 
 // 系统权限
 interface AuthItem {
@@ -242,6 +243,7 @@ export const usePermissionStore = defineStore({
                   hasIndex =
                     menuItem.component === 'layouts/default/index' ||
                     menuItem.component === 'layouts/RouteView' ||
+                    menuItem.component === 'vent/home/configurable/vent182' ||
                     menuItem.path === '/dashboard/analysis';
                 }
                 // 条件2:判断图标是否带有 冒号
@@ -293,6 +295,8 @@ export const usePermissionStore = defineStore({
 
       routes.push(ERROR_LOG_ROUTE);
       patchHomeAffix(routes);
+
+      /** 查询系统模型 */
       return routes;
     },
     setAuthData(systemPermission) {

+ 1 - 0
src/store/modules/vent.ts

@@ -7,6 +7,7 @@ export const useVentStore = defineStore({
     return {
       allTableHeaderColumns: null,
       resultWebSocket: null,
+      modalTypes: null,
     };
   },
   getters: {

+ 5 - 11
src/utils/threejs/useThree.ts

@@ -197,7 +197,6 @@ class UseThree {
 
   setGLTFModel(modalNames, group = null, isBlender = false) {
     window['startTime'] = new Date().getTime();
-    const modelStore = useModelStore();
     return new Promise(async (resolve, reject) => {
       try {
         const gltfLoader = new GLTFLoader();
@@ -217,15 +216,12 @@ class UseThree {
             try {
               // 解析模型
               const modalNameStr = modalNameArr[i];
-              const data = modelStore.modelArr.get(modalNameStr) || null;
               let modalValue;
-              if (!data) {
-                const modalArr = await db.modal.where('modalName').equals(modalNameStr).toArray();
-                if (modalArr.length > 0) {
-                  modalValue = modalArr[0].modalVal;
-                }
-              } else {
-                modalValue = data.modalVal;
+              const modalArr = await db.modal.where('modalName').equals(modalNameStr).toArray();
+              if (modalArr.length > 0 && modalArr[0].modalVal.byteLength > 0) {
+                modalValue = modalArr[0].modalVal;
+              } else if (modalArr.length > 0) {
+                db.modal.delete(modalArr[0].id);
               }
               if (modalValue) {
                 gltfLoader.parse(
@@ -233,7 +229,6 @@ class UseThree {
                   '/model/glft/',
                   (gltf) => {
                     let object: THREE.Object3D = gltf.scene;
-                    // setModalCenter(object);
                     object.traverse((obj) => {
                       if (obj instanceof THREE.Mesh) {
                         obj.material.emissiveIntensity = 1;
@@ -282,7 +277,6 @@ class UseThree {
 
                 const result = (await getList({ fileName: modalNameStr })) || [];
                 const file = result['records'][0];
-                debugger;
                 console.log('需要开启线程下载模型资源。。。。。');
 
                 // 如果是常村时

+ 49 - 44
src/views/system/role/components/RoleDrawer.vue

@@ -4,50 +4,55 @@
   </BasicDrawer>
 </template>
 <script lang="ts" setup>
-  import { ref, computed, unref, useAttrs } from 'vue';
-  import { BasicForm, useForm } from '/src/components/Form';
-  import { BasicDrawer, useDrawerInner } from '/src/components/Drawer';
-  import { BasicTree, TreeItem } from '/src/components/Tree';
-  import { formSchema } from '../role.data';
-  import { saveOrUpdateRole } from '../role.api';
-  // 声明Emits
-  const emit = defineEmits(['success', 'register']);
-  const attrs = useAttrs();
-  const isUpdate = ref(true);
-  const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
-    labelWidth: 90,
-    schemas: formSchema,
-    showActionButtonGroup: false,
-  });
-  const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
-    resetFields();
-    isUpdate.value = !!data?.isUpdate;
+import { ref, computed, unref, useAttrs } from 'vue';
+import { BasicForm, useForm } from '/src/components/Form';
+import { BasicDrawer, useDrawerInner } from '/src/components/Drawer';
+import { BasicTree, TreeItem } from '/src/components/Tree';
+import { formSchema } from '../role.data';
+import { saveOrUpdateRole } from '../role.api';
+// 声明Emits
+const emit = defineEmits(['success', 'register']);
+const attrs = useAttrs();
+const isUpdate = ref(true);
+const [registerForm, { setProps, resetFields, setFieldsValue, validate }] = useForm({
+  labelWidth: 90,
+  schemas: formSchema,
+  showActionButtonGroup: false,
+});
+const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
+  resetFields();
+  isUpdate.value = !!data?.isUpdate;
+  setDrawerProps({ confirmLoading: false });
+  if (unref(isUpdate)) {
+    setFieldsValue({
+      ...data.record,
+    });
+  }
+  //禁用表单
+  setProps({ disabled: !attrs.showFooter });
+});
+/**
+ * 标题
+ */
+const getTitle = computed(() => (!unref(isUpdate) ? '新增角色' : '编辑角色'));
+/**
+ * 提交
+ */
+async function handleSubmit() {
+  try {
+    const values = await validate();
+    setDrawerProps({ confirmLoading: true });
+    // //提交表单
+    // await saveOrUpdateRole(values, isUpdate.value);
+    // closeDrawer();
+    await saveOrUpdateRole(values, isUpdate.value);
+    closeDrawer();
+    emit('success', {
+      isUpdate: unref(isUpdate),
+      record: values,
+    });
+  } finally {
     setDrawerProps({ confirmLoading: false });
-    if (unref(isUpdate)) {
-      setFieldsValue({
-        ...data.record,
-      });
-    }
-    //禁用表单
-    setProps({ disabled: !attrs.showFooter });
-  });
-  /**
-   * 标题
-   */
-  const getTitle = computed(() => (!unref(isUpdate) ? '新增角色' : '编辑角色'));
-  /**
-   * 提交
-   */
-  async function handleSubmit() {
-    try {
-      const values = await validate();
-      setDrawerProps({ confirmLoading: true });
-      //提交表单
-      await saveOrUpdateRole(values, isUpdate.value);
-      closeDrawer();
-      emit('success');
-    } finally {
-      setDrawerProps({ confirmLoading: false });
-    }
   }
+}
 </script>

+ 147 - 147
src/views/system/role/components/RolePermissionDrawer.vue

@@ -46,161 +46,161 @@
   </BasicDrawer>
 </template>
 <script lang="ts" setup>
-  import { ref, computed, unref, onMounted } from 'vue';
-  import { BasicDrawer, useDrawer, useDrawerInner } from '/src/components/Drawer';
-  import { BasicTree, TreeItem } from '/src/components/Tree';
-  import { PopConfirmButton } from '/@/components/Button';
-  import RoleDataRuleDrawer from './RoleDataRuleDrawer.vue';
-  import { queryTreeListForRole, queryRolePermission, saveRolePermission } from '../role.api';
-  const emit = defineEmits(['register']);
-  //树的信息
-  const treeData = ref<TreeItem[]>([]);
-  //树的全部节点信息
-  const allTreeKeys = ref([]);
-  //树的选择节点信息
-  const checkedKeys = ref([]);
-  const defaultCheckedKeys = ref([]);
-  //树的选中的节点信息
-  const selectedKeys = ref([]);
-  const roleId = ref('');
-  //树的实例
-  const treeRef = ref(null);
-  const loading = ref(false);
+import { ref, computed, unref, onMounted } from 'vue';
+import { BasicDrawer, useDrawer, useDrawerInner } from '/src/components/Drawer';
+import { BasicTree, TreeItem } from '/src/components/Tree';
+import { PopConfirmButton } from '/@/components/Button';
+import RoleDataRuleDrawer from './RoleDataRuleDrawer.vue';
+import { queryTreeListForRole, queryRolePermission, saveRolePermission } from '../role.api';
+const emit = defineEmits(['register']);
+//树的信息
+const treeData = ref<TreeItem[]>([]);
+//树的全部节点信息
+const allTreeKeys = ref([]);
+//树的选择节点信息
+const checkedKeys = ref([]);
+const defaultCheckedKeys = ref([]);
+//树的选中的节点信息
+const selectedKeys = ref([]);
+const roleId = ref('');
+//树的实例
+const treeRef = ref(null);
+const loading = ref(false);
 
-  //展开折叠的key
-  const expandedKeys = ref<any>([]);
-  //父子节点选中状态是否关联
-  const checkStrictly = ref<boolean>(true);
-  const [registerDrawer1, { openDrawer: openDataRuleDrawer }] = useDrawer();
-  const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
-    await reset();
-    setDrawerProps({ confirmLoading: false, loading: true });
-    roleId.value = data.roleId;
-    //初始化数据
-    const roleResult = await queryTreeListForRole();
-    treeData.value = roleResult.treeList;
-    allTreeKeys.value = roleResult.ids;
-    expandedKeys.value = roleResult.ids;
-    //初始化角色菜单数据
-    const permResult = await queryRolePermission({ roleId: unref(roleId) });
-    checkedKeys.value = permResult;
-    defaultCheckedKeys.value = permResult;
-    setDrawerProps({ loading: false });
-  });
-  /**
-   * 点击选中
-   */
-  function onCheck(o) {
-    checkedKeys.value = o.checked ? o.checked : o;
-  }
-  /**
-   * 选中节点,打开数据权限抽屉
-   */
-  function onTreeNodeSelect(key) {
-    if (key && key.length > 0) {
-      selectedKeys.value = key;
-    }
-    openDataRuleDrawer(true, { functionId: unref(selectedKeys)[0], roleId: unref(roleId) });
+//展开折叠的key
+const expandedKeys = ref<any>([]);
+//父子节点选中状态是否关联
+const checkStrictly = ref<boolean>(true);
+const [registerDrawer1, { openDrawer: openDataRuleDrawer }] = useDrawer();
+const [registerDrawer, { setDrawerProps, closeDrawer }] = useDrawerInner(async (data) => {
+  await reset();
+  setDrawerProps({ confirmLoading: false, loading: true });
+  roleId.value = data.roleId;
+  //初始化数据
+  const roleResult = await queryTreeListForRole();
+  treeData.value = roleResult.treeList;
+  allTreeKeys.value = roleResult.ids;
+  expandedKeys.value = [];
+  //初始化角色菜单数据
+  const permResult = await queryRolePermission({ roleId: unref(roleId) });
+  checkedKeys.value = permResult;
+  defaultCheckedKeys.value = permResult;
+  setDrawerProps({ loading: false });
+});
+/**
+ * 点击选中
+ */
+function onCheck(o) {
+  checkedKeys.value = o.checked ? o.checked : o;
+}
+/**
+ * 选中节点,打开数据权限抽屉
+ */
+function onTreeNodeSelect(key) {
+  if (key && key.length > 0) {
+    selectedKeys.value = key;
   }
-  /**
-   * 数据重置
-   */
-  function reset() {
-    treeData.value = [];
-    allTreeKeys.value = [];
-    checkedKeys.value = [];
-    defaultCheckedKeys.value = [];
-    selectedKeys.value = [];
-    roleId.value = '';
+  openDataRuleDrawer(true, { functionId: unref(selectedKeys)[0], roleId: unref(roleId) });
+}
+/**
+ * 数据重置
+ */
+function reset() {
+  treeData.value = [];
+  allTreeKeys.value = [];
+  checkedKeys.value = [];
+  defaultCheckedKeys.value = [];
+  selectedKeys.value = [];
+  roleId.value = '';
+}
+/**
+ * 获取tree实例
+ */
+function getTree() {
+  const tree = unref(treeRef);
+  if (!tree) {
+    throw new Error('tree is null!');
   }
-  /**
-   * 获取tree实例
-   */
-  function getTree() {
-    const tree = unref(treeRef);
-    if (!tree) {
-      throw new Error('tree is null!');
-    }
-    return tree;
+  return tree;
+}
+/**
+ * 提交
+ */
+async function handleSubmit(exit) {
+  let params = {
+    roleId: unref(roleId),
+    permissionIds: unref(getTree().getCheckedKeys()).join(','),
+    lastpermissionIds: unref(defaultCheckedKeys).join(','),
+  };
+  //update-begin-author:taoyan date:2023-2-11 for: issues/352 VUE角色授权重复保存
+  if (loading.value === false) {
+    await doSave(params);
+  } else {
+    console.log('请等待上次执行完毕!');
   }
-  /**
-   * 提交
-   */
-  async function handleSubmit(exit) {
-    let params = {
-      roleId: unref(roleId),
-      permissionIds: unref(getTree().getCheckedKeys()).join(','),
-      lastpermissionIds: unref(defaultCheckedKeys).join(','),
-    };
-    //update-begin-author:taoyan date:2023-2-11 for: issues/352 VUE角色授权重复保存
-    if (loading.value === false) {
-      await doSave(params);
-    } else {
-      console.log('请等待上次执行完毕!');
-    }
-    if (exit) {
-      // 如果关闭
-      closeDrawer();
-    } else {
-      // 没有关闭需要重新获取选中数据
-      const permResult = await queryRolePermission({ roleId: unref(roleId) });
-      defaultCheckedKeys.value = permResult;
-    }
+  if (exit) {
+    // 如果关闭
+    closeDrawer();
+  } else {
+    // 没有关闭需要重新获取选中数据
+    const permResult = await queryRolePermission({ roleId: unref(roleId) });
+    defaultCheckedKeys.value = permResult;
   }
+}
 
-  // VUE角色授权重复保存 #352
-  async function doSave(params) {
-    loading.value = true;
-    await saveRolePermission(params);
-    setTimeout(() => {
-      loading.value = false;
-    }, 500);
-  }
-  //update-end-author:taoyan date:2023-2-11 for: issues/352 VUE角色授权重复保存
+// VUE角色授权重复保存 #352
+async function doSave(params) {
+  loading.value = true;
+  await saveRolePermission(params);
+  setTimeout(() => {
+    loading.value = false;
+  }, 500);
+}
+//update-end-author:taoyan date:2023-2-11 for: issues/352 VUE角色授权重复保存
 
-  /**
-   * 树菜单选择
-   * @param key
-   */
-  function treeMenuClick({ key }) {
-    if (key === 'checkAll') {
-      checkedKeys.value = allTreeKeys.value;
-    } else if (key === 'cancelCheck') {
-      checkedKeys.value = [];
-    } else if (key === 'openAll') {
-      expandedKeys.value = allTreeKeys.value;
-    } else if (key === 'closeAll') {
-      expandedKeys.value = [];
-    } else if (key === 'relation') {
-      checkStrictly.value = false;
-    } else {
-      checkStrictly.value = true;
-    }
+/**
+ * 树菜单选择
+ * @param key
+ */
+function treeMenuClick({ key }) {
+  if (key === 'checkAll') {
+    checkedKeys.value = allTreeKeys.value;
+  } else if (key === 'cancelCheck') {
+    checkedKeys.value = [];
+  } else if (key === 'openAll') {
+    expandedKeys.value = allTreeKeys.value;
+  } else if (key === 'closeAll') {
+    expandedKeys.value = [];
+  } else if (key === 'relation') {
+    checkStrictly.value = false;
+  } else {
+    checkStrictly.value = true;
   }
+}
 </script>
 
 <style lang="less" scoped>
-  /** 固定操作按钮 */
-  .jeecg-basic-tree {
-    position: absolute;
-    width: 618px;
-  }
-  //update-begin---author:wangshuai ---date:20230202  for:抽屉弹窗标题图标下拉样式------------
-  .line {
-    height: 1px;
-    width: 100%;
-    border-bottom: 1px solid #f0f0f0;
-  }
-  .more-icon {
-    font-size: 20px !important;
-    color: rgb(255, 255, 255);
-    display: inline-flex;
-    float: right;
-    margin-right: 2px;
-    cursor: pointer;
-  }
-  :deep(.jeecg-tree-header) {
-    border-bottom: none;
-  }
-  //update-end---author:wangshuai ---date:20230202  for:抽屉弹窗标题图标下拉样式------------
-</style>
+/** 固定操作按钮 */
+.jeecg-basic-tree {
+  position: absolute;
+  width: 618px;
+}
+//update-begin---author:wangshuai ---date:20230202  for:抽屉弹窗标题图标下拉样式------------
+.line {
+  height: 1px;
+  width: 100%;
+  border-bottom: 1px solid #f0f0f0;
+}
+.more-icon {
+  font-size: 20px !important;
+  color: rgb(255, 255, 255);
+  display: inline-flex;
+  float: right;
+  margin-right: 2px;
+  cursor: pointer;
+}
+:deep(.jeecg-tree-header) {
+  border-bottom: none;
+}
+//update-end---author:wangshuai ---date:20230202  for:抽屉弹窗标题图标下拉样式------------
+</style>

+ 223 - 148
src/views/system/role/index.vue

@@ -28,7 +28,7 @@
   <!--角色用户表格-->
   <RoleUserTable @register="roleUserDrawer" />
   <!--角色编辑抽屉-->
-  <RoleDrawer @register="registerDrawer" @success="reload" :showFooter="showFooter" />
+  <RoleDrawer @register="registerDrawer" @success="handleRoleSaveSuccess" :showFooter="showFooter" />
   <!--角色详情-->
   <RoleDesc @register="registerDesc"></RoleDesc>
   <!--角色菜单授权抽屉-->
@@ -37,161 +37,236 @@
   <RoleIndexModal @register="registerIndexModal" />
 </template>
 <script lang="ts" name="system-role" setup>
-  import { ref } from 'vue';
-  import { BasicTable, TableAction } from '/@/components/Table';
-  import { useDrawer } from '/@/components/Drawer';
-  import { useModal } from '/@/components/Modal';
-  import RoleDrawer from './components/RoleDrawer.vue';
-  import RoleDesc from './components/RoleDesc.vue';
-  import RolePermissionDrawer from './components/RolePermissionDrawer.vue';
-  import RoleIndexModal from './components/RoleIndexModal.vue';
-  import RoleUserTable from './components/RoleUserTable.vue';
-  import { columns, searchFormSchema } from './role.data';
-  import { list, deleteRole, batchDeleteRole, getExportUrl, getImportUrl } from './role.api';
-  import { useListPage } from '/@/hooks/system/useListPage';
-  const showFooter = ref(true);
-  const [roleUserDrawer, { openDrawer: openRoleUserDrawer }] = useDrawer();
-  const [registerDrawer, { openDrawer }] = useDrawer();
-  const [registerModal, { openModal }] = useModal();
-  const [registerIndexModal, { openModal: openIndexModal }] = useModal();
-  const [rolePermissionDrawer, { openDrawer: openRolePermissionDrawer }] = useDrawer();
-  const [registerDesc, { openDrawer: openRoleDesc }] = useDrawer();
+import { ref } from 'vue';
+import { BasicTable, TableAction } from '/@/components/Table';
+import { useDrawer } from '/@/components/Drawer';
+import { useModal } from '/@/components/Modal';
+import RoleDrawer from './components/RoleDrawer.vue';
+import RoleDesc from './components/RoleDesc.vue';
+import RolePermissionDrawer from './components/RolePermissionDrawer.vue';
+import RoleIndexModal from './components/RoleIndexModal.vue';
+import RoleUserTable from './components/RoleUserTable.vue';
+import { columns, searchFormSchema } from './role.data';
+// 引入对应接口
+import {
+  list,
+  deleteRole,
+  batchDeleteRole,
+  getExportUrl,
+  getImportUrl,
+  queryTreeListForRole,
+  queryRolePermission,
+  saveRolePermission,
+} from './role.api';
+import { useListPage } from '/@/hooks/system/useListPage';
 
-  // 列表页面公共参数、方法
-  const { prefixCls, tableContext, onImportXls, onExportXls } = useListPage({
-    designScope: 'role-template',
-    tableProps: {
-      title: '系统角色列表',
-      api: list,
-      columns: columns,
-      formConfig: {
-        // update-begin--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
-        labelWidth:65,
-        rowProps: { gutter: 24 },
-        // update-end--author:liaozhiyang---date:20230803---for:【QQYUN-5873】查询区域lablel默认居左
-        schemas: searchFormSchema,
-      },
-      actionColumn: {
-        width: 120,
-      },
-      rowSelection: null,
-      //自定义默认排序
-      defSort: {
-        column: 'id',
-        order: 'desc',
-      },
+const showFooter = ref(true);
+const [roleUserDrawer, { openDrawer: openRoleUserDrawer }] = useDrawer();
+const [registerDrawer, { openDrawer }] = useDrawer();
+const [registerModal, { openModal }] = useModal();
+const [registerIndexModal, { openModal: openIndexModal }] = useModal();
+const [rolePermissionDrawer, { openDrawer: openRolePermissionDrawer }] = useDrawer();
+const [registerDesc, { openDrawer: openRoleDesc }] = useDrawer();
+
+// 列表页面公共参数、方法
+const { prefixCls, tableContext, onImportXls, onExportXls } = useListPage({
+  designScope: 'role-template',
+  tableProps: {
+    title: '系统角色列表',
+    api: list,
+    columns: columns,
+    formConfig: {
+      labelWidth: 65,
+      rowProps: { gutter: 24 },
+      schemas: searchFormSchema,
     },
-    exportConfig: {
-      name: '角色列表',
-      url: getExportUrl,
+    actionColumn: {
+      width: 120,
     },
-    importConfig: {
-      url: getImportUrl,
+    rowSelection: null,
+    defSort: {
+      column: 'id',
+      order: 'desc',
     },
-  });
-  const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
+  },
+  exportConfig: {
+    name: '角色列表',
+    url: getExportUrl,
+  },
+  importConfig: {
+    url: getImportUrl,
+  },
+});
+const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
 
-  /**
-   * 新增
-   */
-  function handleCreate() {
-    showFooter.value = true;
-    openDrawer(true, {
-      isUpdate: false,
-    });
-  }
-  /**
-   * 编辑
-   */
-  function handleEdit(record: Recordable) {
-    showFooter.value = true;
-    openDrawer(true, {
-      record,
-      isUpdate: true,
-    });
-  }
-  /**
-   * 详情
-   */
-  function handleDetail(record) {
-    showFooter.value = false;
-    openRoleDesc(true, {
-      record,
-      isUpdate: true,
+/**
+ * 递归收集当前节点 + 全部子节点id
+ * @param node 树节点
+ */
+function collectAllNodeIds(node: Recordable): string[] {
+  const ids: string[] = [node.key];
+  if (node.children && node.children.length > 0) {
+    node.children.forEach((child) => {
+      ids.push(...collectAllNodeIds(child));
     });
   }
-  /**
-   * 删除事件
-   */
-  async function handleDelete(record) {
-    await deleteRole({ id: record.id }, reload);
-  }
-  /**
-   * 批量删除事件
-   */
-  async function batchHandleDelete() {
-    await batchDeleteRole({ ids: selectedRowKeys.value }, reload);
-  }
-  /**
-   * 角色授权弹窗
-   */
-  function handlePerssion(record) {
-    openRolePermissionDrawer(true, { roleId: record.id });
-  }
+  return ids;
+}
 
-  /**
-   * 首页配置弹窗
-   */
-  function handleIndexConfig(roleCode) {
-    openIndexModal(true, { roleCode });
-  }
-  /**
-   * 角色用户
-   */
-  function handleUser(record) {
-    //onSelectChange(selectedRowKeys)
-    openRoleUserDrawer(true, record);
-  }
-  /**
-   * 操作栏
-   */
-  function getTableAction(record) {
-    return [
-      {
-        label: '用户',
-        onClick: handleUser.bind(null, record),
-      },
-      {
-        label: '授权',
-        onClick: handlePerssion.bind(null, record),
-      },
-    ];
+/**
+ * 角色新增/编辑保存回调
+ */
+async function handleRoleSaveSuccess(res: Recordable) {
+  console.log(res, 'res新增1111111');
+  const { isUpdate, record } = res;
+
+  // 只处理新增
+  if (!isUpdate && record?.roleCode) {
+    try {
+      // 1. 请求全部角色列表,用来根据roleCode查询新增角色ID
+      const rolePageRes = await list({ pageNo: 1, pageSize: 9999 });
+      // defHttp 已将响应解包,直接返回 result 字段内容,即 { records: [], total: N }
+      const roleList = rolePageRes.records || [];
+      // 根据唯一编码匹配新增角色
+      const newRoleInfo = roleList.find((item) => item.roleCode === record.roleCode);
+
+      if (!newRoleInfo || !newRoleInfo.id) {
+        console.warn('未能匹配到新增角色,跳过自动授权');
+        reload();
+        return;
+      }
+      const targetRoleId = newRoleInfo.id;
+
+      // 2. 获取权限树,取第一个根节点及其全部子节点授权
+      const treeResult = await queryTreeListForRole();
+      const treeList = treeResult.treeList;
+      if (!Array.isArray(treeList) || treeList.length === 0) {
+        reload();
+        return;
+      }
+      const firstRootNode = treeList[0];
+      const targetIdArr = collectAllNodeIds(firstRootNode);
+      const permissionIdsStr = targetIdArr.join(',');
+
+      // 3. 查询角色旧权限
+      const oldCheckedArr = await queryRolePermission({ roleId: targetRoleId });
+      const lastIdsStr = (Array.isArray(oldCheckedArr) ? oldCheckedArr : []).join(',');
+
+      // 4. 静默授权保存
+      const saveParams = {
+        roleId: targetRoleId,
+        permissionIds: permissionIdsStr,
+        lastpermissionIds: lastIdsStr,
+      };
+      await saveRolePermission(saveParams);
+      console.log('✅自动授权完成');
+    } catch (err) {
+      console.error('新增角色自动授权异常:', err);
+    }
   }
+  reload();
+}
 
-  /**
-   * 下拉操作栏
-   */
-  function getDropDownAction(record) {
-    return [
-      {
-        label: '编辑',
-        onClick: handleEdit.bind(null, record),
-      },
-      {
-        label: '详情',
-        onClick: handleDetail.bind(null, record),
-      },
-      {
-        label: '删除',
-        popConfirm: {
-          title: '是否确认删除',
-          confirm: handleDelete.bind(null, record),
-        },
-      },
-      {
-        label: '首页配置',
-        onClick: handleIndexConfig.bind(null, record.roleCode),
+/**
+ * 新增
+ */
+function handleCreate() {
+  showFooter.value = true;
+  openDrawer(true, {
+    isUpdate: false,
+  });
+}
+/**
+ * 编辑
+ */
+function handleEdit(record: Recordable) {
+  showFooter.value = true;
+  openDrawer(true, {
+    record,
+    isUpdate: true,
+  });
+}
+/**
+ * 详情
+ */
+function handleDetail(record) {
+  showFooter.value = false;
+  openRoleDesc(true, {
+    record,
+    isUpdate: true,
+  });
+}
+/**
+ * 删除事件
+ */
+async function handleDelete(record) {
+  await deleteRole({ id: record.id }, reload);
+}
+/**
+ * 批量删除事件
+ */
+async function batchHandleDelete() {
+  await batchDeleteRole({ ids: selectedRowKeys.value }, reload);
+}
+/**
+ * 角色授权弹窗
+ */
+function handlePerssion(record) {
+  openRolePermissionDrawer(true, { roleId: record.id });
+}
+
+/**
+ * 首页配置弹窗
+ */
+function handleIndexConfig(roleCode) {
+  openIndexModal(true, { roleCode });
+}
+/**
+ * 角色用户
+ */
+function handleUser(record) {
+  openRoleUserDrawer(true, record);
+}
+/**
+ * 操作栏
+ */
+function getTableAction(record) {
+  return [
+    {
+      label: '用户',
+      onClick: handleUser.bind(null, record),
+    },
+    {
+      label: '授权',
+      onClick: handlePerssion.bind(null, record),
+    },
+  ];
+}
+
+/**
+ * 下拉操作栏
+ */
+function getDropDownAction(record) {
+  return [
+    {
+      label: '编辑',
+      onClick: handleEdit.bind(null, record),
+    },
+    {
+      label: '详情',
+      onClick: handleDetail.bind(null, record),
+    },
+    {
+      label: '删除',
+      popConfirm: {
+        title: '是否确认删除',
+        confirm: handleDelete.bind(null, record),
       },
-    ];
-  }
-</script>
+    },
+    {
+      label: '首页配置',
+      onClick: handleIndexConfig.bind(null, record.roleCode),
+    },
+  ];
+}
+</script>

+ 170 - 161
src/views/vent/deviceManager/comment/DeviceModal.vue

@@ -21,7 +21,7 @@
       <a-tab-pane key="11" tab="报表录入">
         <DeviceReportInfo v-if="activeKey == '11'" />
       </a-tab-pane>
-      <a-tab-pane key="2" tab="点表关联">
+      <a-tab-pane key="2" :tab="deviceType === 'gate' ? '监测点位关联' : '点表关联'">
         <PointTable
           :columns="pointColumns"
           :pointType="deviceData.strtype"
@@ -31,6 +31,15 @@
           @delete="deletePointById"
         />
       </a-tab-pane>
+      <a-tab-pane v-if="deviceType === 'gate'" key="15" tab="控制点位关联">
+        <PointTable
+          :columns="pointColumns"
+          :pointType="deviceData.strtype"
+          :deviceId="deviceData.id"
+          @save="savePointData"
+          @delete="deletePointById"
+        />
+      </a-tab-pane>
       <a-tab-pane key="8" tab="虚拟点表配置">
         <PointTable
           :columns="simulationColumns"
@@ -98,176 +107,176 @@
   </BasicModal>
 </template>
 <script lang="ts" setup>
-  import { computed, unref, inject, reactive, ref, watch } from 'vue';
-  import { BasicModal, useModalInner } from '/@/components/Modal';
-  import EditRowTable from '../../comment/EditRowTable.vue';
-  import PointTable from './pointTabel/PointTable.vue';
-  import WarningTable from './warningTabel/index.vue';
-  import ManagerWarningTable from './warningTabel/index1.vue';
-  import ManagerWarningDeviceTable from './warningTabel/index2.vue';
-  import BackWindDeviceTable from './warningTabel/index3.vue';
-  import AccidentPlanTable from './warningTabel/index4.vue';
-  import LEDPlaylistTable from './warningTabel/index5.vue';
-  import WorkFacePointTable from './pointTabel/WorkFacePointTable.vue';
-  import WorkFaceReportTable from './pointTabel/WorkFaceReportTable.vue';
-  import DeviceReportInfo from './DeviceReportInfo.vue';
-  import FaultConfigTable from './faultConfigTable/index.vue';
-  // import editWarnTable from './editWarnTable/index.vue'
-  import FanLineModal from './FanLineModal .vue';
-  import FormModal from './FormModal.vue';
-  import { cloneDeep } from 'lodash-es';
-  import { columns as pointColumns, workFaceColumns, simulationColumns, reportColumns } from './pointTabel/point.data';
-  import { saveOrUpdate as pointSaveOrUpdate, deleteById as pointDeleteById } from './pointTabel/point.api';
-  import { columns as cameraColumns } from './cameraTabel/camera.data';
+import { computed, unref, inject, reactive, ref, watch } from 'vue';
+import { BasicModal, useModalInner } from '/@/components/Modal';
+import EditRowTable from '../../comment/EditRowTable.vue';
+import PointTable from './pointTabel/PointTable.vue';
+import WarningTable from './warningTabel/index.vue';
+import ManagerWarningTable from './warningTabel/index1.vue';
+import ManagerWarningDeviceTable from './warningTabel/index2.vue';
+import BackWindDeviceTable from './warningTabel/index3.vue';
+import AccidentPlanTable from './warningTabel/index4.vue';
+import LEDPlaylistTable from './warningTabel/index5.vue';
+import WorkFacePointTable from './pointTabel/WorkFacePointTable.vue';
+import WorkFaceReportTable from './pointTabel/WorkFaceReportTable.vue';
+import DeviceReportInfo from './DeviceReportInfo.vue';
+import FaultConfigTable from './faultConfigTable/index.vue';
+// import editWarnTable from './editWarnTable/index.vue'
+import FanLineModal from './FanLineModal .vue';
+import FormModal from './FormModal.vue';
+import { cloneDeep } from 'lodash-es';
+import { columns as pointColumns, workFaceColumns, simulationColumns, reportColumns } from './pointTabel/point.data';
+import { saveOrUpdate as pointSaveOrUpdate, deleteById as pointDeleteById } from './pointTabel/point.api';
+import { columns as cameraColumns } from './cameraTabel/camera.data';
 
-  import { list as cameraList, saveOrUpdate as cameraSaveOrUpdate, deleteById as cameraDeleteById } from './cameraTabel/camera.api';
+import { list as cameraList, saveOrUpdate as cameraSaveOrUpdate, deleteById as cameraDeleteById } from './cameraTabel/camera.api';
 
-  const props = defineProps({
-    showTab: { type: Boolean, required: true },
-    // deviceType: { type: String },
-  });
-  // 声明Emits
-  const emit = defineEmits(['saveOrUpdate', 'register', 'closeModal']);
-  const isUpdate = inject('isUpdate');
-  const deviceData = inject('formData') as any;
-  const deviceType = inject('deviceType') as any;
-  const record = reactive({});
-  const activeKey = ref('1');
-  const linkColumns = ref<any[]>([]);
+const props = defineProps({
+  showTab: { type: Boolean, required: true },
+  // deviceType: { type: String },
+});
+// 声明Emits
+const emit = defineEmits(['saveOrUpdate', 'register', 'closeModal']);
+const isUpdate = inject('isUpdate');
+const deviceData = inject('formData') as any;
+const deviceType = inject('deviceType') as any;
+const record = reactive({});
+const activeKey = ref('1');
+const linkColumns = ref<any[]>([]);
 
-  //表单赋值
-  const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
-    //重置表单
-    setModalProps({ confirmLoading: false });
-    Object.assign(deviceData, data.record);
-    // 判断是否是关键阻力路线
-  });
+//表单赋值
+const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
+  //重置表单
+  setModalProps({ confirmLoading: false });
+  Object.assign(deviceData, data.record);
+  // 判断是否是关键阻力路线
+});
 
-  //设置标题
-  const title = computed(() => {
-    if (!unref(isUpdate)) {
-      if (deviceData.strname || deviceData.systemname) {
-        return `新增(${deviceData.strname || deviceData.systemname})`;
-      }
-      return `新增`;
-    } else {
-      if (deviceData['strtype'] == 'sys_majorpath') {
-        linkColumns.value = [
-          ...workFaceColumns,
-          ...[
-            {
-              title: '是否在关键通风路线上',
-              width: 100,
-              dataIndex: 'pathflag',
-              edit: true,
-              editComponent: 'Switch',
-              editValueMap: (value) => {
-                return value ? '是' : '否';
-              },
-            },
-            {
-              title: '传感器类型',
-              width: 100,
-              dataIndex: 'sensorType',
-              edit: true,
-              editComponent: 'Select',
-              editComponentProps: {
-                options: [
-                  {
-                    label: '多参',
-                    value: '1',
-                  },
-                  {
-                    label: '测风',
-                    value: '2',
-                  },
-                ],
-              },
-            },
-            {
-              title: '风向',
-              width: 100,
-              dataIndex: 'winddir',
-              edit: true,
-              editComponent: 'Select',
-              editComponentProps: {
-                options: [
-                  {
-                    label: '进风',
-                    value: '1',
-                  },
-                  {
-                    label: '用风',
-                    value: '2',
-                  },
-                  {
-                    label: '回风',
-                    value: '3',
-                  },
-                ],
-              },
+//设置标题
+const title = computed(() => {
+  if (!unref(isUpdate)) {
+    if (deviceData.strname || deviceData.systemname) {
+      return `新增(${deviceData.strname || deviceData.systemname})`;
+    }
+    return `新增`;
+  } else {
+    if (deviceData['strtype'] == 'sys_majorpath') {
+      linkColumns.value = [
+        ...workFaceColumns,
+        ...[
+          {
+            title: '是否在关键通风路线上',
+            width: 100,
+            dataIndex: 'pathflag',
+            edit: true,
+            editComponent: 'Switch',
+            editValueMap: (value) => {
+              return value ? '是' : '否';
             },
-            {
-              title: '是否总风量',
-              width: 100,
-              dataIndex: 'windflag',
-              edit: true,
-              editComponent: 'Switch',
-              editValueMap: (value) => {
-                return value ? '是' : '否';
-              },
+          },
+          {
+            title: '传感器类型',
+            width: 100,
+            dataIndex: 'sensorType',
+            edit: true,
+            editComponent: 'Select',
+            editComponentProps: {
+              options: [
+                {
+                  label: '多参',
+                  value: '1',
+                },
+                {
+                  label: '测风',
+                  value: '2',
+                },
+              ],
             },
-            {
-              title: '路线名称',
-              width: 100,
-              dataIndex: 'des',
-              edit: true,
-              editComponent: 'Input',
+          },
+          {
+            title: '风向',
+            width: 100,
+            dataIndex: 'winddir',
+            edit: true,
+            editComponent: 'Select',
+            editComponentProps: {
+              options: [
+                {
+                  label: '进风',
+                  value: '1',
+                },
+                {
+                  label: '用风',
+                  value: '2',
+                },
+                {
+                  label: '回风',
+                  value: '3',
+                },
+              ],
             },
-            {
-              title: ' 阻力值',
-              width: 100,
-              dataIndex: 'testDrag',
-              edit: true,
-              editComponent: 'InputNumber',
+          },
+          {
+            title: '是否总风量',
+            width: 100,
+            dataIndex: 'windflag',
+            edit: true,
+            editComponent: 'Switch',
+            editValueMap: (value) => {
+              return value ? '是' : '否';
             },
-          ],
-        ];
-      } else {
-        linkColumns.value = [...workFaceColumns];
-      }
-      if (deviceData.strname || deviceData.systemname) {
-        return `编辑(${deviceData.strname || deviceData.systemname})`;
-      }
-      return `编辑`;
+          },
+          {
+            title: '路线名称',
+            width: 100,
+            dataIndex: 'des',
+            edit: true,
+            editComponent: 'Input',
+          },
+          {
+            title: ' 阻力值',
+            width: 100,
+            dataIndex: 'testDrag',
+            edit: true,
+            editComponent: 'InputNumber',
+          },
+        ],
+      ];
+    } else {
+      linkColumns.value = [...workFaceColumns];
+    }
+    if (deviceData.strname || deviceData.systemname) {
+      return `编辑(${deviceData.strname || deviceData.systemname})`;
     }
-  });
+    return `编辑`;
+  }
+});
 
-  const closeModalFn = () => {
-    activeKey.value = '1';
-    closeModal();
-    emit('closeModal');
-  };
+const closeModalFn = () => {
+  activeKey.value = '1';
+  closeModal();
+  emit('closeModal');
+};
 
-  const savePointData = (data) => {
-    const record = cloneDeep(data.editValueRefs);
-    pointSaveOrUpdate(Object.assign(record, { id: data.id, deviceId: deviceData.id }), data.id);
-  };
-  const saveCameraData = (data: any, reload: Function) => {
-    cameraSaveOrUpdate(Object.assign({ ...data }, { id: data.id, deviceid: deviceData.id }), data.id);
-  };
-  const deletePointById = (id, reload) => {
-    pointDeleteById({ id: id }, reload);
-  };
-  const deleteCameraById = (id, reload) => {
-    cameraDeleteById({ id: id }, reload);
-  };
+const savePointData = (data) => {
+  const record = cloneDeep(data.editValueRefs);
+  pointSaveOrUpdate(Object.assign(record, { id: data.id, deviceId: deviceData.id }), data.id);
+};
+const saveCameraData = (data: any, reload: Function) => {
+  cameraSaveOrUpdate(Object.assign({ ...data }, { id: data.id, deviceid: deviceData.id }), data.id);
+};
+const deletePointById = (id, reload) => {
+  pointDeleteById({ id: id }, reload);
+};
+const deleteCameraById = (id, reload) => {
+  cameraDeleteById({ id: id }, reload);
+};
 </script>
 <style scoped lang="less">
-  ::v-deep .suffix {
-    height: 32px;
-    line-height: 32px;
-    margin-left: 5px;
-    color: #fff;
-  }
-</style>
+::v-deep .suffix {
+  height: 32px;
+  line-height: 32px;
+  margin-left: 5px;
+  color: #fff;
+}
+</style>

+ 9 - 8
src/views/vent/deviceManager/comment/NormalTable.vue

@@ -31,7 +31,7 @@
         <slot name="filterCell" v-bind="{ column, record }"></slot>
       </template>
     </BasicTable>
-    <DeviceModal @register="registerModal" @saveOrUpdate="saveOrUpdateHandler" :showTab="showTab" :deviceType="deviceType" />
+    <DeviceModal @register="registerModal" @save-or-update="saveOrUpdateHandler" :showTab="showTab" :deviceType="deviceType" />
     <!-- <DeviceModal v-model:visible="modalVisible" @saveOrUpdate="saveOrUpdateHandler" @close-modal="closeModal" :showTab="showTab"  /> -->
   </div>
 </template>
@@ -254,13 +254,6 @@
               reject(err);
             }
           );
-          // 兜底:API 未触发任何回调时(如取消弹窗),自动结束 pending 状态
-          setTimeout(() => {
-            if (!done) {
-              done = true;
-              resolve(undefined);
-            }
-          }, 3000);
         }),
       { confirm: false }
     );
@@ -335,3 +328,11 @@
     }
   }
 </style>
+<style lang="less">
+  .delete-confirm-modal {
+    .zxm-modal-confirm-title,
+    .zxm-modal-confirm-content {
+      color: #fff;
+    }
+  }
+</style>

+ 1 - 0
src/views/vent/deviceManager/deviceTable/device.api.ts

@@ -70,6 +70,7 @@ export const batchDeleteById = (params, handleSuccess, handleCancel) => {
     content: '是否删除选中数据',
     okText: '确认',
     cancelText: '取消',
+    wrapClassName: 'delete-confirm-modal',
     onOk: () => {
       return defHttp.delete({ url: Api.deleteBatch, data: params }, { joinParamsToUrl: true }).then(() => {
         handleSuccess();

+ 1 - 1
src/views/vent/home/billboard/billboard.api.ts

@@ -5,7 +5,7 @@ import { defHttp } from '/@/utils/http/axios';
 const store = useUserStore();
 
 enum Api {
-  getSummary = '/ventanaly-company/company/index/getEachMinePlateInfo',
+  getSummary = '/company/index/getEachMinePlateInfo',
 }
 
 /**

+ 377 - 377
src/views/vent/home/configurable/belt/belt-new.vue

@@ -48,366 +48,320 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, watch, nextTick, computed, onUnmounted, provide } from 'vue';
-import customHeader from './components/customHeader-belt.vue';
-import { useInitConfigs, useInitPage } from '../hooks/useInit';
-import { testBeltNew, testYjkf, testSpary } from './configurable.data';
-import ModuleCommon from './components/ModuleCommon.vue';
-import ModuleCommonDual from './components/ModuleCommonDual.vue';
-import Three3D from '/@/views/vent/home/configurable/components/three3D.vue';
-import BeltNav from './components/BeltNav.vue';
-import { useRouter, useRoute } from 'vue-router';
-import { getSystem, getMonitorAndAlertBelt, getWarnInfo, getDevice, getDataHome, getWarnResult, getStaffInfo } from './configurable.api';
-import { modalAnimate, destroy } from './threejs/belt.threejs';
-// 初始化配置
-const { configs, fetchConfigs } = useInitConfigs();
-const cfgs = computed(() => configs.value.filter((_, index) => index !== 2 && index !== 3));
-const cfgA = computed<any>(() => configs.value[2]);
-const cfgB = computed<any>(() => configs.value[3]);
-const { updateEnhancedConfigs, updateData, data } = useInitPage('矿井全域皮带巷三级防灭火系统');
-const isInitModal = ref(false);
-const pageCache = ref({
-  fire_risk_warn: { configs: testBeltNew },
-  emergencyControl: { configs: testYjkf },
-  sprayControl: { configs: testSpary },
-});
-let timer = null,
-  modal = null;
-const pageType = ref('fire_risk_warn');
-const route = useRoute();
-const modalMonitorData = ref({});
-// 下拉框选项
-/** 场景选项 */
-const options = ref([]);
-const optionValue = ref('');
-const isRefreshing = ref(false); // 防止重复刷新
-async function getSysDataSource() {
-  const res = await getDataHome({ dataList: 'risk_evaluator' }).catch(() => {});
-  options.value = res.risk_evaluator || [];
-  await nextTick();
-  if (options.value.length > 0 && !optionValue.value) {
-    const firstId = options.value[0].sys_id;
-    changeSelectRow(firstId);
-  }
-}
-// 切换检测数据
-function changeSelectRow(deviceID) {
-  optionValue.value = deviceID;
-  refresh();
-}
-// 预警等级映射
-const warnTypeMap = {
-  '102': '黄色预警(较大风险)',
-  '103': '橙色预警(重大风险)',
-  '104': '红色预警(特别重大风险)',
-};
-// 处理接口返回数据
-interface WarnResult {
-  warnName: string;
-  coRange?: string;
-  coTrend?: string;
-  tempRange?: string;
-  tempTrend?: string;
-  hclRange?: string;
-  warnCtrl?: string;
-}
-// 处理预警指标展示数据格式
-function groupWarnByType(data: any) {
-  const result: WarnResult[] = [];
-  // 遍历 102、103、104...
-  Object.keys(data).forEach((warnKey) => {
-    const list = data[warnKey] || [];
-    const warnName = warnTypeMap[warnKey as keyof typeof warnTypeMap];
-    function getWarnCtrlByWarnName(warnName) {
-      if (warnName === '黄色预警(较大风险)') return '预警';
-      if (warnName === '橙色预警(重大风险)') return '报警启动喷淋';
-      if (warnName === '红色预警(特别重大风险)') return '启动喷淋与排烟';
-      return '';
+  import { onMounted, ref, watch, nextTick, computed, onUnmounted, provide } from 'vue';
+  import customHeader from './components/customHeader-belt.vue';
+  import { useInitConfigs, useInitPage } from '../hooks/useInit';
+  import { testBeltNew, testYjkf, testSpary } from './configurable.data';
+  import ModuleCommon from './components/ModuleCommon.vue';
+  import ModuleCommonDual from './components/ModuleCommonDual.vue';
+  import Three3D from '/@/views/vent/home/configurable/components/three3D.vue';
+  import BeltNav from './components/BeltNav.vue';
+  import { useRouter, useRoute } from 'vue-router';
+  import { getSystem, getMonitorAndAlertBelt, getWarnInfo, getDevice, getDataHome, getWarnResult, getStaffInfo } from './configurable.api';
+  import { modalAnimate, destroy } from './threejs/belt.threejs';
+  // 初始化配置
+  const { configs, fetchConfigs } = useInitConfigs();
+  const cfgs = computed(() => configs.value.filter((_, index) => index !== 2 && index !== 3));
+  const cfgA = computed<any>(() => configs.value[2]);
+  const cfgB = computed<any>(() => configs.value[3]);
+  const { updateEnhancedConfigs, updateData, data } = useInitPage('矿井全域皮带巷三级防灭火系统');
+  const isInitModal = ref(false);
+  const pageCache = ref({
+    fire_risk_warn: { configs: testBeltNew },
+    emergencyControl: { configs: testYjkf },
+    sprayControl: { configs: testSpary },
+  });
+  let timer = null,
+    modal = null;
+  const pageType = ref('fire_risk_warn');
+  const route = useRoute();
+  const modalMonitorData = ref({});
+  // 下拉框选项
+  /** 场景选项 */
+  const options = ref([]);
+  const optionValue = ref('');
+  const isRefreshing = ref(false); // 防止重复刷新
+  async function getSysDataSource() {
+    const res = await getDataHome({ dataList: 'risk_evaluator' }).catch(() => {});
+    options.value = res.risk_evaluator || [];
+    await nextTick();
+    if (options.value.length > 0 && !optionValue.value) {
+      const firstId = options.value[0].sys_id;
+      changeSelectRow(firstId);
     }
-    // 构建当前预警对象
-    const warnObj: WarnResult = { warnName };
-    warnObj.warnCtrl = getWarnCtrlByWarnName(warnName);
-    list.forEach((item: any) => {
-      const { deviceType, fmin, fmax, trendMin, trendMax, trendCxTimeUnit } = item;
-      // CO
-      if (deviceType === 'modelsensor_co') {
-        if (fmin != null && fmax != null) {
-          warnObj.coRange = `${fmin} - ${fmax}`;
-        } else if (fmin != null && fmax == null) {
-          warnObj.coRange = `> ${fmin}`;
-        }
-        // 处理变化率
-        else if (trendMin != null && trendMax != null) {
-          if (warnName === '黄色预警(较大风险)') {
-            warnObj.coTrend = `加速上升(${trendMin} - ${trendMax})`;
-          } else if (warnName === '橙色预警(重大风险)') {
-            warnObj.coTrend = `急剧上升(${trendMin} - ${trendMax})`;
-          } else if (warnName === '红色预警(特别重大风险)') {
-            warnObj.coTrend = `极端上升(${trendMin} - ${trendMax})`;
+  }
+  // 切换检测数据
+  function changeSelectRow(deviceID) {
+    optionValue.value = deviceID;
+    refresh();
+  }
+  // 预警等级映射
+  const warnTypeMap = {
+    '102': '黄色预警(较大风险)',
+    '103': '橙色预警(重大风险)',
+    '104': '红色预警(特别重大风险)',
+  };
+  // 处理接口返回数据
+  interface WarnResult {
+    warnName: string;
+    coRange?: string;
+    coTrend?: string;
+    tempRange?: string;
+    tempTrend?: string;
+    hclRange?: string;
+    warnCtrl?: string;
+  }
+  // 处理预警指标展示数据格式
+  function groupWarnByType(data: any) {
+    const result: WarnResult[] = [];
+    // 遍历 102、103、104...
+    Object.keys(data).forEach((warnKey) => {
+      const list = data[warnKey] || [];
+      const warnName = warnTypeMap[warnKey as keyof typeof warnTypeMap];
+      function getWarnCtrlByWarnName(warnName) {
+        if (warnName === '黄色预警(较大风险)') return '预警';
+        if (warnName === '橙色预警(重大风险)') return '报警启动喷淋';
+        if (warnName === '红色预警(特别重大风险)') return '启动喷淋与排烟';
+        return '';
+      }
+      // 构建当前预警对象
+      const warnObj: WarnResult = { warnName };
+      warnObj.warnCtrl = getWarnCtrlByWarnName(warnName);
+      list.forEach((item: any) => {
+        const { deviceType, fmin, fmax, trendMin, trendMax, trendCxTimeUnit } = item;
+        // CO
+        if (deviceType === 'modelsensor_co') {
+          if (fmin != null && fmax != null) {
+            warnObj.coRange = `${fmin} - ${fmax}`;
+          } else if (fmin != null && fmax == null) {
+            warnObj.coRange = `> ${fmin}`;
+          }
+          // 处理变化率
+          else if (trendMin != null && trendMax != null) {
+            if (warnName === '黄色预警(较大风险)') {
+              warnObj.coTrend = `加速上升(${trendMin} - ${trendMax})`;
+            } else if (warnName === '橙色预警(重大风险)') {
+              warnObj.coTrend = `急剧上升(${trendMin} - ${trendMax})`;
+            } else if (warnName === '红色预警(特别重大风险)') {
+              warnObj.coTrend = `极端上升(${trendMin} - ${trendMax})`;
+            }
+          } else if (trendMin != null && trendMax == null) {
+            warnObj.coTrend = `极端上升>${trendMin}`;
           }
-        } else if (trendMin != null && trendMax == null) {
-          warnObj.coTrend = `极端上升>${trendMin}`;
         }
-      }
-      // 温度(带单位)
-      else if (deviceType === 'modelsensor_temperature') {
-        if (fmin != null && fmax != null) {
-          warnObj.tempRange = `${fmin} - ${fmax}℃`;
-        } else if (fmin != null && fmax == null) {
-          warnObj.tempRange = `≥ ${fmin}℃(持续高温)`;
-        } else if (trendMin != null) {
-          let unit = '';
-          if (trendCxTimeUnit === 0) unit = '分钟';
-          if (trendCxTimeUnit === 1) unit = '小时';
-          warnObj.tempTrend = `>+${trendMin}℃ /${unit}`;
+        // 温度(带单位)
+        else if (deviceType === 'modelsensor_temperature') {
+          if (fmin != null && fmax != null) {
+            warnObj.tempRange = `${fmin} - ${fmax}℃`;
+          } else if (fmin != null && fmax == null) {
+            warnObj.tempRange = `≥ ${fmin}℃(持续高温)`;
+          } else if (trendMin != null) {
+            let unit = '';
+            if (trendCxTimeUnit === 0) unit = '分钟';
+            if (trendCxTimeUnit === 1) unit = '小时';
+            warnObj.tempTrend = `>+${trendMin}℃ /${unit}`;
+          }
         }
-      }
-      // HCL
-      else if (deviceType === 'modelsensor_hcl') {
-        if (fmin != null && fmax != null) {
-          warnObj.hclRange = `${fmin} - ${fmax}`;
-        } else if (fmin != null && fmax == null) {
-          warnObj.hclRange = `≥ ${fmin}`;
+        // HCL
+        else if (deviceType === 'modelsensor_hcl') {
+          if (fmin != null && fmax != null) {
+            warnObj.hclRange = `${fmin} - ${fmax}`;
+          } else if (fmin != null && fmax == null) {
+            warnObj.hclRange = `≥ ${fmin}`;
+          }
         }
-      }
+      });
+      result.push(warnObj);
     });
-    result.push(warnObj);
-  });
 
-  return result;
-}
-// 刷新数据
-async function refresh() {
-  // 由于模型中需要用到风门的监测数据,这里进行公共调用(后期精确调用风门)
-  const modalRes = {};
-  const systemParams = {
-    devicetype: 'sys',
-    systemID: optionValue.value,
-  };
-  const resSys = await getSystem(systemParams);
-  const params = {
-    sysId: optionValue.value,
-    monitorType: 2,
-  };
-  const warnInfo = await getWarnInfo(params);
-  const staffInfo = await getStaffInfo({ sysId: optionValue.value });
-  Object.assign(modalRes, resSys);
-  if (pageType.value == 'fire_risk_warn') {
-    configs.value = [...testBeltNew];
-    const params = {
-      sysId: optionValue.value,
-      dataList: 'fire_risk_warn,warn_result,vehicle_co_correlate',
-      alarmLevel: '102,103,104',
+    return result;
+  }
+  // 刷新数据
+  async function refresh() {
+    // 由于模型中需要用到风门的监测数据,这里进行公共调用(后期精确调用风门)
+    const modalRes = {};
+    const systemParams = {
+      devicetype: 'sys',
+      systemID: optionValue.value,
     };
-    const resWarn = await getMonitorAndAlertBelt(params);
-    resWarn.warnInfo = groupWarnByType(warnInfo);
-    resWarn.staffInfo = staffInfo;
-    updateData(resWarn);
-    Object.assign(modalRes, resWarn);
-  } else if (pageType.value == 'emergencyControl') {
-    updateData(resSys);
-    configs.value = [...testYjkf];
-    const alarmParams = {
+    const resSys = await getSystem(systemParams);
+    const params = {
       sysId: optionValue.value,
-      alarmLevel: '104',
+      monitorType: 2,
     };
-    const alarmRes = await getWarnResult(alarmParams);
-    if (alarmRes.warn_result) {
-      data.value.warn_result = alarmRes.warn_result;
+    const warnInfo = await getWarnInfo(params);
+    const staffInfo = await getStaffInfo({ sysId: optionValue.value });
+    Object.assign(modalRes, resSys);
+    if (pageType.value == 'fire_risk_warn') {
+      configs.value = [...testBeltNew];
+      const params = {
+        sysId: optionValue.value,
+        dataList: 'fire_risk_warn,warn_result,vehicle_co_correlate',
+        alarmLevel: '102,103,104',
+      };
+      const resWarn = await getMonitorAndAlertBelt(params);
+      resWarn.warnInfo = groupWarnByType(warnInfo);
+      resWarn.staffInfo = staffInfo;
+      updateData(resWarn);
+      Object.assign(modalRes, resWarn);
+    } else if (pageType.value == 'emergencyControl') {
+      updateData(resSys);
+      configs.value = [...testYjkf];
+      const alarmParams = {
+        sysId: optionValue.value,
+        alarmLevel: '104',
+      };
+      const alarmRes = await getWarnResult(alarmParams);
+      if (alarmRes.warn_result) {
+        data.value.warn_result = alarmRes.warn_result;
+      }
+      data.value.warnInfo = groupWarnByType(warnInfo);
+      data.value.staffInfo = staffInfo;
+      updateData(data.value);
+    } else if (pageType.value == 'sprayControl') {
+      updateData(resSys);
+      const params1 = {
+        sysId: optionValue.value,
+        alarmLevel: '103,104',
+      };
+      const sprayData = [];
+      const dustData = [];
+      if (data.value?.deviceInfo) {
+        // 遍历对象的所有 value
+        Object.values(data.value.deviceInfo).forEach((item) => {
+          const hasSprayAuto = item.type && item.type.toLowerCase().includes('spray');
+          if (hasSprayAuto) {
+            sprayData.push({ ...item, ...item.readData });
+          }
+        });
+        Object.values(data.value.deviceInfo).forEach((item) => {
+          const hasDustAuto = item.type && item.type.toLowerCase().includes('dustdev');
+          if (hasDustAuto) {
+            dustData.push({ ...item, ...item.readData });
+          }
+        });
+      }
+      data.value.sprayData = sprayData;
+      data.value.dustData = dustData;
+      data.value.warnInfo = groupWarnByType(warnInfo);
+      data.value.staffInfo = staffInfo;
+      const alarmRes = await getWarnResult(params1);
+      if (alarmRes.warn_result) {
+        data.value.warn_result = alarmRes.warn_result;
+      }
+      configs.value = [...testSpary];
+    } else {
+      configs.value = testBeltNew;
     }
-    data.value.warnInfo = groupWarnByType(warnInfo);
-    data.value.staffInfo = staffInfo;
-    updateData(data.value);
-  } else if (pageType.value == 'sprayControl') {
-    updateData(resSys);
-    const params1 = {
-      sysId: optionValue.value,
-      alarmLevel: '103,104',
-    };
-    const sprayData = [];
-    const dustData = [];
-    if (data.value?.deviceInfo) {
-      // 遍历对象的所有 value
-      Object.values(data.value.deviceInfo).forEach((item) => {
-        const hasSprayAuto = item.type && item.type.toLowerCase().includes('spray');
-        if (hasSprayAuto) {
-          sprayData.push({ ...item, ...item.readData });
-        }
-      });
-      Object.values(data.value.deviceInfo).forEach((item) => {
-        const hasDustAuto = item.type && item.type.toLowerCase().includes('dustdev');
-        if (hasDustAuto) {
-          dustData.push({ ...item, ...item.readData });
-        }
-      });
+    modalMonitorData.value = modalRes;
+  }
+
+  provide('refreshDeviceData', async () => {
+    if (isRefreshing.value) {
+      return;
     }
-    data.value.sprayData = sprayData;
-    data.value.dustData = dustData;
-    data.value.warnInfo = groupWarnByType(warnInfo);
-    data.value.staffInfo = staffInfo;
-    const alarmRes = await getWarnResult(params1);
-    if (alarmRes.warn_result) {
-      data.value.warn_result = alarmRes.warn_result;
+    isRefreshing.value = true;
+    try {
+      await refresh();
+    } catch (error) {
+    } finally {
+      isRefreshing.value = false;
     }
-    configs.value = [...testSpary];
-  } else {
-    configs.value = testBeltNew;
+  });
+  // 定时刷新
+  function initInterval() {
+    if (timer) clearInterval(timer);
+    timer = setInterval(() => {
+      refresh();
+    }, 5000);
   }
-  modalMonitorData.value = modalRes;
-}
 
-provide('refreshDeviceData', async () => {
-  if (isRefreshing.value) {
-    return;
-  }
-  isRefreshing.value = true;
-  try {
+  async function changePage(pageTypeStr) {
+    const target = pageTypeStr || route.query.pageType || 'fire_risk_warn';
+    if (pageType.value === target) return;
+    pageType.value = target;
+    configs.value = pageCache.value[target]?.configs || testBeltNew;
+    await nextTick();
     await refresh();
-  } catch (error) {
-  } finally {
-    isRefreshing.value = false;
   }
-});
-// 定时刷新
-function initInterval() {
-  if (timer) clearInterval(timer);
-  timer = setInterval(() => {
-    refresh();
-  }, 5000);
-}
 
-async function changePage(pageTypeStr) {
-  const target = pageTypeStr || route.query.pageType || 'fire_risk_warn';
-  if (pageType.value === target) return;
-  pageType.value = target;
-  configs.value = pageCache.value[target]?.configs || testBeltNew;
-  await nextTick();
-  await refresh();
-}
-
-function initModalAnimate(modal3D) {
-  modal = modal3D;
-  modal.isRender = true;
-  modalAnimate(modal, modalMonitorData);
-}
-
-function clearTimer() {
-  if (timer) {
-    clearInterval(timer);
-    timer = null;
+  function initModalAnimate(modal3D) {
+    modal = modal3D;
+    modal.isRender = true;
+    modalAnimate(modal, modalMonitorData);
   }
-}
 
-watch(
-  () => route.query.pageType,
-  (newQueryType) => {
-    if (newQueryType) {
-      changePage(newQueryType as string);
+  function clearTimer() {
+    if (timer) {
+      clearInterval(timer);
+      timer = null;
     }
-  },
-  { immediate: true } // 初始化立刻执行
-);
+  }
 
-watch(
-  () => modalMonitorData.value,
-  (newData, oldData) => {
-    if (newData && !Object.keys(oldData).length) {
-      isInitModal.value = true;
+  watch(
+    () => route.query.pageType,
+    (newQueryType) => {
+      if (newQueryType) {
+        changePage(newQueryType as string);
+      }
+    },
+    { immediate: true } // 初始化立刻执行
+  );
+
+  watch(
+    () => modalMonitorData.value,
+    (newData, oldData) => {
+      if (newData && !Object.keys(oldData).length) {
+        isInitModal.value = true;
+      }
     }
-  }
-);
+  );
 
-onMounted(async () => {
-  await getSysDataSource();
-  await refresh();
-  initInterval();
-});
+  onMounted(async () => {
+    await getSysDataSource();
+    await refresh();
+    initInterval();
+  });
 
-onUnmounted(() => {
-  clearTimer();
-  destroy(modal);
-  modal?.destroy();
-});
+  onUnmounted(() => {
+    clearTimer();
+    destroy(modal);
+    modal?.destroy();
+  });
 </script>
 
 <style lang="less" scoped>
-.company-home {
-  background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
-  width: 100%;
-  height: 100%;
-  color: @white;
-  position: relative;
-  font-family: 'Microsoft YaHei', sans-serif;
-  .top-bg {
-    width: 100%;
-    height: 56px;
-    position: absolute;
-    margin-top: 10px;
-    z-index: 1;
-  }
-  .header-container {
-    position: absolute;
-    top: 20px;
-    left: 20px;
-    z-index: 10;
-  }
-
-  .border {
-    width: 100%;
-    height: 94%;
-    background: url('/@/assets/images/beltFire/mainbj.png') no-repeat;
-    background-size: 100% 100%;
-    position: relative;
-    width: 100%;
-  }
-}
-
-.center-warning-container {
-  position: absolute;
-  left: 50%;
-  transform: translateX(-50%);
-  top: 50%;
-  width: 600px;
-  height: 200px;
-  background-color: rgba(0, 0, 0, 0.7);
-  border-radius: 10px;
-  padding: 15px;
-  box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
-  z-index: 5;
-  color: #fff;
-
-  .warning-header {
-    font-size: 18px;
-    font-weight: bold;
-    margin-bottom: 10px;
-    color: #ff6b6b;
-  }
-
-  .warning-list {
+  .company-home {
+    background: url('/@/assets/images/beltFire/baseMap.png') no-repeat center;
     width: 100%;
     height: 100%;
-    overflow-y: auto;
-    display: flex;
-    flex-direction: column;
-    gap: 8px;
-  }
-
-  .warning-item {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    padding: 8px;
-    background-color: rgba(0, 0, 0, 0.5);
-    border-radius: 5px;
-    border-left: 4px solid #ff6b6b;
+    color: @white;
+    position: relative;
+    font-family: 'Microsoft YaHei', sans-serif;
+    .top-bg {
+      width: 100%;
+      height: 56px;
+      position: absolute;
+      margin-top: 10px;
+      z-index: 1;
+    }
+    .header-container {
+      position: absolute;
+      top: 20px;
+      left: 20px;
+      z-index: 10;
+    }
 
-    .warning-time {
-      font-size: 14px;
-      color: #ccc;
+    .border {
+      width: 100%;
+      height: 94%;
+      background: url('/@/assets/images/beltFire/mainbj.png') no-repeat;
+      background-size: 100% 100%;
+      position: relative;
+      width: 100%;
     }
   }
 
-  // 中间预警结果区
   .center-warning-container {
     position: absolute;
     left: 50%;
@@ -451,67 +405,113 @@ onUnmounted(() => {
         font-size: 14px;
         color: #ccc;
       }
+    }
 
-      .warning-level {
-        font-size: 14px;
+    // 中间预警结果区
+    .center-warning-container {
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      top: 50%;
+      width: 600px;
+      height: 200px;
+      background-color: rgba(0, 0, 0, 0.7);
+      border-radius: 10px;
+      padding: 15px;
+      box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
+      z-index: 5;
+      color: #fff;
+
+      .warning-header {
+        font-size: 18px;
         font-weight: bold;
-        padding: 4px 8px;
-        border-radius: 4px;
-        &.level-critical {
-          background-color: #ff6b6b;
-          color: white;
-        }
-        &.level-high {
-          background-color: #ffcc00;
-          color: black;
-        }
-        &.level-normal {
-          background-color: #66cc66;
-          color: white;
-        }
+        margin-bottom: 10px;
+        color: #ff6b6b;
+      }
+
+      .warning-list {
+        width: 100%;
+        height: 100%;
+        overflow-y: auto;
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
       }
 
-      .warning-action {
-        .btn-start-spray {
-          background-color: #00e1ff;
-          color: #000;
-          border: none;
-          padding: 4px 10px;
+      .warning-item {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 8px;
+        background-color: rgba(0, 0, 0, 0.5);
+        border-radius: 5px;
+        border-left: 4px solid #ff6b6b;
+
+        .warning-time {
+          font-size: 14px;
+          color: #ccc;
+        }
+
+        .warning-level {
+          font-size: 14px;
+          font-weight: bold;
+          padding: 4px 8px;
           border-radius: 4px;
-          cursor: pointer;
-          font-size: 12px;
-          transition: all 0.3s;
-          &:hover {
-            background-color: #00c3e6;
+          &.level-critical {
+            background-color: #ff6b6b;
+            color: white;
+          }
+          &.level-high {
+            background-color: #ffcc00;
+            color: black;
+          }
+          &.level-normal {
+            background-color: #66cc66;
+            color: white;
+          }
+        }
+
+        .warning-action {
+          .btn-start-spray {
+            background-color: #00e1ff;
+            color: #000;
+            border: none;
+            padding: 4px 10px;
+            border-radius: 4px;
+            cursor: pointer;
+            font-size: 12px;
+            transition: all 0.3s;
+            &:hover {
+              background-color: #00c3e6;
+            }
           }
         }
       }
     }
-  }
 
-  // 巷道示意图
-  .belt-diagram {
-    position: absolute;
-    left: 50%;
-    transform: translateX(-50%);
-    bottom: 50px;
-    width: 800px;
-    height: 100px;
-    display: flex;
-    justify-content: center;
-    align-items: center;
+    // 巷道示意图
+    .belt-diagram {
+      position: absolute;
+      left: 50%;
+      transform: translateX(-50%);
+      bottom: 50px;
+      width: 800px;
+      height: 100px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
 
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: contain;
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+      }
     }
   }
-}
-.modal-box {
-  width: 100%;
-  height: 100%;
-  position: absolute;
-  z-index: 1;
-}
-</style>
+  .modal-box {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    z-index: 1;
+  }
+</style>

+ 103 - 19
src/views/vent/home/configurable/firedoor/components/fireDoorBoard.vue

@@ -12,7 +12,7 @@
       <div class="left-panel">
         <!-- 风门数据列表 -->
         <div class="door-list">
-          <div class="door-card" v-for="(item, index) in props.data" :key="index">
+          <div class="door-card" v-for="(item, index) in displayData" :key="index">
             <div class="door-position">
               <div class="position"></div>
               <div class="door-name">
@@ -56,7 +56,12 @@
 
             <div class="model-placeholder">
               <!-- <img :src="gatePng" alt="风门" class="model-img" /> -->
-              <pidaihangSVG :ref="(el) => setChildRef(el, index)" :identify="String(index)" />
+              <template v-if="item.readData?.MBOpen?.value">
+                <pidaihangSVG :ref="(el) => setChildRef(el, index)" :identify="String(index)" />
+              </template>
+              <template v-else>
+                <fireDoorSVG :ref="(el) => setChildRef(el, index)" :identify="String(index)" />
+              </template>
             </div>
           </div>
         </div>
@@ -74,9 +79,10 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, onMounted, watch, inject } from 'vue';
+  import { ref, computed, onMounted, watch, inject } from 'vue';
   import { getFormattedText } from '../../hooks/helper';
   import pidaihangSVG from '/@/views/vent/monitorManager/fireDoorMonitor/components/pidaihangSVG.vue';
+  import fireDoorSVG from '/@/views/vent/monitorManager/fireDoorMonitor/components/fireDoorSVG.vue';
   import HandleModal from '/@/views/vent/monitorManager/gateMonitor/modal.vue';
   import { message } from 'ant-design-vue';
   import { doorControlApi } from '/@/views/vent/home/configurable/firedoor/configurable.api.fireDoorMonitor';
@@ -110,6 +116,36 @@
   const paramcode = ref(''); // 模态框操作代码
   const childRefs = ref<(InstanceType<typeof pidaihangSVG> | null)[]>([]);
   const selectData = ref<any>({});
+
+  // 模拟数据开关 - 设为 true 启用模拟数据
+  const useMockData = ref(false);
+  // 模拟数据
+  const mockData = ref([
+    {
+      devicePos: '防火门2号(无皮带)',
+      deviceID: 'FD002',
+      systemDeviceId: 'SYS002',
+      deviceType: 'fireDoor',
+      readData: {
+        frontGateOpen: { value: '0' },
+      },
+    },
+    // {
+    //   devicePos: '防火门1号(有皮带)',
+    //   deviceID: 'FD001',
+    //   systemDeviceId: 'SYS001',
+    //   deviceType: 'fireDoor',
+    //   readData: {
+    //     frontGateOpen: { value: '0' },
+    //     MBOpen: { value: '0' },
+    //   },
+    // },
+  ]);
+
+  // 实际显示的数据
+  const displayData = computed(() => {
+    return useMockData.value ? mockData.value : props.data;
+  });
   const setChildRef = (el, index) => {
     if (el) {
       childRefs.value[index] = el;
@@ -120,21 +156,31 @@
   function executeAnimation(targetIndex: number, code: string) {
     if (targetIndex === -1) return;
     const isOpen = code.includes('Open');
+    const targetItem = displayData.value[targetIndex];
+    if (!targetItem) return;
+    const hasBelt = targetItem.readData?.MBOpen?.value !== undefined;
 
     // 2D SVG 动画
-    if (childRefs.value[targetIndex]) {
-      if (code === 'AllOpen_S' || code === 'AllClose_S') {
-        childRefs.value[targetIndex].animate(isOpen, isOpen, isOpen);
-      } else if (code === 'frontGateOpen_S' || code === 'frontGateClose_S') {
-        childRefs.value[targetIndex].animateFireDoor(isOpen);
-      } else if (code === 'MBOpen_S' || code === 'MBClose_S') {
-        childRefs.value[targetIndex].animateBeltDoor(isOpen);
+    const svgRef = childRefs.value[targetIndex];
+    if (svgRef) {
+      if (hasBelt) {
+        // pidaihangSVG 动画逻辑
+        if (code === 'AllOpen_S' || code === 'AllClose_S') {
+          svgRef.animate(isOpen, isOpen, isOpen);
+        } else if (code === 'frontGateOpen_S' || code === 'frontGateClose_S') {
+          svgRef.animateFireDoor(isOpen);
+        } else if (code === 'MBOpen_S' || code === 'MBClose_S') {
+          svgRef.animateBeltDoor(isOpen);
+        }
+      } else {
+        // fireDoorSVG 动画逻辑
+        svgRef.animate(isOpen, false, false);
       }
     }
 
     // 3D 动画:构造期望状态的门数据并触发
-    if (props.data && Array.isArray(props.data)) {
-      const doors = props.data.map((door: any, i: number) => {
+    if (displayData.value && Array.isArray(displayData.value)) {
+      const doors = displayData.value.map((door: any, i: number) => {
         if (i === targetIndex && door.readData) {
           const updated = {
             ...door,
@@ -143,7 +189,8 @@
           if (code === 'AllOpen_S' || code === 'AllClose_S' || code === 'frontGateOpen_S' || code === 'frontGateClose_S') {
             updated.readData.frontGateOpen = { ...updated.readData.frontGateOpen, value: isOpen ? '1' : '0' };
           }
-          if (code === 'AllOpen_S' || code === 'AllClose_S' || code === 'MBOpen_S' || code === 'MBClose_S') {
+          // 只有原数据有 MBOpen 时才更新
+          if (hasBelt && (code === 'AllOpen_S' || code === 'AllClose_S' || code === 'MBOpen_S' || code === 'MBClose_S')) {
             updated.readData.MBOpen = { ...updated.readData.MBOpen, value: isOpen ? '1' : '0' };
           }
           return updated;
@@ -158,9 +205,16 @@
     if (!selectData?.readData) return;
     const frontOpen = selectData.readData?.frontGateOpen?.value === '1';
     const beltOpen = selectData.readData?.MBOpen?.value === '1';
-    if (childRefs.value[index]) {
-      childRefs.value[index].animateFireDoor(frontOpen);
-      childRefs.value[index].animateBeltDoor(beltOpen);
+    const svgRef = childRefs.value[index];
+    if (svgRef) {
+      if (selectData.readData?.MBOpen?.value) {
+        // pidaihangSVG 动画逻辑
+        svgRef.animateFireDoor(frontOpen);
+        svgRef.animateBeltDoor(beltOpen);
+      } else {
+        // fireDoorSVG 动画逻辑
+        svgRef.animate(frontOpen, false, false);
+      }
     }
   }
 
@@ -178,6 +232,30 @@
   }
 
   function handleOK(passWord, handlerState, value?) {
+    if (useMockData.value) {
+      // 测试模式:跳过 API,直接执行动画
+      modalIsShow.value = false;
+      message.success('【测试模式】动画已触发');
+
+      // 更新模拟数据中的状态
+      const targetItem = displayData.value.find((item: any) => item.deviceID === selectData.value.deviceID);
+      if (targetItem?.readData) {
+        const isOpen = paramcode.value.includes('Open');
+        if (paramcode.value.includes('frontGate') || paramcode.value.includes('All')) {
+          targetItem.readData.frontGateOpen = { value: isOpen ? '1' : '0' };
+        }
+        if (paramcode.value.includes('MB') || paramcode.value.includes('All')) {
+          if (targetItem.readData.MBOpen) {
+            targetItem.readData.MBOpen = { value: isOpen ? '1' : '0' };
+          }
+        }
+      }
+
+      const targetIndex = displayData.value.findIndex((item: any) => item.deviceID === selectData.value.deviceID);
+      executeAnimation(targetIndex, paramcode.value);
+      return;
+    }
+
     if (!passWord && !globalConfig?.simulatedPassword) {
       message.warning('请输入密码');
       return;
@@ -202,7 +280,7 @@
             message.success('指令已下发成功!');
           }
 
-          const targetIndex = props.data.findIndex((item) => item.deviceID === selectData.value.deviceID);
+          const targetIndex = displayData.value.findIndex((item: any) => item.deviceID === selectData.value.deviceID);
           executeAnimation(targetIndex, paramcode.value);
           // 触发刷新事件
           emit('refresh');
@@ -221,10 +299,10 @@
   }
 
   watch(
-    () => props.data,
+    () => displayData.value,
     (newData) => {
       if (!newData || !newData.length) return;
-      newData.forEach((item, index) => {
+      newData.forEach((item: any, index: number) => {
         monitorAnimation(item, index);
       });
     },
@@ -467,6 +545,12 @@
     top: -140%;
     left: -110%;
   }
+  .fire-door-svg {
+    width: 100% !important;
+    height: 100% !important;
+    top: 0 !important;
+    left: 0 !important;
+  }
   .model-img {
     width: 100%;
     display: flex;

+ 62 - 16
src/views/vent/home/configurable/firedoor/threejs/firedoor.threejs.ts

@@ -12,6 +12,8 @@ let currentHandleGateAnimate: ((doors: any[]) => void) | null = null;
 export function trigger3DAnimation(doors: any[]) {
   if (currentHandleGateAnimate) {
     currentHandleGateAnimate(doors);
+  } else {
+    console.warn('[3D动画] currentHandleGateAnimate 未初始化,请确保 modalAnimate 已调用');
   }
 }
 
@@ -100,37 +102,45 @@ export async function modalAnimate(modal: any, modalMonitorData: Ref<any, any>)
     if (!doors || doors.length === 0) return;
 
     doors.forEach((gateData: any, index: number) => {
-      // 根据门的索引查找对应的 3D 模型对象 (men_1, men_2, ...)
-      const doorSuffix = index + 1;
-      const fireDoorMesh = findMesh(doorContainer, [`men_${doorSuffix}`, `men_1`, 'men']);
-      const beltCoverMesh = findMesh(doorContainer, [`gaiban_${doorSuffix}`, 'gaiban', 'gaiban_1']);
-
       // 获取状态值
       const frontGateOpenVal = gateData['readData']?.['frontGateOpen']?.value;
       const mbOpenVal = gateData['readData']?.['MBOpen']?.value;
-
-      // 判断状态 (与 2D 动画保持一致:'1' 为打开)
       const isFireDoorOpen = frontGateOpenVal == '1' || frontGateOpenVal == 1;
       const isBeltCoverOpen = mbOpenVal == '1' || mbOpenVal == 1;
+      const hasBelt = mbOpenVal !== undefined && mbOpenVal !== null;
 
-      // 执行防火门动画
-      if (fireDoorMesh) {
-        animateFireDoor(fireDoorMesh, isFireDoorOpen);
-      }
+      if (hasBelt) {
+        // 有皮带:控制 men_1(防火门)+ gaiban(皮带盖)
+        const fireDoorMesh = findMesh(doorContainer, [`men_${index + 1}`, 'men_1', 'men']);
+        const beltCoverMesh = findMesh(doorContainer, [`gaiban_${index + 1}`, 'gaiban', 'gaiban_1']);
 
-      // 执行皮带密闭动画
-      if (beltCoverMesh) {
-        animateBeltCover(beltCoverMesh, isBeltCoverOpen);
+        if (fireDoorMesh) {
+          animateFireDoorWithBelt(fireDoorMesh, isFireDoorOpen);
+        }
+        if (beltCoverMesh) {
+          animateBeltCover(beltCoverMesh, isBeltCoverOpen);
+        }
+      } else {
+        // 无皮带:控制 men_2 + men3(两扇防火门向两边开)
+        const leftDoorMesh = findMesh(doorContainer, ['men_2']);
+        const rightDoorMesh = findMesh(doorContainer, ['men3']);
+
+        if (leftDoorMesh) {
+          animateFireDoorLeft(leftDoorMesh, isFireDoorOpen);
+        }
+        if (rightDoorMesh) {
+          animateFireDoorRight(rightDoorMesh, isFireDoorOpen);
+        }
       }
     });
   };
 
   /**
-   * 防火门动画
+   * 有皮带时的防火门动画
    * 关闭: (0, 0, 0)
    * 开启: (0, 0, 85度) -> 绕 Z 轴旋转
    */
-  const animateFireDoor = (mesh: THREE.Object3D, isOpen: boolean) => {
+  const animateFireDoorWithBelt = (mesh: THREE.Object3D, isOpen: boolean) => {
     const targetRotationZ = isOpen ? THREE.MathUtils.degToRad(85) : 0;
 
     gsap.to(mesh.rotation, {
@@ -143,6 +153,42 @@ export async function modalAnimate(modal: any, modalMonitorData: Ref<any, any>)
     });
   };
 
+  /**
+   * 无皮带时左侧防火门动画 (men_2)
+   * 关闭: (0, 0, 0)
+   * 开启: (0, 0, -90度) -> 绕 Z 轴旋转
+   */
+  const animateFireDoorLeft = (mesh: THREE.Object3D, isOpen: boolean) => {
+    const targetRotationZ = isOpen ? THREE.MathUtils.degToRad(-90) : 0;
+
+    gsap.to(mesh.rotation, {
+      x: 0,
+      y: 0,
+      z: targetRotationZ,
+      duration: 1.5,
+      ease: 'power2.inOut',
+      overwrite: true,
+    });
+  };
+
+  /**
+   * 无皮带时右侧防火门动画 (men_3)
+   * 关闭: (0, 0, 0)
+   * 开启: (0, 0, 90度) -> 绕 Z 轴旋转
+   */
+  const animateFireDoorRight = (mesh: THREE.Object3D, isOpen: boolean) => {
+    const targetRotationZ = isOpen ? THREE.MathUtils.degToRad(90) : 0;
+
+    gsap.to(mesh.rotation, {
+      x: 0,
+      y: 0,
+      z: targetRotationZ,
+      duration: 1.5,
+      ease: 'power2.inOut',
+      overwrite: true,
+    });
+  };
+
   /**
    * 皮带密闭动画
    * 关闭: (0, 0, 0)

+ 0 - 1
src/views/vent/monitorManager/compressor/components/nitrogenHome.vue

@@ -336,7 +336,6 @@
   function handlerControlModel(data) {}
 
   watch([monitorDataGroupNum, loading], ([newMonitorDataGroupNum, newLoading]) => {
-    debugger;
     nextTick(() => {
       if (newMonitorDataGroupNum && !newLoading) {
         setModelType(props.modalType, newMonitorDataGroupNum);

+ 12 - 9
src/views/vent/monitorManager/compressor/index.vue

@@ -8,7 +8,12 @@
     >
       智能注氮管控系统
     </customHeader>
-    <nitrogenHome v-if="activeKey == 'nitrogen_page' && optionValue" :device-id="optionValue" :modal-type="modalType" :module-type-name="moduleTypeName" />
+    <nitrogenHome
+      v-if="activeKey == 'nitrogen_page' && optionValue"
+      :device-id="optionValue"
+      :modal-type="modalType"
+      :module-type-name="moduleTypeName"
+    />
     <nitrogenEcharts v-if="activeKey == 'yfj_monitor_echarts'" />
     <nitrogenHistory ref="historyTable" :device-id="optionValue" :device-type="optionType" v-if="activeKey == 'yfj_history'" />
     <nitrogenHandleHistory ref="alarmHistoryTable" v-if="activeKey == 'yfj_handler_history'" />
@@ -46,9 +51,9 @@
   const options = ref();
   const optionValue = ref('');
   const optionType = ref('');
-  const modalType = ref('');
+  const modalType = ref('nitrogenUnderground');
   const isRefresh = ref(true);
-  let moduleTypeName=ref('')
+  let moduleTypeName = ref('');
 
   function changeActive(activeValue) {
     activeKey.value = activeValue;
@@ -99,8 +104,6 @@
 
   // 切换检测数据
   async function getSelectRow(deviceID) {
-    console.log(deviceID,'deviceID---')
-    console.log(options.value,'options.value')
     const currentData = options.value.find((item: any) => {
       return item.id == deviceID;
     });
@@ -115,12 +118,12 @@
     if (currentData['strsystype'] == '1') {
       // 空压机
       // modalType.value = 'nitrogen';
-      moduleTypeName.value='KYJ'
-    } else  {
+      moduleTypeName.value = 'KYJ';
+    } else {
       // 制氮机
       // modalType.value = 'nitrogenUnderground';
-      moduleTypeName.value='ZDJ'
-    } 
+      moduleTypeName.value = 'ZDJ';
+    }
   }
 
   onMounted(async () => {

+ 1 - 0
src/views/vent/monitorManager/deviceMonitor/components/device/device.data.ts

@@ -74,6 +74,7 @@ export function getMonitorComponent() {
     case 'sdmtjtswmk': //上湾
     case 'zmhjhzmy': // 韩咀
     case 'dttzjymy': //东泰金源
+    case 'sxmyhgjtlhky': //龙华
       FiberModal = defineAsyncComponent(() => import('./modal/fiber.modal.sw.vue'));
       break;
     case 'sdmtjtwlmlmk': //乌兰木伦

+ 24 - 25
src/views/vent/monitorManager/deviceMonitor/components/device/modal/fiber.modal.sw.vue

@@ -45,8 +45,8 @@
               <SvgIcon class="icon-style" size="38" name="risk-level" />
             </div>
             <div class="item-container">
-              <div class="title">风险等级</div>
-              <div class="warning-value">低风险</div>
+              <div class="title">故障诊断</div>
+              <div class="warning-value">{{ posMonitor.alam_str || '--' }}</div>
             </div>
           </div>
         </div>
@@ -182,18 +182,18 @@
 </script>
 <style lang="less" scoped>
   .fiber-modal {
-    width: 100%;
-    height: 650px;
     display: flex;
     flex-direction: row;
     justify-content: space-between;
+    width: 100%;
+    height: 650px;
 
     .modal-left {
       width: 200px;
       height: 100%;
-      overflow-y: auto;
-      background: #ffffff11;
       padding: 5px;
+      background: #fff1;
+      overflow-y: auto;
       border-radius: 5px;
 
       .active-device-title {
@@ -211,15 +211,15 @@
         }
 
         &::after {
-          content: '';
           position: absolute;
+          top: 12px;
+          left: 10px;
           display: block;
           width: 8px;
           height: 8px;
-          top: 12px;
-          left: 10px;
-          transform: rotateZ(45deg) skew(10deg, 10deg);
           background: #45d3fd;
+          transform: rotateZ(45deg) skew(10deg, 10deg);
+          content: '';
         }
       }
     }
@@ -234,14 +234,14 @@
         padding-left: 20px;
 
         &::after {
-          content: '';
           position: absolute;
+          top: 4px;
+          left: 10px;
           display: block;
           width: 4px;
           height: 12px;
-          top: 4px;
-          left: 10px;
           background: #45d3fd;
+          content: '';
           border-radius: 4px;
         }
       }
@@ -253,14 +253,14 @@
         margin-bottom: 10px;
 
         .top-item {
-          width: 200px;
-          height: 80px;
           display: flex;
           flex-direction: row;
           justify-content: center;
-          border: 1px solid rgba(25, 237, 255, 0.4);
-          box-shadow: inset 0 0 10px rgba(0, 197, 255, 0.6);
-          background: rgba(0, 0, 0, 0.06666666666666667);
+          width: 200px;
+          height: 80px;
+          border: 1px solid rgb(25 237 255 / 40%);
+          background: rgb(0 0 0 / 6.67%);
+          box-shadow: inset 0 0 10px rgb(0 197 255 / 60%);
           padding-top: 16px;
 
           .icon {
@@ -270,10 +270,10 @@
           }
 
           .item-container {
-            width: 100px;
             display: flex;
             flex-direction: column;
             justify-content: center;
+            width: 100px;
 
             div {
               text-align: center;
@@ -284,17 +284,16 @@
             }
 
             .value {
-              text-shadow: 0 0 25px #00fbfe;
+              position: relative;
+              top: -8px;
               background: linear-gradient(0deg, #45d3fd, #45d3fd, #61ddb1, #61ddb1);
+              text-shadow: 0 0 25px #00fbfe;
               font-style: normal;
               background-size: cover;
               font-family: electronicFont;
               font-size: 30px;
-              -webkit-background-clip: text;
               background-clip: text;
               -webkit-text-fill-color: transparent;
-              position: relative;
-              top: -8px;
 
               span {
                 font-family: Arial, Helvetica, sans-serif;
@@ -306,7 +305,7 @@
         }
 
         .warning-box {
-          padding-top: 0px;
+          padding-top: 0;
 
           .icon {
             margin-top: 20px;
@@ -376,7 +375,7 @@
   }
 
   :deep(.zxm-table-body) {
-    border: 1px solid rgba(57, 232, 255, 0.2) !important;
+    border: 1px solid rgb(57 232 255 / 20%) !important;
 
     .zxm-table-tbody > tr > td {
       border: none !important;

+ 24 - 25
src/views/vent/monitorManager/deviceMonitor/components/device/modal/fiber.modal.yjl.vue

@@ -45,8 +45,8 @@
               <SvgIcon class="icon-style" size="38" name="risk-level" />
             </div>
             <div class="item-container">
-              <div class="title">风险等级</div>
-              <div class="warning-value">低风险</div>
+              <div class="title">故障诊断</div>
+              <div class="warning-value">{{ posMonitor.alam_str }}</div>
             </div>
           </div>
         </div>
@@ -199,18 +199,18 @@
 </script>
 <style lang="less" scoped>
   .fiber-modal {
-    width: 100%;
-    height: 650px;
     display: flex;
     flex-direction: row;
     justify-content: space-between;
+    width: 100%;
+    height: 650px;
 
     .modal-left {
       width: 200px;
       height: 100%;
-      overflow-y: auto;
-      background: #ffffff11;
       padding: 5px;
+      background: #fff1;
+      overflow-y: auto;
       border-radius: 5px;
 
       .active-device-title {
@@ -228,15 +228,15 @@
         }
 
         &::after {
-          content: '';
           position: absolute;
+          top: 12px;
+          left: 10px;
           display: block;
           width: 8px;
           height: 8px;
-          top: 12px;
-          left: 10px;
-          transform: rotateZ(45deg) skew(10deg, 10deg);
           background: #45d3fd;
+          transform: rotateZ(45deg) skew(10deg, 10deg);
+          content: '';
         }
       }
     }
@@ -251,14 +251,14 @@
         padding-left: 20px;
 
         &::after {
-          content: '';
           position: absolute;
+          top: 4px;
+          left: 10px;
           display: block;
           width: 4px;
           height: 12px;
-          top: 4px;
-          left: 10px;
           background: #45d3fd;
+          content: '';
           border-radius: 4px;
         }
       }
@@ -270,14 +270,14 @@
         margin-bottom: 10px;
 
         .top-item {
-          width: 200px;
-          height: 80px;
           display: flex;
           flex-direction: row;
           justify-content: center;
-          border: 1px solid rgba(25, 237, 255, 0.4);
-          box-shadow: inset 0 0 10px rgba(0, 197, 255, 0.6);
-          background: rgba(0, 0, 0, 0.06666666666666667);
+          width: 200px;
+          height: 80px;
+          border: 1px solid rgb(25 237 255 / 40%);
+          background: rgb(0 0 0 / 6.67%);
+          box-shadow: inset 0 0 10px rgb(0 197 255 / 60%);
           padding-top: 16px;
 
           .icon {
@@ -287,10 +287,10 @@
           }
 
           .item-container {
-            width: 100px;
             display: flex;
             flex-direction: column;
             justify-content: center;
+            width: 100px;
 
             div {
               text-align: center;
@@ -301,17 +301,16 @@
             }
 
             .value {
-              text-shadow: 0 0 25px #00fbfe;
+              position: relative;
+              top: -8px;
               background: linear-gradient(0deg, #45d3fd, #45d3fd, #61ddb1, #61ddb1);
+              text-shadow: 0 0 25px #00fbfe;
               font-style: normal;
               background-size: cover;
               font-family: electronicFont;
               font-size: 30px;
-              -webkit-background-clip: text;
               background-clip: text;
               -webkit-text-fill-color: transparent;
-              position: relative;
-              top: -8px;
 
               span {
                 font-family: Arial, Helvetica, sans-serif;
@@ -323,7 +322,7 @@
         }
 
         .warning-box {
-          padding-top: 0px;
+          padding-top: 0;
 
           .icon {
             margin-top: 20px;
@@ -393,7 +392,7 @@
   }
 
   :deep(.zxm-table-body) {
-    border: 1px solid rgba(57, 232, 255, 0.2) !important;
+    border: 1px solid rgb(57 232 255 / 20%) !important;
 
     .zxm-table-tbody > tr > td {
       border: none !important;

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 2927 - 0
src/views/vent/monitorManager/fireDoorMonitor/components/fireDoorSVG.vue


+ 61 - 0
src/views/vent/monitorManager/gasPumpMonitor/gasPump.data.bet.ts

@@ -106,6 +106,67 @@ export const leftMonitor = [
       },
     ],
   },
+  {
+    type: 'list',
+    title: '抑爆器',
+    stateHeader: [],
+    children: [
+      {
+        title: '',
+        childTitle: ['1#抑爆器'],
+        key: ['ExplosionSS1'],
+        list: [
+          {
+            title: '火焰传感器',
+            code: '_FS1_str',
+            type: 'txt',
+          },
+          {
+            title: '报警状态',
+            code: '_SysFault_str',
+            type: 'txt',
+          },
+          {
+            title: '1-1抑爆器',
+            code: '_EV1_str',
+            type: 'txt',
+          },
+          {
+            title: '1-2抑爆器',
+            code: '_EV2_str',
+            type: 'txt',
+          },
+        ],
+      },
+      {
+        title: '',
+        childTitle: ['2#抑爆器'],
+        key: ['ExplosionSS2'],
+        list: [
+          {
+            title: '火焰传感器',
+            code: '_FS1_str',
+            type: 'txt',
+          },
+          {
+            title: '报警状态',
+            code: '_SysFault_str',
+            type: 'txt',
+          },
+          {
+            title: '2-1抑爆器',
+            code: '_EV1_str',
+            type: 'txt',
+          },
+          {
+            title: '2-2抑爆器',
+            code: '_EV2_str',
+            type: 'txt',
+          },
+        ],
+      },
+    ],
+  },
 ];
 
 export const rightMonitor = [

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 364 - 0
src/views/vent/monitorManager/gateMonitor/components/GateShrinkSVG.vue


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 384 - 0
src/views/vent/monitorManager/gateMonitor/components/GateYeYaSVG.vue


+ 524 - 524
src/views/vent/monitorManager/gateMonitor/index.vue

@@ -367,21 +367,21 @@
     v-if="renderPlayer"
     ref="playerRef"
     style="
-      z-index: 1;
       position: absolute;
       top: 100px;
-      right: 0px;
+      right: 0;
+      z-index: 1;
+      flex-direction: column;
       width: 100%;
       height: 800px;
       pointer-events: none;
       overflow-y: auto;
-      flex-direction: column;
     "
   >
   </div>
   <LivePlayer
     id="fm-player1"
-    style="height: 220px; width: 300px; position: absolute; top: 0px; z-index: -1"
+    style="position: absolute; top: 0; z-index: -1; width: 300px; height: 220px"
     ref="player1"
     :videoUrl="flvURL1()"
     muted
@@ -401,589 +401,589 @@
 </template>
 
 <script setup lang="ts">
-import { onBeforeUnmount, onUnmounted, onMounted, ref, reactive, nextTick, inject, unref, defineAsyncComponent, provide } from 'vue';
-import MonitorTable from '../comment/MonitorTable.vue';
-import HistoryTable from '../comment/HistoryTable.vue';
-import AlarmHistoryTable from '../comment/AlarmHistoryTable.vue';
-import HandlerHistoryTable from '../comment/HandlerHistoryTable.vue';
-import WindDoorFaultTable from './components/WindDoorFaultTable.vue';
-import CarDamageTable from './components/CarDamageTable.vue';
-import AlarmHistoryTableHj from './components/AlarmHistoryTableHj.vue';
-import HandleModal from './modal.vue';
-import DeviceBaseInfo from '../comment/components/DeviceBaseInfo.vue';
-import { mountedThree, addMonitorText, play, destroy, setModelType, computePlay } from './gate.threejs';
-import { deviceControlApi } from '/@/api/vent/index';
-import { message } from 'ant-design-vue';
-import { list, getTableList, cameraList, cameraAddrList, getOrderByCount, getCarList } from './gate.api';
-import { chartsColumns, getModelComponent } from './gate.data';
-import { cloneDeep } from 'lodash-es';
-import { setDivHeight } from '/@/utils/event';
-import { BorderBox8 as DvBorderBox8 } from '@kjgl77/datav-vue3';
-import { useRouter } from 'vue-router';
-import LivePlayer from '@liveqing/liveplayer-v3';
-import { useModal } from '/@/components/Modal';
-import { useCamera } from '/@/hooks/system/useCamera';
-import { usePermission } from '/@/hooks/web/usePermission';
-import { getDictItems } from '/@/api/common/api';
-import { render } from '/@/utils/common/renderUtils';
-import { useGlobSetting } from '/@/hooks/setting';
-import { getDictItemsByCode } from '/@/utils/dict';
-import HistoryTableChart from '../comment/HistoryTableChart.vue';
-
-const { hasPermission } = usePermission();
-const { sysOrgCode } = useGlobSetting();
-const globalConfig = inject<any>('globalConfig');
-
-const modelRef = ref();
-/** 模型对应的组件,根据实际情况分为二维三维 */
-const modelComponent = getModelComponent(globalConfig.is2DModel);
-
-const { currentRoute } = useRouter();
-const gateId = ref(''); //风门ID
-const MonitorDataTable = ref();
-let contrlValue = '';
-const playerRef = ref();
-const deviceType = ref('gate');
-const activeKey = ref('1'); // tab
-const loading = ref(false);
-const renderPlayer = ref(true);
-// const stationType = ref('plc1');
-const scroll = reactive({
-  y: 230,
-});
-const modelList = ref<{ text: string; value: string }[]>([]);
-const frontDoorIsOpen = ref(false); //前门是否开启
-const backDoorIsOpen = ref(false); //后门是否开启
-const midDoorIsOpen = ref(false); //中间门是否开启
-
-const modalIsShow = ref<boolean>(false); // 是否显示模态框
-const modalTitle = ref(''); // 模态框标题显示内容,根据设备操作类型决定
-const modalType = ref(''); // 模态框内容显示类型,设备操作类型
-
-const selectRowIndex = ref(-1); // 选中行
-const dataSource = ref([]);
-const sharedData = ref([]);
-const deviceBaseList = ref([]); // 设备基本信息
-const updateSharedData = (data) => {
-  sharedData.value = data;
-};
-const Option = {
-  grid: {
-    top: '20%',
-    left: '5%',
-    right: '5%',
-    bottom: '3%',
-    containLabel: true,
-  },
-  toolbox: {
-    feature: null,
-  },
-};
-
-/** 封装通用的模态框打开方法(setControl 模式:点位code直接传参) */
-const openGateControlModal = (paramcode: string, title: string) => {
-  modalType.value = paramcode;
-  modalTitle.value = title;
-  modalIsShow.value = true;
-
-  // 模拟密码模式下直接执行操作
-  if (globalConfig?.simulatedPassword) {
-    handleOK('', paramcode);
-  }
-};
+  import { onBeforeUnmount, onUnmounted, onMounted, ref, reactive, nextTick, inject, unref, defineAsyncComponent, provide } from 'vue';
+  import MonitorTable from '../comment/MonitorTable.vue';
+  import HistoryTable from '../comment/HistoryTable.vue';
+  import AlarmHistoryTable from '../comment/AlarmHistoryTable.vue';
+  import HandlerHistoryTable from '../comment/HandlerHistoryTable.vue';
+  import WindDoorFaultTable from './components/WindDoorFaultTable.vue';
+  import CarDamageTable from './components/CarDamageTable.vue';
+  import AlarmHistoryTableHj from './components/AlarmHistoryTableHj.vue';
+  import HandleModal from './modal.vue';
+  import DeviceBaseInfo from '../comment/components/DeviceBaseInfo.vue';
+  import { mountedThree, addMonitorText, play, destroy, setModelType, computePlay } from './gate.threejs';
+  import { deviceControlApi } from '/@/api/vent/index';
+  import { message } from 'ant-design-vue';
+  import { list, getTableList, cameraList, cameraAddrList, getOrderByCount, getCarList } from './gate.api';
+  import { chartsColumns, getModelComponent } from './gate.data';
+  import { cloneDeep } from 'lodash-es';
+  import { setDivHeight } from '/@/utils/event';
+  import { BorderBox8 as DvBorderBox8 } from '@kjgl77/datav-vue3';
+  import { useRouter } from 'vue-router';
+  import LivePlayer from '@liveqing/liveplayer-v3';
+  import { useModal } from '/@/components/Modal';
+  import { useCamera } from '/@/hooks/system/useCamera';
+  import { usePermission } from '/@/hooks/web/usePermission';
+  import { getDictItems } from '/@/api/common/api';
+  import { render } from '/@/utils/common/renderUtils';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import { getDictItemsByCode } from '/@/utils/dict';
+  import HistoryTableChart from '../comment/HistoryTableChart.vue';
+
+  const { hasPermission } = usePermission();
+  const { sysOrgCode } = useGlobSetting();
+  const globalConfig = inject<any>('globalConfig');
+
+  const modelRef = ref();
+  /** 模型对应的组件,根据实际情况分为二维三维 */
+  const modelComponent = getModelComponent(globalConfig.is2DModel);
+
+  const { currentRoute } = useRouter();
+  const gateId = ref(''); //风门ID
+  const MonitorDataTable = ref();
+  let contrlValue = '';
+  const playerRef = ref();
+  const deviceType = ref('gate');
+  const activeKey = ref('1'); // tab
+  const loading = ref(false);
+  const renderPlayer = ref(true);
+  // const stationType = ref('plc1');
+  const scroll = reactive({
+    y: 230,
+  });
+  const modelList = ref<{ text: string; value: string }[]>([]);
+  const frontDoorIsOpen = ref(false); //前门是否开启
+  const backDoorIsOpen = ref(false); //后门是否开启
+  const midDoorIsOpen = ref(false); //中间门是否开启
+
+  const modalIsShow = ref<boolean>(false); // 是否显示模态框
+  const modalTitle = ref(''); // 模态框标题显示内容,根据设备操作类型决定
+  const modalType = ref(''); // 模态框内容显示类型,设备操作类型
+
+  const selectRowIndex = ref(-1); // 选中行
+  const dataSource = ref([]);
+  const sharedData = ref([]);
+  const deviceBaseList = ref([]); // 设备基本信息
+  const updateSharedData = (data) => {
+    sharedData.value = data;
+  };
+  const Option = {
+    grid: {
+      top: '20%',
+      left: '5%',
+      right: '5%',
+      bottom: '3%',
+      containLabel: true,
+    },
+    toolbox: {
+      feature: null,
+    },
+  };
 
-/** 统一控制入口:点位code + 标题 直接传参(与 windowMonitor 的 setControl 模式一致) */
-const setControl = (paramcode: string, title: string) => {
-  openGateControlModal(paramcode, title);
-};
+  /** 封装通用的模态框打开方法(setControl 模式:点位code直接传参) */
+  const openGateControlModal = (paramcode: string, title: string) => {
+    modalType.value = paramcode;
+    modalTitle.value = title;
+    modalIsShow.value = true;
 
-/** 控制模式切换(paramcode 需根据 contrlMod + contrlValue 动态决定) */
-const setControlMode = (value?: string) => {
-  contrlValue = value;
-  openGateControlModal('modeSwitch', '控制模式切换');
-};
+    // 模拟密码模式下直接执行操作
+    if (globalConfig?.simulatedPassword) {
+      handleOK('', paramcode);
+    }
+  };
 
-const [registerModal, { openModal, closeModal }] = useModal();
+  /** 统一控制入口:点位code + 标题 直接传参(与 windowMonitor 的 setControl 模式一致) */
+  const setControl = (paramcode: string, title: string) => {
+    openGateControlModal(paramcode, title);
+  };
 
-const { getCamera, removeCamera } = useCamera();
+  /** 控制模式切换(paramcode 需根据 contrlMod + contrlValue 动态决定) */
+  const setControlMode = (value?: string) => {
+    contrlValue = value;
+    openGateControlModal('modeSwitch', '控制模式切换');
+  };
 
-const tabChange = (activeKeyVal) => {
-  activeKey.value = activeKeyVal;
-  if (activeKeyVal == 1) {
-    nextTick(() => {
-      if (MonitorDataTable.value) MonitorDataTable.value.setSelectedRowKeys([selectData.deviceID]);
+  const [registerModal, { openModal, closeModal }] = useModal();
+
+  const { getCamera, removeCamera } = useCamera();
+
+  const tabChange = (activeKeyVal) => {
+    activeKey.value = activeKeyVal;
+    if (activeKeyVal == 1) {
+      nextTick(() => {
+        if (MonitorDataTable.value) MonitorDataTable.value.setSelectedRowKeys([selectData.deviceID]);
+      });
+    }
+  };
+
+  const initData = {
+    deviceID: '',
+    deviceType: '',
+    strname: '',
+    frontRearDP: '-', //压差
+    // sourcePressure: '-', //气源压力
+    runRoRecondition: null,
+    autoRoManual: null,
+    netStatus: '0', //通信状态
+    frontGateOpen: '0',
+    frontGateClose: '1',
+    rearGateOpen: '0',
+    rearGateClose: '1',
+    midGateOpen: '0',
+    midGateClose: '1',
+    fault: '气源压力超限',
+    masterComputer: 0,
+    frontGateOpenCtrl: false,
+    rearGateOpenCtrl: false,
+    cameras: [],
+  };
+
+  // 监测数据
+  const selectData = reactive(cloneDeep(initData));
+
+  const flvURL1 = () => {
+    // return ''
+    return `/video/gate.mp4`;
+  };
+
+  function deviceEdit(e: Event, type: string, record) {
+    e.stopPropagation();
+    openModal(true, {
+      type,
+      deviceId: record['deviceID'],
     });
   }
-};
-
-const initData = {
-  deviceID: '',
-  deviceType: '',
-  strname: '',
-  frontRearDP: '-', //压差
-  // sourcePressure: '-', //气源压力
-  runRoRecondition: null,
-  autoRoManual: null,
-  netStatus: '0', //通信状态
-  frontGateOpen: '0',
-  frontGateClose: '1',
-  rearGateOpen: '0',
-  rearGateClose: '1',
-  midGateOpen: '0',
-  midGateClose: '1',
-  fault: '气源压力超限',
-  masterComputer: 0,
-  frontGateOpenCtrl: false,
-  rearGateOpenCtrl: false,
-  cameras: [],
-};
-
-// 监测数据
-const selectData = reactive(cloneDeep(initData));
-
-const flvURL1 = () => {
-  // return ''
-  return `/video/gate.mp4`;
-};
-
-function deviceEdit(e: Event, type: string, record) {
-  e.stopPropagation();
-  openModal(true, {
-    type,
-    deviceId: record['deviceID'],
-  });
-}
-// 获取设备基本信息列表
-function getDeviceBaseList() {
-  getTableList({ pageSize: 1000 }).then((res) => {
-    deviceBaseList.value = res.records;
-  });
-}
-
-// https获取监测数据
-let timer: null | NodeJS.Timeout = null;
-async function getMonitor(flag?) {
-  if (Object.prototype.toString.call(timer) === '[object Null]') {
-    timer = await setTimeout(
-      async () => {
-        const res = await list({ devicetype: deviceType.value, pagetype: 'normal' });
-        if (res.msgTxt && res.msgTxt[0]) {
-          dataSource.value = res.msgTxt[0].datalist || [];
-          dataSource.value.forEach((data: any) => {
-            const readData = data.readData;
-            data = Object.assign(data, readData);
-          });
-          if (dataSource.value.length > 0 && selectRowIndex.value == -1 && MonitorDataTable.value) {
-            // 初始打开页面
-            if (currentRoute.value && currentRoute.value['query'] && currentRoute.value['query']['id']) {
-              MonitorDataTable.value.setSelectedRowKeys([currentRoute.value['query']['id']]);
-            } else {
-              MonitorDataTable.value.setSelectedRowKeys([dataSource.value[0]['deviceID']]);
+  // 获取设备基本信息列表
+  function getDeviceBaseList() {
+    getTableList({ pageSize: 1000 }).then((res) => {
+      deviceBaseList.value = res.records;
+    });
+  }
+
+  // https获取监测数据
+  let timer: null | NodeJS.Timeout = null;
+  async function getMonitor(flag?) {
+    if (Object.prototype.toString.call(timer) === '[object Null]') {
+      timer = await setTimeout(
+        async () => {
+          const res = await list({ devicetype: deviceType.value, pagetype: 'normal' });
+          if (res.msgTxt && res.msgTxt[0]) {
+            dataSource.value = res.msgTxt[0].datalist || [];
+            dataSource.value.forEach((data: any) => {
+              const readData = data.readData;
+              data = Object.assign(data, readData);
+            });
+            if (dataSource.value.length > 0 && selectRowIndex.value == -1 && MonitorDataTable.value) {
+              // 初始打开页面
+              if (currentRoute.value && currentRoute.value['query'] && currentRoute.value['query']['id']) {
+                MonitorDataTable.value.setSelectedRowKeys([currentRoute.value['query']['id']]);
+              } else {
+                MonitorDataTable.value.setSelectedRowKeys([dataSource.value[0]['deviceID']]);
+              }
             }
+            Object.assign(selectData, dataSource.value[selectRowIndex.value]);
+            if (selectData.contrlMod == 'jnjhCtrl') {
+              selectData['autoRoManual'] = selectData['autoRoManual'] == 1 ? true : false;
+              selectData['autoRoManual1'] = selectData['autoRoManual1'] == 1 ? true : false;
+              selectData['autoRoManual2'] = selectData['autoRoManual2'] == 1 ? true : false;
+            }
+            addMonitorText(selectData);
+            monitorAnimation(selectData);
+            if (timer) {
+              timer = null;
+            }
+            getMonitor();
           }
-          Object.assign(selectData, dataSource.value[selectRowIndex.value]);
-          if (selectData.contrlMod == 'jnjhCtrl') {
-            selectData['autoRoManual'] = selectData['autoRoManual'] == 1 ? true : false;
-            selectData['autoRoManual1'] = selectData['autoRoManual1'] == 1 ? true : false;
-            selectData['autoRoManual2'] = selectData['autoRoManual2'] == 1 ? true : false;
-          }
-          addMonitorText(selectData);
-          monitorAnimation(selectData);
-          if (timer) {
-            timer = null;
-          }
-          getMonitor();
-        }
-      },
-      flag ? 0 : 1000
-    );
-  }
-}
-
-// 切换检测数据
-async function getSelectRow(selectRow, index) {
-  if (!selectRow) return;
-  loading.value = true;
-  selectRowIndex.value = index;
-  gateId.value = selectRow.deviceID;
-  const baseData: any = deviceBaseList.value.find((baseData: any) => baseData.id === selectRow.deviceID);
-  Object.assign(selectData, initData, selectRow, baseData);
-  isFrontOpenRunning = false; //开关门动作是否在进行
-  isRearOpenRunning = false; //开关门动作是否在进行
-  isMidOpenRunning = false; //开关门动作是否在进行
-  frontDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
-  rearDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
-  midDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
-
-  let type;
-  const dictCodes = getDictItemsByCode('gateStyle');
-  if (selectData && dictCodes && dictCodes.length > 0) {
-    const gateStyle = selectData['gateStyle'];
-    switch (gateStyle) {
-      case 'gate_qd':
-        type = 'fm3';
-        break;
-      case 'fmtl3':
-        type = 'fmThreeTl';
-        break;
-      case 'fmSs':
-        type = 'fmTwoSs';
-        break;
-      case 'fm_fc':
-        type = 'fmWindow';
-        break;
-      case 'fmXr':
-        type = 'fmXr';
-        break;
-      case 'fmYy':
-        type = 'fm1';
-        break;
-      case 'fmSs3':
-        type = 'fm2';
-        break;
-      case 'fm_fc_hjg':
-        type = 'fmWindowHjg';
-        break;
-      case 'fm_fc_zhq':
-        type = 'fmWindowZhq';
-        break;
-      default:
-        type = gateStyle;
+        },
+        flag ? 0 : 1000
+      );
     }
-  } else {
-    type = selectData.nwindownum == 1 ? 'singleWindow' : 'doubleWindow';
-    if (selectData['doorUse'] == 2) {
-      type = 'fmXr';
-    } else if (selectData.ndoorcount == '3' || selectData.deviceType == 'gate_nomal3') {
-      type = 'fmThreeTl';
+  }
+
+  // 切换检测数据
+  async function getSelectRow(selectRow, index) {
+    if (!selectRow) return;
+    loading.value = true;
+    selectRowIndex.value = index;
+    gateId.value = selectRow.deviceID;
+    const baseData: any = deviceBaseList.value.find((baseData: any) => baseData.id === selectRow.deviceID);
+    Object.assign(selectData, initData, selectRow, baseData);
+    isFrontOpenRunning = false; //开关门动作是否在进行
+    isRearOpenRunning = false; //开关门动作是否在进行
+    isMidOpenRunning = false; //开关门动作是否在进行
+    frontDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
+    rearDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
+    midDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
+
+    let type;
+    const dictCodes = getDictItemsByCode('gateStyle');
+    if (selectData && dictCodes && dictCodes.length > 0) {
+      const gateStyle = selectData['gateStyle'];
+      switch (gateStyle) {
+        case 'gate_qd':
+          type = 'fm3';
+          break;
+        case 'fmtl3':
+          type = 'fmThreeTl';
+          break;
+        case 'fmSs':
+          type = 'fmTwoSs';
+          break;
+        case 'fm_fc':
+          type = 'fmWindow';
+          break;
+        case 'fmXr':
+          type = 'fmXr';
+          break;
+        case 'fmYy':
+          type = 'fm1';
+          break;
+        case 'fmSs3':
+          type = 'fm2';
+          break;
+        case 'fm_fc_hjg':
+          type = 'fmWindowHjg';
+          break;
+        case 'fm_fc_zhq':
+          type = 'fmWindowZhq';
+          break;
+        default:
+          type = gateStyle;
+      }
     } else {
-      if (selectData.deviceType == 'gate_ss') {
-        type = 'fm2';
-      } else if (selectData.deviceType == 'gate_qd' || selectData.deviceType == 'gate_normal') {
-        type = 'fm3';
-      } else if (selectData.deviceType == 'gate_ss_two' || selectData.deviceType == 'gate_ss_two1') {
-        type = 'fmTwoSs';
-      } else if (selectData.deviceType == 'gate_tj') {
-        type = 'fmWindow';
+      type = selectData.nwindownum == 1 ? 'singleWindow' : 'doubleWindow';
+      if (selectData['doorUse'] == 2) {
+        type = 'fmXr';
+      } else if (selectData.ndoorcount == '3' || selectData.deviceType == 'gate_nomal3') {
+        type = 'fmThreeTl';
       } else {
-        type = 'fm1'; // 液压
+        if (selectData.deviceType == 'gate_ss') {
+          type = 'fm2';
+        } else if (selectData.deviceType == 'gate_qd' || selectData.deviceType == 'gate_normal') {
+          type = 'fm3';
+        } else if (selectData.deviceType == 'gate_ss_two' || selectData.deviceType == 'gate_ss_two1') {
+          type = 'fmTwoSs';
+        } else if (selectData.deviceType == 'gate_tj') {
+          type = 'fmWindow';
+        } else {
+          type = 'fm1'; // 液压
+        }
       }
     }
-  }
 
-  await setSVGModelType(type);
-  await setModelType(type);
-  addMonitorText(selectData);
-  // playAnimation(selectRow, selectData.value.maxarea, true);
-  loading.value = false;
-  await getCamera(selectRow.deviceID, playerRef, renderPlayer);
-}
-
-/** 检修切换 */
-const changeOverhaul = (e) => {
-  // 检修值需要在打开模态框前设置(handleOK 中按 toggle 逻辑计算下发值)
-  openGateControlModal('overhaul', '检修控制');
-  contrlValue = e;
-};
-
-/** 统一下发控制指令(仿 windowMonitor 模式:handlerState 即点位code,特殊情况单独判断) */
-function handleOK(passWord, handlerState, value?) {
-  if (!passWord && !globalConfig?.simulatedPassword) {
-    message.warning('请输入密码');
-    return;
+    await setSVGModelType(type);
+    await setModelType(type);
+    addMonitorText(selectData);
+    // playAnimation(selectRow, selectData.value.maxarea, true);
+    loading.value = false;
+    await getCamera(selectRow.deviceID, playerRef, renderPlayer);
   }
 
-  const data = {
-    deviceid: selectData.deviceID,
-    devicetype: selectData.deviceType,
-    paramcode: '',
-    value: value ?? null,
-    password: passWord || globalConfig?.simulatedPassword,
-    masterComputer: selectData.masterComputer,
+  /** 检修切换 */
+  const changeOverhaul = (e) => {
+    // 检修值需要在打开模态框前设置(handleOK 中按 toggle 逻辑计算下发值)
+    openGateControlModal('overhaul', '检修控制');
+    contrlValue = e;
   };
 
-  // 特殊:控制模式切换(paramcode 根据 contrlMod + contrlValue 动态决定)
-  if (handlerState === 'modeSwitch') {
-    if (selectData.contrlMod == 'codeCtrl') {
-      if (contrlValue == '1') {
-        data.paramcode = 'autoRoManualControl1';
-      } else if (contrlValue == '0') {
-        data.paramcode = 'autoRoManualControl2';
+  /** 统一下发控制指令(仿 windowMonitor 模式:handlerState 即点位code,特殊情况单独判断) */
+  function handleOK(passWord, handlerState, value?) {
+    if (!passWord && !globalConfig?.simulatedPassword) {
+      message.warning('请输入密码');
+      return;
+    }
+
+    const data = {
+      deviceid: selectData.deviceID,
+      devicetype: selectData.deviceType,
+      paramcode: '',
+      value: value ?? null,
+      password: passWord || globalConfig?.simulatedPassword,
+      masterComputer: selectData.masterComputer,
+    };
+
+    // 特殊:控制模式切换(paramcode 根据 contrlMod + contrlValue 动态决定)
+    if (handlerState === 'modeSwitch') {
+      if (selectData.contrlMod == 'codeCtrl') {
+        if (contrlValue == '1') {
+          data.paramcode = 'autoRoManualControl1';
+        } else if (contrlValue == '0') {
+          data.paramcode = 'autoRoManualControl2';
+        } else {
+          data.paramcode = 'autoRoManualControl0';
+        }
+        data.value = '';
+      } else if (selectData.contrlMod == 'loopCtrl' || selectData.contrlMod == 'jnjhCtrl') {
+        data.paramcode = 'autoRoManualControl';
+        data.value = '';
       } else {
-        data.paramcode = 'autoRoManualControl0';
+        data.paramcode = 'autoRoManualControl';
+        data.value = contrlValue;
       }
+      selectData.autoRoManual = null;
+    }
+    // 特殊:检修控制(value 取反 toggle)
+    else if (handlerState === 'overhaul') {
+      data.paramcode = 'runRoRecondition_S';
+      data.value = selectData['runRoRecondition'] == 0 ? '1' : '0';
+    }
+    // 默认:handlerState 即点位code(setControl 直接传参)
+    else {
+      data.paramcode = handlerState;
       data.value = '';
-    } else if (selectData.contrlMod == 'loopCtrl' || selectData.contrlMod == 'jnjhCtrl') {
-      data.paramcode = 'autoRoManualControl';
-      data.value = '';
-    } else {
-      data.paramcode = 'autoRoManualControl';
-      data.value = contrlValue;
     }
-    selectData.autoRoManual = null;
-  }
-  // 特殊:检修控制(value 取反 toggle)
-  else if (handlerState === 'overhaul') {
-    data.paramcode = 'runRoRecondition_S';
-    data.value = selectData['runRoRecondition'] == 0 ? '1' : '0';
-  }
-  // 默认:handlerState 即点位code(setControl 直接传参)
-  else {
-    data.paramcode = handlerState;
-    data.value = '';
-  }
 
-  if (data.paramcode) {
-    deviceControlApi(data)
-      .then((res) => {
-        if (res.success) {
-          modalIsShow.value = false;
-          if (globalConfig.History_Type == 'remote') {
-            message.success('指令已下发至生产管控平台成功!');
+    if (data.paramcode) {
+      deviceControlApi(data)
+        .then((res) => {
+          if (res.success) {
+            modalIsShow.value = false;
+            if (globalConfig.History_Type == 'remote') {
+              message.success('指令已下发至生产管控平台成功!');
+            } else {
+              message.success('指令已下发成功!');
+            }
           } else {
-            message.success('指令已下发成功!');
+            message.error(res.message);
           }
-        } else {
-          message.error(res.message);
-        }
-        contrlValue = '';
-      })
-      .catch(() => {
-        message.error('控制异常,请排查问题。。。');
-        contrlValue = '';
-      });
-  }
-}
-let isOpenRunning = false; //开关门动作是否在进行
-/** 开关门动画调用 */
-let isFrontOpenRunning = false; //开关门动作是否在进行
-// let isFrontCloseRunning = false; //开关门动作是否在进行
-let isRearOpenRunning = false; //开关门动作是否在进行
-// let isRearCloseRunning = false; //开关门动作是否在进行
-let isMidOpenRunning = false; //中间门动作是否在进行
-// let isMidCloseRunning = false; //中间门动作是否在进行
-// 0 关闭 1 正在打开 2 打开 3正在关闭
-let frontDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
-let rearDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
-let midDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
-
-function monitorAnimation(selectData) {
-  const timeScale = 0.005;
-  // 带风窗 风窗动画
-  if (selectData['gateStyle'] && selectData['gateStyle'].includes('fm_fc')) playWindowAnimation(selectData);
-
-  if (selectData.frontGateOpen == '1' && selectData.frontGateClose == '0' && !isFrontOpenRunning) {
-    isFrontOpenRunning = true;
-    if (frontDeviceState != 1) {
-      // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(1, timeScale) : play(1);
-      play(1, timeScale);
-      frontDeviceState = 1;
-      frontDoorIsOpen.value = false;
-      backDoorIsOpen.value = true;
+          contrlValue = '';
+        })
+        .catch(() => {
+          message.error('控制异常,请排查问题。。。');
+          contrlValue = '';
+        });
     }
   }
+  let isOpenRunning = false; //开关门动作是否在进行
+  /** 开关门动画调用 */
+  let isFrontOpenRunning = false; //开关门动作是否在进行
+  // let isFrontCloseRunning = false; //开关门动作是否在进行
+  let isRearOpenRunning = false; //开关门动作是否在进行
+  // let isRearCloseRunning = false; //开关门动作是否在进行
+  let isMidOpenRunning = false; //中间门动作是否在进行
+  // let isMidCloseRunning = false; //中间门动作是否在进行
+  // 0 关闭 1 正在打开 2 打开 3正在关闭
+  let frontDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
+  let rearDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
+  let midDeviceState = 0; //记录设备状态,为了与下一次监测数据做比较
+
+  function monitorAnimation(selectData) {
+    const timeScale = 0.005;
+    // 带风窗 风窗动画
+    if (selectData['gateStyle'] && selectData['gateStyle'].includes('fm_fc')) playWindowAnimation(selectData);
+
+    if (selectData.frontGateOpen == '1' && selectData.frontGateClose == '0' && !isFrontOpenRunning) {
+      isFrontOpenRunning = true;
+      if (frontDeviceState != 1) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(1, timeScale) : play(1);
+        play(1, timeScale);
+        frontDeviceState = 1;
+        frontDoorIsOpen.value = false;
+        backDoorIsOpen.value = true;
+      }
+    }
 
-  if (selectData.frontGateOpen == '0' && selectData.frontGateClose == '0' && !isFrontOpenRunning) {
-    isFrontOpenRunning = true;
-    if (frontDeviceState != 1) {
-      // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(1, timeScale) : play(1);
-      play(1, timeScale);
-      frontDeviceState = 1;
-      frontDoorIsOpen.value = false;
-      backDoorIsOpen.value = true;
+    if (selectData.frontGateOpen == '0' && selectData.frontGateClose == '0' && !isFrontOpenRunning) {
+      isFrontOpenRunning = true;
+      if (frontDeviceState != 1) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(1, timeScale) : play(1);
+        play(1, timeScale);
+        frontDeviceState = 1;
+        frontDoorIsOpen.value = false;
+        backDoorIsOpen.value = true;
+      }
     }
-  }
 
-  if (selectData.frontGateClose == '1' && selectData.frontGateOpen == '0' && isFrontOpenRunning) {
-    isFrontOpenRunning = false;
-    if (frontDeviceState != 0) {
-      // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(2, timeScale) : play(2);
-      play(2, timeScale);
-      frontDeviceState = 0;
-      frontDoorIsOpen.value = false;
-      // backDoorIsOpen.value = false
+    if (selectData.frontGateClose == '1' && selectData.frontGateOpen == '0' && isFrontOpenRunning) {
+      isFrontOpenRunning = false;
+      if (frontDeviceState != 0) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(2, timeScale) : play(2);
+        play(2, timeScale);
+        frontDeviceState = 0;
+        frontDoorIsOpen.value = false;
+        // backDoorIsOpen.value = false
+      }
     }
-  }
-  if (selectData.rearGateOpen == '1' && selectData.rearGateClose == '0' && !isRearOpenRunning) {
-    isRearOpenRunning = true;
-
-    if (rearDeviceState != 1) {
-      // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(3, timeScale) : play(3);
-      play(3, timeScale);
-      rearDeviceState = 1;
-      backDoorIsOpen.value = false;
-      frontDoorIsOpen.value = true;
+    if (selectData.rearGateOpen == '1' && selectData.rearGateClose == '0' && !isRearOpenRunning) {
+      isRearOpenRunning = true;
+
+      if (rearDeviceState != 1) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(3, timeScale) : play(3);
+        play(3, timeScale);
+        rearDeviceState = 1;
+        backDoorIsOpen.value = false;
+        frontDoorIsOpen.value = true;
+      }
     }
-  }
-  if (selectData.rearGateOpen == '0' && selectData.rearGateClose == '0' && !isRearOpenRunning) {
-    isRearOpenRunning = true;
-
-    if (rearDeviceState != 1) {
-      // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(3, timeScale) : play(3);
-      play(3, timeScale);
-      rearDeviceState = 1;
-      backDoorIsOpen.value = false;
-      frontDoorIsOpen.value = true;
+    if (selectData.rearGateOpen == '0' && selectData.rearGateClose == '0' && !isRearOpenRunning) {
+      isRearOpenRunning = true;
+
+      if (rearDeviceState != 1) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(3, timeScale) : play(3);
+        play(3, timeScale);
+        rearDeviceState = 1;
+        backDoorIsOpen.value = false;
+        frontDoorIsOpen.value = true;
+      }
     }
-  }
 
-  if (selectData.rearGateClose == '1' && selectData.rearGateOpen == '0' && isRearOpenRunning) {
-    isRearOpenRunning = false;
-    if (rearDeviceState != 0) {
-      // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(4, timeScale) : play(4);
-      play(4, timeScale);
-      rearDeviceState = 0;
-      backDoorIsOpen.value = false;
+    if (selectData.rearGateClose == '1' && selectData.rearGateOpen == '0' && isRearOpenRunning) {
+      isRearOpenRunning = false;
+      if (rearDeviceState != 0) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(4, timeScale) : play(4);
+        play(4, timeScale);
+        rearDeviceState = 0;
+        backDoorIsOpen.value = false;
+      }
     }
-  }
 
-  if (selectData.midGateOpen == '1' && selectData.midGateClose == '0' && !isMidOpenRunning) {
-    isMidOpenRunning = true;
+    if (selectData.midGateOpen == '1' && selectData.midGateClose == '0' && !isMidOpenRunning) {
+      isMidOpenRunning = true;
 
-    if (midDeviceState != 1) {
-      // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(3, timeScale) : play(3);
-      play(8, timeScale);
-      midDeviceState = 1;
-      backDoorIsOpen.value = false;
-      frontDoorIsOpen.value = true;
+      if (midDeviceState != 1) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(3, timeScale) : play(3);
+        play(8, timeScale);
+        midDeviceState = 1;
+        backDoorIsOpen.value = false;
+        frontDoorIsOpen.value = true;
+      }
     }
-  }
 
-  if (selectData.midGateOpen == '0' && selectData.midGateClose == '0' && !isMidOpenRunning) {
-    isMidOpenRunning = true;
+    if (selectData.midGateOpen == '0' && selectData.midGateClose == '0' && !isMidOpenRunning) {
+      isMidOpenRunning = true;
 
-    if (midDeviceState != 1) {
-      // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(3, timeScale) : play(3);
-      play(8, timeScale);
-      midDeviceState = 1;
-      backDoorIsOpen.value = false;
-      frontDoorIsOpen.value = true;
+      if (midDeviceState != 1) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(3, timeScale) : play(3);
+        play(8, timeScale);
+        midDeviceState = 1;
+        backDoorIsOpen.value = false;
+        frontDoorIsOpen.value = true;
+      }
     }
-  }
 
-  if (selectData.midGateClose == '1' && selectData.midGateOpen == '0' && isMidOpenRunning) {
-    isMidOpenRunning = false;
-    if (midDeviceState != 0) {
-      // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(4, timeScale) : play(4);
-      play(9, timeScale);
-      midDeviceState = 0;
-      backDoorIsOpen.value = false;
+    if (selectData.midGateClose == '1' && selectData.midGateOpen == '0' && isMidOpenRunning) {
+      isMidOpenRunning = false;
+      if (midDeviceState != 0) {
+        // import.meta.env.VITE_GLOB_IS_SIMULATE ? play(4, timeScale) : play(4);
+        play(9, timeScale);
+        midDeviceState = 0;
+        backDoorIsOpen.value = false;
+      }
     }
+
+    modelRef.value?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
   }
 
-  modelRef.value?.animate?.(selectData.frontGateOpen == '1', selectData.midGateOpen == '1', selectData.rearGateOpen == '1');
-}
-
-function playWindowAnimation(data, maxarea = 90, isFirst = false) {
-  computePlay(data, maxarea, isFirst);
-}
-
-function handleCancel() {
-  modalIsShow.value = false;
-  modalTitle.value = '';
-  modalType.value = '';
-}
-
-const handleSwitchChange = (checked: boolean) => {
-  contrlValue = checked;
-  setControl('manualAutoMode_w', '手动/自动模式切换');
-};
-
-function setSVGModelType(type) {
-  modelComponent.value = getModelComponent(globalConfig.is2DModel, type);
-  return nextTick();
-}
-
-onMounted(async () => {
-  const { query } = unref(currentRoute);
-  if (query['deviceType']) deviceType.value = query['deviceType'] as string;
-  modelList.value = await getDictItems('gateModel');
-  if (globalConfig.is2DModel) {
-    await getMonitor(true);
-  } else {
-    loading.value = true;
-    // const playerDom = document.getElementById('fm-player1')?.getElementsByClassName('vjs-tech')[0];
-    mountedThree()
-      .then(async () => {
-        if (sysOrgCode != 'zmhjhzmy') {
-          await getMonitor(true);
-          loading.value = false;
-        } else {
-          // 韩咀无风门设备,只有报警历史数据,无其他数据
-          setModelType('fm1').then(async () => {
-            loading.value = false;
-            dataSource.value = [];
-            addMonitorText(selectData);
-          });
-        }
-      })
-      .catch((e) => {
-        console.log(e);
-      });
+  function playWindowAnimation(data, maxarea = 90, isFirst = false) {
+    computePlay(data, maxarea, isFirst);
   }
-});
 
-onBeforeUnmount(() => {
-  removeCamera(playerRef);
-  getDeviceBaseList();
-});
+  function handleCancel() {
+    modalIsShow.value = false;
+    modalTitle.value = '';
+    modalType.value = '';
+  }
 
-onUnmounted(() => {
-  if (timer) {
-    clearTimeout(timer);
-    timer = undefined;
+  const handleSwitchChange = (checked: boolean) => {
+    contrlValue = checked;
+    setControl('manualAutoMode_w', '手动/自动模式切换');
+  };
+
+  function setSVGModelType(type) {
+    modelComponent.value = getModelComponent(globalConfig.is2DModel, type);
+    return nextTick();
   }
-  destroy();
-});
+
+  onMounted(async () => {
+    const { query } = unref(currentRoute);
+    if (query['deviceType']) deviceType.value = query['deviceType'] as string;
+    modelList.value = await getDictItems('gateModel');
+    if (globalConfig.is2DModel) {
+      await getMonitor(true);
+    } else {
+      loading.value = true;
+      // const playerDom = document.getElementById('fm-player1')?.getElementsByClassName('vjs-tech')[0];
+      mountedThree()
+        .then(async () => {
+          if (sysOrgCode != 'zmhjhzmy') {
+            await getMonitor(true);
+            loading.value = false;
+          } else {
+            // 韩咀无风门设备,只有报警历史数据,无其他数据
+            setModelType('fm1').then(async () => {
+              loading.value = false;
+              dataSource.value = [];
+              addMonitorText(selectData);
+            });
+          }
+        })
+        .catch((e) => {
+          console.log(e);
+        });
+    }
+  });
+
+  onBeforeUnmount(() => {
+    removeCamera(playerRef);
+    getDeviceBaseList();
+  });
+
+  onUnmounted(() => {
+    if (timer) {
+      clearTimeout(timer);
+      timer = undefined;
+    }
+    destroy();
+  });
 </script>
 
 <style lang="less" scoped>
-@import '/@/design/theme.less';
-@import '/@/design/vent/modal.less';
+  @import url('/@/design/theme.less');
+  @import url('/@/design/vent/modal.less');
 
-.scene-box {
-  .bottom-tabs-box {
-    height: 350px;
+  .scene-box {
+    .bottom-tabs-box {
+      height: 350px;
+    }
   }
-}
 
-.button-box {
-  border: none !important;
-  height: 34px !important;
+  .button-box {
+    height: 34px !important;
+    border: none !important;
 
-  &:hover {
-    background: var(--vent-device-manager-control-btn-hover) !important;
-  }
+    &:hover {
+      background: var(--vent-device-manager-control-btn-hover) !important;
+    }
 
-  &::before {
-    height: 27px !important;
-    background: var(--vent-device-manager-control-btn) !important;
-  }
+    &::before {
+      height: 27px !important;
+      background: var(--vent-device-manager-control-btn) !important;
+    }
 
-  &::after {
-    top: 35px !important;
+    &::after {
+      top: 35px !important;
+    }
   }
-}
 
-:deep(.@{ventSpace}-tabs-tabpane-active) {
-  height: 100%;
-}
+  :deep(.@{ventSpace}-tabs-tabpane-active) {
+    height: 100%;
+  }
 
-::-webkit-scrollbar-thumb {
-  -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
-  background: #4288a444;
-}
+  ::-webkit-scrollbar-thumb {
+    box-shadow: inset 0 0 5px rgb(0 0 0 / 20%);
+    background: #4288a444;
+  }
 
-:deep(.zxm-radio-disabled + span) {
-  color: var(--vent-font-color) !important;
-}
+  :deep(.zxm-radio-disabled + span) {
+    color: var(--vent-font-color) !important;
+  }
 
-:deep(.zxm-radio-disabled .zxm-radio-inner::after) {
-  background-color: #127cb5 !important;
-}
+  :deep(.zxm-radio-disabled .zxm-radio-inner::after) {
+    background-color: #127cb5 !important;
+  }
 
-:deep(.@{ventSpace}-picker-datetime-panel) {
-  height: 200px !important;
-  overflow-y: auto !important;
-}
+  :deep(.@{ventSpace}-picker-datetime-panel) {
+    height: 200px !important;
+    overflow-y: auto !important;
+  }
 </style>

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 767 - 804
src/views/vent/monitorManager/mainFanMonitor/index.vue


+ 10 - 5
src/views/vent/monitorManager/mainFanMonitor/main.data.ts

@@ -383,11 +383,11 @@ export const modalTypeArr = {
       value: '一键启动/停止',
       permission: 'fan:startAndResetFan',
     },
-    // {
-    //   key: 'resetFan',
-    //   value: '一键复位',
-    //   permission: 'fan:resetFan',
-    // },
+    {
+      key: 'startFanYdt',
+      value: '一键启动/停止',
+      permission: 'fan:startAndResetFanYdt',
+    },
     {
       key: 'changeSmoke',
       value: '不停风倒机',
@@ -408,6 +408,11 @@ export const modalTypeArr = {
       value: '一键调频',
       permission: 'fan:frequency',
     },
+    {
+      key: 'FreqSet',
+      value: '频率设定',
+      permission: 'fan:FreqSet',
+    },
     {
       key: 'fbm',
       value: '防爆门控制',

+ 2 - 2
src/views/vent/monitorManager/nitrogen/nitrogen.data.1.ts

@@ -19,12 +19,12 @@ export function getNitrogenHome() {
     case 'sdmtjtjjmk':
       nitrogenHome = defineAsyncComponent(() => import('./components/nitrogenHome.vue'));
       return nitrogenHome;
-       case 'sxmyhgjtlhky'://龙华
+    case 'sxmyhgjtlhky': //龙华
       nitrogenHome = defineAsyncComponent(() => import('./components/nitrogenHomeLh.vue'));
       return nitrogenHome;
     default:
       //nitrogenHome = defineAsyncComponent(() => import('./components/nitrogenHome.vue')); //测试完,请置于该组件
-       nitrogenHome = defineAsyncComponent(() => import('./components/nitrogenHomeLh.vue'));
+      nitrogenHome = defineAsyncComponent(() => import('./components/nitrogenHomeLh.vue'));
       return nitrogenHome;
   }
 }

+ 5 - 5
src/views/vent/monitorManager/warningMonitor/warning.api.ts

@@ -2,13 +2,13 @@ import { defHttp } from '/@/utils/http/axios';
 
 enum Api {
   // list = '/safety/ventanalyAlarmLog/list',
-  list = '/ventanaly-company/company/index/getEachMineWarnLogList',//获取公司端列表
-  getEachMineWarnCountInfo = '/ventanaly-company/company/index/getEachMineWarnCountInfo',//获取公司端信息
+  list = '/company/index/getEachMineWarnLogList', //获取公司端列表
+  getEachMineWarnCountInfo = '/company/index/getEachMineWarnCountInfo', //获取公司端信息
   getWarnCountInfo = '/monitor/groupCompany/getWarnCountInfo', // 获取矿端信息
   getAlarmLogList = '/monitor/groupCompany/getAlarmLogList', //获取各矿预警统计信息
   getManageAutoLogList = '/monitor/groupCompany/getManageAutoLogList', //获取各矿工作面智能管控预警信息
-  exportXls = '/safety/ventanalyAlarmLog/exportXls',//导出
-  exportSensorDayAlarmReport = '/ventanaly-company/company/index/exportSensorDayAlarmReport'//导出各矿传感器预警历史日统计报表
+  exportXls = '/safety/ventanalyAlarmLog/exportXls', //导出
+  exportSensorDayAlarmReport = '/company/index/exportSensorDayAlarmReport', //导出各矿传感器预警历史日统计报表
 }
 /**
  * 列表接口
@@ -26,7 +26,7 @@ export const getAlarmLogList = (params) => defHttp.post({ url: Api.getAlarmLogLi
 export const getManageAutoLogList = (params) => defHttp.post({ url: Api.getManageAutoLogList, params });
 
 export const getExportUrl = (active) => {
-  console.log(active, '999')
+  console.log(active, '999');
   if (active == 'safety') {
     return Api.exportSensorDayAlarmReport;
   } else {

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 396 - 0
src/views/vent/monitorManager/windowMonitor/components/windowDoorBltSVG.vue


+ 136 - 0
src/views/vent/packageUpload/components/DeployHistory.vue

@@ -0,0 +1,136 @@
+<template>
+  <div class="deploy-history">
+    <div class="search-bar">
+      <div class="search-item">
+        <span class="search-label">时间范围:</span>
+        <a-range-picker
+          v-model:value="searchForm.timeRange"
+          show-time
+          format="YYYY-MM-DD HH:mm:ss"
+          :placeholder="['开始时间', '结束时间']"
+          style="width: 360px"
+        />
+      </div>
+      <div class="search-item">
+        <a-button type="primary" @click="handleSearch">
+          <template #icon><search-outlined /></template>
+          查询
+        </a-button>
+        <a-button style="margin-left: 8px" @click="handleReset">
+          <template #icon><reload-outlined /></template>
+          重置
+        </a-button>
+      </div>
+    </div>
+    <BasicTable @register="registerTable" />
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { reactive, h } from 'vue';
+  import { SearchOutlined, ReloadOutlined } from '@ant-design/icons-vue';
+  import { Tooltip } from 'ant-design-vue';
+  import dayjs from 'dayjs';
+  import { BasicTable, useTable } from '/@/components/Table';
+  import { deployLogListApi } from '../package.api';
+
+  const searchForm = reactive({
+    timeRange: null as any,
+  });
+
+  function stripAnsi(str: string): string {
+    return str.replace(new RegExp(String.fromCharCode(27) + '\[[0-9;]*m', 'g'), '');
+  }
+
+  function parseLogContent(val: string): string {
+    if (!val) return '';
+    try {
+      const obj = JSON.parse(val);
+      return stripAnsi(obj.msg || '');
+    } catch {
+      return stripAnsi(val);
+    }
+  }
+
+  const columns = [
+    { title: '部署时间', dataIndex: 'createTime', width: 180 },
+    { title: '部署包名称', dataIndex: 'packName', width: 240 },
+    {
+      title: '部署类型',
+      dataIndex: 'packType',
+      width: 120,
+      customRender: ({ text }) => (text === 'jar' ? '后端包' : text === 'page' ? '前端包' : text),
+    },
+    { title: '操作人', dataIndex: 'createBy', width: 120 },
+    {
+      title: '部署结果',
+      dataIndex: 'isSuccess',
+      width: 100,
+      customRender: ({ text }) => (text === 1 ? '成功' : '失败'),
+    },
+    {
+      title: '更新日志',
+      dataIndex: 'logContent',
+      width: 120,
+      customRender: ({ text }) => {
+        const content = parseLogContent(text);
+        return h(Tooltip, { title: content, placement: 'topRight', overlayStyle: { maxWidth: '600px', whiteSpace: 'pre-wrap' } }, () =>
+          h('span', { style: { color: '#0a84ff', cursor: 'pointer' } }, '详情')
+        );
+      },
+    },
+  ];
+
+  const [registerTable, { reload }] = useTable({
+    api: deployLogListApi,
+    columns,
+    rowKey: 'id',
+    pagination: true,
+    bordered: true,
+    showIndexColumn: true,
+    beforeFetch: (params) => {
+      if (searchForm.timeRange && searchForm.timeRange.length === 2) {
+        params.startTime = dayjs(searchForm.timeRange[0]).format('YYYY-MM-DD HH:mm:ss');
+        params.endTime = dayjs(searchForm.timeRange[1]).format('YYYY-MM-DD HH:mm:ss');
+      }
+      params.pageNo = params.pageNo || 1;
+      params.pageSize = params.pageSize || 10;
+      return params;
+    },
+  });
+
+  function handleSearch() {
+    reload();
+  }
+
+  function handleReset() {
+    searchForm.timeRange = null;
+    reload();
+  }
+</script>
+
+<style lang="less" scoped>
+  .deploy-history {
+    .search-bar {
+      margin-bottom: 16px;
+      padding: 16px;
+      background: rgba(255, 255, 255, 0.06);
+      border-radius: 4px;
+      display: flex;
+      align-items: center;
+      gap: 12px;
+
+      .search-item {
+        display: flex;
+        align-items: center;
+        flex-shrink: 0;
+      }
+
+      .search-label {
+        color: #fff;
+        white-space: nowrap;
+        margin-right: 4px;
+      }
+    }
+  }
+</style>

+ 289 - 0
src/views/vent/packageUpload/components/PackageUpload.vue

@@ -0,0 +1,289 @@
+<template>
+  <div class="upload-content">
+    <a-card title="上传部署包" :bordered="true" style="width: 100%">
+      <a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }">
+        <a-form-item label="部署包类型">
+          <a-radio-group v-model:value="packageType">
+            <a-radio value="frontend">前端部署包 (zip)</a-radio>
+            <a-radio value="backend">后端部署包 (jar)</a-radio>
+          </a-radio-group>
+        </a-form-item>
+        <a-form-item label="选择文件">
+          <a-alert type="warning" show-icon style="margin-bottom: 12px">
+            <template #message>
+              <template v-if="packageType === 'frontend'">
+                <span style="font-weight: bold">前端包文件命名规则:</span>
+                <div>
+                  dist + 端口号.zip,例如一通三防智能管控平台前端包为
+                  <b>dist8092.zip</b>
+                </div>
+                <div style="margin-top: 4px; font-weight: bold">上传包时请严格按照命名规则,否则会导致部署失败或引起其他系统问题!!!</div>
+              </template>
+              <template v-else>
+                <span style="font-weight: bold">后端包文件命名规则:</span>
+                <div style="margin-top: 4px">
+                  <div>jeecg-cloud-gateway-3.2.0.jar</div>
+                  <div>jeecg-cloud-nacos-3.2.0.jar</div>
+                  <div>jeecg-cloud-system-start-3.2.0.jar</div>
+                  <div>ventanaly-monitor-3.2.0.jar</div>
+                  <div>ventanaly-model-3.2.0.jar</div>
+                  <div>ventanaly-jingtaifengliang-3.2.0.jar</div>
+                  <div>ventanaly-sharefile-3.2.0.jar</div>
+                  <div>ventanaly-province-3.2.0.jar</div>
+                  <div>ventanaly-ai-service-3.2.0.jar</div>
+                  <div>ventanaly-company-3.2.0.jar</div>
+                </div>
+                <div style="margin-top: 4px; font-weight: bold">上传包时请严格按照命名规则,否则会导致部署失败或引起其他系统问题!!!</div>
+              </template>
+            </template>
+          </a-alert>
+          <a-upload :before-upload="beforeUpload" :file-list="fileList" :remove="handleRemove" :max-count="1">
+            <a-button type="primary">
+              <template #icon><upload-outlined /></template>
+              选择文件
+            </a-button>
+          </a-upload>
+          <div class="file-tip" v-if="fileList.length > 0">
+            <span>已选择: {{ fileList[0]?.name }}</span>
+            <span style="margin-left: 12px; color: #999">({{ formatFileSize(fileList[0]?.size) }})</span>
+            <span v-if="nameWarning" class="name-warning">{{ nameWarning }}</span>
+          </div>
+        </a-form-item>
+        <a-form-item :wrapper-col="{ offset: 4, span: 16 }">
+          <a-button type="primary" danger :loading="deploying" :disabled="fileList.length === 0 || !!nameWarning" @click="handleUploadAndDeploy">
+            <template #icon><cloud-upload-outlined /></template>
+            上传并部署
+          </a-button>
+        </a-form-item>
+      </a-form>
+    </a-card>
+  </div>
+
+  <!-- 全屏loading -->
+  <div class="fullscreen-loading" v-if="deploying">
+    <a-spin size="large" />
+    <span class="loading-text">上传并部署中,请勿关闭或操作页面</span>
+  </div>
+
+  <!-- 部署结果弹框 -->
+  <a-modal v-model:visible="resultVisible" :title="deployIsSuccess ? '部署成功' : '部署失败'" :footer="null" width="600px">
+    <div class="deploy-status-icon" :class="deployIsSuccess ? 'is-success' : 'is-fail'">
+      <template v-if="deployIsSuccess">
+        <span class="icon-check"></span>
+      </template>
+      <template v-else>
+        <span class="icon-cross"></span>
+      </template>
+    </div>
+    <div class="deploy-msg">{{ deployMsg }}</div>
+  </a-modal>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue';
+  import { UploadOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
+  import { message } from 'ant-design-vue';
+  import { uploadProgramApi } from '../package.api';
+
+  const packageType = ref<string>('frontend');
+  const fileList = ref<any[]>([]);
+  const deploying = ref<boolean>(false);
+  const resultVisible = ref<boolean>(false);
+  const deployIsSuccess = ref<boolean>(false);
+  const deployMsg = ref<string>('');
+  const nameWarning = ref<string>('');
+
+  const acceptMap: Record<string, string> = {
+    frontend: '.zip',
+    backend: '.jar',
+  };
+
+  const backendJarNames = [
+    'jeecg-cloud-gateway-3.2.0.jar',
+    'jeecg-cloud-nacos-3.2.0.jar',
+    'jeecg-cloud-system-start-3.2.0.jar',
+    'ventanaly-monitor-3.2.0.jar',
+    'ventanaly-model-3.2.0.jar',
+    'ventanaly-jingtaifengliang-3.2.0.jar',
+    'ventanaly-sharefile-3.2.0.jar',
+    'ventanaly-province-3.2.0.jar',
+    'ventanaly-ai-service-3.2.0.jar',
+    'ventanaly-company-3.2.0.jar',
+  ];
+
+  watch(packageType, () => {
+    fileList.value = [];
+    nameWarning.value = '';
+  });
+
+  function beforeUpload(file) {
+    const accept = acceptMap[packageType.value];
+    const acceptExts = accept.split(',');
+    const ext = '.' + file.name.split('.').pop().toLowerCase();
+    if (!acceptExts.includes(ext)) {
+      message.error(`当前类型仅支持 ${accept} 格式的文件`);
+      return false;
+    }
+    if (packageType.value === 'frontend') {
+      nameWarning.value = /^dist\d+\.zip$/i.test(file.name) ? '' : '包名不符合命名规则,请检查!!!';
+    } else {
+      nameWarning.value = backendJarNames.includes(file.name) ? '' : '包名不符合命名规则,请检查!!!';
+    }
+    fileList.value = [file];
+    return false;
+  }
+
+  function handleRemove() {
+    fileList.value = [];
+    nameWarning.value = '';
+  }
+
+  function formatFileSize(size?: number): string {
+    if (!size) return '0 B';
+    const units = ['B', 'KB', 'MB', 'GB'];
+    let i = 0;
+    let s = size;
+    while (s >= 1024 && i < units.length - 1) {
+      s /= 1024;
+      i++;
+    }
+    return s.toFixed(2) + ' ' + units[i];
+  }
+
+  async function handleUploadAndDeploy() {
+    if (fileList.value.length === 0) {
+      message.warning('请先选择文件');
+      return;
+    }
+    deploying.value = true;
+    try {
+      const res = await uploadProgramApi(fileList.value[0]);
+      const result = res?.result || {};
+      deployIsSuccess.value = result.isSuccess === true;
+      deployMsg.value = result.msg || res?.message || '未知结果';
+      resultVisible.value = true;
+      if (result.isSuccess) {
+        fileList.value = [];
+      }
+    } catch (e: any) {
+      deployIsSuccess.value = false;
+      deployMsg.value = e?.message || '上传请求失败,请检查网络连接';
+      resultVisible.value = true;
+    } finally {
+      deploying.value = false;
+    }
+  }
+</script>
+
+<style lang="less" scoped>
+  @import '/@/design/theme.less';
+
+  .upload-content {
+    :deep(.zxm-card-head-title),
+    :deep(.zxm-form-item-label > label) {
+      color: #fff !important;
+    }
+
+    :deep(.anticon-exclamation-circle) {
+      display: none;
+    }
+
+    .file-tip {
+      margin-top: 8px;
+      font-size: 13px;
+      color: #fff;
+    }
+
+    .name-warning {
+      margin-left: 12px;
+      color: #ff4d4f;
+      font-weight: bold;
+    }
+  }
+
+  .fullscreen-loading {
+    position: fixed;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    background: rgba(0, 0, 0, 0.45);
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    z-index: 9999;
+
+    .loading-text {
+      margin-top: 16px;
+      color: #fff;
+      font-size: 16px;
+    }
+  }
+
+  .deploy-status-icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 48px;
+    height: 48px;
+    border-radius: 50%;
+    margin: 8px auto 20px;
+
+    &.is-success {
+      background: #52c41a;
+
+      .icon-check {
+        display: block;
+        width: 20px;
+        height: 36px;
+        border: solid #fff;
+        border-width: 0 5px 5px 0;
+        transform: rotate(45deg) translate(-3px, -3px);
+      }
+    }
+
+    &.is-fail {
+      background: #ff4d4f;
+
+      .icon-cross {
+        display: block;
+        width: 32px;
+        height: 32px;
+        position: relative;
+
+        &::before,
+        &::after {
+          content: '';
+          position: absolute;
+          top: 50%;
+          left: 50%;
+          width: 5px;
+          height: 32px;
+          background: #fff;
+          border-radius: 2px;
+        }
+
+        &::before {
+          transform: translate(-50%, -50%) rotate(45deg);
+        }
+
+        &::after {
+          transform: translate(-50%, -50%) rotate(-45deg);
+        }
+      }
+    }
+  }
+
+  .deploy-msg {
+    white-space: pre-wrap;
+    word-break: break-all;
+    max-height: 400px;
+    overflow-y: auto;
+    padding: 12px;
+    border-radius: 4px;
+    font-size: 20px;
+    line-height: 1.6;
+    text-align: center;
+  }
+</style>

+ 26 - 269
src/views/vent/packageUpload/index.vue

@@ -3,181 +3,23 @@
     <div class="upload-header">
       <span class="title">部署包管理</span>
     </div>
-    <div class="upload-content">
-      <a-card title="上传部署包" :bordered="true" style="width: 100%">
-        <a-form :label-col="{ span: 4 }" :wrapper-col="{ span: 16 }">
-          <a-form-item label="部署包类型">
-            <a-radio-group v-model:value="packageType">
-              <a-radio value="frontend">前端部署包 (zip)</a-radio>
-              <a-radio value="backend">后端部署包 (jar)</a-radio>
-            </a-radio-group>
-          </a-form-item>
-          <a-form-item label="选择文件">
-            <a-alert type="warning" show-icon style="margin-bottom: 12px">
-              <template #message>
-                <template v-if="packageType === 'frontend'">
-                  <span style="font-weight: bold">前端包文件命名规则:</span>
-                  <div>
-                    dist + 端口号.zip,例如一通三防智能管控平台前端包为
-                    <b>dist8092.zip</b>
-                  </div>
-                  <div style="margin-top: 4px; font-weight: bold">上传包时请严格按照命名规则,否则会导致部署失败或引起其他系统问题!!!</div>
-                </template>
-                <template v-else>
-                  <span style="font-weight: bold">后端包文件命名规则:</span>
-                  <div style="margin-top: 4px">
-                    <div>jeecg-cloud-gateway-3.2.0.jar</div>
-                    <div>jeecg-cloud-nacos-3.2.0.jar</div>
-                    <div>jeecg-cloud-system-start-3.2.0.jar</div>
-                    <div>ventanaly-monitor-3.2.0.jar</div>
-                    <div>ventanaly-model-3.2.0.jar</div>
-                    <div>ventanaly-jingtaifengliang-3.2.0.jar</div>
-                    <div>ventanaly-sharefile-3.2.0.jar</div>
-                    <div>ventanaly-province-3.2.0.jar</div>
-                    <div>ventanaly-ai-service-3.2.0.jar</div>
-                    <div>ventanaly-company-3.2.0.jar</div>
-                  </div>
-                  <div style="margin-top: 4px; font-weight: bold">上传包时请严格按照命名规则,否则会导致部署失败或引起其他系统问题!!!</div>
-                </template>
-              </template>
-            </a-alert>
-            <a-upload :before-upload="beforeUpload" :file-list="fileList" :remove="handleRemove" :max-count="1">
-              <a-button type="primary">
-                <template #icon><upload-outlined /></template>
-                选择文件
-              </a-button>
-            </a-upload>
-            <div class="file-tip" v-if="fileList.length > 0">
-              <span>已选择: {{ fileList[0]?.name }}</span>
-              <span style="margin-left: 12px; color: #999">({{ formatFileSize(fileList[0]?.size) }})</span>
-              <span v-if="nameWarning" class="name-warning">{{ nameWarning }}</span>
-            </div>
-          </a-form-item>
-          <a-form-item :wrapper-col="{ offset: 4, span: 16 }">
-            <a-button type="primary" danger :loading="deploying" :disabled="fileList.length === 0" @click="handleUploadAndDeploy">
-              <template #icon><cloud-upload-outlined /></template>
-              上传并部署
-            </a-button>
-          </a-form-item>
-        </a-form>
-      </a-card>
-    </div>
-
-    <!-- 全屏loading -->
-    <div class="fullscreen-loading" v-if="deploying">
-      <a-spin size="large" />
-      <span class="loading-text">上传并部署中,请勿关闭或操作页面</span>
-    </div>
-
-    <!-- 部署结果弹框 -->
-    <a-modal v-model:visible="resultVisible" :title="deployIsSuccess ? '部署成功' : '部署失败'" :footer="null" width="600px">
-      <div class="deploy-status-icon" :class="deployIsSuccess ? 'is-success' : 'is-fail'">
-        <template v-if="deployIsSuccess">
-          <span class="icon-check"></span>
-        </template>
-        <template v-else>
-          <span class="icon-cross"></span>
-        </template>
-      </div>
-      <div class="deploy-msg">{{ deployMsg }}</div>
-    </a-modal>
+    <a-tabs v-model:activeKey="activeTab" type="line">
+      <a-tab-pane key="upload" tab="上传部署包">
+        <PackageUpload />
+      </a-tab-pane>
+      <a-tab-pane key="history" tab="部署历史">
+        <DeployHistory />
+      </a-tab-pane>
+    </a-tabs>
   </div>
 </template>
 
 <script setup lang="ts">
-  import { ref, watch } from 'vue';
-  import { UploadOutlined, CloudUploadOutlined } from '@ant-design/icons-vue';
-  import { message } from 'ant-design-vue';
-  import { uploadProgramApi } from './package.api';
-
-  const packageType = ref<string>('frontend');
-  const fileList = ref<any[]>([]);
-  const deploying = ref<boolean>(false);
-  const resultVisible = ref<boolean>(false);
-  const deployIsSuccess = ref<boolean>(false);
-  const deployMsg = ref<string>('');
-  const nameWarning = ref<string>('');
-
-  const acceptMap: Record<string, string> = {
-    frontend: '.zip',
-    backend: '.jar',
-  };
-
-  const backendJarNames = [
-    'jeecg-cloud-gateway-3.2.0.jar',
-    'jeecg-cloud-nacos-3.2.0.jar',
-    'jeecg-cloud-system-start-3.2.0.jar',
-    'ventanaly-monitor-3.2.0.jar',
-    'ventanaly-model-3.2.0.jar',
-    'ventanaly-jingtaifengliang-3.2.0.jar',
-    'ventanaly-sharefile-3.2.0.jar',
-    'ventanaly-province-3.2.0.jar',
-    'ventanaly-ai-service-3.2.0.jar',
-    'ventanaly-company-3.2.0.jar',
-  ];
-
-  watch(packageType, () => {
-    fileList.value = [];
-    nameWarning.value = '';
-  });
-
-  function beforeUpload(file) {
-    const accept = acceptMap[packageType.value];
-    const acceptExts = accept.split(',');
-    const ext = '.' + file.name.split('.').pop().toLowerCase();
-    if (!acceptExts.includes(ext)) {
-      message.error(`当前类型仅支持 ${accept} 格式的文件`);
-      return false;
-    }
-    if (packageType.value === 'frontend') {
-      nameWarning.value = /^dist\d+\.zip$/i.test(file.name) ? '' : '包名不符合命名规则,请检查!!!';
-    } else {
-      nameWarning.value = backendJarNames.includes(file.name) ? '' : '包名不符合命名规则,请检查!!!';
-    }
-    fileList.value = [file];
-    return false;
-  }
+  import { ref } from 'vue';
+  import PackageUpload from './components/PackageUpload.vue';
+  import DeployHistory from './components/DeployHistory.vue';
 
-  function handleRemove() {
-    fileList.value = [];
-    nameWarning.value = '';
-  }
-
-  function formatFileSize(size?: number): string {
-    if (!size) return '0 B';
-    const units = ['B', 'KB', 'MB', 'GB'];
-    let i = 0;
-    let s = size;
-    while (s >= 1024 && i < units.length - 1) {
-      s /= 1024;
-      i++;
-    }
-    return s.toFixed(2) + ' ' + units[i];
-  }
-
-  async function handleUploadAndDeploy() {
-    if (fileList.value.length === 0) {
-      message.warning('请先选择文件');
-      return;
-    }
-    deploying.value = true;
-    try {
-      const res = await uploadProgramApi(fileList.value[0]);
-      const result = res?.result || {};
-      deployIsSuccess.value = result.isSuccess === true;
-      deployMsg.value = result.msg || res?.message || '未知结果';
-      resultVisible.value = true;
-      if (result.isSuccess) {
-        fileList.value = [];
-      }
-    } catch (e: any) {
-      deployIsSuccess.value = false;
-      deployMsg.value = e?.message || '上传请求失败,请检查网络连接';
-      resultVisible.value = true;
-    } finally {
-      deploying.value = false;
-    }
-  }
+  const activeTab = ref<string>('upload');
 </script>
 
 <style lang="less" scoped>
@@ -188,15 +30,6 @@
     height: 100%;
     overflow-y: auto;
 
-    :deep(.zxm-card-head-title),
-    :deep(.zxm-form-item-label > label) {
-      color: #fff !important;
-    }
-
-    :deep(.anticon-exclamation-circle) {
-      display: none;
-    }
-
     .upload-header {
       margin-bottom: 20px;
 
@@ -207,102 +40,26 @@
       }
     }
 
-    .file-tip {
-      margin-top: 8px;
-      font-size: 13px;
-      color: #fff;
-    }
-
-    .name-warning {
-      margin-left: 12px;
-      color: #ff4d4f;
-      font-weight: bold;
-    }
-  }
-
-  .fullscreen-loading {
-    position: fixed;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    background: rgba(0, 0, 0, 0.45);
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    z-index: 9999;
-
-    .loading-text {
-      margin-top: 16px;
-      color: #fff;
-      font-size: 16px;
-    }
-  }
-
-  .deploy-status-icon {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    width: 48px;
-    height: 48px;
-    border-radius: 50%;
-    margin: 8px auto 20px;
-
-    &.is-success {
-      background: #52c41a;
+    :deep(.zxm-tabs-nav) {
+      .zxm-tabs-tab {
+        color: rgba(255, 255, 255, 0.55);
 
-      .icon-check {
-        display: block;
-        width: 20px;
-        height: 36px;
-        border: solid #fff;
-        border-width: 0 5px 5px 0;
-        transform: rotate(45deg) translate(-3px, -3px);
-      }
-    }
-
-    &.is-fail {
-      background: #ff4d4f;
-
-      .icon-cross {
-        display: block;
-        width: 32px;
-        height: 32px;
-        position: relative;
-
-        &::before,
-        &::after {
-          content: '';
-          position: absolute;
-          top: 50%;
-          left: 50%;
-          width: 5px;
-          height: 32px;
-          background: #fff;
-          border-radius: 2px;
+        &:hover {
+          color: rgba(255, 255, 255, 0.85);
         }
 
-        &::before {
-          transform: translate(-50%, -50%) rotate(45deg);
-        }
+        &.zxm-tabs-tab-active {
+          color: #0a84ff;
 
-        &::after {
-          transform: translate(-50%, -50%) rotate(-45deg);
+          .zxm-tabs-tab-btn {
+            color: #0a84ff;
+          }
         }
       }
-    }
-  }
 
-  .deploy-msg {
-    white-space: pre-wrap;
-    word-break: break-all;
-    max-height: 400px;
-    overflow-y: auto;
-    padding: 12px;
-    border-radius: 4px;
-    font-size: 20px;
-    line-height: 1.6;
-    text-align: center;
+      .zxm-tabs-ink-bar {
+        background: #0a84ff;
+      }
+    }
   }
 </style>

+ 7 - 0
src/views/vent/packageUpload/package.api.ts

@@ -3,6 +3,8 @@ import { defHttp } from '/@/utils/http/axios';
 enum Api {
   sysUploadProgram = '/ventanaly-device/safety/common/uploadProgram',
   uploadProgram = '/jeecg-system/sys/common/uploadProgram',
+  deployLogList = '/ventanaly-device/safety/common/deployLogList',
+
 }
 
 /**
@@ -13,3 +15,8 @@ export const uploadProgramApi = async (file: File) => {
   const url = fileName.includes('system') ? Api.sysUploadProgram : Api.uploadProgram;
   return defHttp.uploadFile({ url, timeout: 15 * 60 * 1000 }, { file, name: 'file' }, { isReturnResponse: true });
 };
+
+/**
+ * 部署包历史记录
+ */
+export const deployLogListApi = (params) => defHttp.get({ url: Api.deployLogList, params }, { joinParamsToUrl: true });

+ 7 - 7
src/views/vent/performance/fileDetail/fileDetail.api.ts

@@ -13,10 +13,10 @@ enum Api {
   commit = '/safety/actBusiness/commit',
   getNowUserAgencyData = '/safety/approvalBusiness/getNowUserAgencyData',
   getNowUserApprovedData = '/safety/approvalBusiness/getNowUserApprovedData',
-  sumVentReport='/ventanaly-company/company/reportData/sumVentReport',
-  zipdownload='/ventanaly-sharefile/fileServer/zipdownload',
-  queryVentMonReportJd='/ventanaly-company/company/reportData/queryVentMonReportJd',
-  updataFileParam='/ventanaly-sharefile/fileServer/updataFileParam'
+  sumVentReport = '/company/reportData/sumVentReport',
+  zipdownload = '/ventanaly-sharefile/fileServer/zipdownload',
+  queryVentMonReportJd = '/company/reportData/queryVentMonReportJd',
+  updataFileParam = '/ventanaly-sharefile/fileServer/updataFileParam',
 }
 
 /**
@@ -32,17 +32,17 @@ export const updataFileParam = (params, handleSuccess) => {
  * 下载文件目录
  * @param params
  */
-export const zipdownload = (params) => defHttp.post({ url: Api.zipdownload, params, responseType: 'blob' },{ joinParamsToUrl: true });
+export const zipdownload = (params) => defHttp.post({ url: Api.zipdownload, params, responseType: 'blob' }, { joinParamsToUrl: true });
 /**
  * 汇总集团通风报表
  * @param params
  */
-export const sumVentReport = () => defHttp.post({ url: Api.sumVentReport, });
+export const sumVentReport = () => defHttp.post({ url: Api.sumVentReport });
 /**
  * 汇总集团通风报表
  * @param params
  */
-export const queryVentMonReportJd = () => defHttp.get({ url: Api.queryVentMonReportJd, });
+export const queryVentMonReportJd = () => defHttp.get({ url: Api.queryVentMonReportJd });
 
 /**
  * 左侧树接口

+ 10 - 11
src/views/vent/reportManager/reportManager.api.ts

@@ -10,20 +10,20 @@ enum Api {
   hisdownload = '/safety/reportLog/download',
   getQuery = '/sys/dict/reportKind/query',
   getUpload = '/safety/reportInfo/upload',
-  reportAdd= '/ventanaly-company/company/reportData/add',
-  queryDepartTreeSync= '/sys/sysDepart/queryDepartTreeSync',
-  synReportFile='/safety/reportInfo/synReportFile',
-  listMap='/ventanaly-company/company/reportData/list',
-  reportEdit='/ventanaly-company/company/reportData/edit',
-  reportDel='/ventanaly-company/company/reportData/delete',
-  queryTreeList='/sys/sysDepart/queryTreeList'
+  reportAdd = '/company/reportData/add',
+  queryDepartTreeSync = '/sys/sysDepart/queryDepartTreeSync',
+  synReportFile = '/safety/reportInfo/synReportFile',
+  listMap = '/company/reportData/list',
+  reportEdit = '/company/reportData/edit',
+  reportDel = '/company/reportData/delete',
+  queryTreeList = '/sys/sysDepart/queryTreeList',
 }
 
 /**
  * 部门树列表
  * @param params
  */
-export const queryTreeList = () => defHttp.get({ url: Api.queryTreeList, });
+export const queryTreeList = () => defHttp.get({ url: Api.queryTreeList });
 /**
  * 报表数据映射列表
  * @param params
@@ -39,7 +39,7 @@ export const synReportFile = (params) => defHttp.post({ url: Api.synReportFile,
  * 报表数据映射-所属部门编号列表
  * @param params
  */
-export const queryDepartTreeSync = () => defHttp.get({ url: Api.queryDepartTreeSync});
+export const queryDepartTreeSync = () => defHttp.get({ url: Api.queryDepartTreeSync });
 
 /**
  * 报表数据映射-添加
@@ -78,7 +78,6 @@ export const hisList = (params) => defHttp.get({ url: Api.hisList, params });
  */
 export const save = (params) => defHttp.post({ url: Api.save, params });
 
-
 /**
  * 删除用户
  */
@@ -108,4 +107,4 @@ export const getQuery = () => defHttp.get({ url: Api.getQuery });
  * 报表或模板上传
  * @param params
  */
-export const getUpload = (params) => defHttp.post({ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, url: Api.getUpload, params });
+export const getUpload = (params) => defHttp.post({ headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, url: Api.getUpload, params });

+ 1 - 1
src/views/ventAI/dataPicker/index.ts

@@ -1,4 +1,4 @@
-export { useDataPicker } from './useDataPicker';
+export { useDataPicker, dataPickerHoverEnabled, setDataPickerHoverEnabled } from './useDataPicker';
 export type { DataPickerOptions } from './useDataPicker';
 export { fetchTunnelInterpretation, fetchDeviceInterpretation } from './api';
 export type { SSERawEvent, SSEEventType, TodoItem, ToolCallRecord } from './types';

+ 34 - 0
src/views/ventAI/dataPicker/useDataPicker.ts

@@ -5,6 +5,34 @@ import type { TodoItem, ToolCallRecord, SSERawEvent } from './types';
 import DataPickerModal from './components/DataPickerModal.vue';
 import aiIcon from '@/assets/images/ventAI/dataPicker/AI-icon.svg';
 
+// ─── 全局开关:控制悬停模式浮动按钮是否启用 ─────────────────────────────────
+const STORAGE_KEY = 'dataPickerHoverEnabled';
+
+export const dataPickerHoverEnabled = ref(localStorage.getItem(STORAGE_KEY) !== 'false');
+
+/** 记录所有活跃的悬停实例,以便全局开关关闭时立即隐藏按钮 */
+const allHoverInstances = new Set<HoverInstance>();
+
+export function setDataPickerHoverEnabled(val: boolean) {
+  dataPickerHoverEnabled.value = val;
+  localStorage.setItem(STORAGE_KEY, String(val));
+  if (!val) {
+    // 关闭时立即隐藏所有浮动按钮并清除定时器
+    allHoverInstances.forEach((inst) => {
+      inst.cancelLeave();
+      inst.clearHideTimer();
+      const btn = (inst as any)._btn as HTMLElement;
+      if (btn) {
+        btn.style.display = 'none';
+        btn.style.transform = 'scale(0.9)';
+        btn.style.opacity = '0';
+      }
+      // 重新挂载 mousemove 监听,以便开启后能正常触发
+      inst.attachMove();
+    });
+  }
+}
+
 // ─── 按钮 DOM 构建(悬停模式专用)──────────────────────────────────────────
 const BUTTON_CLASS = 'di-floating-btn';
 
@@ -116,6 +144,7 @@ function createHoverInstance(btn: HTMLElement) {
       hostEl = el;
     },
     onEnter(e: MouseEvent) {
+      if (!dataPickerHoverEnabled.value) return;
       clearHideTimer();
       if (!visible) {
         setPosition(e.clientX - 36, e.clientY - 34);
@@ -126,6 +155,7 @@ function createHoverInstance(btn: HTMLElement) {
       }
     },
     onMove(e: MouseEvent) {
+      if (!dataPickerHoverEnabled.value) return;
       setPosition(e.clientX - 36, e.clientY - 34);
       clearPauseTimer();
       pauseTimer = setTimeout(show, PAUSE_DELAY);
@@ -146,6 +176,7 @@ function createHoverInstance(btn: HTMLElement) {
     },
     clearHideTimer,
     startHideTimer,
+    attachMove,
     destroy() {
       clearPauseTimer();
       clearHideTimer();
@@ -327,6 +358,8 @@ export function useDataPicker(
     btnContainer!.appendChild(btn);
     const inst = createHoverInstance(btn);
     (btn as any).__di_owner = inst;
+    (inst as any)._btn = btn;
+    allHoverInstances.add(inst);
     btn.addEventListener('click', (e) => {
       e.stopPropagation();
       const item = (inst as any).raw;
@@ -373,6 +406,7 @@ export function useDataPicker(
       if (mode === 'hover') {
         const inst = hoverMap.get(el);
         if (inst) {
+          allHoverInstances.delete(inst);
           inst.destroy();
           hoverMap.delete(el);
         }

+ 84 - 0
src/views/ventAI/manageAssistent/api.ts

@@ -1,10 +1,12 @@
 import { defHttp } from '/@/utils/http/axios';
+import { getToken } from '/@/utils/auth';
 
 enum Api {
   unified = '/ventAI/api/chat',
   getHistoryList = '/ventAI/api/sessions',
   getDetail = '/ventAI/api/chat/history/',
   deleteSession = '/ventAI/api/sessions/',
+  reviewPdf = '/ventAI/api/vent/review/pdf',
 }
 
 /**
@@ -49,6 +51,7 @@ export const unifiedStream = async (
     const response = await fetch(Api.unified, {
       method: 'POST',
       body: formData,
+      headers: { 'X-Access-Token': getToken() },
     });
 
     if (!response.ok) {
@@ -103,3 +106,84 @@ export const unifiedStream = async (
     throw error;
   }
 };
+
+/**
+ * PDF审查接口(SSE流式响应)
+ * @param params.file - 配风计划PDF文件,必填
+ * @param params.session_id - 会话唯一标识ID,可选
+ * @param params.message - 附加消息/审查指令,可选
+ * @param onChunk - 流式数据回调函数
+ * @returns Promise<{ session_id: string }>
+ */
+export const reviewPdfStream = async (
+  params: { file: File; session_id?: string; message?: string },
+  onChunk: (chunk: string) => void
+): Promise<{ session_id: string }> => {
+  try {
+    const formData = new FormData();
+    formData.append('file', params.file);
+    if (params.session_id) {
+      formData.append('session_id', params.session_id);
+    }
+    if (params.message) {
+      formData.append('message', params.message);
+    }
+
+    const response = await fetch(Api.reviewPdf, {
+      method: 'POST',
+      body: formData,
+      headers: { 'X-Access-Token': getToken() },
+    });
+
+    if (!response.ok) {
+      throw new Error(`HTTP错误: ${response.status}`);
+    }
+
+    const reader = response.body!.getReader();
+    const decoder = new TextDecoder();
+    let buffer = '';
+    let sessionId = '';
+    let isDone = false;
+
+    // eslint-disable-next-line no-constant-condition
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done || isDone) break;
+
+      buffer += decoder.decode(value, { stream: true });
+      const lines = buffer.split('\n');
+      buffer = lines.pop() || '';
+
+      for (const line of lines) {
+        if (line.trim() === '') continue;
+
+        if (line.startsWith('data: ')) {
+          const dataStr = line.replace(/^data: /, '');
+
+          try {
+            const data = JSON.parse(dataStr);
+
+            onChunk(line);
+
+            if (data.type === 'done' || data.type === 'error') {
+              if (data.session_id) {
+                sessionId = data.session_id;
+              }
+              isDone = true;
+              break;
+            }
+          } catch (e) {
+            console.warn('SSE数据解析失败:', dataStr);
+          }
+        }
+      }
+
+      if (isDone) break;
+    }
+
+    return { session_id: sessionId };
+  } catch (error) {
+    console.error('PDF审查流式请求失败:', error);
+    throw error;
+  }
+};

+ 86 - 9
src/views/ventAI/manageAssistent/components/AiAssistantModal.vue

@@ -81,7 +81,7 @@
 <script setup lang="ts">
   import { ref, nextTick, watch, computed, onMounted, onBeforeUnmount } from 'vue';
   import dayjs from 'dayjs';
-  import { unifiedStream, getHistoryList, getDetail, deleteSession } from '../api';
+  import { unifiedStream, reviewPdfStream, getHistoryList, getDetail, deleteSession } from '../api';
   import { message, Modal } from 'ant-design-vue';
   import { isPdfFile } from './chatModal/utils';
   import TaskListPanel from './chatModal/TaskListPanel.vue';
@@ -276,18 +276,22 @@
     switchTask(newTask.id);
   };
 
-  const handleDeleteTask = (task: Task, index: number) => {
+  const handleDeleteTask = (task: Task, _index: number) => {
     Modal.confirm({
       title: '提示',
       content: '是否删除该对话?',
       okText: '确认',
       cancelText: '取消',
+      wrapClassName: 'delete-task-confirm-modal',
       onOk: async () => {
         try {
           if (task.sessionId) {
             await deleteSession(task.sessionId);
           }
-          taskList.value.splice(index, 1);
+          const realIndex = taskList.value.findIndex((t) => t.id === task.id);
+          if (realIndex !== -1) {
+            taskList.value.splice(realIndex, 1);
+          }
           if (currentTaskId.value === task.id) {
             if (taskList.value.length > 0) {
               switchTask(taskList.value[0].id);
@@ -632,7 +636,7 @@
         upsertStep(aiMsg.thinkingSteps!, {
           type: 'agent_done',
           agent: data.cn_agent || data.agent || '智能助手',
-          message: `${data.message || '审查完成'}${data.duration_ms ? `(${(data.duration_ms / 1000).toFixed(0)}s)` : ''}${data.progress ? ` [${data.progress}]` : ''}`,
+          message: `${(data.message || '审查完成').replace(/(\d+ms)/g, '')}${data.duration_ms ? `(${(data.duration_ms / 1000).toFixed(0)}s)` : ''}${data.progress ? ` [${data.progress}]` : ''}`,
           timestamp: Date.now(),
         });
         if (data.preview) {
@@ -819,11 +823,11 @@
       throw new Error('缺少原始文件对象');
     }
 
-    const result = await unifiedStream(
+    const result = await reviewPdfStream(
       {
-        message: userInput || `上传了文件:${file.name}`,
+        file: file.originalFile!,
         session_id: getCurrentSessionId() || undefined,
-        file: file.originalFile,
+        message: userInput || undefined,
       },
       (chunk: string) => {
         parseSSEData(chunk, aiMsgIndex);
@@ -1045,7 +1049,7 @@
   }
 
   .task-panel-wrapper {
-    width: 260px;
+    width: 300px;
     flex-shrink: 0;
     overflow: hidden;
     transition:
@@ -1252,6 +1256,7 @@
       padding: 0 !important;
       max-width: 100vw !important;
       margin: 0 !important;
+      background-color: #09172c;
 
       .zxm-modal-wrap,
       .ant-modal-wrap {
@@ -1267,9 +1272,14 @@
         margin: 0;
         border-radius: 0;
         overflow: hidden;
+        background-image: var(--img-chat-fs-modal-bg);
       }
       .zxm-modal-header {
-        height: 120px;
+        height: 90px;
+        background-image: var(--img-chat-fs-header-bg) !important;
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+        margin-bottom: 16px;
       }
       .no-drag {
         cursor: default;
@@ -1287,6 +1297,66 @@
         height: 100%;
         overflow: hidden;
       }
+      .chat-panel {
+        background-image: var(--img-chat-fs-panel-bg) !important;
+      }
+      .chat-header {
+        padding: 0 32px;
+        height: 50px;
+      }
+      .collapse-btn {
+        top: 60px;
+        background-image: var(--img-chat-fs-collapse-btn-bg) !important;
+        background-repeat: no-repeat;
+        background-size: 100% 100%;
+      }
+      .task-list-panel {
+        padding: 20px;
+        background-image: var(--img-chat-fs-task-panel-bg) !important;
+        background-repeat: no-repeat !important;
+        background-size: 100% 100% !important;
+      }
+      .options-area {
+        gap: 12px;
+      }
+      .options-area-item {
+        background-image: var(--img-chat-fs-options-item-bg) !important;
+        background-repeat: no-repeat !important;
+        background-size: 100% 100% !important;
+
+        &.active,
+        &:active {
+          background-image: var(--img-chat-fs-options-item-active-bg) !important;
+        }
+        .options-img {
+          background-image: none !important;
+        }
+      }
+      .chat-header-title {
+        height: 40px;
+        line-height: 40px;
+        position: relative;
+        background-image: var(--img-chat-fs-header-title-bg) !important;
+        background-repeat: no-repeat !important;
+        background-size: auto 100% !important;
+        padding: 0 28px;
+
+        &::before {
+          content: '';
+          position: absolute;
+          left: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          width: 40px;
+          height: 40px;
+          background-image: var(--img-chat-fs-header-title-icon);
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+        }
+      }
+      .chat-input-area {
+        margin: 24px;
+      }
     }
 
     .zxm-modal-header {
@@ -1318,4 +1388,11 @@
       padding: 12px 24px;
     }
   }
+
+  .delete-task-confirm-modal {
+    .zxm-modal-confirm-title,
+    .zxm-modal-confirm-content {
+      color: #fff;
+    }
+  }
 </style>

+ 791 - 11
src/views/ventAI/manageAssistent/components/chatModal/ChatInputArea.vue

@@ -24,14 +24,139 @@
     />
     <div class="input-actions">
       <div class="action-left">
-        <a-tooltip title="从本地上传">
-          <div class="btn-upload" @click="emit('file-upload')">
-            <div class="plus-icon"></div>
+        <div class="btn-upload-wrapper">
+          <a-tooltip title="添加上下文">
+            <div class="btn-upload" @click="togglePopup">
+              <div class="plus-icon"></div>
+            </div>
+          </a-tooltip>
+          <div v-if="showPopup" ref="popupRef" class="upload-popup" @click.stop>
+            <div class="popup-item" @click="handleAddAttachment">
+              <div class="popup-item-icon attach-icon"></div>
+              <span class="popup-item-text">添加附件</span>
+            </div>
+            <div class="popup-item" @click="handleInsertMention">
+              <div class="popup-item-icon mention-icon"></div>
+              <span class="popup-item-text">插入 @ 提及</span>
+            </div>
+            <div class="popup-item" @click="handleInsertSession">
+              <div class="popup-item-icon session-icon"></div>
+              <span class="popup-item-text">插入 # 会话</span>
+            </div>
+            <div class="popup-item" @click="handleInsertCommand">
+              <div class="popup-item-icon command-icon"></div>
+              <span class="popup-item-text">插入 / 命令</span>
+            </div>
           </div>
-        </a-tooltip>
+        </div>
+        <div class="action-divider"></div>
+        <div class="btn-edit-mode-wrapper">
+          <a-tooltip title="切换模式">
+            <div class="btn-edit-mode" @click="toggleEditModePopup">
+              <div class="btn-icon" :class="`mode-icon-${selectedEditMode.key}`"></div>
+              <span class="btn-text">{{ selectedEditMode.label }}</span>
+              <div class="btn-arrow"></div>
+            </div>
+          </a-tooltip>
+          <div v-if="showEditModePopup" ref="editModePopupRef" class="edit-mode-popup">
+            <div
+              v-for="item in editModeOptions"
+              :key="item.key"
+              class="edit-mode-item"
+              :class="{ active: selectedEditMode.key === item.key }"
+              @click="handleSelectEditMode(item)"
+            >
+              <div class="edit-mode-item-icon" :class="`mode-icon-${item.key}`"></div>
+              <div class="edit-mode-item-main">
+                <span class="edit-mode-item-label">{{ item.label }}</span>
+                <span class="edit-mode-item-desc">{{ item.desc }}</span>
+              </div>
+              <div class="edit-mode-check" :class="{ visible: selectedEditMode.key === item.key }"></div>
+            </div>
+          </div>
+        </div>
         <input ref="fileInputRef" type="file" style="display: none" @change="emit('file-change', $event)" accept=".pdf" />
       </div>
       <div class="action-right">
+        <div class="btn-context-wrapper" ref="contextPopupRef">
+          <div class="btn-context" @click="toggleContextPopup" @mouseenter="showContextPopup = true" @mouseleave="showContextPopup = false">
+            <svg class="context-ring" viewBox="0 0 32 32">
+              <circle class="context-ring-bg" cx="16" cy="16" r="13" />
+              <circle
+                class="context-ring-fill"
+                cx="16"
+                cy="16"
+                r="13"
+                :stroke-dasharray="contextCircumference"
+                :stroke-dashoffset="contextOffset"
+                :style="{ stroke: contextColor }"
+              />
+            </svg>
+            <span class="context-percent">{{ contextPercent }}%</span>
+          </div>
+          <div v-if="showContextPopup" class="context-popup" @click.stop>
+            <div class="context-popup-title">上下文容量</div>
+            <div class="context-popup-info">
+              <span class="context-used">{{ contextDisplayUsed }}</span>
+              <span class="context-sep">/</span>
+              <span class="context-max">{{ contextDisplayMax }}</span>
+              <span class="context-pct">({{ contextPercent }}%)</span>
+            </div>
+            <div class="context-popup-bar-track">
+              <div class="context-popup-bar-fill" :style="{ width: contextPercent + '%', background: contextColor }"></div>
+            </div>
+          </div>
+        </div>
+        <div class="action-divider"></div>
+        <div class="btn-model-wrapper" ref="modelPopupRef">
+          <a-tooltip title="选择模型">
+            <div class="btn-model" @click="toggleModelPopup">
+              <span class="btn-text">{{ selectedModel.label }}</span>
+              <div class="btn-arrow"></div>
+            </div>
+          </a-tooltip>
+          <div v-if="showModelPopup" class="model-popup" @click.stop>
+            <div
+              v-for="item in modelOptions"
+              :key="item.key"
+              class="model-popup-item"
+              :class="{ active: selectedModel.key === item.key }"
+              @click="handleSelectModel(item)"
+            >
+              <div class="model-item-main">
+                <span class="model-item-label">{{ item.label }}</span>
+                <span class="model-item-desc">{{ item.desc }}</span>
+              </div>
+              <div class="model-item-check" :class="{ visible: selectedModel.key === item.key }"></div>
+            </div>
+          </div>
+        </div>
+        <div class="action-divider"></div>
+        <div class="btn-think-wrapper" ref="thinkPopupRef">
+          <a-tooltip title="思考级别">
+            <div class="btn-think" @click="toggleThinkPopup">
+              <div class="btn-icon think-icon"></div>
+              <span class="btn-text">{{ selectedThinkLevel.label }}</span>
+              <div class="btn-arrow"></div>
+            </div>
+          </a-tooltip>
+          <div v-if="showThinkPopup" class="think-popup" @click.stop>
+            <div
+              v-for="item in thinkLevelOptions"
+              :key="item.key"
+              class="think-popup-item"
+              :class="{ active: selectedThinkLevel.key === item.key }"
+              @click="handleSelectThinkLevel(item)"
+            >
+              <div class="think-item-main">
+                <span class="think-item-label">{{ item.label }}</span>
+                <span class="think-item-desc">{{ item.desc }}</span>
+              </div>
+              <div class="think-item-check" :class="{ visible: selectedThinkLevel.key === item.key }"></div>
+            </div>
+          </div>
+        </div>
+        <div class="action-divider"></div>
         <div class="btn-send" :class="{ loading, disabled: !modelValue.trim() }" @click="handleSend">
           <div class="send-icon"></div>
         </div>
@@ -41,16 +166,25 @@
 </template>
 
 <script setup lang="ts">
-  import { ref } from 'vue';
+  import { ref, computed } from 'vue';
+  import { onClickOutside } from '@vueuse/core';
   import { CloseOutlined } from '@ant-design/icons-vue';
   import { formatFileSize } from './utils';
   import type { AttachedFile } from './types';
 
-  const props = defineProps<{
-    modelValue: string;
-    pendingFile: AttachedFile | null;
-    loading: boolean;
-  }>();
+  const props = withDefaults(
+    defineProps<{
+      modelValue: string;
+      pendingFile: AttachedFile | null;
+      loading: boolean;
+      contextUsed?: number;
+      contextMax?: number;
+    }>(),
+    {
+      contextUsed: 0,
+      contextMax: 200000,
+    }
+  );
 
   const emit = defineEmits<{
     (e: 'update:modelValue', value: string): void;
@@ -58,9 +192,172 @@
     (e: 'file-upload'): void;
     (e: 'file-change', event: Event): void;
     (e: 'remove-pending-file'): void;
+    (e: 'model-change', modelKey: string): void;
+    (e: 'think-level-change', levelKey: string): void;
   }>();
 
+  const handleSelectModel = (item: ModelOption) => {
+    selectedModel.value = item;
+    showModelPopup.value = false;
+    emit('model-change', item.key);
+  };
+
   const fileInputRef = ref<HTMLInputElement>();
+  const popupRef = ref<HTMLElement>();
+  const showPopup = ref(false);
+
+  // 上下文容量
+  const contextPopupRef = ref<HTMLElement>();
+  const showContextPopup = ref(false);
+
+  const contextPercent = computed(() => {
+    if (!props.contextMax) return 0;
+    return Math.min(100, Math.round((props.contextUsed / props.contextMax) * 100));
+  });
+
+  const contextCircumference = computed(() => 2 * Math.PI * 13);
+  const contextOffset = computed(() => contextCircumference.value * (1 - contextPercent.value / 100));
+
+  const contextColor = computed(() => {
+    if (contextPercent.value >= 90) return '#ff4d4f';
+    if (contextPercent.value >= 70) return '#faad14';
+    return '#52c41a';
+  });
+
+  const contextDisplayUsed = computed(() => {
+    if (props.contextUsed >= 10000) return (props.contextUsed / 10000).toFixed(1) + '万';
+    return props.contextUsed.toLocaleString();
+  });
+
+  const contextDisplayMax = computed(() => {
+    if (props.contextMax >= 10000) return (props.contextMax / 10000).toFixed(0) + '万';
+    return props.contextMax.toLocaleString();
+  });
+
+  const toggleContextPopup = () => {
+    showContextPopup.value = !showContextPopup.value;
+  };
+
+  onClickOutside(contextPopupRef, () => {
+    showContextPopup.value = false;
+  });
+
+  // 模型选择
+  const modelPopupRef = ref<HTMLElement>();
+  const showModelPopup = ref(false);
+
+  interface ModelOption {
+    key: string;
+    label: string;
+    desc: string;
+  }
+
+  const modelOptions: ModelOption[] = [
+    { key: 'deepseek-v4-pro', label: 'DeepSeek-V4-Pro', desc: '默认模型,综合能力强' },
+    { key: 'deepseek-v4', label: 'DeepSeek-V4', desc: '轻量快速,适合简单任务' },
+    { key: 'gpt-4o', label: 'GPT-4o', desc: 'OpenAI 多模态模型' },
+    { key: 'gpt-4o-mini', label: 'GPT-4o Mini', desc: 'OpenAI 轻量模型' },
+    { key: 'claude-sonnet-4', label: 'Claude Sonnet 4', desc: 'Anthropic 高性能模型' },
+    { key: 'qwen-max', label: '通义千问 Max', desc: '阿里云旗舰模型' },
+  ];
+
+  const selectedModel = ref<ModelOption>(modelOptions[0]);
+
+  const toggleModelPopup = () => {
+    showModelPopup.value = !showModelPopup.value;
+  };
+
+  onClickOutside(modelPopupRef, () => {
+    showModelPopup.value = false;
+  });
+
+  // 思考级别
+  const thinkPopupRef = ref<HTMLElement>();
+  const showThinkPopup = ref(false);
+
+  interface ThinkLevelOption {
+    key: string;
+    label: string;
+    desc: string;
+  }
+
+  const thinkLevelOptions: ThinkLevelOption[] = [
+    { key: 'off', label: '关闭', desc: '不使用深度思考,响应最快' },
+    { key: 'high', label: '高', desc: '深度思考,平衡速度与质量' },
+    { key: 'highest', label: '最高', desc: '极致思考,质量最高但较慢' },
+  ];
+
+  const selectedThinkLevel = ref<ThinkLevelOption>(thinkLevelOptions[0]);
+
+  const toggleThinkPopup = () => {
+    showThinkPopup.value = !showThinkPopup.value;
+  };
+
+  const handleSelectThinkLevel = (item: ThinkLevelOption) => {
+    selectedThinkLevel.value = item;
+    showThinkPopup.value = false;
+    emit('think-level-change', item.key);
+  };
+
+  onClickOutside(thinkPopupRef, () => {
+    showThinkPopup.value = false;
+  });
+
+  const editModePopupRef = ref<HTMLElement>();
+  const showEditModePopup = ref(false);
+
+  interface EditModeOption {
+    key: string;
+    label: string;
+    desc: string;
+  }
+
+  const editModeOptions: EditModeOption[] = [
+    { key: 'confirm', label: '变更前确认', desc: '改文件前先问我' },
+    { key: 'auto', label: '自动编辑', desc: '自动编辑文件' },
+    { key: 'plan', label: '计划模式', desc: '编辑前先出计划' },
+    { key: 'full', label: '完全访问', desc: '减少确认次数' },
+  ];
+
+  const selectedEditMode = ref<EditModeOption>(editModeOptions[1]);
+
+  onClickOutside(editModePopupRef, () => {
+    showEditModePopup.value = false;
+  });
+
+  const toggleEditModePopup = () => {
+    showEditModePopup.value = !showEditModePopup.value;
+  };
+
+  const handleSelectEditMode = (item: EditModeOption) => {
+    selectedEditMode.value = item;
+    showEditModePopup.value = false;
+  };
+
+  onClickOutside(popupRef, () => {
+    showPopup.value = false;
+  });
+
+  const togglePopup = () => {
+    showPopup.value = !showPopup.value;
+  };
+
+  const handleAddAttachment = () => {
+    showPopup.value = false;
+    emit('file-upload');
+  };
+
+  const handleInsertMention = () => {
+    showPopup.value = false;
+  };
+
+  const handleInsertSession = () => {
+    showPopup.value = false;
+  };
+
+  const handleInsertCommand = () => {
+    showPopup.value = false;
+  };
 
   const handleEnterKey = (e: KeyboardEvent) => {
     if (!e.shiftKey && props.modelValue.trim()) {
@@ -174,18 +471,64 @@
       align-items: center;
       margin-top: 12px;
 
+      .mode-icon-confirm {
+        background-image: var(--img-chat-mode-confirm-icon);
+      }
+      .mode-icon-auto {
+        background-image: var(--img-chat-mode-auto-icon);
+      }
+      .mode-icon-plan {
+        background-image: var(--img-chat-mode-plan-icon);
+      }
+      .mode-icon-full {
+        background-image: var(--img-chat-mode-full-icon);
+      }
+
       .action-left,
       .action-right {
         display: flex;
         gap: 8px;
+        .action-divider {
+          width: 1px;
+          height: 18px;
+          background: rgba(255, 255, 255, 0.15);
+          align-self: center;
+          flex-shrink: 0;
+        }
+        .btn-icon {
+          width: 14px;
+          height: 14px;
+          background-repeat: no-repeat;
+          background-size: 100% 100%;
+          flex-shrink: 0;
+        }
+        .think-icon {
+          background-image: var(--img-chat-thinking-icon);
+        }
+        .btn-text {
+          font-size: 12px;
+          color: #e0e6ed;
+          white-space: nowrap;
+        }
+        .btn-arrow {
+          width: 0;
+          height: 0;
+          border-left: 4px solid transparent;
+          border-right: 4px solid transparent;
+          border-top: 4px solid #8b949e;
+          flex-shrink: 0;
+        }
+        .btn-upload-wrapper {
+          position: relative;
+        }
         .btn-upload {
           width: 30px;
           height: 30px;
           border-radius: 6px;
-          margin-right: 10px;
           display: flex;
           align-items: center;
           justify-content: center;
+          cursor: pointer;
         }
         .plus-icon {
           width: 20px;
@@ -195,6 +538,443 @@
           background-size: 100% 100%;
           cursor: pointer;
         }
+        .upload-popup {
+          position: absolute;
+          bottom: 38px;
+          left: 0;
+          background: #0a2a3f;
+          border: 1px solid #1a4a6f;
+          border-radius: 8px;
+          min-width: 160px;
+          z-index: 1000;
+          box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+
+          .popup-item {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            padding: 9px 16px;
+            cursor: pointer;
+            transition: background 0.2s;
+            border-radius: 8px;
+
+            &:hover {
+              background: rgba(10, 132, 255, 0.15);
+            }
+
+            .popup-item-icon {
+              width: 18px;
+              height: 18px;
+              background-repeat: no-repeat;
+              background-size: 100% 100%;
+              flex-shrink: 0;
+            }
+
+            .attach-icon {
+              background-image: var(--img-chat-popup-attach-icon);
+            }
+
+            .mention-icon {
+              background-image: var(--img-chat-popup-mention-icon);
+            }
+
+            .session-icon {
+              background-image: var(--img-chat-popup-session-icon);
+            }
+
+            .command-icon {
+              background-image: var(--img-chat-popup-command-icon);
+            }
+
+            .popup-item-text {
+              font-size: 13px;
+              color: #e0e6ed;
+              white-space: nowrap;
+            }
+          }
+        }
+        .btn-edit-mode-wrapper {
+          position: relative;
+        }
+        .btn-edit-mode {
+          height: 28px;
+          padding: 0 10px;
+          border-radius: 6px;
+          display: flex;
+          align-items: center;
+          gap: 4px;
+          cursor: pointer;
+          transition: all 0.2s;
+          border: 1px solid rgba(255, 255, 255, 0.12);
+
+          &:hover {
+            border-color: #3a8fd4;
+            background: rgba(10, 132, 255, 0.1);
+          }
+        }
+        .edit-mode-popup {
+          position: absolute;
+          bottom: 38px;
+          left: 50%;
+          transform: translateX(-50%);
+          background: #0a2a3f;
+          border: 1px solid #1a4a6f;
+          border-radius: 8px;
+          min-width: 200px;
+          z-index: 1000;
+          box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+
+          .edit-mode-item {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 10px 12px;
+            cursor: pointer;
+            transition: background 0.2s;
+            border-radius: 6px;
+
+            &:hover {
+              background: rgba(10, 132, 255, 0.15);
+            }
+
+            &.active {
+              background: rgba(10, 132, 255, 0.1);
+            }
+
+            .edit-mode-item-icon {
+              width: 18px;
+              height: 18px;
+              background-repeat: no-repeat;
+              background-size: 100% 100%;
+              flex-shrink: 0;
+              margin-right: 10px;
+            }
+
+            .edit-mode-item-main {
+              display: flex;
+              flex-direction: column;
+              gap: 2px;
+
+              .edit-mode-item-label {
+                font-size: 13px;
+                color: #e0e6ed;
+                white-space: nowrap;
+              }
+
+              .edit-mode-item-desc {
+                font-size: 11px;
+                color: #8b949e;
+                white-space: nowrap;
+              }
+            }
+
+            .edit-mode-check {
+              width: 16px;
+              height: 16px;
+              flex-shrink: 0;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              visibility: hidden;
+
+              &.visible {
+                visibility: visible;
+              }
+
+              &::after {
+                content: '✓';
+                font-size: 12px;
+                color: #3a8fd4;
+              }
+            }
+          }
+        }
+        .btn-context-wrapper {
+          position: relative;
+          display: flex;
+          align-items: center;
+        }
+        .btn-context {
+          width: 30px;
+          height: 30px;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          cursor: pointer;
+          position: relative;
+          border-radius: 6px;
+          transition: background 0.2s;
+
+          &:hover {
+            background: rgba(10, 132, 255, 0.1);
+          }
+
+          .context-ring {
+            width: 24px;
+            height: 24px;
+            transform: rotate(-90deg);
+
+            .context-ring-bg {
+              fill: none;
+              stroke: rgba(255, 255, 255, 0.1);
+              stroke-width: 3;
+            }
+
+            .context-ring-fill {
+              fill: none;
+              stroke-width: 3;
+              stroke-linecap: round;
+              transition: stroke-dashoffset 0.3s ease;
+            }
+          }
+
+          .context-percent {
+            position: absolute;
+            font-size: 8px;
+            color: #e0e6ed;
+            font-weight: 500;
+            line-height: 1;
+          }
+        }
+        .context-popup {
+          position: absolute;
+          bottom: 42px;
+          left: 50%;
+          transform: translateX(-50%);
+          background: #0a2a3f;
+          border: 1px solid #1a4a6f;
+          border-radius: 8px;
+          min-width: 200px;
+          padding: 14px 16px;
+          z-index: 1000;
+          box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+
+          .context-popup-title {
+            font-size: 13px;
+            color: #8b949e;
+            margin-bottom: 8px;
+          }
+
+          .context-popup-info {
+            display: flex;
+            align-items: baseline;
+            gap: 2px;
+            margin-bottom: 10px;
+
+            .context-used {
+              font-size: 18px;
+              font-weight: 600;
+              color: #e0e6ed;
+            }
+
+            .context-sep {
+              font-size: 14px;
+              color: #8b949e;
+            }
+
+            .context-max {
+              font-size: 14px;
+              color: #8b949e;
+            }
+
+            .context-pct {
+              font-size: 13px;
+              color: #8b949e;
+            }
+          }
+
+          .context-popup-bar-track {
+            width: 100%;
+            height: 6px;
+            background: rgba(255, 255, 255, 0.08);
+            border-radius: 3px;
+            overflow: hidden;
+
+            .context-popup-bar-fill {
+              height: 100%;
+              border-radius: 3px;
+              transition: width 0.3s ease;
+            }
+          }
+        }
+        .btn-model-wrapper {
+          position: relative;
+          display: flex;
+          align-items: center;
+        }
+        .btn-model {
+          height: 28px;
+          padding: 0 10px;
+          border-radius: 6px;
+          display: flex;
+          align-items: center;
+          gap: 4px;
+          cursor: pointer;
+          transition: all 0.2s;
+          border: 1px solid rgba(255, 255, 255, 0.12);
+
+          &:hover {
+            border-color: #3a8fd4;
+            background: rgba(10, 132, 255, 0.1);
+          }
+        }
+        .model-popup {
+          position: absolute;
+          bottom: 38px;
+          left: 50%;
+          transform: translateX(-50%);
+          background: #0a2a3f;
+          border: 1px solid #1a4a6f;
+          border-radius: 8px;
+          min-width: 220px;
+          z-index: 1000;
+          box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+
+          .model-popup-item {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 10px 12px;
+            cursor: pointer;
+            transition: background 0.2s;
+            border-radius: 6px;
+
+            &:hover {
+              background: rgba(10, 132, 255, 0.15);
+            }
+
+            &.active {
+              background: rgba(10, 132, 255, 0.1);
+            }
+
+            .model-item-main {
+              display: flex;
+              flex-direction: column;
+              gap: 2px;
+
+              .model-item-label {
+                font-size: 13px;
+                color: #e0e6ed;
+                white-space: nowrap;
+              }
+
+              .model-item-desc {
+                font-size: 11px;
+                color: #8b949e;
+                white-space: nowrap;
+              }
+            }
+
+            .model-item-check {
+              width: 16px;
+              height: 16px;
+              flex-shrink: 0;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              visibility: hidden;
+
+              &.visible {
+                visibility: visible;
+              }
+
+              &::after {
+                content: '✓';
+                font-size: 12px;
+                color: #3a8fd4;
+              }
+            }
+          }
+        }
+        .btn-think-wrapper {
+          position: relative;
+          display: flex;
+          align-items: center;
+        }
+        .btn-think {
+          height: 28px;
+          padding: 0 10px;
+          border-radius: 6px;
+          display: flex;
+          align-items: center;
+          gap: 4px;
+          cursor: pointer;
+          transition: all 0.2s;
+          border: 1px solid rgba(255, 255, 255, 0.12);
+
+          &:hover {
+            border-color: #3a8fd4;
+            background: rgba(10, 132, 255, 0.1);
+          }
+        }
+        .think-popup {
+          position: absolute;
+          bottom: 38px;
+          left: 50%;
+          transform: translateX(-50%);
+          background: #0a2a3f;
+          border: 1px solid #1a4a6f;
+          border-radius: 8px;
+          min-width: 200px;
+          z-index: 1000;
+          box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
+
+          .think-popup-item {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 10px 12px;
+            cursor: pointer;
+            transition: background 0.2s;
+            border-radius: 6px;
+
+            &:hover {
+              background: rgba(10, 132, 255, 0.15);
+            }
+
+            &.active {
+              background: rgba(10, 132, 255, 0.1);
+            }
+
+            .think-item-main {
+              display: flex;
+              flex-direction: column;
+              gap: 2px;
+
+              .think-item-label {
+                font-size: 13px;
+                color: #e0e6ed;
+                white-space: nowrap;
+              }
+
+              .think-item-desc {
+                font-size: 11px;
+                color: #8b949e;
+                white-space: nowrap;
+              }
+            }
+
+            .think-item-check {
+              width: 16px;
+              height: 16px;
+              flex-shrink: 0;
+              display: flex;
+              align-items: center;
+              justify-content: center;
+              visibility: hidden;
+
+              &.visible {
+                visibility: visible;
+              }
+
+              &::after {
+                content: '✓';
+                font-size: 12px;
+                color: #3a8fd4;
+              }
+            }
+          }
+        }
         .btn-send {
           width: 32px;
           height: 32px;

+ 12 - 18
src/views/ventAI/manageAssistent/components/chatModal/ChatMessages.vue

@@ -8,9 +8,6 @@
       <div v-for="(message, index) in messages" :key="index" :class="['message-wrapper', message.type]" @mouseleave="copiedMessageIndex = -1">
         <!-- AI消息 -->
         <div v-if="message.type === 'ai'" class="ai-message">
-          <!-- <div class="message-agent">
-            <div class="agent-icon"></div>
-          </div> -->
           <div class="message-body">
             <!-- 思考过程折叠卡片 -->
             <div v-if="message.thinkingSteps && message.thinkingSteps.length > 0" class="thinking-card">
@@ -170,6 +167,8 @@
   import dayjs from 'dayjs';
   import { renderMarkdown, formatFileSize, tableHtmlToMarkdown, tableHtmlToCsv } from './utils';
   import type { Message, AttachedFile, ThinkingStep } from './types';
+  // import katex from 'katex';
+  // import 'katex/dist/katex.min.css';
 
   const getCssUrl = (varName: string) => {
     const val = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
@@ -452,19 +451,14 @@
           margin-bottom: 8px;
           opacity: 0;
 
-          .agent-icon {
-            width: 40px;
-            height: 40px;
-            background-image: var(--img-chat-ai-avatar);
-            background-repeat: no-repeat;
-            background-size: 100% 100%;
-          }
         }
 
         .message-body {
           padding: 12px;
           border-radius: 6px;
           width: calc(100% - 50px);
+          min-width: 0;
+          overflow: hidden;
 
           /* 思考过程卡片 */
           .thinking-card {
@@ -702,6 +696,8 @@
             font-weight: 400;
             line-height: 1.6;
             color: #e0e6ed;
+            word-break: break-word;
+            overflow-wrap: break-word;
 
             :deep(h1),
             :deep(h2),
@@ -762,6 +758,8 @@
             :deep(.markdown-table-wrapper) {
               position: relative;
               margin: 6px 0;
+              max-width: 100%;
+              overflow-x: auto;
 
               .table-actions {
                 position: absolute;
@@ -848,14 +846,6 @@
           display: flex;
           margin-bottom: 8px;
           margin-left: 5px;
-
-          .user-icon {
-            width: 40px;
-            height: 40px;
-            background-image: var(--img-chat-collapse-bg);
-            background-repeat: no-repeat;
-            background-size: 100% 100%;
-          }
         }
 
         .message-body {
@@ -863,12 +853,16 @@
           padding: 12px;
           border-radius: 6px 0 6px 6px;
           max-width: 700px;
+          min-width: 0;
+          overflow: hidden;
 
           .message-text {
             font-size: 14px;
             font-weight: 400;
             line-height: 1.6;
             color: #e0e6ed;
+            word-break: break-word;
+            overflow-wrap: break-word;
 
             :deep(h1, h2, h3, h4, h5, h6) {
               color: #fff;

+ 108 - 21
src/views/ventAI/manageAssistent/components/chatModal/TaskListPanel.vue

@@ -9,7 +9,22 @@
         <a-button size="small" type="primary">{{ item.label }}</a-button>
       </div>
     </div>
-
+    <!-- 置顶任务区域 -->
+    <div v-if="pinnedTaskList.length > 0" class="pinned-task-list">
+      <div
+        v-for="(task, index) in pinnedTaskList"
+        :key="'pinned-' + task.id"
+        :class="['task-item', { active: currentTaskId === task.id }]"
+        @click="emit('switch-task', task.id)"
+      >
+        <div class="task-pin-icon pinned" @click.stop="togglePin(task.id)" title="取消置顶"></div>
+        <span class="task-name">{{ task.name }}</span>
+        <div class="task-right">
+          <span class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
+          <div class="task-delete" @click.stop="emit('delete-task', task, index)" title="删除"></div>
+        </div>
+      </div>
+    </div>
     <!-- 搜索模式 -->
     <div v-if="activeOption === 'search'" class="search-header">
       <a-input v-model:value="searchKeyword" placeholder="搜索任务名称..." allow-clear size="small" class="search-input" />
@@ -22,22 +37,21 @@
         :class="['task-item', { active: currentTaskId === task.id }]"
         @click="emit('switch-task', task.id)"
       >
+        <div class="task-pin-icon" @click.stop="togglePin(task.id)" title="置顶"></div>
         <span class="task-name">{{ task.name }}</span>
         <div class="task-right">
           <span class="task-time">{{ formatRelativeTime(task.updatedAt) }}</span>
-          <div class="task-delete" @click.stop="emit('delete-task', task, index)">
-            <DeleteOutlined />
-          </div>
+          <div class="task-delete" @click.stop="emit('delete-task', task, index)" title="删除"></div>
         </div>
       </div>
       <div v-if="activeOption === 'search' && searchKeyword && filteredTaskList.length === 0" class="empty-hint"> 未找到匹配的任务 </div>
+      <div v-else-if="filteredTaskList.length === 0" class="empty-hint"> 还没有任务 </div>
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
   import { ref, computed } from 'vue';
-  import { DeleteOutlined } from '@ant-design/icons-vue';
   import type { Task } from './types';
 
   const props = defineProps<{
@@ -55,6 +69,44 @@
 
   const activeOption = ref<OptionType>('add');
   const searchKeyword = ref('');
+  const PINNED_TASK_KEY = 'chat_pinned_tasks';
+  const pinnedTaskIds = ref<string[]>(loadPinnedTasks());
+
+  function loadPinnedTasks(): string[] {
+    try {
+      const cached = localStorage.getItem(PINNED_TASK_KEY);
+      return cached ? JSON.parse(cached) : [];
+    } catch {
+      return [];
+    }
+  }
+
+  function savePinnedTasks(ids: string[]) {
+    localStorage.setItem(PINNED_TASK_KEY, JSON.stringify(ids));
+  }
+
+  const pinnedTaskList = computed(() => {
+    return props.taskList.filter((task) => pinnedTaskIds.value.includes(task.id));
+  });
+
+  const filteredTaskList = computed(() => {
+    let list = props.taskList.filter((task) => !pinnedTaskIds.value.includes(task.id));
+    if (activeOption.value === 'search' && searchKeyword.value.trim()) {
+      const keyword = searchKeyword.value.trim().toLowerCase();
+      list = list.filter((task) => task.name.toLowerCase().includes(keyword));
+    }
+    return list;
+  });
+
+  const togglePin = (taskId: string) => {
+    const index = pinnedTaskIds.value.indexOf(taskId);
+    if (index >= 0) {
+      pinnedTaskIds.value.splice(index, 1);
+    } else {
+      pinnedTaskIds.value.push(taskId);
+    }
+    savePinnedTasks(pinnedTaskIds.value);
+  };
 
   const optionItems = [
     { type: 'add' as OptionType, label: '新建任务' },
@@ -62,13 +114,6 @@
     { type: 'skill' as OptionType, label: '技能' },
   ];
 
-  const filteredTaskList = computed(() => {
-    if (activeOption.value !== 'search' || !searchKeyword.value.trim()) {
-      return props.taskList;
-    }
-    const keyword = searchKeyword.value.trim().toLowerCase();
-    return props.taskList.filter((task) => task.name.toLowerCase().includes(keyword));
-  });
 
   const formatRelativeTime = (dateStr?: string) => {
     if (!dateStr) return '刚刚';
@@ -95,7 +140,7 @@
 
 <style scoped lang="less">
   .task-list-panel {
-    width: 260px;
+    width: 100%;
     height: 100%;
     display: flex;
     flex-direction: column;
@@ -122,15 +167,26 @@
       }
     }
 
+    .pinned-task-list {
+      max-height: 180px;
+      overflow-y: auto;
+      border-bottom: 1px solid rgba(63, 80, 106, 0.3);
+      margin-bottom: 8px;
+      padding-bottom: 8px;
+    }
+
     .task-list {
       flex: 1;
       overflow-y: auto;
+    }
 
+    .pinned-task-list,
+    .task-list {
       .task-item {
         height: 36px;
         display: flex;
         align-items: center;
-        gap: 10px;
+        gap: 4px;
         padding: 4px;
         cursor: pointer;
         transition: all 0.3s;
@@ -149,6 +205,31 @@
           color: #e0e6ed;
         }
 
+        .task-pin-icon {
+          width: 28px;
+          height: 28px;
+          flex-shrink: 0;
+          background-image: var(--img-chat-pin-icon);
+          background-repeat: no-repeat;
+          background-size: 16px 16px;
+          background-position: center;
+          opacity: 0;
+          transition: opacity 0.2s;
+          border-radius: 4px;
+
+          &.pinned {
+            opacity: 1;
+          }
+
+          &:hover {
+            background-color: rgba(10, 132, 255, 0.2);
+          }
+        }
+
+        &:hover .task-pin-icon {
+          opacity: 1;
+        }
+
         .task-icon {
           width: 16px;
           height: 16px;
@@ -168,6 +249,13 @@
           flex: 1;
         }
 
+        .task-right {
+          display: flex;
+          align-items: center;
+          gap: 6px;
+          flex-shrink: 0;
+        }
+
         .task-time {
           font-size: 12px;
           color: #8b949e;
@@ -180,16 +268,15 @@
           width: 28px;
           height: 28px;
           display: none;
-          align-items: center;
-          justify-content: center;
-          border-radius: 4px;
-          color: #8b949e;
-          font-size: 14px;
+          background-image: var(--img-chat-delete-icon);
+          background-repeat: no-repeat;
+          background-size: 16px 16px;
+          background-position: center;
           transition: all 0.2s;
+          border-radius: 4px;
 
           &:hover {
-            color: #ff4d4f;
-            background: rgba(220, 38, 38, 0.1);
+            background-color: rgba(10, 132, 255, 0.2);
           }
         }
 

+ 24 - 1
src/views/ventAI/manageAssistent/components/chatModal/utils.ts

@@ -1,9 +1,32 @@
 import { marked } from 'marked';
+import katex from 'katex';
+
+const renderLatexInHtml = (html: string): string => {
+  // 匹配块级公式($$...$$)
+  html = html.replace(/\$\$(.*?)\$\$/gs, (_match, formula) => {
+    return katex.renderToString(formula.trim(), {
+      displayMode: true,
+      throwOnError: false,
+      strict: false,
+      trust: true,
+    });
+  });
+  // 匹配行内公式($...$)
+  html = html.replace(/\$(.*?)\$/g, (_match, formula) => {
+    return katex.renderToString(formula.trim(), {
+      displayMode: false,
+      throwOnError: false,
+      strict: false,
+    });
+  });
+  return html;
+};
 
 export const renderMarkdown = (text: string, icons?: { copy: string; download: string; preview: string; copySuccess: string }): string => {
   if (!text) return '';
   const processed = text.replace(/<br\s*\/?>/gi, '  \n');
-  const html = marked.parse(processed) as string;
+  let html = marked.parse(processed) as string;
+  html = renderLatexInHtml(html);
   return icons ? wrapTables(html, icons) : html;
 };
 

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio