Dropdown 下拉菜单
下拉菜单把一组次要操作收进一个触发区,避免主界面被按钮塞满。MDropdown 负责定位、开合与键盘导航,MDropdownItem 负责单项的语义(普通项、分隔线、禁用项);两者靠 #menu 插槽里的 provide / inject 上下文联动,所以子项点击后菜单会自己收起来。
代码演示
基础菜单
触发内容放在 #trigger 插槽,菜单项放在 #menu 插槽,默认点击展开。
<script setup lang="ts">
import { mdiChevronDown } from '@mdi/js'
</script>
<template>
<div class="m-row-demo">
<MDropdown>
<template #trigger>
<MButton variant="outlined">
更多操作
<MIcon :path="mdiChevronDown" :size="16" />
</MButton>
</template>
<template #menu>
<MDropdownItem command="edit">编辑</MDropdownItem>
<MDropdownItem command="duplicate">创建副本</MDropdownItem>
<MDropdownItem command="move">移动到…</MDropdownItem>
</template>
</MDropdown>
</div>
</template>点击命令
每个菜单项用 command 携带一个业务标识,点击后父组件抛出 command 事件,这里用 toast.info() 把收到的值显示出来。
<script setup lang="ts">
import { toast } from 'miao-design'
function onCommand(value: unknown) {
toast.info(`收到 command:${String(value)}`)
}
</script>
<template>
<div class="m-row-demo">
<MDropdown @command="onCommand">
<template #trigger>
<MButton variant="contained" color="primary">执行操作</MButton>
</template>
<template #menu>
<MDropdownItem command="publish">发布</MDropdownItem>
<MDropdownItem command="schedule">定时发布</MDropdownItem>
<MDropdownItem divider />
<MDropdownItem command="delete">删除</MDropdownItem>
<MDropdownItem command="archive" disabled>归档(无权限)</MDropdownItem>
</template>
</MDropdown>
</div>
</template>悬停触发
trigger="hover" 时鼠标移入即展开,移出 150ms 后收起;键盘聚焦触发区同样能打开,保证可访问性。
<script setup lang="ts">
import { ref } from 'vue'
import { mdiChevronDown } from '@mdi/js'
const last = ref('未选择')
</script>
<template>
<div class="m-row-demo">
<MDropdown trigger="hover" placement="bottom-start" @command="last = String($event)">
<template #trigger>
<MButton variant="text">
悬停展开
<MIcon :path="mdiChevronDown" :size="16" />
</MButton>
</template>
<template #menu>
<MDropdownItem command="overview">概览</MDropdownItem>
<MDropdownItem command="members">成员</MDropdownItem>
<MDropdownItem command="settings">设置</MDropdownItem>
</template>
</MDropdown>
<span style="color: var(--m-text-sub)">最近选择:{{ last }}</span>
</div>
</template>选中态、分割线与禁用项
组件没有内置的 selected 属性,选中态由业务自己用 command 记录并用图标 / 颜色表达;分割线用 divider,禁用项点击既不抛事件也不收起菜单。
<script setup lang="ts">
import { ref } from 'vue'
import { mdiCheck } from '@mdi/js'
const selected = ref('list')
const views = [
{ value: 'list', label: '列表视图' },
{ value: 'board', label: '看板视图' },
{ value: 'timeline', label: '时间线视图' }
]
</script>
<template>
<div class="m-row-demo">
<MDropdown width="200px" @command="selected = String($event)">
<template #trigger>
<MButton variant="outlined">切换视图</MButton>
</template>
<template #menu>
<MDropdownItem v-for="view in views" :key="view.value" :command="view.value">
<span
:style="{
display: 'flex',
alignItems: 'center',
gap: '8px',
flex: 1,
color: selected === view.value ? 'var(--m-primary)' : undefined
}">
<MIcon
:path="mdiCheck"
:size="16"
:style="{ visibility: selected === view.value ? 'visible' : 'hidden' }" />
{{ view.label }}
</span>
</MDropdownItem>
<MDropdownItem divider />
<MDropdownItem command="refresh">刷新数据</MDropdownItem>
<MDropdownItem command="export" disabled>导出(需要升级套餐)</MDropdownItem>
</template>
</MDropdown>
</div>
</template>弹出位置
placement 支持上下各三个水平对齐位;某侧空间不足时会自动翻转到对侧,水平对齐位保持不变。
上方空间不够时面板会自动翻到下方,可以拖动页面把按钮推到视口边缘验证。
<script setup lang="ts">
const placements = [
'top-start',
'top-center',
'top-end',
'bottom-start',
'bottom-center',
'bottom-end'
] as const
</script>
<template>
<div class="m-row-demo">
<MDropdown v-for="placement in placements" :key="placement" :placement="placement">
<template #trigger>
<MButton variant="outlined" size="small">{{ placement }}</MButton>
</template>
<template #menu>
<MDropdownItem :command="placement">{{ placement }}</MDropdownItem>
<MDropdownItem command="second">第二项</MDropdownItem>
<MDropdownItem command="third">第三项</MDropdownItem>
</template>
</MDropdown>
</div>
</template>自定义面板
#menu 插槽接受任意节点,不只限于 MDropdownItem;面板宽度、圆角与间距分别由 width、panelRadius、offset 控制。
<script setup lang="ts">
import { ref } from 'vue'
import { mdiContentCopy, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'
const last = ref('未操作')
</script>
<template>
<div class="m-row-demo">
<MDropdown width="240px" panel-radius="12px" :offset="10" @command="last = String($event)">
<template #trigger>
<MButton variant="contained" color="secondary">自定义面板</MButton>
</template>
<template #menu>
<div style="padding: 4px 16px 10px; font-size: 12px; color: var(--m-text-sub)">
最近的操作
</div>
<MDropdownItem command="rename">
<MIcon :path="mdiPencilOutline" :size="18" />
重命名
</MDropdownItem>
<MDropdownItem command="duplicate">
<MIcon :path="mdiContentCopy" :size="18" />
创建副本
</MDropdownItem>
<MDropdownItem divider />
<MDropdownItem command="trash" disabled>
<MIcon :path="mdiTrashCanOutline" :size="18" />
移入回收站
</MDropdownItem>
</template>
</MDropdown>
<span style="color: var(--m-text-sub)">最近选择:{{ last }}</span>
</div>
</template>API
MDropdown Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
placement | 'top-start' | 'top-center' | 'top-end' | 'bottom-start' | 'bottom-center' | 'bottom-end' | 'bottom-start' | 弹出位置;空间不足时自动翻转到对侧 |
trigger | 'click' | 'hover' | 'click' | 触发方式:点击或悬停 |
disabled | boolean | false | 是否禁用(禁用后触发区 pointer-events: none、透明度 0.38) |
width | string | undefined | 菜单宽度(任意 CSS 长度);不传时 min-width 取触发区宽度,内容可继续撑开 |
offset | number | 4 | 菜单与触发区之间的间距(px) |
panelRadius | string | undefined | 菜单面板圆角(任意 CSS 长度,如 '8px');缺省回退 4px |
appendToBody | boolean | true | 是否 Teleport 到 body,避免被父级 overflow / z-index 裁剪 |
MDropdown Events
| 事件 | 参数 | 说明 |
|---|---|---|
command | value: unknown | 任一 MDropdownItem 被点击时抛出其 command 值 |
open | 无 | 菜单展开 |
close | 无 | 菜单收起 |
MDropdown Slots
| 插槽 | 参数 | 说明 |
|---|---|---|
trigger | 无 | 触发内容;未提供时回退到默认插槽 |
default | 无 | 作为 trigger 的兜底内容 |
menu | 无 | 菜单面板内容,放置 MDropdownItem |
MDropdownItem Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
command | unknown | undefined | 点击该项时随 command 事件抛出的值 |
disabled | boolean | false | 是否禁用;禁用时点击不抛事件、不收起菜单 |
divider | boolean | false | 渲染为分隔线(role="separator"),此时不渲染默认插槽内容 |
MDropdownItem Events
| 事件 | 参数 | 说明 |
|---|---|---|
command | value: unknown | 点击该项时抛出自身 command;同时在 MDropdown 上下文里触发父级 command 并收起菜单 |
MDropdownItem Slots
| 插槽 | 参数 | 说明 |
|---|---|---|
default | 无 | 菜单项内容;divider 为 true 时不渲染 |
类型
export type DropdownPlacement =
| 'top-start' | 'top-center' | 'top-end'
| 'bottom-start' | 'bottom-center' | 'bottom-end'
export type DropdownTrigger = 'click' | 'hover'使用建议
父子联动是怎么发生的
MDropdown 通过 provide 向 #menu 插槽内的后代注入了两个回调,MDropdownItem 用 inject 取到它们。一次点击依次发生三件事:
MDropdownItem抛出自己的command事件;- 通过上下文调用父级的
onItemClick,父级据此抛出command事件; - 通过上下文调用父级的
close,菜单自动收起。
所以业务只需要在 MDropdown 上监听一次 command,用 value 做分支即可,不需要在每个菜单项上重复绑定点击处理。
菜单项必须放进 #menu 插槽
触发框内的内容会被当作触发区,而只有 #menu 插槽里的 MDropdownItem 才能注入到父级上下文。把菜单项写在默认插槽里,它会变成触发内容的一部分,既不会渲染成菜单,也不会自动收起。
脱离父组件的子项不会收起菜单
单独使用 MDropdownItem(没有外层的 MDropdown)时,注入的上下文为空:点击只保留涟漪反馈并抛出自身的 command 事件,不会有任何"收起"行为。若在别处复用它,需要自己处理关闭逻辑。
另外,菜单展开时会锁定页面滚动、监听外部 pointerdown 自动收起;面板高度固定在 320px 以内并在内容层内部滚动,超长菜单不会把页面撑高。
相关组件
- Select 选择器:同样是弹层 + 列表,但
MSelect负责维护选中值与输入框形态。 - Dialog 对话框:需要遮罩、必须打断流程时用对话框而非菜单。
- Icon 图标:菜单项左侧的图标通常来自
@mdi/js。 - Toast 轻提示:菜单里执行完命令后给一个轻量反馈。