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,981 +1,987 @@
1
- <script setup lang="ts">
2
- /**
3
- * 仪表盘(/)—— 产线运营数字大屏。
4
- * ECharts 可视化:实时工况趋势(量程归一化)/ 产线运行状态 / 数采管线吞吐 /
5
- * 写控制成功率 / 节点状态分布 + 产线清单卡。
6
- * 数据权威在 server:useDcwStream + useDaqStream(REST 基线 + WS 实时收敛
7
- * + 5s 低频兜底刷新);趋势缓冲为本页每 5s 一次的量程归一化快照。
8
- */
9
- import { useDaqStream } from '@/app/composables/workshop/useDaqStream'
10
- import { useDcwStream } from '@/app/composables/workshop/useDcwStream'
11
- import { useVisibleInterval } from '@/app/composables/workshop/useVisibleInterval'
12
- import { useWorkshopApi, type HarnessMetaDto } from '@/app/composables/workshop/useWorkshopApi'
13
- import AwChart from '@/app/components/AwChart.vue'
14
- import type { EChartsOption } from 'echarts'
15
-
16
- const { t } = useI18n()
17
- // 注意:useHead 的函数 title 在 prod SSR 下立即求值,必须在 useI18n 之后注册
18
- useHead({ title: () => t('titles.dashboard') })
19
-
20
- const site = useSiteConfig()
21
- const store = useAppStore()
22
- const daq = useDaqStream()
23
- const dcw = useDcwStream()
24
-
25
- // 图表色序随主题切换(两套声部,同一语义序):
26
- // 亮 = Warm Editorial 编辑色板(main.css --chart-* 同源,墨绿→苔绿→琥珀→陶赭)
27
- // 暗 = 控制室 tone 系统(绿=运行/成功 · 青=数据 · 琥珀=需关注 · 紫=重试 · 红=告警)
28
- // —— 亮阶把品牌绿当"墨"用,暗阶把同一抹绿当"信号"用,各自在自己的画布上才成立。
29
- const PAL = computed(() => (store.isDark
30
- ? { accent: '#3fe4ab', cyan: '#41c8f4', amber: '#f6c453', violet: '#a795ff', danger: '#ff8080' }
31
- : { accent: '#4a6b57', cyan: '#6f8296', amber: '#c9a26a', violet: '#b3714f', danger: '#c25a4e' }))
32
-
33
- // ---------- 数据装载(WS 实时 + 5s 兜底;可见性调度:后台降频 30s,回前台立即补拍) ----------
34
- onMounted(() => {
35
- daq.ensureWsFeed()
36
- dcw.ensureWsFeed()
37
- void Promise.all([daq.load(), dcw.load()]).then(() => pushTrend())
38
- void loadHarnesses()
39
- useVisibleInterval(() => {
40
- void daq.load()
41
- void dcw.load()
42
- void loadHarnesses()
43
- pushTrend()
44
- }, 5000, { bgMs: 30000 })
45
- })
46
-
47
- // ---------- Harness 可用性(引擎 CLI 环境探测;服务端 30s 探测缓存,随兜底节拍刷新) ----------
48
- const api = useWorkshopApi()
49
- const harnesses = ref<HarnessMetaDto[]>([])
50
-
51
- /**
52
- * 引擎卡片第二行的命令名。
53
- * 返回空串表示"这一行没有新信息":标签已经以命令名开头时
54
- * (omp(真实 LLM) / omp、opencode / opencode、codex / codex…),
55
- * 再写一遍只是把同一件事讲两遍 —— 实测 14 张卡里有 9 张是重复的。
56
- * 真正有信息量的只有:不可用状态、in-process、以及与标签不同的真实二进制名。
57
- */
58
- const harnessCmd = (h: HarnessMetaDto): string => {
59
- if (h.available === false) return t('home.harness.missing')
60
- if (h.inprocess) return 'in-process'
61
- const cmd = String(h.command ?? '')
62
- if (!cmd) return ''
63
- return h.label.startsWith(cmd) ? '' : cmd
64
- }
65
- const loadHarnesses = async (): Promise<void> => {
66
- try {
67
- const res = await api.listHarnesses()
68
- harnesses.value = (res as unknown as { data?: { harnesses?: HarnessMetaDto[] } })?.data?.harnesses ?? []
69
- }
70
- catch { /* 探测不可得时面板留空,不阻塞大屏 */ }
71
- }
72
- const harnessOk = computed(() => harnesses.value.filter(h => h.available !== false).length)
73
-
74
- // ---------- KPI ----------
75
- const linesActive = computed(() => dcw.lines.filter(l => dcw.lineStateOf(l.id).active))
76
- const daqOnline = computed(() => daq.nodes.filter(n => n.enabled && n.state !== 'offline').length)
77
- const daqTotal = computed(() => daq.nodes.length)
78
- const alarmCount = computed(() => daq.nodes.filter(n => n.state === 'alarm').length)
79
- const writeRate = computed(() => {
80
- const total = dcw.controller.writesTotal
81
- if (total === 0) return 100
82
- return Math.round(((total - dcw.controller.writesFailed) / total) * 1000) / 10
83
- })
84
-
85
- // ---------- 趋势缓冲(近 3 分钟,5s 一拍,量程归一化) ----------
86
- interface TrendPoint { t: number, m: Record<string, number | null> }
87
- const trendBuf = ref<TrendPoint[]>([])
88
- /** 趋势通道:有实时值的节点优先(在线优先),稳定取前 4 */
89
- const trendNodes = computed(() => daq.nodes
90
- .filter(n => n.value != null && n.max > n.min)
91
- .sort((a, b) => (a.lineId ? 0 : 1) - (b.lineId ? 0 : 1) || a.id.localeCompare(b.id))
92
- .slice(0, 4))
93
-
94
- function pushTrend(): void {
95
- const m: Record<string, number | null> = {}
96
- for (const n of trendNodes.value) {
97
- m[n.id] = n.value == null ? null : Math.round(((n.value - n.min) / (n.max - n.min)) * 100)
98
- }
99
- trendBuf.value.push({ t: Date.now(), m: m as Record<string, number | null> })
100
- if (trendBuf.value.length > 36) trendBuf.value.shift()
101
- }
102
-
103
- // ---------- 主题感知的图表公共色 ----------
104
- const inkC = computed(() => (store.isDark ? '#e8eef8' : '#1f2a3a'))
105
- const dimC = computed(() => (store.isDark ? 'rgba(143,160,181,0.85)' : 'rgba(80,95,120,0.85)'))
106
- const splitC = computed(() => (store.isDark ? 'rgba(143,160,181,0.13)' : 'rgba(80,95,120,0.14)'))
107
- const tipBg = computed(() => (store.isDark ? 'rgba(10,16,28,0.94)' : 'rgba(255,255,255,0.97)'))
108
-
109
- const baseTooltip = computed(() => ({
110
- backgroundColor: tipBg.value,
111
- borderColor: splitC.value,
112
- textStyle: { color: inkC.value, fontSize: 11 },
113
- }))
114
-
115
- // ---------- 图 1:实时工况趋势(多通道量程归一化) ----------
116
- /** 趋势是否真有数值可画(采样被活动批次门控:未开跑时缓冲里只有 null 占位) */
117
- const trendHasData = computed(() =>
118
- trendNodes.value.length > 0
119
- && trendBuf.value.some(p => trendNodes.value.some(n => typeof p.m[n.id] === 'number')))
120
-
121
- const trendOpt = computed<EChartsOption>(() => ({
122
- backgroundColor: 'transparent',
123
- tooltip: { trigger: 'axis', ...baseTooltip.value },
124
- legend: {
125
- top: 0, right: 4, icon: 'roundRect', itemWidth: 10, itemHeight: 4,
126
- textStyle: { color: dimC.value, fontSize: 10.5 },
127
- },
128
- grid: { left: 42, right: 14, top: 30, bottom: 24 },
129
- xAxis: {
130
- type: 'time',
131
- axisLabel: { color: dimC.value, fontSize: 10, formatter: '{HH}:{mm}:{ss}' },
132
- axisLine: { lineStyle: { color: splitC.value } },
133
- splitLine: { show: false },
134
- },
135
- yAxis: {
136
- // 轴名移到面板头 meta:贴在轴端会和刻度数字挤在一起(见 aw-bench-meta)
137
- type: 'value', min: 0, max: 100,
138
- axisLabel: { color: dimC.value, fontSize: 10 },
139
- splitLine: { lineStyle: { color: splitC.value } },
140
- },
141
- series: trendNodes.value.map((n, i) => {
142
- const color = [PAL.value.accent, PAL.value.cyan, PAL.value.amber, PAL.value.violet][i % 4]
143
- return {
144
- name: n.name,
145
- type: 'line',
146
- smooth: true,
147
- showSymbol: false,
148
- connectNulls: true,
149
- lineStyle: { width: 1.6, color },
150
- itemStyle: { color },
151
- data: trendBuf.value.map(p => [p.t, p.m[n.id] ?? null]),
152
- }
153
- }),
154
- }))
155
-
156
- // ---------- 图 2:产线运行状态(donut) ----------
157
- const lineStateOpt = computed<EChartsOption>(() => ({
158
- backgroundColor: 'transparent',
159
- tooltip: { trigger: 'item', ...baseTooltip.value },
160
- legend: {
161
- bottom: 0, left: 'center', icon: 'roundRect', itemWidth: 10, itemHeight: 4,
162
- textStyle: { color: dimC.value, fontSize: 10.5 },
163
- },
164
- series: [{
165
- type: 'pie',
166
- radius: ['62%', '82%'],
167
- center: ['50%', '44%'],
168
- label: { show: false },
169
- silent: false,
170
- data: [
171
- { value: linesActive.value.length, name: t('home.runNow'), itemStyle: { color: PAL.value.accent } },
172
- { value: Math.max(dcw.lines.length - linesActive.value.length, 0), name: t('home.standBy'), itemStyle: { color: splitC.value } },
173
- ],
174
- }],
175
- }))
176
-
177
- // ---------- 图 3:数采管线吞吐(累计) ----------
178
- const pipelineOpt = computed<EChartsOption>(() => ({
179
- backgroundColor: 'transparent',
180
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, ...baseTooltip.value },
181
- grid: { left: 70, right: 34, top: 10, bottom: 24 },
182
- xAxis: {
183
- type: 'value',
184
- axisLabel: { color: dimC.value, fontSize: 10, formatter: (v: number) => v >= 1000 ? `${Math.round(v / 1000)}k` : String(v) },
185
- splitLine: { lineStyle: { color: splitC.value } },
186
- },
187
- yAxis: {
188
- type: 'category',
189
- data: [t('home.pipelineSeries.stored'), t('home.pipelineSeries.consumed'), t('home.pipelineSeries.produced')],
190
- axisLabel: { color: dimC.value, fontSize: 10.5 },
191
- axisLine: { lineStyle: { color: splitC.value } },
192
- },
193
- series: [{
194
- type: 'bar',
195
- barWidth: 10,
196
- data: [
197
- { value: daq.meta.samplesStored ?? 0, itemStyle: { color: PAL.value.amber } },
198
- { value: daq.meta.consumed ?? 0, itemStyle: { color: PAL.value.cyan } },
199
- { value: daq.meta.produced ?? 0, itemStyle: { color: PAL.value.accent } },
200
- ],
201
- itemStyle: { borderRadius: [0, 5, 5, 0] },
202
- }],
203
- }))
204
-
205
- // ---------- 图 4:写控制成功率(gauge) ----------
206
- const writeOpt = computed<EChartsOption>(() => ({
207
- backgroundColor: 'transparent',
208
- series: [{
209
- type: 'gauge',
210
- startAngle: 210,
211
- endAngle: -30,
212
- min: 0,
213
- max: 100,
214
- radius: '92%',
215
- center: ['50%', '58%'],
216
- progress: { show: true, width: 12, itemStyle: { color: PAL.value.accent } },
217
- axisLine: { lineStyle: { width: 12, color: [[1, splitC.value]] } },
218
- axisTick: { show: false },
219
- splitLine: { show: false },
220
- axisLabel: { show: false },
221
- pointer: { show: false },
222
- anchor: { show: false },
223
- title: { show: false },
224
- detail: {
225
- valueAnimation: true,
226
- formatter: '{value}%',
227
- color: inkC.value,
228
- fontSize: 26,
229
- fontFamily: 'monospace',
230
- offsetCenter: [0, '4%'],
231
- },
232
- data: [{ value: writeRate.value }],
233
- }],
234
- }))
235
-
236
- // ---------- 图 5:节点状态分布(数采/控制 堆叠) ----------
237
- const nodeStates = computed(() => [
238
- { key: 'ok', color: PAL.value.accent },
239
- { key: 'warn', color: PAL.value.amber },
240
- { key: 'alarm', color: PAL.value.danger },
241
- { key: 'writing', color: PAL.value.cyan },
242
- { key: 'idle', color: store.isDark ? '#5f6e84' : '#8fa0b5' },
243
- { key: 'error', color: '#ff8a5c' },
244
- { key: 'offline', color: store.isDark ? 'rgba(95,110,132,0.38)' : 'rgba(143,160,181,0.35)' },
245
- ])
246
-
247
- const nodeStateOpt = computed<EChartsOption>(() => {
248
- const daqCount = new Map<string, number>()
249
- for (const n of daq.nodes) daqCount.set(n.state, (daqCount.get(n.state) ?? 0) + 1)
250
- const dcwCount = new Map<string, number>()
251
- for (const n of dcw.nodes) dcwCount.set(n.state, (dcwCount.get(n.state) ?? 0) + 1)
252
- const states = nodeStates.value.filter(s => daqCount.get(s.key) || dcwCount.get(s.key))
253
- return {
254
- backgroundColor: 'transparent',
255
- tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, ...baseTooltip.value },
256
- legend: {
257
- top: 0, left: 'center', icon: 'roundRect', itemWidth: 10, itemHeight: 4,
258
- textStyle: { color: dimC.value, fontSize: 10 },
259
- },
260
- grid: { left: 56, right: 14, top: 30, bottom: 24 },
261
- xAxis: {
262
- type: 'value',
263
- axisLabel: { color: dimC.value, fontSize: 10 },
264
- splitLine: { lineStyle: { color: splitC.value } },
265
- },
266
- yAxis: {
267
- type: 'category',
268
- data: [t('home.stateDcw'), t('home.stateDaq')],
269
- axisLabel: { color: dimC.value, fontSize: 10.5 },
270
- axisLine: { lineStyle: { color: splitC.value } },
271
- },
272
- series: states.map(s => ({
273
- name: t(`home.states.${s.key}`),
274
- type: 'bar' as const,
275
- stack: 'nodes',
276
- barWidth: 12,
277
- itemStyle: { color: s.color },
278
- data: [dcwCount.get(s.key) ?? 0, daqCount.get(s.key) ?? 0],
279
- })),
280
- }
281
- })
282
-
283
- // ---------- 产线清单(裁策:清单是导航不是数据库导出) ----------
284
- const lineCards = computed(() => dcw.lines.map((l) => {
285
- const st = dcw.lineStateOf(l.id)
286
- return {
287
- id: l.id,
288
- name: l.name,
289
- color: l.color,
290
- active: st.active,
291
- product: st.productName,
292
- recipe: st.recipeName,
293
- runId: st.runId,
294
- tagged: st.taggedSamples,
295
- dcwCount: dcw.nodes.filter(n => n.lineId === l.id).length,
296
- daqCount: daq.nodes.filter(n => n.lineId === l.id).length,
297
- }
298
- }))
299
- /** 运行中优先,其次已打标样本多的(最近活跃),稳定排序 */
300
- const fleetSorted = computed(() => [...lineCards.value].sort((a, b) =>
301
- Number(b.active) - Number(a.active) || b.tagged - a.tagged || a.name.localeCompare(b.name)))
302
- const FLEET_CAP = 8
303
- const fleetShown = computed(() => fleetSorted.value.slice(0, FLEET_CAP))
304
- const fleetOverflow = computed(() => Math.max(lineCards.value.length - FLEET_CAP, 0))
305
- </script>
306
-
307
- <template>
308
- <div class="home">
309
- <!-- Hero:产线运营中枢(校准仪表台母题;右侧 LIVE 实况仪表簇) -->
310
- <section class="hero aw-bench aw-bench--marked aw-stagger">
311
- <div class="hero-main">
312
- <p class="aw-bench-kicker">
313
- {{ t('home.kicker') }} · {{ site.mode }}
314
- </p>
315
- <h1 class="hero-title">
316
- {{ t('home.heroTitle') }}
317
- <span class="aw-serif-accent-italic">{{ t('home.heroAccent') }}</span>
318
- </h1>
319
- <p class="hero-sub">
320
- {{ t('home.heroSub') }}
321
- </p>
322
- <div class="hero-acts">
323
- <button
324
- class="aw-pill im"
325
- @click="navigateTo('/town')"
326
- >
327
- <span class="i-tabler-map-2 im-pop" />
328
- {{ t('home.ctaTown') }}
329
- </button>
330
- <button
331
- class="aw-pill outline im"
332
- @click="navigateTo('/dcw')"
333
- >
334
- <span class="i-tabler-route im-pop" />
335
- {{ t('home.ctaLine') }}
336
- </button>
337
- </div>
338
- </div>
339
-
340
- <!-- 实况仪表簇:LIVE 徽标 + 三条带刻度的读数行(读数比句子更快被扫到) -->
341
- <div class="hero-live">
342
- <span class="live-badge"><span class="live-dot" />{{ t('home.live') }}</span>
343
- <dl class="live-rows">
344
- <div class="live-row">
345
- <dt>{{ t('home.kpi.lines') }}</dt>
346
- <dd class="mono">
347
- <b>{{ linesActive.length }}</b><small>/{{ dcw.lines.length }}</small>
348
- </dd>
349
- </div>
350
- <div class="live-row">
351
- <dt>{{ t('home.kpi.samples') }}</dt>
352
- <dd class="mono">
353
- <b>{{ daq.meta.samplesStored ?? 0 }}</b>
354
- </dd>
355
- </div>
356
- <div
357
- class="live-row"
358
- :class="{ 'is-alarm': alarmCount > 0 }"
359
- >
360
- <dt>{{ t('home.kpi.alarms') }}</dt>
361
- <dd class="mono">
362
- <b>{{ alarmCount }}</b>
363
- </dd>
364
- </div>
365
- </dl>
366
- </div>
367
- <!-- 仪表刻度母题(控制室仪器读数;纯装饰,零动画) -->
368
- <div
369
- class="hero-scale"
370
- aria-hidden="true"
371
- />
372
- </section>
373
-
374
- <!-- 量规统计带:一块仪表盘,不是一排各自为政的卡(底部量程刻度是签名) -->
375
- <div class="aw-gauge-band aw-stagger">
376
- <div class="aw-gauge">
377
- <span class="aw-gauge-label">{{ t('home.kpi.lines') }}</span>
378
- <span class="aw-gauge-value">
379
- <span class="aw-readout">{{ linesActive.length }}<small>/{{ dcw.lines.length }}</small></span>
380
- </span>
381
- </div>
382
- <div class="aw-gauge">
383
- <span class="aw-gauge-label">{{ t('home.kpi.dcwNodes') }}</span>
384
- <span class="aw-gauge-value">
385
- <span class="aw-readout">{{ dcw.controller.nodesOnline }}<small>/{{ dcw.controller.nodesTotal }}</small></span>
386
- </span>
387
- </div>
388
- <div class="aw-gauge">
389
- <span class="aw-gauge-label">{{ t('home.kpi.daqNodes') }}</span>
390
- <span class="aw-gauge-value">
391
- <span class="aw-readout">{{ daqOnline }}<small>/{{ daqTotal }}</small></span>
392
- </span>
393
- </div>
394
- <div class="aw-gauge">
395
- <span class="aw-gauge-label">{{ t('home.kpi.samples') }}</span>
396
- <span class="aw-gauge-value">
397
- <span class="aw-readout">{{ daq.meta.samplesStored ?? 0 }}</span>
398
- </span>
399
- </div>
400
- <div class="aw-gauge">
401
- <span class="aw-gauge-label">{{ t('home.kpi.writeRate') }}</span>
402
- <span class="aw-gauge-value">
403
- <span class="aw-readout">{{ writeRate }}<small>%</small></span>
404
- </span>
405
- </div>
406
- <div
407
- class="aw-gauge"
408
- :class="{ 'is-alarm': alarmCount > 0 }"
409
- >
410
- <span class="aw-gauge-label">{{ t('home.kpi.alarms') }}</span>
411
- <span class="aw-gauge-value">
412
- <span class="aw-readout">{{ alarmCount }}</span>
413
- </span>
414
- </div>
415
- </div>
416
-
417
- <!-- 大屏图阵 -->
418
- <!-- 执行引擎可用性(Harness CLI 环境探测;未安装不可选,派工前强校验) -->
419
- <section
420
- v-if="harnesses.length > 0"
421
- class="harness aw-bench aw-stagger"
422
- >
423
- <header class="aw-bench-hd">
424
- <h3 class="aw-bench-title">
425
- {{ t('home.harness.title') }}
426
- </h3>
427
- <small class="aw-bench-sub">{{ t('home.harness.sub') }}</small>
428
- <span class="aw-bench-meta">
429
- <span><b>{{ harnessOk }}</b>/{{ harnesses.length }}</span>
430
- </span>
431
- </header>
432
- <div class="harness-row">
433
- <a
434
- v-for="h in harnesses"
435
- :key="h.id"
436
- class="h-item"
437
- :class="{ off: h.available === false, link: h.available === false && h.homepage }"
438
- :href="h.available === false && h.homepage ? h.homepage : undefined"
439
- target="_blank"
440
- rel="noopener"
441
- :title="h.available === false ? (h.error ?? '') : (h.resolvedPath ?? h.command ?? '')"
442
- >
443
- <span class="h-dot" />
444
- <span class="h-name">{{ h.label }}</span>
445
- <span
446
- v-if="harnessCmd(h)"
447
- class="h-cmd mono"
448
- >{{ harnessCmd(h) }}</span>
449
- <span
450
- v-if="h.available === false && h.homepage"
451
- class="h-go"
452
- >↗</span>
453
- </a>
454
- </div>
455
- </section>
456
-
457
- <div class="grid aw-stagger">
458
- <section class="aw-bench span8">
459
- <header class="aw-bench-hd">
460
- <h3 class="aw-bench-title">
461
- {{ t('home.charts.trend') }}
462
- </h3>
463
- <small class="aw-bench-sub">{{ t('home.charts.trendSub') }}</small>
464
- <span class="aw-bench-meta">
465
- <span>{{ t('home.trendY') }}</span>
466
- </span>
467
- </header>
468
- <ClientOnly>
469
- <!-- 空态判据必须看「有没有**数值**」,而不是「缓冲里有没有点」:
470
- 采样由活动批次门控(无开跑产线时不采样),此时缓冲里可能有携带 null 的
471
- 占位点 —— 旧写法据此判为"有数据",于是跳过 graphic 提示,
472
- 渲染出一张只有坐标系+图例、X 轴回落到 00:00~24:00 的空图。
473
- 那比一句明确的空态更难理解(实测被评审直接判为"趋势面全是空的")。 -->
474
- <AwChart
475
- v-if="trendHasData"
476
- :option="trendOpt"
477
- class="chart h280"
478
- />
479
- <div
480
- v-else
481
- class="aw-empty chart h280"
482
- >
483
- <p class="aw-empty-title">
484
- {{ t('home.trendWaiting') }}
485
- </p>
486
- <p class="aw-empty-sub">
487
- {{ t('home.trendNeedRun') }}
488
- </p>
489
- </div>
490
- </ClientOnly>
491
- </section>
492
- <section class="aw-bench span4">
493
- <header class="aw-bench-hd">
494
- <h3 class="aw-bench-title">
495
- {{ t('home.charts.lineState') }}
496
- </h3>
497
- <span class="aw-bench-meta">
498
- <span><b>{{ linesActive.length }}</b>/{{ dcw.lines.length }} {{ t('home.kpi.lines') }}</span>
499
- </span>
500
- </header>
501
- <div class="donut-wrap">
502
- <div class="donut-center mono">
503
- <b>{{ linesActive.length }}</b>
504
- <small>/ {{ dcw.lines.length }}</small>
505
- </div>
506
- <ClientOnly>
507
- <AwChart
508
- :option="lineStateOpt"
509
- class="chart h240"
510
- />
511
- </ClientOnly>
512
- </div>
513
- </section>
514
- <section class="aw-bench span4">
515
- <header class="aw-bench-hd">
516
- <h3 class="aw-bench-title">
517
- {{ t('home.charts.pipeline') }}
518
- </h3>
519
- </header>
520
- <ClientOnly>
521
- <AwChart
522
- :option="pipelineOpt"
523
- class="chart h240"
524
- />
525
- </ClientOnly>
526
- </section>
527
- <section class="aw-bench span4">
528
- <header class="aw-bench-hd">
529
- <h3 class="aw-bench-title">
530
- {{ t('home.charts.writeCtl') }}
531
- </h3>
532
- <span class="aw-bench-meta">
533
- <span><b>{{ dcw.controller.writesTotal }}</b> {{ t('home.writesFailed') }} {{ dcw.controller.writesFailed }}</span>
534
- </span>
535
- </header>
536
- <ClientOnly>
537
- <AwChart
538
- :option="writeOpt"
539
- class="chart h240"
540
- />
541
- </ClientOnly>
542
- </section>
543
- <section class="aw-bench span4">
544
- <header class="aw-bench-hd">
545
- <h3 class="aw-bench-title">
546
- {{ t('home.charts.nodeState') }}
547
- </h3>
548
- </header>
549
- <ClientOnly>
550
- <AwChart
551
- :option="nodeStateOpt"
552
- class="chart h240"
553
- />
554
- </ClientOnly>
555
- </section>
556
- </div>
557
-
558
- <!-- 产线清单(运行中优先,最多 8 卡;余量聚合入口) -->
559
- <section class="fleet aw-bench aw-stagger">
560
- <header class="aw-bench-hd">
561
- <h3 class="aw-bench-title">
562
- {{ t('home.charts.lines') }}
563
- </h3>
564
- <small class="aw-bench-sub">{{ t('home.fleetHint') }}</small>
565
- <span class="aw-bench-meta">
566
- <span><b>{{ dcw.lines.length }}</b></span>
567
- </span>
568
- </header>
569
- <div class="fleet-grid">
570
- <NuxtLink
571
- v-for="l in fleetShown"
572
- :key="l.id"
573
- class="line-card"
574
- :class="{ on: l.active }"
575
- :style="{ '--lc': l.color }"
576
- :to="`/dcw/${l.id}`"
577
- >
578
- <div class="lc-head">
579
- <span class="lc-dot" />
580
- <b>{{ l.name }}</b>
581
- <span
582
- class="lc-state"
583
- :class="{ on: l.active }"
584
- >{{ l.active ? t('home.runNow') : t('home.standBy') }}</span>
585
- </div>
586
- <div
587
- v-if="l.active"
588
- class="lc-run mono"
589
- >
590
- <span>{{ l.product }} · {{ l.recipe }}</span>
591
- <small>{{ t('home.batch') }} {{ l.runId?.slice(0, 8) }} · {{ t('home.tagged') }} {{ l.tagged }}</small>
592
- </div>
593
- <div class="lc-meta mono">
594
- <span>{{ t('home.nodesUnit') }} {{ l.dcwCount }}</span>
595
- <span>{{ t('home.daqUnit') }} {{ l.daqCount }}</span>
596
- </div>
597
- </NuxtLink>
598
- <NuxtLink
599
- v-if="fleetOverflow > 0"
600
- class="line-card fleet-all im"
601
- to="/dcw"
602
- >
603
- <span class="fa-n mono">+{{ fleetOverflow }}</span>
604
- <span class="fa-label">{{ t('home.fleetAll') }}</span>
605
- <span class="i-tabler-arrow-right fa-arrow" />
606
- </NuxtLink>
607
- <p
608
- v-if="lineCards.length === 0"
609
- class="fleet-empty"
610
- >
611
- {{ t('home.noLine') }}
612
- </p>
613
- </div>
614
- </section>
615
-
616
- <!-- 插件 UI 注入区:client 面板经 ctx.ui.registerPanel({slot:'dashboard.widgets'}) 注入 -->
617
- <ClientOnly>
618
- <workshop-plugin-slot slot-name="dashboard.widgets" />
619
- </ClientOnly>
620
- </div>
621
- </template>
622
-
623
- <style scoped>
624
- .home { padding: 4px; }
625
-
626
- /* ---------- Hero:中枢横幅 + LIVE 实况(基座 = aw-bench,此处只写专属节奏) ---------- */
627
- .hero {
628
- display: flex;
629
- flex-wrap: wrap;
630
- gap: 24px;
631
- align-items: center;
632
- justify-content: space-between;
633
- margin-bottom: var(--gap-block);
634
- padding: 30px 34px 32px;
635
- overflow: hidden;
636
- }
637
- /* 仪表刻度母题:底部细刻度尺(控制室仪器读数;纯装饰零动画,两端淡出) */
638
- .hero-scale {
639
- position: absolute;
640
- right: 0;
641
- bottom: 0;
642
- left: 0;
643
- height: 9px;
644
- pointer-events: none;
645
- background:
646
- repeating-linear-gradient(90deg, color-mix(in srgb, var(--ink) 26%, transparent) 0 1px, transparent 1px 120px),
647
- repeating-linear-gradient(90deg, color-mix(in srgb, var(--ink) 11%, transparent) 0 1px, transparent 1px 24px);
648
- opacity: 0.55;
649
- -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
650
- mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
651
- }
652
- .hero-main {
653
- display: flex;
654
- flex-direction: column;
655
- gap: 10px;
656
- max-width: 620px;
657
- }
658
- .hero-title {
659
- margin: 0;
660
- font-family: var(--font-display);
661
- font-size: 36px;
662
- font-weight: 400;
663
- line-height: 1.12;
664
- letter-spacing: -0.015em;
665
- color: var(--ink);
666
- }
667
- .hero-sub {
668
- max-width: 54ch;
669
- margin: 0;
670
- font-size: 13px;
671
- line-height: 1.65;
672
- color: var(--ink-faint);
673
- }
674
- .hero-acts {
675
- display: flex;
676
- flex-wrap: wrap;
677
- gap: 10px;
678
- margin-top: 4px;
679
- }
680
- /* 实况仪表簇:左缘一道品牌刻度,与画布分节同语言 */
681
- .hero-live {
682
- display: flex;
683
- flex-direction: column;
684
- gap: 10px;
685
- min-width: 224px;
686
- padding: 15px 18px 13px;
687
- background: var(--frost-bg);
688
- border: 1px solid var(--glass-line);
689
- border-left: 2px solid var(--accent);
690
- border-radius: 0 var(--radius-panel-sm) var(--radius-panel-sm) 0;
691
- }
692
- .live-badge {
693
- display: inline-flex;
694
- gap: 7px;
695
- align-items: center;
696
- width: fit-content;
697
- padding: 2px 10px;
698
- font-size: 10px;
699
- letter-spacing: 0.16em;
700
- color: var(--tone-success-dot);
701
- border: 1px solid color-mix(in srgb, var(--tone-success-dot) 45%, transparent);
702
- border-radius: var(--radius-pill);
703
- }
704
- .live-dot {
705
- width: 6px;
706
- height: 6px;
707
- background: var(--tone-success-dot);
708
- border-radius: 50%;
709
- }
710
- @media (prefers-reduced-motion: no-preference) {
711
- .live-dot { animation: livePulse 1.6s ease-in-out infinite; }
712
- }
713
- @keyframes livePulse {
714
- 0%, 100% { box-shadow: 0 0 3px var(--tone-success-dot); }
715
- 50% { box-shadow: 0 0 9px var(--tone-success-dot); }
716
- }
717
- .live-rows {
718
- display: flex;
719
- flex-direction: column;
720
- margin: 0;
721
- }
722
- .live-row {
723
- display: flex;
724
- gap: 16px;
725
- align-items: baseline;
726
- justify-content: space-between;
727
- padding: 7px 0;
728
- border-top: 1px solid var(--divider-hair);
729
- }
730
- .live-row:first-child { padding-top: 2px; border-top: 0; }
731
- .live-row dt {
732
- font-size: 11.5px;
733
- color: var(--ink-faint);
734
- }
735
- .live-row dd {
736
- margin: 0;
737
- font-size: 13.5px;
738
- color: var(--ink);
739
- }
740
- .live-row dd b { font-weight: 500; }
741
- .live-row dd small { font-size: 11px; color: var(--ink-faint); }
742
- .live-row.is-alarm dt,
743
- .live-row.is-alarm dd { color: var(--tone-danger-dot); }
744
-
745
- /* ---------- 量规统计带:列数按可用宽度收敛(6 → 3 → 2,永不挤成一条) ---------- */
746
- .aw-gauge-band {
747
- --cols: 6;
748
- margin-bottom: var(--gap-block);
749
- }
750
- @media (max-width: 1240px) {
751
- .aw-gauge-band { --cols: 3; }
752
- }
753
- @media (max-width: 720px) {
754
- .aw-gauge-band { --cols: 2; }
755
- }
756
-
757
- /* ---------- 大屏图阵 ---------- */
758
- .grid {
759
- display: grid;
760
- grid-template-columns: repeat(12, 1fr);
761
- gap: var(--gap-block);
762
- margin-bottom: var(--gap-block);
763
- }
764
- .span8 { grid-column: span 8; }
765
- .span4 { grid-column: span 4; }
766
- @media (max-width: 1100px) {
767
- .span8, .span4 { grid-column: span 12; }
768
- }
769
- /* 面板题注:一句话说明这张图在看什么;窄屏让位给读数本身 */
770
- .aw-bench-sub {
771
- overflow: hidden;
772
- font-size: 11px;
773
- letter-spacing: 0.02em;
774
- color: var(--ink-faint);
775
- text-overflow: ellipsis;
776
- white-space: nowrap;
777
- }
778
- @media (max-width: 1500px) {
779
- .aw-bench-sub { display: none; }
780
- }
781
- .chart { width: 100%; }
782
- .h280 { height: 280px; }
783
- .h240 { height: 240px; }
784
- .donut-wrap { position: relative; }
785
- .donut-center {
786
- position: absolute;
787
- top: 38%;
788
- left: 0;
789
- display: flex;
790
- flex-direction: column;
791
- align-items: center;
792
- width: 100%;
793
- transform: translateY(-50%);
794
- pointer-events: none;
795
- }
796
- .donut-center b {
797
- font-family: var(--font-mono);
798
- font-size: 26px;
799
- font-weight: 500;
800
- font-variant-numeric: tabular-nums;
801
- color: var(--ink);
802
- }
803
- .donut-center small { font-size: 11px; color: var(--ink-faint); }
804
-
805
- /* ---------- 产线清单(基座 = aw-bench) ---------- */
806
- .fleet { margin-bottom: var(--gap-block); }
807
- .fleet-grid {
808
- display: grid;
809
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
810
- gap: 12px;
811
- }
812
- .line-card {
813
- padding: 13px 15px;
814
- cursor: pointer;
815
- background: var(--frost-bg);
816
- border: 1px solid var(--glass-line);
817
- border-radius: var(--radius-panel-sm);
818
- transition:
819
- border-color var(--transition-fast),
820
- background-color var(--transition-fast),
821
- transform var(--transition-base),
822
- box-shadow var(--transition-base);
823
- }
824
- @media (hover: hover) and (prefers-reduced-motion: no-preference) {
825
- .line-card:hover {
826
- border-color: var(--lc, var(--accent));
827
- transform: translateY(-1px);
828
- box-shadow: var(--glass-edge), var(--shadow-float);
829
- }
830
- }
831
- .line-card.on {
832
- background: color-mix(in srgb, var(--lc) 7%, var(--paper-deep));
833
- border-color: color-mix(in srgb, var(--lc) 45%, transparent);
834
- }
835
- .lc-head {
836
- display: flex;
837
- gap: 8px;
838
- align-items: center;
839
- }
840
- .lc-dot {
841
- flex: 0 0 auto;
842
- width: 8px;
843
- height: 8px;
844
- border: 1.5px solid var(--lc);
845
- border-radius: 50%;
846
- }
847
- .line-card.on .lc-dot {
848
- background: var(--lc);
849
- box-shadow: 0 0 7px color-mix(in srgb, var(--lc) 70%, transparent);
850
- }
851
- .lc-head b { font-size: 13px; color: var(--ink); }
852
- .lc-state {
853
- margin-left: auto;
854
- padding: 1px 8px;
855
- font-family: var(--font-mono);
856
- font-size: 10px;
857
- letter-spacing: 0.05em;
858
- color: var(--ink-faint);
859
- border: 1px solid var(--line-strong);
860
- border-radius: var(--radius-pill);
861
- }
862
- .lc-state.on {
863
- color: var(--tone-success-dot);
864
- border-color: color-mix(in srgb, var(--tone-success-dot) 45%, transparent);
865
- }
866
- .lc-run {
867
- display: flex;
868
- flex-direction: column;
869
- gap: 2px;
870
- margin-top: 8px;
871
- font-size: 11.5px;
872
- color: color-mix(in srgb, var(--lc) 70%, var(--ink));
873
- }
874
- .lc-run small { font-size: 10px; color: var(--ink-faint); }
875
- .lc-meta {
876
- display: flex;
877
- gap: 14px;
878
- margin-top: 9px;
879
- padding-top: 8px;
880
- font-size: 10.5px;
881
- color: var(--ink-faint);
882
- border-top: 1px solid var(--divider-hair);
883
- }
884
- /* ---------- Harness 可用性条:墨色药丸式引擎清单,未安装灰化 ---------- */
885
- .harness {
886
- margin-bottom: var(--gap-block);
887
- }
888
- .harness-row {
889
- display: flex;
890
- flex-wrap: wrap;
891
- gap: 8px;
892
- padding: 2px 0 4px;
893
- }
894
- .h-item {
895
- display: flex;
896
- gap: 7px;
897
- align-items: center;
898
- padding: 5px 12px;
899
- font-size: 11.5px;
900
- cursor: default;
901
- background: color-mix(in srgb, var(--ink) 3%, transparent);
902
- border: 1px solid var(--divider-hair);
903
- border-radius: var(--radius-pill);
904
- transition: border-color 0.15s, background 0.15s;
905
- }
906
- .h-item:hover { border-color: var(--line-strong); }
907
- .h-item.link { cursor: pointer; }
908
- .h-item.link:hover { border-color: var(--accent); color: var(--accent); }
909
- .h-go { flex: none; font-size: 10px; color: var(--ink-faint); }
910
- .h-item.link:hover .h-go { color: var(--accent); }
911
- .h-dot {
912
- flex: none;
913
- width: 6px;
914
- height: 6px;
915
- border-radius: 50%;
916
- background: var(--tone-success-dot, #4a6b57);
917
- }
918
- .h-item.off { opacity: 0.55; }
919
- .h-item.off .h-dot { background: var(--tone-danger-dot, #c25a4e); }
920
- .h-name { font-weight: 600; color: var(--ink); }
921
- .h-item.off .h-name { color: var(--ink-soft); }
922
- /* 引擎命令名是"铭牌"级信息:桌面 10px 尚可,手持档必须让位给可读性
923
- * (实测 / 在 390 下稳定报出 54 条 <11px,全部来自这里与 .aw-gauge-label) */
924
- .h-cmd { font-size: 10px; color: var(--ink-faint); }
925
-
926
- /* 手持档字号地板:本页所有"铭牌级"微字(引擎命令名、产线状态、产线指标行、
927
- 跳转箭头)统一抬到 11.5px。它们在桌面是仪器铭牌语言,在手机上就是读不了的小字。 */
928
- @media (max-width: 900px) {
929
- .h-cmd,
930
- .h-go,
931
- .lc-state,
932
- .lc-run small,
933
- .lc-meta,
934
- .lc-meta span,
935
- .fleet-all small {
936
- font-size: 11.5px;
937
- }
938
- }
939
- /* 全部产线入口:虚线幽灵卡,聚合清单外的余量 */
940
- .fleet-all {
941
- display: flex;
942
- flex-direction: column;
943
- gap: 6px;
944
- align-items: center;
945
- justify-content: center;
946
- min-height: 86px;
947
- color: var(--ink-faint);
948
- background: transparent;
949
- border-style: dashed;
950
- transition: border-color 0.15s, color 0.15s, background 0.15s;
951
- }
952
- .fleet-all:hover {
953
- color: var(--accent);
954
- background: color-mix(in srgb, var(--accent) 5%, transparent);
955
- border-color: color-mix(in srgb, var(--accent) 45%, transparent);
956
- }
957
- .fa-n {
958
- font-size: 22px;
959
- line-height: 1;
960
- color: var(--ink-soft);
961
- font-variant-numeric: tabular-nums;
962
- }
963
- .fleet-all:hover .fa-n { color: var(--accent); }
964
- .fa-label { font-size: 12px; }
965
- .fa-arrow {
966
- font-size: 15px;
967
- transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
968
- }
969
- .fleet-all:hover .fa-arrow { transform: translateX(3px); }
970
- .fleet-empty {
971
- padding: 18px 0;
972
- font-size: 12px;
973
- color: var(--ink-faint);
974
- text-align: center;
975
- }
976
-
977
- @media (max-width: 640px) {
978
- .hero { padding: 24px 20px; }
979
- .hero-title { font-size: 28px; }
980
- }
981
- </style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * 仪表盘(/)—— 产线运营数字大屏。
4
+ * ECharts 可视化:实时工况趋势(量程归一化)/ 产线运行状态 / 数采管线吞吐 /
5
+ * 写控制成功率 / 节点状态分布 + 产线清单卡。
6
+ * 数据权威在 server:useDcwStream + useDaqStream(REST 基线 + WS 实时收敛
7
+ * + 5s 低频兜底刷新);趋势缓冲为本页每 5s 一次的量程归一化快照。
8
+ */
9
+ import { useDaqStream } from '@/app/composables/workshop/useDaqStream'
10
+ import { useDcwStream } from '@/app/composables/workshop/useDcwStream'
11
+ import { useVisibleInterval } from '@/app/composables/workshop/useVisibleInterval'
12
+ import { useWorkshopApi, type HarnessMetaDto } from '@/app/composables/workshop/useWorkshopApi'
13
+ import AwChart from '@/app/components/AwChart.vue'
14
+ import type { EChartsOption } from 'echarts'
15
+
16
+ const { t } = useI18n()
17
+ // 注意:useHead 的函数 title 在 prod SSR 下立即求值,必须在 useI18n 之后注册
18
+ useHead({ title: () => t('titles.dashboard') })
19
+
20
+ const site = useSiteConfig()
21
+ const store = useAppStore()
22
+ const daq = useDaqStream()
23
+ const dcw = useDcwStream()
24
+
25
+ // 图表色序随主题切换(两套声部,同一语义序):
26
+ // 亮 = Warm Editorial 编辑色板(main.css --chart-* 同源,墨绿→苔绿→琥珀→陶赭)
27
+ // 暗 = 控制室 tone 系统(绿=运行/成功 · 青=数据 · 琥珀=需关注 · 紫=重试 · 红=告警)
28
+ // —— 亮阶把品牌绿当"墨"用,暗阶把同一抹绿当"信号"用,各自在自己的画布上才成立。
29
+ const PAL = computed(() => (store.isDark
30
+ ? { accent: '#3fe4ab', cyan: '#41c8f4', amber: '#f6c453', violet: '#a795ff', danger: '#ff8080' }
31
+ : { accent: '#4a6b57', cyan: '#6f8296', amber: '#c9a26a', violet: '#b3714f', danger: '#c25a4e' }))
32
+
33
+ // ---------- 数据装载(WS 实时 + 5s 兜底;可见性调度:后台降频 30s,回前台立即补拍) ----------
34
+ onMounted(() => {
35
+ daq.ensureWsFeed()
36
+ dcw.ensureWsFeed()
37
+ void Promise.all([daq.load(), dcw.load()]).then(() => pushTrend())
38
+ void loadHarnesses()
39
+ useVisibleInterval(() => {
40
+ void daq.load()
41
+ void dcw.load()
42
+ void loadHarnesses()
43
+ pushTrend()
44
+ }, 5000, { bgMs: 30000 })
45
+ })
46
+
47
+ // ---------- Harness 可用性(引擎 CLI 环境探测;服务端 30s 探测缓存,随兜底节拍刷新) ----------
48
+ const api = useWorkshopApi()
49
+ const harnesses = ref<HarnessMetaDto[]>([])
50
+
51
+ /**
52
+ * 引擎卡片第二行的命令名。
53
+ * 返回空串表示"这一行没有新信息":标签已经以命令名开头时
54
+ * (omp(真实 LLM) / omp、opencode / opencode、codex / codex…),
55
+ * 再写一遍只是把同一件事讲两遍 —— 实测 14 张卡里有 9 张是重复的。
56
+ * 真正有信息量的只有:不可用状态、in-process、以及与标签不同的真实二进制名。
57
+ */
58
+ /** 已知引擎的服务端 label 前端译名(服务端 label 是中文数据,展示层按 id 映射) */
59
+ const harnessLabel = (h: HarnessMetaDto): string => {
60
+ if (h.id === 'mock') return t('agents.hMock')
61
+ if (h.id === 'omp') return t('agents.hOmp')
62
+ return h.label
63
+ }
64
+ const harnessCmd = (h: HarnessMetaDto): string => {
65
+ if (h.available === false) return t('home.harness.missing')
66
+ if (h.inprocess) return 'in-process'
67
+ const cmd = String(h.command ?? '')
68
+ if (!cmd) return ''
69
+ return h.label.startsWith(cmd) ? '' : cmd
70
+ }
71
+ const loadHarnesses = async (): Promise<void> => {
72
+ try {
73
+ const res = await api.listHarnesses()
74
+ harnesses.value = (res as unknown as { data?: { harnesses?: HarnessMetaDto[] } })?.data?.harnesses ?? []
75
+ }
76
+ catch { /* 探测不可得时面板留空,不阻塞大屏 */ }
77
+ }
78
+ const harnessOk = computed(() => harnesses.value.filter(h => h.available !== false).length)
79
+
80
+ // ---------- KPI ----------
81
+ const linesActive = computed(() => dcw.lines.filter(l => dcw.lineStateOf(l.id).active))
82
+ const daqOnline = computed(() => daq.nodes.filter(n => n.enabled && n.state !== 'offline').length)
83
+ const daqTotal = computed(() => daq.nodes.length)
84
+ const alarmCount = computed(() => daq.nodes.filter(n => n.state === 'alarm').length)
85
+ const writeRate = computed(() => {
86
+ const total = dcw.controller.writesTotal
87
+ if (total === 0) return 100
88
+ return Math.round(((total - dcw.controller.writesFailed) / total) * 1000) / 10
89
+ })
90
+
91
+ // ---------- 趋势缓冲(近 3 分钟,5s 一拍,量程归一化) ----------
92
+ interface TrendPoint { t: number, m: Record<string, number | null> }
93
+ const trendBuf = ref<TrendPoint[]>([])
94
+ /** 趋势通道:有实时值的节点优先(在线优先),稳定取前 4 */
95
+ const trendNodes = computed(() => daq.nodes
96
+ .filter(n => n.value != null && n.max > n.min)
97
+ .sort((a, b) => (a.lineId ? 0 : 1) - (b.lineId ? 0 : 1) || a.id.localeCompare(b.id))
98
+ .slice(0, 4))
99
+
100
+ function pushTrend(): void {
101
+ const m: Record<string, number | null> = {}
102
+ for (const n of trendNodes.value) {
103
+ m[n.id] = n.value == null ? null : Math.round(((n.value - n.min) / (n.max - n.min)) * 100)
104
+ }
105
+ trendBuf.value.push({ t: Date.now(), m: m as Record<string, number | null> })
106
+ if (trendBuf.value.length > 36) trendBuf.value.shift()
107
+ }
108
+
109
+ // ---------- 主题感知的图表公共色 ----------
110
+ const inkC = computed(() => (store.isDark ? '#e8eef8' : '#1f2a3a'))
111
+ const dimC = computed(() => (store.isDark ? 'rgba(143,160,181,0.85)' : 'rgba(80,95,120,0.85)'))
112
+ const splitC = computed(() => (store.isDark ? 'rgba(143,160,181,0.13)' : 'rgba(80,95,120,0.14)'))
113
+ const tipBg = computed(() => (store.isDark ? 'rgba(10,16,28,0.94)' : 'rgba(255,255,255,0.97)'))
114
+
115
+ const baseTooltip = computed(() => ({
116
+ backgroundColor: tipBg.value,
117
+ borderColor: splitC.value,
118
+ textStyle: { color: inkC.value, fontSize: 11 },
119
+ }))
120
+
121
+ // ---------- 图 1:实时工况趋势(多通道量程归一化) ----------
122
+ /** 趋势是否真有数值可画(采样被活动批次门控:未开跑时缓冲里只有 null 占位) */
123
+ const trendHasData = computed(() =>
124
+ trendNodes.value.length > 0
125
+ && trendBuf.value.some(p => trendNodes.value.some(n => typeof p.m[n.id] === 'number')))
126
+
127
+ const trendOpt = computed<EChartsOption>(() => ({
128
+ backgroundColor: 'transparent',
129
+ tooltip: { trigger: 'axis', ...baseTooltip.value },
130
+ legend: {
131
+ top: 0, right: 4, icon: 'roundRect', itemWidth: 10, itemHeight: 4,
132
+ textStyle: { color: dimC.value, fontSize: 10.5 },
133
+ },
134
+ grid: { left: 42, right: 14, top: 30, bottom: 24 },
135
+ xAxis: {
136
+ type: 'time',
137
+ axisLabel: { color: dimC.value, fontSize: 10, formatter: '{HH}:{mm}:{ss}' },
138
+ axisLine: { lineStyle: { color: splitC.value } },
139
+ splitLine: { show: false },
140
+ },
141
+ yAxis: {
142
+ // 轴名移到面板头 meta:贴在轴端会和刻度数字挤在一起(见 aw-bench-meta)
143
+ type: 'value', min: 0, max: 100,
144
+ axisLabel: { color: dimC.value, fontSize: 10 },
145
+ splitLine: { lineStyle: { color: splitC.value } },
146
+ },
147
+ series: trendNodes.value.map((n, i) => {
148
+ const color = [PAL.value.accent, PAL.value.cyan, PAL.value.amber, PAL.value.violet][i % 4]
149
+ return {
150
+ name: n.name,
151
+ type: 'line',
152
+ smooth: true,
153
+ showSymbol: false,
154
+ connectNulls: true,
155
+ lineStyle: { width: 1.6, color },
156
+ itemStyle: { color },
157
+ data: trendBuf.value.map(p => [p.t, p.m[n.id] ?? null]),
158
+ }
159
+ }),
160
+ }))
161
+
162
+ // ---------- 图 2:产线运行状态(donut) ----------
163
+ const lineStateOpt = computed<EChartsOption>(() => ({
164
+ backgroundColor: 'transparent',
165
+ tooltip: { trigger: 'item', ...baseTooltip.value },
166
+ legend: {
167
+ bottom: 0, left: 'center', icon: 'roundRect', itemWidth: 10, itemHeight: 4,
168
+ textStyle: { color: dimC.value, fontSize: 10.5 },
169
+ },
170
+ series: [{
171
+ type: 'pie',
172
+ radius: ['62%', '82%'],
173
+ center: ['50%', '44%'],
174
+ label: { show: false },
175
+ silent: false,
176
+ data: [
177
+ { value: linesActive.value.length, name: t('home.runNow'), itemStyle: { color: PAL.value.accent } },
178
+ { value: Math.max(dcw.lines.length - linesActive.value.length, 0), name: t('home.standBy'), itemStyle: { color: splitC.value } },
179
+ ],
180
+ }],
181
+ }))
182
+
183
+ // ---------- 图 3:数采管线吞吐(累计) ----------
184
+ const pipelineOpt = computed<EChartsOption>(() => ({
185
+ backgroundColor: 'transparent',
186
+ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, ...baseTooltip.value },
187
+ grid: { left: 70, right: 34, top: 10, bottom: 24 },
188
+ xAxis: {
189
+ type: 'value',
190
+ axisLabel: { color: dimC.value, fontSize: 10, formatter: (v: number) => v >= 1000 ? `${Math.round(v / 1000)}k` : String(v) },
191
+ splitLine: { lineStyle: { color: splitC.value } },
192
+ },
193
+ yAxis: {
194
+ type: 'category',
195
+ data: [t('home.pipelineSeries.stored'), t('home.pipelineSeries.consumed'), t('home.pipelineSeries.produced')],
196
+ axisLabel: { color: dimC.value, fontSize: 10.5 },
197
+ axisLine: { lineStyle: { color: splitC.value } },
198
+ },
199
+ series: [{
200
+ type: 'bar',
201
+ barWidth: 10,
202
+ data: [
203
+ { value: daq.meta.samplesStored ?? 0, itemStyle: { color: PAL.value.amber } },
204
+ { value: daq.meta.consumed ?? 0, itemStyle: { color: PAL.value.cyan } },
205
+ { value: daq.meta.produced ?? 0, itemStyle: { color: PAL.value.accent } },
206
+ ],
207
+ itemStyle: { borderRadius: [0, 5, 5, 0] },
208
+ }],
209
+ }))
210
+
211
+ // ---------- 图 4:写控制成功率(gauge) ----------
212
+ const writeOpt = computed<EChartsOption>(() => ({
213
+ backgroundColor: 'transparent',
214
+ series: [{
215
+ type: 'gauge',
216
+ startAngle: 210,
217
+ endAngle: -30,
218
+ min: 0,
219
+ max: 100,
220
+ radius: '92%',
221
+ center: ['50%', '58%'],
222
+ progress: { show: true, width: 12, itemStyle: { color: PAL.value.accent } },
223
+ axisLine: { lineStyle: { width: 12, color: [[1, splitC.value]] } },
224
+ axisTick: { show: false },
225
+ splitLine: { show: false },
226
+ axisLabel: { show: false },
227
+ pointer: { show: false },
228
+ anchor: { show: false },
229
+ title: { show: false },
230
+ detail: {
231
+ valueAnimation: true,
232
+ formatter: '{value}%',
233
+ color: inkC.value,
234
+ fontSize: 26,
235
+ fontFamily: 'monospace',
236
+ offsetCenter: [0, '4%'],
237
+ },
238
+ data: [{ value: writeRate.value }],
239
+ }],
240
+ }))
241
+
242
+ // ---------- 图 5:节点状态分布(数采/控制 堆叠) ----------
243
+ const nodeStates = computed(() => [
244
+ { key: 'ok', color: PAL.value.accent },
245
+ { key: 'warn', color: PAL.value.amber },
246
+ { key: 'alarm', color: PAL.value.danger },
247
+ { key: 'writing', color: PAL.value.cyan },
248
+ { key: 'idle', color: store.isDark ? '#5f6e84' : '#8fa0b5' },
249
+ { key: 'error', color: '#ff8a5c' },
250
+ { key: 'offline', color: store.isDark ? 'rgba(95,110,132,0.38)' : 'rgba(143,160,181,0.35)' },
251
+ ])
252
+
253
+ const nodeStateOpt = computed<EChartsOption>(() => {
254
+ const daqCount = new Map<string, number>()
255
+ for (const n of daq.nodes) daqCount.set(n.state, (daqCount.get(n.state) ?? 0) + 1)
256
+ const dcwCount = new Map<string, number>()
257
+ for (const n of dcw.nodes) dcwCount.set(n.state, (dcwCount.get(n.state) ?? 0) + 1)
258
+ const states = nodeStates.value.filter(s => daqCount.get(s.key) || dcwCount.get(s.key))
259
+ return {
260
+ backgroundColor: 'transparent',
261
+ tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, ...baseTooltip.value },
262
+ legend: {
263
+ top: 0, left: 'center', icon: 'roundRect', itemWidth: 10, itemHeight: 4,
264
+ textStyle: { color: dimC.value, fontSize: 10 },
265
+ },
266
+ grid: { left: 56, right: 14, top: 30, bottom: 24 },
267
+ xAxis: {
268
+ type: 'value',
269
+ axisLabel: { color: dimC.value, fontSize: 10 },
270
+ splitLine: { lineStyle: { color: splitC.value } },
271
+ },
272
+ yAxis: {
273
+ type: 'category',
274
+ data: [t('home.stateDcw'), t('home.stateDaq')],
275
+ axisLabel: { color: dimC.value, fontSize: 10.5 },
276
+ axisLine: { lineStyle: { color: splitC.value } },
277
+ },
278
+ series: states.map(s => ({
279
+ name: t(`home.states.${s.key}`),
280
+ type: 'bar' as const,
281
+ stack: 'nodes',
282
+ barWidth: 12,
283
+ itemStyle: { color: s.color },
284
+ data: [dcwCount.get(s.key) ?? 0, daqCount.get(s.key) ?? 0],
285
+ })),
286
+ }
287
+ })
288
+
289
+ // ---------- 产线清单(裁策:清单是导航不是数据库导出) ----------
290
+ const lineCards = computed(() => dcw.lines.map((l) => {
291
+ const st = dcw.lineStateOf(l.id)
292
+ return {
293
+ id: l.id,
294
+ name: l.name,
295
+ color: l.color,
296
+ active: st.active,
297
+ product: st.productName,
298
+ recipe: st.recipeName,
299
+ runId: st.runId,
300
+ tagged: st.taggedSamples,
301
+ dcwCount: dcw.nodes.filter(n => n.lineId === l.id).length,
302
+ daqCount: daq.nodes.filter(n => n.lineId === l.id).length,
303
+ }
304
+ }))
305
+ /** 运行中优先,其次已打标样本多的(最近活跃),稳定排序 */
306
+ const fleetSorted = computed(() => [...lineCards.value].sort((a, b) =>
307
+ Number(b.active) - Number(a.active) || b.tagged - a.tagged || a.name.localeCompare(b.name)))
308
+ const FLEET_CAP = 8
309
+ const fleetShown = computed(() => fleetSorted.value.slice(0, FLEET_CAP))
310
+ const fleetOverflow = computed(() => Math.max(lineCards.value.length - FLEET_CAP, 0))
311
+ </script>
312
+
313
+ <template>
314
+ <div class="home">
315
+ <!-- Hero:产线运营中枢(校准仪表台母题;右侧 LIVE 实况仪表簇) -->
316
+ <section class="hero aw-bench aw-bench--marked aw-stagger">
317
+ <div class="hero-main">
318
+ <p class="aw-bench-kicker">
319
+ {{ t('home.kicker') }} · {{ site.mode }}
320
+ </p>
321
+ <h1 class="hero-title">
322
+ {{ t('home.heroTitle') }}
323
+ <span class="aw-serif-accent-italic">{{ t('home.heroAccent') }}</span>
324
+ </h1>
325
+ <p class="hero-sub">
326
+ {{ t('home.heroSub') }}
327
+ </p>
328
+ <div class="hero-acts">
329
+ <button
330
+ class="aw-pill im"
331
+ @click="navigateTo('/town')"
332
+ >
333
+ <span class="i-tabler-map-2 im-pop" />
334
+ {{ t('home.ctaTown') }}
335
+ </button>
336
+ <button
337
+ class="aw-pill outline im"
338
+ @click="navigateTo('/dcw')"
339
+ >
340
+ <span class="i-tabler-route im-pop" />
341
+ {{ t('home.ctaLine') }}
342
+ </button>
343
+ </div>
344
+ </div>
345
+
346
+ <!-- 实况仪表簇:LIVE 徽标 + 三条带刻度的读数行(读数比句子更快被扫到) -->
347
+ <div class="hero-live">
348
+ <span class="live-badge"><span class="live-dot" />{{ t('home.live') }}</span>
349
+ <dl class="live-rows">
350
+ <div class="live-row">
351
+ <dt>{{ t('home.kpi.lines') }}</dt>
352
+ <dd class="mono">
353
+ <b>{{ linesActive.length }}</b><small>/{{ dcw.lines.length }}</small>
354
+ </dd>
355
+ </div>
356
+ <div class="live-row">
357
+ <dt>{{ t('home.kpi.samples') }}</dt>
358
+ <dd class="mono">
359
+ <b>{{ daq.meta.samplesStored ?? 0 }}</b>
360
+ </dd>
361
+ </div>
362
+ <div
363
+ class="live-row"
364
+ :class="{ 'is-alarm': alarmCount > 0 }"
365
+ >
366
+ <dt>{{ t('home.kpi.alarms') }}</dt>
367
+ <dd class="mono">
368
+ <b>{{ alarmCount }}</b>
369
+ </dd>
370
+ </div>
371
+ </dl>
372
+ </div>
373
+ <!-- 仪表刻度母题(控制室仪器读数;纯装饰,零动画) -->
374
+ <div
375
+ class="hero-scale"
376
+ aria-hidden="true"
377
+ />
378
+ </section>
379
+
380
+ <!-- 量规统计带:一块仪表盘,不是一排各自为政的卡(底部量程刻度是签名) -->
381
+ <div class="aw-gauge-band aw-stagger">
382
+ <div class="aw-gauge">
383
+ <span class="aw-gauge-label">{{ t('home.kpi.lines') }}</span>
384
+ <span class="aw-gauge-value">
385
+ <span class="aw-readout">{{ linesActive.length }}<small>/{{ dcw.lines.length }}</small></span>
386
+ </span>
387
+ </div>
388
+ <div class="aw-gauge">
389
+ <span class="aw-gauge-label">{{ t('home.kpi.dcwNodes') }}</span>
390
+ <span class="aw-gauge-value">
391
+ <span class="aw-readout">{{ dcw.controller.nodesOnline }}<small>/{{ dcw.controller.nodesTotal }}</small></span>
392
+ </span>
393
+ </div>
394
+ <div class="aw-gauge">
395
+ <span class="aw-gauge-label">{{ t('home.kpi.daqNodes') }}</span>
396
+ <span class="aw-gauge-value">
397
+ <span class="aw-readout">{{ daqOnline }}<small>/{{ daqTotal }}</small></span>
398
+ </span>
399
+ </div>
400
+ <div class="aw-gauge">
401
+ <span class="aw-gauge-label">{{ t('home.kpi.samples') }}</span>
402
+ <span class="aw-gauge-value">
403
+ <span class="aw-readout">{{ daq.meta.samplesStored ?? 0 }}</span>
404
+ </span>
405
+ </div>
406
+ <div class="aw-gauge">
407
+ <span class="aw-gauge-label">{{ t('home.kpi.writeRate') }}</span>
408
+ <span class="aw-gauge-value">
409
+ <span class="aw-readout">{{ writeRate }}<small>%</small></span>
410
+ </span>
411
+ </div>
412
+ <div
413
+ class="aw-gauge"
414
+ :class="{ 'is-alarm': alarmCount > 0 }"
415
+ >
416
+ <span class="aw-gauge-label">{{ t('home.kpi.alarms') }}</span>
417
+ <span class="aw-gauge-value">
418
+ <span class="aw-readout">{{ alarmCount }}</span>
419
+ </span>
420
+ </div>
421
+ </div>
422
+
423
+ <!-- 大屏图阵 -->
424
+ <!-- 执行引擎可用性(Harness CLI 环境探测;未安装不可选,派工前强校验) -->
425
+ <section
426
+ v-if="harnesses.length > 0"
427
+ class="harness aw-bench aw-stagger"
428
+ >
429
+ <header class="aw-bench-hd">
430
+ <h3 class="aw-bench-title">
431
+ {{ t('home.harness.title') }}
432
+ </h3>
433
+ <small class="aw-bench-sub">{{ t('home.harness.sub') }}</small>
434
+ <span class="aw-bench-meta">
435
+ <span><b>{{ harnessOk }}</b>/{{ harnesses.length }}</span>
436
+ </span>
437
+ </header>
438
+ <div class="harness-row">
439
+ <a
440
+ v-for="h in harnesses"
441
+ :key="h.id"
442
+ class="h-item"
443
+ :class="{ off: h.available === false, link: h.available === false && h.homepage }"
444
+ :href="h.available === false && h.homepage ? h.homepage : undefined"
445
+ target="_blank"
446
+ rel="noopener"
447
+ :title="h.available === false ? (h.error ?? '') : (h.resolvedPath ?? h.command ?? '')"
448
+ >
449
+ <span class="h-dot" />
450
+ <span class="h-name">{{ harnessLabel(h) }}</span>
451
+ <span
452
+ v-if="harnessCmd(h)"
453
+ class="h-cmd mono"
454
+ >{{ harnessCmd(h) }}</span>
455
+ <span
456
+ v-if="h.available === false && h.homepage"
457
+ class="h-go"
458
+ >↗</span>
459
+ </a>
460
+ </div>
461
+ </section>
462
+
463
+ <div class="grid aw-stagger">
464
+ <section class="aw-bench span8">
465
+ <header class="aw-bench-hd">
466
+ <h3 class="aw-bench-title">
467
+ {{ t('home.charts.trend') }}
468
+ </h3>
469
+ <small class="aw-bench-sub">{{ t('home.charts.trendSub') }}</small>
470
+ <span class="aw-bench-meta">
471
+ <span>{{ t('home.trendY') }}</span>
472
+ </span>
473
+ </header>
474
+ <ClientOnly>
475
+ <!-- 空态判据必须看「有没有**数值**」,而不是「缓冲里有没有点」:
476
+ 采样由活动批次门控(无开跑产线时不采样),此时缓冲里可能有携带 null 的
477
+ 占位点 —— 旧写法据此判为"有数据",于是跳过 graphic 提示,
478
+ 渲染出一张只有坐标系+图例、X 轴回落到 00:00~24:00 的空图。
479
+ 那比一句明确的空态更难理解(实测被评审直接判为"趋势面全是空的")。 -->
480
+ <AwChart
481
+ v-if="trendHasData"
482
+ :option="trendOpt"
483
+ class="chart h280"
484
+ />
485
+ <div
486
+ v-else
487
+ class="aw-empty chart h280"
488
+ >
489
+ <p class="aw-empty-title">
490
+ {{ t('home.trendWaiting') }}
491
+ </p>
492
+ <p class="aw-empty-sub">
493
+ {{ t('home.trendNeedRun') }}
494
+ </p>
495
+ </div>
496
+ </ClientOnly>
497
+ </section>
498
+ <section class="aw-bench span4">
499
+ <header class="aw-bench-hd">
500
+ <h3 class="aw-bench-title">
501
+ {{ t('home.charts.lineState') }}
502
+ </h3>
503
+ <span class="aw-bench-meta">
504
+ <span><b>{{ linesActive.length }}</b>/{{ dcw.lines.length }} {{ t('home.kpi.lines') }}</span>
505
+ </span>
506
+ </header>
507
+ <div class="donut-wrap">
508
+ <div class="donut-center mono">
509
+ <b>{{ linesActive.length }}</b>
510
+ <small>/ {{ dcw.lines.length }}</small>
511
+ </div>
512
+ <ClientOnly>
513
+ <AwChart
514
+ :option="lineStateOpt"
515
+ class="chart h240"
516
+ />
517
+ </ClientOnly>
518
+ </div>
519
+ </section>
520
+ <section class="aw-bench span4">
521
+ <header class="aw-bench-hd">
522
+ <h3 class="aw-bench-title">
523
+ {{ t('home.charts.pipeline') }}
524
+ </h3>
525
+ </header>
526
+ <ClientOnly>
527
+ <AwChart
528
+ :option="pipelineOpt"
529
+ class="chart h240"
530
+ />
531
+ </ClientOnly>
532
+ </section>
533
+ <section class="aw-bench span4">
534
+ <header class="aw-bench-hd">
535
+ <h3 class="aw-bench-title">
536
+ {{ t('home.charts.writeCtl') }}
537
+ </h3>
538
+ <span class="aw-bench-meta">
539
+ <span><b>{{ dcw.controller.writesTotal }}</b> {{ t('home.writesFailed') }} {{ dcw.controller.writesFailed }}</span>
540
+ </span>
541
+ </header>
542
+ <ClientOnly>
543
+ <AwChart
544
+ :option="writeOpt"
545
+ class="chart h240"
546
+ />
547
+ </ClientOnly>
548
+ </section>
549
+ <section class="aw-bench span4">
550
+ <header class="aw-bench-hd">
551
+ <h3 class="aw-bench-title">
552
+ {{ t('home.charts.nodeState') }}
553
+ </h3>
554
+ </header>
555
+ <ClientOnly>
556
+ <AwChart
557
+ :option="nodeStateOpt"
558
+ class="chart h240"
559
+ />
560
+ </ClientOnly>
561
+ </section>
562
+ </div>
563
+
564
+ <!-- 产线清单(运行中优先,最多 8 卡;余量聚合入口) -->
565
+ <section class="fleet aw-bench aw-stagger">
566
+ <header class="aw-bench-hd">
567
+ <h3 class="aw-bench-title">
568
+ {{ t('home.charts.lines') }}
569
+ </h3>
570
+ <small class="aw-bench-sub">{{ t('home.fleetHint') }}</small>
571
+ <span class="aw-bench-meta">
572
+ <span><b>{{ dcw.lines.length }}</b></span>
573
+ </span>
574
+ </header>
575
+ <div class="fleet-grid">
576
+ <NuxtLink
577
+ v-for="l in fleetShown"
578
+ :key="l.id"
579
+ class="line-card"
580
+ :class="{ on: l.active }"
581
+ :style="{ '--lc': l.color }"
582
+ :to="`/dcw/${l.id}`"
583
+ >
584
+ <div class="lc-head">
585
+ <span class="lc-dot" />
586
+ <b>{{ l.name }}</b>
587
+ <span
588
+ class="lc-state"
589
+ :class="{ on: l.active }"
590
+ >{{ l.active ? t('home.runNow') : t('home.standBy') }}</span>
591
+ </div>
592
+ <div
593
+ v-if="l.active"
594
+ class="lc-run mono"
595
+ >
596
+ <span>{{ l.product }} · {{ l.recipe }}</span>
597
+ <small>{{ t('home.batch') }} {{ l.runId?.slice(0, 8) }} · {{ t('home.tagged') }} {{ l.tagged }}</small>
598
+ </div>
599
+ <div class="lc-meta mono">
600
+ <span>{{ t('home.nodesUnit') }} {{ l.dcwCount }}</span>
601
+ <span>{{ t('home.daqUnit') }} {{ l.daqCount }}</span>
602
+ </div>
603
+ </NuxtLink>
604
+ <NuxtLink
605
+ v-if="fleetOverflow > 0"
606
+ class="line-card fleet-all im"
607
+ to="/dcw"
608
+ >
609
+ <span class="fa-n mono">+{{ fleetOverflow }}</span>
610
+ <span class="fa-label">{{ t('home.fleetAll') }}</span>
611
+ <span class="i-tabler-arrow-right fa-arrow" />
612
+ </NuxtLink>
613
+ <p
614
+ v-if="lineCards.length === 0"
615
+ class="fleet-empty"
616
+ >
617
+ {{ t('home.noLine') }}
618
+ </p>
619
+ </div>
620
+ </section>
621
+
622
+ <!-- 插件 UI 注入区:client 面板经 ctx.ui.registerPanel({slot:'dashboard.widgets'}) 注入 -->
623
+ <ClientOnly>
624
+ <workshop-plugin-slot slot-name="dashboard.widgets" />
625
+ </ClientOnly>
626
+ </div>
627
+ </template>
628
+
629
+ <style scoped>
630
+ .home { padding: 4px; }
631
+
632
+ /* ---------- Hero:中枢横幅 + LIVE 实况(基座 = aw-bench,此处只写专属节奏) ---------- */
633
+ .hero {
634
+ display: flex;
635
+ flex-wrap: wrap;
636
+ gap: 24px;
637
+ align-items: center;
638
+ justify-content: space-between;
639
+ margin-bottom: var(--gap-block);
640
+ padding: 30px 34px 32px;
641
+ overflow: hidden;
642
+ }
643
+ /* 仪表刻度母题:底部细刻度尺(控制室仪器读数;纯装饰零动画,两端淡出) */
644
+ .hero-scale {
645
+ position: absolute;
646
+ right: 0;
647
+ bottom: 0;
648
+ left: 0;
649
+ height: 9px;
650
+ pointer-events: none;
651
+ background:
652
+ repeating-linear-gradient(90deg, color-mix(in srgb, var(--ink) 26%, transparent) 0 1px, transparent 1px 120px),
653
+ repeating-linear-gradient(90deg, color-mix(in srgb, var(--ink) 11%, transparent) 0 1px, transparent 1px 24px);
654
+ opacity: 0.55;
655
+ -webkit-mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
656
+ mask-image: linear-gradient(90deg, transparent, #000 10%, #000 90%, transparent);
657
+ }
658
+ .hero-main {
659
+ display: flex;
660
+ flex-direction: column;
661
+ gap: 10px;
662
+ max-width: 620px;
663
+ }
664
+ .hero-title {
665
+ margin: 0;
666
+ font-family: var(--font-display);
667
+ font-size: 36px;
668
+ font-weight: 400;
669
+ line-height: 1.12;
670
+ letter-spacing: -0.015em;
671
+ color: var(--ink);
672
+ }
673
+ .hero-sub {
674
+ max-width: 54ch;
675
+ margin: 0;
676
+ font-size: 13px;
677
+ line-height: 1.65;
678
+ color: var(--ink-faint);
679
+ }
680
+ .hero-acts {
681
+ display: flex;
682
+ flex-wrap: wrap;
683
+ gap: 10px;
684
+ margin-top: 4px;
685
+ }
686
+ /* 实况仪表簇:左缘一道品牌刻度,与画布分节同语言 */
687
+ .hero-live {
688
+ display: flex;
689
+ flex-direction: column;
690
+ gap: 10px;
691
+ min-width: 224px;
692
+ padding: 15px 18px 13px;
693
+ background: var(--frost-bg);
694
+ border: 1px solid var(--glass-line);
695
+ border-left: 2px solid var(--accent);
696
+ border-radius: 0 var(--radius-panel-sm) var(--radius-panel-sm) 0;
697
+ }
698
+ .live-badge {
699
+ display: inline-flex;
700
+ gap: 7px;
701
+ align-items: center;
702
+ width: fit-content;
703
+ padding: 2px 10px;
704
+ font-size: 10px;
705
+ letter-spacing: 0.16em;
706
+ color: var(--tone-success-dot);
707
+ border: 1px solid color-mix(in srgb, var(--tone-success-dot) 45%, transparent);
708
+ border-radius: var(--radius-pill);
709
+ }
710
+ .live-dot {
711
+ width: 6px;
712
+ height: 6px;
713
+ background: var(--tone-success-dot);
714
+ border-radius: 50%;
715
+ }
716
+ @media (prefers-reduced-motion: no-preference) {
717
+ .live-dot { animation: livePulse 1.6s ease-in-out infinite; }
718
+ }
719
+ @keyframes livePulse {
720
+ 0%, 100% { box-shadow: 0 0 3px var(--tone-success-dot); }
721
+ 50% { box-shadow: 0 0 9px var(--tone-success-dot); }
722
+ }
723
+ .live-rows {
724
+ display: flex;
725
+ flex-direction: column;
726
+ margin: 0;
727
+ }
728
+ .live-row {
729
+ display: flex;
730
+ gap: 16px;
731
+ align-items: baseline;
732
+ justify-content: space-between;
733
+ padding: 7px 0;
734
+ border-top: 1px solid var(--divider-hair);
735
+ }
736
+ .live-row:first-child { padding-top: 2px; border-top: 0; }
737
+ .live-row dt {
738
+ font-size: 11.5px;
739
+ color: var(--ink-faint);
740
+ }
741
+ .live-row dd {
742
+ margin: 0;
743
+ font-size: 13.5px;
744
+ color: var(--ink);
745
+ }
746
+ .live-row dd b { font-weight: 500; }
747
+ .live-row dd small { font-size: 11px; color: var(--ink-faint); }
748
+ .live-row.is-alarm dt,
749
+ .live-row.is-alarm dd { color: var(--tone-danger-dot); }
750
+
751
+ /* ---------- 量规统计带:列数按可用宽度收敛(6 → 3 → 2,永不挤成一条) ---------- */
752
+ .aw-gauge-band {
753
+ --cols: 6;
754
+ margin-bottom: var(--gap-block);
755
+ }
756
+ @media (max-width: 1240px) {
757
+ .aw-gauge-band { --cols: 3; }
758
+ }
759
+ @media (max-width: 720px) {
760
+ .aw-gauge-band { --cols: 2; }
761
+ }
762
+
763
+ /* ---------- 大屏图阵 ---------- */
764
+ .grid {
765
+ display: grid;
766
+ grid-template-columns: repeat(12, 1fr);
767
+ gap: var(--gap-block);
768
+ margin-bottom: var(--gap-block);
769
+ }
770
+ .span8 { grid-column: span 8; }
771
+ .span4 { grid-column: span 4; }
772
+ @media (max-width: 1100px) {
773
+ .span8, .span4 { grid-column: span 12; }
774
+ }
775
+ /* 面板题注:一句话说明这张图在看什么;窄屏让位给读数本身 */
776
+ .aw-bench-sub {
777
+ overflow: hidden;
778
+ font-size: 11px;
779
+ letter-spacing: 0.02em;
780
+ color: var(--ink-faint);
781
+ text-overflow: ellipsis;
782
+ white-space: nowrap;
783
+ }
784
+ @media (max-width: 1500px) {
785
+ .aw-bench-sub { display: none; }
786
+ }
787
+ .chart { width: 100%; }
788
+ .h280 { height: 280px; }
789
+ .h240 { height: 240px; }
790
+ .donut-wrap { position: relative; }
791
+ .donut-center {
792
+ position: absolute;
793
+ top: 38%;
794
+ left: 0;
795
+ display: flex;
796
+ flex-direction: column;
797
+ align-items: center;
798
+ width: 100%;
799
+ transform: translateY(-50%);
800
+ pointer-events: none;
801
+ }
802
+ .donut-center b {
803
+ font-family: var(--font-mono);
804
+ font-size: 26px;
805
+ font-weight: 500;
806
+ font-variant-numeric: tabular-nums;
807
+ color: var(--ink);
808
+ }
809
+ .donut-center small { font-size: 11px; color: var(--ink-faint); }
810
+
811
+ /* ---------- 产线清单(基座 = aw-bench) ---------- */
812
+ .fleet { margin-bottom: var(--gap-block); }
813
+ .fleet-grid {
814
+ display: grid;
815
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
816
+ gap: 12px;
817
+ }
818
+ .line-card {
819
+ padding: 13px 15px;
820
+ cursor: pointer;
821
+ background: var(--frost-bg);
822
+ border: 1px solid var(--glass-line);
823
+ border-radius: var(--radius-panel-sm);
824
+ transition:
825
+ border-color var(--transition-fast),
826
+ background-color var(--transition-fast),
827
+ transform var(--transition-base),
828
+ box-shadow var(--transition-base);
829
+ }
830
+ @media (hover: hover) and (prefers-reduced-motion: no-preference) {
831
+ .line-card:hover {
832
+ border-color: var(--lc, var(--accent));
833
+ transform: translateY(-1px);
834
+ box-shadow: var(--glass-edge), var(--shadow-float);
835
+ }
836
+ }
837
+ .line-card.on {
838
+ background: color-mix(in srgb, var(--lc) 7%, var(--paper-deep));
839
+ border-color: color-mix(in srgb, var(--lc) 45%, transparent);
840
+ }
841
+ .lc-head {
842
+ display: flex;
843
+ gap: 8px;
844
+ align-items: center;
845
+ }
846
+ .lc-dot {
847
+ flex: 0 0 auto;
848
+ width: 8px;
849
+ height: 8px;
850
+ border: 1.5px solid var(--lc);
851
+ border-radius: 50%;
852
+ }
853
+ .line-card.on .lc-dot {
854
+ background: var(--lc);
855
+ box-shadow: 0 0 7px color-mix(in srgb, var(--lc) 70%, transparent);
856
+ }
857
+ .lc-head b { font-size: 13px; color: var(--ink); }
858
+ .lc-state {
859
+ margin-left: auto;
860
+ padding: 1px 8px;
861
+ font-family: var(--font-mono);
862
+ font-size: 10px;
863
+ letter-spacing: 0.05em;
864
+ color: var(--ink-faint);
865
+ border: 1px solid var(--line-strong);
866
+ border-radius: var(--radius-pill);
867
+ }
868
+ .lc-state.on {
869
+ color: var(--tone-success-dot);
870
+ border-color: color-mix(in srgb, var(--tone-success-dot) 45%, transparent);
871
+ }
872
+ .lc-run {
873
+ display: flex;
874
+ flex-direction: column;
875
+ gap: 2px;
876
+ margin-top: 8px;
877
+ font-size: 11.5px;
878
+ color: color-mix(in srgb, var(--lc) 70%, var(--ink));
879
+ }
880
+ .lc-run small { font-size: 10px; color: var(--ink-faint); }
881
+ .lc-meta {
882
+ display: flex;
883
+ gap: 14px;
884
+ margin-top: 9px;
885
+ padding-top: 8px;
886
+ font-size: 10.5px;
887
+ color: var(--ink-faint);
888
+ border-top: 1px solid var(--divider-hair);
889
+ }
890
+ /* ---------- Harness 可用性条:墨色药丸式引擎清单,未安装灰化 ---------- */
891
+ .harness {
892
+ margin-bottom: var(--gap-block);
893
+ }
894
+ .harness-row {
895
+ display: flex;
896
+ flex-wrap: wrap;
897
+ gap: 8px;
898
+ padding: 2px 0 4px;
899
+ }
900
+ .h-item {
901
+ display: flex;
902
+ gap: 7px;
903
+ align-items: center;
904
+ padding: 5px 12px;
905
+ font-size: 11.5px;
906
+ cursor: default;
907
+ background: color-mix(in srgb, var(--ink) 3%, transparent);
908
+ border: 1px solid var(--divider-hair);
909
+ border-radius: var(--radius-pill);
910
+ transition: border-color 0.15s, background 0.15s;
911
+ }
912
+ .h-item:hover { border-color: var(--line-strong); }
913
+ .h-item.link { cursor: pointer; }
914
+ .h-item.link:hover { border-color: var(--accent); color: var(--accent); }
915
+ .h-go { flex: none; font-size: 10px; color: var(--ink-faint); }
916
+ .h-item.link:hover .h-go { color: var(--accent); }
917
+ .h-dot {
918
+ flex: none;
919
+ width: 6px;
920
+ height: 6px;
921
+ border-radius: 50%;
922
+ background: var(--tone-success-dot, #4a6b57);
923
+ }
924
+ .h-item.off { opacity: 0.55; }
925
+ .h-item.off .h-dot { background: var(--tone-danger-dot, #c25a4e); }
926
+ .h-name { font-weight: 600; color: var(--ink); }
927
+ .h-item.off .h-name { color: var(--ink-soft); }
928
+ /* 引擎命令名是"铭牌"级信息:桌面 10px 尚可,手持档必须让位给可读性
929
+ * (实测 / 在 390 下稳定报出 54 条 <11px,全部来自这里与 .aw-gauge-label) */
930
+ .h-cmd { font-size: 10px; color: var(--ink-faint); }
931
+
932
+ /* 手持档字号地板:本页所有"铭牌级"微字(引擎命令名、产线状态、产线指标行、
933
+ 跳转箭头)统一抬到 11.5px。它们在桌面是仪器铭牌语言,在手机上就是读不了的小字。 */
934
+ @media (max-width: 900px) {
935
+ .h-cmd,
936
+ .h-go,
937
+ .lc-state,
938
+ .lc-run small,
939
+ .lc-meta,
940
+ .lc-meta span,
941
+ .fleet-all small {
942
+ font-size: 11.5px;
943
+ }
944
+ }
945
+ /* 全部产线入口:虚线幽灵卡,聚合清单外的余量 */
946
+ .fleet-all {
947
+ display: flex;
948
+ flex-direction: column;
949
+ gap: 6px;
950
+ align-items: center;
951
+ justify-content: center;
952
+ min-height: 86px;
953
+ color: var(--ink-faint);
954
+ background: transparent;
955
+ border-style: dashed;
956
+ transition: border-color 0.15s, color 0.15s, background 0.15s;
957
+ }
958
+ .fleet-all:hover {
959
+ color: var(--accent);
960
+ background: color-mix(in srgb, var(--accent) 5%, transparent);
961
+ border-color: color-mix(in srgb, var(--accent) 45%, transparent);
962
+ }
963
+ .fa-n {
964
+ font-size: 22px;
965
+ line-height: 1;
966
+ color: var(--ink-soft);
967
+ font-variant-numeric: tabular-nums;
968
+ }
969
+ .fleet-all:hover .fa-n { color: var(--accent); }
970
+ .fa-label { font-size: 12px; }
971
+ .fa-arrow {
972
+ font-size: 15px;
973
+ transition: transform 160ms cubic-bezier(0.22, 1, 0.36, 1);
974
+ }
975
+ .fleet-all:hover .fa-arrow { transform: translateX(3px); }
976
+ .fleet-empty {
977
+ padding: 18px 0;
978
+ font-size: 12px;
979
+ color: var(--ink-faint);
980
+ text-align: center;
981
+ }
982
+
983
+ @media (max-width: 640px) {
984
+ .hero { padding: 24px 20px; }
985
+ .hero-title { font-size: 28px; }
986
+ }
987
+ </style>