Pārlūkot izejas kodu

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

# Conflicts:
#	frontend/src/views/Photography/detail.vue
panqiuyao 1 dienu atpakaļ
vecāks
revīzija
4738245415

+ 9 - 2
electron/api/setting.js

@@ -1,4 +1,5 @@
 const { post,get } = require('./request')
+const { readConfigFile } = require('../utils/config');
 
 module.exports = {
 
@@ -114,7 +115,10 @@ module.exports = {
     console.log("syncSysConfigs===============", data);
     return post({
       url: '/sync_sys_configs',
-      data: data
+      data: {
+        ...data,
+        env:readConfigFile().env
+      }
     })
   },
 
@@ -125,7 +129,10 @@ module.exports = {
     console.log("syncActions===============", data);
     return post({
       url: '/sync_actions',
-      data: data
+      data: {
+        ...data,
+        env:readConfigFile().env
+      }
     })
   },
 

+ 22 - 0
frontend/src/App.vue

@@ -11,6 +11,7 @@
 </template>
 <script setup lang="ts">
 import { useRoute, useRouter } from 'vue-router'
+import { onMounted, onUnmounted } from 'vue'
 import Login from '@/components/login/index.vue';
 import useUserInfo from "@/stores/modules/user";
 const useUserInfoStore = useUserInfo();
@@ -20,12 +21,33 @@ listenerOnline();
 
 const router = useRouter()
 const route = useRoute()
+
 /* keep alive 的路由名称 */
 const keepAliveIncludes = router
     .getRoutes()
     .filter((router) => router.meta?.keepAlive)
     .map((router) => router.name as string)
 
+/* 监听企业过期事件 */
+const handleCompanyExpired = (event: CustomEvent) => {
+  const { message } = event.detail
+  console.log('企业过期事件触发:', message)
+
+  // 如果当前不在过期页面,进行跳转
+  if (route.path !== '/photography/expired') {
+    console.log('跳转到过期页面')
+    router.replace('/photography/expired')
+  }
+}
+
+onMounted(() => {
+  window.addEventListener('companyExpired', handleCompanyExpired as EventListener)
+})
+
+onUnmounted(() => {
+  window.removeEventListener('companyExpired', handleCompanyExpired as EventListener)
+})
+
 
 
 </script>

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

@@ -139,6 +139,32 @@ export async function setTabName(data){
     return result;
 }
 
+
+
+
+//拷贝配置
+export async function copyDeviceConfig(data){
+    const result = await POST('/api/ai_image/camera_machine/copy_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: 'copy_device_config',
+            id: data.tab_id,
+            mode_name: data.tab_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);

+ 6 - 0
frontend/src/apis/user.ts

@@ -44,3 +44,9 @@ export function selectCompany(data) {
     return POST('/api/backend/account/select_company',data)
 }
 
+
+// 获取企业状态
+export function getCompanyStatus(data) {
+    return GET('/api/ai_image/auto_photo/get_company_status',data)
+}
+

+ 14 - 6
frontend/src/components/ModelGeneration/index.vue

@@ -10,7 +10,7 @@
           <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" />
+              class="selected-model-image" lazy :preview-src-list="[selectedFemaleModel.image_url]" hide-on-click-modal fit="cover" />
             <div v-else class="placeholder-image">
               <span>请在下方列表选择</span>
             </div>
@@ -22,7 +22,7 @@
           <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" />
+              class="selected-model-image" lazy :preview-src-list="[selectedMaleModel.image_url]"  hide-on-click-modal fit="cover" />
             <div v-else class="placeholder-image">
               <span>请在下方列表选择</span>
             </div>
@@ -75,6 +75,7 @@ interface Props {
     female?: any
     male?: any
   }
+  defaultActiveTab?: 'female' | 'male'
 }
 
 const props = defineProps<Props>()
@@ -275,14 +276,21 @@ const preloadImages = (models: ModelData[]) => {
 watch(dialogVisible, (newValue) => {
   if (newValue) {
     fetchModelList()
-    
+
+    // 设置默认激活的标签页
+    if (props.defaultActiveTab) {
+      activeTab.value = props.defaultActiveTab
+    } else {
+      activeTab.value = 'female' // 默认女模特
+    }
+
     // 初始化时接收父组件传递的模特数据
     if (props.initialModels) {
-      
+
       if (props.initialModels.female) {
         selectedFemaleModel.value = props.initialModels.female
       }
-      
+
       if (props.initialModels.male) {
         selectedMaleModel.value = props.initialModels.male
       }
@@ -548,4 +556,4 @@ watch(dialogVisible, (newValue) => {
     font-weight: 600;
     color: #303133;
   }
-}</style>
+}</style>

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

@@ -72,6 +72,15 @@ const routes: RouteRecordRaw[] = [
         }
     },
     {
+        path: "/photography/expired",
+        name: "PhotographyExpired",
+        component: () => import("@/views/Photography/expired.vue"),
+        meta: {
+            title: '企业账户过期',
+            noAuth: true
+        }
+    },
+    {
         path: "/remote_control",
         name: "RemoteControl",
         component: () => import("@/views/RemoteControl/index.vue"),

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

@@ -23,6 +23,12 @@ export function authGuard(router: Router) {
       if(!useUserInfoStore.userInfo.id){
           await useUserInfoStore.getInfo()
       }
+
+      // 检查企业是否过期,如果过期跳转到到期页面
+      if (useUserInfoStore.isExpired && to.path !== '/photography/expired') {
+        return next('/photography/expired')
+      }
+
       return next()
     } else {
       if(to.meta.noAuth)  return next()

+ 87 - 2
frontend/src/stores/modules/user.ts

@@ -1,9 +1,9 @@
 import { defineStore } from 'pinia';
 import { ref, computed } from 'vue';
-import { getUserInfo, login } from '@/apis/user';
+import { getUserInfo, login, getCompanyStatus } from '@/apis/user';
 import tokenInfo from '@/stores/modules/token';
 import client from '@/stores/modules/client';
-import { useRouter } from 'vue-router';
+import { ElMessage } from 'element-plus';
 
 
 export const useUserInfo = defineStore('userInfo', () => {
@@ -13,6 +13,12 @@ export const useUserInfo = defineStore('userInfo', () => {
 
   const loginShow = ref(false); // 控制登录弹窗的显示状态
   const userInfo = ref({}); // 用户的详细信息
+  const isExpired = ref(false); // 企业账户是否过期
+  const expiredMessage = ref(''); // 过期提示消息
+
+  // 定时检查相关
+  let checkInterval: NodeJS.Timeout | null = null; // 定时器
+  const CHECK_INTERVAL = 10 * 60 * 1000; // 10分钟检查一次
 
   // Actions(操作)
 
@@ -83,6 +89,9 @@ export const useUserInfo = defineStore('userInfo', () => {
         // 关闭窗口失败不影响退出登录流程
       }
 
+      // 停止定期检查
+      stopPeriodicCheck();
+
       // 清空用户信息
       try {
         tokenInfoStore.clearToken()
@@ -112,6 +121,70 @@ export const useUserInfo = defineStore('userInfo', () => {
   };
 
   /**
+   * 检查企业状态并处理跳转
+   * @returns {Promise<boolean>} 是否过期
+   */
+  const checkCompanyStatus = async (): Promise<boolean> => {
+    try {
+      const statusRes = await getCompanyStatus({});
+      const statusData = statusRes.data;
+      if (statusData && statusData.status === 0) {
+        // 已过期,设置过期状态
+        isExpired.value = true;
+        expiredMessage.value = statusData.message || '企业账户已过期,请联系管理员';
+
+        // 触发自定义事件,通知组件进行跳转
+        if (typeof window !== 'undefined') {
+          window.dispatchEvent(new CustomEvent('companyExpired', {
+            detail: { message: expiredMessage.value }
+          }));
+        }
+
+        return true;
+      } else {
+        // 未过期,重置状态
+        isExpired.value = false;
+        expiredMessage.value = '';
+        return false;
+      }
+    } catch (statusError) {
+      console.error('获取企业状态失败:', statusError);
+      // 企业状态获取失败不影响正常使用,只记录错误
+      isExpired.value = false;
+      expiredMessage.value = '';
+      return false;
+    }
+  };
+
+  /**
+   * 启动定期检查企业状态
+   */
+  const startPeriodicCheck = () => {
+    // 清除已有的定时器
+    if (checkInterval) {
+      clearInterval(checkInterval);
+    }
+
+    // 设置新的定时器,每5分钟检查一次
+    checkInterval = setInterval(async () => {
+      await checkCompanyStatus();
+    }, CHECK_INTERVAL);
+
+    console.log('企业状态定期检查已启动');
+  };
+
+  /**
+   * 停止定期检查企业状态
+   */
+  const stopPeriodicCheck = () => {
+    if (checkInterval) {
+      clearInterval(checkInterval);
+      checkInterval = null;
+      console.log('企业状态定期检查已停止');
+    }
+  };
+
+  /**
    * 获取用户信息并更新状态。
    *
    * @returns {Promise<any>} 用户信息数据。
@@ -132,6 +205,13 @@ export const useUserInfo = defineStore('userInfo', () => {
         throw new Error('请重新登录!');
       }
       updateUserInfo(data); // 更新用户信息
+
+      // 获取企业状态,检查是否过期
+      await checkCompanyStatus();
+
+      // 启动定期检查
+      startPeriodicCheck();
+
       return data;
     } catch (error) {
       console.error('获取用户信息失败:', error);
@@ -142,11 +222,16 @@ export const useUserInfo = defineStore('userInfo', () => {
   return {
     loginShow,
     userInfo,
+    isExpired,
+    expiredMessage,
     updateUserInfo,
     updateLoginShow,
     loginAction,
     loginOut,
     getInfo,
+    checkCompanyStatus,
+    startPeriodicCheck,
+    stopPeriodicCheck,
   };
 });
 

+ 46 - 11
frontend/src/views/Photography/detail.vue

@@ -448,8 +448,13 @@
   </el-dialog>
 
   <!-- 模特生成弹窗 -->
-  <ModelGenerationDialog v-model="modelDialogVisible" :initial-models="selectedModels" @confirm="handleModelSelection"
-    @cancel="modelDialogVisible = false" />
+  <ModelGenerationDialog
+    v-model="modelDialogVisible"
+    :initial-models="selectedModels"
+    :default-active-tab="modelDialogDefaultTab"
+    @confirm="handleModelSelection"
+    @cancel="modelDialogVisible = false"
+  />
 
   <!-- 场景提示词弹窗 -->
   <ScenePromptDialog v-model="scenePromptDialogVisible" :initial-prompt="scenePrompt"
@@ -520,7 +525,6 @@
 
 import { getCompanyTemplatesApi } from '@/apis/other'
 import tokenInfo from '@/stores/modules/token';
-import useUserInfo from "@/stores/modules/user";
 import { useRoute, useRouter } from 'vue-router'
 import { clickLog, setLogInfo } from '@/utils/log'
 
@@ -1162,6 +1166,7 @@ const onlineStoreTempList = ref<any[]>([]) // 国内电商平台列表
 const onlineStoreTempListForeign = ref<any[]>([]) // 国外电商平台列表
 // 模特与场景弹窗
 const modelDialogVisible = ref(false)
+const modelDialogDefaultTab = ref<'female' | 'male'>('female')
 const scenePromptDialogVisible = ref(false)
 const selectedModels = ref<{ female: any; male: any } | null>(null)
 const scenePrompt = ref('')
@@ -1304,8 +1309,21 @@ const loadTemplateFromCache = () => {
   ensureDefaultTemplateSelection({ ensureVisible: true })
 }
 
-const openModelDialog = () => {
+const openModelDialog = (eventOrDefaultTab?: MouseEvent | 'female' | 'male') => {
   modelDialogVisible.value = true
+  let defaultTab: 'female' | 'male' | undefined
+
+  if (typeof eventOrDefaultTab === 'string') {
+    defaultTab = eventOrDefaultTab
+  }
+
+  if (defaultTab) {
+    // 传递给子组件的默认标签页
+    modelDialogDefaultTab.value = defaultTab
+  } else {
+    // 重置为默认值
+    modelDialogDefaultTab.value = 'female'
+  }
 }
 const openScenePromptDialog = () => {
   scenePromptDialogVisible.value = true
@@ -1317,7 +1335,7 @@ const handleModelSelection = (models: { female: any; male: any }) => {
   selectedModels.value = models
   saveModelsToCache(models)
   modelDialogVisible.value = false
-  ElMessage.success('模特选择完成!')
+  // ElMessage.success('模特选择完成!')
 }
 const handleScenePromptConfirm = (prompt: string) => {
   scenePrompt.value = prompt
@@ -1575,12 +1593,29 @@ const generate = async function () {
   }
 
   // 必填验证
-  if (form.services.includes('is_upper_footer') && !(selectedModels.value && selectedModels.value.male?.id && selectedModels.value.female?.id)) {
-    openModelDialog();
-    setTimeout(() => {
-      ElMessage.error('请选择模特')
-    }, 200)
-    return
+  if (form.services.includes('is_upper_footer')) {
+    const hasMale = selectedModels.value?.male?.id
+    const hasFemale = selectedModels.value?.female?.id
+
+    if (!hasMale && !hasFemale) {
+      openModelDialog();
+      setTimeout(()=>{
+        ElMessage.error('请选择男模特和女模特')
+      },200)
+      return
+    } else if (!hasMale) {
+      openModelDialog('male');
+      setTimeout(()=>{
+        ElMessage.error('请选择男模特')
+      },200)
+      return
+    } else if (!hasFemale) {
+      openModelDialog('female');
+      setTimeout(()=>{
+        ElMessage.error('请选择女模特')
+      },200)
+      return
+    }
   }
 
   if (form.services.includes('is_product_scene') && !scenePrompt.value) {

+ 292 - 0
frontend/src/views/Photography/expired.vue

@@ -0,0 +1,292 @@
+<template>
+  <div class="expired-page">
+    <div class="expired-container">
+      <!-- 顶部警告图标区域 -->
+      <div class="expired-header">
+        <div class="expired-icon">
+          <el-icon size="80" color="#FF4D4F">
+            <Warning />
+          </el-icon>
+        </div>
+        <h1 class="expired-title">企业账户已过期</h1>
+      </div>
+
+      <!-- 主要内容区域 -->
+      <div class="expired-content">
+        <div class="expired-message">
+          <p class="message-text">{{ expiredMessage }}</p>
+<!--          <p class="message-hint">请联系管理员处理后重新登录使用</p>-->
+        </div>
+
+        <!-- 操作按钮 -->
+        <div class="expired-actions">
+<!--          <el-button
+            type="primary"
+            size="large"
+            @click="handleContactAdmin"
+            class="action-btn"
+          >
+            联系管理员
+          </el-button>-->
+          <el-button
+            type="default"
+            size="large"
+            @click="handleLogout"
+            class="action-btn logout-btn"
+          >
+            重新登录
+          </el-button>
+        </div>
+      </div>
+
+      <!-- 底部装饰 -->
+      <div class="expired-footer">
+        <div class="footer-decoration">
+          <div class="decoration-line"></div>
+          <div class="decoration-dot"></div>
+          <div class="decoration-line"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { Warning } from '@element-plus/icons-vue'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import useUserInfo from '@/stores/modules/user'
+
+const router = useRouter()
+const useUserInfoStore = useUserInfo()
+const { expiredMessage, loginOut } = useUserInfoStore
+
+// 获取过期消息
+const expiredMessageText = computed(() => {
+  return expiredMessage.value || '您的企业账户已过期,无法继续使用系统功能。'
+})
+
+// 联系管理员
+const handleContactAdmin = () => {
+  ElMessageBox({
+    title: '联系方式',
+    message: '请通过以下方式联系管理员:\n\n电话:400-xxx-xxxx\n邮箱:admin@company.com\n\n或扫描下方二维码添加客服微信',
+    showCancelButton: false,
+    confirmButtonText: '知道了',
+    type: 'info'
+  })
+}
+
+// 重新登录
+const handleLogout = async () => {
+  try {
+    await loginOut()
+    // 退出登录后会自动跳转到首页
+  } catch (error) {
+    console.error('退出登录失败:', error)
+    ElMessage.error('退出登录失败,请重试')
+  }
+}
+
+onMounted(() => {
+  // 页面加载时的埋点或其他初始化逻辑
+  console.log('企业过期页面已加载')
+})
+</script>
+
+<style lang="scss" scoped>
+.expired-page {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 20px;
+}
+
+.expired-container {
+  background: #fff;
+  border-radius: 20px;
+  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
+  padding: 60px;
+  max-width: 600px;
+  width: 100%;
+  text-align: center;
+  position: relative;
+  overflow: hidden;
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    height: 4px;
+    background: linear-gradient(90deg, #FF4D4F 0%, #FF7875 100%);
+  }
+}
+
+.expired-header {
+  margin-bottom: 40px;
+
+  .expired-icon {
+    margin-bottom: 24px;
+    display: flex;
+    justify-content: center;
+  }
+
+  .expired-title {
+    font-size: 32px;
+    font-weight: 700;
+    color: #1D2129;
+    margin: 0;
+    letter-spacing: 1px;
+  }
+}
+
+.expired-content {
+  .expired-message {
+    margin-bottom: 50px;
+
+    .message-text {
+      font-size: 18px;
+      color: #4E5969;
+      line-height: 1.6;
+      margin-bottom: 16px;
+      font-weight: 500;
+    }
+
+    .message-hint {
+      font-size: 16px;
+      color: #86909C;
+      line-height: 1.5;
+    }
+  }
+
+  .expired-actions {
+    display: flex;
+    gap: 20px;
+    justify-content: center;
+    flex-wrap: wrap;
+
+    .action-btn {
+      min-width: 140px;
+      height: 48px;
+      border-radius: 8px;
+      font-size: 16px;
+      font-weight: 600;
+
+      &.logout-btn {
+        border-color: #D9D9D9;
+        color: #666;
+        background: #FAFAFA;
+
+        &:hover {
+          border-color: #BFBFBF;
+          background: #F5F5F5;
+        }
+      }
+    }
+  }
+}
+
+.expired-footer {
+  margin-top: 50px;
+
+  .footer-decoration {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 12px;
+
+    .decoration-line {
+      width: 40px;
+      height: 2px;
+      background: #E5E6EB;
+      border-radius: 1px;
+    }
+
+    .decoration-dot {
+      width: 8px;
+      height: 8px;
+      background: #E5E6EB;
+      border-radius: 50%;
+    }
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .expired-page {
+    padding: 10px;
+  }
+
+  .expired-container {
+    padding: 40px 30px;
+    max-width: 100%;
+  }
+
+  .expired-header {
+    .expired-title {
+      font-size: 28px;
+    }
+  }
+
+  .expired-content {
+    .expired-message {
+      margin-bottom: 40px;
+
+      .message-text {
+        font-size: 16px;
+      }
+
+      .message-hint {
+        font-size: 14px;
+      }
+    }
+
+    .expired-actions {
+      flex-direction: column;
+      align-items: center;
+
+      .action-btn {
+        width: 100%;
+        max-width: 280px;
+      }
+    }
+  }
+
+  .expired-footer {
+    margin-top: 40px;
+  }
+}
+
+@media (max-width: 480px) {
+  .expired-container {
+    padding: 30px 20px;
+  }
+
+  .expired-header {
+    margin-bottom: 30px;
+
+    .expired-icon .el-icon {
+      font-size: 60px;
+    }
+
+    .expired-title {
+      font-size: 24px;
+    }
+  }
+
+  .expired-content {
+    .expired-message {
+      margin-bottom: 30px;
+    }
+
+    .expired-actions {
+      gap: 12px;
+    }
+  }
+}
+</style>

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

@@ -62,6 +62,7 @@
                     v-if="image.PhotoRecord.image_path"
                     :src="getFilePath(image.PhotoRecord.image_path)"
                     :preview-src-list="getPreviewImageList(item)"
+                    hide-on-click-modal
                     :initial-index="getPreviewIndex(item, index)"
                     class="preview-image"
                     fit="contain"

+ 71 - 2
frontend/src/views/Setting/components/action_config.vue

@@ -7,12 +7,14 @@
   </el-tabs>
 
   <div class="two_tabs">
+    <div class="two_tabs_wrap">
       <div class="item"
            :class="item.id === activeTab.id ? 'active' : ''"
            v-for="item in tabs" :key="item.id"
            @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>
   <div class="form-table">
     <div v-if="isSortMode" class="sort-tip">
@@ -23,6 +25,7 @@
       <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="copySetting" 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>
@@ -89,7 +92,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, defineProps, defineEmits , watch,onMounted, reactive,onBeforeUnmount } from 'vue'
+import { ref, defineProps, defineEmits , watch,onMounted, reactive,onBeforeUnmount, nextTick } from 'vue'
 import EditDialog from "./EditDialog.vue";
 import { ElMessage, ElMessageBox } from 'element-plus';
 import { Rank, Warning } from '@element-plus/icons-vue';
@@ -101,7 +104,7 @@ import socket from "@/stores/modules/socket";
 const socketStore = socket(); // WebSocket状态管理实例
 const tokenInfoStore = tokenInfo();
 
-import  { getTopTabs, getDeviceConfigs,setLeftRightConfig,restConfig,sortDeviceConfig,setTabName,delDviceConfig } from '@/apis/setting'
+import  { getTopTabs, getDeviceConfigs,setLeftRightConfig,restConfig,sortDeviceConfig,setTabName,delDviceConfig, copyDeviceConfig } from '@/apis/setting'
 
 // 表格数据和对话框状态
 const tableData = ref([]); // 配置表格数据
@@ -199,6 +202,11 @@ const getTopList = async ()=>{
     }
 
     getList()
+    
+    // 等待DOM更新后滚动到选中标签
+    nextTick(() => {
+      scrollToActiveTab();
+    });
   }
 
 }
@@ -209,6 +217,11 @@ const toggleTab = (item) => {
 
   activeTab.value = item
   getList()
+
+  // 滚动到选中元素的位置
+  nextTick(() => {
+    scrollToActiveTab();
+  });
 };
 
 
@@ -351,6 +364,55 @@ const reName = ()=>{
         }
       })
 }
+const copySetting = ()=>{
+  if (isSortMode.value) return; // 排序模式下禁用
+
+  ElMessageBox.prompt('', '复制配置', {
+    confirmButtonText: '保存',
+    cancelButtonText: '取消',
+    inputValue: "",
+    inputPlaceholder:'请输入配置名称',
+    inputValidator: (value) => {
+      if (value === '') {
+        return '请输入配置名称';
+      }
+      return true;
+    },
+  })
+      .then(async ({ value }) => {
+
+
+
+        const result =  await copyDeviceConfig({
+          tab_id: activeTab.value.id,
+          tab_name:value,
+        })
+        if (result.code == 0) {
+          getTopList()
+        }
+      })
+}
+
+// 滚动到选中标签的位置
+const scrollToActiveTab = () => {
+  const activeElement = document.querySelector('.two_tabs_wrap .item.active');
+  if (activeElement) {
+    const container = document.querySelector('.two_tabs_wrap');
+    if (container) {
+      const containerRect = container.getBoundingClientRect();
+      const elementRect = activeElement.getBoundingClientRect();
+      
+      // 计算需要滚动的位置
+      const scrollLeft = container.scrollLeft + elementRect.left - containerRect.left - containerRect.width / 2 + elementRect.width / 2;
+      
+      // 执行平滑滚动
+      container.scrollTo({
+        left: scrollLeft,
+        behavior: 'smooth'
+      });
+    }
+  }
+};
 
 /**
  * 新增一行配置。
@@ -585,6 +647,13 @@ const exitSortMode = () => {
   background: #fff;
   border: 1px solid #c8c8c8;
   border-top: none;
+
+  .two_tabs_wrap {
+     overflow-x: auto;
+     overflow-y: hidden;
+     display: flex;
+     min-height: 30px;
+  }
   .item {
     float: left;
     padding: 0 15px;

+ 11 - 1
frontend/src/views/Setting/index.vue

@@ -94,6 +94,12 @@
                     </el-select>
                     </div>
                 </div>
+              <div class="form-item">
+                <label>800图颜色配置:</label>
+                <div class="select-wrapper flex left">
+                  <el-color-picker v-model="formData.basic_configs.color_800image" />
+                </div>
+              </div>
 
                <DebugPanel ref="debugPanel" />
         </div>
@@ -237,7 +243,8 @@ const formData = reactive({
     "image_out_format": "",//图片输出格式
     "image_sharpening": "", //图片锐化
     "padding_800image": 100, //800图自定义边距
-    "is_flip_800image": 1 //800图是否翻转
+    "is_flip_800image": 1, //800图是否翻转
+    "color_800image": '#ffffff' //800图是否翻转
   },
   //拍照配置
   take_photo_configs:{
@@ -426,6 +433,9 @@ const getConfig =  async (typeValue)=>{
     if (formData.basic_configs.is_flip_800image === undefined || formData.basic_configs.is_flip_800image === null) {
       formData.basic_configs.is_flip_800image = 0;
     }
+    if (formData.basic_configs.color_800image === undefined || formData.basic_configs.color_800image === null) {
+      formData.basic_configs.color_800image = '#ffffff';
+    }
   }
 
 }