Pārlūkot izejas kodu

refactor(remote-control): 重构远程控制组件的上下文菜单实现

- 使用 Teleport 将菜单渲染到 body 元素以解决层级问题
- 添加菜单位置智能定位功能,避免菜单超出视窗边界
- 为菜单项添加滚动条和最大高度限制以改善用户体验
- 调整菜单样式包括宽度、高度和层级数值
- 修复拍照配置菜单在多相机模式下的显示问题
- 在摄影页面调整远程控制区域的高度以适应新布局
panqiuyao 1 dienu atpakaļ
vecāks
revīzija
116b200750

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

@@ -650,7 +650,8 @@ watch(goodsList, () => {
       margin:  auto;
       justify-content: flex-start;
       flex-direction: column;
-      overflow: hidden;
+      overflow-x: hidden;
+      overflow-y: auto;
       border-right:1px solid rgba(0,0,0,.1);
 
       .step-section {
@@ -802,7 +803,7 @@ watch(goodsList, () => {
 
           .remote-control-wrap {
             width: 353px;
-            height: 300px;
+            height: 460px;
           }
 
           .shooting-tips {

+ 130 - 49
frontend/src/views/RemoteControl/index.vue

@@ -40,58 +40,68 @@
     </el-row>
 
     <!-- 左脚配置菜单 -->
-    <div v-if="showLeftMenu" class="context-menu" :style="{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }" @click.stop>
-      <div class="menu-title">左脚配置</div>
-      <div class="menu-items">
-        <div
-            v-for="tab in leftTabs"
-            :key="tab.id"
-            class="menu-item"
-            :class="{ active: leftConfigId === tab.id }"
-            @click="selectConfig('left', tab.id)"
-        >
-          {{ leftConfigId === tab.id ? '✓ ' : '' }}{{ tab.mode_name }}
-      </div>
+    <Teleport to="body">
+      <div v-if="showLeftMenu" class="context-menu" :style="menuStyle" @click.stop>
+        <div class="menu-title">左脚配置</div>
+        <div class="menu-items" :style="{ maxHeight: menuMaxHeight + 'px' }">
+          <div
+              v-for="tab in leftTabs"
+              :key="tab.id"
+              class="menu-item"
+              :class="{ active: leftConfigId === tab.id }"
+              :title="tab.mode_name"
+              @click="selectConfig('left', tab.id)"
+          >
+            {{ leftConfigId === tab.id ? '✓ ' : '' }}{{ tab.mode_name }}
+        </div>
+        </div>
       </div>
-    </div>
+    </Teleport>
 
     <!-- 右脚配置菜单 -->
-    <div v-if="showRightMenu" class="context-menu" :style="{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }" @click.stop>
-      <div class="menu-title">右脚配置</div>
-      <div class="menu-items">
-        <div
-          v-for="tab in rightTabs"
-          :key="tab.id"
-          class="menu-item"
-          :class="{ active: rightConfigId === tab.id }"
-          @click="selectConfig('right', tab.id)"
-        >
-          {{ rightConfigId === tab.id ? '✓ ' : '' }}{{ tab.mode_name }}
+    <Teleport to="body">
+      <div v-if="showRightMenu" class="context-menu" :style="menuStyle" @click.stop>
+        <div class="menu-title">右脚配置</div>
+        <div class="menu-items" :style="{ maxHeight: menuMaxHeight + 'px' }">
+          <div
+            v-for="tab in rightTabs"
+            :key="tab.id"
+            class="menu-item"
+            :class="{ active: rightConfigId === tab.id }"
+            :title="tab.mode_name"
+            @click="selectConfig('right', tab.id)"
+          >
+            {{ rightConfigId === tab.id ? '✓ ' : '' }}{{ tab.mode_name }}
+          </div>
         </div>
       </div>
-    </div>
+    </Teleport>
 
     <!-- ========== 拍照配置菜单(仅多相机模式显示)========== -->
-    <div v-if="isMultiCameraMode && showPhotoMenu" class="context-menu photo-menu" :style="{ left: menuPosition.x + 'px', top: menuPosition.y + 'px' }" @click.stop>
-      <div class="menu-title">拍照配置</div>
-      <div class="menu-items">
-        <div
-          v-for="point in pointList"
-          :key="point.key"
-          class="menu-item photo-point-item"
-          :class="{ active: selectedPoint === point.key }"
-          @click="selectPhotoPoint(point.key)"
-        >
-          <span class="point-name">{{ selectedPoint === point.key ? '✓ ' : '' }}点位 {{ point.key }}</span>
-          <el-tag :type="point.connected ? 'success' : 'danger'" size="small">
-            {{ point.connected ? '已连接' : '未连接' }}
-          </el-tag>
+    <Teleport to="body">
+      <div v-if="isMultiCameraMode && showPhotoMenu" class="context-menu photo-menu" :style="menuStyle" @click.stop>
+        <div class="menu-title">拍照配置</div>
+        <div class="menu-items" :style="{ maxHeight: menuMaxHeight + 'px' }">
+          <div
+            v-for="point in pointList"
+            :key="point.key"
+            class="menu-item photo-point-item"
+            :class="{ active: selectedPoint === point.key }"
+            @click="selectPhotoPoint(point.key)"
+          >
+            <span class="point-name">{{ selectedPoint === point.key ? '✓ ' : '' }}点位 {{ point.key }}</span>
+            <el-tag :type="point.connected ? 'success' : 'danger'" size="small">
+              {{ point.connected ? '已连接' : '未连接' }}
+            </el-tag>
+          </div>
         </div>
       </div>
-    </div>
+    </Teleport>
 
     <!-- 点击其他地方关闭菜单 -->
-    <div v-if="showLeftMenu || showRightMenu || (isMultiCameraMode && showPhotoMenu)" class="menu-overlay" @click="closeMenus"></div>
+    <Teleport to="body">
+      <div v-if="showLeftMenu || showRightMenu || (isMultiCameraMode && showPhotoMenu)" class="menu-overlay" @click="closeMenus"></div>
+    </Teleport>
   </div>
 
 </template>
@@ -204,6 +214,59 @@ const showLeftMenu = ref(false) // 显示左脚菜单
 const showRightMenu = ref(false) // 显示右脚菜单
 const showPhotoMenu = ref(false) // 显示拍照配置菜单
 const menuPosition = ref({ x: 0, y: 0 }) // 菜单位置
+const menuMaxHeight = ref(240)
+
+const menuStyle = computed(() => ({
+  left: menuPosition.value.x + 'px',
+  top: menuPosition.value.y + 'px',
+}))
+
+const MENU_MARGIN = 8
+const MENU_TITLE_HEIGHT = 37
+const MENU_WIDTH = 220
+const MENU_LIST_MAX = 240
+const MENU_LIST_MIN = 80
+
+function placeMenu(event: MouseEvent) {
+  const vw = window.innerWidth
+  const vh = window.innerHeight
+  const cx = event.clientX
+  const cy = event.clientY
+
+  let x = cx
+  if (x + MENU_WIDTH > vw - MENU_MARGIN) {
+    x = Math.max(MENU_MARGIN, vw - MENU_WIDTH - MENU_MARGIN)
+  }
+
+  const spaceBelow = vh - cy - MENU_MARGIN
+  const spaceAbove = cy - MENU_MARGIN
+  const preferred = MENU_TITLE_HEIGHT + MENU_LIST_MAX
+
+  let y = cy
+  let listMax = MENU_LIST_MAX
+
+  if (spaceBelow >= preferred) {
+    y = cy
+    listMax = MENU_LIST_MAX
+  } else if (spaceBelow >= MENU_TITLE_HEIGHT + MENU_LIST_MIN) {
+    y = cy
+    listMax = spaceBelow - MENU_TITLE_HEIGHT
+  } else if (spaceAbove >= MENU_TITLE_HEIGHT + MENU_LIST_MIN) {
+    listMax = Math.min(MENU_LIST_MAX, spaceAbove - MENU_TITLE_HEIGHT)
+    y = cy - MENU_TITLE_HEIGHT - listMax
+  } else if (spaceBelow >= spaceAbove) {
+    listMax = Math.max(48, spaceBelow - MENU_TITLE_HEIGHT)
+    y = cy
+  } else {
+    listMax = Math.max(48, spaceAbove - MENU_TITLE_HEIGHT)
+    y = Math.max(MENU_MARGIN, cy - MENU_TITLE_HEIGHT - listMax)
+  }
+
+  if (y < MENU_MARGIN) y = MENU_MARGIN
+
+  menuPosition.value = { x, y }
+  menuMaxHeight.value = listMax
+}
 
 const runLeft = async () => {
   emit('onRemoteControl','left')
@@ -248,7 +311,8 @@ const handleLeftRightClick = (event, type) => {
      return
   }
   event.preventDefault()
-  menuPosition.value = { x: event.clientX, y: event.clientY }
+  placeMenu(event)
+  showPhotoMenu.value = false
 
   if (type === 'left') {
     showLeftMenu.value = true
@@ -330,7 +394,7 @@ const handlePhotoConfigClick = (event: MouseEvent) => {
     return
   }
   event.preventDefault()
-  menuPosition.value = { x: event.clientX, y: event.clientY }
+  placeMenu(event)
 
   // 关闭其他菜单
   showLeftMenu.value = false
@@ -413,9 +477,11 @@ const selectPhotoPoint = (pointKey: string) => {
   border: 1px solid #e4e7ed;
   border-radius: 4px;
   box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-  z-index: 2000;
-  min-width: 120px;
-  max-width: 200px;
+  z-index: 3000;
+  min-width: 160px;
+  max-width: 260px;
+  display: flex;
+  flex-direction: column;
 }
 
 .menu-title {
@@ -425,11 +491,24 @@ const selectPhotoPoint = (pointKey: string) => {
   color: #606266;
   border-bottom: 1px solid #ebeef5;
   background-color: #f5f7fa;
+  flex-shrink: 0;
 }
 .menu-items {
+  max-height: 240px;
+  overflow-x: hidden;
+  overflow-y: auto;
+  min-height: 0;
 
-  max-height: 150px;
-  overflow: auto;
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+  &::-webkit-scrollbar-thumb {
+    background: #c0c4cc;
+    border-radius: 3px;
+  }
+  &::-webkit-scrollbar-track {
+    background: #f5f7fa;
+  }
 }
 .menu-item {
   padding: 8px 12px;
@@ -437,6 +516,8 @@ const selectPhotoPoint = (pointKey: string) => {
   color: #606266;
   cursor: pointer;
   transition: background-color 0.2s;
+  word-break: break-all;
+  line-height: 20px;
 
   &:hover {
     background-color: #f5f7fa;
@@ -454,7 +535,7 @@ const selectPhotoPoint = (pointKey: string) => {
   left: 0;
   right: 0;
   bottom: 0;
-  z-index: 1999;
+  z-index: 2999;
 }
 
 /* 新增:拍照配置样式 */