Ver código fonte

[Wip 0000] 各页面数据对接

houzekong 3 dias atrás
pai
commit
5d4175364a
35 arquivos alterados com 2106 adições e 868 exclusões
  1. 1 0
      package.json
  2. 488 5
      pnpm-lock.yaml
  3. 12 0
      public/js/global.js
  4. 15 11
      src/api/sys/map.ts
  5. 16 0
      src/api/workingface/index.ts
  6. 1 1
      src/enums/pageEnum.ts
  7. 3 2
      src/layouts/default/feature/SimpleMap.vue
  8. 506 0
      src/layouts/default/feature/SimpleMapTest.bak.txt
  9. 27 13
      src/layouts/default/feature/SimpleMapTest.vue
  10. 19 65
      src/layouts/default/feature/components/LeafPopup.vue
  11. 2 1
      src/layouts/default/plain.vue
  12. 1 6
      src/views/analysis/warningAnalysis/globalAlarm/globalAlarm.api.ts
  13. 2 0
      src/views/analysis/warningAnalysis/globalAlarm/globalAlarm.data.ts
  14. 46 34
      src/views/analysis/warningAnalysis/globalAlarm/index.vue
  15. 16 50
      src/views/analysis/warningAnalysis/windPointManage/index.vue
  16. 16 41
      src/views/analysis/warningAnalysis/windPointManage/windPointManage.api.ts
  17. 39 15
      src/views/analysis/warningAnalysis/windPointManage/windPointManage.data.ts
  18. 253 93
      src/views/analysis/warningAnalysis/windPointManage/windTopology/hooks/useTopology.ts
  19. 36 20
      src/views/analysis/warningAnalysis/windPointManage/windTopology/index.vue
  20. 34 18
      src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.api.ts
  21. 175 70
      src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data.ts
  22. 53 69
      src/views/dashboard/HiddenSurface/configurable.data.ts
  23. 6 83
      src/views/dashboard/HiddenSurface/hiddenSurface.api.ts
  24. 123 105
      src/views/dashboard/HiddenSurface/index.vue
  25. 1 1
      src/views/dashboard/Overhaul/overhaul.api.ts
  26. 1 1
      src/views/dashboard/basicInfo/basicInfo.api.ts
  27. 1 11
      src/views/dashboard/basicInfo/minesInfo/index.vue
  28. 47 87
      src/views/dashboard/basicInfo/minesInfo/minesInfo.data.ts
  29. 26 5
      src/views/monitor/sensorMonitor/index.vue
  30. 12 13
      src/views/monitor/sensorMonitor/sensorMonitor.data.ts
  31. 24 3
      src/views/monitor/surfaceMonitor/index.vue
  32. 80 20
      src/views/monitor/surfaceMonitor/surfaceMonitor.data.ts
  33. 7 7
      src/views/system/cadFile/app.ts
  34. 8 8
      src/views/system/cadFile/cad.api.ts
  35. 9 10
      src/views/system/cadFile/components/SideDrawer.vue

+ 1 - 0
package.json

@@ -47,6 +47,7 @@
     "lodash": "^4.17.21",
     "lodash-es": "^4.17.21",
     "lodash.get": "^4.4.2",
+    "map-cad-viewer": "file:../map-cad",
     "md5": "^2.3.0",
     "mockjs": "^1.1.0",
     "moment": "^2.30.1",

+ 488 - 5
pnpm-lock.yaml

@@ -76,6 +76,9 @@ dependencies:
   lodash.get:
     specifier: ^4.4.2
     version: 4.4.2
+  map-cad-viewer:
+    specifier: file:../map-cad
+    version: file:../map-cad(@loaders.gl/core@4.4.4)(@luma.gl/core@9.3.6)(@luma.gl/engine@9.3.6)(vue@3.5.25)
   md5:
     specifier: ^2.3.0
     version: 2.3.0
@@ -2011,6 +2014,77 @@ packages:
     engines: {node: '>=10'}
     dev: false
 
+  /@deck.gl/core@9.3.7:
+    resolution: {integrity: sha512-l+YPm1I0Fwj+b7xsu8FvyDT5MGPgEPJe2cQJUpQNgG742b2goB4tyYpteUhNAOeVo1gmenvqv36ZJGNsGxipJA==}
+    dependencies:
+      '@loaders.gl/core': 4.4.4
+      '@loaders.gl/images': 4.4.4(@loaders.gl/core@4.4.4)
+      '@luma.gl/core': 9.3.6
+      '@luma.gl/engine': 9.3.6(@luma.gl/core@9.3.6)(@luma.gl/shadertools@9.3.6)
+      '@luma.gl/shadertools': 9.3.6(@luma.gl/core@9.3.6)
+      '@luma.gl/webgl': 9.3.6(@luma.gl/core@9.3.6)
+      '@math.gl/core': 4.1.0
+      '@math.gl/sun': 4.1.0
+      '@math.gl/types': 4.1.0
+      '@math.gl/web-mercator': 4.1.0
+      '@probe.gl/env': 4.1.1
+      '@probe.gl/log': 4.1.1
+      '@probe.gl/stats': 4.1.1
+      '@types/offscreencanvas': 2019.7.3
+      gl-matrix: 3.4.4
+      mjolnir.js: 3.0.1
+    dev: false
+
+  /@deck.gl/extensions@9.3.7(@deck.gl/core@9.3.7)(@luma.gl/core@9.3.6)(@luma.gl/engine@9.3.6):
+    resolution: {integrity: sha512-CY4Hi7yeVNMOjYfbrxNZ5PF3ftH16zGwV4JSH6g9opCTfLFxBgT2Rxbs3cwywanAs5yO7RraDDM9WyQSzAU7qg==}
+    peerDependencies:
+      '@deck.gl/core': ~9.3.0
+      '@luma.gl/core': ~9.3.3
+      '@luma.gl/engine': ~9.3.3
+    dependencies:
+      '@deck.gl/core': 9.3.7
+      '@luma.gl/core': 9.3.6
+      '@luma.gl/engine': 9.3.6(@luma.gl/core@9.3.6)(@luma.gl/shadertools@9.3.6)
+      '@luma.gl/shadertools': 9.3.6(@luma.gl/core@9.3.6)
+      '@luma.gl/webgl': 9.3.6(@luma.gl/core@9.3.6)
+      '@math.gl/core': 4.1.0
+    dev: false
+
+  /@deck.gl/layers@9.3.7(@deck.gl/core@9.3.7)(@loaders.gl/core@4.4.4)(@luma.gl/core@9.3.6)(@luma.gl/engine@9.3.6):
+    resolution: {integrity: sha512-T2CnkEU1QEqmusrSdot7+8Nl09gzntG5DvhaogyBbTjmABhTh4rUnS6dLOjf5/ju3aJ+Q58/FvDzgBBcK4V7kg==}
+    peerDependencies:
+      '@deck.gl/core': ~9.3.0
+      '@loaders.gl/core': ^4.4.3
+      '@luma.gl/core': ~9.3.3
+      '@luma.gl/engine': ~9.3.3
+    dependencies:
+      '@deck.gl/core': 9.3.7
+      '@loaders.gl/core': 4.4.4
+      '@loaders.gl/images': 4.4.4(@loaders.gl/core@4.4.4)
+      '@loaders.gl/schema': 4.4.4
+      '@luma.gl/core': 9.3.6
+      '@luma.gl/engine': 9.3.6(@luma.gl/core@9.3.6)(@luma.gl/shadertools@9.3.6)
+      '@luma.gl/shadertools': 9.3.6(@luma.gl/core@9.3.6)
+      '@mapbox/tiny-sdf': 2.2.0
+      '@math.gl/core': 4.1.0
+      '@math.gl/polygon': 4.1.0
+      '@math.gl/web-mercator': 4.1.0
+      '@types/geojson': 7946.0.16
+      earcut: 2.2.4
+    dev: false
+
+  /@deck.gl/mapbox@9.3.7(@deck.gl/core@9.3.7)(@luma.gl/core@9.3.6)(@math.gl/web-mercator@4.1.0):
+    resolution: {integrity: sha512-hamgwBS2BbB8DHOjYWwPMrFki+KhcedjoHv/9cUpQHnjfXUswzmyhAURuyowO/tAVJb3vF+CmMzaDVUTlI40Sw==}
+    peerDependencies:
+      '@deck.gl/core': ~9.3.0
+      '@luma.gl/core': ~9.3.3
+      '@math.gl/web-mercator': ^4.1.0
+    dependencies:
+      '@deck.gl/core': 9.3.7
+      '@luma.gl/core': 9.3.6
+      '@math.gl/web-mercator': 4.1.0
+    dev: false
+
   /@dual-bundle/import-meta-resolve@4.2.1:
     resolution: {integrity: sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==}
     dev: true
@@ -2949,6 +3023,63 @@ packages:
     resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
     dev: true
 
+  /@loaders.gl/core@4.4.4:
+    resolution: {integrity: sha512-uUZLs1N1VQXj3jC81LS7OkrWw1NRQHb0rdGUZI/FOFvirFEIR+TAKcvMO1Zz9WQj2dT9r6HhVjMDJQxU42LsoA==}
+    dependencies:
+      '@loaders.gl/loader-utils': 4.4.4(@loaders.gl/core@4.4.4)
+      '@loaders.gl/schema': 4.4.4
+      '@loaders.gl/schema-utils': 4.4.4(@loaders.gl/core@4.4.4)
+      '@loaders.gl/worker-utils': 4.4.4(@loaders.gl/core@4.4.4)
+      '@probe.gl/log': 4.1.1
+    dev: false
+
+  /@loaders.gl/images@4.4.4(@loaders.gl/core@4.4.4):
+    resolution: {integrity: sha512-pqCfWRR7yEHCnAaWDu1aoD7NILts6mvY+B47lQXJ00Ffm4/cfx35dTZ4GUung/iqdVAbxC7fpGKwTPn3T06x4w==}
+    peerDependencies:
+      '@loaders.gl/core': ~4.4.0
+    dependencies:
+      '@loaders.gl/core': 4.4.4
+      '@loaders.gl/loader-utils': 4.4.4(@loaders.gl/core@4.4.4)
+    dev: false
+
+  /@loaders.gl/loader-utils@4.4.4(@loaders.gl/core@4.4.4):
+    resolution: {integrity: sha512-+0IizJUB6GvPQjl2xC2V1vpcMPcidZykWy27LFnIIqG1lesxfALnFmRoq9MjbOj2TPcUo7VSBxl8z55qRlX0rQ==}
+    dependencies:
+      '@loaders.gl/schema': 4.4.4
+      '@loaders.gl/worker-utils': 4.4.4(@loaders.gl/core@4.4.4)
+      '@probe.gl/log': 4.1.1
+      '@probe.gl/stats': 4.1.1
+    transitivePeerDependencies:
+      - '@loaders.gl/core'
+    dev: false
+
+  /@loaders.gl/schema-utils@4.4.4(@loaders.gl/core@4.4.4):
+    resolution: {integrity: sha512-/ykSrQlfn7Lx+kmQNuGzTaywLVy2RoaAZ5YiEXkP9UJ+4lYY2wP5KgUlQGwODDdzNPLFmYzd+FXeE9GEmlXIUg==}
+    peerDependencies:
+      '@loaders.gl/core': ~4.4.0
+    dependencies:
+      '@loaders.gl/core': 4.4.4
+      '@loaders.gl/schema': 4.4.4
+      '@math.gl/types': 4.1.0
+      '@types/geojson': 7946.0.16
+      apache-arrow: 21.2.0
+    dev: false
+
+  /@loaders.gl/schema@4.4.4:
+    resolution: {integrity: sha512-3spgUoN505V2OprDDzww5nhWfn8Htupcfdr+/lxswAhCbGjwM3RWzA3Zif3dW6ZxOCeSIRJ4GNON78Eiede2dw==}
+    dependencies:
+      '@types/geojson': 7946.0.16
+      apache-arrow: 21.2.0
+    dev: false
+
+  /@loaders.gl/worker-utils@4.4.4(@loaders.gl/core@4.4.4):
+    resolution: {integrity: sha512-UNQCwK7tHd7Qh0K7e6Flvac8qd14SoAgaN8SOjggd5ngYqwytOQiAo5uEuhRN+r7+EqdwU+qwcpRtjYByT+RqA==}
+    peerDependencies:
+      '@loaders.gl/core': ~4.4.0
+    dependencies:
+      '@loaders.gl/core': 4.4.4
+    dev: false
+
   /@logicflow/core@2.2.0:
     resolution: {integrity: sha512-QpiJijEPEpCeGk6v8Rx/NAjk9S17VJ9/fjOm7JCbuijmOPD7k0i4kn7enEObbBq3ofL5U2NzzNHQ4RrZaYP/pg==}
     dependencies:
@@ -3000,6 +3131,125 @@ packages:
       vue-demi: 0.14.10(vue@3.5.25)
     dev: false
 
+  /@luma.gl/core@9.3.6:
+    resolution: {integrity: sha512-eqHnCPh2xHYkdd9rEkiIIkGMixNiJkq1ROrnTob6npvmZNVHXL0ubIw5KueL7rqW0+J1tm+p2+TXZaCmn0sP7Q==}
+    dependencies:
+      '@math.gl/types': 4.1.0
+      '@probe.gl/env': 4.1.1
+      '@probe.gl/log': 4.1.1
+      '@probe.gl/stats': 4.1.1
+      '@types/offscreencanvas': 2019.7.3
+    dev: false
+
+  /@luma.gl/engine@9.3.6(@luma.gl/core@9.3.6)(@luma.gl/shadertools@9.3.6):
+    resolution: {integrity: sha512-NYTdhn2NaH/MjN8qXCl2ukrT1Ac9iTKu1mgN0wz11XRd1QYnlMNBXswRROD7IZ2PUsBWdmYHc9qE8wKBQqMuIA==}
+    peerDependencies:
+      '@luma.gl/core': ~9.3.0
+      '@luma.gl/shadertools': ~9.3.0
+    dependencies:
+      '@luma.gl/core': 9.3.6
+      '@luma.gl/shadertools': 9.3.6(@luma.gl/core@9.3.6)
+      '@math.gl/core': 4.1.0
+      '@math.gl/types': 4.1.0
+      '@probe.gl/log': 4.1.1
+      '@probe.gl/stats': 4.1.1
+    dev: false
+
+  /@luma.gl/shadertools@9.3.6(@luma.gl/core@9.3.6):
+    resolution: {integrity: sha512-dDuD8lCOkAE1L7hJGZEyZAi7upR1HG3wEEIQ1gCLaTTbJWC7tNtnVz853lqUA+VXgjgS9NiQFFRcosiIZmW4Vg==}
+    peerDependencies:
+      '@luma.gl/core': ~9.3.0
+    dependencies:
+      '@luma.gl/core': 9.3.6
+      '@math.gl/core': 4.1.0
+      '@math.gl/types': 4.1.0
+    dev: false
+
+  /@luma.gl/webgl@9.3.6(@luma.gl/core@9.3.6):
+    resolution: {integrity: sha512-tGm7FGWPmJKxGZMvkRPRi219x3tKmBxR7Uke09nTp2ujNak/O5V9xPIQ9lUBGTxqAb8U2yjKkXG8j+f/4b2+sw==}
+    peerDependencies:
+      '@luma.gl/core': ~9.3.0
+    dependencies:
+      '@luma.gl/core': 9.3.6
+      '@math.gl/types': 4.1.0
+      '@probe.gl/env': 4.1.1
+    dev: false
+
+  /@mapbox/geojson-rewind@0.5.2:
+    resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==}
+    hasBin: true
+    dependencies:
+      get-stream: 6.0.1
+      minimist: 1.2.8
+    dev: false
+
+  /@mapbox/jsonlint-lines-primitives@2.0.3:
+    resolution: {integrity: sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==}
+    engines: {node: '>= 22'}
+    dev: false
+
+  /@mapbox/point-geometry@0.1.0:
+    resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==}
+    dev: false
+
+  /@mapbox/tiny-sdf@2.2.0:
+    resolution: {integrity: sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==}
+    dev: false
+
+  /@mapbox/unitbezier@0.0.1:
+    resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==}
+    dev: false
+
+  /@mapbox/vector-tile@1.3.1:
+    resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==}
+    dependencies:
+      '@mapbox/point-geometry': 0.1.0
+    dev: false
+
+  /@mapbox/whoots-js@3.1.0:
+    resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==}
+    engines: {node: '>=6.0.0'}
+    dev: false
+
+  /@maplibre/maplibre-gl-style-spec@20.4.0:
+    resolution: {integrity: sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==}
+    hasBin: true
+    dependencies:
+      '@mapbox/jsonlint-lines-primitives': 2.0.3
+      '@mapbox/unitbezier': 0.0.1
+      json-stringify-pretty-compact: 4.0.0
+      minimist: 1.2.8
+      quickselect: 2.0.0
+      rw: 1.3.3
+      tinyqueue: 3.0.0
+    dev: false
+
+  /@math.gl/core@4.1.0:
+    resolution: {integrity: sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==}
+    dependencies:
+      '@math.gl/types': 4.1.0
+    dev: false
+
+  /@math.gl/polygon@4.1.0:
+    resolution: {integrity: sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA==}
+    dependencies:
+      '@math.gl/core': 4.1.0
+    dev: false
+
+  /@math.gl/sun@4.1.0:
+    resolution: {integrity: sha512-i3q6OCBLSZ5wgZVhXg+X7gsjY/TUtuFW/2KBiq/U1ypLso3S4sEykoU/MGjxUv1xiiGtr+v8TeMbO1OBIh/HmA==}
+    dev: false
+
+  /@math.gl/types@4.1.0:
+    resolution: {integrity: sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==}
+    dev: false
+
+  /@math.gl/web-mercator@4.1.0:
+    resolution: {integrity: sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw==}
+    dependencies:
+      '@math.gl/core': 4.1.0
+    dev: false
+
   /@nodelib/fs.scandir@2.1.5:
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
     engines: {node: '>= 8'}
@@ -3046,6 +3296,20 @@ packages:
     resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
     dev: true
 
+  /@probe.gl/env@4.1.1:
+    resolution: {integrity: sha512-+68seNDMVsEegRB47pFA/Ws1Fjy8agcFYXxzorKToyPcD6zd+gZ5uhwoLd7TzsSw6Ydns//2KEszWn+EnNHTbA==}
+    dev: false
+
+  /@probe.gl/log@4.1.1:
+    resolution: {integrity: sha512-kcZs9BT44pL7hS1OkRGKYRXI/SN9KejUlPD+BY40DguRLzdC5tLG/28WGMyfKdn/51GT4a0p+0P8xvDn1Ez+Kg==}
+    dependencies:
+      '@probe.gl/env': 4.1.1
+    dev: false
+
+  /@probe.gl/stats@4.1.1:
+    resolution: {integrity: sha512-4VpAyMHOqydSvPlEyHwXaE+AkIdR03nX+Qhlxsk2D/IW4OVmDZgIsvJB1cDzyEEtcfKcnaEbfXeiPgejBceT6g==}
+    dev: false
+
   /@purge-icons/core@0.10.0:
     resolution: {integrity: sha512-AtJbZv5Yy+vWX5v32DPTr+CW7AkSK8HJx52orDbrYt/9s4lGM2t4KKAmwaTQEH2HYr2HVh1mlqs54/S1s3WT1g==}
     dependencies:
@@ -3671,6 +3935,12 @@ packages:
       '@types/node': 20.19.25
     dev: true
 
+  /@types/geojson-vt@3.2.5:
+    resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==}
+    dependencies:
+      '@types/geojson': 7946.0.16
+    dev: false
+
   /@types/geojson@7946.0.16:
     resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
     dev: false
@@ -3731,6 +4001,18 @@ packages:
     resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==}
     dev: true
 
+  /@types/mapbox__point-geometry@0.1.4:
+    resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==}
+    dev: false
+
+  /@types/mapbox__vector-tile@1.3.4:
+    resolution: {integrity: sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==}
+    dependencies:
+      '@types/geojson': 7946.0.16
+      '@types/mapbox__point-geometry': 0.1.4
+      '@types/pbf': 3.0.5
+    dev: false
+
   /@types/minimist@1.2.5:
     resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
     dev: true
@@ -3749,6 +4031,12 @@ packages:
       undici-types: 6.21.0
     dev: true
 
+  /@types/node@25.9.5:
+    resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==}
+    dependencies:
+      undici-types: 7.24.6
+    dev: false
+
   /@types/normalize-package-data@2.4.4:
     resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
     dev: true
@@ -3757,6 +4045,14 @@ packages:
     resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==}
     dev: true
 
+  /@types/offscreencanvas@2019.7.3:
+    resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==}
+    dev: false
+
+  /@types/pbf@3.0.5:
+    resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==}
+    dev: false
+
   /@types/qrcode@1.5.6:
     resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
     dependencies:
@@ -3783,6 +4079,12 @@ packages:
     resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
     dev: true
 
+  /@types/supercluster@7.1.3:
+    resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
+    dependencies:
+      '@types/geojson': 7946.0.16
+    dev: false
+
   /@types/svgo@2.6.4:
     resolution: {integrity: sha512-l4cmyPEckf8moNYHdJ+4wkHvFxjyW6ulm9l4YGaOxeyBWPhBOT0gvni1InpFPdzx1dKf/2s62qGITwxNWnPQng==}
     dependencies:
@@ -4634,6 +4936,16 @@ packages:
       picomatch: 2.3.2
     dev: true
 
+  /apache-arrow@21.2.0:
+    resolution: {integrity: sha512-Hxe6Agq26gQOM954qpzYSllJBPJl+e16U5CkfuMUhLrNba+5nKkttIVlflaovN6oaTratqMGAO8H5u/aNhmHWQ==}
+    hasBin: true
+    dependencies:
+      '@types/node': 25.9.5
+      flatbuffers: 25.9.23
+      json-with-bigint: 3.5.10
+      tslib: 2.8.1
+    dev: false
+
   /arg@4.1.3:
     resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
     dev: true
@@ -6278,6 +6590,14 @@ packages:
     resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
     dev: true
 
+  /earcut@2.2.4:
+    resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
+    dev: false
+
+  /earcut@3.2.3:
+    resolution: {integrity: sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==}
+    dev: false
+
   /eastasianwidth@0.2.0:
     resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
     dev: true
@@ -7327,6 +7647,10 @@ packages:
       hookified: 1.15.1
     dev: true
 
+  /flatbuffers@25.9.23:
+    resolution: {integrity: sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==}
+    dev: false
+
   /flatted@3.4.3:
     resolution: {integrity: sha512-/zipXxyO6rGvuNGDiULY9MvEGSkb2gaG4GGH4ygMi0ZZzyMHdUZBmntJmx5x1G2VuPytCwGN4xsJP6cw+sK+vQ==}
     dev: true
@@ -7460,6 +7784,10 @@ packages:
     engines: {node: '>=6.9.0'}
     dev: true
 
+  /geojson-vt@4.0.3:
+    resolution: {integrity: sha512-jR1MwkLaZGa8Zftct9ZFruyWFrdl9ZyD2OliXNy9Qq5bBPeg5wHVpBQF9p5GjnicSDQqvBVpysxTPKmWdsfWMA==}
+    dev: false
+
   /get-caller-file@2.0.5:
     resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
     engines: {node: 6.* || 8.* || >= 10.*}
@@ -7503,7 +7831,6 @@ packages:
   /get-stream@6.0.1:
     resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
     engines: {node: '>=10'}
-    dev: true
 
   /get-stream@8.0.1:
     resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==}
@@ -7558,6 +7885,10 @@ packages:
       semver: 7.8.5
     dev: true
 
+  /gl-matrix@3.4.4:
+    resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==}
+    dev: false
+
   /glob-parent@5.1.2:
     resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
     engines: {node: '>= 6'}
@@ -7662,6 +7993,15 @@ packages:
       which: 1.3.1
     dev: true
 
+  /global-prefix@4.0.0:
+    resolution: {integrity: sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==}
+    engines: {node: '>=16'}
+    dependencies:
+      ini: 4.1.3
+      kind-of: 6.0.3
+      which: 4.0.0
+    dev: false
+
   /globals@13.24.0:
     resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==}
     engines: {node: '>=8'}
@@ -7998,7 +8338,6 @@ packages:
 
   /ieee754@1.2.1:
     resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
-    dev: true
 
   /ignore@5.3.2:
     resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
@@ -8059,6 +8398,11 @@ packages:
     resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
     dev: true
 
+  /ini@4.1.3:
+    resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==}
+    engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
+    dev: false
+
   /ini@6.0.0:
     resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==}
     engines: {node: ^20.17.0 || >=22.9.0}
@@ -8512,6 +8856,11 @@ packages:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
     dev: true
 
+  /isexe@3.1.5:
+    resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==}
+    engines: {node: '>=18'}
+    dev: false
+
   /isobject@2.1.0:
     resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
     engines: {node: '>=0.10.0'}
@@ -9107,10 +9456,18 @@ packages:
     resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
     dev: true
 
+  /json-stringify-pretty-compact@4.0.0:
+    resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==}
+    dev: false
+
   /json-stringify-safe@5.0.1:
     resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
     dev: true
 
+  /json-with-bigint@3.5.10:
+    resolution: {integrity: sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==}
+    dev: false
+
   /json5@1.0.2:
     resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
     hasBin: true
@@ -9142,6 +9499,10 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /kdbush@4.1.0:
+    resolution: {integrity: sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==}
+    dev: false
+
   /keyv@4.5.4:
     resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
     dependencies:
@@ -9176,7 +9537,6 @@ packages:
   /kind-of@6.0.3:
     resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
     engines: {node: '>=0.10.0'}
-    dev: true
 
   /kleur@3.0.3:
     resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
@@ -9501,6 +9861,38 @@ packages:
       object-visit: 1.0.1
     dev: true
 
+  /maplibre-gl@4.7.1:
+    resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==}
+    engines: {node: '>=16.14.0', npm: '>=8.1.0'}
+    dependencies:
+      '@mapbox/geojson-rewind': 0.5.2
+      '@mapbox/jsonlint-lines-primitives': 2.0.3
+      '@mapbox/point-geometry': 0.1.0
+      '@mapbox/tiny-sdf': 2.2.0
+      '@mapbox/unitbezier': 0.0.1
+      '@mapbox/vector-tile': 1.3.1
+      '@mapbox/whoots-js': 3.1.0
+      '@maplibre/maplibre-gl-style-spec': 20.4.0
+      '@types/geojson': 7946.0.16
+      '@types/geojson-vt': 3.2.5
+      '@types/mapbox__point-geometry': 0.1.4
+      '@types/mapbox__vector-tile': 1.3.4
+      '@types/pbf': 3.0.5
+      '@types/supercluster': 7.1.3
+      earcut: 3.2.3
+      geojson-vt: 4.0.3
+      gl-matrix: 3.4.4
+      global-prefix: 4.0.0
+      kdbush: 4.1.0
+      murmurhash-js: 1.0.0
+      pbf: 3.3.0
+      potpack: 2.1.0
+      quickselect: 3.0.0
+      supercluster: 8.0.1
+      tinyqueue: 3.0.0
+      vt-pbf: 3.1.3
+    dev: false
+
   /math-intrinsics@1.1.0:
     resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
     engines: {node: '>= 0.4'}
@@ -9707,7 +10099,6 @@ packages:
 
   /minimist@1.2.8:
     resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
-    dev: true
 
   /minipass@7.1.3:
     resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
@@ -9722,6 +10113,10 @@ packages:
       is-extendable: 1.0.1
     dev: true
 
+  /mjolnir.js@3.0.1:
+    resolution: {integrity: sha512-/RMi8Jm3NKleOkVI8D2ai+1OVwtfRJsSVBVjbTXNm83nfsN4uORYaN3u1/hsg5CqVI+di8enTkvgDNDOywn6cQ==}
+    dev: false
+
   /mlly@1.8.2:
     resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
     dependencies:
@@ -9793,6 +10188,10 @@ packages:
     resolution: {integrity: sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==}
     dev: true
 
+  /murmurhash-js@1.0.0:
+    resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==}
+    dev: false
+
   /mute-stream@0.0.8:
     resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
     dev: true
@@ -10327,6 +10726,14 @@ packages:
     resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
     dev: true
 
+  /pbf@3.3.0:
+    resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==}
+    hasBin: true
+    dependencies:
+      ieee754: 1.2.1
+      resolve-protobuf-schema: 2.1.0
+    dev: false
+
   /perfect-debounce@1.0.0:
     resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
     dev: true
@@ -10576,6 +10983,10 @@ packages:
       posthtml-render: 1.4.0
     dev: true
 
+  /potpack@2.1.0:
+    resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==}
+    dev: false
+
   /preact@10.29.7:
     resolution: {integrity: sha512-DCHYrK/B10yUD3ZjLfhZ3WIE/9Vf9VFUODcRE2dRomTYDpJk6z6L9wecSfhfE6M9ZTHUdyQkoC46arIDhEV84Q==}
     peerDependencies:
@@ -10655,6 +11066,10 @@ packages:
     resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
     dev: true
 
+  /protocol-buffers-schema@3.6.1:
+    resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==}
+    dev: false
+
   /proxy-from-env@1.1.0:
     resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
 
@@ -10716,6 +11131,14 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /quickselect@2.0.0:
+    resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==}
+    dev: false
+
+  /quickselect@3.0.0:
+    resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
+    dev: false
+
   /rangy@1.3.2:
     resolution: {integrity: sha512-fS1C4MOyk8T+ZJZdLcgrukPWxkyDXa+Hd2Kj+Zg4wIK71yrWgmjzHubzPMY1G+WD9EgGxMp3fIL0zQ1ickmSWA==}
     dev: false
@@ -10931,6 +11354,12 @@ packages:
       global-dirs: 0.1.1
     dev: true
 
+  /resolve-protobuf-schema@2.1.0:
+    resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==}
+    dependencies:
+      protocol-buffers-schema: 3.6.1
+    dev: false
+
   /resolve-url@0.2.1:
     resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==}
     deprecated: https://github.com/lydell/resolve-url#deprecated
@@ -11111,6 +11540,10 @@ packages:
       queue-microtask: 1.2.3
     dev: true
 
+  /rw@1.3.3:
+    resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
+    dev: false
+
   /rxjs@7.8.2:
     resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
     dependencies:
@@ -11847,6 +12280,12 @@ packages:
     resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==}
     dev: false
 
+  /supercluster@8.0.1:
+    resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==}
+    dependencies:
+      kdbush: 4.1.0
+    dev: false
+
   /supports-color@2.0.0:
     resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
     engines: {node: '>=0.8.0'}
@@ -12045,6 +12484,10 @@ packages:
       picomatch: 4.0.5
     dev: true
 
+  /tinyqueue@3.0.0:
+    resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==}
+    dev: false
+
   /tmp@0.0.33:
     resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
     engines: {node: '>=0.6.0'}
@@ -12202,7 +12645,6 @@ packages:
 
   /tslib@2.8.1:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
-    dev: true
 
   /tsutils@3.21.0(typescript@5.9.3):
     resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
@@ -12374,6 +12816,10 @@ packages:
     resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
     dev: true
 
+  /undici-types@7.24.6:
+    resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
+    dev: false
+
   /unicode-canonical-property-names-ecmascript@2.0.1:
     resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==}
     engines: {node: '>=4'}
@@ -12778,6 +13224,14 @@ packages:
       '@types/geojson': 7946.0.16
     dev: false
 
+  /vt-pbf@3.1.3:
+    resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==}
+    dependencies:
+      '@mapbox/point-geometry': 0.1.0
+      '@mapbox/vector-tile': 1.3.1
+      pbf: 3.3.0
+    dev: false
+
   /vue-component-type-helpers@2.2.12:
     resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
     dev: true
@@ -13040,6 +13494,14 @@ packages:
       isexe: 2.0.0
     dev: true
 
+  /which@4.0.0:
+    resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==}
+    engines: {node: ^16.13.0 || >=18.0.0}
+    hasBin: true
+    dependencies:
+      isexe: 3.1.5
+    dev: false
+
   /wmf@1.0.2:
     resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==}
     engines: {node: '>=0.8'}
@@ -13402,3 +13864,24 @@ packages:
     dependencies:
       tslib: 2.3.0
     dev: false
+
+  file:../map-cad(@loaders.gl/core@4.4.4)(@luma.gl/core@9.3.6)(@luma.gl/engine@9.3.6)(vue@3.5.25):
+    resolution: {directory: ../map-cad, type: directory}
+    id: file:../map-cad
+    name: map-cad-viewer
+    version: 0.1.0
+    peerDependencies:
+      vue: ^3.5.13
+    dependencies:
+      '@deck.gl/core': 9.3.7
+      '@deck.gl/extensions': 9.3.7(@deck.gl/core@9.3.7)(@luma.gl/core@9.3.6)(@luma.gl/engine@9.3.6)
+      '@deck.gl/layers': 9.3.7(@deck.gl/core@9.3.7)(@loaders.gl/core@4.4.4)(@luma.gl/core@9.3.6)(@luma.gl/engine@9.3.6)
+      '@deck.gl/mapbox': 9.3.7(@deck.gl/core@9.3.7)(@luma.gl/core@9.3.6)(@math.gl/web-mercator@4.1.0)
+      '@math.gl/web-mercator': 4.1.0
+      maplibre-gl: 4.7.1
+      vue: 3.5.25(typescript@5.9.3)
+    transitivePeerDependencies:
+      - '@loaders.gl/core'
+      - '@luma.gl/core'
+      - '@luma.gl/engine'
+    dev: false

+ 12 - 0
public/js/global.js

@@ -340,6 +340,18 @@ const __STATIC_ROUTES__ = [
         },
         name: 'warningAnalysis-wind-point-manage',
       },
+      {
+        path: '/warningAnalysis/cadFile',
+        component: '/system/cadFile/index',
+        meta: {
+          keepAlive: true,
+          internalOrExternal: false,
+          icon: '',
+          componentName: 'index',
+          title: '地图布点管理',
+        },
+        name: 'warningAnalysis-cadFile',
+      },
     ],
     meta: {
       keepAlive: false,

+ 15 - 11
src/api/sys/map.ts

@@ -5,7 +5,7 @@ import { StatusColorEnum } from '/@/enums/jeecgEnum';
 // import { findNode } from '/@/utils/helper/treeHelper';
 
 enum Api {
-  list = '/province/mineData/getMineData',
+  list = '/workingface/mineData/getMineData',
   getGoafAlarmLevel = '/province/homeData/getGoafAlarmLevel',
 }
 /**
@@ -51,18 +51,19 @@ export function getMarkerCollection(params: any) {
   //   };
   //   return get(colorMap, gb.slice(0, -2), StatusColorEnum.blue);
   // };
-  const getAlarmColor = (lv) => {
+  const getJudgeColor = (lv) => {
     const colorMap = {
-      4: StatusColorEnum.red,
-      3: StatusColorEnum.gold,
-      2: StatusColorEnum.yellow,
-      1: StatusColorEnum.blue,
-      0: StatusColorEnum.blue,
+      1: StatusColorEnum.red,
+      2: StatusColorEnum.green,
+      0: StatusColorEnum.gray,
     };
 
-    return get(colorMap, lv, StatusColorEnum.blue);
+    return get(colorMap, lv, StatusColorEnum.gray);
   };
 
+  // judgeResult 风险优先聚合:疑似(1) > 正常(2) > 数据不足(0)
+  const mergeJudgeResult = (a, b) => (a === 1 || b === 1 ? 1 : a === 2 || b === 2 ? 2 : 0);
+
   const positionPresets = new Map([
     [
       '陕西局',
@@ -172,9 +173,10 @@ export function getMarkerCollection(params: any) {
         element.isLeaf = true;
         element.dynamic = false;
         element.alarmLevel = parseInt(element.alarmLevel) || 0;
+        element.judgeResult = parseInt(element.judgeResult) || 0;
         element.longitude = parseFloat(element.longitude);
         element.latitude = parseFloat(element.latitude);
-        element.fillColor = getAlarmColor(element.alarmLevel);
+        element.fillColor = getJudgeColor(element.judgeResult);
         // 地图缩放等级
         element.zoom = 8;
 
@@ -185,7 +187,8 @@ export function getMarkerCollection(params: any) {
         const node = map.get(element.parentId);
         node.count += element.count;
         node.alarmLevel = Math.max(node.alarmLevel, element.alarmLevel);
-        node.fillColor = getAlarmColor(node.alarmLevel);
+        node.judgeResult = mergeJudgeResult(node.judgeResult, element.judgeResult);
+        node.fillColor = getJudgeColor(node.judgeResult);
         continue;
       }
 
@@ -201,9 +204,10 @@ export function getMarkerCollection(params: any) {
         isLeaf: false,
         dynamic: true,
         alarmLevel: element.alarmLevel,
+        judgeResult: element.judgeResult,
         longitude: positionPresets.get(ref.departName)?.y || element.longitude,
         latitude: positionPresets.get(ref.departName)?.x || element.latitude,
-        fillColor: getAlarmColor(element.alarmLevel),
+        fillColor: getJudgeColor(element.judgeResult),
         zoom: 5 + parseInt(ref.orgType),
       };
       map.set(element.parentId, node);

+ 16 - 0
src/api/workingface/index.ts

@@ -0,0 +1,16 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  /** 获取当前层级统计(全省监测总体情况 / 矿井监测总览 / 各监测页顶部统计卡片) */
+  getCurrentStatistics = '/workingface/mineData/getCurrentStatistics',
+  /** 获取煤矿数量、判识数量统计(执法处判识情况) */
+  getMineJudgeCount = '/workingface/mineData/getMineJudgeCount',
+}
+
+/** 当前层级统计(GET,deptId 为空时取当前登录用户部门) */
+export const getCurrentStatistics = (params?: any) =>
+  defHttp.get({ url: Api.getCurrentStatistics, params }, { joinParamsToUrl: true });
+
+/** 执法处判识情况列表(GET,deptId 为空时取当前登录用户部门) */
+export const getMineJudgeCount = (params?: any) =>
+  defHttp.get({ url: Api.getMineJudgeCount, params }, { joinParamsToUrl: true });

+ 1 - 1
src/enums/pageEnum.ts

@@ -4,7 +4,7 @@ export enum PageEnum {
   // basic home path
   BASE_HOME = '/dashboard/hiddenSurface',
   // secondary home path
-  SECONDARY_HOME = '/basicinfo/overhaul',
+  SECONDARY_HOME = '/basicinfo/mines-info',
   // tertiary home path
   TERTIARY_HOME = '/system/menu',
   // quaternary home path

+ 3 - 2
src/layouts/default/feature/SimpleMap.vue

@@ -44,7 +44,7 @@
   // import { env } from '/@/views/system/cadFile/env';
   import { StatusColorEnum } from '/@/enums/jeecgEnum';
   import { generateSimplePopup } from './hooks/popup';
-  import { getGoafList, getMineFileList } from '/@/views/system/cadFile/cad.api';
+  import { getMineAreaList, getMineFileList } from '/@/views/system/cadFile/cad.api';
   import LeafPopup from './components/LeafPopup.vue';
   import { watchTriggerable } from '@vueuse/core';
   import { findPath, listToTree } from '/@/utils/helper/treeHelper';
@@ -223,6 +223,7 @@
     // 待到用户确实点击了详情框标题后在推一项历史可保证历史栈的正确
     // historyStack.value.pop();
 
+    // 地图标记点数据(getMarkerCollection 已携带完整矿井记录),弹窗直接展示节点数据,不额外请求接口
     const popup = new vjmap.Popup({ maxWidth: '1000' });
     const app = createApp(LeafPopup, {
       node,
@@ -281,7 +282,7 @@
       if (cadOpened.value) {
         const [__, res] = await Promise.all([
           initMap2d(mapContainer.value!, { wjId: fileId.value, style: { backcolor: 0xe6f3ff } }),
-          props.silence ? Promise.resolve([]) : getGoafList({ deptId: mineStore.getDepartId }),
+          props.silence ? Promise.resolve([]) : getMineAreaList({ deptId: mineStore.getDepartId }),
         ]);
 
         renderGoafMarkers(res); // 渲染密闭标记

+ 506 - 0
src/layouts/default/feature/SimpleMapTest.bak.txt

@@ -0,0 +1,506 @@
+<template>
+  <div class="myapp map-container">
+    <MapCadViewer
+      ref="viewerRef"
+      :map-config="mapConfig"
+      :boundaries="boundaryConfig"
+      :markers="markers"
+      :start-in-cad-mode="START_IN_CAD_MODE"
+      @mode-change="onModeChange"
+      @feature-select="onFeatureSelect"
+      @marker-click="onMarkerClick"
+      @marker-open-cad="onMarkerOpenCad"
+      @icon-placed="onIconPlaced"
+      @icon-moved="onIconMoved"
+    />
+
+    <!-- ============ CAD 模式外部工具栏 ============ -->
+    <template v-if="isCadMode">
+      <div class="cad-toolbar">
+        <button class="cad-btn" @click="goBackToMap" title="返回地图">← 返回地图</button>
+        <button class="cad-btn" @click="toggleLayerPanel" title="图层管理">📑 图层</button>
+        <button class="cad-btn" :class="{ active: currentMode === 'drawing' }" @click="toggleDrawing" title="绘制模式">✏️ 绘制</button>
+        <button class="cad-btn" @click="zoomFit" title="缩放至全图">⊞ 全图</button>
+      </div>
+
+      <!-- 绘制模式工具栏 -->
+      <div v-if="currentMode === 'drawing'" class="drawing-toolbar" style="display: none">
+        <span class="drawing-label">点击地图放置图标</span>
+        <div class="drawing-icons">
+          <button
+            v-for="item in svgIconsStatic"
+            :key="item.type"
+            :class="item.type"
+            class="icon-btn"
+            @click="selectIcon(item)"
+            :title="item.name"
+          ></button>
+        </div>
+        <button class="cad-btn" @click="clearIcons">清除</button>
+      </div>
+    </template>
+
+    <template v-else>
+      <div class="cad-toolbar">
+        <button class="cad-btn" @click="changeView" title="切换城市">切换城市</button>
+        <button class="cad-btn" @click="setMapView([113.7, 37.8], 8, 1000)" title="切换中心点">切换中心点</button>
+      </div>
+    </template> </div
+  ><div v-if="!isTopLevel" class="map-reset-btn">
+    <Button :loading="mapLoading" type="primary" @click="revertStack">返回上级</Button>
+  </div>
+  <div v-if="cadOpened" class="map-cad-btn">
+    <a-select
+      v-model:value="fileId"
+      :options="fileOptions"
+      :fieldNames="{ label: 'fileName', value: 'wjId' }"
+      :style="{ minWidth: '200px' }"
+      @change="toggleCADMap(true)"
+    ></a-select>
+  </div>
+  <!-- 省域地图监测区图例(图3-1) -->
+  <div v-if="!cadOpened" class="map-legend">
+    <div class="legend-title">图例</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-red"></span>疑似隐蔽工作面</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-orange"></span>风量异常</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-yellow"></span>数据掉线</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-green"></span>监测正常</div>
+  </div>
+  <!-- 矿端通风系统图监测区图例(图3-3) -->
+  <div v-if="cadOpened" class="map-legend">
+    <div class="legend-title">图例</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-green"></span>测风正常</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-orange"></span>风量异常</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-red-dashed"></span>缺失测风点</div>
+    <div class="legend-row"><span class="legend-line-red"></span>疑似隐蔽巷道</div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, onUnmounted, computed, createApp } from 'vue';
+  // 本地调试组件时使用
+  import 'map-cad-viewer/dist/map-cad-viewer.css';
+  import { MapCadViewer } from 'map-cad-viewer';
+  import type { CadData, MapConfig, BoundaryConfig, MapMarker } from 'map-cad-viewer/dist/types/index.d';
+  // 直接加载打包好的组件
+  // import { MapCadViewer } from '/@/components/map/map-cad-viewer.es';
+  // import '/@/components/map/map-cad-viewer.css';
+  // import type { CadData, MapConfig, BoundaryConfig, MapMarker } from '/@/components/map/types/index.d';
+
+  import { Button } from 'ant-design-vue';
+  import { useAppStore } from '/@/store/modules/app';
+
+  import { useMineDepartmentStore } from '/@/store/modules/mine';
+  import { get, last } from 'lodash-es';
+
+  const props = defineProps<{
+    silence: boolean;
+  }>();
+
+  const appStore = useAppStore();
+  const mineStore = useMineDepartmentStore();
+  const mapContainer = ref<HTMLElement>();
+  const mapLoading = ref(false);
+
+  // CAD是否可见
+  const cadOpened = ref<boolean>();
+
+  const DEFAULT_NODE = {
+    id: mineStore.getRootId!,
+    name: '根节点',
+    parentId: null,
+    longitude: 109.425873,
+    latitude: 35.83317,
+    zoom: 6,
+    isLeaf: false,
+  };
+
+  // 用于存储历史节点的栈,实现返回上一级功能
+  const historyStack = ref([DEFAULT_NODE]);
+  const isTopLevel = computed(() => get(historyStack.value, 'length', 1) === 1);
+  const fileOptions = ref<any[]>([]);
+  const fileId = ref();
+
+  // 设置地图中当前的参数以供其他页面使用
+  appStore.setSimpleMapParams({ deptId: mineStore.getRootId, isLeaf: mineStore.getRoot?.isLeaf });
+
+  const viewerRef = ref<InstanceType<typeof MapCadViewer>>();
+  // _dwg_req_newcal@
+
+  // ===== 启动配置 =====
+  /** 启动后默认进入的视图:false=地图页面, true=CAD 图纸页面 */
+  const START_IN_CAD_MODE = false;
+
+  /** 地图配置 */
+  const mapConfig: Partial<MapConfig> = {
+    center: [114.0, 38.0],
+    zoom: 5,
+    tileUrl: 'http://182.92.126.35:9999/sys/common/static/map/{z}/{x}/{y}.png',
+  };
+
+  const boundaryConfig: BoundaryConfig = {
+    provinces: ['山东省', '山西省'],
+    cities: [],
+    counties: [],
+    fillColor: [129, 230, 246, 30],
+    strokeColor: [59, 130, 246, 160],
+    regionColors: {
+      石家庄市: { fillColor: [50, 280, 50, 30], strokeColor: [50, 280, 50, 200] },
+      太原市: { fillColor: [140, 20, 112, 30], strokeColor: [140, 20, 112, 180] },
+      保定市: { fillColor: [140, 120, 12, 30], strokeColor: [140, 120, 12, 180] },
+    },
+  };
+
+  const markers: MapMarker[] = [
+    { coords: [113.2, 37.8], label: '4', color: [255, 60, 60, 250], fontColor: [255, 255, 255, 255], borderColor: [0, 0, 0, 255] },
+    {
+      coords: [112.7, 38.6],
+      label: '',
+      color: [60, 200, 60, 250],
+      fontColor: [255, 255, 255, 255],
+      borderColor: [0, 0, 0, 255],
+      switchToCad: true,
+      popup: {
+        title: '煤矿 #1',
+        items: [
+          { subtitle: '矿井名称', content: '一号煤矿' },
+          { subtitle: '设计产能', content: '120 万吨/年' },
+          { subtitle: '开采深度', content: '850 m' },
+          { subtitle: '通风方式', content: '中央并列式' },
+        ],
+      },
+    },
+  ];
+
+  const cadData = ref<CadData | undefined>();
+  const currentIconId = ref('speed');
+
+  /**
+   * App.vue — 应用主入口
+   *
+   * 集成 MapCadViewer 组件,提供:
+   * - 地图配置(默认使用内网瓦片服务)
+   * - 行政区划边界(高亮省市区域)
+   * - 标注点及 CAD 模式切换
+   * - 工具栏:CAD 绘制模式(风门/传感器/密闭/测风装置)
+   *
+   * 数据流:
+   * 1. 页面加载 → 获取 /features.json → 传给 MapCadViewer
+   * 2. 用户点击标注点 → switchToCadMode(marker, cadData)
+   * 3. 用户点击工具栏图标 → toggleDrawingMode / clearPlacedIcons
+   */
+
+  // 内置的 4 种绘制图标定义(用于工具栏按钮的 SVG 预览和内联 HTML)
+  const svgIconsStatic = [
+    {
+      type: 'gate',
+      name: '风门',
+      iconUrl: '/icons/gate.svg',
+      svg: "<svg width='20' height='20' viewBox='0 0 20 20'><rect x='4' y='2' width='12' height='16' rx='2' fill='none' stroke='#4dc9f6' stroke-width='1.5'/><line x1='7' y1='6' x2='13' y2='6' stroke='#4dc9f6' stroke-width='1.5'/><line x1='7' y1='10' x2='13' y2='10' stroke='#4dc9f6' stroke-width='1.5'/><line x1='7' y1='14' x2='13' y2='14' stroke='#4dc9f6' stroke-width='1.5'/></svg>",
+    },
+    {
+      type: 'modelsensor',
+      name: '传感器',
+      iconUrl: '/icons/modelsensor.svg',
+      svg: "<svg width='20' height='20' viewBox='0 0 20 20'><circle cx='10' cy='10' r='6' fill='none' stroke='#44ff88' stroke-width='1.5'/><circle cx='10' cy='10' r='2' fill='#44ff88'/></svg>",
+    },
+    {
+      type: 'obfurage',
+      name: '密闭',
+      iconUrl: '/icons/obfurage.svg',
+      svg: "<svg width='20' height='20' viewBox='0 0 20 20'><rect x='4' y='3' width='12' height='14' fill='none' stroke='#ff6644' stroke-width='1.5'/><line x1='4' y1='3' x2='16' y2='17' stroke='#ff6644' stroke-width='1.5'/></svg>",
+    },
+    {
+      type: 'speed',
+      name: '测风装置',
+      iconUrl: '/icons/speed.svg',
+      svg: "<svg width='20' height='20' viewBox='0 0 20 20'><polygon points='10,2 18,18 2,18' fill='none' stroke='#ffaa00' stroke-width='1.5'/><line x1='10' y1='2' x2='10' y2='12' stroke='#ffaa00' stroke-width='1.5'/><line x1='6' y1='14' x2='14' y2='14' stroke='#ffaa00' stroke-width='1.5'/></svg>",
+    },
+  ];
+
+  function onModeChange(mode: string) {
+    console.log('Viewer mode changed:', mode);
+    currentMode.value = mode as any;
+  }
+
+  const currentMode = ref<'map' | 'cad' | 'drawing'>('map');
+  const isCadMode = computed(() => currentMode.value === 'cad' || currentMode.value === 'drawing');
+
+  function onFeatureSelect(feature: any) {
+    console.log('Selected feature:', feature.id, feature.typename, 'layer:', feature.layer);
+  }
+
+  function onMarkerClick(marker: any) {
+    console.log('Marker clicked:', marker.label, 'coords:', marker.coords, 'switchToCad:', marker.switchToCad, 'popup:', marker.popup);
+  }
+
+  function onMarkerOpenCad(marker: any) {
+    console.log('Marker title clicked, opening CAD:', marker.label);
+    if (cadData.value) {
+      viewerRef.value?.switchToCadMode(marker, cadData.value);
+    }
+  }
+  // 切换当前选择的省,市,和描点,重新绘制蒙版和标识
+  function changeView() {
+    const boundary: BoundaryConfig = {
+      provinces: [],
+      cities: ['太原市', '晋中市', '长治市'],
+      counties: [],
+      fillColor: [129, 230, 246, 30],
+      strokeColor: [59, 130, 246, 160],
+      regionColors: {
+        太原市: { fillColor: [50, 280, 50, 30], strokeColor: [50, 280, 50, 200] },
+        晋中市: { fillColor: [140, 20, 112, 30], strokeColor: [140, 20, 112, 180] },
+      },
+    };
+
+    const newMarkers: MapMarker[] = [
+      { coords: [112.7, 37.8], label: '2', color: [255, 60, 60, 250], fontColor: [255, 255, 255, 255], borderColor: [0, 0, 0, 255] },
+      {
+        coords: [112.2, 38.2],
+        label: '',
+        color: [60, 200, 60, 250],
+        fontColor: [255, 255, 255, 255],
+        borderColor: [0, 0, 0, 255],
+        switchToCad: true,
+        popup: {
+          title: '太原市',
+          items: [
+            { subtitle: '矿井名称', content: '一号煤矿' },
+            { subtitle: '设计产能', content: '120 万吨/年' },
+          ],
+        },
+      },
+    ];
+    viewerRef.value?.changeView(boundary, newMarkers);
+    setMapView([112.7, 37.8], 7.2, 1000);
+  }
+  /** 切换地图中心点和缩放级别,仅在地图模式下生效 */
+  // 飞到位置,zoom 14,动画 1 秒
+  //setMapView([113.7, 38.6], 14, 1000)
+  function setMapView(center: [number, number], zoom: number, duration: number = 500) {
+    viewerRef.value?.setMapView(center, zoom, duration);
+  }
+  // CAD 控制方法
+  function goBackToMap() {
+    viewerRef.value?.goBackToMap();
+  }
+  function toggleLayerPanel() {
+    viewerRef.value?.toggleLayerPanel();
+  }
+  function toggleDrawing() {
+    const item = svgIconsStatic.find((i) => i.type === currentIconId.value);
+    if (!item) return;
+    viewerRef.value?.toggleDrawingMode({
+      type: item.type,
+      iconUrl: item.iconUrl,
+      name: item.name,
+    });
+  }
+  function zoomFit() {
+    viewerRef.value?.zoomToFit();
+  }
+  function selectIcon(item: { type: string; name: string; iconUrl: string }) {
+    currentIconId.value = item.type;
+    const tool = { type: item.type, iconUrl: item.iconUrl, name: item.name };
+    if (currentMode.value === 'drawing' && viewerRef.value) {
+      viewerRef.value.setDrawingTool(tool);
+    } else {
+      viewerRef.value?.setSelectedIcon(item.type);
+    }
+  }
+  function clearIcons() {
+    viewerRef.value?.clearPlacedIcons();
+  }
+
+  function onIconPlaced(icon: any) {
+    console.log('Icon placed:', icon);
+  }
+
+  function onIconMoved(icon: any) {
+    console.log('Icon moved:', icon);
+  }
+
+  onMounted(async () => {
+    try {
+      const resp = await fetch('/hll.json');
+      const data: CadData = await resp.json();
+      cadData.value = data;
+      console.log(`Loaded ${data.features.length} CAD features`);
+
+      // 根据配置选择初始视图
+      if (START_IN_CAD_MODE && viewerRef.value) {
+        // 不飞向标注点,直接进入 CAD 全图视图
+        viewerRef.value.switchToCadMode(undefined, data);
+      }
+    } catch (e) {
+      console.warn('Could not load hll.json (expected in dev)', e);
+    }
+  });
+</script>
+
+<style lang="less">
+  .map-container {
+    flex: 1;
+    height: 100%;
+    width: 100%;
+    position: fixed;
+    top: 0;
+    left: 0;
+    z-index: @simple-map-z-index;
+    // filter: grayscale(80%) invert(0%) sepia(10%) hue-rotate(150deg) saturate(200%) brightness(100%) contrast(100%);
+
+    canvas {
+      /* width: 1920px !important;
+      height: 948px !important; */
+    }
+  }
+
+  /* 图例浮层 */
+  .map-legend {
+    position: fixed;
+    bottom: 24px;
+    left: 24px;
+    z-index: @simple-map-z-index + 1;
+    background: rgba(255, 255, 255, 0.92);
+    border-radius: 8px;
+    padding: 10px 14px;
+    font-size: 12px;
+    line-height: 1.8;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    color: #333;
+    min-width: 140px;
+    pointer-events: none;
+
+    .legend-title {
+      font-weight: 600;
+      margin-bottom: 4px;
+      font-size: 13px;
+    }
+    .legend-row {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+    .legend-dot {
+      display: inline-block;
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      flex-shrink: 0;
+    }
+    .legend-dot-green {
+      background: #52c41a;
+    }
+    .legend-dot-orange {
+      background: #fa8c16;
+    }
+    .legend-dot-yellow {
+      background: #fadb14;
+    }
+    .legend-dot-red {
+      background: #f5222d;
+    }
+    .legend-dot-red-dashed {
+      background: transparent;
+      border: 2px dashed #f5222d;
+    }
+    .legend-line-red {
+      display: inline-block;
+      width: 14px;
+      height: 3px;
+      background: #f5222d;
+      flex-shrink: 0;
+    }
+  }
+</style>
+<style>
+  /* CAD 工具栏 — 由 App.vue 控制界面 */
+  .cad-toolbar {
+    position: absolute;
+    top: 12px;
+    left: 50%;
+    transform: translateX(-50%);
+    z-index: 400;
+    display: flex;
+    gap: 6px;
+    background: rgba(20, 20, 30, 0.9);
+    border: 1px solid rgba(255, 255, 255, 0.15);
+    border-radius: 8px;
+    padding: 6px 10px;
+    backdrop-filter: blur(8px);
+  }
+  .cad-btn {
+    background: rgba(255, 255, 255, 0.08);
+    border: 1px solid rgba(255, 255, 255, 0.12);
+    color: #ccc;
+    padding: 6px 12px;
+    border-radius: 6px;
+    font-size: 12px;
+    cursor: pointer;
+    transition: all 0.15s;
+    white-space: nowrap;
+  }
+  .cad-btn:hover {
+    background: rgba(255, 255, 255, 0.15);
+    color: #fff;
+  }
+  .cad-btn.active {
+    background: rgba(59, 130, 246, 0.4);
+    border-color: rgba(59, 130, 246, 0.6);
+    color: #fff;
+  }
+
+  .drawing-toolbar {
+    position: absolute;
+    bottom: 24px;
+    left: 50%;
+    transform: translateX(-50%);
+    z-index: 400;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    background: rgba(20, 20, 30, 0.92);
+    border: 1px solid rgba(255, 255, 255, 0.15);
+    border-radius: 8px;
+    padding: 8px 14px;
+    backdrop-filter: blur(8px);
+  }
+  .drawing-label {
+    color: #aaa;
+    font-size: 12px;
+  }
+  .drawing-icons {
+    display: flex;
+    gap: 4px;
+  }
+  .icon-btn {
+    background: rgba(255, 255, 255, 0.06);
+    border: 1px solid rgba(255, 255, 255, 0.1);
+    border-radius: 6px;
+    padding: 4px;
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 32px;
+    height: 32px;
+    transition: all 0.15s;
+  }
+  .gate {
+    background: url(./assets/icons/gate.svg) center no-repeat;
+    background-size: 100% 100%;
+  }
+  .speed {
+    background: url(./assets/icons/speed.svg) center no-repeat;
+    background-size: 100% 100%;
+  }
+  .modelsensor {
+    background: url(./assets/icons/modelsensor.svg) center no-repeat;
+    background-size: 100% 100%;
+  }
+  .obfurage {
+    background: url(./assets/icons/obfurage.svg) center no-repeat;
+    background-size: 100% 100%;
+  }
+</style>

+ 27 - 13
src/layouts/default/feature/SimpleMapTest.vue

@@ -17,15 +17,12 @@
   ><div v-if="!isTopLevel" class="map-reset-btn">
     <Button :loading="mapLoading" type="primary" @click="revertStack">返回上级</Button>
   </div>
-  <div v-if="cadOpened" class="map-cad-btn">
-    <a-select
-      v-model:value="fileId"
-      :options="fileOptions"
-      :fieldNames="{ label: 'fileName', value: 'wjId' }"
-      :style="{ minWidth: '200px' }"
-      @change="toggleCADMap(true)"
-    ></a-select>
+
+  <!-- 返回上级 -->
+  <div v-if="!isTopLevel" class="map-reset-btn">
+    <Button :loading="mapLoading" type="primary" @click="revertStack">返回上级</Button>
   </div>
+
   <!-- 省域地图监测区图例(图3-1) -->
   <div v-if="!cadOpened" class="map-legend">
     <div class="legend-title">图例</div>
@@ -51,15 +48,22 @@
   // import { MapCadViewer } from 'map-cad-viewer';
   // import type { CadData, MapConfig, BoundaryConfig, MapMarker } from 'map-cad-viewer/dist/types/index.d';
   // 直接加载打包好的组件
-  import { MapCadViewer } from '/@/components/map/map-cad-viewer.es'
-  import  '/@/components/map/map-cad-viewer.css'
-  import type { CadData, MapConfig, BoundaryConfig, MapMarker } from '/@/components/map/types/index.d'
+  // import { MapCadViewer } from '/@/components/map/map-cad-viewer.es';
+  // import '/@/components/map/map-cad-viewer.css';
+  // import type { CadData, MapConfig, BoundaryConfig, MapMarker } from '/@/components/map/types/index.d';
 
   import { Button } from 'ant-design-vue';
   import { useAppStore } from '/@/store/modules/app';
 
   import { useMineDepartmentStore } from '/@/store/modules/mine';
   import { get, last } from 'lodash-es';
+  // import { getMarkerCollection, getShanxiGeoJSON } from '/@/api/sys/map';
+  // import { findPath, listToTree } from '/@/utils/helper/treeHelper';
+  // import { watchTriggerable } from '@vueuse/core';
+
+  import { MapCadViewer } from '/@/components/map/map-cad-viewer.es';
+  import '/@/components/map/map-cad-viewer.css';
+  import type { CadData, MapConfig, MapMarker, MarkerPopup } from '/@/components/map/types/index.d';
 
   const props = defineProps<{
     silence: boolean;
@@ -69,8 +73,14 @@
   const mineStore = useMineDepartmentStore();
   const mapLoading = ref(false);
 
+  // 设置地图中当前的参数以供其他页面使用
+  appStore.setSimpleMapParams({ deptId: mineStore.getRootId, isLeaf: mineStore.getRoot?.isLeaf });
+
   // CAD是否可见
-  const cadOpened = ref<boolean>();
+  const cadOpened = ref<boolean>(false);
+
+  const GEO_LAYER_ID = 'geojson-layer';
+  const GEO_BORDER_ID = 'geoline-layer';
 
   const DEFAULT_NODE = {
     id: mineStore.getRootId!,
@@ -81,13 +91,17 @@
     zoom: 6,
     isLeaf: false,
   };
+  const DEFAULT_TREE_CONFIG = {
+    id: 'id',
+    pid: 'parentId',
+  };
 
   const DEFAULT_TREE_CONFIG = {
     id: 'id',
     pid: 'parentId',
   };
   // 用于存储历史节点的栈,实现返回上一级功能
-  const historyStack = ref([DEFAULT_NODE]);
+  const historyStack = ref<any[]>([DEFAULT_NODE]);
   const isTopLevel = computed(() => get(historyStack.value, 'length', 1) === 1);
   const fileOptions = ref<any[]>([]);
   const fileId = ref();

+ 19 - 65
src/layouts/default/feature/components/LeafPopup.vue

@@ -8,84 +8,29 @@
     </div>
     <div class="vjmapgis-popup-content__board mb-1">
       <div class="mr-5px">测风点数</div>
-      <div>11</div>
+      <div>{{ node.mineAreaList?.length ?? '-' }}</div>
     </div>
     <div class="vjmapgis-popup-content__board mb-1">
-      <div class="mr-5px">在线测风点</div>
-      <div>22</div>
+      <div class="mr-5px">测风装置数</div>
+      <div>{{ node.windrectList?.length ?? '-' }}</div>
     </div>
     <div class="vjmapgis-popup-content__board mb-1">
       <div class="mr-5px">风量异常</div>
-      <div>11</div>
+      <div>{{ node.m3Exception ?? '-' }}</div>
     </div>
     <div class="vjmapgis-popup-content__board">
       <div class="mr-5px">判识结果</div>
-      <div>正常</div>
+      <div>{{ judgeResultMap[String(node.judgeResult)] ?? '-' }}</div>
     </div>
-    <div class="vjmapgis-popup-content__title">主要风险</div>
-    <div class="vjmapgis-popup-content__divider"></div>
-    <!-- 主要风险(判定依据) -->
     <template v-if="riskItems.length > 0">
+      <div class="vjmapgis-popup-content__title">主要风险</div>
+      <div class="vjmapgis-popup-content__divider"></div>
+      <!-- 主要风险(判定依据) -->
       <div class="vjmapgis-popup-content__board mb-1" v-for="(item, idx) in riskItems" :key="'risk-' + idx">
         <div class="mr-5px">{{ item.label }}</div>
         <div>{{ item.value }}</div>
       </div>
     </template>
-    <!-- <div class="vjmapgis-popup-content__table_container">
-      <table class="vjmapgis-popup-content__table w-full mt-1">
-        <thead>
-          <tr>
-            <th>序号</th>
-            <th class="w-200px">煤层名称</th>
-            <th>自燃发火倾向性</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(item, index) in coalSeamList" :key="item.id">
-            <td>{{ (index as number) + 1 }}</td>
-            <td>{{ item.coalSeamName }}</td>
-            <td>
-              {{ item.coalSeamLevel }}
-            </td>
-          </tr>
-        </tbody>
-      </table>
-      <div v-if="!goafDataList.length" class="vjmapgis-popup-content__table_empty">暂无数据</div>
-    </div>
-    <div class="vjmapgis-popup-content__divider"></div>
-    <div class="vjmapgis-popup-content__table_container">
-      <table class="vjmapgis-popup-content__table w-full mt-1">
-        <thead>
-          <tr>
-            <th>序号</th>
-            <th class="w-140px">隐蔽工作面名称</th>
-            <th>最高风险等级</th>
-            <th>通讯状态</th>
-          </tr>
-        </thead>
-        <tbody>
-          <tr v-for="(item, index) in goafDataList" :key="item.id">
-            <td>{{ (index as number) + 1 }}</td>
-            <td>{{ item.devicePos }}</td>
-            <td
-              :style="{
-                color: get(alarmLevelColor, node.alarmLevel, alarmLevelText[0]),
-              }"
-            >
-              {{ get(alarmLevelText, node.alarmLevel, alarmLevelText[0]) }}
-            </td>
-            <td
-              :style="{
-                color: get(linkStatusColor, node.status, linkStatusText[0]),
-              }"
-            >
-              {{ get(linkStatusText, node.status, linkStatusText[0]) }}
-            </td>
-          </tr>
-        </tbody>
-      </table>
-      <div v-if="!goafDataList.length" class="vjmapgis-popup-content__table_empty">暂无数据</div>
-    </div> -->
   </div>
 </template>
 <script lang="ts" setup>
@@ -100,6 +45,13 @@
     props.callback(props.node);
   }
 
+  // 判识结果映射(Mine.judgeResult:0数据不足、1疑似、2正常)
+  const judgeResultMap: Record<string, string> = {
+    '0': '数据不足',
+    '1': '疑似',
+    '2': '正常',
+  };
+
   // 主要风险字段(与 IdentifyResult 一致)
   const riskFields = [
     { label: '风量较均值偏低', prop: 'volumeLowPercent', unit: '%' },
@@ -111,9 +63,11 @@
     return riskFields
       .map((f) => {
         const val = props.node[f.prop];
-        return { label: f.label, value: (val || '- ') + (f.unit || '') };
+        // 无数据(undefined/null/空字符串)时剔除该项,整个区块在有数据时才渲染
+        if (val === null || val === undefined || val === '') return null;
+        return { label: f.label, value: `${val}${f.unit || ''}` };
       })
-      .filter(Boolean);
+      .filter((item): item is { label: string; value: string } => item !== null);
   });
 </script>
 <style lang="less">

+ 2 - 1
src/layouts/default/plain.vue

@@ -19,7 +19,8 @@
   import LayoutHeader from './header/index.vue';
   import LayoutContent from './content/index.vue';
   // import SystemSelect from './feature/SystemSelect.vue';
-  import SimpleMap from './feature/SimpleMapTest.vue';
+  import SimpleMap from './feature/SimpleMap.vue';
+  // import SimpleMap from './feature/SimpleMapTest.vue';
 
   // import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
   import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';

+ 1 - 6
src/views/analysis/warningAnalysis/globalAlarm/globalAlarm.api.ts

@@ -3,18 +3,13 @@ import { defHttp } from '/@/utils/http/axios';
 enum Api {
   getAlarmList = '/workingface/alarm/getAlarmInfo',
   getAlarmHistory = '/workingface/alarm/getAlarmRecord',
-  getBoardNum = '/province/wind/getWindOverlimitBoardNum',
   /* getPointList = '/workingface/windrect/getWindrectList', */
-  getAlarmTrend = '/province/wind/getAlarmTrend',
+  getAlarmTrend = '/workingface/alarm/getAlarmTrend',
   /* resolveAlarm = '/province/wind/resolveAlarm',
   getHandleRecords = '/province/wind/getHandleRecords', */
   exportAlarm = '/province/wind/exportWindOverlimitAlarm',
 }
 
-/** 获取顶部统计卡片数据 */
-export const getBoardNum = (params?: any) =>
-  defHttp.post({ url: Api.getBoardNum, params }, { joinParamsToUrl: true });
-
 /** 查询报警实时数据(AlarmController.getAlarmInfo) */
 export const getAlarmList = (params?: any) =>
   defHttp.post({ url: Api.getAlarmList, params }, { joinParamsToUrl: true });

+ 2 - 0
src/views/analysis/warningAnalysis/globalAlarm/globalAlarm.data.ts

@@ -16,6 +16,7 @@ export const alarmTypeMap: Record<string, string> = {
   vaOverLimit: '风速超限',
   m3Lack: '风量不足',
   m3NotClosed: '风量不闭合',
+  disconnect: '断线',
 };
 
 /** 报警全览实时监测表格列(AlarmInfo) */
@@ -135,6 +136,7 @@ export const schemas: FormSchema[] = [
         { label: '风速超限', value: 'vaOverLimit' },
         { label: '风量不足', value: 'm3Lack' },
         { label: '风量不闭合', value: 'm3NotClosed' },
+        { label: '断线', value: 'disconnect' },
       ],
     },
     rules: [],

+ 46 - 34
src/views/analysis/warningAnalysis/globalAlarm/index.vue

@@ -4,7 +4,7 @@
     <!-- 顶部统计卡片 -->
     <div class="board-info">
       <MiniBoard
-        v-for="(item, index) in boardData"
+        v-for="(item, index) in boardDataRef"
         :key="index"
         type="A"
         :label="item.label"
@@ -17,19 +17,18 @@
     <!-- Tab 切换:实时监测 / 历史数据 -->
     <Tabs v-model:activeKey="activeTab" type="line" class="common-page-tabs">
       <TabPane tab="实时监测" key="realtime">
-        <BasicTable @register="registerRealtimeTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }">
-          <template #action="{ record }">
+        <BasicTable @register="registerRealtimeTable" :style="{ padding: 0 }">
+          <!-- <template #action="{ record }">
             <div class="action-buttons">
-              <!-- 报警处理(暂注释,待接口就绪后恢复)
               <button v-if="record.status !== '2'" @click="openResolveModal(record)" class="action-btn resolved-btn" title="处理">
                 <SvgIcon name="solved" />
               </button>
-              -->
+             
               <button @click="handleViewDetail(record)" class="action-btn" title="详情">
                 <SvgIcon name="details" />
               </button>
             </div>
-          </template>
+          </template> -->
           <template #resetBefore>
             <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="onExportXlsTime"> 导出 </a-button>
           </template>
@@ -40,14 +39,14 @@
       </TabPane>
 
       <TabPane tab="历史数据" key="history">
-        <BasicTable @register="registerHistoryTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }">
-          <template #action="{ record }">
+        <BasicTable @register="registerHistoryTable" :style="{ padding: 0 }">
+          <!-- <template #action="{ record }">
             <div class="action-buttons">
               <button @click="handleViewDetail(record)" class="action-btn" title="详情">
                 <SvgIcon name="details" />
               </button>
             </div>
-          </template>
+          </template> -->
           <template #resetBefore>
             <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="onExportXls"> 导出 </a-button>
           </template>
@@ -106,16 +105,19 @@
   import { BasicTable } from '/@/components/Table';
   import { Tabs, TabPane } from 'ant-design-vue';
   import MiniBoard from '/@/components/Configurable/detail/MiniBoard.vue';
-  import { SvgIcon } from '/@/components/Icon';
+  // import { SvgIcon } from '/@/components/Icon';
   import { useListPage } from '/@/hooks/system/useListPage';
   import { useInitForm } from '../../common/analysis';
-  import { advancedRoutePush } from '/@/utils';
+  // import { advancedRoutePush } from '/@/utils';
   import * as echarts from 'echarts';
   import { boardData, columns, historyColumns, schemas, historySchemas } from './globalAlarm.data';
-  import { getAlarmList, getAlarmHistory, getBoardNum, getAlarmTrend } from './globalAlarm.api';
+  import { getAlarmList, getAlarmHistory, getAlarmTrend } from './globalAlarm.api';
+  import { getCurrentStatistics } from '/@/api/workingface';
+  import { useMineDepartmentStore } from '/@/store/modules/mine';
 
   // 激活的Tab页签
   const activeTab = ref('realtime');
+  const mineStore = useMineDepartmentStore();
 
   // 卡片数据
   const boardDataRef = ref(boardData.map((item) => ({ ...item })));
@@ -149,6 +151,8 @@
         slots: { customRender: 'action' },
         fixed: undefined,
       },
+      showActionColumn: false,
+      maxHeight: 200,
     },
     exportConfig: {
       url: '/province/wind/exportWindOverlimitAlarm',
@@ -193,6 +197,8 @@
       useSearchForm: true,
       bordered: true,
       showIndexColumn: true,
+      showActionColumn: false,
+      maxHeight: 200,
     },
     exportConfig: {
       url: '/province/wind/exportWindOverlimitAlarmHistory',
@@ -201,14 +207,14 @@
   });
   const [registerHistoryTable] = tableContext;
 
-  // ====== 加载卡片数据 ======
+  // ====== 加载卡片数据(getCurrentStatistics)======
   async function loadBoardData(params?: any) {
     try {
-      const result = await getBoardNum(params);
-      boardDataRef.value[0].value = result.windSpeedOver ?? '-';
-      boardDataRef.value[1].value = result.windVolumeLow ?? '-';
-      boardDataRef.value[2].value = result.windVolumeUnclosed ?? '-';
-      boardDataRef.value[3].value = result.todayResolved ?? '-';
+      const result = await getCurrentStatistics({ deptId: params?.deptId });
+      boardDataRef.value[0].value = result.vaOverLimitNum ?? '-';
+      boardDataRef.value[1].value = result.m3LackNum ?? '-';
+      boardDataRef.value[2].value = result.m3NotClosedNum ?? '-';
+      // 第4卡(今日已处理报警)暂不接入,保持 '-'
     } catch {
       // 使用默认值
     }
@@ -220,15 +226,24 @@
 
   async function renderChart() {
     try {
-      const data = await getAlarmTrend({ days: 7 });
+      // /workingface/alarm/getAlarmTrend:deptId 必填,返回 {dates, data:[{alarmType, alarmName, counts}]}
+      const data = await getAlarmTrend({ deptId: mineStore.getDepartId || mineStore.getRootId });
       if (!chartRef.value) return;
       if (!chartInstance) {
         chartInstance = echarts.init(chartRef.value);
       }
+      const legendData = (data.data || []).map((item) => item.alarmName);
+      const series = (data.data || []).map((item) => ({
+        name: item.alarmName,
+        type: 'line',
+        data: item.counts || [],
+        smooth: true,
+        lineStyle: { width: 2 },
+      }));
       chartInstance.setOption({
         tooltip: { trigger: 'axis' },
         legend: {
-          data: ['风速超限', '风量不足', '风量不闭合'],
+          data: legendData,
           bottom: 0,
         },
         grid: { left: 50, right: 20, top: 20, bottom: 40 },
@@ -238,12 +253,8 @@
           axisLabel: { fontSize: 12 },
         },
         yAxis: { type: 'value', minInterval: 1 },
-        series: [
-          { name: '风速超限', type: 'line', data: data.windSpeedOver || [], smooth: true, lineStyle: { width: 2 } },
-          { name: '风量不足', type: 'line', data: data.windVolumeLow || [], smooth: true, lineStyle: { width: 2 } },
-          { name: '风量不闭合', type: 'line', data: data.windVolumeUnclosed || [], smooth: true, lineStyle: { width: 2 } },
-        ],
-        color: ['#f5222d', '#fa8c16', '#1890ff'],
+        series,
+        color: ['#f5222d', '#fa8c16', '#1890ff', '#722ed1'],
       });
     } catch {
       // 图表数据加载失败时保持空白
@@ -251,18 +262,19 @@
   }
 
   // ====== 页面跳转 ======
-  function handleViewDetail(record: any) {
-    advancedRoutePush({
-      path: '/warningAnalysis/connectAnalysis',
-      query: { deptId: record.deptId, filter: 'windSpeed,windVolume' },
-    });
-  }
+  // function handleViewDetail(record: any) {
+  //   advancedRoutePush({
+  //     path: '/warningAnalysis/connectAnalysis',
+  //     query: { deptId: record.deptId, filter: 'windSpeed,windVolume' },
+  //   });
+  // }
 
   function handleResize() {
     chartInstance?.resize();
   }
 
   onMounted(() => {
+    loadBoardData();
     nextTick(() => renderChart());
     window.addEventListener('resize', handleResize);
   });
@@ -340,7 +352,7 @@
 
   .global-alarm {
     height: 100%;
-    overflow-y: auto;
+    overflow: hidden;
   }
 
   .bottom-section {
@@ -365,7 +377,7 @@
 
     .trend-chart {
       width: 100%;
-      height: 280px;
+      height: 200px;
     }
   }
 

+ 16 - 50
src/views/analysis/warningAnalysis/windPointManage/index.vue

@@ -16,10 +16,6 @@
                     <SvgIcon name="delete" />
                   </button>
                 </a-popconfirm>
-                <a-button size="small" type="link" class="bind-btn" @click="handleBind(record)"> 绑定 </a-button>
-                <a-popconfirm title="确认解绑?" @confirm="handleUnbind(record)">
-                  <a-button size="small" type="link" danger class="bind-btn"> 解绑 </a-button>
-                </a-popconfirm>
               </template>
               <template #resetBefore>
                 <a-button class="ml-2" preIcon="ant-design:plus-outlined" @click="openAddModal"> 新增 </a-button>
@@ -80,19 +76,24 @@
   import { BasicModal } from '/@/components/Modal/index';
   import { useModal } from '/@/components/Modal';
   import { useListPage } from '/@/hooks/system/useListPage';
-  import { pointColumns, windrectColumns, /* modelConfigColumns, defaultModelConfigs, */ formSchema /*, modelConfigFormSchema */ } from './windPointManage.data';
+  import {
+    pointColumns,
+    windrectColumns,
+    /* modelConfigColumns, defaultModelConfigs, */ formSchema /*, modelConfigFormSchema */,
+  } from './windPointManage.data';
   import {
     getPointList,
     savePoint,
     deletePoint,
     getWindrectList,
-    getMineAreaRelation,
-    addMineAreaRelation,
-    deleteMineAreaRelation,
+    // getMineAreaRelation,
+    // addMineAreaRelation,
+    // deleteMineAreaRelation,
     /* getModelConfigs,
     saveModelConfigs, */
   } from './windPointManage.api';
   import WindTopology from './windTopology/index.vue';
+  import { useMineDepartmentStore } from '/@/store/modules/mine';
 
   // 激活的Tab页签
   const activeTab = ref('pointManage');
@@ -152,6 +153,9 @@
   });
   const [registerPointTable, pointTable] = pointCtx;
 
+  // 组织树 store(fax↔部门 id 换算)
+  const mineStore = useMineDepartmentStore();
+
   // ====== 上传数据布点表格(getWindrectList)======
   const { tableContext: windrectCtx } = useListPage({
     tableProps: {
@@ -188,7 +192,7 @@
       rowSelection: { type: 'radio' },
     },
   });
-  const [registerWindrectTable, windrectTable, { selectedRows: windrectSelectedRows }] = windrectCtx;
+  const [registerWindrectTable] = windrectCtx;
 
   // ====== 判识模型参数表格(暂注释,待接口就绪后恢复)======
   /*
@@ -264,44 +268,6 @@
   }
   */
 
-  // ====== 绑定/解绑(addMineAreaRelation / deleteMineAreaRelation)======
-  async function handleBind(record: any) {
-    const windrect = windrectSelectedRows.value[0];
-    if (!windrect) {
-      message.warn('请先在右侧"上传数据布点"中选择要绑定的测风装置');
-      return;
-    }
-    try {
-      await addMineAreaRelation({ mineCode: record.mineCode, parentId: record.id, childId: windrect.id });
-      message.success('绑定成功');
-      pointTable?.reload();
-      windrectTable?.reload();
-    } catch {
-      message.error('绑定失败');
-    }
-  }
-
-  async function handleUnbind(record: any) {
-    try {
-      const res = await getMineAreaRelation({ mineCode: record.mineCode });
-      const relList = res?.mineAreaRelationList || [];
-      const windrectId = windrectSelectedRows.value[0]?.id || record.windrectId;
-      const rel =
-        relList.find((r: any) => r.parentId === record.id && r.childId === windrectId) ||
-        relList.find((r: any) => r.parentId === record.id || r.childId === record.id);
-      if (!rel) {
-        message.warn('未找到该测风点位的绑定关系');
-        return;
-      }
-      await deleteMineAreaRelation({ ids: rel.id });
-      message.success('解绑成功');
-      pointTable?.reload();
-      windrectTable?.reload();
-    } catch {
-      message.error('解绑失败');
-    }
-  }
-
   // ====== 新增/编辑点位弹窗 ======
   const isUpdate = ref(true);
   const modalTitle = computed(() => (!isUpdate.value ? '新增测风点位' : '编辑测风点位'));
@@ -331,7 +297,9 @@
     isUpdate.value = true;
     openModal(true, {});
     await resetFields();
-    return setFieldsValue({ ...record });
+    // 回显:record.mineCode 为矿编码(fax),换算为部门 id 供 MineCascader 正确显示
+    const mine = mineStore.findDepart((n) => n.fax === record.mineCode, mineStore.getDepartTree);
+    return setFieldsValue({ ...record, mineCode: mine?.id ?? record.mineCode });
   }
 
   // ====== 判识模型参数编辑弹窗(暂注释,待接口就绪后恢复)======
@@ -377,7 +345,6 @@
   async function handleDelete(record: any) {
     try {
       await deletePoint({ ids: record.id });
-      message.success('删除成功');
       pointTable?.reload();
     } catch {
       message.error('删除失败');
@@ -389,7 +356,6 @@
       const values = await validate();
       setModalProps({ confirmLoading: true });
       await savePoint(values);
-      message.success('保存成功');
       closeModal();
       pointTable?.reload();
     } catch {

+ 16 - 41
src/views/analysis/warningAnalysis/windPointManage/windPointManage.api.ts

@@ -15,38 +15,19 @@ enum Api {
   saveModelConfigs = '/province/windpoint/saveModelConfigs', */
 }
 
-/** 模拟成功响应 */
-const mockSuccess = { success: true, code: 200, message: '操作成功' };
-
-/** 模拟点位列表数据 */
-const mockPointList = [
-  { id: 'p001', mineCode: 'M001', name: '榆林执法处-大柳塔煤矿北翼回风巷', level: 4, airVolume: 1800, regulationId: '1', windrectId: 'W001', status: 'active', createTime: '2026-07-27 10:00:00' },
-  { id: 'p002', mineCode: 'M001', name: '榆林执法处-大柳塔煤矿南翼进风巷', level: 4, airVolume: 2100, regulationId: '1', windrectId: 'W002', status: 'active' },
-  { id: 'p003', mineCode: 'M002', name: '延安执法处-黄陵一号矿101工作面', level: 4, airVolume: 720, regulationId: '2', windrectId: 'W003', status: 'active' },
-  { id: 'p004', mineCode: 'M003', name: '铜川执法处-红柳林煤矿东翼回风大巷', level: 4, airVolume: 1600, regulationId: '2', windrectId: 'W004', status: 'active' },
-  { id: 'p005', mineCode: 'M001', name: '榆林执法处-活鸡兔煤矿副立井', level: 4, airVolume: 2800, regulationId: '3', windrectId: 'W005', status: 'active' },
-  { id: 'm001', mineCode: 'M101', name: '大柳塔煤矿', level: 3, airVolume: 0, regulationId: '', windrectId: '', status: 'active' },
-  { id: 'm002', mineCode: 'M102', name: '活鸡兔煤矿', level: 3, airVolume: 0, regulationId: '', windrectId: '', status: 'active' },
-  { id: 'm003', mineCode: 'M106', name: '黄陵一号矿', level: 3, airVolume: 0, regulationId: '', windrectId: '', status: 'active' },
-  { id: 'm004', mineCode: 'M107', name: '红柳林煤矿', level: 3, airVolume: 0, regulationId: '', windrectId: '', status: 'active' },
-  { id: 'e001', mineCode: 'M001', name: '榆林执法处', level: 2, airVolume: 0, regulationId: '', windrectId: '', status: 'active' },
-  { id: 'e002', mineCode: 'M002', name: '延安执法处', level: 2, airVolume: 0, regulationId: '', windrectId: '', status: 'active' },
-  { id: 'e003', mineCode: 'M003', name: '铜川执法处', level: 2, airVolume: 0, regulationId: '', windrectId: '', status: 'active' },
-];
-
-/** 获取 MineArea 列表(后端未部署时回退到模拟数据) */
+/** 获取 MineArea 列表 */
 export const getPointList = (params?: any) => {
   const p = { ...params };
   // 搜索表单 MineCascader 提交的是部门 id,需换算为 API 查询条件要求的矿编码(fax)
-  if (p.deptId) {
-    const mine = useMineDepartmentStore().findDepartById(p.deptId);
-    if (mine?.fax) p.mineCode = mine.fax;
-    delete p.deptId;
-  }
-  return defHttp.post({ url: Api.getPointList, params: p }, { joinParamsToUrl: true }).catch(() => mockPointList);
+  // if (p.deptId) {
+  //   const mine = useMineDepartmentStore().findDepartById(p.deptId);
+  //   if (mine?.fax) p.mineCode = mine.fax;
+  //   delete p.deptId;
+  // }
+  return defHttp.post({ url: Api.getPointList, params: p }, { joinParamsToUrl: true });
 };
 
-/** 新增或编辑 MineArea(根据是否有 id 自动切换;后端未部署时返回模拟成功) */
+/** 新增或编辑 MineArea(根据是否有 id 自动切换) */
 export const savePoint = (params?: any) => {
   const url = params?.id ? Api.updateMineArea : Api.addMineArea;
   const payload = { ...params };
@@ -55,32 +36,26 @@ export const savePoint = (params?: any) => {
     const mine = useMineDepartmentStore().findDepartById(payload.mineCode);
     if (mine?.fax) payload.mineCode = mine.fax;
   }
-  return defHttp.post({ url, params: payload }).catch(() => mockSuccess);
+  return defHttp.post({ url, params: payload });
 };
 
-/** 删除 MineArea(含关联关系;后端未部署时返回模拟成功) */
-export const deletePoint = (params?: any) =>
-  defHttp.post({ url: Api.deleteMineArea, params }, { joinParamsToUrl: true }).catch(() => mockSuccess);
+/** 删除 MineArea(含关联关系) */
+export const deletePoint = (params?: any) => defHttp.post({ url: Api.deleteMineArea, params }, { joinParamsToUrl: true });
 
 /** 根据矿ID查询下面所有测风装置信息(WindrectController.getWindrectList) */
-export const getWindrectList = (params?: any) =>
-  defHttp.post({ url: Api.getWindrectList, params }, { joinParamsToUrl: true });
+export const getWindrectList = (params?: any) => defHttp.post({ url: Api.getWindrectList, params }, { joinParamsToUrl: true });
 
 /** 查询规程值列表(MineDataController.getWindrectRegulation,无参数) */
-export const getWindrectRegulation = (params?: any) =>
-  defHttp.get({ url: Api.getWindrectRegulation, params }, { joinParamsToUrl: true });
+export const getWindrectRegulation = (params?: any) => defHttp.get({ url: Api.getWindrectRegulation, params }, { joinParamsToUrl: true });
 
 /** 查询 MineArea 关联关系(返回 { mineAreaList, mineAreaRelationList }) */
-export const getMineAreaRelation = (params?: any) =>
-  defHttp.post({ url: Api.getMineAreaRelation, params }, { joinParamsToUrl: true });
+export const getMineAreaRelation = (params?: any) => defHttp.post({ url: Api.getMineAreaRelation, params }, { joinParamsToUrl: true });
 
 /** 绑定 MineArea 关联关系(RequestBody:mineCode/parentId/childId) */
-export const addMineAreaRelation = (params?: any) =>
-  defHttp.post({ url: Api.addMineAreaRelation, params });
+export const addMineAreaRelation = (params?: any) => defHttp.post({ url: Api.addMineAreaRelation, params });
 
 /** 删除 MineArea 关联关系(ids 必填,逗号分隔) */
-export const deleteMineAreaRelation = (params?: any) =>
-  defHttp.post({ url: Api.deleteMineAreaRelation, params }, { joinParamsToUrl: true });
+export const deleteMineAreaRelation = (params?: any) => defHttp.post({ url: Api.deleteMineAreaRelation, params }, { joinParamsToUrl: true });
 
 /* ====== 判识模型配置接口(暂注释,待接口就绪后恢复)======
 /* 获取判识模型配置

+ 39 - 15
src/views/analysis/warningAnalysis/windPointManage/windPointManage.data.ts

@@ -2,15 +2,24 @@ import type { BasicColumn } from '/@/components/Table';
 import type { FormSchema } from '/@/components/Table';
 import { h } from 'vue';
 import { StatusColorEnum } from '/@/enums/jeecgEnum';
-import { getWindrectRegulation } from './windPointManage.api';
+import { useMineDepartmentStore } from '/@/store/modules/mine';
+import { getWindrectList, getWindrectRegulation } from './windPointManage.api';
+
+/** 按矿编码(fax)在组织树中匹配部门名称,未匹配时回退显示原编码 */
+function renderMineNameByFax(mineCode: string): string {
+  if (!mineCode) return '-';
+  const mine = useMineDepartmentStore().findDepart((n) => n.fax === mineCode, useMineDepartmentStore().getDepartTree);
+  return mine?.departName || mineCode;
+}
 
 /** 注册测风点位管理表格列 */
 export const pointColumns: BasicColumn[] = [
   {
     title: '煤矿编号',
     dataIndex: 'mineCode',
-    width: 100,
+    width: 140,
     fixed: 'left',
+    customRender: ({ text }) => h('span', renderMineNameByFax(String(text ?? ''))),
   },
   {
     title: '区域名称',
@@ -37,15 +46,18 @@ export const pointColumns: BasicColumn[] = [
     dataIndex: 'airVolume',
     width: 100,
   },
-  {
-    title: '规程值',
-    dataIndex: 'regulationId',
-    width: 90,
-  },
+  // {
+  //   title: '规程值',
+  //   dataIndex: 'regulationId',
+  //   width: 90,
+  // },
   {
     title: '测风设备',
     dataIndex: 'windrectId',
     width: 100,
+    customRender({ record }) {
+      return h('span', record.windrectId ? '已绑定' : '未绑定');
+    },
   },
   {
     title: '创建时间',
@@ -94,25 +106,32 @@ export const windrectColumns: BasicColumn[] = [
       return h('span', { style: { color, fontWeight: 'bold' } }, label);
     },
   },
-  {
-    title: '创建时间',
-    dataIndex: 'createTime',
-    width: 160,
-  },
+  // {
+  //   title: '创建时间',
+  //   dataIndex: 'createTime',
+  //   width: 160,
+  // },
 ];
 
 /** 新增/编辑 MineArea 表单 */
 export const formSchema: FormSchema[] = [
+  {
+    label: 'id',
+    field: 'id',
+    component: 'Input',
+    show: false,
+  },
   {
     label: '煤矿编号',
     field: 'mineCode',
     component: 'MineCascader',
+    required: true,
     componentProps: {
       placeholder: '请输入煤矿编号',
     },
   },
   {
-    label: '矿井名称',
+    label: '测风地点名称',
     field: 'name',
     required: true,
     component: 'Input',
@@ -158,9 +177,14 @@ export const formSchema: FormSchema[] = [
   {
     label: '测风设备',
     field: 'windrectId',
-    component: 'Input',
+    component: 'ApiSelect',
+    required: true,
     componentProps: {
-      placeholder: '请输入测风设备 ID',
+      api: getWindrectList,
+      labelField: 'devicePos',
+      valueField: 'id',
+      placeholder: '请输入测风设备',
+      searchable: true,
     },
   },
 ];

+ 253 - 93
src/views/analysis/warningAnalysis/windPointManage/windTopology/hooks/useTopology.ts

@@ -2,33 +2,39 @@ import { ref } from 'vue';
 import * as echarts from 'echarts';
 import type { EChartsOption } from 'echarts';
 import { message, Modal } from 'ant-design-vue';
+import { useMineDepartmentStore } from '/@/store/modules/mine';
 import {
   createGraphOption,
   categories,
   statusColorMap,
   nodeDetailFields,
-  levelY,
   weightKey,
+  transformToTopologyData,
+  collectOrgSubtree,
+  LAYOUT_WIDTH,
+  LAYOUT_HEIGHT,
 } from '../windTopology.data';
-import type { TopologyData, TopoNodeData } from '../windTopology.data';
-import { transformToTopologyData } from '../windTopology.data';
-import { getTopologyData, resolveMineCode } from '../windTopology.api';
-import { addMineAreaRelation, deleteMineAreaRelation } from '../../windPointManage.api';
+import type { TopologyData, TopoNodeData, OrgPathNode } from '../windTopology.data';
+import { getTopologyData, updateArea } from '../windTopology.api';
 
-/** 节点分类索引:0=测风点位(源),1=数据布点(目标) */
-const CAT_POINT = 0;
-const CAT_DEVICE = 1;
+/** 节点分类索引:0=部门(只读),1=测风点位,2=数据布点 */
+const CAT_ORG = 0;
+const CAT_POINT = 1;
+const CAT_DEVICE = 2;
 
 export function useTopology() {
   const chartRef = ref<HTMLDivElement | null>(null);
   let chartInstance: echarts.ECharts | null = null;
   let topologyData: TopologyData = { nodes: [], links: [] };
+  let resizeObserver: ResizeObserver | null = null;
 
   // —— 模式状态 ——
   const editMode = ref(false);
-  /** 绑定选择态:双击节点后等待单击目标 */
+  /** 绑定选择态:双击「测风点位/数据布点」后等待单击目标 */
   const selectMode = ref(false);
   const selectedSourceId = ref('');
+  /** 操作栏部门选择:选中任意层级部门后过滤部门树(该部门及其后代)并按该部门查询数据;空=全局 */
+  const selectedDeptId = ref('');
 
   // —— 选中节点详情 ——
   const selectedNode = ref<TopoNodeData | null>(null);
@@ -38,41 +44,128 @@ export function useTopology() {
   function initChart(el: HTMLDivElement) {
     chartInstance = echarts.init(el);
     window.addEventListener('resize', handleResize);
+    // 容器尺寸变化(如 Tab 由隐藏变为可见 / 窗口尺寸变化)时自适应重绘,
+    // 避免 0×0 初始化导致节点布局越界而空白
+    if (typeof ResizeObserver !== 'undefined') {
+      resizeObserver = new ResizeObserver(() => {
+        if (!chartInstance) return;
+        const rect = el.getBoundingClientRect();
+        if (rect.width > 0 && rect.height > 0) {
+          chartInstance.resize();
+          renderTopology(topologyData);
+        }
+      });
+      resizeObserver.observe(el);
+    }
     return chartInstance;
   }
 
-  // ==================== 两行布局 ====================
+  // ==================== 三层布局 ====================
 
   function applyHierarchicalLayout(echartsNodes: any[], forceUnfixed = false, chartW = 1200, chartH = 700) {
-    const groups: Record<number, any[]> = {};
+    // 容器过小/未初始化时用参考尺寸布局,避免节点全部落在画布外
+    if (chartW < 100) chartW = LAYOUT_WIDTH;
+    if (chartH < 100) chartH = LAYOUT_HEIGHT;
+
+    // 行序列:部门节点按树层级(depth)逐层分组为多行,测风点位/数据布点各占一行
+    const rows: { list: any[]; weight?: string }[] = [];
+    const depthGroups: Record<number, any[]> = {};
+    const catGroups: Record<number, any[]> = {};
     for (const en of echartsNodes) {
-      const cat = en.category ?? 0;
-      if (!groups[cat]) groups[cat] = [];
-      groups[cat].push(en);
+      if (en.category === 0) {
+        const depth = en.raw?.depth ?? 0;
+        if (!depthGroups[depth]) depthGroups[depth] = [];
+        depthGroups[depth].push(en);
+      } else {
+        if (!catGroups[en.category]) catGroups[en.category] = [];
+        catGroups[en.category].push(en);
+      }
     }
-    for (let cat = 0; cat < categories.length; cat++) {
-      const list = groups[cat];
-      if (!list || list.length === 0) continue;
-      const wk = weightKey[cat];
+    const depths = Object.keys(depthGroups)
+      .map(Number)
+      .sort((a, b) => a - b);
+    for (const d of depths) rows.push({ list: depthGroups[d] });
+    if (catGroups[1]) rows.push({ list: catGroups[1], weight: weightKey[1] });
+    if (catGroups[2]) rows.push({ list: catGroups[2], weight: weightKey[2] });
+
+    const rowCount = rows.length || 1;
+    const topMargin = 40;
+    const bottomMargin = 40;
+    // 层级行间距(加宽间隔,可调)
+    const rowGap = 60;
+    const rowHeight = (chartH - topMargin - bottomMargin - rowGap * (rowCount - 1)) / rowCount;
+    const margin = 60;
+    const tw = chartW - margin * 2;
+
+    rows.forEach((row, idx) => {
+      const list = row.list;
+      if (!list.length) return;
+      // 行内排序(按权重字段,值越大越靠左)
+      const wk = row.weight;
       list.sort((a, b) => {
-        const va = wk && a.raw ? Number(a.raw[wk]) || 0 : 0;
-        const vb = wk && b.raw ? Number(b.raw[wk]) || 0 : 0;
+        const va = wk ? Number(a.raw?.[wk]) || 0 : 0;
+        const vb = wk ? Number(b.raw?.[wk]) || 0 : 0;
         return vb - va;
       });
-      const margin = 60;
-      const tw = chartW - margin * 2;
       const gap = list.length > 1 ? tw / (list.length - 1) : 0;
       const sx = margin + (tw - (list.length - 1) * gap) / 2;
-      const yPct = levelY[cat] ?? 50;
-      const y = (yPct / 100) * chartH;
-      list.forEach((en, idx) => {
-        en.x = sx + idx * gap;
+      const y = topMargin + idx * (rowHeight + rowGap) + rowHeight / 2;
+      list.forEach((en, i) => {
+        en.x = sx + i * gap;
         en.y = y;
         en.fixed = !forceUnfixed;
       });
+    });
+  }
+
+  // —— 加载并渲染拓扑 ——
+  async function loadTopology() {
+    try {
+      const mineStore = useMineDepartmentStore();
+      // 任意层级部门均可过滤:选中部门 → 部门层为其子树(该部门及全部后代),数据按该部门 deptId 查询
+      const dept = selectedDeptId.value ? mineStore.findDepartById(selectedDeptId.value) : undefined;
+
+      let orgNodes: OrgPathNode[];
+      let queryDeptId: string | undefined;
+      if (dept) {
+        orgNodes = collectOrgSubtree(dept);
+        queryDeptId = dept.id;
+      } else {
+        let root = mineStore.getRoot;
+        if (!root) {
+          // 组织树未就绪(极端情况):短暂等待后重试一次
+          console.warn('组织树未就绪,300ms 后重试加载拓扑');
+          await new Promise((r) => setTimeout(r, 300));
+          root = mineStore.getRoot;
+        }
+        if (!root) {
+          console.error('组织树仍未就绪,无法加载拓扑');
+          message.error('组织树未就绪,无法加载拓扑');
+          return;
+        }
+        // 全局视图:部门层 = 当前用户部门(getRoot)及其全部后代
+        orgNodes = collectOrgSubtree(root);
+      }
+
+      const apiData = await getTopologyData(queryDeptId ? { deptId: queryDeptId } : {});
+      renderTopology(transformToTopologyData(orgNodes, apiData.mineAreaList, apiData.windrectList));
+      // 数据为空时给出诊断信息(部门树仍会渲染)
+      if (!apiData.mineAreaList.length && !apiData.windrectList.length) {
+        console.warn('拓扑数据为空:未获取到测风点位/数据布点(deptId=' + (queryDeptId || mineStore.getRootId || '') + ')');
+      }
+    } catch (e) {
+      console.error('拓扑数据加载失败:', e);
+      message.error('拓扑数据加载失败,请查看控制台日志');
     }
   }
 
+  /** 操作栏部门选择:过滤部门树(该部门及后代)并按所选部门查询数据。
+   *  注意:v-model 已先行更新 selectedDeptId,此处只需直接触发加载(不能按值去抖,否则恒等跳过) */
+  function setSelectedDept(id: string) {
+    selectedDeptId.value = id || '';
+    loadTopology();
+  }
+
   // —— 渲染拓扑 ——
   function renderTopology(data: TopologyData, forceUnfixed = false) {
     topologyData = data;
@@ -81,10 +174,18 @@ export function useTopology() {
     const option: EChartsOption = createGraphOption();
     const series: any = (option.series as any[])[0];
 
-    const echartsNodes = data.nodes.map((n) => {
+    // 最终按 id 去重兜底(数据层已去重,此处防止异常数据导致 ECharts 重复 id 报错)
+    const seenIds = new Set<string>();
+    const echartsNodes: any[] = [];
+    for (const n of data.nodes) {
+      if (seenIds.has(n.id)) {
+        console.warn('拓扑节点 id 重复,已跳过:', n.id);
+        continue;
+      }
+      seenIds.add(n.id);
       const cat = categories[n.category] || categories[0];
       const statusColor = statusColorMap[n.status || 'normal'];
-      return {
+      echartsNodes.push({
         id: n.id,
         name: n.name,
         category: n.category,
@@ -93,8 +194,8 @@ export function useTopology() {
         symbolSize: cat.symbolSize,
         itemStyle: { color: statusColor || cat.color },
         raw: n,
-      };
-    });
+      });
+    }
 
     const cw = chartInstance.getWidth();
     const ch = chartInstance.getHeight();
@@ -103,6 +204,7 @@ export function useTopology() {
     series.links = data.links.map((l) => ({
       source: l.source,
       target: l.target,
+      kind: l.kind,
     }));
 
     if (editMode.value) {
@@ -142,28 +244,24 @@ export function useTopology() {
       if (!params.data) return;
 
       if (params.dataType === 'edge' && editMode.value) {
-        // 双击连线 → 解绑
-        handleUnbind(params.data.source, params.data.target);
+        // 双击连线 → 解绑(仅测风点位↔数据布点)
+        handleUnbind(params.data.source, params.data.target, params.data.kind);
         return;
       }
 
       if (params.dataType === 'node' && editMode.value) {
-        // 双击节点 → 进入选择目标态
-        enterSelectMode(params.data.id);
+        // 「测风点位」作为绑定到矿井的源;「数据布点」作为绑定到测风点位的源
+        if (params.data.category === CAT_POINT || params.data.category === CAT_DEVICE) {
+          enterSelectMode(params.data.id);
+        } else {
+          message.info('请双击「测风点位」或「数据布点」节点作为绑定源');
+        }
       }
     });
 
     // ——— 拖拽结束 ———
     chartInstance.on('dragend', (params: any) => {
       if (!params.data || params.dataType !== 'node') return;
-
-      // 普通模式下拖拽 → 高亮关联路径(节点不移动)
-      if (!editMode.value) {
-        highlightPath(params.data.id);
-        return;
-      }
-
-      // 绑定模式下拖拽 → 物理移动 + 高亮
       highlightPath(params.data.id);
     });
   }
@@ -173,15 +271,18 @@ export function useTopology() {
   function enterSelectMode(sourceId: string) {
     const src = topologyData.nodes.find((n) => n.id === sourceId);
     if (!src) return;
-    // 仅允许以「测风点位」为绑定源
-    if (src.category !== CAT_POINT) {
-      message.info('请双击「测风点位」节点作为绑定源,再单击「数据布点」节点绑定');
+    if (src.category !== CAT_POINT && src.category !== CAT_DEVICE) {
+      message.info('请双击「测风点位」或「数据布点」节点作为绑定源');
       return;
     }
     selectMode.value = true;
     selectedSourceId.value = sourceId;
     highlightPath(sourceId);
-    message.info(`已选中"${src.name}",请单击一个「数据布点」节点进行绑定。双击空白处取消。`);
+    const tip =
+      src.category === CAT_POINT
+        ? `已选中"${src.name}",请单击一个「矿井」绑定所属矿。双击空白处取消。`
+        : `已选中"${src.name}",请单击一个「测风点位」绑定。双击空白处取消。`;
+    message.info(tip);
   }
 
   function exitSelectMode() {
@@ -202,40 +303,88 @@ export function useTopology() {
     const tgt = topologyData.nodes.find((n) => n.id === targetId);
     if (!src || !tgt) return;
 
-    // 仅允许「测风点位」→「数据布点」方向(与测风点位管理一致)
-    if (src.category !== CAT_POINT || tgt.category !== CAT_DEVICE) {
-      message.warning('仅支持「测风点位」绑定到「数据布点」');
+    if (src.category === CAT_POINT && tgt.category === CAT_ORG) {
+      // 测风点位 → 矿井:编辑测风点位的 mineCode
+      if (!tgt.isLeaf) {
+        message.warning('仅支持绑定到矿井(叶子节点)');
+        return;
+      }
+      const fax = tgt.fax || '';
+      if (!fax) {
+        message.warning('目标矿井缺少矿编码,无法绑定');
+        return;
+      }
+      if (src.mineCode === fax) {
+        message.warning('该测风点位已绑定到该矿');
+        return;
+      }
+      const confirmText = src.mineCode
+        ? `该测风点位已绑定到矿编码"${src.mineCode}",确认改绑到"${tgt.name}"?`
+        : `确认将"${src.name}"绑定到"${tgt.name}"?`;
+      Modal.confirm({
+        title: '确认绑定',
+        content: confirmText,
+        okText: '确认绑定',
+        cancelText: '取消',
+        onOk: async () => {
+          try {
+            await updateArea({ id: src.rawId, mineCode: fax });
+            await loadTopology();
+            message.success('绑定成功');
+          } catch {
+            message.error('绑定失败');
+          }
+        },
+      });
       return;
     }
 
-    // 检查是否已存在该绑定
-    const exists = topologyData.links.some((l) => l.source === sourceId && l.target === targetId);
-    if (exists) {
-      message.warning('已存在该绑定关系');
+    if (src.category === CAT_DEVICE && tgt.category === CAT_POINT) {
+      // 数据布点 → 测风点位:编辑测风点位的 windrectId
+      // 一对一:该数据布点已被其他测风点位绑定则拦截
+      const boundPoint = topologyData.links.find((l) => l.target === src.id && l.source !== tgt.id && l.kind === 'sensor');
+      if (boundPoint) {
+        const other = topologyData.nodes.find((n) => n.id === boundPoint.source);
+        message.warning(`该数据布点已被测风点位"${other?.name || boundPoint.source}"绑定`);
+        return;
+      }
+      const alreadyLinked = topologyData.links.some((l) => l.kind === 'sensor' && l.source === tgt.id && l.target === src.id);
+      if (alreadyLinked) {
+        message.warning('已存在该绑定关系');
+        return;
+      }
+      const confirmText = tgt.windrectId
+        ? `该测风点位已绑定数据布点"${tgt.windrectId}",确认改绑到"${src.name}"?`
+        : `确认将"${src.name}"绑定到测风点位"${tgt.name}"?`;
+      Modal.confirm({
+        title: '确认绑定',
+        content: confirmText,
+        okText: '确认绑定',
+        cancelText: '取消',
+        onOk: async () => {
+          try {
+            await updateArea({ id: tgt.rawId, windrectId: src.rawId });
+            await loadTopology();
+            message.success('绑定成功');
+          } catch {
+            message.error('绑定失败');
+          }
+        },
+      });
       return;
     }
 
-    Modal.confirm({
-      title: '确认绑定',
-      content: `确认建立"${src.name}" → "${tgt.name}"的绑定关系?`,
-      okText: '确认绑定',
-      cancelText: '取消',
-      onOk: async () => {
-        try {
-          await addMineAreaRelation({ mineCode: resolveMineCode(), parentId: sourceId, childId: targetId });
-          const freshData = await getTopologyData({ mineCode: resolveMineCode() });
-          renderTopology(transformToTopologyData(freshData), true);
-          message.success(`绑定成功:${src!.name} → ${tgt!.name}`);
-        } catch {
-          message.error('绑定失败');
-        }
-      },
-    });
+    message.warning('仅支持「测风点位→矿井」或「数据布点→测风点位」的绑定');
   }
 
   // ==================== 解绑逻辑 ====================
 
-  async function handleUnbind(sourceId: string, targetId: string) {
+  async function handleUnbind(sourceId: string, targetId: string, kind?: string) {
+    // 仅允许删除「测风点位↔数据布点」的绑定关系
+    if (kind !== 'sensor') {
+      message.warning('仅允许删除「测风点位」与「数据布点」的绑定关系');
+      return;
+    }
     const src = topologyData.nodes.find((n) => n.id === sourceId);
     const tgt = topologyData.nodes.find((n) => n.id === targetId);
     Modal.confirm({
@@ -244,18 +393,17 @@ export function useTopology() {
       okText: '确认解绑',
       cancelText: '取消',
       onOk: async () => {
-        const link = topologyData.links.find((l) => l.source === sourceId && l.target === targetId);
-        if (link?.id) {
-          try {
-            await deleteMineAreaRelation({ ids: link.id });
-            const freshData = await getTopologyData({ mineCode: resolveMineCode() });
-            renderTopology(transformToTopologyData(freshData), true);
-            message.success('解绑成功');
-          } catch {
-            message.error('解绑失败');
+        try {
+          // 解绑即清空测风点位(source 为测风点位)的 windrectId,用原始实体 id 调用接口
+          if (!src?.rawId) {
+            message.warning('缺少测风点位原始 id,无法解绑');
+            return;
           }
-        } else {
-          message.warning('未找到对应绑定关系');
+          await updateArea({ id: src.rawId, windrectId: '' });
+          await loadTopology();
+          message.success('解绑成功');
+        } catch {
+          message.error('解绑失败');
         }
       },
     });
@@ -284,16 +432,27 @@ export function useTopology() {
   function highlightPath(nodeId: string) {
     if (!chartInstance) return;
     const s = (chartInstance.getOption().series as any[])[0];
+    const nodes = s.data || [];
     const links = s.links || [];
-    const indices: number[] = [];
+    // 单个高亮派发失败不拖垮页面
+    const safeDispatch = (action: any) => {
+      try {
+        chartInstance?.dispatchAction(action);
+      } catch (e) {
+        console.warn('高亮派发失败:', e);
+      }
+    };
+    safeDispatch({ type: 'downplay' });
+    // 按节点 id 定位 dataIndex 高亮(name 是显示名,不能用作定位)
+    const nodeIndex = nodes.findIndex((d: any) => d && d.id === nodeId);
+    if (nodeIndex >= 0) {
+      safeDispatch({ type: 'highlight', seriesIndex: 0, dataIndex: nodeIndex, dataType: 'node' });
+    }
+    const edgeIndices: number[] = [];
     links.forEach((l: any, i: number) => {
-      if (l.source === nodeId || l.target === nodeId) indices.push(i);
-    });
-    chartInstance.dispatchAction({ type: 'downplay' });
-    chartInstance.dispatchAction({ type: 'highlight', seriesIndex: 0, name: nodeId });
-    indices.forEach((i) => {
-      chartInstance!.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: i, dataType: 'edge' });
+      if (l.source === nodeId || l.target === nodeId) edgeIndices.push(i);
     });
+    edgeIndices.forEach((i) => safeDispatch({ type: 'highlight', seriesIndex: 0, dataIndex: i, dataType: 'edge' }));
   }
 
   // ==================== 缩放 ====================
@@ -325,7 +484,7 @@ export function useTopology() {
     if (on) {
       renderTopology(topologyData, true);
       chartInstance.setOption({ series: [{ roam: false }] as any });
-      message.info('绑定模式已开启:双击「测风点位」节点选择源 → 单击「数据布点」节点绑定;双击连线解绑');
+      message.info('绑定模式已开启:双击「测风点位」→ 单击「矿井」绑定所属矿;双击「数据布点」→ 单击「测风点位」绑定;双击连线解绑');
     } else {
       renderTopology(topologyData);
       message.info('已退出绑定模式');
@@ -339,6 +498,8 @@ export function useTopology() {
   }
 
   function dispose() {
+    resizeObserver?.disconnect();
+    resizeObserver = null;
     window.removeEventListener('resize', handleResize);
     chartInstance?.dispose();
     chartInstance = null;
@@ -347,18 +508,17 @@ export function useTopology() {
   return {
     chartRef,
     editMode,
-    selectMode,
     selectedNode,
     detailFields,
+    selectedDeptId,
+    setSelectedDept,
     initChart,
-    renderTopology,
+    loadTopology,
     zoomIn,
     zoomOut,
     resetView,
-    highlightPath,
     setEditMode,
     clearSelection,
-    exitSelectMode,
     dispose,
   };
 }

+ 36 - 20
src/views/analysis/warningAnalysis/windPointManage/windTopology/index.vue

@@ -4,6 +4,18 @@
     <!-- 工具栏 -->
     <div class="topo-toolbar">
       <div class="toolbar-left">
+        <div class="mine-select">
+          <span class="select-label">部门:</span>
+          <MineCascader
+            v-model:value="selectedDeptId"
+            style="width: 220px"
+            :init-from-store="false"
+            :sync-from-store="false"
+            :change-on-select="true"
+            @change="setSelectedDept"
+          />
+        </div>
+        <a-divider type="vertical" />
         <a-button-group>
           <a-button @click="zoomIn" title="放大">+</a-button>
           <a-button @click="zoomOut" title="缩小">-</a-button>
@@ -19,11 +31,13 @@
           刷新
         </a-button>
         <a-divider type="vertical" />
-        <span v-if="editMode" class="select-text">双击「测风点位」节点选择绑定源,单击「数据布点」节点确认绑定;双击连线解绑</span>
+        <span v-if="editMode" class="select-text"
+          >双击「测风点位」→ 单击「矿井」绑定所属矿;双击「数据布点」→ 单击「测风点位」绑定;双击连线解绑</span
+        >
       </div>
 
       <!-- 图例 -->
-      <div class="toolbar-right">
+      <!-- <div class="toolbar-right">
         <div class="legend">
           <span v-for="cat in categories" :key="cat.name" class="legend-item">
             <span class="legend-dot" :style="{ background: cat.color }"></span>
@@ -38,7 +52,7 @@
             离线
           </span>
         </div>
-      </div>
+      </div> -->
     </div>
 
     <!-- 主体 -->
@@ -67,26 +81,26 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, onMounted, onUnmounted } from 'vue';
+  import { onMounted, onUnmounted } from 'vue';
   import { SvgIcon } from '/@/components/Icon';
+  import MineCascader from '/@/components/Form/src/jeecg/components/MineCascader/MineCascader.vue';
   import { categories } from './windTopology.data';
-  import { getTopologyData, resolveMineCode } from './windTopology.api';
   import { useTopology } from './hooks/useTopology';
 
   const {
     chartRef,
     editMode,
-    // selectMode,
     selectedNode,
     detailFields,
+    selectedDeptId,
+    setSelectedDept,
     initChart,
-    renderTopology,
+    loadTopology,
     zoomIn,
     zoomOut,
     resetView,
     setEditMode,
     clearSelection,
-    // exitSelectMode,
     dispose,
   } = useTopology();
 
@@ -94,22 +108,14 @@
     setEditMode(!editMode.value);
   }
 
-  const mineCode = ref(resolveMineCode());
-
-  async function refreshData() {
-    try {
-      const data = await getTopologyData({ mineCode: mineCode.value });
-      renderTopology(data);
-    } catch {
-      /* cache */
-    }
+  function refreshData() {
+    loadTopology();
   }
 
-  onMounted(async () => {
+  onMounted(() => {
     if (!chartRef.value) return;
     initChart(chartRef.value);
-    const data = await getTopologyData({ mineCode: mineCode.value });
-    renderTopology(data);
+    loadTopology();
   });
 
   onUnmounted(() => {
@@ -138,6 +144,16 @@
       display: flex;
       align-items: center;
       gap: 8px;
+      .mine-select {
+        display: flex;
+        align-items: center;
+        gap: 4px;
+        .select-label {
+          font-size: 13px;
+          color: #333;
+          white-space: nowrap;
+        }
+      }
       .toolbar-title {
         font-size: 15px;
         font-weight: 600;

+ 34 - 18
src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.api.ts

@@ -1,30 +1,46 @@
 import { defHttp } from '/@/utils/http/axios';
 import { useMineDepartmentStore } from '/@/store/modules/mine';
-import type { MineAreaApiResponse, WindrectNode } from './windTopology.data';
+import type { MineAreaApiResponse, MineAreaNode, WindrectNode } from './windTopology.data';
 
 enum Api {
-  getMineAreaRelation = '/workingface/mineArea/getMineAreaRelation',
+  getMineAreaList = '/workingface/mineArea/getMineAreaList',
   getWindrectList = '/workingface/windrect/getWindrectList',
+  updateMineArea = '/workingface/mineArea/updateMineArea',
 }
 
-/** 解析当前矿编码:取组织树根节点(当前登录机构)的 id,无值时保留 'M001' 兜底 */
-export const resolveMineCode = (): string => {
-  const rootId = useMineDepartmentStore().getRoot?.id;
-  return rootId || 'M001';
-};
-
-/** 获取测风点位/数据布点绑定关系(getMineAreaRelation + getWindrectList,均为真实接口) */
+/**
+ * 获取测风网络拓扑数据(均为真实接口,按全局部门 getRoot 获取):
+ *   getMineAreaList  -> 测风点位列表(参考:getMineAreaList?column=createTime&order=desc&pageNo=1&pageSize=100&deptId=)
+ *   getWindrectList  -> 数据布点列表(参考:getWindrectList?column=createTime&order=desc&deptId=)
+ * 绑定关系由 MineArea 字段派生:mineCode(所属矿 fax) / windrectId(绑定数据布点 id),不使用 MineAreaRelation 系列接口。
+ */
 export const getTopologyData = async (params?: any): Promise<MineAreaApiResponse> => {
-  const p = { ...params };
-  if (!p.mineCode) p.mineCode = resolveMineCode();
-  const [relationRes, windrectRes] = await Promise.all([
-    defHttp.post({ url: Api.getMineAreaRelation, params: p }, { joinParamsToUrl: true }),
-    defHttp.post({ url: Api.getWindrectList, params: p }, { joinParamsToUrl: true }),
+  const mineStore = useMineDepartmentStore();
+  // 全局获取:deptId 传当前用户部门(组织树根部门)id
+  const deptId = params?.deptId || mineStore.getRootId;
+  // 兼容两种响应结构:分页 { records } 或裸数组
+  const toList = (res: any): any[] => (Array.isArray(res) ? res : Array.isArray(res?.records) ? res.records : []);
+
+  const [areaRes, windrectRes] = await Promise.allSettled([
+    defHttp.post(
+      { url: Api.getMineAreaList, params: { deptId, column: 'createTime', order: 'desc', pageNo: 1, pageSize: 100 } },
+      { joinParamsToUrl: true }
+    ),
+    defHttp.post(
+      { url: Api.getWindrectList, params: { deptId, column: 'createTime', order: 'desc' } },
+      { joinParamsToUrl: true }
+    ),
   ]);
-  const windrectList: WindrectNode[] = Array.isArray(windrectRes) ? windrectRes : [];
+  // 单个接口失败不影响另一个(避免整体空白);失败时记录原因便于定位
+  const areaList = areaRes.status === 'fulfilled' ? toList(areaRes.value) : (console.error('获取测风点位失败:', areaRes.reason), []);
+  const windrectList =
+    windrectRes.status === 'fulfilled' ? toList(windrectRes.value) : (console.error('获取数据布点失败:', windrectRes.reason), []);
   return {
-    mineAreaList: relationRes?.mineAreaList || [],
-    mineAreaRelationList: relationRes?.mineAreaRelationList || [],
-    windrectList,
+    mineAreaList: areaList as MineAreaNode[],
+    mineAreaRelationList: [],
+    windrectList: windrectList as WindrectNode[],
   };
 };
+
+/** 编辑测风点位(绑定/解绑通过字段:mineCode=所属矿编码、windrectId=绑定数据布点 id) */
+export const updateArea = (params?: any) => defHttp.post({ url: Api.updateMineArea, params });

+ 175 - 70
src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data.ts

@@ -5,14 +5,15 @@ import type { EChartsOption } from 'echarts';
 export interface CategoryDef {
   name: string; // 分类名称
   color: string; // 节点颜色
-  symbol: string; // ECharts 图形:circle / rect / diamond / roundRect
+  symbol: string; // ECharts 图形:circle / rect / diamond / roundRect / pin
   symbolSize: number; // 节点大小
 }
 
-/** 2 类节点:测风点位(注册的 level=4 测风点)、数据布点(Windrect 测风装置) */
+/** 3 类节点:部门(组织树,只读)、测风点位、数据布点 */
 export const categories: CategoryDef[] = [
+  { name: '部门', color: '#722ed1', symbol: 'diamond', symbolSize: 26 },
   { name: '测风点位', color: '#fa8c16', symbol: 'pin', symbolSize: 30 },
-  { name: '数据布点', color: '#1890ff', symbol: 'roundRect', symbolSize: 28 },
+  { name: '数据布点', color: '#1890ff', symbol: 'roundRect', symbolSize: 26 },
 ];
 
 // ==================== 状态颜色映射 ====================
@@ -23,14 +24,11 @@ export const statusColorMap: Record<string, string> = {
   offline: '#999999', // 离线
 };
 
-// ==================== 两行布局参数 ====================
+// ==================== 三层布局参数 ====================
 
-/** 每个分类在画布高度中的百分比 y 坐标(从上到下):测风点位在上、数据布点在下 */
-export const levelY = [30, 70];
-
-/** 每类节点用于排序的权重字段(值越大越靠左) */
+/** 每类节点用于排序的权重字段(值越大越靠左):测风点位按已绑定数据布点数排序 */
 export const weightKey: Record<number, string> = {
-  0: 'childCount', // 测风点位:按已绑定数据布点数排序
+  1: 'childCount',
 };
 
 /** 节点横向间距基数(px),实际 = basePadding / 该层节点数 */
@@ -80,11 +78,16 @@ export interface DetailField {
 }
 
 export const nodeDetailFields: Record<string, DetailField[]> = {
+  部门: [
+    { label: '部门名称', key: 'name' },
+    { label: '是否为矿井', key: 'isLeafText' },
+  ],
   测风点位: [
     { label: '测风点位名称', key: 'name' },
-    { label: '风量(m³/min)', key: 'windVolume' },
+    { label: '所属矿井', key: 'mineName' },
+    { label: '矿编码', key: 'mineCode' },
+    { label: '风量(m³/min)', key: 'airVolume' },
     { label: '已绑定数据布点数', key: 'childCount' },
-    { label: '测风设备ID', key: 'windrectId' },
   ],
   数据布点: [
     { label: '设备编码', key: 'deviceCode' },
@@ -98,12 +101,16 @@ export const nodeDetailFields: Record<string, DetailField[]> = {
 
 export interface TopoNodeData {
   id: string;
+  /** 原始实体 id(MineArea/Windrect/部门 id),绑定/解绑调 updateMineArea 时使用 */
+  rawId?: string;
   name: string;
   category: number; // categories 索引
+  parentId?: string | null;
+  isLeaf?: boolean;
+  isLeafText?: string;
   status?: string; // normal / abnormal / offline
-  windVolume?: number;
+  airVolume?: number;
   childCount?: number;
-  windrectId?: string;
   mineCode?: string;
   deviceCode?: string;
   devicePos?: string;
@@ -115,7 +122,8 @@ export interface TopoLinkData {
   source: string;
   target: string;
   label?: string;
-  id?: string; // 关联关系数据库主键,用于解绑
+  /** 连线类型:org=部门内部 / mine=测风点位→矿 / sensor=测风点位→数据布点 */
+  kind?: 'org' | 'mine' | 'sensor';
 }
 
 export interface TopologyData {
@@ -125,7 +133,48 @@ export interface TopologyData {
 
 // ==================== API 响应类型 ====================
 
-/** API -> MineArea 节点(注册的测风点位) */
+/** 组织树节点(部门拓扑,只读) */
+export interface OrgPathNode {
+  id: string;
+  departName: string;
+  parentId: string | null;
+  isLeaf: boolean;
+  /** 矿井编码(仅矿井叶子节点有值) */
+  fax?: string;
+  /** 树层级深度(根=0,子级+1),用于按层级分行布局 */
+  depth: number;
+}
+
+/**
+ * 递归收集以 depart 为根的组织子树(含 depart 自身及其全部后代),作为部门层节点。
+ * 数据来源:useMineDepartmentStore.getRoot(当前用户部门)。
+ */
+export function collectOrgSubtree(depart?: any): OrgPathNode[] {
+  if (!depart) return [];
+  const result: OrgPathNode[] = [];
+  const seen = new Set<string>();
+  const walk = (node: any, depth: number) => {
+    if (!node) return;
+    // 去重兜底,避免组织树数据异常导致节点重复
+    if (seen.has(node.id)) return;
+    seen.add(node.id);
+    result.push({
+      id: node.id,
+      departName: node.departName,
+      parentId: node.parentId ?? null,
+      isLeaf: !!node.isLeaf,
+      fax: node.fax,
+      depth,
+    });
+    if (Array.isArray(node.childDepart)) {
+      for (const child of node.childDepart) walk(child, depth + 1);
+    }
+  };
+  walk(depart, 0);
+  return result;
+}
+
+/** API -> MineArea(测风点位,level=4 为测风点) */
 export interface MineAreaNode {
   id: string;
   mineCode: string;
@@ -133,22 +182,12 @@ export interface MineAreaNode {
   level: number; // 1=省局 2=执法处 3=煤矿 4=测风点
   airVolume: number;
   regulationId?: string;
-  windrectId: string;
+  windrectId?: string;
   createTime?: string;
   updateTime?: string;
   [key: string]: any;
 }
 
-/** API -> MineArea 关联关系 */
-export interface MineAreaRelation {
-  id: string;
-  mineCode: string;
-  parentId: string;
-  childId: string;
-  createTime?: string;
-  updateTime?: string;
-}
-
 /** API -> Windrect(数据布点/测风装置) */
 export interface WindrectNode {
   id: string;
@@ -161,10 +200,10 @@ export interface WindrectNode {
   [key: string]: any;
 }
 
-/** API -> getTopologyData 合并返回值 */
+/** API -> getTopologyData 合并返回值(关系接口已弃用,mineAreaRelationList 恒为空) */
 export interface MineAreaApiResponse {
   mineAreaList: MineAreaNode[];
-  mineAreaRelationList: MineAreaRelation[];
+  mineAreaRelationList: never[];
   windrectList: WindrectNode[];
 }
 
@@ -176,52 +215,118 @@ export const statusTextMap: Record<string, string> = {
 };
 
 /**
- * 将 API 响应转换为 TopologyData
- * mineAreaList(level=4) -> 测风点位节点,windrectList -> 数据布点节点,
- * mineAreaRelationList(parentId=测风点位, childId=数据布点) -> 边
+ * 将 API 响应与组织子树转换为 TopologyData(不使用 MineAreaRelation 关系表)
+ * 三类节点:部门(orgNodes,只读)、测风点位(getMineAreaList 全部记录,与测风点位管理列表一致)、数据布点(Windrect)
+ * 三类边(按字段派生):
+ *   1) 部门内部(org parent → child)
+ *   2) 测风点位 → 矿(area.mineCode 匹配部门 fax)
+ *   3) 测风点位 → 数据布点(area.windrectId 匹配数据布点 id)
+ * 节点 id 按分类前缀(org:/point:/device:)保证全局唯一,过滤缺失 id 并按原始 id 去重,
+ * 避免 ECharts graph 因重复/缺失 id 报错;rawId 保留原始实体 id 供接口调用。
  */
-export function transformToTopologyData(apiResp: MineAreaApiResponse): TopologyData {
-  const { mineAreaList = [], mineAreaRelationList = [], windrectList = [] } = apiResp;
-
-  const pointIds = new Set<string>(
-    mineAreaList.filter((item) => Number(item.level) === 4).map((item) => item.id)
-  );
-  const deviceIds = new Set<string>(windrectList.map((item) => item.id));
-
-  // 边:仅保留两端都能在图中找到的绑定关系(测风点位→数据布点)
-  const childCountMap: Record<string, number> = {};
-  const links: TopoLinkData[] = mineAreaRelationList
-    .filter((rel) => pointIds.has(rel.parentId) && deviceIds.has(rel.childId))
-    .map((rel) => {
-      childCountMap[rel.parentId] = (childCountMap[rel.parentId] || 0) + 1;
-      return { source: rel.parentId, target: rel.childId, id: rel.id };
-    });
+export function transformToTopologyData(
+  orgNodes: OrgPathNode[],
+  mineAreas: MineAreaNode[],
+  windrects: WindrectNode[]
+): TopologyData {
+  const prefix = (cat: string, id: any) => (id === undefined || id === null || id === '' ? '' : `${cat}:${id}`);
+
+  // 部门节点:过滤缺失 id + 去重
+  const orgById = new Map<string, OrgPathNode>();
+  for (const o of orgNodes) {
+    if (!o.id) continue;
+    const pid = prefix('org', o.id);
+    if (!orgById.has(pid)) orgById.set(pid, o);
+  }
+  const faxToOrgId = new Map<string, string>();
+  for (const o of orgById.values()) {
+    if (o.fax) faxToOrgId.set(o.fax, prefix('org', o.id));
+  }
+
+  // 测风点位:getMineAreaList 返回的全部记录(与测风点位管理列表一致),过滤缺失 id + 去重
+  const pointById = new Map<string, MineAreaNode>();
+  for (const a of mineAreas) {
+    if (!a.id) continue;
+    const pid = prefix('point', a.id);
+    if (!pointById.has(pid)) pointById.set(pid, a);
+  }
+  const pointRawToId = new Map<string, string>();
+  for (const [pid, a] of pointById) pointRawToId.set(a.id, pid);
+
+  // 数据布点:过滤缺失 id + 去重
+  const deviceById = new Map<string, WindrectNode>();
+  for (const d of windrects) {
+    if (!d.id) continue;
+    const pid = prefix('device', d.id);
+    if (!deviceById.has(pid)) deviceById.set(pid, d);
+  }
+  const deviceRawToId = new Map<string, string>();
+  for (const [pid, d] of deviceById) deviceRawToId.set(d.id, pid);
+
+  const links: TopoLinkData[] = [];
+  // 测风点位已绑定数据布点数(一对一约束用)
+  const areaSensorCount: Record<string, number> = {};
+
+  // 1) 部门内部边
+  for (const o of orgById.values()) {
+    const pid = prefix('org', o.id);
+    if (o.parentId && orgById.has(prefix('org', o.parentId))) {
+      links.push({ source: prefix('org', o.parentId), target: pid, kind: 'org' });
+    }
+  }
+  // 2) 测风点位 → 矿
+  for (const [pid, a] of pointById) {
+    const orgId = a.mineCode ? faxToOrgId.get(a.mineCode) : undefined;
+    if (orgId) links.push({ source: pid, target: orgId, kind: 'mine' });
+  }
+  // 3) 测风点位 → 数据布点
+  for (const [pid, a] of pointById) {
+    const deviceId = a.windrectId ? deviceRawToId.get(a.windrectId) : undefined;
+    if (deviceId) {
+      links.push({ source: pid, target: deviceId, kind: 'sensor' });
+      areaSensorCount[pid] = (areaSensorCount[pid] || 0) + 1;
+    }
+  }
 
   const nodes: TopoNodeData[] = [
+    // 部门节点(只读)
+    ...Array.from(orgById.entries()).map(([pid, o]) => ({
+      id: pid,
+      rawId: o.id,
+      name: o.departName,
+      category: 0,
+      parentId: o.parentId,
+      isLeaf: o.isLeaf,
+      isLeafText: o.isLeaf ? '是' : '否',
+      depth: o.depth ?? 0,
+    })),
     // 测风点位节点
-    ...mineAreaList
-      .filter((item) => Number(item.level) === 4)
-      .map((item) => ({
-        id: item.id,
-        name: item.name,
-        category: 0,
-        status: 'normal',
-        windVolume: item.airVolume,
-        childCount: childCountMap[item.id] || 0,
-        windrectId: item.windrectId,
-        mineCode: item.mineCode,
-      })),
-    // 数据布点节点(Windrect 测风装置)
-    ...windrectList.map((item) => ({
-      id: item.id,
-      name: item.devicePos || item.deviceCode || item.id,
-      category: 1,
-      status: String(item.status) === '1' ? 'normal' : 'offline',
-      statusText: String(item.status) === '1' ? '正常' : '停用',
-      deviceCode: item.deviceCode,
-      devicePos: item.devicePos,
-      mineName: item.mineName,
-      mineCode: item.mineCode,
+    ...Array.from(pointById.entries()).map(([pid, a]) => {
+      const orgId = a.mineCode ? faxToOrgId.get(a.mineCode) : undefined;
+      return {
+        id: pid,
+        rawId: a.id,
+        name: a.name,
+        category: 1,
+        mineCode: a.mineCode,
+        mineName: orgId ? orgById.get(orgId)?.departName || a.mineCode : '',
+        airVolume: a.airVolume,
+        windrectId: a.windrectId,
+        childCount: areaSensorCount[pid] || 0,
+      };
+    }),
+    // 数据布点节点
+    ...Array.from(deviceById.entries()).map(([pid, d]) => ({
+      id: pid,
+      rawId: d.id,
+      name: d.devicePos || d.deviceCode || d.id,
+      category: 2,
+      deviceCode: d.deviceCode,
+      devicePos: d.devicePos,
+      mineName: d.mineName,
+      mineCode: d.mineCode,
+      status: String(d.status) === '1' ? 'normal' : 'offline',
+      statusText: String(d.status) === '1' ? '正常' : '停用',
     })),
   ];
 

+ 53 - 69
src/views/dashboard/HiddenSurface/configurable.data.ts

@@ -15,7 +15,7 @@ export const testConfigProvinceMonitor: Config[] = [
         show: true,
         readFrom: '',
         selector: { show: false, value: '' },
-        slot: { show: true, value: '(总计:${totalMines})' },
+        slot: { show: true, value: '' },
       },
       background: { show: false, type: 'video', link: '' },
       layout: {
@@ -35,34 +35,34 @@ export const testConfigProvinceMonitor: Config[] = [
           items: [
             {
               label: '接入矿井总数',
-              value: '386',
+              value: '${accessMineNum}',
               to: '/basicinfo/mines-info',
             },
             {
-              label: '应接入测风点',
-              value: '1380',
-              to: '/basicinfo/mines-info',
-            },
-            {
-              label: '在线测风点',
-              value: '1245',
-              to: '/basicinfo/mines-info',
-            },
-            {
-              label: '掉线测风点',
-              value: '135',
+              label: '测风点在线数',
+              value: '${areaOnlineNum}',
               to: '/basicinfo/mines-info',
             },
             {
               label: '风量异常矿井',
-              value: '23',
+              value: '${exceptionNum}',
               to: '/warningAnalysis/globalAlarm',
             },
             {
               label: '疑似隐蔽工作面',
-              value: '8',
+              value: '${suspectedNum}',
               to: '/warningAnalysis/globalAlarm',
             },
+            {
+              label: '测风点总数',
+              value: '${areaNum}',
+              to: '/basicinfo/mines-info',
+            },
+            {
+              label: '掉线测风点',
+              value: '${disconnectNum.length}',
+              to: '/basicinfo/mines-info',
+            },
           ],
         },
       ],
@@ -96,10 +96,10 @@ export const testConfigProvinceMonitor: Config[] = [
           type: 'B',
           readFrom: 'enforceOffices',
           columns: [
-            { name: '执法处', prop: 'officeName' },
-            { name: '矿井数', prop: 'mineCount' },
-            { name: '风量异常', prop: 'abnormalCount' },
-            { name: '疑似', prop: 'suspectedCount' },
+            { name: '执法处', prop: 'name' },
+            { name: '矿井数', prop: 'mineNum' },
+            { name: '风量异常', prop: 'exceptionNum' },
+            { name: '疑似', prop: 'suspectedNum' },
           ],
         },
       ],
@@ -140,24 +140,13 @@ export const testConfigProvinceMonitor: Config[] = [
           readFrom: 'windSensors',
           columns: [
             { name: '矿井名称', prop: 'mineName' },
-            { name: '测点名称', prop: 'pointName' },
-            { name: '风速(m/s)', prop: 'windSpeed' },
-            { name: '风量(m³/min)', prop: 'windVolume' },
+            { name: '测风地点', prop: 'mineAreaName' },
+            { name: '风速(m/s)', prop: 'va' },
+            { name: '风量(m³/min)', prop: 'm3' },
             // { name: '状态', prop: 'status' },
           ],
         },
       ],
-      // 暂无接口对接,使用模拟数据先行展示
-      mock: {
-        windSensors: [
-          { mineName: '大同一矿', pointName: '主井进风', windSpeed: 5.2, windVolume: 2850, status: '正常' },
-          { mineName: '阳泉二矿', pointName: '副井进风', windSpeed: 4.8, windVolume: 2630, status: '正常' },
-          { mineName: '朔州三矿', pointName: '北翼回风', windSpeed: 6.8, windVolume: 3720, status: '异常' },
-          { mineName: '临汾一矿', pointName: '南翼回风', windSpeed: 4.1, windVolume: 2250, status: '正常' },
-          { mineName: '晋中二矿', pointName: '1201工作面', windSpeed: 3.5, windVolume: 1920, status: '正常' },
-          { mineName: '长治一矿', pointName: '1202工作面', windSpeed: 0, windVolume: 0, status: '缺失' },
-        ],
-      },
     },
     showStyle: {
       size: 'width:400px;height:580px;',
@@ -192,24 +181,14 @@ export const testConfigProvinceMonitor: Config[] = [
           readFrom: 'hiddenWorkfaces',
           columns: [
             { name: '矿井名称', prop: 'mineName' },
-            { name: '工作面名称', prop: 'goafName' },
-            // { name: '风险等级', prop: 'riskLevel' },
-            { name: '判识类型', prop: 'identifyType' },
-            { name: '发现时间', prop: 'detectTime' },
+            // { name: '父级组织', prop: 'parentArea' },
+            // { name: '判识结果', prop: 'judgeResult' },
+            { name: '风量闭合', prop: 'm3Closed' },
+            { name: '风量异常数', prop: 'm3Exception' },
+            // { name: '最近实时上传', prop: 'lastCdssTime' },
           ],
         },
       ],
-      // 暂无接口对接,使用模拟数据先行展示
-      mock: {
-        hiddenWorkfaces: [
-          { mineName: '朔州三矿', goafName: '北翼采空区', riskLevel: '高风险', identifyType: '风量不闭合', detectTime: '2025-07-28 11:45' },
-          { mineName: '晋中二矿', goafName: '1201采空区', riskLevel: '较高风险', identifyType: '风量较均值偏低', detectTime: '2025-07-28 09:30' },
-          { mineName: '大同一矿', goafName: '主井东侧采空区', riskLevel: '一般风险', identifyType: '进回风不闭合', detectTime: '2025-07-27 16:20' },
-          { mineName: '长治一矿', goafName: '1202采空区', riskLevel: '较高风险', identifyType: '测风点缺失', detectTime: '2025-07-27 08:00' },
-          { mineName: '临汾一矿', goafName: '南翼采空区', riskLevel: '一般风险', identifyType: '多测点关联异常', detectTime: '2025-07-26 14:10' },
-          { mineName: '阳泉二矿', goafName: '副井西侧采空区', riskLevel: '低风险', identifyType: '风量较均值偏低', detectTime: '2025-07-26 10:05' },
-        ],
-      },
     },
     showStyle: {
       size: 'width:400px;height:500px;',
@@ -253,24 +232,28 @@ export const testConfigVentilationMonitor: Config[] = [
           readFrom: '',
           items: [
             {
-              label: '总进风量(m³/min)',
-              value: '${totalInletVolume}',
+              label: '测风点总数',
+              value: '${areaNum}',
             },
             {
-              label: '总回风量(m³/min)',
-              value: '${totalOutletVolume}',
+              label: '在线测风点',
+              value: '${areaOnlineNum}',
             },
             {
-              label: '平均风速(m/s)',
-              value: '${avgWindSpeed}',
+              label: '风量异常',
+              value: '${exceptionNum}',
             },
             {
-              label: '在线测风点',
-              value: '${onlinePointCount}',
+              label: '疑似隐蔽工作面',
+              value: '${suspectedNum}',
+            },
+            {
+              label: '风速超限',
+              value: '${vaOverLimitNum}',
             },
             {
-              label: '异常测风点',
-              value: '${abnormalPointCount}',
+              label: '掉线测风点',
+              value: '${disconnectNum.length}',
             },
           ],
         },
@@ -310,12 +293,13 @@ export const testConfigVentilationMonitor: Config[] = [
           readFrom: 'identifyData',
           conclusion: '${conclusion}',
           items: [
-            { label: '风量较均值偏低', prop: 'volumeLowPercent', unit: '%' },
-            { label: '进回风不闭合差值', prop: 'unclosedDiff', unit: 'm³/min' },
-            { label: '缺失规定测风点数', prop: 'missingCount', unit: '个' },
-            { label: '多测点关联异常数', prop: 'relatedAbnormal', unit: '个' },
+            { label: '风量异常数', prop: 'm3Exception', unit: '个' },
+            { label: '风量闭合', prop: 'm3Closed' },
+            { label: '测风地点数', prop: 'mineAreaCount', unit: '个' },
+            { label: '测风装置数', prop: 'windrectCount', unit: '个' },
           ],
-          suggestions: ['请现场核查疑似隐蔽巷道的通风设施状态', '建议对该区域风量不闭合点进行复测', '缺失测风点应尽快补装并接入系统'],
+          // suggestions: ['请现场核查疑似隐蔽巷道的通风设施状态', '建议对该区域风量不闭合点进行复测', '缺失测风点应尽快补装并接入系统'],
+          suggestions: ['暂无建议'],
         },
       ],
     },
@@ -351,17 +335,17 @@ export const testConfigVentilationMonitor: Config[] = [
           type: 'D',
           readFrom: 'pointRecords',
           columns: [
-            { name: '测点编号', prop: 'pointCode' },
-            { name: '测点名称', prop: 'pointName' },
-            { name: '风速(m/s)', prop: 'windSpeed' },
-            { name: '风量(m³/min)', prop: 'windVolume' },
+            { name: '矿井名称', prop: 'mineName' },
+            { name: '测风地点', prop: 'mineAreaName' },
+            { name: '风速(m/s)', prop: 'va' },
+            { name: '风量(m³/min)', prop: 'm3' },
             // { name: '状态', prop: 'status' },
           ],
         },
       ],
     },
     showStyle: {
-      size: 'width:400px;height:300px;',
+      size: 'width:400px;height:500px;',
       version: '原版',
       position: 'top:350px;left:25px;',
     },

+ 6 - 83
src/views/dashboard/HiddenSurface/hiddenSurface.api.ts

@@ -1,86 +1,13 @@
-// 模拟数据 — 首页所有接口返回模拟数据用于展示
-// 后端接口完成后替换为真实的 defHttp 请求
+// 首页接口 — 省域/矿端首页数据
+// 已对接真实接口:getCurrentStatistics(全省监测总体情况/矿井监测总览)、getMineJudgeCount(执法处判识情况)
+// 判识结果已对接 getMineData(矿端视图);矿端报警列表暂无接口,仍返回模拟数据
 
-// ========== 省域地图监测区(图3-1)==========
+export { getCurrentStatistics, getMineJudgeCount } from '/@/api/workingface';
 
-/** 省域监测总览统计卡片 */
-export const getProvinceOverview = (params?: any) =>
-  Promise.resolve({
-    connectedMines: 386,
-    onlinePoints: 1245,
-    abnormalMines: 23,
-    suspectedWorkface: 8,
-    offlinePoints: 135,
-    totalMines: 386,
-  });
-
-/** 联网状态 — 应接入/在线/中断/未接入 统计 */
-export const getProvinceAccessCount = (params?: any) =>
-  Promise.resolve([
-    { name: '执法一处', yjNum: 84, zxNum: 80, zdNum: 2, wjNum: 2, deptId: '1' },
-    { name: '执法二处', yjNum: 72, zxNum: 68, zdNum: 3, wjNum: 1, deptId: '2' },
-    { name: '执法三处', yjNum: 65, zxNum: 60, zdNum: 3, wjNum: 2, deptId: '3' },
-    { name: '执法四处', yjNum: 78, zxNum: 75, zdNum: 1, wjNum: 2, deptId: '4' },
-    { name: '执法五处', yjNum: 56, zxNum: 52, zdNum: 2, wjNum: 2, deptId: '5' },
-  ]);
-
-/** 当日报警情况统计 */
-export const getProvinceAlarmNum = (params?: any) =>
-  Promise.resolve({
-    windSpeedOverlimit: 12,
-    volumeLow: 8,
-    unclosed: 5,
-    missingPoints: 3,
-    resolved: 6,
-  });
-
-/** 执法处判识情况列表 */
-export const getEnforceOfficeList = (params?: any) =>
-  Promise.resolve([
-    { officeName: '执法一处', mineCount: 84, onlineRate: '95%', abnormalCount: 5, suspectedCount: 2 },
-    { officeName: '执法二处', mineCount: 72, onlineRate: '94%', abnormalCount: 4, suspectedCount: 1 },
-    { officeName: '执法三处', mineCount: 65, onlineRate: '92%', abnormalCount: 7, suspectedCount: 3 },
-    { officeName: '执法四处', mineCount: 78, onlineRate: '96%', abnormalCount: 3, suspectedCount: 1 },
-    { officeName: '执法五处', mineCount: 56, onlineRate: '93%', abnormalCount: 4, suspectedCount: 1 },
-  ]);
-
-// ========== 通风系统图监测区(图3-3)==========
-
-/** 通风系统状态总览 */
-export const getVentilationStatus = (params?: any) =>
-  Promise.resolve({
-    totalInletVolume: 485,
-    totalOutletVolume: 472,
-    avgWindSpeed: 4.2,
-    onlinePointCount: 22,
-    abnormalPointCount: 3,
-  });
-
-/** 测风点实时数据列表 */
-export const getPointRealtimeData = (params?: any) =>
-  Promise.resolve({
-    records: [
-      { pointCode: 'FC001', pointName: '主井进风', monitorArea: '主井区', windSpeed: 5.2, windVolume: 2850, status: 'normal' },
-      { pointCode: 'FC002', pointName: '副井进风', monitorArea: '副井区', windSpeed: 4.8, windVolume: 2630, status: 'normal' },
-      { pointCode: 'FC003', pointName: '北翼回风', monitorArea: '北翼采区', windSpeed: 6.8, windVolume: 3720, status: 'abnormal' },
-      { pointCode: 'FC004', pointName: '南翼回风', monitorArea: '南翼采区', windSpeed: 4.1, windVolume: 2250, status: 'normal' },
-      { pointCode: 'FC005', pointName: '1201工作面', monitorArea: '1201采面', windSpeed: 3.5, windVolume: 1920, status: 'normal' },
-      { pointCode: 'FC006', pointName: '1202工作面', monitorArea: '1202采面', windSpeed: 0, windVolume: 0, status: 'missing' },
-    ],
-  });
-
-/** 判识结果 */
-export const getIdentifyResult = (params?: any) =>
-  Promise.resolve({
-    conclusion: '疑似',
-    volumeLowPercent: 15,
-    unclosedDiff: 120,
-    missingCount: 2,
-    relatedAbnormal: 1,
-  });
+// ========== 暂无接口模块(模拟数据)==========
 
 /** 矿端报警信息列表 */
-export const getMineAlarmList = (params?: any) =>
+export const getMineAlarmList = (_params?: any) =>
   Promise.resolve({
     records: [
       { alarmType: '风速超限', pointName: 'FC003', alarmDesc: '实测6.8m/s超阈值6.0m/s', alarmLevel: '二级', alarmTime: '2025-07-28 14:30' },
@@ -96,7 +23,3 @@ export const getMineAlarmList = (params?: any) =>
       { alarmType: '风速超限', pointName: 'FC003', alarmDesc: '实测6.5m/s超阈值6.0m/s', alarmLevel: '三级', alarmTime: '2025-07-27 22:10' },
     ],
   });
-
-/** 建议措施 */
-export const getSuggestions = (params?: any) =>
-  Promise.resolve(['请现场核查疑似隐蔽巷道的通风设施状态', '建议对该区域风量不闭合点进行复测', '缺失测风点应尽快补装并接入系统']);

+ 123 - 105
src/views/dashboard/HiddenSurface/index.vue

@@ -37,16 +37,9 @@
   import { computed, onUnmounted, ref, watch, shallowRef } from 'vue';
   import { useInitConfigs, useInitPage } from '@/components/Configurable/hooks/useInit';
   import { testConfigProvinceMonitor, testConfigVentilationMonitor } from './configurable.data';
-  import {
-    getProvinceOverview,
-    getProvinceAccessCount,
-    getProvinceAlarmNum,
-    getEnforceOfficeList,
-    getVentilationStatus,
-    getPointRealtimeData,
-    getIdentifyResult,
-    getMineAlarmList,
-  } from './hiddenSurface.api';
+  import { getCurrentStatistics, getMineJudgeCount } from './hiddenSurface.api';
+  import { getWindrectData } from '/@/views/monitor/sensorMonitor/sensorMonitor.api';
+  import { getMineData } from '/@/views/monitor/surfaceMonitor/surfaceMonitor.api';
   import ModulePrimary from '/@/components/Configurable/ModulePrimary.vue';
   import { useGlobSetting } from '/@/hooks/setting';
   import { useAppStore } from '/@/store/modules/app';
@@ -62,100 +55,118 @@
   const cfgs = computed(() => configs.value);
   const { configs } = useInitConfigs();
 
-  // 省域级数据汇总
+  // 省域级数据汇总(getCurrentStatistics + getMineJudgeCount + getWindrectData + getMineData)
   const provinceData = shallowRef({
-    connectedMines: 0, // 接入矿井总数
-    onlinePoints: 0, // 测风点在线数
-    abnormalMines: 0, // 风量异常矿井
-    suspectedWorkface: 0, // 疑似隐蔽工作面
-    offlinePoints: 0, // 数据掉线点
-    accessTotal: {
-      // 联网汇总
-      yjTotal: 0,
-      zxTotal: 0,
-      zdTotal: 0,
-      wjTotal: 0,
-    },
-    accessDetails: null, // 联网详情(各执法处)
+    accessMineNum: 0, // 接入矿井总数
+    areaNum: 0, // 测风点总数
+    areaOnlineNum: 0, // 测风点在线数
+    exceptionNum: 0, // 风量异常数量
+    suspectedNum: 0, // 疑似隐蔽工作面数量
+    vaOverLimitNum: 0, // 风速超限异常数量
+    m3LackNum: 0, // 风量不足异常数量
+    m3NotClosedNum: 0, // 风量不闭合异常数量
+    disconnectNum: [] as any[], // 数据掉线点列表
     enforceOffices: null, // 执法处判识情况列表
-    totalMines: 0, // 总计(header slot显示)
+    windSensors: [] as any[], // 风速风量传感器实时数据
+    hiddenWorkfaces: [] as any[], // 疑似隐蔽工作面列表
   });
 
   // 矿端数据汇总
   const mineData = shallowRef({
-    totalInletVolume: 0,
-    totalOutletVolume: 0,
-    avgWindSpeed: 0,
-    onlinePointCount: 0,
-    abnormalPointCount: 0,
-    pointRecords: null, // 测风点实时数据
+    accessMineNum: 0,
+    areaNum: 0, // 测风点总数
+    areaOnlineNum: 0, // 测风点在线数
+    exceptionNum: 0, // 风量异常数量
+    suspectedNum: 0,
+    vaOverLimitNum: 0,
+    m3LackNum: 0,
+    m3NotClosedNum: 0,
+    disconnectNum: [] as any[],
+    pointRecords: [] as any[], // 测风点实时数据
     identifyData: {
-      // 判识结果数据
+      // 判识结果数据(对接 getMineData → Mine 记录)
       conclusion: '正常',
-      volumeLowPercent: 0,
-      unclosedDiff: 0,
-      missingCount: 0,
-      relatedAbnormal: 0,
+      m3Exception: 0, // 风量异常数
+      m3Closed: '数据不足', // 风量闭合
+      mineAreaCount: 0, // 测风地点数
+      windrectCount: 0, // 测风装置数
     },
-    alarmRecords: null, // 报警信息列表
   });
 
+  // ===== 枚举映射(CustomTable 仅展示文本,状态/结果预先转换为文案)=====
+  const windrectStatusMap: Record<string, string> = { '0': '正常', '128': '传感器故障' };
+  const judgeResultMap: Record<string, string> = { '0': '数据不足', '1': '疑似', '2': '正常' };
+  const m3ClosedMap: Record<string, string> = { '0': '数据不足', '1': '不闭合', '2': '闭合' };
+
+  /** WindrectData 分页记录 → 首页风速风量传感器表格行 */
+  function mapWindrectRecords(records: any[]) {
+    return (records || []).map((r) => ({
+      mineName: r.mineName,
+      mineAreaName: r.mineAreaName,
+      va: r.va,
+      m3: r.m3,
+      status: windrectStatusMap[String(r.status)] ?? '-',
+      readTime: r.readTime,
+    }));
+  }
+
+  /** Mine 分页记录(judgeResult=1 疑似)→ 首页隐蔽工作面表格行 */
+  function mapSuspectedMines(records: any[]) {
+    return (records || [])
+      .filter((r) => Number(r.judgeResult) === 1)
+      .map((r) => ({
+        mineName: r.mineName,
+        parentArea: r.parentArea,
+        judgeResult: judgeResultMap[String(r.judgeResult)] ?? '-',
+        m3Closed: m3ClosedMap[String(r.m3Closed)] ?? '-',
+        m3Exception: r.m3Exception ?? '-',
+        lastCdssTime: r.lastCdssTime,
+      }));
+  }
+
   async function fetchHomeData() {
     try {
-      const [overview, accessResult, alarmResult, officeList] = await Promise.all([
-        getProvinceOverview(appStore.simpleMapParams)
-          .then((r) => r)
-          .catch((e) => {
-            console.error('省域总览数据获取失败:', e);
-            return null;
-          }),
-
-        getProvinceAccessCount(appStore.simpleMapParams)
-          .then((r) => {
-            let yjTotal = 0,
-              zxTotal = 0,
-              zdTotal = 0,
-              wjTotal = 0;
-            if (Array.isArray(r)) {
-              r.forEach((item) => {
-                yjTotal += Number(item.yjNum) || 0;
-                zxTotal += Number(item.zxNum) || 0;
-                zdTotal += Number(item.zdNum) || 0;
-                wjTotal += Number(item.wjNum) || 0;
-              });
-            }
-            return { accessDetails: r, accessTotal: { yjTotal, zxTotal, zdTotal, wjTotal } };
-          })
+      const deptId = appStore.getSimpleMapParams?.deptId;
+      const [statistics, judgeList, windrectPage, minePage] = await Promise.all([
+        getCurrentStatistics({ deptId })
+          .then((r) => r || {})
           .catch((e) => {
-            console.error('联网状态数据获取失败:', e);
-            return { accessDetails: null, accessTotal: { yjTotal: 0, zxTotal: 0, zdTotal: 0, wjTotal: 0 } };
+            console.error('全省监测总体情况获取失败:', e);
+            return {};
           }),
-
-        getProvinceAlarmNum(appStore.simpleMapParams)
+        getMineJudgeCount({ deptId })
           .then((r) => r)
           .catch((e) => {
-            console.error('报警统计获取失败:', e);
+            console.error('执法处判识情况获取失败:', e);
             return null;
           }),
-
-        getEnforceOfficeList(appStore.simpleMapParams)
-          .then((r) => r)
+        getWindrectData({ deptId, pageNo: 1, pageSize: 20 })
+          .then((r) => mapWindrectRecords(r?.records))
           .catch((e) => {
-            console.error('执法处列表获取失败:', e);
-            return null;
+            console.error('风速风量传感器数据获取失败:', e);
+            return [];
+          }),
+        getMineData({ deptId, pageNo: 1, pageSize: 20, judgeResult: 1 })
+          .then((r) => mapSuspectedMines(r?.records))
+          .catch((e) => {
+            console.error('隐蔽工作面数据获取失败:', e);
+            return [];
           }),
       ]);
 
       provinceData.value = {
-        connectedMines: overview?.connectedMines ?? 0,
-        onlinePoints: overview?.onlinePoints ?? 0,
-        abnormalMines: overview?.abnormalMines ?? 0,
-        suspectedWorkface: overview?.suspectedWorkface ?? 0,
-        offlinePoints: overview?.offlinePoints ?? 0,
-        totalMines: overview?.connectedMines ?? 0,
-        ...(alarmResult ?? {}),
-        ...accessResult,
-        enforceOffices: officeList,
+        accessMineNum: statistics.accessMineNum ?? 0,
+        areaNum: statistics.areaNum ?? 0,
+        areaOnlineNum: statistics.areaOnlineNum ?? 0,
+        exceptionNum: statistics.exceptionNum ?? 0,
+        suspectedNum: statistics.suspectedNum ?? 0,
+        vaOverLimitNum: statistics.vaOverLimitNum ?? 0,
+        m3LackNum: statistics.m3LackNum ?? 0,
+        m3NotClosedNum: statistics.m3NotClosedNum ?? 0,
+        disconnectNum: Array.isArray(statistics.disconnectNum) ? statistics.disconnectNum : [],
+        enforceOffices: judgeList,
+        windSensors: windrectPage,
+        hiddenWorkfaces: minePage,
       };
       updateData(provinceData.value);
     } catch (error) {
@@ -165,45 +176,52 @@
 
   async function fetchLeafData() {
     try {
-      const [ventilationStatus, pointData, identifyResult, alarmList, suggestData] = await Promise.all([
-        getVentilationStatus(appStore.simpleMapParams)
+      const deptId = appStore.getSimpleMapParams?.deptId;
+      const [statistics, identifyResult, windrectPage] = await Promise.all([
+        getCurrentStatistics({ deptId })
           .then((r) => r || {})
           .catch((e) => {
-            console.error('通风系统状态获取失败:', e);
+            console.error('矿井监测总览获取失败:', e);
             return {};
           }),
 
-        getPointRealtimeData(appStore.simpleMapParams)
-          .then((r) => r)
-          .catch((e) => {
-            console.error('测风点实时数据获取失败:', e);
-            return null;
-          }),
-
-        getIdentifyResult(appStore.simpleMapParams)
-          .then((r) => r || { conclusion: '正常', volumeLowPercent: 0, unclosedDiff: 0, missingCount: 0, relatedAbnormal: 0 })
+        getMineData({ deptId, pageNo: 1, pageSize: 1 })
+          .then((r) => {
+            const mine = r?.records?.[0] || {};
+            // 判识结果对接 getMineData:judgeResult → 结论,其余取 Mine 真实字段
+            return {
+              conclusion: judgeResultMap[String(mine.judgeResult)] ?? '数据不足',
+              m3Exception: mine.m3Exception ?? 0,
+              m3Closed: m3ClosedMap[String(mine.m3Closed)] ?? '数据不足',
+              mineAreaCount: Array.isArray(mine.mineAreaList) ? mine.mineAreaList.length : 0,
+              windrectCount: Array.isArray(mine.windrectList) ? mine.windrectList.length : 0,
+            };
+          })
           .catch((e) => {
             console.error('判识结果获取失败:', e);
-            return { conclusion: '正常', volumeLowPercent: 0, unclosedDiff: 0, missingCount: 0, relatedAbnormal: 0 };
+            return { conclusion: '正常', m3Exception: 0, m3Closed: '数据不足', mineAreaCount: 0, windrectCount: 0 };
           }),
 
-        getMineAlarmList(appStore.simpleMapParams)
-          .then((r) => r)
+        getWindrectData({ deptId, pageNo: 1, pageSize: 20 })
+          .then((r) => mapWindrectRecords(r?.records))
           .catch((e) => {
-            console.error('报警列表获取失败:', e);
-            return null;
+            console.error('测风点实时数据获取失败:', e);
+            return [];
           }),
       ]);
 
       mineData.value = {
-        totalInletVolume: ventilationStatus?.totalInletVolume ?? 0,
-        totalOutletVolume: ventilationStatus?.totalOutletVolume ?? 0,
-        avgWindSpeed: ventilationStatus?.avgWindSpeed ?? 0,
-        onlinePointCount: ventilationStatus?.onlinePointCount ?? 0,
-        abnormalPointCount: ventilationStatus?.abnormalPointCount ?? 0,
-        pointRecords: pointData?.records ?? pointData,
-        identifyData: identifyResult || { conclusion: '正常', volumeLowPercent: 0, unclosedDiff: 0, missingCount: 0, relatedAbnormal: 0 },
-        alarmRecords: alarmList?.records ?? alarmList,
+        accessMineNum: statistics.accessMineNum ?? 0,
+        areaNum: statistics.areaNum ?? 0,
+        areaOnlineNum: statistics.areaOnlineNum ?? 0,
+        exceptionNum: statistics.exceptionNum ?? 0,
+        suspectedNum: statistics.suspectedNum ?? 0,
+        vaOverLimitNum: statistics.vaOverLimitNum ?? 0,
+        m3LackNum: statistics.m3LackNum ?? 0,
+        m3NotClosedNum: statistics.m3NotClosedNum ?? 0,
+        disconnectNum: Array.isArray(statistics.disconnectNum) ? statistics.disconnectNum : [],
+        pointRecords: windrectPage,
+        identifyData: identifyResult || { conclusion: '正常', m3Exception: 0, m3Closed: '数据不足', mineAreaCount: 0, windrectCount: 0 },
       };
       updateData(mineData.value);
     } catch (error) {

+ 1 - 1
src/views/dashboard/Overhaul/overhaul.api.ts

@@ -4,7 +4,7 @@ enum Api {
   getGoafAlarmNum = '/province/homeData/getGoafAlarmNum',
   getGoafData = '/province/device/getGoafData',
   getProvinceAlarm = '/province/alarm/getProvinceAlarm',
-  getMineData = '/province/mineData/getMineData',
+  getMineData = '/workingface/mineData/getMineData',
   // 接入统计
   getGoafAccessCount = '/ventanaly-province/province/mineData/getGoafAccessCount',
   getRealAlarmNum = '/ventanaly-province/province/mineData/getRealAlarmNum',

+ 1 - 1
src/views/dashboard/basicInfo/basicInfo.api.ts

@@ -2,7 +2,7 @@ import { defHttp } from '/@/utils/http/axios';
 
 enum Api {
   // 矿山信息
-  getMineData = '/ventanaly-province/province/mineData/getMineData',
+  getMineData = '/workingface/mineData/getMineData',
   // 接入统计
   getGoafAccessCount = '/ventanaly-province/province/mineData/getGoafAccessCount',
   // 数据质量

+ 1 - 11
src/views/dashboard/basicInfo/minesInfo/index.vue

@@ -15,16 +15,6 @@
         <SvgIcon name="chart" />
       </button>
     </template>
-    <template #expandedRowRender="{ record }">
-      <BasicTable :dataSource="record.coalSeamList" :columns="coalSeamColumns" :pagination="false">
-        <!-- <template #action="{ record }">
-            <button @click="handleEdit(record, 'goaf')" class="action-btn" title="编辑">
-              <SvgIcon name="edit" />
-            </button>
-          </template> -->
-        <template #emptyText> 暂无数据 </template>
-      </BasicTable>
-    </template>
   </BasicTable>
 </template>
 
@@ -33,7 +23,7 @@
   import { BasicTable } from '/@/components/Table';
   import { SvgIcon } from '/@/components/Icon';
   // 引入动态列/表单配置函数 + 类型
-  import { coalSeamColumns, columns, searchFormSchema } from './minesInfo.data';
+  import { columns, searchFormSchema } from './minesInfo.data';
   import { getMineData } from '../basicInfo.api';
   // 引入字典获取方法
   import { useListPage } from '/@/hooks/system/useListPage';

+ 47 - 87
src/views/dashboard/basicInfo/minesInfo/minesInfo.data.ts

@@ -26,57 +26,59 @@ export type ProductionStatusMap = Record<
 
 // 3. 生成动态表格列(接收动态状态映射)
 export const columns: BasicColumn[] = [
-  {
-    title: '煤矿编号',
-    dataIndex: 'mineCode',
-    width: 100,
-  },
-  {
-    title: '所属上级',
-    dataIndex: 'parentArea',
-    width: 100,
-  },
   {
     title: '煤矿名称',
     dataIndex: 'mineName',
     width: 200,
   },
   {
-    title: '煤矿简称',
-    dataIndex: 'mineNameAbbr',
+    title: '生产状态',
+    dataIndex: 'gjMineStatus',
     width: 100,
-    ifShow: false,
   },
   {
-    title: '生产状态',
-    dataIndex: 'gjMineStatus',
+    title: '判识结果',
+    dataIndex: 'judgeResult',
     width: 100,
-    // customRender: ({ record }) => {
-    //   // 空值/异常值处理
-    //   const status = String(record.productionStatus || '');
-    //   // 从动态映射取值,兜底未知状态
-    //   const { label, color } = dynamicStatusMap.value[status] || {
-    //     label: '-',
-    //   };
-    //   // 渲染带颜色的文字
-    //   return h(
-    //     'span',
-    //     {
-    //       style: { color: StatusColorEnum[color] },
-    //     },
-    //     label
-    //   );
-    // },
+    customRender: ({ record }) => {
+      const text = String(record.judgeResult);
+      if (isEmpty(text)) return h('span', '-');
+      const map: Record<string, { label: string; color: string }> = {
+        '0': { label: '数据不足', color: StatusColorEnum.gray },
+        '1': { label: '疑似', color: StatusColorEnum.red },
+        '2': { label: '正常', color: StatusColorEnum.green },
+      };
+      const info = map[text] || { label: '-', color: StatusColorEnum.gray };
+      return h('span', { style: { color: info.color } }, info.label);
+    },
   },
   {
-    title: '煤层自燃倾向性',
-    dataIndex: 'fire',
+    title: '风量闭合状态',
+    dataIndex: 'm3Closed',
     width: 100,
+    customRender: ({ record }) => {
+      const text = String(record.m3Closed);
+      if (isEmpty(text)) return h('span', '-');
+      const map: Record<string, { label: string; color: string }> = {
+        '0': { label: '数据不足', color: StatusColorEnum.gray },
+        '1': { label: '不闭合', color: StatusColorEnum.red },
+        '2': { label: '闭合', color: StatusColorEnum.green },
+      };
+      const info = map[text] || { label: '-', color: StatusColorEnum.gray };
+      return h('span', { style: { color: info.color } }, info.label);
+    },
   },
   {
-    title: '瓦斯等级',
-    dataIndex: 'gasLevelName',
+    title: '测风地点数',
+    dataIndex: 'mineAreaList',
     width: 100,
+    customRender: ({ text }) => (Array.isArray(text) ? text.length : '-'),
+  },
+  {
+    title: '测风装置数',
+    dataIndex: 'windrectList',
+    width: 100,
+    customRender: ({ text }) => (Array.isArray(text) ? text.length : '-'),
   },
   {
     title: '接入状态',
@@ -107,21 +109,6 @@ export const columns: BasicColumn[] = [
       return h('span', { style: { color: textColor } }, text);
     },
   },
-  // {
-  //   title: '应接数量',
-  //   dataIndex: 'yingjieNum',
-  //   width: 100,
-  // },
-  {
-    title: '已接数量',
-    dataIndex: 'accessGoafNum',
-    width: 100,
-  },
-  // {
-  //   title: '未接数量',
-  //   dataIndex: 'notAccessGoafNum',
-  //   width: 100,
-  // },
 ];
 
 // 4. 生成动态搜索表单配置(接收动态下拉选项)
@@ -178,56 +165,29 @@ export const searchFormSchema: FormSchema[] = [
     colProps: { span: 6 },
   },
   {
-    field: 'gasLevelName',
-    label: '瓦斯等级',
+    field: 'judgeResult',
+    label: '判识结果',
     component: 'Select',
     componentProps: {
       options: [
-        { label: '低瓦斯', value: '低瓦斯' },
-        { label: '高瓦斯', value: '高瓦斯' },
-        { label: '突出', value: '突出' },
+        { label: '数据不足', value: '0' },
+        { label: '疑似', value: '1' },
+        { label: '正常', value: '2' },
       ],
     },
     colProps: { span: 6 },
   },
   {
-    field: 'riskLevel',
-    label: '风险等级',
+    field: 'm3Closed',
+    label: '风量闭合',
     component: 'Select',
     componentProps: {
       options: [
-        { label: 'Ⅰ类容易自燃', value: '0' },
-        { label: 'Ⅱ类自燃', value: '1' },
-        { label: 'Ⅲ类不易自燃', value: '2' },
+        { label: '数据不足', value: '0' },
+        { label: '不闭合', value: '1' },
+        { label: '闭合', value: '2' },
       ],
     },
     colProps: { span: 6 },
-    show: false,
-  },
-  {
-    field: 'fire',
-    label: '煤层自燃倾向性',
-    component: 'Select',
-    componentProps: {
-      options: [
-        { label: '不易自燃', value: '不易自燃' },
-        { label: '容易自燃', value: '容易自燃' },
-        { label: '未检测', value: '未检测' },
-        { label: '自燃', value: '自燃' },
-      ],
-    },
-    colProps: { span: 6 },
-  },
-];
-
-// 3. 生成动态表格列(接收动态状态映射)
-export const coalSeamColumns: BasicColumn[] = [
-  {
-    title: '煤层名称',
-    dataIndex: 'coalSeamName',
-  },
-  {
-    title: '自燃倾向性',
-    dataIndex: 'coalSeamLevel',
   },
 ];

+ 26 - 5
src/views/monitor/sensorMonitor/index.vue

@@ -1,10 +1,10 @@
 <!-- eslint-disable vue/multi-word-component-names -->
 <template>
   <div class="sensor-monitor">
-    <!-- 顶部统计卡片(占位,接口就绪后接入) -->
+    <!-- 顶部统计卡片(getCurrentStatistics) -->
     <div class="board-info">
       <MiniBoard
-        v-for="(item, index) in boardData"
+        v-for="(item, index) in boardDataRef"
         :key="index"
         type="A"
         :label="item.label"
@@ -18,13 +18,13 @@
     <Tabs v-model:activeKey="activeTab" type="line" class="common-page-tabs">
       <TabPane tab="实时监测" key="realtime">
         <BasicTable @register="registerRealtimeTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }">
-          <template #action="{ record }">
+          <!-- <template #action="{ record }">
             <div class="action-buttons">
               <button @click="handleViewChart(record)" class="action-btn" title="图表">
                 <SvgIcon name="chart" />
               </button>
             </div>
-          </template>
+          </template> -->
           <template #resetBefore>
             <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="onExportXlsTime"> 导出 </a-button>
           </template>
@@ -63,6 +63,7 @@
   import { useMineDepartmentStore } from '/@/store/modules/mine';
   import { boardData, columns, historyColumns, schemas, historySchemas } from './sensorMonitor.data';
   import { getWindrectData, getWindrectHistory } from './sensorMonitor.api';
+  import { getCurrentStatistics } from '/@/api/workingface';
   import { useInitForm } from './hooks/form';
 
   // 路由参数初始化(迁移自密闭监测)
@@ -72,12 +73,31 @@
   // 激活的Tab页签
   const activeTab = ref('realtime');
 
+  // ====== 顶部统计卡片(getCurrentStatistics)======
+  const boardDataRef = ref(boardData.map((item) => ({ ...item })));
+
+  async function loadBoardData(params?: any) {
+    try {
+      const result = await getCurrentStatistics({ deptId: params?.deptId });
+      boardDataRef.value[0].value = result.areaOnlineNum ?? '-';
+      boardDataRef.value[1].value = result.areaNum ?? '-';
+      boardDataRef.value[2].value = result.vaOverLimitNum ?? '-';
+      boardDataRef.value[3].value = result.exceptionNum ?? '-';
+    } catch {
+      // 使用默认值
+    }
+  }
+
   // ====== 实时监测表格(getWindrectData)======
+  const wrappedGetWindrectData = (params) => {
+    return Promise.all([loadBoardData(params), getWindrectData(params)]).then(([__, res]) => res);
+  };
+
   const { tableContext: ctxRealtime, onExportXls: onExportXlsTime } = useListPage({
     tableProps: {
       immediate: false,
       columns,
-      api: getWindrectData,
+      api: wrappedGetWindrectData,
       formConfig: {
         autoSearch: true,
         labelWidth: 120,
@@ -104,6 +124,7 @@
       useSearchForm: true,
       bordered: true,
       showIndexColumn: false,
+      showActionColumn: false,
       actionColumn: {
         width: 90,
         title: '操作',

+ 12 - 13
src/views/monitor/sensorMonitor/sensorMonitor.data.ts

@@ -4,18 +4,18 @@ import { StatusColorEnum } from '/@/enums/jeecgEnum';
 import dayjs from 'dayjs';
 import { getWindrectList } from './sensorMonitor.api';
 
-/** 顶部统计卡片(暂为占位,接口就绪后接入) */
+/** 顶部统计卡片(getCurrentStatistics) */
 export const boardData = [
   { label: '在线测风装置', value: '-' },
-  { label: '平均风速(m/s)', value: '-' },
+  { label: '测风点总数', value: '-' },
   { label: '风速超限报警', value: '-' },
   { label: '风量异常报警', value: '-' },
 ];
 
-/** 数据状态映射(WindrectData.status) */
+/** 数据状态映射(WindrectData.status:0正常、128传感器故障) */
 export const statusMap: Record<string, string> = {
-  '1': '正常',
-  '0': '异常',
+  '0': '正常',
+  '128': '传感器故障',
 };
 
 /** 风速风量实时监测表格列(WindrectData) */
@@ -31,6 +31,11 @@ export const columns: BasicColumn[] = [
     dataIndex: 'mineCode',
     width: 110,
   },
+  {
+    title: '测风地点',
+    dataIndex: 'mineAreaName',
+    width: 140,
+  },
   {
     title: '设备编码',
     dataIndex: 'deviceCode',
@@ -54,10 +59,10 @@ export const columns: BasicColumn[] = [
   {
     title: '数据状态',
     dataIndex: 'status',
-    width: 100,
+    width: 110,
     customRender: ({ text }) => {
       const label = statusMap[String(text)] ?? '-';
-      const color = String(text) === '1' ? StatusColorEnum.green : StatusColorEnum.red;
+      const color = String(text) === '0' ? StatusColorEnum.green : StatusColorEnum.red;
       return h('span', { style: { color, fontWeight: 'bold' } }, label);
     },
   },
@@ -124,12 +129,6 @@ export const schemas: FormSchema[] = [
     component: 'Input',
     colProps: { span: 6 },
   },
-  {
-    label: '瓦斯等级',
-    field: 'gasLevelName',
-    component: 'Input',
-    colProps: { span: 6 },
-  },
 ];
 
 /** 历史数据查询表单(getWindrectHistory:startTime/endTime/windrectId 必填) */

+ 24 - 3
src/views/monitor/surfaceMonitor/index.vue

@@ -1,10 +1,10 @@
 <!-- eslint-disable vue/multi-word-component-names -->
 <template>
   <div class="surface-monitor">
-    <!-- 顶部统计卡片(占位,接口就绪后接入) -->
+    <!-- 顶部统计卡片(getCurrentStatistics) -->
     <div class="board-info">
       <MiniBoard
-        v-for="(item, index) in boardData"
+        v-for="(item, index) in boardDataRef"
         :key="index"
         type="A"
         :label="item.label"
@@ -46,6 +46,7 @@
   import { useListPage } from '/@/hooks/system/useListPage';
   import { boardData, columns, historyColumns, schemas, historySchemas } from './surfaceMonitor.data';
   import { getMineData } from './surfaceMonitor.api';
+  import { getCurrentStatistics } from '/@/api/workingface';
   import { useInitForm } from './hooks/form';
 
   // 路由参数初始化(迁移自密闭监测)
@@ -54,12 +55,32 @@
   // 激活的Tab页签
   const activeTab = ref('realtime');
 
+  // ====== 顶部统计卡片(getCurrentStatistics)======
+  const boardDataRef = ref(boardData.map((item) => ({ ...item })));
+
+  async function loadBoardData(params?: any) {
+    try {
+      const result = await getCurrentStatistics({ deptId: params?.deptId });
+      boardDataRef.value[0].value = result.accessMineNum ?? '-';
+      const onlineRate = result.areaNum ? Math.round((result.areaOnlineNum / result.areaNum) * 100) : '-';
+      boardDataRef.value[1].value = onlineRate === '-' ? '-' : `${onlineRate}%`;
+      boardDataRef.value[2].value = result.exceptionNum ?? '-';
+      boardDataRef.value[3].value = result.suspectedNum ?? '-';
+    } catch {
+      // 使用默认值
+    }
+  }
+
   // ====== 实时监测表格(getMineData)======
+  const wrappedGetMineData = (params) => {
+    return Promise.all([loadBoardData(params), getMineData(params)]).then(([__, res]) => res);
+  };
+
   const { tableContext: ctxRealtime, onExportXls: onExportXlsTime } = useListPage({
     tableProps: {
       immediate: false,
       columns,
-      api: getMineData,
+      api: wrappedGetMineData,
       formConfig: {
         autoSearch: true,
         labelWidth: 120,

+ 80 - 20
src/views/monitor/surfaceMonitor/surfaceMonitor.data.ts

@@ -16,30 +16,39 @@ export const accessStatusMap: Record<string, string> = {
   '0': '未接入',
 };
 
+/** 隐蔽工作面判断结果映射(Mine.judgeResult:0数据不足、1疑似、2正常) */
+export const judgeResultMap: Record<string, string> = {
+  '0': '数据不足',
+  '1': '疑似',
+  '2': '正常',
+};
+
+/** 风量闭合结果映射(Mine.m3Closed:0数据不足、1不闭合、2闭合) */
+export const m3ClosedMap: Record<string, string> = {
+  '0': '数据不足',
+  '1': '不闭合',
+  '2': '闭合',
+};
+
 /** 隐蔽工作面判识全览实时表格列(getMineData → Mine) */
 export const columns: BasicColumn[] = [
   {
     title: '矿井名称',
     dataIndex: 'mineName',
     fixed: 'left',
-    width: 180,
+    width: 220,
   },
   {
     title: '矿编码',
     dataIndex: 'mineCode',
     width: 110,
   },
-  {
-    title: '矿井全称',
-    dataIndex: 'corpName',
-    width: 220,
-    ellipsis: true,
-  },
-  {
-    title: '瓦斯等级',
-    dataIndex: 'gasLevelName',
-    width: 120,
-  },
+  // {
+  //   title: '矿井全称',
+  //   dataIndex: 'corpName',
+  //   width: 220,
+  //   ellipsis: true,
+  // },
   {
     title: '接入状态',
     dataIndex: 'accessStatus',
@@ -55,6 +64,37 @@ export const columns: BasicColumn[] = [
     dataIndex: 'mkMineStatus',
     width: 110,
   },
+  // {
+  //   title: '父级组织',
+  //   dataIndex: 'parentArea',
+  //   width: 120,
+  //   ellipsis: true,
+  // },
+  {
+    title: '判识结果',
+    dataIndex: 'judgeResult',
+    width: 110,
+    customRender: ({ text }) => {
+      const label = judgeResultMap[String(text)] ?? '-';
+      const color = String(text) === '1' ? StatusColorEnum.red : String(text) === '2' ? StatusColorEnum.green : StatusColorEnum.gray;
+      return h('span', { style: { color, fontWeight: 'bold' } }, label);
+    },
+  },
+  {
+    title: '风量闭合',
+    dataIndex: 'm3Closed',
+    width: 110,
+    customRender: ({ text }) => {
+      const label = m3ClosedMap[String(text)] ?? '-';
+      const color = String(text) === '1' ? StatusColorEnum.red : String(text) === '2' ? StatusColorEnum.green : StatusColorEnum.gray;
+      return h('span', { style: { color, fontWeight: 'bold' } }, label);
+    },
+  },
+  {
+    title: '风量异常数',
+    dataIndex: 'm3Exception',
+    width: 110,
+  },
   {
     title: '测风地点数',
     dataIndex: 'mineAreaList',
@@ -72,11 +112,11 @@ export const columns: BasicColumn[] = [
     dataIndex: 'lastCdssTime',
     width: 180,
   },
-  {
-    title: '上次定义数据上传时间',
-    dataIndex: 'lastCddyTime',
-    width: 180,
-  },
+  // {
+  //   title: '上次定义数据上传时间',
+  //   dataIndex: 'lastCddyTime',
+  //   width: 180,
+  // },
 ];
 
 /** 历史数据表格列(暂无接口,先用同构占位列) */
@@ -141,10 +181,30 @@ export const schemas: FormSchema[] = [
     },
   },
   {
-    label: '瓦斯等级',
-    field: 'gasLevelName',
-    component: 'Input',
+    label: '判识结果',
+    field: 'judgeResult',
+    component: 'Select',
     colProps: { span: 6 },
+    componentProps: {
+      options: [
+        { label: '数据不足', value: 0 },
+        { label: '疑似', value: 1 },
+        { label: '正常', value: 2 },
+      ],
+    },
+  },
+  {
+    label: '风量闭合',
+    field: 'm3Closed',
+    component: 'Select',
+    colProps: { span: 6 },
+    componentProps: {
+      options: [
+        { label: '数据不足', value: 0 },
+        { label: '不闭合', value: 1 },
+        { label: '闭合', value: 2 },
+      ],
+    },
   },
 ];
 

+ 7 - 7
src/views/system/cadFile/app.ts

@@ -3,7 +3,7 @@ import vjmap, { Map, GeoBounds, GeoProjection } from 'vjmap';
 import 'vjmap/dist/vjmap.min.css';
 import { useAppStore } from '/@/store/modules/app';
 import { message, Modal } from 'ant-design-vue';
-import { updatateGoaf } from './cad.api';
+import { updateMineArea } from './cad.api';
 import { createApp, h } from 'vue';
 import GoafPopup from './components/GoafPopup.vue';
 import { get, remove } from 'lodash-es';
@@ -193,7 +193,7 @@ export function useMap2D(): UseMap2DReturn {
       }, 150);
       const lnglat = marker.getLngLat();
       if (id) {
-        updatateGoaf({
+        updateMineArea({
           id,
           xcoordinate: lnglat.lng.toString(),
           ycoordinate: lnglat.lat.toString(),
@@ -279,7 +279,7 @@ export function useMap2D(): UseMap2DReturn {
         text-overflow: ellipsis;
         bottom: -20px;
       `;
-      devicePosDiv.textContent = goafItem.devicePos;
+      devicePosDiv.textContent = goafItem.name || goafItem.devicePos;
       el.appendChild(devicePosDiv);
     }
 
@@ -328,7 +328,7 @@ export function useMap2D(): UseMap2DReturn {
     const el = createMarkerElement(
       {
         alarmLevel: props?.alarmLevel,
-        devicePos: props?.devicePos,
+        devicePos: props?.name || props?.devicePos,
       },
       false
     );
@@ -371,7 +371,7 @@ export function useMap2D(): UseMap2DReturn {
       }
 
       try {
-        await updatateGoaf({
+        await updateMineArea({
           id,
           xcoordinate: lnglat.lng.toString(),
           ycoordinate: lnglat.lat.toString(),
@@ -388,7 +388,7 @@ export function useMap2D(): UseMap2DReturn {
       const finalEl = createMarkerElement(
         {
           alarmLevel: props?.alarmLevel,
-          devicePos: props?.devicePos,
+          devicePos: props?.name || props?.devicePos,
         },
         true
       );
@@ -506,7 +506,7 @@ export function useMap2D(): UseMap2DReturn {
       }
 
       try {
-        await updatateGoaf({
+        await updateMineArea({
           id,
           xcoordinate: '',
           ycoordinate: '',

+ 8 - 8
src/views/system/cadFile/cad.api.ts

@@ -1,8 +1,8 @@
 import { defHttp } from '/@/utils/http/axios';
 
 export enum Api {
-  getGoafList = '/province/device/getGoafList',
-  updatateGoaf = '/province/device/updateGoaf',
+  getMineAreaList = '/workingface/mineArea/getMineAreaList',
+  updateMineArea = '/workingface/mineArea/updateMineArea',
   getGoafData = '/province/device/getGoafData',
   getMineFileList = '/province/mineData/getMineFileList',
   uploadFile = '/sys/common/upload',
@@ -10,14 +10,14 @@ export enum Api {
   addMineFile = '/province/mineData/addMineFile',
   deleteMineFile = '/province/mineData/deleteMineFile',
 }
-//查询密闭列表
-export function getGoafList(params: any) {
-  return defHttp.post({ url: Api.getGoafList, params }, { joinParamsToUrl: true });
+// 查询测风地点列表(布点管理)
+export function getMineAreaList(params: any) {
+  return defHttp.post({ url: Api.getMineAreaList, params }, { joinParamsToUrl: true });
 }
 
-// 更新密闭
-export function updatateGoaf(params: any) {
-  return defHttp.post({ url: Api.updatateGoaf, params });
+// 更新测风地点(布点坐标保存/移除)
+export function updateMineArea(params: any) {
+  return defHttp.post({ url: Api.updateMineArea, params });
 }
 // 查询密闭监测数据
 export function getGoafData(params: any) {

+ 9 - 10
src/views/system/cadFile/components/SideDrawer.vue

@@ -73,7 +73,7 @@
   import { Drawer, message } from 'ant-design-vue';
   import { BasicColumn, BasicTable } from '/@/components/Table';
   import { bindPosition, removePosition, getMarkerStatusList, renderGoafMarkers } from '../app';
-  import { getGoafList } from '../cad.api';
+  import { getMineAreaList } from '../cad.api';
   import { CloseOutlined } from '@ant-design/icons-vue';
 
   const props = defineProps<{
@@ -107,9 +107,9 @@
       fixed: 'left',
     },
     {
-      title: '密闭位置',
-      dataIndex: 'devicePos', // 修正拼写错误
-      key: 'devicePos',
+      title: '区域名称',
+      dataIndex: 'name', // MineArea.name(原密闭位置 devicePos)
+      key: 'name',
       ellipsis: true,
       // tooltip: true,
     },
@@ -119,8 +119,8 @@
   const filteredDataWithStatus = computed(() => {
     // 从接口返回的goafList过滤
     let filteredData = goafList.value.filter((item) => {
-      // 密闭位置搜索匹配(兼容空值)
-      const isSearchMatch = searchValue.value ? (item.devicePos || '').includes(searchValue.value) : true;
+      // 区域名称搜索匹配(兼容空值)
+      const isSearchMatch = searchValue.value ? (item.name || '').includes(searchValue.value) : true;
       return isSearchMatch;
     });
 
@@ -158,9 +158,8 @@
     }
 
     try {
-      // 调用接口
-      const res = await getGoafList({
-        order: 'desc',
+      // 调用接口(MineArea 列表)
+      const res = await getMineAreaList({
         mineCode: selectedMineId.value,
       });
       goafList.value = res || [];
@@ -197,7 +196,7 @@
     try {
       const marker = await bindPosition({
         id: record.id,
-        devicePos: record.devicePos,
+        name: record.name,
       });
       if (!marker) {
         message.warning('布点操作已取消');