Przeglądaj źródła

Merge remote-tracking branch 'origin/dev-frontend_0804' into dev-frontend

# Conflicts:
#	electron/config/config.prod.js
#	frontend/src/config.json
panqiuyao 3 miesięcy temu
rodzic
commit
36a1a491a5
49 zmienionych plików z 3432 dodań i 1465 usunięć
  1. 3 7
      electron/api/camera.js
  2. 2 2
      electron/config/config.prod.js
  3. 12 4
      electron/controller/camera.js
  4. 4 3
      electron/controller/utils.js
  5. 5 2
      electron/utils/camera.js
  6. 5 4
      electron/utils/config.default.json
  7. 188 11
      electron/utils/socket.js
  8. 1 1
      frontend/.env.production
  9. 1 1
      frontend/src/App.vue
  10. 112 0
      frontend/src/apis/log.ts
  11. 10 0
      frontend/src/apis/other.ts
  12. BIN
      frontend/src/assets/images/Photography/cj.png
  13. BIN
      frontend/src/assets/images/Photography/mt.png
  14. BIN
      frontend/src/assets/images/Photography/xq.png
  15. 551 0
      frontend/src/components/ModelGeneration/index.vue
  16. 280 0
      frontend/src/components/ScenePromptDialog/index.vue
  17. 1 1
      frontend/src/components/check/index.vue
  18. 5 3
      frontend/src/components/header-bar/index.vue
  19. 2 2
      frontend/src/components/login/index.vue
  20. 12 2
      frontend/src/config.json
  21. 6 0
      frontend/src/main.ts
  22. 2 1
      frontend/src/router/index.ts
  23. 5 0
      frontend/src/router/plugins/authGuard.ts
  24. 5 0
      frontend/src/stores/modules/check.ts
  25. 32 0
      frontend/src/stores/modules/uuid.ts
  26. 8 2
      frontend/src/utils/appfun.ts
  27. 19 3
      frontend/src/utils/http.ts
  28. 2 1
      frontend/src/utils/ipc.ts
  29. 171 0
      frontend/src/utils/log.ts
  30. 1 1
      frontend/src/views/Developer/cmd.vue
  31. 3 3
      frontend/src/views/Developer/index.vue
  32. 2 2
      frontend/src/views/Developer/mcu.vue
  33. 61 25
      frontend/src/views/Home/index.vue
  34. 4 4
      frontend/src/views/OTA/index.vue
  35. 32 6
      frontend/src/views/Photography/check.vue
  36. 6 3
      frontend/src/views/Photography/components/LoadingDialog.vue
  37. 9 9
      frontend/src/views/Photography/components/editRow.vue
  38. 751 59
      frontend/src/views/Photography/detail.vue
  39. 6 6
      frontend/src/views/Photography/seniorDetail.vue
  40. 339 80
      frontend/src/views/Photography/shot.vue
  41. 25 2
      frontend/src/views/RemoteControl/index.vue
  42. 177 0
      frontend/src/views/Setting/components/CameraConfig.vue
  43. 53 0
      frontend/src/views/Setting/components/DebugPanel.vue
  44. 333 13
      frontend/src/views/Setting/components/action_config.vue
  45. 34 1
      frontend/src/views/Setting/components/otherConfig.vue
  46. 149 94
      frontend/src/views/Setting/index.vue
  47. 0 1104
      frontend/src/views/Setting/index_old.vue
  48. 1 1
      package.json
  49. 2 2
      public/dist/index.html

+ 3 - 7
electron/api/camera.js

@@ -59,7 +59,7 @@ const { readConfigFile } = require('../utils/config');
 
 module.exports = {
   async liveShow(){
-    if(readConfigFile().type === 'digiCamContro'){
+    if(readConfigFile().controlType === 'digiCamControl'){
       return get({
         url: '?CMD=LiveViewWnd_Show'
       })
@@ -80,7 +80,7 @@ module.exports = {
     }
   },
   async liveHide(){
-    if(readConfigFile().type === 'digiCamContro'){
+    if(readConfigFile().controlType === 'digiCamControl'){
       return get({
         url: '?CMD=LiveViewWnd_Hide'
       })
@@ -101,27 +101,23 @@ module.exports = {
     }
   },
   captureLive(){
-    return  {}
     return get({
       url: '?CMD=LiveView_Capture'
     })
   },
   capture(){
-    return  {}
     return get({
       url: '?CMD=Capture'
     })
   },
 
   CMD(cmd){
-    return  {}
     return get({
       url: '?CMD='+cmd
     })
   },
 
   getParams(params){
-    return  {}
     return  fetchExampleData(`?slc=get&param1=${params}`)
   },
   setParams(params){
@@ -136,7 +132,7 @@ module.exports = {
     })
   },
   async checkCamera(){
-    if(readConfigFile().type === 'digiCamContro'){
+    if(readConfigFile().controlType === 'digiCamControl'){
       return  fetchExampleData(`?slc=get&param1=iso`)
     }else {
 

+ 2 - 2
electron/config/config.prod.js

@@ -11,7 +11,7 @@ module.exports = (appInfo) => {
   /**
    * 开发者工具
    */
-  config.openDevTools = configDeault.openDevTools;
+  config.openDevTools = configDeault.debug;
 
   /**
    * 应用程序顶部菜单
@@ -28,7 +28,7 @@ module.exports = (appInfo) => {
    * 远程模式-web地址
    */
   config.remoteUrl = {
-    enable: false,
+    enable: configDeault.remoteUrl || false,
     url: 'http://localhost:3000/#/home'
   };
 

+ 12 - 4
electron/controller/camera.js

@@ -16,14 +16,16 @@ class CameraController extends Controller {
 
   async connect() {
     try {
+      console.log('==================');
+      console.log(readConfigFile());
+      if(readConfigFile().controlType === 'digiCamControl'){
 
-      if(readConfigFile().type === 'digiCamContro'){
-
+        console.log('========1==========');
         await getParams('iso').catch(e=>{
           isOPen = false;
         })
         if(!isOPen){
-          await checkCameraControlCmdExists(digiCamControlPath)
+          await checkCameraControlCmdExists()
           await  CMD('All_Minimize')
           await closeCameraControlTips()
           isOPen = true
@@ -63,6 +65,12 @@ class CameraController extends Controller {
         }
 
 
+        return {
+          status:-1,
+          msg:"相机未连接,请链接相机。",
+        }
+
+
       }
 
 
@@ -83,7 +91,7 @@ class CameraController extends Controller {
   async liveShow() {
     try {
       await liveShow();
-      if(readConfigFile().type === 'digiCamContro'){
+      if(readConfigFile().controlType === 'digiCamControl'){
         await  CMD('All_Minimize')
       }
       return true;

+ 4 - 3
electron/controller/utils.js

@@ -52,7 +52,7 @@ class UtilsController extends Controller {
       const win = this.app.electron[id];
 
       // 切换到指定的 URL
-      if(id === 'generate') await win.loadURL(url);
+      await win.loadURL(url);
 
       win.focus();
       win.show();
@@ -72,7 +72,7 @@ class UtilsController extends Controller {
     });
     await win.loadURL(config.url); // 设置窗口的 URL
     // 监听窗口关闭事件
-    if(configDeault.openDevTools)  win.webContents.openDevTools(config.openDevTools)
+    if(configDeault.debug)  win.webContents.openDevTools()
    //
     win.on('close', () => {
       delete this.app.electron[config.id]; // 删除窗口引用
@@ -163,7 +163,8 @@ class UtilsController extends Controller {
     const config  =  readConfigFile()
     return  {
       ...config,
-      userDataPath: app.getPath('userData')
+      userDataPath: app.getPath('userData'),
+      appPath: app.getAppPath()
     }
   }
   async readFileImageForPath(filePath,maxWidth=1500){

+ 5 - 2
electron/utils/camera.js

@@ -22,10 +22,13 @@ const exe = {
 function getExePath () {
   let exePath =  ""
   if(readConfigFile().controlType === 'digiCamControl'){
-    exePath =  path.join( readConfigFile().digiCamControlPath, exe["digiCamControl"]);
+    exePath =  path.join( readConfigFile().controlPath || readConfigFile().digiCamControlPath, exe["digiCamControl"]);
   }else if(readConfigFile().controlType === 'SmartShooter'){
-    exePath =  path.join( readConfigFile().SmartShooterPath,  exe["SmartShooter"]);
+    exePath =  path.join( readConfigFile().controlPath || readConfigFile().SmartShooterPath,  exe["SmartShooter"]);
   }
+
+  console.log('ex============ePath');
+  console.log(exePath);
   return exePath
 }
 

+ 5 - 4
electron/utils/config.default.json

@@ -1,9 +1,10 @@
 {
-  "openDevTools":false,
-  "remoteUrl": false,
+  "debug":false,
   "controlType": "SmartShooter",
-  "controlPath": "",
+  "controlPath": "C:\\Program Files\\Smart Shooter 5",
   "digiCamControlPath":"C:\\Program Files (x86)\\digiCamControl",
   "SmartShooterPath":"C:\\Program Files\\Smart Shooter 5",
-  "pyapp": "127.0.0.1"
+  "pyapp": "127.0.0.1",
+  "remoteUrl": false,
+  "env": "prod"
 }

+ 188 - 11
electron/utils/socket.js

@@ -14,7 +14,14 @@ const typeToMessage = {
   set_deviation:"developer",
   get_mcu_other_info:"developer",
   set_mcu_other_info:"developer",
-  send_command:"developer"
+  send_command:"developer",
+  smart_shooter_get_camera_property:"seeting",
+  detail_progress:"PhotographyDetail",
+  segment_progress:"PhotographyDetail",
+  upper_footer_progress:"PhotographyDetail",
+  scene_progress:"PhotographyDetail",
+  upload_goods_progress:"PhotographyDetail",
+  detail_result_progress:"PhotographyDetail"
 }
 
 
@@ -55,6 +62,128 @@ function livePreview(data){
 const pySocket = function () {
 
   this.app = null;
+  // 重连配置
+  this.reconnectConfig = {
+    maxRetries: 5,           // 最大重连次数
+    retryInterval: 3000,     // 重连间隔(毫秒)
+    maxRetryInterval: 30000, // 最大重连间隔
+    retryMultiplier: 1.5     // 重连间隔递增倍数
+  };
+  this.reconnectAttempts = 0;  // 当前重连次数
+  this.isReconnecting = false; // 是否正在重连
+  this.shouldReconnect = true; // 是否应该重连(用于手动断开时阻止重连)
+
+  // 心跳配置
+  this.heartbeatConfig = {
+    interval: 10000,        // 心跳间隔(10秒)
+    timeout: 30000,         // 心跳超时时间(30秒)
+    maxMissed: 3            // 最大允许丢失的心跳次数
+  };
+  this.heartbeatTimer = null;     // 心跳定时器
+  this.heartbeatTimeout = null;   // 心跳超时定时器
+  this.missedHeartbeats = 0;      // 丢失的心跳次数
+  this.lastHeartbeatTime = 0;     // 最后一次心跳时间
+
+  // 启动心跳机制
+  this.startHeartbeat = function() {
+    this.stopHeartbeat(); // 先停止之前的心跳
+
+    console.log('启动心跳机制,间隔:', this.heartbeatConfig.interval + 'ms');
+
+    this.heartbeatTimer = setInterval(() => {
+      if (app.socket && app.socket.readyState === WebSocket.OPEN) {
+        this.sendPing();
+        this.lastHeartbeatTime = Date.now();
+
+        // 设置心跳超时检测
+        this.setHeartbeatTimeout();
+      }
+    }, this.heartbeatConfig.interval);
+  };
+
+  // 停止心跳机制
+  this.stopHeartbeat = function() {
+    if (this.heartbeatTimer) {
+      clearInterval(this.heartbeatTimer);
+      this.heartbeatTimer = null;
+    }
+    if (this.heartbeatTimeout) {
+      clearTimeout(this.heartbeatTimeout);
+      this.heartbeatTimeout = null;
+    }
+    this.missedHeartbeats = 0;
+    console.log('停止心跳机制');
+  };
+
+  // 设置心跳超时检测
+  this.setHeartbeatTimeout = function() {
+    if (this.heartbeatTimeout) {
+      clearTimeout(this.heartbeatTimeout);
+    }
+
+    this.heartbeatTimeout = setTimeout(() => {
+      this.missedHeartbeats++;
+      console.log(`心跳超时,丢失次数: ${this.missedHeartbeats}/${this.heartbeatConfig.maxMissed}`);
+
+      if (this.missedHeartbeats >= this.heartbeatConfig.maxMissed) {
+        console.log('心跳超时次数过多,主动断开连接');
+        if (app.socket) {
+          app.socket.close();
+        }
+      }
+    }, this.heartbeatConfig.timeout);
+  };
+
+  // 处理心跳响应
+  this.handleHeartbeatResponse = function() {
+    this.missedHeartbeats = 0;
+    if (this.heartbeatTimeout) {
+      clearTimeout(this.heartbeatTimeout);
+      this.heartbeatTimeout = null;
+    }
+    console.log('收到心跳响应');
+  };
+
+  // 重连逻辑函数
+  this.attemptReconnect = async function() {
+    if (!this.shouldReconnect || this.isReconnecting) {
+      return;
+    }
+
+    if (this.reconnectAttempts >= this.reconnectConfig.maxRetries) {
+      Log.info('达到最大重连次数,停止重连');
+      this.isReconnecting = false;
+      this.reconnectAttempts = 0;
+      return;
+    }
+
+    this.isReconnecting = true;
+    this.reconnectAttempts++;
+
+    // 计算重连间隔(指数退避)
+    const interval = Math.min(
+      this.reconnectConfig.retryInterval * Math.pow(this.reconnectConfig.retryMultiplier, this.reconnectAttempts - 1),
+      this.reconnectConfig.maxRetryInterval
+    );
+
+    Log.info(`第${this.reconnectAttempts}次重连尝试,${interval}ms后开始...`);
+
+    setTimeout(async () => {
+      try {
+        Log.info('开始重连...');
+        await this.init(this.app);
+        Log.info('重连成功');
+        this.isReconnecting = false;
+        this.reconnectAttempts = 0;
+      } catch (error) {
+        Log.info('重连失败:', error);
+        this.isReconnecting = false;
+        // 继续尝试重连
+        this.attemptReconnect();
+      }
+    }, interval);
+  };
+
   this.init = async function (this_app) {
     if(this_app)   this.app = this_app;
     await new Promise(async (resolve,reject) => {
@@ -66,13 +195,20 @@ const pySocket = function () {
         return;
       }
 
+      // 重置重连状态
+      this.shouldReconnect = true;
+      this.isReconnecting = false;
+
       app.socket = new WebSocket('ws://'+pyapp+':7074/ws');
 
       // 监听连接成功事件
       app.socket.on('open', () => {
-        console.log('socket open')
+        Log.info('socket open')
         resolve(true);
         win.webContents.send('controller.socket.connect_open', true);
+
+        // 启动心跳机制
+        this.startHeartbeat();
       });
 
       // 监听消息事件
@@ -80,7 +216,10 @@ const pySocket = function () {
         try {
           let this_data = JSON.parse(data.toString());
 
-          if(!['blue_tooth','smart_shooter_enable_preview'].includes(this_data.msg_type)){ console.log(this_data);}
+          if(!['blue_tooth','smart_shooter_enable_preview','smart_shooter_getinfo'].includes(this_data.msg_type)){
+            console.log('message');
+            console.log(this_data);
+          }
           if(this_data.msg_type){
             let notAllMessage = false
             switch (this_data.msg_type){
@@ -88,6 +227,11 @@ const pySocket = function () {
                 notAllMessage = true;
                 livePreview(this_data);
                 break;
+              case 'pong':
+                // 处理心跳响应
+                this.handleHeartbeatResponse();
+                notAllMessage = true;
+                break;
             }
             if(notAllMessage) return;
             let channel = 'controller.socket.message_'+this_data.msg_type;
@@ -98,11 +242,15 @@ const pySocket = function () {
                   if(item === 'default'){
                     win.webContents.send(channel, this_data);
                   }else{
-                    if(this.app.electron[item]) this.app.electron[item].webContents.send(channel, this_data);
+                    if(this.app.electron[item]) {
+                      this.app.electron[item].webContents.send(channel, this_data);
+                    }
                   }
                 })
               }else{
-                if(this.app.electron[typeToMessage[this_data.msg_type]]) this.app.electron[typeToMessage[this_data.msg_type]].webContents.send(channel, this_data);
+                if(this.app.electron[typeToMessage[this_data.msg_type]]) {
+                  this.app.electron[typeToMessage[this_data.msg_type]].webContents.send(channel, this_data);
+                }
               }
             }else{
               win.webContents.send(channel, this_data);
@@ -114,19 +262,33 @@ const pySocket = function () {
       });
 
       // 监听连接关闭事件
-      app.socket.on('close', () => {
-        console.log('socket close');
+      app.socket.on('close', (e) => {
+        Log.info('socket close');
+        Log.info(e);
         win.webContents.send('controller.socket.disconnect', null);
-        app.socket = null
 
+        // 停止心跳机制
+        this.stopHeartbeat();
+
+        app.socket = null;
+
+        // 启动重连机制
+        this.attemptReconnect();
       });
 
       // 监听错误事件
       app.socket.on('error', (err) => {
-        console.log('socket error');
+        Log.info('socket error:', err);
         win.webContents.send('controller.socket.disconnect', null);
-        reject(true);
 
+        // 停止心跳机制
+        this.stopHeartbeat();
+
+        app.socket = null;
+
+        // 启动重连机制
+        this.attemptReconnect();
+        reject(true);
       });
 
 
@@ -143,7 +305,7 @@ const pySocket = function () {
     console.log(message);
     console.log(app.socket?.readyState);
     if(!app.socket){
-      await  this.init()
+      await  this.init(this.app)
     }
     // 检查连接状态
     if (app.socket?.readyState === WebSocket.OPEN) {
@@ -153,11 +315,26 @@ const pySocket = function () {
   }
 
   this.disconnect = function () {
+    // 设置标志,阻止重连
+    this.shouldReconnect = false;
+    this.isReconnecting = false;
+    this.reconnectAttempts = 0;
+
+    // 停止心跳机制
+    this.stopHeartbeat();
+
     if (app.socket) {
       app.socket.close(); // 使用 close() 方法
       app.socket = null;
     }
   }
+
+  // 重新启用重连功能
+  this.enableReconnect = function () {
+    this.shouldReconnect = true;
+    this.reconnectAttempts = 0;
+    this.isReconnecting = false;
+  }
   this.onSocketMessage = async function (message_type,callback) {  // 监听消息事件
     return new Promise(async (resolve,reject) => {
       app.socket.on('message', onSocketMessage);

+ 1 - 1
frontend/.env.production

@@ -1,3 +1,3 @@
 NODE_ENV=production
-API_HOST = http://dev2.pubdata.cn
+API_HOST = https://dev2.valimart.net
 

+ 1 - 1
frontend/src/App.vue

@@ -6,7 +6,7 @@
       </keep-alive>
     </router-view>
 
-    <Login v-model:dialogVisible="useUserInfoStore.loginShow" />
+    <Login v-if="useUserInfoStore.loginShow" v-model:dialogVisible="useUserInfoStore.loginShow" />
 
 </template>
 <script setup lang="ts">

+ 112 - 0
frontend/src/apis/log.ts

@@ -0,0 +1,112 @@
+import { GET, POST } from "@/utils/http";
+import { useUuidStore } from "@/stores/modules/uuid";
+import pinia from "@/stores/index";
+
+// 定义埋点参数的类型
+interface LogParams {
+  type?: number;
+  channel?: string;
+  uuid?: string;
+  page?: string;
+  page_url?: string;
+  describe?: any;
+  time?: number;
+  [key: string]: any;
+}
+
+// 定义UUID响应的类型
+interface UuidResponse {
+  code: number;
+  data: {
+    uuid: string;
+  };
+  message: string;
+}
+
+// 获取UUID store实例
+const getUuidStore = () => {
+  return useUuidStore(pinia);
+};
+
+// 公共参数
+const getPubParams = () => {
+  const uuidStore = getUuidStore();
+  return {
+    channel: 'aigc-camera',
+    uuid: uuidStore.getUuid || ''
+  };
+};
+
+/**
+ * 获取UUID
+ */
+export async function getUUid(): Promise<UuidResponse> {
+  return GET('/api/uuid', {}, {
+    loading: false,
+    showErrorMessage: false
+  });
+}
+
+/**
+ * 设置UUID(用于与store集成)
+ * @param uuid UUID字符串
+ */
+export function setUuid(uuid: string): void {
+  const uuidStore = getUuidStore();
+  uuidStore.setUuid(uuid);
+}
+
+/**
+ * 获取当前UUID
+ * @returns 当前UUID或null
+ */
+export function getCurrentUuid(): string | null {
+  const uuidStore = getUuidStore();
+  return uuidStore.getUuid;
+}
+
+/**
+ * 埋点接口
+ * @param params 埋点参数
+ */
+export async function setLog(params: LogParams): Promise<void> {
+  const uuidStore = getUuidStore();
+  
+  // 埋点函数
+  const setLogFun = async (logParams: LogParams) => {
+    const pubParams = getPubParams();
+    const requestData = {
+      type: 1,
+      ...pubParams,
+      uuid: uuidStore.getUuid || '',
+      ...logParams
+    };
+    
+    try {
+      await POST('/api/record/point', requestData, {
+        loading: false,
+        showErrorMessage: false
+      });
+    } catch (error) {
+      console.error('埋点请求失败:', error);
+    }
+  };
+
+  // 检查UUID是否存在
+  if (uuidStore.hasUuid) {
+    await setLogFun(params);
+  } else {
+    // UUID不存在,先获取UUID
+    try {
+      const res = await getUUid();
+      if (res.code === 0 && res.data?.uuid) {
+        uuidStore.setUuid(res.data.uuid);
+        await setLogFun(params);
+      } else {
+        console.error('获取UUID失败:', res.message);
+      }
+    } catch (error) {
+      console.error('获取UUID异常:', error);
+    }
+  }
+}

+ 10 - 0
frontend/src/apis/other.ts

@@ -7,5 +7,15 @@ export async function getCompanyTemplatesApi(){
     return GET('/api/ai_image/auto_photo/get_company_templates')
 }
 
+// 获取模特列表
+export async function getShoesModelTemplateApi(params: { status: number }){
+    return GET('/api/ai_image/main/shoes_model_template', params)
+}
+
+// AI 扩写相机场景提示词
+export async function expandCameraWordsApi(params: { words: string }){
+    return POST('/api/ai_image/main/expand_camera_words', params)
+}
+
 
 

BIN
frontend/src/assets/images/Photography/cj.png


BIN
frontend/src/assets/images/Photography/mt.png


BIN
frontend/src/assets/images/Photography/xq.png


+ 551 - 0
frontend/src/components/ModelGeneration/index.vue

@@ -0,0 +1,551 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="选择模特" width="1000px" :close-on-click-modal="false"
+    :close-on-press-escape="false"  custom-class="model-generation-dialog" @close="handleClose">
+    <div class="model-generation-container">
+      <!-- 主要内容区域 -->
+      <div class="main-content">
+
+        <!-- 左侧:女模特选择 -->
+        <div class="model-section">
+          <h2>女模特</h2>
+          <div class="model-display">
+            <el-image v-if="selectedFemaleModel" :src="selectedFemaleModel.image_url" :alt="selectedFemaleModel.name"
+              class="selected-model-image" lazy :preview-src-list="[selectedFemaleModel.image_url]" fit="cover" />
+            <div v-else class="placeholder-image">
+              <span>请在下方列表选择</span>
+            </div>
+          </div>
+        </div>
+
+        <!-- 右侧:男模特选择 -->
+        <div class="model-section">
+          <h2>男模特</h2>
+          <div class="model-display">
+            <el-image v-if="selectedMaleModel" :src="selectedMaleModel.image_url" :alt="selectedMaleModel.name"
+              class="selected-model-image" lazy :preview-src-list="[selectedMaleModel.image_url]" fit="cover" />
+            <div v-else class="placeholder-image">
+              <span>请在下方列表选择</span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 底部模特列表区域 -->
+      <div class="model-list-section">
+        <!-- 标签页切换 -->
+        <div class="tabs-container">
+          <div class="tab-item" :class="{ active: activeTab === 'female' }" @click="switchTab('female')">
+            女模特
+          </div>
+          <div class="tab-item" :class="{ active: activeTab === 'male' }" @click="switchTab('male')">
+            男模特
+          </div>
+        </div>
+
+        <!-- 模特网格列表 -->
+        <div class="model-grid">
+          <div v-for="model in currentModelList" :key="model.id" class="model-item"
+            :class="{ selected: isModelSelected(model) }" @click="selectModel(model)">
+            <el-image :src="model.image_url" :alt="model.name" class="model-thumbnail" lazy fit="cover" />
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="handleCancel">取消</el-button>
+        <el-button type="primary" @click="handleConfirm" :disabled="!canConfirm">
+          确认
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive, computed, watch, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getShoesModelTemplateApi } from '@/apis/other'
+
+// 定义组件的 props
+interface Props {
+  modelValue: boolean
+  initialModels?: {
+    female?: any
+    male?: any
+  }
+}
+
+const props = defineProps<Props>()
+
+// 定义组件的事件
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+  confirm: [models: { female: any; male: any }]
+  cancel: []
+}>()
+
+// Dialog 显示状态
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 模特数据类型定义
+interface ModelData {
+  id: number
+  name: string
+  image_url: string
+  gender: 'male' | 'female'
+  keywords: string
+  status: number
+}
+
+// 当前激活的标签页
+const activeTab = ref<'female' | 'male'>('female')
+
+// 选中的女模特
+const selectedFemaleModel = ref<ModelData | null>(null)
+
+// 选中的男模特
+const selectedMaleModel = ref<ModelData | null>(null)
+
+// 本地缓存 key
+const MODEL_SELECTION_CACHE_KEY = 'model_selection_cache'
+
+// 从本地缓存读取
+const loadModelSelectionFromCache = () => {
+  try {
+    const cache = localStorage.getItem(MODEL_SELECTION_CACHE_KEY)
+    if (cache) {
+      const parsed = JSON.parse(cache)
+      if (parsed?.female) {
+        selectedFemaleModel.value = parsed.female
+      }
+      if (parsed?.male) {
+        selectedMaleModel.value = parsed.male
+      }
+    }
+  } catch {}
+}
+
+// 写入本地缓存(只保存必要字段)
+const saveModelSelectionToCache = () => {
+  try {
+    const payload = {
+      female: selectedFemaleModel.value ? {
+        id: selectedFemaleModel.value.id,
+        name: selectedFemaleModel.value.name,
+        image_url: selectedFemaleModel.value.image_url,
+        gender: selectedFemaleModel.value.gender,
+        keywords: selectedFemaleModel.value.keywords,
+        status: selectedFemaleModel.value.status
+      } : null,
+      male: selectedMaleModel.value ? {
+        id: selectedMaleModel.value.id,
+        name: selectedMaleModel.value.name,
+        image_url: selectedMaleModel.value.image_url,
+        gender: selectedMaleModel.value.gender,
+        keywords: selectedMaleModel.value.keywords,
+        status: selectedMaleModel.value.status
+      } : null
+    }
+    localStorage.setItem(MODEL_SELECTION_CACHE_KEY, JSON.stringify(payload))
+  } catch {}
+}
+
+// 女模特列表
+const femaleModels = ref<ModelData[]>([])
+
+// 男模特列表
+const maleModels = ref<ModelData[]>([])
+
+// 当前显示的模特列表
+const currentModelList = computed(() => {
+  return activeTab.value === 'female' ? femaleModels.value : maleModels.value
+})
+
+// 是否可以确认选择
+const canConfirm = computed(() => {
+  return selectedFemaleModel.value || selectedMaleModel.value
+})
+
+// 选择模特
+const selectModel = (model: ModelData) => {
+  if (model.keywords === '女性') {
+    selectedFemaleModel.value = model
+  } else {
+    selectedMaleModel.value = model
+  }
+}
+
+// 判断模特是否被选中
+const isModelSelected = (model: ModelData) => {
+  if (model.keywords === '女性') {
+    return selectedFemaleModel.value?.id === model.id
+  } else {
+    return selectedMaleModel.value?.id === model.id
+  }
+}
+
+// 切换标签页并滚动到顶部
+const switchTab = (tab: 'female' | 'male') => {
+  activeTab.value = tab
+  // 使用 nextTick 确保 DOM 更新后再滚动
+  nextTick(() => {
+    const modelGrid = document.querySelector('.model-grid') as HTMLElement
+    if (modelGrid) {
+      modelGrid.scrollTop = 0
+    }
+  })
+}
+
+// 确认选择
+const handleConfirm = () => {
+  // 只传递必要的数据字段,避免序列化问题
+  const selectedModels = {
+    female: selectedFemaleModel.value ? {
+      id: selectedFemaleModel.value.id,
+      name: selectedFemaleModel.value.name,
+      image_url: selectedFemaleModel.value.image_url,
+      gender: selectedFemaleModel.value.gender,
+      keywords: selectedFemaleModel.value.keywords,
+      status: selectedFemaleModel.value.status
+    } : null,
+    male: selectedMaleModel.value ? {
+      id: selectedMaleModel.value.id,
+      name: selectedMaleModel.value.name,
+      image_url: selectedMaleModel.value.image_url,
+      gender: selectedMaleModel.value.gender,
+      keywords: selectedMaleModel.value.keywords,
+      status: selectedMaleModel.value.status
+    } : null
+  }
+
+
+  // 通过事件将数据发送给父组件
+  saveModelSelectionToCache()
+  emit('confirm', selectedModels)
+  dialogVisible.value = false
+}
+
+// 取消选择
+const handleCancel = () => {
+  emit('cancel')
+  dialogVisible.value = false
+}
+
+// 关闭弹窗
+const handleClose = () => {
+  emit('cancel')
+}
+
+// 获取模特列表
+const fetchModelList = async () => {
+  try {
+    const response = await getShoesModelTemplateApi({ status: 2 })
+    if (response && response.data) {
+      // 根据性别分类模特
+      femaleModels.value = response.data.filter((model: ModelData) => model.keywords === '女性')
+      maleModels.value = response.data.filter((model: ModelData) => model.keywords === '男性')
+
+      // 预加载前几个图片以提高性能
+      setTimeout(() => {
+        preloadImages(femaleModels.value.slice(0, 10))
+        preloadImages(maleModels.value.slice(0, 10))
+      }, 100)
+    }
+  } catch (error) {
+    ElMessage.error('获取模特列表失败')
+  }
+}
+
+// 预加载图片
+const preloadImages = (models: ModelData[]) => {
+  models.forEach(model => {
+    if (model.image_url) {
+      const img = new Image()
+      img.src = model.image_url
+    }
+  })
+}
+
+// 监听弹窗显示状态变化,初始化模特数据
+watch(dialogVisible, (newValue) => {
+  if (newValue) {
+    fetchModelList()
+    
+    // 初始化时接收父组件传递的模特数据
+    if (props.initialModels) {
+      
+      if (props.initialModels.female) {
+        selectedFemaleModel.value = props.initialModels.female
+      }
+      
+      if (props.initialModels.male) {
+        selectedMaleModel.value = props.initialModels.male
+      }
+    }
+
+    // 如果父组件未传入,尝试读取本地缓存
+    if (!props.initialModels || (!props.initialModels.female && !props.initialModels.male)) {
+      loadModelSelectionFromCache()
+    }
+  }
+}, { immediate: true })
+</script>
+
+<style lang="scss" scoped>
+.model-generation-container {
+  padding: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+.page-header {
+  text-align: center;
+  padding: 20px 0;
+  background: #ffffff;
+  border-bottom: 1px solid #e4e7ed;
+
+  h1 {
+    color: #303133;
+    margin: 0;
+    font-size: 20px;
+    font-weight: 500;
+  }
+}
+
+.main-content {
+  display: flex;
+  gap: 15px;
+  padding: 15px;
+  position: relative;
+  max-width: 50%;
+  margin: 0 auto;
+}
+
+.model-section {
+  flex: 1;
+  border-radius: 6px;
+  overflow: hidden;
+
+  h2 {
+    color: #303133;
+    margin: 0;
+    padding: 8px;
+    font-size: 13px;
+    font-weight: 550;
+    text-align: center;
+  }
+}
+
+.model-display {
+  width: 100%;
+  aspect-ratio: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+  background: #f8f9fa;
+  border: 2px solid #e4e7ed;
+  border-radius: 4px;
+
+  .selected-model-image {
+    width: 100%;
+    height: 100%;
+
+    :deep(.el-image__inner) {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+  }
+
+  .placeholder-image {
+    color: #c0c4cc;
+    font-size: 11px;
+    text-align: center;
+
+    span {
+      color: #c0c4cc;
+      display: block;
+      line-height: 1.2;
+    }
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 12px;
+  padding: 15px;
+}
+
+
+
+.model-list-section {
+  margin: 2px 0px 0px 0;
+  overflow: hidden;
+}
+
+.tabs-container {
+  display: flex;
+  /* background: #f8f9fa; */
+  border-bottom: 1px solid #e4e7ed;
+  align-items: center;
+  justify-content: center;
+}
+
+.tab-item {
+  padding: 8px 16px;
+  cursor: pointer;
+  font-size: 13px;
+  font-weight: 550;
+  color: #606266;
+  border-bottom: 2px solid transparent;
+  transition: all 0.2s ease;
+  background: transparent;
+
+  &:hover {
+    color: #409eff;
+
+  }
+
+  &.active {
+    color: #409eff;
+    border-bottom-color: #409eff;
+  }
+}
+
+.model-grid {
+  display: grid;
+  grid-template-columns: repeat(5, 1fr);
+  gap: 6px;
+  padding: 12px;
+  max-height: 320px;
+  overflow-y: auto;
+  margin-top: 8px;
+}
+
+.model-item {
+  aspect-ratio: 1;
+  border: 1px solid #e4e7ed;
+  border-radius: 3px;
+  overflow: hidden;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  background: #f8f9fa;
+
+  &:hover {
+    border-color: #409eff;
+    transform: scale(1.01);
+  }
+
+  &.selected {
+    border-color: #409eff;
+    border-width: 2px;
+    box-shadow: 0 0 0 1px #409eff;
+  }
+
+  .model-thumbnail {
+    width: 100%;
+    height: 100%;
+
+    :deep(.el-image__inner) {
+      width: 100%;
+      height: 100%;
+      object-fit: cover;
+    }
+
+    :deep(.el-image__placeholder) {
+      background: #f8f9fa;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #c0c4cc;
+      font-size: 12px;
+    }
+
+    :deep(.el-image__error) {
+      background: #fef0f0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      color: #f56c6c;
+      font-size: 12px;
+    }
+  }
+}
+
+// 滚动条样式
+.model-grid::-webkit-scrollbar {
+  width: 6px;
+}
+
+.model-grid::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.model-grid::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.model-grid::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .main-content {
+    flex-direction: column;
+    gap: 20px;
+    padding: 20px;
+  }
+
+  .model-grid {
+    grid-template-columns: repeat(4, 1fr);
+    gap: 10px;
+    padding: 15px;
+  }
+
+  .confirm-button-container {
+    position: static;
+    text-align: center;
+    margin-top: 20px;
+  }
+
+  .model-list-section {
+    margin: 0 20px 20px;
+  }
+}
+</style>
+
+<style lang="scss">
+.model-generation-dialog {
+  .el-dialog__body {
+    padding: 0;
+    background: #EAECED;
+  }
+
+ .el-dialog__footer {
+    padding: 0;
+    border-top: 1px solid #e4e7ed;
+    background: #fafafa;
+  }
+
+  .el-dialog {
+    border-radius: 8px;
+  }
+
+  .el-dialog__header {
+  }
+
+  .el-dialog__title {
+    font-size: 18px;
+    font-weight: 600;
+    color: #303133;
+  }
+}</style>

+ 280 - 0
frontend/src/components/ScenePromptDialog/index.vue

@@ -0,0 +1,280 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="场景图生成" width="600px" :close-on-click-modal="false"
+    :close-on-press-escape="false" custom-class="scene-prompt-dialog" @close="handleClose">
+    <div class="scene-prompt-container">
+      <!-- 场景提示词输入区域 -->
+      <div class="input-section">
+        <div class="input-wrapper">
+          <el-input v-model="scenePrompt" type="textarea" :rows="8" placeholder="请输入场景提示词" class="scene-input"
+            resize="none" maxlength="500"  />
+          <!-- AI帮我写按钮 -->
+          <div class="ai-help-button-container">
+            <el-button type="primary"  @click="handleAIHelp" :loading="aiLoading" class="ai-help-button">
+              AI帮我写
+            </el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="handleConfirm" :disabled="!canConfirm" class="confirm-button">
+          确认
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { expandCameraWordsApi } from '@/apis/other'
+
+// 定义组件的 props
+interface Props {
+  modelValue: boolean
+  initialPrompt?: string
+}
+
+const props = defineProps<Props>()
+
+// 定义组件的事件
+const emit = defineEmits<{
+  'update:modelValue': [value: boolean]
+  confirm: [prompt: string]
+  cancel: []
+}>()
+
+// Dialog 显示状态
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (value) => emit('update:modelValue', value)
+})
+
+// 场景提示词
+const scenePrompt = ref('')
+
+// 本地缓存 key
+const SCENE_PROMPT_CACHE_KEY = 'scene_prompt_cache'
+
+// 从本地缓存读取
+const loadScenePromptFromCache = () => {
+  try {
+    const cache = localStorage.getItem(SCENE_PROMPT_CACHE_KEY)
+    if (cache && !props.initialPrompt) {
+      scenePrompt.value = cache
+    }
+  } catch {}
+}
+
+// 写入本地缓存
+const saveScenePromptToCache = () => {
+  try {
+    const value = (scenePrompt.value || '').trim()
+    if (value) {
+      localStorage.setItem(SCENE_PROMPT_CACHE_KEY, value)
+    }
+  } catch {}
+}
+
+// AI 加载状态
+const aiLoading = ref(false)
+
+// 是否可以确认
+const canConfirm = computed(() => {
+  return scenePrompt.value.trim().length > 0
+})
+
+// AI 帮我写
+const handleAIHelp = async () => {
+  if (aiLoading.value) return
+
+  aiLoading.value = true
+  try {
+    // 调用后端接口生成场景提示词
+    const res: any = await expandCameraWordsApi()
+
+    const result = res?.data?.result || ''
+    if (result) {
+      scenePrompt.value = result
+      ElMessage.success('AI 已为您生成场景提示词')
+    } else {
+      ElMessage.error('AI 未返回结果,请稍后重试')
+    }
+  } catch (error) {
+    ElMessage.error('AI 生成失败,请重试')
+  } finally {
+    aiLoading.value = false
+  }
+}
+
+// 确认
+const handleConfirm = () => {
+  if (!canConfirm.value) return
+
+  saveScenePromptToCache()
+  emit('confirm', scenePrompt.value.trim())
+  dialogVisible.value = false
+  resetForm()
+}
+
+
+// 关闭弹窗
+const handleClose = () => {
+  emit('cancel')
+}
+
+// 重置表单
+const resetForm = () => {
+  scenePrompt.value = ''
+}
+
+// 监听弹窗显示状态变化,初始化场景提示词
+watch(dialogVisible, (newValue) => {
+  if (newValue) {
+    if (props.initialPrompt) {
+      scenePrompt.value = props.initialPrompt
+    } else {
+      loadScenePromptFromCache()
+    }
+  }
+}, { immediate: true })
+</script>
+
+<style lang="scss" scoped>
+.scene-prompt-container {
+  padding: 20px;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+  background: #EAECED;
+}
+
+.input-section {
+  position: relative;
+}
+
+.input-wrapper {
+  position: relative;
+  background: #f8f9fa;
+  border-radius: 8px;
+  border: 1px solid #e4e7ed;
+}
+
+.scene-input {
+  :deep(.el-textarea__inner) {
+    border: none;
+    background: transparent;
+    font-size: 16px;
+    line-height: 1.6;
+    color: #303133;
+    resize: none;
+
+    &::placeholder {
+      color: #c0c4cc;
+    }
+
+    &:focus {
+      box-shadow: none;
+    }
+  }
+}
+
+.ai-help-button-container {
+  position: absolute;
+  bottom: 15px;
+  height: 25px;
+  right: 6px;
+}
+
+.ai-help-button {
+  background: #2957FF;
+  border: none;
+  border-radius: 20px;
+  padding: 8px 16px;
+  font-size: 13px;
+  font-weight: 500;
+  color: white;
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
+  transition: all 0.3s ease;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
+  }
+
+  &:active {
+    transform: translateY(0);
+  }
+
+  .ai-icon {
+    margin-right: 6px;
+    font-size: 14px;
+  }
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: center;
+  gap: 16px;
+  padding: 14px 20px;
+}
+
+.confirm-button {
+  background: linear-gradient(135deg, #409eff 0%, #a855f7 100%);
+  border: none;
+  border-radius: 6px;
+  height: 36px;
+  padding: 12px 32px;
+  font-size: 14px;
+  font-weight: 500;
+  color: white;
+  box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
+  transition: all 0.3s ease;
+
+  &:hover {
+    transform: translateY(-1px);
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
+  }
+
+  &:active {
+    transform: translateY(0);
+  }
+
+  &:disabled {
+    background: #c0c4cc;
+    transform: none;
+    box-shadow: none;
+    cursor: not-allowed;
+  }
+}
+</style>
+
+<style lang="scss">
+.scene-prompt-dialog {
+  .el-dialog__body {
+    padding: 0;
+  }
+
+  .el-dialog__footer {
+    padding: 0 !important;
+    background: #EAECED;
+  }
+
+  .el-dialog {
+    border-radius: 12px !important;
+    overflow: hidden;
+  }
+
+  .el-dialog__header {
+    background: #f8f9fa;
+  }
+
+  .el-dialog__title {
+    font-size: 18px !important;
+    font-weight: 600 !important;
+    color: #303133 !important;
+    text-align: center !important;
+  }
+}
+</style>

+ 1 - 1
frontend/src/components/check/index.vue

@@ -51,7 +51,7 @@
     </div>
     <template #footer v-if="!checkLoading">
         <div class="flex" v-if="!checkSuccess">
-            <div class="check-btn cu-p" @click="reCheck">重新监测</div>
+            <div class="check-btn cu-p"  style="width: 160px" @click="reCheck">重新监测一次</div>
         </div>
         <div class="flex" v-else>
             <div class="check-btn cu-p" style="width: 180px" @click="confirm()">检测成功,继续操作!</div>

+ 5 - 3
frontend/src/components/header-bar/index.vue

@@ -66,7 +66,9 @@
       </div>
     </div>
     <div class="header-bar__title">
-      <span class="header-bar__text">{{ title }}</span>
+      <span class="header-bar__text">
+        <slot name="title">{{ title }}</slot>
+      </span>
     </div>
     <div class="header-bar__buttons" v-if="showUser">
       <div class="header-bar__button header-bar__button__user">
@@ -198,8 +200,8 @@ function openSetting() {
   clientStore.ipc.removeAllListeners(icpList.utils.openMain)
   let params = {
     title: '设置',
-    width: 1920,
-    height: 1080,
+    width: 3840,
+    height: 2160,
     frame: true,
     id: 'seeting',
     url: getRouterUrl(href)

+ 2 - 2
frontend/src/components/login/index.vue

@@ -213,7 +213,7 @@ const handleLogin = async () => {
       await  useUserInfoStore.getInfo()
       useUserInfoStore.updateLoginShow(false)
       setTimeout(()=>{
-        window.location.reload()
+      //  window.location.reload()
       },100)
         break;
   }
@@ -253,7 +253,7 @@ async function toggleCompany() {
   await  useUserInfoStore.getInfo()
   useUserInfoStore.updateLoginShow(false)
   setTimeout(()=>{
-    window.location.reload()
+    // window.location.reload()
   },100)
 }
 

+ 12 - 2
frontend/src/config.json

@@ -1,4 +1,14 @@
 {
-    "api": "https://dev2.pubdata.cn",
-    "tkkWebUrl": "https://tkk.pubdata.cn"
+    "dev": {
+        "api": "https://dev2.pubdata.cn",
+        "tkkWebUrl": "https://tkk.pubdata.cn"
+    },
+    "prod": {
+        "api": "https://dev2.valimart.net",
+        "tkkWebUrl": "https://tkk.valimart.net"
+    },
+    "local": {
+        "api": "",
+        "tkkWebUrl": "https://localhost:3000/tkk"
+    }
 }

+ 6 - 0
frontend/src/main.ts

@@ -6,6 +6,7 @@ import App from './App.vue'
 import ElementPlus from 'element-plus'
 import 'element-plus/dist/index.css'
 import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+import { lissenLog, log } from './utils/log'
 
 const app = createApp(App)
 app.use(ElementPlus)
@@ -14,4 +15,9 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
 }
 app.use(pinia)
 app.use(router)
+
+// 注册埋点指令和路由监听 - 确保在router使用后立即注册
+lissenLog(app)
+log(router)
+
 app.mount('#app')

+ 2 - 1
frontend/src/router/index.ts

@@ -14,8 +14,9 @@ const routes: RouteRecordRaw[] = [
     {
         path: "/home",
         name: "home",
-        component: () => import("@/views/Home/index_old.vue"),
+        component: () => import("@/views/Home/index.vue"),
         meta: {
+            title: '首页',
             noAuth: true,
         },
     },

+ 5 - 0
frontend/src/router/plugins/authGuard.ts

@@ -2,6 +2,7 @@ import { Router, useRoute } from 'vue-router'
 import useUserInfo from "@/stores/modules/user";
 import tokenInfo from "@/stores/modules/token";
 const route = useRoute()
+import pinia from "@/stores/index";
 /**
  * 除了注册页,当没有 token 则跳转至注册页
  * @param router
@@ -14,6 +15,10 @@ export function authGuard(router: Router) {
 
     const useUserInfoStore = useUserInfo();
     const tokenInfoStore = tokenInfo();
+    const appConfig = pinia.state.value.config?.appConfig;
+    if(!appConfig)  return next()
+
+
     if (tokenInfoStore.getToken /* 已登录 */) {
       if(!useUserInfoStore.userInfo.id){
           await useUserInfoStore.getInfo()

+ 5 - 0
frontend/src/stores/modules/check.ts

@@ -122,6 +122,11 @@ export const checkInfo = defineStore('checkInfo', () => {
                           devices.cam_control.msg = result.msg;
                       }
                   }
+
+                  if(!result  && checkTime.value > 0){
+                      devices.cam_control.status = -1;
+                      devices.cam_control.msg = '相机未连接,请链接相机。';
+                  }
               });
 
     }

+ 32 - 0
frontend/src/stores/modules/uuid.ts

@@ -0,0 +1,32 @@
+import { defineStore } from 'pinia'
+
+export const useUuidStore = defineStore('uuid', {
+  state: () => ({
+    uuid: null as string | null
+  }),
+  
+  getters: {
+    getUuid: (state) => state.uuid,
+    hasUuid: (state) => !!state.uuid
+  },
+  
+  actions: {
+    setUuid(uuid: string) {
+      this.uuid = uuid
+    },
+    
+    clearUuid() {
+      this.uuid = null
+    }
+  },
+  
+  persist: {
+    enabled: true,
+    strategies: [
+      {
+        key: 'uuid-store',
+        storage: localStorage
+      }
+    ]
+  }
+}) 

+ 8 - 2
frontend/src/utils/appfun.ts

@@ -1,7 +1,8 @@
 
-import { configs } from '@/utils/appconfig';
+import ENV_CONFIG from "@/config.json";
 import tokenInfo from '@/stores/modules/token';
 const tokenInfoStore = tokenInfo();
+import configInfo from "@/stores/modules/config";
 
 //获取文件路径
 export  function getFilePath (file_path){
@@ -23,6 +24,11 @@ export  function getWebUrlrUrl (config:{
     query:Object
 }){
 
+    const useConfigInfoStore = configInfo();
+    let env =  useConfigInfoStore.appConfig.env
+    const tkkWebUrl = ENV_CONFIG[env]?.tkkWebUrl || 'https://tkk.valimart.net';
+
+
     let params = '?source=camera&token=' + tokenInfoStore.getToken
     if(config.query){
         params +=  '&'
@@ -30,6 +36,6 @@ export  function getWebUrlrUrl (config:{
             return encodeURIComponent(key) + '=' + encodeURIComponent(config.query[key])
         }).join('&')
     }
-    let url  = configs.tkkWebUrl + config.url + params
+    let url  = tkkWebUrl + config.url + params
     return url
 }

+ 19 - 3
frontend/src/utils/http.ts

@@ -2,6 +2,8 @@ import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
 import { ElMessage as Message, ElMessageBox as MessageBox, ElLoading as Loading } from 'element-plus';
 import  tokenInfo  from '@/stores/modules/token';
 import useUserInfo from "@/stores/modules/user";
+import pinia from "@/stores/index";
+import ENV_CONFIG from "@/config.json";
 
 // 加载动画的并发管理
 const activeRequests = new Set<string>();
@@ -24,13 +26,26 @@ function loadingClose(requestId: string) {
  */
 const service = axios.create({
     timeout: 5000, // 设置请求超时时间
-    baseURL: '__API__',
+   // baseURL: '__API__',
 });
 console.log('__API__');
 
 // 请求拦截器
 service.interceptors.request.use(
     (config: AxiosRequestConfig) => {
+
+        // 动态设置baseURL
+        const appConfig = pinia.state.value.config?.appConfig;
+        let env = appConfig?.env || 'prod'; // 默认环境
+        if (env) {
+            // 从ENV_CONFIG获取对应环境的API地址
+            const apiConfig = ENV_CONFIG[env];
+            if (apiConfig?.api) {
+                config.baseURL = apiConfig.api;
+            }
+        }
+        console.log(`使用环境: ${env}, API地址: ${config.baseURL || '__API__'}`);
+
         // 在发送请求之前做些什么,例如添加 token
         const tokenInfoStore = tokenInfo();
         const token = tokenInfoStore.getToken; // 使用 getToken() 获取 token
@@ -169,10 +184,11 @@ service.interceptors.response.use(
  *
  * @template T 泛型,表示返回数据的类型
  * @param {string} url 请求的 URL
- * @param {any} [params] 请求参数
+ * @param {any} [data] 请求参数
+ * @param {any} [config] 请求配置
  * @returns {Promise<T>} 返回一个 Promise,解析为响应数据
  */
-export function GET<T>(url: string, data?: any,config): Promise<T> {
+export function GET<T>(url: string, data?: any, config?: any): Promise<T> {
     return service.get(url, {
         params: data,
         loading: config?.loading ?? false,

+ 2 - 1
frontend/src/utils/ipc.ts

@@ -32,7 +32,8 @@ const icpList = {
         updateLeftRightConfig:'controller.setting.updateLeftRightConfig',
         getSysConfig: 'controller.setting.getSysConfig',
         updateSysConfigs: 'controller.setting.updateSysConfigs',
-        updateTabName: 'controller.setting.updateTabName'
+        updateTabName: 'controller.setting.updateTabName',
+        updateDeviceConfigSort: 'controller.setting.updateDeviceConfigSort'
     },
     takePhoto:{
         getPhotoRecords: 'controller.takephoto.getPhotoRecords',

+ 171 - 0
frontend/src/utils/log.ts

@@ -0,0 +1,171 @@
+import { App } from 'vue'
+import router from '@/router/index'
+import { setLog } from '@/apis/log'
+import tokenInfo from '@/stores/modules/token'
+
+// 使用时间戳精确计算页面停留时间,避免间隔计时带来的误差
+let pageEnterAt = 0;
+const resetEnterTime = () => { pageEnterAt = Date.now(); };
+const getStaySeconds = () => Math.max(0, Math.round((Date.now() - pageEnterAt) / 1000));
+
+export function setLogInfo(router: any, describe: any,type = 5) {
+  setLog({
+    page: router.meta.title,
+    page_url: router.fullPath,
+    describe: describe,
+    type,
+  });
+
+  resetEnterTime();
+}
+
+export function setLogInfoHide(router: any, describe: any) {
+  setLog({
+    page: router.meta.title,
+    page_url: router.fullPath,
+    describe: describe,
+    time: getStaySeconds(),
+    type: 3
+  });
+}
+
+// 停留
+export function setLogInfoRemain(router: any) {
+  setLog({
+    page: router.meta.title,
+    page_url: router.fullPath,
+    describe: {
+      action: '停留' + router.meta.title + '10s',
+      query: { ...router.query, ...router.params }
+    },
+    time: getStaySeconds(),
+    type: 6
+  });
+}
+
+/*
+* 埋点--操作
+*/
+export function lissenLog(app: App) {
+  function log(el: HTMLElement, binding: any, thisRoute = router.currentRoute.value) {
+    return function(e: Event) {
+      e.stopPropagation();
+      let log = binding.value;
+      if (!log) {
+        try {
+          log = JSON.parse(el.getAttribute('log') || '{}');
+        } catch (e) {
+          log = {};
+        }
+      }
+
+      setLog({
+        page: thisRoute.meta.title,
+        page_url: thisRoute.fullPath,
+        describe: log.describe,
+        type: 5,
+      });
+    }
+  }
+
+  /*
+  * 获取具体参数,传参数到后台,
+  * 因为vue3.0中用了vue2.0的写法,在v-log中,数据无法响应式,故部分log信息 会绑定到dom的log下
+  */
+  /* 绑定 v-log 事件 默认为click */
+  app.directive('log', {
+    mounted(el: HTMLElement, binding: any) {
+      el.addEventListener('click', log(el, binding));
+    },
+    unmounted(el: HTMLElement, binding: any) {
+      el.removeEventListener('click', log(el, binding));
+    }
+  });
+}
+
+/*埋点 点击*/
+export async function clickLog(describe: any, route?: any, type = 5) {
+  const currentRoute = router.currentRoute.value;
+  const page = route?.meta?.title || currentRoute?.meta?.title;
+  const page_url = route?.path || currentRoute?.fullPath;
+
+  await setLog({
+    page,
+    page_url,
+    describe: describe,
+    type,
+  });
+}
+
+/*
+* 埋点--进入页面
+*/
+export function log(router: any) {
+  console.log('注册路由监听器...'); // 调试信息
+
+  // 确保router对象存在
+  if (!router) {
+    console.error('Router对象不存在');
+    return;
+  }
+
+  // 检查router.afterEach方法是否存在
+  if (typeof router.afterEach !== 'function') {
+    console.error('router.afterEach方法不存在');
+    return;
+  }
+
+  router.afterEach((to: any, from: any) => {
+    console.log('路由变化:', {
+      from: from?.path || 'null',
+      to: to?.path || 'null',
+      toMeta: to?.meta,
+      fromMeta: from?.meta
+    }); // 调试信息
+
+    /*
+    * 第一次 进入页面
+    */
+    const tokenStore = tokenInfo();
+    const hasToken = tokenStore.getToken;
+
+    let this_to = {
+      ...to,
+      ...{},
+    };
+    let this_from = {
+      ...from,
+      ...{},
+    };
+
+    // 离开页面埋点 - 排除重定向情况
+    if(from && from.path !== '/' && from.meta?.log !== false && from.meta?.title){
+      console.log('离开页面埋点:', from.meta.title); // 调试信息
+      setLogInfoHide(this_from, {
+        action: '离开' + this_from.meta.title,
+        query: { ...from.query, ...from.params }
+      });
+    }
+
+    // 进入页面埋点 - 排除根路径和重定向
+    if (to.path !== '/' && to.meta?.log !== false && to.meta?.title) {
+      console.log('进入页面埋点:', to.meta.title); // 调试信息
+      setLogInfo(this_to, {
+        action: '进入' + this_to.meta.title,
+        query: { ...to.query, ...to.params }
+      },1);
+    }
+  });
+
+  console.log('路由监听器注册完成'); // 调试信息
+}
+
+// 测试函数 - 用于验证路由监听是否工作
+export function testLogFunction() {
+  console.log('测试埋点函数被调用');
+  setLog({
+    page: '测试页面',
+    page_url: '/test',
+    describe: { action: '测试埋点' }
+  });
+}

+ 1 - 1
frontend/src/views/Developer/cmd.vue

@@ -19,7 +19,7 @@
 
     <el-row align="middle" justify="middle" class="mar-top-10">
       <el-col :span="24">
-        <el-button type="primary" @click="send_command">发送</el-button>
+        <el-button type="primary" @click="send_command" v-log="{ describe: { action: '点击RS485发送命令' } }">发送</el-button>
       </el-col>
     </el-row>
 

+ 3 - 3
frontend/src/views/Developer/index.vue

@@ -7,9 +7,9 @@
 
   <div class="page">
   <div class="tabs">
-    <div class="tab" @click="handleSelect(1)" :class="{active:activeIndex == 1}">设置</div>
-    <div class="tab" @click="handleSelect(2)" :class="{active:activeIndex == 2}">MCU其他配置设置</div>
-    <div class="tab" @click="handleSelect(3)" :class="{active:activeIndex == 3}">RS485调试发送</div>
+    <div class="tab" @click="handleSelect(1)" :class="{active:activeIndex == 1}" v-log="{ describe: { action: '点击开发者页切换Tab', tab: '设置' } }">设置</div>
+    <div class="tab" @click="handleSelect(2)" :class="{active:activeIndex == 2}" v-log="{ describe: { action: '点击开发者页切换Tab', tab: 'MCU其他配置设置' } }">MCU其他配置设置</div>
+    <div class="tab" @click="handleSelect(3)" :class="{active:activeIndex == 3}" v-log="{ describe: { action: '点击开发者页切换Tab', tab: 'RS485调试发送' } }">RS485调试发送</div>
   </div>
 
 

+ 2 - 2
frontend/src/views/Developer/mcu.vue

@@ -18,8 +18,8 @@
 
   <el-row align="middle" justify="middle" class="bottom-wrap">
     <el-col :span="24">
-      <el-button type="primary" @click="get_deviation">读取配置</el-button>
-      <el-button type="primary" @click="set_deviation">设置配置</el-button>
+      <el-button type="primary" @click="get_deviation" v-log="{ describe: { action: '点击读取MCU其他配置' } }">读取配置</el-button>
+      <el-button type="primary" @click="set_deviation" v-log="{ describe: { action: '点击设置MCU其他配置' } }">设置配置</el-button>
     </el-col>
   </el-row>
   </div>

+ 61 - 25
frontend/src/views/Home/index.vue

@@ -4,19 +4,22 @@
               [{
                 type:'ota'
               }]"
-  />
+  >
+
+    <template  #title><div @click="handleSettingClick" v-log="{ describe: { action: '点击首页标题' } }">首页</div></template>
+  </headerBar>
   <div class="home-container" v-loading="loading">
     <!-- 背景图片 -->
     <img src="@/assets/images/home/bg.png" alt="背景图片" class="background-image" />
 
     <!-- 左侧图片区域 -->
-    <div class="image-container left-image" @click="goCheck">
+    <div class="image-container left-image" @click="goCheck" v-log="{ describe: { action: '点击拍照检查入口' } }">
       <img src="@/assets/images/home/left.jpg" alt="拍摄产品并处理图像" class="zoom-on-hover" />
       <div class="overlay-text">拍摄产品<br>并处理图像</div>
     </div>
 
     <!-- 右侧图片区域 -->
-    <div class="image-container right-image" @click="goShot">
+    <div class="image-container right-image" @click="goShot" v-log="{ describe: { action: '点击仅处理图像入口' } }">
       <img src="@/assets/images/home/right.jpg" alt="仅处理图像" class="zoom-on-hover" />
       <div class="overlay-text" style="line-height: 80px;">仅处理图像</div>
     </div>
@@ -29,6 +32,8 @@ import { useRouter } from "vue-router";
 import configInfo from '@/stores/modules/config';
 import { ref, onMounted } from 'vue';
 import axios from 'axios';
+import client from "@/stores/modules/client";
+import icpList from '@/utils/ipc';
 
 const configInfoStore = configInfo();
 const router = useRouter();
@@ -40,43 +45,74 @@ import socket from "@/stores/modules/socket";
 const socketStore = socket();
 
 function socketConnect(){
-    socketStore.connectSocket();
+  socketStore.connectSocket();
 }
 
 const goCheck = () => {
-    configInfoStore.updateAppModel(1);
-    router.push({
-        name: 'PhotographyCheck'
-    });
+  configInfoStore.updateAppModel(1);
+  router.push({
+    name: 'PhotographyCheck'
+  });
 };
 
 const goShot = () => {
-    socketConnect();
-    configInfoStore.updateAppModel(2);
-    router.push({
-        name: 'PhotographyShot'
-    });
+  socketConnect();
+  configInfoStore.updateAppModel(2);
+  router.push({
+    name: 'PhotographyShot'
+  });
 };
 
 // 健康检查函数
 const checkHealth = async () => {
-    try {
-        const response = await axios.get('http://127.0.0.1:7074');
-        if (response.status === 200) {
-            loading.value = false; // 健康检查成功,关闭 loading
-        }
-    } catch (error) {
-        console.error('健康检查失败:', error);
-        setTimeout(() => {
-            checkHealth(); // 延迟检查
-        }, 2000);
-        // 可以在这里处理错误,例如显示错误提示
+  try {
+    const response = await axios.get('http://127.0.0.1:7074');
+    if (response.status === 200) {
+      loading.value = false; // 健康检查成功,关闭 loading
     }
+  } catch (error) {
+    console.error('健康检查失败:', error);
+    setTimeout(() => {
+      checkHealth(); // 延迟检查
+    }, 2000);
+    // 可以在这里处理错误,例如显示错误提示
+  }
 };
 
+const settingClickCount = ref(0);
+// 修改headerBar的点击处理函数
+function handleSettingClick() {
+  console.log('handleSettingClickhandleSettingClick')
+  settingClickCount.value++;
+
+  if (settingClickCount.value >= 5) {
+    openResourceDirectory()
+    settingClickCount.value = 0;
+  }
+
+  setTimeout(() => {
+    settingClickCount.value = 0;
+  }, 3000); // 3秒内未再次点击则重置计数器
+}
+
+const clientStore = client();
+
+function openResourceDirectory() {
+  clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
+  let params = {
+    action: 'openPath',
+    params: configInfoStore.appConfig.userDataPath.replaceAll('/', '\\')
+  };
+  clientStore.ipc.send(icpList.utils.shellFun, params);
+}
+
+
+
+
+
 // 在组件挂载时执行健康检查
 onMounted(() => {
-    checkHealth();
+  checkHealth();
 });
 </script>
 

+ 4 - 4
frontend/src/views/OTA/index.vue

@@ -30,7 +30,7 @@ const fetchVersions = async () => {
   try {
     // 添加时间戳避免缓存问题
     const timestamp = new Date().getTime();
-    const response = await axios.get('https://huilimaimg.cnhqt.com/frontend/html/zhihuiyin/version.json', {
+    const response = await axios.get('https://ossimg.valimart.net/frontend/html/zhihuiyin/version.json', {
       params: {
         _t: timestamp
       }
@@ -156,7 +156,7 @@ onMounted(() => {
 
           <p>状态: <span :class="{ 'text-green': isLatest, 'text-red': !isLatest }">{{ isLatest ? '您已经是最新版本' : '发现新版本' }}</span></p>
           <p v-if="!isLatest">新版本: {{ latestVersion }}</p>
-          <el-button v-if="!isLatest" type="primary" @click="downloadUpdate">下载更新</el-button>
+          <el-button v-if="!isLatest" type="primary" @click="downloadUpdate" v-log="{ describe: { action: '点击下载最新版本', latestVersion } }">下载更新</el-button>
         </el-card>
 
         <el-card class="history-versions-card mar-top-10">
@@ -168,13 +168,13 @@ onMounted(() => {
             <el-table-column label="描述">
               <template #default="{ row }">
                 <el-tooltip :content="row.describe" placement="top" :show-when="hover" :width="500">
-                  <span class="version-describe">{{ row.describe }}</span>
+                  <span class="version-describe" v-html="row.describe"></span>
                 </el-tooltip>
               </template>
             </el-table-column>
             <el-table-column label="操作"  width="80">
               <template #default="{ row }">
-                <el-button size="small" @click="downloadSpecificVersion(row.url)">下载</el-button>
+                <el-button size="small" @click="downloadSpecificVersion(row.url)" v-log="{ describe: { action: '点击下载历史版本', version: row.version, url: row.url } }">下载</el-button>
               </template>
             </el-table-column>
           </el-table>

+ 32 - 6
frontend/src/views/Photography/check.vue

@@ -23,7 +23,9 @@
             <div class="camera-preview  flex col center ">
               <div class="camera-preview-img" v-if="step === 1">
                 <img v-if="previewKey" class="camera-img" :src="previewSrc" />
-                <div class="example-image flex-col" v-if="!isSetting && previewKey > 1"><img src="https://huilimaimg.cnhqt.com/frontend/zhihuiyin/demo.jpg?x-oss-process=image/resize,w_400"></div>
+                <div class="example-image flex-col" v-if="!isSetting && previewKey > 1">
+                  <img :src="exampleImage">
+                </div>
               </div>
               <template v-if="step === 2" >
                 <img class="camera-img"  :src="getFilePath(imageTplPath)" />
@@ -36,18 +38,18 @@
 
         <template v-if="!isSetting">
           <div v-if="step === 1" class="action-button flex cente">
-            <div @click="takePictures" class="check-button  button--primary1 flex-col"><span class="button-text" v-loading="loading">拍照检查</span>
+            <div @click="takePictures" class="check-button  button--primary1 flex-col" v-log="{ describe: { action: '点击拍照检查' } }"><span class="button-text" v-loading="loading">拍照检查</span>
             </div>
           </div>
 
           <div v-else class="action-button flex center">
-            <div @click="checkConfirm(false)" class="check-button  button--white flex-col">
+            <div @click="checkConfirm(false)" class="check-button  button--white flex-col" v-log="{ describe: { action: '点击重新拍照检查' } }">
               <span class="button-text cu-p">重新拍照检查</span>
             </div>
             <router-link class="mar-left-20 " :to="{
               name: 'PhotographyShot'
             }">
-              <div class="check-button   button--primary1 flex-col">
+              <div class="check-button   button--primary1 flex-col" v-log="{ describe: { action: '点击确认无误下一步' } }">
                 <span class="button-text cu-p">确认无误,下一步</span>
               </div>
             </router-link>
@@ -198,6 +200,7 @@ const loading = ref(true)
 const editData = ref(null);
 const imageTplPath = ref(null)
 function takePictures() {
+  if(loading.value) return;
   console.log(editData);
   console.log(editData.value.editRowData);
 
@@ -261,7 +264,14 @@ clientStore.ipc.on(icpList.socket.message+'_smart_shooter_photo_take', async (ev
   console.log('_smart_shooter_photo_take');
   console.log(result);
   if(result.code === 0 && result.data?.photo_file_name){
-    createMainImage(result.data?.photo_file_name)
+    imageTplPath.value  = result.data?.photo_file_name
+    hideVideo()
+    step.value = 2
+    loading.value = false;
+  }else {
+    loading.value = false;
+    showVideo()
+    if(result.code !== 0 && result.msg) ElMessage.error(result.msg)
   }
 
 })
@@ -273,6 +283,18 @@ clientStore.ipc.on(icpList.socket.message+'_run_mcu_single', async (event, resul
   console.log(result)
 
   if(result.code === 0 && result.data?.file_path){
+    imageTplPath.value  = result.data?.file_path
+    hideVideo()
+    step.value = 2
+    loading.value = false;
+  }else {
+ //   loading.value = false;
+    showVideo()
+    if(result.code !== 0 && result.msg) ElMessage.error(result.msg)
+  }
+
+
+/*  if(result.code === 0 && result.data?.file_path){
 
     createMainImage(result.data?.file_path)
 
@@ -283,15 +305,19 @@ clientStore.ipc.on(icpList.socket.message+'_run_mcu_single', async (event, resul
       loading.value = false;
       showVideo()
     }
-  }
+  }*/
 
 })
+
+const exampleImage = ref('https://huilimaimg.cnhqt.com/frontend/zhihuiyin/demo.jpg?x-oss-process=image/resize,w_400')
 onMounted(async ()=>{
   if(isSetting.value)   showVideo()
   await  configInfoStore.getAppConfig()
   if(configInfoStore.appConfig.controlType === "SmartShooter"){
     preview.value = configInfoStore.appConfig.userDataPath  + "\\preview\\liveview.png"
   }
+  exampleImage.value =  configInfoStore.appConfig.exampleImage || 'https://huilimaimg.cnhqt.com/frontend/zhihuiyin/demo.jpg?x-oss-process=image/resize,w_400'
+
 })
 
 /**

+ 6 - 3
frontend/src/views/Photography/components/LoadingDialog.vue

@@ -4,7 +4,7 @@
     :show-close="requesting"
     :close-on-click-modal="false"
     :close-on-press-escape="false"
-    width="400px"
+    width="600px"
     custom-class="loading-dialog-EL"
     align-center
     append-to-body
@@ -23,8 +23,11 @@
       <div class="message">{{ message }}</div>
       <!-- 错误信息插槽 -->
       <slot name="errList"></slot>
+      <!-- 进度消息插槽 -->
+      <slot name="progressMessages"></slot>
 
       <el-button
+        v-if="!disabledButton"
         :disabled="disabledButton"
         type="primary"
         class="action-button   button--primary1  mar-top-20"
@@ -115,7 +118,7 @@ const handleButtonClick = () => {
 
 .progress-container {
   width: 100%;
-  margin-bottom: 20px;
+  margin-bottom: 10px;
   display: flex;
   align-items: center;
   gap: 10px;
@@ -145,7 +148,7 @@ const handleButtonClick = () => {
 .message {
   color: #606266;
   font-size: 14px;
-  margin-bottom: 20px;
+  margin-bottom: 0px;
 }
 
 .action-button {

+ 9 - 9
frontend/src/views/Photography/components/editRow.vue

@@ -1,9 +1,9 @@
 <template>
-  <div class="editrow_wrap" v-if="initStatus" v-loading="captureLoading">
+  <div class="editrow_wrap" v-if="initStatus">
     <div class="config-type">参数值编辑:
       <!-- <el-checkbox v-model="isDefault">开启运动调试</el-checkbox>-->
     </div>
-    <el-form class="editForm" :model="editRowData" label-width="100px" >
+    <el-form class="editForm" :model="editRowData"  v-loading="captureLoading" label-width="100px" >
       <el-form-item label="动作名称">
         <el-input v-model="editRowData.action_name" :disabled="editRowData.is_system" style="width: 170px;"/>
       </el-form-item>
@@ -19,9 +19,9 @@
         <div class="error-msg">最小0,最大350</div>
       </el-form-item>
       <el-form-item label="相机倾角">
-        <el-input v-model="editRowData.camera_angle" :min="-40" :max="40" :step=".1" @change="changeNum('camera_steering',-40, 40)" style="width: 170px;" type="number">
+        <el-input v-model="editRowData.camera_angle" :min="-5" :max="30" :step=".1" @change="changeNum('camera_steering',-40, 40)" style="width: 170px;" type="number">
         </el-input>
-        <div class="error-msg">最小-40,最大40</div>
+        <div class="error-msg">最小-5,最大30</div>
       </el-form-item>
       <el-form-item label="转盘前后位置">
         <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">
@@ -39,7 +39,7 @@
             <el-radio :label="true">翻转</el-radio>
             <el-radio :label="false">不翻转</el-radio>
           </el-radio-group>
-          <a class="cursor-pointer" @click="changeNum('overturn_steering')">测试翻转</a>
+          <a class="cursor-pointer" @click="changeNum('overturn_steering')" v-log="{ describe: { action: '点击测试翻转' } }">测试翻转</a>
         </div>
       </el-form-item>
       <el-form-item label="LED灯光开光" @change="changeNum('laser_position')">
@@ -65,9 +65,9 @@
       </el-form-item>
     </el-form>
     <div class="btn-row mar-top-20">
-      <div class="normal-btn" @click="close" v-if="id">取消</div>
-      <div class="normal-btn"  v-if="!id && editRowData.is_system" @click="testShoesFlip">运行</div>
-      <div class="primary-btn" @click="saveRow">{{ id ? '保存并关闭' : '保存' }}</div>
+      <div class="normal-btn" @click="close" v-if="id" v-log="{ describe: { action: '点击关闭编辑行' } }">取消</div>
+      <div class="normal-btn"  v-loading="captureLoading"  v-if="!id && editRowData.is_system" @click="testShoesFlip" v-log="{ describe: { action: '点击运行测试拍照' } }">运行</div>
+      <div class="primary-btn"  v-loading="captureLoading" @click="saveRow" v-log="{ describe: { action: '点击保存设备配置' } }">{{ id ? '保存并关闭' : '保存' }}</div>
       </div>
   </div>
 
@@ -188,7 +188,7 @@ function testShoesFlip(){
         camera_angle:  Number(editRowData.value.camera_angle),
         led_switch:editRowData.value.led_switch,
         id:0,
-        mode_type:editRowData.mode_type,
+        mode_type:editRowData.value.mode_type,
         turntable_position:Number(editRowData.value.turntable_position),
         action_name:editRowData.value.action_name || '测试',
         turntable_angle: Number(editRowData.value.turntable_angle),

+ 751 - 59
frontend/src/views/Photography/detail.vue

@@ -4,31 +4,61 @@
 
   <div class="detail-container">
     <div>
-      <!-- 处理模式 -->
-      <div class="logo-section flex left top" >
-        <div class="section-title" style="margin-bottom: 0px;">
+      <!-- 顶部:勾选服务内容(多选) -->
+      <div class="service-section flex between top">
+        <div class="section-title">
           <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
-          处理模式:
-
-            <el-checkbox v-model="form.is_only_cutout"
-                         true-value="1"
-                         false-value="0"
-                         true-label="1"
-                         false-label="0"
-                         label="仅抠图模式"/>
-        <span class="flex left" style="font-weight: normal; font-size: 12px; color: #666">
-          <el-icon><QuestionFilled /></el-icon>
-          如您勾选仅抠图模式,将只做抠图处理,不再自动生成详情页。
-        </span>
+          勾选服务内容(多选):
+        </div>
+        <div class="service-cards">
+          <div class="service-card" :class="{ active: form.services.includes('is_product_scene') }" @click="toggleService('is_product_scene')" v-log="{ describe: { action: '点击服务卡片', service: '场景图生成' } }">
+            <div class="service-checkbox" @click.stop>
+                              <el-checkbox size="large" :model-value="form.services.includes('is_product_scene')" @change="toggleService('is_product_scene')" />
+            </div>
+            <div class="service-content">
+              <div class="service-image">
+                <img src="@/assets/images/Photography/cj.png" alt="场景图生成" />
+                <div class="service-icon" @click.stop="openScenePromptDialog" v-log="{ describe: { action: '点击服务卡片图标', service: '场景图生成-弹窗' } }">
+                  <el-icon><EditPen /></el-icon>
+                </div>
+              </div>
+              <div class="service-name">场景图生成</div>
+            </div>
+          </div>
+          <div class="service-card" :class="{ active: form.services.includes('is_upper_footer') }" @click="toggleService('is_upper_footer')" v-log="{ describe: { action: '点击服务卡片', service: '模特图生成' } }">
+            <div class="service-checkbox" @click.stop>
+                              <el-checkbox size="large" :model-value="form.services.includes('is_upper_footer')" @change="toggleService('is_upper_footer')" />
+            </div>
+            <div class="service-content">
+              <div class="service-image">
+                <img src="@/assets/images/Photography/mt.png" alt="模特图生成" />
+                <div class="service-icon" @click.stop="openModelDialog" v-log="{ describe: { action: '点击服务卡片图标', service: '模特图生成-弹窗' } }">
+                  <el-icon><EditPen /></el-icon>
+                </div>
+              </div>
+              <div class="service-name">模特图生成</div>
+            </div>
+          </div>
+          <div class="service-card" :class="{ active: form.services.includes('is_detail') }" @click="toggleService('is_detail')" v-log="{ describe: { action: '点击服务卡片', service: '详情页生成' } }">
+            <div class="service-checkbox" @click.stop>
+                              <el-checkbox size="large" :model-value="form.services.includes('is_detail')" @change="toggleService('is_detail')" />
+            </div>
+            <div class="service-content">
+              <div class="service-image">
+                <img src="@/assets/images/Photography/xq.png" alt="详情页生成" />
+              </div>
+              <div class="service-name">详情页生成</div>
+            </div>
+          </div>
         </div>
       </div>
 
-      <template v-if="form.is_only_cutout == 0">
       <!-- 主图LOGO部分 -->
+<!--
       <div class="logo-section flex left top" >
         <div class="section-title" style="margin-bottom: 0px;">
           <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
-          主图LOGO:
+          主图LOGO 与 选择详情模板
         </div>
       </div>
       <div class="logo-section flex left top multi-line">
@@ -43,6 +73,7 @@
         ></upload>
         <upload @input="onInput"></upload>
       </div>
+-->
 
       <el-divider />
 <!--      &lt;!&ndash; 图片抠图与货号图生成 &ndash;&gt;
@@ -89,40 +120,85 @@
       </div>
       <el-divider />-->
 
-      <!-- 选择详情模板部分 -->
-      <div class="template-section ">
-        <div class="flex between">
-          <div class="section-title">
-            <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
-            选择详情模版
+      <!-- 主图LOGO 与 选择详情模板:左右布局 -->
+      <div class="logo-template-row">
+        <!-- 左:主图LOGO -->
+        <div class="logo-col">
+          <div class="logo-section flex left top" >
+            <div class="section-title" style="margin-bottom: 0px;">
+              <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
+              主图LOGO:
+            </div>
           </div>
-          <div class="template-pagination">
-            <el-pagination background layout="prev, pager, next" v-model:current-page="queryParams.current"
-              v-model:page-size.sync="queryParams.size" :total="totalPage" @current-change="onCurrentChange"
-              @size-change="onSizeChange" />
+          <div class="logo-section flex left top multi-line">
+            <upload v-for="item,index in logoList"   :value="item" :key="item"
+                    v-show="item"
+                    @input="onRemove(index)"
+                    class="mar-right-10 upload-item"
+                    :class="{
+                        active: item === form.logo_path
+                    }"
+                    @click.native="form.logo_path = item"
+            ></upload>
+            <upload @input="onInput"></upload>
           </div>
         </div>
 
-        <div class="template-list">
-          <div v-for="(template, index) in visibleTemplates" :key="index" class="template-item"
-            @click="form.selectTemplate = template">
-            <el-image :src="template.template_cover_image" fit="contain" class="cur-p"
-              style="width: 100%; display: block;" />
-            <div class="select-warp" :class="form.selectTemplate.id == template.id ? 'active' : ''">
-              <el-icon color="#FFFFFF">
-                <Select />
-              </el-icon>
+        <!-- 右:选择详情模板 -->
+        <div class="template-col">
+          <div class="template-section ">
+            <div class="flex between">
+              <div class="section-title">
+                <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
+                选择详情模版
+              </div>
+              <div class="template-pagination">
+                <el-pagination background layout="prev, pager, next" v-model:current-page="queryParams.current"
+                  v-model:page-size.sync="queryParams.size" :total="totalPage" @current-change="onCurrentChange"
+                  @size-change="onSizeChange" />
+              </div>
             </div>
-            <div class="template-info">
-              <span class="mar-left-10 chaochu_1">{{ template.template_name }}</span>
-              <div class="template-view" @click="viewTemplate(template)">查看</div>
+
+            <div class="template-list">
+              <div v-for="(template, index) in visibleTemplates" :key="index" class="template-item"
+                @click="form.selectTemplate = template" v-log="{ describe: { action: '点击选择详情模板', template_name: template.template_name } }">
+                <el-image :src="template.template_cover_image" fit="contain" class="cur-p"
+                  style="width: 100%; display: block;" />
+                <div class="select-warp" :class="form.selectTemplate.id == template.id ? 'active' : ''">
+                  <el-icon color="#FFFFFF">
+                    <Select />
+                  </el-icon>
+                </div>
+                <div class="template-info">
+                  <span class="mar-left-10 chaochu_1">{{ template.template_name }}</span>
+                  <div class="template-view" @click="viewTemplate(template)" v-log="{ describe: { action: '点击查看模板详情', template_name: template.template_name } }">查看</div>
+                </div>
+              </div>
             </div>
-          </div>
-        </div>
 
-        <div class="template-tips c-333 fs-14 line-20 te-l mar-top-20 flex left">
-          <el-icon><WarningFilled /></el-icon>
-          <span class="mar-left-10">该模版图片顺序说明:{{form.selectTemplate.template_image_order}}</span>
+            <div class="template-tips c-333 fs-14 line-20 te-l mar-top-20 flex left">
+              <el-icon><WarningFilled /></el-icon>
+              <span class="mar-left-10">该模版图片顺序说明:{{form.selectTemplate.template_image_order}}</span>
+            </div>
+
+            <!-- 模板下:一键上架 和 国内电商平台(多选) -->
+            <div class="publish-section flex left" v-if="onlineStores.length">
+              <div class="form-item flex left">
+                <div class="fw-b">一键上架:</div>
+               </div>
+              <div class="form-item flex left mar-top-10">
+                <div class="label">国内电商平台:</div>
+                <el-select  v-model="domesticPlatforms" multiple placeholder="请选择平台" style="min-width: 200px;">
+                  <el-option
+                    v-for="store in onlineStores"
+                    :key="store"
+                    :label="store"
+                    :value="store"
+                  />
+                </el-select>
+              </div>
+            </div>
+          </div>
         </div>
       </div>
       <el-divider />
@@ -171,7 +247,7 @@
             <img src="@/assets/images/Photography/zhuangshi.png" style="width: 32px; height: 32px;" />
             详情资料准备 (2选1)
 
-            <el-button v-if="form.dataType == '1'" type="text" class="mar-left-10 fs-16"  @click="downloadExcel">下载商品基础资料模版</el-button>
+            <el-button v-if="form.dataType == '1'" type="text" class="mar-left-10 fs-16"  @click="downloadExcel" v-log="{ describe: { action: '点击下载Excel模板' } }">下载商品基础资料模版</el-button>
           </div>
         </div>
 
@@ -187,19 +263,18 @@
             <div class="flex bottom mar-left-20" style="flex-grow: 1;">
               <el-input type="textarea" v-model="form.excel_path" />
             </div>
-            <el-button class="select-button button--primary1  mar-left-20" type="primary" @click="selectExcel">
+            <el-button class="select-button button--primary1  mar-left-20" type="primary" @click="selectExcel" v-log="{ describe: { action: '点击选择Excel文件' } }">
               <img src="@/assets/images/Photography/wenjian.png" style="width: 16px; margin-right: 4px;" />
               选择</el-button>
           </div>
         </div>
       </div>
-      </template>
     </div>
     <!-- 底部按钮 -->
     <div class="footer">
       <!-- <el-button class="button--primary1 footer-button" type="primary" @click="saveConfig">保存配置</el-button> -->
       <!--  <el-button class="button--primary1 footer-button" type="primary" @click="startProcess">开始处理</el-button>  -->
-      <el-button class="button--primary1 footer-button" type="primary" @click="generate">开始生成</el-button>
+      <el-button class="button--primary1 footer-button" type="primary" @click="generate" v-log="{ describe: { action: '点击开始生成详情页' } }">开始生成</el-button>
     </div>
   </div>
 
@@ -211,12 +286,48 @@
         <span>{{ item.goods_art_no }}</span>:<span>{{ item.info }}</span>
       </div>
     </template>
-
+    <template #progressMessages>
+      <div class="progress-messages" v-if="progressMessages.length">
+        <div class="message-header">
+          <span>处理进度</span>
+          <div class="flex right" style="gap:8px; align-items:center;">
+<!--            <el-button type="text" @click="openOutputDir" v-log="{ describe: { action: '点击打开输出目录' } }">打开目录</el-button>-->
+            <el-button type="text" @click="showMessageHistory = !showMessageHistory" v-log="{ describe: { action: '点击查看进度详情' } }">
+              {{ showMessageHistory ? '收起' : '查看详情' }}
+            </el-button>
+          </div>
+        </div>
+        <div class="message-list" v-if="showMessageHistory" ref="messageListRef">
+          <div v-for="(msg, index) in progressMessages" :key="index" class="message-item flex left">
+            <div class="message-time">{{ formatTime(msg.timestamp) }}</div>
+            <div class="message-content mar-left-10">
+              <span class="goods-no">货号{{ msg.goods_art_nos.join(', ') }}:</span>
+              <span class="message-text">{{ msg.msg }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </template>
   </loading-dialog>
   <el-dialog v-model="dialogVisible">
     <img style="width: 100%;" :src="dialogImageUrl" alt="Preview Image" />
   </el-dialog>
 
+  <!-- 模特生成弹窗 -->
+  <ModelGenerationDialog
+    v-model="modelDialogVisible"
+    :initial-models="selectedModels"
+    @confirm="handleModelSelection"
+    @cancel="modelDialogVisible = false"
+  />
+
+  <!-- 场景提示词弹窗 -->
+  <ScenePromptDialog
+    v-model="scenePromptDialogVisible"
+    :initial-prompt="scenePrompt"
+    @confirm="handleScenePromptConfirm"
+    @cancel="scenePromptDialogVisible = false"
+  />
 
 
 </template>
@@ -227,23 +338,35 @@ import { getCompanyTemplatesApi } from '@/apis/other'
 import tokenInfo from '@/stores/modules/token';
 import useUserInfo from "@/stores/modules/user";
 import { useRoute, useRouter } from 'vue-router'
+import { clickLog, setLogInfo } from '@/utils/log'
 
 
 import { ElMessage, ElMessageBox } from 'element-plus'
 
 import headerBar from '@/components/header-bar/index.vue'
-import { ref, computed, reactive, onMounted } from 'vue';
-import { Select } from '@element-plus/icons-vue'
+import { ref, computed, reactive, onMounted, onBeforeUnmount, nextTick } from 'vue';
+import { Select, EditPen } from '@element-plus/icons-vue'
 import upload from '@/components/upload'
 import client from "@/stores/modules/client";
 import icpList from '@/utils/ipc'
 const clientStore = client();
 import { getRouterUrl } from '@/utils/appfun'
+import { useUuidStore } from '@/stores/modules/uuid'
+import socket from "@/stores/modules/socket";
+const socketStore = socket();
+import ModelGenerationDialog from '@/components/ModelGeneration/index.vue'
+import ScenePromptDialog from '@/components/ScenePromptDialog/index.vue'
 
 import { Close, Warning } from '@element-plus/icons-vue'
 import LoadingDialog from '@/views/Photography/components/LoadingDialog.vue'
 
 
+import configInfo from "@/stores/modules/config";
+
+const useConfigInfoStore = configInfo();
+
+
+
 import { useCheckInfo } from '@/composables/userCheck';
 useCheckInfo();
 
@@ -258,6 +381,7 @@ const specificPage = ref('')  // 可指定页面独修改
 // 路由和状态管理初始化
 const route = useRoute();
 const router = useRouter();
+const uuidStore = useUuidStore();
 
 // 完成目录
 const completeDirectory = ref('')
@@ -267,6 +391,27 @@ const loadingDialogVisible = ref(false)
 const progress = ref(0)
 const message = ref('正在为您处理,请稍后')
 const disabledButton = ref(true)
+// 进度消息队列
+const progressMessages = ref<Array<{
+  goods_no: string
+  temp_name: string
+  status: string
+  goods_art_nos: string[]
+  msg: string
+  timestamp: number
+}>>([])
+const showMessageHistory = ref(true)
+const messageListRef = ref<HTMLElement | null>(null)
+
+// 新消息时自动滚动到底部
+const scrollMessageListToBottom = () => {
+  nextTick(() => {
+    const el = messageListRef.value
+    if (el) {
+      el.scrollTop = el.scrollHeight
+    }
+  })
+}
 
 let templates = ref([])
 let goods_art_nos = ref([])
@@ -299,13 +444,28 @@ const form = reactive({
   dataType: '1', // 1: 选择excel文件 2: 系统对接
   logo_path: '', // 主图LOGO
   excel_path: '', // 商品基础资料EXCEL文件选择
-  is_only_cutout:0, //是否仅抠图模式
+  services: ['is_detail'], // 勾选服务内容(多选)默认包含详情页生成
 })
 onMounted(() => {
+  // 页面访问埋点
+
   const goods_art_data = route.query.goods_art_nos
   goods_art_nos.value = Array.isArray(goods_art_data) ? goods_art_data : [goods_art_data]
   getCompanyTemplates()
   getLogolist()
+
+  loadDetailCache()
+})
+
+// 页面卸载时清理监听器
+onBeforeUnmount(() => {
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_upper_footer_progress');
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_scene_progress');
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_upload_goods_progress');
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_result_progress');
+  clearInterval(INTERVAL.value);
 })
 
 
@@ -326,6 +486,7 @@ const viewTemplate = (template) => {
 const getCompanyTemplates = async () => {
   const { data } = await getCompanyTemplatesApi()
   templates.value = data.list
+  onlineStores.value = data.online_stores || [] // 获取电商平台列表
   // 默认选中第一个模板
   if (templates.value.length > 0) {
     form.selectTemplate = templates.value[0]
@@ -345,6 +506,106 @@ const downloadExcel =  () => {
     document.body.removeChild(a);
   }, 1000);
 }
+// 服务内容切换
+const toggleService = (key: string) => {
+  const idx = form.services.indexOf(key)
+  if (idx > -1) form.services.splice(idx, 1)
+  else form.services.push(key)
+}
+
+// 国内电商平台多选与一键上架
+const domesticPlatforms = ref<string[]>([])
+const onlineStores = ref<any[]>([]) // 从接口获取的电商平台列表
+const publishToPlatforms = () => {
+  if (!domesticPlatforms.value.length) {
+    ElMessage.warning('请选择至少一个电商平台')
+    return
+  }
+  clickLog({ describe: { action: '点击一键上架', platforms: domesticPlatforms.value } }, route)
+  // 具体上架逻辑按后续接口对接
+}
+
+// 模特与场景弹窗
+const modelDialogVisible = ref(false)
+const scenePromptDialogVisible = ref(false)
+const selectedModels = ref<{ female: any; male: any } | null>(null)
+const scenePrompt = ref('')
+
+// 本地缓存键(与弹窗组件保持一致)
+const DETAIL_MODEL_CACHE_KEY = 'model_selection_cache'
+const DETAIL_SCENE_PROMPT_CACHE_KEY = 'scene_prompt_cache'
+
+// 读取本地缓存
+const loadDetailCache = () => {
+  console.log('loadDetailCache');
+  try {
+    const m = localStorage.getItem(DETAIL_MODEL_CACHE_KEY)
+    if (m) {
+      const parsed = JSON.parse(m)
+      if (parsed && (parsed.female || parsed.male)) {
+        selectedModels.value = parsed
+        console.log('loadDetailCache');
+        console.log(selectedModels.value);
+      }
+    }
+  } catch {}
+  try {
+    const p = localStorage.getItem(DETAIL_SCENE_PROMPT_CACHE_KEY)
+    if (p) {
+      console.log('scenePrompt');
+      scenePrompt.value = p
+      console.log(scenePrompt.value);
+    }
+  } catch {}
+}
+
+// 保存到本地缓存(仅保存必要字段)
+const saveModelsToCache = (models: { female: any; male: any }) => {
+  try {
+    const payload = {
+      female: models?.female ? {
+        id: models.female.id,
+        name: models.female.name,
+        image_url: models.female.image_url,
+        gender: models.female.gender,
+        keywords: models.female.keywords,
+        status: models.female.status
+      } : null,
+      male: models?.male ? {
+        id: models.male.id,
+        name: models.male.name,
+        image_url: models.male.image_url,
+        gender: models.male.gender,
+        keywords: models.male.keywords,
+        status: models.male.status
+      } : null
+    }
+    localStorage.setItem(DETAIL_MODEL_CACHE_KEY, JSON.stringify(payload))
+  } catch {}
+}
+const saveScenePromptToCache = (prompt: string) => {
+  try {
+    const v = (prompt || '').trim()
+    if (v) localStorage.setItem(DETAIL_SCENE_PROMPT_CACHE_KEY, v)
+  } catch {}
+}
+
+const openModelDialog = () => {
+  modelDialogVisible.value = true
+}
+const openScenePromptDialog = () => {
+  scenePromptDialogVisible.value = true
+}
+const handleModelSelection = (models: { female: any; male: any }) => {
+  selectedModels.value = models
+  saveModelsToCache(models)
+  modelDialogVisible.value = false
+  ElMessage.success('模特选择完成!')
+}
+const handleScenePromptConfirm = (prompt: string) => {
+  scenePrompt.value = prompt
+  saveScenePromptToCache(prompt)
+}
 
 
 
@@ -357,15 +618,175 @@ const onSizeChange = (data) => {
 
 
 
+// 格式化时间
+const formatTime = (timestamp: number) => {
+  const date = new Date(timestamp)
+  return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`
+}
+
+// 处理进度消息
+const handleProgressMessage = (data: any) => {
+  console.log("detail_progress", data);
+
+  if (data.code === 0 && data.msg_type === 'detail_progress') {
+    const messageData = {
+      goods_no: data.data.goods_no,
+      temp_name: data.data.temp_name,
+      status: data.data.status,
+      goods_art_nos: data.data.goods_art_nos,
+      msg: data.msg,
+      timestamp: Date.now()
+    }
+    progressMessages.value.push(messageData)
+    // 更新当前显示的消息
+    message.value = `货号${data.data.goods_art_nos.join(', ')}:${data.msg}`
+    scrollMessageListToBottom()
+  }
+}
+
+// 处理抠图进度消息
+const handleSegmentProgressMessage = (data: any) => {
+  console.log("segment_progress", data);
+
+  if (data.code === 0 && data.msg_type === 'segment_progress') {
+    const messageData = {
+      goods_no: '',
+      temp_name: '',
+      status: data.data.status,
+      goods_art_nos: data.data.goods_art_nos,
+      msg: data.msg,
+      timestamp: Date.now()
+    }
+    progressMessages.value.push(messageData)
+    // 更新当前显示的消息
+    message.value = `货号${data.data.goods_art_nos.join(', ')}:${data.msg}`
+    scrollMessageListToBottom()
+  }
+}
+
+// 处理上脚图进度
+const handleUpperFooterProgressMessage = (data: any) => {
+  console.log("upper_footer_progress", data);
+  if (data.code === 0 && data.msg_type === 'upper_footer_progress') {
+    const messageData = {
+      goods_no: '',
+      temp_name: '',
+      status: data.data?.status || '',
+      goods_art_nos: data.data?.goods_art_nos || [],
+      msg: data.msg,
+      timestamp: Date.now()
+    }
+    progressMessages.value.push(messageData)
+    message.value = `货号${(data.data?.goods_art_nos || []).join(', ')}:${data.msg}`
+    scrollMessageListToBottom()
+  }
+}
+
+// 处理场景图进度
+const handleSceneProgressMessage = (data: any) => {
+  console.log("scene_progress", data);
+  if (data.code === 0 && data.msg_type === 'scene_progress') {
+    const messageData = {
+      goods_no: '',
+      temp_name: '',
+      status: data.data?.status || '',
+      goods_art_nos: data.data?.goods_art_nos || [],
+      msg: data.msg,
+      timestamp: Date.now()
+    }
+    progressMessages.value.push(messageData)
+    message.value = `货号${(data.data?.goods_art_nos || []).join(', ')}:${data.msg}`
+    scrollMessageListToBottom()
+  }
+}
+
+// 处理商品上传进度
+const handleUploadGoodsProgressMessage = (data: any) => {
+  console.log("upload_goods_progress", data);
+  if (data.code === 0 && data.msg_type === 'upload_goods_progress') {
+    const messageData = {
+      goods_no: '',
+      temp_name: '',
+      status: data.data?.status || '',
+      goods_art_nos: data.data?.goods_art_nos || [],
+      msg: data.msg,
+      timestamp: Date.now()
+    }
+    progressMessages.value.push(messageData)
+    message.value = `货号${(data.data?.goods_art_nos || []).join(', ')}:${data.msg}`
+    scrollMessageListToBottom()
+  }
+}
+
+// 打开输出目录:appConfig.appPath + '/build/extraResources/py/output'
+const openOutputDir = () => {
+  try {
+    const appPath = useConfigInfoStore?.appConfig?.appPath || ''
+    if (!appPath) {
+      ElMessage.error('未获取到应用目录 appPath')
+      return
+    }
+    const fullPath = `${appPath}/build/extraResources/py/output`
+    clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
+    clientStore.ipc.send(icpList.utils.shellFun, {
+      action: 'openPath',
+      params: fullPath.replaceAll('/', '\\')
+    });
+  } catch (e) {
+    console.error(e)
+    ElMessage.error('打开目录失败')
+  }
+}
+
 // 开始生成操作
 const generate = async function () {
-  if(form.is_only_cutout == 0 ){
 
+  if(form.services.length == 0){
+    ElMessage.error('请选择服务内容')
+    return
+  }
+  // 埋点:开始生成详情页
+  clickLog({
+    describe: {
+      action: '点击开始生成详情页',
+      services: form.services,
+      dataType: form.dataType,
+      template_name: form.selectTemplate.template_name,
+      goods_count: goods_art_nos.value.length,
+      goods_art_nos: goods_art_nos.value
+    }
+  }, route);
+
+  // 必填验证
+  if (form.services.includes('is_upper_footer') && !( selectedModels.value && selectedModels.value.male?.id  &&  selectedModels.value.female?.id)) {
+    openModelDialog();
+    setTimeout(()=>{
+      ElMessage.error('请选择模特')
+    },200)
+    return
+  }
+
+  if (form.services.includes('is_product_scene') && !scenePrompt.value) {
+    openScenePromptDialog();
+    setTimeout(()=>{
+      ElMessage.error('请设置场景提示词')
+    },200)
+    return
+  }
+
+  if(form.services.includes('is_detail')){
     if ( form.dataType == '1'  && !form.excel_path) {
       ElMessage.error('请上传商品基础资料')
       return
     }
   }
+
+
+
+  const useConfigInfoStore = configInfo();
+  console.log(useConfigInfoStore.appConfig);
+
+
   const tokenInfoStore = tokenInfo();
   const token = tokenInfoStore.getToken; // 使用 getToken() 获取 token
   let temp_list = []
@@ -375,29 +796,104 @@ const generate = async function () {
       template_local_classes: item.template_local_classes,
     })
   })
+  // 根据选择的服务内容设置参数
+  const isDetail = form.services.includes('is_detail') ? 1 : 0
+  const isProductScene = form.services.includes('is_product_scene') ? 1 : 0
+  const isUpperFooter = form.services.includes('is_upper_footer') ? 1 : 0
+
   const params = {
     goods_art_no: JSON.parse(JSON.stringify(goods_art_nos.value)),
-    is_only_cutout: form.is_only_cutout || '0',
     logo_path: form.logo_path || '',
     temp_name: form.selectTemplate.template_id || '',
     excel_path: form.dataType == '1' ? form.excel_path : '',
     template_image_order: form.selectTemplate.template_image_order,
     temp_list,
     token,
+    uuid: uuidStore.getUuid || '',
+          // 新增服务参数
+      online_stores: Object.values(domesticPlatforms.value || {}),
+    is_detail: isDetail,
+    is_product_scene: isProductScene,
+    is_upper_footer: isUpperFooter,
+    upper_footer_params: selectedModels.value ? {
+      man_id: selectedModels.value.male?.id || "",
+      women_id: selectedModels.value.female?.id || ""
+    } :  {},
+    product_scene_prompt: scenePrompt.value || ''
   }
+
+  console.log(params)
   // 开启进度弹窗
   requesting.value =  false
-  console.log("params", "color:#3f7cff", params);
   partErrList.value = []
   message.value = '正在为您处理,请稍后'
   progress.value = 0
+  // 清空之前的进度消息
+  progressMessages.value = []
+  showMessageHistory.value = true
+
   openLoadingDialog(goods_art_nos.value.length * 10)
-  clientStore.ipc.removeAllListeners(icpList.generate.generatePhotoDetail);
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_result_progress');
   clientStore.ipc.send(icpList.generate.generatePhotoDetail, params);
-  clientStore.ipc.on(icpList.generate.generatePhotoDetail, (event, result) => {
+
+
+  /*
+  * detail_result_progress
+  *
+  * */
+  // 监听进度消息
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
+  clientStore.ipc.on(icpList.socket.message + '_detail_progress', (event, data) => {
+    console.log('_detail_progress',data);
+    handleProgressMessage(data)
+  });
+
+  // 监听抠图进度消息
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
+  clientStore.ipc.on(icpList.socket.message + '_segment_progress', (event, data) => {
+    console.log('_segment_progress',data);
+    handleSegmentProgressMessage(data)
+  });
+  // 监听上脚图进度消息
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_upper_footer_progress');
+  clientStore.ipc.on(icpList.socket.message + '_upper_footer_progress', (event, data) => {
+    console.log('_upper_footer_progress',data);
+    handleUpperFooterProgressMessage(data)
+  });
+  // 监听场景图进度消息
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_scene_progress');
+  clientStore.ipc.on(icpList.socket.message + '_scene_progress', (event, data) => {
+    console.log('_scene_progress',data);
+    handleSceneProgressMessage(data)
+  });
+  // 监听商品上传进度消息
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_upload_goods_progress');
+  clientStore.ipc.on(icpList.socket.message + '_upload_goods_progress', (event, data) => {
+    console.log('_upload_goods_progress',data);
+    handleUploadGoodsProgressMessage(data)
+  });
+
+  clientStore.ipc.on(icpList.socket.message + '_detail_result_progress', (event, result) => {
+    if(result.code !== 0 ){
+     if(result.msg){
+       handleFail(result.msg)
+       message.value = 'result.msg'
+     }
+      progress.value = 0
+      loadingDialogVisible.value = false
+      return;
+    }
+
     console.log('result', result)
     requesting.value =  true
-    clientStore.ipc.removeAllListeners(icpList.generate.generatePhotoDetail);
+    setTimeout(() => {
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_result_progress');
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_upper_footer_progress');
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_scene_progress');
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_upload_goods_progress');
+    }, 100)
     clearInterval(INTERVAL.value)
     if (result.code === 0) {
       const { output_folder, list } = result.data
@@ -420,18 +916,27 @@ const generate = async function () {
     //生成失败 (接口请求失败)
     function handleFail(errorMsg: string) {
      // loadingDialogVisible.value = false
-      disabledButton.value = false
+     //  disabledButton.value = false
       if (errorMsg) {
         ElMessage.error(errorMsg)
       }
+
+      // 处理完成后停止监听进度消息
+     // clientStore.ipc.removeAllListeners(icpList.socket.message + '_detail_progress');
+    //  clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
     }
 
     // 全部生成成功
     function handleSuccess(href, loadingMsg) {
+      // 埋点:生成完成
+      setLogInfo(route, { action: '生成完成', output_folder: href, message: loadingMsg });
+
       completeDirectory.value = href
       progress.value = 100
       disabledButton.value = false
       message.value = loadingMsg
+
+
       handleComplete()
     }
     // 部分成功
@@ -447,6 +952,10 @@ const generate = async function () {
     }
     // 全部生成失败
     function handleFailure(partSuccessList) {
+      // 埋点:生成失败(携带货号)
+      const failedGoods = (partSuccessList || []).map(item => item.goods_art_no).filter(Boolean)
+      setLogInfo(route, { action: '生成失败', error_count: partSuccessList.length, goods_art_nos: goods_art_nos.value, failed_goods_art_nos: failedGoods });
+
       let errorList = []
       partSuccessList.map(item => {
         if (!item.success) {
@@ -458,6 +967,7 @@ const generate = async function () {
       progress.value = 100
       disabledButton.value = true
       message.value = '全部货号生成失败'
+
     }
   });
 }
@@ -585,6 +1095,10 @@ function openPhotographySeniorDetail() {
 const handleComplete = () => {
   // loadingDialogVisible.value = false
   // 这里可以添加打开目录的逻辑
+  if(!completeDirectory.value){
+     openOutputDir()
+    return
+  }
   clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
   let params = {
     action: 'openPath',
@@ -616,12 +1130,126 @@ const selectFolder = () => {
 
 }
 
+.service-section {
+  margin-bottom: 20px;
+  .service-cards {
+    display: flex;
+    gap: 60px;
+    margin-top: 16px;
+  }
+  .service-card {
+    width: 240px;
+    height: 140px;
+
+    border-radius: 8px;
+    background: #fff;
+    border: 1px solid #e0e0e0;
+    display: flex;
+    align-items: flex-start;
+    padding: 12px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    position: relative;
+
+    &:hover {
+      border-color: #2957FF;
+      box-shadow: 0 2px 8px rgba(41, 87, 255, 0.1);
+    }
+
+    &.active {
+      border: 3px solid #2957FF;
+      background: #f8f9ff;
+      box-shadow: 0 4px 12px rgba(41, 87, 255, 0.25);
+    }
+
+          .service-checkbox {
+        position: absolute;
+        left: -40px;
+        top: 10px;
+        width: 30px;
+        transform: scale(1.5);
+        z-index: 10;
+        ::v-deep{
+          .el-checkbox {
+            display: block;
+            margin-right: 0;
+          }
+          .el-checkbox__input {
+            cursor: pointer;
+          }
+        }
+      }
+
+    .service-content {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      position: absolute;
+      left: 0;
+      top: 0;
+      right: 0;
+      bottom: 0;;
+    }
+
+    .service-image {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+
+      img {
+        width: 100%;
+        height: 100%;
+        object-fit: cover;
+        border-radius: 4px;
+      }
+
+      .service-icon {
+        position: absolute;
+        top: 10px;
+        right: 10px;
+        width: 30px;
+        height: 30px;
+        background: #2957FF;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        color: white;
+        font-size: 18px;
+      }
+    }
+
+    .service-name {
+      font-size: 14px;
+      font-weight: 500;
+      text-align: center;
+      position: absolute;
+      background: rgba(0,0,0,.5);
+      height: 30px;
+      line-height: 30px;
+      color: #fff;
+      left: 0;
+      right: 0;
+      bottom:0px
+    }
+  }
+}
+
 .logo-section,
 .template-section,
 .data-prep-section {
   margin-bottom: 20px;
 }
 
+.logo-template-row {
+  display: flex;
+  gap: 24px;
+  align-items: flex-start;
+  .logo-col { flex: 2; min-width: 300px; }
+  .template-col { flex: 3; }
+}
+
 .logo-section {
   .upload-item {
     border: 2px solid rgba(0,0,0,0);
@@ -713,6 +1341,13 @@ const selectFolder = () => {
   }
 }
 
+.publish-section {
+  .label {
+    min-width: 120px;
+    margin-right: 12px;
+  }
+}
+
 
 
 .excel-upload {
@@ -881,4 +1516,61 @@ const selectFolder = () => {
   padding-right: 20px;
   color: #2957FF !important;
 }
+
+.progress-messages {
+  margin-top: 20px;
+  border-top: 1px solid #e4e7ed;
+  padding-top: 20px;
+  width: 100%;
+
+  .message-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 15px;
+    font-weight: 500;
+    color: #606266;
+  }
+
+  .message-list {
+    max-height: 200px;
+    overflow-y: auto;
+    border: 1px solid #e4e7ed;
+    border-radius: 4px;
+    padding: 10px;
+    background: #fafafa;
+
+    .message-item {
+      margin-bottom: 12px;
+      padding-bottom: 12px;
+      border-bottom: 1px solid #f0f0f0;
+
+      &:last-child {
+        margin-bottom: 0;
+        padding-bottom: 0;
+        border-bottom: none;
+      }
+
+      .message-time {
+        font-size: 12px;
+        color: #909399;
+        margin-bottom: 4px;
+      }
+
+      .message-content {
+        font-size: 14px;
+        line-height: 1.4;
+
+        .goods-no {
+          color: #2957FF;
+          font-weight: 500;
+        }
+
+        .message-text {
+          color: #606266;
+        }
+      }
+    }
+  }
+}
 </style>

+ 6 - 6
frontend/src/views/Photography/seniorDetail.vue

@@ -21,7 +21,7 @@
               <li>图片的名称不能随意修改,否则无法正常生成详情页。</li>
               <li>现有图片名称有:俯视、侧视、后视、鞋底、内里</li>
             </ol>
-            <el-icon @click="showTips = false" class="close-icon">
+            <el-icon @click="showTips = false" class="close-icon" v-log="{ describe: { action: '点击关闭高级配置提示' } }">
               <Close />
             </el-icon>
           </div>
@@ -32,7 +32,7 @@
             <div class="folder-warp">
               <div class="folder-input">
                 <el-input style="width: 60%;" v-model="folderPath" type="textarea"  :rows="2" readonly placeholder="请选择货号文件夹" />
-                <el-button class="check-button" type="primary" @click="selectFolder">
+                <el-button class="check-button" type="primary" @click="selectFolder" v-log="{ describe: { action: '选择货号文件夹' } }">
                   <img src="@/assets/images/Photography/wenjian.png" style="width: 14px; " />
                   选择目标文件夹</el-button>
               </div>
@@ -68,7 +68,7 @@
             <div class="label">图片顺序:</div>
             <el-input v-model="imageOrder" placeholder="请输入图片顺序" class="specific-page-input">
               <template #append>
-                <el-button class="explain-btn" link type="primary">说明</el-button>
+                <el-button class="explain-btn" link type="primary" v-log="{ describe: { action: '点击图片顺序说明' } }">说明</el-button>
               </template>
             </el-input>
           </div>
@@ -85,7 +85,7 @@
             <el-input v-model="specificPage" placeholder="请输入入需要单独修改的页面,示例:4:1 (需修改模版的编号:第一张)"
               class="specific-page-input">
               <template #append>
-                <el-button class="explain-btn" link type="primary">说明</el-button>
+                <el-button class="explain-btn" link type="primary" v-log="{ describe: { action: '点击指定页面说明' } }">说明</el-button>
               </template>
             </el-input>
           </div>
@@ -94,9 +94,9 @@
 
       <!-- 底部按钮 -->
       <div class="footer">
-        <el-button class="button--primary1  footer-button" type="primary" @click="saveConfig">保存配置</el-button>
+        <el-button class="button--primary1  footer-button" type="primary" @click="saveConfig" v-log="{ describe: { action: '点击保存高级配置' } }">保存配置</el-button>
 
-        <el-button class="button--primary1 footer-button" type="primary" @click="startProcess">开始处理</el-button>
+        <el-button class="button--primary1 footer-button" type="primary" @click="startProcess" v-log="{ describe: { action: '点击开始处理高级配置' } }">开始处理</el-button>
       </div>
 
     </div>

+ 339 - 80
frontend/src/views/Photography/shot.vue

@@ -25,7 +25,7 @@
             <div class="step-content flex-row justify-between">
               <div class="method-container flex-col">
                 <div class="input-container flex-row">
-                  <el-input class="input-item" v-model="goods_art_no" placeholder="请输入货号"> </el-input>
+                  <el-input class="input-item" ref="goodsArtNo" v-model="goods_art_no" placeholder="请输入货号"> </el-input>
 
                 </div>
                 <div class="auto-method flex-row justify-between">
@@ -66,8 +66,17 @@
         </div>
 
         <div class="last-photo" v-show="showlastPhoto" v-key="lastPhoto.file_path">
-          <div>{{lastPhoto.action_name || ''}}</div>
-          <el-image  :src="getFilePath(lastPhoto.file_path)"  fit="contain" ></el-image>
+
+<!--          <div class="flex between">
+            <div></div>
+            <div class="flex-item">{{ lastPhoto.action_name || '' }}</div>
+            <div class="close">
+              <el-icon style="color: #000" @click="showlastPhoto = false" class="close-icon" v-log="{ describe: { action: '关闭最近拍照预览' } }">
+                <Close/>
+              </el-icon>
+            </div>
+          </div>-->
+          <el-image  :src="getFilePath(lastPhoto.file_path)"  fit="cover" ></el-image>
         </div>
 
       </template>
@@ -77,8 +86,8 @@
           <span class="history-title flex between">
             <div>拍摄记录</div>
             <div class="c-666 fs-12" v-if="goodsList.length" >
-                    <el-button :disabled="!(runLoading || takePictureLoading)" @click="oneClickStop" v-if="configInfoStore.appModel === 1" class="input-button" type="primary" size="mini">一键停止</el-button>
-                    <el-button :disabled="runLoading || takePictureLoading" @click="delAll" class="input-button" type="primary" size="mini">一键删除</el-button>
+                    <el-button :disabled="!(runLoading || takePictureLoading)" @click="oneClickStop" v-if="configInfoStore.appModel === 1" class="input-button" type="primary" size="mini" v-log="{ describe: { action: '一键停止拍摄' } }">一键停止</el-button>
+                    <el-button :disabled="runLoading || takePictureLoading" @click="delAll" class="input-button" type="danger" size="mini" v-log="{ describe: { action: '一键删除所有记录' } }">一键删除</el-button>
             </div>
           </span>
           <img class="divider-line" referrerpolicy="no-referrer" src="@/assets/images/Photography/divider-line.png" />
@@ -108,12 +117,12 @@
                       </template>
                     </el-dropdown>
 
-                    <el-button size="small" :disabled="runLoading || takePictureLoading" @click="delGoods({goods_art_nos:[item.goods_art_no]})">删除</el-button>
+                    <el-button size="small" :disabled="runLoading || takePictureLoading" @click="delGoods({goods_art_nos:[item.goods_art_no]})" v-log="{ describe: { action: '删除货号', goods_art_no: item.goods_art_no } }">删除</el-button>
                   </div>
                 </div>
                 <div class="flex  between flex-item  c-333" style="margin-top: 5px">
                   <div class="c-999 fs-12">{{ getTime(item.action_time) }}</div>
-                  <el-button size="small" :disabled="runLoading || takePictureLoading"  type="primary"  @click="reTakePictureNos(item.goods_art_no,item)" plain v-if="configInfoStore.appModel === 1">重拍</el-button>
+                  <el-button size="small" :disabled="runLoading || takePictureLoading"  type="primary"  @click="reTakePictureNos(item.goods_art_no,item)" plain v-if="configInfoStore.appModel === 1" v-log="{ describe: { action: '重拍货号', goods_art_no: item.goods_art_no } }">重拍</el-button>
 
                 </div>
                 <div class="mar-top-10 clearfix history-item_image_wrap" style="width: 100%" >
@@ -139,7 +148,7 @@
                                 <div class="image-slot"></div>
                               </template>
                             </el-image>
-                            <el-button :disabled="runLoading || takePictureLoading" class="reset-button" @click="reTakePicture(image.PhotoRecord)">重拍</el-button>
+                            <el-button :disabled="runLoading || takePictureLoading" class="reset-button" @click="reTakePicture(image.PhotoRecord)" v-log="{ describe: { action: '重拍单张图片', goods_art_no: image.PhotoRecord.goods_art_no, action_name: image.action_name } }">重拍</el-button>
                           </div>
                         </template>
 
@@ -156,7 +165,7 @@
                       <el-image :src="getFilePath(image.PhotoRecord.image_path)"  fit="contain">
                         <template #error>
                           <div class="image-slot"></div>
-                          <el-button :disabled="runLoading || takePictureLoading" class="reset-button" @click="reTakePicture(image.PhotoRecord)">重拍</el-button>
+                          <el-button :disabled="runLoading || takePictureLoading" class="reset-button" @click="reTakePicture(image.PhotoRecord)" v-log="{ describe: { action: '重拍单张图片', goods_art_no: image.PhotoRecord.goods_art_no, action_name: image.action_name } }">重拍</el-button>
                         </template>
                       </el-image>
                       </div>
@@ -182,8 +191,14 @@
             />
             <span class="empty-text">暂无数据</span>
           </div>-->
-          <div v-if="goodsList.length" class="next-step button--primary1 flex-col" @click="openPhotographyDetail"><span
-              class="next-step-text">{{ configInfoStore.appModel === 1 ? '开始生成' : '开始生成'}}</span></div>
+          <div
+            class="next-step button--primary1 flex-col"
+            :class="{ 'is-disabled': !goodsList.length || runLoading || takePictureLoading }"
+            @click="(!goodsList.length || runLoading || takePictureLoading) ? null : openPhotographyDetail()"
+            v-log="{ describe: { action: '点击开始生成' } }"
+          >
+            <span class="next-step-text">{{ configInfoStore.appModel === 1 ? '开始生成' : '开始生成'}}</span>
+          </div>
         </div>
 
     </div>
@@ -205,14 +220,21 @@ import  generate from '@/utils/menus/generate'
 const loading = ref(false)
 const runLoading = ref(false)
 const takePictureLoading = ref(false)
-
+import { Close } from '@element-plus/icons-vue'
+import { clickLog, setLogInfo } from '@/utils/log'
+import { useUuidStore } from '@/stores/modules/uuid'
+import { useRoute } from 'vue-router'
 
 import useUserInfo from "@/stores/modules/user";
 const useUserInfoStore = useUserInfo();
+const route = useRoute();
 
 import  configInfo  from '@/stores/modules/config';
 const configInfoStore = configInfo();
 import qiehuan from '@/components/header-bar/assets/qiehuan.svg'
+import tokenInfo from "@/stores/modules/token";
+
+const tokenInfoStore = tokenInfo();
 
 const menu = computed(()=>{
   if(configInfoStore.appModel === 2){
@@ -237,7 +259,7 @@ const menu = computed(()=>{
       }
     ]
   }
-  if(useUserInfoStore.userInfo.brand_company_code === '1300'){
+  if(useUserInfoStore.userInfo.brand_company_code === '1300' || configInfoStore.appConfig.debug){
     return [
       {
         type:'setting'
@@ -261,8 +283,7 @@ const menu = computed(()=>{
     },
     {
       ...generate
-    },
-    {
+    },    {
       type:'ota'
     }
   ]
@@ -315,6 +336,47 @@ const runAction = ref({
 
 // 初始化 WebSocket 状态管理
 const socketStore = socket()
+const uuidStore = useUuidStore();
+
+// 抠图请求去重与延迟队列(key: goods_art_no, value: timeoutId)
+const segmentQueue = new Map<string, ReturnType<typeof setTimeout>>()
+
+function isGoodsStillInList(goodsArtNo: string): boolean {
+  return goodsList.value?.some((g: any) => g.goods_art_no === goodsArtNo) || false
+}
+
+function scheduleSegment(goodsArtNo: string) {
+  if (!goodsArtNo) return
+  // 若已存在,则重置计时(重新插入)
+  if (segmentQueue.has(goodsArtNo)) {
+    const t = segmentQueue.get(goodsArtNo)
+    if (t) clearTimeout(t)
+    segmentQueue.delete(goodsArtNo)
+  }
+  const timeoutId = setTimeout(async () => {
+    segmentQueue.delete(goodsArtNo)
+    if (!isGoodsStillInList(goodsArtNo)) return
+    try {
+      await socketStore.connectSocket();
+      console.log('segment_progress',{
+        token:tokenInfoStore.getToken,
+        uuid: uuidStore?.getUuid || '',
+        goods_art_no: [goodsArtNo],
+      })
+      socketStore.sendMessage({
+        type: 'segment_progress',
+        data: {
+          token:tokenInfoStore.getToken,
+          uuid: uuidStore?.getUuid || '',
+          goods_art_no: [goodsArtNo],
+        }
+      })
+    } catch (e) {
+      // 忽略发送异常,避免打断主流程
+    }
+  }, 30000)
+  segmentQueue.set(goodsArtNo, timeoutId)
+}
 
 /**
  * 保存货号模板到货号变量中。
@@ -376,7 +438,6 @@ async function runGoods(data) {
     type: 'run_mcu',
     data,
   })
-  ElMessage.success('开始拍摄,请稍后')
   runLoading.value = true;
   runAction.value.action = data.action
   runAction.value.goods_art_no = data.goods_art_no
@@ -394,6 +455,9 @@ async function runGoods(data) {
       ElMessage.error(result.msg)
       runLoading.value = false
       return;
+    }else{
+
+      ElMessage.success('开始拍摄,请稍后')
     }
   })
 
@@ -415,11 +479,16 @@ const getTime = function(time){
  */
 async function delAll(){
     let params = goodsList.value.map(item=>item.goods_art_no)
-    await ElMessageBox.confirm('确定要删除当下的历史记录吗?', '提示', {
-      confirmButtonText: '确定',
-      cancelButtonText: '取消',
-    })
-    del({goods_art_nos:params})
+    try {
+      await ElMessageBox.confirm('确定要删除当下的历史记录吗?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+      })
+      await clickLog({ describe: { action: '点击确认一键删除', goods_art_nos: params } }, route)
+      del({goods_art_nos:params})
+    } catch (e) {
+      await clickLog({ describe: { action: '点击取消一键删除' } }, route)
+    }
 }
 
 /**
@@ -427,13 +496,16 @@ async function delAll(){
  * @param params - 包含需要删除的货号列表的对象。
  */
 const delGoods = async function(params){
-
-
-  await ElMessageBox.confirm('确定要删除货号:'+params.goods_art_nos[0]+'的拍摄数据吗?', '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-  })
-  del(params)
+  try {
+    await ElMessageBox.confirm('确定要删除货号:'+params.goods_art_nos[0]+'的拍摄数据吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+    })
+    await clickLog({ describe: { action: '点击确认删除货号', goods_art_no: params.goods_art_nos?.[0] } }, route)
+    del(params)
+  } catch (e) {
+    await clickLog({ describe: { action: '点击取消删除货号', goods_art_no: params.goods_art_nos?.[0] } }, route)
+  }
 }
 
 /**
@@ -452,7 +524,12 @@ const del = async function(params){
       ElMessage.info('货号删除成功')
       getPhotoRecords()
       if(reNosObj.value.goods_art_no){
-        runGoods(reNosObj.value)
+        runGoods(
+            {
+              "action": reNosObj.value.action,
+              "goods_art_no":reNosObj.value.goods_art_no
+            })
+
       }
     }else if(result.msg) {
       ElMessage.error(result.msg)
@@ -465,32 +542,105 @@ const del = async function(params){
 
 const reTakePicture = async (img)=>{
   if(!img.id) return;
-  await ElMessageBox.confirm('此操作会先删除此数据,需要继续吗?', '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-  })
+  if(img.image_path){
+    try {
+        await ElMessageBox.confirm('此操作会先删除此数据,需要继续吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+        })
+      await clickLog({ describe: { action: '点击确认单张重拍', goods_art_no: img.goods_art_no, action_name: img.action_name } }, route)
+    } catch (e) {
+      await clickLog({ describe: { action: '点击取消单张重拍', goods_art_no: img.goods_art_no, action_name: img.action_name } }, route)
+      return
+    }
+
+  }
+
+  runLoading.value = true;
+  reNosObj.value.goods_art_no = img.goods_art_no
+  reNosObj.value.action = 're_take_picture'
+
+  let params = {
+    id: img.action_id
+  }
+
+  clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigDetail);
+
+  clientStore.ipc.send(icpList.setting.getDeviceConfigDetail, params);
 
-  socketStore.sendMessage({
-    type: 're_take_picture',
-    "data":{"record_id":img.id},
-  })
 
-  clientStore.ipc.removeAllListeners(icpList.socket.message + '_re_take_picture');
+  clientStore.ipc.on(icpList.setting.getDeviceConfigDetail, (event, result) => {
 
-  clientStore.ipc.on(icpList.socket.message + '_re_take_picture', (event, result) => {
-    console.log('_re_take_picture')
+    console.log('getDeviceConfigDetail')
     console.log(result)
-    if(result.code === 0){
-      getPhotoRecords()
-      setTimeout(()=>{
-        showlastPhoto.value = false
-      },6000)
-    }else if(result.msg) {
+    if(result.code == 0 && result.data){
+      clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigDetail);
+
+      this_run_mcu_single(result.data)
+    }else if(result.msg){
+      runLoading.value = false;
+      reNosObj.value.goods_art_no = ''
+      reNosObj.value.action = ''
       ElMessage.error(result.msg)
     }
-    clientStore.ipc.removeAllListeners(icpList.socket.message + '_re_take_picture');
+  });
 
-  })
+  function  this_run_mcu_single(data){
+
+    clientStore.ipc.removeAllListeners(icpList.socket.message+'_run_mcu_single');
+    socketStore.sendMessage({
+      type: 'run_mcu_single',
+      data: {
+        camera_height: Number(data.camera_height),
+        camera_angle:  Number(data.camera_angle),
+        led_switch:data.led_switch,
+        id:0,
+        mode_type:data.mode_type,
+        turntable_position:Number(data.turntable_position),
+        action_name:data.action_name || '测试',
+        turntable_angle: Number(data.turntable_angle),
+        shoe_upturn: Number(data.shoe_upturn),
+        action_index:1,
+        number_focus:0,
+        take_picture:false,
+        pre_delay:0,
+        after_delay:0,
+      }
+    });
+
+
+    clientStore.ipc.on(icpList.socket.message+'_run_mcu_single', async (event, result) => {
+      console.log('_run_mcu_single_row')
+      clientStore.ipc.removeAllListeners(icpList.socket.message+'_run_mcu_single');
+        this_re_take_picture()
+    })
+  }
+  async function this_re_take_picture(){
+
+    await ElMessageBox.alert('已复位到该视图下,请把鞋子摆放完毕之后,点击按钮开始重拍', '提示',{
+      confirmButtonText:"开始重拍",
+      showClose:false,
+      closeOnClickModal:false,
+      closeOnPressEscape:false
+    })
+    await clickLog({ describe: { action: '点击开始重拍', goods_art_no: img.goods_art_no } }, route)
+
+
+    socketStore.sendMessage({
+      type: 'smart_shooter_photo_take',
+      "data":{"id":img.id,"goods_art_no":img.goods_art_no},
+    })
+  }
+
+
+}
+
+const resetStatus = ()=>{
+  runLoading.value = false;
+  reNosObj.value.goods_art_no = ''
+  reNosObj.value.action = ''
+  runAction.value.goods_art_no = '';
+  runAction.value.action = '';
 }
 
 const reNosObj = ref({
@@ -500,13 +650,20 @@ const reNosObj = ref({
 //货号重拍
 const reTakePictureNos = async (goods_art_no,item)=>{
 
-  await ElMessageBox.confirm('此操作会先删除删除货号:'+goods_art_no+'的拍摄数据吗,需要继续吗?', '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-  })
+  try {
+    await ElMessageBox.confirm('此操作会先删除删除货号:'+goods_art_no+'的拍摄数据吗,需要继续吗?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+    })
+    await clickLog({ describe: { action: '点击确认重拍货号', goods_art_no } }, route)
+  } catch (e) {
+    await clickLog({ describe: { action: '点击取消重拍货号', goods_art_no } }, route)
+    return
+  }
   reNosObj.value.goods_art_no = goods_art_no
   reNosObj.value.action = '执行左脚程序'
-  if(item.items && typeof item.items === 'object' && item.items[0].image_deal_mode){
+  console.log(item);
+  if(item.items && typeof item.items === 'object' && item.items[0].PhotoRecord.image_deal_mode){
       reNosObj.value.action = '执行右脚程序'
   }
   del({goods_art_nos:[goods_art_no]})
@@ -538,6 +695,7 @@ const oneClickStop = ()=>{
     })
   }
 }
+const goodsArtNo = ref()
 /**
  * 页面挂载时初始化事件监听器并获取初始数据。
  */
@@ -549,7 +707,8 @@ onMounted(async () => {
     if (result.code === 0 && result.data?.data) {
       console.log(goods_art_no.value);
       if(!goods_art_no.value){
-          ElMessage.error('请先扫描货号或者手动输入货号!')
+          ElMessage.error('请在左侧第一步中,先扫描货号或者手动输入货号!')
+          goodsArtNo.value?.focus() // 聚焦输入框
           return;
       }
       runGoods({
@@ -606,17 +765,30 @@ onMounted(async () => {
 
   })
 
+  // 监听一键停止结束
+  clientStore.ipc.on(icpList.socket.message + '_run_mcu_stop', (event, result) => {
+    console.log('_run_mcu_stop')
+
+    resetStatus()
+  })
+
+
   // 监听拍照完成后的最终状态事件
   clientStore.ipc.on(icpList.socket.message + '_photo_take_finish', (event, result) => {
     console.log('_photo_take_finish')
     console.log(result)
     if(result.code === 0) {
+      setLogInfo(route, { action: '全部拍摄完成', goods_art_no: runAction.value.goods_art_no });
+      // 全部拍摄完成后,触发抠图队列
+      if (runAction.value.goods_art_no) {
+        scheduleSegment(runAction.value.goods_art_no)
+      }
       runLoading.value = false;
       runAction.value.goods_art_no = '';
       runAction.value.action = '';
       setTimeout(()=>{
         showlastPhoto.value = false
-      },5000)
+      },3000)
     }
 
   })
@@ -655,6 +827,8 @@ onMounted(async () => {
 
 const onRemoteControl = (type)=>{
   if(type == 'take_picture'){
+    // 埋点:手动拍照
+    clickLog({ describe: { action: '点击遥控器拍照按钮' } }, route);
 
     if(runLoading.value || takePictureLoading.value){
       ElMessage.error('拍摄程序正在运行,请稍候')
@@ -670,11 +844,16 @@ const onRemoteControl = (type)=>{
 
 
   if(!goods_art_no.value){
-    ElMessage.error('请先扫描货号或者手动输入货号!')
+    ElMessage.error('请在左侧第一步中,先扫描货号或者手动输入货号!')
+    goodsArtNo.value?.focus() // 聚焦输入框
     return;
   }
   let action = '执行左脚程序'
   if(type  === 'right')  action = '执行右脚程序'
+
+  // 埋点:遥控器启动拍摄
+  clickLog({ describe: { action: `点击遥控器${type === 'left' ? '左脚' : '右脚'}按钮`, goods_art_no: goods_art_no.value } }, route);
+
   runGoods({
     "action": action,
     "goods_art_no": goods_art_no.value,
@@ -721,9 +900,19 @@ onBeforeUnmount(() => {
   clientStore.ipc.removeAllListeners(icpList.socket.message + '_run_mcu_update');
   clientStore.ipc.removeAllListeners(icpList.socket.message + '_stop_action');
   clientStore.ipc.removeAllListeners(icpList.socket.message + '_smart_shooter_photo_take');
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_run_mcu_stop');
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_digicam_take_picture');
+  clientStore.ipc.removeAllListeners(icpList.socket.message + '_segment_progress');
+
 /*  window.removeEventListener('storage', handleStorageEvent);*/
 
 
+  // 清理抠图队列的定时器
+  try {
+    segmentQueue.forEach((t) => { if (t) clearTimeout(t) })
+    segmentQueue.clear()
+  } catch (e) {}
+
 })
 
 /*
@@ -734,18 +923,18 @@ const showlastPhoto = ref(false)
 const  getLastPhotoRecord = async ()=>{
 
   return;
+
   if(goodsList.value && goodsList.value.length === 0) return;
   clientStore.ipc.removeAllListeners(icpList.takePhoto.getLastPhotoRecord);
   clientStore.ipc.send(icpList.takePhoto.getLastPhotoRecord,);
 
   clientStore.ipc.on(icpList.takePhoto.getLastPhotoRecord, (event, result) => {
-    console.log('getLastPhotoRecord')
-    console.log(result)
-    console.log(runAction.value)
+    console.log('getLastPhotoRecord');
+    console.log( result.data?.goods_art_no);
     clientStore.ipc.removeAllListeners(icpList.takePhoto.getLastPhotoRecord);
     if(result.code === 0){
-      if(lastPhoto.value?.image_path){
-        if(  lastPhoto.value?.image_path == result.data?.image_path) return;
+      if(lastPhoto.value?.photo_file_name){
+     //   if(  lastPhoto.value?.image_path == result.data?.image_path) return;
 
         if(runAction.value.goods_art_no === result.data?.goods_art_no){
           showlastPhoto.value = true
@@ -764,15 +953,46 @@ let smartShooterTimeout = null; // 在合适的位置定义一个全局变量用
 clientStore.ipc.on(icpList.socket.message+'_smart_shooter_photo_take', async (event, result) => {
   console.log('_smart_shooter_photo_take');
   console.log(result);
-  if(result.code === 0 && result.data?.photo_file_name){
+
+  // runLoading.value = false;
+
+  //
+  if(result.code === 0){
+    if(!result.data.goods_art_no )  return;
+      setLogInfo(route, { action: '单张拍摄完成', goods_art_no: result.data.goods_art_no });
+      if(reNosObj.value?.goods_art_no === result.data.goods_art_no){
+        runLoading.value = false;
+        // 单张重拍完成且存在重拍货号时,触发抠图队列
+        scheduleSegment(result.data.goods_art_no)
+      }
     if (smartShooterTimeout) {
       clearTimeout(smartShooterTimeout);
     }
+    setTimeout(() => {
+      showlastPhoto.value =  true;
+      lastPhoto.value = {
+        file_path:result.data.photo_file_name
+      };
+        setTimeout(()=>{
+          if(!runAction.value.goods_art_no){
+            showlastPhoto.value =  false;
+          }
+        },3000)
+    }, 100);
     smartShooterTimeout = setTimeout(() => {
       getPhotoRecords();
+
+      if(!runAction.value.goods_art_no){
+        showlastPhoto.value =  false;
+      }
     }, 2000);
-  }
+  }else if(result.msg) {
 
+    runLoading.value = false;
+    reNosObj.value.goods_art_no = ''
+    reNosObj.value.action = ''
+    ElMessage.error(result.msg)
+  }
 })
 
 
@@ -784,20 +1004,20 @@ clientStore.ipc.on(icpList.socket.message + '_run_mcu_update', (event, result) =
   if(result.code === 0){
     if(result.data?.file_path){
       if(  lastPhoto.value?.file_path == result.data?.file_path) return;
-
-      if(runAction.value.goods_art_no === result.data?.goods_art_no){
+      let goods_art_no =  runAction.value.goods_art_no ||  reNosObj.value.goods_art_no
+      if(goods_art_no === result.data?.goods_art_no){
         showlastPhoto.value = true
         goodsList.value.map(item=>{
           if(item.goods_art_no === result.data?.goods_art_no){
-            console.log('========item');
-            console.log(item);
             item.items[result.data.image_index].PhotoRecord.image_path = result.data?.file_path
             result.data.action_name =  item.items[result.data.image_index].action_name
             setTimeout(()=>{
               item.items[result.data.image_index].PhotoRecord.image_path = result.data?.file_path
             },1000)
-            console.log(item);
-            console.log( goodsList.value);
+
+            setTimeout(()=>{
+              showlastPhoto.value = false
+            },3000)
           }
         })
        // getPhotoRecords()
@@ -810,6 +1030,9 @@ clientStore.ipc.on(icpList.socket.message + '_run_mcu_update', (event, result) =
   }else if(result.msg) {
     ElMessage.error(result.msg)
   }
+  if(reNosObj.value.goods_art_no){
+    resetStatus()
+  }
 })
 
 
@@ -817,6 +1040,8 @@ clientStore.ipc.on(icpList.socket.message + '_run_mcu_update', (event, result) =
  * 打开主图详情页面。
  */
 function openPhotographyDetail() {
+  // 埋点:开始生成
+  clickLog({ describe: { action: '开始生成', goods_count: goodsList.value.length, goods_art_nos: goodsList.value.map(item=>item.goods_art_no) } }, route);
 
   if(runLoading.value || takePictureLoading.value){
     ElMessage.error('正在拍摄中,请稍候')
@@ -832,8 +1057,8 @@ function openPhotographyDetail() {
   clientStore.ipc.removeAllListeners(icpList.utils.openMain);
   let params = {
     title: '主图与详情生成',
-    width: 1000,
-    height: 630,
+    width: 3840,
+    height: 2160,
     frame: true,
     id: "PhotographyDetail",
     url: getRouterUrl(href)
@@ -871,10 +1096,10 @@ const onGenerateCLick = (menu,item)=>{
 
 <style  lang="scss">
 .shot-image-popper {
-  width: calc(100vw - 370px) !important;
-  left: 20px !important;
-  top: 50px !important;
-  height: calc(100vh - 70px) !important;
+  width: calc(100vw - 470px) !important;
+  left: 70px !important;
+  top: 100px !important;
+  height: calc(100vh - 170px) !important;
   transform: translate(0px, 0px) !important;
 
   .el-image {
@@ -1445,12 +1670,18 @@ const onGenerateCLick = (menu,item)=>{
           background-size: 100% 100%;
           width: 100%;
           line-height: 50px;
+          cursor: pointer;
+
+          &.is-disabled {
+            opacity: 0.5;
+            cursor: not-allowed;
+            pointer-events: none;
+          }
 
           .next-step-text {
             width: 100%;
             overflow-wrap: break-word;
             color: rgba(255, 255, 255, 1);
-            font-weight: NaN;
             text-align: center;
             white-space: nowrap;
 
@@ -1460,11 +1691,39 @@ const onGenerateCLick = (menu,item)=>{
       }
 .last-photo{
   position: fixed;
-  left: 20px;
-  top: 50px;
-  bottom: 20px;
-  right: 350px;
+  padding: 10px;
+  box-shadow: 0 0 5px rgb(0 0 0 / 50%);
+  left: 70px;
+  top: 100px;
+  bottom: 60px;
+  right: 400px;
   z-index: 10;
+
+  .close {
+    position: absolute;
+    right: -7px;
+    top: -1px;
+    background: #fff;
+    width: 30px;
+    height: 30px;
+    border-radius: 30px;
+    z-index: 11;
+    text-align: center;
+    line-height: 30px;
+    box-shadow: 0 0 5px rgb(0 0 0 / 50%);
+  }
+  .el-image {
+    width: 100%;
+    height:100%;
+    display: block;
+
+    .el-image__inner {
+      width: 100%;
+      height:100%;
+      display: block;
+
+    }
+  }
 }
 </style>
 

+ 25 - 2
frontend/src/views/RemoteControl/index.vue

@@ -14,8 +14,19 @@
       <el-col :span="6"><div class="button up" @click="runRight">右脚</div></el-col>
       <el-col :span="3"></el-col>
     </el-row>
-    <div class="te-c mar-top-50 fs-14"  style="color: #8C92A7">左脚控制左脚鞋启动拍摄</div>
-    <div class="te-c mar-top-10 fs-14"  style="color: #8C92A7">右脚控制右脚鞋启动拍摄</div>
+    <el-row align="middle">
+      <el-col :span="6"></el-col>
+      <el-col :span="6">
+        <div class="button up" @click="switchLED(1)" v-log="{ describe: { action: 'LED开启' } }">LED开</div>
+      </el-col>
+      <el-col :span="1"></el-col>
+      <el-col :span="6">
+        <div class="button up" @click="switchLED(0)" v-log="{ describe: { action: 'LED关闭' } }">LED关</div>
+      </el-col>
+      <el-col :span="4"></el-col>
+    </el-row>
+    <div class="te-c  fs-14"  style="color: #8C92A7">左脚控制左脚鞋启动拍摄</div>
+    <div class="te-c  fs-14"  style="color: #8C92A7">右脚控制右脚鞋启动拍摄</div>
   </div>
 
 </template>
@@ -26,6 +37,7 @@ import headerBar from '@/components/header-bar/index.vue'
 import icpList from '@/utils/ipc'
 import client from "@/stores/modules/client";
 import socket from "@/stores/modules/socket";
+import {Switch} from "@element-plus/icons-vue";
 
 const clientStore = client();
 // 初始化 WebSocket 状态管理
@@ -47,6 +59,17 @@ const run_take_picture = () => {
   emit('onRemoteControl','take_picture')
 }
 
+//LED
+const switchLED = async (value) => {
+  socketStore.sendMessage({
+    type: 'control_mcu',
+    data: {
+      device_name: "laser_position",
+      value,
+    }
+  });
+}
+
 </script>
 
 <style scoped lang="scss">

+ 177 - 0
frontend/src/views/Setting/components/CameraConfig.vue

@@ -0,0 +1,177 @@
+<template>
+
+  <div class="flex left fw-b fs-16 mar-top-20" style="padding-left: 100px">相机ISO参数<span style="color: #FD5E1A">(<el-icon style="position: relative; top:2px; margin: 0 3px"><WarningFilled /></el-icon>相机设置ISO auto时无效)</span></div>
+  <div class="selectBox">
+    <div class="form-item" style="padding-bottom: 30px;">
+      <div class="iso-inputs mar-top-20">
+        <div class="iso-group">
+          <span class="iso-label">用曝光灯时:</span>
+          <div class="select-wrapper">
+            <el-select
+              v-model="iso_config.low"
+              filterable
+              default-first-option
+              placeholder="请选择或输入ISO值"
+              class="iso-input"
+            >
+              <el-option
+                v-for="item in iso_options"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </div>
+        </div>
+        <div class="iso-group">
+          <span class="iso-label">不用时:</span>
+          <div class="select-wrapper">
+            <el-select
+              v-model="iso_config.high"
+              filterable
+              default-first-option
+              placeholder="请选择或输入ISO值"
+              class="iso-input"
+            >
+              <el-option
+                v-for="item in iso_options"
+                :key="item"
+                :label="item"
+                :value="item"
+              />
+            </el-select>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { reactive,onMounted,ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import client from "@/stores/modules/client";
+import icpList from '@/utils/ipc';
+import socket from "@/stores/modules/socket.js";
+const clientStore = client();
+const socketStore = socket(); // WebSocket状态管理实例
+
+const iso_config = reactive({
+  low: 100,
+  high: 6000,
+  mode:"auto22"
+})
+
+const iso_options = ref(['auto',100,200,400,800,1600,3200,6400,12800])
+
+onMounted( () => {
+  // 读取已保存的相机配置
+  clientStore.ipc.removeAllListeners(icpList.setting.getSysConfig);
+  clientStore.ipc.send(icpList.setting.getSysConfig,{key: 'camera_configs'});
+  clientStore.ipc.on(icpList.setting.getSysConfig, (event, result) => {
+    if(result.code == 0 && result.data){
+      iso_config.low = result.data.iso_low ?? iso_config.low
+      iso_config.high = result.data.iso_high ?? iso_config.high
+    }
+    clientStore.ipc.removeAllListeners(icpList.setting.getSysConfig);
+  });
+
+  // 读取设备当前可用的 ISO 档位
+  clientStore.ipc.removeAllListeners(icpList.socket.message+'_smart_shooter_get_camera_property');
+  socketStore.sendMessage({
+    type: 'smart_shooter_get_camera_property'
+  });
+
+  clientStore.ipc.on(icpList.socket.message+'_smart_shooter_get_camera_property', async (event, result) => {
+    if(result.code == 0 && result.data){
+      const ISO  =  result.data.filter(item => item.CameraPropertyType == 'ISO')
+      if(ISO.length > 0){
+        iso_options.value = ISO[0].CameraPropertyRange
+      }
+    }
+    clientStore.ipc.removeAllListeners(icpList.socket.message+'_smart_shooter_get_camera_property');
+  })
+})
+
+// 暴露保存方法,给父组件调用
+const save = async () => {
+  // 必填校验(允许填写数字或 'auto')
+  const isEmpty = (v) => v === undefined || v === null || v === ''
+  if (isEmpty(iso_config.low)) {
+    ElMessage.error('请填写“用曝光灯时”的 ISO 值')
+    return false
+  }
+  if (isEmpty(iso_config.high)) {
+    ElMessage.error('请填写“不用时”的 ISO 值')
+    return false
+  }
+
+  // 若均为数字,做简单范围校验(>0)
+  const lowNum = Number(iso_config.low)
+  const highNum = Number(iso_config.high)
+  if (!Number.isNaN(lowNum) && lowNum <= 0) {
+    ElMessage.error('“用曝光灯时”的 ISO 必须大于 0')
+    return false
+  }
+  if (!Number.isNaN(highNum) && highNum <= 0) {
+    ElMessage.error('“不用时”的 ISO 必须大于 0')
+    return false
+  }
+
+  return await new Promise((resolve, reject) => {
+    clientStore.ipc.removeAllListeners(icpList.setting.updateSysConfigs);
+    clientStore.ipc.send(icpList.setting.updateSysConfigs,{
+      key: 'camera_configs',
+      value: JSON.stringify({
+        iso_low: iso_config.low,
+        iso_high: iso_config.high
+      })
+    });
+
+    clientStore.ipc.on(icpList.setting.updateSysConfigs, async (event, result) => {
+      clientStore.ipc.removeAllListeners(icpList.setting.updateSysConfigs);
+      if(result.code === 0 && result.msg){
+        resolve(true)
+      } else {
+        resolve(false)
+      }
+    });
+  });
+}
+
+defineExpose({ save })
+
+</script>
+
+<style lang="scss" scoped>
+.iso-inputs {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.iso-group {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.iso-label {
+  min-width: 80px;
+  font-size: 14px;
+  color: #1A1A1A;
+}
+
+.iso-input {
+  width: 200px;
+}
+
+.select-wrapper {
+  :deep(.el-input__inner) {
+    border-radius: 6px;
+  }
+}
+.selectBox {
+  padding-top: 10px;
+}
+</style>

+ 53 - 0
frontend/src/views/Setting/components/DebugPanel.vue

@@ -0,0 +1,53 @@
+<template>
+  <div class="debug-panel" v-if="isVisible">
+    <div class="form-item flex left">
+        <el-button @click="openResourceDirectory" v-log="{ describe: { action: '打开资源目录' } }">打开资源目录</el-button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import client from "@/stores/modules/client";
+import  configInfo  from '@/stores/modules/config';
+import icpList from '@/utils/ipc';
+
+const clientStore = client();
+const configInfoStore = configInfo();
+
+const isVisible = ref(false);
+
+function showDebugPanel() {
+  isVisible.value = true;
+}
+
+function hideDebugPanel() {
+  isVisible.value = false;
+}
+
+function toggleVisibility() {
+  isVisible.value = !isVisible.value;
+}
+
+function openResourceDirectory() {
+  clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
+  let params = {
+    action: 'openPath',
+    params: configInfoStore.appConfig.userDataPath.replaceAll('/', '\\')
+  };
+  clientStore.ipc.send(icpList.utils.shellFun, params);
+}
+
+// 挂载时注册事件监听器
+defineExpose({
+  showDebugPanel,
+  hideDebugPanel,
+  toggleVisibility
+});
+</script>
+
+<style scoped>
+.debug-panel {
+  padding-bottom: 10px;
+}
+</style>

+ 333 - 13
frontend/src/views/Setting/components/action_config.vue

@@ -1,6 +1,6 @@
 <template>
 
-  <el-tabs v-model="topsTab" type="card" class="top_tabs">
+  <el-tabs v-model="topsTab" type="card" class="top_tabs" :disabled="isSortMode">
     <el-tab-pane label="执行左脚程序" name="left">
     </el-tab-pane>
     <el-tab-pane label="执行右脚程序" name="right"></el-tab-pane>
@@ -10,20 +10,48 @@
       <div class="item"
            :class="item.id === activeTab.id ? 'active' : ''"
            v-for="item in tabs" :key="item.id"
-           @click="toggleTab(item)"
+           @click="toggleTab(item)" v-log="{ describe: { action: '点击切换动作Tab', tabName: item.mode_name, tabId: item.id } }"
+           :style="{ cursor: isSortMode ? 'not-allowed' : 'pointer', opacity: isSortMode ? 0.5 : 1 }"
       >{{item.mode_name}}</div>
   </div>
   <div class="form-table">
+    <div v-if="isSortMode" class="sort-tip">
+      <el-icon><Warning /></el-icon>
+      <span>排序模式:请拖拽行进行排序,完成后点击"保存排序"</span>
+    </div>
     <div class="btnBox">
-      <div class="primary-btn" @click="addRow">新增一行</div>
-      <div class="primary-btn" @click="resetConfig">重新初始化</div>
-      <div class="primary-btn" @click="reName">重命名配置</div>
-      <el-radio-group style="margin-left: 10px" v-model="selectID" @click="changeSelectId(activeTab.id)">
+      <div class="primary-btn" @click="addRow" v-log="{ describe: { action: '点击新增一行' } }" :class="{ disabled: isSortMode }" :style="{ opacity: isSortMode ? 0.5 : 1, cursor: isSortMode ? 'not-allowed' : 'pointer' }">新增一行</div>
+      <div class="primary-btn" @click="resetConfig" v-log="{ describe: { action: '点击重新初始化', tab: topsTab } }" :class="{ disabled: isSortMode }" :style="{ opacity: isSortMode ? 0.5 : 1, cursor: isSortMode ? 'not-allowed' : 'pointer' }">重新初始化</div>
+      <div class="primary-btn" @click="reName" v-log="{ describe: { action: '点击重命名配置' } }" :class="{ disabled: isSortMode }" :style="{ opacity: isSortMode ? 0.5 : 1, cursor: isSortMode ? 'not-allowed' : 'pointer' }">重命名配置</div>
+<!--      <div class="primary-btn" @click="toggleSortMode" v-log="{ describe: { action: isSortMode ? '点击保存排序' : '点击排序' } }">
+        {{ isSortMode ? '保存排序' : '排序' }}
+      </div>-->
+      <div v-if="isSortMode" class="normal-btn" @click="cancelSort" v-log="{ describe: { action: '点击取消排序' } }">
+        取消排序
+      </div>
+      <el-radio-group style="margin-left: 10px" v-model="selectID" @click="changeSelectId(activeTab.id)" v-log="{ describe: { action: '点击切换执行配置', tabId: activeTab.id } }" :disabled="isSortMode">
         <el-radio :label="activeTab.id">切换成执行配置</el-radio>
       </el-radio-group>
     </div>
-    <el-table max-height="700" :data="tableData" style="width: 100%" border>
-<!--      <el-table-column prop="id" label="id" />-->
+    <el-table
+      max-height="700"
+      :data="tableData"
+      style="width: 100%"
+      border
+      row-key="id"
+      :row-class-name="getRowClassName"
+      ref="tableRef"
+    >
+      <el-table-column prop="sort" label="排序" width="80" v-if="isSortMode">
+        <template #default="scope">
+          <div class="sort-content">
+            <span class="sort-number">{{ scope.row.sort }}</span>
+            <div class="sort-handle">
+              <el-icon><Rank /></el-icon>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
       <el-table-column prop="action_name" label="步骤" >
         <template #default="scope">
           {{ scope.row.action_name }}
@@ -41,8 +69,8 @@
       </el-table-column>
       <el-table-column prop="value" label="操作" >
         <template #default="{row, $index}">
-          <a class="mar-right-10 cursor-pointer" @click="editRow(row, $index)">编辑</a>
-          <a class="cursor-pointer" v-if="!row.is_system" @click="deleteRow(row, $index)">删除</a>
+          <a class="mar-right-10 cursor-pointer" @click="editRow(row, $index)" v-log="{ describe: { action: '点击编辑步骤', id: row.id, action_name: row.action_name } }">编辑</a>
+          <a class="cursor-pointer" v-if="!row.is_system" @click="deleteRow(row, $index)" v-log="{ describe: { action: '点击删除步骤', id: row.id, action_name: row.action_name } }">删除</a>
         </template>
       </el-table-column>
     </el-table>
@@ -63,7 +91,9 @@
 <script setup lang="ts">
 import { ref, defineProps, defineEmits , watch,onMounted, reactive,onBeforeUnmount } from 'vue'
 import EditDialog from "./EditDialog.vue";
-import { ElMessage, ElMessageBox } from 'element-plus';import client from "@/stores/modules/client";
+import { ElMessage, ElMessageBox } from 'element-plus';
+import { Rank, Warning } from '@element-plus/icons-vue';
+import client from "@/stores/modules/client";
 import icpList from '@/utils/ipc';
 const clientStore = client();
 import socket from "@/stores/modules/socket";
@@ -80,10 +110,18 @@ const activeTab = ref({}); // 当前激活的标签页
 const tabs = ref([]); // 所有标签页
 const editId = ref(0); // 当前编辑行的索引
 const selectID = ref(0) //当前默认的ID
+const isSortMode = ref(false); // 是否处于排序模式
+const originalTableData = ref([]); // 保存原始数据用于排序
+const tableRef = ref(null); // 表格引用
+let dragEventHandlers = null; // 拖拽事件处理器
 
 
 onBeforeUnmount(()=>{
     window.removeEventListener('beforeunload', handleBeforeUnload);
+    // 清理排序模式
+    if (isSortMode.value) {
+      exitSortMode();
+    }
 })
 
 onMounted(()=>{
@@ -106,7 +144,9 @@ const handleBeforeUnload = (e)=>{
  * 监听topsTab变化,获取对应标签页的设备配置列表。
  */
 watch(() => topsTab.value, (newTab) => {
-  getTopList();
+  if (!isSortMode.value) {
+    getTopList();
+  }
 });
 
 const getTopList = ()=>{
@@ -140,6 +180,8 @@ const getTopList = ()=>{
 
 //切换tab
 const toggleTab = (item) => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   activeTab.value = item
   getList()
 };
@@ -150,6 +192,11 @@ const calibrationId = ref(null) //校准位
  * 获取设备配置列表。
  */
 const getList = () => {
+  // 如果正在排序模式,先退出
+  if (isSortMode.value) {
+    exitSortMode();
+  }
+
   clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
   let params = {
     tab_id: activeTab.value.id
@@ -177,6 +224,7 @@ const getList = () => {
 
 
 const changeSelectId = (id)=>{
+  if (isSortMode.value) return; // 排序模式下禁用
   if(id === selectID.value) return;
   clientStore.ipc.removeAllListeners(icpList.setting.updateLeftRightConfig);
   let params = {
@@ -205,6 +253,8 @@ const changeSelectId = (id)=>{
  * @param {number} index - 当前行的索引
  */
 const editRow = (row, index) => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   addRowData.value = {}
   dialogVisible.value = true;
   editId.value = row.id
@@ -216,6 +266,8 @@ const editRow = (row, index) => {
  * @param {number} index - 当前行的索引
  */
 const deleteRow = (row, index) => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   ElMessageBox.confirm('确定删除该步骤吗?', '提示', {
     confirmButtonText: '确定',
     cancelButtonText: '取消',
@@ -242,6 +294,8 @@ const deleteRow = (row, index) => {
  * 重置设备配置。
  */
 const resetConfig = () => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   console.log(activeTab.value);
   ElMessageBox.confirm(`确定初始化${activeTab.value.mode_name}吗?`, '提示', {
     confirmButtonText: '确定',
@@ -267,6 +321,8 @@ const resetConfig = () => {
 };
 
 const reName = ()=>{
+  if (isSortMode.value) return; // 排序模式下禁用
+
   ElMessageBox.prompt('', '重命名配置', {
     confirmButtonText: '保存',
     cancelButtonText: '取消',
@@ -296,7 +352,7 @@ const reName = ()=>{
             ElMessage.success('重命名成功');
           }  else if(result.mssg){
             ElMessage.error(result.mssg);
-          }else {
+            }else {
             ElMessage.error('重命名失败');
           }
           clientStore.ipc.removeAllListeners(icpList.setting.updateTabName);
@@ -308,6 +364,8 @@ const reName = ()=>{
  * 新增一行配置。
  */
 const addRow = () => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   editId.value = -1
   let length = Number(tableData.value.length)+1
   addRowData.value =   {
@@ -329,6 +387,203 @@ const addRow = () => {
   editTitle.value = '新增步骤';
 };
 
+/**
+ * 切换排序模式
+ */
+const toggleSortMode = () => {
+  if (isSortMode.value) {
+    // 保存排序
+    saveSortOrder();
+  } else {
+    // 进入排序模式
+    enterSortMode();
+  }
+};
+
+/**
+ * 取消排序
+ */
+const cancelSort = () => {
+  exitSortMode();
+  ElMessage.info('已取消排序');
+};
+
+/**
+ * 进入排序模式
+ */
+const enterSortMode = () => {
+  isSortMode.value = true;
+  // 保存原始数据
+  originalTableData.value = JSON.parse(JSON.stringify(tableData.value));
+  // 为每行添加排序值
+  tableData.value.forEach((item, index) => {
+    item.sort = index + 1;
+  });
+
+  // 等待DOM更新后初始化Sortable
+  setTimeout(() => {
+    initSortable();
+  }, 100);
+
+  ElMessage.info('请拖拽行进行排序,完成后点击"保存排序"');
+};
+
+/**
+ * 初始化拖拽排序
+ */
+const initSortable = () => {
+  if (!tableRef.value) return;
+
+  const tbody = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody');
+  if (!tbody) return;
+
+  // 使用原生拖拽API实现排序
+  let draggedRow = null;
+  let draggedIndex = -1;
+
+  // 创建事件处理器
+  dragEventHandlers = {
+    handleDragStart: (e) => {
+      if (!isSortMode.value) return;
+      draggedRow = e.target.closest('tr');
+      if (draggedRow) {
+        e.dataTransfer.effectAllowed = 'move';
+        draggedRow.style.opacity = '0.5';
+        const rows = Array.from(tbody.querySelectorAll('tr'));
+        draggedIndex = rows.indexOf(draggedRow);
+      }
+    },
+
+    handleDragEnd: (e) => {
+      if (draggedRow) {
+        draggedRow.style.opacity = '';
+        draggedRow = null;
+        draggedIndex = -1;
+      }
+    },
+
+    handleDragOver: (e) => {
+      if (!isSortMode.value || !draggedRow) return;
+      e.preventDefault();
+      e.dataTransfer.dropEffect = 'move';
+    },
+
+    handleDrop: (e) => {
+      if (!isSortMode.value || !draggedRow) return;
+      e.preventDefault();
+
+      const dropRow = e.target.closest('tr');
+      if (!dropRow || dropRow === draggedRow) return;
+
+      // 获取目标行的索引
+      const rows = Array.from(tbody.querySelectorAll('tr'));
+      const dropIndex = rows.indexOf(dropRow);
+
+      if (draggedIndex !== -1 && dropIndex !== -1 && draggedIndex !== dropIndex) {
+        // 重新排序数据
+        const newData = [...tableData.value];
+        const [draggedItem] = newData.splice(draggedIndex, 1);
+        newData.splice(dropIndex, 0, draggedItem);
+
+        // 更新排序值
+        newData.forEach((item, index) => {
+          item.sort = index + 1;
+        });
+
+        tableData.value = newData;
+      }
+    }
+  };
+
+  // 添加事件监听器
+  tbody.addEventListener('dragstart', dragEventHandlers.handleDragStart);
+  tbody.addEventListener('dragend', dragEventHandlers.handleDragEnd);
+  tbody.addEventListener('dragover', dragEventHandlers.handleDragOver);
+  tbody.addEventListener('drop', dragEventHandlers.handleDrop);
+
+  // 为每行添加拖拽属性
+  const rows = tbody.querySelectorAll('tr');
+  rows.forEach(row => {
+    row.draggable = isSortMode.value;
+  });
+};
+
+/**
+ * 保存排序
+ */
+const saveSortOrder = () => {
+  // 准备排序数据
+  const sortData = tableData.value.map((item, index) => ({
+    id: item.id,
+    sort: index + 1
+  }));
+
+  console.log("sort_data",{
+    tab_id: activeTab.value.id,
+    sort_data: sortData
+  })
+  // 调用后端接口保存排序
+  clientStore.ipc.removeAllListeners(icpList.setting.updateDeviceConfigSort);
+  clientStore.ipc.send(icpList.setting.updateDeviceConfigSort, {
+    tab_id: activeTab.value.id,
+    sort_data: sortData
+  });
+
+  clientStore.ipc.on(icpList.setting.updateDeviceConfigSort, (event, result) => {
+    if (result.code == 0) {
+      ElMessage.success('排序保存成功');
+      isSortMode.value = false;
+      // 重新获取列表以更新数据
+      getList();
+    } else if (result.mssg) {
+      ElMessage.error(result.mssg);
+    } else {
+      ElMessage.error('排序保存失败');
+      // 保存失败时退出排序模式
+      exitSortMode();
+    }
+    clientStore.ipc.removeAllListeners(icpList.setting.updateDeviceConfigSort);
+  });
+};
+
+/**
+ * 获取行类名
+ */
+const getRowClassName = ({ row, rowIndex }) => {
+  if (isSortMode.value) {
+    return 'sortable-row';
+  }
+  return '';
+};
+
+/**
+ * 退出排序模式
+ */
+const exitSortMode = () => {
+  isSortMode.value = false;
+  // 恢复原始数据
+  tableData.value = JSON.parse(JSON.stringify(originalTableData.value));
+
+  // 移除拖拽事件监听器和属性
+  if (tableRef.value && dragEventHandlers) {
+    const tbody = tableRef.value.$el.querySelector('.el-table__body-wrapper tbody');
+    if (tbody) {
+      // 移除事件监听器
+      tbody.removeEventListener('dragstart', dragEventHandlers.handleDragStart);
+      tbody.removeEventListener('dragend', dragEventHandlers.handleDragEnd);
+      tbody.removeEventListener('dragover', dragEventHandlers.handleDragOver);
+      tbody.removeEventListener('drop', dragEventHandlers.handleDrop);
+
+      // 移除拖拽属性
+      const rows = tbody.querySelectorAll('tr');
+      rows.forEach(row => {
+        row.draggable = false;
+      });
+    }
+    dragEventHandlers = null;
+  }
+};
+
 </script>
 
 <style lang="scss" scoped>
@@ -410,6 +665,71 @@ const addRow = () => {
   .cursor-pointer{
     cursor: pointer;
   }
+
+  // 排序相关样式
+  .sort-content {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    height: 100%;
+
+    .sort-number {
+      font-weight: bold;
+      color: #2957FF;
+      font-size: 14px;
+    }
+
+    .sort-handle {
+      cursor: move;
+      color: #909399;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      &:hover {
+        color: #2957FF;
+      }
+    }
+  }
+
+  :deep(.sortable-row) {
+    cursor: move;
+
+    &:hover {
+      background-color: #f5f7fa !important;
+    }
+  }
+
+  :deep(.el-table__row.sortable-row) {
+    transition: all 0.3s ease;
+  }
+
+  :deep(.el-table__body-wrapper tbody tr) {
+    &.sortable-row {
+      cursor: move;
+
+      &:hover {
+        background-color: #f5f7fa !important;
+      }
+    }
+  }
+
+  .disabled {
+    pointer-events: none;
+  }
+
+  .sort-tip {
+    background: #EAF3FF;
+    border: 1px solid #CBE1FF;
+    border-radius: 4px;
+    padding: 8px 12px;
+    margin-bottom: 12px;
+    display: flex;
+    align-items: center;
+    gap: 8px;
+    color: #2957FF;
+    font-size: 14px;
+  }
 }
 .editDialog{
   .el-dialog__body{

+ 34 - 1
frontend/src/views/Setting/components/otherConfig.vue

@@ -1,7 +1,25 @@
 <template>
+<!--
   <div class="flex left fs-14 line-40 mar-top-10" style="margin-left: 100px">
     剩余金币:{{ useUserInfoStore.userInfo.coin_amount }}
-    <el-button class="mar-left-10" @click="openDialog()">充值金币</el-button>
+    <el-button class="mar-left-10" @click="openDialog()" v-log="{ describe: { action: '点击充值金币' } }">充值金币</el-button>
+  </div>
+-->
+
+
+  <div class="form-item">
+    <label>剩余金币:</label>
+    <div class="select-wrapper">
+      <el-input
+          type="text"
+          readonly="readonly"
+          v-model="useUserInfoStore.userInfo.coin_amount"
+          class="w-full px-3 py-2 border rounded-md"
+      >
+        <template #append> <el-button class="recharge-btn" @click="openDialog()" v-log="{ describe: { action: '点击充值' } }">充值</el-button></template>
+      </el-input>
+    </div>
+    <div class="fs-12 c-666">(用户视频生成,需单独金币支付)</div>
   </div>
 
   <!-- 新增:充值金币弹窗 -->
@@ -71,3 +89,18 @@ onUnmounted(() => {
   window.removeEventListener('message', handleWindowMessage);
 });
 </script>
+<style scoped lang="scss">
+.recharge-btn {
+  background: linear-gradient( 180deg, #FFAF51 0%, #FF7272 100%);
+  border-radius: 6px;
+  color: #fff !important;
+}
+.select-wrapper {
+  ::v-deep {
+
+    .el-input-group__append{
+
+    }
+  }
+}
+</style>

+ 149 - 94
frontend/src/views/Setting/index.vue

@@ -1,20 +1,26 @@
 <template>
   <headerBar
-    title="设置"
-  />
-  <div class="container">
+  >
+    <template  #title><div @click="handleSettingClick" v-log="{ describe: { action: '点击设置标题' } }">设置</div></template>
+  </headerBar>
+  <div class="container setting-wrap">
     <nav class="settings-nav">
-      <div class="nav-item" :class="{'active': activeIndex === 0}" @click="toggleTab(0)">
+      <div class="nav-item" :class="{'active': activeIndex === 0}" @click="toggleTab(0)" v-log="{ describe: { action: '点击切换设置Tab', tab: '基础配置' } }">
         <img src="@/assets/images/setting/icon1.png" class="nav-icon" v-if="activeIndex !== 0"/>
         <img src="@/assets/images/setting/icon1a.png" class="nav-icon" v-else/>
         <span>基础配置</span>
       </div>
-      <div class="nav-item" :class="{'active': activeIndex === 2}" @click="toggleTab(2)">
+      <div class="nav-item" v-if="configInfoStore.appModel === 1" :class="{'active': activeIndex === 3}" @click="toggleTab(3)" v-log="{ describe: { action: '点击切换设置Tab', tab: '相机配置' } }">
+        <img src="@/assets/images/setting/icon2.png" class="nav-icon" v-if="activeIndex !== 3"/>
+        <img src="@/assets/images/setting/icon2a.png" class="nav-icon" v-else/>
+        <span>相机配置</span>
+      </div>
+      <div class="nav-item" :class="{'active': activeIndex === 2}" @click="toggleTab(2)" v-log="{ describe: { action: '点击切换设置Tab', tab: '其他设置' } }">
         <img src="@/assets/images/setting/icon3.png" class="nav-icon" v-if="activeIndex !== 2"/>
         <img src="@/assets/images/setting/icon3a.png" class="nav-icon" v-else/>
         <span>其他设置</span>
       </div>
-      <div class="nav-item" v-if="configInfoStore.appModel === 1" :class="{'active': activeIndex === 4}"  @click="toggleTab(4)">
+      <div class="nav-item" v-if="configInfoStore.appModel === 1" :class="{'active': activeIndex === 4}"  @click="toggleTab(4)" v-log="{ describe: { action: '点击切换设置Tab', tab: '左右脚程序设置' } }">
         <img src="@/assets/images/setting/icon4.png" class="nav-icon" v-if="activeIndex !== 4"/>
         <img src="@/assets/images/setting/icon4a.png" class="nav-icon" v-else/>
         <span>左右脚程序设置</span>
@@ -66,49 +72,57 @@
                     </el-select>
                     </div>
                 </div>
+
+               <DebugPanel ref="debugPanel" />
         </div>
       <!--基础配置-->
+      <!--相机配置-->
+      <template v-if="activeIndex === 3">
+      <CameraConfig ref="cameraConfigRef"/>
+
+      </template>
+      <!--相机配置-->
       <!--其他设置-->
-          <template v-if="activeIndex === 2">
-
-            <div class="selectBox" style="padding-top: 0px" >
-              <div class="form-item">
-                <label>产品类型:</label>
-                <div class="select-wrapper">
-                  <el-select v-model="formData.other_configs.product_type" placeholder="请选择">
-                    <el-option v-for="item in productTypeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                  </el-select>
-                </div>
-              </div>
-              <div class="form-item">
-                <label>默认抠图模式:</label>
-                <div class="select-wrapper">
-                  <el-select v-model="formData.other_configs.cutout_mode" placeholder="请选择">
-                    <el-option v-for="item in defaultCutoutModeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                  </el-select>
-                </div>
-              </div>
-              <div class="form-item">
-                <label>设备运动速度:</label>
-                <div class="select-wrapper">
-                  <el-select v-model="formData.other_configs.device_speed" placeholder="请选择">
-                    <el-option v-for="item in deviceSpeedList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                  </el-select>
-                </div>
-              </div>
-
-              <!--                <div class="form-item">
-                                  <label>运行模式:</label>
-                                  <div class="select-wrapper">
-                                  <el-select v-model="formData.other_configs.running_mode" placeholder="请选择">
-                                    <el-option v-for="item in runModeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                                  </el-select>
-                                  </div>
-                              </div>-->
+      <template v-if="activeIndex === 2">
+
+        <div class="selectBox" style="padding-top: 0px" >
+          <div class="form-item">
+            <label>产品类型:</label>
+            <div class="select-wrapper">
+              <el-select v-model="formData.other_configs.product_type" placeholder="请选择">
+                <el-option v-for="item in productTypeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+              </el-select>
             </div>
+          </div>
+          <div class="form-item">
+            <label>默认抠图模式:</label>
+            <div class="select-wrapper">
+              <el-select v-model="formData.other_configs.cutout_mode" placeholder="请选择">
+                <el-option v-for="item in defaultCutoutModeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+              </el-select>
+            </div>
+          </div>
+          <div class="form-item">
+            <label>设备运动速度:</label>
+            <div class="select-wrapper">
+              <el-select v-model="formData.other_configs.device_speed" placeholder="请选择">
+                <el-option v-for="item in deviceSpeedList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+              </el-select>
+            </div>
+          </div>
 
-            <other-config/>
-          </template>
+          <other-config/>
+          <!--                <div class="form-item">
+                              <label>运行模式:</label>
+                              <div class="select-wrapper">
+                              <el-select v-model="formData.other_configs.running_mode" placeholder="请选择">
+                                <el-option v-for="item in runModeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
+                              </el-select>
+                              </div>
+                          </div>-->
+        </div>
+
+      </template>
       <!--其他设置-->
           <div class="selectBox" style="padding-top: 0px;padding-left: 0;" v-if="activeIndex === 4">
             <actionConfig/>
@@ -116,7 +130,7 @@
           </div>
     </div>
       <div class="text-center mt-8">
-        <button class="bg-gradient-to-r from-primary" @click="onSava(activeIndex)" v-if="activeIndex !== 4">
+        <button class="bg-gradient-to-r from-primary" @click="onSava(activeIndex)" v-if="activeIndex !== 4" v-log="{ describe: { action: '点击保存设置', tabIndex: activeIndex } }">
           保存
         </button>
       </div>
@@ -145,8 +159,37 @@ import { useCheckInfo } from '@/composables/userCheck';
 import { preview } from '@planckdev/element-plus/utils'
 import actionConfig from './components/action_config.vue'
 import otherConfig from './components/otherConfig'
+import CameraConfig from './components/CameraConfig';
 useCheckInfo();
 
+//点击三次  打开资源目录
+
+import DebugPanel from './components/DebugPanel.vue';
+// 在setup函数中创建调试面板实例
+const debugPanel = ref(null);
+const cameraConfigRef = ref(null);
+// 添加设置点击计数器
+const settingClickCount = ref(0);
+
+// 修改headerBar的点击处理函数
+function handleSettingClick() {
+  console.log('handleSettingClickhandleSettingClick')
+  settingClickCount.value++;
+
+  if (settingClickCount.value >= 3) {
+    if (debugPanel.value) {
+      debugPanel.value.showDebugPanel();
+    }
+    settingClickCount.value = 0;
+  }
+
+  setTimeout(() => {
+    settingClickCount.value = 0;
+  }, 3000); // 3秒内未再次点击则重置计数器
+}
+
+
+
 // 路由和状态管理初始化
 const route = useRoute();
 const router = useRouter();
@@ -306,7 +349,7 @@ watch(() => route.query.type, async (newType,oldType) => {
  //   await  saveSetting(oldType)
   }
   const typeValue = parseInt(newType) || 0;
-  if(typeValue === 4) return;
+  if([3,4].includes(typeValue)) return;
   switch (typeValue) {
       default:
         clientStore.ipc.removeAllListeners(icpList.setting.getSysConfig);
@@ -384,13 +427,14 @@ const handleInput = (value) => {
 };
 
 const toggleTab = async (item) => {
-  let oldType = activeIndex.value;
-
-  if([0,1,2].includes(oldType)){
-   const next =  await  saveSetting(oldType)
-    if(next === false) return ;
+  const oldType = activeIndex.value;
+  // 切换前保存当前 Tab 配置(包含相机配置 3)
+  if ([0,1,2,3].includes(oldType)) {
+    const next = await saveSetting(oldType);
+    if (next === false) return false;
   }
-   activeIndex.value = item;
+  activeIndex.value = item;
+  return true;
 };
 
 const onSava = async (index)=>{
@@ -407,6 +451,12 @@ const onSava = async (index)=>{
  */
 const saveSetting = async (index) => {
   // 构建临时提交数据
+  if(index === 3) {
+    if (cameraConfigRef.value && typeof cameraConfigRef.value.save === 'function') {
+      return await cameraConfigRef.value.save()
+    }
+    return false
+  }
   const submitData = {
     ...formData[indexKey[index]]
   };
@@ -463,6 +513,53 @@ const saveSetting = async (index) => {
 .el-image-viewer__wrapper{
   z-index: 9999 !important;
 }
+.setting-wrap {
+
+  .selectBox{
+    padding-top: 30px;
+    padding-left: 100px;
+    border-bottom: 1px solid rgba(0,0,0,0.1);
+    :deep(.el-tabs__header){
+      padding-left: 0;
+    }
+    :deep(.el-tabs--card>.el-tabs__header){
+      border-bottom: 1px solid #CCCCCC;
+    }
+    :deep(.el-tabs__item){
+      height: 30px;
+      line-height: 30px;
+    }
+    :deep(.el-tabs__nav-wrap){
+      margin-bottom: 0;
+    }
+    :deep(.el-tabs__item.is-active){
+      color: #333;
+      font-weight: bold;
+      background: #fff;
+    }
+  }
+
+  .form-item {
+    margin-bottom: 24px;
+    display: flex;
+    align-items: center;
+  }
+  .form-item label {
+    display: block;
+    min-width: 98px;
+    text-align: right;
+    font-size: 14px;
+    color: #1A1A1A;
+  }
+
+  .select-wrapper {
+    position: relative;
+    width: 200px;
+    :deep(.el-input__inner){
+      border-radius: 6px;
+    }
+  }
+}
 </style>
 <style lang="scss" scoped>
 body {
@@ -520,18 +617,6 @@ body {
   margin: 0 auto;
   height: 306px;
 }
-.form-item {
-  margin-bottom: 24px;
-  display: flex;
-  align-items: center;
-}
-.form-item label {
-  display: block;
-  min-width: 98px;
-  text-align: right;
-  font-size: 14px;
-  color: #1A1A1A;
-}
 .input-group {
   display: flex;
   gap: 12px;
@@ -543,13 +628,6 @@ body {
   padding: 8px 12px;
   font-size: 14px;
 }
-.select-wrapper {
-  position: relative;
-  width: 200px;
-  :deep(.el-input__inner){
-    border-radius: 6px;
-  }
-}
 .error-text {
   color: #dc2626;
   font-size: 12px;
@@ -568,29 +646,6 @@ body {
 .captureBox{
   border-bottom: 1px solid rgba(0,0,0,0.1);
 }
-.selectBox{
-    padding-top: 30px;
-    padding-left: 100px;
-    border-bottom: 1px solid rgba(0,0,0,0.1);
-    :deep(.el-tabs__header){
-      padding-left: 0;
-    }
-    :deep(.el-tabs--card>.el-tabs__header){
-      border-bottom: 1px solid #CCCCCC;
-    }
-    :deep(.el-tabs__item){
-      height: 30px;
-      line-height: 30px;
-    }
-    :deep(.el-tabs__nav-wrap){
-      margin-bottom: 0;
-    }
-    :deep(.el-tabs__item.is-active){
-      color: #333;
-      font-weight: bold;
-      background: #fff;
-    }
-}
 .select-btn{
   display: flex;
   align-items: center;

+ 0 - 1104
frontend/src/views/Setting/index_old.vue

@@ -1,1104 +0,0 @@
-<template>
-  <headerBar
-    title="设置"
-  />
-  <div class="container">
-    <nav class="settings-nav">
-      <div class="nav-item" :class="{'active': activeIndex === 0}" @click="activeIndex = 0">
-        <img src="@/assets/images/setting/icon1.png" class="nav-icon" v-if="activeIndex !== 0"/>
-        <img src="@/assets/images/setting/icon1a.png" class="nav-icon" v-else/>
-        <span>基础配置</span>
-      </div>
-<!--      <div class="nav-item" :class="{'active': activeIndex === 1}" @click="activeIndex = 1">
-        <img src="@/assets/images/setting/icon2.png" class="nav-icon" v-if="activeIndex !== 1"/>
-        <img src="@/assets/images/setting/icon2a.png" class="nav-icon" v-else/>
-        <span>拍照设置</span>
-      </div>-->
-      <div class="nav-item" :class="{'active': activeIndex === 2}" @click="activeIndex = 2">
-        <img src="@/assets/images/setting/icon3.png" class="nav-icon" v-if="activeIndex !== 2"/>
-        <img src="@/assets/images/setting/icon3a.png" class="nav-icon" v-else/>
-        <span>其他设置</span>
-      </div>
-<!--      <div class="nav-item" :class="{'active': activeIndex === 3}" @click="activeIndex = 3">
-        <img src="@/assets/images/setting/icon4.png" class="nav-icon" v-if="activeIndex !== 3"/>
-        <img src="@/assets/images/setting/icon4a.png" class="nav-icon" v-else/>
-        <span>遥控器设置</span>
-      </div>-->
-      <div class="nav-item" :class="{'active': activeIndex === 4}" @click="activeIndex = 4">
-        <img src="@/assets/images/setting/icon4.png" class="nav-icon" v-if="activeIndex !== 4"/>
-        <img src="@/assets/images/setting/icon4a.png" class="nav-icon" v-else/>
-        <span>左右脚程序设置</span>
-      </div>
-    </nav>
-
-    <div class="form-container">
-<!--        <div class="captureBox" v-if="activeIndex === 0">
-            <div class="form-item">
-                <label>canpture_one图片输出文件夹:</label>
-                <div class="input-group">
-                  <el-input style="width: 430px;" type="textarea" :rows="2" v-model="formData.captureOneFolder" readonly></el-input>
-                  <div class="select-btn" @click="selectFolder">
-                   <img src="@/assets/images/setting/folder.png" /> 选择文件夹
-                  </div>
-                </div>
-            </div>
-&lt;!&ndash;            <p class="error-text">capture one的导出拍照图像自动和智能拍的待处理图像自不一致,请重新选择</p>&ndash;&gt;
-          </div>-->
-      <!--基础配置-->
-          <div class="selectBox" v-if="activeIndex === 0">
-                <div class="form-item">
-                    <label>主图尺寸:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.basic_configs.main_image_size" placeholder="请选择">
-                      <el-option v-for="item in mainImageSizeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>图片输出格式:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.basic_configs.image_out_format" placeholder="请选择">
-                      <el-option v-for="item in imageFormatList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>图片锐化:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.basic_configs.image_sharpening" placeholder="请选择">
-                      <el-option v-for="item in imageSharpeningList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-        </div>
-      <!--基础配置-->
-      <!--拍照设置-->
-          <div class="selectBox" style="padding-top: 0px" v-if="activeIndex === 1">
-                <div class="form-item">
-                    <label>重复拍摄警告:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.take_photo_configs.repart_take_photo_warning" placeholder="请选择">
-                      <el-option v-for="item in repeatWarningList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>单次张数警告:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.take_photo_configs.single_photo_warning" placeholder="请选择">
-                      <el-option v-for="item in singleWarningList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>累计拍照警告:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.take_photo_configs.total_photo_warning" placeholder="请选择">
-                      <el-option v-for="item in totalWarningList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-<!--                <div class="form-item">
-                    <label>对焦时间:</label>
-                    <div class="select-wrapper">
-                      <el-input v-model="formData.focusTime" placeholder="请输入">
-                        <template #append>秒</template>
-                      </el-input>
-                    </div>
-                </div>-->
-                <div class="form-item">
-                    <label>拍照停留:</label>
-                    <div class="select-wrapper">
-                      <el-input v-model="formData.take_photo_configs.camera_delay" placeholder="请输入">
-                        <template #append>秒</template>
-                      </el-input>
-                    </div>
-                </div>
-        </div>
-      <!--拍照配置-->
-      <!--其他设置-->
-          <div class="selectBox" style="padding-top: 0px" v-if="activeIndex === 2">
-                <div class="form-item">
-                    <label>产品类型:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.other_configs.product_type" placeholder="请选择">
-                      <el-option v-for="item in productTypeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>默认抠图模式:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.other_configs.cutout_mode" placeholder="请选择">
-                      <el-option v-for="item in defaultCutoutModeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>设备运动速度:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.other_configs.device_speed" placeholder="请选择">
-                      <el-option v-for="item in deviceSpeedList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-<!--                <div class="form-item">
-                    <label>运行模式:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.other_configs.running_mode" placeholder="请选择">
-                      <el-option v-for="item in runModeList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>-->
-          </div>
-      <!--其他设置-->
-          <div class="selectBox" style="padding-top: 0px" v-if="activeIndex === 3">
-                <div class="form-item">
-                    <label>接收器:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.receiver" placeholder="请选择">
-                      <el-option v-for="item in receiverList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>左:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.left" placeholder="请选择">
-                      <el-option v-for="item in leftList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>右:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.right" placeholder="请选择">
-                      <el-option v-for="item in rightList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>上:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.up" placeholder="请选择">
-                      <el-option v-for="item in upList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-                <div class="form-item">
-                    <label>下:</label>
-                    <div class="select-wrapper">
-                    <el-select v-model="formData.down" placeholder="请选择">
-                      <el-option v-for="item in downList" :key="item.value" :label="item.label" :value="item.value"></el-option>
-                    </el-select>
-                    </div>
-                </div>
-          </div>
-          <div class="selectBox" style="padding-top: 0px;padding-left: 0;" v-if="activeIndex === 4">
-            <el-tabs v-model="activeTab" type="card">
-              <el-tab-pane label="执行左脚程序" name="left">
-              </el-tab-pane>
-              <el-tab-pane label="执行右脚程序" name="right"></el-tab-pane>
-            </el-tabs>
-            <div class="form-table">
-              <div class="btnBox">
-                <div class="primary-btn" @click="addRow">新增一行</div>
-                <div class="normal-btn" @click="resetConfig">重新初始化</div>
-              </div>
-              <el-table height="200px" :data="activeTab === 'left' ? lefttableData : righttableData" style="width: 100%" border>
-                <el-table-column prop="id" label="id" />
-                <el-table-column prop="action_name" label="步骤" />
-                <el-table-column prop="take_picture" label="是否拍照" width="200px">
-                  <template #default="scope">
-                    <el-radio-group v-model="scope.row.take_picture">
-                      <el-radio :label="true">拍照</el-radio>
-                      <el-radio :label="false">不拍照</el-radio>
-                    </el-radio-group>
-                  </template>
-                </el-table-column>
-                <el-table-column prop="action_index" label="排序" >
-                  <template #default="{row, $index}">
-                    <a v-if="$index !== 0" class="cursor-pointer" @click="upRow(row)">上移</a>
-                    <a class="mar-left-10 cursor-pointer" v-if="$index !== activeTab === 'left' ? lefttableData.length - 1 : righttableData.length - 1" @click="downRow(row)">下移</a>
-                  </template>
-                </el-table-column>
-                <el-table-column prop="value" label="操作" >
-                  <template #default="{row, $index}">
-                    <a class="mar-right-10 cursor-pointer" @click="editRow(row, $index)">编辑</a>
-                    <a class="cursor-pointer" @click="deleteRow(row, $index)">删除</a>
-                  </template>
-                </el-table-column>
-              </el-table>
-            </div>
-          </div>
-    </div>
-      <div class="text-center mt-8">
-        <button class="bg-gradient-to-r from-primary" @click="saveSetting(activeIndex)" v-if="activeIndex !== 4">
-          保存
-        </button>
-      </div>
-      <el-dialog custom-class="editDialog" v-model="dialogVisible" :title="editTitle" width="660px">
-        <div class="config-type">配置类型:执行{{ activeTab === 'left' ? '左脚' : '右脚' }}程序 <el-checkbox v-model="isDefault">开启运动调试</el-checkbox></div>
-        <el-form class="editForm" :model="editRowData" label-width="100px">
-          <el-form-item label="动作名称">
-            <el-input v-model="editRowData.action_name" style="width: 170px;"/>
-          </el-form-item>
-          <el-form-item label="是否拍照">
-            <el-radio-group v-model="editRowData.take_picture">
-              <el-radio :label="true">拍照</el-radio>
-              <el-radio :label="false">不拍照</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item label="相机高度(mm)">
-            <el-input v-model="editRowData.camera_height" @change="changeNum('camera_high_motor',0, 400)" :min="0" :max="400" :step="1"  style="width: 170px;" type="number">
-            </el-input>
-            <div class="error-msg">最小0,最大400</div>
-          </el-form-item>
-          <el-form-item label="相机倾角">
-            <el-input v-model="editRowData.camera_angle" :min="-40" :max="40" :step=".1" @change="changeNum('camera_steering',-40, 40)" style="width: 170px;" type="number">
-            </el-input>
-            <div class="error-msg">最小-40,最大40</div>
-          </el-form-item>
-          <el-form-item label="转盘前后位置">
-            <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">
-            </el-input>
-            <div class="error-msg">最小0,最大800</div>
-          </el-form-item>
-          <el-form-item label="转盘角度">
-            <el-input v-model="editRowData.turntable_angle" @change="changeNum('turntable_steering',-720, 720)" :min="-720" :max="720" :step="1"  style="width: 170px;" type="number">
-            </el-input>
-            <div class="error-msg">最小-720,最大720</div>
-          </el-form-item>
-          <el-form-item label="鞋子翻转">
-            <div class="flex-row">
-            <el-radio-group v-model="editRowData.shoe_upturn">
-              <el-radio :label="true">翻转</el-radio>
-              <el-radio :label="false">不翻转</el-radio>
-              </el-radio-group>
-              <a class="cursor-pointer" @click="changeNum('overturn_steering')">测试翻转</a>
-            </div>
-          </el-form-item>
-          <el-form-item label="LED灯光开光" @change="changeNum('laser_position')">
-            <el-radio-group v-model="editRowData.led_switch">
-              <el-radio :label="false">关闭</el-radio>
-              <el-radio :label="true">开启</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item label="对焦次数">
-            <el-input v-model="editRowData.number_focus" @change="changeNum('take_picture',0, 1)" :min="0" :max="1" :step="1"  style="width: 170px;" type="number">
-            </el-input>
-            <div class="error-msg">最小0,最大1</div>
-          </el-form-item>
-          <el-form-item label="拍照前延时(秒)">
-            <el-input v-model="editRowData.pre_delay" :min="0" :max="99" :step="1" @change="changeNum('pre_delay',0, 99)" style="width: 170px;" type="number">
-            </el-input>
-            <div class="error-msg">最小0,最大99</div>
-          </el-form-item>
-          <el-form-item label="拍照后延时(秒)">
-            <el-input v-model="editRowData.after_delay" :min="0" :max="99" :step="1" @change="changeNum('after_delay',0, 99)" style="width: 170px;" type="number">
-            </el-input>
-            <div class="error-msg">最小0,最大99</div>
-          </el-form-item>
-          <!-- <el-form-item label="是否等待">
-            <el-radio-group v-model="editRowData.wait_user">
-              <el-radio :label="false">否</el-radio>
-              <el-radio :label="true">是</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-form-item label="待用户确认照片">
-            <el-radio-group v-model="editRowData.confirm_photo">
-              <el-radio :label="false">否</el-radio>
-              <el-radio :label="true">是</el-radio>
-            </el-radio-group>
-          </el-form-item> -->
-        </el-form>
-        <template #footer>
-          <div class="btn-row">
-            <div class="normal-btn" @click="dialogVisible = false">关闭</div>
-             <div class="primary-btn" v-loading="captureLoading" @click="testShoesFlip">运行并拍照</div>
-            <div class="primary-btn" @click="saveRow">保存并关闭</div>
-          </div>
-        </template>
-      </el-dialog>
-  </div>
-</template>
-
-<script setup>
-/**
- * Vue组件逻辑部分,包含与设备配置相关的功能。
- * 主要功能包括:表单数据管理、设备配置列表获取、新增/编辑/删除步骤、保存配置等。
- */
-
-// 引入Vue相关功能和第三方库
-import { ref, reactive } from 'vue';
-import { useRoute, useRouter } from 'vue-router';
-import { onMounted, watch } from 'vue';
-import socket from "@/stores/modules/socket";
-import headerBar from '@/components/header-bar/index.vue';
-import client from "@/stores/modules/client";
-import icpList from '@/utils/ipc';
-const clientStore = client();
-import { ElMessage, ElMessageBox } from 'element-plus';
-import { digiCamControlWEB } from  '@/utils/appconfig'
-import { useCheckInfo } from '@/composables/userCheck';
-import { preview } from '@planckdev/element-plus/utils'
-useCheckInfo();
-
-// 路由和状态管理初始化
-const route = useRoute();
-const router = useRouter();
-
-// 定义响应式变量
-const folderPath = ref(''); // 文件夹路径
-const activeIndex = ref(0); // 当前激活的索引
-const socketStore = socket(); // WebSocket状态管理实例
-
-/**
- * 表单数据对象,用于存储设备配置信息。
- */
-const formData = reactive({
-  //基础配置
-  basic_configs:{
-    "main_image_size": "",//主图尺寸
-    "image_out_format": "",//图片输出格式
-    "image_sharpening": "" //图片锐化
-  },
-  //拍照配置
-  take_photo_configs:{
-    "repart_take_photo_warning": false,//重复拍摄警告
-    "single_photo_warning": "",//单次张数警告
-    "total_photo_warning": "",//累计拍照警告
-    "camera_delay": ""//拍照停留
-  },
-  other_configs:{
-    "product_type": "",//产品类型
-    "cutout_mode": "",//默认抠图模式
-    "device_speed": "",//设备运动速度
-    "running_mode": "" //运行模式
-  },
-  captureOneFolder: '', // Capture One文件夹路径
-  mainImageSize: '', // 主图尺寸
-  imageFormat: '', // 图片格式
-  imageSharpening: '', // 图片锐化
-  repeatWarning: '', // 重复警告
-  singleWarning: '', // 单次警告
-  totalWarning: '', // 总警告
-  focusTime: '', // 对焦时间
-  photoTime: '', // 拍照时间
-  productType: '', // 产品类型
-  defaultCutoutMode: '', // 默认抠图模式
-  deviceSpeed: '', // 设备速度
-  runMode: '', // 运行模式
-  receiver: '', // 接收器类型
-  left: '', // 左脚配置
-  right: '', // 右脚配置
-  up: '', // 上移配置
-  down: '', // 下移配置
-});
-
-// 配置选项列表
-const mainImageSizeList = ref([
-  { label: '800', value: '800' },
-  { label: '1024', value: '1024' },
-  { label: '1200', value: '1200' },
-  { label: '1600', value: '1600' },
-]);
-const imageFormatList = ref([
-  { label: 'jpg', value: 'jpg' },
-  { label: 'png', value: 'png' },
-  { label: 'jpeg', value: 'jpeg' },
-]);
-const imageSharpeningList = ref([
-  { label: '0', value: '0' },
-  { label: '1', value: '1' },
-  { label: '2', value: '2' },
-  { label: '3', value: '3' },
-]);
-const repeatWarningList = ref([
-  { label: '关闭', value: false },
-  { label: '开启', value: true },
-]);
-const singleWarningList = ref([
-  { label: '11', value: '11' },
-  { label: '12', value: '12' },
-  { label: '13', value: '13' },
-]);
-const totalWarningList = ref([
-  { label: '1.0', value: '1.0' },
-  { label: '1.5', value: '1.5' },
-  { label: '2.0', value: '2.0' },
-]);
-const productTypeList = ref([
-  { label: '鞋类', value: '鞋类' },
-  { label: '服装', value: '服装' },
-  { label: '箱包', value: '箱包' },
-]);
-const defaultCutoutModeList = ref([
-  { label: '普通抠图', value: '普通抠图' },
-  { label: '精细化抠图', value: '精细化抠图' },
-]);
-const deviceSpeedList = ref([
-  { label: '一档', value: '1' },
-  { label: '二档', value: '2' },
-  { label: '三档', value: '3' },
-]);
-const runModeList = ref([
-  { label: '普通模式', value: '普通模式' },
-  { label: '待用户确认模式', value: '待用户确认模式' }
-]);
-const receiverList = ref([
-  { label: '蓝牙', value: '1' },
-  { label: '2.4G', value: '2' },
-  { label: '5.8G', value: '3' },
-]);
-const leftList = ref([
-  { label: '左脚', value: '1' },
-  { label: '右脚', value: '2' },
-  { label: '左右脚', value: '3' },
-]);
-const rightList = ref([
-  { label: '左脚', value: '1' },
-  { label: '右脚', value: '2' },
-  { label: '左右脚', value: '3' },
-]);
-const upList = ref([
-  { label: '上移', value: '1' },
-  { label: '下移', value: '2' },
-  { label: '左右移', value: '3' },
-]);
-const downList = ref([
-  { label: '上移', value: '1' },
-  { label: '下移', value: '2' },
-  { label: '左右移', value: '3' },
-]);
-
-// 表格数据和对话框状态
-const lefttableData = ref([]); // 左脚配置表格数据
-const righttableData = ref([]); // 右脚配置表格数据
-const dialogVisible = ref(false); // 编辑对话框可见状态
-const editTitle = ref(''); // 编辑对话框标题
-const editRowData = ref({}); // 当前编辑行的数据
-const activeTab = ref('left'); // 当前激活的标签页
-const editIndex = ref(0); // 当前编辑行的索引
-const isDefault = ref(false); // 是否为默认配置
-
-const indexKey  ={
-  0:"basic_configs",
-  1:"take_photo_configs",
-  2:"other_configs",
-}
-
-/**
- * 监听路由参数变化,更新activeIndex和activeTab。
- */
-watch(() => route.query.type, async (newType,oldType) => {
-
-  if(['0','1','2'].includes(oldType)){
-    await  saveSetting(oldType)
-  }
-  const typeValue = parseInt(newType) || 0;
-  switch (typeValue) {
-      case 4:
-        activeTab.value = 'left';
-        clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
-        clientStore.ipc.send(icpList.setting.getDeviceConfigList, {
-          mode_type: '执行左脚程序'
-        });
-        clientStore.ipc.on(icpList.setting.getDeviceConfigList, (event, result) => {
-          if (result?.data?.list) {
-            lefttableData.value = result.data.list;
-          }
-          clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
-        });
-        break;
-      default:
-        clientStore.ipc.removeAllListeners(icpList.setting.getSysConfig);
-        clientStore.ipc.send(icpList.setting.getSysConfig,{
-          key: indexKey[typeValue]
-        });
-        clientStore.ipc.on(icpList.setting.getSysConfig, (event, result) => {
-          if(result.code == 0 && result.data){
-            formData[indexKey[typeValue]] = result.data
-          }
-          console.log('icpList.setting.getSysConfig')
-          console.log(result)
-          clientStore.ipc.removeAllListeners(icpList.setting.getSysConfig);
-        });
-        break;
-    }
-}, { immediate: true });
-
-/**
- * 监听activeTab变化,获取对应标签页的设备配置列表。
- */
-watch(() => activeTab.value, (newTab) => {
-  if (newTab === 'left') {
-    clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
-    clientStore.ipc.send(icpList.setting.getDeviceConfigList, {
-      mode_type: '执行左脚程序'
-    });
-    clientStore.ipc.on(icpList.setting.getDeviceConfigList, (event, result) => {
-      if (result?.data?.list) {
-        lefttableData.value = result.data.list;
-      }
-      clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
-    });
-  } else {
-    clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
-    clientStore.ipc.send(icpList.setting.getDeviceConfigList, {
-      mode_type: '执行右脚程序'
-    });
-    clientStore.ipc.on(icpList.setting.getDeviceConfigList, (event, result) => {
-      if (result?.data?.list) {
-        righttableData.value = result.data.list;
-      }
-      clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
-    });
-  }
-});
-
-/**
- * 监听activeIndex变化,更新URL中的查询参数。
- */
-watch(() => activeIndex.value, (newIndex) => {
-  router.push({
-    query: {
-      ...route.query,
-      type: newIndex.toString()
-    }
-  });
-});
-
-/**
- * 组件挂载时初始化activeIndex。
- */
-onMounted(() => {
-  if (route.query.type) {
-    const typeValue = parseInt(route.query.type);
-    if (!isNaN(typeValue) && typeValue >= 0 && typeValue <= 3) {
-      activeIndex.value = typeValue;
-    }
-  }
-
-
-});
-
-/**
- * 打开文件夹选择对话框并更新表单数据。
- */
-const selectFolder = () => {
-  clientStore.ipc.removeAllListeners(icpList.utils.openDirectory);
-  clientStore.ipc.send(icpList.utils.openDirectory);
-  clientStore.ipc.on(icpList.utils.openDirectory, async (event, result) => {
-    formData.captureOneFolder = result;
-    clientStore.ipc.removeAllListeners(icpList.utils.openDirectory);
-  });
-};
-
-/**
- * 保存当前表单配置。
- */
-const saveSetting = async (index) => {
-
-  await new Promise((resolve, reject) => {
-
-    clientStore.ipc.removeAllListeners(icpList.setting.updateSysConfigs);
-    clientStore.ipc.send(icpList.setting.updateSysConfigs,{
-      key: indexKey[index],
-      value:JSON.stringify({
-        ...formData[indexKey[index]]
-      })
-    });
-    clientStore.ipc.on(icpList.setting.updateSysConfigs, async (event, result) => {
-      clientStore.ipc.removeAllListeners(icpList.setting.updateSysConfigs);
-      if(result.code === 0 && result.msg){
-        resolve(result)
-      }
-
-    });
-  });
-
-
-};
-
-/**
- * 新增一行配置。
- */
-const addRow = () => {
-  if (activeTab.value === 'left') {
-    editRowData.value = {
-      mode_type: '执行左脚程序',
-      action_name: '',
-      take_picture: false,
-      camera_height: 0,
-      camera_angle: 0,
-      turntable_position: 0,
-      turntable_angle: 0,
-      shoe_upturn: false,
-      led_switch: false,
-      number_focus: 0,
-      pre_delay: 0,
-      after_delay: 0,
-    };
-    dialogVisible.value = true;
-    editTitle.value = '新增步骤';
-  } else {
-    editRowData.value = {
-      mode_type: '执行右脚程序',
-      action_name: '',
-      take_picture: false,
-      camera_height: 0,
-      camera_angle: 0,
-      turntable_position: 0,
-      turntable_angle: 0,
-      shoe_upturn: false,
-      led_switch: false,
-      number_focus: 0,
-      pre_delay: 0,
-      after_delay: 0,
-    };
-    dialogVisible.value = true;
-    editTitle.value = '新增步骤';
-  }
-};
-
-/**
- * 获取设备配置列表。
- */
-const getList = () => {
-  clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
-  clientStore.ipc.send(icpList.setting.getDeviceConfigList, {
-    mode_type: activeTab.value === 'left' ? '执行左脚程序' : '执行右脚程序'
-  });
-  clientStore.ipc.on(icpList.setting.getDeviceConfigList, (event, result) => {
-    if (result.code == 0) {
-      if (activeTab.value === 'left') {
-        lefttableData.value = result.data.list;
-      } else {
-        righttableData.value = result.data.list;
-      }
-    } else {
-      ElMessage.error('获取列表失败');
-    }
-    clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
-  });
-};
-
-/**
- * 编辑指定行的配置。
- * @param {Object} row - 当前行的数据
- * @param {number} index - 当前行的索引
- */
-const editRow = (row, index) => {
-  dialogVisible.value = true;
-  editTitle.value = row.action_name + '编辑';
-  clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigDetail);
-  clientStore.ipc.send(icpList.setting.getDeviceConfigDetail, {
-    id: row.id
-  });
-  clientStore.ipc.on(icpList.setting.getDeviceConfigDetail, (event, result) => {
-    editRowData.value = result.data;
-    clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigDetail);
-  });
-  editIndex.value = index;
-};
-
-/**
- * 删除指定行的配置。
- * @param {Object} row - 当前行的数据
- * @param {number} index - 当前行的索引
- */
-const deleteRow = (row, index) => {
-  ElMessageBox.confirm('确定删除该步骤吗?', '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-    type: 'warning'
-  }).then(() => {
-    clientStore.ipc.send(icpList.setting.removeDeviceConfig, {
-      id: row.id
-    });
-    clientStore.ipc.on(icpList.setting.removeDeviceConfig, (event, result) => {
-      if (result.code == 0) {
-        getList();
-        ElMessage.success('删除成功');
-      } else {
-        ElMessage.error('删除失败');
-      }
-      clientStore.ipc.removeAllListeners(icpList.setting.removeDeviceConfig);
-    });
-  });
-};
-
-/**
- * 保存当前编辑的配置。
- */
-const saveRow = () => {
-  clientStore.ipc.send(icpList.setting.saveDeviceConfig, {
-    ...editRowData.value
-  });
-  clientStore.ipc.on(icpList.setting.saveDeviceConfig, (event, result) => {
-    if (result.code == 0) {
-      getList();
-      ElMessage.success('保存成功');
-      dialogVisible.value = false;
-      clientStore.ipc.removeAllListeners(icpList.setting.saveDeviceConfig);
-    } else {
-      ElMessage.error('保存失败');
-    }
-  });
-};
-
-/**
- * 重置设备配置。
- */
-const resetConfig = () => {
-  ElMessageBox.confirm(`确定初始化执行${activeTab.value === 'left' ? '左脚' : '右脚'}程序吗?`, '提示', {
-    confirmButtonText: '确定',
-    cancelButtonText: '取消',
-    type: 'warning'
-  }).then(() => {
-    clientStore.ipc.send(icpList.setting.resetDeviceConfig, {
-      mode_type: activeTab.value === 'left' ? '执行左脚程序' : '执行右脚程序'
-    });
-    clientStore.ipc.on(icpList.setting.resetDeviceConfig, (event, result) => {
-      if (result.code == 0) {
-        getList();
-        ElMessage.success('重置成功');
-      } else {
-        ElMessage.error('重置失败');
-      }
-      clientStore.ipc.removeAllListeners(icpList.setting.resetDeviceConfig);
-    });
-  });
-};
-
-
-
-
-</script>
-
-<style lang="scss">
-.el-image-viewer__wrapper{
-  z-index: 9999 !important;
-}
-</style>
-<style lang="scss" scoped>
-body {
-  background: #EAECED;
-}
-.container {
-  margin: 0 auto;
-  height: calc(100vh - 30px);
-  background: #EAECED;
-  display: flex;
-  flex-direction: column;
-  justify-content: flex-start;
-}
-.settings-nav {
-  display: flex;
-  justify-content: center;
-  gap: 40px;
-  padding: 30px 0;
-  background: #EDEFF0;
-  border-bottom: 1px solid rgba(0,0,0,0.1);
-}
-.nav-item {
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  cursor: pointer;
-  color: #666;
-  transition: all 0.3s;
-  width: 100px;
-  justify-content: center;
-  height: 70px;
-  font-size: 14px;
-}
-.nav-item.active {
-    background: #DFE2E3;
-    border-radius: 10px;
-    color: #2957FF;
-}
-.nav-item i {
-  font-size: 24px;
-  margin-bottom: 8px;
-  width: 40px;
-  height: 40px;
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  background: #fff;
-  border-radius: 8px;
-}
-.form-container {
-  border-radius: 12px;
-  padding: 30px;
-  padding-top: 10px;
-  width: 800px;
-  margin: 0 auto;
-  height: 306px;
-}
-.form-item {
-  margin-bottom: 24px;
-  display: flex;
-  align-items: center;
-}
-.form-item label {
-  display: block;
-  min-width: 98px;
-  text-align: right;
-  font-size: 14px;
-  color: #1A1A1A;
-}
-.input-group {
-  display: flex;
-  gap: 12px;
-}
-.input-group input {
-  flex: 1;
-  border: 1px solid #e5e7eb;
-  border-radius: 4px;
-  padding: 8px 12px;
-  font-size: 14px;
-}
-.select-wrapper {
-  position: relative;
-  width: 200px;
-  :deep(.el-input__inner){
-    border-radius: 6px;
-  }
-}
-.error-text {
-  color: #dc2626;
-  font-size: 12px;
-  margin-top: 4px;
-}
-.from-primary{
-    width: 150px;
-    height: 40px;
-    color: #FFFFFF;
-    background: linear-gradient( 135deg, #2FB0FF 0%, #B863FB 100%);
-}
-.nav-icon{
-    width: 32px;
-    height: 32px;
-}
-.captureBox{
-  border-bottom: 1px solid rgba(0,0,0,0.1);
-}
-.selectBox{
-    padding-top: 30px;
-    padding-left: 100px;
-    border-bottom: 1px solid rgba(0,0,0,0.1);
-    :deep(.el-tabs__header){
-      padding-left: 0;
-    }
-    :deep(.el-tabs--card>.el-tabs__header){
-      border-bottom: 1px solid #CCCCCC;
-    }
-    :deep(.el-tabs__item){
-      height: 30px;
-      line-height: 30px;
-    }
-    :deep(.el-tabs__nav-wrap){
-      margin-bottom: 0;
-    }
-    :deep(.el-tabs__item.is-active){
-      color: #333;
-      font-weight: bold;
-      background: #fff;
-    }
-}
-.select-btn{
-  display: flex;
-  align-items: center;
-  flex-shrink: 0;
-  width: 120px;
-  height: 30px;
-  background: #DFE2E3;
-  border-radius: 6px;
-  justify-content: center;
-  font-size: 14px;
-  color: #2957FF;
-  gap: 5px;
-  img{
-    width: 16px;
-    height: 16px;
-  }
-}
-.mt-8{
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  padding: 30px 0;
-  height: 100px;
-}
-.form-table{
-  margin-top: 10px;
-  .btnBox{
-    display: flex;
-    align-items: center;
-    margin-bottom: 12px;
-  }
-  :deep(.el-table .el-table__header){
-    padding: 0;
-    height: 30px;
-    .el-table__cell{
-      background: #F1F4FF;
-    }
-  }
-  :deep(.el-table .el-table__cell){
-    padding: 0;
-    text-align: center;
-  }
-  :deep(.el-table__row) {
-    height: 30px;
-    padding: 0;
-    &:nth-child(even) {
-      background: #F1F4FF;
-    }
-    &:nth-child(odd) {
-      background: #FFFFFF;
-    }
-  }
-  .primary-btn{
-    width: 80px;
-    height: 30px;
-    background: linear-gradient( 135deg, #2FB0FF 0%, #B863FB 100%);
-    border-radius: 4px;
-    color: #fff;
-    font-size: 14px;
-    text-align: center;
-    cursor: pointer;
-    line-height: 30px;
-    margin-right: 10px;
-  }
-  .normal-btn{
-    width: 80px;
-    height: 30px;
-    background: #fff;
-    border: 1px solid #CCCCCC;
-    border-radius: 4px;
-    font-size: 14px;
-    text-align: center;
-    line-height: 30px;
-    cursor: pointer;
-  }
-  .cursor-pointer{
-    cursor: pointer;
-  }
-}
-.editDialog{
-  .el-dialog__body{
-    padding: 0 !important;
-  }
-  .btn-row{
-    display: flex;
-    align-items: center;
-    justify-content: flex-end;
-    gap: 10px;
-  }
-  .primary-btn{
-    width: 100px;
-    height: 30px;
-    background: linear-gradient( 135deg, #2FB0FF 0%, #B863FB 100%);
-    border-radius: 4px;
-    color: #fff;
-    font-size: 14px;
-    text-align: center;
-    line-height: 30px;
-    cursor: pointer;
-  }
-  .normal-btn{
-    width: 100px;
-    height: 30px;
-    background: #fff;
-    border: 1px solid #CCCCCC;
-    border-radius: 4px;
-    font-size: 14px;
-    text-align: center;
-    line-height: 30px;
-    cursor: pointer;
-  }
-}
-  .config-type{
-    font-size: 14px;
-    color: #333333;
-    display: flex;
-    align-items: center;
-    justify-content: flex-start;
-    padding: 10px 0;
-    .el-checkbox{
-      margin-left: 10px;
-    }
-  }
-.editForm{
-  display: grid;
-  grid-template-columns: repeat(2, 1fr);
-  gap: 0;
-  .flex-row{
-    display: flex;
-    align-items: center;
-    :deep(.el-radio){
-      margin-right: 6px !important;
-    }
-    :deep(.el-radio__label){
-      padding-left: 4px;
-    }
-  }
-  :deep(.el-form-item) {
-    margin-bottom: 0;
-    .el-form-item__label {
-      width: 120px !important;
-      padding-right: 0 !important;
-        background: #FBFCFF;
-        border: 1px solid #EEEEEE;
-        height: 41px;
-        line-height: 41px;
-        padding-left: 5px;
-        text-align: left;
-      }
-      .el-form-item__content {
-        width: 190px;
-        position: relative;
-        height: 41px;
-        background: #FFFFFF;
-        padding-left: 7px;
-        border: 1px solid #EEEEEE;
-        .el-input__wrapper {
-          box-shadow: none;
-        }
-        .error-msg{
-          display: none;
-          position: absolute;
-          top: 41px;
-          top: 28px;
-          left: 8px;
-          z-index: 22;
-          color: #dc2626;
-          font-size: 12px;
-        }
-        &:hover{
-          .error-msg{
-            display: block;
-          }
-        }
-
-        // 确保number类型输入框的上下箭头始终显示
-        input[type="number"]::-webkit-inner-spin-button,
-        input[type="number"]::-webkit-outer-spin-button {
-          opacity: 1;
-          height: 28px;
-          position: absolute;
-          top: 2px;
-          right: 2px;
-          cursor: pointer;
-        }
-
-        input[type="number"] {
-          -moz-appearance: number-input; /* Firefox */
-        }
-      }
-    }
-}
-</style>

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "ZhiHuiYin",
-  "version": "1.2.0",
+  "version": "1.2.5",
   "description": "1、支持SmartShooter5软件控制相机",
   "main": "main.js",
   "scripts": {

+ 2 - 2
public/dist/index.html

@@ -5,8 +5,8 @@
     <link rel="icon" type="image/svg+xml" href="./vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>智惠映AI自动拍照机</title>
-    <script type="module" crossorigin src="./assets/index-Dx7tl9Wa.js"></script>
-    <link rel="stylesheet" crossorigin href="./assets/index-CWbjMgyJ.css">
+    <script type="module" crossorigin src="./assets/index-B2uDJGOa.js"></script>
+    <link rel="stylesheet" crossorigin href="./assets/index-BPSCl2ER.css">
   </head>
   <body>
     <div id="app"></div>