agentworkshop 0.7.41 → 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 (265) 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/DKySmNPI.js +14 -0
  11. package/.output/public/_nuxt/Di0NY_ht.js +1 -0
  12. package/.output/public/_nuxt/Du9v6qGB.js +1 -0
  13. package/.output/public/_nuxt/{DGbd-UiG.js → EHlEMRii.js} +1 -1
  14. package/.output/public/_nuxt/Eke1fs-U.js +1 -0
  15. package/.output/public/_nuxt/{DeJykBFf.js → H0BcqZtT.js} +1 -1
  16. package/.output/public/_nuxt/{C4oW1HsE.js → HSYEkkce.js} +1 -1
  17. package/.output/public/_nuxt/{D_OTvQza.js → J9oU00D1.js} +1 -1
  18. package/.output/public/_nuxt/OmpTerminalPanel.BxR5-lvi.css +1 -0
  19. package/.output/public/_nuxt/SSyAPaHc.js +1 -0
  20. package/.output/public/_nuxt/TownView.li4EMQxK.css +1 -0
  21. package/.output/public/_nuxt/VdLk4jLE2.js +1 -0
  22. package/.output/public/_nuxt/_id_.BhsXoEuk.css +1 -0
  23. package/.output/public/_nuxt/_id_.DQyj-WKN.css +1 -0
  24. package/.output/public/_nuxt/{_wsId_.B4WiqkIS.css → _wsId_.CTx-BlfF.css} +1 -1
  25. package/.output/public/_nuxt/aShsv_rG.js +19 -0
  26. package/.output/public/_nuxt/{yF0aYMF1.js → aesgXKSr.js} +3 -3
  27. package/.output/public/_nuxt/agents.B21nUEUX.css +1 -0
  28. package/.output/public/_nuxt/aml.ekTjxFkf.css +1 -0
  29. package/.output/public/_nuxt/builds/latest.json +1 -1
  30. package/.output/public/_nuxt/builds/meta/b5506ef0-f48e-4f14-a6c0-d65e505d0b09.json +1 -0
  31. package/.output/public/_nuxt/channel-templates.DplM1pJ3.css +1 -0
  32. package/.output/public/_nuxt/daq.BdVmoIfe.css +1 -0
  33. package/.output/public/_nuxt/dcw.DCgyzpWV.css +1 -0
  34. package/.output/public/_nuxt/{default.J32SgqFY.css → default.Bh3L2HU8.css} +1 -1
  35. package/.output/public/_nuxt/lHAxdSsj.js +1 -0
  36. package/.output/public/_nuxt/pages.j9rzRocr.css +1 -0
  37. package/.output/public/_nuxt/plugins.DhiZbHU8.css +1 -0
  38. package/.output/public/_nuxt/settings.BRx9rIDg.css +1 -0
  39. package/.output/public/_nuxt/teams.BLlOPB_Z.css +1 -0
  40. package/.output/public/_nuxt/tokens.De8xzszn.css +1 -0
  41. package/.output/public/_nuxt/workshop.D1jFNkrn.css +1 -0
  42. package/.output/server/chunks/_/nitro.mjs +748 -733
  43. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs → OmpTerminalPanel-CVvxpsF7.mjs} +15 -15
  44. package/.output/server/chunks/build/{OmpTerminalPanel-Cu_s4i0A.mjs.map → OmpTerminalPanel-CVvxpsF7.mjs.map} +1 -1
  45. package/.output/server/chunks/build/{TownView--otsnr_7.mjs → TownView-CJ6IsDCc.mjs} +226 -225
  46. package/.output/server/chunks/build/{TownView--otsnr_7.mjs.map → TownView-CJ6IsDCc.mjs.map} +1 -1
  47. package/.output/server/chunks/build/{_id_-Brv-JSvr.mjs → _id_-CVD_H0ir.mjs} +143 -142
  48. package/.output/server/chunks/build/_id_-CVD_H0ir.mjs.map +1 -0
  49. package/.output/server/chunks/build/{_id_-D9pl1OZy.mjs → _id_-CtxMb3qp.mjs} +41 -41
  50. package/.output/server/chunks/build/_id_-CtxMb3qp.mjs.map +1 -0
  51. package/.output/server/chunks/build/{_wsId_-BASfDf0s.mjs → _wsId_-DpY3T-md.mjs} +514 -444
  52. package/.output/server/chunks/build/{_wsId_-BASfDf0s.mjs.map → _wsId_-DpY3T-md.mjs.map} +1 -1
  53. package/.output/server/chunks/build/{agents-CLQqJF2y.mjs → agents-DsCkuXDV.mjs} +49 -46
  54. package/.output/server/chunks/build/agents-DsCkuXDV.mjs.map +1 -0
  55. package/.output/server/chunks/build/{aml-_7FdFL2p.mjs → aml-CT0xKGKI.mjs} +122 -105
  56. package/.output/server/chunks/build/aml-CT0xKGKI.mjs.map +1 -0
  57. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs +117 -0
  58. package/.output/server/chunks/build/builtin-catalog-edpDcgfL.mjs.map +1 -0
  59. package/.output/server/chunks/build/{channel-templates-p6SZNjlM.mjs → channel-templates-Dla9FbYa.mjs} +49 -37
  60. package/.output/server/chunks/build/channel-templates-Dla9FbYa.mjs.map +1 -0
  61. package/.output/server/chunks/build/{daq-DkMvD8sg.mjs → daq-DWWSKGAV.mjs} +89 -88
  62. package/.output/server/chunks/build/daq-DWWSKGAV.mjs.map +1 -0
  63. package/.output/server/chunks/build/{dcw-C1jgVJkO.mjs → dcw-CT8k17_E.mjs} +44 -43
  64. package/.output/server/chunks/build/dcw-CT8k17_E.mjs.map +1 -0
  65. package/.output/server/chunks/build/{default-BE4r0FZN.mjs → default-D3525UFX.mjs} +16 -15
  66. package/.output/server/chunks/build/{default-BE4r0FZN.mjs.map → default-D3525UFX.mjs.map} +1 -1
  67. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs → monitor-Dh2Kvaq5.mjs} +2 -2
  68. package/.output/server/chunks/build/{monitor-C0okQaXz.mjs.map → monitor-Dh2Kvaq5.mjs.map} +1 -1
  69. package/.output/server/chunks/build/{pages-B0bUWnC6.mjs → pages-BHcSBM2D.mjs} +24 -18
  70. package/.output/server/chunks/build/pages-BHcSBM2D.mjs.map +1 -0
  71. package/.output/server/chunks/build/{plugins-lGlUr1lj.mjs → plugins-BNgIi61W.mjs} +23 -17
  72. package/.output/server/chunks/build/plugins-BNgIi61W.mjs.map +1 -0
  73. package/.output/server/chunks/build/{settings-Ddd1qx2Z.mjs → settings-ipYBDWdn.mjs} +45 -44
  74. package/.output/server/chunks/build/settings-ipYBDWdn.mjs.map +1 -0
  75. package/.output/server/chunks/build/{teams-MEDLZdMn.mjs → teams-5lx-CwFa.mjs} +47 -44
  76. package/.output/server/chunks/build/teams-5lx-CwFa.mjs.map +1 -0
  77. package/.output/server/chunks/build/{tokens-C7fTM5C2.mjs → tokens-q-IDT8Io.mjs} +73 -53
  78. package/.output/server/chunks/build/tokens-q-IDT8Io.mjs.map +1 -0
  79. package/.output/server/chunks/build/{town-CoQdblbZ.mjs → town-QJWB5wng.mjs} +3 -2
  80. package/.output/server/chunks/build/town-QJWB5wng.mjs.map +1 -0
  81. package/.output/server/chunks/build/{workshop-BL5bhfs0.mjs → workshop-BIlCj_ZP.mjs} +45 -45
  82. package/.output/server/chunks/build/workshop-BIlCj_ZP.mjs.map +1 -0
  83. package/.output/server/chunks/routes/renderer.mjs +1 -1
  84. package/.output/server/chunks/virtual/entry.mjs +637 -86
  85. package/.output/server/chunks/virtual/entry.mjs.map +1 -1
  86. package/.output/server/chunks/virtual/precomputed.mjs +1 -1
  87. package/.output/server/package.json +1 -1
  88. package/README-zh.md +13 -3
  89. package/README.md +13 -3
  90. package/app/components/AppHeader.vue +961 -959
  91. package/app/components/workshop/A2aRpcDebugger.vue +237 -237
  92. package/app/components/workshop/AgentInspectorDrawer.vue +424 -424
  93. package/app/components/workshop/AgentLanesView.vue +998 -998
  94. package/app/components/workshop/ArtifactCard.vue +120 -120
  95. package/app/components/workshop/AssetLibrary.vue +452 -452
  96. package/app/components/workshop/ChannelSessionList.vue +812 -804
  97. package/app/components/workshop/CommandPalette.vue +269 -269
  98. package/app/components/workshop/Composer.vue +827 -827
  99. package/app/components/workshop/DeviceTwinPanel.vue +643 -643
  100. package/app/components/workshop/EventBlock.vue +602 -602
  101. package/app/components/workshop/FileSelectorModal.vue +188 -188
  102. package/app/components/workshop/InspectorPanel.vue +301 -301
  103. package/app/components/workshop/MemoryPanel.vue +364 -364
  104. package/app/components/workshop/MultiChannelView.vue +189 -189
  105. package/app/components/workshop/TaskBoardView.vue +686 -686
  106. package/app/components/workshop/TaskInspectorDrawer.vue +390 -390
  107. package/app/components/workshop/TranscriptTimeline.vue +601 -601
  108. package/app/components/workshop/blocks/ClusterArtifact.vue +61 -61
  109. package/app/components/workshop/blocks/ClusterError.vue +85 -85
  110. package/app/components/workshop/blocks/ClusterLife.vue +87 -87
  111. package/app/components/workshop/blocks/ClusterMember.vue +95 -93
  112. package/app/components/workshop/blocks/ClusterMemory.vue +84 -84
  113. package/app/components/workshop/blocks/ClusterOther.vue +67 -67
  114. package/app/components/workshop/blocks/ClusterStatus.vue +149 -147
  115. package/app/components/workshop/blocks/ClusterTool.vue +158 -158
  116. package/app/components/workshop/terminal/OmpTerminalPanel.vue +1011 -1011
  117. package/app/components/workshop/town/TownView.vue +56 -56
  118. package/app/middleware/locale-cookie.global.ts +22 -0
  119. package/app/pages/aml/index.vue +24 -2
  120. package/app/pages/daq/[id].vue +11 -11
  121. package/app/pages/daq/index.vue +2316 -2316
  122. package/app/pages/dcw/[id].vue +18 -19
  123. package/app/pages/dcw/index.vue +3 -3
  124. package/app/pages/index.vue +987 -981
  125. package/app/pages/plugins/index.vue +317 -311
  126. package/app/pages/settings.vue +1564 -1562
  127. package/app/pages/tokens.vue +15 -15
  128. package/app/pages/workshop/agents.vue +612 -609
  129. package/app/pages/workshop/channel-templates.vue +18 -11
  130. package/app/pages/workshop/index.vue +668 -668
  131. package/app/pages/workshop/teams.vue +743 -740
  132. package/app/pages/workshop/w/[wsId].vue +704 -704
  133. package/app/plugins/locale-restore.client.ts +16 -1
  134. package/app/utils/builtin-catalog.ts +104 -0
  135. package/config.yml +170 -170
  136. package/docs/full-test-plan.md +2 -3
  137. package/i18n/locales/en.ts +293 -35
  138. package/i18n/locales/zh-CN.ts +265 -7
  139. package/package.json +2 -2
  140. package/scripts/_dbg-perms-e2e.mjs +132 -109
  141. package/scripts/_demo-agentteam-capture.mjs +278 -0
  142. package/scripts/_e2e-ui-functional.mjs +371 -0
  143. package/scripts/_i18n-en-cjk-scan.mjs +28 -0
  144. package/scripts/_i18n-en-verify.mjs +107 -0
  145. package/scripts/_i18n-extra-checks.mjs +53 -0
  146. package/scripts/_i18n-scan-hardcoded.mjs +180 -0
  147. package/scripts/_i18n-verify-keys.mjs +50 -0
  148. package/scripts/_loop-biax-campaign.mjs +15 -9
  149. package/scripts/_sweep-button-contrast.mjs +87 -0
  150. package/scripts/ui/record-demo.mjs +557 -0
  151. package/server/plugins-builtin/diag-bridge/index.mjs +608 -661
  152. package/server/plugins-builtin/rag-bridge/index.mjs +57 -133
  153. package/server/services/workshop/daq/drivers.ts +11 -0
  154. package/.output/public/_nuxt/88GOowgO2.js +0 -1
  155. package/.output/public/_nuxt/BN16aRdQ.js +0 -1
  156. package/.output/public/_nuxt/BWVhZntO.js +0 -1
  157. package/.output/public/_nuxt/BXxH0UQE.js +0 -1
  158. package/.output/public/_nuxt/BsROmY89.js +0 -1
  159. package/.output/public/_nuxt/C0ZrS1KL.js +0 -19
  160. package/.output/public/_nuxt/C4s_niy-.js +0 -14
  161. package/.output/public/_nuxt/D6mbFA-a.js +0 -1
  162. package/.output/public/_nuxt/DS8qj8EL.js +0 -1
  163. package/.output/public/_nuxt/DUjW5Yq5.js +0 -1
  164. package/.output/public/_nuxt/Dap5PmTL.js +0 -2
  165. package/.output/public/_nuxt/Dqw1lpr-.js +0 -1
  166. package/.output/public/_nuxt/DvLxILQP.js +0 -1
  167. package/.output/public/_nuxt/OmpTerminalPanel.B_Pl5Ynm.css +0 -1
  168. package/.output/public/_nuxt/TownView.f-YOdXRs.css +0 -1
  169. package/.output/public/_nuxt/_id_.D9ymtHB5.css +0 -1
  170. package/.output/public/_nuxt/_id_.jf-_8E_c.css +0 -1
  171. package/.output/public/_nuxt/agents.ZD0rVdHE.css +0 -1
  172. package/.output/public/_nuxt/aml.BN8ZEW4h.css +0 -1
  173. package/.output/public/_nuxt/builds/meta/78a295bb-2072-48a4-8b46-6d545fdae743.json +0 -1
  174. package/.output/public/_nuxt/channel-templates.BOtahzPi.css +0 -1
  175. package/.output/public/_nuxt/daq.DjSgY94l.css +0 -1
  176. package/.output/public/_nuxt/dcw.8Y7FqCVN.css +0 -1
  177. package/.output/public/_nuxt/eqq3wWQF.js +0 -18
  178. package/.output/public/_nuxt/pages.D7aSDlUI.css +0 -1
  179. package/.output/public/_nuxt/plugins.sb4nDGqr.css +0 -1
  180. package/.output/public/_nuxt/qr6R6Ha1.js +0 -1
  181. package/.output/public/_nuxt/settings.BTgR-NiE.css +0 -1
  182. package/.output/public/_nuxt/teams.CxnxKVVs.css +0 -1
  183. package/.output/public/_nuxt/tokens.BFY7qv4f.css +0 -1
  184. package/.output/public/_nuxt/workshop.CR_QzeAN.css +0 -1
  185. package/.output/server/chunks/build/_id_-Brv-JSvr.mjs.map +0 -1
  186. package/.output/server/chunks/build/_id_-D9pl1OZy.mjs.map +0 -1
  187. package/.output/server/chunks/build/agents-CLQqJF2y.mjs.map +0 -1
  188. package/.output/server/chunks/build/aml-_7FdFL2p.mjs.map +0 -1
  189. package/.output/server/chunks/build/channel-templates-p6SZNjlM.mjs.map +0 -1
  190. package/.output/server/chunks/build/daq-DkMvD8sg.mjs.map +0 -1
  191. package/.output/server/chunks/build/dcw-C1jgVJkO.mjs.map +0 -1
  192. package/.output/server/chunks/build/pages-B0bUWnC6.mjs.map +0 -1
  193. package/.output/server/chunks/build/plugins-lGlUr1lj.mjs.map +0 -1
  194. package/.output/server/chunks/build/settings-Ddd1qx2Z.mjs.map +0 -1
  195. package/.output/server/chunks/build/teams-MEDLZdMn.mjs.map +0 -1
  196. package/.output/server/chunks/build/tokens-C7fTM5C2.mjs.map +0 -1
  197. package/.output/server/chunks/build/town-CoQdblbZ.mjs.map +0 -1
  198. package/.output/server/chunks/build/workshop-BL5bhfs0.mjs.map +0 -1
  199. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.continue.json +0 -1
  200. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.json +0 -1
  201. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/054bb4bc4e4217a2ab0b0c6b663747214a9af464f5568ea60e784704e340bb0f.source +0 -153
  202. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/0554197c3d2bac203a1f5c48a5d52495d1336db1bd7aa4f4c19875810363e684.source +0 -153
  203. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/05c203d12a954fffba942e1d626e3f5fec05b18a5aef2bac003b7884a4b77406.source +0 -431
  204. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/145a6e6ba811af35aed31e0cc3211f53706a14980fa069ee9e3c1591139f96f0.source +0 -180
  205. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/1ae0fb281f97899c025e368ca73f41349b01fb5f85b19e08d7ebccff49095ed5.source +0 -112
  206. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/20ee9967aa0541c516db7575bdd4a1a745939bc7016bd10d7635903113edb8ec.source +0 -227
  207. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/233751843aeba9d4c5263f275b8102de00443c06f286918cbd23e0b6729e01aa.source +0 -131
  208. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/263c2e05b425e5c322e60c6475279f963bab3a873eb521b21df7fec935a46597.source +0 -160
  209. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/2ac5eaac28eaaee2178c4b32014df8e732f9dd432a285a872f5b7f31c1395fa4.source +0 -157
  210. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/33a0db0d2d95d2c11d0720090458e5fba1425376f5ceb5b422d1d3471be3226b.source +0 -348
  211. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/359e474ae83db35e4615d3728de6bf694b64d4dce9b536730a3b6518f9c0494a.source +0 -296
  212. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/37a4c3c31c4b70f6b719c53d1e9c4fcba603f31adfdd11bb6ef9f0d094923c56.source +0 -285
  213. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/461feca1c14a695eb8b0f17a6f95fe97eeceabc0510faaea4ba6d25bde147d8d.source +0 -217
  214. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/4cfadbda8591f29d5e1e230dc000e701b756e850441b3900fb596a4d2f6e98c2.source +0 -301
  215. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/50ca2810cf0b248c96948994c0c404531bbad6e553166b6816b3b03bfa0f75d2.source +0 -151
  216. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/52d9e8cb591a9ae86e238c3620a610cd386717bee222c0f37b25b2ac7e523dc4.source +0 -135
  217. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/6c87daafa4c006560a954636a97333f6123153c9d34d96203f7ee3a14e556357.source +0 -156
  218. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/77bc47854b577900621cacf1697f81fa2bc005af129d37614a97a3441c08f99f.source +0 -72
  219. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/7bbb789d3a527817118cf8399ed1861d6a962ade10878fd07223435d333a7089.source +0 -469
  220. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/86b7c1daafc8b60ba9982dde5bf1bcc74265709483c176e71f23ec6fc267451f.source +0 -259
  221. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/8df66d62bd41984d0970e8236882080c5085ac937d07a29779463d6a03b37c76.source +0 -97
  222. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/9962f2d1b3a85137bd40cc835725600b39f89ee02a27c06b2e0402e1eafde115.source +0 -277
  223. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a1090ea066b792cb201739f4994e37bf7c12f52e805239a1eab87f18a537f266.source +0 -171
  224. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/a5241091650134d0919897e086a606a3846c8b2b54f8b4c84a8696c145ac3e9e.source +0 -425
  225. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/af19fa4c6e34d5540028be4b17600161283af3a2a2dfc61a3fdc0834436a0c14.source +0 -99
  226. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3713d78e2b2f93905ff6be8d58a8d0a2f42dea322eb67c987fda22ff6a084d5.source +0 -325
  227. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/b3849943c3aff102d1af815631fc843a2a52c17646a4b04696ece323e483bbec.source +0 -332
  228. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/bac735a3803a6650b7f1eb8f587e6dc5c68ea055000c3c99a11789438b78e837.source +0 -43
  229. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d32c24d61eb8b33c73e5a8fc7e4dc7ef437aed238ee4371c2b00617e6104090a.source +0 -154
  230. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/d5170dc9a9879fda04fdf91d8a110d4efb6787ee38dda596d4da899883d36a72.source +0 -531
  231. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/dc59bcd7bb397be63ba2f2c3fec23f3782dcb17d2d75a9981eeced761f33eb32.source +0 -439
  232. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/eb5ff8cf66b92aec7bf6726781d8df74faa2b030c3aaac17d6b00c1fc04c9b32.source +0 -323
  233. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f10615a6fae18436e70d1201b2d44fdae3174a3aa7bb4db97dea1b3ed291d721.source +0 -433
  234. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f36d3e271668bc1260c2823542d22dbf1d0f7041ff16b1ac175bce2256e582ca.source +0 -280
  235. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/f94449380af09f31ac352152007146a75cc80bae8d6411ffe57b2c56f933b0ff.source +0 -117
  236. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/fe4dfd81178fd6b648335f6405d9ede71319aebde796008e50db21f8adf44c99.source +0 -160
  237. package/scripts/.mimosa/hook-state/sess_aeaecb58-8833-4b83-b93c-050467d21e03.mtxwtxme-61028-1f4519fbc4.baseline/ffb4e3aa548cfdadabaa44ab599f9b6a3ca6288176a2cc7a4b2fc2a12b5fb848.source +0 -101
  238. package/scripts/.mimosa/hook-status/sess_aeaecb58-8833-4b83-b93c-050467d21e03-d944d71bb3.json +0 -14
  239. package/scripts/_dbg-audit-all-pages.mjs +0 -43
  240. package/scripts/_dbg-daq-frames-e2e.mjs +0 -118
  241. package/scripts/_dbg-daq-rowstyle-verify.mjs +0 -112
  242. package/scripts/_dbg-hitl-timeout-e2e.mjs +0 -93
  243. package/scripts/_dbg-live-line-e2e.mjs +0 -279
  244. package/scripts/_dbg-minimal-omp.mjs +0 -77
  245. package/scripts/_dbg-mode-nav.mjs +0 -173
  246. package/scripts/_dbg-real-scenario-timeline.log +0 -55
  247. package/scripts/_dbg-team-repro.mjs +0 -76
  248. package/scripts/_dbg-town-agents-probe.mjs +0 -60
  249. package/scripts/_dbg-visual-acceptance.mjs +0 -63
  250. package/scripts/_townview_style.css +0 -1185
  251. package/scripts/_townview_template.html +0 -1215
  252. package/server/data/agent-node-bindings.json +0 -554
  253. package/server/data/daq-templates.json +0 -16
  254. package/server/data/daqs.json +0 -7464
  255. package/server/data/dcw-lines.json +0 -1787
  256. package/server/data/dcw-products.json +0 -1626
  257. package/server/data/dcw-recipes.json +0 -4672
  258. package/server/data/dcw-rollback.json +0 -20754
  259. package/server/data/dcw-runs.json +0 -6354
  260. package/server/data/dcw-templates.json +0 -146
  261. package/server/data/dcw-writes.json +0 -15650
  262. package/server/data/dcws.json +0 -9033
  263. package/server/data/device-twins.json +0 -133
  264. package/server/data/line-runs.json +0 -322
  265. 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>