oneDataSet.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. <template>
  2. <div class="page">
  3. <aside class="side">
  4. <div class="panel-head">
  5. <span class="label">命令列表</span>
  6. <span v-if="commands.length" class="count">{{ commands.length }}</span>
  7. </div>
  8. <div class="command-list">
  9. <button
  10. v-for="(item, index) in commands"
  11. :key="index"
  12. type="button"
  13. class="command-btn"
  14. :class="{ active: activeIndex === index }"
  15. @click="selectCommand(item, index)"
  16. v-log="{ describe: { action: '点击任一参数命令', name: item.name } }"
  17. >
  18. <span class="command-mark">#</span>
  19. <span class="command-name">{{ item.name }}</span>
  20. </button>
  21. <div v-if="loaded && !commands.length" class="empty">暂无命令</div>
  22. </div>
  23. </aside>
  24. <section class="editor">
  25. <div class="panel">
  26. <div class="panel-head">
  27. <span class="label">JSON内容</span>
  28. <span v-if="activeName" class="hint">#{{ activeName }}</span>
  29. </div>
  30. <textarea
  31. v-model="jsonText"
  32. class="json-box"
  33. spellcheck="false"
  34. placeholder="选择左侧命令,或直接填写 JSON / 十六进制命令"
  35. />
  36. </div>
  37. <div class="panel">
  38. <div class="panel-head">
  39. <span class="label">数据返回</span>
  40. <span v-if="returnLines.length" class="count">{{ returnLines.length }}</span>
  41. </div>
  42. <div ref="returnBox" class="return-box">
  43. <div v-if="!returnLines.length" class="empty return-empty">下发后,设备返回会显示在这里</div>
  44. <div
  45. v-for="(line, index) in returnLines"
  46. :key="index"
  47. class="return-line"
  48. :class="{ 'is-error': isErrorMessage(line.message) }"
  49. >
  50. <span v-if="line.time" class="return-time">{{ line.time }}</span>
  51. <span class="return-msg">{{ line.message }}</span>
  52. </div>
  53. </div>
  54. </div>
  55. <div class="action-row">
  56. <button
  57. type="button"
  58. class="action-btn action-btn--ghost"
  59. @click="clearReturn"
  60. v-log="{ describe: { action: '点击清空任一参数返回' } }"
  61. >
  62. 清空
  63. </button>
  64. <button
  65. type="button"
  66. class="action-btn action-btn--primary"
  67. @click="sendJson"
  68. v-log="{ describe: { action: '点击下发JSON' } }"
  69. >
  70. 下发JSON
  71. </button>
  72. </div>
  73. </section>
  74. </div>
  75. </template>
  76. <script setup lang="ts">
  77. import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
  78. import { ElMessage } from 'element-plus'
  79. import client from '@/stores/modules/client'
  80. import socket from '@/stores/modules/socket'
  81. import icpList from '@/utils/ipc'
  82. type CommandItem = {
  83. name: string
  84. command: string
  85. }
  86. const clientStore = client()
  87. const socketStore = socket()
  88. type ReturnLine = {
  89. time: string
  90. message: string
  91. }
  92. const commands = ref<CommandItem[]>([])
  93. const activeIndex = ref(-1)
  94. const jsonText = ref('')
  95. const returnLines = ref<ReturnLine[]>([])
  96. const returnBox = ref<HTMLElement | null>(null)
  97. const loaded = ref(false)
  98. const activeName = computed(() => commands.value[activeIndex.value]?.name || '')
  99. const listChannel = icpList.socket.message + '_get_one_data_set'
  100. const sendChannel = icpList.socket.message + '_send_get_one_data_set'
  101. const printChannel = icpList.mcu.errorLog
  102. function selectCommand(item: CommandItem, index: number) {
  103. activeIndex.value = index
  104. jsonText.value = item.command || ''
  105. }
  106. function clearReturn() {
  107. returnLines.value = []
  108. }
  109. function isErrorMessage(message: string) {
  110. const text = String(message || '').trimStart()
  111. return text.startsWith('!') || text.startsWith('!')
  112. }
  113. async function appendReturn(message: string, time = '') {
  114. if (!message && !time) return
  115. returnLines.value.push({ time, message })
  116. await nextTick()
  117. if (returnBox.value) {
  118. returnBox.value.scrollTop = returnBox.value.scrollHeight
  119. }
  120. }
  121. function onCommandList(_event: unknown, result: any) {
  122. console.log('[任一参数下发] get_one_data_set 回包', result)
  123. loaded.value = true
  124. const list = result?.data?.commands
  125. if (result?.code === 0 && Array.isArray(list)) {
  126. commands.value = list
  127. console.log('[任一参数下发] 命令条数', list.length, list)
  128. return
  129. }
  130. commands.value = []
  131. console.log('[任一参数下发] 未解析出命令列表', {
  132. code: result?.code,
  133. msg: result?.msg,
  134. data: result?.data
  135. })
  136. if (result?.msg) ElMessage.error(result.msg)
  137. }
  138. function onSendResult(_event: unknown, result: any) {
  139. console.log('[任一参数下发] send_get_one_data_set 回包', result)
  140. if (result?.code === 0) return
  141. const text = result?.msg || '下发失败'
  142. appendReturn(text)
  143. ElMessage.error(text)
  144. }
  145. function onPrint(_event: unknown, result: any) {
  146. console.log('[任一参数下发] print_mcu_error_data 回包', result)
  147. const time = result?.data?.current_time || ''
  148. const message = result?.data?.message || ''
  149. appendReturn(message, time)
  150. }
  151. function bindListeners() {
  152. if (!clientStore.isClient) return
  153. clientStore.ipc.removeListener(listChannel, onCommandList)
  154. clientStore.ipc.removeListener(sendChannel, onSendResult)
  155. clientStore.ipc.removeListener(printChannel, onPrint)
  156. clientStore.ipc.on(listChannel, onCommandList)
  157. clientStore.ipc.on(sendChannel, onSendResult)
  158. clientStore.ipc.on(printChannel, onPrint)
  159. }
  160. function unbindListeners() {
  161. if (!clientStore.isClient) return
  162. clientStore.ipc.removeListener(listChannel, onCommandList)
  163. clientStore.ipc.removeListener(sendChannel, onSendResult)
  164. clientStore.ipc.removeListener(printChannel, onPrint)
  165. }
  166. function loadCommands() {
  167. const payload = {
  168. type: 'get_one_data_set'
  169. }
  170. console.log('[任一参数下发] 发送', payload, '监听', listChannel)
  171. socketStore.sendMessage(payload)
  172. }
  173. function sendJson() {
  174. if (!jsonText.value.trim()) {
  175. ElMessage.warning('请先选择或填写要下发的命令')
  176. return
  177. }
  178. const payload = {
  179. type: 'send_get_one_data_set',
  180. data: {
  181. command: jsonText.value
  182. }
  183. }
  184. console.log('[任一参数下发] 发送', payload)
  185. socketStore.sendMessage(payload)
  186. }
  187. onMounted(() => {
  188. bindListeners()
  189. if (clientStore.isClient) loadCommands()
  190. })
  191. onUnmounted(() => {
  192. unbindListeners()
  193. })
  194. </script>
  195. <style scoped lang="scss">
  196. .page {
  197. height: 100vh;
  198. box-sizing: border-box;
  199. display: flex;
  200. gap: 14px;
  201. padding: 14px;
  202. background: #f3f5f9;
  203. color: #1f2937;
  204. text-align: left;
  205. }
  206. .side,
  207. .panel {
  208. background: #fff;
  209. border: 1px solid #e6ebf2;
  210. border-radius: 12px;
  211. box-shadow: 0 8px 24px rgba(31, 45, 80, 0.05);
  212. }
  213. .side {
  214. width: 228px;
  215. flex-shrink: 0;
  216. display: flex;
  217. flex-direction: column;
  218. min-height: 0;
  219. padding: 12px 10px 10px;
  220. }
  221. .editor {
  222. flex: 1;
  223. min-width: 0;
  224. display: flex;
  225. flex-direction: column;
  226. gap: 12px;
  227. }
  228. .panel {
  229. flex: 1;
  230. min-height: 0;
  231. display: flex;
  232. flex-direction: column;
  233. padding: 12px 14px 14px;
  234. }
  235. .panel-head {
  236. display: flex;
  237. align-items: center;
  238. gap: 8px;
  239. margin-bottom: 10px;
  240. flex-shrink: 0;
  241. }
  242. .label {
  243. font-size: 14px;
  244. font-weight: 600;
  245. line-height: 1.4;
  246. color: #1f2937;
  247. }
  248. .count {
  249. min-width: 20px;
  250. height: 20px;
  251. padding: 0 6px;
  252. border-radius: 10px;
  253. background: #eef3fb;
  254. color: #4b628a;
  255. font-size: 12px;
  256. line-height: 20px;
  257. text-align: center;
  258. }
  259. .hint {
  260. margin-left: auto;
  261. max-width: 50%;
  262. overflow: hidden;
  263. text-overflow: ellipsis;
  264. white-space: nowrap;
  265. color: #6b7c93;
  266. font-size: 12px;
  267. }
  268. .command-list,
  269. .return-box {
  270. flex: 1;
  271. min-height: 0;
  272. overflow-y: auto;
  273. }
  274. .command-list::-webkit-scrollbar,
  275. .return-box::-webkit-scrollbar,
  276. .json-box::-webkit-scrollbar {
  277. width: 6px;
  278. }
  279. .command-list::-webkit-scrollbar-thumb,
  280. .return-box::-webkit-scrollbar-thumb,
  281. .json-box::-webkit-scrollbar-thumb {
  282. background: #d5dce8;
  283. border-radius: 6px;
  284. }
  285. .command-btn {
  286. display: flex;
  287. align-items: flex-start;
  288. gap: 6px;
  289. width: 100%;
  290. margin: 0 0 6px;
  291. padding: 8px 10px;
  292. border: 1px solid transparent;
  293. border-radius: 8px;
  294. background: #f6f8fb;
  295. color: #334155;
  296. font-size: 13px;
  297. line-height: 1.4;
  298. text-align: left;
  299. cursor: pointer;
  300. }
  301. .command-mark {
  302. color: #8aa0bd;
  303. flex-shrink: 0;
  304. }
  305. .command-name {
  306. word-break: break-all;
  307. }
  308. .command-btn:hover,
  309. .command-btn:focus {
  310. outline: none;
  311. border-color: #d5e4fb;
  312. background: #f3f8ff;
  313. }
  314. .command-btn.active {
  315. border-color: transparent;
  316. color: #fff;
  317. background: linear-gradient(135deg, #2fb0ff 0%, #b863fb 100%);
  318. box-shadow: 0 6px 14px rgba(90, 120, 230, 0.28);
  319. }
  320. .command-btn.active .command-mark {
  321. color: rgba(255, 255, 255, 0.85);
  322. }
  323. .empty {
  324. color: #94a3b8;
  325. font-size: 13px;
  326. text-align: center;
  327. padding: 28px 8px;
  328. }
  329. .json-box,
  330. .return-box {
  331. width: 100%;
  332. box-sizing: border-box;
  333. border: 1px solid #e6ebf2;
  334. border-radius: 8px;
  335. background: #fbfcfe;
  336. color: #1f2937;
  337. font-size: 13px;
  338. line-height: 1.55;
  339. }
  340. .json-box {
  341. flex: 1;
  342. min-height: 0;
  343. resize: none;
  344. padding: 12px;
  345. font-family: Consolas, Monaco, monospace;
  346. outline: none;
  347. }
  348. .json-box:focus {
  349. border-color: #8eb7ff;
  350. background: #fff;
  351. box-shadow: 0 0 0 3px rgba(47, 176, 255, 0.12);
  352. }
  353. .json-box::placeholder {
  354. color: #b0bac8;
  355. }
  356. .return-box {
  357. padding: 8px;
  358. text-align: left;
  359. }
  360. .return-empty {
  361. padding: 24px 8px;
  362. }
  363. .return-line {
  364. display: flex;
  365. align-items: flex-start;
  366. gap: 8px;
  367. padding: 8px 10px;
  368. border-radius: 8px;
  369. background: #fff;
  370. }
  371. .return-line + .return-line {
  372. margin-top: 6px;
  373. }
  374. .return-line.is-error {
  375. background: #fff6f5;
  376. }
  377. .return-time {
  378. flex-shrink: 0;
  379. color: #7b8ba3;
  380. font-size: 12px;
  381. line-height: 1.5;
  382. font-variant-numeric: tabular-nums;
  383. }
  384. .return-msg {
  385. flex: 1;
  386. min-width: 0;
  387. white-space: pre-wrap;
  388. word-break: break-all;
  389. }
  390. .return-line.is-error .return-msg {
  391. color: #c2410c;
  392. }
  393. .action-row {
  394. display: flex;
  395. gap: 12px;
  396. flex-shrink: 0;
  397. }
  398. .action-btn {
  399. flex: 1;
  400. height: 42px;
  401. margin: 0;
  402. padding: 0 12px;
  403. border-radius: 10px;
  404. font-size: 14px;
  405. font-weight: 600;
  406. cursor: pointer;
  407. }
  408. .action-btn:hover,
  409. .action-btn:focus {
  410. outline: none;
  411. }
  412. .action-btn--ghost {
  413. border: 1px solid #d7deea;
  414. background: #fff;
  415. color: #475569;
  416. }
  417. .action-btn--ghost:hover,
  418. .action-btn--ghost:focus {
  419. border-color: #b9c6da;
  420. background: #f8fafc;
  421. }
  422. .action-btn--primary {
  423. border: none;
  424. color: #fff;
  425. background: linear-gradient(135deg, #2fb0ff 0%, #b863fb 100%);
  426. box-shadow: 0 8px 16px rgba(90, 120, 230, 0.28);
  427. }
  428. .action-btn--primary:hover,
  429. .action-btn--primary:focus {
  430. filter: brightness(1.04);
  431. }
  432. </style>