agentworkshop 0.7.40 → 0.7.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (327) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/_nuxt/B8uy9iRW.js +1 -0
  3. package/.output/public/_nuxt/BGTYYQH-.js +1 -0
  4. package/.output/public/_nuxt/BUmHEJ77.js +2 -0
  5. package/.output/public/_nuxt/BY7YrV_D.js +1 -0
  6. package/.output/public/_nuxt/Bqcgz95s.js +18 -0
  7. package/.output/public/_nuxt/C7UlqPeD.js +1 -0
  8. package/.output/public/_nuxt/CTmDn-YP.js +1 -0
  9. package/.output/public/_nuxt/Cs8pVeVG.js +1 -0
  10. package/.output/public/_nuxt/{DcsCcxp4.js → D98NH8kG.js} +1 -1
  11. package/.output/public/_nuxt/DKySmNPI.js +14 -0
  12. package/.output/public/_nuxt/Di0NY_ht.js +1 -0
  13. package/.output/public/_nuxt/Du9v6qGB.js +1 -0
  14. package/.output/public/_nuxt/{C_iYD-EB.js → EHlEMRii.js} +1 -1
  15. package/.output/public/_nuxt/Eke1fs-U.js +1 -0
  16. package/.output/public/_nuxt/{DeJykBFf.js → H0BcqZtT.js} +1 -1
  17. package/.output/public/_nuxt/{C4oW1HsE.js → HSYEkkce.js} +1 -1
  18. package/.output/public/_nuxt/{DZshyNLa.js → IREZ1eC2.js} +1 -1
  19. package/.output/public/_nuxt/{D_OTvQza.js → J9oU00D1.js} +1 -1
  20. package/.output/public/_nuxt/OmpTerminalPanel.BxR5-lvi.css +1 -0
  21. package/.output/public/_nuxt/SSyAPaHc.js +1 -0
  22. package/.output/public/_nuxt/TownView.li4EMQxK.css +1 -0
  23. package/.output/public/_nuxt/VdLk4jLE2.js +1 -0
  24. package/.output/public/_nuxt/_id_.BhsXoEuk.css +1 -0
  25. package/.output/public/_nuxt/_id_.DQyj-WKN.css +1 -0
  26. package/.output/public/_nuxt/{_wsId_.B4WiqkIS.css → _wsId_.CTx-BlfF.css} +1 -1
  27. package/.output/public/_nuxt/aShsv_rG.js +19 -0
  28. package/.output/public/_nuxt/{yF0aYMF1.js → aesgXKSr.js} +3 -3
  29. package/.output/public/_nuxt/agents.B21nUEUX.css +1 -0
  30. package/.output/public/_nuxt/aml.ekTjxFkf.css +1 -0
  31. package/.output/public/_nuxt/builds/latest.json +1 -1
  32. package/.output/public/_nuxt/builds/meta/b5506ef0-f48e-4f14-a6c0-d65e505d0b09.json +1 -0
  33. package/.output/public/_nuxt/ca4TJ3Op.js +1 -0
  34. package/.output/public/_nuxt/channel-templates.DplM1pJ3.css +1 -0
  35. package/.output/public/_nuxt/daq.BdVmoIfe.css +1 -0
  36. package/.output/public/_nuxt/dcw.DCgyzpWV.css +1 -0
  37. package/.output/public/_nuxt/{default.J32SgqFY.css → default.Bh3L2HU8.css} +1 -1
  38. package/.output/public/_nuxt/lHAxdSsj.js +1 -0
  39. package/.output/public/_nuxt/pages.j9rzRocr.css +1 -0
  40. package/.output/public/_nuxt/plugins.DhiZbHU8.css +1 -0
  41. package/.output/public/_nuxt/settings.BRx9rIDg.css +1 -0
  42. package/.output/public/_nuxt/teams.BLlOPB_Z.css +1 -0
  43. package/.output/public/_nuxt/tokens.De8xzszn.css +1 -0
  44. package/.output/public/_nuxt/workshop.D1jFNkrn.css +1 -0
  45. package/.output/server/chunks/_/nitro.mjs +1058 -830
  46. package/.output/server/chunks/_/nitro.mjs.map +1 -1
  47. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs → OmpTerminalPanel-CVvxpsF7.mjs} +15 -15
  48. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs.map → OmpTerminalPanel-CVvxpsF7.mjs.map} +1 -1
  49. package/.output/server/chunks/build/{TownView-BuLAsmpR.mjs → TownView-CJ6IsDCc.mjs} +229 -228
  50. package/.output/server/chunks/build/{TownView-BuLAsmpR.mjs.map → TownView-CJ6IsDCc.mjs.map} +1 -1
  51. package/.output/server/chunks/build/{_id_-BU1wfOID.mjs → _id_-CVD_H0ir.mjs} +149 -147
  52. package/.output/server/chunks/build/_id_-CVD_H0ir.mjs.map +1 -0
  53. package/.output/server/chunks/build/{_id_-BYsEua57.mjs → _id_-CtxMb3qp.mjs} +47 -46
  54. package/.output/server/chunks/build/_id_-CtxMb3qp.mjs.map +1 -0
  55. package/.output/server/chunks/build/{_wsId_-BxlDcJHg.mjs → _wsId_-DpY3T-md.mjs} +516 -446
  56. package/.output/server/chunks/build/{_wsId_-BxlDcJHg.mjs.map → _wsId_-DpY3T-md.mjs.map} +1 -1
  57. package/.output/server/chunks/build/{agents-CLQqJF2y.mjs → agents-DsCkuXDV.mjs} +49 -46
  58. package/.output/server/chunks/build/agents-DsCkuXDV.mjs.map +1 -0
  59. package/.output/server/chunks/build/{aml-_7FdFL2p.mjs → aml-CT0xKGKI.mjs} +122 -105
  60. package/.output/server/chunks/build/aml-CT0xKGKI.mjs.map +1 -0
  61. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs +117 -0
  62. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs.map +1 -0
  63. package/.output/server/chunks/build/{channel-templates-p6SZNjlM.mjs → channel-templates-Dla9FbYa.mjs} +49 -37
  64. package/.output/server/chunks/build/channel-templates-Dla9FbYa.mjs.map +1 -0
  65. package/.output/server/chunks/build/{daq-cgyjKmJu.mjs → daq-DWWSKGAV.mjs} +95 -93
  66. package/.output/server/chunks/build/daq-DWWSKGAV.mjs.map +1 -0
  67. package/.output/server/chunks/build/{dcw-CoTxBlAZ.mjs → dcw-CT8k17_E.mjs} +46 -45
  68. package/.output/server/chunks/build/dcw-CT8k17_E.mjs.map +1 -0
  69. package/.output/server/chunks/build/{default-BE4r0FZN.mjs → default-D3525UFX.mjs} +16 -15
  70. package/.output/server/chunks/build/{default-BE4r0FZN.mjs.map → default-D3525UFX.mjs.map} +1 -1
  71. package/.output/server/chunks/build/{logs-BSOeZwwI.mjs → logs--dByNyhw.mjs} +2 -2
  72. package/.output/server/chunks/build/{logs-BSOeZwwI.mjs.map → logs--dByNyhw.mjs.map} +1 -1
  73. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs → monitor-Dh2Kvaq5.mjs} +2 -2
  74. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs.map → monitor-Dh2Kvaq5.mjs.map} +1 -1
  75. package/.output/server/chunks/build/{pages-DFxjn9Xa.mjs → pages-BHcSBM2D.mjs} +26 -20
  76. package/.output/server/chunks/build/pages-BHcSBM2D.mjs.map +1 -0
  77. package/.output/server/chunks/build/{plugins-lGlUr1lj.mjs → plugins-BNgIi61W.mjs} +23 -17
  78. package/.output/server/chunks/build/plugins-BNgIi61W.mjs.map +1 -0
  79. package/.output/server/chunks/build/{settings-Ddd1qx2Z.mjs → settings-ipYBDWdn.mjs} +45 -44
  80. package/.output/server/chunks/build/settings-ipYBDWdn.mjs.map +1 -0
  81. package/.output/server/chunks/build/{teams-MEDLZdMn.mjs → teams-5lx-CwFa.mjs} +47 -44
  82. package/.output/server/chunks/build/teams-5lx-CwFa.mjs.map +1 -0
  83. package/.output/server/chunks/build/{tokens-C7fTM5C2.mjs → tokens-q-IDT8Io.mjs} +73 -53
  84. package/.output/server/chunks/build/tokens-q-IDT8Io.mjs.map +1 -0
  85. package/.output/server/chunks/build/{town-qMPql9Se.mjs → town-QJWB5wng.mjs} +5 -4
  86. package/.output/server/chunks/build/town-QJWB5wng.mjs.map +1 -0
  87. package/.output/server/chunks/build/{useDaqStream-CIBaiGAl.mjs → useDaqStream-DfCFpb3p.mjs} +9 -3
  88. package/.output/server/chunks/build/useDaqStream-DfCFpb3p.mjs.map +1 -0
  89. package/.output/server/chunks/build/{useDcwStream-CBbiYnWe.mjs → useDcwStream-UPiX-3iO.mjs} +11 -2
  90. package/.output/server/chunks/build/useDcwStream-UPiX-3iO.mjs.map +1 -0
  91. package/.output/server/chunks/build/{workshop-BL5bhfs0.mjs → workshop-BIlCj_ZP.mjs} +45 -45
  92. package/.output/server/chunks/build/workshop-BIlCj_ZP.mjs.map +1 -0
  93. package/.output/server/chunks/routes/api/workshop/channel-templates/_id/instantiate.post.mjs +1 -1
  94. package/.output/server/chunks/routes/api/workshop/channels.post.mjs +3 -3
  95. package/.output/server/chunks/routes/api/workshop/dcw/index5.post.mjs +1 -1
  96. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/start.post.mjs +1 -1
  97. package/.output/server/chunks/routes/api/workshop/dcw/lines/_id/stop.post.mjs +1 -1
  98. package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.delete.mjs +1 -1
  99. package/.output/server/chunks/routes/api/workshop/dcw/params/_id_.patch.mjs +1 -1
  100. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.delete.mjs +1 -1
  101. package/.output/server/chunks/routes/api/workshop/dcw/templates/_key_.patch.mjs +1 -1
  102. package/.output/server/chunks/routes/api/workshop/harnesses/_id/providers.get.mjs +1 -1
  103. package/.output/server/chunks/routes/api/workshop/harnesses.get.mjs +1 -1
  104. package/.output/server/chunks/routes/api/workshop/hitl/pending.get.mjs +1 -1
  105. package/.output/server/chunks/routes/api/workshop/hitl/respond.post.mjs +1 -1
  106. package/.output/server/chunks/routes/api/workshop/index.put.mjs +1 -1
  107. package/.output/server/chunks/routes/api/workshop/index5.get.mjs +3 -1
  108. package/.output/server/chunks/routes/api/workshop/index5.get.mjs.map +1 -1
  109. package/.output/server/chunks/routes/api/workshop/index6.get.mjs +3 -1
  110. package/.output/server/chunks/routes/api/workshop/index6.get.mjs.map +1 -1
  111. package/.output/server/chunks/routes/api/workshop/plugins/_name/disable.post.mjs +1 -1
  112. package/.output/server/chunks/routes/api/workshop/plugins/_name/enable.post.mjs +1 -1
  113. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.delete.mjs +1 -1
  114. package/.output/server/chunks/routes/api/workshop/scene/layouts/_channelId_.put.mjs +1 -1
  115. package/.output/server/chunks/routes/api/workshop/workspaces/_id/channel-templates/_tplId_.post.mjs +1 -1
  116. package/.output/server/chunks/routes/renderer.mjs +2 -2
  117. package/.output/server/chunks/virtual/entry.mjs +639 -88
  118. package/.output/server/chunks/virtual/entry.mjs.map +1 -1
  119. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  120. package/.output/server/package.json +1 -1
  121. package/README-zh.md +16 -6
  122. package/README.md +17 -7
  123. package/app/components/AppHeader.vue +961 -959
  124. package/app/components/workshop/A2aRpcDebugger.vue +237 -237
  125. package/app/components/workshop/AgentInspectorDrawer.vue +424 -424
  126. package/app/components/workshop/AgentLanesView.vue +998 -998
  127. package/app/components/workshop/ArtifactCard.vue +120 -120
  128. package/app/components/workshop/AssetLibrary.vue +452 -452
  129. package/app/components/workshop/ChannelSessionList.vue +812 -804
  130. package/app/components/workshop/CommandPalette.vue +269 -269
  131. package/app/components/workshop/Composer.vue +827 -827
  132. package/app/components/workshop/DeviceTwinPanel.vue +643 -643
  133. package/app/components/workshop/EventBlock.vue +602 -602
  134. package/app/components/workshop/FileSelectorModal.vue +188 -188
  135. package/app/components/workshop/InspectorPanel.vue +301 -301
  136. package/app/components/workshop/MemoryPanel.vue +364 -364
  137. package/app/components/workshop/MultiChannelView.vue +189 -189
  138. package/app/components/workshop/TaskBoardView.vue +686 -686
  139. package/app/components/workshop/TaskInspectorDrawer.vue +390 -390
  140. package/app/components/workshop/TranscriptTimeline.vue +601 -601
  141. package/app/components/workshop/blocks/ClusterArtifact.vue +61 -61
  142. package/app/components/workshop/blocks/ClusterError.vue +85 -85
  143. package/app/components/workshop/blocks/ClusterLife.vue +87 -87
  144. package/app/components/workshop/blocks/ClusterMember.vue +95 -93
  145. package/app/components/workshop/blocks/ClusterMemory.vue +84 -84
  146. package/app/components/workshop/blocks/ClusterOther.vue +67 -67
  147. package/app/components/workshop/blocks/ClusterStatus.vue +149 -147
  148. package/app/components/workshop/blocks/ClusterTool.vue +158 -158
  149. package/app/components/workshop/terminal/OmpTerminalPanel.vue +1011 -1011
  150. package/app/components/workshop/town/TownView.vue +56 -56
  151. package/app/composables/workshop/useDaqStream.ts +7 -4
  152. package/app/composables/workshop/useDcwStream.ts +8 -2
  153. package/app/middleware/locale-cookie.global.ts +22 -0
  154. package/app/pages/aml/index.vue +24 -2
  155. package/app/pages/daq/[id].vue +20 -16
  156. package/app/pages/daq/index.vue +2310 -2307
  157. package/app/pages/dcw/[id].vue +26 -23
  158. package/app/pages/dcw/index.vue +3 -3
  159. package/app/pages/index.vue +987 -981
  160. package/app/pages/plugins/index.vue +317 -311
  161. package/app/pages/settings.vue +1564 -1562
  162. package/app/pages/tokens.vue +15 -15
  163. package/app/pages/workshop/agents.vue +612 -609
  164. package/app/pages/workshop/channel-templates.vue +18 -11
  165. package/app/pages/workshop/index.vue +668 -668
  166. package/app/pages/workshop/teams.vue +743 -740
  167. package/app/pages/workshop/w/[wsId].vue +704 -704
  168. package/app/plugins/locale-restore.client.ts +16 -1
  169. package/app/utils/builtin-catalog.ts +104 -0
  170. package/config.yml +170 -170
  171. package/docs/cli.en.md +2 -2
  172. package/docs/cli.md +2 -2
  173. package/docs/full-test-plan.md +2 -3
  174. package/docs/plugins.en.md +33 -1
  175. package/docs/plugins.md +29 -1
  176. package/docs/sdk.en.md +1 -1
  177. package/docs/sdk.md +1 -1
  178. package/i18n/locales/en.ts +293 -35
  179. package/i18n/locales/zh-CN.ts +265 -7
  180. package/package.json +4 -3
  181. package/scripts/_dbg-perms-e2e.mjs +132 -109
  182. package/scripts/_demo-agentteam-capture.mjs +278 -0
  183. package/scripts/_e2e-ui-functional.mjs +371 -0
  184. package/scripts/_i18n-en-cjk-scan.mjs +28 -0
  185. package/scripts/_i18n-en-verify.mjs +107 -0
  186. package/scripts/_i18n-extra-checks.mjs +53 -0
  187. package/scripts/_i18n-scan-hardcoded.mjs +180 -0
  188. package/scripts/_i18n-verify-keys.mjs +50 -0
  189. package/scripts/_loop-biax-campaign.mjs +130 -0
  190. package/scripts/_sweep-button-contrast.mjs +87 -0
  191. package/scripts/test-serial-bridge.mjs +418 -0
  192. package/scripts/ui/e2e-interactive.mjs +328 -0
  193. package/scripts/ui/record-demo.mjs +557 -0
  194. package/scripts/ui/seed-industrial-demo.mjs +171 -0
  195. package/server/api/workshop/daq/index.get.ts +3 -1
  196. package/server/api/workshop/dcw/index.get.ts +3 -0
  197. package/server/plugins/aw-plugins.ts +9 -7
  198. package/server/plugins-builtin/diag-bridge/index.mjs +608 -661
  199. package/server/plugins-builtin/rag-bridge/index.mjs +57 -133
  200. package/server/plugins-builtin/serial-bridge/client.mjs +210 -0
  201. package/server/plugins-builtin/serial-bridge/drivers.mjs +409 -0
  202. package/server/plugins-builtin/serial-bridge/i18n.json +62 -0
  203. package/server/plugins-builtin/serial-bridge/index.mjs +247 -0
  204. package/server/plugins-builtin/serial-bridge/lib.mjs +208 -0
  205. package/server/plugins-builtin/serial-bridge/rtu.mjs +168 -0
  206. package/server/services/workshop/daq/daq-controller.ts +7 -2
  207. package/server/services/workshop/daq/drivers.ts +89 -3
  208. package/server/services/workshop/dcw/dcw-controller.ts +5 -1
  209. package/server/services/workshop/dcw/drivers.ts +85 -2
  210. package/server/services/workshop/dcw/plugin-bridge.ts +45 -0
  211. package/server/services/workshop/plugins/host.mjs +30 -0
  212. package/shared/daq-protocol.ts +15 -0
  213. package/.output/public/_nuxt/88GOowgO2.js +0 -1
  214. package/.output/public/_nuxt/BOt2SP4z.js +0 -1
  215. package/.output/public/_nuxt/BP-NyKVG.js +0 -18
  216. package/.output/public/_nuxt/BXxH0UQE.js +0 -1
  217. package/.output/public/_nuxt/BsROmY89.js +0 -1
  218. package/.output/public/_nuxt/ClILjvWH.js +0 -1
  219. package/.output/public/_nuxt/CzOCSGLe.js +0 -1
  220. package/.output/public/_nuxt/D8qvkfyY.js +0 -1
  221. package/.output/public/_nuxt/DCn7QZ5N.js +0 -2
  222. package/.output/public/_nuxt/DS8qj8EL.js +0 -1
  223. package/.output/public/_nuxt/DUjW5Yq5.js +0 -1
  224. package/.output/public/_nuxt/Dihnimlk.js +0 -19
  225. package/.output/public/_nuxt/Dqw1lpr-.js +0 -1
  226. package/.output/public/_nuxt/Dr9v6C0f.js +0 -14
  227. package/.output/public/_nuxt/Du9ZJtnj.js +0 -1
  228. package/.output/public/_nuxt/OmpTerminalPanel.B_Pl5Ynm.css +0 -1
  229. package/.output/public/_nuxt/TownView.f-YOdXRs.css +0 -1
  230. package/.output/public/_nuxt/_id_.CM4bIgKY.css +0 -1
  231. package/.output/public/_nuxt/_id_.bNV69Dtf.css +0 -1
  232. package/.output/public/_nuxt/agents.ZD0rVdHE.css +0 -1
  233. package/.output/public/_nuxt/aml.BN8ZEW4h.css +0 -1
  234. package/.output/public/_nuxt/builds/meta/f4707884-7cf4-4a31-80d2-e3d72b73745f.json +0 -1
  235. package/.output/public/_nuxt/channel-templates.BOtahzPi.css +0 -1
  236. package/.output/public/_nuxt/daq.B0D3dPAc.css +0 -1
  237. package/.output/public/_nuxt/dcw.8Y7FqCVN.css +0 -1
  238. package/.output/public/_nuxt/pages.D7aSDlUI.css +0 -1
  239. package/.output/public/_nuxt/plugins.sb4nDGqr.css +0 -1
  240. package/.output/public/_nuxt/qr6R6Ha1.js +0 -1
  241. package/.output/public/_nuxt/settings.BTgR-NiE.css +0 -1
  242. package/.output/public/_nuxt/teams.CxnxKVVs.css +0 -1
  243. package/.output/public/_nuxt/tokens.BFY7qv4f.css +0 -1
  244. package/.output/public/_nuxt/workshop.CR_QzeAN.css +0 -1
  245. package/.output/server/chunks/build/_id_-BU1wfOID.mjs.map +0 -1
  246. package/.output/server/chunks/build/_id_-BYsEua57.mjs.map +0 -1
  247. package/.output/server/chunks/build/agents-CLQqJF2y.mjs.map +0 -1
  248. package/.output/server/chunks/build/aml-_7FdFL2p.mjs.map +0 -1
  249. package/.output/server/chunks/build/channel-templates-p6SZNjlM.mjs.map +0 -1
  250. package/.output/server/chunks/build/daq-cgyjKmJu.mjs.map +0 -1
  251. package/.output/server/chunks/build/dcw-CoTxBlAZ.mjs.map +0 -1
  252. package/.output/server/chunks/build/pages-DFxjn9Xa.mjs.map +0 -1
  253. package/.output/server/chunks/build/plugins-lGlUr1lj.mjs.map +0 -1
  254. package/.output/server/chunks/build/settings-Ddd1qx2Z.mjs.map +0 -1
  255. package/.output/server/chunks/build/teams-MEDLZdMn.mjs.map +0 -1
  256. package/.output/server/chunks/build/tokens-C7fTM5C2.mjs.map +0 -1
  257. package/.output/server/chunks/build/town-qMPql9Se.mjs.map +0 -1
  258. package/.output/server/chunks/build/useDaqStream-CIBaiGAl.mjs.map +0 -1
  259. package/.output/server/chunks/build/useDcwStream-CBbiYnWe.mjs.map +0 -1
  260. package/.output/server/chunks/build/workshop-BL5bhfs0.mjs.map +0 -1
  261. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +0 -1
  262. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +0 -1
  263. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +0 -153
  264. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +0 -153
  265. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +0 -431
  266. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +0 -180
  267. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +0 -112
  268. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +0 -227
  269. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +0 -131
  270. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +0 -160
  271. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +0 -157
  272. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +0 -348
  273. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +0 -296
  274. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +0 -285
  275. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +0 -217
  276. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +0 -301
  277. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +0 -151
  278. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +0 -135
  279. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +0 -156
  280. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +0 -72
  281. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +0 -469
  282. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +0 -259
  283. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +0 -97
  284. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +0 -277
  285. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +0 -171
  286. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +0 -425
  287. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +0 -99
  288. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +0 -325
  289. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +0 -332
  290. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +0 -43
  291. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +0 -154
  292. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +0 -531
  293. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +0 -439
  294. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +0 -323
  295. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +0 -433
  296. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +0 -280
  297. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +0 -117
  298. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +0 -160
  299. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +0 -101
  300. package/scripts/.mimosa/hook-status/sess_aeaecb58-8833-4b83-b93c-050467d21e03-d944d71bb3.json +0 -14
  301. package/scripts/_dbg-audit-all-pages.mjs +0 -43
  302. package/scripts/_dbg-daq-frames-e2e.mjs +0 -118
  303. package/scripts/_dbg-daq-rowstyle-verify.mjs +0 -112
  304. package/scripts/_dbg-hitl-timeout-e2e.mjs +0 -93
  305. package/scripts/_dbg-live-line-e2e.mjs +0 -279
  306. package/scripts/_dbg-minimal-omp.mjs +0 -77
  307. package/scripts/_dbg-mode-nav.mjs +0 -173
  308. package/scripts/_dbg-real-scenario-timeline.log +0 -55
  309. package/scripts/_dbg-team-repro.mjs +0 -76
  310. package/scripts/_dbg-town-agents-probe.mjs +0 -60
  311. package/scripts/_dbg-visual-acceptance.mjs +0 -63
  312. package/scripts/_townview_style.css +0 -1185
  313. package/scripts/_townview_template.html +0 -1215
  314. package/server/data/agent-node-bindings.json +0 -554
  315. package/server/data/daq-templates.json +0 -16
  316. package/server/data/daqs.json +0 -7464
  317. package/server/data/dcw-lines.json +0 -1787
  318. package/server/data/dcw-products.json +0 -1626
  319. package/server/data/dcw-recipes.json +0 -4672
  320. package/server/data/dcw-rollback.json +0 -20754
  321. package/server/data/dcw-runs.json +0 -6354
  322. package/server/data/dcw-templates.json +0 -146
  323. package/server/data/dcw-writes.json +0 -15650
  324. package/server/data/dcws.json +0 -9033
  325. package/server/data/device-twins.json +0 -133
  326. package/server/data/line-runs.json +0 -322
  327. package/server/data/scene-layouts.json +0 -277
@@ -1,2313 +1,2316 @@
1
- <script setup lang="ts">
2
- /**
3
- * 数采中心(DAQ Console)—— server 驱动数采的总控面。
4
- * 后端能力自描述(tsdb/queue/驱动族 + 管线指标)、控制器全局启停/周期、
5
- * 节点清单(状态/实时值/周期/绑定/驱动),点进 /daq/[id] 进入单节点专业控制台。
6
- */
7
- import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
8
- import { message } from 'ant-design-vue'
9
- import { useDaqStream, type DaqNodeLive } from '@/app/composables/workshop/useDaqStream'
10
- import { useDcwStream } from '@/app/composables/workshop/useDcwStream'
11
- import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
12
- import { DAQ_TEMPLATES, DAQ_DRIVERS, DAQ_TEMPLATE_ICONS, daqKeyFromRef, type DaqNodeView, type DaqNodeState, type DriverConfigField, type DriverTestResult as DaqDriverTestResult, type DaqTemplateDef, type DaqTemplateIcon } from '#shared/daq-protocol'
13
- import { useDeviceTwins } from '@/app/composables/workshop/useDeviceTwins'
14
- import { useOpsLog } from '@/app/composables/workshop/useOpsLog'
15
- import { useVisibleInterval } from '@/app/composables/workshop/useVisibleInterval'
16
-
17
- const { t: tt } = useI18n()
18
-
19
- definePageMeta({ layout: 'default' })
20
- useHead({ title: () => tt('titles.daq') })
21
-
22
- const daq = useDaqStream()
23
- const dcw = useDcwStream()
24
- /** WS 会话:连接层即注册 scene peer —— daq.reading 实时帧直达本页(不再等 5s 轮询) */
25
- const ws = useWorkshopWs()
26
-
27
- /** 是否存在任意运行中的产线(横幅判定) */
28
- const anyLineActive = computed(() => dcw.lines.some(l => dcw.lineStateOf(l.id).active))
29
-
30
- // ---------- 运维日志(全操作统一记录):实时事件轨渲染摘要,详情在 /logs 日志管理 ----------
31
- const opsLog = useOpsLog()
32
-
33
- function opsKindLabel(kind: string): string {
34
- const m: Record<string, string> = { write: tt('logs.kind.write'), manual: tt('logs.kind.manual'), alarm: tt('logs.kind.alarm'), line: tt('logs.kind.line'), recipe: tt('logs.kind.recipe'), rollback: tt('logs.kind.rollback'), daq: tt('logs.kind.daq'), system: tt('logs.kind.system') }
35
- return m[kind] ?? (kind || tt('logs.kind.system'))
36
- }
37
-
38
- function opsTime(at: string): string {
39
- const d = new Date(at)
40
- return Number.isFinite(d.getTime())
41
- ? `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
42
- : ''
43
- }
44
-
45
- /** 未确认报警确认(失败 toast 后端可读原因;成功后 fetchAlarms 已由 store 内部刷新) */
46
- async function ackOne(id: string): Promise<void> {
47
- try {
48
- await daq.ackAlarm(id)
49
- message.success(tt('daq.k1acked144'))
50
- }
51
- catch (err) {
52
- message.error(apiErrorMessage(err))
53
- }
54
- }
55
-
56
- let unsub: (() => void) | null = null
57
- onMounted(() => {
58
- // WS 会话建连(连接层即注册 scene peer):daq.reading 实时帧直达,不再等 5s 轮询
59
- ws.ensureConnected()
60
- unsub = daq.ensureWsFeed()
61
- // 运维日志实时事件轨(ops.log 帧;越限告警亦入轨 —— server 权威,详情在 /logs)
62
- opsLog.ensureLive()
63
- void daq.load()
64
- // 未确认报警首轮拉取(S5;WS daq.alarm 直推 + 此后随低频刷新拍轮询兜底)
65
- void daq.fetchAlarms()
66
- // 产线门控状态(无活动配方不采集;状态仅展示,控制在产线运营页)
67
- void dcw.load()
68
- // 设备注册表(绑定设备列:显示名 + 可编辑换绑;与数字孪生同源 bind REST)
69
- void deviceTwins.load()
70
- // Agent 优化记录首屏预取(面板展开即有数据;展开时仍会重拉一次保新鲜)
71
- void dcw.loadOptimizations()
72
- // meta 指标随读数帧落库节奏低频刷新(诚实可见的管线运行数据);
73
- // 产线运行态(横幅/产线列注记)同拍刷新 —— 开跑/停线后本页 ≤5s 收敛;
74
- // dcw 全量快照(95+ 线/配方/历史)是重载荷且行控状态已有 WS 帧收敛,降至 15s 兜底;
75
- // 轮询走可见性调度:后台自动降频(5s→30s / 15s→60s),回前台立即补拍,主线程不被占满
76
- useVisibleInterval(() => {
77
- void daq.load()
78
- void daq.fetchAlarms()
79
- }, 5000, { bgMs: 30000 })
80
- useVisibleInterval(() => {
81
- void dcw.load()
82
- }, 15000, { bgMs: 60000 })
83
- })
84
- onBeforeUnmount(() => {
85
- unsub?.()
86
- })
87
-
88
- // ---------- 节点筛选(产线/设备绑定/产线运行态/节点状态 + 模板/驱动/数据时间/名称搜索;全部 AND 复合) ----------
89
- const filters = reactive({ lineId: '', deviceId: '', template: '', driver: '', lineRun: '', state: '', time: '', search: '' })
90
- const hasFilters = computed(() => !!(filters.lineId || filters.deviceId || filters.template || filters.driver || filters.lineRun || filters.state || filters.time || filters.search.trim()))
91
- function clearFilters(): void {
92
- filters.lineId = ''
93
- filters.deviceId = ''
94
- filters.template = ''
95
- filters.driver = ''
96
- filters.lineRun = ''
97
- filters.state = ''
98
- filters.time = ''
99
- filters.search = ''
100
- }
101
-
102
- /** 模板筛选选项:节点中出现过的 templateRef(计数),名称走 server 模板目录降级原文 */
103
- const templateOptions = computed(() => {
104
- const used = new Map<string, number>()
105
- for (const n of daq.nodes) used.set(n.templateRef, (used.get(n.templateRef) ?? 0) + 1)
106
- return [...used.entries()].map(([ref, count]) => ({ ref, count, label: daqTemplateRefCh(ref) }))
107
- })
108
-
109
- /** 驱动筛选选项:节点中出现过的驱动(计数) */
110
- const driverOptions = computed(() => {
111
- const used = new Map<string, number>()
112
- for (const n of daq.nodes) used.set(n.driver, (used.get(n.driver) ?? 0) + 1)
113
- return [...used.entries()].map(([kind, count]) => ({
114
- kind,
115
- count,
116
- label: daq.meta.drivers.find(d => d.kind === kind)?.label ?? kind,
117
- }))
118
- })
119
-
120
- /** 数据时间语义桶:最后采样距 now 的Fresh度(与行级 stale 判定同源的诚实时间观) */
121
- function timeBucketOf(n: DaqNodeView): 'live' | 'hour' | 'day' | 'old' | 'never' {
122
- if (!n.lastAt) return 'never'
123
- const age = Date.now() - Date.parse(n.lastAt)
124
- if (age <= 5 * 60_000) return 'live'
125
- if (age <= 3_600_000) return 'hour'
126
- if (age <= 86_400_000) return 'day'
127
- return 'old'
128
- }
129
- function timeMatch(n: DaqNodeView): boolean {
130
- return !filters.time || timeBucketOf(n) === filters.time
131
- }
132
- function searchMatch(n: DaqNodeView): boolean {
133
- const q = filters.search.trim().toLowerCase()
134
- if (!q) return true
135
- return n.name.toLowerCase().includes(q) || n.id.toLowerCase().includes(q)
136
- }
137
-
138
- // ---------- Agent 优化记录(调控闭环;数采中心视角) ----------
139
- const optOpen = ref(false)
140
- /** 首次展开后保持内容挂载(手风琴动画需要);收起态内容不再重渲染 */
141
- const optMounted = ref(false)
142
- const optFilterLine = ref('')
143
- const optFilterRecipe = ref('')
144
- const optSeriesOf = ref<Record<string, { channels: Array<{ nodeId: string, nodeName: string, ch: string, unit: string, points: Array<{ at: number, value?: number, avg?: number }> }> }>>({})
145
- const optSeriesLoading = ref('')
146
-
147
- /** Recipe 选项:选定产线时只列该线配方,否则全量(与产线筛选级联) */
148
- const optRecipeOptions = computed(() =>
149
- dcw.recipes.filter(r => !optFilterLine.value || r.lineId === optFilterLine.value))
150
-
151
- function optStatusKey(s: string): string {
152
- const map: Record<string, string> = { 'open': 'daq.k1optst001', 'judged': 'daq.k1optst008', 'judged-keep': 'daq.k1optst002', 'rolled-back': 'daq.k1optst003', 'superseded': 'daq.k1optst004', 'superseded-manual': 'daq.k1optst005', 'closed-line-stop': 'daq.k1optst006', 'failed': 'daq.k1optst007' }
153
- return map[s] ?? 'daq.k1optst001'
154
- }
155
-
156
- async function toggleOptPanel(): Promise<void> {
157
- optOpen.value = !optOpen.value
158
- if (optOpen.value) {
159
- optMounted.value = true
160
- await filterOpts()
161
- }
162
- }
163
-
164
- async function filterOpts(): Promise<void> {
165
- await dcw.loadOptimizations({
166
- lineId: optFilterLine.value || undefined,
167
- recipeId: optFilterRecipe.value || undefined,
168
- })
169
- }
170
-
171
- /** 产线筛选变化:Recipe 级联重置(所选 Recipe 不在新产线内时) */
172
- function onOptLineChange(): void {
173
- if (optFilterRecipe.value && !optRecipeOptions.value.some(r => r.id === optFilterRecipe.value))
174
- optFilterRecipe.value = ''
175
- void filterOpts()
176
- }
177
-
178
- async function showOptSeries(id: string): Promise<void> {
179
- optSeriesLoading.value = id
180
- try {
181
- const s = await dcw.optimizationSeries(id)
182
- optSeriesOf.value = { ...optSeriesOf.value, [id]: s }
183
- }
184
- finally {
185
- optSeriesLoading.value = ''
186
- }
187
- }
188
-
189
- function seriesPath(points: Array<{ at: number, value?: number, avg?: number }>, w = 240, h = 36): string {
190
- const vals = points.map(p => p.value ?? p.avg ?? 0).filter(Number.isFinite)
191
- if (vals.length < 2) return ''
192
- const min = Math.min(...vals)
193
- const max = Math.max(...vals)
194
- const span = max - min || 1
195
- return vals.map((v, i) => `${(i / (vals.length - 1)) * w},${h - ((v - min) / span) * h}`).join(' ')
196
- }
197
-
198
- async function rollbackRecord(id: string): Promise<void> {
199
- await dcw.rollbackOptimization(id)
200
- await filterOpts()
201
- }
202
-
203
- /** 序列点换算的小数位(避免 sparkline 抖动) */
204
- function fmtPoint(v: number | null | undefined): string {
205
- return v == null ? '--' : String(Number(v.toFixed(2)))
206
- }
207
-
208
- /** 筛选下拉的设备选项:仅列出至少绑定了一个节点的设备(空设备筛选无意义) */
209
- const boundDevices = computed<Array<{ id: string, name: string, count: number }>>(() => {
210
- const used = new Map<string, number>()
211
- for (const n of daq.nodes) {
212
- for (const d of n.deviceIds ?? (n.deviceBindingId ? [n.deviceBindingId] : [])) {
213
- used.set(d, (used.get(d) ?? 0) + 1)
214
- }
215
- }
216
- return deviceTwins.twins
217
- .filter(t => used.has(t.id))
218
- .map(t => ({ id: t.id, name: nodeDevices.get(t.id) ?? t.name, count: used.get(t.id)! }))
219
- })
220
-
221
- /** 产线运行态筛选语义:on = 节点所属产线开跑中;off = 待机或未分配(均未采集) */
222
- function lineRunMatch(n: DaqNodeView): boolean {
223
- if (!filters.lineRun) return true
224
- const active = !!n.lineId && dcw.lineStateOf(n.lineId).active
225
- return filters.lineRun === 'on' ? active : !active
226
- }
227
-
228
- // ---------- 筛选产线的上下文横幅(运行态/产品/Recipe;未筛选到具体产线时回退全局门控提示) ----------
229
- const filteredLine = computed(() => dcw.lines.find(l => l.id === filters.lineId) ?? null)
230
- const filteredLineState = computed(() =>
231
- filteredLine.value ? dcw.lineStateOf(filteredLine.value.id) : null)
232
-
233
- // ---------- 行级展示状态(真实场景语义;筛选用,pill 呈现在 DaqNodeRow 行组件) ----------
234
- // 状态机:disabled(人为停用) → unassigned(未挂产线,永不采集) → idle(产线未开跑,采集门控)
235
- // → offline(网关暂停,或产线运行中却采不到新数据) → ok/warn/alarm(新鲜数据按量程/预警带/配方窗口派生)
236
- type RowState = DaqNodeState | 'idle' | 'disabled' | 'unassigned'
237
-
238
- /** 数据新鲜度:超过 max(4×有效周期, 12s) 无新样本视为「采不到数据」(容忍 5s REST 刷新拍与时钟偏差) */
239
- function staleOf(n: DaqNodeView): boolean {
240
- if (!n.lastAt) return true
241
- const iv = n.intervalMs ?? daq.controller.defaultIntervalMs
242
- return Date.now() - Date.parse(n.lastAt) > Math.max(iv * 4, 12_000)
243
- }
244
-
245
- function rowStateOf(n: DaqNodeView): RowState {
246
- if (!n.enabled) return 'disabled'
247
- if (!n.lineId) return 'unassigned'
248
- if (!dcw.lineStateOf(n.lineId).active) return 'idle'
249
- if (!daq.controller.running || staleOf(n)) return 'offline'
250
- return n.state
251
- }
252
-
253
- const filteredNodes = computed<DaqNodeLive[]>(() => daq.nodes.filter((n) => {
254
- if (filters.lineId && (filters.lineId === 'none' ? !!n.lineId : (n.lineId ?? '') !== filters.lineId)) return false
255
- const nodeDevIds = n.deviceIds ?? (n.deviceBindingId ? [n.deviceBindingId] : [])
256
- if (filters.deviceId && (filters.deviceId === 'none' ? nodeDevIds.length > 0 : !nodeDevIds.includes(filters.deviceId))) return false
257
- if (filters.template && n.templateRef !== filters.template) return false
258
- if (filters.driver && n.driver !== filters.driver) return false
259
- if (!lineRunMatch(n)) return false
260
- if (filters.state && rowStateOf(n) !== filters.state) return false
261
- if (!timeMatch(n)) return false
262
- if (!searchMatch(n)) return false
263
- return true
264
- }))
265
-
266
- /* ── 节点表窗口化渲染(virtual window) ────────────────────────────────────────
267
- * 为什么必须做:节点表是**全量**渲染的,而每一行都带实时读数 + sparkline,
268
- * 每次读数合批(500ms)都会点亮所有行。实测 498 个节点时:
269
- * · DOM 节点 13.5 万
270
- * · 主线程每秒 ≈3.8 万次 DOM 变更(其中 99.6% 来自这张表)
271
- * · 帧率掉到 24 FPS、92% 的帧超时、单帧最大间隔 361ms、长任务阻塞 936ms
272
- * 也就是说"数据刷新太频繁导致卡顿"的根因不是刷新频率,而是**离屏行也在被补丁**。
273
- *
274
- * 做法:按页面滚动窗口只挂载可见行 + 上下各 OVERSCAN 行,用两条占位 <tr> 撑起
275
- * 滚动高度。行的实际高度由 contain-intrinsic-size 固定为 42px(见样式),所以
276
- * 窗口计算不需要测量 DOM。
277
- *
278
- * 为什么用**页面滚动**而不是容器滚动:这张表的滚动条是页面的(.table-card 只负责
279
- * 横向),所以窗口要跟着 window.scrollY 走。
280
- */
281
- const VS_ROW_H = 42
282
- const VS_OVERSCAN = 8
283
- const nodesTableRef = ref<HTMLTableElement | null>(null)
284
- const winStart = ref(0)
285
- const winEnd = ref(60)
286
-
287
- const visibleNodes = computed<DaqNodeLive[]>(() => {
288
- const list = filteredNodes.value
289
- const s = Math.min(winStart.value, Math.max(0, list.length - 1))
290
- return list.slice(s, winEnd.value)
291
- })
292
- const padTopPx = computed(() => winStart.value * VS_ROW_H)
293
- const padBottomPx = computed(() => Math.max(0, (filteredNodes.value.length - winEnd.value) * VS_ROW_H))
294
-
295
- let winRaf = 0
296
- function updateNodeWindow(): void {
297
- const el = nodesTableRef.value
298
- if (!el) return
299
- const total = filteredNodes.value.length
300
- const vh = window.innerHeight || 800
301
- // 表格顶部相对文档的坐标(表头 40px 也算进"还没到第一行")
302
- const tableTop = el.getBoundingClientRect().top + window.scrollY + 40
303
- const firstVisible = Math.floor((window.scrollY - tableTop) / VS_ROW_H)
304
- const count = Math.ceil(vh / VS_ROW_H) + VS_OVERSCAN * 2
305
- const s = Math.max(0, Math.min(firstVisible - VS_OVERSCAN, Math.max(0, total - 1)))
306
- const e = Math.max(s, Math.min(total, s + count))
307
- if (s !== winStart.value) winStart.value = s
308
- if (e !== winEnd.value) winEnd.value = e
309
- }
310
- function scheduleNodeWindow(): void {
311
- if (winRaf) return
312
- winRaf = requestAnimationFrame(() => {
313
- winRaf = 0
314
- updateNodeWindow()
315
- })
316
- }
317
-
318
- onMounted(() => {
319
- updateNodeWindow()
320
- window.addEventListener('scroll', scheduleNodeWindow, { passive: true })
321
- window.addEventListener('resize', scheduleNodeWindow, { passive: true })
322
- })
323
- onBeforeUnmount(() => {
324
- window.removeEventListener('scroll', scheduleNodeWindow)
325
- window.removeEventListener('resize', scheduleNodeWindow)
326
- if (winRaf) cancelAnimationFrame(winRaf)
327
- })
328
- // 筛选/搜索改变行数后窗口必须重算(否则会停在旧的切片上,看起来"表空了")
329
- watch(() => filteredNodes.value.length, () => {
330
- winStart.value = 0
331
- winEnd.value = Math.min(filteredNodes.value.length, winEnd.value || 60)
332
- scheduleNodeWindow()
333
- })
334
-
335
- // 设备名映射(device-twins 注册表;绑定筛选下拉展示用)
336
- const deviceTwins = useDeviceTwins()
337
- const nodeDevices = new Map<string, string>()
338
- watch(() => deviceTwins.twins, (list) => {
339
- for (const t of list) nodeDevices.set(t.id, t.name)
340
- }, { immediate: true, deep: true })
341
-
342
- // ---------- 添加节点向导(mock / 真实场景 + 动态参数表单 + 测试连接) ----------
343
- const addOpen = ref(false)
344
- const addScenario = ref<'mock' | 'real'>('mock')
345
- const addTemplate = ref(DAQ_TEMPLATES[0]?.key ?? 'temp-tc')
346
- const addDriver = ref('modbus-tcp')
347
- const addName = ref('')
348
- const addInterval = ref<number | null>(null)
349
- const addCfg = ref<Record<string, string | number>>({})
350
- const addTransform = reactive({ kind: 'none' as 'none' | 'linear', scale: 1, offset: 0 })
351
- const addTesting = ref(false)
352
- const addTest = ref<DaqDriverTestResult | null>(null)
353
- const addSaving = ref(false)
354
- const addError = ref('')
355
-
356
- const addDriverMeta = computed(() => DAQ_DRIVERS.find(d => d.kind === addDriver.value))
357
- const addFields = computed<DriverConfigField[]>(() => addDriverMeta.value?.configFields ?? [])
358
- const driverReady = (kind: string): boolean =>
359
- daq.meta.drivers.find(d => d.kind === kind)?.status !== 'planned' && (daq.meta.driverAvailable?.[kind] !== false)
360
-
361
- watch(addDriver, () => {
362
- // 切协议:表单重置为 schema 缺省值
363
- const cfg: Record<string, string | number> = {}
364
- for (const f of addFields.value) {
365
- if (f.default !== undefined) cfg[f.key] = f.default as string | number
366
- }
367
- addCfg.value = cfg
368
- addTest.value = null
369
- }, { immediate: true })
370
-
371
- async function doTestConnection(): Promise<void> {
372
- addTesting.value = true
373
- addTest.value = null
374
- try {
375
- addTest.value = await daq.testDriver(addDriver.value, addCfg.value)
376
- }
377
- catch (err) {
378
- addTest.value = { ok: false, message: apiErrorMessage(err) }
379
- }
380
- finally {
381
- addTesting.value = false
382
- }
383
- }
384
-
385
- async function doAddNode(): Promise<void> {
386
- addSaving.value = true
387
- addError.value = ''
388
- try {
389
- const transform = addTransform.kind === 'linear'
390
- ? { kind: 'linear' as const, scale: Number(addTransform.scale), offset: Number(addTransform.offset) }
391
- : undefined
392
- if (addScenario.value === 'mock') {
393
- await daq.createFromTemplate(`daq-${addTemplate.value}`, {
394
- name: addName.value ? { name: addName.value }.name : undefined,
395
- transform,
396
- })
397
- }
398
- else {
399
- // 校验必填
400
- for (const f of addFields.value) {
401
- if (f.required && !addCfg.value[f.key] && addCfg.value[f.key] !== 0) {
402
- throw new Error(tt('daq.kyuadl0125', { p0: f.label }))
403
- }
404
- }
405
- const tpl = daq.templates.find(t => t.key === addTemplate.value)
406
- await daq.createFromTemplate(`daq-${addTemplate.value}`, {
407
- name: addName.value || undefined,
408
- driver: addDriver.value as never,
409
- driverConfig: { ...addCfg.value },
410
- transform,
411
- intervalMs: addInterval.value,
412
- unit: tpl?.unit,
413
- min: tpl?.min,
414
- max: tpl?.max,
415
- decimals: tpl?.decimals,
416
- } as never)
417
- }
418
- addOpen.value = false
419
- addName.value = ''
420
- addTest.value = null
421
- }
422
- catch (err) {
423
- addError.value = apiErrorMessage(err)
424
- }
425
- finally {
426
- addSaving.value = false
427
- }
428
- }
429
-
430
- /** 模板通道语义(server 目录为唯一事实源;模板已删除 → 显示 templateRef 原文降级) */
431
- function daqTemplateRefCh(templateRef: string): string {
432
- const tpl = daq.templates.find(t => t.key === daqKeyFromRef(templateRef))
433
- return tpl ? `${tpl.name} · ${tpl.ch}` : templateRef || '-'
434
- }
435
-
436
- // ---------- 自定义信号模板管理(server 权威 CRUD;内置只读可复制) ----------
437
- const tplOpen = ref(false)
438
- const tplEditing = ref<string | null>(null)
439
- const tplSaving = ref(false)
440
- const tplError = ref('')
441
- const confirmingDel = ref('')
442
- const tplForm = reactive({
443
- name: '', ch: '', code: '', unit: '',
444
- min: 0, max: 100, base: 50, amp: 2,
445
- decimals: 2, icon: 'thermo' as DaqTemplateIcon,
446
- })
447
- const ICON_LABEL: Record<DaqTemplateIcon, string> = {
448
- thermo: tt('daq.k422b8079'), pressure: tt('daq.k3x6ff080'), tension: tt('daq.k3z9xc081'), encoder: tt('daq.k3subk4082'), camera: tt('daq.k47atw083'), gateway: tt('daq.kz7yhbj084'),
449
- }
450
-
451
- const customTpls = computed<DaqTemplateDef[]>(() => daq.templates.filter(t => !t.builtin))
452
- const builtinTpls = computed<DaqTemplateDef[]>(() => daq.templates.filter(t => t.builtin))
453
-
454
- function fillTplForm(t: DaqTemplateDef, asCopy = false): void {
455
- tplForm.name = asCopy ? tt('daq.k2hbo3c126', { p0: t.name }) : t.name
456
- tplForm.ch = t.ch
457
- tplForm.code = t.code
458
- tplForm.unit = t.unit
459
- tplForm.min = t.min
460
- tplForm.max = t.max
461
- tplForm.base = t.base
462
- tplForm.amp = t.amp
463
- tplForm.decimals = t.decimals
464
- tplForm.icon = t.icon
465
- }
466
-
467
- function resetTplForm(): void {
468
- tplEditing.value = null
469
- tplError.value = ''
470
- confirmingDel.value = ''
471
- fillTplForm({ key: '', name: '', code: '', ch: '', unit: '', base: 50, amp: 2, min: 0, max: 100, decimals: 2, icon: 'thermo' })
472
- }
473
-
474
- function editTpl(t: DaqTemplateDef): void {
475
- tplEditing.value = t.key
476
- tplError.value = ''
477
- fillTplForm(t)
478
- }
479
-
480
- function copyTpl(t: DaqTemplateDef): void {
481
- tplEditing.value = null
482
- tplError.value = ''
483
- fillTplForm(t, true)
484
- }
485
-
486
- async function saveTpl(): Promise<void> {
487
- tplSaving.value = true
488
- tplError.value = ''
489
- try {
490
- const num = (v: number | string): number | undefined => {
491
- const n = Number(v)
492
- return v === '' || v == null || !Number.isFinite(n) ? undefined : n
493
- }
494
- const input = {
495
- name: tplForm.name.trim(),
496
- ch: tplForm.ch.trim() || undefined,
497
- code: tplForm.code.trim() || undefined,
498
- unit: tplForm.unit.trim(),
499
- min: num(tplForm.min)!,
500
- max: num(tplForm.max)!,
501
- base: num(tplForm.base),
502
- amp: num(tplForm.amp),
503
- decimals: num(tplForm.decimals),
504
- icon: tplForm.icon,
505
- }
506
- if (tplEditing.value) await daq.updateTemplate(tplEditing.value, input)
507
- else await daq.createTemplate(input)
508
- resetTplForm()
509
- }
510
- catch (err) {
511
- tplError.value = apiErrorMessage(err)
512
- }
513
- finally {
514
- tplSaving.value = false
515
- }
516
- }
517
-
518
- /** 两段式删除确认(避免误删;空目录风格与节点表一致) */
519
- async function askDelTpl(t: DaqTemplateDef): Promise<void> {
520
- if (confirmingDel.value !== t.key) {
521
- confirmingDel.value = t.key
522
- return
523
- }
524
- try {
525
- await daq.removeTemplate(t.key)
526
- }
527
- catch (err) {
528
- tplError.value = apiErrorMessage(err)
529
- }
530
- finally {
531
- confirmingDel.value = ''
532
- }
533
- }
534
-
535
- const reconnecting = ref(false)
536
- async function doReconnect(): Promise<void> {
537
- reconnecting.value = true
538
- try {
539
- await daq.reconnectInfra()
540
- }
541
- catch { /* 横幅仍在,30s 后台也会自动重试 */ }
542
- finally {
543
- reconnecting.value = false
544
- }
545
- }
546
- </script>
547
-
548
- <template>
549
- <div class="page">
550
- <div class="aw-page-head">
551
- <div>
552
- <p class="aw-kicker">
553
- AGENTWORKSHOP / DAQ CONSOLE
554
- </p>
555
- <h1>{{ $t('daq.k1emg364011') }}</h1>
556
- <p class="sub">
557
- {{ $t('daq.k1wmg2bz012') }}
558
- </p>
559
- </div>
560
- <div class="badges mono">
561
- <span
562
- class="badge"
563
- :title="$t('daq.k1tsdbtip133')"
564
- >TSDB · {{ daq.meta.tsdb }}</span>
565
- <span
566
- class="badge"
567
- :title="$t('daq.k1queuet134')"
568
- >QUEUE · {{ daq.meta.queue }}</span>
569
- </div>
570
- </div>
571
-
572
- <!-- 产线门控横幅:筛选到具体产线 → 展示该产线运行态/产品/Recipe(待机可直达产线管理);
573
- 未筛选具体产线 → 全局门控提示 -->
574
- <div
575
- v-if="daq.loaded && filteredLine && filteredLineState"
576
- class="infra-banner"
577
- :class="{ good: filteredLineState.active }"
578
- >
579
- <span :class="filteredLineState.active ? 'i-tabler-circle-check' : 'i-tabler-info-circle'" />
580
- <span class="txt">
581
- <template v-if="filteredLineState.active">
582
- {{ $t('daq.k3ktxbr085') }}{{ filteredLine.name }}」{{ $t('daq.k15lvw0o105') }}<b>{{ filteredLineState.productName }}</b> · Recipe:<b>{{ filteredLineState.recipeName }}</b>
583
- <small class="mono">{{ $t('daq.k400lb086') }} {{ filteredLineState.runId?.slice(0, 8) }} · {{ $t('daq.k3zkt2106') }} {{ filteredLineState.startedAt?.slice(11, 19) }} {{ $t('daq.k69vag8108') }} {{ filteredLineState.taggedSamples }} {{ $t('daq.k4118o092') }}</small>
584
- </template>
585
- <template v-else>
586
- {{ $t('daq.k3ktxbr085') }}{{ filteredLine.name }}」{{ $t('daq.k12uge2o107') }}</template>
587
- </span>
588
- <NuxtLink
589
- class="pill-btn"
590
- :to="`/dcw/${filteredLine.id}`"
591
- >
592
- {{ filteredLineState.active ? $t('daq.k1ukoy0v093') : $t('daq.k1cg78i8109') }}
593
- </NuxtLink>
594
- </div>
595
- <div
596
- v-else-if="daq.loaded && !anyLineActive"
597
- class="infra-banner"
598
- >
599
- <span class="i-tabler-info-circle" />
600
- <span class="txt">{{ $t('daq.k19onw5w013') }}<NuxtLink to="/dcw">{{ $t('daq.k1b2tk5c014') }}</NuxtLink>{{ $t('daq.k1u9wnlg015') }}</span>
601
- </div>
602
-
603
- <!-- 基础设施降级横幅(MQTT/Timescale 不可达:在线采集停用 + 一键重连) -->
604
- <div
605
- v-if="daq.meta.infra?.degraded"
606
- class="infra-banner"
607
- >
608
- <span class="i-tabler-alert-triangle" />
609
- <span class="txt">{{ daq.meta.infra.warning }}</span>
610
- <button
611
- class="pill-btn"
612
- :disabled="reconnecting"
613
- @click="doReconnect"
614
- >
615
- {{ reconnecting ? $t('daq.k1ld43ur094') : $t('daq.kzg9805110') }}
616
- </button>
617
- </div>
618
-
619
- <!-- 未确认报警条(S5):边缘触发产生 / 轮询收敛,确认后立即消失 -->
620
- <div
621
- v-if="daq.alarms.length"
622
- class="alarm-bar"
623
- >
624
- <span class="bar-label">
625
- <span class="i-tabler-bell-ringing bell" />
626
- {{ $t('daq.k1alarm141') }}
627
- <b class="mono">{{ daq.alarms.length }}</b>
628
- </span>
629
- <div class="alarm-scroll">
630
- <span
631
- v-for="a in daq.alarms"
632
- :key="a.id"
633
- class="alarm-item"
634
- :title="$t('daq.k1esc143', { p0: a.escalation })"
635
- >
636
- <b>{{ a.nodeName }}</b>
637
- <span class="mono dim">{{ a.metric }}</span>
638
- <span class="mono">{{ a.value }} {{ a.rule === 'lt-min' ? '<' : '>' }} {{ a.threshold }}</span>
639
- <small
640
- v-if="a.escalation > 0"
641
- class="esc mono"
642
- >+{{ a.escalation }}</small>
643
- <button
644
- class="mini-btn ack"
645
- @click="ackOne(a.id)"
646
- >
647
- {{ $t('daq.k1ack142') }}
648
- </button>
649
- </span>
650
- </div>
651
- </div>
652
-
653
- <!-- 控制器总控条:第一行 控制/指标/操作,第二行 产线状态带 -->
654
- <section class="aw-tile ctrl-card">
655
- <div class="ctrl-row">
656
- <div class="ctrl-left">
657
- <button
658
- class="aw-pill"
659
- :class="{ running: daq.controller.running }"
660
- :title="$t('daq.k1snap145')"
661
- @click="daq.controllerAction(daq.controller.running ? 'pause' : 'resume')"
662
- >
663
- <span :class="daq.controller.running ? 'i-tabler-player-pause' : 'i-tabler-player-play'" />
664
- {{ daq.controller.running ? $t('daq.kpui00095') : $t('daq.knxpdpt111') }}
665
- </button>
666
- <label class="cycle mono">
667
- {{ $t('daq.k1ifahxj016') }}
668
- <input
669
- v-model.number="daq.controller.defaultIntervalMs"
670
- type="number"
671
- min="200"
672
- max="60000"
673
- step="100"
674
- @change="daq.controllerAction('config', daq.controller.defaultIntervalMs)"
675
- >ms
676
- </label>
677
- <label
678
- class="cycle mono"
679
- :title="$t('daq.k1e8lcoo001')"
680
- >
681
- {{ $t('daq.k1if98n0017') }}
682
- <input
683
- v-model.number="daq.controller.defaultPublishIntervalMs"
684
- type="number"
685
- min="0"
686
- max="60000"
687
- step="100"
688
- @change="daq.controllerAction('config', daq.controller.defaultIntervalMs, daq.controller.defaultPublishIntervalMs)"
689
- >ms
690
- </label>
691
- </div>
692
- <div class="ctrl-right">
693
- <span class="ctrl-metrics mono">
694
- <span>{{ $t('daq.k45uio067') }} {{ daq.controller.nodesOnline }}/{{ daq.controller.nodesTotal }}</span>
695
- <span class="sep">·</span>
696
- <span :title="$t('daq.k1yztsu4002')">{{ $t('daq.k3xagp087') }} {{ daq.meta.produced }}</span>
697
- <span class="sep">·</span>
698
- <span :title="$t('daq.ko09iha003')">{{ $t('daq.k427eu088') }} {{ daq.meta.consumed }}</span>
699
- <span class="sep">·</span>
700
- <span
701
- :class="{ warn: (daq.meta.dropped ?? 0) > 0 }"
702
- :title="$t('daq.k1droptip135')"
703
- >{{ $t('daq.k3w8go089') }} {{ daq.meta.dropped }}</span>
704
- <span class="sep">·</span>
705
- <span :title="$t('daq.kj1jbmw004')">{{ $t('daq.k3wusd090') }} {{ daq.meta.samplesStored }}</span>
706
- </span>
707
- <button
708
- class="aw-pill outline add-btn"
709
- @click="tplOpen = true; resetTplForm()"
710
- >
711
- <span class="i-tabler-adjustments-horizontal" />
712
- {{ $t('daq.k1f5cv0s018') }}
713
- </button>
714
- <button
715
- class="aw-pill add-btn"
716
- @click="addOpen = true"
717
- >
718
- <span class="i-tabler-plus" />
719
- {{ $t('daq.k1fn0ukb019') }}
720
- </button>
721
- </div>
722
- </div>
723
- <!-- 产线状态带:单行横向滚动,每产线一枚 pill(空心点=待机 / 实心呼吸点=运行中) -->
724
- <div class="line-strip">
725
- <span class="strip-label">
726
- <span class="i-tabler-route" />
727
- {{ $t('daq.k1b2o3b6020') }}<small class="mono">{{ dcw.lines.length }}</small>
728
- </span>
729
- <div class="strip-scroll">
730
- <NuxtLink
731
- v-for="l in dcw.lines"
732
- :key="l.id"
733
- class="line-pill"
734
- :class="{ on: dcw.lineStateOf(l.id).active }"
735
- :style="{ '--lc': l.color }"
736
- :to="`/dcw/${l.id}`"
737
- :title="dcw.lineStateOf(l.id).active
738
- ? $t('daq.k3v957u120', { p0: l.name, p1: dcw.lineStateOf(l.id).productName, p2: dcw.lineStateOf(l.id).recipeName })
739
- : $t('daq.k1x7jnru121', { p0: l.name })"
740
- >
741
- <span class="lp-dot" />
742
- <b>{{ l.name }}</b>
743
- <small
744
- v-if="dcw.lineStateOf(l.id).active"
745
- class="lp-run"
746
- >{{ dcw.lineStateOf(l.id).recipeName ?? $t('daq.k3vp67i096') }}</small>
747
- <small
748
- v-else
749
- class="lp-idle"
750
- >{{ $t('daq.k3zgkk021') }}</small>
751
- </NuxtLink>
752
- <NuxtLink
753
- v-if="dcw.lines.length === 0"
754
- class="line-pill"
755
- to="/dcw"
756
- >
757
- <span class="lp-dot" />
758
- <b>{{ $t('daq.k1el12b1022') }}</b>
759
- <small class="lp-idle">{{ $t('daq.k8jxxe8023') }}</small>
760
- </NuxtLink>
761
- </div>
762
- </div>
763
- </section>
764
-
765
- <!-- 添加节点向导:mock 模拟 / 真实设备(协议参数 + 测试连接) -->
766
- <div
767
- v-if="addOpen"
768
- class="modal-mask"
769
- @click.self="addOpen = false"
770
- >
771
- <div class="modal">
772
- <h3 class="m-title">
773
- {{ $t('daq.k19rioqa024') }}
774
- </h3>
775
-
776
- <div class="seg-row">
777
- <button
778
- class="seg"
779
- :class="{ on: addScenario === 'mock' }"
780
- @click="addScenario = 'mock'"
781
- >
782
- {{ $t('daq.k1p5c1un025') }}
783
- </button>
784
- <button
785
- class="seg"
786
- :class="{ on: addScenario === 'real' }"
787
- @click="addScenario = 'real'"
788
- >
789
- {{ $t('daq.k1cbcp3o026') }}
790
- </button>
791
- </div>
792
-
793
- <div class="f-grid">
794
- <label class="f">
795
- <span>{{ $t('daq.k1fsgerc027') }}</span>
796
- <select
797
- v-model="addTemplate"
798
- class="inp"
799
- >
800
- <option
801
- v-for="t in daq.templates"
802
- :key="t.key"
803
- :value="t.key"
804
- >
805
- {{ t.name }} · {{ t.ch }}({{ t.min }}~{{ t.max }} {{ t.unit }}){{ t.builtin ? '' : $t('daq.kr45rk9097') }}
806
- </option>
807
- </select>
808
- </label>
809
- <label class="f">
810
- <span>{{ $t('daq.k1ce2k1y028') }}</span>
811
- <input
812
- v-model="addName"
813
- class="inp"
814
- :placeholder="$t('daq.kgpxzy3005')"
815
- >
816
- </label>
817
- <label
818
- v-if="addScenario === 'real'"
819
- class="f"
820
- >
821
- <span>{{ $t('daq.k13mfsfc029') }}</span>
822
- <input
823
- v-model.number="addInterval"
824
- type="number"
825
- min="1000"
826
- max="60000"
827
- step="500"
828
- class="inp"
829
- :placeholder="$t('daq.k1eg1000a136')"
830
- >
831
- </label>
832
- </div>
833
-
834
- <!-- 真实场景:协议选择 + 动态参数表单 + 测试连接 -->
835
- <template v-if="addScenario === 'real'">
836
- <div class="f-grid">
837
- <label class="f">
838
- <span>{{ $t('daq.k1kt87rx030') }}</span>
839
- <select
840
- v-model="addDriver"
841
- class="inp"
842
- >
843
- <option
844
- v-for="d in DAQ_DRIVERS.filter(x => x.status !== 'planned')"
845
- :key="d.kind"
846
- :value="d.kind"
847
- >
848
- {{ d.label }}{{ driverReady(d.kind) ? '' : $t('daq.kjbqphp098') }}
849
- </option>
850
- </select>
851
- </label>
852
- </div>
853
-
854
- <div
855
- v-if="addFields.length"
856
- class="f-grid driver-form"
857
- >
858
- <label
859
- v-for="f in addFields"
860
- :key="f.key"
861
- class="f"
862
- >
863
- <span>{{ f.label }}<em v-if="f.required">*</em></span>
864
- <select
865
- v-if="f.type === 'select'"
866
- v-model="addCfg[f.key]"
867
- class="inp"
868
- >
869
- <option
870
- v-for="o in f.options"
871
- :key="o.value"
872
- :value="o.value"
873
- >
874
- {{ o.label }}
875
- </option>
876
- </select>
877
- <input
878
- v-else
879
- v-model="addCfg[f.key]"
880
- :type="f.type === 'number' ? 'number' : 'text'"
881
- :placeholder="f.placeholder"
882
- class="inp"
883
- >
884
- <small
885
- v-if="f.hint"
886
- class="hint"
887
- >{{ f.hint }}</small>
888
- </label>
889
- </div>
890
-
891
- <div class="test-row">
892
- <button
893
- class="pill-btn"
894
- :disabled="addTesting"
895
- @click="doTestConnection"
896
- >
897
- {{ addTesting ? $t('daq.k1fsh720099') : $t('daq.k1fstglk112') }}
898
- </button>
899
- <span
900
- v-if="addTest"
901
- class="test-result"
902
- :class="addTest.ok ? 'ok' : 'bad'"
903
- >{{ addTest.ok ? '✓' : '✗' }} {{ addTest.message }}<template v-if="addTest.latencyMs != null">({{ addTest.latencyMs }}ms)</template></span>
904
- </div>
905
- </template>
906
-
907
- <p
908
- v-if="addError"
909
- class="m-err"
910
- >
911
- {{ addError }}
912
- </p>
913
-
914
- <div class="m-actions">
915
- <button
916
- class="aw-pill outline"
917
- @click="addOpen = false"
918
- >
919
- {{ $t('daq.k3xdnn031') }}
920
- </button>
921
- <button
922
- class="aw-pill"
923
- :disabled="addSaving || (addScenario === 'real' && addTest != null && !addTest.ok)"
924
- :title="addScenario === 'real' && !(addTest && addTest.ok) ? $t('daq.k1needtst137') : ''"
925
- @click="doAddNode"
926
- >
927
- {{ addSaving ? $t('daq.k1bg4759100') : (addScenario === 'real' ? $t('daq.k7pxjxo113') : $t('daq.k1bge46t118')) }}
928
- </button>
929
- </div>
930
- </div>
931
- </div>
932
-
933
- <!-- 模板管理:自定义模板增删改 + 内置模板复制 -->
934
- <div
935
- v-if="tplOpen"
936
- class="modal-mask"
937
- @click.self="tplOpen = false"
938
- >
939
- <div class="modal">
940
- <h3 class="m-title">
941
- {{ $t('daq.k11mv96s032') }}
942
- </h3>
943
-
944
- <p class="sec-label">
945
- {{ $t('daq.ktji4ky033') }}
946
- </p>
947
- <table class="tpl-table">
948
- <tbody>
949
- <tr
950
- v-for="t in customTpls"
951
- :key="t.key"
952
- >
953
- <td>
954
- <b>{{ t.name }}</b>
955
- <small class="mono dim">{{ t.code }}</small>
956
- </td>
957
- <td class="mono range">
958
- {{ t.min }}~{{ t.max }} {{ t.unit }} · {{ t.decimals }} {{ $t('daq.k48oi091') }}
959
- </td>
960
- <td class="right actions">
961
- <button
962
- class="mini-btn"
963
- @click="editTpl(t)"
964
- >
965
- {{ $t('daq.k45eb0034') }}
966
- </button>
967
- <button
968
- class="mini-btn danger"
969
- @click="askDelTpl(t)"
970
- >
971
- {{ confirmingDel === t.key ? $t('daq.k1hhheu3101') : $t('daq.k3xakp114') }}
972
- </button>
973
- </td>
974
- </tr>
975
- <tr v-if="!customTpls.length">
976
- <td
977
- colspan="3"
978
- class="empty"
979
- >
980
- {{ $t('daq.kztvzbo035') }}
981
- </td>
982
- </tr>
983
- </tbody>
984
- </table>
985
-
986
- <p class="sec-label">
987
- {{ tplEditing ? $t('daq.k1iiph0s102') : $t('daq.k1efixrj115') }}
988
- </p>
989
- <div class="f-grid">
990
- <label class="f">
991
- <span>{{ $t('daq.k3xhia036') }}<em>*</em></span>
992
- <input
993
- v-model="tplForm.name"
994
- class="inp"
995
- :placeholder="$t('daq.k5opd2t006')"
996
- >
997
- </label>
998
- <label class="f">
999
- <span>{{ $t('daq.k3x4ef037') }}<em>*</em></span>
1000
- <input
1001
- v-model="tplForm.unit"
1002
- class="inp"
1003
- placeholder="如 %RH"
1004
- >
1005
- </label>
1006
- <label class="f">
1007
- <span>{{ $t('daq.k1l477m0038') }}</span>
1008
- <input
1009
- v-model="tplForm.ch"
1010
- class="inp"
1011
- :placeholder="$t('daq.kk8o4tl007')"
1012
- >
1013
- </label>
1014
- <label class="f">
1015
- <span>{{ $t('daq.k1ayxrqb039') }}</span>
1016
- <input
1017
- v-model="tplForm.code"
1018
- class="inp"
1019
- :placeholder="$t('daq.kzn3l7l008')"
1020
- >
1021
- </label>
1022
- <label class="f">
1023
- <span>{{ $t('daq.k1l9jv5m040') }}<em>*</em></span>
1024
- <input
1025
- v-model.number="tplForm.min"
1026
- type="number"
1027
- class="inp"
1028
- >
1029
- </label>
1030
- <label class="f">
1031
- <span>{{ $t('daq.k1l9jv4p041') }}<em>*</em></span>
1032
- <input
1033
- v-model.number="tplForm.max"
1034
- type="number"
1035
- class="inp"
1036
- >
1037
- </label>
1038
- <label class="f">
1039
- <span>{{ $t('daq.k3mxmcx042') }}</span>
1040
- <input
1041
- v-model.number="tplForm.decimals"
1042
- type="number"
1043
- min="0"
1044
- max="4"
1045
- class="inp"
1046
- >
1047
- </label>
1048
- <label class="f">
1049
- <span>{{ $t('daq.k3xx56043') }}</span>
1050
- <select
1051
- v-model="tplForm.icon"
1052
- class="inp"
1053
- >
1054
- <option
1055
- v-for="ic in DAQ_TEMPLATE_ICONS"
1056
- :key="ic"
1057
- :value="ic"
1058
- >
1059
- {{ ICON_LABEL[ic] }}
1060
- </option>
1061
- </select>
1062
- </label>
1063
- <label class="f">
1064
- <span>{{ $t('daq.k1f4eknv044') }}</span>
1065
- <input
1066
- v-model.number="tplForm.base"
1067
- type="number"
1068
- class="inp"
1069
- >
1070
- <small class="hint">{{ $t('daq.kytd15s045') }}</small>
1071
- </label>
1072
- <label class="f">
1073
- <span>{{ $t('daq.k1f4ifpo046') }}</span>
1074
- <input
1075
- v-model.number="tplForm.amp"
1076
- type="number"
1077
- class="inp"
1078
- >
1079
- <small class="hint">{{ $t('daq.ksp2aji047') }}</small>
1080
- </label>
1081
- </div>
1082
- <p
1083
- v-if="tplError"
1084
- class="m-err"
1085
- >
1086
- {{ tplError }}
1087
- </p>
1088
- <div class="m-actions">
1089
- <button
1090
- class="aw-pill outline"
1091
- @click="resetTplForm"
1092
- >
1093
- {{ $t('daq.k48p40048') }}
1094
- </button>
1095
- <button
1096
- class="pill-btn"
1097
- :disabled="tplSaving"
1098
- @click="saveTpl"
1099
- >
1100
- {{ tplSaving ? $t('daq.k1b38d59103') : (tplEditing ? $t('daq.k1b39281116') : $t('daq.k1b3dtga119')) }}
1101
- </button>
1102
- </div>
1103
-
1104
- <p class="sec-label">
1105
- {{ $t('daq.k1w8s84s049') }}
1106
- </p>
1107
- <table class="tpl-table">
1108
- <tbody>
1109
- <tr
1110
- v-for="t in builtinTpls"
1111
- :key="t.key"
1112
- >
1113
- <td>
1114
- <b>{{ t.name }}</b>
1115
- <small class="mono dim">{{ t.code }}</small>
1116
- </td>
1117
- <td class="mono range">
1118
- {{ t.min }}~{{ t.max }} {{ t.unit }} · {{ t.decimals }} {{ $t('daq.k48oi091') }}
1119
- </td>
1120
- <td class="right actions">
1121
- <button
1122
- class="mini-btn"
1123
- @click="copyTpl(t)"
1124
- >
1125
- {{ $t('daq.k3y694050') }}
1126
- </button>
1127
- </td>
1128
- </tr>
1129
- </tbody>
1130
- </table>
1131
- </div>
1132
- </div>
1133
-
1134
- <!-- Agent 优化记录(调控闭环:设定历史 / 判定 / 窗口数采;序列为客户端渲染) -->
1135
- <section class="aw-tile opt-card">
1136
- <button
1137
- class="opt-head"
1138
- @click="toggleOptPanel"
1139
- >
1140
- <span class="i-tabler-flask" />
1141
- <b>{{ $t('daq.k1optt001') }}</b>
1142
- <span class="opt-count mono">{{ dcw.optimizations.length }}</span>
1143
- <span
1144
- class="opt-chevron"
1145
- :class="{ open: optOpen }"
1146
- />
1147
- </button>
1148
- <div
1149
- class="opt-body"
1150
- :class="{ open: optOpen }"
1151
- >
1152
- <template v-if="optMounted">
1153
- <div class="opt-filters">
1154
- <select
1155
- v-model="optFilterLine"
1156
- class="inp-sel"
1157
- @change="onOptLineChange"
1158
- >
1159
- <option value="">
1160
- {{ $t('daq.k1optf001') }}
1161
- </option>
1162
- <option
1163
- v-for="l in dcw.lines"
1164
- :key="l.id"
1165
- :value="l.id"
1166
- >
1167
- {{ l.name }}
1168
- </option>
1169
- </select>
1170
- <select
1171
- v-model="optFilterRecipe"
1172
- class="inp-sel"
1173
- @change="filterOpts"
1174
- >
1175
- <option value="">
1176
- {{ $t('daq.k1optf004') }}
1177
- </option>
1178
- <option
1179
- v-for="r in optRecipeOptions"
1180
- :key="r.id"
1181
- :value="r.id"
1182
- >
1183
- {{ r.name }}
1184
- </option>
1185
- </select>
1186
- <button
1187
- class="mini-act"
1188
- @click="filterOpts"
1189
- >
1190
- {{ $t('daq.k1optf002') }}
1191
- </button>
1192
- </div>
1193
- <p
1194
- v-if="dcw.optimizations.length === 0"
1195
- class="opt-empty"
1196
- >
1197
- {{ $t('daq.k1opte001') }}
1198
- </p>
1199
- <ul class="opt-list">
1200
- <li
1201
- v-for="r in dcw.optimizations.slice(0, 30)"
1202
- :key="r.id"
1203
- class="opt-row"
1204
- :class="r.status"
1205
- >
1206
- <div class="opt-line1">
1207
- <span
1208
- class="opt-status"
1209
- :class="r.status"
1210
- >{{ $t(optStatusKey(r.status)) }}</span>
1211
- <b class="opt-node">{{ r.nodeName }}</b>
1212
- <span class="mono opt-params">{{ r.params[0]?.from ?? '?' }} → {{ r.params[0]?.to }}</span>
1213
- <span class="mono opt-time">{{ r.setAt.slice(5, 16).replace('T', ' ') }}</span>
1214
- <button
1215
- v-if="!optSeriesOf[r.id]"
1216
- class="mini-act"
1217
- :disabled="optSeriesLoading === r.id"
1218
- @click="showOptSeries(r.id)"
1219
- >
1220
- {{ $t('daq.k1opta001') }}
1221
- </button>
1222
- <button
1223
- v-if="r.status === 'open'"
1224
- class="mini-act danger"
1225
- @click="rollbackRecord(r.id)"
1226
- >
1227
- {{ $t('daq.k1opta002') }}
1228
- </button>
1229
- </div>
1230
- <div class="opt-line2 mono">
1231
- <span
1232
- v-if="r.judge"
1233
- class="opt-judge"
1234
- :class="r.judge.verdict"
1235
- >{{ r.judge.verdict }}({{ r.judge.by }}): {{ r.judge.reason.slice(0, 70) }}</span>
1236
- <span
1237
- v-else-if="r.status === 'open'"
1238
- class="opt-open-hint"
1239
- >{{ $t('daq.k1opto001') }}</span>
1240
- <span
1241
- v-if="r.aggPending"
1242
- class="opt-agg"
1243
- >{{ $t('daq.k1optw001') }}</span>
1244
- <span
1245
- v-else-if="r.windowAgg && r.windowAgg.channels.length"
1246
- class="opt-agg"
1247
- >
1248
- <template
1249
- v-for="c in r.windowAgg.channels"
1250
- :key="c.daqNodeId"
1251
- >{{ c.ch }} {{ fmtPoint(c.min) }}~{{ fmtPoint(c.max) }}({{ $t('daq.k1optw002') }}{{ c.breaches < 0 ? '-' : c.breaches }}) </template>
1252
- </span>
1253
- </div>
1254
- <div
1255
- v-if="optSeriesOf[r.id]"
1256
- class="opt-series"
1257
- >
1258
- <div
1259
- v-for="ch in (optSeriesOf[r.id]?.channels ?? [])"
1260
- :key="ch.nodeId"
1261
- class="opt-ch"
1262
- >
1263
- <svg
1264
- class="opt-spark"
1265
- viewBox="0 0 240 36"
1266
- preserveAspectRatio="none"
1267
- >
1268
- <polyline :points="seriesPath(ch.points)" />
1269
- </svg>
1270
- <span class="mono">{{ ch.ch }} {{ fmtPoint(ch.points.at(-1)?.value ?? ch.points.at(-1)?.avg) }}{{ ch.unit }}</span>
1271
- </div>
1272
- <p
1273
- v-if="(optSeriesOf[r.id]?.channels.length ?? 0) === 0"
1274
- class="opt-empty"
1275
- >
1276
- {{ $t('daq.k1opte002') }}
1277
- </p>
1278
- </div>
1279
- </li>
1280
- </ul>
1281
- </template>
1282
- </div>
1283
- </section>
1284
-
1285
- <!-- 节点清单:筛选工具条(产线/设备/产线运行/状态) + 节点表 -->
1286
- <a-spin :spinning="!daq.loaded && !daq.error">
1287
- <section class="aw-tile table-card">
1288
- <div class="tbl-toolbar">
1289
- <div class="filters">
1290
- <label class="flt">
1291
- <span>{{ $t('daq.k3wj9n051') }}</span>
1292
- <select
1293
- v-model="filters.lineId"
1294
- class="inp-sel"
1295
- >
1296
- <option value="">
1297
- {{ $t('daq.k1bkl9uj052') }}
1298
- </option>
1299
- <option
1300
- v-for="l in dcw.lines"
1301
- :key="l.id"
1302
- :value="l.id"
1303
- >
1304
- {{ l.name }}
1305
- </option>
1306
- <option value="none">
1307
- {{ $t('daq.k3ootr6053') }}
1308
- </option>
1309
- </select>
1310
- </label>
1311
- <label class="flt">
1312
- <span>{{ $t('daq.k1i8rtqt054') }}</span>
1313
- <select
1314
- v-model="filters.deviceId"
1315
- class="inp-sel"
1316
- >
1317
- <option value="">
1318
- {{ $t('daq.k1bkw4m2055') }}
1319
- </option>
1320
- <option
1321
- v-for="d in boundDevices"
1322
- :key="d.id"
1323
- :value="d.id"
1324
- >
1325
- {{ d.name }}({{ d.count }})
1326
- </option>
1327
- <option value="none">
1328
- {{ $t('daq.k3own4q056') }}
1329
- </option>
1330
- </select>
1331
- </label>
1332
- <label
1333
- class="flt"
1334
- :title="$t('daq.khb8mru009')"
1335
- >
1336
- <span>{{ $t('daq.k1b2tkyv057') }}</span>
1337
- <select
1338
- v-model="filters.lineRun"
1339
- class="inp-sel"
1340
- >
1341
- <option value="">
1342
- {{ $t('daq.k3x4t1058') }}
1343
- </option>
1344
- <option value="on">
1345
- {{ $t('daq.k1nxm3p7059') }}
1346
- </option>
1347
- <option value="off">
1348
- {{ $t('daq.k1ulx34e060') }}
1349
- </option>
1350
- </select>
1351
- </label>
1352
- <label class="flt">
1353
- <span>{{ $t('daq.k1iwdfef061') }}</span>
1354
- <select
1355
- v-model="filters.state"
1356
- class="inp-sel"
1357
- >
1358
- <option value="">
1359
- {{ $t('daq.k3x4t1058') }}
1360
- </option>
1361
- <option value="ok">
1362
- {{ $t('daq.k41k5c062') }}
1363
- </option>
1364
- <option value="warn">
1365
- {{ $t('daq.k49z8v063') }}
1366
- </option>
1367
- <option value="alarm">
1368
- {{ $t('daq.k3xmid064') }}
1369
- </option>
1370
- <option value="offline">
1371
- {{ $t('daq.k44c2n065') }}
1372
- </option>
1373
- </select>
1374
- </label>
1375
- <label class="flt">
1376
- <span>{{ $t('daq.k1flttpl010') }}</span>
1377
- <select
1378
- v-model="filters.template"
1379
- class="inp-sel"
1380
- >
1381
- <option value="">
1382
- {{ $t('daq.k1flttpl011') }}
1383
- </option>
1384
- <option
1385
- v-for="t in templateOptions"
1386
- :key="t.ref"
1387
- :value="t.ref"
1388
- >
1389
- {{ t.label }}({{ t.count }})
1390
- </option>
1391
- </select>
1392
- </label>
1393
- <label class="flt">
1394
- <span>{{ $t('daq.k1fltdrv012') }}</span>
1395
- <select
1396
- v-model="filters.driver"
1397
- class="inp-sel"
1398
- >
1399
- <option value="">
1400
- {{ $t('daq.k1fltdrv013') }}
1401
- </option>
1402
- <option
1403
- v-for="d in driverOptions"
1404
- :key="d.kind"
1405
- :value="d.kind"
1406
- >
1407
- {{ d.label }}({{ d.count }})
1408
- </option>
1409
- </select>
1410
- </label>
1411
- <label
1412
- class="flt"
1413
- :title="$t('daq.k1flttim014')"
1414
- >
1415
- <span>{{ $t('daq.k1flttim015') }}</span>
1416
- <select
1417
- v-model="filters.time"
1418
- class="inp-sel"
1419
- >
1420
- <option value="">
1421
- {{ $t('daq.k1flttim016') }}
1422
- </option>
1423
- <option value="live">
1424
- {{ $t('daq.k1timlv017') }}
1425
- </option>
1426
- <option value="hour">
1427
- {{ $t('daq.k1timhr018') }}
1428
- </option>
1429
- <option value="day">
1430
- {{ $t('daq.k1timdy019') }}
1431
- </option>
1432
- <option value="old">
1433
- {{ $t('daq.k1timld020') }}
1434
- </option>
1435
- <option value="never">
1436
- {{ $t('daq.k1timnv021') }}
1437
- </option>
1438
- </select>
1439
- </label>
1440
- <label class="flt">
1441
- <span>{{ $t('daq.k1fltsrc022') }}</span>
1442
- <input
1443
- v-model="filters.search"
1444
- class="inp-sel flt-search"
1445
- type="search"
1446
- :placeholder="$t('daq.k1fltsph023')"
1447
- >
1448
- </label>
1449
- <button
1450
- v-if="hasFilters"
1451
- class="mini-btn clear-btn"
1452
- @click="clearFilters"
1453
- >
1454
- {{ $t('daq.k1fygck2066') }}
1455
- </button>
1456
- </div>
1457
- <span class="count mono">{{ filteredNodes.length }} / {{ daq.nodes.length }} {{ $t('daq.k45uio067') }}</span>
1458
- </div>
1459
- <!-- 实时告警 + 实时事件:告警 = server 权威未确认报警(WS 直推/可确认);事件 = 全操作摘要轨(详情在日志管理) -->
1460
- <div class="ops-panels">
1461
- <div
1462
- class="ops-panel alarm-panel"
1463
- :class="{ live: daq.alarms.length > 0 }"
1464
- >
1465
- <div class="panel-head">
1466
- <span class="i-tabler-bell-ringing bell" />
1467
- <b>{{ $t('daq.k1alarm141') }}</b>
1468
- <span
1469
- class="mono cnt"
1470
- :class="{ hot: daq.alarms.length > 0 }"
1471
- >{{ daq.alarms.length }}</span>
1472
- </div>
1473
- <div class="panel-body">
1474
- <p
1475
- v-if="daq.alarms.length === 0"
1476
- class="panel-empty"
1477
- >
1478
- {{ $t('daq.k1evt001') }}
1479
- </p>
1480
- <div
1481
- v-for="a in daq.alarms"
1482
- v-else
1483
- :key="a.id"
1484
- class="alarm-row"
1485
- >
1486
- <span class="i-tabler-alert-triangle tri" />
1487
- <b>{{ a.nodeName }}</b>
1488
- <span class="mono dim">{{ a.metric }}</span>
1489
- <span class="mono">{{ a.value }} {{ a.rule === 'lt-min' ? '<' : '>' }} {{ a.threshold }}</span>
1490
- <small
1491
- v-if="a.escalation > 0"
1492
- class="esc mono"
1493
- >+{{ a.escalation }}</small>
1494
- <button
1495
- class="mini-btn ack"
1496
- @click="ackOne(a.id)"
1497
- >
1498
- {{ $t('daq.k1ack142') }}
1499
- </button>
1500
- </div>
1501
- </div>
1502
- </div>
1503
- <div class="ops-panel event-panel">
1504
- <div class="panel-head">
1505
- <span class="i-tabler-activity" />
1506
- <b>{{ $t('daq.k1evt002') }}</b>
1507
- <NuxtLink
1508
- class="panel-link"
1509
- to="/logs"
1510
- >
1511
- {{ $t('daq.k1evt003') }}<span class="i-tabler-arrow-right" />
1512
- </NuxtLink>
1513
- </div>
1514
- <div class="panel-body">
1515
- <p
1516
- v-if="opsLog.recent.length === 0"
1517
- class="panel-empty"
1518
- >
1519
- {{ $t('daq.k1evt004') }}
1520
- </p>
1521
- <div
1522
- v-for="(r, i) in opsLog.recent"
1523
- v-else
1524
- :key="`${r.at}:${r.actor}:${r.action}:${i}`"
1525
- class="evt-row"
1526
- >
1527
- <span class="mono dim t">{{ opsTime(r.at) }}</span>
1528
- <span
1529
- class="src-badge"
1530
- :class="r.actorKind"
1531
- >{{ r.actorKind === 'agent' ? 'Agent' : r.actorKind === 'user' ? $t('logs.src.user') : $t('logs.src.system') }}</span>
1532
- <span
1533
- class="kind-chip"
1534
- :class="r.kind"
1535
- >{{ opsKindLabel(r.kind) }}</span>
1536
- <span
1537
- class="txt"
1538
- :title="r.summary"
1539
- >{{ r.summary }}</span>
1540
- </div>
1541
- </div>
1542
- </div>
1543
- </div>
1544
- <table
1545
- ref="nodesTableRef"
1546
- class="nodes-table"
1547
- >
1548
- <thead>
1549
- <tr>
1550
- <th>{{ $t('daq.k45uio067') }}</th>
1551
- <th>{{ $t('daq.k42w8s068') }}</th>
1552
- <th>{{ $t('daq.k3mv305069') }}</th>
1553
- <th>{{ $t('daq.k1trnd001') }}</th>
1554
- <th>{{ $t('daq.k1l6g2ga070') }}</th>
1555
- <th>{{ $t('daq.k3zi0nf071') }}</th>
1556
- <th>{{ $t('daq.k4a0la072') }}</th>
1557
- <th>{{ $t('daq.k3wj9n051') }}</th>
1558
- <th :title="$t('daq.k1runtipc131')">
1559
- {{ $t('daq.k1b2tkyv057') }}
1560
- </th>
1561
- <th :title="$t('daq.k1prdrtip132')">
1562
- {{ $t('daq.k1prodrc128') }}
1563
- </th>
1564
- <th>{{ $t('daq.k1i8rtqt054') }}</th>
1565
- <th class="right">
1566
- {{ $t('daq.k40aa6073') }}
1567
- </th>
1568
- </tr>
1569
- </thead>
1570
- <tbody>
1571
- <!-- 上占位行:撑起"已滚过"的高度,让滚动条与真实全量列表一致 -->
1572
- <tr
1573
- v-if="padTopPx > 0"
1574
- class="vs-pad"
1575
- aria-hidden="true"
1576
- >
1
+ <script setup lang="ts">
2
+ /**
3
+ * 数采中心(DAQ Console)—— server 驱动数采的总控面。
4
+ * 后端能力自描述(tsdb/queue/驱动族 + 管线指标)、控制器全局启停/周期、
5
+ * 节点清单(状态/实时值/周期/绑定/驱动),点进 /daq/[id] 进入单节点专业控制台。
6
+ */
7
+ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
8
+ import { message } from 'ant-design-vue'
9
+ import { useDaqStream, type DaqNodeLive } from '@/app/composables/workshop/useDaqStream'
10
+ import { useDcwStream } from '@/app/composables/workshop/useDcwStream'
11
+ import { useWorkshopWs } from '@/app/composables/workshop/useWorkshopWs'
12
+ import { DAQ_TEMPLATES, DAQ_DRIVERS, DAQ_TEMPLATE_ICONS, daqKeyFromRef, type DaqDriverCatalogEntry, type DaqNodeView, type DaqNodeState, type DriverConfigField, type DriverTestResult as DaqDriverTestResult, type DaqTemplateDef, type DaqTemplateIcon } from '#shared/daq-protocol'
13
+ import { useDeviceTwins } from '@/app/composables/workshop/useDeviceTwins'
14
+ import { useOpsLog } from '@/app/composables/workshop/useOpsLog'
15
+ import { useVisibleInterval } from '@/app/composables/workshop/useVisibleInterval'
16
+
17
+ const { t: tt } = useI18n()
18
+
19
+ definePageMeta({ layout: 'default' })
20
+ useHead({ title: () => tt('titles.daq') })
21
+
22
+ const daq = useDaqStream()
23
+ const dcw = useDcwStream()
24
+ /** WS 会话:连接层即注册 scene peer —— daq.reading 实时帧直达本页(不再等 5s 轮询) */
25
+ const ws = useWorkshopWs()
26
+
27
+ /** 是否存在任意运行中的产线(横幅判定) */
28
+ const anyLineActive = computed(() => dcw.lines.some(l => dcw.lineStateOf(l.id).active))
29
+
30
+ // ---------- 运维日志(全操作统一记录):实时事件轨渲染摘要,详情在 /logs 日志管理 ----------
31
+ const opsLog = useOpsLog()
32
+
33
+ function opsKindLabel(kind: string): string {
34
+ const m: Record<string, string> = { write: tt('logs.kind.write'), manual: tt('logs.kind.manual'), alarm: tt('logs.kind.alarm'), line: tt('logs.kind.line'), recipe: tt('logs.kind.recipe'), rollback: tt('logs.kind.rollback'), daq: tt('logs.kind.daq'), system: tt('logs.kind.system') }
35
+ return m[kind] ?? (kind || tt('logs.kind.system'))
36
+ }
37
+
38
+ function opsTime(at: string): string {
39
+ const d = new Date(at)
40
+ return Number.isFinite(d.getTime())
41
+ ? `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`
42
+ : ''
43
+ }
44
+
45
+ /** 未确认报警确认(失败 toast 后端可读原因;成功后 fetchAlarms 已由 store 内部刷新) */
46
+ async function ackOne(id: string): Promise<void> {
47
+ try {
48
+ await daq.ackAlarm(id)
49
+ message.success(tt('daq.k1acked144'))
50
+ }
51
+ catch (err) {
52
+ message.error(apiErrorMessage(err))
53
+ }
54
+ }
55
+
56
+ let unsub: (() => void) | null = null
57
+ onMounted(() => {
58
+ // WS 会话建连(连接层即注册 scene peer):daq.reading 实时帧直达,不再等 5s 轮询
59
+ ws.ensureConnected()
60
+ unsub = daq.ensureWsFeed()
61
+ // 运维日志实时事件轨(ops.log 帧;越限告警亦入轨 —— server 权威,详情在 /logs)
62
+ opsLog.ensureLive()
63
+ void daq.load()
64
+ // 未确认报警首轮拉取(S5;WS daq.alarm 直推 + 此后随低频刷新拍轮询兜底)
65
+ void daq.fetchAlarms()
66
+ // 产线门控状态(无活动配方不采集;状态仅展示,控制在产线运营页)
67
+ void dcw.load()
68
+ // 设备注册表(绑定设备列:显示名 + 可编辑换绑;与数字孪生同源 bind REST)
69
+ void deviceTwins.load()
70
+ // Agent 优化记录首屏预取(面板展开即有数据;展开时仍会重拉一次保新鲜)
71
+ void dcw.loadOptimizations()
72
+ // meta 指标随读数帧落库节奏低频刷新(诚实可见的管线运行数据);
73
+ // 产线运行态(横幅/产线列注记)同拍刷新 —— 开跑/停线后本页 ≤5s 收敛;
74
+ // dcw 全量快照(95+ 线/配方/历史)是重载荷且行控状态已有 WS 帧收敛,降至 15s 兜底;
75
+ // 轮询走可见性调度:后台自动降频(5s→30s / 15s→60s),回前台立即补拍,主线程不被占满
76
+ useVisibleInterval(() => {
77
+ void daq.load()
78
+ void daq.fetchAlarms()
79
+ }, 5000, { bgMs: 30000 })
80
+ useVisibleInterval(() => {
81
+ void dcw.load()
82
+ }, 15000, { bgMs: 60000 })
83
+ })
84
+ onBeforeUnmount(() => {
85
+ unsub?.()
86
+ })
87
+
88
+ // ---------- 节点筛选(产线/设备绑定/产线运行态/节点状态 + 模板/驱动/数据时间/名称搜索;全部 AND 复合) ----------
89
+ const filters = reactive({ lineId: '', deviceId: '', template: '', driver: '', lineRun: '', state: '', time: '', search: '' })
90
+ const hasFilters = computed(() => !!(filters.lineId || filters.deviceId || filters.template || filters.driver || filters.lineRun || filters.state || filters.time || filters.search.trim()))
91
+ function clearFilters(): void {
92
+ filters.lineId = ''
93
+ filters.deviceId = ''
94
+ filters.template = ''
95
+ filters.driver = ''
96
+ filters.lineRun = ''
97
+ filters.state = ''
98
+ filters.time = ''
99
+ filters.search = ''
100
+ }
101
+
102
+ /** 模板筛选选项:节点中出现过的 templateRef(计数),名称走 server 模板目录降级原文 */
103
+ const templateOptions = computed(() => {
104
+ const used = new Map<string, number>()
105
+ for (const n of daq.nodes) used.set(n.templateRef, (used.get(n.templateRef) ?? 0) + 1)
106
+ return [...used.entries()].map(([ref, count]) => ({ ref, count, label: daqTemplateRefCh(ref) }))
107
+ })
108
+
109
+ /** 驱动筛选选项:节点中出现过的驱动(计数) */
110
+ const driverOptions = computed(() => {
111
+ const used = new Map<string, number>()
112
+ for (const n of daq.nodes) used.set(n.driver, (used.get(n.driver) ?? 0) + 1)
113
+ return [...used.entries()].map(([kind, count]) => ({
114
+ kind,
115
+ count,
116
+ label: daq.meta.drivers.find(d => d.kind === kind)?.label ?? kind,
117
+ }))
118
+ })
119
+
120
+ /** 数据时间语义桶:最后采样距 now 的Fresh度(与行级 stale 判定同源的诚实时间观) */
121
+ function timeBucketOf(n: DaqNodeView): 'live' | 'hour' | 'day' | 'old' | 'never' {
122
+ if (!n.lastAt) return 'never'
123
+ const age = Date.now() - Date.parse(n.lastAt)
124
+ if (age <= 5 * 60_000) return 'live'
125
+ if (age <= 3_600_000) return 'hour'
126
+ if (age <= 86_400_000) return 'day'
127
+ return 'old'
128
+ }
129
+ function timeMatch(n: DaqNodeView): boolean {
130
+ return !filters.time || timeBucketOf(n) === filters.time
131
+ }
132
+ function searchMatch(n: DaqNodeView): boolean {
133
+ const q = filters.search.trim().toLowerCase()
134
+ if (!q) return true
135
+ return n.name.toLowerCase().includes(q) || n.id.toLowerCase().includes(q)
136
+ }
137
+
138
+ // ---------- Agent 优化记录(调控闭环;数采中心视角) ----------
139
+ const optOpen = ref(false)
140
+ /** 首次展开后保持内容挂载(手风琴动画需要);收起态内容不再重渲染 */
141
+ const optMounted = ref(false)
142
+ const optFilterLine = ref('')
143
+ const optFilterRecipe = ref('')
144
+ const optSeriesOf = ref<Record<string, { channels: Array<{ nodeId: string, nodeName: string, ch: string, unit: string, points: Array<{ at: number, value?: number, avg?: number }> }> }>>({})
145
+ const optSeriesLoading = ref('')
146
+
147
+ /** Recipe 选项:选定产线时只列该线配方,否则全量(与产线筛选级联) */
148
+ const optRecipeOptions = computed(() =>
149
+ dcw.recipes.filter(r => !optFilterLine.value || r.lineId === optFilterLine.value))
150
+
151
+ function optStatusKey(s: string): string {
152
+ const map: Record<string, string> = { 'open': 'daq.k1optst001', 'judged': 'daq.k1optst008', 'judged-keep': 'daq.k1optst002', 'rolled-back': 'daq.k1optst003', 'superseded': 'daq.k1optst004', 'superseded-manual': 'daq.k1optst005', 'closed-line-stop': 'daq.k1optst006', 'failed': 'daq.k1optst007' }
153
+ return map[s] ?? 'daq.k1optst001'
154
+ }
155
+
156
+ async function toggleOptPanel(): Promise<void> {
157
+ optOpen.value = !optOpen.value
158
+ if (optOpen.value) {
159
+ optMounted.value = true
160
+ await filterOpts()
161
+ }
162
+ }
163
+
164
+ async function filterOpts(): Promise<void> {
165
+ await dcw.loadOptimizations({
166
+ lineId: optFilterLine.value || undefined,
167
+ recipeId: optFilterRecipe.value || undefined,
168
+ })
169
+ }
170
+
171
+ /** 产线筛选变化:Recipe 级联重置(所选 Recipe 不在新产线内时) */
172
+ function onOptLineChange(): void {
173
+ if (optFilterRecipe.value && !optRecipeOptions.value.some(r => r.id === optFilterRecipe.value))
174
+ optFilterRecipe.value = ''
175
+ void filterOpts()
176
+ }
177
+
178
+ async function showOptSeries(id: string): Promise<void> {
179
+ optSeriesLoading.value = id
180
+ try {
181
+ const s = await dcw.optimizationSeries(id)
182
+ optSeriesOf.value = { ...optSeriesOf.value, [id]: s }
183
+ }
184
+ finally {
185
+ optSeriesLoading.value = ''
186
+ }
187
+ }
188
+
189
+ function seriesPath(points: Array<{ at: number, value?: number, avg?: number }>, w = 240, h = 36): string {
190
+ const vals = points.map(p => p.value ?? p.avg ?? 0).filter(Number.isFinite)
191
+ if (vals.length < 2) return ''
192
+ const min = Math.min(...vals)
193
+ const max = Math.max(...vals)
194
+ const span = max - min || 1
195
+ return vals.map((v, i) => `${(i / (vals.length - 1)) * w},${h - ((v - min) / span) * h}`).join(' ')
196
+ }
197
+
198
+ async function rollbackRecord(id: string): Promise<void> {
199
+ await dcw.rollbackOptimization(id)
200
+ await filterOpts()
201
+ }
202
+
203
+ /** 序列点换算的小数位(避免 sparkline 抖动) */
204
+ function fmtPoint(v: number | null | undefined): string {
205
+ return v == null ? '--' : String(Number(v.toFixed(2)))
206
+ }
207
+
208
+ /** 筛选下拉的设备选项:仅列出至少绑定了一个节点的设备(空设备筛选无意义) */
209
+ const boundDevices = computed<Array<{ id: string, name: string, count: number }>>(() => {
210
+ const used = new Map<string, number>()
211
+ for (const n of daq.nodes) {
212
+ for (const d of n.deviceIds ?? (n.deviceBindingId ? [n.deviceBindingId] : [])) {
213
+ used.set(d, (used.get(d) ?? 0) + 1)
214
+ }
215
+ }
216
+ return deviceTwins.twins
217
+ .filter(t => used.has(t.id))
218
+ .map(t => ({ id: t.id, name: nodeDevices.get(t.id) ?? t.name, count: used.get(t.id)! }))
219
+ })
220
+
221
+ /** 产线运行态筛选语义:on = 节点所属产线开跑中;off = 待机或未分配(均未采集) */
222
+ function lineRunMatch(n: DaqNodeView): boolean {
223
+ if (!filters.lineRun) return true
224
+ const active = !!n.lineId && dcw.lineStateOf(n.lineId).active
225
+ return filters.lineRun === 'on' ? active : !active
226
+ }
227
+
228
+ // ---------- 筛选产线的上下文横幅(运行态/产品/Recipe;未筛选到具体产线时回退全局门控提示) ----------
229
+ const filteredLine = computed(() => dcw.lines.find(l => l.id === filters.lineId) ?? null)
230
+ const filteredLineState = computed(() =>
231
+ filteredLine.value ? dcw.lineStateOf(filteredLine.value.id) : null)
232
+
233
+ // ---------- 行级展示状态(真实场景语义;筛选用,pill 呈现在 DaqNodeRow 行组件) ----------
234
+ // 状态机:disabled(人为停用) → unassigned(未挂产线,永不采集) → idle(产线未开跑,采集门控)
235
+ // → offline(网关暂停,或产线运行中却采不到新数据) → ok/warn/alarm(新鲜数据按量程/预警带/配方窗口派生)
236
+ type RowState = DaqNodeState | 'idle' | 'disabled' | 'unassigned'
237
+
238
+ /** 数据新鲜度:超过 max(4×有效周期, 12s) 无新样本视为「采不到数据」(容忍 5s REST 刷新拍与时钟偏差) */
239
+ function staleOf(n: DaqNodeView): boolean {
240
+ if (!n.lastAt) return true
241
+ const iv = n.intervalMs ?? daq.controller.defaultIntervalMs
242
+ return Date.now() - Date.parse(n.lastAt) > Math.max(iv * 4, 12_000)
243
+ }
244
+
245
+ function rowStateOf(n: DaqNodeView): RowState {
246
+ if (!n.enabled) return 'disabled'
247
+ if (!n.lineId) return 'unassigned'
248
+ if (!dcw.lineStateOf(n.lineId).active) return 'idle'
249
+ if (!daq.controller.running || staleOf(n)) return 'offline'
250
+ return n.state
251
+ }
252
+
253
+ const filteredNodes = computed<DaqNodeLive[]>(() => daq.nodes.filter((n) => {
254
+ if (filters.lineId && (filters.lineId === 'none' ? !!n.lineId : (n.lineId ?? '') !== filters.lineId)) return false
255
+ const nodeDevIds = n.deviceIds ?? (n.deviceBindingId ? [n.deviceBindingId] : [])
256
+ if (filters.deviceId && (filters.deviceId === 'none' ? nodeDevIds.length > 0 : !nodeDevIds.includes(filters.deviceId))) return false
257
+ if (filters.template && n.templateRef !== filters.template) return false
258
+ if (filters.driver && n.driver !== filters.driver) return false
259
+ if (!lineRunMatch(n)) return false
260
+ if (filters.state && rowStateOf(n) !== filters.state) return false
261
+ if (!timeMatch(n)) return false
262
+ if (!searchMatch(n)) return false
263
+ return true
264
+ }))
265
+
266
+ /* ── 节点表窗口化渲染(virtual window) ────────────────────────────────────────
267
+ * 为什么必须做:节点表是**全量**渲染的,而每一行都带实时读数 + sparkline,
268
+ * 每次读数合批(500ms)都会点亮所有行。实测 498 个节点时:
269
+ * · DOM 节点 13.5 万
270
+ * · 主线程每秒 ≈3.8 万次 DOM 变更(其中 99.6% 来自这张表)
271
+ * · 帧率掉到 24 FPS、92% 的帧超时、单帧最大间隔 361ms、长任务阻塞 936ms
272
+ * 也就是说"数据刷新太频繁导致卡顿"的根因不是刷新频率,而是**离屏行也在被补丁**。
273
+ *
274
+ * 做法:按页面滚动窗口只挂载可见行 + 上下各 OVERSCAN 行,用两条占位 <tr> 撑起
275
+ * 滚动高度。行的实际高度由 contain-intrinsic-size 固定为 42px(见样式),所以
276
+ * 窗口计算不需要测量 DOM。
277
+ *
278
+ * 为什么用**页面滚动**而不是容器滚动:这张表的滚动条是页面的(.table-card 只负责
279
+ * 横向),所以窗口要跟着 window.scrollY 走。
280
+ */
281
+ const VS_ROW_H = 42
282
+ const VS_OVERSCAN = 8
283
+ const nodesTableRef = ref<HTMLTableElement | null>(null)
284
+ const winStart = ref(0)
285
+ const winEnd = ref(60)
286
+
287
+ const visibleNodes = computed<DaqNodeLive[]>(() => {
288
+ const list = filteredNodes.value
289
+ const s = Math.min(winStart.value, Math.max(0, list.length - 1))
290
+ return list.slice(s, winEnd.value)
291
+ })
292
+ const padTopPx = computed(() => winStart.value * VS_ROW_H)
293
+ const padBottomPx = computed(() => Math.max(0, (filteredNodes.value.length - winEnd.value) * VS_ROW_H))
294
+
295
+ let winRaf = 0
296
+ function updateNodeWindow(): void {
297
+ const el = nodesTableRef.value
298
+ if (!el) return
299
+ const total = filteredNodes.value.length
300
+ const vh = window.innerHeight || 800
301
+ // 表格顶部相对文档的坐标(表头 40px 也算进"还没到第一行")
302
+ const tableTop = el.getBoundingClientRect().top + window.scrollY + 40
303
+ const firstVisible = Math.floor((window.scrollY - tableTop) / VS_ROW_H)
304
+ const count = Math.ceil(vh / VS_ROW_H) + VS_OVERSCAN * 2
305
+ const s = Math.max(0, Math.min(firstVisible - VS_OVERSCAN, Math.max(0, total - 1)))
306
+ const e = Math.max(s, Math.min(total, s + count))
307
+ if (s !== winStart.value) winStart.value = s
308
+ if (e !== winEnd.value) winEnd.value = e
309
+ }
310
+ function scheduleNodeWindow(): void {
311
+ if (winRaf) return
312
+ winRaf = requestAnimationFrame(() => {
313
+ winRaf = 0
314
+ updateNodeWindow()
315
+ })
316
+ }
317
+
318
+ onMounted(() => {
319
+ updateNodeWindow()
320
+ window.addEventListener('scroll', scheduleNodeWindow, { passive: true })
321
+ window.addEventListener('resize', scheduleNodeWindow, { passive: true })
322
+ })
323
+ onBeforeUnmount(() => {
324
+ window.removeEventListener('scroll', scheduleNodeWindow)
325
+ window.removeEventListener('resize', scheduleNodeWindow)
326
+ if (winRaf) cancelAnimationFrame(winRaf)
327
+ })
328
+ // 筛选/搜索改变行数后窗口必须重算(否则会停在旧的切片上,看起来"表空了")
329
+ watch(() => filteredNodes.value.length, () => {
330
+ winStart.value = 0
331
+ winEnd.value = Math.min(filteredNodes.value.length, winEnd.value || 60)
332
+ scheduleNodeWindow()
333
+ })
334
+
335
+ // 设备名映射(device-twins 注册表;绑定筛选下拉展示用)
336
+ const deviceTwins = useDeviceTwins()
337
+ const nodeDevices = new Map<string, string>()
338
+ watch(() => deviceTwins.twins, (list) => {
339
+ for (const t of list) nodeDevices.set(t.id, t.name)
340
+ }, { immediate: true, deep: true })
341
+
342
+ // ---------- 添加节点向导(mock / 真实场景 + 动态参数表单 + 测试连接) ----------
343
+ const addOpen = ref(false)
344
+ const addScenario = ref<'mock' | 'real'>('mock')
345
+ const addTemplate = ref(DAQ_TEMPLATES[0]?.key ?? 'temp-tc')
346
+ const addDriver = ref('modbus-tcp')
347
+ const addName = ref('')
348
+ const addInterval = ref<number | null>(null)
349
+ const addCfg = ref<Record<string, string | number>>({})
350
+ const addTransform = reactive({ kind: 'none' as 'none' | 'linear', scale: 1, offset: 0 })
351
+ const addTesting = ref(false)
352
+ const addTest = ref<DaqDriverTestResult | null>(null)
353
+ const addSaving = ref(false)
354
+ const addError = ref('')
355
+
356
+ // 驱动目录:server 权威(内置 + 协议插件自描述;REST 未返回时回落静态目录)
357
+ const driverCatalog = computed<DaqDriverCatalogEntry[]>(() =>
358
+ daq.meta.drivers.length ? daq.meta.drivers : DAQ_DRIVERS.map(d => ({ ...d })))
359
+ const addDriverMeta = computed(() => driverCatalog.value.find(d => d.kind === addDriver.value))
360
+ const addFields = computed<DriverConfigField[]>(() => addDriverMeta.value?.configFields ?? [])
361
+ const driverReady = (kind: string): boolean =>
362
+ daq.meta.drivers.find(d => d.kind === kind)?.status !== 'planned' && (daq.meta.driverAvailable?.[kind] !== false)
363
+
364
+ watch(addDriver, () => {
365
+ // 切协议:表单重置为 schema 缺省值
366
+ const cfg: Record<string, string | number> = {}
367
+ for (const f of addFields.value) {
368
+ if (f.default !== undefined) cfg[f.key] = f.default as string | number
369
+ }
370
+ addCfg.value = cfg
371
+ addTest.value = null
372
+ }, { immediate: true })
373
+
374
+ async function doTestConnection(): Promise<void> {
375
+ addTesting.value = true
376
+ addTest.value = null
377
+ try {
378
+ addTest.value = await daq.testDriver(addDriver.value, addCfg.value)
379
+ }
380
+ catch (err) {
381
+ addTest.value = { ok: false, message: apiErrorMessage(err) }
382
+ }
383
+ finally {
384
+ addTesting.value = false
385
+ }
386
+ }
387
+
388
+ async function doAddNode(): Promise<void> {
389
+ addSaving.value = true
390
+ addError.value = ''
391
+ try {
392
+ const transform = addTransform.kind === 'linear'
393
+ ? { kind: 'linear' as const, scale: Number(addTransform.scale), offset: Number(addTransform.offset) }
394
+ : undefined
395
+ if (addScenario.value === 'mock') {
396
+ await daq.createFromTemplate(`daq-${addTemplate.value}`, {
397
+ name: addName.value ? { name: addName.value }.name : undefined,
398
+ transform,
399
+ })
400
+ }
401
+ else {
402
+ // 校验必填
403
+ for (const f of addFields.value) {
404
+ if (f.required && !addCfg.value[f.key] && addCfg.value[f.key] !== 0) {
405
+ throw new Error(tt('daq.kyuadl0125', { p0: f.label }))
406
+ }
407
+ }
408
+ const tpl = daq.templates.find(t => t.key === addTemplate.value)
409
+ await daq.createFromTemplate(`daq-${addTemplate.value}`, {
410
+ name: addName.value || undefined,
411
+ driver: addDriver.value as never,
412
+ driverConfig: { ...addCfg.value },
413
+ transform,
414
+ intervalMs: addInterval.value,
415
+ unit: tpl?.unit,
416
+ min: tpl?.min,
417
+ max: tpl?.max,
418
+ decimals: tpl?.decimals,
419
+ } as never)
420
+ }
421
+ addOpen.value = false
422
+ addName.value = ''
423
+ addTest.value = null
424
+ }
425
+ catch (err) {
426
+ addError.value = apiErrorMessage(err)
427
+ }
428
+ finally {
429
+ addSaving.value = false
430
+ }
431
+ }
432
+
433
+ /** 模板通道语义(server 目录为唯一事实源;模板已删除 → 显示 templateRef 原文降级) */
434
+ function daqTemplateRefCh(templateRef: string): string {
435
+ const tpl = daq.templates.find(t => t.key === daqKeyFromRef(templateRef))
436
+ return tpl ? `${catalogTplName(tt, tpl)} · ${tpl.ch}` : templateRef || '-'
437
+ }
438
+
439
+ // ---------- 自定义信号模板管理(server 权威 CRUD;内置只读可复制) ----------
440
+ const tplOpen = ref(false)
441
+ const tplEditing = ref<string | null>(null)
442
+ const tplSaving = ref(false)
443
+ const tplError = ref('')
444
+ const confirmingDel = ref('')
445
+ const tplForm = reactive({
446
+ name: '', ch: '', code: '', unit: '',
447
+ min: 0, max: 100, base: 50, amp: 2,
448
+ decimals: 2, icon: 'thermo' as DaqTemplateIcon,
449
+ })
450
+ const ICON_LABEL: Record<DaqTemplateIcon, string> = {
451
+ thermo: tt('daq.k422b8079'), pressure: tt('daq.k3x6ff080'), tension: tt('daq.k3z9xc081'), encoder: tt('daq.k3subk4082'), camera: tt('daq.k47atw083'), gateway: tt('daq.kz7yhbj084'),
452
+ }
453
+
454
+ const customTpls = computed<DaqTemplateDef[]>(() => daq.templates.filter(t => !t.builtin))
455
+ const builtinTpls = computed<DaqTemplateDef[]>(() => daq.templates.filter(t => t.builtin))
456
+
457
+ function fillTplForm(t: DaqTemplateDef, asCopy = false): void {
458
+ tplForm.name = asCopy ? tt('daq.k2hbo3c126', { p0: catalogTplName(tt, t) }) : catalogTplName(tt, t)
459
+ tplForm.ch = t.ch
460
+ tplForm.code = t.code
461
+ tplForm.unit = t.unit
462
+ tplForm.min = t.min
463
+ tplForm.max = t.max
464
+ tplForm.base = t.base
465
+ tplForm.amp = t.amp
466
+ tplForm.decimals = t.decimals
467
+ tplForm.icon = t.icon
468
+ }
469
+
470
+ function resetTplForm(): void {
471
+ tplEditing.value = null
472
+ tplError.value = ''
473
+ confirmingDel.value = ''
474
+ fillTplForm({ key: '', name: '', code: '', ch: '', unit: '', base: 50, amp: 2, min: 0, max: 100, decimals: 2, icon: 'thermo' })
475
+ }
476
+
477
+ function editTpl(t: DaqTemplateDef): void {
478
+ tplEditing.value = t.key
479
+ tplError.value = ''
480
+ fillTplForm(t)
481
+ }
482
+
483
+ function copyTpl(t: DaqTemplateDef): void {
484
+ tplEditing.value = null
485
+ tplError.value = ''
486
+ fillTplForm(t, true)
487
+ }
488
+
489
+ async function saveTpl(): Promise<void> {
490
+ tplSaving.value = true
491
+ tplError.value = ''
492
+ try {
493
+ const num = (v: number | string): number | undefined => {
494
+ const n = Number(v)
495
+ return v === '' || v == null || !Number.isFinite(n) ? undefined : n
496
+ }
497
+ const input = {
498
+ name: tplForm.name.trim(),
499
+ ch: tplForm.ch.trim() || undefined,
500
+ code: tplForm.code.trim() || undefined,
501
+ unit: tplForm.unit.trim(),
502
+ min: num(tplForm.min)!,
503
+ max: num(tplForm.max)!,
504
+ base: num(tplForm.base),
505
+ amp: num(tplForm.amp),
506
+ decimals: num(tplForm.decimals),
507
+ icon: tplForm.icon,
508
+ }
509
+ if (tplEditing.value) await daq.updateTemplate(tplEditing.value, input)
510
+ else await daq.createTemplate(input)
511
+ resetTplForm()
512
+ }
513
+ catch (err) {
514
+ tplError.value = apiErrorMessage(err)
515
+ }
516
+ finally {
517
+ tplSaving.value = false
518
+ }
519
+ }
520
+
521
+ /** 两段式删除确认(避免误删;空目录风格与节点表一致) */
522
+ async function askDelTpl(t: DaqTemplateDef): Promise<void> {
523
+ if (confirmingDel.value !== t.key) {
524
+ confirmingDel.value = t.key
525
+ return
526
+ }
527
+ try {
528
+ await daq.removeTemplate(t.key)
529
+ }
530
+ catch (err) {
531
+ tplError.value = apiErrorMessage(err)
532
+ }
533
+ finally {
534
+ confirmingDel.value = ''
535
+ }
536
+ }
537
+
538
+ const reconnecting = ref(false)
539
+ async function doReconnect(): Promise<void> {
540
+ reconnecting.value = true
541
+ try {
542
+ await daq.reconnectInfra()
543
+ }
544
+ catch { /* 横幅仍在,30s 后台也会自动重试 */ }
545
+ finally {
546
+ reconnecting.value = false
547
+ }
548
+ }
549
+ </script>
550
+
551
+ <template>
552
+ <div class="page">
553
+ <div class="aw-page-head">
554
+ <div>
555
+ <p class="aw-kicker">
556
+ AGENTWORKSHOP / DAQ CONSOLE
557
+ </p>
558
+ <h1>{{ $t('daq.k1emg364011') }}</h1>
559
+ <p class="sub">
560
+ {{ $t('daq.k1wmg2bz012') }}
561
+ </p>
562
+ </div>
563
+ <div class="badges mono">
564
+ <span
565
+ class="badge"
566
+ :title="$t('daq.k1tsdbtip133')"
567
+ >TSDB · {{ daq.meta.tsdb }}</span>
568
+ <span
569
+ class="badge"
570
+ :title="$t('daq.k1queuet134')"
571
+ >QUEUE · {{ daq.meta.queue }}</span>
572
+ </div>
573
+ </div>
574
+
575
+ <!-- 产线门控横幅:筛选到具体产线 → 展示该产线运行态/产品/Recipe(待机可直达产线管理);
576
+ 未筛选具体产线 → 全局门控提示 -->
577
+ <div
578
+ v-if="daq.loaded && filteredLine && filteredLineState"
579
+ class="infra-banner"
580
+ :class="{ good: filteredLineState.active }"
581
+ >
582
+ <span :class="filteredLineState.active ? 'i-tabler-circle-check' : 'i-tabler-info-circle'" />
583
+ <span class="txt">
584
+ <template v-if="filteredLineState.active">
585
+ {{ $t('daq.k3ktxbr085') }}{{ filteredLine.name }}」{{ $t('daq.k15lvw0o105') }}<b>{{ filteredLineState.productName }}</b> · Recipe:<b>{{ filteredLineState.recipeName }}</b>
586
+ <small class="mono">{{ $t('daq.k400lb086') }} {{ filteredLineState.runId?.slice(0, 8) }} · {{ $t('daq.k3zkt2106') }} {{ filteredLineState.startedAt?.slice(11, 19) }} {{ $t('daq.k69vag8108') }} {{ filteredLineState.taggedSamples }} {{ $t('daq.k4118o092') }}</small>
587
+ </template>
588
+ <template v-else>
589
+ {{ $t('daq.k3ktxbr085') }}{{ filteredLine.name }}」{{ $t('daq.k12uge2o107') }}</template>
590
+ </span>
591
+ <NuxtLink
592
+ class="pill-btn"
593
+ :to="`/dcw/${filteredLine.id}`"
594
+ >
595
+ {{ filteredLineState.active ? $t('daq.k1ukoy0v093') : $t('daq.k1cg78i8109') }}
596
+ </NuxtLink>
597
+ </div>
598
+ <div
599
+ v-else-if="daq.loaded && !anyLineActive"
600
+ class="infra-banner"
601
+ >
602
+ <span class="i-tabler-info-circle" />
603
+ <span class="txt">{{ $t('daq.k19onw5w013') }}<NuxtLink to="/dcw">{{ $t('daq.k1b2tk5c014') }}</NuxtLink>{{ $t('daq.k1u9wnlg015') }}</span>
604
+ </div>
605
+
606
+ <!-- 基础设施降级横幅(MQTT/Timescale 不可达:在线采集停用 + 一键重连) -->
607
+ <div
608
+ v-if="daq.meta.infra?.degraded"
609
+ class="infra-banner"
610
+ >
611
+ <span class="i-tabler-alert-triangle" />
612
+ <span class="txt">{{ daq.meta.infra.warning }}</span>
613
+ <button
614
+ class="pill-btn"
615
+ :disabled="reconnecting"
616
+ @click="doReconnect"
617
+ >
618
+ {{ reconnecting ? $t('daq.k1ld43ur094') : $t('daq.kzg9805110') }}
619
+ </button>
620
+ </div>
621
+
622
+ <!-- 未确认报警条(S5):边缘触发产生 / 轮询收敛,确认后立即消失 -->
623
+ <div
624
+ v-if="daq.alarms.length"
625
+ class="alarm-bar"
626
+ >
627
+ <span class="bar-label">
628
+ <span class="i-tabler-bell-ringing bell" />
629
+ {{ $t('daq.k1alarm141') }}
630
+ <b class="mono">{{ daq.alarms.length }}</b>
631
+ </span>
632
+ <div class="alarm-scroll">
633
+ <span
634
+ v-for="a in daq.alarms"
635
+ :key="a.id"
636
+ class="alarm-item"
637
+ :title="$t('daq.k1esc143', { p0: a.escalation })"
638
+ >
639
+ <b>{{ a.nodeName }}</b>
640
+ <span class="mono dim">{{ a.metric }}</span>
641
+ <span class="mono">{{ a.value }} {{ a.rule === 'lt-min' ? '<' : '>' }} {{ a.threshold }}</span>
642
+ <small
643
+ v-if="a.escalation > 0"
644
+ class="esc mono"
645
+ >+{{ a.escalation }}</small>
646
+ <button
647
+ class="mini-btn ack"
648
+ @click="ackOne(a.id)"
649
+ >
650
+ {{ $t('daq.k1ack142') }}
651
+ </button>
652
+ </span>
653
+ </div>
654
+ </div>
655
+
656
+ <!-- 控制器总控条:第一行 控制/指标/操作,第二行 产线状态带 -->
657
+ <section class="aw-tile ctrl-card">
658
+ <div class="ctrl-row">
659
+ <div class="ctrl-left">
660
+ <button
661
+ class="aw-pill"
662
+ :class="{ running: daq.controller.running }"
663
+ :title="$t('daq.k1snap145')"
664
+ @click="daq.controllerAction(daq.controller.running ? 'pause' : 'resume')"
665
+ >
666
+ <span :class="daq.controller.running ? 'i-tabler-player-pause' : 'i-tabler-player-play'" />
667
+ {{ daq.controller.running ? $t('daq.kpui00095') : $t('daq.knxpdpt111') }}
668
+ </button>
669
+ <label class="cycle mono">
670
+ {{ $t('daq.k1ifahxj016') }}
671
+ <input
672
+ v-model.number="daq.controller.defaultIntervalMs"
673
+ type="number"
674
+ min="200"
675
+ max="60000"
676
+ step="100"
677
+ @change="daq.controllerAction('config', daq.controller.defaultIntervalMs)"
678
+ >ms
679
+ </label>
680
+ <label
681
+ class="cycle mono"
682
+ :title="$t('daq.k1e8lcoo001')"
683
+ >
684
+ {{ $t('daq.k1if98n0017') }}
685
+ <input
686
+ v-model.number="daq.controller.defaultPublishIntervalMs"
687
+ type="number"
688
+ min="0"
689
+ max="60000"
690
+ step="100"
691
+ @change="daq.controllerAction('config', daq.controller.defaultIntervalMs, daq.controller.defaultPublishIntervalMs)"
692
+ >ms
693
+ </label>
694
+ </div>
695
+ <div class="ctrl-right">
696
+ <span class="ctrl-metrics mono">
697
+ <span>{{ $t('daq.k45uio067') }} {{ daq.controller.nodesOnline }}/{{ daq.controller.nodesTotal }}</span>
698
+ <span class="sep">·</span>
699
+ <span :title="$t('daq.k1yztsu4002')">{{ $t('daq.k3xagp087') }} {{ daq.meta.produced }}</span>
700
+ <span class="sep">·</span>
701
+ <span :title="$t('daq.ko09iha003')">{{ $t('daq.k427eu088') }} {{ daq.meta.consumed }}</span>
702
+ <span class="sep">·</span>
703
+ <span
704
+ :class="{ warn: (daq.meta.dropped ?? 0) > 0 }"
705
+ :title="$t('daq.k1droptip135')"
706
+ >{{ $t('daq.k3w8go089') }} {{ daq.meta.dropped }}</span>
707
+ <span class="sep">·</span>
708
+ <span :title="$t('daq.kj1jbmw004')">{{ $t('daq.k3wusd090') }} {{ daq.meta.samplesStored }}</span>
709
+ </span>
710
+ <button
711
+ class="aw-pill outline add-btn"
712
+ @click="tplOpen = true; resetTplForm()"
713
+ >
714
+ <span class="i-tabler-adjustments-horizontal" />
715
+ {{ $t('daq.k1f5cv0s018') }}
716
+ </button>
717
+ <button
718
+ class="aw-pill add-btn"
719
+ @click="addOpen = true"
720
+ >
721
+ <span class="i-tabler-plus" />
722
+ {{ $t('daq.k1fn0ukb019') }}
723
+ </button>
724
+ </div>
725
+ </div>
726
+ <!-- 产线状态带:单行横向滚动,每产线一枚 pill(空心点=待机 / 实心呼吸点=运行中) -->
727
+ <div class="line-strip">
728
+ <span class="strip-label">
729
+ <span class="i-tabler-route" />
730
+ {{ $t('daq.k1b2o3b6020') }}<small class="mono">{{ dcw.lines.length }}</small>
731
+ </span>
732
+ <div class="strip-scroll">
733
+ <NuxtLink
734
+ v-for="l in dcw.lines"
735
+ :key="l.id"
736
+ class="line-pill"
737
+ :class="{ on: dcw.lineStateOf(l.id).active }"
738
+ :style="{ '--lc': l.color }"
739
+ :to="`/dcw/${l.id}`"
740
+ :title="dcw.lineStateOf(l.id).active
741
+ ? $t('daq.k3v957u120', { p0: l.name, p1: dcw.lineStateOf(l.id).productName, p2: dcw.lineStateOf(l.id).recipeName })
742
+ : $t('daq.k1x7jnru121', { p0: l.name })"
743
+ >
744
+ <span class="lp-dot" />
745
+ <b>{{ l.name }}</b>
746
+ <small
747
+ v-if="dcw.lineStateOf(l.id).active"
748
+ class="lp-run"
749
+ >{{ dcw.lineStateOf(l.id).recipeName ?? $t('daq.k3vp67i096') }}</small>
750
+ <small
751
+ v-else
752
+ class="lp-idle"
753
+ >{{ $t('daq.k3zgkk021') }}</small>
754
+ </NuxtLink>
755
+ <NuxtLink
756
+ v-if="dcw.lines.length === 0"
757
+ class="line-pill"
758
+ to="/dcw"
759
+ >
760
+ <span class="lp-dot" />
761
+ <b>{{ $t('daq.k1el12b1022') }}</b>
762
+ <small class="lp-idle">{{ $t('daq.k8jxxe8023') }}</small>
763
+ </NuxtLink>
764
+ </div>
765
+ </div>
766
+ </section>
767
+
768
+ <!-- 添加节点向导:mock 模拟 / 真实设备(协议参数 + 测试连接) -->
769
+ <div
770
+ v-if="addOpen"
771
+ class="modal-mask"
772
+ @click.self="addOpen = false"
773
+ >
774
+ <div class="modal">
775
+ <h3 class="m-title">
776
+ {{ $t('daq.k19rioqa024') }}
777
+ </h3>
778
+
779
+ <div class="seg-row">
780
+ <button
781
+ class="seg"
782
+ :class="{ on: addScenario === 'mock' }"
783
+ @click="addScenario = 'mock'"
784
+ >
785
+ {{ $t('daq.k1p5c1un025') }}
786
+ </button>
787
+ <button
788
+ class="seg"
789
+ :class="{ on: addScenario === 'real' }"
790
+ @click="addScenario = 'real'"
791
+ >
792
+ {{ $t('daq.k1cbcp3o026') }}
793
+ </button>
794
+ </div>
795
+
796
+ <div class="f-grid">
797
+ <label class="f">
798
+ <span>{{ $t('daq.k1fsgerc027') }}</span>
799
+ <select
800
+ v-model="addTemplate"
801
+ class="inp"
802
+ >
803
+ <option
804
+ v-for="t in daq.templates"
805
+ :key="t.key"
806
+ :value="t.key"
807
+ >
808
+ {{ t.name }} · {{ t.ch }}({{ t.min }}~{{ t.max }} {{ t.unit }}){{ t.builtin ? '' : $t('daq.kr45rk9097') }}
809
+ </option>
810
+ </select>
811
+ </label>
812
+ <label class="f">
813
+ <span>{{ $t('daq.k1ce2k1y028') }}</span>
814
+ <input
815
+ v-model="addName"
816
+ class="inp"
817
+ :placeholder="$t('daq.kgpxzy3005')"
818
+ >
819
+ </label>
820
+ <label
821
+ v-if="addScenario === 'real'"
822
+ class="f"
823
+ >
824
+ <span>{{ $t('daq.k13mfsfc029') }}</span>
825
+ <input
826
+ v-model.number="addInterval"
827
+ type="number"
828
+ min="1000"
829
+ max="60000"
830
+ step="500"
831
+ class="inp"
832
+ :placeholder="$t('daq.k1eg1000a136')"
833
+ >
834
+ </label>
835
+ </div>
836
+
837
+ <!-- 真实场景:协议选择 + 动态参数表单 + 测试连接 -->
838
+ <template v-if="addScenario === 'real'">
839
+ <div class="f-grid">
840
+ <label class="f">
841
+ <span>{{ $t('daq.k1kt87rx030') }}</span>
842
+ <select
843
+ v-model="addDriver"
844
+ class="inp"
845
+ >
846
+ <option
847
+ v-for="d in driverCatalog.filter(x => x.status !== 'planned')"
848
+ :key="d.kind"
849
+ :value="d.kind"
850
+ >
851
+ {{ d.label }}{{ d.plugin ? ' ⌁' : '' }}{{ driverReady(d.kind) ? '' : $t('daq.kjbqphp098') }}
852
+ </option>
853
+ </select>
854
+ </label>
855
+ </div>
856
+
857
+ <div
858
+ v-if="addFields.length"
859
+ class="f-grid driver-form"
860
+ >
861
+ <label
862
+ v-for="f in addFields"
863
+ :key="f.key"
864
+ class="f"
865
+ >
866
+ <span>{{ f.label }}<em v-if="f.required">*</em></span>
867
+ <select
868
+ v-if="f.type === 'select'"
869
+ v-model="addCfg[f.key]"
870
+ class="inp"
871
+ >
872
+ <option
873
+ v-for="o in f.options"
874
+ :key="o.value"
875
+ :value="o.value"
876
+ >
877
+ {{ o.label }}
878
+ </option>
879
+ </select>
880
+ <input
881
+ v-else
882
+ v-model="addCfg[f.key]"
883
+ :type="f.type === 'number' ? 'number' : 'text'"
884
+ :placeholder="f.placeholder"
885
+ class="inp"
886
+ >
887
+ <small
888
+ v-if="f.hint"
889
+ class="hint"
890
+ >{{ f.hint }}</small>
891
+ </label>
892
+ </div>
893
+
894
+ <div class="test-row">
895
+ <button
896
+ class="pill-btn"
897
+ :disabled="addTesting"
898
+ @click="doTestConnection"
899
+ >
900
+ {{ addTesting ? $t('daq.k1fsh720099') : $t('daq.k1fstglk112') }}
901
+ </button>
902
+ <span
903
+ v-if="addTest"
904
+ class="test-result"
905
+ :class="addTest.ok ? 'ok' : 'bad'"
906
+ >{{ addTest.ok ? '✓' : '✗' }} {{ addTest.message }}<template v-if="addTest.latencyMs != null">({{ addTest.latencyMs }}ms)</template></span>
907
+ </div>
908
+ </template>
909
+
910
+ <p
911
+ v-if="addError"
912
+ class="m-err"
913
+ >
914
+ {{ addError }}
915
+ </p>
916
+
917
+ <div class="m-actions">
918
+ <button
919
+ class="aw-pill outline"
920
+ @click="addOpen = false"
921
+ >
922
+ {{ $t('daq.k3xdnn031') }}
923
+ </button>
924
+ <button
925
+ class="aw-pill"
926
+ :disabled="addSaving || (addScenario === 'real' && addTest != null && !addTest.ok)"
927
+ :title="addScenario === 'real' && !(addTest && addTest.ok) ? $t('daq.k1needtst137') : ''"
928
+ @click="doAddNode"
929
+ >
930
+ {{ addSaving ? $t('daq.k1bg4759100') : (addScenario === 'real' ? $t('daq.k7pxjxo113') : $t('daq.k1bge46t118')) }}
931
+ </button>
932
+ </div>
933
+ </div>
934
+ </div>
935
+
936
+ <!-- 模板管理:自定义模板增删改 + 内置模板复制 -->
937
+ <div
938
+ v-if="tplOpen"
939
+ class="modal-mask"
940
+ @click.self="tplOpen = false"
941
+ >
942
+ <div class="modal">
943
+ <h3 class="m-title">
944
+ {{ $t('daq.k11mv96s032') }}
945
+ </h3>
946
+
947
+ <p class="sec-label">
948
+ {{ $t('daq.ktji4ky033') }}
949
+ </p>
950
+ <table class="tpl-table">
951
+ <tbody>
952
+ <tr
953
+ v-for="t in customTpls"
954
+ :key="t.key"
955
+ >
956
+ <td>
957
+ <b>{{ t.name }}</b>
958
+ <small class="mono dim">{{ t.code }}</small>
959
+ </td>
960
+ <td class="mono range">
961
+ {{ t.min }}~{{ t.max }} {{ t.unit }} · {{ t.decimals }} {{ $t('daq.k48oi091') }}
962
+ </td>
963
+ <td class="right actions">
964
+ <button
965
+ class="mini-btn"
966
+ @click="editTpl(t)"
967
+ >
968
+ {{ $t('daq.k45eb0034') }}
969
+ </button>
970
+ <button
971
+ class="mini-btn danger"
972
+ @click="askDelTpl(t)"
973
+ >
974
+ {{ confirmingDel === t.key ? $t('daq.k1hhheu3101') : $t('daq.k3xakp114') }}
975
+ </button>
976
+ </td>
977
+ </tr>
978
+ <tr v-if="!customTpls.length">
979
+ <td
980
+ colspan="3"
981
+ class="empty"
982
+ >
983
+ {{ $t('daq.kztvzbo035') }}
984
+ </td>
985
+ </tr>
986
+ </tbody>
987
+ </table>
988
+
989
+ <p class="sec-label">
990
+ {{ tplEditing ? $t('daq.k1iiph0s102') : $t('daq.k1efixrj115') }}
991
+ </p>
992
+ <div class="f-grid">
993
+ <label class="f">
994
+ <span>{{ $t('daq.k3xhia036') }}<em>*</em></span>
995
+ <input
996
+ v-model="tplForm.name"
997
+ class="inp"
998
+ :placeholder="$t('daq.k5opd2t006')"
999
+ >
1000
+ </label>
1001
+ <label class="f">
1002
+ <span>{{ $t('daq.k3x4ef037') }}<em>*</em></span>
1003
+ <input
1004
+ v-model="tplForm.unit"
1005
+ class="inp"
1006
+ :placeholder="$t('daq.unitPh')"
1007
+ >
1008
+ </label>
1009
+ <label class="f">
1010
+ <span>{{ $t('daq.k1l477m0038') }}</span>
1011
+ <input
1012
+ v-model="tplForm.ch"
1013
+ class="inp"
1014
+ :placeholder="$t('daq.kk8o4tl007')"
1015
+ >
1016
+ </label>
1017
+ <label class="f">
1018
+ <span>{{ $t('daq.k1ayxrqb039') }}</span>
1019
+ <input
1020
+ v-model="tplForm.code"
1021
+ class="inp"
1022
+ :placeholder="$t('daq.kzn3l7l008')"
1023
+ >
1024
+ </label>
1025
+ <label class="f">
1026
+ <span>{{ $t('daq.k1l9jv5m040') }}<em>*</em></span>
1027
+ <input
1028
+ v-model.number="tplForm.min"
1029
+ type="number"
1030
+ class="inp"
1031
+ >
1032
+ </label>
1033
+ <label class="f">
1034
+ <span>{{ $t('daq.k1l9jv4p041') }}<em>*</em></span>
1035
+ <input
1036
+ v-model.number="tplForm.max"
1037
+ type="number"
1038
+ class="inp"
1039
+ >
1040
+ </label>
1041
+ <label class="f">
1042
+ <span>{{ $t('daq.k3mxmcx042') }}</span>
1043
+ <input
1044
+ v-model.number="tplForm.decimals"
1045
+ type="number"
1046
+ min="0"
1047
+ max="4"
1048
+ class="inp"
1049
+ >
1050
+ </label>
1051
+ <label class="f">
1052
+ <span>{{ $t('daq.k3xx56043') }}</span>
1053
+ <select
1054
+ v-model="tplForm.icon"
1055
+ class="inp"
1056
+ >
1057
+ <option
1058
+ v-for="ic in DAQ_TEMPLATE_ICONS"
1059
+ :key="ic"
1060
+ :value="ic"
1061
+ >
1062
+ {{ ICON_LABEL[ic] }}
1063
+ </option>
1064
+ </select>
1065
+ </label>
1066
+ <label class="f">
1067
+ <span>{{ $t('daq.k1f4eknv044') }}</span>
1068
+ <input
1069
+ v-model.number="tplForm.base"
1070
+ type="number"
1071
+ class="inp"
1072
+ >
1073
+ <small class="hint">{{ $t('daq.kytd15s045') }}</small>
1074
+ </label>
1075
+ <label class="f">
1076
+ <span>{{ $t('daq.k1f4ifpo046') }}</span>
1077
+ <input
1078
+ v-model.number="tplForm.amp"
1079
+ type="number"
1080
+ class="inp"
1081
+ >
1082
+ <small class="hint">{{ $t('daq.ksp2aji047') }}</small>
1083
+ </label>
1084
+ </div>
1085
+ <p
1086
+ v-if="tplError"
1087
+ class="m-err"
1088
+ >
1089
+ {{ tplError }}
1090
+ </p>
1091
+ <div class="m-actions">
1092
+ <button
1093
+ class="aw-pill outline"
1094
+ @click="resetTplForm"
1095
+ >
1096
+ {{ $t('daq.k48p40048') }}
1097
+ </button>
1098
+ <button
1099
+ class="pill-btn"
1100
+ :disabled="tplSaving"
1101
+ @click="saveTpl"
1102
+ >
1103
+ {{ tplSaving ? $t('daq.k1b38d59103') : (tplEditing ? $t('daq.k1b39281116') : $t('daq.k1b3dtga119')) }}
1104
+ </button>
1105
+ </div>
1106
+
1107
+ <p class="sec-label">
1108
+ {{ $t('daq.k1w8s84s049') }}
1109
+ </p>
1110
+ <table class="tpl-table">
1111
+ <tbody>
1112
+ <tr
1113
+ v-for="t in builtinTpls"
1114
+ :key="t.key"
1115
+ >
1116
+ <td>
1117
+ <b>{{ t.name }}</b>
1118
+ <small class="mono dim">{{ t.code }}</small>
1119
+ </td>
1120
+ <td class="mono range">
1121
+ {{ t.min }}~{{ t.max }} {{ t.unit }} · {{ t.decimals }} {{ $t('daq.k48oi091') }}
1122
+ </td>
1123
+ <td class="right actions">
1124
+ <button
1125
+ class="mini-btn"
1126
+ @click="copyTpl(t)"
1127
+ >
1128
+ {{ $t('daq.k3y694050') }}
1129
+ </button>
1130
+ </td>
1131
+ </tr>
1132
+ </tbody>
1133
+ </table>
1134
+ </div>
1135
+ </div>
1136
+
1137
+ <!-- Agent 优化记录(调控闭环:设定历史 / 判定 / 窗口数采;序列为客户端渲染) -->
1138
+ <section class="aw-tile opt-card">
1139
+ <button
1140
+ class="opt-head"
1141
+ @click="toggleOptPanel"
1142
+ >
1143
+ <span class="i-tabler-flask" />
1144
+ <b>{{ $t('daq.k1optt001') }}</b>
1145
+ <span class="opt-count mono">{{ dcw.optimizations.length }}</span>
1146
+ <span
1147
+ class="opt-chevron"
1148
+ :class="{ open: optOpen }"
1149
+ />
1150
+ </button>
1151
+ <div
1152
+ class="opt-body"
1153
+ :class="{ open: optOpen }"
1154
+ >
1155
+ <template v-if="optMounted">
1156
+ <div class="opt-filters">
1157
+ <select
1158
+ v-model="optFilterLine"
1159
+ class="inp-sel"
1160
+ @change="onOptLineChange"
1161
+ >
1162
+ <option value="">
1163
+ {{ $t('daq.k1optf001') }}
1164
+ </option>
1165
+ <option
1166
+ v-for="l in dcw.lines"
1167
+ :key="l.id"
1168
+ :value="l.id"
1169
+ >
1170
+ {{ l.name }}
1171
+ </option>
1172
+ </select>
1173
+ <select
1174
+ v-model="optFilterRecipe"
1175
+ class="inp-sel"
1176
+ @change="filterOpts"
1177
+ >
1178
+ <option value="">
1179
+ {{ $t('daq.k1optf004') }}
1180
+ </option>
1181
+ <option
1182
+ v-for="r in optRecipeOptions"
1183
+ :key="r.id"
1184
+ :value="r.id"
1185
+ >
1186
+ {{ r.name }}
1187
+ </option>
1188
+ </select>
1189
+ <button
1190
+ class="mini-act"
1191
+ @click="filterOpts"
1192
+ >
1193
+ {{ $t('daq.k1optf002') }}
1194
+ </button>
1195
+ </div>
1196
+ <p
1197
+ v-if="dcw.optimizations.length === 0"
1198
+ class="opt-empty"
1199
+ >
1200
+ {{ $t('daq.k1opte001') }}
1201
+ </p>
1202
+ <ul class="opt-list">
1203
+ <li
1204
+ v-for="r in dcw.optimizations.slice(0, 30)"
1205
+ :key="r.id"
1206
+ class="opt-row"
1207
+ :class="r.status"
1208
+ >
1209
+ <div class="opt-line1">
1210
+ <span
1211
+ class="opt-status"
1212
+ :class="r.status"
1213
+ >{{ $t(optStatusKey(r.status)) }}</span>
1214
+ <b class="opt-node">{{ r.nodeName }}</b>
1215
+ <span class="mono opt-params">{{ r.params[0]?.from ?? '?' }} → {{ r.params[0]?.to }}</span>
1216
+ <span class="mono opt-time">{{ r.setAt.slice(5, 16).replace('T', ' ') }}</span>
1217
+ <button
1218
+ v-if="!optSeriesOf[r.id]"
1219
+ class="mini-act"
1220
+ :disabled="optSeriesLoading === r.id"
1221
+ @click="showOptSeries(r.id)"
1222
+ >
1223
+ {{ $t('daq.k1opta001') }}
1224
+ </button>
1225
+ <button
1226
+ v-if="r.status === 'open'"
1227
+ class="mini-act danger"
1228
+ @click="rollbackRecord(r.id)"
1229
+ >
1230
+ {{ $t('daq.k1opta002') }}
1231
+ </button>
1232
+ </div>
1233
+ <div class="opt-line2 mono">
1234
+ <span
1235
+ v-if="r.judge"
1236
+ class="opt-judge"
1237
+ :class="r.judge.verdict"
1238
+ >{{ r.judge.verdict }}({{ r.judge.by }}): {{ r.judge.reason.slice(0, 70) }}</span>
1239
+ <span
1240
+ v-else-if="r.status === 'open'"
1241
+ class="opt-open-hint"
1242
+ >{{ $t('daq.k1opto001') }}</span>
1243
+ <span
1244
+ v-if="r.aggPending"
1245
+ class="opt-agg"
1246
+ >{{ $t('daq.k1optw001') }}</span>
1247
+ <span
1248
+ v-else-if="r.windowAgg && r.windowAgg.channels.length"
1249
+ class="opt-agg"
1250
+ >
1251
+ <template
1252
+ v-for="c in r.windowAgg.channels"
1253
+ :key="c.daqNodeId"
1254
+ >{{ c.ch }} {{ fmtPoint(c.min) }}~{{ fmtPoint(c.max) }}({{ $t('daq.k1optw002') }}{{ c.breaches < 0 ? '-' : c.breaches }}) </template>
1255
+ </span>
1256
+ </div>
1257
+ <div
1258
+ v-if="optSeriesOf[r.id]"
1259
+ class="opt-series"
1260
+ >
1261
+ <div
1262
+ v-for="ch in (optSeriesOf[r.id]?.channels ?? [])"
1263
+ :key="ch.nodeId"
1264
+ class="opt-ch"
1265
+ >
1266
+ <svg
1267
+ class="opt-spark"
1268
+ viewBox="0 0 240 36"
1269
+ preserveAspectRatio="none"
1270
+ >
1271
+ <polyline :points="seriesPath(ch.points)" />
1272
+ </svg>
1273
+ <span class="mono">{{ ch.ch }} {{ fmtPoint(ch.points.at(-1)?.value ?? ch.points.at(-1)?.avg) }}{{ ch.unit }}</span>
1274
+ </div>
1275
+ <p
1276
+ v-if="(optSeriesOf[r.id]?.channels.length ?? 0) === 0"
1277
+ class="opt-empty"
1278
+ >
1279
+ {{ $t('daq.k1opte002') }}
1280
+ </p>
1281
+ </div>
1282
+ </li>
1283
+ </ul>
1284
+ </template>
1285
+ </div>
1286
+ </section>
1287
+
1288
+ <!-- 节点清单:筛选工具条(产线/设备/产线运行/状态) + 节点表 -->
1289
+ <a-spin :spinning="!daq.loaded && !daq.error">
1290
+ <section class="aw-tile table-card">
1291
+ <div class="tbl-toolbar">
1292
+ <div class="filters">
1293
+ <label class="flt">
1294
+ <span>{{ $t('daq.k3wj9n051') }}</span>
1295
+ <select
1296
+ v-model="filters.lineId"
1297
+ class="inp-sel"
1298
+ >
1299
+ <option value="">
1300
+ {{ $t('daq.k1bkl9uj052') }}
1301
+ </option>
1302
+ <option
1303
+ v-for="l in dcw.lines"
1304
+ :key="l.id"
1305
+ :value="l.id"
1306
+ >
1307
+ {{ l.name }}
1308
+ </option>
1309
+ <option value="none">
1310
+ {{ $t('daq.k3ootr6053') }}
1311
+ </option>
1312
+ </select>
1313
+ </label>
1314
+ <label class="flt">
1315
+ <span>{{ $t('daq.k1i8rtqt054') }}</span>
1316
+ <select
1317
+ v-model="filters.deviceId"
1318
+ class="inp-sel"
1319
+ >
1320
+ <option value="">
1321
+ {{ $t('daq.k1bkw4m2055') }}
1322
+ </option>
1323
+ <option
1324
+ v-for="d in boundDevices"
1325
+ :key="d.id"
1326
+ :value="d.id"
1327
+ >
1328
+ {{ d.name }}({{ d.count }})
1329
+ </option>
1330
+ <option value="none">
1331
+ {{ $t('daq.k3own4q056') }}
1332
+ </option>
1333
+ </select>
1334
+ </label>
1335
+ <label
1336
+ class="flt"
1337
+ :title="$t('daq.khb8mru009')"
1338
+ >
1339
+ <span>{{ $t('daq.k1b2tkyv057') }}</span>
1340
+ <select
1341
+ v-model="filters.lineRun"
1342
+ class="inp-sel"
1343
+ >
1344
+ <option value="">
1345
+ {{ $t('daq.k3x4t1058') }}
1346
+ </option>
1347
+ <option value="on">
1348
+ {{ $t('daq.k1nxm3p7059') }}
1349
+ </option>
1350
+ <option value="off">
1351
+ {{ $t('daq.k1ulx34e060') }}
1352
+ </option>
1353
+ </select>
1354
+ </label>
1355
+ <label class="flt">
1356
+ <span>{{ $t('daq.k1iwdfef061') }}</span>
1357
+ <select
1358
+ v-model="filters.state"
1359
+ class="inp-sel"
1360
+ >
1361
+ <option value="">
1362
+ {{ $t('daq.k3x4t1058') }}
1363
+ </option>
1364
+ <option value="ok">
1365
+ {{ $t('daq.k41k5c062') }}
1366
+ </option>
1367
+ <option value="warn">
1368
+ {{ $t('daq.k49z8v063') }}
1369
+ </option>
1370
+ <option value="alarm">
1371
+ {{ $t('daq.k3xmid064') }}
1372
+ </option>
1373
+ <option value="offline">
1374
+ {{ $t('daq.k44c2n065') }}
1375
+ </option>
1376
+ </select>
1377
+ </label>
1378
+ <label class="flt">
1379
+ <span>{{ $t('daq.k1flttpl010') }}</span>
1380
+ <select
1381
+ v-model="filters.template"
1382
+ class="inp-sel"
1383
+ >
1384
+ <option value="">
1385
+ {{ $t('daq.k1flttpl011') }}
1386
+ </option>
1387
+ <option
1388
+ v-for="t in templateOptions"
1389
+ :key="t.ref"
1390
+ :value="t.ref"
1391
+ >
1392
+ {{ t.label }}({{ t.count }})
1393
+ </option>
1394
+ </select>
1395
+ </label>
1396
+ <label class="flt">
1397
+ <span>{{ $t('daq.k1fltdrv012') }}</span>
1398
+ <select
1399
+ v-model="filters.driver"
1400
+ class="inp-sel"
1401
+ >
1402
+ <option value="">
1403
+ {{ $t('daq.k1fltdrv013') }}
1404
+ </option>
1405
+ <option
1406
+ v-for="d in driverOptions"
1407
+ :key="d.kind"
1408
+ :value="d.kind"
1409
+ >
1410
+ {{ d.label }}({{ d.count }})
1411
+ </option>
1412
+ </select>
1413
+ </label>
1414
+ <label
1415
+ class="flt"
1416
+ :title="$t('daq.k1flttim014')"
1417
+ >
1418
+ <span>{{ $t('daq.k1flttim015') }}</span>
1419
+ <select
1420
+ v-model="filters.time"
1421
+ class="inp-sel"
1422
+ >
1423
+ <option value="">
1424
+ {{ $t('daq.k1flttim016') }}
1425
+ </option>
1426
+ <option value="live">
1427
+ {{ $t('daq.k1timlv017') }}
1428
+ </option>
1429
+ <option value="hour">
1430
+ {{ $t('daq.k1timhr018') }}
1431
+ </option>
1432
+ <option value="day">
1433
+ {{ $t('daq.k1timdy019') }}
1434
+ </option>
1435
+ <option value="old">
1436
+ {{ $t('daq.k1timld020') }}
1437
+ </option>
1438
+ <option value="never">
1439
+ {{ $t('daq.k1timnv021') }}
1440
+ </option>
1441
+ </select>
1442
+ </label>
1443
+ <label class="flt">
1444
+ <span>{{ $t('daq.k1fltsrc022') }}</span>
1445
+ <input
1446
+ v-model="filters.search"
1447
+ class="inp-sel flt-search"
1448
+ type="search"
1449
+ :placeholder="$t('daq.k1fltsph023')"
1450
+ >
1451
+ </label>
1452
+ <button
1453
+ v-if="hasFilters"
1454
+ class="mini-btn clear-btn"
1455
+ @click="clearFilters"
1456
+ >
1457
+ {{ $t('daq.k1fygck2066') }}
1458
+ </button>
1459
+ </div>
1460
+ <span class="count mono">{{ filteredNodes.length }} / {{ daq.nodes.length }} {{ $t('daq.k45uio067') }}</span>
1461
+ </div>
1462
+ <!-- 实时告警 + 实时事件:告警 = server 权威未确认报警(WS 直推/可确认);事件 = 全操作摘要轨(详情在日志管理) -->
1463
+ <div class="ops-panels">
1464
+ <div
1465
+ class="ops-panel alarm-panel"
1466
+ :class="{ live: daq.alarms.length > 0 }"
1467
+ >
1468
+ <div class="panel-head">
1469
+ <span class="i-tabler-bell-ringing bell" />
1470
+ <b>{{ $t('daq.k1alarm141') }}</b>
1471
+ <span
1472
+ class="mono cnt"
1473
+ :class="{ hot: daq.alarms.length > 0 }"
1474
+ >{{ daq.alarms.length }}</span>
1475
+ </div>
1476
+ <div class="panel-body">
1477
+ <p
1478
+ v-if="daq.alarms.length === 0"
1479
+ class="panel-empty"
1480
+ >
1481
+ {{ $t('daq.k1evt001') }}
1482
+ </p>
1483
+ <div
1484
+ v-for="a in daq.alarms"
1485
+ v-else
1486
+ :key="a.id"
1487
+ class="alarm-row"
1488
+ >
1489
+ <span class="i-tabler-alert-triangle tri" />
1490
+ <b>{{ a.nodeName }}</b>
1491
+ <span class="mono dim">{{ a.metric }}</span>
1492
+ <span class="mono">{{ a.value }} {{ a.rule === 'lt-min' ? '<' : '>' }} {{ a.threshold }}</span>
1493
+ <small
1494
+ v-if="a.escalation > 0"
1495
+ class="esc mono"
1496
+ >+{{ a.escalation }}</small>
1497
+ <button
1498
+ class="mini-btn ack"
1499
+ @click="ackOne(a.id)"
1500
+ >
1501
+ {{ $t('daq.k1ack142') }}
1502
+ </button>
1503
+ </div>
1504
+ </div>
1505
+ </div>
1506
+ <div class="ops-panel event-panel">
1507
+ <div class="panel-head">
1508
+ <span class="i-tabler-activity" />
1509
+ <b>{{ $t('daq.k1evt002') }}</b>
1510
+ <NuxtLink
1511
+ class="panel-link"
1512
+ to="/logs"
1513
+ >
1514
+ {{ $t('daq.k1evt003') }}<span class="i-tabler-arrow-right" />
1515
+ </NuxtLink>
1516
+ </div>
1517
+ <div class="panel-body">
1518
+ <p
1519
+ v-if="opsLog.recent.length === 0"
1520
+ class="panel-empty"
1521
+ >
1522
+ {{ $t('daq.k1evt004') }}
1523
+ </p>
1524
+ <div
1525
+ v-for="(r, i) in opsLog.recent"
1526
+ v-else
1527
+ :key="`${r.at}:${r.actor}:${r.action}:${i}`"
1528
+ class="evt-row"
1529
+ >
1530
+ <span class="mono dim t">{{ opsTime(r.at) }}</span>
1531
+ <span
1532
+ class="src-badge"
1533
+ :class="r.actorKind"
1534
+ >{{ r.actorKind === 'agent' ? 'Agent' : r.actorKind === 'user' ? $t('logs.src.user') : $t('logs.src.system') }}</span>
1535
+ <span
1536
+ class="kind-chip"
1537
+ :class="r.kind"
1538
+ >{{ opsKindLabel(r.kind) }}</span>
1539
+ <span
1540
+ class="txt"
1541
+ :title="r.summary"
1542
+ >{{ r.summary }}</span>
1543
+ </div>
1544
+ </div>
1545
+ </div>
1546
+ </div>
1547
+ <table
1548
+ ref="nodesTableRef"
1549
+ class="nodes-table"
1550
+ >
1551
+ <thead>
1552
+ <tr>
1553
+ <th>{{ $t('daq.k45uio067') }}</th>
1554
+ <th>{{ $t('daq.k42w8s068') }}</th>
1555
+ <th>{{ $t('daq.k3mv305069') }}</th>
1556
+ <th>{{ $t('daq.k1trnd001') }}</th>
1557
+ <th>{{ $t('daq.k1l6g2ga070') }}</th>
1558
+ <th>{{ $t('daq.k3zi0nf071') }}</th>
1559
+ <th>{{ $t('daq.k4a0la072') }}</th>
1560
+ <th>{{ $t('daq.k3wj9n051') }}</th>
1561
+ <th :title="$t('daq.k1runtipc131')">
1562
+ {{ $t('daq.k1b2tkyv057') }}
1563
+ </th>
1564
+ <th :title="$t('daq.k1prdrtip132')">
1565
+ {{ $t('daq.k1prodrc128') }}
1566
+ </th>
1567
+ <th>{{ $t('daq.k1i8rtqt054') }}</th>
1568
+ <th class="right">
1569
+ {{ $t('daq.k40aa6073') }}
1570
+ </th>
1571
+ </tr>
1572
+ </thead>
1573
+ <tbody>
1574
+ <!-- 上占位行:撑起"已滚过"的高度,让滚动条与真实全量列表一致 -->
1575
+ <tr
1576
+ v-if="padTopPx > 0"
1577
+ class="vs-pad"
1578
+ aria-hidden="true"
1579
+ >
1577
1580
  <td
1578
1581
  :colspan="11"
1579
1582
  :style="{ height: padTopPx + 'px' }"
1580
- />
1581
- </tr>
1582
- <WorkshopDaqNodeRow
1583
- v-for="n in visibleNodes"
1584
- :key="n.id"
1585
- :n="n"
1586
- />
1587
- <!-- 下占位行 -->
1588
- <tr
1589
- v-if="padBottomPx > 0"
1590
- class="vs-pad"
1591
- aria-hidden="true"
1592
- >
1583
+ />
1584
+ </tr>
1585
+ <WorkshopDaqNodeRow
1586
+ v-for="n in visibleNodes"
1587
+ :key="n.id"
1588
+ :n="n"
1589
+ />
1590
+ <!-- 下占位行 -->
1591
+ <tr
1592
+ v-if="padBottomPx > 0"
1593
+ class="vs-pad"
1594
+ aria-hidden="true"
1595
+ >
1593
1596
  <td
1594
1597
  :colspan="11"
1595
1598
  :style="{ height: padBottomPx + 'px' }"
1596
- />
1597
- </tr>
1598
- <tr v-if="daq.loaded && daq.nodes.length === 0">
1599
- <td colspan="11">
1600
- <div
1601
- class="pane-empty"
1602
- style="min-height: 120px;"
1603
- >
1604
- <p class="pe-sub">
1605
- {{ $t('daq.kvhjyc3075') }}
1606
- </p>
1607
- </div>
1608
- </td>
1609
- </tr>
1610
- <tr v-else-if="daq.loaded && filteredNodes.length === 0">
1611
- <td colspan="11">
1612
- <div
1613
- class="pane-empty"
1614
- style="min-height: 90px;"
1615
- >
1616
- <p class="pe-sub">
1617
- {{ $t('daq.k1w20qew076') }}
1618
- </p>
1619
- </div>
1620
- </td>
1621
- </tr>
1622
- </tbody>
1623
- </table>
1624
- </section>
1625
- </a-spin>
1626
-
1627
- <p
1628
- v-if="daq.error"
1629
- class="err"
1630
- >
1631
- {{ daq.error }}(<NuxtLink to="/workshop">{{ $t('daq.k1bhhheq077') }}</NuxtLink>)
1632
- </p>
1633
- </div>
1634
- </template>
1635
-
1636
- <style scoped>
1637
- .page { padding: 4px; }
1638
- h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.015em; }
1639
- .sub { margin: 0; font-size: 12.5px; opacity: 0.6; }
1640
- .mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
1641
- .dim { opacity: 0.55; }
1642
-
1643
- .badges { display: flex; gap: 8px; }
1644
- .badge {
1645
- padding: 3px 10px;
1646
- font-size: 11px;
1647
- letter-spacing: 0.05em;
1648
- color: var(--ink-soft);
1649
- border: 1px solid var(--line-strong);
1650
- border-radius: var(--radius-chip);
1651
- }
1652
-
1653
- .ctrl-card {
1654
- display: flex;
1655
- flex-direction: column;
1656
- gap: 11px;
1657
- padding: 12px 18px 11px;
1658
- margin-bottom: 14px;
1659
- }
1660
- .ctrl-row {
1661
- display: flex;
1662
- flex-wrap: wrap;
1663
- gap: 14px;
1664
- align-items: center;
1665
- justify-content: space-between;
1666
- }
1667
- /* 窄屏前先写死「不可压缩」:药丸/周期项被 flex 收缩到 min-content → 中文退化成单列竖排。
1668
- 保持自然宽度,由外层 flex-wrap 决定换行位置。 */
1669
- .ctrl-left { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; min-width: 0; }
1670
- .ctrl-left > *, .ctrl-right > * { flex: 0 0 auto; }
1671
- .aw-pill { flex: 0 0 auto; white-space: nowrap; }
1672
- /* 行内小按钮(确认/筛选/回滚…)同族:窄屏禁止中文折成两行 */
1673
- .mini-btn, .mini-act, .pill-btn, .node-toggle { white-space: nowrap; }
1674
- .aw-pill.running { background: var(--accent); }
1675
- .cycle { display: inline-flex; flex: 0 0 auto; gap: 6px; align-items: center; font-size: 12px; white-space: nowrap; color: var(--ink-faint); }
1676
- .cycle input {
1677
- width: 84px;
1678
- padding: 4px 8px;
1679
- color: var(--ink);
1680
- background: var(--paper-deep);
1681
- border: 1px solid var(--line-strong);
1682
- border-radius: var(--radius-chip);
1683
- }
1684
- .ctrl-metrics { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; min-width: 0; font-size: 11.5px; color: var(--ink-soft); }
1685
- .ctrl-metrics > span { white-space: nowrap; }
1686
- .ctrl-metrics .sep { opacity: 0.4; }
1687
- .ctrl-metrics .warn { color: var(--tone-warning-dot); }
1688
-
1689
- /* 产线状态带:label 固定 + 单行横向滚动(任意产线数量恒定一行高) */
1690
- .line-strip {
1691
- display: flex;
1692
- gap: 12px;
1693
- align-items: center;
1694
- padding-top: 10px;
1695
- border-top: 1px solid var(--divider-hair);
1696
- }
1697
- .strip-label {
1698
- display: inline-flex;
1699
- flex: 0 0 auto;
1700
- gap: 6px;
1701
- align-items: center;
1702
- font-size: 10.5px;
1703
- letter-spacing: 0.08em;
1704
- text-transform: uppercase;
1705
- color: var(--ink-faint);
1706
- }
1707
- .strip-label small { font-size: 10px; color: var(--ink-faint); }
1708
- .strip-scroll {
1709
- display: flex;
1710
- gap: 8px;
1711
- flex: 1 1 auto;
1712
- min-width: 0;
1713
- overflow-x: auto;
1714
- padding-bottom: 2px;
1715
- scrollbar-width: thin;
1716
- scrollbar-color: var(--line-strong) transparent;
1717
- }
1718
- .strip-scroll::-webkit-scrollbar { height: 4px; }
1719
- .strip-scroll::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 2px; }
1720
- .strip-scroll::-webkit-scrollbar-track { background: transparent; }
1721
- .line-pill {
1722
- display: inline-flex;
1723
- flex: 0 0 auto;
1724
- gap: 7px;
1725
- align-items: center;
1726
- padding: 4px 12px;
1727
- font-size: 11.5px;
1728
- color: var(--ink-soft);
1729
- background: var(--paper-deep);
1730
- border: 1px solid var(--line-strong);
1731
- border-radius: var(--radius-pill);
1732
- transition: color 0.15s, border-color 0.15s, background 0.15s;
1733
- }
1734
- /* 空心点 = 待机;实心呼吸点 = 运行(状态语义承载体,非装饰) */
1735
- .line-pill .lp-dot {
1736
- width: 7px;
1737
- height: 7px;
1738
- border: 1.5px solid var(--lc, var(--ink-fainter));
1739
- border-radius: 50%;
1740
- opacity: 0.55;
1741
- }
1742
- .line-pill b { font-weight: 600; }
1743
- .line-pill small { font-size: 10.5px; color: var(--ink-faint); }
1744
- .line-pill:hover { border-color: var(--lc, var(--accent)); color: var(--ink); }
1745
- .line-pill.on {
1746
- color: var(--ink);
1747
- background: color-mix(in srgb, var(--lc) 10%, transparent);
1748
- border-color: color-mix(in srgb, var(--lc) 50%, transparent);
1749
- }
1750
- .line-pill.on .lp-dot {
1751
- border-color: transparent;
1752
- background: var(--lc);
1753
- opacity: 1;
1754
- }
1755
- .line-pill.on .lp-run { color: color-mix(in srgb, var(--lc) 75%, var(--ink)); }
1756
- @media (prefers-reduced-motion: no-preference) {
1757
- .line-pill.on .lp-dot { box-shadow: 0 0 7px color-mix(in srgb, var(--lc) 75%, transparent); animation: lpPulse 1.8s ease-in-out infinite; }
1758
- }
1759
- @keyframes lpPulse {
1760
- 0%, 100% { box-shadow: 0 0 3px color-mix(in srgb, var(--lc) 55%, transparent); }
1761
- 50% { box-shadow: 0 0 9px color-mix(in srgb, var(--lc) 85%, transparent); }
1762
- }
1763
-
1764
- .table-card { overflow-x: auto; }
1765
- /* 窗口化占位行:只提供高度,不参与绘制/命中,也不继承单元格内距与下边框
1766
- (否则占位行会自带一条分割线和一个 9px 内距,滚动高度算不准) */
1767
- .vs-pad td { padding: 0 !important; border: 0 !important; }
1768
- .nodes-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
1769
- /* 离屏行跳过布局/绘制(102 行长表;contain-intrinsic-size 给出占位高度防滚动条跳动) */
1770
- .nodes-table tbody tr { content-visibility: auto; contain-intrinsic-size: auto 42px; }
1771
- /* 单元格规则须穿透行组件(:deep):行组件是渲染隔离单元,其内部 td 只带子作用域,
1772
- 父页 scoped 选择器不命中 —— 行内一切样式见 DaqNodeRow.vue 的 scoped 块。 */
1773
- .nodes-table th, .nodes-table :deep(td) { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--divider-hair); }
1774
- .nodes-table th {
1775
- font-size: 11px;
1776
- font-weight: 600;
1777
- letter-spacing: 0.06em;
1778
- text-transform: uppercase;
1779
- color: var(--ink-faint);
1780
- border-bottom: 1px solid var(--line-strong);
1781
- }
1782
- .nodes-table :deep(td b) { margin-left: 8px; }
1783
- .right, .nodes-table :deep(.right) { text-align: right; }
1784
-
1785
- /* ── 实时告警 + 实时事件双面板(server 权威;玻璃面板语言) ── */
1786
- .ops-panels {
1787
- display: grid;
1788
- grid-template-columns: minmax(320px, 5fr) minmax(320px, 7fr);
1789
- gap: 10px;
1790
- margin: 0 0 10px;
1791
- }
1792
- @media (max-width: 1100px) {
1793
- .ops-panels { grid-template-columns: 1fr; }
1794
- }
1795
- .ops-panel {
1796
- min-width: 0;
1797
- background: var(--surface-glass);
1798
- border: 1px solid var(--glass-line);
1799
- border-radius: 10px;
1800
- backdrop-filter: var(--aurora-blur) saturate(1.15);
1801
- }
1802
- .ops-panel.alarm-panel.live { border-color: color-mix(in srgb, var(--tone-danger-dot) 40%, var(--glass-line)); }
1803
- .panel-head {
1804
- display: flex;
1805
- gap: 7px;
1806
- align-items: center;
1807
- padding: 8px 12px;
1808
- font-size: 12.5px;
1809
- color: var(--ink-soft);
1810
- border-bottom: 1px solid var(--glass-line);
1811
- }
1812
- .panel-head .bell { color: var(--tone-danger-dot); }
1813
- .panel-head .cnt { margin-left: auto; font-size: 11px; color: var(--ink-faint); }
1814
- .panel-head .cnt.hot { color: var(--tone-danger-dot); font-weight: 700; }
1815
- .panel-link {
1816
- display: inline-flex;
1817
- gap: 3px;
1818
- align-items: center;
1819
- margin-left: auto;
1820
- font-size: 11.5px;
1821
- color: var(--tone-info-dot);
1822
- text-decoration: none;
1823
- }
1824
- .panel-head .cnt + .panel-link { margin-left: 12px; }
1825
- .panel-body {
1826
- display: flex;
1827
- flex-direction: column;
1828
- gap: 5px;
1829
- max-height: 168px;
1830
- padding: 8px 12px;
1831
- overflow-y: auto;
1832
- }
1833
- .panel-empty { margin: 4px 0; font-size: 12px; color: var(--ink-faint); }
1834
- .alarm-row {
1835
- display: flex;
1836
- gap: 7px;
1837
- align-items: center;
1838
- padding: 5px 8px;
1839
- font-size: 12px;
1840
- color: var(--tone-danger-dot);
1841
- background: var(--tone-danger-bg);
1842
- border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 35%, transparent);
1843
- border-radius: 7px;
1844
- }
1845
- .alarm-row .tri { flex: none; }
1846
- .alarm-row b { color: var(--ink); }
1847
- .alarm-row .esc { color: var(--tone-danger-dot); }
1848
- .alarm-row .ack { margin-left: auto; }
1849
- .evt-row {
1850
- display: flex;
1851
- gap: 7px;
1852
- align-items: center;
1853
- padding: 4px 6px;
1854
- font-size: 12px;
1855
- border-radius: 6px;
1856
- }
1857
- .evt-row:hover { background: var(--frost-bg); }
1858
- .evt-row .t { flex: none; font-size: 11px; }
1859
- .evt-row .txt {
1860
- min-width: 0;
1861
- overflow: hidden;
1862
- color: var(--ink-soft);
1863
- text-overflow: ellipsis;
1864
- white-space: nowrap;
1865
- }
1866
- .src-badge {
1867
- flex: none;
1868
- padding: 1px 6px;
1869
- font-size: 10px;
1870
- border: 1px solid var(--glass-line);
1871
- border-radius: 99px;
1872
- color: var(--ink-faint);
1873
- }
1874
- .src-badge.agent { color: var(--tone-info-dot); border-color: color-mix(in srgb, var(--tone-info-dot) 40%, transparent); }
1875
- .src-badge.user { color: var(--tone-success-dot); border-color: color-mix(in srgb, var(--tone-success-dot) 40%, transparent); }
1876
- .src-badge.system { color: var(--ink-faint); }
1877
- .kind-chip {
1878
- flex: none;
1879
- padding: 1px 6px;
1880
- font-size: 10px;
1881
- color: var(--ink-soft);
1882
- background: var(--frost-bg);
1883
- border-radius: 5px;
1884
- }
1885
- .kind-chip.alarm { color: var(--tone-danger-dot); background: var(--tone-danger-bg); }
1886
- .kind-chip.write { color: var(--tone-info-dot); }
1887
- .kind-chip.rollback { color: var(--tone-warning-dot); }
1888
-
1889
- /* ── 行内单元格样式(状态 pill/实时值/趋势/驱动/产线/绑定/操作)见行组件 ── */
1890
-
1891
- .err { margin-top: 14px; font-size: 13px; color: var(--tone-danger-dot); }
1892
- @media (prefers-reduced-motion: no-preference) {
1893
- .nodes-table tbody tr:hover { background: var(--hover-tint); }
1894
- }
1895
-
1896
- .infra-banner {
1897
- display: flex;
1898
- gap: 10px;
1899
- align-items: center;
1900
- padding: 10px 14px;
1901
- margin-bottom: 14px;
1902
- color: var(--tone-warning-dot);
1903
- background: var(--tone-warning-bg);
1904
- border: 1px solid color-mix(in srgb, var(--tone-warning-dot) 40%, transparent);
1905
- border-radius: var(--radius-chip);
1906
- }
1907
- /* 筛选产线运行中:绿色语义变体 */
1908
- .infra-banner.good {
1909
- color: var(--tone-success-dot);
1910
- background: var(--tone-success-bg);
1911
- border-color: color-mix(in srgb, var(--tone-success-dot) 40%, transparent);
1912
- }
1913
- .infra-banner .txt { flex: 1 1 auto; font-size: 12.5px; line-height: 1.5; }
1914
- .infra-banner .txt a { color: var(--accent); text-decoration: underline; }
1915
- .infra-banner .txt b { font-weight: 600; }
1916
- .infra-banner .txt small { margin-left: 8px; font-size: 10.5px; opacity: 0.75; }
1917
- .infra-banner .pill-btn { flex: 0 0 auto; }
1918
-
1919
- /* ---------- 未确认报警条(S5) ---------- */
1920
- .alarm-bar {
1921
- display: flex;
1922
- flex-wrap: wrap;
1923
- gap: 12px;
1924
- align-items: center;
1925
- padding: 8px 14px;
1926
- margin-bottom: 14px;
1927
- color: var(--tone-danger-dot);
1928
- background: var(--tone-danger-bg);
1929
- border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 40%, transparent);
1930
- border-radius: var(--radius-chip);
1931
- }
1932
- .bar-label {
1933
- display: inline-flex;
1934
- flex: 0 0 auto;
1935
- gap: 6px;
1936
- align-items: center;
1937
- font-size: 11px;
1938
- font-weight: 600;
1939
- letter-spacing: 0.05em;
1940
- text-transform: uppercase;
1941
- }
1942
- .bar-label .bell { animation: bellShake 1.6s ease-in-out infinite; }
1943
- @media (prefers-reduced-motion: reduce) {
1944
- .bar-label .bell { animation: none; }
1945
- }
1946
- @keyframes bellShake {
1947
- 0%, 80%, 100% { transform: rotate(0deg); }
1948
- 85% { transform: rotate(12deg); }
1949
- 90% { transform: rotate(-10deg); }
1950
- 95% { transform: rotate(6deg); }
1951
- }
1952
- .alarm-scroll {
1953
- display: flex;
1954
- flex: 1 1 auto;
1955
- gap: 8px;
1956
- min-width: 0;
1957
- overflow-x: auto;
1958
- scrollbar-width: thin;
1959
- scrollbar-color: var(--line-strong) transparent;
1960
- }
1961
- .alarm-item {
1962
- display: inline-flex;
1963
- flex: 0 0 auto;
1964
- gap: 8px;
1965
- align-items: center;
1966
- padding: 4px 10px;
1967
- font-size: 11.5px;
1968
- color: var(--ink-soft);
1969
- background: var(--paper-deep);
1970
- border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 30%, transparent);
1971
- border-radius: var(--radius-pill);
1972
- }
1973
- .alarm-item b { font-weight: 600; }
1974
- .alarm-item .dim { opacity: 0.6; }
1975
- .alarm-item .esc { color: var(--tone-warning-dot); }
1976
- .alarm-item .ack {
1977
- padding: 2px 9px;
1978
- color: var(--paper-raised);
1979
- background: var(--tone-danger-dot);
1980
- border-color: transparent;
1981
- }
1982
- .alarm-item .ack:hover { opacity: 0.85; border-color: transparent; color: var(--paper-raised); }
1983
-
1984
- /* ---------- 添加节点向导 ---------- */
1985
- .ctrl-right { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; min-width: 0; }
1986
- .add-btn { padding: 8px 16px; font-size: 13px; white-space: nowrap; }
1987
- .modal-mask {
1988
- position: fixed;
1989
- z-index: 50;
1990
- inset: 0;
1991
- display: flex;
1992
- align-items: center;
1993
- justify-content: center;
1994
- background: var(--scrim);
1995
- backdrop-filter: blur(2px);
1996
- }
1997
- .modal {
1998
- width: 640px;
1999
- max-width: 94vw;
2000
- max-height: 88vh;
2001
- overflow-y: auto;
2002
- padding: 22px 24px;
2003
- background: var(--surface-glass-strong);
2004
- backdrop-filter: var(--aurora-blur);
2005
- border: 1px solid var(--glass-line);
2006
- border-radius: var(--radius-panel);
2007
- box-shadow: var(--glass-edge), var(--shadow-float);
2008
- }
2009
- .m-title { margin: 0 0 14px; font-size: 17px; }
2010
- .seg-row { display: flex; gap: 8px; margin-bottom: 14px; }
2011
- .seg {
2012
- flex: 1;
2013
- padding: 8px 0;
2014
- font-size: 13px;
2015
- cursor: pointer;
2016
- color: var(--ink-faint);
2017
- background: var(--paper-deep);
2018
- border: 1px solid var(--line);
2019
- border-radius: var(--radius-chip);
2020
- }
2021
- .seg.on {
2022
- font-weight: 600;
2023
- color: var(--on-accent);
2024
- background: var(--accent);
2025
- border-color: var(--accent);
2026
- }
2027
- .f-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
2028
- .driver-form { grid-template-columns: repeat(3, 1fr); }
2029
- .cal-form { grid-template-columns: 2fr 1fr; align-items: end; }
2030
- .f { display: flex; flex-direction: column; gap: 4px; font-size: 11.5px; color: var(--ink-faint); }
2031
- .f em { margin-left: 3px; font-style: normal; color: var(--tone-danger-dot); }
2032
- .inp {
2033
- width: 100%;
2034
- padding: 6px 9px;
2035
- font-size: 12.5px;
2036
- color: var(--ink);
2037
- background: var(--paper-deep);
2038
- border: 1px solid var(--line-strong);
2039
- border-radius: var(--radius-chip);
2040
- }
2041
- .hint { font-size: 10px; color: var(--ink-faint); }
2042
- .test-row { display: flex; gap: 10px; align-items: center; margin: 6px 0 4px; }
2043
- .test-result { font-family: var(--font-mono); font-size: 11px; }
2044
- .test-result.ok { color: var(--tone-success-dot); }
2045
- .test-result.bad { color: var(--tone-danger-dot); }
2046
- .m-err { margin: 8px 0 0; font-size: 12px; color: var(--tone-danger-dot); }
2047
- .m-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; }
2048
- .aw-pill.outline { color: var(--ink); background: var(--paper-raised); border-color: var(--line-strong); }
2049
-
2050
- /* ---------- 模板管理 ---------- */
2051
- .sec-label {
2052
- margin: 14px 0 6px;
2053
- font-size: 11px;
2054
- font-weight: 600;
2055
- letter-spacing: 0.06em;
2056
- text-transform: uppercase;
2057
- color: var(--ink-faint);
2058
- }
2059
- .tpl-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
2060
- .tpl-table td { padding: 7px 8px; border-bottom: 1px solid var(--divider-hair); }
2061
- .tpl-table td b { margin-right: 8px; }
2062
- .tpl-table .range { font-size: 11.5px; color: var(--ink-soft); }
2063
- .tpl-table .empty { color: var(--ink-faint); font-size: 12px; text-align: center; padding: 14px 0; }
2064
- .tpl-table .actions { white-space: nowrap; }
2065
-
2066
- /* ---------- 节点筛选工具条 ---------- */
2067
- .tbl-toolbar {
2068
- display: flex;
2069
- flex-wrap: wrap;
2070
- gap: 10px;
2071
- align-items: flex-end;
2072
- justify-content: space-between;
2073
- padding: 11px 14px;
2074
- border-bottom: 1px solid var(--divider-hair);
2075
- }
2076
- .filters { display: flex; flex-wrap: wrap; gap: 9px; align-items: flex-end; }
2077
- .flt {
2078
- display: flex;
2079
- flex-direction: column;
2080
- gap: 3px;
2081
- font-size: 10px;
2082
- letter-spacing: 0.05em;
2083
- text-transform: uppercase;
2084
- color: var(--ink-faint);
2085
- }
2086
- .inp-sel {
2087
- min-width: 112px;
2088
- padding: 4px 8px;
2089
- font-size: 11.5px;
2090
- color: var(--ink);
2091
- background: var(--paper-deep);
2092
- border: 1px solid var(--line-strong);
2093
- border-radius: var(--radius-chip);
2094
- transition: border-color 0.15s;
2095
- }
2096
- .inp-sel:focus { outline: none; border-color: var(--accent); }
2097
- .clear-btn { margin-bottom: 1px; }
2098
- /* 名称/ID 搜索:与下拉同族,宽度独立(复合筛选的最后一个自由维度) */
2099
- .flt-search {
2100
- width: 168px;
2101
- min-width: 0;
2102
- }
2103
- .flt-search::placeholder { color: var(--ink-faint); }
2104
- .count { padding-bottom: 4px; font-size: 11px; color: var(--ink-faint); }
2105
-
2106
- /* ── 行内产线/绑定/运行态样式见 DaqNodeRow.vue scoped 块 ── */
2107
-
2108
- /* ── Agent 优化记录面板(调控闭环;与全站玻璃语言同族) ── */
2109
- .opt-card { padding: 0; }
2110
- .opt-head {
2111
- display: flex;
2112
- gap: 9px;
2113
- align-items: center;
2114
- width: 100%;
2115
- padding: 12px 16px;
2116
- font-size: 13.5px;
2117
- color: var(--ink);
2118
- background: transparent;
2119
- border: 0;
2120
- cursor: pointer;
2121
- }
2122
- .opt-head > .i-tabler-flask { font-size: 16px; color: var(--tone-info-dot); }
2123
- .opt-count {
2124
- padding: 1px 8px;
2125
- font-size: 10.5px;
2126
- color: var(--tone-info-dot);
2127
- background: var(--tone-info-bg);
2128
- border-radius: 999px;
2129
- }
2130
- .opt-chevron {
2131
- width: 7px;
2132
- height: 7px;
2133
- margin-left: auto;
2134
- border-right: 1.5px solid var(--ink-faint);
2135
- border-bottom: 1.5px solid var(--ink-faint);
2136
- transform: rotate(45deg);
2137
- transition: transform var(--transition-base);
2138
- }
2139
- .opt-chevron.open { transform: rotate(225deg); }
2140
- /* 手风琴展开/收起(occasional 频次;状态指示;height 豁免 = 手风琴;进出对称 260ms;
2141
- * 收起态 visibility 联动,内部链接/按钮不可聚焦) */
2142
- .opt-body {
2143
- display: flex;
2144
- flex-direction: column;
2145
- gap: 10px;
2146
- max-height: 0;
2147
- padding: 0 16px;
2148
- overflow: hidden;
2149
- visibility: hidden;
2150
- opacity: 0;
2151
- transition:
2152
- max-height 260ms cubic-bezier(0.22, 0.68, 0.36, 1),
2153
- opacity 200ms ease,
2154
- visibility 260ms;
2155
- }
2156
- .opt-body.open {
2157
- max-height: 64vh;
2158
- padding-bottom: 14px;
2159
- overflow-y: auto;
2160
- visibility: visible;
2161
- opacity: 1;
2162
- }
2163
- .opt-filters { display: flex; gap: 8px; align-items: center; }
2164
- .opt-empty { font-size: 11.5px; color: var(--ink-faint); }
2165
- .opt-list { display: flex; flex-direction: column; gap: 7px; margin: 0; padding: 0; list-style: none; }
2166
- .opt-row {
2167
- display: flex;
2168
- flex-direction: column;
2169
- gap: 5px;
2170
- padding: 9px 12px;
2171
- background: var(--frost-bg);
2172
- border: 1px solid var(--glass-line);
2173
- border-radius: 10px;
2174
- }
2175
- .opt-row.open { border-color: color-mix(in srgb, var(--tone-warning-dot) 36%, var(--glass-line)); }
2176
- .opt-row.rolled-back { border-color: color-mix(in srgb, var(--tone-danger-dot) 30%, var(--glass-line)); }
2177
- .opt-line1 { display: flex; gap: 9px; align-items: center; min-width: 0; }
2178
- .opt-status {
2179
- flex: none;
2180
- padding: 1px 8px;
2181
- font-family: var(--font-mono);
2182
- font-size: 9.5px;
2183
- color: var(--ink-faint);
2184
- border: 1px solid var(--line-strong);
2185
- border-radius: 5px;
2186
- }
2187
- .opt-status.open { color: var(--tone-warning-dot); border-color: color-mix(in srgb, var(--tone-warning-dot) 45%, transparent); }
2188
- .opt-status.judged-keep { color: var(--tone-success-dot); border-color: color-mix(in srgb, var(--tone-success-dot) 45%, transparent); }
2189
- .opt-status.rolled-back { color: var(--tone-danger-dot); border-color: color-mix(in srgb, var(--tone-danger-dot) 45%, transparent); }
2190
- .opt-node { overflow: hidden; font-size: 12.5px; color: var(--ink); text-overflow: ellipsis; white-space: nowrap; }
2191
- .opt-params { flex: none; font-size: 11px; color: var(--tone-info-dot); }
2192
- .opt-time { flex: none; margin-left: auto; font-size: 10px; color: var(--ink-faint); }
2193
- .mini-act {
2194
- flex: none;
2195
- padding: 2px 9px;
2196
- font-size: 10.5px;
2197
- color: var(--tone-info-dot);
2198
- background: var(--tone-info-bg);
2199
- border: 0;
2200
- border-radius: 6px;
2201
- cursor: pointer;
2202
- }
2203
- .mini-act.danger { color: var(--tone-danger-dot); background: var(--tone-danger-bg); }
2204
- .mini-act:disabled { opacity: 0.5; cursor: default; }
2205
- .opt-line2 { display: flex; flex-wrap: wrap; gap: 8px; font-size: 10.5px; color: var(--ink-faint); }
2206
- .opt-judge.rollback, .opt-judge.keep, .opt-judge.uncertain { color: var(--ink-soft); }
2207
- .opt-judge b { font-weight: 600; }
2208
- .opt-open-hint { color: var(--tone-warning-dot); }
2209
- .opt-agg { color: var(--ink-faint); }
2210
- .opt-series { display: flex; flex-wrap: wrap; gap: 10px; padding-top: 4px; }
2211
- .opt-ch { display: flex; flex: 1 1 180px; gap: 8px; align-items: center; min-width: 160px; }
2212
- .opt-spark { width: 120px; height: 30px; }
2213
- .opt-spark polyline {
2214
- fill: none;
2215
- stroke: var(--tone-info-dot);
2216
- stroke-width: 1.5;
2217
- }
2218
-
2219
- /* ══════════════════════════════════════════════════════════════════════════
2220
- 页面级自适应加固(375 / 768 / 1440 三档)
2221
- 根因统一:flex 子项被压缩到 min-content → 中文按钮退化成「单列竖排」,
2222
- 左右两组控件抢同一行 → 右边缘越界。策略:先禁止压缩(nowrap),
2223
- 再在窄屏把控制条改成「整行 / 两列」分层排布;筛选区单列堆叠 → 两列网格。
2224
- ══════════════════════════════════════════════════════════════════════════ */
2225
- /* —— 标签字号地板(全档位,不只是窄屏)——
2226
- 本页所有自有小标签原为 9.5–11px,低于「标签 ≥11.5px」验收线;统一抬到 11.5px。
2227
- 注:节点表行内 10/10.5px 徽标在 app/components/workshop/DaqNodeRow.vue,不在本页职责内。 —— */
2228
- .badge, .sec-label, .flt, .hint, .strip-label, .strip-label small,
2229
- .line-pill small, .opt-status, .opt-time, .opt-line2, .mini-act,
2230
- .src-badge, .kind-chip, .evt-row .t, .opt-count,
2231
- .infra-banner .txt small { font-size: 11.5px; }
2232
- .sub { font-size: 13px; }
2233
- .alarm-item small, .alarm-row small { font-size: 11.5px; }
2234
-
2235
- @media (max-width: 900px) {
2236
- /* —— 兜底字号地板:本页任何 small/mono 小字在手持档不得低于 11.5px ——
2237
- 逐类点名容易漏(实测 infra-banner 的配方名、line-pill 的 mono 小字都漏过),
2238
- 这里用元素级兜底把整页收口;它放在本媒体块的**最后面**,
2239
- 同特异度时后写者胜,所以不会破坏上面更细的排版意图。 */
2240
- .page small,
2241
- .page small.mono,
2242
- .page small.mono b {
2243
- font-size: 11.5px;
2244
- }
2245
-
2246
- /* —— 触摸目标:报警「确认」在窄屏只有 44×22,手指点不中 —— */
2247
- .alarm-item { padding: 6px 10px; }
2248
- .alarm-item .ack, .mini-btn.ack { min-height: 32px; padding: 4px 12px; white-space: nowrap; }
2249
- .alarm-row { flex-wrap: wrap; }
2250
-
2251
- /* —— 窄屏字号地板(正文 ≥12.5px / 标签 ≥11.5px)——
2252
- 注:侧栏 8.5px/9.5px 属全局层(main.css / AppSidebar.vue),不在本页职责内 —— */
2253
- .flt, .sec-label, .hint { font-size: 11.5px; }
2254
- .inp-sel { font-size: 12.5px; }
2255
- .badge, .strip-label, .line-pill small { font-size: 11.5px; }
2256
- .ctrl-metrics { font-size: 12px; }
2257
- .evt-row { font-size: 12.5px; }
2258
- .evt-row .t, .src-badge, .kind-chip { font-size: 11.5px; }
2259
- .panel-head { font-size: 13px; }
2260
- .alarm-item, .alarm-item small, .alarm-row small, .opt-status, .mini-act,
2261
- .opt-time, .opt-line2, .clear-btn, .opt-count, .strip-label small { font-size: 11.5px; }
2262
- .tpl-table .range, .opt-empty, .panel-empty { font-size: 12px; }
2263
- .infra-banner .txt small { font-size: 11.5px; }
2264
- .m-title { font-size: 16px; }
2265
- .sub { font-size: 13px; }
2266
- }
2267
-
2268
- @media (max-width: 700px) {
2269
- /* —— 控制器总控条:暂停全部采集 整行;两个周期项两列;两个 CTA 两列 —— */
2270
- .ctrl-card { padding: 12px 14px 11px; }
2271
- .ctrl-row { flex-direction: column; align-items: stretch; gap: 12px; }
2272
- .ctrl-left { gap: 10px; }
2273
- .ctrl-left > .aw-pill { flex: 1 1 100%; }
2274
- .ctrl-left > .cycle { flex: 1 1 calc(50% - 5px); justify-content: space-between; }
2275
- .cycle input { width: 62px; }
2276
- .ctrl-right { gap: 10px; }
2277
- .ctrl-metrics { flex: 1 1 100%; }
2278
- .ctrl-right > .add-btn { flex: 1 1 calc(50% - 5px); }
2279
-
2280
- /* —— 未确认报警横条:标题独占一行,滚动轨吃满整宽(右侧内容不再被挤掉) —— */
2281
- .alarm-bar { flex-direction: column; align-items: stretch; gap: 8px; padding: 10px 12px; }
2282
- .bar-label { justify-content: flex-start; }
2283
- .alarm-scroll { width: 100%; padding-bottom: 3px; }
2284
- .alarm-item { max-width: 100%; }
2285
-
2286
- /* —— 节点筛选工具条:单列下拉堆叠 → 两列网格 —— */
2287
- .tbl-toolbar { flex-direction: column; align-items: stretch; gap: 8px; padding: 11px 12px; }
2288
- .filters { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px; }
2289
- .filters > .flt { min-width: 0; }
2290
- .inp-sel { width: 100%; min-width: 0; }
2291
- .flt-search { width: 100%; }
2292
- .clear-btn { grid-column: 1 / -1; min-height: 34px; }
2293
- .count { padding-bottom: 0; }
2294
-
2295
- /* —— Agent 优化记录:筛选区两列网格;记录行允许换行,长节点名不再被切 —— */
2296
- .opt-filters { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
2297
- .opt-filters > .mini-act { grid-column: 1 / -1; min-height: 34px; }
2298
- .opt-line1 { flex-wrap: wrap; }
2299
- .opt-node { flex: 1 1 100%; overflow: visible; white-space: normal; }
2300
- .opt-time { margin-left: 0; }
2301
- .opt-ch { flex: 1 1 100%; }
2302
- .opt-spark { flex: 1 1 auto; width: auto; min-width: 96px; }
2303
-
2304
- /* —— 主表:压缩单元格内边距,缩短横向滚动距离(表格横滚 + 首列 sticky 是全局层设计) —— */
2305
- .nodes-table th, .nodes-table :deep(td) { padding: 7px 8px; }
2306
- .table-card { -webkit-overflow-scrolling: touch; }
2307
- .opt-body.open { max-height: 70vh; }
2308
-
2309
- /* —— 实时事件行:摘要原来只剩 141px 被省略号切掉 → 换行后独占整宽 —— */
2310
- .evt-row { flex-wrap: wrap; }
2311
- .evt-row .txt { flex: 1 1 100%; white-space: normal; overflow: visible; }
2312
- }
2313
- </style>
1599
+ />
1600
+ </tr>
1601
+ <tr v-if="daq.loaded && daq.nodes.length === 0">
1602
+ <td colspan="11">
1603
+ <div
1604
+ class="pane-empty"
1605
+ style="min-height: 120px;"
1606
+ >
1607
+ <p class="pe-sub">
1608
+ {{ $t('daq.kvhjyc3075') }}
1609
+ </p>
1610
+ </div>
1611
+ </td>
1612
+ </tr>
1613
+ <tr v-else-if="daq.loaded && filteredNodes.length === 0">
1614
+ <td colspan="11">
1615
+ <div
1616
+ class="pane-empty"
1617
+ style="min-height: 90px;"
1618
+ >
1619
+ <p class="pe-sub">
1620
+ {{ $t('daq.k1w20qew076') }}
1621
+ </p>
1622
+ </div>
1623
+ </td>
1624
+ </tr>
1625
+ </tbody>
1626
+ </table>
1627
+ </section>
1628
+ </a-spin>
1629
+
1630
+ <p
1631
+ v-if="daq.error"
1632
+ class="err"
1633
+ >
1634
+ {{ daq.error }}(<NuxtLink to="/workshop">{{ $t('daq.k1bhhheq077') }}</NuxtLink>)
1635
+ </p>
1636
+ </div>
1637
+ </template>
1638
+
1639
+ <style scoped>
1640
+ .page { padding: 4px; }
1641
+ h1 { margin: 2px 0 4px; font-size: 30px; font-weight: 400; letter-spacing: -0.015em; }
1642
+ .sub { margin: 0; font-size: 12.5px; opacity: 0.6; }
1643
+ .mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
1644
+ .dim { opacity: 0.55; }
1645
+
1646
+ .badges { display: flex; gap: 8px; }
1647
+ .badge {
1648
+ padding: 3px 10px;
1649
+ font-size: 11px;
1650
+ letter-spacing: 0.05em;
1651
+ color: var(--ink-soft);
1652
+ border: 1px solid var(--line-strong);
1653
+ border-radius: var(--radius-chip);
1654
+ }
1655
+
1656
+ .ctrl-card {
1657
+ display: flex;
1658
+ flex-direction: column;
1659
+ gap: 11px;
1660
+ padding: 12px 18px 11px;
1661
+ margin-bottom: 14px;
1662
+ }
1663
+ .ctrl-row {
1664
+ display: flex;
1665
+ flex-wrap: wrap;
1666
+ gap: 14px;
1667
+ align-items: center;
1668
+ justify-content: space-between;
1669
+ }
1670
+ /* 窄屏前先写死「不可压缩」:药丸/周期项被 flex 收缩到 min-content → 中文退化成单列竖排。
1671
+ 保持自然宽度,由外层 flex-wrap 决定换行位置。 */
1672
+ .ctrl-left { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; min-width: 0; }
1673
+ .ctrl-left > *, .ctrl-right > * { flex: 0 0 auto; }
1674
+ .aw-pill { flex: 0 0 auto; white-space: nowrap; }
1675
+ /* 行内小按钮(确认/筛选/回滚…)同族:窄屏禁止中文折成两行 */
1676
+ .mini-btn, .mini-act, .pill-btn, .node-toggle { white-space: nowrap; }
1677
+ .aw-pill.running { background: var(--accent); }
1678
+ .cycle { display: inline-flex; flex: 0 0 auto; gap: 6px; align-items: center; font-size: 12px; white-space: nowrap; color: var(--ink-faint); }
1679
+ .cycle input {
1680
+ width: 84px;
1681
+ padding: 4px 8px;
1682
+ color: var(--ink);
1683
+ background: var(--paper-deep);
1684
+ border: 1px solid var(--line-strong);
1685
+ border-radius: var(--radius-chip);
1686
+ }
1687
+ .ctrl-metrics { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; min-width: 0; font-size: 11.5px; color: var(--ink-soft); }
1688
+ .ctrl-metrics > span { white-space: nowrap; }
1689
+ .ctrl-metrics .sep { opacity: 0.4; }
1690
+ .ctrl-metrics .warn { color: var(--tone-warning-dot); }
1691
+
1692
+ /* 产线状态带:label 固定 + 单行横向滚动(任意产线数量恒定一行高) */
1693
+ .line-strip {
1694
+ display: flex;
1695
+ gap: 12px;
1696
+ align-items: center;
1697
+ padding-top: 10px;
1698
+ border-top: 1px solid var(--divider-hair);
1699
+ }
1700
+ .strip-label {
1701
+ display: inline-flex;
1702
+ flex: 0 0 auto;
1703
+ gap: 6px;
1704
+ align-items: center;
1705
+ font-size: 10.5px;
1706
+ letter-spacing: 0.08em;
1707
+ text-transform: uppercase;
1708
+ color: var(--ink-faint);
1709
+ }
1710
+ .strip-label small { font-size: 10px; color: var(--ink-faint); }
1711
+ .strip-scroll {
1712
+ display: flex;
1713
+ gap: 8px;
1714
+ flex: 1 1 auto;
1715
+ min-width: 0;
1716
+ overflow-x: auto;
1717
+ padding-bottom: 2px;
1718
+ scrollbar-width: thin;
1719
+ scrollbar-color: var(--line-strong) transparent;
1720
+ }
1721
+ .strip-scroll::-webkit-scrollbar { height: 4px; }
1722
+ .strip-scroll::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 2px; }
1723
+ .strip-scroll::-webkit-scrollbar-track { background: transparent; }
1724
+ .line-pill {
1725
+ display: inline-flex;
1726
+ flex: 0 0 auto;
1727
+ gap: 7px;
1728
+ align-items: center;
1729
+ padding: 4px 12px;
1730
+ font-size: 11.5px;
1731
+ color: var(--ink-soft);
1732
+ background: var(--paper-deep);
1733
+ border: 1px solid var(--line-strong);
1734
+ border-radius: var(--radius-pill);
1735
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
1736
+ }
1737
+ /* 空心点 = 待机;实心呼吸点 = 运行(状态语义承载体,非装饰) */
1738
+ .line-pill .lp-dot {
1739
+ width: 7px;
1740
+ height: 7px;
1741
+ border: 1.5px solid var(--lc, var(--ink-fainter));
1742
+ border-radius: 50%;
1743
+ opacity: 0.55;
1744
+ }
1745
+ .line-pill b { font-weight: 600; }
1746
+ .line-pill small { font-size: 10.5px; color: var(--ink-faint); }
1747
+ .line-pill:hover { border-color: var(--lc, var(--accent)); color: var(--ink); }
1748
+ .line-pill.on {
1749
+ color: var(--ink);
1750
+ background: color-mix(in srgb, var(--lc) 10%, transparent);
1751
+ border-color: color-mix(in srgb, var(--lc) 50%, transparent);
1752
+ }
1753
+ .line-pill.on .lp-dot {
1754
+ border-color: transparent;
1755
+ background: var(--lc);
1756
+ opacity: 1;
1757
+ }
1758
+ .line-pill.on .lp-run { color: color-mix(in srgb, var(--lc) 75%, var(--ink)); }
1759
+ @media (prefers-reduced-motion: no-preference) {
1760
+ .line-pill.on .lp-dot { box-shadow: 0 0 7px color-mix(in srgb, var(--lc) 75%, transparent); animation: lpPulse 1.8s ease-in-out infinite; }
1761
+ }
1762
+ @keyframes lpPulse {
1763
+ 0%, 100% { box-shadow: 0 0 3px color-mix(in srgb, var(--lc) 55%, transparent); }
1764
+ 50% { box-shadow: 0 0 9px color-mix(in srgb, var(--lc) 85%, transparent); }
1765
+ }
1766
+
1767
+ .table-card { overflow-x: auto; }
1768
+ /* 窗口化占位行:只提供高度,不参与绘制/命中,也不继承单元格内距与下边框
1769
+ (否则占位行会自带一条分割线和一个 9px 内距,滚动高度算不准) */
1770
+ .vs-pad td { padding: 0 !important; border: 0 !important; }
1771
+ .nodes-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
1772
+ /* 离屏行跳过布局/绘制(102 行长表;contain-intrinsic-size 给出占位高度防滚动条跳动) */
1773
+ .nodes-table tbody tr { content-visibility: auto; contain-intrinsic-size: auto 42px; }
1774
+ /* 单元格规则须穿透行组件(:deep):行组件是渲染隔离单元,其内部 td 只带子作用域,
1775
+ 父页 scoped 选择器不命中 —— 行内一切样式见 DaqNodeRow.vue 的 scoped 块。 */
1776
+ .nodes-table th, .nodes-table :deep(td) { padding: 9px 12px; text-align: left; border-bottom: 1px solid var(--divider-hair); }
1777
+ .nodes-table th {
1778
+ font-size: 11px;
1779
+ font-weight: 600;
1780
+ letter-spacing: 0.06em;
1781
+ text-transform: uppercase;
1782
+ color: var(--ink-faint);
1783
+ border-bottom: 1px solid var(--line-strong);
1784
+ }
1785
+ .nodes-table :deep(td b) { margin-left: 8px; }
1786
+ .right, .nodes-table :deep(.right) { text-align: right; }
1787
+
1788
+ /* ── 实时告警 + 实时事件双面板(server 权威;玻璃面板语言) ── */
1789
+ .ops-panels {
1790
+ display: grid;
1791
+ grid-template-columns: minmax(320px, 5fr) minmax(320px, 7fr);
1792
+ gap: 10px;
1793
+ margin: 0 0 10px;
1794
+ }
1795
+ @media (max-width: 1100px) {
1796
+ .ops-panels { grid-template-columns: 1fr; }
1797
+ }
1798
+ .ops-panel {
1799
+ min-width: 0;
1800
+ background: var(--surface-glass);
1801
+ border: 1px solid var(--glass-line);
1802
+ border-radius: 10px;
1803
+ backdrop-filter: var(--aurora-blur) saturate(1.15);
1804
+ }
1805
+ .ops-panel.alarm-panel.live { border-color: color-mix(in srgb, var(--tone-danger-dot) 40%, var(--glass-line)); }
1806
+ .panel-head {
1807
+ display: flex;
1808
+ gap: 7px;
1809
+ align-items: center;
1810
+ padding: 8px 12px;
1811
+ font-size: 12.5px;
1812
+ color: var(--ink-soft);
1813
+ border-bottom: 1px solid var(--glass-line);
1814
+ }
1815
+ .panel-head .bell { color: var(--tone-danger-dot); }
1816
+ .panel-head .cnt { margin-left: auto; font-size: 11px; color: var(--ink-faint); }
1817
+ .panel-head .cnt.hot { color: var(--tone-danger-dot); font-weight: 700; }
1818
+ .panel-link {
1819
+ display: inline-flex;
1820
+ gap: 3px;
1821
+ align-items: center;
1822
+ margin-left: auto;
1823
+ font-size: 11.5px;
1824
+ color: var(--tone-info-dot);
1825
+ text-decoration: none;
1826
+ }
1827
+ .panel-head .cnt + .panel-link { margin-left: 12px; }
1828
+ .panel-body {
1829
+ display: flex;
1830
+ flex-direction: column;
1831
+ gap: 5px;
1832
+ max-height: 168px;
1833
+ padding: 8px 12px;
1834
+ overflow-y: auto;
1835
+ }
1836
+ .panel-empty { margin: 4px 0; font-size: 12px; color: var(--ink-faint); }
1837
+ .alarm-row {
1838
+ display: flex;
1839
+ gap: 7px;
1840
+ align-items: center;
1841
+ padding: 5px 8px;
1842
+ font-size: 12px;
1843
+ color: var(--tone-danger-dot);
1844
+ background: var(--tone-danger-bg);
1845
+ border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 35%, transparent);
1846
+ border-radius: 7px;
1847
+ }
1848
+ .alarm-row .tri { flex: none; }
1849
+ .alarm-row b { color: var(--ink); }
1850
+ .alarm-row .esc { color: var(--tone-danger-dot); }
1851
+ .alarm-row .ack { margin-left: auto; }
1852
+ .evt-row {
1853
+ display: flex;
1854
+ gap: 7px;
1855
+ align-items: center;
1856
+ padding: 4px 6px;
1857
+ font-size: 12px;
1858
+ border-radius: 6px;
1859
+ }
1860
+ .evt-row:hover { background: var(--frost-bg); }
1861
+ .evt-row .t { flex: none; font-size: 11px; }
1862
+ .evt-row .txt {
1863
+ min-width: 0;
1864
+ overflow: hidden;
1865
+ color: var(--ink-soft);
1866
+ text-overflow: ellipsis;
1867
+ white-space: nowrap;
1868
+ }
1869
+ .src-badge {
1870
+ flex: none;
1871
+ padding: 1px 6px;
1872
+ font-size: 10px;
1873
+ border: 1px solid var(--glass-line);
1874
+ border-radius: 99px;
1875
+ color: var(--ink-faint);
1876
+ }
1877
+ .src-badge.agent { color: var(--tone-info-dot); border-color: color-mix(in srgb, var(--tone-info-dot) 40%, transparent); }
1878
+ .src-badge.user { color: var(--tone-success-dot); border-color: color-mix(in srgb, var(--tone-success-dot) 40%, transparent); }
1879
+ .src-badge.system { color: var(--ink-faint); }
1880
+ .kind-chip {
1881
+ flex: none;
1882
+ padding: 1px 6px;
1883
+ font-size: 10px;
1884
+ color: var(--ink-soft);
1885
+ background: var(--frost-bg);
1886
+ border-radius: 5px;
1887
+ }
1888
+ .kind-chip.alarm { color: var(--tone-danger-dot); background: var(--tone-danger-bg); }
1889
+ .kind-chip.write { color: var(--tone-info-dot); }
1890
+ .kind-chip.rollback { color: var(--tone-warning-dot); }
1891
+
1892
+ /* ── 行内单元格样式(状态 pill/实时值/趋势/驱动/产线/绑定/操作)见行组件 ── */
1893
+
1894
+ .err { margin-top: 14px; font-size: 13px; color: var(--tone-danger-dot); }
1895
+ @media (prefers-reduced-motion: no-preference) {
1896
+ .nodes-table tbody tr:hover { background: var(--hover-tint); }
1897
+ }
1898
+
1899
+ .infra-banner {
1900
+ display: flex;
1901
+ gap: 10px;
1902
+ align-items: center;
1903
+ padding: 10px 14px;
1904
+ margin-bottom: 14px;
1905
+ color: var(--tone-warning-dot);
1906
+ background: var(--tone-warning-bg);
1907
+ border: 1px solid color-mix(in srgb, var(--tone-warning-dot) 40%, transparent);
1908
+ border-radius: var(--radius-chip);
1909
+ }
1910
+ /* 筛选产线运行中:绿色语义变体 */
1911
+ .infra-banner.good {
1912
+ color: var(--tone-success-dot);
1913
+ background: var(--tone-success-bg);
1914
+ border-color: color-mix(in srgb, var(--tone-success-dot) 40%, transparent);
1915
+ }
1916
+ .infra-banner .txt { flex: 1 1 auto; font-size: 12.5px; line-height: 1.5; }
1917
+ .infra-banner .txt a { color: var(--accent); text-decoration: underline; }
1918
+ .infra-banner .txt b { font-weight: 600; }
1919
+ .infra-banner .txt small { margin-left: 8px; font-size: 10.5px; opacity: 0.75; }
1920
+ .infra-banner .pill-btn { flex: 0 0 auto; }
1921
+
1922
+ /* ---------- 未确认报警条(S5) ---------- */
1923
+ .alarm-bar {
1924
+ display: flex;
1925
+ flex-wrap: wrap;
1926
+ gap: 12px;
1927
+ align-items: center;
1928
+ padding: 8px 14px;
1929
+ margin-bottom: 14px;
1930
+ color: var(--tone-danger-dot);
1931
+ background: var(--tone-danger-bg);
1932
+ border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 40%, transparent);
1933
+ border-radius: var(--radius-chip);
1934
+ }
1935
+ .bar-label {
1936
+ display: inline-flex;
1937
+ flex: 0 0 auto;
1938
+ gap: 6px;
1939
+ align-items: center;
1940
+ font-size: 11px;
1941
+ font-weight: 600;
1942
+ letter-spacing: 0.05em;
1943
+ text-transform: uppercase;
1944
+ }
1945
+ .bar-label .bell { animation: bellShake 1.6s ease-in-out infinite; }
1946
+ @media (prefers-reduced-motion: reduce) {
1947
+ .bar-label .bell { animation: none; }
1948
+ }
1949
+ @keyframes bellShake {
1950
+ 0%, 80%, 100% { transform: rotate(0deg); }
1951
+ 85% { transform: rotate(12deg); }
1952
+ 90% { transform: rotate(-10deg); }
1953
+ 95% { transform: rotate(6deg); }
1954
+ }
1955
+ .alarm-scroll {
1956
+ display: flex;
1957
+ flex: 1 1 auto;
1958
+ gap: 8px;
1959
+ min-width: 0;
1960
+ overflow-x: auto;
1961
+ scrollbar-width: thin;
1962
+ scrollbar-color: var(--line-strong) transparent;
1963
+ }
1964
+ .alarm-item {
1965
+ display: inline-flex;
1966
+ flex: 0 0 auto;
1967
+ gap: 8px;
1968
+ align-items: center;
1969
+ padding: 4px 10px;
1970
+ font-size: 11.5px;
1971
+ color: var(--ink-soft);
1972
+ background: var(--paper-deep);
1973
+ border: 1px solid color-mix(in srgb, var(--tone-danger-dot) 30%, transparent);
1974
+ border-radius: var(--radius-pill);
1975
+ }
1976
+ .alarm-item b { font-weight: 600; }
1977
+ .alarm-item .dim { opacity: 0.6; }
1978
+ .alarm-item .esc { color: var(--tone-warning-dot); }
1979
+ .alarm-item .ack {
1980
+ padding: 2px 9px;
1981
+ color: var(--paper-raised);
1982
+ background: var(--tone-danger-dot);
1983
+ border-color: transparent;
1984
+ }
1985
+ .alarm-item .ack:hover { opacity: 0.85; border-color: transparent; color: var(--paper-raised); }
1986
+
1987
+ /* ---------- 添加节点向导 ---------- */
1988
+ .ctrl-right { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; min-width: 0; }
1989
+ .add-btn { padding: 8px 16px; font-size: 13px; white-space: nowrap; }
1990
+ .modal-mask {
1991
+ position: fixed;
1992
+ z-index: 50;
1993
+ inset: 0;
1994
+ display: flex;
1995
+ align-items: center;
1996
+ justify-content: center;
1997
+ background: var(--scrim);
1998
+ backdrop-filter: blur(2px);
1999
+ }
2000
+ .modal {
2001
+ width: 640px;
2002
+ max-width: 94vw;
2003
+ max-height: 88vh;
2004
+ overflow-y: auto;
2005
+ padding: 22px 24px;
2006
+ background: var(--surface-glass-strong);
2007
+ backdrop-filter: var(--aurora-blur);
2008
+ border: 1px solid var(--glass-line);
2009
+ border-radius: var(--radius-panel);
2010
+ box-shadow: var(--glass-edge), var(--shadow-float);
2011
+ }
2012
+ .m-title { margin: 0 0 14px; font-size: 17px; }
2013
+ .seg-row { display: flex; gap: 8px; margin-bottom: 14px; }
2014
+ .seg {
2015
+ flex: 1;
2016
+ padding: 8px 0;
2017
+ font-size: 13px;
2018
+ cursor: pointer;
2019
+ color: var(--ink-faint);
2020
+ background: var(--paper-deep);
2021
+ border: 1px solid var(--line);
2022
+ border-radius: var(--radius-chip);
2023
+ }
2024
+ .seg.on {
2025
+ font-weight: 600;
2026
+ color: var(--on-accent);
2027
+ background: var(--accent);
2028
+ border-color: var(--accent);
2029
+ }
2030
+ .f-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
2031
+ .driver-form { grid-template-columns: repeat(3, 1fr); }
2032
+ .cal-form { grid-template-columns: 2fr 1fr; align-items: end; }
2033
+ .f { display: flex; flex-direction: column; gap: 4px; font-size: 11.5px; color: var(--ink-faint); }
2034
+ .f em { margin-left: 3px; font-style: normal; color: var(--tone-danger-dot); }
2035
+ .inp {
2036
+ width: 100%;
2037
+ padding: 6px 9px;
2038
+ font-size: 12.5px;
2039
+ color: var(--ink);
2040
+ background: var(--paper-deep);
2041
+ border: 1px solid var(--line-strong);
2042
+ border-radius: var(--radius-chip);
2043
+ }
2044
+ .hint { font-size: 10px; color: var(--ink-faint); }
2045
+ .test-row { display: flex; gap: 10px; align-items: center; margin: 6px 0 4px; }
2046
+ .test-result { font-family: var(--font-mono); font-size: 11px; }
2047
+ .test-result.ok { color: var(--tone-success-dot); }
2048
+ .test-result.bad { color: var(--tone-danger-dot); }
2049
+ .m-err { margin: 8px 0 0; font-size: 12px; color: var(--tone-danger-dot); }
2050
+ .m-actions { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; }
2051
+ .aw-pill.outline { color: var(--ink); background: var(--paper-raised); border-color: var(--line-strong); }
2052
+
2053
+ /* ---------- 模板管理 ---------- */
2054
+ .sec-label {
2055
+ margin: 14px 0 6px;
2056
+ font-size: 11px;
2057
+ font-weight: 600;
2058
+ letter-spacing: 0.06em;
2059
+ text-transform: uppercase;
2060
+ color: var(--ink-faint);
2061
+ }
2062
+ .tpl-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
2063
+ .tpl-table td { padding: 7px 8px; border-bottom: 1px solid var(--divider-hair); }
2064
+ .tpl-table td b { margin-right: 8px; }
2065
+ .tpl-table .range { font-size: 11.5px; color: var(--ink-soft); }
2066
+ .tpl-table .empty { color: var(--ink-faint); font-size: 12px; text-align: center; padding: 14px 0; }
2067
+ .tpl-table .actions { white-space: nowrap; }
2068
+
2069
+ /* ---------- 节点筛选工具条 ---------- */
2070
+ .tbl-toolbar {
2071
+ display: flex;
2072
+ flex-wrap: wrap;
2073
+ gap: 10px;
2074
+ align-items: flex-end;
2075
+ justify-content: space-between;
2076
+ padding: 11px 14px;
2077
+ border-bottom: 1px solid var(--divider-hair);
2078
+ }
2079
+ .filters { display: flex; flex-wrap: wrap; gap: 9px; align-items: flex-end; }
2080
+ .flt {
2081
+ display: flex;
2082
+ flex-direction: column;
2083
+ gap: 3px;
2084
+ font-size: 10px;
2085
+ letter-spacing: 0.05em;
2086
+ text-transform: uppercase;
2087
+ color: var(--ink-faint);
2088
+ }
2089
+ .inp-sel {
2090
+ min-width: 112px;
2091
+ padding: 4px 8px;
2092
+ font-size: 11.5px;
2093
+ color: var(--ink);
2094
+ background: var(--paper-deep);
2095
+ border: 1px solid var(--line-strong);
2096
+ border-radius: var(--radius-chip);
2097
+ transition: border-color 0.15s;
2098
+ }
2099
+ .inp-sel:focus { outline: none; border-color: var(--accent); }
2100
+ .clear-btn { margin-bottom: 1px; }
2101
+ /* 名称/ID 搜索:与下拉同族,宽度独立(复合筛选的最后一个自由维度) */
2102
+ .flt-search {
2103
+ width: 168px;
2104
+ min-width: 0;
2105
+ }
2106
+ .flt-search::placeholder { color: var(--ink-faint); }
2107
+ .count { padding-bottom: 4px; font-size: 11px; color: var(--ink-faint); }
2108
+
2109
+ /* ── 行内产线/绑定/运行态样式见 DaqNodeRow.vue scoped 块 ── */
2110
+
2111
+ /* ── Agent 优化记录面板(调控闭环;与全站玻璃语言同族) ── */
2112
+ .opt-card { padding: 0; }
2113
+ .opt-head {
2114
+ display: flex;
2115
+ gap: 9px;
2116
+ align-items: center;
2117
+ width: 100%;
2118
+ padding: 12px 16px;
2119
+ font-size: 13.5px;
2120
+ color: var(--ink);
2121
+ background: transparent;
2122
+ border: 0;
2123
+ cursor: pointer;
2124
+ }
2125
+ .opt-head > .i-tabler-flask { font-size: 16px; color: var(--tone-info-dot); }
2126
+ .opt-count {
2127
+ padding: 1px 8px;
2128
+ font-size: 10.5px;
2129
+ color: var(--tone-info-dot);
2130
+ background: var(--tone-info-bg);
2131
+ border-radius: 999px;
2132
+ }
2133
+ .opt-chevron {
2134
+ width: 7px;
2135
+ height: 7px;
2136
+ margin-left: auto;
2137
+ border-right: 1.5px solid var(--ink-faint);
2138
+ border-bottom: 1.5px solid var(--ink-faint);
2139
+ transform: rotate(45deg);
2140
+ transition: transform var(--transition-base);
2141
+ }
2142
+ .opt-chevron.open { transform: rotate(225deg); }
2143
+ /* 手风琴展开/收起(occasional 频次;状态指示;height 豁免 = 手风琴;进出对称 260ms;
2144
+ * 收起态 visibility 联动,内部链接/按钮不可聚焦) */
2145
+ .opt-body {
2146
+ display: flex;
2147
+ flex-direction: column;
2148
+ gap: 10px;
2149
+ max-height: 0;
2150
+ padding: 0 16px;
2151
+ overflow: hidden;
2152
+ visibility: hidden;
2153
+ opacity: 0;
2154
+ transition:
2155
+ max-height 260ms cubic-bezier(0.22, 0.68, 0.36, 1),
2156
+ opacity 200ms ease,
2157
+ visibility 260ms;
2158
+ }
2159
+ .opt-body.open {
2160
+ max-height: 64vh;
2161
+ padding-bottom: 14px;
2162
+ overflow-y: auto;
2163
+ visibility: visible;
2164
+ opacity: 1;
2165
+ }
2166
+ .opt-filters { display: flex; gap: 8px; align-items: center; }
2167
+ .opt-empty { font-size: 11.5px; color: var(--ink-faint); }
2168
+ .opt-list { display: flex; flex-direction: column; gap: 7px; margin: 0; padding: 0; list-style: none; }
2169
+ .opt-row {
2170
+ display: flex;
2171
+ flex-direction: column;
2172
+ gap: 5px;
2173
+ padding: 9px 12px;
2174
+ background: var(--frost-bg);
2175
+ border: 1px solid var(--glass-line);
2176
+ border-radius: 10px;
2177
+ }
2178
+ .opt-row.open { border-color: color-mix(in srgb, var(--tone-warning-dot) 36%, var(--glass-line)); }
2179
+ .opt-row.rolled-back { border-color: color-mix(in srgb, var(--tone-danger-dot) 30%, var(--glass-line)); }
2180
+ .opt-line1 { display: flex; gap: 9px; align-items: center; min-width: 0; }
2181
+ .opt-status {
2182
+ flex: none;
2183
+ padding: 1px 8px;
2184
+ font-family: var(--font-mono);
2185
+ font-size: 9.5px;
2186
+ color: var(--ink-faint);
2187
+ border: 1px solid var(--line-strong);
2188
+ border-radius: 5px;
2189
+ }
2190
+ .opt-status.open { color: var(--tone-warning-dot); border-color: color-mix(in srgb, var(--tone-warning-dot) 45%, transparent); }
2191
+ .opt-status.judged-keep { color: var(--tone-success-dot); border-color: color-mix(in srgb, var(--tone-success-dot) 45%, transparent); }
2192
+ .opt-status.rolled-back { color: var(--tone-danger-dot); border-color: color-mix(in srgb, var(--tone-danger-dot) 45%, transparent); }
2193
+ .opt-node { overflow: hidden; font-size: 12.5px; color: var(--ink); text-overflow: ellipsis; white-space: nowrap; }
2194
+ .opt-params { flex: none; font-size: 11px; color: var(--tone-info-dot); }
2195
+ .opt-time { flex: none; margin-left: auto; font-size: 10px; color: var(--ink-faint); }
2196
+ .mini-act {
2197
+ flex: none;
2198
+ padding: 2px 9px;
2199
+ font-size: 10.5px;
2200
+ color: var(--tone-info-dot);
2201
+ background: var(--tone-info-bg);
2202
+ border: 0;
2203
+ border-radius: 6px;
2204
+ cursor: pointer;
2205
+ }
2206
+ .mini-act.danger { color: var(--tone-danger-dot); background: var(--tone-danger-bg); }
2207
+ .mini-act:disabled { opacity: 0.5; cursor: default; }
2208
+ .opt-line2 { display: flex; flex-wrap: wrap; gap: 8px; font-size: 10.5px; color: var(--ink-faint); }
2209
+ .opt-judge.rollback, .opt-judge.keep, .opt-judge.uncertain { color: var(--ink-soft); }
2210
+ .opt-judge b { font-weight: 600; }
2211
+ .opt-open-hint { color: var(--tone-warning-dot); }
2212
+ .opt-agg { color: var(--ink-faint); }
2213
+ .opt-series { display: flex; flex-wrap: wrap; gap: 10px; padding-top: 4px; }
2214
+ .opt-ch { display: flex; flex: 1 1 180px; gap: 8px; align-items: center; min-width: 160px; }
2215
+ .opt-spark { width: 120px; height: 30px; }
2216
+ .opt-spark polyline {
2217
+ fill: none;
2218
+ stroke: var(--tone-info-dot);
2219
+ stroke-width: 1.5;
2220
+ }
2221
+
2222
+ /* ══════════════════════════════════════════════════════════════════════════
2223
+ 页面级自适应加固(375 / 768 / 1440 三档)
2224
+ 根因统一:flex 子项被压缩到 min-content → 中文按钮退化成「单列竖排」,
2225
+ 左右两组控件抢同一行 → 右边缘越界。策略:先禁止压缩(nowrap),
2226
+ 再在窄屏把控制条改成「整行 / 两列」分层排布;筛选区单列堆叠 → 两列网格。
2227
+ ══════════════════════════════════════════════════════════════════════════ */
2228
+ /* —— 标签字号地板(全档位,不只是窄屏)——
2229
+ 本页所有自有小标签原为 9.5–11px,低于「标签 ≥11.5px」验收线;统一抬到 11.5px。
2230
+ 注:节点表行内 10/10.5px 徽标在 app/components/workshop/DaqNodeRow.vue,不在本页职责内。 —— */
2231
+ .badge, .sec-label, .flt, .hint, .strip-label, .strip-label small,
2232
+ .line-pill small, .opt-status, .opt-time, .opt-line2, .mini-act,
2233
+ .src-badge, .kind-chip, .evt-row .t, .opt-count,
2234
+ .infra-banner .txt small { font-size: 11.5px; }
2235
+ .sub { font-size: 13px; }
2236
+ .alarm-item small, .alarm-row small { font-size: 11.5px; }
2237
+
2238
+ @media (max-width: 900px) {
2239
+ /* —— 兜底字号地板:本页任何 small/mono 小字在手持档不得低于 11.5px ——
2240
+ 逐类点名容易漏(实测 infra-banner 的配方名、line-pill 的 mono 小字都漏过),
2241
+ 这里用元素级兜底把整页收口;它放在本媒体块的**最后面**,
2242
+ 同特异度时后写者胜,所以不会破坏上面更细的排版意图。 */
2243
+ .page small,
2244
+ .page small.mono,
2245
+ .page small.mono b {
2246
+ font-size: 11.5px;
2247
+ }
2248
+
2249
+ /* —— 触摸目标:报警「确认」在窄屏只有 44×22,手指点不中 —— */
2250
+ .alarm-item { padding: 6px 10px; }
2251
+ .alarm-item .ack, .mini-btn.ack { min-height: 32px; padding: 4px 12px; white-space: nowrap; }
2252
+ .alarm-row { flex-wrap: wrap; }
2253
+
2254
+ /* —— 窄屏字号地板(正文 ≥12.5px / 标签 ≥11.5px)——
2255
+ 注:侧栏 8.5px/9.5px 属全局层(main.css / AppSidebar.vue),不在本页职责内 —— */
2256
+ .flt, .sec-label, .hint { font-size: 11.5px; }
2257
+ .inp-sel { font-size: 12.5px; }
2258
+ .badge, .strip-label, .line-pill small { font-size: 11.5px; }
2259
+ .ctrl-metrics { font-size: 12px; }
2260
+ .evt-row { font-size: 12.5px; }
2261
+ .evt-row .t, .src-badge, .kind-chip { font-size: 11.5px; }
2262
+ .panel-head { font-size: 13px; }
2263
+ .alarm-item, .alarm-item small, .alarm-row small, .opt-status, .mini-act,
2264
+ .opt-time, .opt-line2, .clear-btn, .opt-count, .strip-label small { font-size: 11.5px; }
2265
+ .tpl-table .range, .opt-empty, .panel-empty { font-size: 12px; }
2266
+ .infra-banner .txt small { font-size: 11.5px; }
2267
+ .m-title { font-size: 16px; }
2268
+ .sub { font-size: 13px; }
2269
+ }
2270
+
2271
+ @media (max-width: 700px) {
2272
+ /* —— 控制器总控条:暂停全部采集 整行;两个周期项两列;两个 CTA 两列 —— */
2273
+ .ctrl-card { padding: 12px 14px 11px; }
2274
+ .ctrl-row { flex-direction: column; align-items: stretch; gap: 12px; }
2275
+ .ctrl-left { gap: 10px; }
2276
+ .ctrl-left > .aw-pill { flex: 1 1 100%; }
2277
+ .ctrl-left > .cycle { flex: 1 1 calc(50% - 5px); justify-content: space-between; }
2278
+ .cycle input { width: 62px; }
2279
+ .ctrl-right { gap: 10px; }
2280
+ .ctrl-metrics { flex: 1 1 100%; }
2281
+ .ctrl-right > .add-btn { flex: 1 1 calc(50% - 5px); }
2282
+
2283
+ /* —— 未确认报警横条:标题独占一行,滚动轨吃满整宽(右侧内容不再被挤掉) —— */
2284
+ .alarm-bar { flex-direction: column; align-items: stretch; gap: 8px; padding: 10px 12px; }
2285
+ .bar-label { justify-content: flex-start; }
2286
+ .alarm-scroll { width: 100%; padding-bottom: 3px; }
2287
+ .alarm-item { max-width: 100%; }
2288
+
2289
+ /* —— 节点筛选工具条:单列下拉堆叠 → 两列网格 —— */
2290
+ .tbl-toolbar { flex-direction: column; align-items: stretch; gap: 8px; padding: 11px 12px; }
2291
+ .filters { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px; }
2292
+ .filters > .flt { min-width: 0; }
2293
+ .inp-sel { width: 100%; min-width: 0; }
2294
+ .flt-search { width: 100%; }
2295
+ .clear-btn { grid-column: 1 / -1; min-height: 34px; }
2296
+ .count { padding-bottom: 0; }
2297
+
2298
+ /* —— Agent 优化记录:筛选区两列网格;记录行允许换行,长节点名不再被切 —— */
2299
+ .opt-filters { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
2300
+ .opt-filters > .mini-act { grid-column: 1 / -1; min-height: 34px; }
2301
+ .opt-line1 { flex-wrap: wrap; }
2302
+ .opt-node { flex: 1 1 100%; overflow: visible; white-space: normal; }
2303
+ .opt-time { margin-left: 0; }
2304
+ .opt-ch { flex: 1 1 100%; }
2305
+ .opt-spark { flex: 1 1 auto; width: auto; min-width: 96px; }
2306
+
2307
+ /* —— 主表:压缩单元格内边距,缩短横向滚动距离(表格横滚 + 首列 sticky 是全局层设计) —— */
2308
+ .nodes-table th, .nodes-table :deep(td) { padding: 7px 8px; }
2309
+ .table-card { -webkit-overflow-scrolling: touch; }
2310
+ .opt-body.open { max-height: 70vh; }
2311
+
2312
+ /* —— 实时事件行:摘要原来只剩 141px 被省略号切掉 → 换行后独占整宽 —— */
2313
+ .evt-row { flex-wrap: wrap; }
2314
+ .evt-row .txt { flex: 1 1 100%; white-space: normal; overflow: visible; }
2315
+ }
2316
+ </style>