| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- <template>
- <div class="page">
- <aside class="side">
- <div class="panel-head">
- <span class="label">命令列表</span>
- <span v-if="commands.length" class="count">{{ commands.length }}</span>
- </div>
- <div class="command-list">
- <button
- v-for="(item, index) in commands"
- :key="index"
- type="button"
- class="command-btn"
- :class="{ active: activeIndex === index }"
- @click="selectCommand(item, index)"
- v-log="{ describe: { action: '点击任一参数命令', name: item.name } }"
- >
- <span class="command-mark">#</span>
- <span class="command-name">{{ item.name }}</span>
- </button>
- <div v-if="loaded && !commands.length" class="empty">暂无命令</div>
- </div>
- </aside>
- <section class="editor">
- <div class="panel">
- <div class="panel-head">
- <span class="label">JSON内容</span>
- <span v-if="activeName" class="hint">#{{ activeName }}</span>
- </div>
- <textarea
- v-model="jsonText"
- class="json-box"
- spellcheck="false"
- placeholder="选择左侧命令,或直接填写 JSON / 十六进制命令"
- />
- </div>
- <div class="panel">
- <div class="panel-head">
- <span class="label">数据返回</span>
- <span v-if="returnLines.length" class="count">{{ returnLines.length }}</span>
- </div>
- <div ref="returnBox" class="return-box">
- <div v-if="!returnLines.length" class="empty return-empty">下发后,设备返回会显示在这里</div>
- <div
- v-for="(line, index) in returnLines"
- :key="index"
- class="return-line"
- :class="{ 'is-error': isErrorMessage(line.message) }"
- >
- <span v-if="line.time" class="return-time">{{ line.time }}</span>
- <span class="return-msg">{{ line.message }}</span>
- </div>
- </div>
- </div>
- <div class="action-row">
- <button
- type="button"
- class="action-btn action-btn--ghost"
- @click="clearReturn"
- v-log="{ describe: { action: '点击清空任一参数返回' } }"
- >
- 清空
- </button>
- <button
- type="button"
- class="action-btn action-btn--primary"
- @click="sendJson"
- v-log="{ describe: { action: '点击下发JSON' } }"
- >
- 下发JSON
- </button>
- </div>
- </section>
- </div>
- </template>
- <script setup lang="ts">
- import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
- import { ElMessage } from 'element-plus'
- import client from '@/stores/modules/client'
- import socket from '@/stores/modules/socket'
- import icpList from '@/utils/ipc'
- type CommandItem = {
- name: string
- command: string
- }
- const clientStore = client()
- const socketStore = socket()
- type ReturnLine = {
- time: string
- message: string
- }
- const commands = ref<CommandItem[]>([])
- const activeIndex = ref(-1)
- const jsonText = ref('')
- const returnLines = ref<ReturnLine[]>([])
- const returnBox = ref<HTMLElement | null>(null)
- const loaded = ref(false)
- const activeName = computed(() => commands.value[activeIndex.value]?.name || '')
- const listChannel = icpList.socket.message + '_get_one_data_set'
- const sendChannel = icpList.socket.message + '_send_get_one_data_set'
- const printChannel = icpList.mcu.errorLog
- function selectCommand(item: CommandItem, index: number) {
- activeIndex.value = index
- jsonText.value = item.command || ''
- }
- function clearReturn() {
- returnLines.value = []
- }
- function isErrorMessage(message: string) {
- const text = String(message || '').trimStart()
- return text.startsWith('!') || text.startsWith('!')
- }
- async function appendReturn(message: string, time = '') {
- if (!message && !time) return
- returnLines.value.push({ time, message })
- await nextTick()
- if (returnBox.value) {
- returnBox.value.scrollTop = returnBox.value.scrollHeight
- }
- }
- function onCommandList(_event: unknown, result: any) {
- console.log('[任一参数下发] get_one_data_set 回包', result)
- loaded.value = true
- const list = result?.data?.commands
- if (result?.code === 0 && Array.isArray(list)) {
- commands.value = list
- console.log('[任一参数下发] 命令条数', list.length, list)
- return
- }
- commands.value = []
- console.log('[任一参数下发] 未解析出命令列表', {
- code: result?.code,
- msg: result?.msg,
- data: result?.data
- })
- if (result?.msg) ElMessage.error(result.msg)
- }
- function onSendResult(_event: unknown, result: any) {
- console.log('[任一参数下发] send_get_one_data_set 回包', result)
- if (result?.code === 0) return
- const text = result?.msg || '下发失败'
- appendReturn(text)
- ElMessage.error(text)
- }
- function onPrint(_event: unknown, result: any) {
- console.log('[任一参数下发] print_mcu_error_data 回包', result)
- const time = result?.data?.current_time || ''
- const message = result?.data?.message || ''
- appendReturn(message, time)
- }
- function bindListeners() {
- if (!clientStore.isClient) return
- clientStore.ipc.removeListener(listChannel, onCommandList)
- clientStore.ipc.removeListener(sendChannel, onSendResult)
- clientStore.ipc.removeListener(printChannel, onPrint)
- clientStore.ipc.on(listChannel, onCommandList)
- clientStore.ipc.on(sendChannel, onSendResult)
- clientStore.ipc.on(printChannel, onPrint)
- }
- function unbindListeners() {
- if (!clientStore.isClient) return
- clientStore.ipc.removeListener(listChannel, onCommandList)
- clientStore.ipc.removeListener(sendChannel, onSendResult)
- clientStore.ipc.removeListener(printChannel, onPrint)
- }
- function loadCommands() {
- const payload = {
- type: 'get_one_data_set'
- }
- console.log('[任一参数下发] 发送', payload, '监听', listChannel)
- socketStore.sendMessage(payload)
- }
- function sendJson() {
- if (!jsonText.value.trim()) {
- ElMessage.warning('请先选择或填写要下发的命令')
- return
- }
- const payload = {
- type: 'send_get_one_data_set',
- data: {
- command: jsonText.value
- }
- }
- console.log('[任一参数下发] 发送', payload)
- socketStore.sendMessage(payload)
- }
- onMounted(() => {
- bindListeners()
- if (clientStore.isClient) loadCommands()
- })
- onUnmounted(() => {
- unbindListeners()
- })
- </script>
- <style scoped lang="scss">
- .page {
- height: 100vh;
- box-sizing: border-box;
- display: flex;
- gap: 14px;
- padding: 14px;
- background: #f3f5f9;
- color: #1f2937;
- text-align: left;
- }
- .side,
- .panel {
- background: #fff;
- border: 1px solid #e6ebf2;
- border-radius: 12px;
- box-shadow: 0 8px 24px rgba(31, 45, 80, 0.05);
- }
- .side {
- width: 228px;
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- min-height: 0;
- padding: 12px 10px 10px;
- }
- .editor {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .panel {
- flex: 1;
- min-height: 0;
- display: flex;
- flex-direction: column;
- padding: 12px 14px 14px;
- }
- .panel-head {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 10px;
- flex-shrink: 0;
- }
- .label {
- font-size: 14px;
- font-weight: 600;
- line-height: 1.4;
- color: #1f2937;
- }
- .count {
- min-width: 20px;
- height: 20px;
- padding: 0 6px;
- border-radius: 10px;
- background: #eef3fb;
- color: #4b628a;
- font-size: 12px;
- line-height: 20px;
- text-align: center;
- }
- .hint {
- margin-left: auto;
- max-width: 50%;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- color: #6b7c93;
- font-size: 12px;
- }
- .command-list,
- .return-box {
- flex: 1;
- min-height: 0;
- overflow-y: auto;
- }
- .command-list::-webkit-scrollbar,
- .return-box::-webkit-scrollbar,
- .json-box::-webkit-scrollbar {
- width: 6px;
- }
- .command-list::-webkit-scrollbar-thumb,
- .return-box::-webkit-scrollbar-thumb,
- .json-box::-webkit-scrollbar-thumb {
- background: #d5dce8;
- border-radius: 6px;
- }
- .command-btn {
- display: flex;
- align-items: flex-start;
- gap: 6px;
- width: 100%;
- margin: 0 0 6px;
- padding: 8px 10px;
- border: 1px solid transparent;
- border-radius: 8px;
- background: #f6f8fb;
- color: #334155;
- font-size: 13px;
- line-height: 1.4;
- text-align: left;
- cursor: pointer;
- }
- .command-mark {
- color: #8aa0bd;
- flex-shrink: 0;
- }
- .command-name {
- word-break: break-all;
- }
- .command-btn:hover,
- .command-btn:focus {
- outline: none;
- border-color: #d5e4fb;
- background: #f3f8ff;
- }
- .command-btn.active {
- border-color: transparent;
- color: #fff;
- background: linear-gradient(135deg, #2fb0ff 0%, #b863fb 100%);
- box-shadow: 0 6px 14px rgba(90, 120, 230, 0.28);
- }
- .command-btn.active .command-mark {
- color: rgba(255, 255, 255, 0.85);
- }
- .empty {
- color: #94a3b8;
- font-size: 13px;
- text-align: center;
- padding: 28px 8px;
- }
- .json-box,
- .return-box {
- width: 100%;
- box-sizing: border-box;
- border: 1px solid #e6ebf2;
- border-radius: 8px;
- background: #fbfcfe;
- color: #1f2937;
- font-size: 13px;
- line-height: 1.55;
- }
- .json-box {
- flex: 1;
- min-height: 0;
- resize: none;
- padding: 12px;
- font-family: Consolas, Monaco, monospace;
- outline: none;
- }
- .json-box:focus {
- border-color: #8eb7ff;
- background: #fff;
- box-shadow: 0 0 0 3px rgba(47, 176, 255, 0.12);
- }
- .json-box::placeholder {
- color: #b0bac8;
- }
- .return-box {
- padding: 8px;
- text-align: left;
- }
- .return-empty {
- padding: 24px 8px;
- }
- .return-line {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- padding: 8px 10px;
- border-radius: 8px;
- background: #fff;
- }
- .return-line + .return-line {
- margin-top: 6px;
- }
- .return-line.is-error {
- background: #fff6f5;
- }
- .return-time {
- flex-shrink: 0;
- color: #7b8ba3;
- font-size: 12px;
- line-height: 1.5;
- font-variant-numeric: tabular-nums;
- }
- .return-msg {
- flex: 1;
- min-width: 0;
- white-space: pre-wrap;
- word-break: break-all;
- }
- .return-line.is-error .return-msg {
- color: #c2410c;
- }
- .action-row {
- display: flex;
- gap: 12px;
- flex-shrink: 0;
- }
- .action-btn {
- flex: 1;
- height: 42px;
- margin: 0;
- padding: 0 12px;
- border-radius: 10px;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- }
- .action-btn:hover,
- .action-btn:focus {
- outline: none;
- }
- .action-btn--ghost {
- border: 1px solid #d7deea;
- background: #fff;
- color: #475569;
- }
- .action-btn--ghost:hover,
- .action-btn--ghost:focus {
- border-color: #b9c6da;
- background: #f8fafc;
- }
- .action-btn--primary {
- border: none;
- color: #fff;
- background: linear-gradient(135deg, #2fb0ff 0%, #b863fb 100%);
- box-shadow: 0 8px 16px rgba(90, 120, 230, 0.28);
- }
- .action-btn--primary:hover,
- .action-btn--primary:focus {
- filter: brightness(1.04);
- }
- </style>
|