Pārlūkot izejas kodu

Merge remote-tracking branch 'origin/dev-frontend_v132' into feature-frontend

# Conflicts:
#	frontend/src/router/index.ts
#	public/dist/index.html
panqiuyao 1 nedēļu atpakaļ
vecāks
revīzija
8bf6a7fbec
100 mainītis faili ar 8301 papildinājumiem un 1770 dzēšanām
  1. 2 1
      .gitignore
  2. 86 8
      electron/api/camera.js
  3. 2 1
      electron/api/request.js
  4. 23 0
      electron/api/setting.js
  5. 0 3
      electron/config/app.config.json
  6. 6 0
      electron/config/builder.json
  7. 4 2
      electron/config/config.prod.js
  8. 66 136
      electron/controller/camera.js
  9. 184 0
      electron/controller/ota.js
  10. 39 1
      electron/controller/setting.js
  11. 7 91
      electron/controller/socket.js
  12. 45 6
      electron/controller/takephoto.js
  13. 230 8
      electron/controller/utils.js
  14. 1 1
      electron/preload/bridge.js
  15. 151 0
      electron/utils/camera.js
  16. 10 0
      electron/utils/config.default.json
  17. 78 0
      electron/utils/config.js
  18. 361 0
      electron/utils/socket.js
  19. 1 1
      frontend/.env.development
  20. 1 1
      frontend/.env.production
  21. 1 1
      frontend/src/App.vue
  22. 117 0
      frontend/src/apis/log.ts
  23. 10 0
      frontend/src/apis/other.ts
  24. 239 0
      frontend/src/apis/setting.ts
  25. BIN
      frontend/src/assets/images/Photography/cj.png
  26. BIN
      frontend/src/assets/images/Photography/mt.png
  27. BIN
      frontend/src/assets/images/Photography/xq.png
  28. 10 0
      frontend/src/assets/images/detail/bdt.svg
  29. 11 0
      frontend/src/assets/images/detail/cjt.svg
  30. 11 0
      frontend/src/assets/images/detail/cjt_h.svg
  31. 10 0
      frontend/src/assets/images/detail/cptc.svg
  32. BIN
      frontend/src/assets/images/detail/excel.png
  33. BIN
      frontend/src/assets/images/detail/excel_h.png
  34. BIN
      frontend/src/assets/images/detail/file-excel.png
  35. BIN
      frontend/src/assets/images/detail/logo.png
  36. 10 0
      frontend/src/assets/images/detail/mtt.svg
  37. 10 0
      frontend/src/assets/images/detail/mtt_h.svg
  38. BIN
      frontend/src/assets/images/detail/sctp.png
  39. 10 0
      frontend/src/assets/images/detail/xqmb.svg
  40. 11 0
      frontend/src/assets/images/detail/xqy.svg
  41. 11 0
      frontend/src/assets/images/detail/xqy_h.svg
  42. BIN
      frontend/src/assets/images/detail/xtdj.png
  43. BIN
      frontend/src/assets/images/detail/xtdj_h.png
  44. BIN
      frontend/src/assets/images/home/left.jpg
  45. BIN
      frontend/src/assets/images/home/left.png
  46. BIN
      frontend/src/assets/images/home/right.jpg
  47. BIN
      frontend/src/assets/images/home/right.png
  48. BIN
      frontend/src/assets/images/processImage.vue/go.png
  49. BIN
      frontend/src/assets/images/processImage.vue/riq.png
  50. BIN
      frontend/src/assets/images/processImage.vue/sc.png
  51. BIN
      frontend/src/assets/images/processImage.vue/tup.png
  52. 551 0
      frontend/src/components/ModelGeneration/index.vue
  53. 280 0
      frontend/src/components/ScenePromptDialog/index.vue
  54. 57 0
      frontend/src/components/UpdateDialog/index.vue
  55. 1 1
      frontend/src/components/check/index.vue
  56. 1 0
      frontend/src/components/header-bar/assets/gengxin.svg
  57. 1 0
      frontend/src/components/header-bar/assets/qiehuan.svg
  58. 166 0
      frontend/src/components/header-bar/blue-header.vue
  59. 272 62
      frontend/src/components/header-bar/index.vue
  60. 91 45
      frontend/src/components/login/index.vue
  61. 4 1
      frontend/src/composables/userCheck.ts
  62. 14 0
      frontend/src/config.json
  63. 17 0
      frontend/src/main.ts
  64. 12 0
      frontend/src/router/index.ts
  65. 14 0
      frontend/src/router/module/ota.ts
  66. 5 0
      frontend/src/router/plugins/authGuard.ts
  67. 6 7
      frontend/src/stores/modules/check.ts
  68. 27 1
      frontend/src/stores/modules/config.ts
  69. 5 1
      frontend/src/stores/modules/token.ts
  70. 47 9
      frontend/src/stores/modules/user.ts
  71. 32 0
      frontend/src/stores/modules/uuid.ts
  72. 10 3
      frontend/src/styles/pub.scss
  73. 4 0
      frontend/src/utils/appconfig.ts
  74. 29 0
      frontend/src/utils/appfun.ts
  75. 82 14
      frontend/src/utils/http.ts
  76. 10 2
      frontend/src/utils/ipc.ts
  77. 171 0
      frontend/src/utils/log.ts
  78. 84 0
      frontend/src/utils/menus/generate.ts
  79. 1 1
      frontend/src/views/Developer/cmd.vue
  80. 3 3
      frontend/src/views/Developer/index.vue
  81. 2 2
      frontend/src/views/Developer/mcu.vue
  82. 6 0
      frontend/src/views/Developer/normal.vue
  83. 272 38
      frontend/src/views/Home/index.vue
  84. 203 0
      frontend/src/views/OTA/index.vue
  85. 101 28
      frontend/src/views/Photography/check.vue
  86. 68 23
      frontend/src/views/Photography/components/LoadingDialog.vue
  87. 347 0
      frontend/src/views/Photography/components/ProgressSteps.vue
  88. 56 49
      frontend/src/views/Photography/components/editRow.vue
  89. 711 185
      frontend/src/views/Photography/detail.vue
  90. 9 0
      frontend/src/views/Photography/mixin/generate.vue
  91. 860 0
      frontend/src/views/Photography/mixin/usePhotography.ts
  92. 603 0
      frontend/src/views/Photography/processImage.vue
  93. 6 6
      frontend/src/views/Photography/seniorDetail.vue
  94. 117 772
      frontend/src/views/Photography/shot.vue
  95. 25 2
      frontend/src/views/RemoteControl/index.vue
  96. 193 0
      frontend/src/views/Setting/components/CameraConfig.vue
  97. 53 0
      frontend/src/views/Setting/components/DebugPanel.vue
  98. 443 121
      frontend/src/views/Setting/components/action_config.vue
  99. 106 0
      frontend/src/views/Setting/components/otherConfig.vue
  100. 365 133
      frontend/src/views/Setting/index.vue

+ 2 - 1
.gitignore

@@ -1,5 +1,6 @@
 node_modules
 out/
+output/
 logs/
 run/
 .idea/
@@ -12,4 +13,4 @@ __pycache__
 *.pyc
 build/*
 **/dist/
-.DS_Store
+.DS_Store

+ 86 - 8
electron/api/camera.js

@@ -2,6 +2,7 @@ const axios = require('axios');
 const http = require('http');
 const { net } = require('electron');
 const { post } = require('./request')
+const { windowManager } = require('node-window-manager');
 //
 const baseURL = 'http://localhost:5513/';
 // 创建 Axios 实例
@@ -52,17 +53,53 @@ async function fetchExampleData(url) {
 }
 
 
+const socket = require('../utils/socket')
+const pySocket = new socket()
+const { readConfigFile } = require('../utils/config');
+
 
 module.exports = {
-  liveShow(){
-    return get({
-      url: '?CMD=LiveViewWnd_Show'
-    })
+  async liveShow(){
+    if(readConfigFile().controlType === 'digiCamControl'){
+      return get({
+        url: '?CMD=LiveViewWnd_Show'
+      })
+    }else{
+
+      await pySocket.sendMessage(JSON.stringify({
+        type: 'smart_shooter_enable_preview',
+        data:{
+          value:true
+        }
+      }))
+      return  new Promise(async (resolve, reject) => {
+        pySocket.onSocketMessage('smart_shooter_enable_preview',(message)=>{
+
+          resolve(message)
+        })
+      })
+    }
   },
-  liveHide(){
-    return get({
-      url: '?CMD=LiveViewWnd_Hide'
-    })
+  async liveHide(){
+    if(readConfigFile().controlType === 'digiCamControl'){
+      return get({
+        url: '?CMD=LiveViewWnd_Hide'
+      })
+    }else{
+
+      await pySocket.sendMessage(JSON.stringify({
+        type: 'smart_shooter_enable_preview',
+        data:{
+          value:false
+        }
+      }))
+      return  new Promise(async (resolve, reject) => {
+        pySocket.onSocketMessage('smart_shooter_enable_preview',(message)=>{
+
+          resolve(message)
+        })
+      })
+    }
   },
   captureLive(){
     return get({
@@ -95,6 +132,47 @@ module.exports = {
       url: '/close_other_window',
     })
   },
+  async minimizeSmartShooter(){
+    try {
+      // 获取所有窗口
+      const windows = windowManager.getWindows();
+      
+      // 查找SmartShooter窗口
+      const smartShooterWindow = windows.find(window => {
+        const title = window.getTitle().toLowerCase();
+        return title.includes('smartshooter') || title.includes('smart shooter');
+      });
+      
+      if (smartShooterWindow) {
+        // 最小化窗口
+        smartShooterWindow.minimize();
+        console.log('SmartShooter窗口已最小化');
+        return { success: true, message: 'SmartShooter窗口已最小化' };
+      } else {
+        console.log('未找到SmartShooter窗口');
+        return { success: false, message: '未找到SmartShooter窗口' };
+      }
+    } catch (error) {
+      console.error('最小化SmartShooter失败:', error);
+      return { success: false, message: '最小化失败: ' + error.message };
+    }
+  },
+  async checkCamera(){
+    if(readConfigFile().controlType === 'digiCamControl'){
+      return  fetchExampleData(`?slc=get&param1=iso`)
+    }else {
+
+      await pySocket.sendMessage(JSON.stringify({
+        type: 'smart_shooter_getinfo',
+        data:{}
+      }))
+     return  new Promise(async (resolve, reject) => {
+        pySocket.onSocketMessage('smart_shooter_getinfo',(message)=>{
+          resolve(message)
+        })
+      })
+    }
+  }
 }
 
 

+ 2 - 1
electron/api/request.js

@@ -1,5 +1,6 @@
 const axios = require('axios')
-const { pyapp } = require('../config/app.config.json')
+const { readConfigFile } = require('../utils/config');
+const pyapp = readConfigFile().pyapp
 
 
 /* axios.defaults.withCredentials = true*/

+ 23 - 0
electron/api/setting.js

@@ -108,4 +108,27 @@ module.exports = {
 
 
 
+
+  //同步配置接口
+  syncSysConfigs(data){
+    console.log("syncSysConfigs===============", data);
+    return post({
+      url: '/sync_sys_configs',
+      data: data
+    })
+  },
+
+
+
+  //同步左右脚配置
+  syncActions(data){
+    console.log("syncActions===============", data);
+    return post({
+      url: '/sync_actions',
+      data: data
+    })
+  },
+
+
+
 }

+ 0 - 3
electron/config/app.config.json

@@ -1,3 +0,0 @@
-{
-  "pyapp": "127.0.0.1"
-}

+ 6 - 0
electron/config/builder.json

@@ -20,6 +20,12 @@
     "from": "build/extraResources/",
     "to": "extraResources"
   },
+  "extraFiles": [
+    {
+      "from": "build/extraResources/智慧映拍照机辅助工具箱.exe",
+      "to": "."
+    }
+  ],
   "nsis": {
     "oneClick": false,
     "allowElevation": true,

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

@@ -1,5 +1,7 @@
 'use strict';
 
+const { readConfigFile } = require('../utils/config');
+const configDeault = readConfigFile();
 /**
  * 生产环境配置,覆盖 config.default.js
  */
@@ -9,7 +11,7 @@ module.exports = (appInfo) => {
   /**
    * 开发者工具
    */
-  config.openDevTools = false;
+  config.openDevTools = configDeault.debug;
 
   /**
    * 应用程序顶部菜单
@@ -26,7 +28,7 @@ module.exports = (appInfo) => {
    * 远程模式-web地址
    */
   config.remoteUrl = {
-    enable: false,
+    enable: configDeault.remoteUrl || false,
     url: 'http://localhost:3000/#/home'
   };
 

+ 66 - 136
electron/controller/camera.js

@@ -1,158 +1,86 @@
 'use strict';
-
-const path = require('path');
-const fs = require('fs');
 const { Controller } = require('ee-core');
-const { spawn } = require('child_process');
-const { liveShow, liveHide, setParams, capture, getParams,CMD,captureLive,closeOtherWindow } = require('../api/camera');
-
-const { dialog } = require('electron'); // 引入 electron 的 dialog 模块
-const { windowManager } = require('node-window-manager');
-const CoreWindow = require("ee-core/electron/window");
-
-async function checkCameraControlCmdExists(digiCamControlPath) {
-  try {
-
-    // 拼接 CameraControlCmd.exe 的完整路径
-    const exePath = path.join(digiCamControlPath, 'CameraControl.exe');
-
-    // 检查文件是否存在
-    const exists = await fs.promises.access(exePath, fs.constants.F_OK)
-      .then(() => true)
-      .catch(() => false);
-
-    if (!exists) {
-      // 弹出文件夹选择对话框
-      const { canceled, filePaths } = await dialog.showOpenDialog({
-        title: '选择 digiCamControl 文件夹',
-        properties: ['openDirectory']
-      });
-
-      if (!canceled && filePaths.length > 0) {
-        const selectedPath = filePaths[0];
-        // 更新 app.config.json 中的 digiCamControl 值
-       // config.digiCamControl = selectedPath;
-      //  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
-        digiCamControlPath = selectedPath
-        const win = CoreWindow.getMainWindow()
-        win.webContents.send('controller.camera.digiCamControlPath', {
-          code:0,
-          data:selectedPath,
-        });
-      //  return true; // 重新检查文件是否存在
-      } else {
-        console.error('用户未选择文件夹');
-        return {
-          status:-1,
-          msg:"无法找到或运行 CameraControlCmd.exe",
-        }
-      }
-    }
-    const res = await openCameraControlCmd(digiCamControlPath);
-    return res;
-  } catch (error) {
-    console.error('检查 CameraControlCmd.exe 是否存在时出错:', error);
-    throw error;
-  }
-}
-
-
-async function openCameraControlCmd(digiCamControlPath) {
- return  new Promise(async (resolve, reject) => {
-    try {
-      // 获取 digiCamControl 文件夹路径
-
-      // 拼接 CameraControlCmd.exe 的完整路径
-      const exePath = path.join(digiCamControlPath, 'CameraControl.exe');
-
-      // 检查文件是否存在
-      await fs.promises.access(exePath, fs.constants.F_OK);
-      try {
-
-          const child = spawn(exePath);
+const Log = require('ee-core/log');
+const { checkCameraControlCmdExists, closeCameraControlTips} = require('../utils/camera');
+const {
+  checkCamera,
+  liveShow, liveHide, setParams, capture, getParams,CMD,captureLive,closeOtherWindow } = require('../api/camera');
 
-          child.stdout.on('data', (data) => {
+const { readConfigFile } = require('../utils/config');
 
-            resolve(true)
-          });
-
-          child.on('close', (code) => {
-            if (code === 0) {
-              reject(false)
-            }
-          });
-
-
-
-      } catch (error) {
-        console.error('error CameraControlCmd.exe:', error);
-        throw error;
-      }
-
-      console.log('CameraControlCmd.exe start');
-    } catch (error) {
-      console.error('无法找到或运行 CameraControlCmd.exe:', error);
-      throw error;
-    }
-  })
-}
-
-async function closeCameraControlTips() {
-  try {
-    await  closeOtherWindow()
-/*    const windows = windowManager.getWindows();
-
-    for (const window of windows) {
-      const title = window.getTitle();
-
-      if (title.indexOf('digiCamControl')>=0) {
-        console.log(title);
-        console.log(window);
-        window.minimize(); // 关闭窗口
-        //   window.hide()
-      //  break;
-      }
-    }*/
-  }catch (e) {
-    console.log(e)
-  }
-
-
-}
 let  isOPen = true
 class CameraController extends Controller {
   constructor(ctx) {
     super(ctx);
   }
 
-  async connect(digiCamControlPath) {
+  async connect() {
     try {
-      await getParams('iso').catch(e=>{
-        isOPen = false;
-      })
-      if(!isOPen){
-        await checkCameraControlCmdExists(digiCamControlPath)
-        await  CMD('All_Minimize')
-        await closeCameraControlTips()
-        isOPen = true
-      }
+      console.log('==================');
+      console.log(readConfigFile());
+      if(readConfigFile().controlType === 'digiCamControl'){
+
+        console.log('========1==========');
+        await getParams('iso').catch(e=>{
+          isOPen = false;
+        })
+        if(!isOPen){
+          await checkCameraControlCmdExists()
+          await  CMD('All_Minimize')
+          await closeCameraControlTips()
+          isOPen = true
+        }
+        const res = await getParams('iso')
+        if(res  === '未将对象引用设置到对象的实例。'){
+          return {
+            status:-1,
+            msg:"相机未连接,请链接相机。",
+          }
+        }
+        return {
+          status:2,
+          msg:res,
+        }
+
+
+      }else{
+        let res = await checkCamera().catch(e=>{
+          isOPen = false;
+        })
+
+        if(res?.device_status  === -1 ){
+          isOPen = false;
+          await checkCameraControlCmdExists()
+          isOPen = true
+          await new Promise(resolve => setTimeout(resolve, 10000)); // 等待5秒
+          res = await checkCamera()
+        }
+
+        if(res?.device_status  === 2){
+          isOPen = true;
+          return  {
+            ...res,
+            status:2
+          };
+        }
+
 
-      const res = await getParams('iso')
-      if(res  === '未将对象引用设置到对象的实例。'){
         return {
           status:-1,
           msg:"相机未连接,请链接相机。",
         }
+
+
       }
-      return {
-        status:2,
-        msg:res,
-      }
-      return true;
+
+
     } catch (error) {
+
+      let msg = '请安装digiCamControl软件,并打开digiCamControl软件的web服务,端口为5513'
+      if(readConfigFile().controlType === 'SmartShooter') msg = '请安装SmartShooter5软件'
       return {
         status:-1,
-        msg:"请安装digiCamControl软件,并打开digiCamControl软件的web服务,端口为5513",
+        msg,
       }
     }
   }
@@ -163,7 +91,9 @@ class CameraController extends Controller {
   async liveShow() {
     try {
       await liveShow();
-      await  CMD('All_Minimize')
+      if(readConfigFile().controlType === 'digiCamControl'){
+        await  CMD('All_Minimize')
+      }
       return true;
     } catch (error) {
       console.error('eeee启动直播失败:', error);

+ 184 - 0
electron/controller/ota.js

@@ -0,0 +1,184 @@
+'use strict';
+
+const Addon = require('ee-core/addon');
+const { Controller } = require('ee-core');
+const config = require('../config/config.default');
+const path = require('path');
+const fs = require('fs');
+const { ipcMain, app, BrowserWindow, shell, dialog } = require('electron');
+const { session } = require('electron');
+const CoreWindow = require("ee-core/electron/window");
+const Log = require('ee-core/log');
+const { spawn } = require('child_process');
+/**
+ * example
+ * @class
+ */
+class OTAController extends Controller {
+
+  constructor(ctx) {
+    super(ctx);
+  }
+
+  async updateVersion(url) {
+    const status = {
+      error: -1,
+      available: 1,
+      noAvailable: 2,
+      downloading: 3,
+      downloaded: 4,
+    };
+
+    const win = new BrowserWindow({ show: false });
+
+
+    // 设置下载路径为系统临时目录
+
+    win.webContents.downloadURL(url);
+
+    win.webContents.session.removeAllListeners('will-download');
+
+    win.webContents.session.on('will-download', (event, item, webContents) => {
+      // event.preventDefault();
+      // 设置默认下载路径为系统下载目录
+      const fileName = item.getFilename();
+      const downloadPath = path.join(app.getPath('downloads'), fileName);
+      console.log('下载路径:', downloadPath);
+
+      // 确保目录存在
+      const dir = path.dirname(downloadPath);
+      if (!fs.existsSync(dir)) {
+        fs.mkdirSync(dir, { recursive: true });
+      }
+      if (fs.existsSync(downloadPath)) {
+        fs.unlinkSync(downloadPath); // 删除旧文件
+      }
+      console.log(item);
+      item.setSavePath(downloadPath);
+
+
+      item.on('updated', (event, state) => {
+
+        Log.info('[addon:updated] 状态: ', state);
+        if (state === 'interrupted') {
+          Log.error('[addon:autoUpdater] 下载中断');
+        } else if (state === 'progressing') {
+          const receivedBytes = item.getReceivedBytes();
+          const totalBytes = item.getTotalBytes();
+          const percentNumber = Math.floor((receivedBytes / totalBytes) * 100);
+          const transferredSize = this.bytesChange(receivedBytes);
+          const totalSize = this.bytesChange(totalBytes);
+          const text = `已下载 ${percentNumber}% (${transferredSize}/${totalSize})`;
+
+          let info = {
+            status: status.downloading,
+            desc: text,
+            percentNumber: percentNumber,
+            totalSize: totalSize,
+            transferredSize: transferredSize
+          };
+          Log.info('[addon:updated] 下载进度: ', text);
+          this.sendStatusToWindow(info);
+        }
+      });
+
+      item.once('done', (event, state) => {
+
+        Log.info('[addon:done] 状态: ', state);
+
+        if (state === 'completed') {
+          Log.info('[addon:autoUpdater] 文件已下载完成: ', item.getSavePath());
+          let info = {
+            status: status.downloaded,
+            desc: '下载完成',
+            filePath: item.getSavePath()
+          };
+          this.sendStatusToWindow(info);
+
+          // 提醒用户选择操作
+          dialog.showMessageBox({
+            type: 'info',
+            title: '下载完成',
+            message: '文件已下载完成,请选择操作:',
+            buttons: ['关闭智惠映并自动安装', '打开目录手动安装', '取消']
+          }).then(result => {
+            if (result.response === 0) {
+              // 用户选择“立即安装”,执行安装操作
+              this.install(item.getSavePath());
+
+            } else if (result.response === 1) {
+              // 用户选择“打开目录”,打开文件所在目录
+              shell.openPath(path.dirname(item.getSavePath()));
+            }
+          });
+        } else {
+          Log.error('[addon:autoUpdater] 下载失败: ', state);
+          let info = {
+            status: status.error,
+            desc: `下载失败: ${state}`
+          };
+          this.sendStatusToWindow(info);
+        }
+
+        win.close(); // 关闭隐藏窗口
+      });
+    });
+  }
+
+  install(filePath) {
+
+    // 启动安装程序并脱离主进程
+    const child = spawn(filePath, [], {
+      detached: true,
+      stdio: 'ignore'
+    });
+
+    child.on('error', (err) => {
+      console.error('启动安装程序失败:', err);
+    });
+
+    // 让安装程序独立运行后,退出当前应用
+    child.unref();
+    app.quit();
+  }
+
+  /**
+   * 向前端发消息
+   */
+  sendStatusToWindow(content = {}) {
+    const textJson = JSON.stringify(content);
+    const channel = 'app.updater';
+    this.app.electron['ota'].webContents.send(channel, textJson);
+/*    const win = CoreWindow.getMainWindow();
+    win.webContents.send(channel, textJson);*/
+  }
+
+  /**
+   * 单位转换
+   */
+  bytesChange (limit) {
+    let size = "";
+    if(limit < 0.1 * 1024){
+      size = limit.toFixed(2) + "B";
+    }else if(limit < 0.1 * 1024 * 1024){
+      size = (limit/1024).toFixed(2) + "KB";
+    }else if(limit < 0.1 * 1024 * 1024 * 1024){
+      size = (limit/(1024 * 1024)).toFixed(2) + "MB";
+    }else{
+      size = (limit/(1024 * 1024 * 1024)).toFixed(2) + "GB";
+    }
+
+    let sizeStr = size + "";
+    let index = sizeStr.indexOf(".");
+    let dou = sizeStr.substring(index + 1 , index + 3);
+    if(dou == "00"){
+      return sizeStr.substring(0, index) + sizeStr.substring(index + 3, index + 5);
+    }
+
+    return size;
+  }
+
+}
+
+OTAController.toString = () => '[class OTAController]';
+module.exports = OTAController;

+ 39 - 1
electron/controller/setting.js

@@ -16,7 +16,9 @@ const {
   getSysConfig,
   getDeviceTabs,
   updateLeftRightConfig,
-  updateTabName
+  updateTabName,
+  syncSysConfigs,
+  syncActions
 } = require('../api/setting');
 
 const errData = {
@@ -77,6 +79,7 @@ class SettingController extends Controller {
       if(result.data)  return result.data
       return errData;
     } catch (error) {
+      Log.error('获取设备配置详情失败:', error);
       return errData;
     }
   }
@@ -182,6 +185,41 @@ class SettingController extends Controller {
       return errData;
     }
   }
+
+
+  /**
+   * 同步配置接口
+   * @param {Object} token
+   */
+  async syncSysConfigs(args) {
+    try {
+      const result = await syncSysConfigs(args);
+      if(result.data)  return result.data
+      return errData;
+    } catch (error) {
+      Log.error('同步配置接口:', error);
+      return errData;
+    }
+  }
+
+
+  /**
+   * 同步左右脚配置
+   * @param {Object} token
+   */
+  async syncActions(args) {
+    try {
+      const result = await syncActions(args);
+      if(result.data){
+        await syncSysConfigs(args);
+        return result.data
+      }
+      return errData;
+    } catch (error) {
+      Log.error('同步左右脚配置:', error);
+      return errData;
+    }
+  }
 }
 
 SettingController.toString = () => '[class SettingController]';

+ 7 - 91
electron/controller/socket.js

@@ -1,20 +1,10 @@
 'use strict';
 
 const { Controller } = require('ee-core');
-const Log = require('ee-core/log');
-const CoreWindow = require('ee-core/electron/window');
-const WebSocket = require('ws'); // 引入原生 ws 库
-let socket = null;
-const { pyapp } = require('../config/app.config.json')
+const socket = require('../utils/socket')
+const pySocket = new socket()
+
 
-const typeToMessage = {
-  run_mcu_single:['seeting','default'],
-  get_deviation_data:"developer",
-  set_deviation:"developer",
-  get_mcu_other_info:"developer",
-  set_mcu_other_info:"developer",
-  send_command:"developer"
-}
 class SocketController extends Controller {
   constructor(ctx) {
     super(ctx);
@@ -26,71 +16,7 @@ class SocketController extends Controller {
   async connect() {
 
      await new Promise(async (resolve,reject) => {
-
-       const win = CoreWindow.getMainWindow()
-       if(socket){
-         console.log('has socket ')
-         resolve(true);
-         win.webContents.send('controller.socket.connect_open', true);
-         return;
-       }
-
-      socket = new WebSocket('ws://'+pyapp+':7074/ws');
-
-      // 监听连接成功事件
-      socket.on('open', () => {
-        console.log('socket open')
-        resolve(true);
-        win.webContents.send('controller.socket.connect_open', true);
-      });
-
-      // 监听消息事件
-      socket.on('message', (data) => {
-        try {
-          let this_data = JSON.parse(data.toString());
-          console.log(this_data.msg_type);
-          console.log(this_data);
-          if(this_data.msg_type){
-            let channel = 'controller.socket.message_'+this_data.msg_type;
-            if(typeToMessage[this_data.msg_type]){
-              if(typeof typeToMessage[this_data.msg_type] === 'object'){
-
-                typeToMessage[this_data.msg_type].map(item=>{
-                  if(item === 'default'){
-                    win.webContents.send(channel, this_data);
-                  }else{
-                    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);
-              }
-            }else{
-              win.webContents.send(channel, this_data);
-            }
-          }
-        }catch (e){
-          console.log(e)
-        }
-      });
-
-      // 监听连接关闭事件
-      socket.on('close', () => {
-        console.log('socket close');
-        win.webContents.send('controller.socket.disconnect', null);
-        socket = null
-
-      });
-
-      // 监听错误事件
-      socket.on('error', (err) => {
-        console.log('socket error');
-        win.webContents.send('controller.socket.disconnect', null);
-        reject(true);
-
-      });
-
-      console.log('socket end')
+       pySocket.init(this.app)
 
     })
 
@@ -100,8 +26,7 @@ class SocketController extends Controller {
    * 发送 ping 消息
    */
   sendPing() {
-    const message = JSON.stringify({ data: 'node', type: 'ping' });
-    this.sendMessage(message);
+    pySocket.sendPing()
   }
 
   /**
@@ -109,23 +34,14 @@ class SocketController extends Controller {
    * @param {string} message - JSON 字符串
    */
   sendMessage(message) {
-    // 检查连接状态
-    console.log(message);
-    console.log(typeof socket);
-    if (socket?.readyState === WebSocket.OPEN) {
-      socket.send(message); // 使用 send() 发送
-    } else {
-    }
+    pySocket.sendMessage(message)
   }
 
   /**
    * 断开连接
    */
   disconnect() {
-    if (socket) {
-      socket.close(); // 使用 close() 方法
-      socket = null;
-    }
+    pySocket.disconnect()
   }
 }
 

+ 45 - 6
electron/controller/takephoto.js

@@ -1,4 +1,6 @@
 'use strict';
+const fs = require('fs');
+const Log = require('ee-core/log');
 const { Controller } = require('ee-core');
 const  { getPhotoRecords,delectGoodsArts,createMainImage,getLastPhotoRecord } =  require('../api/takephoto');
 const errData = {
@@ -40,7 +42,7 @@ class takePhotoController extends Controller {
       if(result.data)  return result.data
       return errData;
     } catch (error) {
-      console.log('getPhotoRecords error')
+      Log.error('获取照片记录失败:', error);
       return errData;
     }
   }
@@ -55,7 +57,7 @@ class takePhotoController extends Controller {
       return errData;
     } catch (error) {
       console.log('error')
-      console.log(error)
+      Log.error('删除商品货号失败:', error);
       return errData;
     }
   }
@@ -65,11 +67,49 @@ class takePhotoController extends Controller {
       const result = await createMainImage(params);
       console.log('result')
       console.log(result)
+      if(result.code === 0 && result.data?.main_out_path){
+        const filePath =  result.data?.main_out_path
+        const fileBuffer = fs.readFileSync(filePath);
+        const base64Image = fileBuffer.toString('base64');
+
+
+        // 获取文件扩展名
+        const extension = path.extname(filePath).toLowerCase().replace('.', '');
+
+        // 根据扩展名确定 MIME 类型
+        let mimeType = '';
+        switch (extension) {
+          case 'jpg':
+          case 'jpeg':
+            mimeType = 'image/jpeg';
+            break;
+          case 'png':
+            mimeType = 'image/png';
+            break;
+          case 'gif':
+            mimeType = 'image/gif';
+            break;
+          case 'webp':
+            mimeType = 'image/webp';
+            break;
+          case 'avif':
+            mimeType = 'image/avif';
+            break;
+          default:
+            mimeType = 'application/octet-stream'; // 默认 MIME 类型
+            break;
+        }
+
+        // 构建 data URL
+        const dataUrl = `data:${mimeType};base64,${base64Image}`;
+
+        result.data.main_out_path = dataUrl
+
+      }
       if(result.data)  return result.data
       return errData;
     } catch (error) {
-      console.log('error')
-      console.log(error)
+      Log.error('创建主图失败:', error);
       return errData;
     }
   }
@@ -83,8 +123,7 @@ class takePhotoController extends Controller {
       if(result.data)  return result.data
       return errData;
     } catch (error) {
-      console.log('error')
-      console.log(error)
+      Log.error('获取最新照片记录失败:', error);
       return errData;
     }
   }

+ 230 - 8
electron/controller/utils.js

@@ -7,11 +7,16 @@ const { dialog } = require('electron');
 const fs = require('fs');
 const path = require('path');
 const CoreWindow = require('ee-core/electron/window');
-const { BrowserWindow, Menu } = require('electron');
+const { BrowserWindow, Menu,app } = require('electron');
+const { spawn } = require('child_process');
+
+const { readConfigFile } = require('../utils/config');
+const configDeault = readConfigFile();
 const errData = {
   msg :'请求失败,请联系管理员',
   code:999
 }
+const sharp = require('sharp'); // 确保安装:npm install sharp
 
 
 /**
@@ -24,6 +29,102 @@ class UtilsController extends Controller {
     super(ctx);
   }
 
+  /**
+   * 运行外部工具(如exe)
+   * @param {{exeName?: string, exePath?: string, args?: string[]}} params
+   */
+  async runExternalTool (params = {}) {
+    try {
+      const { exeName, exePath, args = [] } = params;
+      const targetName = exePath || exeName;
+
+      if (!targetName) {
+        throw new Error('缺少可执行文件名称');
+      }
+
+      const isPackaged = app.isPackaged;
+      const execDir = isPackaged ? path.dirname(app.getPath('exe')) : path.join(app.getAppPath(), '.');
+      const resourcesDir = process.resourcesPath;
+      const candidates = [];
+
+      const pushCandidate = (candidatePath, isAbsolute = false) => {
+        if (!candidatePath) return;
+        const normalized = isAbsolute || path.isAbsolute(candidatePath)
+          ? candidatePath
+          : path.join(execDir, candidatePath);
+        if (!candidates.includes(normalized)) {
+          candidates.push(normalized);
+        }
+      };
+
+      // 允许前端显式传入候选路径数组
+      if (Array.isArray(params.candidatePaths)) {
+        params.candidatePaths.forEach((p) => pushCandidate(p));
+      }
+
+      if (path.isAbsolute(targetName)) {
+        pushCandidate(targetName, true);
+      } else {
+        if (isPackaged) {
+          pushCandidate(path.join(execDir, targetName), true);
+          pushCandidate(path.join(resourcesDir, targetName), true);
+          pushCandidate(path.join(resourcesDir, 'extraResources', targetName), true);
+        } else {
+          pushCandidate(targetName);
+          pushCandidate(path.join('build', 'extraResources', targetName));
+          pushCandidate(path.join('extraResources', targetName));
+        }
+      }
+
+      console.log('=======');
+      console.log(candidates);
+      const resolvedPath = candidates.find((filePath) => filePath && fs.existsSync(filePath));
+
+      if (!resolvedPath) {
+        throw new Error(`未找到工具:${targetName}`);
+      }
+
+      // 如果已经有正在运行的子进程,则先关闭旧进程,再重新启动新的(保证始终只有一个进程,同时刷新页面参数)
+      if (this.app.externalToolProcess && !this.app.externalToolProcess.killed) {
+        try {
+          this.app.externalToolProcess.kill();
+        } catch (e) {
+          console.warn('关闭旧 externalTool 进程失败(忽略继续启动新进程):', e);
+        }
+        this.app.externalToolProcess = null;
+      }
+
+      const child = spawn(resolvedPath, args, {
+        cwd: path.dirname(resolvedPath),
+        detached: true,
+        stdio: 'ignore',
+        windowsHide: false
+      });
+
+      // 记录子进程,方便后续复用/判断是否已退出
+      this.app.externalToolProcess = child;
+      child.on('exit', () => {
+        if (this.app.externalToolProcess === child) {
+          this.app.externalToolProcess = null;
+        }
+      });
+      child.unref();
+      return {
+        code: 0,
+        data: {
+          path: resolvedPath,
+          reused: false
+        }
+      };
+    } catch (error) {
+      console.error('runExternalTool error:', error);
+      return {
+        code: 1,
+        msg: error.message || '运行外部工具失败'
+      };
+    }
+  }
+
 
   /**
    * 所有方法接收两个参数
@@ -35,19 +136,41 @@ class UtilsController extends Controller {
    * upload
    */
   async shellFun (params) {
-    console.log(params)
+    // 如果是打开路径操作,确保路径存在
+    if (params.action === 'openMkPath') {
+      try {
+        // 确保目录存在,如果不存在就创建
+        if (!fs.existsSync(params.params)) {
+          fs.mkdirSync(params.params, { recursive: true });
+        }
+
+        shell.openPath(params.params)
+
+        return;
+      } catch (err) {
+        console.error('创建目录失败:', err);
+        // 即使创建目录失败,也尝试打开路径(可能已经存在)
+      }
+    }
     shell[params.action](params.params)
   }
 
 
   async openMain (config) {
 
+    const { id, url } = config;
 
-    if( this.app.electron[config.id]){
-      this.app.electron[config.id].focus();
-      this.app.electron[config.id].show();
+    if (this.app.electron[id]) {
+      const win = this.app.electron[id];
+
+      // 切换到指定的 URL
+      await win.loadURL(url);
+
+      win.focus();
+      win.show();
       return;
     }
+
     const win = new BrowserWindow({
       ...config,
 
@@ -59,10 +182,10 @@ class UtilsController extends Controller {
 
       },
     });
-    win.loadURL(config.url); // 设置窗口的 URL
+    await win.loadURL(config.url); // 设置窗口的 URL
     // 监听窗口关闭事件
-
-   // win.webContents.openDevTools(config.openDevTools);
+    if(configDeault.debug)  win.webContents.openDevTools()
+   //
     win.on('close', () => {
       delete this.app.electron[config.id]; // 删除窗口引用
     });
@@ -82,6 +205,35 @@ class UtilsController extends Controller {
     return filePaths
   }
 
+  /**
+   * 关闭所有子窗口
+   */
+  closeAllWindows() {
+    try {
+      // 获取所有窗口
+      const windows = this.app.electron;
+
+      // 关闭除主窗口外的所有窗口
+      for (const [id, window] of Object.entries(windows)) {
+
+        if (!['mainWindow','extra'].includes(id)) { // 保留主窗口
+          try {
+            window.close();
+            delete this.app.electron[id];
+          } catch (error) {
+            console.error(`关闭窗口 ${id} 失败:`, error);
+          }
+        }
+      }
+
+      console.log('所有子窗口已关闭');
+      return { success: true, message: '所有子窗口已关闭' };
+    } catch (error) {
+      console.error('关闭所有窗口失败:', error);
+      return { success: false, message: '关闭所有窗口失败: ' + error.message };
+    }
+  }
+
   async openImage(
       optiops= {
         title:"选择图片",
@@ -148,6 +300,76 @@ class UtilsController extends Controller {
 
   }
 
+  getAppConfig(){
+    const config  =  readConfigFile()
+    const appPath = path.join(app.getAppPath(), '../..');
+    const pyPath = path.join(path.dirname(appPath), 'extraResources', 'py');
+    return  {
+      ...config,
+      userDataPath: app.getPath('userData'),
+      appPath: appPath,
+      pyPath:pyPath,
+    }
+  }
+  async readFileImageForPath(filePath,maxWidth=1500){
+
+    const getMimeType = (fileName)=>{
+      const extension = path.extname(fileName).toLowerCase().replace('.', '');
+      let mimeType = '';
+      switch (extension) {
+        case 'jpg':
+        case 'jpeg':
+          mimeType = 'image/jpeg';
+          break;
+        case 'png':
+          mimeType = 'image/png';
+          break;
+        case 'gif':
+          mimeType = 'image/gif';
+          break;
+        case 'webp':
+          mimeType = 'image/webp';
+          break;
+        case 'avif':
+          mimeType = 'image/avif';
+          break;
+        default:
+          mimeType = 'application/octet-stream';
+          break;
+      }
+      return mimeType;
+    }
+
+    try {
+      const fileName = path.basename(filePath);
+      const image = sharp(filePath);
+      const metadata = await image.metadata();
+
+      let mimeType = getMimeType(fileName); // 调用下面定义的私有方法获取 MIME 类型
+
+      let fileBuffer;
+
+      if (metadata.width > maxWidth) {
+        // 如果宽度大于 1500px,压缩至 1500px 宽度,保持比例
+        fileBuffer = await image.resize(maxWidth).toBuffer();
+      } else {
+        // 否则直接读取原图
+        fileBuffer = fs.readFileSync(filePath);
+      }
+
+      return {
+        fileBuffer,
+        fileName,
+        mimeType
+      };
+    } catch (error) {
+      console.error('Error processing image:', error);
+      throw error;
+    }
+
+
+  }
+
 
 }
 

+ 1 - 1
electron/preload/bridge.js

@@ -7,4 +7,4 @@ const { contextBridge, ipcRenderer } = require('electron')
 
 contextBridge.exposeInMainWorld('electron', {
   ipcRenderer: ipcRenderer,
-})
+})

+ 151 - 0
electron/utils/camera.js

@@ -0,0 +1,151 @@
+// electron/utils/readConfig.js
+
+const path = require('path');
+const fs = require('fs');
+const Log = require('ee-core/log');
+const { spawn } = require('child_process');
+const { liveShow, liveHide, setParams, capture, getParams,CMD,captureLive,closeOtherWindow,minimizeSmartShooter } = require('../api/camera');
+
+const { dialog } = require('electron'); // 引入 electron 的 dialog 模块
+const { windowManager } = require('node-window-manager');
+const CoreWindow = require("ee-core/electron/window");
+
+
+const { readConfigFile, writeConfigFile } = require('../utils/config');
+
+const exe = {
+  "digiCamControl":"CameraControl.exe",
+  "SmartShooter":"SmartShooter5.exe",
+}
+
+
+function getExePath () {
+  let exePath =  ""
+  if(readConfigFile().controlType === 'digiCamControl'){
+    exePath =  path.join( readConfigFile().controlPath || readConfigFile().digiCamControlPath, exe["digiCamControl"]);
+  }else if(readConfigFile().controlType === 'SmartShooter'){
+    exePath =  path.join( readConfigFile().controlPath || readConfigFile().SmartShooterPath,  exe["SmartShooter"]);
+  }
+
+  console.log('ex============ePath');
+  console.log(exePath);
+  return exePath
+}
+
+
+
+
+async function checkCameraControlCmdExists() {
+  try {
+
+    // 拼接 CameraControlCmd.exe 的完整路径
+
+    let exePath = getExePath()
+    // 检查文件是否存在
+    const exists = await fs.promises.access(exePath, fs.constants.F_OK)
+        .then(() => true)
+        .catch(() => false);
+
+    if (!exists) {
+      // 弹出文件夹选择对话框
+      const { canceled, filePaths } = await dialog.showOpenDialog({
+        title: '选择 相机控制安装软件 文件夹',
+        properties: ['openDirectory']
+      });
+
+      if (!canceled && filePaths.length > 0) {
+        const selectedPath = filePaths[0];
+
+        // Check if SmartShooter5.exe exists in the selected directory
+        const hasExe = path.join(selectedPath, exe["SmartShooter"]);
+
+        if (fs.existsSync(hasExe)) {
+          writeConfigFile("controlType","SmartShooter")
+        }else{
+          writeConfigFile("controlType","digiCamControl")
+        }
+        writeConfigFile("controlPath",selectedPath)
+      } else {
+        console.error('用户未选择文件夹');
+        return {
+          status:-1,
+          msg:"无法找到 CameraControlCmd.exe 或者  SmartShooter5.exe",
+        }
+      }
+    }
+    const res = await openCameraControlCmd();
+    
+    // 如果是SmartShooter,启动后最小化窗口
+    if(readConfigFile().controlType === 'SmartShooter'){
+      // 等待软件启动完成
+      setTimeout(async () => {
+        try {
+          await minimizeSmartShooter();
+        } catch (error) {
+          console.log('最小化SmartShooter失败:', error);
+        }
+      }, 3000); // 等待3秒让软件完全启动
+    }
+    
+    return res;
+  } catch (error) {
+    Log.error('检查 第三方相机控制器 是否存在时出错:', error);
+    throw error;
+  }
+}
+
+
+async function openCameraControlCmd(digiCamControlPath) {
+  return  new Promise(async (resolve, reject) => {
+    try {
+      // 获取 digiCamControl 文件夹路径
+
+      // 拼接 CameraControlCmd.exe 的完整路径
+      let exePath = getExePath()
+
+      // 检查文件是否存在
+      await fs.promises.access(exePath, fs.constants.F_OK);
+      try {
+
+        const child = spawn(exePath);
+
+        child.stdout.on('data', (data) => {
+          resolve(true)
+        });
+
+        child.on('close', (code) => {
+          if (code === 0) {
+            reject(false)
+          }
+        });
+
+
+
+      } catch (error) {
+        Log.error('error 第三方相机控制器:', error);
+        throw error;
+      }
+
+    } catch (error) {
+      Log.error('无法找到或运行 第三方相机控制器:', error);
+      throw error;
+    }
+  })
+}
+
+async function closeCameraControlTips() {
+  try {
+    await  closeOtherWindow()
+
+  }catch (e) {
+    console.log(e)
+  }
+
+
+}
+
+module.exports = {
+  checkCameraControlCmdExists,
+  closeCameraControlTips,
+  minimizeSmartShooter
+};

+ 10 - 0
electron/utils/config.default.json

@@ -0,0 +1,10 @@
+{
+  "debug":false,
+  "controlType": "SmartShooter",
+  "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",
+  "remoteUrl": false,
+  "env": "prod"
+}

+ 78 - 0
electron/utils/config.js

@@ -0,0 +1,78 @@
+// electron/utils/readConfig.js
+
+const fs = require('fs');
+const path = require('path');
+const { app } = require('electron');
+const defaultConfig = require('./config.default');
+
+const configPath = path.join(app.getPath("userData"),'config.default.json');
+
+
+/**
+ * 读取配置文件
+ */
+function readConfigFile() {
+
+  try {
+    if (fs.existsSync(configPath)) {
+      const data = fs.readFileSync(configPath, 'utf8');
+
+      return {
+        ...defaultConfig,
+        ...JSON.parse(data),
+      };
+    } else {
+      console.log('配置文件不存在');
+      // 创建空白JSON文件
+      fs.writeFileSync(configPath, '{}', 'utf8');
+      return {
+        ...defaultConfig,
+      };
+    }
+  } catch (error) {
+    console.error('读取配置文件出错:', error);
+    return null;
+  }
+}
+
+/**
+ * 写入配置文件
+ * @param {string} key - 要写入的配置项键名
+ * @param {*} value - 要写入的配置项值
+ */
+function writeConfigFile(key, value) {
+
+  try {
+    let config = {};
+
+    // 如果配置文件存在,则读取现有内容
+    if (fs.existsSync(configPath)) {
+      const data = fs.readFileSync(configPath, 'utf8');
+      config = {
+        ...defaultConfig,
+        ...JSON.parse(data),
+      };
+    }else {
+      console.log('配置文件不存在');
+      // 创建空白JSON文件
+      fs.writeFileSync(configPath, '{}', 'utf8');
+      config = {
+        ...defaultConfig,
+      }
+    }
+
+    // 更新配置项
+    config[key] = value;
+
+    // 将更新后的配置写回文件
+    fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
+    console.log(`成功写入配置项: ${key}`);
+  } catch (error) {
+    console.error('写入配置文件出错:', error);
+  }
+}
+
+module.exports = {
+  readConfigFile,
+  writeConfigFile
+};

+ 361 - 0
electron/utils/socket.js

@@ -0,0 +1,361 @@
+
+const Log = require('ee-core/log');
+const CoreWindow = require('ee-core/electron/window');
+const WebSocket = require('ws'); // 引入原生 ws 库
+const { readConfigFile } = require('./config');
+const pyapp = readConfigFile().pyapp
+const { app } = require('electron');
+const path = require('path');
+const fs = require('fs');
+
+const typeToMessage = {
+  run_mcu_single:['seeting','default'],
+  get_deviation_data:"developer",
+  set_deviation:"developer",
+  get_mcu_other_info:"developer",
+  set_mcu_other_info:"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"
+}
+
+
+const previewPath = path.join(app.getPath("userData"),'preview','liveview.png');
+
+// 确保目录存在的函数
+function ensureDirectoryExistence(filePath) {
+  const dir = path.dirname(filePath);
+  if (fs.existsSync(dir)) {
+    return true;
+  }
+  ensureDirectoryExistence(path.dirname(dir));
+  fs.mkdirSync(dir);
+}
+
+function livePreview(data){
+  if(data.msg === '预览数据发送' && data.code === 1){
+
+    ensureDirectoryExistence(previewPath);
+    const tempFilePath = `${previewPath}.tmp`;
+
+    fs.writeFile(tempFilePath, data.data.smart_shooter_preview, 'base64', (err) => {
+      if (err) {
+        Log.error('写入临时文件失败:', err);
+      } else {
+        fs.rename(tempFilePath, previewPath, (renameErr) => {
+          if (renameErr) {
+          } else {
+          }
+        });
+      }
+    });
+
+ }
+
+}
+
+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();
+        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) => {
+
+      const win = CoreWindow.getMainWindow()
+      if(app.socket){
+        resolve(true);
+        win.webContents.send('controller.socket.connect_open', true);
+        return;
+      }
+
+      // 重置重连状态
+      this.shouldReconnect = true;
+      this.isReconnecting = false;
+
+      app.socket = new WebSocket('ws://'+pyapp+':7074/ws');
+
+      // 监听连接成功事件
+      app.socket.on('open', () => {
+        Log.info('socket open')
+        resolve(true);
+        win.webContents.send('controller.socket.connect_open', true);
+
+        // 启动心跳机制
+        this.startHeartbeat();
+      });
+
+      // 监听消息事件
+      app.socket.on('message', (data) => {
+        try {
+          let this_data = JSON.parse(data.toString());
+
+          if(!['blue_tooth','smart_shooter_enable_preview','smart_shooter_getinfo'].includes(this_data.msg_type)){
+            console.log('message');
+            console.log(this_data);
+          //  Log.info(this_data);
+          }
+          if(this_data.msg_type){
+            let notAllMessage = false
+            switch (this_data.msg_type){
+              case 'smart_shooter_enable_preview':
+                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;
+            if(typeToMessage[this_data.msg_type]){
+              if(typeof typeToMessage[this_data.msg_type] === 'object'){
+
+                typeToMessage[this_data.msg_type].map(item=>{
+                  if(item === 'default'){
+                    win.webContents.send(channel, this_data);
+                  }else{
+                    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);
+                }
+              }
+            }else{
+              win.webContents.send(channel, this_data);
+            }
+          }
+        }catch (e){
+          console.log(e)
+        }
+      });
+
+      // 监听连接关闭事件
+      app.socket.on('close', (e) => {
+        Log.info('socket close');
+        Log.info(e);
+        win.webContents.send('controller.socket.disconnect', null);
+
+        // 停止心跳机制
+        this.stopHeartbeat();
+
+        app.socket = null;
+
+        // 启动重连机制
+        this.attemptReconnect();
+      });
+
+      // 监听错误事件
+      app.socket.on('error', (err) => {
+        Log.info('socket error:', err);
+        win.webContents.send('controller.socket.disconnect', null);
+
+        // 停止心跳机制
+        this.stopHeartbeat();
+
+        app.socket = null;
+
+        // 启动重连机制
+        this.attemptReconnect();
+        reject(true);
+      });
+
+
+    })
+
+  }
+  this.sendPing = function () {
+    const message = JSON.stringify({ data: 'node', type: 'ping' });
+    this.sendMessage(message);
+  }
+  this.sendMessage = async function (message) {
+    console.log('socket.=========================sendMessage');
+    console.log('socket.sendMessage');
+    console.log(message);
+    console.log(app.socket?.readyState);
+    if(!app.socket){
+      await  this.init()
+    }
+    // 检查连接状态
+    if (app.socket?.readyState === WebSocket.OPEN) {
+      console.log('send');
+      app.socket.send(message); // 使用 send() 发送
+    }
+  }
+
+  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);
+      async function onSocketMessage(data){
+          try {
+            let this_data = JSON.parse(data.toString());
+            if(this_data.msg_type === message_type){
+              app.socket.off('message', onSocketMessage);
+              callback(this_data)
+              resolve()
+            }
+          }catch (e){
+            Log.error(e)
+            reject(e)
+          }
+      }
+    })
+  }
+  return this;
+}
+
+
+module.exports = pySocket;

+ 1 - 1
frontend/.env.development

@@ -1,2 +1,2 @@
 NODE_ENV=development
-API_HOST = http://dev2.pubdata.cn
+API_HOST = http://dev2.valimart.net

+ 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">

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

@@ -0,0 +1,117 @@
+import { GET, POST } from "@/utils/http";
+import { useUuidStore } from "@/stores/modules/uuid";
+import pinia from "@/stores/index";
+import packageJson from '@/../../package.json';
+
+// 定义埋点参数的类型
+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,
+      describe: {
+        ...logParams.describe,
+        version:packageJson.version,
+      },
+    };
+
+    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,6 +7,16 @@ 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)
+}
+
 
 
 

+ 239 - 0
frontend/src/apis/setting.ts

@@ -0,0 +1,239 @@
+import { GET,POST  } from "@/utils/http";
+import client from "@/stores/modules/client";
+import icpList from '@/utils/ipc'
+import tokenInfo from '@/stores/modules/token';
+
+//获取配置
+export async function getAllUserConfigs(data){
+    return GET('/api/ai_image/camera_machine/get_all_user_configs',data)
+
+}
+
+
+//更新配置
+export async function setAllUserConfigs(data){
+    const result = await POST('/api/ai_image/camera_machine/update_all_user_configs',data);
+
+    // 同步到Python
+    try {
+        const clientStore = client();
+        const tokenInfoStore = tokenInfo();
+        const token = tokenInfoStore.getToken;
+        await clientStore.ipc.invoke(icpList.setting.syncSysConfigs, {
+            token: token || ''
+        });
+    } catch (error) {
+        console.error('同步系统配置到Python失败:', error);
+        // 同步失败不影响主流程
+    }
+
+    return result;
+}
+
+
+
+
+//获取左右脚配置
+export async function getTopTabs(data){
+    return GET('/api/ai_image/camera_machine/get_top_tabs',data)
+
+}
+
+//获取配置下的动作列表
+export async function getDeviceConfigs(data){
+    return GET('/api/ai_image/camera_machine/get_device_configs',data)
+
+}
+
+
+//点击切换执行配置
+export async function setLeftRightConfig(data){
+    const result = await POST('/api/ai_image/camera_machine/update_left_right_config',data);
+
+    // 同步到Python
+    try {
+        const clientStore = client();
+        const tokenInfoStore = tokenInfo();
+        const token = tokenInfoStore.getToken;
+        await clientStore.ipc.invoke(icpList.setting.syncActions, {
+            token: token || '',
+            action: 'update',
+            data: data
+        });
+    } catch (error) {
+        console.error('同步左右脚配置到Python失败:', error);
+        // 同步失败不影响主流程
+    }
+
+    return result;
+}
+
+
+//重置配置
+export async function restConfig(data){
+    const result = await POST('/api/ai_image/camera_machine/reset_config',data);
+
+    // 同步到Python
+    try {
+        const clientStore = client();
+        const tokenInfoStore = tokenInfo();
+        const token = tokenInfoStore.getToken;
+        await clientStore.ipc.invoke(icpList.setting.syncActions, {
+            token: token || '',
+            action: 'reset',
+            tab_id: data.tab_id
+        });
+    } catch (error) {
+        console.error('同步重置配置到Python失败:', error);
+        // 同步失败不影响主流程
+    }
+
+    return result;
+}
+
+
+
+//排序
+export async function sortDeviceConfig(data){
+    console.log(data);
+    const result = await POST('/api/ai_image/camera_machine/sort_device_config',data);
+
+    // 同步到Python
+    try {
+        const clientStore = client();
+        const tokenInfoStore = tokenInfo();
+        const token = tokenInfoStore.getToken;
+        await clientStore.ipc.invoke(icpList.setting.syncActions, {
+            token: token || '',
+            action: 'sort',
+        });
+    } catch (error) {
+        console.error('同步重置配置到Python失败:', error);
+        // 同步失败不影响主流程
+    }
+
+    return result;
+}
+
+
+//更新顶部TOP
+export async function setTabName(data){
+    const result = await POST('/api/ai_image/camera_machine/update_tab_name',data);
+
+    // 同步到Python
+    try {
+        const clientStore = client();
+        const tokenInfoStore = tokenInfo();
+        const token = tokenInfoStore.getToken;
+        await clientStore.ipc.invoke(icpList.setting.syncActions, {
+            token: token || '',
+            action: 'rename',
+            id: data.id,
+            mode_name: data.mode_name
+        });
+    } catch (error) {
+        console.error('同步重命名配置到Python失败:', error);
+        // 同步失败不影响主流程
+    }
+
+    return result;
+}
+
+//删除可执行命令
+export async function delDviceConfig(data){
+    const result = await POST('/api/ai_image/camera_machine/remove_device_config',data);
+
+    // 同步到Python
+    try {
+        const clientStore = client();
+        const tokenInfoStore = tokenInfo();
+        const token = tokenInfoStore.getToken;
+        await clientStore.ipc.invoke(icpList.setting.syncActions, {
+            token: token || '',
+            action: 'delete',
+            id: data.id
+        });
+    } catch (error) {
+        console.error('同步删除配置到Python失败:', error);
+        // 同步失败不影响主流程
+    }
+
+    return result;
+}
+
+
+
+
+//获取设备配置详情
+export async function getDeviceConfigDetail(data){
+    return GET('/api/ai_image/camera_machine/device_config_detail',data)
+
+}
+
+//根据条件查询可执行程序-单条
+export async function getDeviceConfigDetailQuery(data){
+    return GET('/api/ai_image/camera_machine/device_config_detail_query',data)
+
+}
+
+
+
+//创建或者保存动作
+export async function saveDeviceConfig(data){
+    const result = await POST('/api/ai_image/camera_machine/save_device_config',data);
+
+    // 同步到Python
+    try {
+        const clientStore = client();
+        const tokenInfoStore = tokenInfo();
+        const token = tokenInfoStore.getToken;
+        await clientStore.ipc.invoke(icpList.setting.syncActions, {
+            token: token || '',
+            action: 'save',
+            data: data
+        });
+    } catch (error) {
+        console.error('同步保存配置到Python失败:', error);
+        // 同步失败不影响主流程
+    }
+
+    return result;
+}
+
+// 登录后同步数据
+export async function syncAfterLogin() {
+    try {
+        const clientStore = client();
+        const tokenInfoStore = tokenInfo();
+        const token = tokenInfoStore.getToken;
+
+        if (!token) {
+            console.warn('没有token,跳过数据同步');
+            return;
+        }
+
+        // 同步系统配置
+        await clientStore.ipc.invoke(icpList.setting.syncSysConfigs, {
+            token: token
+        });
+
+        // 同步动作配置
+        await clientStore.ipc.invoke(icpList.setting.syncActions, {
+            token: token,
+            action: 'sync_all'
+        });
+
+
+        // 线上数据同步到本地
+        await clientStore.ipc.invoke(icpList.setting.syncSysConfigs, {
+            token: token
+        });
+
+        console.log('登录后数据同步成功');
+    } catch (error) {
+        console.error('登录后数据同步失败:', error);
+        // 同步失败不影响登录流程
+    }
+}
+
+

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


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


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


+ 10 - 0
frontend/src/assets/images/detail/bdt.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>bdt</title>
+    <g id="8.22" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="bdt" fill-rule="nonzero">
+            <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
+            <path d="M18.9596875,26.2165625 C18.9601097,26.2574675 18.9440467,26.2968202 18.9151209,26.3257459 C18.8861952,26.3546717 18.8468425,26.3707347 18.8059375,26.3703125 L5.40625,26.3703125 C5.365345,26.3707347 5.32599232,26.3546717 5.29706658,26.3257459 C5.26814084,26.2968202 5.25207783,26.2574675 5.2525,26.2165625 L5.2525,5.3828125 C5.25207783,5.3419075 5.26814084,5.30255482 5.29706658,5.27362908 C5.32599232,5.24470334 5.365345,5.22864033 5.40625,5.2290625 L18.8059375,5.2290625 C18.8468425,5.22864033 18.8861952,5.24470334 18.9151209,5.27362908 C18.9440467,5.30255482 18.9601097,5.3419075 18.9596875,5.3828125 L18.9596875,8.598125 L20.8509375,8.598125 L20.8509375,5.3828125 C20.8497318,4.25388996 19.93486,3.33901815 18.8059375,3.3378125 L5.40625,3.3378125 C4.27732746,3.33901815 3.36245565,4.25388996 3.36125,5.3828125 L3.36125,26.2165625 C3.36245565,27.345485 4.27732746,28.2603568 5.40625,28.2615625 L18.8059375,28.2615625 C19.93486,28.2603568 20.8497318,27.345485 20.8509375,26.2165625 L20.8509375,22.315625 L18.9596875,22.315625 L18.9596875,26.2165625 Z M22.25125,9.1953125 L20.9990625,10.613125 L25.8046875,14.856875 L13.71125,14.856875 L13.71125,16.7484375 L25.565,16.7484375 L21.0415625,20.295 L22.20875,21.7834375 L29.78125,15.8459375 L22.25125,9.1953125 L22.25125,9.1953125 Z" id="形状" fill="#9334EA"></path>
+        </g>
+    </g>
+</svg>

+ 11 - 0
frontend/src/assets/images/detail/cjt.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>cjt</title>
+    <g id="8.22" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="cjt" fill-rule="nonzero">
+            <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
+            <path d="M6.4,7.4 C6.06862915,7.4 5.8,7.66862915 5.8,8 L5.8,24 C5.8,24.3311875 6.0688125,24.6 6.4,24.6 L25.6,24.6 C25.9313708,24.6 26.2,24.3313708 26.2,24 L26.2,8 C26.2,7.66862915 25.9313708,7.4 25.6,7.4 L6.4,7.4 Z M6.4,5.4 L25.6,5.4 C27.0351875,5.4 28.2,6.5648125 28.2,8 L28.2,24 C28.2,25.4351875 27.0351875,26.6 25.6,26.6 L6.4,26.6 C4.96405965,26.6 3.8,25.4359403 3.8,24 L3.8,8 C3.8,6.5648125 4.9648125,5.4 6.4,5.4 Z" id="形状" fill="#2664EB"></path>
+            <path d="M25.6,24.6 C25.9313708,24.6 26.2,24.3313708 26.2,24 L26.2,19.5775938 C26.2,18.7455938 25.7295938,17.9824063 24.984,17.6095938 L22.0575938,16.1471875 C21.3357465,15.7862168 20.4741982,15.8494719 19.8128125,16.312 L7.9728125,24.6 L25.6,24.6 Z M25.8784062,15.8208125 C27.301317,16.5323196 28.2,17.986708 28.2,19.5775938 L28.2,24 C28.2,25.4351875 27.0351875,26.6000003 25.6,26.6000003 L6.83040625,26.6000003 C6.11426415,26.600433 5.48065289,26.1361387 5.26531291,25.4531392 C5.04997292,24.7701396 5.30270633,24.0263935 5.88959375,23.616 L18.665625,14.6735938 C19.9283816,13.790051 21.5736182,13.6690736 22.9520312,14.3584063 L25.8784375,15.8208125 L25.8784062,15.8208125 Z M10.4,13.6 C11.2836556,13.6 12,12.8836556 12,12 C12,11.1163444 11.2836556,10.4 10.4,10.4 C9.51634442,10.4 8.80000005,11.1163444 8.80000005,12 C8.80000005,12.8836556 9.51634442,13.6 10.4,13.6 L10.4,13.6 Z" id="形状" fill="#2664EB"></path>
+        </g>
+    </g>
+</svg>

+ 11 - 0
frontend/src/assets/images/detail/cjt_h.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>cjt</title>
+    <g id="8.22" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="cjt" fill-rule="nonzero">
+            <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
+            <path d="M6.4,7.4 C6.06862915,7.4 5.8,7.66862915 5.8,8 L5.8,24 C5.8,24.3311875 6.0688125,24.6 6.4,24.6 L25.6,24.6 C25.9313708,24.6 26.2,24.3313708 26.2,24 L26.2,8 C26.2,7.66862915 25.9313708,7.4 25.6,7.4 L6.4,7.4 Z M6.4,5.4 L25.6,5.4 C27.0351875,5.4 28.2,6.5648125 28.2,8 L28.2,24 C28.2,25.4351875 27.0351875,26.6 25.6,26.6 L6.4,26.6 C4.96405965,26.6 3.8,25.4359403 3.8,24 L3.8,8 C3.8,6.5648125 4.9648125,5.4 6.4,5.4 Z" id="形状" fill="#FFFFFF"></path>
+            <path d="M25.6,24.6 C25.9313708,24.6 26.2,24.3313708 26.2,24 L26.2,19.5775938 C26.2,18.7455938 25.7295938,17.9824063 24.984,17.6095938 L22.0575938,16.1471875 C21.3357465,15.7862168 20.4741982,15.8494719 19.8128125,16.312 L7.9728125,24.6 L25.6,24.6 Z M25.8784062,15.8208125 C27.301317,16.5323196 28.2,17.986708 28.2,19.5775938 L28.2,24 C28.2,25.4351875 27.0351875,26.6000003 25.6,26.6000003 L6.83040625,26.6000003 C6.11426415,26.600433 5.48065289,26.1361387 5.26531291,25.4531392 C5.04997292,24.7701396 5.30270633,24.0263935 5.88959375,23.616 L18.665625,14.6735938 C19.9283816,13.790051 21.5736182,13.6690736 22.9520312,14.3584063 L25.8784375,15.8208125 L25.8784062,15.8208125 Z M10.4,13.6 C11.2836556,13.6 12,12.8836556 12,12 C12,11.1163444 11.2836556,10.4 10.4,10.4 C9.51634442,10.4 8.80000005,11.1163444 8.80000005,12 C8.80000005,12.8836556 9.51634442,13.6 10.4,13.6 L10.4,13.6 Z" id="形状" fill="#FFFFFF"></path>
+        </g>
+    </g>
+</svg>

+ 10 - 0
frontend/src/assets/images/detail/cptc.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>cptc</title>
+    <g id="8.22" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="cptc" fill-rule="nonzero">
+            <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
+            <path d="M19.3673798,26.1351351 L26.7692308,26.1351351 C27.4486058,26.1351351 28,25.5781524 28,24.8918919 L28,6.24324324 C28,5.55698269 27.4486058,5 26.7692308,5 L17.8424519,5 L16,6.86112542 L14.1575481,5 L5.23076923,5 C4.55139423,5 4,5.55698269 4,6.24324324 L4,24.8918919 C4,25.5781524 4.55139423,26.1351351 5.23076923,26.1351351 L12.6326202,26.1351351 C13.3439904,27.2559238 14.5846154,28 16,28 C17.4153846,28 18.6560096,27.2559238 19.3673798,26.1351351 Z M16,26.1351351 C14.9156971,26.1351351 14.0276923,25.3226563 13.8769231,24.2702703 L5.84615385,24.2702703 L5.84615385,6.86486486 L13.5329327,6.86486486 L15.0769231,8.4245038 L15.0769231,18.3648649 C15.0769231,18.8795724 15.4898558,19.2972973 16,19.2972973 C16.5095433,19.2972973 16.9230769,18.8795724 16.9230769,18.3648649 L16.9230769,8.4245038 L18.4676923,6.86486486 L26.1538462,6.86486486 L26.1538462,24.2702703 L18.1224519,24.2702703 C17.9723077,25.3226563 17.0849279,26.1351351 16,26.1351351 Z" id="形状" fill="#9334EA"></path>
+        </g>
+    </g>
+</svg>

BIN
frontend/src/assets/images/detail/excel.png


BIN
frontend/src/assets/images/detail/excel_h.png


BIN
frontend/src/assets/images/detail/file-excel.png


BIN
frontend/src/assets/images/detail/logo.png


+ 10 - 0
frontend/src/assets/images/detail/mtt.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>mtt</title>
+    <g id="8.22" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="mtt" fill-rule="nonzero">
+            <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
+            <path d="M19.2,16.64 C24.8556875,16.64 29.44,21.2243125 29.44,26.88 L29.44,27.52 C29.44,28.933849 28.293849,30.08 26.88,30.08 L5.12,30.08 C3.70615104,30.08 2.56,28.933849 2.56,27.52 L2.56,26.88 C2.56,21.2243125 7.1443125,16.64 12.8,16.64 L19.2,16.64 L19.2,16.64 Z M19.2,18.56 L12.8,18.56 C8.27071875,18.56 4.586875,22.1791875 4.4825625,26.6835313 L4.48,26.88 L4.48,27.52 C4.48,27.8444699 4.72288331,28.1175708 5.045125,28.1555312 L5.12,28.16 L26.88,28.16 C27.2044699,28.16 27.4775708,27.9171167 27.5155313,27.594875 L27.52,27.52 L27.52,26.88 C27.52,22.3507188 23.9008125,18.666875 19.3964688,18.5625625 L19.2,18.56 Z M16,1.92 C19.5347187,1.92 22.4,4.78528125 22.4,8.32 C22.4,11.8547188 19.5347187,14.72 16,14.72 C12.4652813,14.72 9.6,11.8547188 9.6,8.32 C9.6,4.78528125 12.4652813,1.92 16,1.92 Z M16,3.84 C13.5257643,3.84 11.52,5.84576432 11.52,8.32 C11.52,10.7942357 13.5257643,12.8 16,12.8 C18.4742357,12.8 20.48,10.7942357 20.48,8.32 C20.48,5.84576432 18.4742357,3.84 16,3.84 Z" id="形状" fill="#2664EB"></path>
+        </g>
+    </g>
+</svg>

+ 10 - 0
frontend/src/assets/images/detail/mtt_h.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>mtt</title>
+    <g id="8.22" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="mtt" fill-rule="nonzero">
+            <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
+            <path d="M19.2,16.64 C24.8556875,16.64 29.44,21.2243125 29.44,26.88 L29.44,27.52 C29.44,28.933849 28.293849,30.08 26.88,30.08 L5.12,30.08 C3.70615104,30.08 2.56,28.933849 2.56,27.52 L2.56,26.88 C2.56,21.2243125 7.1443125,16.64 12.8,16.64 L19.2,16.64 L19.2,16.64 Z M19.2,18.56 L12.8,18.56 C8.27071875,18.56 4.586875,22.1791875 4.4825625,26.6835313 L4.48,26.88 L4.48,27.52 C4.48,27.8444699 4.72288331,28.1175708 5.045125,28.1555312 L5.12,28.16 L26.88,28.16 C27.2044699,28.16 27.4775708,27.9171167 27.5155313,27.594875 L27.52,27.52 L27.52,26.88 C27.52,22.3507188 23.9008125,18.666875 19.3964688,18.5625625 L19.2,18.56 Z M16,1.92 C19.5347187,1.92 22.4,4.78528125 22.4,8.32 C22.4,11.8547188 19.5347187,14.72 16,14.72 C12.4652813,14.72 9.6,11.8547188 9.6,8.32 C9.6,4.78528125 12.4652813,1.92 16,1.92 Z M16,3.84 C13.5257643,3.84 11.52,5.84576432 11.52,8.32 C11.52,10.7942357 13.5257643,12.8 16,12.8 C18.4742357,12.8 20.48,10.7942357 20.48,8.32 C20.48,5.84576432 18.4742357,3.84 16,3.84 Z" id="形状" fill="#FFFFFF"></path>
+        </g>
+    </g>
+</svg>

BIN
frontend/src/assets/images/detail/sctp.png


+ 10 - 0
frontend/src/assets/images/detail/xqmb.svg

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>xqmb</title>
+    <g id="8.22" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="xqmb" fill-rule="nonzero">
+            <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
+            <path d="M11.1895,26.5155 L11.1895,13.6646875 L26.512125,13.6646875 L26.5178125,26.5085625 L11.1905312,26.5155 L11.1895,26.5155 Z M5.4914375,13.6646875 L8.8543125,13.6646875 L8.8543125,26.51675 L5.4914375,26.5178125 L5.4914375,13.6646875 L5.4914375,13.6646875 Z M5.510875,5.33902274 L26.5085646,5.32953125 L26.510875,11.3295313 L5.510875,11.3295313 L5.510875,5.33902274 Z M25.8268613,4 L6.17313869,4 C4.97363504,4.00183942 4.00183942,4.97363504 4,6.17313869 L4,25.8268613 C4,27.0247007 4.97529927,28 6.17313869,28 L25.8268613,28 C27.0265109,27.9988613 27.9988321,27.0265401 28,25.8268613 L28,6.17313869 C27.9988613,4.97345985 27.0265401,4.00116788 25.8268613,4 Z" id="形状" fill="#9334EA"></path>
+        </g>
+    </g>
+</svg>

+ 11 - 0
frontend/src/assets/images/detail/xqy.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>xqy</title>
+    <g id="8.22" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="xqy" fill-rule="nonzero">
+            <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
+            <path d="M22.125,30 L9.875,30 C6.55,30 3.75,27.2 3.75,23.875 L3.75,8.125 C3.75,4.8 6.55,2 9.875,2 L22.125,2 C25.45,2 28.25,4.8 28.25,8.125 L28.25,23.875 C28.25,27.2 25.45,30 22.125,30 Z M9.875,3.75 C7.425,3.75 5.5,5.675 5.5,8.125 L5.5,23.875 C5.5,26.325 7.425,28.25 9.875,28.25 L22.125,28.25 C24.575,28.25 26.5,26.325 26.5,23.875 L26.5,8.125 C26.5,5.675 24.575,3.75 22.125,3.75 L9.875,3.75 Z" id="形状" fill="#2664EB"></path>
+            <path d="M22.125,9.875 L9.875,9.875 C9.35,9.875 9,9.525 9,9 C9,8.475 9.35,8.125 9.875,8.125 L22.125,8.125 C22.65,8.125 23,8.475 23,9 C23,9.525 22.65,9.875 22.125,9.875 Z M22.125,15.125 L9.875,15.125 C9.35,15.125 9,14.775 9,14.25 C9,13.725 9.35,13.375 9.875,13.375 L22.125,13.375 C22.65,13.375 23,13.725 23,14.25 C23,14.775 22.65,15.125 22.125,15.125 Z M22.125,20.375 L9.875,20.375 C9.35,20.375 9,20.025 9,19.5 C9,18.975 9.35,18.625 9.875,18.625 L22.125,18.625 C22.65,18.625 23,18.975 23,19.5 C23,20.025 22.65,20.375 22.125,20.375 Z" id="形状" fill="#2664EB"></path>
+        </g>
+    </g>
+</svg>

+ 11 - 0
frontend/src/assets/images/detail/xqy_h.svg

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="32px" height="32px" viewBox="0 0 32 32" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>xqy</title>
+    <g id="8.22" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="xqy" fill-rule="nonzero">
+            <rect id="矩形" fill="#000000" opacity="0" x="0" y="0" width="32" height="32"></rect>
+            <path d="M22.125,30 L9.875,30 C6.55,30 3.75,27.2 3.75,23.875 L3.75,8.125 C3.75,4.8 6.55,2 9.875,2 L22.125,2 C25.45,2 28.25,4.8 28.25,8.125 L28.25,23.875 C28.25,27.2 25.45,30 22.125,30 Z M9.875,3.75 C7.425,3.75 5.5,5.675 5.5,8.125 L5.5,23.875 C5.5,26.325 7.425,28.25 9.875,28.25 L22.125,28.25 C24.575,28.25 26.5,26.325 26.5,23.875 L26.5,8.125 C26.5,5.675 24.575,3.75 22.125,3.75 L9.875,3.75 Z" id="形状" fill="#FFFFFF"></path>
+            <path d="M22.125,9.875 L9.875,9.875 C9.35,9.875 9,9.525 9,9 C9,8.475 9.35,8.125 9.875,8.125 L22.125,8.125 C22.65,8.125 23,8.475 23,9 C23,9.525 22.65,9.875 22.125,9.875 Z M22.125,15.125 L9.875,15.125 C9.35,15.125 9,14.775 9,14.25 C9,13.725 9.35,13.375 9.875,13.375 L22.125,13.375 C22.65,13.375 23,13.725 23,14.25 C23,14.775 22.65,15.125 22.125,15.125 Z M22.125,20.375 L9.875,20.375 C9.35,20.375 9,20.025 9,19.5 C9,18.975 9.35,18.625 9.875,18.625 L22.125,18.625 C22.65,18.625 23,18.975 23,19.5 C23,20.025 22.65,20.375 22.125,20.375 Z" id="形状" fill="#FFFFFF"></path>
+        </g>
+    </g>
+</svg>

BIN
frontend/src/assets/images/detail/xtdj.png


BIN
frontend/src/assets/images/detail/xtdj_h.png


BIN
frontend/src/assets/images/home/left.jpg


BIN
frontend/src/assets/images/home/left.png


BIN
frontend/src/assets/images/home/right.jpg


BIN
frontend/src/assets/images/home/right.png


BIN
frontend/src/assets/images/processImage.vue/go.png


BIN
frontend/src/assets/images/processImage.vue/riq.png


BIN
frontend/src/assets/images/processImage.vue/sc.png


BIN
frontend/src/assets/images/processImage.vue/tup.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>

+ 57 - 0
frontend/src/components/UpdateDialog/index.vue

@@ -0,0 +1,57 @@
+<template>
+  <el-dialog
+    :visible.sync="show"
+    :close-on-click-modal="false"
+    title="自动更新中"
+    :show-close="false"
+    @update:visible="close"
+  >
+    <template>
+      <div class="desc line-30 fs-16">{{res.desc}}</div>
+    </template>
+    <slot></slot>
+  </el-dialog>
+</template>
+<script>
+export default {
+  name: 'FullDialog',
+  components: {
+  },
+  props: {
+    show: {
+      type: Boolean,
+      default: false
+    },
+    res:{
+      type: Object,
+      default:()=>{
+        return {}
+      }
+    },
+  },
+  data() {
+    return {
+    }
+  },
+  computed: {
+  },
+  watch: {
+  },
+  destroyed() {
+  },
+  mounted() {
+  },
+  created() {
+
+  },
+  methods: {
+    close() {
+      this.$emit('onClose')
+      this.$emit('update:show', false)
+    },
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+</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>

+ 1 - 0
frontend/src/components/header-bar/assets/gengxin.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750736576156" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6548" xmlns:xlink="http://www.w3.org/1999/xlink" width="200.1953125" height="200"><path d="M737.600098 289.600098c-46.401367-96-144-161.599609-257.60083-161.599609-151.999512 0-276.800049 118.399902-286.399902 267.199219-112.000488 32.000977-193.599365 129.601074-193.599365 244.800781 0 136.000488 113.599854 247.999512 255.999756 255.999023 0 0 475.199463 0 480 0 158.398926 0 288.000244-135.999023 288.000244-303.999023C1024 430.400879 897.599121 297.601074 737.600098 289.600098L737.600098 289.600098zM681.864746 642.988281l-140.050049 160.469238c-16.470703 15.620606-43.184082 15.620606-59.650147 0l-140.050049-160.469238-0.001465 0c-4.11499-5.342285-6.545898-11.847168-6.545898-18.958008 0-17.593262 15.021729-31.862305 33.605713-31.862305l33.604492 0c9.242188 0 16.79126-7.179199 16.79126-15.93457l0.424561-162.904785 0.045898 0c0-17.446289 14.948486-31.578125 33.448486-31.578125l117.060791 0c18.480225 0 33.448486 14.13916 33.448486 31.578125l0.426025 162.904785c0 8.77832 7.556641 15.93457 16.791504 15.93457l33.601074 0c18.568848 0 33.61499 14.264648 33.61499 31.862305C688.43042 631.127441 685.987305 637.644043 681.864746 642.988281L681.864746 642.988281z" fill="#2c2c2c" p-id="6549"></path></svg>

+ 1 - 0
frontend/src/components/header-bar/assets/qiehuan.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1750736436552" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4561" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M990.08 111.36a32 32 0 0 0-32 32V960H140.16a32 32 0 0 0 0 64h849.92a32 32 0 0 0 32-32V143.36a32 32 0 0 0-32-32z" fill="#2c2c2c" p-id="4562"></path><path d="M393.6 841.6a32 32 0 0 0-32-32H64V64h745.6v300.8a32 32 0 0 0 64 0V32a32 32 0 0 0-32-32H32a32 32 0 0 0-32 32v809.6a32 32 0 0 0 32 32h329.6a32 32 0 0 0 32-32z" fill="#2c2c2c" p-id="4563"></path><path d="M625.28 841.6a32 32 0 0 0 32 32h184.32a32 32 0 0 0 32-32v-183.68a32 32 0 1 0-64 0v106.24L256 210.56h106.24a32 32 0 0 0 0-64H178.56a32 32 0 0 0-32 32v184.96a32 32 0 0 0 64 0V256l553.6 553.6h-106.88a32 32 0 0 0-32 32z" fill="#2c2c2c" p-id="4564"></path></svg>

+ 166 - 0
frontend/src/components/header-bar/blue-header.vue

@@ -0,0 +1,166 @@
+<template>
+  <div class="blue-header-bar">
+    <div class="blue-header-bar__left">
+      <img src="@/assets/images/detail/logo.png" class="blue-header-bar__logo" alt="logo" />
+      <span class="blue-header-bar__title">智惠映AI自动拍照机 <span class="blue-header-bar__version"  @click="openOTA">{{ currentVersion }}</span></span>
+    </div>
+    <div class="blue-header-bar__right">
+      <div class="blue-header-bar__user">
+        <span class="blue-header-bar__user-label">昵称:</span>
+        <span class="blue-header-bar__user-name">
+          {{  useUserInfoStore.userInfo.account_name
+            || useUserInfoStore.userInfo.real_name
+            || useUserInfoStore.userInfo.login_name
+            || '未登录' }}
+        </span>
+      </div>
+    </div>
+  </div>
+  <div class="blue-header-bar_blank"></div>
+</template>
+
+<script setup lang="ts">
+import {defineProps, reactive, onMounted, onUnmounted, ref} from 'vue'
+import useUserInfo from '@/stores/modules/user'
+import tokenInfo from '@/stores/modules/token';
+import packageJson from '@/../../package.json'
+import client from '@/stores/modules/client'
+import {useRouter} from "vue-router";
+import icpList from '@/utils/ipc'
+import { getRouterUrl } from '@/utils/appfun'
+
+const useUserInfoStore = useUserInfo()
+const tokenInfoStore = tokenInfo();
+const currentVersion = ref(packageJson.version)
+const clientStore = client()
+
+const Router = useRouter()
+onMounted(async ()=>{
+
+  if (tokenInfoStore.getToken /* 已登录 */) {
+    if(!useUserInfoStore.userInfo.id){
+      await useUserInfoStore.getInfo()
+    }
+  }
+})
+
+function openOTA() {
+  const { href } = Router.resolve({
+    name: 'ota'
+  })
+
+  clientStore.ipc.removeAllListeners(icpList.utils.openMain)
+  let params = {
+    title: '版本更新',
+    width: 900,
+    height: 700,
+    frame: true,
+    id: 'ota',
+    url: getRouterUrl(href)
+  }
+  clientStore.ipc.send(icpList.utils.openMain, params)
+}
+</script>
+
+<style lang="scss" scoped>
+.blue-header-bar_blank {
+  width: 100%;
+  height: 50px;
+}
+
+.blue-header-bar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 50px;
+  background: linear-gradient(135deg, #2957FF 0%, #1E3A8A 100%);
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+  z-index: 100;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+  app-drag: drag;
+  -webkit-app-region: drag;
+
+  &__left {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+  }
+
+  &__logo {
+    width: 36px;
+    height: 36px;
+  }
+
+  &__title {
+    color: #fff;
+    font-size: 16px;
+    font-weight: 600;
+  }
+  &__version {
+    background: rgba(255,255,255,0.2);
+    border-radius: 12px;
+    font-size: 12px;
+    padding: 3px 10px;
+    font-weight: normal;
+    margin-left: 5px;
+  }
+
+  &__right {
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    app-drag: no-drag;
+    -webkit-app-region: no-drag;
+  }
+
+  &__user {
+    display: flex;
+    align-items: center;
+    color: #fff;
+    font-size: 14px;
+
+    &-label {
+      opacity: 0.8;
+      margin-right: 4px;
+    }
+
+    &-name {
+      font-weight: 500;
+    }
+  }
+
+  &__controls {
+    display: flex;
+    gap: 2px;
+  }
+
+  .control-btn {
+    width: 30px;
+    height: 30px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    color: #fff;
+    font-size: 18px;
+    transition: background-color 0.2s;
+
+    &:hover {
+      background-color: rgba(255, 255, 255, 0.2);
+    }
+
+    &--close:hover {
+      background-color: rgba(255, 0, 0, 0.3);
+    }
+
+    span {
+      line-height: 1;
+    }
+  }
+}
+</style>
+

+ 272 - 62
frontend/src/components/header-bar/index.vue

@@ -1,21 +1,87 @@
 <template>
   <div class="header-bar">
     <div class="header-bar__menu">
-      <div v-for="(item, index) in menu" :key="index" class="header-bar__menu-item" @click="getItemClick(item)">
-        <img v-if="getItemIcon(item)" :src="getItemIcon(item)" class="header-bar__menu-icon" />
-        <span class="header-bar__menu-name">{{ getItemName(item) }}</span>
+      <div v-for="(item, index) in menu" :key="index" class="header-bar__menu-item">
+        <div
+          v-if="!item.children || item.children.length === 0"
+          @click="getItemClick(item)"
+          class="header-bar__menu-item-content flex"
+        >
+          <img v-if="getItemIcon(item)" :src="getItemIcon(item)" class="header-bar__menu-icon" />
+          <span class="header-bar__menu-name">{{ getItemName(item) }}</span>
+        </div>
+        <div v-else class="header-bar__submenu">
+          <div
+            class="header-bar__submenu-header"
+            @click.stop="toggleSubmenu(index)"
+          >
+            <img v-if="getItemIcon(item)" :src="getItemIcon(item)" class="header-bar__menu-icon" />
+            <span class="header-bar__menu-name">{{ getItemName(item) }}</span>
+          </div>
+          <!-- 二级菜单 -->
+          <div
+            class="header-bar__submenu-body"
+            :class="{ 'submenu-open': submenuOpen[index] }"
+          >
+            <div
+              v-for="(child, childIndex) in item.children"
+              :key="childIndex"
+              class="header-bar__submenu-item"
+              @click="getItemClick(child)"
+            >
+              <div
+                v-if="!child.children || child.children.length === 0"
+                class="header-bar__submenu-item-content flex left"
+              >
+                <img v-if="getItemIcon(child)" :src="getItemIcon(child)" class="header-bar__menu-icon" />
+                <span class="header-bar__menu-name">{{ getItemName(child) }}</span>
+              </div>
+              <div v-else class="header-bar__submenu-third-level">
+                <div
+                  class="header-bar__submenu-third-header"
+                  @click.stop="toggleThirdLevel(index, childIndex)"
+                >
+                  <img v-if="getItemIcon(child)" :src="getItemIcon(child)" class="header-bar__menu-icon" />
+                  <span class="header-bar__menu-name">{{ getItemName(child) }}</span>
+                </div>
+                <!-- 三级菜单 -->
+                <div
+                  class="header-bar__submenu-third-body"
+                  :class="{ 'submenu-open': thirdLevelOpen[`${index}-${childIndex}`] }"
+                >
+                  <div
+                    v-for="(grandChild, grandChildIndex) in child.children"
+                    :key="grandChildIndex"
+                    class="header-bar__submenu-third-item"
+                    @click="getItemClick(grandChild)"
+                  >
+                    <img v-if="getItemIcon(grandChild)" :src="getItemIcon(grandChild)" class="header-bar__menu-icon" />
+                    <span class="header-bar__menu-name">{{ getItemName(grandChild) }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
       </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" >
+    <div class="header-bar__buttons">
+      <!-- 版本信息 - 始终显示 -->
+      <div class="header-bar__button header-bar__button__version">
+        <span class="version-text" @click="openOTA" title="点击查看版本详情">
+          当前版本:{{ currentVersion }}
+        </span>
+      </div>
+      <!-- 用户信息 - 仅在需要时显示 -->
       <div class="header-bar__button header-bar__button__user" v-if="showUser">
-
-
         <el-dropdown>
           <span class="el-dropdown-link">
-           {{useUserInfoStore.userInfo.account_name || useUserInfoStore.userInfo.real_name || useUserInfoStore.userInfo.login_name}}
+            {{ useUserInfoStore.userInfo.account_name || useUserInfoStore.userInfo.real_name || useUserInfoStore.userInfo.login_name }}
           </span>
           <template #dropdown>
             <el-dropdown-menu>
@@ -30,25 +96,28 @@
 </template>
 
 <script setup lang="ts">
-import { defineProps, reactive } from 'vue';
-import useUserInfo from "@/stores/modules/user";
-import { useRouter} from "vue-router";
+import {defineProps, reactive, onMounted, onUnmounted, ref} from 'vue'
+import useUserInfo from '@/stores/modules/user'
+import { useRouter } from 'vue-router'
 import iconsz from './assets/shezhi@2x.png'
 import iconykq from './assets/yaokong@2x.png'
-import iconMinimize from './assets/suoxiao@2x.png' // 新增
-import iconClose from './assets/guanbi@2x.png' // 新增
+import gengxin from './assets/gengxin.svg'
 import icpList from '@/utils/ipc'
 import { getRouterUrl } from '@/utils/appfun'
-import client from "@/stores/modules/client";
-const clientStore = client();
-const useUserInfoStore = useUserInfo();
+import client from '@/stores/modules/client'
+import packageJson from '@/../../package.json';
 
+const clientStore = client()
+const useUserInfoStore = useUserInfo()
+
+const currentVersion = ref(packageJson.version);
 // 定义 menu 项的类型
 interface MenuItem {
-  name?: string;    //名称
-  icon?: string;    // 图标
-  click?: () => void;   // 点击事件
-  type?: keyof typeof menuType; // 类型 menuType里面的值
+  name?: string //名称
+  icon?: string // 图标
+  click?: () => void // 点击事件
+  type?: keyof typeof menuType // 类型 menuType里面的值
+  children?: MenuItem[] // 支持嵌套菜单
 }
 
 // 定义 props
@@ -61,12 +130,26 @@ const props = defineProps({
     type: Array as () => MenuItem[],
     default: () => []
   },
-  showUser:{
+  showUser: {
     type: Boolean,
     default: false
   }
-});
+})
+
 const Router = useRouter()
+
+const submenuOpen = reactive<{ [key: number]: boolean }>({})
+const thirdLevelOpen = reactive<{ [key: string]: boolean }>({})
+
+const toggleSubmenu = (index: number) => {
+  submenuOpen[index] = !submenuOpen[index]
+}
+
+const toggleThirdLevel = (parentIndex: number, childIndex: number) => {
+  const key = `${parentIndex}-${childIndex}`
+  thirdLevelOpen[key] = !thirdLevelOpen[key]
+}
+
 const menuType = reactive({
   setting: {
     name: '设置',
@@ -82,106 +165,142 @@ const menuType = reactive({
     name: '初始设备调频设置',
     icon: iconsz,
     click: openDeveloper
+  },
+  ota: {
+    name: '当前版本:'+currentVersion.value,
+    click: openOTA
   }
-});
+})
 
 function getItemClick(item: MenuItem) {
-  const menuItem = item.type ? { ...menuType[item.type], ...item } : item;
+  const menuItem = item.type ? { ...menuType[item.type], ...item } : item
   if (menuItem && menuItem.click) {
-    menuItem.click();
+    menuItem.click()
   }
-}
 
+  // 关闭所有展开的一级和三级菜单
+  for (const key in submenuOpen) {
+    submenuOpen[key] = false
+  }
+  for (const key in thirdLevelOpen) {
+    thirdLevelOpen[key] = false
+  }
+}
 function getItemName(item: MenuItem) {
-  const menuItem = item.type ? { ...menuType[item.type], ...item } : item;
-  return menuItem.name;
+  const menuItem = item.type ? { ...menuType[item.type], ...item } : item
+  return menuItem.name
 }
 
 function getItemIcon(item: MenuItem) {
-  const menuItem = item.type ? { ...menuType[item.type], ...item } : item;
-  return menuItem.icon;
+  const menuItem = item.type ? { ...menuType[item.type], ...item } : item
+  return menuItem.icon
 }
 
 function openSetting() {
-
   const { href } = Router.resolve({
     name: 'setting',
-    query:{
-      type:0,
+    query: {
+      type: 0
     }
   })
 
-  clientStore.ipc.removeAllListeners(icpList.utils.openMain);
+  clientStore.ipc.removeAllListeners(icpList.utils.openMain)
   let params = {
     title: '设置',
-    width: 1920,
-    height: 1080,
+    width: 3840,
+    height: 2160,
     frame: true,
-    id: "seeting",
+    id: 'seeting',
     url: getRouterUrl(href)
   }
-  clientStore.ipc.send(icpList.utils.openMain, params);
+  clientStore.ipc.send(icpList.utils.openMain, params)
 }
 
-
-function openRemoteControl(){
-
+function openRemoteControl() {
   const { href } = Router.resolve({
-    name: 'RemoteControl',
+    name: 'RemoteControl'
   })
 
-  clientStore.ipc.removeAllListeners(icpList.utils.openMain);
+  clientStore.ipc.removeAllListeners(icpList.utils.openMain)
   let params = {
     title: '模拟遥控器',
     width: 350,
     height: 600,
     frame: true,
-    id: "RemoteControl",
+    id: 'RemoteControl',
     url: getRouterUrl(href)
   }
-  clientStore.ipc.send(icpList.utils.openMain, params);
+  clientStore.ipc.send(icpList.utils.openMain, params)
 }
 
+function openDeveloper() {
+  const { href } = Router.resolve({
+    name: 'developer'
+  })
 
-function openDeveloper(){
+  clientStore.ipc.removeAllListeners(icpList.utils.openMain)
+  let params = {
+    title: '初始设备调频设置',
+    width: 900,
+    height: 700,
+    frame: true,
+    id: 'developer',
+    url: getRouterUrl(href)
+  }
+  clientStore.ipc.send(icpList.utils.openMain, params)
+}
 
+function openOTA() {
   const { href } = Router.resolve({
-    name: 'developer',
+    name: 'ota'
   })
 
-  clientStore.ipc.removeAllListeners(icpList.utils.openMain);
+  clientStore.ipc.removeAllListeners(icpList.utils.openMain)
   let params = {
-    title: '初始设备调频设置',
+    title: '版本更新',
     width: 900,
     height: 700,
     frame: true,
-    id: "developer",
+    id: 'ota',
     url: getRouterUrl(href)
   }
-  clientStore.ipc.send(icpList.utils.openMain, params);
+  clientStore.ipc.send(icpList.utils.openMain, params)
 }
 
-function loginOut(){
-  useUserInfoStore.loginOut();
+function loginOut() {
+  useUserInfoStore.loginOut()
   useUserInfoStore.updateLoginShow(true)
 }
 
-// 新增
-function minimizeWindow() {
-  clientStore.ipc.send(icpList.utils.minimizeWindow);
-}
+onMounted(() => {
+  document.addEventListener('click', handleOutsideClick)
+})
 
-// 新增
-function closeWindow() {
-  clientStore.ipc.send(icpList.utils.closeWindow);
+onUnmounted(() => {
+  document.removeEventListener('click', handleOutsideClick)
+})
+
+function handleOutsideClick(event: MouseEvent) {
+  const menuElement = document.querySelector('.header-bar__menu')
+  if (menuElement && !menuElement.contains(event.target as Node)) {
+    // 关闭所有一级菜单
+    for (const key in submenuOpen) {
+      submenuOpen[key] = false
+    }
+    // 关闭所有三级菜单
+    for (const key in thirdLevelOpen) {
+      thirdLevelOpen[key] = false
+    }
+  }
 }
 </script>
 
-<style  lang="scss" scoped>
+<style lang="scss" scoped>
 .header-bar_blank {
   width: 100%;
-  height:30px;
+  height: 30px;
 }
+
 .header-bar {
   position: fixed;
   app-drag: drag;
@@ -270,6 +389,21 @@ function closeWindow() {
 .header-bar__button__user {
   padding: 0 10px;
 }
+
+.header-bar__button__version {
+  padding: 0 8px;
+  margin-right: 5px;
+}
+
+.version-text {
+  font-size: 12px;
+  color: #666;
+  cursor: pointer;
+  padding: 2px 6px;
+  border-radius: 3px;
+  transition: all 0.2s ease;
+}
+
 .header-bar__button:hover {
   background-color: #e0e0e0;
 }
@@ -278,4 +412,80 @@ function closeWindow() {
   width: 16px;
   height: 16px;
 }
+
+.header-bar__submenu {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+}
+
+.header-bar__submenu-header {
+  display: flex;
+  align-items: center;
+  padding: 2px 10px;
+  cursor: pointer;
+}
+
+.header-bar__submenu-body {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  background: white;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 160px;
+  border-radius: 4px;
+  overflow: hidden;
+  display: none;
+  margin-top: 5px;
+
+  &.submenu-open {
+    display: block;
+  }
+}
+
+.header-bar__submenu-item {
+  display: flex;
+  align-items: center;
+  padding: 6px 12px;
+  white-space: nowrap;
+
+  &:hover {
+    background-color: #f0f0f0;
+  }
+}
+
+.header-bar__submenu-third-level {
+  position: relative;
+  display: flex;
+  flex-direction: column;
+}
+
+.header-bar__submenu-third-header {
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+
+  &:hover {
+    background-color: #f0f0f0;
+  }
+}
+
+.header-bar__submenu-third-body {
+  position: fixed;
+  margin-left: 150px;
+  top: 0;
+  left: 100%;
+  background: white;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 160px;
+  border-radius: 4px;
+  overflow: hidden;
+  display: none;
+
+  &.submenu-open {
+    display: block;
+  }
+}
 </style>

+ 91 - 45
frontend/src/components/login/index.vue

@@ -19,7 +19,6 @@
         </el-tabs>
         <el-form
             :model="loginForm"
-            :rules="loginRules"
             label-position="left"
         >
           <div class="title__main">欢迎!</div>
@@ -32,13 +31,14 @@
             </div>
             <el-input
                 v-model="loginForm.username"
-                placeholder="请输入用户名"
+                :placeholder="usernamePlaceholder"
                 name="username"
                 type="text"
                 maxlength="30"
                 style="width: 270px;"
                 tabindex="1"
                 autocomplete="on"
+                @keyup.enter.native="handleLogin"
             >
             </el-input>
           </el-form-item>
@@ -60,6 +60,7 @@
                   maxlength="30"
                   tabindex="2"
                   autocomplete="on"
+                  @keyup.enter.native="handleLogin"
               >
               </el-input>
             </el-form-item>
@@ -81,6 +82,7 @@
                   tabindex="2"
                   style="width: 270px;"
                   autocomplete="off"
+                  @keyup.enter.native="handleLogin"
               >
                 <template #append>
                   <el-button
@@ -149,6 +151,7 @@ export default defineComponent({
 import { ref, reactive, computed } from 'vue'
 import { ElMessage } from 'element-plus'
 import { getAccountCompany, selectCompany, sendCode } from '@/apis/user';
+import { syncAfterLogin } from "@/apis/setting";
 import useUserInfo from "@/stores/modules/user";
 import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'
 import { publicKey } from '@/utils/publickey'
@@ -177,46 +180,71 @@ const codeButtonText = computed(() => {
   return isCodeSending.value ? `${countdown.value}s后重新获取` : '获取验证码'
 })
 
-const loginRules = reactive({
-  username: [
-    { required: true, message: '请输入用户名', trigger: 'blur' },
-    { min: 3, max: 30, message: '长度在 3 到 30 个字符', trigger: 'blur' }
-  ],
-  password: [
-    { required: true, message: '请输入密码', trigger: 'blur' },
-    { min: 6, max: 30, message: '长度在 6 到 30 个字符', trigger: 'blur' }
-  ],
-  code: [
-    { required: true, message: '请输入验证码', trigger: 'blur' },
-    { len: 6, message: '验证码长度为6位', trigger: 'blur' }
-  ]
-})
+const usernamePlaceholder = computed(() => activeTab.value === '1' ? '请输入手机号' : '请输入用户名')
+
+const isValidPhone = (value) => /^1\d{10}$/.test(value)
 
 const handleLogin = async () => {
+  const username = loginForm.username.trim()
+  const password = loginForm.password.trim()
+  const code = loginForm.code.trim()
 
-  const  res = await  useUserInfoStore.loginAction({
-    "site":1,
-    "username":loginForm.username,
-    "password":activeTab.value === '0' ? loginForm.password : loginForm.code,
-    "type": activeTab.value,
-    "device":"aigc-photo"
-  })
-  switch(res.data.is_need_select_company){
-    case 40006:
-      showCompany()
-      break;
-    case 40007:
-      ElMessage.error('当前没有所属组织')
-      break;
-    default:
-      await  useUserInfoStore.getInfo()
-      useUserInfoStore.updateLoginShow(false)
-      setTimeout(()=>{
-        window.location.reload()
-      },100)
-        break;
+  if (!username) {
+    ElMessage.warning(activeTab.value === '1' ? '请输入手机号' : '请输入用户名')
+    return
+  }
+
+  if (activeTab.value === '0' && !password) {
+    ElMessage.warning('请输入密码')
+    return
   }
 
+  if (activeTab.value === '1') {
+    if (!isValidPhone(username)) {
+      ElMessage.warning('请输入手机号')
+      return
+    }
+    if (!code) {
+      ElMessage.warning('请输入验证码')
+      return
+    }
+  }
+  loading.value = true;
+
+  try {
+    const  res = await  useUserInfoStore.loginAction({
+      "site":1,
+      "username":loginForm.username,
+      "password":activeTab.value === '0' ? loginForm.password : loginForm.code,
+      "type": activeTab.value,
+      "device":"aigc-camera",
+      "platform":"aigc-camera"
+    })
+    switch(res.data.is_need_select_company){
+      case 40006:
+        showCompany()
+        break;
+      case 40007:
+        ElMessage.error('当前没有所属组织')
+        break;
+      default:
+        await  useUserInfoStore.getInfo()
+        // 登录成功后同步数据
+        await syncAfterLogin()
+        useUserInfoStore.updateLoginShow(false)
+        // 触发首页重新检查同步状态
+        window.dispatchEvent(new CustomEvent('login-success'));
+        setTimeout(()=>{
+        //  window.location.reload()
+        },100)
+          break;
+    }
+  } catch (error) {
+    console.error('登录失败:', error);
+   // ElMessage.error('登录失败,请重试');
+  } finally {
+    loading.value = false;
+  }
 }
 const pageStatus = ref(1)
 const companyId = ref('')
@@ -246,14 +274,28 @@ async function  showCompany() {
 // 切换组织
 async function toggleCompany() {
   if (!companyId.value) return false
-  await selectCompany({
-    id: companyId.value
-  })
-  await  useUserInfoStore.getInfo()
-  useUserInfoStore.updateLoginShow(false)
-  setTimeout(()=>{
-    window.location.reload()
-  },100)
+
+  loading.value = true;
+
+  try {
+    const res = await selectCompany({
+      id: companyId.value
+    })
+    await  useUserInfoStore.getInfo()
+    // 选择公司后也需要同步数据
+    await syncAfterLogin()
+    useUserInfoStore.updateLoginShow(false)
+    // 触发首页重新检查同步状态
+    window.dispatchEvent(new CustomEvent('login-success'));
+    setTimeout(()=>{
+      // window.location.reload()
+    },100)
+  } catch (error) {
+    console.error('切换组织失败:', error);
+    ElMessage.error('切换组织失败,请重试');
+  } finally {
+    loading.value = false;
+  }
 }
 
 const sendVerificationCode = () => {
@@ -261,6 +303,10 @@ const sendVerificationCode = () => {
     ElMessage.warning('请先输入手机号')
     return
   }
+  if (!isValidPhone(loginForm.username)) {
+    ElMessage.warning('请输入手机号')
+    return
+  }
 
   const encryptor = new JSEncrypt()
     encryptor.setPublicKey(publicKey)

+ 4 - 1
frontend/src/composables/userCheck.ts

@@ -1,5 +1,8 @@
 
 import { ElMessageBox  } from 'element-plus';
+
+import  configInfo  from '@/stores/modules/config';
+const configInfoStore = configInfo();
 export function useCheckInfo() {
 
         if(localStorage.getItem('check') === 'false'){
@@ -11,7 +14,7 @@ export function useCheckInfo() {
             }
         })
         function ShowError(){
-
+            if(configInfoStore.appModel === 2)  return;
             ElMessageBox({
                 title:"连接出错!",
                 message:'设备连接出错,请在主窗口中重新连接设备后,在重新打开此窗口后进行操作',

+ 14 - 0
frontend/src/config.json

@@ -0,0 +1,14 @@
+{
+    "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"
+    }
+}

+ 17 - 0
frontend/src/main.ts

@@ -6,6 +6,8 @@ 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'
+import useUserInfo from './stores/modules/user'
 
 const app = createApp(App)
 app.use(ElementPlus)
@@ -14,4 +16,19 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
 }
 app.use(pinia)
 app.use(router)
+
+// 注册埋点指令和路由监听 - 确保在router使用后立即注册
+lissenLog(app)
+log(router)
+
 app.mount('#app')
+
+// 刷新后根据会话标记显示登录弹窗
+try {
+    const flag = sessionStorage.getItem('NEED_LOGIN_MODAL')
+    if (flag) {
+        const userStore = useUserInfo()
+        userStore.updateLoginShow(true)
+        sessionStorage.removeItem('NEED_LOGIN_MODAL')
+    }
+} catch {}

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

@@ -4,6 +4,8 @@ import { createRouter, createWebHistory, createWebHashHistory, RouteRecordRaw }
 
 import { authGuard } from './plugins/authGuard'
 
+import otaRoutes from "./module/ota";
+
 import tpl from './model/tpl'
 
 const routes: RouteRecordRaw[] = [
@@ -16,6 +18,7 @@ const routes: RouteRecordRaw[] = [
         name: "home",
         component: () => import("@/views/Home/index.vue"),
         meta: {
+            title: '首页',
             noAuth: true,
         },
     },
@@ -44,6 +47,14 @@ const routes: RouteRecordRaw[] = [
         }
     },
     {
+        path: "/photography/processImage",
+        name: "PhotographyProcessImage",
+        component: () => import("@/views/Photography/processImage.vue"),
+        meta: {
+            title: '处理图像'
+        }
+    },
+    {
         path: "/photography/detail",
         name: "PhotographyDetail",
         component: () => import("@/views/Photography/detail.vue"),
@@ -76,6 +87,7 @@ const routes: RouteRecordRaw[] = [
         }
     },
     ...tpl,
+    ...otaRoutes,
 ];
 
 const router = createRouter({

+ 14 - 0
frontend/src/router/module/ota.ts

@@ -0,0 +1,14 @@
+
+
+const otaRoutes = [
+    {
+        path: "/ota",
+        name: "ota",
+        component: () => import("@/views/OTA/index.vue"),
+        meta: {
+            title: '版本更新'
+        }
+    },
+];
+
+export default otaRoutes;

+ 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()

+ 6 - 7
frontend/src/stores/modules/check.ts

@@ -88,7 +88,7 @@ export const checkInfo = defineStore('checkInfo', () => {
             clearInterval(CKTimerInterval)
             checkTime.value = 0
             mcu.isInitSend = false
-            return '拍照机连接失败,请重新检查,可以尝试重新启动拍照机,插拔USB口,强制初始化等';
+            return '拍照机连接失败,请重新检查,可以尝试重新启动拍照机,插拔USB口,强制初始化等,请检查相机和支撑转盘是否待机休眠。如果确认未休眠,请检查各种连接线是否,正常连上。然后再重试一次,一直无法解决问题,请致电:15957854217,18805712182。';
         }
         for (const device of Object.values(devices)) {
             if (device.status === -1  && device.msg_type !== 'connect_bluetooth') {
@@ -110,13 +110,7 @@ export const checkInfo = defineStore('checkInfo', () => {
 
               clientStore.ipc.removeAllListeners(icpList.camera.connect);
               clientStore.ipc.send(icpList.camera.connect,configInfoStore.digiCamControlPath);
-              clientStore.ipc.on(icpList.camera.digiCamControlPath, async (event, result) => {
-                clientStore.ipc.removeAllListeners(icpList.camera.digiCamControlPath);
-                if(result.code === 0 && result.data){
-                    configInfoStore.updateDigiCamControlPath(result.data)
-                }
 
-              })
               clientStore.ipc.on(icpList.camera.connect, async (event, result) => {
 
 
@@ -128,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 = '相机未连接,请链接相机。';
+                  }
               });
 
     }

+ 27 - 1
frontend/src/stores/modules/config.ts

@@ -1,13 +1,37 @@
 import { defineStore } from 'pinia';
 import { ref, computed } from 'vue';
+import client from "./client";
+import icpList from "../../utils/ipc";
 
+const clientStore = client();
 
 export const configInfo = defineStore('config',()=>{
+  //作废了
   const digiCamControlPath = ref("C:\\Program Files (x86)\\digiCamControl")
   const updateDigiCamControlPath = (data:string)=>{
     digiCamControlPath.value = data
   }
 
+  const appConfig = ref({})
+
+
+  const getAppConfig = async ()=>{
+    return new Promise((resolve, reject) => {
+      clientStore.ipc.send(icpList.utils.getAppConfig);
+      clientStore.ipc.on(icpList.utils.getAppConfig, async (event, result) => {
+        appConfig.value = result;
+        resolve(appConfig.value)
+      })
+    })
+  }
+
+
+  clientStore.ipc.send(icpList.utils.getAppConfig);
+  clientStore.ipc.on(icpList.utils.getAppConfig, async (event, result) => {
+    appConfig.value = result;
+  })
+
+
   //  1 为拍照并处理图像 2 为仅处理图像
   const appModel = ref(1)
   const updateAppModel = (data:number)=>{
@@ -17,7 +41,9 @@ export const configInfo = defineStore('config',()=>{
     digiCamControlPath,
     updateDigiCamControlPath,
     appModel,
-    updateAppModel
+    updateAppModel,
+    appConfig,
+    getAppConfig,
   }
 },{
   persist:true,

+ 5 - 1
frontend/src/stores/modules/token.ts

@@ -9,10 +9,14 @@ export const tokenInfo = defineStore('tokenInfo',()=>{
   const updateToken = (data:string)=>{
     token.value = data
   }
+  const clearToken = () => {
+    token.value = ''
+  }
   return {
     token,
     getToken,
-    updateToken
+    updateToken,
+    clearToken
   }
 },{
   persist:true,

+ 47 - 9
frontend/src/stores/modules/user.ts

@@ -2,6 +2,8 @@ import { defineStore } from 'pinia';
 import { ref, computed } from 'vue';
 import { getUserInfo, login } from '@/apis/user';
 import tokenInfo from '@/stores/modules/token';
+import client from '@/stores/modules/client';
+import { useRouter } from 'vue-router';
 
 
 export const useUserInfo = defineStore('userInfo', () => {
@@ -63,18 +65,48 @@ export const useUserInfo = defineStore('userInfo', () => {
 
 
   /**
-   * 执行用户登录操作。
+   * 执行用户退出登录操作。
    *
-   * @param {any} data - 登录所需的用户凭据
-   * @returns {Promise<any>} 登录接口返回的结果。
-   * @throws {Error} 如果登录失败,抛出错误。
+   * @param {any} data - 退出登录所需的参数
+   * @returns {Promise<any>} 退出登录的结果。
+   * @throws {Error} 如果退出登录失败,抛出错误。
    */
   const loginOut = async (data: any) => {
     try {
-      await updateToken(''); // 更新登录令牌
-      await updateUserInfo({})
+      // 关闭所有子窗口
+      try {
+        const clientStore = client();
+        await clientStore.ipc.invoke('controller.utils.closeAllWindows');
+        console.log('所有子窗口已关闭');
+      } catch (error) {
+        console.error('关闭子窗口失败:', error);
+        // 关闭窗口失败不影响退出登录流程
+      }
+
+      // 清空用户信息
+      try {
+        tokenInfoStore.clearToken()
+      } catch (e) {
+        await updateToken('')
+      }
+      await updateUserInfo({});
+
+      // 跳转到首页
+      try {
+        // 在Electron环境中使用正确的协议
+        if (window.location.protocol === 'file:') {
+          window.location.href = window.location.href.split('#')[0] + '#/';
+          window.location.reload()
+        } else {
+          window.location.href = '/';
+        }
+        console.log('已跳转到首页');
+      } catch (error) {
+        console.error('跳转到首页失败:', error);
+        // 跳转失败不影响退出登录流程
+      }
     } catch (error) {
-      console.error('登录失败:', error);
+      console.error('退出登录失败:', error);
       throw error;
     }
   };
@@ -87,10 +119,16 @@ export const useUserInfo = defineStore('userInfo', () => {
    */
   const getInfo = async () => {
     try {
-      const res = await getUserInfo(); // 调用获取用户信息接口
+      const res = await getUserInfo({
+        device:'aigc',
+      }); // 调用获取用户信息接口
       const { data } = res;
       if (!data) {
-        updateToken(''); // 如果没有数据,清空令牌
+        try {
+          tokenInfoStore.clearToken()
+        } catch (e) {
+          updateToken('')
+        }
         throw new Error('请重新登录!');
       }
       updateUserInfo(data); // 更新用户信息

+ 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
+      }
+    ]
+  }
+}) 

+ 10 - 3
frontend/src/styles/pub.scss

@@ -105,8 +105,8 @@
 
 
 
-::-webkit-scrollbar {/*滚动条整体样式*/ width: 8px;     /*高宽分别对应横竖滚动条的尺寸*/ height: 8px; background: #000;}
-::-webkit-scrollbar-thumb {/*滚动条里面小方块*/ border-radius: 10px;  background: #ddd;}
+::-webkit-scrollbar {/*滚动条整体样式*/ width: 8px;     /*高宽分别对应横竖滚动条的尺寸*/ height: 8px; background: #fff;}
+::-webkit-scrollbar-thumb {/*滚动条里面小方块*/ border-radius: 10px;  background: #999999;}
 ::-webkit-scrollbar-track {/*滚动条里面轨道*/ -webkit-box-shadow: none; border-radius: 10px; background: none;}
 
 
@@ -120,9 +120,16 @@
 .anm { transition: 0.5s;  }
 
 .page—wrap {
-  width: 1200px;
+  width: 1360px;
   margin:  0 auto;
 }
+.max-w-full{
+  max-width: 100%;
+}
+
+.bg-F5F6F7 {
+  background: #F5F6F7;
+}
 
 .el-button__primary {
   background: #2CFFFC;

+ 4 - 0
frontend/src/utils/appconfig.ts

@@ -1,3 +1,7 @@
+
+import config from "@/config.json";
+
+export const configs = config;
 export const imagesUpload = {
   accept: ['jpg', 'png', 'gif'],
   fileSize: 2048 * 10,

+ 29 - 0
frontend/src/utils/appfun.ts

@@ -1,4 +1,9 @@
 
+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){
     if(file_path) return file_path;
@@ -10,3 +15,27 @@ export  function getFilePath (file_path){
 export  function getRouterUrl (href){
     return window.location.origin+window.location.pathname+href
 }
+
+
+
+//获取TKK地址
+export  function getWebUrlrUrl (config:{
+    url:string,
+    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 +=  '&'
+        params += Object.keys(config.query).map(key => {
+            return encodeURIComponent(key) + '=' + encodeURIComponent(config.query[key])
+        }).join('&')
+    }
+    let url  = tkkWebUrl + config.url + params
+    return url
+}

+ 82 - 14
frontend/src/utils/http.ts

@@ -2,6 +2,9 @@ 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";
+import client from '@/stores/modules/client';
 
 // 加载动画的并发管理
 const activeRequests = new Set<string>();
@@ -19,23 +22,82 @@ function loadingClose(requestId: string) {
 }
 
 /**
+ * 处理token失效
+ * 关闭所有子窗口,清空用户信息,跳转到首页
+ */
+async function handleTokenExpiry() {
+    try {
+        // 关闭所有子窗口
+        try {
+            const clientStore = client();
+            await clientStore.ipc.invoke('controller.utils.closeAllWindows');
+            console.log('Token失效:所有子窗口已关闭');
+        } catch (error) {
+            console.error('Token失效:关闭子窗口失败:', error);
+        }
+
+        // 清空用户信息
+        const useUserInfoStore = useUserInfo();
+        try {
+            const tokenStore = tokenInfo();
+            tokenStore.clearToken();
+        } catch (e) {
+            await useUserInfoStore.updateToken('');
+        }
+        await useUserInfoStore.updateUserInfo({});
+
+        // 跳转到首页(在会话存储中标记需要显示登录框)
+        try {
+            sessionStorage.setItem('NEED_LOGIN_MODAL', '1');
+            // 在Electron环境中使用正确的协议
+            if (window.location.protocol === 'file:') {
+                window.location.href = window.location.href.split('#')[0] + '#/';
+                window.location.reload()
+            } else {
+                window.location.href = '/';
+            }
+            console.log('Token失效:已跳转到首页');
+        } catch (error) {
+            console.error('Token失效:跳转到首页失败:', error);
+        }
+        // 刷新后在 main.ts 中处理显示登录弹窗
+    } catch (error) {
+        console.error('Token失效处理失败:', error);
+    }
+}
+
+/**
  * 创建一个axios实例,用于发送HTTP请求
  * 配置了请求拦截器和响应拦截器,支持加载动画和错误提示
  */
 const service = axios.create({
-    timeout: 5000, // 设置请求超时时间
-    baseURL: '__API__',
+    timeout: 10000, // 设置请求超时时间
+   // 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
-        if (token) {
-            config.headers['Authorization'] = `Bearer ${token}`;
+        const token = tokenInfoStore.getToken; // computed ref
+        const tokenValue = typeof token === 'object' && token !== null && 'value' in token ? (token as any).value : token as any;
+        if (tokenValue) {
+            config.headers['Authorization'] = `Bearer ${tokenValue}`;
         }
 
         // 如果配置中启用了加载动画,则显示加载动画
@@ -76,12 +138,16 @@ service.interceptors.response.use(
         if (res.code !== 0) {
             switch (res.code) {
                 case 401:
-                    Message({
-                        message: '登录状态已失效,请重新登录',
-                        type: 'error',
-                        duration: 3 * 1000,
-                    });
-                    useUserInfoStore.updateLoginShow(true)
+                    setTimeout(()=>{
+
+                        Message({
+                            message: '登录状态已失效,请重新登录',
+                            type: 'error',
+                            duration: 3 * 1000,
+                        });
+                    },1000)
+                    // 处理token失效
+                    handleTokenExpiry();
                     break;
                 default:
                     if (response.config.showErrorMessage) {
@@ -121,7 +187,8 @@ service.interceptors.response.use(
                 switch (error.response.status) {
                     case 400: errMessage = '请求错误(400)'; break;
                     case 401: errMessage = '登录状态已失效,请重新登录';
-                    useUserInfoStore.updateLoginShow(true)
+                    // 处理token失效
+                    handleTokenExpiry();
                      break;
                     case 403: errMessage = '拒绝访问(403)'; break;
                     case 404: errMessage = '请求出错(404)'; break;
@@ -169,10 +236,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,

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

@@ -19,7 +19,10 @@ const icpList = {
         shellFun: 'controller.utils.shellFun',
         openDirectory:"controller.utils.openDirectory",
         openImage:"controller.utils.openImage",
-        openFile:"controller.utils.openFile"
+        getAppConfig:"controller.utils.getAppConfig",
+        openFile:"controller.utils.openFile",
+        runExternalTool:"controller.utils.runExternalTool",
+        closeAllWindows: 'controller.utils.closeAllWindows'
     },
     setting:{
         getDeviceConfigDetail: 'controller.setting.getDeviceConfigDetail',
@@ -31,7 +34,9 @@ const icpList = {
         updateLeftRightConfig:'controller.setting.updateLeftRightConfig',
         getSysConfig: 'controller.setting.getSysConfig',
         updateSysConfigs: 'controller.setting.updateSysConfigs',
-        updateTabName: 'controller.setting.updateTabName'
+        updateTabName: 'controller.setting.updateTabName',
+        syncSysConfigs: 'controller.setting.syncSysConfigs',
+        syncActions: 'controller.setting.syncActions'
     },
     takePhoto:{
         getPhotoRecords: 'controller.takephoto.getPhotoRecords',
@@ -44,6 +49,9 @@ const icpList = {
         addLogo: 'controller.generate.addLogo',
         getLogoList: 'controller.generate.getLogoList',
         deleteLogo: 'controller.generate.deleteLogo',
+    },
+    ota:{
+        updateVersion: 'controller.ota.updateVersion'
     }
 
 

+ 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: '测试埋点' }
+  });
+}

+ 84 - 0
frontend/src/utils/menus/generate.ts

@@ -0,0 +1,84 @@
+
+import icpList from '@/utils/ipc'
+import { getRouterUrl } from '@/utils/appfun'
+import client from '@/stores/modules/client'
+import {useRouter} from "vue-router";
+import { getWebUrlrUrl } from '@/utils/appfun'
+const Router = useRouter()
+
+const clientStore = client()
+const generate =  {
+        name:'高级生成',
+        children:[
+            {
+                name:'模特图',
+                click(configs){
+                    console.log(configs);
+                    openGaenrate('onFeetImage',configs)
+                },
+            },
+            {
+                name:'场景图',
+                click(configs){
+                    openGaenrate('attachScenarios',configs)
+                },
+            },
+            {
+                name:'生成视频',
+                click(configs){
+                    openGaenrate('video',configs)
+                },
+            },{
+                name:'历史记录',
+                click(configs){
+                    openGaenrate('mine',configs)
+                },
+            }
+        ]
+    }
+
+
+
+
+
+export function openGaenrate(type,configs) {
+
+    const config= {
+        "onFeetImage":{
+            url:"/onFeetImage",
+        },
+        "attachScenarios":{
+            url:"/attachScenarios"
+        },
+        "video":{
+            url:"/create_video"
+        },
+        "mine":{
+            url:"/mine"
+        }
+    }
+
+    let urlParams = config[type]
+    if(configs){
+        urlParams = {
+            ... config[type],
+            ...configs
+        }
+    }
+
+
+    clientStore.ipc.removeAllListeners(icpList.utils.openMain)
+    let params = {
+        title: '高级生成',
+        width: 1400,
+        height: 900,
+        frame: true,
+        id: 'generate',
+        url: getWebUrlrUrl(urlParams)
+    }
+    clientStore.ipc.send(icpList.utils.openMain, params)
+}
+
+
+
+export default generate;

+ 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>

+ 6 - 0
frontend/src/views/Developer/normal.vue

@@ -141,6 +141,7 @@ const clientStore = client();
 const socketStore = socket()
 
 
+const status = ref(0);
 const editRowData = ref({
   "camera_high_motor_deviation": '',
   "camera_steering_deviation": '',
@@ -169,6 +170,7 @@ async function  get_deviation(){
       console.log('_get_deviation_data')
       console.log(result)
       if(result.code === 0){
+        status.value = true;
         editRowData.value.camera_high_motor_deviation = result.data.camera_high_motor_deviation
         editRowData.value.camera_steering_deviation = result.data.camera_steering_deviation
         editRowData.value.turntable_steering_deviation = result.data.turntable_steering_deviation
@@ -229,6 +231,10 @@ async  function  AllChangeNum (){
 
 //设置 移动 调整
 async function changeNum(action_name, type, key, min, max) {
+  if(!status.value){
+    ElMessage.error('请先获取设备参数');
+    return;
+  }
   if(key && (min || max)){
     if(editRowData.value[key] < min || editRowData.value[key] > max){
       if(editRowData.value[key] < min){

+ 272 - 38
frontend/src/views/Home/index.vue

@@ -1,18 +1,25 @@
 <template>
-  <headerBar title="首页" />
-  <div class="home-container" v-loading="loading">
+  <headerBar title="首页">
+
+    <template  #title><div @click="handleSettingClick" v-log="{ describe: { action: '点击首页标题' } }">首页</div></template>
+  </headerBar>
+  <div
+    class="home-container"
+    v-loading="loading || !healthReady || !syncCompleted"
+    :element-loading-text="loadingText"
+  >
     <!-- 背景图片 -->
     <img src="@/assets/images/home/bg.png" alt="背景图片" class="background-image" />
 
     <!-- 左侧图片区域 -->
-    <div class="image-container left-image" @click="goCheck">
-      <img src="@/assets/images/home/left.jpg" alt="拍摄产品并处理图像" class="zoom-on-hover" />
+    <div class="image-container left-image" @click="goCheck" v-log="{ describe: { action: '点击拍照检查入口' } }">
+      <img src="@/assets/images/home/left.png" alt="拍摄产品并处理图像" class="zoom-on-hover" />
       <div class="overlay-text">拍摄产品<br>并处理图像</div>
     </div>
 
     <!-- 右侧图片区域 -->
-    <div class="image-container right-image" @click="goShot">
-      <img src="@/assets/images/home/right.jpg" alt="仅处理图像" class="zoom-on-hover" />
+    <div class="image-container right-image"  @click="goShot" v-log="{ describe: { action: '点击仅处理图像入口' } }">
+      <img src="@/assets/images/home/right.png" alt="仅处理图像" class="zoom-on-hover" />
       <div class="overlay-text" style="line-height: 80px;">仅处理图像</div>
     </div>
   </div>
@@ -22,56 +29,280 @@
 import headerBar from "@/components/header-bar/index.vue";
 import { useRouter } from "vue-router";
 import configInfo from '@/stores/modules/config';
-import { ref, onMounted } from 'vue';
+import { ref, onMounted, onUnmounted, computed } from 'vue';
 import axios from 'axios';
+import client from "@/stores/modules/client";
+import icpList from '@/utils/ipc';
+import packageJson from '@/../../package.json';
+import { getRouterUrl } from '@/utils/appfun';
+import useUserInfo from "@/stores/modules/user";
+import tokenInfo from "@/stores/modules/token";
 
-const configInfoStore = configInfo();
 const router = useRouter();
 const loading = ref(true);
+const healthReady = ref(false); // 程序是否已完成自检
+const syncLoading = ref(false); // 同步配置的loading状态
+const syncCompleted = ref(false); // 同步是否完成
+const loadingText = computed(() => {
+  if (!healthReady.value) {
+    return '程序启动中...';
+  }
+  if (!syncCompleted.value) {
+    return '正在同步配置...';
+  }
+  return '正在加载...';
+});
+
+// 用户状态管理 - 在 onMounted 中初始化
+let configInfoStore: any;
+let useUserInfoStore: any;
+let tokenInfoStore: any;
+
+// 版本检查相关
+const currentVersion = ref(packageJson.version);
+const latestVersion = ref('');
+const isLatest = ref(true);
 
 
 import socket from "@/stores/modules/socket";
+import {ElMessage} from "element-plus";
 // 初始化 WebSocket 状态管理
 const socketStore = socket();
 
 function socketConnect(){
-    socketStore.connectSocket();
+  socketStore.connectSocket();
 }
 
-const goCheck = () => {
-    configInfoStore.updateAppModel(1);
-    router.push({
-        name: 'PhotographyCheck'
-    });
+const goCheck = async () => {
+  // 检查登录状态
+  if (!tokenInfoStore.getToken) {
+    useUserInfoStore.updateLoginShow(true);
+    return;
+  }
+
+  // 如果正在同步,显示提示
+  if (syncLoading.value) {
+    console.log('正在同步配置,请稍候...');
+    return;
+  }
+
+  // 如果未同步完成,等待同步
+  if (!syncCompleted.value) {
+    ElMessage.error('等待配置同步完成');
+    return;
+  }
+
+  configInfoStore.updateAppModel(1);
+  router.push({
+    name: 'PhotographyCheck'
+  });
 };
 
-const goShot = () => {
-    socketConnect();
-    configInfoStore.updateAppModel(2);
-    router.push({
-        name: 'PhotographyShot'
-    });
+const goShot = async () => {
+  // 检查登录状态
+  if (!tokenInfoStore.getToken) {
+    useUserInfoStore.updateLoginShow(true);
+    return;
+  }
+
+  // 如果正在同步,显示提示
+  if (syncLoading.value) {
+    console.log('正在同步配置,请稍候...');
+    return;
+  }
+
+  // 如果未同步完成,等待同步
+  if (!syncCompleted.value) {
+    console.log('等待配置同步完成...');
+    return;
+  }
+
+  socketConnect();
+  configInfoStore.updateAppModel(2);
+  router.push({
+    name: 'PhotographyProcessImage'
+  });
 };
 
 // 健康检查函数
 const checkHealth = async () => {
-    try {
-        const response = await axios.get('http://127.0.0.1:7074');
-        if (response.status === 200) {
-            loading.value = false; // 健康检查成功,关闭 loading
+  loading.value = false;
+  try {
+    const healthUrl = configInfoStore?.appConfig?.pyapp ? 'http://'+configInfoStore?.appConfig?.pyapp+':7074' :  'http://127.0.0.1:7074'
+    const response = await axios.get(healthUrl);
+    if (response.status === 200) {
+      loading.value = false; // 健康检查成功,关闭 loading
+      healthReady.value = true;
+
+      // 健康检查成功后,如果用户已登录则执行数据同步
+      if (tokenInfoStore && tokenInfoStore.getToken) {
+        const token = tokenInfoStore.getToken;
+        if (token && token.trim() !== '') {
+          try {
+            syncLoading.value = true; // 开始同步
+            syncCompleted.value = false; // 重置同步状态
+
+            // 导入同步函数
+            const { syncAfterLogin } = await import('@/apis/setting');
+            await syncAfterLogin();
+            console.log('健康检查后数据同步成功');
+
+            syncCompleted.value = true; // 同步完成
+          } catch (syncError) {
+            console.error('健康检查后数据同步失败:', syncError);
+            syncCompleted.value = false; // 同步失败
+            // 同步失败不影响主流程
+          } finally {
+            syncLoading.value = false; // 结束同步loading
+          }
+        } else {
+          // 未登录状态,直接设置同步完成
+          syncCompleted.value = true;
         }
-    } catch (error) {
-        console.error('健康检查失败:', error);
-        setTimeout(() => {
-            checkHealth(); // 延迟检查
-        }, 2000);
-        // 可以在这里处理错误,例如显示错误提示
+      } else {
+        // 未登录状态,直接设置同步完成
+        syncCompleted.value = true;
+      }
     }
+  } catch (error) {
+    console.error('健康检查失败:', error);
+    healthReady.value = false;
+    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秒内未再次点击则重置计数器
+}
+
+function openResourceDirectory() {
+  const clientStore = client();
+  clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
+  let params = {
+    action: 'openPath',
+    params: configInfoStore.appConfig.userDataPath.replaceAll('/', '\\')
+  };
+  clientStore.ipc.send(icpList.utils.shellFun, params);
+}
+
+// 版本号比较函数
+const compareVersions = (v1, v2) => {
+  const parts1 = v1.split('.').map(Number);
+  const parts2 = v2.split('.').map(Number);
+
+  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
+    const num1 = parts1[i] || 0;
+    const num2 = parts2[i] || 0;
+
+    if (num1 > num2) return 1;
+    if (num1 < num2) return -1;
+  }
+
+  return 0;
+};
+
+// 打开OTA窗口
+const openOTA = () => {
+  const { href } = router.resolve({
+    name: 'ota'
+  });
+
+  const clientStore = client();
+  clientStore.ipc.removeAllListeners(icpList.utils.openMain);
+  let params = {
+    title: '版本更新',
+    width: 900,
+    height: 700,
+    frame: true,
+    id: 'ota',
+    url: getRouterUrl(href)
+  };
+  clientStore.ipc.send(icpList.utils.openMain, params);
+};
+
+// 获取版本信息并检查更新
+const checkForUpdates = async () => {
+  try {
+    // 添加时间戳避免缓存问题
+    const timestamp = new Date().getTime();
+    const response = await axios.get('https://ossimg.valimart.net/frontend/html/zhihuiyin/version.json', {
+      params: {
+        _t: timestamp
+      }
+    });
+
+    // 确保 response.data 是 JSON 数据
+    let data;
+    if (typeof response.data === 'string') {
+      data = JSON.parse(response.data);
+    } else {
+      data = response.data;
+    }
+
+    if (data.length > 0) {
+      const latest = data[data.length - 1];
+      latestVersion.value = latest.version;
+
+      // 比较版本号
+      isLatest.value = compareVersions(currentVersion.value, latest.version) >= 0;
+
+      // 如果发现新版本,自动打开OTA窗口
+      if (!isLatest.value) {
+        openOTA();
+      }
+    }
+  } catch (error) {
+    console.error('检查版本更新失败:', error);
+    // 静默处理错误,不影响用户体验
+  }
+};
+
+
+
+
+
+// 监听登录成功事件
+const handleLoginSuccess = () => {
+  console.log('检测到登录成功,重新检查同步状态');
+  // 重新执行健康检查和同步
+  checkHealth();
+};
+
+// 在组件挂载时执行健康检查和版本检查
 onMounted(() => {
-    checkHealth();
+  // 初始化 store
+  configInfoStore = configInfo();
+  useUserInfoStore = useUserInfo();
+  tokenInfoStore = tokenInfo();
+
+  // 监听登录成功事件
+  window.addEventListener('login-success', handleLoginSuccess);
+
+  checkHealth();
+  // 延迟执行版本检查,避免影响健康检查
+  setTimeout(() => {
+    checkForUpdates();
+  }, 1000);
+});
+
+// 组件卸载时清理事件监听器
+onUnmounted(() => {
+  window.removeEventListener('login-success', handleLoginSuccess);
 });
 </script>
 
@@ -80,7 +311,7 @@ onMounted(() => {
 .home-container {
   position: relative;
   width: 100%;
-  height: 100vh;
+  height: calc(100vh - 30px);
   overflow: hidden;
 }
 
@@ -98,21 +329,24 @@ onMounted(() => {
   position: absolute;
   cursor: pointer;
   width: 400px; /* 设置宽度 */
-  height: 400px; /* 设置高度 */
   overflow: hidden;
-  box-shadow: 0 4px 10px rgba(0, 0, 0, 0.6); /* 添加阴影效果 */
+  // box-shadow: 0 4px 10px rgba(0, 0, 0, 0.6); /* 添加阴影效果 */
+  border-radius: 30px;
+  transition: transform 0.3s ease;
 
   .zoom-on-hover {
     transition: transform 0.3s ease;
     width: 100%; /* 确保图片充满容器 */
     height: 100%; /* 确保图片充满容器 */
     object-fit: cover; /* 裁剪图片以适应容器 */
+    display: block;
   }
 
-  &:hover .zoom-on-hover {
-    transform: scale(1.1);
+  &:hover {
+    box-shadow: 0 4px 10px rgba(0, 0, 0, 0.6);
+    transform: translateY(-55%);
+    transform:translateY(-55%) scale(1.05);
   }
-
 }
 
 .left-image {

+ 203 - 0
frontend/src/views/OTA/index.vue

@@ -0,0 +1,203 @@
+<script setup>
+import { ref, onMounted, computed } from 'vue';
+import axios from 'axios';
+import packageJson from '@/../../package.json';
+import { ElMessage } from 'element-plus';
+import client from "@/stores/modules/client";
+import  icpList from '@/utils/ipc'
+import socket from "@/stores/modules/socket";
+import UpdateDialog from '@/components/UpdateDialog'
+
+const currentVersion = ref(packageJson.version);
+const latestVersion = ref('');
+const isLatest = ref(true);
+const versions = ref([]); // 所有版本数据
+
+// 分页相关
+const currentPage = ref(1);
+const pageSize = ref(20);
+const totalItems = ref(0);
+
+// 计算属性:获取当前页的数据
+const paginatedVersions = computed(() => {
+  const start = (currentPage.value - 1) * pageSize.value;
+  const end = start + pageSize.value;
+  return (versions.value || []).slice(start, end).reverse();
+});
+
+// 获取版本信息
+const fetchVersions = async () => {
+  try {
+    // 添加时间戳避免缓存问题
+    const timestamp = new Date().getTime();
+    const response = await axios.get('https://ossimg.valimart.net/frontend/html/zhihuiyin/version.json', {
+      params: {
+        _t: timestamp
+      }
+    });
+
+    // 确保 response.data 是 JSON 数据
+    let data;
+    if (typeof response.data === 'string') {
+      data = JSON.parse(response.data);
+    } else {
+      data = response.data;
+    }
+
+    versions.value = data;
+
+    if (data.length > 0) {
+      const latest = data[data.length - 1];
+      latestVersion.value = latest.version;
+
+      // 比较版本号
+      isLatest.value = compareVersions(currentVersion.value, latest.version) >= 0;
+    }
+
+    // 初始化分页信息
+    totalItems.value = versions.value.length;
+  } catch (error) {
+    console.error('获取版本信息失败:', error);
+    ElMessage.error('无法获取版本信息');
+  }
+};
+
+// 版本号比较函数
+const compareVersions = (v1, v2) => {
+  const parts1 = v1.split('.').map(Number);
+  const parts2 = v2.split('.').map(Number);
+
+  for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
+    const num1 = parts1[i] || 0;
+    const num2 = parts2[i] || 0;
+
+    if (num1 > num2) return 1;
+    if (num1 < num2) return -1;
+  }
+
+  return 0;
+};
+
+// 下载最新版本
+const downloadUpdate = () => {
+  downloadSpecificVersion(versions.value[versions.value.length - 1].url)
+};
+
+
+const updateVisible = ref( false)
+const updateResult = ref({})
+
+const clientStore = client();
+const socketStore = socket()
+// 下载特定版本
+const downloadSpecificVersion = (url) => {
+
+  clientStore.ipc.removeAllListeners('app.updater');
+  clientStore.ipc.removeAllListeners(icpList.ota.updateVersion);
+  clientStore.ipc.send(icpList.ota.updateVersion,url);
+  clientStore.ipc.on(icpList.ota.updateVersion, async (event, result) => {
+    console.log('==============================')
+    console.log('checkUpdate')
+    console.log(event)
+    console.log(result)
+
+  })
+
+  clientStore.ipc.on('app.updater', async (event, result) => {
+
+
+    try {
+      let res =  JSON.parse(result)
+      if([1,3,4].includes(res.status)){
+        updateResult.value =  res
+        updateVisible.value = true
+        console.log(updateResult.value)
+        console.log(updateVisible.value)
+      }else{
+        updateVisible.value = false
+        ElMessage.error('下载失败')
+      }
+    }catch (e) {
+      console.log(e)
+    }
+
+
+  })
+
+
+
+};
+
+// 处理分页变化
+const handlePageChange = (page) => {
+  currentPage.value = page;
+};
+
+onMounted(() => {
+  fetchVersions();
+});
+</script>
+
+<template>
+  <el-container>
+
+
+    <el-dialog
+        v-model="updateVisible"
+        :close-on-click-modal="false"
+        title="软件下载中"
+    >
+        <div class="desc line-30 fs-16">{{updateResult.desc}}</div>
+    </el-dialog>
+    <el-main>
+      <div class="version-check-container">
+        <el-card class="current-version-card">
+          <p>当前版本  <span class="fs-14">版本号: {{ currentVersion }}</span></p>
+
+          <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" v-log="{ describe: { action: '点击下载最新版本', latestVersion } }">下载更新</el-button>
+        </el-card>
+
+        <el-card class="history-versions-card mar-top-10">
+          <h3>历史版本</h3>
+          <!-- 使用计算属性获取分页数据 -->
+          <el-table :data="paginatedVersions" border>
+            <el-table-column prop="version" label="版本号" width="70"></el-table-column>
+            <el-table-column prop="date" label="发布日期" width="100"></el-table-column>
+            <el-table-column label="描述">
+              <template #default="{ row }">
+                <el-tooltip :content="row.describe" placement="top" :show-when="hover" :width="500">
+                  <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)" v-log="{ describe: { action: '点击下载历史版本', version: row.version, url: row.url } }">下载</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <el-pagination
+            layout="prev, pager, next"
+            :total="totalItems"
+            :page-size="pageSize"
+            v-model:current-page="currentPage"
+            @current-change="handlePageChange"
+            style="margin-top: 15px;"
+          />
+        </el-card>
+      </div>
+    </el-main>
+  </el-container>
+</template>
+
+<style scoped>
+.version-describe {
+  display: inline-block;
+  width: 100%; /* 根据需要调整 */
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+</style>

+ 101 - 28
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>
@@ -64,6 +66,7 @@
       @confirm="confirm"
       ref="editData"
       @onClose="onClose"
+      @onRunMcuSingle="onRunMcuSingle"
       :addRowData="addRowData"
     />
 
@@ -86,6 +89,10 @@ const socketStore = socket(); // WebSocket状态管理实例
 
 const emit = defineEmits([ 'confirm','onClose']);
 
+
+import  configInfo  from '@/stores/modules/config';
+const configInfoStore = configInfo();
+
 const confirm = ()=>{
   hideVideo()
   emit('confirm')
@@ -120,14 +127,17 @@ import { digiCamControlWEB } from  '@/utils/appconfig'
 import { getFilePath } from '@/utils/appfun'
 import {ElMessage} from "element-plus";
 const previewKey = ref(0)
+
 const preview = ref(digiCamControlWEB+'liveview.jpg')
 
+
+
 const previewSrc = computed(()=>{
   let time = new Date().getTime()
   return preview.value+'?key='+previewKey.value+'&time='+time
 })
 const step = ref(1)
-function checkConfirm(init){
+async function checkConfirm(init){
   step.value =1
   if(menu.length === 0){
     menu.push({
@@ -141,8 +151,16 @@ function checkConfirm(init){
 
   showVideo()
   showrEditRow.value = true
+  loading.value = false;
 }
 
+const init = ref(true)
+function onRunMcuSingle (){
+  if(init.value) {
+    loading.value = false
+    init.value = false
+  }
+}
 const showrEditRow = ref(false)
 
 
@@ -160,7 +178,7 @@ function showVideo(){
         interval = setInterval(()=>{
           previewKey.value++;
         },200)
-      },2000)
+      },500)
 
     })
 
@@ -176,10 +194,14 @@ function hideVideo(){
 
 }
 
-const loading = ref(false)
+function goArts(){
+
+}
+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);
 
@@ -212,31 +234,70 @@ function takePictures() {
   }
 }
 
+// 获取主图
+function  createMainImage (file_path){
+
+  loading.value = true;
+  clientStore.ipc.removeAllListeners(icpList.takePhoto.createMainImage);
+  clientStore.ipc.send(icpList.takePhoto.createMainImage,{
+    file_path:file_path
+  });
+  clientStore.ipc.on(icpList.takePhoto.createMainImage, async (event, result) => {
+    if(result.code === 0 && result.data?.main_out_path){
+      imageTplPath.value  = result.data?.main_out_path
+      hideVideo()
+      step.value = 2
+      loading.value = false;
+    }else if(result.msg){
+      loading.value = false;
+      showVideo()
+      if(result.code !== 0) ElMessage.error(result.msg)
+    }
+    clientStore.ipc.removeAllListeners(icpList.takePhoto.createMainImage);
+
+
+  });
+}
+
+
+//拍照成功  SmartShooter
+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){
+    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)
+  }
+
+})
+
+
+//运行的时候  直接拍照  digiCamControl
 clientStore.ipc.on(icpList.socket.message+'_run_mcu_single', async (event, result) => {
-  console.log('_run_mcu_single_check')
+  console.log('_run_mcu_single_check_on')
   console.log(result)
 
   if(result.code === 0 && result.data?.file_path){
-    clientStore.ipc.removeAllListeners(icpList.takePhoto.createMainImage);
-    clientStore.ipc.send(icpList.takePhoto.createMainImage,{
-      file_path:result.data.file_path
-    });
-    clientStore.ipc.on(icpList.takePhoto.createMainImage, async (event, result) => {
-      console.log('icpList.utils.createMainImage');
-      console.log(result);
-      if(result.code === 0 && result.data?.main_out_path){
-        imageTplPath.value  = result.data?.main_out_path
-        hideVideo()
-        step.value = 2
-        loading.value = false;
-      }else if(result.msg){
-        if(result.code !== 0) ElMessage.error(result.msg)
-      }
-      clientStore.ipc.removeAllListeners(icpList.takePhoto.createMainImage);
+    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)
 
   }else if(result.msg){
     if( result.msg.indexOf('处理失败,请重试') >= 0){
@@ -245,11 +306,19 @@ clientStore.ipc.on(icpList.socket.message+'_run_mcu_single', async (event, resul
       loading.value = false;
       showVideo()
     }
-  }
+  }*/
 
 })
-onMounted(()=>{
-  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'
+
 })
 
 /**
@@ -260,6 +329,10 @@ onBeforeUnmount(() => {
   clientStore.ipc.removeAllListeners(icpList.camera.takePictures);
   clientStore.ipc.removeAllListeners(icpList.camera.PreviewHide);
   clientStore.ipc.removeAllListeners(icpList.camera.PreviewShow);
+  clientStore.ipc.removeAllListeners(icpList.socket.message+'_run_mcu_single');
+  clientStore.ipc.removeAllListeners(icpList.socket.message+'_smart_shooter_photo_take');
+
+
 })
 </script>
 <style scoped lang="scss">

+ 68 - 23
frontend/src/views/Photography/components/LoadingDialog.vue

@@ -1,36 +1,62 @@
 <template>
   <el-dialog
-    v-model="visible"
-    :show-close="requesting"
+    v-model.sync="visible"
+    :show-close="!requesting"
     :close-on-click-modal="false"
     :close-on-press-escape="false"
-    width="400px"
+    width="700px"
     custom-class="loading-dialog-EL"
     align-center
     append-to-body
   >
-    <div class="loading-content mar-top-10">
-      <div class="progress-container">
+    <div class="loading-content">
+      <!-- 新的步骤进度条 -->
+      <ProgressSteps
+        v-if="useNewProgress && progressSteps.length > 0"
+        :steps="progressSteps"
+        :disabled-button="disabledButton"
+        :use-new-progress="useNewProgress"
+        @complete="handleButtonClick"
+        :on-open-folder="onOpenFolder"
+        :message="message"
+      />
+
+
+      <!-- 原有的简单进度条 -->
+      <div v-else class="progress-container">
         <div class="progress-bar">
-          <div 
+          <div
             class="progress-inner"
             :style="{ width: `${progress}%` }"
           ></div>
         </div>
-        <span class="progress-text">{{ progress }}%</span>
+        <span class="progress-text">{{ progress.toFixed(2) }}%</span>
       </div>
-      
-      <div class="message">{{ message }}</div>
+
+      <div class="message" style="display: none">{{ message }}</div>
       <!-- 错误信息插槽 -->
       <slot name="errList"></slot>
+      <!-- 进度消息插槽 -->
+      <slot name="progressMessages"></slot>
 
       <el-button
-        :disabled="disabledButton"
-        type="primary"
-        class="action-button   button--primary1  mar-top-20"
-        @click="handleButtonClick"
+          v-if="!disabledButton && !useNewProgress"
+          :disabled="disabledButton"
+          type="primary"
+          class="action-button   button--primary1  mar-top-20"
+          @click="handleButtonClick"
       >
-        {{ buttonText }} 
+        {{ buttonText }}
+      </el-button>
+
+
+      <el-button
+          v-if="message  === '全部货号生成失败'"
+          type="primary"
+          class="action-button   button--primary1  mar-top-20"
+          @click="visible = false"
+      >
+        {{ message }}
       </el-button>
     </div>
   </el-dialog>
@@ -38,14 +64,29 @@
 
 <script setup lang="ts">
 import { ref, defineProps, defineEmits , watch } from 'vue'
+import ProgressSteps from './ProgressSteps.vue'
+
+interface StepData {
+  goods_art_no: string
+  msg_type: string
+  name: string
+  status: '等待处理' | '正在处理' | '处理完成' | '处理失败'
+  current: number
+  total: number
+  error: number
+  folder?: string // 添加folder字段
+}
 
 interface Props {
   modelValue: boolean
   progress?: number
   message?: string
   disabledButton?: boolean
-  buttonText?: string,
+  buttonText?: string
   requesting?: boolean
+  useNewProgress?: boolean
+  progressSteps?: StepData[]
+  onOpenFolder?: (folder: string) => void // 添加打开目录的回调函数
 }
 
 const props = withDefaults(defineProps<Props>(), {
@@ -53,7 +94,10 @@ const props = withDefaults(defineProps<Props>(), {
   message: '正在为您处理,请稍后...',
   disabledButton: true,
   buttonText: '处理完毕,点击打开最终图片目录',
-  requesting: false
+  requesting: false,
+  useNewProgress: false,
+  progressSteps: () => [],
+  onOpenFolder: () => {} // 默认空函数
 })
 
 const emit = defineEmits<{
@@ -77,18 +121,19 @@ const handleButtonClick = () => {
   emit('button-click')
 }
 </script>
-<style>
+<style lang="scss">
 .loading-dialog-EL{
+  border-radius: 10px;
   .el-dialog__header {
     display: none;
     padding: 0;
   }
  .el-dialog__body {
-    background-image: url(@/assets/images/Photography/loading-bg.png);  /* 添加背景图片 */
+   // background-image: url(@/assets/images/Photography/loading-bg.png);  /* 添加背景图片 */
     background-size: cover;  /* 确保图片覆盖整个区域 */
     background-position: center;  /* 图片居中显示 */
     background-repeat: no-repeat;  /* 防止图片重复 */
-    padding: 40px 20px;  /* 添加内边距 */
+    padding: 20px 20px;  /* 添加内边距 */
   }
 
 }
@@ -100,7 +145,7 @@ const handleButtonClick = () => {
     display: none;
     padding: 0;
   }
-  
+
   :deep(.el-dialog__body) {
     padding:0;
   }
@@ -115,7 +160,7 @@ const handleButtonClick = () => {
 
 .progress-container {
   width: 100%;
-  margin-bottom: 20px;
+  margin-bottom: 10px;
   display: flex;
   align-items: center;
   gap: 10px;
@@ -145,10 +190,10 @@ const handleButtonClick = () => {
 .message {
   color: #606266;
   font-size: 14px;
-  margin-bottom: 20px;
+  margin-bottom: 0px;
 }
 
 .action-button {
   padding: 10px  20px;
 }
-</style> 
+</style>

+ 347 - 0
frontend/src/views/Photography/components/ProgressSteps.vue

@@ -0,0 +1,347 @@
+<template>
+  <div class="progress-steps">
+    <div class="steps-container">
+      <div
+        v-for="(step, index) in steps"
+        :key="step.msg_type"
+        class="step-item"
+        :class="getStepClass(step)"
+      >
+        <!-- 步骤圆圈 -->
+        <div class="step-circle">
+          <div v-if="step.status === '处理完成'" class="step-icon completed">
+            <el-icon><Check /></el-icon>
+          </div>
+          <div v-else-if="step.status === '正在处理'" class="step-icon processing">
+            {{ index + 1 }}
+          </div>
+          <div v-else-if="step.status === '处理失败'" class="step-icon failed">
+            <el-icon><Close /></el-icon>
+          </div>
+          <div v-else class="step-icon waiting">
+            {{ index + 1 }}
+          </div>
+        </div>
+
+        <!-- 步骤内容 -->
+        <div class="step-content">
+          <div class="step-title">{{ step.name }}</div>
+          <div class="step-status">
+            {{ getStepTitle(step) }}
+            <div v-if="step.status !== '等待处理'" class="step-details">
+              第{{ step.status === '正在处理' && step.name === '抠图' ?  step.current+1 : step.current }}/{{ step.total }}款
+              {{ getCurrentGoodsNo(step) }}
+              <span v-if="step.error" style="color: red; margin-left:5px;">失败:{{step.error}}款</span>
+
+
+            </div>
+          </div>
+          <div v-if="step.folder" class="step-actions">
+            <span class="view-results" @click="handleViewResults(step)">查看结果</span>
+          </div>
+        </div>
+
+        <!-- 连接线 -->
+        <div v-if="index < steps.length - 1" class="step-connector"></div>
+      </div>
+    </div>
+
+    <!-- 完成按钮 -->
+    <div
+        v-if="!disabledButton"
+         class="completion-section">
+      <el-button
+        type="primary"
+        class="completion-button"
+        @click="handleComplete"
+      >
+        处理完毕,点击打开最终图片目录
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue'
+import { Check, Close } from '@element-plus/icons-vue'
+
+interface StepData {
+  msg_type: string
+  goods_art_no: string
+  name: string
+  status: '等待处理' | '正在处理' | '处理完成' | '处理失败'
+  current: number
+  total: number
+  error: number
+  folder?: string // 添加folder字段
+}
+
+interface Props {
+  steps: StepData[]
+  disabledButton?: boolean
+  useNewProgress?: boolean
+  onComplete?: () => void
+  onOpenFolder?: (folder: string) => void // 添加打开目录的回调函数
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  steps: () => [],
+  useNewProgress: false,
+  disabledButton: true,
+  onComplete: () => {},
+  onOpenFolder: () => {} // 默认空函数
+})
+
+const emit = defineEmits<{
+  (e: 'complete'): void
+}>()
+
+
+// 获取步骤标题
+const getStepTitle = (step: StepData) => {
+  return  step.status
+}
+
+// 获取步骤样式类
+const getStepClass = (step: StepData) => {
+  return {
+    'step-completed': step.status === '处理完成',
+    'step-processing': step.status === '正在处理',
+    'step-failed': step.status === '处理失败',
+    'step-waiting': step.status === '等待处理'
+  }
+}
+
+// 获取进度百分比
+const getProgressPercentage = (step: StepData) => {
+  if (step.total === 0) return 0
+  return Math.round((step.current / step.total) * 100)
+}
+
+// 检查是否所有步骤都完成
+const allStepsCompleted = computed(() => {
+  return props.steps.every(step => step.status === '处理完成')
+})
+
+// 获取当前货号(这里需要根据实际数据结构调整)
+const getCurrentGoodsNo = (step: StepData) => {
+  // 这里需要根据实际的数据结构来获取当前货号
+  console.log('step===================');
+  console.log(step);
+  // 暂时返回一个示例值
+  return step.goods_art_no
+}
+
+// 处理查看结果
+const handleViewResults = (step: StepData) => {
+  console.log('查看结果:', step)
+  // 如果有folder路径,则调用外部传入的打开目录方法
+  if (step.folder && props.onOpenFolder) {
+    props.onOpenFolder(step.folder)
+  }
+}
+
+// 处理完成按钮点击
+const handleComplete = () => {
+  emit('complete')
+  props.onComplete()
+}
+
+// 监听步骤变化
+watch(() => props.steps, (newSteps) => {
+  console.log('步骤更新:', newSteps)
+}, { deep: true })
+</script>
+
+<style lang="scss" scoped>
+.progress-steps {
+  width: 100%;
+  padding: 20px 0;
+  background: #fff;
+}
+
+.steps-container {
+  display: flex;
+  flex-direction: column;
+  gap: 0;
+  position: relative;
+  padding: 0 20px;
+}
+
+.step-item {
+  display: flex;
+  flex-direction: row;
+  align-items: flex-start;
+  position: relative;
+  padding: 20px 0;
+  min-height: 60px;
+}
+
+.step-circle {
+  width: 32px;
+  height: 32px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-right: 10px;
+  flex-shrink: 0;
+  position: relative;
+  z-index: 2;
+}
+
+.step-icon {
+  width: 100%;
+  height: 100%;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-weight: bold;
+  color: white;
+  font-size: 14px;
+  background: #FFF;
+  border:1px solid #E5E5E5;
+}
+
+.step-icon.completed {
+  border-color: #2957FF;
+  color: #2957FF;
+}
+
+.step-icon.processing {
+  border-color: #2957FF;
+  color: #2957FF;
+}
+
+.step-icon.failed {
+  border-color: #F56C6C;
+  color: #F56C6C;
+}
+
+.step-icon.waiting {
+  border-color:#E5E5E5;
+  color: #E5E5E5;
+}
+
+.step-content {
+  flex: 1;
+  display: flex;
+  justify-content: space-between;
+  min-height: 32px;
+  line-height: 32px;
+}
+
+.step-title {
+  font-size: 16px;
+  font-weight: 500;
+  color: #303133;
+  margin-bottom: 4px;
+  min-width: 120px;
+  text-align: left;
+}
+
+.step-status {
+  font-size: 14px;
+  color: #606266;
+  margin-bottom: 4px;
+  flex: 1;
+  text-align: left;
+  margin-left: 20px;
+  display: flex;
+}
+
+.step-details {
+  margin-left: 10px;
+}
+
+.step-actions {
+  margin-top: 8px;
+}
+
+.view-results {
+  color: #2957FF;
+  font-size: 14px;
+  cursor: pointer;
+  text-decoration: none;
+
+  &:hover {
+    text-decoration: underline;
+  }
+}
+
+.step-connector {
+  position: absolute;
+  top: 52px;
+  left: 15px;
+  width: 2px;
+  height: calc(100% - 32px);
+  background: #E4E7ED;
+  z-index: 1;
+}
+
+.step-item:last-child .step-connector {
+  display: none;
+}
+
+.completion-section {
+  margin-top: 30px;
+  text-align: center;
+  padding: 0 20px;
+}
+
+.completion-button {
+  background: linear-gradient(135deg, #2FB0FF, #B863FB);
+  border: none;
+  padding: 25px 30px;
+  font-size: 16px;
+  font-weight: 600;
+  border-radius: 10px;
+  box-shadow: 0 4px 15px rgba(47, 176, 255, 0.3);
+  transition: all 0.3s ease;
+
+  &:hover {
+    transform: translateY(-2px);
+    box-shadow: 0 6px 20px rgba(47, 176, 255, 0.4);
+  }
+}
+
+// 步骤状态样式
+.step-completed {
+  .step-title {
+    color: rgba(0,0,0,1);
+    font-size: 16px;
+  }
+  .step-status {
+    color: rgba(0,0,0,0.85);
+    font-size: 14px;
+  }
+}
+
+.step-processing {
+  .step-title {
+    font-weight: bold;
+  }
+  .step-status {
+    color: #000;
+    font-size: 16px;
+  }
+}
+
+.step-failed {
+  .step-title {
+    color: #F56C6C;
+  }
+}
+
+.step-waiting {
+  .step-title {
+    color: rgba(0,0,0,0.45);
+    font-size: 16px;
+  }
+  .step-status {
+    color: rgba(0,0,0,0.45);
+    font-size: 14px;
+  }
+}
+</style>

+ 56 - 49
frontend/src/views/Photography/components/editRow.vue

@@ -1,16 +1,16 @@
 <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>
       <el-form-item label="是否拍照" v-if="!editRowData.is_system">
         <el-radio-group v-model="editRowData.take_picture">
-          <el-radio :label="true">拍照</el-radio>
-          <el-radio :label="false">不拍照</el-radio>
+          <el-radio :label="1">拍照</el-radio>
+          <el-radio :label="0">不拍照</el-radio>
         </el-radio-group>
       </el-form-item>
       <el-form-item label="相机高度(mm)">
@@ -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">
@@ -36,16 +36,16 @@
       <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 :label="1">翻转</el-radio>
+            <el-radio :label="0">不翻转</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')">
         <el-radio-group v-model="editRowData.led_switch">
-          <el-radio :label="false">关闭</el-radio>
-          <el-radio :label="true">开启</el-radio>
+          <el-radio :label="0">关闭</el-radio>
+          <el-radio :label="1">开启</el-radio>
         </el-radio-group>
       </el-form-item>
       <el-form-item label="对焦次数">
@@ -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>
 
@@ -78,6 +78,7 @@ import { ref, defineProps, defineEmits , watch, onMounted } from 'vue'
 import icpList from '@/utils/ipc';
 import { digiCamControlWEB } from  '@/utils/appconfig'
 import {ElMessage} from "element-plus";
+import  { getDeviceConfigDetail,getDeviceConfigDetailQuery,saveDeviceConfig } from '@/apis/setting'
 import client from "@/stores/modules/client";
 const clientStore = client();
 import socket from "@/stores/modules/socket";
@@ -100,7 +101,7 @@ const props = defineProps({
 const initStatus = ref(false)
 const isDefault = ref(true); // 是否为默认配置
 const editRowData = ref({}); // 当前编辑行的数据
-onMounted(()=>{
+onMounted(async ()=>{
   console.log('editrow')
   if(props.addRowData.mode_type){
     console.log(props.addRowData);
@@ -109,29 +110,27 @@ onMounted(()=>{
     testShoesFlip()
     return
   }
+  let fun = getDeviceConfigDetail;
   let params = {
     id: props.id
   }
-  if(!props.id) params = {
-    mode_type:"执行左脚程序",
-    action_name:"侧视",
+  if(!props.id){
+    params = {
+      mode_type:"执行左脚程序",
+      action_name:"侧视",
+    }
+    fun =  getDeviceConfigDetailQuery
   }
-  clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigDetail);
-  clientStore.ipc.send(icpList.setting.getDeviceConfigDetail, params);
-  clientStore.ipc.on(icpList.setting.getDeviceConfigDetail, (event, result) => {
 
-    console.log('getDeviceConfigDetail')
-    console.log(result)
-    if(result.code == 0 && result.data){
 
-      editRowData.value = result.data;
-      clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigDetail);
-      initStatus.value = true;
-      testShoesFlip()
-    }else if(result.msg){
-      ElMessage.error(result.msg)
-    }
-  });
+
+  const result = await fun(params)
+
+  if(result.code == 0 && result.data){
+    editRowData.value = result.data;
+    initStatus.value = true;
+    testShoesFlip()
+  }
 
 })
 
@@ -179,6 +178,9 @@ async function changeNum(type, min, max) {
 /*测试拍照*/
 const captureLoading = ref(false)
 function testShoesFlip(){
+  if (captureLoading.value) {
+    return
+  }
   if (clientStore.isClient) {
 
     socketStore.sendMessage({
@@ -188,7 +190,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),
@@ -204,15 +206,16 @@ function testShoesFlip(){
 
 
     clientStore.ipc.on(icpList.socket.message+'_run_mcu_single', async (event, result) => {
-      console.log('_run_mcu_single_row')
 
       captureLoading.value = false;
 
+      emit('onRunMcuSingle')
+
     })
   }
 }
 
-const emit = defineEmits([ 'confirm','onClose']);
+const emit = defineEmits([ 'confirm','onClose','onRunMcuSingle']);
 const close = ()=>{
   console.log('onClose')
   emit('onClose')
@@ -221,25 +224,29 @@ const close = ()=>{
 /**
  * 保存当前编辑的配置。
  */
-const saveRow = () => {
+const saveRow = async () => {
+  if (captureLoading.value) {
+    return
+  }
+
   if(!editRowData.value.action_name){
     ElMessage.error('请输入动作名称')
     return;
   }
-  clientStore.ipc.send(icpList.setting.saveDeviceConfig, {
+
+  captureLoading.value = true
+  const result = await  saveDeviceConfig({
     ...editRowData.value
-  });
-  clientStore.ipc.on(icpList.setting.saveDeviceConfig, (event, result) => {
-    console.log('saveDeviceConfig');
-    console.log(editRowData.value);
-    if (result.code == 0) {
-      emit('confirm')
-      ElMessage.success('保存成功');
-      clientStore.ipc.removeAllListeners(icpList.setting.saveDeviceConfig);
-    } else {
-      ElMessage.error('保存失败');
-    }
-  });
+  })
+  captureLoading.value = false
+  if (result.code == 0) {
+    emit('confirm')
+    ElMessage.success('保存成功');
+    clientStore.ipc.removeAllListeners(icpList.setting.saveDeviceConfig);
+  }
+
+
+
 };
 
 // 暴露给父组件

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 711 - 185
frontend/src/views/Photography/detail.vue


+ 9 - 0
frontend/src/views/Photography/mixin/generate.vue

@@ -0,0 +1,9 @@
+<script>
+export default {
+name: "generate"
+}
+</script>
+
+<style scoped>
+
+</style>

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

@@ -0,0 +1,860 @@
+import { ref, onMounted, onBeforeUnmount, watchEffect, computed } from 'vue'
+import icpList from '@/utils/ipc'
+import client from "@/stores/modules/client";
+import socket from "@/stores/modules/socket";
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getFilePath, getRouterUrl } from '@/utils/appfun'
+import { useRouter, useRoute } from "vue-router";
+import checkInfo from "@/stores/modules/check";
+import generate from '@/utils/menus/generate'
+import { clickLog, setLogInfo } from '@/utils/log'
+import { useUuidStore } from '@/stores/modules/uuid'
+import useUserInfo from "@/stores/modules/user";
+import configInfo from '@/stores/modules/config';
+import tokenInfo from "@/stores/modules/token";
+
+export default function usePhotography() {
+    const loading = ref(false)
+    const runLoading = ref(false)
+    const takePictureLoading = ref(false)
+    const goodsList = ref([])
+    const goods_art_no_tpl = ref('')
+    const goods_art_no = ref('')
+    const runAction = ref({
+      "action": "",
+      "goods_art_no": ""
+    })
+    const lastPhoto = ref({})
+    const showlastPhoto = ref(false)
+    const isDelGoodsGetList = ref(false)
+    const reNosObj = ref({
+      goods_art_no: null,
+      action: null,
+    })
+    const goodsArtNo = ref()
+    let smartShooterTimeout: ReturnType<typeof setTimeout> | null = null
+
+    // 初始化 WebSocket 状态管理
+    const socketStore = socket()
+    const uuidStore = useUuidStore();
+    const clientStore = client();
+    const Router = useRouter()
+    const route = useRoute();
+    const useUserInfoStore = useUserInfo();
+    const configInfoStore = configInfo();
+    const tokenInfoStore = tokenInfo();
+    const checkInfoStore = checkInfo()
+
+    // 抠图请求去重与延迟队列(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();
+
+          socketStore.sendMessage({
+            type: 'segment_progress',
+            data: {
+              token: tokenInfoStore.getToken,
+              uuid: uuidStore?.getUuid || '',
+              goods_art_no: [goodsArtNo],
+            }
+          })
+        } catch (e) {
+          // 忽略发送异常,避免打断主流程
+        }
+      }, 20000)
+      segmentQueue.set(goodsArtNo, timeoutId)
+    }
+
+    /**
+     * 保存货号模板到货号变量中。
+     */
+    function saveGoodsArtNo() {
+      if (goods_art_no_tpl.value) {
+        goods_art_no.value = goods_art_no_tpl.value
+        ElMessage.success('商品货号' + goods_art_no.value + '获取成功,请在遥控器上按下左或右脚按键,启动拍摄')
+      }
+    }
+
+    /**
+     * 获取拍照记录。
+     * @param params - 可选参数,用于分页或其他筛选条件。
+     */
+    async function getPhotoRecords(params?: {}) {
+      if (loading.value) return;
+      loading.value = true;
+      clientStore.ipc.send(icpList.takePhoto.getPhotoRecords, {
+        ...params,
+        page: 1,
+        size: 100,
+      });
+      clientStore.ipc.on(icpList.takePhoto.getPhotoRecords, (event, result) => {
+
+        loading.value = false;
+        if (result.code === 0) {
+
+          clientStore.ipc.removeAllListeners(icpList.takePhoto.getPhotoRecords);
+
+          console.log('getPhotoRecords  print_time:' + new Date().toLocaleString())
+          console.log('getPhotoRecords  print_time:' + JSON.stringify(result.data.list))
+          goodsList.value = result.data.list
+          if (isDelGoodsGetList.value) {
+            isDelGoodsGetList.value = false;
+            return;
+          }
+          getLastPhotoRecord()
+
+        } else if (result.msg) {
+          ElMessage.error(result.msg)
+        }
+      });
+    }
+
+    /**
+     * 执行拍照操作。
+     * @param data - 包含拍摄所需的数据对象。
+     */
+    async function runGoods(data) {
+      if (runLoading.value || takePictureLoading.value) {
+        ElMessage.error('拍摄程序正在运行,请稍候')
+        return
+      }
+
+      await socketStore.connectSocket();
+
+      socketStore.sendMessage({
+        type: 'run_mcu',
+        data,
+      })
+      runLoading.value = true;
+      runAction.value.action = data.action
+      runAction.value.goods_art_no = data.goods_art_no
+      goods_art_no.value = ''
+      goods_art_no_tpl.value = ''
+      reNosObj.value.goods_art_no = null;
+      reNosObj.value.action = null;
+
+      clientStore.ipc.on(icpList.socket.message + '_run_mcu', (event, result) => {
+
+        clientStore.ipc.removeAllListeners(icpList.socket.message + '_run_mcu');
+        console.log('_run_mcu');
+        console.log(result);
+        if (result.code !== 0 && result.msg) {
+          ElMessage.error(result.msg)
+          runLoading.value = false
+          return;
+        } else {
+
+          ElMessage.success('开始拍摄,请稍后')
+        }
+      })
+
+
+    }
+
+    /**
+     * 格式化时间字符串。
+     * @param time - 原始时间字符串。
+     * @returns 格式化后的时间字符串,若输入为空则返回 null。
+     */
+    const getTime = function (time) {
+      if (!time) return null
+      return time.replace('T', ' ').substr(5, 11)
+    }
+
+    /**
+     * 删除所有商品货号的历史记录。
+     */
+    async function delAll() {
+      let params = goodsList.value.map(item => item.goods_art_no)
+      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)
+      }
+    }
+
+    /**
+     * 删除指定的商品货号。
+     * @param params - 包含需要删除的货号列表的对象。
+     */
+    const delGoods = async function (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)
+      }
+    }
+
+    /**
+     * 删除指定的商品货号。
+     * @param params - 包含需要删除的货号列表的对象。
+     */
+    const del = async function (params) {
+      console.log(icpList.takePhoto.delectGoodsArts, params);
+      clientStore.ipc.removeAllListeners(icpList.takePhoto.delectGoodsArts);
+      clientStore.ipc.send(icpList.takePhoto.delectGoodsArts, params);
+      clientStore.ipc.on(icpList.takePhoto.delectGoodsArts, (event, result) => {
+        clientStore.ipc.removeAllListeners(icpList.takePhoto.delectGoodsArts);
+        console.log("icpList.takePhoto.delectGoodsArts", params);
+        if (result.code === 0) {
+          isDelGoodsGetList.value = true
+          ElMessage.info('货号删除成功')
+          getPhotoRecords()
+          if (reNosObj.value.goods_art_no) {
+            runGoods(
+              {
+                "action": reNosObj.value.action,
+                "goods_art_no": reNosObj.value.goods_art_no
+              })
+
+          }
+        } else if (result.msg) {
+          ElMessage.error(result.msg)
+        }
+      });
+
+    }
+
+    //单个重拍
+
+    const reTakePicture = async (img) => {
+      if (!img.id) return;
+      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);
+
+
+      clientStore.ipc.on(icpList.setting.getDeviceConfigDetail, (event, result) => {
+
+        console.log('getDeviceConfigDetail')
+        console.log(result)
+        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)
+        }
+      });
+
+      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 reTakePictureNos = async (goods_art_no, item) => {
+
+      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 = '执行左脚程序'
+      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] })
+    }
+
+    /**
+     * 检查是否可以进入下一步操作。
+     */
+    const next = async function () {
+      if (runLoading.value) {
+        ElMessage.error('正在拍摄中,请稍候')
+        return;
+      }
+      if (goodsList.length) {
+        ElMessage.error('请先拍摄商品。')
+        return;
+      }
+    }
+
+    const oneClickStop = () => {
+
+      if (!(runLoading.value || takePictureLoading.value)) {
+        ElMessage.error('拍摄程序已结束,不需要单独停止!')
+        return
+      } else {
+
+        socketStore.sendMessage({
+          type: 'stop_action',
+        })
+      }
+    }
+
+    /**
+     * 打开最近一张拍摄图
+     */
+    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.data?.goods_art_no);
+        clientStore.ipc.removeAllListeners(icpList.takePhoto.getLastPhotoRecord);
+        if (result.code === 0) {
+          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
+            }
+          }
+          lastPhoto.value = result.data
+        } else if (result.msg) {
+          ElMessage.error(result.msg)
+        }
+      });
+    }
+
+    /**
+     * 打开主图详情页面。
+     */
+    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('正在拍摄中,请稍候')
+        return;
+      }
+      const { href } = Router.resolve({
+        name: 'PhotographyDetail',
+        query: {
+          goods_art_nos: goodsList.value.map(item => item.goods_art_no),
+        }
+      })
+
+      clientStore.ipc.removeAllListeners(icpList.utils.openMain);
+      let params = {
+        title: '主图与详情生成',
+        width: 3840,
+        height: 2160,
+        frame: true,
+        id: "PhotographyDetail",
+        url: getRouterUrl(href)
+      }
+      clientStore.ipc.send(icpList.utils.openMain, params);
+    }
+
+    /*高级生成*/
+    const onGenerateCLick = (menu, item) => {
+      if (menu.name === '历史记录') {
+
+        menu.click()
+        return
+      }
+      const firstWithImagePath = item.items.find(
+        (image) => image.PhotoRecord.image_path
+      );
+
+      if (firstWithImagePath) {
+        menu.click({
+          query: {
+            image_path: firstWithImagePath.PhotoRecord.image_path
+          }
+        })
+      } else {
+        menu.click()
+      }
+
+    }
+
+    // 打开输出目录:appConfig.appPath + '/build/extraResources/py/output'
+    const openOutputDir = () => {
+      try {
+
+        const appPath = configInfoStore?.appConfig?.appPath || ''
+        if (!appPath) {
+          ElMessage.error('未获取到应用目录 appPath')
+          return
+        }
+        const fullPath = `${appPath}\\output`
+        clientStore.ipc.removeAllListeners(icpList.utils.shellFun);
+        clientStore.ipc.send(icpList.utils.shellFun, {
+          action: 'openMkPath',
+          params: fullPath.replace(/\//g, '\\')
+        });
+      } catch (e) {
+        console.error(e)
+        ElMessage.error('打开目录失败')
+      }
+    }
+
+    const menu = computed(() => {
+      if (configInfoStore.appModel === 2) {
+        return [
+          {
+            type: 'setting'
+          },
+          {
+            name: '切换模式',
+            click() {
+              configInfoStore.updateAppModel(1)
+              Router.push({
+                name: 'PhotographyCheck'
+              })
+            }
+          },
+          {
+            name: '生成图目录',
+            click() {
+              openOutputDir()
+            }
+          },
+          {
+            ...generate
+          }
+        ]
+      }
+      if (useUserInfoStore.userInfo.brand_company_code === '1300' || configInfoStore.appConfig.debug) {
+        return [
+          {
+            type: 'setting'
+          },
+          {
+            type: 'developer'
+          },
+          {
+            name: '生成图目录',
+            click() {
+              openOutputDir()
+            }
+          },
+          {
+            ...generate,
+          }
+        ]
+      }
+
+
+      return [
+        {
+          type: 'setting'
+        },
+        {
+          name: '生成图目录',
+          click() {
+            openOutputDir()
+          }
+        },
+        {
+          ...generate
+        }
+      ]
+
+
+    })
+
+    const onRemoteControl = (type) => {
+      if (type == 'take_picture') {
+        // 埋点:手动拍照
+        clickLog({ describe: { action: '点击遥控器拍照按钮' } }, route);
+
+        if (runLoading.value || takePictureLoading.value) {
+          ElMessage.error('拍摄程序正在运行,请稍候')
+          return
+        }
+
+        ElMessage.success('正在拍摄中,请稍候')
+        socketStore.sendMessage({
+          type: 'handler_take_picture',
+        })
+        return;
+      }
+
+
+      if (!goods_art_no.value) {
+        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,
+      })
+
+    }
+
+    // 初始化事件监听
+    const initEventListeners = () => {
+      // 监听蓝牙扫描事件
+      clientStore.ipc.on(icpList.socket.message + '_blue_tooth_scan', (event, result) => {
+
+        console.log('_blue_tooth_scan')
+        if (result.code === 0 && result.data?.data) {
+          console.log(goods_art_no.value);
+          if (!goods_art_no.value) {
+            ElMessage.error('请在左侧第一步中,先扫描货号或者手动输入货号!')
+            goodsArtNo.value?.focus() // 聚焦输入框
+            return;
+          }
+          runGoods({
+            ...result.data?.data,
+            goods_art_no: goods_art_no.value
+          })
+
+        }
+      });
+
+      // 监听图片处理完成事件
+      clientStore.ipc.on(icpList.socket.message + '_image_process', (event, result) => {
+        console.log('_image_process')
+        console.log(result)
+        getPhotoRecords()
+        // 延迟两秒再获取一遍数据
+        setTimeout(() => {
+          getPhotoRecords()
+        }, 3000)
+      })
+
+      // 监听拍照完成事件
+      clientStore.ipc.on(icpList.socket.message + '_photo_take', (event, result) => {
+        console.log('_photo_take')
+        console.log(result)
+        if (result.status === 2 && result.msg.includes('执行完成')) {
+          getPhotoRecords()
+          // 延迟两秒再获取一遍数据
+          setTimeout(() => {
+            getPhotoRecords()
+          }, 3000)
+          takePictureLoading.value = false;
+          return;
+        }
+        if (result.code !== 0 && result.msg) {
+          ElMessage.error(result.msg)
+          takePictureLoading.value = false;
+        }
+
+      })
+
+      // 监听一键停止
+      clientStore.ipc.on(icpList.socket.message + '_stop_action', (event, result) => {
+        console.log('_stop_action')
+        console.log(result)
+        oneClickStop()
+
+      })
+
+      // 监听一键停止结束
+      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
+          }, 3000)
+        }
+
+      })
+
+      // 监听手动触发拍照事件
+      clientStore.ipc.on(icpList.socket.message + '_handler_take_picture', async (event, result) => {
+        console.log('_handler_take_picture')
+        console.log(result)
+        if (result.code === 0) {
+          if (runLoading.value || takePictureLoading.value) {
+            ElMessage.error('拍摄程序正在运行,请稍候')
+            return
+          }
+
+          ElMessage.success('正在拍摄中,请稍候')
+
+          takePictureLoading.value = true;
+          await socketStore.connectSocket();
+          socketStore.sendMessage(result.data)
+
+          getPhotoRecords()
+          // 延迟两秒再获取一遍数据
+          setTimeout(() => {
+            getPhotoRecords()
+          }, 3000)
+
+        } else if (result.msg) {
+          ElMessage.error(result.msg)
+        }
+
+      })
+
+      //拍照成功  SmartShooter
+      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) {
+          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)
+        }
+      })
+
+      // 监听拍照完成后的最终状态事件
+      clientStore.ipc.on(icpList.socket.message + '_run_mcu_update', (event, result) => {
+        console.log('run_mcu_updat print_time:' + new Date().toLocaleString())
+        console.log('run_mcu_update  print_time:' + JSON.stringify(result))
+
+        if (result.code === 0) {
+          if (result.data?.file_path) {
+            if (lastPhoto.value?.file_path == result.data?.file_path) return;
+            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) {
+                  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)
+
+                  setTimeout(() => {
+                    showlastPhoto.value = false
+                  }, 3000)
+                }
+              })
+              setTimeout(() => {
+                getPhotoRecords()
+              }, 2000)
+            }
+            lastPhoto.value = result.data
+          }
+        } else if (result.msg) {
+          ElMessage.error(result.msg)
+        }
+        if (reNosObj.value.goods_art_no) {
+          resetStatus()
+        }
+      })
+    }
+
+    // 清理事件监听
+    const cleanupEventListeners = () => {
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_blue_tooth_scan');
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_image_process');
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_run_mcu');
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_photo_take');
+      clientStore.ipc.removeAllListeners(icpList.socket.message + '_photo_take_finish');
+      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');
+
+      // 清理抠图队列的定时器
+      try {
+        segmentQueue.forEach((t) => { if (t) clearTimeout(t) })
+        segmentQueue.clear()
+      } catch (e) { }
+    }
+
+    // 监听蓝牙扫描
+    checkInfoStore.set_blue_tooth_scan_NO('')
+    watchEffect(async () => {
+      if (checkInfoStore.blue_tooth_scan_NO) {
+        ElMessage.success('商品货号' + checkInfoStore.blue_tooth_scan_NO + '获取成功,请在遥控器上按下左或右脚按键,启动拍摄')
+        goods_art_no.value = checkInfoStore.blue_tooth_scan_NO
+        checkInfoStore.set_blue_tooth_scan_NO('')
+      }
+    })
+
+    return {
+      loading,
+      runLoading,
+      takePictureLoading,
+      goodsList,
+      goods_art_no_tpl,
+      goods_art_no,
+      runAction,
+      lastPhoto,
+      showlastPhoto,
+      goodsArtNo,
+      menu,
+      configInfoStore,
+      getTime,
+      getFilePath,
+      getPhotoRecords,
+      delAll,
+      delGoods,
+      reTakePicture,
+      reTakePictureNos,
+      oneClickStop,
+      onRemoteControl,
+      openPhotographyDetail,
+      onGenerateCLick,
+      initEventListeners,
+      cleanupEventListeners,
+    }
+}
+

+ 603 - 0
frontend/src/views/Photography/processImage.vue

@@ -0,0 +1,603 @@
+<template>
+  <headerBar
+      title="处理图像"
+      showUser
+      :menu="menu"
+  />
+
+  <hardware-check/>
+
+  <div class="photography-page flex-col bg-F5F6F7 ">
+    <div class="main-container page—wrap max-w-full">
+      <div class="history-section flex-col koutu-section">
+
+          <div class="history-warp">
+            <div v-if="!goodsList.length" class="fs-14 c-666 mar-top-50">
+              {{ loading ? '数据正在加载中,请稍候...' : '暂无数据,请先进行拍摄'}}
+            </div>
+            <div v-else class="history-item"  v-for="item in goodsList" :key="item.goods_art_no"   >
+              <div class="history-item-header">
+                <div class="history-item-left">
+                  <el-checkbox
+                      :model-value="selectedGoods.has(item.goods_art_no)"
+                      @change="toggleGoods(item.goods_art_no)"
+                      class="goods-checkbox"
+                  />
+                  <span class="goods-art-no">{{ item.goods_art_no }}</span>
+
+                  <div class="history-item-meta ">
+                    <span class="action-time flex left">
+
+                       <img src="@/assets/images/processImage.vue/riq.png" />
+                      {{ getTime(item.action_time) }}</span>
+                    <span class="image-count mar-left-10 flex left">
+                       <img src="@/assets/images/processImage.vue/tup.png" />
+                      {{ item.items?.length || 0 }}张图片</span>
+                  </div>
+                </div>
+                <div class="history-item-right">
+                  <el-dropdown :disabled="runLoading || takePictureLoading" trigger="click">
+                    <el-button :disabled="runLoading || takePictureLoading" size="small" plain>高级生成</el-button>
+                    <template #dropdown>
+                      <el-dropdown-menu>
+                        <el-dropdown-item
+                            v-for="menu in generate.children"
+                            @click.native="onGenerateCLick(menu,item)">{{ menu.name }}</el-dropdown-item>
+                      </el-dropdown-menu>
+                    </template>
+                  </el-dropdown>
+
+                  <el-button style="color: #FF4C00"  size="small" class="mar-left-10" :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="history-item-images" >
+                <div
+                  v-for="(image, index) in item.items"
+                  :key="image.action_id || image.action_name"
+                  class="history-item_image"
+                  v-loading="!image.PhotoRecord.image_path && runAction.goods_art_no == item.goods_art_no"
+                >
+                  <span class="tag" v-if="!image.PhotoRecord.image_path">{{ image.action_name }}</span>
+                  <el-image
+                    v-if="image.PhotoRecord.image_path"
+                    :src="getFilePath(image.PhotoRecord.image_path)"
+                    :preview-src-list="getPreviewImageList(item)"
+                    :initial-index="getPreviewIndex(item, index)"
+                    class="preview-image"
+                    fit="contain"
+                    :preview-teleported="true"
+                    lazy
+                  >
+                    <template #error>
+                      <div class="image-slot">
+                        <span class="tag">{{ image.action_name }}</span>
+                      </div>
+                    </template>
+                  </el-image>
+                  <div v-else class="image-placeholder">
+                    <span class="tag">{{ image.action_name }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+
+          </div>
+
+          <div class="footer-controls">
+            <div class="footer-left">
+              <el-checkbox
+                :model-value="isSelectAll"
+                :indeterminate="isIndeterminate"
+                @change="toggleSelectAll"
+                class="select-all-checkbox"
+              >
+                全选
+              </el-checkbox>
+              <span class="image-count-text">
+                已选择 <span style="color: #2957FF">{{ selectedImageCount }}</span> 张图片 共 <span style="color: #2957FF">{{ totalImageCount }}</span> 张图片
+              </span>
+            </div>
+            <div class="footer-right">
+              <el-button
+                :disabled="selectedGoods.size === 0 || runLoading || takePictureLoading"
+                @click="deleteSelected"
+                v-log="{ describe: { action: '删除选中货号' } }"
+              >
+                删除
+              </el-button>
+              <el-button
+                type="primary"
+                :disabled="!goodsList.length || runLoading || takePictureLoading"
+                @click="openPhotographyDetail()"
+                v-log="{ describe: { action: '点击开始生成' } }"
+              >
+
+                <img src="@/assets/images/processImage.vue/sc.png" />
+                开始生成
+                <img src="@/assets/images/processImage.vue/go.png"  class="go"/>
+              </el-button>
+            </div>
+          </div>
+        </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import headerBar from '@/components/header-bar/index.vue'
+import { onMounted, onBeforeUnmount, ref, computed } from 'vue'
+import HardwareCheck from '@/components/check/index.vue'
+import usePhotography from './mixin/usePhotography'
+import generate from '@/utils/menus/generate'
+import { ElMessage,ElMessageBox } from 'element-plus'
+import { clickLog, setLogInfo } from '@/utils/log'
+import {useRoute, useRouter} from "vue-router";
+import client from "@/stores/modules/client";
+import icpList from '@/utils/ipc'
+import { getFilePath, getRouterUrl } from '@/utils/appfun'
+
+const {
+  loading,
+  runLoading,
+  takePictureLoading,
+  goodsList,
+  runAction,
+  menu,
+  getTime,
+  getFilePath,
+  getPhotoRecords,
+  delGoods,
+  onGenerateCLick,
+  initEventListeners,
+  cleanupEventListeners,
+} = usePhotography()
+
+
+const Router = useRouter()
+const route = useRoute();
+const clientStore = client();
+// 覆盖 openPhotographyDetail 方法,只传递选中的货号
+const openPhotographyDetail = () => {
+
+  if (selectedGoods.value.size === 0) {
+    ElMessage.error('请选择要生成的货号')
+    return;
+  }
+  // 埋点:开始生成
+  clickLog({ describe: { action: '开始生成', goods_count: selectedGoods.value.size, goods_art_nos: Array.from(selectedGoods.value) } }, route);
+  console.log(Array.from(selectedGoods.value));
+  console.log(selectedGoods.value);
+
+  const { href } = Router.resolve({
+    name: 'PhotographyDetail',
+    query: {
+      goods_art_nos: Array.from(selectedGoods.value),
+    }
+  })
+
+  clientStore.ipc.removeAllListeners(icpList.utils.openMain);
+  let params = {
+    title: '主图与详情生成',
+    width: 3840,
+    height: 2160,
+    frame: true,
+    id: "PhotographyDetail",
+    url: getRouterUrl(href)
+  }
+  clientStore.ipc.send(icpList.utils.openMain, params);
+}
+
+// 选中的货号列表
+const selectedGoods = ref<Set<string>>(new Set())
+
+// 全选状态
+const isSelectAll = computed(() => {
+  return goodsList.value.length > 0 && selectedGoods.value.size === goodsList.value.length
+})
+
+// 是否半选状态
+const isIndeterminate = computed(() => {
+  return selectedGoods.value.size > 0 && selectedGoods.value.size < goodsList.value.length
+})
+
+// 切换单个货号的选中状态
+const toggleGoods = (goodsArtNo: string) => {
+  if (selectedGoods.value.has(goodsArtNo)) {
+    selectedGoods.value.delete(goodsArtNo)
+  } else {
+    selectedGoods.value.add(goodsArtNo)
+  }
+}
+
+// 全选/取消全选
+const toggleSelectAll = () => {
+  if (isSelectAll.value) {
+    selectedGoods.value.clear()
+  } else {
+    goodsList.value.forEach((item: any) => {
+      selectedGoods.value.add(item.goods_art_no)
+    })
+  }
+}
+
+// 计算已选择的图片数量
+const selectedImageCount = computed(() => {
+  let count = 0
+  goodsList.value.forEach((item: any) => {
+    if (selectedGoods.value.has(item.goods_art_no)) {
+      count += item.items?.length || 0
+    }
+  })
+  return count
+})
+
+// 计算总图片数量
+const totalImageCount = computed(() => {
+  let count = 0
+  goodsList.value.forEach((item: any) => {
+    count += item.items?.length || 0
+  })
+  return count
+})
+
+// 删除选中的货号
+const deleteSelected = async () => {
+  if (selectedGoods.value.size === 0) {
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除选中的 ${selectedGoods.value.size} 个货号的拍摄数据吗?`,
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+      }
+    )
+
+    const goodsArtNos = Array.from(selectedGoods.value)
+    await delGoods({ goods_art_nos: goodsArtNos })
+    // 删除成功后清空选中状态
+    selectedGoods.value.clear()
+  } catch (e) {
+    // 用户取消
+  }
+}
+
+// 获取预览图片列表(只包含有图片路径的,保持原始顺序)
+const getPreviewImageList = (item: any) => {
+  if (!item || !item.items) return []
+  return item.items
+    .filter((img: any) => img.PhotoRecord?.image_path)
+    .map((img: any) => getFilePath(img.PhotoRecord.image_path))
+}
+
+// 获取当前图片在预览列表中的索引
+const getPreviewIndex = (item: any, currentIndex: number) => {
+  if (!item || !item.items) return 0
+  // 计算当前图片在过滤后的预览列表中的索引
+  let previewIndex = 0
+  for (let i = 0; i <= currentIndex; i++) {
+    if (item.items[i]?.PhotoRecord?.image_path) {
+      if (i === currentIndex) break
+      previewIndex++
+    }
+  }
+  return previewIndex
+}
+
+onMounted(async () => {
+  await getPhotoRecords()
+  initEventListeners()
+})
+
+onBeforeUnmount(() => {
+  cleanupEventListeners()
+})
+</script>
+
+<style lang="scss">
+.koutu-image-popper {
+  width: calc(100vw - 470px) !important;
+  right: 70px !important;
+  top: 100px !important;
+  height: calc(100vh - 170px) !important;
+  transform: translate(0px, 0px) !important;
+
+  .el-image {
+    width: 100%;
+    height:100%;
+    display: block;
+
+    .el-image__inner {
+      width: 100%;
+      height:100%;
+      display: block;
+
+    }
+  }
+}
+</style>
+
+<style scoped lang="scss">
+.photography-page {
+  position: relative;
+  .main-container {
+    position: relative;
+    display: flex;
+  }
+}
+
+.history-section {
+        width: 100%;
+        height: calc(100vh - 30px);
+        display: flex;
+        flex-direction: column;
+        padding: 20px;
+        overflow-y: auto;
+
+        ::v-deep {
+          .el-checkbox__input {
+            transform: scale(1.4);
+          }
+        }
+        .history-warp {
+          flex: 1;
+
+          .history-item {
+            background: #FFFFFF;
+            box-shadow: 0px 2px 4px 0px rgba(23,33,71,0.1);
+            border-radius: 10px;
+            border: 1px solid #D9DEE6;
+            margin-bottom: 20px;
+
+            .history-item-header {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+              height: 40px;
+              padding: 0 10px;
+
+              background: linear-gradient( 90deg, #F4ECFF 0%, #DFEDFF 100%);
+              border-radius: 10px 10px 0px 0px;
+
+              .history-item-left {
+                display: flex;
+                align-items: center;
+                gap: 10px;
+
+                .goods-checkbox {
+                  margin-right: 0;
+                }
+
+                .goods-art-no {
+                  font-size: 16px;
+                  font-weight: 500;
+                  color: #333;
+                }
+              }
+
+              .history-item-right {
+                display: flex;
+                align-items: center;
+                ::v-deep {
+                  .el-button { height: 30px; line-height: 30px;}
+                }
+              }
+            }
+
+            .history-item-meta {
+              display: flex;
+              justify-content: space-between;
+              align-items: center;
+              font-size: 12px;
+              color: #666;
+
+              img {
+                height: 14px;
+                margin-right: 2px;
+              }
+
+              .action-time {
+                color: #666;
+              }
+
+              .image-count {
+                color: #666;
+              }
+            }
+
+            .history-item-images {
+              display: grid;
+              grid-template-columns: repeat(5, 1fr);
+              gap: 10px;
+              padding: 15px;
+              border-top: 1px solid #f0f0f0;
+              overflow-x: auto;
+
+              // 如果图片数量超过5个,允许横向滚动
+              @media (min-width: 1200px) {
+                grid-template-columns: repeat(5, 1fr);
+              }
+
+              // 响应式:小屏幕时每行3个
+              @media (max-width: 768px) {
+                grid-template-columns: repeat(3, 1fr);
+              }
+            }
+
+            .history-item_image_wrap {
+              padding-bottom: 0;
+              border-bottom: none;
+            }
+            .history-item_image {
+              position: relative;
+              width: 100%;
+              aspect-ratio: 1;
+              background: #F7F7F7;
+              border-radius: 10px;
+              overflow: hidden;
+              cursor: pointer;
+              border: 1px solid #D9DEE6;
+              transition: all 0.3s;
+
+
+              .tag {
+                color: #bbb;
+                position: absolute;
+                left: 0;
+                right: 0;
+                top: 50%;
+                margin-top: -10px;
+                line-height: 20px;
+                text-align: center;
+                font-size: 12px;
+                z-index: 1;
+                pointer-events: none;
+              }
+
+              .preview-image {
+                width: 100%;
+                height: 100%;
+
+                :deep(.el-image__inner) {
+                  width: 100%;
+                  height: 100%;
+                  object-fit: cover;
+                }
+              }
+
+              .image-placeholder {
+                width: 100%;
+                height: 100%;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: #F7F7F7;
+              }
+
+              .image-slot {
+                width: 100%;
+                height: 100%;
+                display: flex;
+                align-items: center;
+                justify-content: center;
+                background: #F7F7F7;
+              }
+
+              &:hover {
+                border-color: #409eff;
+                transform: scale(1.02);
+                box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
+              }
+
+              &.el-loading-parent--relative{
+                ::v-deep {
+                  .el-loading-mask { display: none}
+                }
+              }
+            }
+
+
+            .el-image_view {
+              display: flex;
+              width: 100%;
+              height: 100%;
+
+              .reset-button {
+                width: 40px;
+                text-align: center;
+                height: 20px;
+                position: absolute;
+                left:50%;
+                top:50%;
+                padding: 0px;
+                margin-left:-20px;
+                margin-top:-10px;
+                color: #ffffff;
+                font-size: 14px;
+                background: rgba(0,0,0,0.6);
+                border-radius: 12px;
+                display: none;
+                cursor: pointer;
+              }
+              &:hover {
+                .reset-button {
+                  display: block;
+                }
+              }
+            }
+            p:first-of-type {
+              ::v-deep {
+                .el-loading-mask { display: block !important;}
+              }
+            }
+
+
+          }
+        }
+
+        .footer-controls {
+          display: flex;
+          justify-content: space-between;
+          align-items: center;
+          padding: 0px 20px;
+          border-top: 1px solid #D9DEE6;
+          background-color: #fff;
+          min-height: 50px;
+          flex-shrink: 0;
+          position: fixed;
+          bottom:0;
+          left: 0;
+          right: 0;
+          font-size: 14px;
+          z-index: 100;
+          img {
+            height: 12px;
+            margin: 0 5px;
+          }
+          .go {
+            height: 12px;
+            opacity: .8;
+          }
+          ::v-deep {
+            .el-button {
+              border-radius: 10px;
+              height: 40px;
+              line-height: 40px;
+            }
+          }
+
+          .footer-left {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+
+            .select-all-checkbox {
+              margin-right: 0;
+              ::v-deep {
+                .el-checkbox__label {
+
+                  font-size: 14px;
+                  color: #666;
+                }
+              }
+            }
+
+            .image-count-text {
+              font-size: 14px;
+              color: #333;
+              margin-left: 0;
+            }
+          }
+
+          .footer-right {
+            display: flex;
+            align-items: center;
+            gap: 10px;
+          }
+        }
+
+      }
+</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>

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 117 - 772
frontend/src/views/Photography/shot.vue


+ 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">

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

@@ -0,0 +1,193 @@
+<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, watch } 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状态管理实例
+
+// 定义props
+const props = defineProps({
+  camera_configs: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+// 定义emits
+const emit = defineEmits(['update:camera_configs'])
+
+const iso_config = reactive({
+  low: 100,
+  high: 6000,
+  mode: "auto22"
+})
+
+const iso_options = ref(['auto', 100, 200, 400, 800, 1600, 3200, 6400, 12800])
+
+// 监听iso_config变化并更新父组件
+watch(iso_config, (newVal) => {
+  emit('update:camera_configs', {
+    iso_config,
+  })
+}, { deep: true })
+
+onMounted(() => {
+  // 从props初始化数据
+  if (props.camera_configs.iso_config.low !== undefined) {
+    iso_config.low = props.camera_configs.iso_config.low
+  }
+  if (props.camera_configs.iso_config.high !== undefined) {
+    iso_config.high = props.camera_configs.iso_config.high
+  }
+
+  // 读取设备当前可用的 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 =  () => {
+  // 必填校验(允许填写数字或 '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  true;
+/*  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>

+ 443 - 121
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.native.stop="changeSelectId($event,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>
@@ -61,14 +89,19 @@
 </template>
 
 <script setup lang="ts">
-import { ref, defineProps, defineEmits , watch,onMounted, reactive } from 'vue'
+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';
+import tokenInfo from '@/stores/modules/token';
 const clientStore = client();
 import socket from "@/stores/modules/socket";
 const socketStore = socket(); // WebSocket状态管理实例
+const tokenInfoStore = tokenInfo();
 
+import  { getTopTabs, getDeviceConfigs,setLeftRightConfig,restConfig,sortDeviceConfig,setTabName,delDviceConfig } from '@/apis/setting'
 
 // 表格数据和对话框状态
 const tableData = ref([]); // 配置表格数据
@@ -80,50 +113,100 @@ 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(()=>{
+  window.addEventListener('beforeunload', handleBeforeUnload);
   topsTab.value = 'left';
   getTopList()
 })
 
+const handleBeforeUnload = (e)=>{
+  if(dialogVisible.value){
+    // 没有token时,直接关闭实时预览,不显示提示
+    dialogVisible.value = false;
+    // 调用hideVideo逻辑
+    hideVideo();
+    return;
+
+
+    // 检查是否有token
+    const token = tokenInfoStore.getToken;
+    if (!token || token.trim() === '') {
+      // 没有token时,直接关闭实时预览,不显示提示
+      dialogVisible.value = false;
+      // 调用hideVideo逻辑
+      hideVideo();
+      return;
+    }
+
+    // 有token时,显示提示阻止关闭
+    e.preventDefault();
+    const message = '您已打开实时预览弹出框,请先取消或者保存后,关闭编辑弹出框,后再关闭此窗口';
+    e.returnValue = message; // 标准方式
+    ElMessage.error('您已打开实时预览弹出框,请先取消或者保存后,关闭编辑弹出框,后再关闭此窗口,')
+    return message; // 兼容某些浏览器
+  }
+
+}
+
+// 添加hideVideo函数
+function hideVideo(){
+  clientStore.ipc.removeAllListeners(icpList.camera.PreviewHide);
+  clientStore.ipc.send(icpList.camera.PreviewHide);
+  clientStore.ipc.on(icpList.camera.PreviewHide, async () => {
+    // 这里可以添加清理逻辑,比如清除定时器等
+  })
+}
 /**
  * 监听topsTab变化,获取对应标签页的设备配置列表。
  */
 watch(() => topsTab.value, (newTab) => {
-  getTopList();
+  if (!isSortMode.value) {
+    getTopList();
+  }
 });
 
-const getTopList = ()=>{
-      clientStore.ipc.removeAllListeners(icpList.setting.getDeviceTabs);
-      clientStore.ipc.send(icpList.setting.getDeviceTabs, {
+const getTopList = async ()=>{
+      const result = await getTopTabs({
         type: topsTab.value == 'left' ? 0 :1
       })
-      clientStore.ipc.on(icpList.setting.getDeviceTabs, (event, result) => {
-        console.log('getDeviceTabs')
-        console.log(topsTab.value)
-        console.log(result)
-        if(result.code == 0 && result.data){
-          tabs.value = result.data.tabs
-          tabs.value.some(item=>{
-            console.log('=======' )
-            console.log(item.id )
-            console.log(result.data.select_configs[topsTab.value] )
-            if(item.id === result.data.select_configs[topsTab.value]){
-              selectID.value = item.id
-              activeTab.value =  item
-              return true;
-            }
-          })
 
-          getList()
-        }
-        clientStore.ipc.removeAllListeners(icpList.setting.getDeviceTabs);
-      });
+  if(result.code == 0 && result.data){
+    tabs.value = result.data.tabs
+    tabs.value.some(item=>{
+      if(item.id === result.data.select_configs[topsTab.value]){
+        selectID.value = item.id
+        activeTab.value =  item
+        return true;
+      }
+    })
+    if(!selectID.value){
+      selectID.value = tabs.value[0].id
+      activeTab.value =  tabs.value[0]
+    }
+
+    getList()
+  }
 
 }
 
 //切换tab
 const toggleTab = (item) => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   activeTab.value = item
   getList()
 };
@@ -133,54 +216,42 @@ const calibrationId = ref(null) //校准位
 /**
  * 获取设备配置列表。
  */
-const getList = () => {
-  clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
+const getList = async () => {
+  // 如果正在排序模式,先退出
+  if (isSortMode.value) {
+    exitSortMode();
+  }
+
   let params = {
     tab_id: activeTab.value.id
   }
-  clientStore.ipc.send(icpList.setting.getDeviceConfigList, params);
-  clientStore.ipc.on(icpList.setting.getDeviceConfigList, (event, result) => {
-    console.log('getDeviceConfigList')
-    console.log(params)
-    console.log(result)
-    if (result.code == 0) {
-      tableData.value = result.data.list;
-      const calibration =  result.data.list.filter(item=>(item.action_name === '侧视'))
-      if(calibration.length >= 1){
-        calibrationId.value = calibration[0].id
-      }else{
+  const result =  await getDeviceConfigs(params)
 
-        calibrationId.value = tableData.value[0].id
-      }
-    } else {
-      ElMessage.error('获取列表失败');
+  if (result.code == 0) {
+    tableData.value = result.data.list;
+    const calibration =  result.data.list.filter(item=>(item.action_name === '侧视'))
+    if(calibration.length >= 1){
+      calibrationId.value = calibration[0].id
+    }else{
+      calibrationId.value = tableData.value[0].id
     }
-    clientStore.ipc.removeAllListeners(icpList.setting.getDeviceConfigList);
-  });
+  }
 };
 
 
-const changeSelectId = (id)=>{
+const changeSelectId = async (e,id)=>{
+  if (e.target.tagName === 'INPUT') return;
+  if (isSortMode.value) return; // 排序模式下禁用
   if(id === selectID.value) return;
-  clientStore.ipc.removeAllListeners(icpList.setting.updateLeftRightConfig);
   let params = {
     type: topsTab.value,
     id,
   }
-  clientStore.ipc.send(icpList.setting.updateLeftRightConfig, params);
-  clientStore.ipc.on(icpList.setting.updateLeftRightConfig, (event, result) => {
-    console.log('updateLeftRightConfig')
-    console.log(params)
-    console.log(result)
-    if (result.code == 0) {
-      selectID.value = id;
-    } else if(result.mssg){
-      ElMessage.error(result.mssg);
-    }else{
-      ElMessage.error('切换失败');
-    }
-    clientStore.ipc.removeAllListeners(icpList.setting.updateLeftRightConfig);
-  });
+  const result = await setLeftRightConfig(params)
+  if (result.code == 0) {
+    selectID.value = id;
+  }
+
 }
 
 /**
@@ -189,6 +260,8 @@ const changeSelectId = (id)=>{
  * @param {number} index - 当前行的索引
  */
 const editRow = (row, index) => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   addRowData.value = {}
   dialogVisible.value = true;
   editId.value = row.id
@@ -200,25 +273,23 @@ const editRow = (row, index) => {
  * @param {number} index - 当前行的索引
  */
 const deleteRow = (row, index) => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   ElMessageBox.confirm('确定删除该步骤吗?', '提示', {
     confirmButtonText: '确定',
     cancelButtonText: '取消',
     type: 'warning'
-  }).then(() => {
-    clientStore.ipc.send(icpList.setting.removeDeviceConfig, {
+  }).then(async () => {
+
+
+    const result =  await delDviceConfig({
       id: row.id
-    });
-    clientStore.ipc.on(icpList.setting.removeDeviceConfig, (event, result) => {
-      if (result.code == 0) {
-        getList();
-        ElMessage.success('删除成功');
-      }  else if(result.mssg){
-        ElMessage.error(result.mssg);
-      }else {
-        ElMessage.error('删除失败');
-      }
-      clientStore.ipc.removeAllListeners(icpList.setting.removeDeviceConfig);
-    });
+    })
+
+    if (result.code == 0) {
+      getList();
+      ElMessage.success('删除成功');
+    }
   });
 };
 
@@ -226,31 +297,29 @@ const deleteRow = (row, index) => {
  * 重置设备配置。
  */
 const resetConfig = () => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   console.log(activeTab.value);
   ElMessageBox.confirm(`确定初始化${activeTab.value.mode_name}吗?`, '提示', {
     confirmButtonText: '确定',
     cancelButtonText: '取消',
     type: 'warning'
-  }).then(() => {
-    clientStore.ipc.removeAllListeners(icpList.setting.resetDeviceConfig);
-    clientStore.ipc.send(icpList.setting.resetDeviceConfig, {
+  }).then(async () => {
+
+    const result =  await restConfig({
       tab_id: activeTab.value.id
-    });
-    clientStore.ipc.on(icpList.setting.resetDeviceConfig, (event, result) => {
-      if (result.code == 0) {
-        getList();
-        ElMessage.success('重置成功');
-      } else if(result.mssg){
-        ElMessage.error(result.mssg);
-      } else {
-        ElMessage.error('重置失败');
-      }
-      clientStore.ipc.removeAllListeners(icpList.setting.resetDeviceConfig);
-    });
+    })
+    if (result.code == 0) {
+      getList();
+      ElMessage.success('重置成功');
+    }
+
   });
 };
 
 const reName = ()=>{
+  if (isSortMode.value) return; // 排序模式下禁用
+
   ElMessageBox.prompt('', '重命名配置', {
     confirmButtonText: '保存',
     cancelButtonText: '取消',
@@ -263,28 +332,23 @@ const reName = ()=>{
       return true;
     },
   })
-      .then(({ value }) => {
-        clientStore.ipc.removeAllListeners(icpList.setting.updateTabName);
-        clientStore.ipc.send(icpList.setting.updateTabName, {
+      .then(async ({ value }) => {
+
+
+
+        const result =  await setTabName({
           id: activeTab.value.id,
           mode_name:value,
-        });
-        clientStore.ipc.on(icpList.setting.updateTabName, (event, result) => {
-          if (result.code == 0) {
-            activeTab.value.mode_name = value
-            tabs.value.some(item=>{
-              if(item.id ===  activeTab.value.id){
-                item.mode_name   =  activeTab.value.mode_name
-              }
-            })
-            ElMessage.success('重命名成功');
-          }  else if(result.mssg){
-            ElMessage.error(result.mssg);
-          }else {
-            ElMessage.error('重命名失败');
-          }
-          clientStore.ipc.removeAllListeners(icpList.setting.updateTabName);
-        });
+        })
+        if (result.code == 0) {
+          activeTab.value.mode_name = value
+          tabs.value.some(item=>{
+            if(item.id ===  activeTab.value.id){
+              item.mode_name   =  activeTab.value.mode_name
+            }
+          })
+          ElMessage.success('重命名成功');
+        }
       })
 }
 
@@ -292,6 +356,8 @@ const reName = ()=>{
  * 新增一行配置。
  */
 const addRow = () => {
+  if (isSortMode.value) return; // 排序模式下禁用
+
   editId.value = -1
   let length = Number(tableData.value.length)+1
   addRowData.value =   {
@@ -313,14 +379,205 @@ 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 = async () => {
+  // 准备排序数据
+  const sortData = tableData.value.map((item, index) => ({
+    id: item.id,
+    action_index: index + 1
+  }));
+
+  console.log("sort_data",sortData)
+  const result =  await sortDeviceConfig({
+    sorts: sortData
+  })
+  if (result.code == 0) {
+    getList();
+    ElMessage.success('排序成功');
+  }
+};
+
+/**
+ * 获取行类名
+ */
+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>
 .top_tabs {
-  height: 30px;
+  height: 40px;
   overflow: hidden;
   border: 1px solid #c8c8c8;
   border-bottom: none;
+  ::v-deep {
+    .el-tabs__item {
+      height: 40px;
+      line-height: 40px;
+      padding: 0 15px;
+      font-size: 14px;
+      color: #333;
+      &.is-active {
+        color: #2957FF;
+      }
+    }
+  }
 }
 .two_tabs {
   width: 100%;
@@ -394,6 +651,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{

+ 106 - 0
frontend/src/views/Setting/components/otherConfig.vue

@@ -0,0 +1,106 @@
+<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()" 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>
+
+  <!-- 新增:充值金币弹窗 -->
+  <el-dialog v-model="showRechargeDialog"
+             title="充值金币"
+             width="700px"
+             :height="630 + 'px'"
+             @close="onClose"
+             :destroy-on-close="true">
+    <!-- 显示 loading 提示 -->
+    <div v-if="loading" class="loading-text flex" style="width: 660px; height: 610px;" >加载中,请稍候...</div>
+    <iframe v-show="!loading" :src="rechargeUrl" width="660" height="610" frameborder="0" @load="onIframeLoad"></iframe>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { configs } from '@/utils/appconfig';
+import { ref, onMounted, onUnmounted } from "vue";
+import useUserInfo from "@/stores/modules/user";
+const useUserInfoStore = useUserInfo();
+import tokenInfo from '@/stores/modules/token';
+const tokenInfoStore = tokenInfo();
+import { getWebUrlrUrl } from '@/utils/appfun'
+
+
+
+// 数据定义
+const showRechargeDialog = ref(false);
+const rechargeUrl = ref(getWebUrlrUrl({
+  url:'/other/recharge_gold_coin'
+}));
+const loading = ref(true); // 新增 loading 状态
+
+// iframe 加载完成回调
+const onIframeLoad = () => {
+  loading.value = false;
+};
+
+// 消息监听函数
+const handleWindowMessage = async (event) => {
+  // 可选:验证 event.origin 来确保消息来源可信
+  const message = event.data;
+
+  if (message.type === 'close') {
+    showRechargeDialog.value = false;
+    await useUserInfoStore.getInfo();
+    loading.value = false;
+  }
+};
+
+const onClose = async () => {
+  await useUserInfoStore.getInfo();
+  loading.value = false;
+};
+
+const openDialog = () => {
+  showRechargeDialog.value = true;
+  loading.value = false;
+};
+// 绑定监听
+onMounted(() => {
+  window.addEventListener('message', handleWindowMessage);
+});
+
+// 移除监听
+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>

+ 365 - 133
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="activeIndex = 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="activeIndex = 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="activeIndex = 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>
@@ -25,15 +31,30 @@
       <!--基础配置-->
           <div class="selectBox" v-if="activeIndex === 0">
                 <div class="form-item">
-                    <label>主图尺寸:</label>
-                    <div class="select-wrapper">
+                  <label>主图尺寸:</label>
+                  <div class="select-wrapper">
                     <el-select multiple
                                collapse-tags
                                multiple-limit="3"
                                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>
+                 <!-- 新增自定义输入框 -->
+                <div class="form-item" v-if="formData.basic_configs.main_image_size.includes('custom')">
+                  <label>自定义尺寸:</label>
+                  <div class="select-wrapper">
+                    <el-input
+                        type="text"
+                        v-model.number="customInput"
+                        maxlength="4"
+                        placeholder="请输入1-2000的尺寸值"
+                        class="w-full px-3 py-2 border rounded-md"
+                        @keypress="handleKeyPress"
+                        @input="handleInput"
+                    />
+                  </div>
                 </div>
                 <div class="form-item">
                     <label>图片输出格式:</label>
@@ -51,43 +72,79 @@
                     </el-select>
                     </div>
                 </div>
-        </div>
-      <!--基础配置-->
-      <!--其他设置-->
-          <div class="selectBox" style="padding-top: 0px" v-if="activeIndex === 2">
                 <div class="form-item">
-                    <label>产品类型:</label>
+                    <label>800图自定义边距:</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>
+                      <el-input
+                          type="number"
+                          v-model.number="formData.basic_configs.padding_800image"
+                          :min="0"
+                          :max="500"
+                          placeholder="请输入0-500的整数"
+                          @keypress="handlePaddingKeyPress"
+                          @input="handlePaddingInput"
+                      />
                     </div>
                 </div>
                 <div class="form-item">
-                    <label>默认抠图模式:</label>
+                    <label>800图是否翻转:</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 v-model.number="formData.basic_configs.is_flip_800image" placeholder="请选择">
+                      <el-option v-for="item in isFlip800ImageList" :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>-->
+
+               <DebugPanel ref="debugPanel" />
+        </div>
+      <!--基础配置-->
+      <!--相机配置-->
+      <template v-if="activeIndex === 3">
+      <CameraConfig ref="cameraConfigRef" :camera_configs="formData.camera_configs" @update:camera_configs="updateCameraConfigs"/>
+
+      </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>
+
+          <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/>
@@ -95,7 +152,7 @@
           </div>
     </div>
       <div class="text-center mt-8">
-        <button class="bg-gradient-to-r from-primary" @click="saveSetting(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>
@@ -113,6 +170,7 @@
 import { ref, reactive } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { onMounted, watch } from 'vue';
+import  { getAllUserConfigs,  setAllUserConfigs } from '@/apis/setting'
 import socket from "@/stores/modules/socket";
 import headerBar from '@/components/header-bar/index.vue';
 import client from "@/stores/modules/client";
@@ -123,8 +181,39 @@ import { digiCamControlWEB } from  '@/utils/appconfig'
 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();
@@ -146,7 +235,9 @@ const formData = reactive({
   basic_configs:{
     "main_image_size": [],//主图尺寸
     "image_out_format": "",//图片输出格式
-    "image_sharpening": "" //图片锐化
+    "image_sharpening": "", //图片锐化
+    "padding_800image": 100, //800图自定义边距
+    "is_flip_800image": 1 //800图是否翻转
   },
   //拍照配置
   take_photo_configs:{
@@ -161,7 +252,7 @@ const formData = reactive({
     "device_speed": "",//设备运动速度
     "running_mode": "" //运行模式
   },
-  captureOneFolder: '', // Capture One文件夹路径
+/*  captureOneFolder: '', // Capture One文件夹路径
   mainImageSize: '', // 主图尺寸
   imageFormat: '', // 图片格式
   imageSharpening: '', // 图片锐化
@@ -178,7 +269,7 @@ const formData = reactive({
   left: '', // 左脚配置
   right: '', // 右脚配置
   up: '', // 上移配置
-  down: '', // 下移配置
+  down: '', // 下移配置*/
 });
 
 // 配置选项列表
@@ -191,11 +282,17 @@ const mainImageSizeList = ref([
   { label: '1200*1200', value: 1200 },
   { label: '1400*1400', value: 1400 },
   { label: '1600*1600', value: 1600 },
+  { label: '自定义', value: 'custom' } // 新增自定义选项
 ]);
+
+const customInput = ref(null); // 新增自定义输入值
+
 const imageFormatList = ref([
   { label: 'jpg', value: 'jpg' },
   { label: 'png', value: 'png' },
   { label: 'jpeg', value: 'jpeg' },
+  { label: 'webp', value: 'webp' },
+  { label: 'avif', value: 'avif' },
 ]);
 const imageSharpeningList = ref([
   { label: '0', value: '0' },
@@ -203,6 +300,10 @@ const imageSharpeningList = ref([
   { label: '2', value: '2' },
   { label: '3', value: '3' },
 ]);
+const isFlip800ImageList = ref([
+  { label: '是', value: 1 },
+  { label: '否', value: 0 },
+]);
 const repeatWarningList = ref([
   { label: '关闭', value: false },
   { label: '开启', value: true },
@@ -227,9 +328,9 @@ const defaultCutoutModeList = ref([
   { label: '精细化抠图', value: '精细化抠图' },
 ]);
 const deviceSpeedList = ref([
-  { label: '一档', value: '1' },
-  { label: '二档', value: '2' },
-  { label: '三档', value: '3' },
+  { label: '快', value: '一档' },
+  { label: '中', value: '二档' },
+  { label: '慢', value: '三档' },
 ]);
 /*
 const runModeList = ref([
@@ -269,33 +370,15 @@ const indexKey  ={
   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;
-  if(typeValue === 4) return;
-  switch (typeValue) {
-      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 });
+  getConfig()
+});
+
 
 
 /**
@@ -310,12 +393,52 @@ watch(() => activeIndex.value, (newIndex) => {
   });
 });
 
+const getConfig =  async (typeValue)=>{
+
+  const resultPHP = await getAllUserConfigs();
+  if(resultPHP.code == 0 &&  resultPHP.data.configs ){
+      Object.keys(resultPHP.data.configs).map(item=>{
+        formData[item] =  resultPHP.data.configs[item]
+      })
+
+    const presetSizes = mainImageSizeList.value.map(item => item.value);
+    const receivedSizes = formData.basic_configs.main_image_size ? [...formData.basic_configs.main_image_size] : [];
+
+    // 分离自定义值
+    const customValues = receivedSizes.filter(v => !presetSizes.includes(v));
+    if (customValues.length > 0 && activeIndex.value == 0) {
+      customInput.value = customValues[0]; // 保留第一个自定义值
+      // 更新选中状态
+      console.log('111')
+      formData.basic_configs.main_image_size = receivedSizes
+          .filter(v => presetSizes.includes(v))
+          .concat('custom');
+    } else {
+      console.log('222')
+      formData.basic_configs.main_image_size = receivedSizes;
+    }
+    console.log(formData.basic_configs.main_image_size);
+
+    // 确保新字段有默认值(如果服务器没有返回)
+    if (formData.basic_configs.padding_800image === undefined || formData.basic_configs.padding_800image === null) {
+      formData.basic_configs.padding_800image = 100;
+    }
+    if (formData.basic_configs.is_flip_800image === undefined || formData.basic_configs.is_flip_800image === null) {
+      formData.basic_configs.is_flip_800image = 0;
+    }
+  }
+
+}
 /**
  * 组件挂载时初始化activeIndex。
  */
-onMounted(() => {
+onMounted(async () => {
+  getConfig()
+
+  let  type = 0 ;
   if (route.query.type) {
     const typeValue = parseInt(route.query.type);
+    type  = typeValue
     if (!isNaN(typeValue) && typeValue >= 0 && typeValue <= 3) {
       activeIndex.value = typeValue;
     }
@@ -324,36 +447,137 @@ onMounted(() => {
 
 });
 
+
+const handleKeyPress = (event) => {
+  const char = event.key;
+  // 只允许输入数字字符
+  if (!/^\d+$/.test(char)) {
+    event.preventDefault(); // 阻止非数字输入
+  }
+};
+const handleInput = (value) => {
+  if (value > 2000) {
+    customInput.value = 2000;
+  } else if (value < 1) {
+    customInput.value = 1;
+  }
+};
+
+// 处理800图边距输入
+const handlePaddingKeyPress = (event) => {
+  const char = event.key;
+  // 只允许输入数字字符
+  if (!/^\d+$/.test(char)) {
+    event.preventDefault(); // 阻止非数字输入
+  }
+};
+
+const handlePaddingInput = () => {
+  let value = formData.basic_configs.padding_800image;
+  // 处理空值或无效值
+  if (value === null || value === undefined || value === '' || isNaN(value)) {
+    formData.basic_configs.padding_800image = 0;
+    return;
+  }
+  // 转换为数字并确保是整数
+  value = Number(value);
+  value = Math.floor(value);
+
+  // 限制范围
+  if (value > 500) {
+    formData.basic_configs.padding_800image = 500;
+  } else if (value < 0) {
+    formData.basic_configs.padding_800image = 0;
+  } else {
+    formData.basic_configs.padding_800image = value;
+  }
+};
+
+// 添加更新camera_configs的方法
+const updateCameraConfigs = (configs) => {
+  formData.camera_configs = configs;
+};
+
+const toggleTab = async (item) => {
+  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;
+  return true;
+};
+
+const onSava = async (index)=>{
+  const next =  await  saveSetting(index)
+
+  if(next !== false){
+    ElMessage.success('保存成功')
+  }
+
+}
+
 /**
  * 保存当前表单配置。
  */
 const saveSetting = async (index) => {
-
-  if(index === 0){
-    if(formData.basic_configs.main_image_size.length === 0){
-
-      ElMessage.error('请选择主图尺寸!');
-      return;
+  // 构建临时提交数据
+  if(index === 3) {
+    if (cameraConfigRef.value && typeof cameraConfigRef.value.save === 'function') {
+      if(! cameraConfigRef.value.save()) return false;
     }
   }
-  await new Promise((resolve, reject) => {
+  const submitData = JSON.parse(JSON.stringify(formData));
+  if(index === 0) {
+    if (formData.basic_configs.main_image_size.length === 0) {
+      ElMessage.error('请选择主图尺寸!');
+      return false;
+    }
 
-    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 selectedSizes = [...formData.basic_configs.main_image_size]; // 创建副本避免修改原始数据
+
+    if (selectedSizes.includes('custom')) {
+      if (!customInput.value || isNaN(customInput.value) ||
+          customInput.value < 1 || customInput.value > 2000) {
+        ElMessage.error('请输入1-2000之间的有效数值');
+        return false;
       }
 
-    });
-  });
+      // 创建新数组用于提交
+      const submitSizes = selectedSizes
+          .filter(v => v !== 'custom')
+          .concat(parseInt(customInput.value));
 
+      submitData.basic_configs.main_image_size = submitSizes
+    }
+
+    // 验证800图自定义边距
+    const paddingValue = formData.basic_configs.padding_800image;
+    if (paddingValue === undefined || paddingValue === null || isNaN(paddingValue) ||
+        paddingValue < 0 || paddingValue > 500) {
+      ElMessage.error('800图自定义边距必须在0-500之间');
+      return false;
+    }
+    // 确保是整数
+    submitData.basic_configs.padding_800image = Math.floor(paddingValue);
+
+    // 验证800图是否翻转
+    const flipValue = formData.basic_configs.is_flip_800image;
+    if (flipValue === undefined || flipValue === null || (flipValue !== 0 && flipValue !== 1)) {
+      ElMessage.error('请选择800图是否翻转');
+      return false;
+    }
+    submitData.basic_configs.is_flip_800image = flipValue;
+
+  }
+  const params = JSON.parse(JSON.stringify({
+    configs:submitData
+  }))
+  const result = await setAllUserConfigs(params)
+  if(result.code != 0)  return  false;
+  return  true;
 
 };
 
@@ -365,6 +589,55 @@ 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);
+    ::v-deep(.el-tabs__header){
+      padding-left: 0;
+    }
+    ::v-deep(.el-tabs--card>.el-tabs__header){
+      border-bottom: 1px solid #CCCCCC;
+    }
+    ::v-deep(.el-tabs__item){
+      height: 30px;
+      line-height: 30px;
+    }
+    ::v-deep(.el-tabs__nav-wrap){
+      margin-bottom: 0;
+    }
+    ::v-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;
+    width: 140px;
+    flex-shrink: 0;
+    text-align: right;
+    font-size: 14px;
+    color: #1A1A1A;
+    padding-right: 12px;
+  }
+
+  .select-wrapper {
+    position: relative;
+    width: 200px;
+    ::v-deep(.el-input__inner){
+      border-radius: 6px;
+    }
+  }
+}
 </style>
 <style lang="scss" scoped>
 body {
@@ -418,21 +691,10 @@ body {
   border-radius: 12px;
   padding: 30px;
   padding-top: 10px;
+  padding-bottom: 40px;
   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;
+  min-height: 400px;
 }
 .input-group {
   display: flex;
@@ -445,13 +707,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;
@@ -470,29 +725,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;
@@ -535,14 +767,14 @@ body {
   .flex-row{
     display: flex;
     align-items: center;
-    :deep(.el-radio){
+    ::v-deep(.el-radio){
       margin-right: 6px !important;
     }
-    :deep(.el-radio__label){
+    ::v-deep(.el-radio__label){
       padding-left: 4px;
     }
   }
-  :deep(.el-form-item) {
+  ::v-deep(.el-form-item) {
     margin-bottom: 0;
     .el-form-item__label {
       width: 120px !important;
@@ -592,7 +824,7 @@ body {
         }
 
         input[type="number"] {
-          -moz-appearance: number-input; /* Firefox */
+          -moz-appearance: textfield; /* Firefox */
         }
       }
     }

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels