TinyRobot DropdownMenu 下拉菜单:三种触发方式与实现原理
AI 回复里给你一排「去续费 / 去退订 / 查账单」的快捷操作,直接平铺五六个按钮既占地方又不好扫读------这是 TinyRobot 在 AI 对话场景里给 SuggestionPills 配套开发下拉菜单的动机。文档开头明确写了一句边界:「此组件目前仅针对 SuggestionPills 组件开发,可配置项暂不全面」。这句话描述的是当前边界,不过组件该有的交互已经齐全,实现思路也值得阅读。本文用同一份菜单项数据,从「怎么组装」讲到「为什么三种 trigger 行为不一样」。
组装菜单:items 和 trigger 插槽
组件从 @opentiny/tiny-robot 导出,注册名是 TrDropdownMenu。先用最基本的方式把它搭起来:
vue
<script setup lang="ts">
import { TrDropdownMenu, TrSuggestionPillButton } from '@opentiny/tiny-robot'
import { ref } from 'vue'
const dropdownMenuItems = ref([
{ id: '1', text: '去续费' },
{ id: '2', text: '去退订' },
{ id: '3', text: '查账单' },
{ id: '4', text: '导账单' },
{ id: '5', text: '对帐单' },
])
</script>
<template>
<TrDropdownMenu :items="dropdownMenuItems">
<template #trigger>
<TrSuggestionPillButton>更多操作</TrSuggestionPillButton>
</template>
</TrDropdownMenu>
</template>
两个要点:
items是唯一必填的 prop,每个菜单项是一个{ id: string, text: string }------id是菜单项唯一标识,text是显示文本。官方示例里用它配合addDropdownMenu/removeDropdownMenu动态增删菜单项,也就是说items是响应式数据驱动的。trigger插槽用来放自定义触发元素,可以是任意按钮或组件,不限定TrSuggestionPillButton。
三种触发方式:先记住一个心智模型
trigger 决定菜单什么时候显示、谁有权限改 show。先记住一个要点:只有 click 和 hover 模式下,组件内部才允许修改显示状态;manual 模式下 show 完全由外部控制,组件内部不会修改它。
click:点一下开,再点一下关
vue
<TrDropdownMenu v-model:show="clickShow" :items="dropdownMenuItems">
<template #trigger>
<TrSuggestionPillButton>Trigger 为 click</TrSuggestionPillButton>
</template>
</TrDropdownMenu>
默认模式。点击触发元素开合菜单,点击菜单外部自动关闭,组件内外都可以改 show,所以它作为双向绑定属性使用(v-model:show)。
hover:移入显示,移出隐藏
vue
<TrDropdownMenu v-model:show="hoverShow" :items="dropdownMenuItems" trigger="hover">
<template #trigger>
<TrSuggestionPillButton>Trigger 为 hover</TrSuggestionPillButton>
</template>
</TrDropdownMenu>
鼠标移入触发元素显示菜单,移出后隐藏,组件内部有 100ms 的进入/离开延迟(usePopperHover 的 delayEnter / delayLeave),避免鼠标在边界附近移动时菜单反复开合。
manual:显示状态完全交给外部
vue
<TrDropdownMenu :items="dropdownMenuItems" :show="show" trigger="manual" @click-outside="show = false">
<template #trigger>
<TrSuggestionPillButton @click="show = !show">Trigger 为 manual</TrSuggestionPillButton>
</template>
</TrDropdownMenu>
manual 模式下 show 是单向 prop:组件内部不会自动修改它的值,开合完全由外部代码控制。这也是最容易踩坑的地方:如果按习惯写成 v-model:show,会发现开关逻辑不生效,因为组件在 manual 模式下不会写回这个值。
接收结果:item-click 和 click-outside
点击菜单项后,需要把选中结果带回业务逻辑:
vue
<TrDropdownMenu
v-model:show="clickShow"
:items="dropdownMenuItems"
@item-click="(item) => console.log('选择:', item)"
>
<template #trigger>
<TrSuggestionPillButton>更多操作</TrSuggestionPillButton>
</template>
</TrDropdownMenu>
item-click参数就是{ id, text }本身,点击菜单项后组件先关闭菜单,再触发事件,因此无需再自己处理关闭菜单的逻辑。click-outside参数是MouseEvent,点击菜单外部区域时触发;注意它只在trigger为 click 或 manual 时有效,hover 模式没有这个事件。
定位与外观:appendTo 和 CSS 变量
appendTo 指定下拉菜单挂载的容器元素或选择器,默认跟随组件所在文档流。菜单外观通过一组 CSS 变量定制:
| 变量 | 默认值 | 作用 |
|---|---|---|
--tr-dropdown-menu-bg-color |
#ffffff |
背景色 |
--tr-dropdown-menu-box-shadow |
0 0 20px rgba(0, 0, 0, 0.08) |
阴影 |
--tr-dropdown-menu-min-width |
130px |
最小宽度 |
--tr-dropdown-menu-item-color |
rgb(25, 25, 25) |
菜单项文字颜色 |
--tr-dropdown-menu-item-hover-bg-color |
#f5f5f5 |
菜单项悬停背景色 |
--tr-dropdown-menu-item-font-weight |
normal |
菜单项字体粗细 |
还有 --tr-dropdown-menu-min-top / --tr-dropdown-menu-max-bottom / --tr-dropdown-menu-min-left / --tr-dropdown-menu-max-right 一组位置约束变量。改皮肤时直接覆盖这些变量即可,不用深入组件内部样式。
实现原理:三个模式背后的同一个 Popper
看 packages/components/src/dropdown-menu/index.vue(develop @ fd9f9233),会发现三个模式其实共享同一个 TrBasePopper,差异在于「谁在改 show」这一层:
- click :
handleTriggerClick里show.value = !show.value切换,同时用onClickOutside(来自 VueUse,ignore掉触发元素)监听外部点击,触发click-outside事件并把show置为false。 - hover :走
usePopperHover(triggerRef, dropdownMenuRef, { delayEnter: 100, delayLeave: 100 }),把悬停状态直接写进show。 - manual :
show的 setter 直接return------组件内部任何地方都改不了它,完全依赖外部绑定。
组件内部对 show 的读取也分两路:manual 时读 props.show,否则读 showModel(defineModel('show'))。菜单项点击统一走 handleItemClick:先 show.value = false 关闭菜单,再 emit('item-click', item)。
另外组件通过 defineExpose 暴露了 update() 方法,透传给底层 TrBasePopper 的 update(),需要刷新浮层状态时按需调用。
边界与常见坑
- manual 模式不是双向绑定 。写
v-model:show期待组件帮你开关,是理解偏差;外部负责所有开关逻辑。 - hover 模式没有 click-outside。hover 的显示/隐藏完全由悬停状态驱动,需要「点击外部关闭」时,请使用 click 或 manual。
items必填。忘了传会直接缺失菜单数据;传了空数组菜单里就没有可点项。- 组件仍处早期。官方文档明确「仅针对 SuggestionPills 组件开发,可配置项暂不全面」,正式接入前先在小范围验证,不要把它当作成熟通用组件硬套复杂交互。
关于 OpenTiny NEXT
OpenTiny NEXT 是一套企业智能前端开发解决方案,以生成式 UI 和 WebMCP 两大核心技术为基础,对现有传统的 TinyVue 组件库、TinyEngine 低代码引擎等产品进行智能化升级,构建出面向 Agent 应用的前端 NEXT-SDKs、AI Extension、TinyRobot 智能助手、GenUI 等新产品,实现 AI 理解用户意图自主完成任务,加速企业应用的智能化改造。
欢迎加入 OpenTiny 开源社区。 OpenTiny 官网:opentiny.design TinyRobot 代码仓库:github.com/opentiny/ti...(欢迎 star ⭐) 如果你也想要共建,可以进入代码仓库,找到 good first issue 标签,一起参与开源贡献~如果你有任何问题,欢迎在评论区留言交流!