Pārlūkot izejas kodu

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

lxh 1 dienu atpakaļ
vecāks
revīzija
f16c979d0e

+ 1 - 1
.env.development

@@ -6,7 +6,7 @@ VITE_PUBLIC_PATH = /
 
 # 跨域代理,您可以配置多个 ,请注意,没有换行符
 #VITE_PROXY = [["/jeecgboot","http://localhost:8080/jeecg-boot"],["/upload","http://localhost:3300/upload"]]
-VITE_PROXY = [["/sw","http://182.92.126.35:6008"],["/jeecgsystem","http://182.92.126.35:9999"],["/upload","http://182.92.126.35:9999/upload"],["/documents", "http://182.92.126.35:9050"],["/modelreq", "http://182.92.126.35:9999"],["/webRtc", "http://182.92.126.35:8051"], ["/python", "http://127.0.0.1:8008"], ["/tun2D", "http://127.0.0.1:8088/micro-vent-2dModal/tun2D"],["/dataCenter", "http://39.97.59.228:10999"]]
+VITE_PROXY = [["/sw","http://182.92.126.35:6008"],["/jeecgsystem","http://182.92.126.35:9999"],["/upload","http://182.92.126.35:9999/upload"],["/documents", "http://182.92.126.35:9050"],["/modelreq", "http://182.92.126.35:9999"],["/webRtc", "http://182.92.126.35:8051"], ["/python", "http://127.0.0.1:8008"], ["/tun2D", "http://127.0.0.1:8088/micro-vent-2dModal/tun2D"],["/dataCenter", "http://39.97.59.228:10999"],["/ventAI", "http://39.97.59.228:8070"]]
 # VITE_PROXY = [["/jeecgsystem","http://192.168.183.88:9999"],["/upload","http://192.168.183.88:9999/upload"],["/documents", "http://192.168.183.88:9050"],["/modelreq", "http://192.168.183.88:9999"],["/webRtc", "http://192.168.183.88:8051"]]
 # VITE_PROXY = [["/jeecgsystem","http://10.10.150.72:9999"],["/upload","http://localhost:3300/upload"],["/documents", "http://10.10.150.72:9050"],["/modelreq", "http://10.10.150.72:9999"],["/webRtc", "http://192.168.183.216:8051"]]
 # VITE_PROXY = [["/jeecgsystem","http://192.168.1.8:9999"],["/upload","http://localhost:3300/upload"]]

+ 1 - 1
.env.production

@@ -17,7 +17,7 @@ VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
 #VITE_PROXY = [["/jeecgsystem","http://10.248.223.116:9999"],["/upload","http://localhost:3300/upload"]]
 #VITE_PROXY = [["/jeecgsystem","http://10.248.210.152:9999"],["/upload","http://10.248.210.152:9999/upload"]]
 #VITE_PROXY = [["/jeecgsystem","http://182.92.126.35:9999"],["/upload","http://182.92.126.35:9999/upload"]]
-VITE_PROXY = [["/sw","http://10.246.167.205:6008"],["/modelreq","http://172.16.41.171:9999"],["/upload","http://172.16.41.171:9999/upload"],["/documents", "http://182.92.126.35:9050"],["/dataCenter", "http://39.97.59.228:10999"]]
+VITE_PROXY = [["/sw","http://10.246.167.205:6008"],["/modelreq","http://172.16.41.171:9999"],["/upload","http://172.16.41.171:9999/upload"],["/documents", "http://182.92.126.35:9050"],["/dataCenter", "http://39.97.59.228:10999"],["/ventAI", "http://39.97.59.228:8070"]]
 #VITE_PROXY = [["/jeecgsystem","http://172.16.41.171:9999"],["/upload","http://172.16.41.171:9999/upload"]]
 #VITE_PROXY = [["/jeecgsystem","http://10.120.120.163:9999"],["/upload","http://10.120.120.163:9999/upload"]]
 #VITE_PROXY = [["/jeecgsystem","http://192.168.1.4:9999"],["/upload","http://localhost:3300/upload"]]

+ 3 - 1
package.json

@@ -30,6 +30,7 @@
     "@logicflow/core": "^1.2.12",
     "@logicflow/extension": "^1.2.13",
     "@qiaoqiaoyun/drag-free": "^1.1.4",
+    "@types/markdown-it": "^14.1.2",
     "@vue-office/docx": "^1.6.3",
     "@vue-office/excel": "^1.7.14",
     "@vue-office/pdf": "^2.0.10",
@@ -62,6 +63,7 @@
     "katex": "^0.16.25",
     "lodash-es": "^4.17.21",
     "lodash.get": "^4.4.2",
+    "markdown-it": "^14.3.0",
     "marked": "^17.0.1",
     "md5": "^2.3.0",
     "mky-svg": "^1.0.2",
@@ -158,6 +160,7 @@
     "jest": "^29.6.4",
     "less": "^4.2.0",
     "lint-staged": "14.0.1",
+    "mockjs": "^1.1.0",
     "npm-run-all": "^4.1.5",
     "picocolors": "^1.0.0",
     "postcss": "^8.4.28",
@@ -178,7 +181,6 @@
     "ts-node": "^10.9.1",
     "typescript": "^4.9.5",
     "unocss": "^0.55.3",
-    "mockjs": "^1.1.0",
     "video.js": "^8.23.4",
     "videojs-flvjs-es6": "^1.0.1",
     "vite": "^4.4.9",

+ 88 - 12
pnpm-lock.yaml

@@ -32,6 +32,9 @@ importers:
       '@qiaoqiaoyun/drag-free':
         specifier: ^1.1.4
         version: 1.1.4(@aesoper/normal-utils@0.1.5)(@interactjs/core@1.10.27(@interactjs/utils@1.10.27))(@interactjs/utils@1.10.27)(@popperjs/core@2.11.8)(gradient-parser@1.1.1)(tinycolor2@1.6.0)(typescript@4.9.5)
+      '@types/markdown-it':
+        specifier: ^14.1.2
+        version: 14.1.2
       '@vue-office/docx':
         specifier: ^1.6.3
         version: 1.6.3(vue-demi@0.14.10(vue@3.5.24(typescript@4.9.5)))(vue@3.5.24(typescript@4.9.5))
@@ -122,15 +125,15 @@ importers:
       katex:
         specifier: ^0.16.25
         version: 0.16.25
-      lodash:
-        specifier: ^4.17.21
-        version: 4.17.21
       lodash-es:
         specifier: ^4.17.21
         version: 4.17.21
       lodash.get:
         specifier: ^4.4.2
         version: 4.4.2
+      markdown-it:
+        specifier: ^14.3.0
+        version: 14.3.0
       marked:
         specifier: ^17.0.1
         version: 17.0.1
@@ -140,9 +143,6 @@ importers:
       mky-svg:
         specifier: ^1.0.2
         version: 1.0.2
-      mockjs:
-        specifier: ^1.1.0
-        version: 1.1.0
       moment:
         specifier: ^2.29.4
         version: 2.30.1
@@ -392,7 +392,7 @@ importers:
         version: 11.3.2
       http-server:
         specifier: ^14.1.1
-        version: 14.1.1
+        version: 14.1.1(debug@4.4.3)
       husky:
         specifier: ^8.0.3
         version: 8.0.3
@@ -411,6 +411,9 @@ importers:
       lint-staged:
         specifier: 14.0.1
         version: 14.0.1
+      mockjs:
+        specifier: ^1.1.0
+        version: 1.1.0
       npm-run-all:
         specifier: ^4.1.5
         version: 4.1.5
@@ -2002,12 +2005,21 @@ packages:
   '@types/jsonfile@6.1.4':
     resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==}
 
+  '@types/linkify-it@5.0.0':
+    resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
+
   '@types/lodash-es@4.17.12':
     resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==}
 
   '@types/lodash@4.17.20':
     resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==}
 
+  '@types/markdown-it@14.1.2':
+    resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
+
+  '@types/mdurl@2.0.0':
+    resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
+
   '@types/minimist@1.2.5':
     resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
 
@@ -2176,6 +2188,7 @@ packages:
 
   '@ungap/structured-clone@1.3.0':
     resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+    deprecated: Potential CWE-502 - Update to 1.3.1 or higher
 
   '@unocss/astro@0.55.7':
     resolution: {integrity: sha512-mw8r14ArxUQBVCCisAJlF/WsZb650iBsduD/lXMk56N/nQ3MMArCcn62kcAxgZSb5tfIOQGQu/tbR8hEcD8y2g==}
@@ -2425,6 +2438,7 @@ packages:
   '@xmldom/xmldom@0.8.11':
     resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
     engines: {node: '>=10.0.0'}
+    deprecated: this version has critical issues, please update to the latest version
 
   '@zxcvbn-ts/core@3.0.4':
     resolution: {integrity: sha512-aQeiT0F09FuJaAqNrxynlAwZ2mW/1MdXakKWNmGM1Qp/VaY6CnB/GfnMS2T8gB2231Esp1/maCWd8vTG4OuShw==}
@@ -3082,15 +3096,18 @@ packages:
   conventional-changelog-atom@3.0.0:
     resolution: {integrity: sha512-pnN5bWpH+iTUWU3FaYdw5lJmfWeqSyrUkG+wyHBI9tC1dLNnHkbAOg1SzTQ7zBqiFrfo55h40VsGXWMdopwc5g==}
     engines: {node: '>=14'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
 
   conventional-changelog-cli@3.0.0:
     resolution: {integrity: sha512-3zMYi0IrfNd6AAHdPMrcgCg5DbcffiqNaEBf8cYrlntXPbBIXaELTbnRmUy5TQAe0Hkgi0J6+/VmRCkkJQflcQ==}
     engines: {node: '>=14'}
+    deprecated: This package is no longer maintained. Please use the conventional-changelog package instead.
     hasBin: true
 
   conventional-changelog-codemirror@3.0.0:
     resolution: {integrity: sha512-wzchZt9HEaAZrenZAUUHMCFcuYzGoZ1wG/kTRMICxsnW5AXohYMRxnyecP9ob42Gvn5TilhC0q66AtTPRSNMfw==}
     engines: {node: '>=14'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
 
   conventional-changelog-conventionalcommits@6.1.0:
     resolution: {integrity: sha512-3cS3GEtR78zTfMzk0AizXKKIdN4OvSh7ibNz6/DPbhWWQu7LqE/8+/GqSodV+sywUR2gpJAdP/1JFf4XtN7Zpw==}
@@ -3099,26 +3116,32 @@ packages:
   conventional-changelog-core@5.0.2:
     resolution: {integrity: sha512-RhQOcDweXNWvlRwUDCpaqXzbZemKPKncCWZG50Alth72WITVd6nhVk9MJ6w1k9PFNBcZ3YwkdkChE+8+ZwtUug==}
     engines: {node: '>=14'}
+    deprecated: Deprecated and no longer maintained. Please use conventional-changelog instead.
 
   conventional-changelog-ember@3.0.0:
     resolution: {integrity: sha512-7PYthCoSxIS98vWhVcSphMYM322OxptpKAuHYdVspryI0ooLDehRXWeRWgN+zWSBXKl/pwdgAg8IpLNSM1/61A==}
     engines: {node: '>=14'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
 
   conventional-changelog-eslint@4.0.0:
     resolution: {integrity: sha512-nEZ9byP89hIU0dMx37JXQkE1IpMmqKtsaR24X7aM3L6Yy/uAtbb+ogqthuNYJkeO1HyvK7JsX84z8649hvp43Q==}
     engines: {node: '>=14'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
 
   conventional-changelog-express@3.0.0:
     resolution: {integrity: sha512-HqxihpUMfIuxvlPvC6HltA4ZktQEUan/v3XQ77+/zbu8No/fqK3rxSZaYeHYant7zRxQNIIli7S+qLS9tX9zQA==}
     engines: {node: '>=14'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
 
   conventional-changelog-jquery@4.0.0:
     resolution: {integrity: sha512-TTIN5CyzRMf8PUwyy4IOLmLV2DFmPtasKN+x7EQKzwSX8086XYwo+NeaeA3VUT8bvKaIy5z/JoWUvi7huUOgaw==}
     engines: {node: '>=14'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
 
   conventional-changelog-jshint@3.0.0:
     resolution: {integrity: sha512-bQof4byF4q+n+dwFRkJ/jGf9dCNUv4/kCDcjeCizBvfF81TeimPZBB6fT4HYbXgxxfxWXNl/i+J6T0nI4by6DA==}
     engines: {node: '>=14'}
+    deprecated: This preset is deprecated. Please use conventional-changelog-conventionalcommits or conventional-changelog-angular instead.
 
   conventional-changelog-preset-loader@3.0.0:
     resolution: {integrity: sha512-qy9XbdSLmVnwnvzEisjxdDiLA4OmV3o8db+Zdg4WiFw14fP3B6XNz98X0swPPpkTd/pc1K7+adKgEDM1JCUMiA==}
@@ -4396,11 +4419,13 @@ packages:
   git-raw-commits@2.0.11:
     resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
     engines: {node: '>=10'}
+    deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
     hasBin: true
 
   git-raw-commits@3.0.0:
     resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==}
     engines: {node: '>=14'}
+    deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
     hasBin: true
 
   git-remote-origin-url@2.0.0:
@@ -4410,6 +4435,7 @@ packages:
   git-semver-tags@5.0.1:
     resolution: {integrity: sha512-hIvOeZwRbQ+7YEUmCkHqo8FOLQZCEn18yevLHADlFPZY02KJGsu5FZt9YW/lybfK2uhWFI7Qg/07LekJiTv7iA==}
     engines: {node: '>=14'}
+    deprecated: Deprecated and no longer maintained. Use @conventional-changelog/git-client instead.
     hasBin: true
 
   gitconfiglocal@1.0.0:
@@ -4425,11 +4451,12 @@ packages:
 
   glob@10.4.5:
     resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
+    deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
     hasBin: true
 
   glob@7.2.3:
     resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
-    deprecated: Glob versions prior to v9 are no longer supported
+    deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
 
   global-directory@4.0.1:
     resolution: {integrity: sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==}
@@ -5416,6 +5443,9 @@ packages:
   lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
+  linkify-it@5.0.2:
+    resolution: {integrity: sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==}
+
   lint-staged@14.0.1:
     resolution: {integrity: sha512-Mw0cL6HXnHN1ag0mN/Dg4g6sr8uf8sn98w2Oc1ECtFto9tvRF7nkXGJRbx8gPlHyoR0pLyBr2lQHbWwmUHe1Sw==}
     engines: {node: ^16.14.0 || >=18.0.0}
@@ -5588,6 +5618,10 @@ packages:
     resolution: {integrity: sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==}
     engines: {node: '>=0.10.0'}
 
+  markdown-it@14.3.0:
+    resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==}
+    hasBin: true
+
   marked@17.0.1:
     resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==}
     engines: {node: '>= 20'}
@@ -5609,6 +5643,9 @@ packages:
   mdn-data@2.0.30:
     resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
 
+  mdurl@2.0.0:
+    resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
   memorystream@0.3.1:
     resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==}
     engines: {node: '>= 0.10.0'}
@@ -6297,6 +6334,10 @@ packages:
   pump@3.0.3:
     resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
 
+  punycode.js@2.3.1:
+    resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+    engines: {node: '>=6'}
+
   punycode@2.3.1:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
@@ -7275,6 +7316,9 @@ packages:
     engines: {node: '>=4.2.0'}
     hasBin: true
 
+  uc.micro@2.1.0:
+    resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
   ufo@1.6.1:
     resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
 
@@ -7389,11 +7433,12 @@ packages:
 
   uuid@10.0.0:
     resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
+    deprecated: uuid@10 and below is no longer supported.  For ESM codebases, update to uuid@latest.  For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
     hasBin: true
 
   uuid@3.4.0:
     resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==}
-    deprecated: Please upgrade  to version 7 or higher.  Older versions may use Math.random() in certain circumstances, which is known to be problematic.  See https://v8.dev/blog/math-random for details.
+    deprecated: uuid@10 and below is no longer supported.  For ESM codebases, update to uuid@latest.  For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
     hasBin: true
 
   v8-compile-cache-lib@3.0.1:
@@ -7560,6 +7605,7 @@ packages:
   vue-i18n@9.2.2:
     resolution: {integrity: sha512-yswpwtj89rTBhegUAv9Mu37LNznyu3NpyLQmozF3i1hYOhwpG8RjcjIFIIfnu+2MDZJGSZPXaKWvnQA71Yv9TQ==}
     engines: {node: '>= 14'}
+    deprecated: v9 and v10 no longer supported. please migrate to v11. about maintenance status, see https://vue-i18n.intlify.dev/guide/maintenance.html
     peerDependencies:
       vue: ^3.0.0
 
@@ -7679,10 +7725,12 @@ packages:
 
   whatwg-encoding@1.0.5:
     resolution: {integrity: sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==}
+    deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
 
   whatwg-encoding@2.0.0:
     resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==}
     engines: {node: '>=12'}
+    deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
 
   whatwg-mimetype@2.3.0:
     resolution: {integrity: sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==}
@@ -9861,12 +9909,21 @@ snapshots:
     dependencies:
       '@types/node': 20.19.25
 
+  '@types/linkify-it@5.0.0': {}
+
   '@types/lodash-es@4.17.12':
     dependencies:
       '@types/lodash': 4.17.20
 
   '@types/lodash@4.17.20': {}
 
+  '@types/markdown-it@14.1.2':
+    dependencies:
+      '@types/linkify-it': 5.0.0
+      '@types/mdurl': 2.0.0
+
+  '@types/mdurl@2.0.0': {}
+
   '@types/minimist@1.2.5': {}
 
   '@types/mockjs@1.0.10': {}
@@ -12866,7 +12923,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  http-proxy@1.18.1:
+  http-proxy@1.18.1(debug@4.4.3):
     dependencies:
       eventemitter3: 4.0.7
       follow-redirects: 1.15.11(debug@4.4.3)
@@ -12874,14 +12931,14 @@ snapshots:
     transitivePeerDependencies:
       - debug
 
-  http-server@14.1.1:
+  http-server@14.1.1(debug@4.4.3):
     dependencies:
       basic-auth: 2.0.1
       chalk: 4.1.2
       corser: 2.0.1
       he: 1.2.0
       html-encoding-sniffer: 3.0.0
-      http-proxy: 1.18.1
+      http-proxy: 1.18.1(debug@4.4.3)
       mime: 1.6.0
       minimist: 1.2.8
       opener: 1.5.2
@@ -14096,6 +14153,10 @@ snapshots:
 
   lines-and-columns@1.2.4: {}
 
+  linkify-it@5.0.2:
+    dependencies:
+      uc.micro: 2.1.0
+
   lint-staged@14.0.1:
     dependencies:
       chalk: 5.3.0
@@ -14270,6 +14331,15 @@ snapshots:
     dependencies:
       object-visit: 1.0.1
 
+  markdown-it@14.3.0:
+    dependencies:
+      argparse: 2.0.1
+      entities: 4.5.0
+      linkify-it: 5.0.2
+      mdurl: 2.0.0
+      punycode.js: 2.3.1
+      uc.micro: 2.1.0
+
   marked@17.0.1: {}
 
   math-intrinsics@1.1.0: {}
@@ -14286,6 +14356,8 @@ snapshots:
 
   mdn-data@2.0.30: {}
 
+  mdurl@2.0.0: {}
+
   memorystream@0.3.1: {}
 
   meow@10.1.5:
@@ -14970,6 +15042,8 @@ snapshots:
       end-of-stream: 1.4.5
       once: 1.4.0
 
+  punycode.js@2.3.1: {}
+
   punycode@2.3.1: {}
 
   pure-rand@6.1.0: {}
@@ -16046,6 +16120,8 @@ snapshots:
 
   typescript@4.9.5: {}
 
+  uc.micro@2.1.0: {}
+
   ufo@1.6.1: {}
 
   uglify-js@3.19.3:

+ 9 - 0
pnpm-workspace.yaml

@@ -0,0 +1,9 @@
+allowBuilds:
+  '@swc/core': true
+  '@vue-office/docx': true
+  '@vue-office/excel': true
+  '@vue-office/pdf': true
+  core-js: true
+  es5-ext: true
+  esbuild: true
+  vue-demi: true

+ 44 - 4
src/views/vent/home/configurable/components/preset/SysWindCard.vue

@@ -2,8 +2,26 @@
 <template>
   <!-- 基准的画廊模块,通过不同的 type 展示不同的样式 -->
   <div ref="container" class="syswind__container w-full h-full">
-    <div v-for="item in data" :key="item.deviceID" class="syswind__card">
-      <!-- 画廊项的具体内容填充剩余宽度 -->
+    <!-- 画廊项的具体内容填充剩余宽度 -->
+    <div
+      v-for="item in data"
+      :key="item.deviceID"
+      class="syswind__card"
+      @mouseenter="getTrigger(item.deviceID).onEnter($event)"
+      @mousemove="getTrigger(item.deviceID).onMove($event)"
+      @mouseleave="getTrigger(item.deviceID).onLeave()"
+    >
+      <!-- 数据解读悬浮按钮(公共组件) -->
+      <DataInterpretationBtn
+        v-if="getTrigger(item.deviceID).state.visible"
+        :x="getTrigger(item.deviceID).state.x"
+        :y="getTrigger(item.deviceID).state.y"
+        @click="handleInterpret(item)"
+        @btn-enter="getTrigger(item.deviceID).onBtnEnter()"
+        @btn-leave="getTrigger(item.deviceID).onBtnLeave()"
+      />
+
+      <!-- 卡片内容 -->
       <div class="syswind__card__title font-bold" :title="getFormattedText(item, config.title)">{{ getFormattedText(item, config.title) }}</div>
       <div class="flex items-center justify-between syswind__card__vol_wrapper">
         <div class="syswind__card__vol_image" :class="{ overlimit: isVolumnOverLimit(item) }"></div>
@@ -31,11 +49,24 @@
             }"
           >
             <span>{{ getFormattedText(item, ctx.label) }}</span>
-            <span class=".font-bold">{{ getFormattedText(item, ctx.value) }}</span>
+            <span class="font-bold">{{ getFormattedText(item, ctx.value) }}</span>
           </div>
         </div>
       </div>
     </div>
+
+    <!-- 数据解读弹框(公共组件) -->
+    <DataInterpretationModal
+      :visible="modalVisible"
+      :loading="loading"
+      :streaming-text="streamingText"
+      :error="error"
+      :thinking="thinking"
+      :todos="todos"
+      :executing-tools="executingTools"
+      :tool-calls="toolCalls"
+      @close="handleClose"
+    />
   </div>
 </template>
 <script lang="ts" setup>
@@ -46,6 +77,7 @@
 
   // import { get } from 'lodash-es';
   // import { computed } from 'vue';
+  import { useDataInterpretationList, DataInterpretationBtn, DataInterpretationModal } from '/@/views/ventAI/dataPicker';
 
   const props = defineProps<{
     config: {
@@ -57,8 +89,15 @@
     data: any[];
   }>();
   //   defineEmits(['click']);
-
   // 判断val是否在low、high指定的范围内
+
+  const { modalVisible, loading, streamingText, error, thinking, todos, executingTools, toolCalls, getTrigger, handleInterpret, handleClose } =
+    useDataInterpretationList((item) => ({
+      device_id: item.deviceID,
+      device_name: item.strinstallpos,
+      device_type: 'windrect',
+    }));
+
   function isOverLimit(val: string, low: string = '', high: string = '') {
     const l = parseFloat(low);
     const h = parseFloat(high);
@@ -137,6 +176,7 @@
     --image-syswind-icon-spd: url(/@/assets/images/home-container/configurable/preset/syswind-icon-spd.svg);
     --image-syswind-icon-spd-red: url(/@/assets/images/home-container/configurable/preset/syswind-icon-spd-red.svg);
 
+    position: relative;
     background-image: var(--image-syswind-module);
     background-repeat: no-repeat;
     width: 320px;

+ 94 - 0
src/views/ventAI/dataPicker/api.ts

@@ -0,0 +1,94 @@
+import type { SSERawEvent } from './types';
+
+enum Api {
+  clickTun = '/ventAI/api/interpret/click/tun',
+  clickDevice = '/ventAI/api/interpret/click/device',
+}
+
+/**
+ * 巷道数据解读(SSE 流式)
+ * @param params - { tun_id, tun_name }
+ * @param onEvent - 每个 SSE 事件的回调
+ * @returns 流完成后的 thread_id、session_id
+ */
+export async function fetchTunnelInterpretation(
+  params: { tun_id: string; tun_name: string },
+  onEvent: (event: SSERawEvent) => void
+): Promise<{ thread_id: string; session_id: string }> {
+  return ssePost(Api.clickTun, params, onEvent);
+}
+
+/**
+ * 设备数据解读(SSE 流式)
+ * @param params - { device_id, device_name, device_type }
+ * @param onEvent - 每个 SSE 事件的回调
+ * @returns 流完成后的 thread_id、session_id
+ */
+export async function fetchDeviceInterpretation(
+  params: { device_id: string; device_name: string; device_type: string },
+  onEvent: (event: SSERawEvent) => void
+): Promise<{ thread_id: string; session_id: string }> {
+  return ssePost(Api.clickDevice, params, onEvent);
+}
+
+/** SSE 流式 POST 请求的通用实现 */
+async function ssePost(
+  url: string,
+  body: Record<string, string>,
+  onEvent: (event: SSERawEvent) => void
+): Promise<{ thread_id: string; session_id: string }> {
+  const response = await fetch(url, {
+    method: 'POST',
+    headers: { 'Content-Type': 'application/json' },
+    body: JSON.stringify(body),
+  });
+
+  if (!response.ok) {
+    throw new Error(`HTTP错误: ${response.status}`);
+  }
+
+  const reader = response.body!.getReader();
+  const decoder = new TextDecoder();
+  let buffer = '';
+  let threadId = '';
+  let sessionId = '';
+  let isDone = false;
+
+  // eslint-disable-next-line no-constant-condition
+  while (true) {
+    const { done, value } = await reader.read();
+    if (done || isDone) break;
+
+    buffer += decoder.decode(value, { stream: true });
+    const lines = buffer.split('\n');
+    buffer = lines.pop() || '';
+
+    for (const line of lines) {
+      if (line.trim() === '') continue;
+
+      if (line.startsWith('data: ')) {
+        const dataStr = line.replace(/^data: /, '');
+
+        try {
+          const event: SSERawEvent = JSON.parse(dataStr);
+          onEvent(event);
+
+          if (event.type === 'done') {
+            threadId = event.thread_id || '';
+            sessionId = event.session_id || '';
+            isDone = true;
+            break;
+          }
+          if (event.type === 'error') {
+            isDone = true;
+            break;
+          }
+        } catch (e) {
+          console.warn('SSE数据解析失败:', dataStr);
+        }
+      }
+    }
+  }
+
+  return { thread_id: threadId, session_id: sessionId };
+}

+ 60 - 0
src/views/ventAI/dataPicker/components/DataInterpretationBtn.vue

@@ -0,0 +1,60 @@
+<template>
+  <div
+    class="di-floating-btn"
+    :style="{ left: x + 'px', top: y + 'px' }"
+    @click.stop="$emit('click')"
+    @mouseenter="$emit('btn-enter')"
+    @mouseleave="$emit('btn-leave')"
+  >
+    <span>数据解读</span>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  defineProps<{
+    x: number;
+    y: number;
+  }>();
+
+  defineEmits<{
+    click: [];
+    'btn-enter': [];
+    'btn-leave': [];
+  }>();
+</script>
+
+<style lang="less" scoped>
+  .di-floating-btn {
+    position: absolute;
+    top: 0;
+    left: 0;
+    z-index: 10;
+    padding: 3px 12px;
+    background: linear-gradient(135deg, #1e90ff, #4da6ff);
+    border-radius: 3px;
+    cursor: pointer;
+    font-size: 12px;
+    color: #fff;
+    letter-spacing: 1px;
+    white-space: nowrap;
+    pointer-events: auto;
+    box-shadow: 0 0 10px rgba(30, 144, 255, 0.4);
+    animation: diFadeIn 0.15s ease-out;
+
+    &:hover {
+      background: linear-gradient(135deg, #4da6ff, #79c0ff);
+      box-shadow: 0 0 16px rgba(30, 144, 255, 0.6);
+    }
+  }
+
+  @keyframes diFadeIn {
+    from {
+      opacity: 0;
+      transform: scale(0.9);
+    }
+    to {
+      opacity: 1;
+      transform: scale(1);
+    }
+  }
+</style>

+ 355 - 0
src/views/ventAI/dataPicker/components/DataInterpretationModal.vue

@@ -0,0 +1,355 @@
+<template>
+  <a-modal
+    :visible="visible"
+    :title="null"
+    :footer="null"
+    :width="620"
+    :closable="false"
+    :destroy-on-close="true"
+    wrap-class-name="data-interpretation-modal"
+    @cancel="$emit('close')"
+  >
+    <!-- 标题栏 -->
+    <div class="di-header">
+      <span class="di-header__title">数据解读</span>
+      <span class="di-header__close" @click="$emit('close')">✕</span>
+    </div>
+
+    <!-- 内容区域 -->
+    <div class="di-body">
+      <!-- 过程状态展示区 -->
+      <div v-if="showProcess" class="di-process">
+        <!-- 思考中 -->
+        <div v-if="thinking" class="di-process__row di-process__thinking"><a-spin size="small" /><span>AI 正在思考...</span></div>
+
+        <!-- Todo 列表 -->
+        <div v-if="todos.length > 0" class="di-process__todos">
+          <div v-for="(todo, i) in todos" :key="i" class="di-todo-item" :class="`di-todo-item--${todo.status}`">
+            <span class="di-todo-item__icon">
+              <span v-if="todo.status === 'completed'" class="di-todo-check">✓</span>
+              <a-spin v-else-if="todo.status === 'in_progress'" size="small" />
+              <span v-else class="di-todo-dot"></span>
+            </span>
+            <span class="di-todo-item__text">{{ todo.content }}</span>
+          </div>
+        </div>
+
+        <!-- 执行工具中 -->
+        <!-- <div v-if="executingTools.length > 0" class="di-process__row di-process__executing">
+          <a-spin size="small" />
+          <span>正在执行:{{ executingTools.join(', ') }}</span>
+        </div> -->
+
+        <!-- 工具调用记录 -->
+        <!-- <div v-if="toolCalls.length > 0" class="di-process__tools">
+          <div v-for="(tc, i) in toolCalls" :key="i" class="di-tool-item">
+            <span class="di-tool-item__dot" :class="{ 'di-tool-item__dot--done': tc.status === 'result' }"></span>
+            <span class="di-tool-item__name">{{ tc.tool }}</span>
+            <span v-if="tc.status === 'call'" class="di-tool-item__tag">执行中</span>
+            <span v-else class="di-tool-item__tag di-tool-item__tag--done">完成</span>
+          </div>
+        </div> -->
+      </div>
+
+      <!-- 无过程且加载中 -->
+      <div v-if="loading && !hasContent && !hasProcess" class="di-loading">
+        <a-spin tip="正在分析数据..." />
+      </div>
+
+      <!-- Markdown 正文 -->
+      <div v-if="hasContent" class="di-markdown" v-html="renderedText"></div>
+
+      <!-- 错误 -->
+      <div v-else-if="error && !loading" class="di-error">数据解读失败:{{ error }}</div>
+    </div>
+
+    <!-- 流式加载指示条 -->
+    <div v-if="loading && hasContent" class="di-streaming-bar">
+      <a-spin size="small" /><span class="di-streaming-bar__tip">AI 正在生成...</span>
+    </div>
+  </a-modal>
+</template>
+
+<script lang="ts" setup>
+  import { computed } from 'vue';
+  import markdownIt from 'markdown-it';
+  import type { TodoItem, ToolCallRecord } from '../types';
+
+  const props = defineProps<{
+    visible: boolean;
+    loading: boolean;
+    streamingText: string;
+    error: string | null;
+    thinking: boolean;
+    todos: TodoItem[];
+    executingTools: string[];
+    toolCalls: ToolCallRecord[];
+  }>();
+
+  defineEmits<{ close: [] }>();
+
+  const md = markdownIt();
+
+  const hasContent = computed(() => !!props.streamingText);
+  const hasProcess = computed(() => props.thinking || props.todos.length > 0 || props.executingTools.length > 0 || props.toolCalls.length > 0);
+  const showProcess = computed(() => hasProcess.value);
+
+  const renderedText = computed(() => {
+    if (!props.streamingText) return '';
+    const processed = props.streamingText.replace(/<br\s*\/?>/gi, '  \n');
+    return md.render(processed);
+  });
+</script>
+
+<style lang="less">
+  .data-interpretation-modal {
+    .ant-modal-content {
+      background: linear-gradient(180deg, #0a1a3a 0%, #0d2450 100%);
+      border: 1px solid rgba(30, 144, 255, 0.3);
+      border-radius: 8px;
+      box-shadow: 0 0 30px rgba(0, 120, 255, 0.15);
+      color: #c8d6e5;
+    }
+    .ant-modal-body {
+      padding: 0;
+      max-height: 75vh;
+      overflow-y: auto;
+    }
+    .ant-spin-text {
+      color: #c8d6e5;
+    }
+  }
+</style>
+
+<style lang="less" scoped>
+  .di-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 18px 24px;
+    background: linear-gradient(90deg, rgba(30, 144, 255, 0.15) 0%, transparent 100%);
+    border-bottom: 1px solid rgba(30, 144, 255, 0.15);
+    flex-shrink: 0;
+
+    &__title {
+      font-size: 18px;
+      font-weight: bold;
+      color: #4da6ff;
+      letter-spacing: 2px;
+    }
+    &__close {
+      cursor: pointer;
+      font-size: 18px;
+      color: #8899aa;
+      &:hover {
+        color: #fff;
+      }
+    }
+  }
+
+  .di-body {
+    padding: 18px 24px;
+    min-height: 120px;
+    max-height: 700px;
+    overflow-y: auto;
+  }
+
+  .di-loading {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    padding: 40px 0;
+  }
+
+  // ── 过程状态区 ──
+  .di-process {
+    margin-bottom: 16px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid rgba(30, 144, 255, 0.12);
+
+    &__row {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 6px 0;
+      font-size: 13px;
+      color: #8899aa;
+    }
+  }
+
+  .di-process__thinking {
+    color: #4da6ff;
+    font-size: 13px;
+  }
+
+  .di-process__executing {
+    color: #c8a853;
+    font-size: 13px;
+  }
+
+  // ── Todo 列表 ──
+  .di-process__todos {
+    margin: 6px 0;
+  }
+
+  .di-todo-item {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 4px 0;
+    font-size: 13px;
+    line-height: 1.6;
+
+    &__icon {
+      flex-shrink: 0;
+      width: 16px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+    &__text {
+      color: #c8d6e5;
+    }
+
+    &--completed &__text {
+      color: #6b7a8a;
+      text-decoration: line-through;
+    }
+    &--in_progress &__text {
+      color: #4da6ff;
+    }
+  }
+
+  .di-todo-check {
+    color: #27ae60;
+    font-size: 12px;
+    font-weight: bold;
+  }
+
+  .di-todo-dot {
+    width: 6px;
+    height: 6px;
+    border-radius: 50%;
+    background: #4a5568;
+    display: inline-block;
+  }
+
+  // ── 工具调用记录 ──
+  .di-process__tools {
+    margin-top: 4px;
+  }
+
+  .di-tool-item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 2px 0;
+    font-size: 12px;
+    color: #6b7a8a;
+
+    &__dot {
+      width: 6px;
+      height: 6px;
+      border-radius: 50%;
+      background: #c8a853;
+      flex-shrink: 0;
+
+      &--done {
+        background: #27ae60;
+      }
+    }
+    &__name {
+      color: #8899aa;
+    }
+    &__tag {
+      font-size: 10px;
+      padding: 0 4px;
+      border-radius: 2px;
+      background: rgba(200, 168, 83, 0.2);
+      color: #c8a853;
+
+      &--done {
+        background: rgba(39, 174, 96, 0.15);
+        color: #27ae60;
+      }
+    }
+  }
+
+  // ── Markdown 正文 ──
+  .di-markdown {
+    line-height: 1.8;
+    color: #c8d6e5;
+    word-break: break-word;
+
+    :deep(h1),
+    :deep(h2),
+    :deep(h3),
+    :deep(h4) {
+      color: #4da6ff;
+      margin: 12px 0 8px;
+    }
+    :deep(p) {
+      margin: 6px 0;
+    }
+    :deep(ul),
+    :deep(ol) {
+      padding-left: 20px;
+      margin: 6px 0;
+    }
+    :deep(strong) {
+      color: #fff;
+    }
+    :deep(code) {
+      background: rgba(30, 144, 255, 0.15);
+      padding: 1px 5px;
+      border-radius: 3px;
+      font-size: 13px;
+    }
+    :deep(pre) {
+      background: rgba(0, 0, 0, 0.3);
+      padding: 12px;
+      border-radius: 6px;
+      overflow-x: auto;
+    }
+    :deep(blockquote) {
+      border-left: 3px solid #4da6ff;
+      padding-left: 12px;
+      margin: 8px 0;
+      color: #8899aa;
+    }
+    :deep(table) {
+      width: 100%;
+      border-collapse: collapse;
+      margin: 8px 0;
+      th,
+      td {
+        border: 1px solid rgba(30, 144, 255, 0.2);
+        padding: 6px 10px;
+        text-align: left;
+      }
+      th {
+        background: rgba(30, 144, 255, 0.1);
+        color: #4da6ff;
+      }
+    }
+  }
+
+  .di-streaming-bar {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    padding: 8px 24px 16px;
+    border-top: 1px solid rgba(30, 144, 255, 0.1);
+
+    &__tip {
+      font-size: 12px;
+      color: #8899aa;
+    }
+  }
+
+  .di-error {
+    padding: 40px;
+    text-align: center;
+    color: #e74c3c;
+  }
+</style>

+ 7 - 0
src/views/ventAI/dataPicker/index.ts

@@ -0,0 +1,7 @@
+export { useDataInterpretation, useDataInterpretationList } from './useDataInterpretation';
+export { createTrigger } from './useDataInterpretationTrigger';
+export { fetchTunnelInterpretation, fetchDeviceInterpretation } from './api';
+export type { TriggerState, TriggerHandle, TriggerOptions } from './useDataInterpretationTrigger';
+export type { TunnelInterpretRequest, DeviceInterpretRequest, InterpretationResult, SSERawEvent, SSEEventType, TodoItem, ToolCallRecord } from './types';
+export { default as DataInterpretationModal } from './components/DataInterpretationModal.vue';
+export { default as DataInterpretationBtn } from './components/DataInterpretationBtn.vue';

+ 49 - 0
src/views/ventAI/dataPicker/types.ts

@@ -0,0 +1,49 @@
+/** 巷道解读请求参数 */
+export interface TunnelInterpretRequest {
+  tun_id: string;
+  tun_name: string;
+}
+
+/** 设备解读请求参数 */
+export interface DeviceInterpretRequest {
+  device_id: string;
+  device_name: string;
+  device_type: string;
+}
+
+/** SSE 事件类型 */
+export type SSEEventType = 'thinking' | 'executing' | 'tool_call' | 'tool_result' | 'updated_todo_list' | 'token' | 'done' | 'error';
+
+/** SSE 原始事件(后端推送的 JSON 结构) */
+export interface SSERawEvent {
+  type: SSEEventType;
+  content?: string;
+  source?: 'main' | 'subagent';
+  thread_id?: string;
+  session_id?: string;
+  tools?: string[];
+  tool?: string;
+  node?: string;
+  todos?: TodoItem[];
+  [key: string]: any;
+}
+
+/** Todo 项 */
+export interface TodoItem {
+  content: string;
+  status: 'pending' | 'in_progress' | 'completed';
+}
+
+/** 工具调用记录 */
+export interface ToolCallRecord {
+  tool: string;
+  source: string;
+  status: 'call' | 'result';
+}
+
+/** 解读完成后的结果 */
+export interface InterpretationResult {
+  content: string;
+  threadId: string;
+  sessionId: string;
+}

+ 185 - 0
src/views/ventAI/dataPicker/useDataInterpretation.ts

@@ -0,0 +1,185 @@
+import { ref } from 'vue';
+import { fetchTunnelInterpretation, fetchDeviceInterpretation } from './api';
+import { createTrigger, type TriggerHandle } from './useDataInterpretationTrigger';
+import type { TodoItem, ToolCallRecord } from './types';
+
+/** 数据解读 hook —— 调用后端 SSE 流式接口,实时追踪所有过程状态 */
+export function useDataInterpretation() {
+  const loading = ref(false);
+  const streamingText = ref('');
+  const error = ref<string | null>(null);
+  const threadId = ref('');
+  const sessionId = ref('');
+
+  // 过程状态
+  const thinking = ref(false);
+  const todos = ref<TodoItem[]>([]);
+  const executingTools = ref<string[]>([]);
+  const toolCalls = ref<ToolCallRecord[]>([]);
+
+  /** 统一的事件处理 */
+  function handleEvent(event: any) {
+    switch (event.type) {
+      case 'thinking':
+        thinking.value = true;
+        break;
+      case 'executing':
+        thinking.value = false;
+        executingTools.value = event.tools || [];
+        break;
+      case 'tool_call':
+        executingTools.value = [];
+        toolCalls.value = [...toolCalls.value, { tool: event.tool || '', source: event.source || 'main', status: 'call' as const }];
+        break;
+      case 'tool_result':
+        // 更新最近一条 tool_call 为 result 状态
+        if (toolCalls.value.length > 0) {
+          const last = toolCalls.value[toolCalls.value.length - 1];
+          if (last.tool === event.tool && last.status === 'call') {
+            last.status = 'result';
+          }
+        }
+        break;
+      case 'updated_todo_list':
+        thinking.value = false;
+        todos.value = event.todos || [];
+        break;
+      case 'token':
+        thinking.value = false;
+        if (event.content) {
+          streamingText.value += event.content;
+        }
+        break;
+    }
+  }
+
+  async function interpretTunnel(params: { tun_id: string; tun_name: string }) {
+    loading.value = true;
+    error.value = null;
+    streamingText.value = '';
+    thinking.value = false;
+    todos.value = [];
+    executingTools.value = [];
+    toolCalls.value = [];
+
+    try {
+      const result = await fetchTunnelInterpretation(params, handleEvent);
+      threadId.value = result.thread_id;
+      sessionId.value = result.session_id;
+    } catch (e: any) {
+      const msg = e?.message || '数据解读请求失败';
+      error.value = msg;
+      throw e;
+    } finally {
+      loading.value = false;
+      thinking.value = false;
+      executingTools.value = [];
+    }
+  }
+
+  async function interpretDevice(params: { device_id: string; device_name: string; device_type: string }) {
+    loading.value = true;
+    error.value = null;
+    streamingText.value = '';
+    thinking.value = false;
+    todos.value = [];
+    executingTools.value = [];
+    toolCalls.value = [];
+
+    try {
+      const result = await fetchDeviceInterpretation(params, handleEvent);
+      threadId.value = result.thread_id;
+      sessionId.value = result.session_id;
+    } catch (e: any) {
+      const msg = e?.message || '数据解读请求失败';
+      error.value = msg;
+      throw e;
+    } finally {
+      loading.value = false;
+      thinking.value = false;
+      executingTools.value = [];
+    }
+  }
+
+  function reset() {
+    streamingText.value = '';
+    error.value = null;
+    threadId.value = '';
+    sessionId.value = '';
+    thinking.value = false;
+    todos.value = [];
+    executingTools.value = [];
+    toolCalls.value = [];
+  }
+
+  return {
+    loading,
+    streamingText,
+    error,
+    threadId,
+    sessionId,
+    thinking,
+    todos,
+    executingTools,
+    toolCalls,
+    interpretTunnel,
+    interpretDevice,
+    reset,
+  };
+}
+
+/**
+ * 数据解读列表级便捷 Hook。
+ * 整合了弹框状态 + 每行数据的悬浮触发器管理 + SSE 流式解读。
+ *
+ * @param mapParams - 将列表行数据映射为接口请求参数
+ *
+ * 使用:在列表/表格/卡片组等 v-for 场景的组件中调用一次即可。
+ */
+export function useDataInterpretationList(
+  mapParams: (item: any) => { tun_id: string; tun_name: string } | { device_id: string; device_name: string; device_type: string }
+) {
+  const { loading, streamingText, error, threadId, sessionId, thinking, todos, executingTools, toolCalls, interpretTunnel, interpretDevice, reset } =
+    useDataInterpretation();
+
+  const modalVisible = ref(false);
+  const triggerMap = new Map<string, TriggerHandle>();
+
+  function getTrigger(id: string): TriggerHandle {
+    if (!triggerMap.has(id)) {
+      triggerMap.set(id, createTrigger());
+    }
+    return triggerMap.get(id)!;
+  }
+
+  async function handleInterpret(item: any) {
+    modalVisible.value = true;
+    const params = mapParams(item);
+    if ('tun_id' in params) {
+      await interpretTunnel(params);
+    } else {
+      await interpretDevice(params);
+    }
+  }
+
+  function handleClose() {
+    modalVisible.value = false;
+    reset();
+  }
+
+  return {
+    modalVisible,
+    loading,
+    streamingText,
+    error,
+    threadId,
+    sessionId,
+    thinking,
+    todos,
+    executingTools,
+    toolCalls,
+    getTrigger,
+    handleInterpret,
+    handleClose,
+  };
+}

+ 124 - 0
src/views/ventAI/dataPicker/useDataInterpretationTrigger.ts

@@ -0,0 +1,124 @@
+import { reactive } from 'vue';
+
+export interface TriggerState {
+  visible: boolean;
+  x: number;
+  y: number;
+}
+
+export interface TriggerHandle {
+  state: TriggerState;
+  onEnter: (e: MouseEvent) => void;
+  onMove: (e: MouseEvent) => void;
+  onLeave: () => void;
+  onBtnEnter: () => void;
+  onBtnLeave: () => void;
+  destroy: () => void;
+}
+
+export interface TriggerOptions {
+  /** 鼠标停顿多久后显示按钮 (ms),默认 400 */
+  pauseDelay?: number;
+  /** 按钮显示后多久自动消失 (ms),默认 3000 */
+  hideDelay?: number;
+}
+
+/**
+ * 创建一个悬浮按钮触发器实例。
+ * 每个需要数据解读的 DOM 元素(卡片行、表格行、3D 标记等)对应一个 trigger。
+ *
+ * 使用方式:
+ * 1. 在 v-for 中为每行数据 createTrigger()
+ * 2. 将 onEnter/onMove/onLeave 绑定到容器元素的 mouseenter/mousemove/mouseleave
+ * 3. 将 DataInterpretationBtn 放在容器内,绑定 state 和 onBtnEnter/onBtnLeave
+ */
+export function createTrigger(options?: TriggerOptions): TriggerHandle {
+  const PAUSE_DELAY = options?.pauseDelay ?? 400;
+  const HIDE_DELAY = options?.hideDelay ?? 3000;
+
+  const state = reactive<TriggerState>({
+    visible: false,
+    x: 0,
+    y: 0,
+  });
+
+  let pauseTimer: ReturnType<typeof setTimeout> | null = null;
+  let hideTimer: ReturnType<typeof setTimeout> | null = null;
+
+  function trackMouse(e: MouseEvent) {
+    const target = e.currentTarget as HTMLElement;
+    const rect = target.getBoundingClientRect();
+    state.x = e.clientX - rect.left - 36;
+    state.y = e.clientY - rect.top - 34;
+  }
+
+  function startPauseTimer() {
+    pauseTimer = setTimeout(() => {
+      state.visible = true;
+      startHideTimer();
+    }, PAUSE_DELAY);
+  }
+
+  function resetPauseTimer() {
+    if (pauseTimer) {
+      clearTimeout(pauseTimer);
+      pauseTimer = null;
+    }
+  }
+
+  function startHideTimer() {
+    clearHideTimer();
+    hideTimer = setTimeout(() => {
+      state.visible = false;
+    }, HIDE_DELAY);
+  }
+
+  function clearHideTimer() {
+    if (hideTimer) {
+      clearTimeout(hideTimer);
+      hideTimer = null;
+    }
+  }
+
+  function onEnter(e: MouseEvent) {
+    clearHideTimer();
+    trackMouse(e);
+    startPauseTimer();
+  }
+
+  function onMove(e: MouseEvent) {
+    if (state.visible) return;
+    trackMouse(e);
+    resetPauseTimer();
+    startPauseTimer();
+  }
+
+  function onLeave() {
+    resetPauseTimer();
+    clearHideTimer();
+    state.visible = false;
+  }
+
+  function onBtnEnter() {
+    clearHideTimer();
+  }
+
+  function onBtnLeave() {
+    startHideTimer();
+  }
+
+  function destroy() {
+    resetPauseTimer();
+    clearHideTimer();
+  }
+
+  return {
+    state,
+    onEnter,
+    onMove,
+    onLeave,
+    onBtnEnter,
+    onBtnLeave,
+    destroy,
+  };
+}