You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue2+Vuetify:如何让v-menu在选中项处居中?

解决方案

方案一:不依赖auto属性,实现选中项居中

如果不需要auto的自动避溢功能,直接关闭auto,通过滚动控制让选中项居中:

  1. 给v-list-item-group绑定选中状态,监听选中变化事件
  2. 菜单打开后,找到选中项的DOM元素,调用scrollIntoView让它滚动到列表容器中央

示例代码:

<template>
  <v-menu
    v-model="menuOpen"
    :close-on-content-click="false"
    offset-y  <!-- 强制菜单固定在触发元素下方 -->
  >
    <template v-slot:activator="{ props }">
      <v-text-field v-bind="props" label="选择项" :value="selectedItem" />
    </template>
    <v-list
      style="max-height: 300px; overflow-y: auto;"  <!-- 固定列表高度,显示滚动条 -->
    >
      <v-list-item-group
        v-model="selectedItem"
        @update:model-value="scrollToCenter"
      >
        <v-list-item
          v-for="item in items"
          :key="item.id"
          :value="item.id"
        >
          <v-list-item-title>{{ item.name }}</v-list-item-title>
        </v-list-item>
      </v-list-item-group>
    </v-list>
  </v-menu>
</template>

<script setup>
import { ref, nextTick } from 'vue'

const menuOpen = ref(false)
const selectedItem = ref(1)
const items = ref(Array.from({length: 50}, (_, i) => ({id: i+1, name: `选项 ${i+1}`})))

const scrollToCenter = async () => {
  await nextTick()  // 等待DOM更新完成
  const activeItem = document.querySelector('.v-list-item-group--active')
  if (activeItem) {
    activeItem.scrollIntoView({ block: 'center', behavior: 'smooth' })
  }
}

// 菜单打开时触发滚动,确保初始选中项也居中
const handleMenuOpen = async () => {
  await nextTick()
  scrollToCenter()
}
</script>

关键说明:

  • 用offset-y替代auto,强制菜单贴在触发元素下方
  • 列表容器设置固定高度和滚动属性,通过scrollIntoView({block: 'center'})实现选中项居中
  • 监听菜单打开事件,保证初始状态选中项也能居中显示

方案二:保留auto属性,修复定位问题

如果必须依赖auto的避溢功能,可通过以下方式强制菜单优先显示在下方,并调整选中项居中:

  1. 启用auto,同时设置offset-y和min-height,约束菜单的最小高度,减少Vuetify自动向上调整的概率
  2. 监听菜单的opened事件,手动计算触发元素位置,覆盖auto的自动定位
  3. 同样用scrollIntoView实现选中项居中

示例代码:

<template>
  <v-menu
    v-model="menuOpen"
    auto
    offset-y
    :min-height="300"
    @opened="adjustMenuPosition"
  >
    <template v-slot:activator="{ props, attrs }">
      <v-text-field v-bind="props" v-bind="attrs" label="选择项" :value="selectedItem" ref="trigger" />
    </template>
    <v-list
      style="max-height: 300px; overflow-y: auto;"
      ref="listContainer"
    >
      <v-list-item-group
        v-model="selectedItem"
        @update:model-value="scrollToCenter"
      >
        <v-list-item
          v-for="item in items"
          :key="item.id"
          :value="item.id"
        >
          <v-list-item-title>{{ item.name }}</v-list-item-title>
        </v-list-item>
      </v-list-item-group>
    </v-list>
  </v-menu>
</template>

<script setup>
import { ref, nextTick } from 'vue'

const menuOpen = ref(false)
const selectedItem = ref(1)
const trigger = ref(null)
const items = ref(Array.from({length: 50}, (_, i) => ({id: i+1, name: `选项 ${i+1}`})))

const scrollToCenter = async () => {
  await nextTick()
  const activeItem = document.querySelector('.v-list-item-group--active')
  if (activeItem) {
    activeItem.scrollIntoView({ block: 'center', behavior: 'smooth' })
  }
}

const adjustMenuPosition = async () => {
  await nextTick()
  const triggerRect = trigger.value.getBoundingClientRect()
  const menuEl = document.querySelector('.v-menu__content')
  if (menuEl && triggerRect) {
    // 强制菜单定位在触发元素下方,覆盖auto的自动调整
    menuEl.style.top = `${triggerRect.bottom + window.scrollY}px`
    menuEl.style.left = `${triggerRect.left + window.scrollX}px`
    // 移除auto添加的transform属性,避免定位冲突
    menuEl.style.transform = 'none'
  }
  scrollToCenter()
}
</script>

关键说明:

  • offset-y和min-height能引导Vuetify优先将菜单放在触发元素下方
  • 通过getBoundingClientRect()获取触发元素的视窗位置,手动设置菜单的top和left,同时加上滚动偏移量确保定位准确
  • 移除菜单的transform属性,避免和手动定位逻辑冲突

额外注意事项

  • auto属性会直接覆盖nudge-right/nudge-bottom的偏移设置,两者不要同时使用
  • 菜单挂载在body上时,计算位置必须加上window.scrollX/window.scrollY,避免滚动页面后定位偏移
  • 可添加persistent属性临时关闭菜单自动关闭,方便调试定位问题

内容的提问来源于stack exchange,提问作者Lissi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 07:43:13