Vant 4 DropdownMenu 下拉菜单组件完整指南:从基础用法到源码原理
【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant
下拉菜单(DropdownMenu)是移动端应用中极为常用的筛选与排序交互组件。本文以 Vant 4 的DropdownMenu与DropdownItem组件为核心,结合其完整 API、实战示例与仓库源码实现,带你掌握菜单栏的引入方式、全部配置项、自定义内容插槽、实例方法调用、TypeScript 类型使用以及主题定制技巧,并深入理解"下拉面板如何定位、如何互斥展开、如何修复 transform 祖先元素导致的定位错乱"等底层原理,读完即可在真实业务中熟练落地该组件。
组件概览与引入
功能定位
DropdownMenu是一组"向下弹出的菜单列表",通常由顶部的**菜单栏(bar)和点击后展开的下拉面板(popup 内容区)**两部分组成。一个DropdownMenu内部可以挂载多个DropdownItem,每个DropdownItem负责一个筛选条件(如"全部商品/新款商品/活动商品")或排序规则(如"默认排序/好评排序/销量排序")。
从组件结构上看,二者是严格的父子关系:DropdownMenu负责管理多个菜单项的展示互斥、定位与关闭逻辑,DropdownItem负责渲染单个菜单项的标题、选项列表或自定义插槽内容。
安装与注册
DropdownMenu和DropdownItem是两个独立组件,需要分别注册。推荐通过app.use进行全局注册:
import { createApp } from 'vue'; import { DropdownMenu, DropdownItem } from 'vant'; const app = createApp(); app.use(DropdownMenu); app.use(DropdownItem);注册完成后即可在模板中使用van-dropdown-menu与van-dropdown-item标签。仓库中两个组件均通过withInstall包装导出(见 dropdown-menu/index.ts 与 dropdown-item/index.ts),并在declare module 'vue'中声明了VanDropdownMenu/VanDropdownItem全局组件类型,因此使用全局注册时,编辑器与 TS 都能获得完整的类型提示。更多注册方式(如按需引入)可参考项目文档中的组件注册章节。
基础用法:快速实现筛选排序菜单
完整示例代码
在DropdownMenu内放置多个绑定v-model并提供options选项数组的DropdownItem即可:
<van-dropdown-menu> <van-dropdown-item v-model="value1" :options="option1" /> <van-dropdown-item v-model="value2" :options="option2" /> </van-dropdown-menu>import { ref } from 'vue'; export default { setup() { const value1 = ref(0); const value2 = ref('a'); const option1 = [ { text: '全部商品', value: 0 }, { text: '新款商品', value: 1 }, { text: '活动商品', value: 2 }, ]; const option2 = [ { text: '默认排序', value: 'a' }, { text: '好评排序', value: 'b' }, { text: '销量排序', value: 'c' }, ]; return { value1, value2, option1, option2, }; }, };要点说明:
v-model绑定的是当前选中项的value,类型为number | string;当用户点击某个选项时,组件内部会通过update:modelValue事件同步回绑定的值,并触发change事件。- 每个
DropdownItem未显式指定title时,菜单栏上显示的标题默认取当前选中项对应的text(由源码中renderTitle的逻辑实现:优先插槽 →title属性 → 匹配选中项文本,见 DropdownItem.tsx)。 - 多个
DropdownItem是互斥展开的:点击其中一个菜单项展开时,其他已展开的菜单会被立即关闭(见下文源码剖析)。
Option 数据结构
options数组中的每个选项支持以下键:
| 键名 | 说明 | 类型 |
|---|---|---|
| text | 文字 | string |
| value | 标识符 | number | string | boolean |
| disabled | 是否禁用选项 | boolean |
| icon | 左侧图标名称或图片链接,等同于 Icon 组件的 name 属性 | string |
对应的 TS 类型定义在 dropdown-item/types.ts:
export type DropdownItemOption = { disabled?: boolean; text: string; icon?: string; value: DropdownItemOptionValue; // Numeric | boolean };从源码 DropdownItem.tsx 可以看到,渲染选项时组件内部复用了Cell与Icon组件:选中项会渲染一个success图标并套用选中色,被禁用的选项不可点击、点击事件直接忽略。
进阶用法
自定义菜单内容(插槽 + 手动控制显示)
通过DropdownItem的默认插槽可以完全自定义下拉面板内容。注意:使用自定义内容时,菜单不会因点击选项自动关闭,需要手动调用DropdownMenu实例上的close方法,或指定DropdownItem的toggle方法控制显示状态。
<van-dropdown-menu ref="menuRef"> <van-dropdown-item v-model="value" :options="options" /> <van-dropdown-item title="筛选" ref="itemRef"> <van-cell center title="包邮"> <template #right-icon> <van-switch v-model="switch1" /> </template> </van-cell> <van-cell center title="团购"> <template #right-icon> <van-switch v-model="switch2" /> </template> </van-cell> <div style="padding: 5px 16px;"> <van-button type="primary" block round @click="onConfirm"> 确认 </van-button> </div> </van-dropdown-item> </van-dropdown-menu>import { ref } from 'vue'; export default { setup() { const menuRef = ref(null); const itemRef = ref(null); const value = ref(0); const switch1 = ref(false); const switch2 = ref(false); const options = [ { text: '全部商品', value: 0 }, { text: '新款商品', value: 1 }, { text: '活动商品', value: 2 }, ]; const onConfirm = () => { itemRef.value.toggle(); // 或者 // menuRef.value.close(); }; return { menuRef, itemRef, value, switch1, switch2, options, onConfirm, }; }, };该用法非常适合"筛选面板"类需求:下拉面板中嵌入Switch、Slider、Calendar等复杂筛选控件,点击"确认"按钮后再统一收起面板。真实示例可参考仓库中的 dropdown-menu/demo/index.vue。
自定义选中态颜色
通过active-color属性可以同时自定义菜单标题与选项文字的选中态颜色(默认值为品牌蓝#1989fa):
<van-dropdown-menu active-color="#ee0a24"> <van-dropdown-item v-model="value1" :options="option1" /> <van-dropdown-item v-model="value2" :options="option2" /> </van-dropdown-menu>该颜色定义在DropdownMenu上,并通过provide/inject机制透传给所有子DropdownItem(源码见 DropdownMenu.tsx 的linkChildren调用),选项渲染时直接读取parent.props.activeColor作为文字与勾选图标的颜色。
横向滚动
当菜单项数量较多、总宽度超过菜单栏宽度时,可以设置swipe-threshold阈值开启横向滚动:
<van-dropdown-menu swipe-threshold="4"> <van-dropdown-item v-model="value1" :options="option1" /> <van-dropdown-item v-model="value2" :options="option2" /> <van-dropdown-item v-model="value2" :options="option2" /> <van-dropdown-item v-model="value2" :options="option2" /> <van-dropdown-item v-model="value2" :options="option2" /> </van-dropdown-menu>源码中的判定逻辑为:选项数量超过阈值且总宽度超过菜单栏宽度时,才允许横向滚动(见 DropdownMenu.tsx 的scrollable计算属性)。样式层面,滚动态下菜单栏开启overflow-x: auto,同时通过::-webkit-scrollbar { display: none }隐藏滚动条以保持界面整洁(见 dropdown-menu/index.less)。
向上展开
将direction属性设置为up,下拉面板即可从菜单栏向上展开:
<van-dropdown-menu direction="up"> <van-dropdown-item v-model="value1" :options="option1" /> <van-dropdown-item v-model="value2" :options="option2" /> </van-dropdown-menu>该属性支持down(默认)与up两个值,其类型DropdownMenuDirection定义在 dropdown-menu/types.ts。direction不仅决定面板展开方向,还影响标题右侧的三角指示器旋转方向(见 dropdown-menu/index.less 中--down修饰符的rotate(135deg)),以及弹层定位的计算方式(见下文"弹层定位计算")。
禁用菜单
给DropdownItem添加disabled属性即可禁用整个菜单项(标题置灰、不可点击、不响应展开):
<van-dropdown-menu> <van-dropdown-item v-model="value1" disabled :options="option1" /> <van-dropdown-item v-model="value2" disabled :options="option2" /> </van-dropdown-menu>API 参考
DropdownMenu Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| active-color | 菜单标题和选项的选中态颜色 | string | #1989fa |
| direction | 菜单展开方向,可选值为up | string | down |
| z-index | 菜单栏 z-index 层级 | number | string | 10 |
| duration | 动画时长,单位秒,设置为0可以禁用动画 | number | string | 0.2 |
| overlay | 是否显示遮罩层 | boolean | true |
| close-on-click-overlay | 是否在点击遮罩层后关闭菜单 | boolean | true |
| close-on-click-outside | 是否在点击外部元素后关闭菜单 | boolean | true |
| swipe-threshold | 滚动阈值,选项数量超过阈值且总宽度超过菜单栏宽度时,可以横向滚动 | number | string | - |
| auto-locate | 当祖先元素设置了 transform 时,自动调整下拉菜单的位置 | boolean | false |
这些属性的默认值在 DropdownMenu.tsx 的dropdownMenuProps中定义:其中overlay、closeOnClickOutside、closeOnClickOverlay为truthProp(布尔真值属性),duration默认0.2,direction默认down,zIndex与swipeThreshold为数值型属性。
DropdownItem Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| v-model | 当前选中项对应的 value | number | string | - |
| title | 菜单项标题 | string | 当前选中项文字 |
| options | 选项数组 | Option[] | [] |
| disabled | 是否禁用菜单 | boolean | false |
| lazy-render | 是否在首次展开时才渲染菜单内容 | boolean | true |
| title-class | 标题额外类名 | string | Array | object | - |
| teleport | 指定挂载的节点,等同于 Teleport 组件的 to 属性 | string | Element | - |
lazy-render默认开启,对应源码 DropdownItem.tsx 中的lazyRender: truthProp,该值最终透传给内部复用的Popup组件,实现"首次展开才渲染内容"的性能优化。teleport的 TS 类型为TeleportProps['to'],可将整个弹层挂载到任意节点(详见文末 FAQ 的 transform 定位问题)。
DropdownItem Events
| 事件名 | 说明 | 回调参数 |
|---|---|---|
| change | 点击选项导致 value 变化时触发 | value |
| open | 打开菜单栏时触发 | - |
| close | 关闭菜单栏时触发 | - |
| opened | 打开菜单栏且动画结束后触发 | - |
| closed | 关闭菜单栏且动画结束后触发 | - |
其中change只在选项值真正发生变化时才触发——源码中先判断option.value !== props.modelValue,随后依次emit('update:modelValue', ...)与emit('change', ...)(见 DropdownItem.tsx)。open/opened/close/closed事件则来自内部 Popup 的透传,用于感知动画生命周期。
DropdownItem Slots
| 名称 | 说明 |
|---|---|
| default | 菜单内容 |
| title | 自定义菜单项标题 |
title插槽的优先级最高:源码renderTitle中依次检查slots.title→props.title→ 匹配选项的text。
实例方法:close 与 toggle
通过ref可以获取到组件实例并调用实例方法(更多组件实例方法说明可参考项目文档的"组件实例方法"章节)。
DropdownMenu 方法:
| 方法名 | 说明 | 参数 | 返回值 |
|---|---|---|---|
| close | 关闭所有菜单的展示状态 | - | - |
DropdownItem 方法:
| 方法名 | 说明 | 参数 | 返回值 |
|---|---|---|---|
| toggle | 切换菜单展示状态,传true为显示,false为隐藏,不传参为取反 | show?: boolean | - |
close的实现很简洁——遍历所有子项并逐个调用item.toggle(false)(见 DropdownMenu.tsx)。toggle的默认行为是取反当前展示状态,并且支持通过第二个参数{ immediate: true }跳过过渡动画(见 DropdownItem.tsx)。
TypeScript 类型定义
组件导出以下类型定义,便于在业务代码中获得完整的类型约束:
import type { DropdownMenuProps, DropdownItemProps, DropdownItemOption, DropdownItemInstance, DropdownMenuInstance, DropdownMenuDirection, } from 'vant';DropdownMenuInstance和DropdownItemInstance是组件实例的类型,用法如下:
import { ref } from 'vue'; import type { DropdownMenuInstance, DropdownItemInstance } from 'vant'; const dropdownMenuRef = ref<DropdownMenuInstance>(); const dropdownItemRef = ref<DropdownItemInstance>(); dropdownMenuRef.value?.close(); dropdownItemRef.value?.toggle();这些类型分别定义于 dropdown-menu/types.ts(含DropdownMenuExpose、DropdownMenuThemeVars)与 dropdown-item/types.ts(含DropdownItemExpose、DropdownItemOption、DropdownItemThemeVars),并由两个组件的 index.ts 统一对外导出。
源码实现原理剖析
父子组件通信机制
DropdownMenu通过Symbol类型的注入键DROPDOWN_KEY(见 DropdownMenu.tsx)配合@vant/use的useChildren/useParent建立父子通信:父组件持有全部DropdownItem的引用(children),每个DropdownItem通过useParent(DROPDOWN_KEY)拿到父级上下文(id、props、offset、opened、updateOffset)。如果DropdownItem没有被包在DropdownMenu内,开发环境下会输出错误提示:<DropdownItem> must be a child component of <DropdownMenu>.。
展开互斥与关闭逻辑
点击某个菜单标题时,DropdownMenu的toggleItem会遍历所有子项:目标项执行toggle()取反展开,其他已展开的项则立即关闭({ immediate: true }跳过动画),从而保证同一时刻只有一个面板展开(见 DropdownMenu.tsx)。
弹层定位计算
面板位置由DropdownItem根据父级提供的offset计算:DropdownMenu在展开、滚动时调用updateOffset,通过useRect读取菜单栏底边(direction === 'down'时取rect.bottom,向上展开时取windowHeight - rect.top)得到偏移量(见 DropdownMenu.tsx),并监听滚动容器(useScrollParent+useEventListener('scroll'))实时刷新位置,保证面板在页面滚动时始终贴合菜单栏。定位样式随后写入弹层的top或bottom(见 DropdownItem.tsx)。
auto-locate 与 transform 定位修复
当祖先元素设置了transform时,position: fixed会相对该元素而非视口计算,导致面板位置异常。开启auto-locate后,组件通过getContainingBlock(wrapperRef.value)找到最近的包含块,并用offset -= useRect(offsetParent).top校正偏移量(见 DropdownItem.tsx)。这也是官方 FAQ 推荐的两种解决方案之一(详见下文)。
点击外部关闭
closeOnClickOutside通过useClickAway(root, onClickAway)实现——点击组件根节点以外区域时关闭所有菜单(见 DropdownMenu.tsx)。值得注意的细节是:当DropdownItem设置了teleport时,弹层已不在根节点内部,因此onClickWrapper会stopPropagation防止被误判为"点击外部"(见 DropdownItem.tsx)。
底层组件复用
整个下拉面板底层复用了 Vant 的Popup组件(负责遮罩层、过渡动画、open/opened/close/closed事件与lazyRender),选项列表复用Cell组件,勾选图标复用Icon组件。这意味着 DropdownMenu 天然继承了 Popup 在移动端多年的遮罩层交互与动画打磨,duration等属性正是透传给了 Popup(见 DropdownItem.tsx)。
主题定制:CSS 变量
组件提供了以下 CSS 变量用于自定义样式,可通过 ConfigProvider 组件 或直接覆盖:root变量使用:
| 名称 | 默认值 | 描述 |
|---|---|---|
| --van-dropdown-menu-height | 48px | 菜单栏高度 |
| --van-dropdown-menu-background | var(--van-background-2) | 菜单栏背景 |
| --van-dropdown-menu-shadow | 0 2px 12px rgba(100, 101, 102, 0.12) | 菜单栏阴影 |
| --van-dropdown-menu-title-font-size | 15px | 标题字号 |
| --van-dropdown-menu-title-text-color | var(--van-text-color) | 标题文字颜色 |
| --van-dropdown-menu-title-active-text-color | var(--van-primary-color) | 标题选中态颜色 |
| --van-dropdown-menu-title-disabled-text-color | var(--van-text-color-2) | 标题禁用态颜色 |
| --van-dropdown-menu-title-padding | 0 var(--van-padding-xs) | 标题内边距 |
| --van-dropdown-menu-title-line-height | var(--van-line-height-lg) | 标题行高 |
| --van-dropdown-menu-option-active-color | var(--van-primary-color) | 选项选中态颜色 |
| --van-dropdown-menu-option-disabled-color | var(--van-text-color-3) | 选项禁用态颜色 |
| --van-dropdown-menu-content-max-height | 80% | 面板内容最大高度 |
| --van-dropdown-item-z-index | 10 | 弹层 z-index |
这些变量的实际默认值统一声明在 dropdown-menu/index.less 的:root作用域内,并贯穿于菜单栏、标题三角箭头、选项与弹层的所有样式规则中;对应的 TS 主题变量类型DropdownMenuThemeVars、DropdownItemThemeVars也已导出,方便在使用 ConfigProvider 时获得类型提示。
常见问题(FAQ)
父元素设置 transform 后,下拉菜单的位置错误?
把DropdownMenu嵌套在Tabs等组件内部使用时,可能会遇到下拉菜单位置错误的问题。这是因为 transform 元素内部的 fixed 定位会相对于该元素进行计算,而不是相对于整个文档,从而导致下拉菜单的布局异常。
方案一:将DropdownItem的teleport属性设置为body,把弹层挂载到body下即可避免此问题:
<van-dropdown-menu> <van-dropdown-item teleport="body" /> <van-dropdown-item teleport="body" /> </van-dropdown-menu>方案二:将DropdownMenu的auto-locate属性设置为true,让组件自动检测包含块并校正位置:
<van-dropdown-menu auto-locate> <van-dropdown-item /> <van-dropdown-item /> </van-dropdown-menu>两种方案的取舍:teleport直接改变挂载层级,从根上规避 fixed 定位受 transform 影响的问题;auto-locate不改变 DOM 结构,而是在定位计算时减去包含块的偏移(见上文源码剖析),适合无法改变挂载节点的场景。此外,由于closeOnClickOutside依赖"点击外部"判定,使用teleport时组件已通过事件冒泡拦截保证了关闭逻辑仍能正常工作。
测试与稳定性保障
组件在仓库中配有完整的单元测试(dropdown-menu/test/index.spec.tsx),覆盖了以下关键行为:
- 点击标题展开/收起下拉面板、多菜单项切换;
- 选项
icon的渲染; close-on-click-outside开启与关闭两种场景下点击外部区域的关闭行为;direction="up"向上展开、菜单标题渲染、选项点击触发change等。
这些测试与官方文档(英文文档、中文文档)共同保障了组件 API 行为的一致性,也为你理解每个属性的实际效果提供了可运行、可验证的参考。
【免费下载链接】vantA lightweight, customizable Vue UI library for mobile web apps.项目地址: https://gitcode.com/GitHub_Trending/va/vant
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考