Просмотр исходного кода

feat(device): 添加设备状态监控和MCU错误日志功能

- 在header-bar和photography页面添加设备状态按钮
- 实现统一的设备状态对话框组件
- 添加MCU错误日志收集和展示功能
- 集成设备运行状态实时监控
- 配置MCU错误日志最大条数限制
- 移除Python服务器自动创建功能
- 注释掉模型切换菜单项以简化界面
panqiuyao 14 часов назад
Родитель
Сommit
7c86061047

+ 7 - 0
electron/config/config.default.js

@@ -158,6 +158,13 @@ module.exports = (appInfo) => {
   };
 
   /**
+   * MCU 错误日志配置
+   */
+  config.mcuErrorLog = {
+    maxLogs: 100,  // 最大日志条数(可调整)
+  };
+
+  /**
    * jobs
    */
   config.jobs = {

+ 29 - 0
electron/controller/socket.js

@@ -43,6 +43,35 @@ class SocketController extends Controller {
   disconnect() {
     pySocket.disconnect()
   }
+
+  /**
+   * 获取 MCU 错误日志列表
+   */
+  getMcuErrorLogs() {
+    return pySocket.getMcuErrorLogs();
+  }
+
+  /**
+   * 获取 MCU 错误日志未读数
+   */
+  getMcuErrorUnreadCount() {
+    return pySocket.getMcuErrorUnreadCount();
+  }
+
+  /**
+   * 清除 MCU 错误日志未读状态
+   */
+  clearMcuErrorUnread() {
+    return pySocket.clearMcuErrorUnread();
+  }
+
+  /**
+   * 设置 MCU 错误日志最大条数
+   * @param {number} max - 最大条数
+   */
+  setMcuErrorLogMax(max) {
+    return pySocket.setMcuErrorLogMax(max);
+  }
 }
 
 SocketController.toString = () => '[class SocketController]';

+ 1 - 1
electron/preload/index.js

@@ -13,5 +13,5 @@ module.exports = async () => {
   Addon.get("security").create();
   Addon.get("awaken").create();
   Addon.get("autoUpdater").create();
-  Services.get("cross").createPythonServer();
+  // Services.get("cross").createPythonServer();
 };

+ 75 - 0
electron/utils/socket.js

@@ -25,6 +25,25 @@ const typeToMessage = {
   detail_result_progress:"PhotographyDetail"
 }
 
+// MCU 错误日志配置(从 config 读取,默认100条)
+let mcuErrorLogConfig = {
+  maxLogs: 100,  // 最大日志条数
+};
+
+// 初始化时从配置读取
+try {
+  const config = readConfigFile();
+  if (config.mcuErrorLog && typeof config.mcuErrorLog.maxLogs === 'number') {
+    mcuErrorLogConfig.maxLogs = config.mcuErrorLog.maxLogs;
+  }
+} catch (e) {
+  console.log('读取 MCU 日志配置失败,使用默认值');
+}
+
+// MCU 错误日志存储
+const mcuErrorLogs = [];
+let mcuErrorUnreadCount = 0;
+
 
 const previewPath = path.join(app.getPath("userData"),'preview','liveview.png');
 
@@ -234,6 +253,11 @@ const pySocket = function () {
                 this.handleHeartbeatResponse();
                 notAllMessage = true;
                 break;
+              case 'print_mcu_error_data':
+                // 处理 MCU 错误日志
+                this.handleMcuErrorData(this_data, win);
+                notAllMessage = true;
+                break;
             }
             if(notAllMessage) return;
             let channel = 'controller.socket.message_'+this_data.msg_type;
@@ -355,6 +379,57 @@ const pySocket = function () {
       }
     })
   }
+
+  // 处理 MCU 错误数据
+  this.handleMcuErrorData = function(data, win) {
+    const logEntry = {
+      message: data.data?.message || '',
+      current_time: data.data?.current_time || new Date().toLocaleString('zh-CN', { hour12: false }).replace(/\//g, '-'),
+      raw: data  // 保留原始数据
+    };
+
+    // 添加到日志数组(限制最大条数)
+    mcuErrorLogs.unshift(logEntry);
+    if (mcuErrorLogs.length > mcuErrorLogConfig.maxLogs) {
+      mcuErrorLogs.pop();
+    }
+
+    // 未读数 +1
+    mcuErrorUnreadCount++;
+
+    // 发送未读状态变更通知
+    win.webContents.send('controller.mcu.errorLogUnread', { count: mcuErrorUnreadCount });
+
+    // 同时将原始消息转发给前端(保持原有功能)
+    win.webContents.send('controller.socket.message_print_mcu_error_data', data);
+  };
+
+  // 获取 MCU 错误日志列表
+  this.getMcuErrorLogs = function() {
+    return [...mcuErrorLogs];
+  };
+
+  // 获取未读数
+  this.getMcuErrorUnreadCount = function() {
+    return mcuErrorUnreadCount;
+  };
+
+  // 清除未读状态
+  this.clearMcuErrorUnread = function() {
+    mcuErrorUnreadCount = 0;
+    return true;
+  };
+
+  // 设置最大日志条数(供外部调用)
+  this.setMcuErrorLogMax = function(max) {
+    mcuErrorLogConfig.maxLogs = max;
+    // 如果当前日志超过限制,裁剪
+    while (mcuErrorLogs.length > mcuErrorLogConfig.maxLogs) {
+      mcuErrorLogs.pop();
+    }
+    return true;
+  };
+
   return this;
 }
 

+ 20 - 143
frontend/src/components/check/index.vue

@@ -60,43 +60,32 @@
     </template>
   </el-dialog>
 
-  <!-- 设备状态对话框 -->
-  <el-dialog
-    v-model="showStatusDialog"
-    title="设备运行状态"
-    width="600px"
-    :close-on-click-modal="false"
-  >
-    <div class="status-content">
-      <div class="status-item" v-for="(status, key) in deviceStatus" :key="key">
-        <span class="status-label">{{ getStatusLabel(key) }}:</span>
-        <span class="status-value" :class="getStatusClass(status)">{{ getStatusText(status) }}</span>
-      </div>
-    </div>
-    <template #footer>
-      <span class="dialog-footer">
-        <el-button @click="showStatusDialog = false">取消</el-button>
-        <el-button type="primary" :loading="statusLoading" @click="getDeviceStatus">刷新</el-button>
-      </span>
-    </template>
-  </el-dialog>
+  <!-- 使用统一的设备状态对话框组件 -->
+  <DeviceStatusDialog />
 </template>
 
 <script setup>
-import { ref, computed, watch, onBeforeUnmount, nextTick, watchEffect } from 'vue';
-import { ElMessage, ElDialog, ElButton } from 'element-plus';
+import { ref, watchEffect } from 'vue';
+import { ElMessage } from 'element-plus';
 import useUserInfo from "@/stores/modules/user";
 import checkInfo from "@/stores/modules/check";
 import client from "@/stores/modules/client";
-import socket from "@/stores/modules/socket";
-import icpList from '@/utils/ipc';
-const clientStore = client();
+import DeviceStatusDialog from '@/components/device-status-dialog/index.vue';
+import useDeviceStatusStore from '@/stores/modules/deviceStatus';
+
+const useUserInfoStore = useUserInfo()
+const checkInfoStore = checkInfo()
+const clientStore = client()
+const deviceStatusStore = useDeviceStatusStore()
+
+// 检测是否成功的状态
+const checkSuccess = ref(false);
+
+// 检测加载状态
+const checkLoading = ref(true);
 
 /**
  * 定义组件的 props。
- * @param {Object} props
- * @param {Boolean} props.modelValue - 控制对话框显示状态的双向绑定值,默认为 false。
- * @param {String} props.title - 对话框标题,默认为 '检测硬件'。
  */
 const props = defineProps({
   isInitCheck: {
@@ -109,31 +98,7 @@ const props = defineProps({
   }
 });
 
-
-// 初始化用户信息状态管理
-const useUserInfoStore = useUserInfo()
-const checkInfoStore = checkInfo()
-const socketStore = socket()
-
-// 检测是否成功的状态
-const checkSuccess = ref(false);
-
-// 检测加载状态
-const checkLoading = ref(true);
-
-// 设备状态相关
-const showStatusDialog = ref(false);
-const statusLoading = ref(false);
-const deviceStatus = ref({
-  state_camera_motor: 0,
-  state_camera_steering: 0,
-  state_turntable_steering: 0,
-  state_move_turntable_steering: 0,
-  state_overturn_steering: 0
-});
-
-// 定义事件发射器,用于更新父组件的 modelValue 和触发 confirm 事件
-const emit = defineEmits([ 'confirm']);
+const emit = defineEmits(['confirm']);
 
 // 检测次数计数器
 const checkCount = ref(props.isInitCheck ? 0 : 1);
@@ -231,98 +196,10 @@ function confirm(){
   emit('confirm')
 }
 
-onBeforeUnmount(() => {
-  clientStore.ipc.removeAllListeners(icpList.socket.message + '_get_mcu_info')
-})
-
-// 状态文本映射
-const stateTextMap = {
-  0: "未初始化",
-  1: "运动中",
-  2: "已停止",
-  3: "未在线",
-  4: "堵转"
-}
-
-// 获取状态标签
-function getStatusLabel(key) {
-  const labelMap = {
-    state_camera_motor: '相机高度状态',
-    state_camera_steering: '相机角度状态',
-    state_turntable_steering: '转盘状态',
-    state_move_turntable_steering: '转盘前后移动状态',
-    state_overturn_steering: '翻板状态'
-  }
-  return labelMap[key] || key
-}
-
-// 获取状态文本
-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 openStatusDialog() {
-  showStatusDialog.value = true
-  getDeviceStatus()
+  deviceStatusStore.openDialog();
 }
-
-// 获取设备状态
-function getDeviceStatus() {
-  statusLoading.value = true
-
-  // 移除之前的监听器
-  clientStore.ipc.removeAllListeners(icpList.socket.message + '_get_mcu_info')
-
-  // 发送获取MCU信息的消息
-  socketStore.sendMessage({
-    type: 'get_mcu_info',
-    data: null
-  })
-
-  // 监听MCU信息响应
-  clientStore.ipc.on(icpList.socket.message + '_get_mcu_info', (event, result) => {
-    console.log('_get_mcu_info')
-    console.log(result)
-    statusLoading.value = false
-
-    if (result.code === 0 && result.data.data_state) {
-      deviceStatus.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
-      }
-    } else {
-      ElMessage.error(result.msg || '获取设备状态失败')
-    }
-
-    clientStore.ipc.removeAllListeners(icpList.socket.message + '_get_mcu_info')
-  })
-
-  // 设置超时
-  setTimeout(() => {
-    if (statusLoading.value) {
-      statusLoading.value = false
-      ElMessage.error('获取设备状态超时')
-      clientStore.ipc.removeAllListeners(icpList.socket.message + '_get_mcu_info')
-    }
-  }, 10000)
-}
-
 </script>
 
 

+ 280 - 0
frontend/src/components/device-status-dialog/index.vue

@@ -0,0 +1,280 @@
+<template>
+  <el-dialog
+    v-model="deviceStatusStore.showDialog"
+    title="设备状态"
+    width="700px"
+    :close-on-click-modal="true"
+    @close="handleClose"
+  >
+    <div class="device-status-content">
+      <!-- 设备运行状态区域 -->
+      <div class="status-section">
+        <div class="section-title">
+          <span>运行状态</span>
+          <el-button size="small" :loading="deviceStatusStore.statusLoading" @click="refreshStatus">
+            刷新
+          </el-button>
+        </div>
+        <div class="status-grid">
+          <div class="status-item">
+            <span class="status-label">相机高度状态</span>
+            <span class="status-value" :class="getStatusClass(deviceStatusStore.runStatus.state_camera_motor)">
+              {{ getStatusText(deviceStatusStore.runStatus.state_camera_motor) }}
+            </span>
+          </div>
+          <div class="status-item">
+            <span class="status-label">相机角度状态</span>
+            <span class="status-value" :class="getStatusClass(deviceStatusStore.runStatus.state_camera_steering)">
+              {{ getStatusText(deviceStatusStore.runStatus.state_camera_steering) }}
+            </span>
+          </div>
+          <div class="status-item">
+            <span class="status-label">转盘状态</span>
+            <span class="status-value" :class="getStatusClass(deviceStatusStore.runStatus.state_turntable_steering)">
+              {{ getStatusText(deviceStatusStore.runStatus.state_turntable_steering) }}
+            </span>
+          </div>
+          <div class="status-item">
+            <span class="status-label">转盘前后移动状态</span>
+            <span class="status-value" :class="getStatusClass(deviceStatusStore.runStatus.state_move_turntable_steering)">
+              {{ getStatusText(deviceStatusStore.runStatus.state_move_turntable_steering) }}
+            </span>
+          </div>
+          <div class="status-item">
+            <span class="status-label">翻板状态</span>
+            <span class="status-value" :class="getStatusClass(deviceStatusStore.runStatus.state_overturn_steering)">
+              {{ getStatusText(deviceStatusStore.runStatus.state_overturn_steering) }}
+            </span>
+          </div>
+        </div>
+      </div>
+
+      <!-- 日志区域 -->
+      <div class="log-section">
+        <div class="section-title">
+          <span>运行日志</span>
+          <div class="log-header-right">
+            <el-input
+              v-model="searchKeyword"
+              placeholder="搜索日志内容"
+              size="small"
+              clearable
+              style="width: 200px; margin-right: 10px;"
+              @input="filterLogs"
+            />
+            <span class="log-count">共 {{ filteredLogs.length }} 条记录</span>
+          </div>
+        </div>
+
+        <!-- 空状态 -->
+        <div v-if="filteredLogs.length === 0" class="empty-state">
+          <el-empty :description="searchKeyword ? '未找到匹配的日志' : '暂无设备状态日志'" :image-size="60" />
+        </div>
+
+        <!-- 日志列表 -->
+        <div v-else class="log-list">
+          <div
+            v-for="(log, index) in filteredLogs"
+            :key="index"
+            class="log-item"
+          >
+            <span class="log-time">{{ log.current_time }}</span>
+            <span class="log-message">{{ log.message }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <el-button @click="handleClose">关闭</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed } from 'vue';
+import useDeviceStatusStore from '@/stores/modules/deviceStatus';
+
+const deviceStatusStore = useDeviceStatusStore();
+
+// 搜索关键词
+const searchKeyword = ref('');
+
+// 过滤后的日志
+const filteredLogs = computed(() => {
+  if (!searchKeyword.value) {
+    return deviceStatusStore.logs;
+  }
+  const keyword = searchKeyword.value.toLowerCase();
+  return deviceStatusStore.logs.filter(log =>
+    log.message.toLowerCase().includes(keyword) ||
+    log.current_time.toLowerCase().includes(keyword)
+  );
+});
+
+// 过滤日志(防抖处理)
+let filterTimer: ReturnType<typeof setTimeout> | null = null;
+function filterLogs() {
+  // 使用 computed 直接计算,不需要额外处理
+}
+
+// 状态文本映射
+const stateTextMap: Record<number, string> = {
+  0: "未初始化",
+  1: "运动中",
+  2: "已停止",
+  3: "未在线",
+  4: "堵转"
+};
+
+// 获取状态文本
+function getStatusText(status: number): string {
+  return stateTextMap[status] || '未知状态';
+}
+
+// 获取状态样式类
+function getStatusClass(status: number): string {
+  const classMap: Record<number, string> = {
+    0: 'status-uninit',
+    1: 'status-moving',
+    2: 'status-stopped',
+    3: 'status-offline',
+    4: 'status-blocked'
+  };
+  return classMap[status] || 'status-unknown';
+}
+
+// 刷新设备状态
+async function refreshStatus() {
+  await deviceStatusStore.fetchRunStatus();
+}
+
+function handleClose() {
+  searchKeyword.value = '';
+  deviceStatusStore.closeDialog();
+}
+</script>
+
+<style scoped lang="scss">
+.device-status-content {
+  max-height: 500px;
+  overflow-y: auto;
+
+  .section-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    font-weight: 600;
+    font-size: 14px;
+    color: #333;
+    margin-bottom: 12px;
+    padding-bottom: 8px;
+    border-bottom: 1px solid #eee;
+
+    .log-header-right {
+      display: flex;
+      align-items: center;
+    }
+
+    .log-count {
+      font-weight: normal;
+      font-size: 12px;
+      color: #999;
+    }
+  }
+
+  .status-section {
+    margin-bottom: 20px;
+    padding-bottom: 16px;
+    border-bottom: 1px solid #eee;
+
+    .status-grid {
+      display: grid;
+      grid-template-columns: repeat(2, 1fr);
+      gap: 10px;
+
+      .status-item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 8px 12px;
+        background: #f5f5f5;
+        border-radius: 4px;
+
+        .status-label {
+          font-size: 13px;
+          color: #666;
+        }
+
+        .status-value {
+          font-weight: bold;
+          font-size: 13px;
+
+          &.status-uninit {
+            color: #909399;
+          }
+
+          &.status-moving {
+            color: #e6a23c;
+          }
+
+          &.status-stopped {
+            color: #67c23a;
+          }
+
+          &.status-offline {
+            color: #f56c6c;
+          }
+
+          &.status-blocked {
+            color: #f56c6c;
+          }
+
+          &.status-unknown {
+            color: #909399;
+          }
+        }
+      }
+    }
+  }
+
+  .log-section {
+    .empty-state {
+      padding: 20px 0;
+    }
+
+    .log-list {
+      max-height: 250px;
+      overflow-y: auto;
+
+      .log-item {
+        display: flex;
+        align-items: center;
+        padding: 8px 12px;
+        border-bottom: 1px solid #f0f0f0;
+        background: #fafafa;
+        margin-bottom: 4px;
+        border-radius: 4px;
+        font-size: 13px;
+
+        &:last-child {
+          border-bottom: none;
+          margin-bottom: 0;
+        }
+
+        .log-time {
+          flex-shrink: 0;
+          color: #999;
+          margin-right: 12px;
+          min-width: 150px;
+        }
+
+        .log-message {
+          color: #333;
+          word-break: break-all;
+        }
+      }
+    }
+  }
+}
+</style>

+ 42 - 2
frontend/src/components/header-bar/index.vue

@@ -64,6 +64,18 @@
           </div>
         </div>
       </div>
+      <!-- 设备状态按钮 -->
+      <div
+        v-if="showDeviceStatus"
+        class="header-bar__menu-item"
+        @click="openDeviceStatus"
+        title="设备状态"
+      >
+        <div class="header-bar__menu-item-content flex">
+          <img :src="deviceStatusIcon" class="header-bar__menu-icon" />
+          <span class="header-bar__menu-name">设备状态</span>
+        </div>
+      </div>
     </div>
     <div class="header-bar__title">
       <span class="header-bar__text">
@@ -98,6 +110,10 @@
       </div>
     </div>
   </div>
+
+  <!-- 设备状态日志弹窗 -->
+  <DeviceStatusDialog />
+
   <div class="header-bar_blank"></div>
 </template>
 
@@ -113,6 +129,17 @@ import { getRouterUrl } from '@/utils/appfun'
 import client from '@/stores/modules/client'
 import packageJson from '@/../../package.json';
 import { getCameraMachineDoc } from '@/apis/other';
+import useDeviceStatusStore from '@/stores/modules/deviceStatus';
+import DeviceStatusDialog from '@/components/device-status-dialog/index.vue';
+
+// 设备状态 store
+const deviceStatusStore = useDeviceStatusStore();
+
+// 设备状态图标
+const deviceStatusIcon = ref(iconsz);
+
+// 是否显示设备状态按钮
+/*const showDeviceStatus = ref(true);*/
 
 const clientStore = client()
 const useUserInfoStore = useUserInfo()
@@ -140,6 +167,10 @@ const props = defineProps({
   showUser: {
     type: Boolean,
     default: false
+  },
+  showDeviceStatus: {
+    type: Boolean,
+    default: false  // 默认显示设备状态按钮
   }
 })
 
@@ -301,12 +332,21 @@ function loginOut() {
   useUserInfoStore.updateLoginShow(true)
 }
 
+// 打开设备状态弹窗
+function openDeviceStatus() {
+  deviceStatusStore.openDialog();
+}
+
 onMounted(() => {
-  document.addEventListener('click', handleOutsideClick)
+  document.addEventListener('click', handleOutsideClick);
+  // 初始化设备状态监听
+  deviceStatusStore.initListener();
 })
 
 onUnmounted(() => {
-  document.removeEventListener('click', handleOutsideClick)
+  document.removeEventListener('click', handleOutsideClick);
+  // 清理监听
+  deviceStatusStore.cleanup();
 })
 
 function handleOutsideClick(event: MouseEvent) {

+ 197 - 0
frontend/src/stores/modules/deviceStatus.ts

@@ -0,0 +1,197 @@
+import { defineStore } from 'pinia';
+import { ref } from 'vue';
+import client from "@/stores/modules/client";
+import icpList from '@/utils/ipc';
+import socket from "@/stores/modules/socket";
+
+export interface McuErrorLog {
+  message: string;
+  current_time: string;
+  raw?: any;
+}
+
+export interface DeviceRunStatus {
+  state_camera_motor: number;
+  state_camera_steering: number;
+  state_turntable_steering: number;
+  state_move_turntable_steering: number;
+  state_overturn_steering: number;
+}
+
+const useDeviceStatusStore = defineStore('deviceStatus', () => {
+  const clientStore = client();
+  const socketStore = socket();
+
+  // 日志列表
+  const logs = ref<McuErrorLog[]>([]);
+
+  // 未读数量
+  const unreadCount = ref(0);
+
+  // 弹窗是否显示
+  const showDialog = ref(false);
+
+  // 设备运行状态
+  const runStatus = ref<DeviceRunStatus>({
+    state_camera_motor: 0,
+    state_camera_steering: 0,
+    state_turntable_steering: 0,
+    state_move_turntable_steering: 0,
+    state_overturn_steering: 0
+  });
+
+  // 设备运行状态加载状态
+  const statusLoading = ref(false);
+
+  // 是否已初始化监听
+  let isListenerInitialized = false;
+
+  // 初始化事件监听
+  function initListener() {
+    if (!clientStore.isClient || isListenerInitialized) return;
+
+    isListenerInitialized = true;
+
+    // 监听未读状态变更
+    clientStore.ipc.on(icpList.mcu.errorLogUnread, (event, result) => {
+      if (result && typeof result.count === 'number') {
+        unreadCount.value = result.count;
+      }
+    });
+
+    // 监听 MCU 错误日志消息
+    clientStore.ipc.on(icpList.mcu.errorLog, (event, result) => {
+      if (result && result.data) {
+        const logEntry: McuErrorLog = {
+          message: result.data.message || '',
+          current_time: result.data.current_time || new Date().toLocaleString('zh-CN', { hour12: false }),
+          raw: result
+        };
+        logs.value.unshift(logEntry);
+      }
+    });
+  }
+
+  // 获取日志列表
+  async function fetchLogs() {
+    if (!clientStore.isClient) return [];
+
+    return new Promise((resolve) => {
+      clientStore.ipc.removeAllListeners(icpList.mcu.getErrorLogs);
+      clientStore.ipc.send(icpList.mcu.getErrorLogs);
+      clientStore.ipc.once(icpList.mcu.getErrorLogs, (event, result) => {
+        if (Array.isArray(result)) {
+          logs.value = result;
+        }
+        resolve(result);
+      });
+    });
+  }
+
+  // 清除未读状态
+  async function clearUnread() {
+    if (!clientStore.isClient) return;
+
+    return new Promise((resolve) => {
+      clientStore.ipc.removeAllListeners(icpList.mcu.clearErrorUnread);
+      clientStore.ipc.send(icpList.mcu.clearErrorUnread);
+      clientStore.ipc.once(icpList.mcu.clearErrorUnread, (event, result) => {
+        unreadCount.value = 0;
+        resolve(result);
+      });
+    });
+  }
+
+  // 获取设备运行状态
+  async function fetchRunStatus(): 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;
+
+        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);
+        }
+      });
+
+      // 设置超时
+      setTimeout(() => {
+        if (statusLoading.value) {
+          statusLoading.value = false;
+          resolve(null);
+        }
+      }, 10000);
+    });
+  }
+
+  // 打开弹窗
+  async function openDialog() {
+    showDialog.value = true;
+    // 先清除未读,再并行获取日志和设备状态
+    clearUnread();
+    await Promise.all([
+      fetchLogs(),
+      fetchRunStatus()
+    ]);
+  }
+
+  // 关闭弹窗
+  function closeDialog() {
+    showDialog.value = false;
+  }
+
+  // 设置最大日志条数
+  async function setMaxLogs(max: number) {
+    if (!clientStore.isClient) return;
+
+    clientStore.ipc.removeAllListeners(icpList.mcu.setErrorLogMax);
+    clientStore.ipc.send(icpList.mcu.setErrorLogMax, { max });
+  }
+
+  // 清理监听
+  function cleanup() {
+    if (!clientStore.isClient) return;
+
+    clientStore.ipc.removeAllListeners(icpList.mcu.errorLogUnread);
+    clientStore.ipc.removeAllListeners(icpList.mcu.errorLog);
+    clientStore.ipc.removeAllListeners(icpList.socket.message + '_get_mcu_info');
+    isListenerInitialized = false;
+  }
+
+  return {
+    logs,
+    unreadCount,
+    showDialog,
+    runStatus,
+    statusLoading,
+    initListener,
+    fetchLogs,
+    fetchRunStatus,
+    clearUnread,
+    openDialog,
+    closeDialog,
+    setMaxLogs,
+    cleanup
+  };
+});
+
+export default useDeviceStatusStore;

+ 7 - 0
frontend/src/utils/ipc.ts

@@ -15,6 +15,13 @@ const icpList = {
         sendMessage: 'controller.socket.sendMessage',
         disconnect: 'controller.socket.disconnect',
     },
+    mcu:{
+        errorLog: 'controller.socket.message_print_mcu_error_data',  // MCU 错误日志消息
+        errorLogUnread: 'controller.mcu.errorLogUnread',              // 未读状态变更通知
+        getErrorLogs: 'controller.mcu.getErrorLogs',                  // 获取日志列表
+        clearErrorUnread: 'controller.mcu.clearErrorUnread',          // 清除未读状态
+        setErrorLogMax: 'controller.mcu.setErrorLogMax',              // 设置最大日志条数
+    },
     utils:{
         openMain: 'controller.utils.openMain',
         shellFun: 'controller.utils.shellFun',

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

@@ -5,6 +5,7 @@
       title="拍摄物体镜头矫正"
       :menu="menu"
       showUser
+      showDeviceStatus
   />
   <div class="check-wrap">
 
@@ -156,9 +157,9 @@ async function checkConfirm(init){
     menu.push({
       type:'developer'
     })
-    menu.push({
+/*    menu.push({
       type:'toggleModel'
-    })
+    })*/
 
     menu.push({
       type:'setting'

+ 1 - 0
frontend/src/views/Photography/shot.vue

@@ -2,6 +2,7 @@
   <headerBar
       title="拍摄商品"
       showUser
+      showDeviceStatus
       :menu="menu"
   />