浏览代码

feat(setting): 优化高级配置参数管理界面

- 为每个参数输入框添加独立的保存按钮和保存状态显示
- 实现单个参数的实时保存功能,支持成功/失败状态反馈
- 添加参数保存加载状态指示器和防重复提交保护
- 集成设备状态对话框到应用根组件,移除各页面重复挂载
- 为相机焦段距离添加提示工具和初始化功能
- 重构参数网格布局样式,优化响应式显示效果
- 移除前端不再使用的设备信息获取接口映射
panqiuyao 3 周之前
父节点
当前提交
8834542768

+ 1 - 2
electron/utils/socket.js

@@ -24,8 +24,7 @@ const typeToMessage = {
   upload_goods_progress:"PhotographyDetail",
   detail_result_progress:"PhotographyDetail",
   "get_dynamic_config":"seeting",
-  "set_dynamic_config":"seeting",
-  "get_mcu_info":"seeting",
+  "set_dynamic_config":"seeting"
 }
 
 // MCU 错误日志配置(从 config 读取,默认100条)

+ 9 - 0
frontend/src/App.vue

@@ -8,13 +8,19 @@
 
     <Login v-if="useUserInfoStore.loginShow" v-model:dialogVisible="useUserInfoStore.loginShow" />
 
+    <!-- 全局唯一的设备状态弹窗,避免多页面同时挂载时重复弹出 -->
+    <DeviceStatusDialog />
+
 </template>
 <script setup lang="ts">
 import { useRoute, useRouter } from 'vue-router'
 import { onMounted, onUnmounted } from 'vue'
 import Login from '@/components/login/index.vue';
+import DeviceStatusDialog from '@/components/device-status-dialog/index.vue';
 import useUserInfo from "@/stores/modules/user";
+import useDeviceStatusStore from '@/stores/modules/deviceStatus';
 const useUserInfoStore = useUserInfo();
+const deviceStatusStore = useDeviceStatusStore();
 
 import { listenerOnline } from '@/composables/online';
 listenerOnline();
@@ -42,10 +48,13 @@ const handleCompanyExpired = (event: CustomEvent) => {
 
 onMounted(() => {
   window.addEventListener('companyExpired', handleCompanyExpired as EventListener)
+  // 在 App 级别初始化设备状态监听,整个会话只初始化一次
+  deviceStatusStore.initListener();
 })
 
 onUnmounted(() => {
   window.removeEventListener('companyExpired', handleCompanyExpired as EventListener)
+  deviceStatusStore.cleanup();
 })
 
 

+ 0 - 4
frontend/src/components/check/index.vue

@@ -59,9 +59,6 @@
         </div>
     </template>
   </el-dialog>
-
-  <!-- 使用统一的设备状态对话框组件 -->
-  <DeviceStatusDialog />
 </template>
 
 <script setup>
@@ -70,7 +67,6 @@ import { ElMessage } from 'element-plus';
 import useUserInfo from "@/stores/modules/user";
 import checkInfo from "@/stores/modules/check";
 import client from "@/stores/modules/client";
-import DeviceStatusDialog from '@/components/device-status-dialog/index.vue';
 import useDeviceStatusStore from '@/stores/modules/deviceStatus';
 
 const useUserInfoStore = useUserInfo()

+ 1 - 9
frontend/src/components/header-bar/index.vue

@@ -121,9 +121,6 @@
     </div>
   </div>
 
-  <!-- 设备状态日志弹窗 -->
-  <DeviceStatusDialog />
-
   <div class="header-bar_blank"></div>
 </template>
 
@@ -140,11 +137,10 @@ 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';
 import socket from "@/stores/modules/socket";
 import {  ElMessageBox } from 'element-plus'
 
-// 设备状态 store
+// 设备状态 store(弹窗由 App.vue 全局挂载,这里仅用于触发 openDialog)
 const deviceStatusStore = useDeviceStatusStore();
 
 // 设备状态图标
@@ -374,14 +370,10 @@ async function  OneClickRelease(){
 
 onMounted(() => {
   document.addEventListener('click', handleOutsideClick);
-  // 初始化设备状态监听
-  deviceStatusStore.initListener();
 })
 
 onUnmounted(() => {
   document.removeEventListener('click', handleOutsideClick);
-  // 清理监听
-  deviceStatusStore.cleanup();
 })
 
 function handleOutsideClick(event: MouseEvent) {

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

@@ -118,6 +118,8 @@ const useDeviceStatusStore = defineStore('deviceStatus', () => {
 
       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 = {

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

@@ -5,6 +5,7 @@
       title="拍摄物体镜头矫正"
       :menu="menu"
       showUser
+      showDeviceStatus
   />
   <div class="check-wrap">
 

+ 28 - 3
frontend/src/views/Photography/components/editRow.vue

@@ -46,9 +46,24 @@
       </el-form-item>
       <!-- 相机焦段距离 - 仅当设备支持时显示 -->
       <el-form-item label="焦段距离(mm)" v-if="hasCameraFocal" :class="{'is-disabled': isMultiCameraMode && editRowData.is_move_device === 0}">
-        <el-input v-model="editRowData.camera_focal_distance" @change="changeNum('camera_focal_distance', 0, maxFocalDistance)" :min="0" :max="maxFocalDistance" :step="1" style="width: 170px;" type="number" :disabled="isMultiCameraMode && editRowData.is_move_device === 0">
-        </el-input>
-        <div class="error-msg">最小0,最大{{ maxFocalDistance }}</div>
+
+        <template #label>
+
+          焦段距离(mm)
+          <el-tooltip class="box-item" effect="dark" placement="left-end">
+            <template #content>
+              请注意,焦段距离如果过大,可能会导致设备滑牙,需要重新调整设备以及初始化;
+            </template>
+            <el-icon><QuestionFilled /></el-icon>
+          </el-tooltip>
+        </template>
+        <template #default>
+
+          <el-input v-model="editRowData.camera_focal_distance" @change="changeNum('camera_focal_distance', 0, maxFocalDistance)" :min="0" :max="maxFocalDistance" :step="1" style="width: 100px;" type="number" :disabled="isMultiCameraMode && editRowData.is_move_device === 0">
+          </el-input>
+          <el-button size="small" plain style="margin-left: 8px;" @click="onRecheckFocal" v-log="{ describe: { action: '点击焦段距离初始化' } }">初始化</el-button>
+          <div class="error-msg">最小0,最大{{ maxFocalDistance }},</div>
+        </template>
       </el-form-item>
       <el-form-item label="转盘前后位置" :class="{'is-disabled': isMultiCameraMode && editRowData.is_move_device === 0}">
         <el-input v-model="editRowData.turntable_position" @change="changeNum('turntable_position_motor',0, 800)" :min="0" :max="800" :step="1"  style="width: 170px;" type="number" :disabled="isMultiCameraMode && editRowData.is_move_device === 0">
@@ -172,6 +187,16 @@ const onRefreshPointList = async () => {
   }
 }
 
+
+
+const onRecheckFocal = async () => {
+  socketStore.sendMessage({
+    type: 'to_recheck_focal',
+    data: 'to_recheck_focal'
+  });
+
+}
+
 // 定义 props
 const props = defineProps({
   id:{

+ 121 - 80
frontend/src/views/Setting/components/AdvancedConfig.vue

@@ -30,6 +30,7 @@
                 controls-position="right"
                 size="default"
                 placeholder="请输入"
+                class="param-input"
               />
               <el-input
                 v-else
@@ -37,17 +38,32 @@
                 :disabled="param.readonly"
                 size="default"
                 placeholder="请输入"
+                class="param-input"
               />
+              <el-button
+                type="primary"
+                size="default"
+                :loading="savingIndex === index"
+                :disabled="param.readonly"
+                @click="saveParam(param, index)"
+                class="param-save-btn"
+              >
+                <el-icon v-if="savingIndex !== index"><Check /></el-icon>
+                保存
+              </el-button>
+            </div>
+            <div
+              v-if="param.saveStatus"
+              class="param-status"
+              :class="`status-${param.saveStatus}`"
+            >
+              <el-icon>
+                <component :is="param.saveStatus === 'success' ? CircleCheck : CircleClose" />
+              </el-icon>
+              <span>{{ param.saveStatus === 'success' ? '已保存' : '保存失败' }}</span>
             </div>
           </div>
         </div>
-        <div class="content-footer" v-if="dynamicParams.length > 0">
-          <span class="param-count">共 {{ dynamicParams.length }} 个参数</span>
-          <el-button type="primary" @click="saveDynamicParams" :loading="saving">
-            <el-icon v-if="!saving"><Check /></el-icon>
-            保存参数
-          </el-button>
-        </div>
       </div>
     </div>
   </div>
@@ -56,7 +72,7 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 import { ElMessage } from 'element-plus';
-import { Refresh, Check, FolderOpened } from '@element-plus/icons-vue';
+import { Refresh, Check, FolderOpened, CircleCheck, CircleClose } from '@element-plus/icons-vue';
 import socket from "@/stores/modules/socket";
 import client from "@/stores/modules/client";
 import icpList from '@/utils/ipc';
@@ -69,7 +85,10 @@ const dynamicParams = ref<any[]>([]);
 
 // 加载状态
 const loading = ref(false);
-const saving = ref(false);
+const savingIndex = ref<number>(-1);
+
+// 状态自动清除定时器
+const statusTimers = new Map<number, number>();
 
 // 获取动态参数配置
 async function fetchDynamicConfig() {
@@ -87,7 +106,8 @@ async function fetchDynamicConfig() {
           readonly: result.data[key].readonly,
           type: result.data[key].type,
           precision: result.data[key].precision,
-          value: result.data[key].value
+          value: result.data[key].value,
+          saveStatus: ''
         }));
       } else if (result.msg) {
         ElMessage.error(result.msg);
@@ -105,71 +125,76 @@ async function fetchDynamicConfig() {
   });
 }
 
-// 保存动态参数
-async function saveDynamicParams() {
-  saving.value = true;
-
-  const paramsToSave = dynamicParams.value.filter(p => !p.readonly);
-
-  if (paramsToSave.length === 0) {
-    ElMessage.warning('没有可保存的参数');
-    saving.value = false;
+// 保存单个参数
+function saveParam(param: any, index: number) {
+  if (param.readonly || savingIndex.value !== -1) {
     return;
   }
 
-  let successCount = 0;
-  let failCount = 0;
-
-  for (const param of paramsToSave) {
-    const success = await saveSingleParam(param);
-    if (success) {
-      successCount++;
-    } else {
-      failCount++;
+  savingIndex.value = index;
+  param.saveStatus = '';
+
+  socketStore.sendMessage({
+    type: 'set_dynamic_config',
+    data: {
+      addr: param.addr,
+      tips: param.tips,
+      readonly: param.readonly,
+      type: param.type,
+      precision: param.precision,
+      value: String(param.value)
     }
-  }
+  });
 
-  if (failCount === 0) {
-    ElMessage.success(`保存成功 (${successCount}项)`);
-  } else {
-    ElMessage.warning(`部分保存成功 (成功${successCount}项, 失败${failCount}项)`);
-  }
+  const handler = (event: any, result: any) => {
+    clientStore.ipc.removeAllListeners(icpList.socket.message + '_set_dynamic_config');
+    savingIndex.value = -1;
 
-  saving.value = false;
-}
+    const name = param.tips || `参数${param.addr}`;
+    if (result.code === 0) {
+      param.saveStatus = 'success';
+      ElMessage.success(`${name} 保存成功`);
+    } else {
+      param.saveStatus = 'fail';
+      ElMessage.error(result.msg || `${name} 保存失败`);
+    }
 
-// 保存单个参数
-function saveSingleParam(param: any): Promise<boolean> {
-  return new Promise((resolve) => {
-    socketStore.sendMessage({
-      type: 'set_dynamic_config',
-      data: {
-        addr: param.addr,
-        tips: param.tips,
-        readonly: param.readonly,
-        type: param.type,
-        precision: param.precision,
-        value: String(param.value)
-      }
-    });
+    scheduleClearStatus(index);
+  };
 
-    const handler = (event: any, result: any) => {
-      clientStore.ipc.removeAllListeners(icpList.socket.message + '_set_dynamic_config');
-      resolve(result.code === 0);
-    };
+  clientStore.ipc.once(icpList.socket.message + '_set_dynamic_config', handler);
 
-    clientStore.ipc.once(icpList.socket.message + '_set_dynamic_config', handler);
+  setTimeout(() => {
+    clientStore.ipc.removeAllListeners(icpList.socket.message + '_set_dynamic_config');
+    if (savingIndex.value === index) {
+      savingIndex.value = -1;
+      param.saveStatus = 'fail';
+      ElMessage.error('保存超时');
+      scheduleClearStatus(index);
+    }
+  }, 5000);
+}
 
-    setTimeout(() => {
-      clientStore.ipc.removeAllListeners(icpList.socket.message + '_set_dynamic_config');
-      resolve(false);
-    }, 5000);
-  });
+// 延时清除保存状态
+function scheduleClearStatus(index: number) {
+  const oldTimer = statusTimers.get(index);
+  if (oldTimer) {
+    clearTimeout(oldTimer);
+  }
+  const timer = window.setTimeout(() => {
+    if (dynamicParams.value[index]) {
+      dynamicParams.value[index].saveStatus = '';
+    }
+    statusTimers.delete(index);
+  }, 3000);
+  statusTimers.set(index, timer);
 }
 
 // 刷新数据
 async function refreshAll() {
   loading.value = true;
+  statusTimers.forEach(timer => clearTimeout(timer));
+  statusTimers.clear();
   await fetchDynamicConfig();
   loading.value = false;
 }
@@ -227,27 +252,9 @@ $text-secondary: #909399;
   padding: 20px;
 }
 
-.content-footer {
-  margin-top: 24px;
-  padding-top: 20px;
-  border-top: 1px dashed $border-color;
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-
-  .param-count {
-    font-size: 13px;
-    color: $text-secondary;
-  }
-
-  .el-button {
-    min-width: 120px;
-  }
-}
-
 .param-grid {
   display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
   gap: 16px;
 }
 
@@ -277,6 +284,20 @@ $text-secondary: #909399;
   }
 
   .param-body {
+    display: flex;
+    align-items: center;
+    gap: 8px;
+
+    .param-input {
+      flex: 1;
+      min-width: 0;
+    }
+
+    .param-save-btn {
+      flex-shrink: 0;
+      min-width: 80px;
+    }
+
     :deep(.el-input-number) {
       width: 100% !important;
 
@@ -293,6 +314,26 @@ $text-secondary: #909399;
       }
     }
   }
+
+  .param-status {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    margin-top: 10px;
+    font-size: 12px;
+
+    &.status-success {
+      color: #67c23a;
+    }
+
+    &.status-fail {
+      color: #f56c6c;
+    }
+
+    .el-icon {
+      font-size: 14px;
+    }
+  }
 }
 
 .empty-tip {
@@ -310,4 +351,4 @@ $text-secondary: #909399;
     color: #dcdfe6;
   }
 }
-</style>
+</style>