Quellcode durchsuchen

refactor(components): 抽取常用操作悬浮按钮为独立组件

- 新建 QuickActions 组件,包含一键释放、一键回落、设备关机功能
- 从 shot.vue 中移除内联悬浮按钮代码,替换为 QuickActions 组件
- 在 check.vue 中引入 QuickActions,mcuConnected 时展示
- 支持小屏(≤1366px)折叠交互与大屏直接展示模式
- 为 hardware-check 组件新增 zIndex prop 支持
- check.vue 挂载时自动请求硬件检测信息
panqiuyao vor 1 Tag
Ursprung
Commit
af4e52674c

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

@@ -7,6 +7,7 @@
     :show-close="false"
     width="680px"
     class="hardware-check-dialog"
+    :z-index="zIndex"
   >
     <div class="hardware-check-container">
       <el-timeline>
@@ -117,6 +118,10 @@ const props = defineProps({
   title: {
     type: String,
     default: '检测硬件'
+  },
+  zIndex: {
+    type: Number,
+    default: undefined
   }
 });
 

+ 244 - 0
frontend/src/components/quick-actions/index.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="quick-actions" :class="{ 'is-open': open, 'is-compact': isSmallScreen }">
+    <div v-if="open" class="quick-actions__mask" @click="open = false"></div>
+    <div class="quick-actions__list">
+      <div
+        class="quick-actions__item"
+        @click="handleOneClickRelease"
+        v-log="{ describe: { action: '点击一键释放' } }"
+      >
+        <span>一键释放</span>
+      </div>
+      <div
+        class="quick-actions__item"
+        @click="handleOneKeyDown"
+        v-log="{ describe: { action: '点击一键回落' } }"
+      >
+        <span>一键回落</span>
+      </div>
+      <div
+        class="quick-actions__item is-danger"
+        @click="handlePowerOff"
+        v-log="{ describe: { action: '点击设备关机' } }"
+      >
+        <span>设备关机</span>
+      </div>
+    </div>
+    <div
+      class="quick-actions__toggle"
+      @click="open = !open"
+      v-log="{ describe: { action: '点击常用操作' } }"
+    >
+      <span>{{ open ? '收起' : '常用' }}</span>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { onBeforeUnmount, onMounted, ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import socket from '@/stores/modules/socket'
+
+const SMALL_SCREEN_MAX = 1366
+
+const socketStore = socket()
+const open = ref(false)
+const isSmallScreen = ref(window.screen.width <= SMALL_SCREEN_MAX)
+
+function syncScreen() {
+  isSmallScreen.value = window.screen.width <= SMALL_SCREEN_MAX
+}
+
+onMounted(() => {
+  syncScreen()
+  window.addEventListener('resize', syncScreen)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', syncScreen)
+})
+
+function closeMenu() {
+  open.value = false
+}
+
+async function handleOneClickRelease() {
+  closeMenu()
+  try {
+    await ElMessageBox.confirm(
+      '确定要一键释放设备吗?',
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      },
+    )
+    socketStore.sendMessage({
+      type: 'oneclick_release',
+      data: 'oneclick_release',
+    })
+    ElMessage.success('已发送一键释放指令')
+  } catch {
+    // 用户取消
+  }
+}
+
+async function handleOneKeyDown() {
+  closeMenu()
+  try {
+    await ElMessageBox.confirm(
+      '确定要一键回落设备吗?',
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      },
+    )
+    socketStore.sendMessage({
+      type: 'one_key_down',
+      data: 'one_key_down',
+    })
+    ElMessage.success('已发送一键回落指令')
+  } catch {
+    // 用户取消
+  }
+}
+
+async function handlePowerOff() {
+  closeMenu()
+  try {
+    await ElMessageBox.confirm(
+      '确定要关闭拍照机吗?关机后需要手动重新开机。',
+      '设备关机',
+      {
+        confirmButtonText: '确定关机',
+        cancelButtonText: '取消',
+        type: 'warning',
+      },
+    )
+    socketStore.sendMessage({
+      type: 'mcu_power_off',
+      data: 'mcu_power_off',
+    })
+    ElMessage.success('已发送关机指令')
+  } catch {
+    // 用户取消
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.quick-actions {
+  position: fixed;
+  left: 24px;
+  bottom: 24px;
+  z-index: 2000;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  pointer-events: none;
+  -webkit-app-region: no-drag;
+  app-region: no-drag;
+
+  .quick-actions__mask {
+    display: none;
+    position: fixed;
+    inset: 0;
+    z-index: 1999;
+    background: transparent;
+    pointer-events: auto;
+  }
+
+  .quick-actions__list {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 10px;
+    position: relative;
+    z-index: 2001;
+    opacity: 1;
+    pointer-events: auto;
+    transform: none;
+  }
+
+  .quick-actions__item {
+    min-width: 112px;
+    height: 40px;
+    padding: 0 16px;
+    background: #fff;
+    border-radius: 20px;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    user-select: none;
+
+    span {
+      font-size: 14px;
+      color: #333;
+      font-weight: 500;
+    }
+
+    &:hover {
+      background: #f5f7fa;
+    }
+
+    &.is-danger span {
+      color: #ff4c00;
+    }
+  }
+
+  .quick-actions__toggle {
+    display: none;
+    width: 56px;
+    height: 56px;
+    margin-top: 12px;
+    border-radius: 50%;
+    background: linear-gradient(135deg, #2fb0ff 0%, #b863fb 100%);
+    color: #fff;
+    font-size: 14px;
+    font-weight: 600;
+    align-items: center;
+    justify-content: center;
+    cursor: pointer;
+    box-shadow: 0 6px 16px rgba(47, 176, 255, 0.4);
+    user-select: none;
+    pointer-events: auto;
+    position: relative;
+    z-index: 2001;
+
+    &:hover {
+      opacity: 0.92;
+    }
+  }
+}
+
+/* 屏幕宽度 1366 及以下视为小屏,保留「常用 / 收起」 */
+.quick-actions.is-compact {
+  .quick-actions__list {
+    opacity: 0;
+    pointer-events: none;
+    transform: translateY(12px);
+    transition: all 0.2s ease;
+  }
+
+  &.is-open {
+    .quick-actions__mask {
+      display: block;
+    }
+
+    .quick-actions__list {
+      opacity: 1;
+      pointer-events: auto;
+      transform: translateY(0);
+    }
+  }
+
+  .quick-actions__toggle {
+    display: flex;
+  }
+}
+</style>

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

@@ -84,8 +84,10 @@
   </div>
   <hardware-check v-if="!isSetting"
    isInitCheck
+   :z-index="2000"
    @confirm="checkConfirm(true)"
   />
+  <QuickActions v-if="!isSetting && mcuConnected" />
 </template>
 <script setup lang="ts">
 import {watchEffect, ref, defineEmits, defineProps, computed, onBeforeUnmount, onMounted} from 'vue'
@@ -95,6 +97,8 @@ import icpList from '@/utils/ipc'
 import useUserInfo from "@/stores/modules/user";
 import headerBar from '@/components/header-bar/index.vue'
 import  editRow  from './components/editRow'
+import QuickActions from '@/components/quick-actions/index.vue'
+import checkInfo from '@/stores/modules/check'
 const clientStore = client();
 const socketStore = socket(); // WebSocket状态管理实例
 
@@ -104,6 +108,8 @@ const emit = defineEmits([ 'confirm','onClose']);
 import  configInfo  from '@/stores/modules/config';
 const configInfoStore = configInfo();
 const useUserInfoStore = useUserInfo();
+const checkInfoStore = checkInfo();
+const mcuConnected = computed(() => checkInfoStore.mcuConnected)
 
 const confirm = async () => {
   console.log('confirm');
@@ -369,7 +375,7 @@ function handleAutoFocus() {
     }
   });
 }
-const loading = ref(true)
+const loading = ref(false)
 const editData = ref(null);
 const imageTplPath = ref(null)
 function backToPreview() {
@@ -508,6 +514,9 @@ clientStore.ipc.on(icpList.socket.message+'_run_mcu_single', async (event, resul
 
 const exampleImage = ref('https://huilimaimg.cnhqt.com/frontend/zhihuiyin/demo.jpg?x-oss-process=image/resize,w_400')
 onMounted(async ()=>{
+  if (!isSetting.value) {
+    checkInfoStore.requestHardwareCheckInfo()
+  }
   await  configInfoStore.getAppConfig()
   if(configInfoStore.appConfig.controlType === "SmartShooter" || configInfoStore.appConfig.controlType === "SmartShooter6"){
     preview.value = configInfoStore.appConfig.userDataPath  + "\\preview\\liveview.png"

+ 2 - 191
frontend/src/views/Photography/shot.vue

@@ -299,40 +299,7 @@
     </el-button>
   </div>
 
-  <!-- 常用操作悬浮按钮 -->
-  <div class="shot-fab" v-if="!isDemoMode" :class="{ open: fabOpen }">
-    <div v-if="fabOpen" class="shot-fab-mask" @click="fabOpen = false"></div>
-    <div class="shot-fab-actions">
-      <div
-        class="shot-fab-action"
-        @click="handleOneClickRelease"
-        v-log="{ describe: { action: '点击一键释放' } }"
-      >
-        <span class="shot-fab-action-text">一键释放</span>
-      </div>
-      <div
-        class="shot-fab-action"
-        @click="handleOneKeyDown"
-        v-log="{ describe: { action: '点击一键回落' } }"
-      >
-        <span class="shot-fab-action-text">一键回落</span>
-      </div>
-      <div
-        class="shot-fab-action danger"
-        @click="handlePowerOff"
-        v-log="{ describe: { action: '点击设备关机' } }"
-      >
-        <span class="shot-fab-action-text">设备关机</span>
-      </div>
-    </div>
-    <div
-      class="shot-fab-btn"
-      @click="fabOpen = !fabOpen"
-      v-log="{ describe: { action: '点击常用操作' } }"
-    >
-      <span>{{ fabOpen ? '收起' : '常用' }}</span>
-    </div>
-  </div>
+  <QuickActions v-if="!isDemoMode" />
 </template>
 <script setup lang="ts">
 import headerBar from '@/components/header-bar/index.vue'
@@ -345,10 +312,8 @@ import { useThumbnails } from './composables/useThumbnails'
 import ImagePreview, { PreviewItem } from '@/components/PreviewImage/index.vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import useUserInfo from '@/stores/modules/user'
-import socket from '@/stores/modules/socket'
 import { formatScaleWeight, useScale } from '@/composables/useScale'
-
-const socketStore = socket()
+import QuickActions from '@/components/quick-actions/index.vue'
 const {
   everConnected: scaleEverConnected,
   connected: scaleConnected,
@@ -435,74 +400,6 @@ const headerMenu = computed(() => {
   return items
 })
 
-const fabOpen = ref(false)
-
-async function handleOneClickRelease() {
-  fabOpen.value = false
-  try {
-    await ElMessageBox.confirm(
-      '确定要一键释放设备吗?',
-      '提示',
-      {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning',
-      },
-    )
-    socketStore.sendMessage({
-      type: 'oneclick_release',
-      data: 'oneclick_release',
-    })
-    ElMessage.success('已发送一键释放指令')
-  } catch {
-    // 用户取消
-  }
-}
-
-async function handleOneKeyDown() {
-  fabOpen.value = false
-  try {
-    await ElMessageBox.confirm(
-      '确定要一键回落设备吗?',
-      '提示',
-      {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning',
-      },
-    )
-    socketStore.sendMessage({
-      type: 'one_key_down',
-      data: 'one_key_down',
-    })
-    ElMessage.success('已发送一键回落指令')
-  } catch {
-    // 用户取消
-  }
-}
-
-async function handlePowerOff() {
-  fabOpen.value = false
-  try {
-    await ElMessageBox.confirm(
-      '确定要关闭拍照机吗?关机后需要手动重新开机。',
-      '设备关机',
-      {
-        confirmButtonText: '确定关机',
-        cancelButtonText: '取消',
-        type: 'warning',
-      },
-    )
-    socketStore.sendMessage({
-      type: 'mcu_power_off',
-      data: 'mcu_power_off',
-    })
-    ElMessage.success('已发送关机指令')
-  } catch {
-    // 用户取消
-  }
-}
-
 // 搜索货号
 const handleSearch = () => {
   getPhotoRecords({ page: 1, goods_art_no: searchGoodsArtNo.value })
@@ -1357,90 +1254,4 @@ watch(goodsList, () => {
   }
 }
 
-.shot-fab {
-  position: fixed;
-  left: 24px;
-  bottom: 24px;
-  z-index: 2000;
-  display: flex;
-  flex-direction: column;
-  align-items: flex-start;
-
-  .shot-fab-mask {
-    position: fixed;
-    inset: 0;
-    z-index: 1999;
-    background: transparent;
-  }
-
-  .shot-fab-actions {
-    display: flex;
-    flex-direction: column;
-    align-items: flex-start;
-    gap: 10px;
-    margin-bottom: 12px;
-    opacity: 0;
-    pointer-events: none;
-    transform: translateY(12px);
-    transition: all 0.2s ease;
-    position: relative;
-    z-index: 2001;
-  }
-
-  &.open .shot-fab-actions {
-    opacity: 1;
-    pointer-events: auto;
-    transform: translateY(0);
-  }
-
-  .shot-fab-action {
-    min-width: 112px;
-    height: 40px;
-    padding: 0 16px;
-    background: #fff;
-    border-radius: 20px;
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.16);
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    cursor: pointer;
-    user-select: none;
-
-    .shot-fab-action-text {
-      font-size: 14px;
-      color: #333;
-      font-weight: 500;
-    }
-
-    &:hover {
-      background: #f5f7fa;
-    }
-
-    &.danger .shot-fab-action-text {
-      color: #ff4c00;
-    }
-  }
-
-  .shot-fab-btn {
-    width: 56px;
-    height: 56px;
-    border-radius: 50%;
-    background: linear-gradient(135deg, #2fb0ff 0%, #b863fb 100%);
-    color: #fff;
-    font-size: 14px;
-    font-weight: 600;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    cursor: pointer;
-    box-shadow: 0 6px 16px rgba(47, 176, 255, 0.4);
-    user-select: none;
-    position: relative;
-    z-index: 2001;
-
-    &:hover {
-      opacity: 0.92;
-    }
-  }
-}
 </style>