Quellcode durchsuchen

feat(photography): 添加常用操作悬浮按钮功能

- 新增一键释放、一键回落和设备关机操作按钮
- 实现悬浮按钮展开收起交互效果
- 集成socket通信发送设备控制指令
- 添加操作确认弹窗防止误操作
- 增加操作日志记录功能
- 实现遮罩层点击关闭功能
- 添加危险操作样式区分
- 适配demo模式下隐藏功能
panqiuyao vor 1 Monat
Ursprung
Commit
2345d8af55

+ 272 - 120
frontend/src/components/check/index.vue

@@ -5,17 +5,14 @@
     :close-on-click-modal="false"
     :close-on-press-escape="false"
     :show-close="false"
-    width="500px"
+    width="680px"
     class="hardware-check-dialog"
   >
     <div class="hardware-check-container">
       <el-timeline>
-        <!-- Step 1 -->
-        <el-timeline-item
-          :type="'primary'"
-          :hollow="true">
+        <el-timeline-item type="primary" :hollow="true">
           <template #dot>
-            <img src="@/assets/images/check/icon1.png" class="custom-timeline-icon" />
+            <div class="step-dot">1</div>
           </template>
           <div class="step-content">
             <div class="step-title">
@@ -24,21 +21,20 @@
               <template v-else>初始化检测硬件失败</template>
             </div>
             <el-progress
+              v-if="checkLoading"
               :percentage="checkInfoStore.getProgress"
               :colors="['#2FB0FF', '#B863FB']"
               :stroke-width="8"
-              v-if="checkLoading"
-            ></el-progress>
-            <div class="check-error-result" v-else-if="!checkSuccess">失败的原因,{{ checkInfoStore.getErrorMsg }}</div>
+            />
+            <div class="check-error-result" v-else-if="!checkSuccess">
+              失败的原因,{{ checkInfoStore.getErrorMsg }}
+            </div>
           </div>
         </el-timeline-item>
 
-        <!-- Step 2 -->
-        <el-timeline-item
-          :type="'primary'"
-          :hollow="true">
+        <el-timeline-item type="primary" :hollow="true">
           <template #dot>
-            <img src="@/assets/images/check/icon2.png" class="custom-timeline-icon" />
+            <div class="step-dot">2</div>
           </template>
           <div class="step-content">
             <div class="step-title">自我检查</div>
@@ -49,22 +45,56 @@
           </div>
         </el-timeline-item>
       </el-timeline>
-    </div>
-    <template #footer v-if="!checkLoading">
-        <div class="flex" v-if="!checkSuccess">
-            <div class="check-btn cu-p" style="width: 160px; margin-right: 10px;" @click="reCheck">重新监测一次</div>
-            <div class="check-btn check-btn-secondary cu-p" style="width: 160px;" @click="openStatusDialog">查看设备状态</div>
+
+      <div class="device-summary">
+        <div class="device-row">
+          <span class="device-label">MCU设备</span>
+          <span class="device-value" :class="mcuConnected ? 'is-ok' : 'is-bad'">
+            {{ mcuConnected ? '已连接MCU' : '未连接MCU' }}
+          </span>
+          <span class="device-serial" v-if="serialSummary">{{ serialSummary }}</span>
         </div>
-        <div class="flex" v-else>
-            <div class="check-btn cu-p" style="width: 180px" @click="confirm()">检测成功,继续操作!</div>
+        <div class="device-row">
+          <span class="device-label">相机设备</span>
+          <span class="device-value" :class="cameraConnected ? 'is-ok' : 'is-bad'">
+            {{ cameraConnected ? '已连接相机' : '未连接相机' }}
+          </span>
         </div>
+
+        <div class="motor-grid">
+          <div class="motor-item" v-for="item in motorItems" :key="item.key">
+            <div class="motor-name">{{ item.label }}</div>
+            <div class="motor-status" :class="getStatusClass(deviceStatusStore.runStatus[item.key])">
+              {{ getStatusText(deviceStatusStore.runStatus[item.key]) }}
+            </div>
+          </div>
+        </div>
+
+        <div class="device-log-box">
+          <div v-if="recentLogs.length === 0" class="log-empty">暂无设备上报信息</div>
+          <div v-else class="log-lines">
+            <div v-for="(log, index) in recentLogs" :key="index" class="log-line">
+              {{ log.message }}
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <template #footer v-if="!checkLoading">
+      <div class="flex" v-if="!checkSuccess">
+        <div class="check-btn cu-p" style="width: 160px; margin-right: 10px;" @click="reCheck">重新监测一次</div>
+        <div class="check-btn check-btn-secondary cu-p" style="width: 160px;" @click="openStatusDialog">查看设备状态</div>
+      </div>
+      <div class="flex" v-else>
+        <div class="check-btn cu-p" style="width: 180px" @click="confirm()">检测成功,继续操作!</div>
+      </div>
     </template>
   </el-dialog>
 </template>
 
 <script setup>
-import { ref, watchEffect } from 'vue';
-import { ElMessage } from 'element-plus';
+import { ref, watch, watchEffect, computed, onUnmounted } from 'vue';
 import useUserInfo from "@/stores/modules/user";
 import checkInfo from "@/stores/modules/check";
 import client from "@/stores/modules/client";
@@ -75,15 +105,9 @@ const checkInfoStore = checkInfo()
 const clientStore = client()
 const deviceStatusStore = useDeviceStatusStore()
 
-// 检测是否成功的状态
 const checkSuccess = ref(false);
-
-// 检测加载状态
 const checkLoading = ref(true);
 
-/**
- * 定义组件的 props。
- */
 const props = defineProps({
   isInitCheck: {
     type: Boolean,
@@ -97,45 +121,71 @@ const props = defineProps({
 
 const emit = defineEmits(['confirm']);
 
-// 检测次数计数器
 const checkCount = ref(props.isInitCheck ? 0 : 1);
+const visible = ref(false);
 
-// 进度条的当前进度值
+const motorItems = [
+  { key: 'state_camera_motor', label: '相机升降' },
+  { key: 'state_camera_steering', label: '相机角度' },
+  { key: 'state_move_turntable_steering', label: '前后移动' },
+  { key: 'state_turntable_steering', label: '转盘电机' },
+  { key: 'state_overturn_steering', label: '翻板' },
+]
+
+const stateTextMap = {
+  0: '未初始化',
+  1: '运动中',
+  2: '已停止',
+  3: '未在线',
+  4: '堵转'
+}
 
-// 定时器变量,用于控制进度条的递增
-let timer = null;
+const mcuConnected = computed(() => checkInfoStore.mcuConnected)
+const serialSummary = computed(() => checkInfoStore.serialSummary)
+const cameraConnected = computed(() => checkInfoStore.devices.cam_control.status === 2)
+const recentLogs = computed(() =>
+  (deviceStatusStore.logs || []).filter((log) => log.message).slice(0, 6)
+)
 
-// 控制组件可见性的状态
-const visible = ref(false);
+function getStatusText(status) {
+  return stateTextMap[status] || '未知状态'
+}
+
+function getStatusClass(status) {
+  const classMap = {
+    0: 'status-uninit',
+    1: 'status-moving',
+    2: 'status-stopped',
+    3: 'status-offline',
+    4: 'status-blocked'
+  }
+  return classMap[status] || 'status-unknown'
+}
 
+function syncMotorListen() {
+  if (visible.value && checkInfoStore.mcuConnected) {
+    deviceStatusStore.startRunStatusListen()
+  } else {
+    deviceStatusStore.stopRunStatusListen()
+    if (!checkInfoStore.mcuConnected) {
+      deviceStatusStore.resetRunStatus()
+    }
+  }
+}
 
-/**
- * 开始进度条的递增逻辑。
- * 该函数会重置进度条并启动定时器,逐步增加进度值直到达到 80。
- */
 function startProgress() {
   checkInfoStore.set_isCheckStatus(true);
   checkLoading.value = true;
   checkSuccess.value = false;
+  deviceStatusStore.resetRunStatus()
   checkInfoStore.reCheckAction()
+  deviceStatusStore.fetchLogs()
 }
 
-/**
- * 重新执行检测逻辑。
- * 调用 startProgress 函数以重新开始进度条。
- */
 function reCheck() {
   startProgress()
- // checkInfoStore.reCheckAction()
 }
 
-// 初始化客户端状态管理
-
-/**
- * 监听用户信息和检测次数的变化,自动触发相关逻辑。
- * 当用户信息存在且检测次数为 0 时,设置组件可见性为 true,
- * 并尝试连接 WebSocket 和发送消息。
- */
 watchEffect(async ()=>{
   if( useUserInfoStore.userInfo.id && checkCount.value === 0 && checkInfoStore.isCheckStatus){
     if(clientStore.isClient){
@@ -170,7 +220,6 @@ watchEffect(async ()=>{
 
 
 
-
 watchEffect(async ()=>{
   if( checkInfoStore.getErrorMsg){
     checkLoading.value = false;
@@ -185,6 +234,20 @@ watchEffect(async ()=>{
   }
 })
 
+watch(visible, (val) => {
+  if (val) {
+    deviceStatusStore.fetchLogs()
+    checkInfoStore.requestHardwareCheckInfo()
+  }
+  syncMotorListen()
+})
+
+watch(() => checkInfoStore.mcuConnected, (connected) => {
+  if (visible.value && connected) {
+    checkInfoStore.requestHardwareCheckInfo()
+  }
+  syncMotorListen()
+})
 
 function confirm(){
   visible.value = false;
@@ -193,16 +256,19 @@ function confirm(){
   emit('confirm')
 }
 
-// 打开设备状态对话框
 function openStatusDialog() {
   deviceStatusStore.openDialog();
 }
+
+onUnmounted(() => {
+  deviceStatusStore.stopRunStatusListen()
+})
 </script>
 
 
 <style lang="scss" scoped>
 .hardware-check-container {
-  padding: 20px 0;
+  padding: 8px 8px 0;
 }
 
 .step-content {
@@ -210,22 +276,16 @@ function openStatusDialog() {
 }
 
 .step-title {
-font-weight: 600;
-font-size: 14px;
-color: #000000;
-  margin-bottom: 15px;
+  font-weight: 600;
+  font-size: 14px;
+  color: #000000;
+  margin-bottom: 12px;
   padding-top: 4px;
   text-align: left;
 }
 
-.progress-text {
-  text-align: right;
-  margin-top: 5px;
-  color: #606266;
-}
-
 .check-result {
-    color: rgba(0,0,0,0.45);
+  color: rgba(0,0,0,0.45);
   text-align: left;
   p {
     margin: 8px 0;
@@ -235,75 +295,167 @@ color: #000000;
 :deep(.el-progress-bar__inner) {
   background-image: linear-gradient(to right, #409EFF, #7e57c2);
 }
-.custom-timeline-icon{
-    width: 24px;
-    height: 24px;
-    position: relative;
-    left: -7px;
+
+:deep(.el-timeline) {
+  padding-left: 4px;
 }
+
+:deep(.el-timeline-item__tail) {
+  left: 7px;
+}
+
+.step-dot {
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  background: #2F6BFF;
+  color: #fff;
+  font-size: 13px;
+  font-weight: 600;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
+  left: -5px;
+}
+
 .check-btn{
-    width: 100px;
-    height: 40px;
-    background: linear-gradient( 135deg, #2FB0FF 0%, #B863FB 100%);
-    border-radius: 4px;
-    font-weight: 400;
-    font-size: 16px;
-    color: #FFFFFF;
-    text-align: center;
-    line-height: 40px;
+  width: 100px;
+  height: 40px;
+  background: linear-gradient( 135deg, #2FB0FF 0%, #B863FB 100%);
+  border-radius: 4px;
+  font-weight: 400;
+  font-size: 16px;
+  color: #FFFFFF;
+  text-align: center;
+  line-height: 40px;
 }
+
 .check-error-result{
-    font-size: 14px;
-    text-align: left;
-    color: #FF4C00;
+  font-size: 14px;
+  text-align: left;
+  color: #FF4C00;
 }
 
 .check-btn-secondary {
-    background: linear-gradient(135deg, #909399 0%, #C0C4CC 100%);
-}
-
-// 设备状态对话框样式
-.status-content {
-  .status-item {
-    display: flex;
-    align-items: center;
-    margin-bottom: 12px;
-    padding: 8px 12px;
-    background: #f5f5f5;
-    border-radius: 4px;
-
-    .status-label {
-      font-weight: 500;
-      min-width: 120px;
-    }
+  background: linear-gradient(135deg, #909399 0%, #C0C4CC 100%);
+}
 
-    .status-value {
-      font-weight: bold;
+.device-summary {
+  margin-top: 4px;
+  padding-left: 36px;
+}
 
-      &.status-uninit {
-        color: #909399;
-      }
+.device-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+  font-size: 14px;
+  line-height: 22px;
+}
 
-      &.status-moving {
-        color: #e6a23c;
-      }
+.device-label {
+  color: #333;
+  min-width: 72px;
+  margin-right: 12px;
+}
 
-      &.status-stopped {
-        color: #67c23a;
-      }
+.device-value {
+  font-weight: 600;
 
-      &.status-offline {
-        color: #f56c6c;
-      }
+  &.is-ok {
+    color: #67C23A;
+  }
 
-      &.status-blocked {
-        color: #f56c6c;
-      }
+  &.is-bad {
+    color: #F56C6C;
+  }
+}
 
-      &.status-unknown {
-        color: #909399;
-      }
-    }
+.device-serial {
+  margin-left: auto;
+  color: #F56C6C;
+  font-size: 13px;
+  text-align: right;
+  max-width: 280px;
+}
+
+.motor-grid {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 8px;
+  margin: 8px 0 14px;
+}
+
+.motor-item {
+  text-align: left;
+}
+
+.motor-name {
+  font-size: 13px;
+  color: #333;
+  margin-bottom: 4px;
+}
+
+.motor-status {
+  font-size: 13px;
+  font-weight: 500;
+
+  &.status-uninit,
+  &.status-unknown {
+    color: #909399;
+  }
+
+  &.status-moving {
+    color: #E6A23C;
+  }
+
+  &.status-stopped {
+    color: #67C23A;
+  }
+
+  &.status-offline,
+  &.status-blocked {
+    color: #F56C6C;
+  }
+}
+
+.device-log-box {
+  background: #F5F6F7;
+  border-radius: 8px;
+  padding: 12px 14px;
+  min-height: 108px;
+  max-height: 132px;
+  overflow-y: auto;
+}
+
+.log-empty {
+  color: #999;
+  font-size: 13px;
+  line-height: 22px;
+}
+
+.log-line {
+  color: #666;
+  font-size: 13px;
+  line-height: 22px;
+  word-break: break-all;
+}
+</style>
+
+<style lang="scss">
+.hardware-check-dialog {
+  .el-dialog__title {
+    display: block;
+    width: 100%;
+    text-align: center;
+    font-size: 16px;
+    font-weight: 400;
+    color: #333;
+  }
+
+  .el-dialog__body {
+    padding-top: 10px;
   }
 }
 </style>

+ 196 - 3
frontend/src/stores/modules/check.ts

@@ -10,6 +10,19 @@ const socketStore = socket();
 const clientStore = client();
 const userInfoStore = useUserInfo();
 
+export type SerialPortStatus = 'connected' | 'occupied' | 'idle'
+
+export interface SerialPortInfo {
+    name: string
+    status: SerialPortStatus
+    role?: string
+}
+
+const SERIAL_STATUS_TEXT: Record<SerialPortStatus, string> = {
+    connected: '已连接',
+    occupied: '占用',
+    idle: '空闲',
+}
 
 /**
  * 定义检查信息的Store
@@ -83,6 +96,171 @@ export const checkInfo = defineStore('checkInfo', () => {
         isInitSend:false,
         status:0,
     })
+
+    // MCU 是否已拿到设备 ID(连接标记,不等于初始化完成)
+    const mcuConnected = ref(false)
+    const mcuDeviceId = ref<number | null>(null)
+    const serialPorts = ref<SerialPortInfo[]>([])
+    let hardwareInfoListenerBound = false
+
+    const serialSummary = computed(() => {
+        if (!serialPorts.value.length) return ''
+        return serialPorts.value
+            .map((port) => `${port.name}${SERIAL_STATUS_TEXT[port.status] || port.status}`)
+            .join('、')
+    })
+
+    function upsertSerialPort(name: string, status: SerialPortStatus, role?: string) {
+        if (!name) return
+        const upper = String(name).toUpperCase()
+        const idx = serialPorts.value.findIndex((port) => port.name.toUpperCase() === upper)
+        if (idx >= 0) {
+            serialPorts.value[idx] = {
+                ...serialPorts.value[idx],
+                status,
+                role: role || serialPorts.value[idx].role,
+            }
+        } else {
+            serialPorts.value.push({ name: upper, status, role })
+        }
+    }
+
+    function applyHardwareCheckInfo(data: any) {
+        if (!data) return
+        if (data.mcu) {
+            if (typeof data.mcu.connected === 'boolean') {
+                mcuConnected.value = data.mcu.connected
+            }
+            if (data.mcu.device_id != null && data.mcu.device_id !== '') {
+                mcuDeviceId.value = Number(data.mcu.device_id)
+                mcuConnected.value = true
+            }
+            if (data.mcu.port_name) {
+                upsertSerialPort(data.mcu.port_name, 'connected', 'MCU')
+            }
+        }
+        if (data.device_id != null && data.device_id !== '') {
+            mcuDeviceId.value = Number(data.device_id)
+            mcuConnected.value = true
+        }
+        if (Array.isArray(data.serial_ports)) {
+            serialPorts.value = data.serial_ports
+                .map((port: any) => ({
+                    name: String(port?.name || '').toUpperCase(),
+                    status: (['connected', 'occupied', 'idle'].includes(port?.status)
+                        ? port.status
+                        : 'idle') as SerialPortStatus,
+                    role: port?.role,
+                }))
+                .filter((port: SerialPortInfo) => port.name)
+        }
+    }
+
+    function parseComFromText(text: string): string | null {
+        if (!text) return null
+        const match = String(text).match(/COM\d+/i)
+        return match ? match[0].toUpperCase() : null
+    }
+
+    function parseMcuMessageForHardware(result: any) {
+        if (!result) return
+        const msg = result.msg || ''
+        const data = result.data
+        const inner = data?.data
+
+        if (result.msg_type === 'hardware_check_info' || result.msg_type === 'get_hardware_check_info') {
+            applyHardwareCheckInfo(data)
+            return
+        }
+        if (data?.serial_ports || data?.mcu) {
+            applyHardwareCheckInfo(data)
+        }
+
+        if (data?._type === 'select_port_name' && inner) {
+            if (inner.port_name) {
+                const isMcu = inner.device_name === 'mcu' || inner.device_name === 'MCU'
+                upsertSerialPort(inner.port_name, 'connected', isMcu ? 'MCU' : inner.device_name)
+                if (isMcu) mcuConnected.value = true
+            } else {
+                Object.keys(inner).forEach((key) => {
+                    if (/^COM\d+/i.test(key)) {
+                        const existing = serialPorts.value.find((port) => port.name.toUpperCase() === key.toUpperCase())
+                        upsertSerialPort(key, existing?.status === 'connected' ? 'connected' : 'idle')
+                    }
+                })
+            }
+        }
+
+        if (data?._type === 'remove_port' && (inner?.port_name || data?.data?.port_name)) {
+            const portName = String(inner?.port_name || data.data.port_name).toUpperCase()
+            serialPorts.value = serialPorts.value.filter((port) => port.name.toUpperCase() !== portName)
+        }
+
+        const textSources = [
+            msg,
+            typeof inner === 'string' ? inner : '',
+            inner?.text,
+            typeof data?.data === 'string' ? data.data : '',
+        ].filter(Boolean) as string[]
+
+        textSources.forEach((text) => {
+            const comName = parseComFromText(text)
+            if (!comName) return
+            if (text.includes('被占用')) {
+                upsertSerialPort(comName, 'occupied')
+            } else if (text.includes('开始识别接口')) {
+                const existing = serialPorts.value.find((port) => port.name === comName)
+                if (!existing || existing.status === 'idle') {
+                    upsertSerialPort(comName, 'idle')
+                }
+            }
+        })
+
+        if (msg.includes('MCU 已连接') || msg.includes('MCU已连接') || msg.includes('MCU连接成功')) {
+            mcuConnected.value = true
+            if (inner?.device_id != null) mcuDeviceId.value = Number(inner.device_id)
+            if (inner?.port_name) {
+                upsertSerialPort(inner.port_name, 'connected', 'MCU')
+            } else {
+                const lastIdle = [...serialPorts.value].reverse().find((port) => port.status === 'idle')
+                if (lastIdle) {
+                    upsertSerialPort(lastIdle.name, 'connected', 'MCU')
+                } else if (serialPorts.value.length === 1) {
+                    upsertSerialPort(serialPorts.value[0].name, 'connected', 'MCU')
+                }
+            }
+        }
+
+        if (msg.includes('MCU 连接失败') || msg.includes('MCU串口未连接')) {
+            mcuConnected.value = false
+            mcuDeviceId.value = null
+        }
+    }
+
+    function resetHardwareSummary() {
+        mcuConnected.value = false
+        mcuDeviceId.value = null
+        serialPorts.value = []
+    }
+
+    function ensureHardwareInfoListener() {
+        if (!clientStore.isClient || hardwareInfoListenerBound) return
+        hardwareInfoListenerBound = true
+        const handler = (_event: any, result: any) => {
+            if (result?.data) applyHardwareCheckInfo(result.data)
+        }
+        clientStore.ipc.on(icpList.socket.message + '_hardware_check_info', handler)
+        clientStore.ipc.on(icpList.socket.message + '_get_hardware_check_info', handler)
+    }
+
+    function requestHardwareCheckInfo() {
+        ensureHardwareInfoListener()
+        if (!clientStore.isClient) return
+        socketStore.sendMessage({
+            type: 'get_hardware_check_info',
+            data: null,
+        })
+    }
     // 计算完成进度
     const getProgress = computed(() => {
         let completed = 0;
@@ -101,7 +279,7 @@ export const checkInfo = defineStore('checkInfo', () => {
             clearInterval(CKTimerInterval)
             checkTime.value = 0
             mcu.isInitSend = false
-            return '拍照机连接失败,请重新检查,可以尝试重新启动拍照机,插拔USB口,强制初始化等,请检查相机和支撑转盘是否待机休眠。如果确认未休眠,请检查各种连接线是否,正常连上。然后再重试一次,一直无法解决问题,请致电:15957854217,18805712182。';
+            return 'MCU硬件连接失败,请重新检查,可以尝试重新启动MCU硬件,插拔USB口,强制初始化等,请检查相机和支撑转盘是否待机休眠。如果确认未休眠,请检查各种连接线是否,正常连上。然后再重试一次,一直无法解决问题,请致电:15957854217,18805712182。';
         }
         for (const device of Object.values(devices)) {
             if (device.status === -1  && device.msg_type !== 'connect_bluetooth') {
@@ -191,11 +369,13 @@ export const checkInfo = defineStore('checkInfo', () => {
         try {
             await socketStore.connectSocket();
             await socketStore.sendMessage({ type: messageType });
+            clientStore.ipc.removeAllListeners(icpList.socket.message+'_'+deviceName);
             clientStore.ipc.on(icpList.socket.message+'_'+deviceName, (event, result) => {
 
                 console.log(result);
                 if (result && checkTime.value > 0) {
                     if(deviceName === 'mcu'){
+                        parseMcuMessageForHardware(result)
                         if(result.status === 2 ){
                             if(!mcu.isInitSend){
                                 socketStore.sendMessage({
@@ -218,6 +398,10 @@ export const checkInfo = defineStore('checkInfo', () => {
                             }
                         }
                         if([-1,0].includes(result.status)){
+                            // 扫描到的某个 CH34 口被占用,不作为 MCU 整体连接失败
+                            if ((result.msg || '').includes('被占用')) {
+                                return;
+                            }
                    /*         if(result.status === -1 ){
                                 if(mcuErrorCount.value  === 0){
                                     mcuErrorCount.value++;
@@ -267,6 +451,7 @@ export const checkInfo = defineStore('checkInfo', () => {
                      await checkDevice(deviceName, devices[deviceName].msg_type);
             }
         }
+        requestHardwareCheckInfo();
 
     };
 
@@ -280,7 +465,9 @@ export const checkInfo = defineStore('checkInfo', () => {
         }
         mcu.status = -0;
         mcu.isInitSend = false;
+        resetHardwareSummary();
         checkTime.value++;
+        if (CKTimerInterval) clearInterval(CKTimerInterval)
         CKTimerInterval = setInterval(()=>{
             checkTime.value++;
 
@@ -294,11 +481,11 @@ export const checkInfo = defineStore('checkInfo', () => {
                     devices.mcu.status = -1;
                     mcu.status = -1;
                     mcu.isInitSend = false;
-                    devices.mcu.msg = '拍照机连接失败';
+                    devices.mcu.msg = 'MCU硬件连接失败';
                 }
                 if( devices.cam_control.status !== 2 ){
                     devices.cam_control.status = -1;
-                    devices.cam_control.msg = '拍照机连接失败';
+                    devices.cam_control.msg = 'MCU硬件连接失败';
                 }
             }
         },1000)
@@ -326,6 +513,12 @@ export const checkInfo = defineStore('checkInfo', () => {
         set_blue_tooth_scan_NO,
         checkAction,
         reCheckAction,
+        mcuConnected,
+        mcuDeviceId,
+        serialPorts,
+        serialSummary,
+        requestHardwareCheckInfo,
+        resetHardwareSummary,
         // 多相机模式相关
         cameraList,
         currentCameraKey,

+ 72 - 33
frontend/src/stores/modules/deviceStatus.ts

@@ -45,12 +45,65 @@ const useDeviceStatusStore = defineStore('deviceStatus', () => {
 
   // 是否已初始化监听
   let isListenerInitialized = false;
+  let isRunStatusListening = false;
+  let runStatusPollTimer: ReturnType<typeof setInterval> | null = null;
+
+  function applyRunStatus(result: any): boolean {
+    const dataState = result?.data?.data_state;
+    if (result?.code === 0 && dataState) {
+      runStatus.value = {
+        state_camera_motor: dataState.state_camera_motor ?? 0,
+        state_camera_steering: dataState.state_camera_steering ?? 0,
+        state_turntable_steering: dataState.state_turntable_steering ?? 0,
+        state_move_turntable_steering: dataState.state_move_turntable_steering ?? 0,
+        state_overturn_steering: dataState.state_overturn_steering ?? 0
+      };
+      return true;
+    }
+    return false;
+  }
+
+  function bindRunStatusListener() {
+    if (!clientStore.isClient || isRunStatusListening) return;
+    isRunStatusListening = true;
+    clientStore.ipc.on(icpList.socket.message + '_get_mcu_info', (event, result) => {
+      statusLoading.value = false;
+      applyRunStatus(result);
+    });
+  }
+
+  function resetRunStatus() {
+    runStatus.value = {
+      state_camera_motor: 0,
+      state_camera_steering: 0,
+      state_turntable_steering: 0,
+      state_move_turntable_steering: 0,
+      state_overturn_steering: 0
+    };
+  }
+
+  function startRunStatusListen(interval = 2000) {
+    bindRunStatusListener();
+    fetchRunStatus({ silent: true });
+    if (runStatusPollTimer) clearInterval(runStatusPollTimer);
+    runStatusPollTimer = setInterval(() => {
+      fetchRunStatus({ silent: true });
+    }, interval);
+  }
+
+  function stopRunStatusListen() {
+    if (runStatusPollTimer) {
+      clearInterval(runStatusPollTimer);
+      runStatusPollTimer = null;
+    }
+  }
 
   // 初始化事件监听
   function initListener() {
     if (!clientStore.isClient || isListenerInitialized) return;
 
     isListenerInitialized = true;
+    bindRunStatusListener();
 
     // 监听未读状态变更
     clientStore.ipc.on(icpList.mcu.errorLogUnread, (event, result) => {
@@ -102,47 +155,28 @@ const useDeviceStatusStore = defineStore('deviceStatus', () => {
     });
   }
 
-  // 获取设备运行状态
-  async function fetchRunStatus(): Promise<DeviceRunStatus | null> {
+  // 获取设备运行状态(由常驻监听更新 runStatus,避免 once 冲掉检测弹窗的监听)
+  async function fetchRunStatus(options?: { silent?: boolean }): Promise<DeviceRunStatus | null> {
     if (!clientStore.isClient) return null;
 
-    statusLoading.value = true;
-
-    return new Promise((resolve) => {
-      clientStore.ipc.removeAllListeners(icpList.socket.message + '_get_mcu_info');
-
-      socketStore.sendMessage({
-        type: 'get_mcu_info',
-        data: null
-      });
-
-      clientStore.ipc.once(icpList.socket.message + '_get_mcu_info', (event, result) => {
-        statusLoading.value = false;
-        console.log('_get_mcu_info')
-        console.log(result)
-
-        if (result.code === 0 && result.data.data_state) {
-          runStatus.value = {
-            state_camera_motor: result.data.data_state.state_camera_motor || 0,
-            state_camera_steering: result.data.data_state.state_camera_steering || 0,
-            state_turntable_steering: result.data.data_state.state_turntable_steering || 0,
-            state_move_turntable_steering: result.data.data_state.state_move_turntable_steering || 0,
-            state_overturn_steering: result.data.data_state.state_overturn_steering || 0
-          };
-          resolve(runStatus.value);
-        } else {
-          resolve(null);
-        }
-      });
+    bindRunStatusListener();
+    if (!options?.silent) {
+      statusLoading.value = true;
+    }
+    socketStore.sendMessage({
+      type: 'get_mcu_info',
+      data: null
+    });
 
-      // 设置超时
+    if (!options?.silent) {
       setTimeout(() => {
         if (statusLoading.value) {
           statusLoading.value = false;
-          resolve(null);
         }
       }, 10000);
-    });
+    }
+
+    return runStatus.value;
   }
 
   // 打开弹窗
@@ -173,10 +207,12 @@ const useDeviceStatusStore = defineStore('deviceStatus', () => {
   function cleanup() {
     if (!clientStore.isClient) return;
 
+    stopRunStatusListen();
     clientStore.ipc.removeAllListeners(icpList.mcu.errorLogUnread);
     clientStore.ipc.removeAllListeners(icpList.mcu.errorLog);
     clientStore.ipc.removeAllListeners(icpList.socket.message + '_get_mcu_info');
     isListenerInitialized = false;
+    isRunStatusListening = false;
   }
 
   return {
@@ -188,6 +224,9 @@ const useDeviceStatusStore = defineStore('deviceStatus', () => {
     initListener,
     fetchLogs,
     fetchRunStatus,
+    resetRunStatus,
+    startRunStatusListen,
+    stopRunStatusListen,
     clearUnread,
     openDialog,
     closeDialog,