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,643 +1,643 @@
1
- <script setup lang="ts">
2
- /**
3
- * 设备控制台(Device Twin Console) —— 工业数字孪生 HMI:
4
- * 设备实体直接列表展示(状态灯/遥测数据网格/下发控制),点击行聚焦小镇场景中的对应实体。
5
- *
6
- * 与 3D 小镇联动:拖 `dev` 模型进场景会创建设备 twin;本面板实时刷新其 state/telemetry,
7
- * 并展示绑定智控通道的当前 set 值,支持窗口内直写下发(替代旧 power_on/power_off/set_speed)。
8
- */
9
- import { useDeviceTwins } from '@/app/composables/workshop/useDeviceTwins'
10
- import { useDcwStream } from '@/app/composables/workshop/useDcwStream'
11
-
12
- const { t } = useI18n()
13
-
14
- /** 实时数采数据(twinId → 该实体相关的实时通道:
15
- * 数采节点 = 自身通道;设备 = 绑定到它的全部数采通道;由 TownView 从模拟上报喂入) */
16
- interface DaqLiveRow { ch: string, value: string, unit: string, alarm?: boolean }
17
- /** 智控设定数据(twinId → 该设备绑定的全部智控通道;含当前 set 值与生效上下限) */
18
- export interface DcwLiveRow {
19
- id: string
20
- ch: string
21
- name: string
22
- unit: string
23
- /** 当前设定值(工程量;null = 从未下发) */
24
- value: number | null
25
- /** PLC 当前读数(读写集成;null = 从未读到或驱动不支持) */
26
- readValue: number | null
27
- lastReadAt: string | null
28
- decimals: number
29
- /** 生效下/上限(活动配方工艺窗口优先,否则节点全局量程;-∞/+∞ 表示不限定) */
30
- lo: number
31
- hi: number
32
- src: 'recipe' | 'global'
33
- }
34
- defineProps<{
35
- daqLive?: Record<string, DaqLiveRow[]>
36
- dcwLive?: Record<string, DcwLiveRow[]>
37
- }>()
38
-
39
- defineEmits<{ (e: 'focus-device', twin: { id: string, posX?: number, posZ?: number }): void }>()
40
-
41
- const twins = useDeviceTwins()
42
- const dcw = useDcwStream()
43
-
44
- /** 数采实体(绿卡):kind=daq 或旧数据 modelRef 前缀兜底 */
45
- const isDaq = (t: { kind?: string, modelRef?: string }): boolean =>
46
- t.kind === 'daq' || (t.modelRef ?? '').startsWith('daq-')
47
-
48
- // 节流轮询刷新(不打断拖拽/交互)
49
- let timer: ReturnType<typeof setInterval> | null = null
50
- onMounted(() => {
51
- void twins.load()
52
- timer = setInterval(() => void twins.load(), 2000)
53
- })
54
- onBeforeUnmount(() => {
55
- if (timer) clearInterval(timer)
56
- })
57
-
58
- const busyId = ref('')
59
- const ctrlMsg = ref('')
60
- /** 行内删除(两步确认:首击布防 3s,再击执行) */
61
- const armedId = ref('')
62
- let armedTimer: ReturnType<typeof setTimeout> | null = null
63
- async function removeTwin(t: { id: string, name: string }): Promise<void> {
64
- if (armedId.value !== t.id) {
65
- armedId.value = t.id
66
- if (armedTimer) clearTimeout(armedTimer)
67
- armedTimer = setTimeout(() => {
68
- armedId.value = ''
69
- }, 3000)
70
- return
71
- }
72
- if (armedTimer) clearTimeout(armedTimer)
73
- armedId.value = ''
74
- busyId.value = t.id
75
- ctrlMsg.value = ''
76
- try {
77
- await twins.remove(t.id)
78
- }
79
- catch (err) {
80
- ctrlMsg.value = apiErrorMessage(err)
81
- }
82
- finally {
83
- busyId.value = ''
84
- }
85
- }
86
- const stateColor: Record<string, string> = {
87
- idle: 'var(--hud-amber)',
88
- running: 'var(--hud-ok)',
89
- alarm: 'var(--hud-danger)',
90
- offline: 'var(--hud-dim)',
91
- }
92
- const stateLabel: Record<string, string> = {
93
- idle: 'STANDBY', running: 'RUNNING', alarm: 'ALARM', offline: 'OFFLINE',
94
- }
95
- const modelTag = (name: string): string => name.replace(/[^A-Za-z0-9]/g, '').slice(0, 4).toUpperCase() || 'DEV'
96
- const devNo = (id: string): string => id.replace(/^dev-/, '').replace(/[^a-z0-9]/gi, '').slice(-6).toUpperCase()
97
- const fmt = (v: unknown): string => (typeof v === 'number' ? (Math.round(v * 100) / 100).toString() : String(v))
98
-
99
- // ---------- 智控设定(每张设备卡:当前 set 值展示 + 窗口内直写下发) ----------
100
- /** 智控上下限文案(无穷大显示 ±∞) */
101
- const dcwWinText = (r: DcwLiveRow): string =>
102
- `${Number.isFinite(r.lo) ? r.lo : '-∞'} ~ ${Number.isFinite(r.hi) ? r.hi : '+∞'} ${r.unit}`
103
- /** 写入草稿/错误(智控节点 id 键控) */
104
- const dcwDrafts = reactive<Record<string, number | ''>>({})
105
- const dcwErrs = reactive<Record<string, string>>({})
106
- const dcwBusy = ref('')
107
- /** 下发设定值:先本地窗口校验(不越限),再走 server write(配方联锁二道门) */
108
- function doDcwWrite(r: DcwLiveRow): void {
109
- const raw = dcwDrafts[r.id]
110
- if (raw == null || raw === '') return
111
- const v = Number(raw)
112
- if (v < r.lo || v > r.hi) {
113
- dcwErrs[r.id] = t('deviceTwinPanel.kxddket010', { p0: r.src === 'recipe' ? t('deviceTwinPanel.kq2jssk008') : t('deviceTwinPanel.k1iwj796009'), p1: dcwWinText(r) })
114
- return
115
- }
116
- dcwErrs[r.id] = ''
117
- dcwBusy.value = r.id
118
- void dcw.write(r.id, v).then((out) => {
119
- if (!out.ok) dcwErrs[r.id] = out.message
120
- }).catch((err: unknown) => {
121
- dcwErrs[r.id] = apiErrorMessage(err)
122
- }).finally(() => {
123
- dcwBusy.value = ''
124
- })
125
- }
126
- </script>
127
-
128
- <template>
129
- <aside class="twin-panel">
130
- <div class="twin-head">
131
- <span class="twin-kicker">{{ $t('deviceTwinPanel.k7k6evr002') }}</span>
132
- <span class="twin-count">{{ twins.twins.length }}</span>
133
- </div>
134
-
135
- <!-- 加载失败横幅:错误可见,旧数据保留不清空 -->
136
- <div
137
- v-if="twins.error"
138
- class="twin-error"
139
- >
140
- {{ twins.error }}
141
- </div>
142
-
143
- <div
144
- v-if="!twins.loaded"
145
- class="twin-empty"
146
- >
147
- {{ $t('deviceTwinPanel.kkw7sn5003') }}
148
- </div>
149
- <div
150
- v-else-if="twins.twins.length === 0"
151
- class="twin-empty"
152
- >
153
- <div class="twin-empty-frame">
154
- <div class="twin-empty-title">
155
- {{ $t('deviceTwinPanel.k13y2qmg004') }}
156
- </div>
157
- <div class="twin-empty-sub">
158
- {{ $t('deviceTwinPanel.kotf23l005') }}
159
- </div>
160
- </div>
161
- </div>
162
- <div
163
- v-else
164
- class="twin-list"
165
- >
166
- <button
167
- v-for="tw in twins.twins"
168
- :key="tw.id"
169
- class="twin-card"
170
- :class="{ daq: isDaq(tw) }"
171
- type="button"
172
- @click="$emit('focus-device', tw)"
173
- >
174
- <div class="twin-row">
175
- <span class="twin-model-tag">{{ modelTag(tw.modelRef || tw.name) }}</span>
176
- <div class="twin-idbar">
177
- <span class="twin-name">{{ tw.name }}</span>
178
- <span class="twin-code">{{ isDaq(tw) ? 'DAQ' : devNo(tw.id) }}</span>
179
- </div>
180
- <button
181
- class="twin-del"
182
- :class="{ armed: armedId === tw.id }"
183
- :disabled="busyId === tw.id"
184
- :title="armedId === tw.id ? '再次点击确认删除该设备实例' : '删除该设备实例'"
185
- @click.stop="removeTwin(tw)"
186
- >
187
- {{ armedId === tw.id ? $t('deviceTwinPanel.k44653007') : '✕' }}
188
- </button>
189
- <span
190
- class="twin-state"
191
- :class="`s-${tw.state}`"
192
- >
193
- <i
194
- class="twin-state-dot"
195
- :style="{ background: stateColor[tw.state] || 'var(--hud-dim)' }"
196
- />
197
- {{ stateLabel[tw.state] || tw.state }}
198
- </span>
199
- </div>
200
- <div
201
- v-if="Object.keys(tw.telemetry).length"
202
- class="twin-tele"
203
- >
204
- <span
205
- v-for="(v, k) in tw.telemetry"
206
- :key="k"
207
- class="tele-item"
208
- ><em>{{ k }}</em><b>{{ fmt(v) }}</b></span>
209
- </div>
210
- <!-- 实时数采(绿色;数采节点 = 自身通道,设备 = 绑定通道) -->
211
- <div
212
- v-if="daqLive?.[tw.id]?.length"
213
- class="twin-daq"
214
- >
215
- <span
216
- v-for="(d, i) in daqLive[tw.id]"
217
- :key="`${d.ch}-${i}`"
218
- class="daq-item"
219
- :class="{ alarm: d.alarm }"
220
- >
221
- <em>{{ d.ch }}</em>
222
- <b>{{ d.value }}<i>{{ d.unit }}</i></b>
223
- </span>
224
- </div>
225
- <!-- 智控设定(绑定通道的当前 set 值展示 + 生效上下限 + 窗口内直写下发) -->
226
- <div
227
- v-if="dcwLive?.[tw.id]?.length"
228
- class="twin-dcw"
229
- >
230
- <div
231
- v-for="r in dcwLive[tw.id]"
232
- :key="r.id"
233
- class="dcw-item"
234
- >
235
- <div class="dcw-head">
236
- <em>{{ r.ch }}<i
237
- v-if="r.src === 'recipe'"
238
- class="dcw-src"
239
- >{{ $t('deviceTwinPanel.k48grv006') }}</i></em>
240
- <b class="dcw-set">{{ r.value != null ? r.value.toFixed(r.decimals) : '--' }}<i>{{ r.unit }}</i></b>
241
- </div>
242
- <div
243
- v-if="r.readValue != null || r.lastReadAt"
244
- class="dcw-act"
245
- :title="$t('deviceTwinPanel.k9r7d4e030')"
246
- >
247
- <em>ACT</em>
248
- <b>{{ r.readValue != null ? r.readValue.toFixed(r.decimals) : '--' }}<i>{{ r.unit }}</i></b>
249
- <i
250
- v-if="r.lastReadAt"
251
- class="dcw-act-at"
252
- >{{ r.lastReadAt.slice(11, 19) }}</i>
253
- </div>
254
- <div class="dcw-win">
255
- <span :title="r.src === 'recipe' ? '当前运行配方工艺窗口' : '节点全局量程'">{{ dcwWinText(r) }}</span>
256
- </div>
257
- <div class="dcw-ctrl">
258
- <input
259
- v-model.number="dcwDrafts[r.id]"
260
- type="number"
261
- class="ctrl-input"
262
- :step="10 ** -r.decimals"
263
- :min="Number.isFinite(r.lo) ? r.lo : undefined"
264
- :max="Number.isFinite(r.hi) ? r.hi : undefined"
265
- :placeholder="`${Number.isFinite(r.lo) ? r.lo : ''} ~ ${Number.isFinite(r.hi) ? r.hi : ''}`"
266
- @keydown.enter="doDcwWrite(r)"
267
- >
268
- <button
269
- class="ctrl-btn dcw-send"
270
- :disabled="dcwDrafts[r.id] == null || dcwDrafts[r.id] === '' || dcwBusy === r.id"
271
- :title="$t('deviceTwinPanel.k1j04u8001')"
272
- @click.stop="doDcwWrite(r)"
273
- >
274
- {{ dcwBusy === r.id ? '···' : 'SET' }}
275
- </button>
276
- </div>
277
- <small
278
- v-if="dcwErrs[r.id]"
279
- class="dcw-err"
280
- >{{ dcwErrs[r.id] }}</small>
281
- </div>
282
- </div>
283
- </button>
284
- </div>
285
-
286
- <span
287
- v-if="ctrlMsg"
288
- class="twin-err"
289
- >{{ ctrlMsg }}</span>
290
- </aside>
291
- </template>
292
-
293
- <style scoped>
294
- .twin-panel {
295
- display: flex;
296
- flex-direction: column;
297
- gap: 8px;
298
- width: auto;
299
- flex: none;
300
- padding: 0;
301
- background: transparent;
302
- border: 0;
303
- border-radius: 0;
304
- box-shadow: none;
305
- }
306
- .twin-head {
307
- display: flex;
308
- gap: 8px;
309
- align-items: center;
310
- padding: 10px 12px 6px;
311
- border-bottom: 1px solid rgba(38, 51, 64, 0.45);
312
- }
313
- /* 加载失败横幅:错误可见(旧数据保留,不再静默清空) */
314
- .twin-error {
315
- padding: 7px 10px;
316
- font-size: 11px;
317
- line-height: 1.5;
318
- color: #ffb4b4;
319
- background: rgba(255, 107, 107, 0.1);
320
- border: 1px solid rgba(255, 107, 107, 0.35);
321
- border-radius: 8px;
322
- word-break: break-all;
323
- }
324
- .twin-kicker {
325
- font-family: var(--font-mono);
326
- font-size: 10px;
327
- font-weight: 600;
328
- letter-spacing: 0.16em;
329
- color: var(--hud-text, #d9e4ee);
330
- }
331
- .twin-count {
332
- margin-left: auto;
333
- font-family: var(--font-mono);
334
- font-size: 10px;
335
- font-variant-numeric: tabular-nums;
336
- color: var(--hud-dim, #8496a5);
337
- }
338
- .twin-empty {
339
- padding: 10px;
340
- }
341
- .twin-empty-frame {
342
- border: 1px dashed var(--hud-line);
343
- padding: 12px 10px;
344
- display: flex;
345
- flex-direction: column;
346
- gap: 4px;
347
- }
348
- .twin-empty-title {
349
- font-family: var(--font-mono);
350
- font-size: 11px;
351
- letter-spacing: 0.1em;
352
- color: var(--hud-text);
353
- }
354
- .twin-empty-sub {
355
- font-size: 10px;
356
- line-height: 1.6;
357
- color: var(--hud-dim);
358
- }
359
- .twin-list {
360
- display: flex;
361
- flex-direction: column;
362
- gap: 4px;
363
- max-height: 46vh;
364
- overflow: hidden auto;
365
- padding: 4px 0 10px;
366
- }
367
- .twin-card {
368
- display: flex;
369
- flex-direction: column;
370
- gap: 6px;
371
- margin: 0 10px;
372
- padding: 8px 9px;
373
- text-align: left;
374
- background: rgba(20, 27, 38, 0.65);
375
- border: 0;
376
- border-radius: 2px;
377
- cursor: pointer;
378
- transition: background 0.16s ease;
379
- }
380
- .twin-card:hover {
381
- background: var(--hud-panel-hover);
382
- }
383
- .twin-card:active {
384
- background: var(--hud-panel-raised);
385
- }
386
- .twin-card:hover .twin-name { color: #e8f1f8; }
387
- /* 数采节点绿卡(用户指定:设备监控中数采卡为绿色) */
388
- .twin-card.daq {
389
- background: rgba(53, 224, 160, 0.06);
390
- border: 1px solid rgba(53, 224, 160, 0.32);
391
- }
392
- .twin-card.daq:hover {
393
- background: rgba(53, 224, 160, 0.11);
394
- border-color: rgba(53, 224, 160, 0.5);
395
- }
396
- .twin-card.daq .twin-model-tag,
397
- .twin-card.daq .twin-code { color: var(--hud-accent, #35e0a0); }
398
- .twin-row { display: flex; gap: 8px; align-items: center; min-width: 0; }
399
- .twin-model-tag {
400
- flex: none;
401
- min-width: 30px;
402
- font-family: var(--font-mono);
403
- /* 9px 的 mono 铭牌在 1440 下也读不动(实测每屏稳定被判为不可读);
404
- 10px 是孪生侧栏这套 HUD 在桌面档的地板。 */
405
- font-size: 10px;
406
- font-weight: 600;
407
- letter-spacing: 0.08em;
408
- color: var(--hud-faint);
409
- }
410
- .twin-idbar { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
411
- .twin-name {
412
- font-size: 11.5px;
413
- font-weight: 600;
414
- color: var(--hud-text);
415
- overflow: hidden;
416
- text-overflow: ellipsis;
417
- white-space: nowrap;
418
- }
419
- .twin-code {
420
- font-family: var(--font-mono);
421
- font-size: 10px;
422
- letter-spacing: 0.08em;
423
- color: var(--hud-dim);
424
- }
425
- .twin-del {
426
- flex: none;
427
- width: 18px;
428
- height: 18px;
429
- font-family: var(--font-mono);
430
- font-size: 10px;
431
- line-height: 1;
432
- color: var(--hud-dim, #8496a5);
433
- background: transparent;
434
- border: 1px solid var(--hud-line, #263340);
435
- border-radius: 2px;
436
- cursor: pointer;
437
- transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease;
438
- }
439
- .twin-del:hover:not(:disabled) {
440
- border-color: var(--hud-danger, #ff6b5c);
441
- color: var(--hud-danger, #ff6b5c);
442
- }
443
- .twin-del.armed {
444
- width: auto;
445
- padding: 0 5px;
446
- color: #1a0d0a;
447
- background: var(--hud-danger, #ff6b5c);
448
- border-color: var(--hud-danger, #ff6b5c);
449
- }
450
- .twin-del:disabled { opacity: 0.4; cursor: default; }
451
- .twin-state {
452
- flex: none;
453
- display: inline-flex;
454
- gap: 5px;
455
- align-items: center;
456
- font-family: var(--font-mono);
457
- font-size: 10px;
458
- letter-spacing: 0.08em;
459
- color: var(--hud-dim);
460
- padding: 2px 5px;
461
- border: 1px solid var(--hud-line);
462
- border-radius: 2px;
463
- }
464
- .twin-state.s-alarm { color: var(--hud-danger); border-color: rgba(255, 107, 92, 0.5); }
465
- .twin-state.s-running { color: var(--hud-ok); border-color: rgba(127, 212, 160, 0.4); }
466
- .twin-state-dot { width: 5px; height: 5px; border-radius: 50%; }
467
- .twin-tele {
468
- display: grid;
469
- grid-template-columns: repeat(2, minmax(0, 1fr));
470
- gap: 2px 10px;
471
- margin: 0 0 2px;
472
- padding-top: 6px;
473
- border-top: 1px dashed var(--hud-line);
474
- }
475
- .tele-item {
476
- display: flex;
477
- justify-content: space-between;
478
- gap: 6px;
479
- font-family: var(--font-mono);
480
- font-size: 9.5px;
481
- font-variant-numeric: tabular-nums;
482
- color: var(--hud-dim);
483
- min-width: 0;
484
- }
485
- .tele-item em { font-style: normal; overflow: hidden; text-overflow: ellipsis; }
486
- .tele-item b { color: var(--hud-text); font-weight: 500; }
487
- /* 实时数采行(绿;越限琥珀) */
488
- .twin-daq {
489
- display: flex;
490
- flex-direction: column;
491
- gap: 2px;
492
- margin: 0 0 2px;
493
- padding-top: 6px;
494
- border-top: 1px dashed rgba(53, 224, 160, 0.3);
495
- }
496
- .daq-item {
497
- display: flex;
498
- justify-content: space-between;
499
- align-items: baseline;
500
- gap: 6px;
501
- font-family: var(--font-mono);
502
- font-size: 9.5px;
503
- font-variant-numeric: tabular-nums;
504
- color: var(--hud-accent, #35e0a0);
505
- min-width: 0;
506
- }
507
- .daq-item em {
508
- font-style: normal;
509
- overflow: hidden;
510
- text-overflow: ellipsis;
511
- white-space: nowrap;
512
- opacity: 0.85;
513
- }
514
- .daq-item b { font-weight: 700; font-size: 10.5px; white-space: nowrap; }
515
- .daq-item b i { font-style: normal; font-size: 10px; font-weight: 500; margin-left: 2px; opacity: 0.75; }
516
- .daq-item.alarm { color: var(--hud-amber, #f6c453); }
517
- /* 智控设定区(每通道:set 值 + 上下限 + 窗口内直写) */
518
- .twin-dcw {
519
- display: flex;
520
- flex-direction: column;
521
- gap: 6px;
522
- margin: 0 0 2px;
523
- padding-top: 6px;
524
- border-top: 1px dashed rgba(240, 160, 76, 0.35);
525
- }
526
- .dcw-item { display: flex; flex-direction: column; gap: 3px; }
527
- .dcw-head {
528
- display: flex;
529
- justify-content: space-between;
530
- align-items: baseline;
531
- gap: 6px;
532
- font-family: var(--font-mono);
533
- font-size: 9.5px;
534
- color: var(--hud-dim);
535
- }
536
- .dcw-head em {
537
- font-style: normal;
538
- overflow: hidden;
539
- text-overflow: ellipsis;
540
- white-space: nowrap;
541
- }
542
- .dcw-src {
543
- font-style: normal;
544
- font-size: 10px;
545
- padding: 0 3px;
546
- margin-left: 4px;
547
- color: var(--hud-amber, #f6c453);
548
- border: 1px solid rgba(240, 160, 76, 0.5);
549
- border-radius: 2px;
550
- }
551
- .dcw-set {
552
- font-size: 12px;
553
- font-weight: 700;
554
- font-variant-numeric: tabular-nums;
555
- color: var(--hud-amber, #f6c453);
556
- white-space: nowrap;
557
- }
558
- .dcw-set i {
559
- font-style: normal;
560
- font-size: 10px;
561
- font-weight: 500;
562
- margin-left: 2px;
563
- opacity: 0.75;
564
- }
565
- .dcw-act {
566
- display: flex;
567
- justify-content: space-between;
568
- align-items: baseline;
569
- gap: 6px;
570
- font-family: var(--font-mono);
571
- font-size: 10px;
572
- color: var(--hud-dim);
573
- }
574
- .dcw-act em { font-style: normal; font-size: 10px; letter-spacing: 0.08em; color: var(--hud-faint); }
575
- .dcw-act b {
576
- font-size: 11.5px;
577
- font-weight: 700;
578
- font-variant-numeric: tabular-nums;
579
- color: var(--hud-accent, #35e0a0);
580
- white-space: nowrap;
581
- }
582
- .dcw-act b i { font-style: normal; font-size: 10px; font-weight: 500; margin-left: 2px; opacity: 0.75; }
583
- .dcw-act-at { font-style: normal; font-size: 10px; color: var(--hud-faint); }
584
- .dcw-win {
585
- font-family: var(--font-mono);
586
- font-size: 10px;
587
- font-variant-numeric: tabular-nums;
588
- color: var(--hud-faint);
589
- letter-spacing: 0.04em;
590
- }
591
- .dcw-win span {
592
- border-bottom: 1px dotted var(--hud-line);
593
- padding-bottom: 1px;
594
- }
595
- .dcw-ctrl { display: flex; gap: 4px; align-items: center; }
596
- .dcw-ctrl .ctrl-input { flex: 1; min-width: 0; width: auto; }
597
- .dcw-send {
598
- flex: none;
599
- color: var(--hud-amber, #f6c453);
600
- border-color: rgba(240, 160, 76, 0.5);
601
- }
602
- .dcw-send:hover:not(:disabled) {
603
- background: rgba(240, 160, 76, 0.12);
604
- color: var(--hud-amber, #f6c453);
605
- border-color: var(--hud-amber, #f6c453);
606
- }
607
- .dcw-err {
608
- font-family: var(--font-mono);
609
- font-size: 10px;
610
- color: var(--hud-danger, #ff6b5c);
611
- }
612
- .ctrl-btn {
613
- padding: 3px 7px;
614
- font-size: 10px;
615
- letter-spacing: 0.06em;
616
- font-weight: 600;
617
- font-family: var(--font-mono);
618
- color: var(--hud-text);
619
- background: transparent;
620
- border: 1px solid var(--hud-line);
621
- border-radius: 2px;
622
- cursor: pointer;
623
- transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease;
624
- }
625
- .ctrl-btn:hover:not(:disabled) {
626
- border-color: var(--hud-accent);
627
- color: var(--hud-accent);
628
- }
629
- .ctrl-btn:disabled { opacity: 0.45; cursor: default; }
630
- .ctrl-input {
631
- width: 48px;
632
- font-family: var(--font-mono);
633
- font-size: 10px;
634
- padding: 3px 6px;
635
- border: 1px solid var(--hud-line);
636
- border-radius: 2px;
637
- background: var(--hud-input);
638
- color: var(--hud-text);
639
- font-variant-numeric: tabular-nums;
640
- }
641
- .ctrl-input:focus { outline: none; border-color: var(--hud-accent); }
642
- .twin-err { font-size: 9.5px; color: var(--hud-danger); padding: 0 10px 8px; }
643
- </style>
1
+ <script setup lang="ts">
2
+ /**
3
+ * 设备控制台(Device Twin Console) —— 工业数字孪生 HMI:
4
+ * 设备实体直接列表展示(状态灯/遥测数据网格/下发控制),点击行聚焦小镇场景中的对应实体。
5
+ *
6
+ * 与 3D 小镇联动:拖 `dev` 模型进场景会创建设备 twin;本面板实时刷新其 state/telemetry,
7
+ * 并展示绑定智控通道的当前 set 值,支持窗口内直写下发(替代旧 power_on/power_off/set_speed)。
8
+ */
9
+ import { useDeviceTwins } from '@/app/composables/workshop/useDeviceTwins'
10
+ import { useDcwStream } from '@/app/composables/workshop/useDcwStream'
11
+
12
+ const { t } = useI18n()
13
+
14
+ /** 实时数采数据(twinId → 该实体相关的实时通道:
15
+ * 数采节点 = 自身通道;设备 = 绑定到它的全部数采通道;由 TownView 从模拟上报喂入) */
16
+ interface DaqLiveRow { ch: string, value: string, unit: string, alarm?: boolean }
17
+ /** 智控设定数据(twinId → 该设备绑定的全部智控通道;含当前 set 值与生效上下限) */
18
+ export interface DcwLiveRow {
19
+ id: string
20
+ ch: string
21
+ name: string
22
+ unit: string
23
+ /** 当前设定值(工程量;null = 从未下发) */
24
+ value: number | null
25
+ /** PLC 当前读数(读写集成;null = 从未读到或驱动不支持) */
26
+ readValue: number | null
27
+ lastReadAt: string | null
28
+ decimals: number
29
+ /** 生效下/上限(活动配方工艺窗口优先,否则节点全局量程;-∞/+∞ 表示不限定) */
30
+ lo: number
31
+ hi: number
32
+ src: 'recipe' | 'global'
33
+ }
34
+ defineProps<{
35
+ daqLive?: Record<string, DaqLiveRow[]>
36
+ dcwLive?: Record<string, DcwLiveRow[]>
37
+ }>()
38
+
39
+ defineEmits<{ (e: 'focus-device', twin: { id: string, posX?: number, posZ?: number }): void }>()
40
+
41
+ const twins = useDeviceTwins()
42
+ const dcw = useDcwStream()
43
+
44
+ /** 数采实体(绿卡):kind=daq 或旧数据 modelRef 前缀兜底 */
45
+ const isDaq = (t: { kind?: string, modelRef?: string }): boolean =>
46
+ t.kind === 'daq' || (t.modelRef ?? '').startsWith('daq-')
47
+
48
+ // 节流轮询刷新(不打断拖拽/交互)
49
+ let timer: ReturnType<typeof setInterval> | null = null
50
+ onMounted(() => {
51
+ void twins.load()
52
+ timer = setInterval(() => void twins.load(), 2000)
53
+ })
54
+ onBeforeUnmount(() => {
55
+ if (timer) clearInterval(timer)
56
+ })
57
+
58
+ const busyId = ref('')
59
+ const ctrlMsg = ref('')
60
+ /** 行内删除(两步确认:首击布防 3s,再击执行) */
61
+ const armedId = ref('')
62
+ let armedTimer: ReturnType<typeof setTimeout> | null = null
63
+ async function removeTwin(t: { id: string, name: string }): Promise<void> {
64
+ if (armedId.value !== t.id) {
65
+ armedId.value = t.id
66
+ if (armedTimer) clearTimeout(armedTimer)
67
+ armedTimer = setTimeout(() => {
68
+ armedId.value = ''
69
+ }, 3000)
70
+ return
71
+ }
72
+ if (armedTimer) clearTimeout(armedTimer)
73
+ armedId.value = ''
74
+ busyId.value = t.id
75
+ ctrlMsg.value = ''
76
+ try {
77
+ await twins.remove(t.id)
78
+ }
79
+ catch (err) {
80
+ ctrlMsg.value = apiErrorMessage(err)
81
+ }
82
+ finally {
83
+ busyId.value = ''
84
+ }
85
+ }
86
+ const stateColor: Record<string, string> = {
87
+ idle: 'var(--hud-amber)',
88
+ running: 'var(--hud-ok)',
89
+ alarm: 'var(--hud-danger)',
90
+ offline: 'var(--hud-dim)',
91
+ }
92
+ const stateLabel: Record<string, string> = {
93
+ idle: 'STANDBY', running: 'RUNNING', alarm: 'ALARM', offline: 'OFFLINE',
94
+ }
95
+ const modelTag = (name: string): string => name.replace(/[^A-Za-z0-9]/g, '').slice(0, 4).toUpperCase() || 'DEV'
96
+ const devNo = (id: string): string => id.replace(/^dev-/, '').replace(/[^a-z0-9]/gi, '').slice(-6).toUpperCase()
97
+ const fmt = (v: unknown): string => (typeof v === 'number' ? (Math.round(v * 100) / 100).toString() : String(v))
98
+
99
+ // ---------- 智控设定(每张设备卡:当前 set 值展示 + 窗口内直写下发) ----------
100
+ /** 智控上下限文案(无穷大显示 ±∞) */
101
+ const dcwWinText = (r: DcwLiveRow): string =>
102
+ `${Number.isFinite(r.lo) ? r.lo : '-∞'} ~ ${Number.isFinite(r.hi) ? r.hi : '+∞'} ${r.unit}`
103
+ /** 写入草稿/错误(智控节点 id 键控) */
104
+ const dcwDrafts = reactive<Record<string, number | ''>>({})
105
+ const dcwErrs = reactive<Record<string, string>>({})
106
+ const dcwBusy = ref('')
107
+ /** 下发设定值:先本地窗口校验(不越限),再走 server write(配方联锁二道门) */
108
+ function doDcwWrite(r: DcwLiveRow): void {
109
+ const raw = dcwDrafts[r.id]
110
+ if (raw == null || raw === '') return
111
+ const v = Number(raw)
112
+ if (v < r.lo || v > r.hi) {
113
+ dcwErrs[r.id] = t('deviceTwinPanel.kxddket010', { p0: r.src === 'recipe' ? t('deviceTwinPanel.kq2jssk008') : t('deviceTwinPanel.k1iwj796009'), p1: dcwWinText(r) })
114
+ return
115
+ }
116
+ dcwErrs[r.id] = ''
117
+ dcwBusy.value = r.id
118
+ void dcw.write(r.id, v).then((out) => {
119
+ if (!out.ok) dcwErrs[r.id] = out.message
120
+ }).catch((err: unknown) => {
121
+ dcwErrs[r.id] = apiErrorMessage(err)
122
+ }).finally(() => {
123
+ dcwBusy.value = ''
124
+ })
125
+ }
126
+ </script>
127
+
128
+ <template>
129
+ <aside class="twin-panel">
130
+ <div class="twin-head">
131
+ <span class="twin-kicker">{{ $t('deviceTwinPanel.k7k6evr002') }}</span>
132
+ <span class="twin-count">{{ twins.twins.length }}</span>
133
+ </div>
134
+
135
+ <!-- 加载失败横幅:错误可见,旧数据保留不清空 -->
136
+ <div
137
+ v-if="twins.error"
138
+ class="twin-error"
139
+ >
140
+ {{ twins.error }}
141
+ </div>
142
+
143
+ <div
144
+ v-if="!twins.loaded"
145
+ class="twin-empty"
146
+ >
147
+ {{ $t('deviceTwinPanel.kkw7sn5003') }}
148
+ </div>
149
+ <div
150
+ v-else-if="twins.twins.length === 0"
151
+ class="twin-empty"
152
+ >
153
+ <div class="twin-empty-frame">
154
+ <div class="twin-empty-title">
155
+ {{ $t('deviceTwinPanel.k13y2qmg004') }}
156
+ </div>
157
+ <div class="twin-empty-sub">
158
+ {{ $t('deviceTwinPanel.kotf23l005') }}
159
+ </div>
160
+ </div>
161
+ </div>
162
+ <div
163
+ v-else
164
+ class="twin-list"
165
+ >
166
+ <button
167
+ v-for="tw in twins.twins"
168
+ :key="tw.id"
169
+ class="twin-card"
170
+ :class="{ daq: isDaq(tw) }"
171
+ type="button"
172
+ @click="$emit('focus-device', tw)"
173
+ >
174
+ <div class="twin-row">
175
+ <span class="twin-model-tag">{{ modelTag(tw.modelRef || tw.name) }}</span>
176
+ <div class="twin-idbar">
177
+ <span class="twin-name">{{ tw.name }}</span>
178
+ <span class="twin-code">{{ isDaq(tw) ? 'DAQ' : devNo(tw.id) }}</span>
179
+ </div>
180
+ <button
181
+ class="twin-del"
182
+ :class="{ armed: armedId === tw.id }"
183
+ :disabled="busyId === tw.id"
184
+ :title="armedId === tw.id ? $t('deviceTwinPanel.armDelete') : $t('deviceTwinPanel.delDevice')"
185
+ @click.stop="removeTwin(tw)"
186
+ >
187
+ {{ armedId === tw.id ? $t('deviceTwinPanel.k44653007') : '✕' }}
188
+ </button>
189
+ <span
190
+ class="twin-state"
191
+ :class="`s-${tw.state}`"
192
+ >
193
+ <i
194
+ class="twin-state-dot"
195
+ :style="{ background: stateColor[tw.state] || 'var(--hud-dim)' }"
196
+ />
197
+ {{ stateLabel[tw.state] || tw.state }}
198
+ </span>
199
+ </div>
200
+ <div
201
+ v-if="Object.keys(tw.telemetry).length"
202
+ class="twin-tele"
203
+ >
204
+ <span
205
+ v-for="(v, k) in tw.telemetry"
206
+ :key="k"
207
+ class="tele-item"
208
+ ><em>{{ k }}</em><b>{{ fmt(v) }}</b></span>
209
+ </div>
210
+ <!-- 实时数采(绿色;数采节点 = 自身通道,设备 = 绑定通道) -->
211
+ <div
212
+ v-if="daqLive?.[tw.id]?.length"
213
+ class="twin-daq"
214
+ >
215
+ <span
216
+ v-for="(d, i) in daqLive[tw.id]"
217
+ :key="`${d.ch}-${i}`"
218
+ class="daq-item"
219
+ :class="{ alarm: d.alarm }"
220
+ >
221
+ <em>{{ d.ch }}</em>
222
+ <b>{{ d.value }}<i>{{ d.unit }}</i></b>
223
+ </span>
224
+ </div>
225
+ <!-- 智控设定(绑定通道的当前 set 值展示 + 生效上下限 + 窗口内直写下发) -->
226
+ <div
227
+ v-if="dcwLive?.[tw.id]?.length"
228
+ class="twin-dcw"
229
+ >
230
+ <div
231
+ v-for="r in dcwLive[tw.id]"
232
+ :key="r.id"
233
+ class="dcw-item"
234
+ >
235
+ <div class="dcw-head">
236
+ <em>{{ r.ch }}<i
237
+ v-if="r.src === 'recipe'"
238
+ class="dcw-src"
239
+ >{{ $t('deviceTwinPanel.k48grv006') }}</i></em>
240
+ <b class="dcw-set">{{ r.value != null ? r.value.toFixed(r.decimals) : '--' }}<i>{{ r.unit }}</i></b>
241
+ </div>
242
+ <div
243
+ v-if="r.readValue != null || r.lastReadAt"
244
+ class="dcw-act"
245
+ :title="$t('deviceTwinPanel.k9r7d4e030')"
246
+ >
247
+ <em>ACT</em>
248
+ <b>{{ r.readValue != null ? r.readValue.toFixed(r.decimals) : '--' }}<i>{{ r.unit }}</i></b>
249
+ <i
250
+ v-if="r.lastReadAt"
251
+ class="dcw-act-at"
252
+ >{{ r.lastReadAt.slice(11, 19) }}</i>
253
+ </div>
254
+ <div class="dcw-win">
255
+ <span :title="r.src === 'recipe' ? $t('deviceTwinPanel.winRecipe') : $t('deviceTwinPanel.winRange')">{{ dcwWinText(r) }}</span>
256
+ </div>
257
+ <div class="dcw-ctrl">
258
+ <input
259
+ v-model.number="dcwDrafts[r.id]"
260
+ type="number"
261
+ class="ctrl-input"
262
+ :step="10 ** -r.decimals"
263
+ :min="Number.isFinite(r.lo) ? r.lo : undefined"
264
+ :max="Number.isFinite(r.hi) ? r.hi : undefined"
265
+ :placeholder="`${Number.isFinite(r.lo) ? r.lo : ''} ~ ${Number.isFinite(r.hi) ? r.hi : ''}`"
266
+ @keydown.enter="doDcwWrite(r)"
267
+ >
268
+ <button
269
+ class="ctrl-btn dcw-send"
270
+ :disabled="dcwDrafts[r.id] == null || dcwDrafts[r.id] === '' || dcwBusy === r.id"
271
+ :title="$t('deviceTwinPanel.k1j04u8001')"
272
+ @click.stop="doDcwWrite(r)"
273
+ >
274
+ {{ dcwBusy === r.id ? '···' : 'SET' }}
275
+ </button>
276
+ </div>
277
+ <small
278
+ v-if="dcwErrs[r.id]"
279
+ class="dcw-err"
280
+ >{{ dcwErrs[r.id] }}</small>
281
+ </div>
282
+ </div>
283
+ </button>
284
+ </div>
285
+
286
+ <span
287
+ v-if="ctrlMsg"
288
+ class="twin-err"
289
+ >{{ ctrlMsg }}</span>
290
+ </aside>
291
+ </template>
292
+
293
+ <style scoped>
294
+ .twin-panel {
295
+ display: flex;
296
+ flex-direction: column;
297
+ gap: 8px;
298
+ width: auto;
299
+ flex: none;
300
+ padding: 0;
301
+ background: transparent;
302
+ border: 0;
303
+ border-radius: 0;
304
+ box-shadow: none;
305
+ }
306
+ .twin-head {
307
+ display: flex;
308
+ gap: 8px;
309
+ align-items: center;
310
+ padding: 10px 12px 6px;
311
+ border-bottom: 1px solid rgba(38, 51, 64, 0.45);
312
+ }
313
+ /* 加载失败横幅:错误可见(旧数据保留,不再静默清空) */
314
+ .twin-error {
315
+ padding: 7px 10px;
316
+ font-size: 11px;
317
+ line-height: 1.5;
318
+ color: #ffb4b4;
319
+ background: rgba(255, 107, 107, 0.1);
320
+ border: 1px solid rgba(255, 107, 107, 0.35);
321
+ border-radius: 8px;
322
+ word-break: break-all;
323
+ }
324
+ .twin-kicker {
325
+ font-family: var(--font-mono);
326
+ font-size: 10px;
327
+ font-weight: 600;
328
+ letter-spacing: 0.16em;
329
+ color: var(--hud-text, #d9e4ee);
330
+ }
331
+ .twin-count {
332
+ margin-left: auto;
333
+ font-family: var(--font-mono);
334
+ font-size: 10px;
335
+ font-variant-numeric: tabular-nums;
336
+ color: var(--hud-dim, #8496a5);
337
+ }
338
+ .twin-empty {
339
+ padding: 10px;
340
+ }
341
+ .twin-empty-frame {
342
+ border: 1px dashed var(--hud-line);
343
+ padding: 12px 10px;
344
+ display: flex;
345
+ flex-direction: column;
346
+ gap: 4px;
347
+ }
348
+ .twin-empty-title {
349
+ font-family: var(--font-mono);
350
+ font-size: 11px;
351
+ letter-spacing: 0.1em;
352
+ color: var(--hud-text);
353
+ }
354
+ .twin-empty-sub {
355
+ font-size: 10px;
356
+ line-height: 1.6;
357
+ color: var(--hud-dim);
358
+ }
359
+ .twin-list {
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 4px;
363
+ max-height: 46vh;
364
+ overflow: hidden auto;
365
+ padding: 4px 0 10px;
366
+ }
367
+ .twin-card {
368
+ display: flex;
369
+ flex-direction: column;
370
+ gap: 6px;
371
+ margin: 0 10px;
372
+ padding: 8px 9px;
373
+ text-align: left;
374
+ background: rgba(20, 27, 38, 0.65);
375
+ border: 0;
376
+ border-radius: 2px;
377
+ cursor: pointer;
378
+ transition: background 0.16s ease;
379
+ }
380
+ .twin-card:hover {
381
+ background: var(--hud-panel-hover);
382
+ }
383
+ .twin-card:active {
384
+ background: var(--hud-panel-raised);
385
+ }
386
+ .twin-card:hover .twin-name { color: #e8f1f8; }
387
+ /* 数采节点绿卡(用户指定:设备监控中数采卡为绿色) */
388
+ .twin-card.daq {
389
+ background: rgba(53, 224, 160, 0.06);
390
+ border: 1px solid rgba(53, 224, 160, 0.32);
391
+ }
392
+ .twin-card.daq:hover {
393
+ background: rgba(53, 224, 160, 0.11);
394
+ border-color: rgba(53, 224, 160, 0.5);
395
+ }
396
+ .twin-card.daq .twin-model-tag,
397
+ .twin-card.daq .twin-code { color: var(--hud-accent, #35e0a0); }
398
+ .twin-row { display: flex; gap: 8px; align-items: center; min-width: 0; }
399
+ .twin-model-tag {
400
+ flex: none;
401
+ min-width: 30px;
402
+ font-family: var(--font-mono);
403
+ /* 9px 的 mono 铭牌在 1440 下也读不动(实测每屏稳定被判为不可读);
404
+ 10px 是孪生侧栏这套 HUD 在桌面档的地板。 */
405
+ font-size: 10px;
406
+ font-weight: 600;
407
+ letter-spacing: 0.08em;
408
+ color: var(--hud-faint);
409
+ }
410
+ .twin-idbar { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
411
+ .twin-name {
412
+ font-size: 11.5px;
413
+ font-weight: 600;
414
+ color: var(--hud-text);
415
+ overflow: hidden;
416
+ text-overflow: ellipsis;
417
+ white-space: nowrap;
418
+ }
419
+ .twin-code {
420
+ font-family: var(--font-mono);
421
+ font-size: 10px;
422
+ letter-spacing: 0.08em;
423
+ color: var(--hud-dim);
424
+ }
425
+ .twin-del {
426
+ flex: none;
427
+ width: 18px;
428
+ height: 18px;
429
+ font-family: var(--font-mono);
430
+ font-size: 10px;
431
+ line-height: 1;
432
+ color: var(--hud-dim, #8496a5);
433
+ background: transparent;
434
+ border: 1px solid var(--hud-line, #263340);
435
+ border-radius: 2px;
436
+ cursor: pointer;
437
+ transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease;
438
+ }
439
+ .twin-del:hover:not(:disabled) {
440
+ border-color: var(--hud-danger, #ff6b5c);
441
+ color: var(--hud-danger, #ff6b5c);
442
+ }
443
+ .twin-del.armed {
444
+ width: auto;
445
+ padding: 0 5px;
446
+ color: #1a0d0a;
447
+ background: var(--hud-danger, #ff6b5c);
448
+ border-color: var(--hud-danger, #ff6b5c);
449
+ }
450
+ .twin-del:disabled { opacity: 0.4; cursor: default; }
451
+ .twin-state {
452
+ flex: none;
453
+ display: inline-flex;
454
+ gap: 5px;
455
+ align-items: center;
456
+ font-family: var(--font-mono);
457
+ font-size: 10px;
458
+ letter-spacing: 0.08em;
459
+ color: var(--hud-dim);
460
+ padding: 2px 5px;
461
+ border: 1px solid var(--hud-line);
462
+ border-radius: 2px;
463
+ }
464
+ .twin-state.s-alarm { color: var(--hud-danger); border-color: rgba(255, 107, 92, 0.5); }
465
+ .twin-state.s-running { color: var(--hud-ok); border-color: rgba(127, 212, 160, 0.4); }
466
+ .twin-state-dot { width: 5px; height: 5px; border-radius: 50%; }
467
+ .twin-tele {
468
+ display: grid;
469
+ grid-template-columns: repeat(2, minmax(0, 1fr));
470
+ gap: 2px 10px;
471
+ margin: 0 0 2px;
472
+ padding-top: 6px;
473
+ border-top: 1px dashed var(--hud-line);
474
+ }
475
+ .tele-item {
476
+ display: flex;
477
+ justify-content: space-between;
478
+ gap: 6px;
479
+ font-family: var(--font-mono);
480
+ font-size: 9.5px;
481
+ font-variant-numeric: tabular-nums;
482
+ color: var(--hud-dim);
483
+ min-width: 0;
484
+ }
485
+ .tele-item em { font-style: normal; overflow: hidden; text-overflow: ellipsis; }
486
+ .tele-item b { color: var(--hud-text); font-weight: 500; }
487
+ /* 实时数采行(绿;越限琥珀) */
488
+ .twin-daq {
489
+ display: flex;
490
+ flex-direction: column;
491
+ gap: 2px;
492
+ margin: 0 0 2px;
493
+ padding-top: 6px;
494
+ border-top: 1px dashed rgba(53, 224, 160, 0.3);
495
+ }
496
+ .daq-item {
497
+ display: flex;
498
+ justify-content: space-between;
499
+ align-items: baseline;
500
+ gap: 6px;
501
+ font-family: var(--font-mono);
502
+ font-size: 9.5px;
503
+ font-variant-numeric: tabular-nums;
504
+ color: var(--hud-accent, #35e0a0);
505
+ min-width: 0;
506
+ }
507
+ .daq-item em {
508
+ font-style: normal;
509
+ overflow: hidden;
510
+ text-overflow: ellipsis;
511
+ white-space: nowrap;
512
+ opacity: 0.85;
513
+ }
514
+ .daq-item b { font-weight: 700; font-size: 10.5px; white-space: nowrap; }
515
+ .daq-item b i { font-style: normal; font-size: 10px; font-weight: 500; margin-left: 2px; opacity: 0.75; }
516
+ .daq-item.alarm { color: var(--hud-amber, #f6c453); }
517
+ /* 智控设定区(每通道:set 值 + 上下限 + 窗口内直写) */
518
+ .twin-dcw {
519
+ display: flex;
520
+ flex-direction: column;
521
+ gap: 6px;
522
+ margin: 0 0 2px;
523
+ padding-top: 6px;
524
+ border-top: 1px dashed rgba(240, 160, 76, 0.35);
525
+ }
526
+ .dcw-item { display: flex; flex-direction: column; gap: 3px; }
527
+ .dcw-head {
528
+ display: flex;
529
+ justify-content: space-between;
530
+ align-items: baseline;
531
+ gap: 6px;
532
+ font-family: var(--font-mono);
533
+ font-size: 9.5px;
534
+ color: var(--hud-dim);
535
+ }
536
+ .dcw-head em {
537
+ font-style: normal;
538
+ overflow: hidden;
539
+ text-overflow: ellipsis;
540
+ white-space: nowrap;
541
+ }
542
+ .dcw-src {
543
+ font-style: normal;
544
+ font-size: 10px;
545
+ padding: 0 3px;
546
+ margin-left: 4px;
547
+ color: var(--hud-amber, #f6c453);
548
+ border: 1px solid rgba(240, 160, 76, 0.5);
549
+ border-radius: 2px;
550
+ }
551
+ .dcw-set {
552
+ font-size: 12px;
553
+ font-weight: 700;
554
+ font-variant-numeric: tabular-nums;
555
+ color: var(--hud-amber, #f6c453);
556
+ white-space: nowrap;
557
+ }
558
+ .dcw-set i {
559
+ font-style: normal;
560
+ font-size: 10px;
561
+ font-weight: 500;
562
+ margin-left: 2px;
563
+ opacity: 0.75;
564
+ }
565
+ .dcw-act {
566
+ display: flex;
567
+ justify-content: space-between;
568
+ align-items: baseline;
569
+ gap: 6px;
570
+ font-family: var(--font-mono);
571
+ font-size: 10px;
572
+ color: var(--hud-dim);
573
+ }
574
+ .dcw-act em { font-style: normal; font-size: 10px; letter-spacing: 0.08em; color: var(--hud-faint); }
575
+ .dcw-act b {
576
+ font-size: 11.5px;
577
+ font-weight: 700;
578
+ font-variant-numeric: tabular-nums;
579
+ color: var(--hud-accent, #35e0a0);
580
+ white-space: nowrap;
581
+ }
582
+ .dcw-act b i { font-style: normal; font-size: 10px; font-weight: 500; margin-left: 2px; opacity: 0.75; }
583
+ .dcw-act-at { font-style: normal; font-size: 10px; color: var(--hud-faint); }
584
+ .dcw-win {
585
+ font-family: var(--font-mono);
586
+ font-size: 10px;
587
+ font-variant-numeric: tabular-nums;
588
+ color: var(--hud-faint);
589
+ letter-spacing: 0.04em;
590
+ }
591
+ .dcw-win span {
592
+ border-bottom: 1px dotted var(--hud-line);
593
+ padding-bottom: 1px;
594
+ }
595
+ .dcw-ctrl { display: flex; gap: 4px; align-items: center; }
596
+ .dcw-ctrl .ctrl-input { flex: 1; min-width: 0; width: auto; }
597
+ .dcw-send {
598
+ flex: none;
599
+ color: var(--hud-amber, #f6c453);
600
+ border-color: rgba(240, 160, 76, 0.5);
601
+ }
602
+ .dcw-send:hover:not(:disabled) {
603
+ background: rgba(240, 160, 76, 0.12);
604
+ color: var(--hud-amber, #f6c453);
605
+ border-color: var(--hud-amber, #f6c453);
606
+ }
607
+ .dcw-err {
608
+ font-family: var(--font-mono);
609
+ font-size: 10px;
610
+ color: var(--hud-danger, #ff6b5c);
611
+ }
612
+ .ctrl-btn {
613
+ padding: 3px 7px;
614
+ font-size: 10px;
615
+ letter-spacing: 0.06em;
616
+ font-weight: 600;
617
+ font-family: var(--font-mono);
618
+ color: var(--hud-text);
619
+ background: transparent;
620
+ border: 1px solid var(--hud-line);
621
+ border-radius: 2px;
622
+ cursor: pointer;
623
+ transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease;
624
+ }
625
+ .ctrl-btn:hover:not(:disabled) {
626
+ border-color: var(--hud-accent);
627
+ color: var(--hud-accent);
628
+ }
629
+ .ctrl-btn:disabled { opacity: 0.45; cursor: default; }
630
+ .ctrl-input {
631
+ width: 48px;
632
+ font-family: var(--font-mono);
633
+ font-size: 10px;
634
+ padding: 3px 6px;
635
+ border: 1px solid var(--hud-line);
636
+ border-radius: 2px;
637
+ background: var(--hud-input);
638
+ color: var(--hud-text);
639
+ font-variant-numeric: tabular-nums;
640
+ }
641
+ .ctrl-input:focus { outline: none; border-color: var(--hud-accent); }
642
+ .twin-err { font-size: 9.5px; color: var(--hud-danger); padding: 0 10px 8px; }
643
+ </style>