Browse Source

feat(developer): 新增任一参数下发功能

- 创建 oneDataSet 页面,支持命令列表选择与 JSON 编辑下发
- 添加路由、菜单入口及窗口打开逻辑
- 在 socket 路由映射中注册 get/send 消息通道
- 将 MCU 错误数据同时转发至 oneDataSet 窗口
- 修复窗口登记顺序,确保 loadURL 前完成引用注册
panqiuyao 20 hours ago
parent
commit
ac9b987a8a

+ 4 - 6
electron/controller/utils.js

@@ -183,15 +183,13 @@ class UtilsController extends Controller {
 
       },
     });
-    await win.loadURL(config.url); // 设置窗口的 URL
-    // 监听窗口关闭事件
-    if(configDeault.debug)  win.webContents.openDevTools()
-   //
+    // 先登记窗口,页面加载时发出的 socket 回包才能路由到这个窗口
+    this.app.electron[config.id] = win ;
     win.on('close', () => {
       delete this.app.electron[config.id]; // 删除窗口引用
     });
-
-    this.app.electron[config.id] = win ;
+    await win.loadURL(config.url); // 设置窗口的 URL
+    if(configDeault.debug && !win.isDestroyed())  win.webContents.openDevTools()
 
   }
 

+ 7 - 0
electron/utils/socket.js

@@ -18,6 +18,8 @@ const typeToMessage = {
   get_mcu_other_info:"developer",
   set_mcu_other_info:"developer",
   send_command:"developer",
+  get_one_data_set:"oneDataSet",
+  send_get_one_data_set:"oneDataSet",
   smart_shooter_get_camera_property:['seeting','default'],
   detail_progress:"PhotographyDetail",
   segment_progress:"PhotographyDetail",
@@ -414,6 +416,11 @@ const pySocket = function () {
 
     // 同时将原始消息转发给前端(保持原有功能)
     win.webContents.send('controller.socket.message_print_mcu_error_data', data);
+
+    const paramWin = this.app?.electron?.oneDataSet;
+    if (paramWin && !paramWin.isDestroyed()) {
+      paramWin.webContents.send('controller.socket.message_print_mcu_error_data', data);
+    }
   };
 
   // 获取 MCU 错误日志列表

+ 22 - 0
frontend/src/components/header-bar/index.vue

@@ -242,6 +242,11 @@ const menuType = reactive({
     icon: iconsz,
     click: openDeveloper
   },
+  oneDataSet: {
+    name: '任一参数下发',
+    icon: iconsz,
+    click: openOneDataSet
+  },
   ota: {
     name: '当前版本:'+currentVersion.value,
     click: openOTA
@@ -326,6 +331,23 @@ function openDeveloper() {
   clientStore.ipc.send(icpList.utils.openMain, params)
 }
 
+function openOneDataSet() {
+  const { href } = Router.resolve({
+    name: 'oneDataSet'
+  })
+
+  clientStore.ipc.removeAllListeners(icpList.utils.openMain)
+  let params = {
+    title: '任一参数下发',
+    width: 1000,
+    height: 720,
+    frame: true,
+    id: 'oneDataSet',
+    url: getRouterUrl(href)
+  }
+  clientStore.ipc.send(icpList.utils.openMain, params)
+}
+
 function openOTA() {
   const { href } = Router.resolve({
     name: 'ota'

+ 8 - 0
frontend/src/router/index.ts

@@ -104,6 +104,14 @@ const routes: RouteRecordRaw[] = [
             title: '初始设备调频设置'
         }
     },
+    {
+        path: "/developer/one-data-set",
+        name: "oneDataSet",
+        component: () => import("@/views/Developer/oneDataSet.vue"),
+        meta: {
+            title: '任一参数下发'
+        }
+    },
     ...tpl,
     ...otaRoutes,
 ];

+ 492 - 0
frontend/src/views/Developer/oneDataSet.vue

@@ -0,0 +1,492 @@
+<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>

+ 3 - 0
frontend/src/views/Photography/check.vue

@@ -224,6 +224,9 @@ const menu = computed(() => {
         children: [
           {
             type: 'developer'
+          },
+          {
+            type: 'oneDataSet'
           }
         ]
       },

+ 3 - 0
frontend/src/views/Photography/mixin/usePhotography.ts

@@ -716,6 +716,9 @@ export default function usePhotography() {
             children: [
               {
                 type: 'developer'
+              },
+              {
+                type: 'oneDataSet'
               }
             ]
           },