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 的进入/离开延迟(usePopperHoverdelayEnter / 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」这一层:

  • clickhandleTriggerClickshow.value = !show.value 切换,同时用 onClickOutside(来自 VueUse,ignore 掉触发元素)监听外部点击,触发 click-outside 事件并把 show 置为 false
  • hover :走 usePopperHover(triggerRef, dropdownMenuRef, { delayEnter: 100, delayLeave: 100 }),把悬停状态直接写进 show
  • manualshow 的 setter 直接 return------组件内部任何地方都改不了它,完全依赖外部绑定。

组件内部对 show 的读取也分两路:manual 时读 props.show,否则读 showModeldefineModel('show'))。菜单项点击统一走 handleItemClick:先 show.value = false 关闭菜单,再 emit('item-click', item)

另外组件通过 defineExpose 暴露了 update() 方法,透传给底层 TrBasePopperupdate(),需要刷新浮层状态时按需调用。

边界与常见坑

  1. manual 模式不是双向绑定 。写 v-model:show 期待组件帮你开关,是理解偏差;外部负责所有开关逻辑。
  2. hover 模式没有 click-outside。hover 的显示/隐藏完全由悬停状态驱动,需要「点击外部关闭」时,请使用 click 或 manual。
  3. items 必填。忘了传会直接缺失菜单数据;传了空数组菜单里就没有可点项。
  4. 组件仍处早期。官方文档明确「仅针对 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 标签,一起参与开源贡献~如果你有任何问题,欢迎在评论区留言交流!

相关推荐
zhedream2 小时前
a-select / a-input 自定义下拉、Vue2 Fragment ,以及 transfer-dom
前端·vue.js
无人生还2 小时前
从 Vue3 到 React · 快速上手系列第 10 篇:路由
前端·vue.js·react.js
rememberme0012 小时前
华为云 CodeArts Pipeline前端 Vue 项目自动打包发布配置指南
前端·vue.js·华为云
A24207349302 小时前
Vue.js 初学者注意事项与项目开发实践指南
前端·javascript·vue.js
90后的晨仔13 小时前
从 H5 到 uni-app:一篇写给前端小白的"翻译指南"
前端·vue.js·前端框架
java1234_小锋18 小时前
Vue3+Vite简介以及构建第一个HelloWorld实例
前端·javascript·vue.js·vite
西门啐血20 小时前
Vue 缓存之坑,变量赋值方式和响应式数据
前端·vue.js·缓存
观察员20 小时前
Vue3 猜字游戏前端实战:从零搭建聊天交互界面
vue.js
春波petal1 天前
Vue3防抖搜索:从Lodash到customRef全解析
vue.js·vue3·防抖搜索