Files
workspace/code/fms/.codebuddy/plans/dropdown-enhance_866d28ec.md
T
2026-08-17 17:29:54 +08:00

9.2 KiB
Raw Blame History

name, overview, todos
name overview todos
dropdown-enhance 增强组件库 Dropdown:支持 item 自带 onClick、新增 #header 富头部插槽、新增 DropdownItem 子组件(类 el-dropdown-item 写法),并把 AppTopbar 用户下拉迁移到新能力(头像+名字+组织头部)。同时保留现有 items 数组 + 全局 @click 与 #menu 逃生舱。
id content status
enhance-menu-onclick 修改 menu.vue 普通项点击先执行 item.onClick 再 selectItem completed
id content status dependencies
add-dropdown-item 新建 DropdownItem.vue 声明式子组件并定义 props completed
enhance-menu-onclick
id content status dependencies
enhance-dropdown-header dropdown.vue 增加 #header 插槽与 DropdownItem VNode 收集转 items completed
add-dropdown-item
id content status dependencies
add-header-style index.scss 新增 .dropdown-header 样式 completed
enhance-dropdown-header
id content status dependencies
refactor-topbar AppTopbar 用户下拉改用 #header 富头部并精简 items completed
enhance-dropdown-header
add-header-style
id content status dependencies
verify-uses 用 [subagent:code-explorer] 核查所有 Dropdown 用法无回归 completed
refactor-topbar

用户需求

参考 Element Plus 的 <el-dropdown-item> 写法,增强项目组件库 @/components/ui/dropdown 的下拉菜单能力,使其更灵活、可自定义。

产品概述

为 Dropdown 组件库新增三种能力并保持向后兼容:

  1. items 数组项支持自带 onClick(item) 回调,点击某项时优先执行该项自己的回调,同时保留全局 @click 分发。
  2. 支持 <DropdownItem> 子组件写法(具名/默认插槽声明每个菜单项),类似 el-dropdown-item 的声明式用法。
  3. 新增 #header 插槽,在菜单项列表上方渲染「头像 + 名字 + 组织」等富头部内容。

现有能力(items + 全局 @click、#menu 逃生舱、键盘导航、子菜单 hover)必须全部保留不破坏。

核心特性

  • item 级别 onClick:在 items 配置里直接写点击处理函数,命中该项时直接触发,无需统一在 @click 里 switch。
  • <DropdownItem> 子组件:支持 key/icon/danger/disabled/divider 等属性与默认插槽内容,可替代纯数组声明。
  • #header 富头部插槽:Dropdown 浮层顶部可插入任意自定义富内容(顶部 header 区,与菜单项列表之间有分隔)。
  • 向后兼容:未配置 onClick 的项仍走全局 @click;未使用 DropdownItem 时仍可用 items 数组;未使用 #header 时浮层不变。
  • AppTopbar 用户下拉改造:将「头像 + 名字 + 组织」从 trigger 移到菜单 #header 插槽,trigger 仅保留头像或精简触发按钮。

技术栈

  • 前端框架:Vue 3(<script setup> SFC + scoped 样式)
  • 组件库模式:@/components/ui/ 下 SFC + index.scss(@use 引入),复用现有 Dropdown / DropdownMenu / Popover
  • 样式 Token:--fms-card / --fms-border / --fms-primary / --fms-secondary-hover / --fms-text / --fms-text-secondary / --fms-control-radius / --fms-danger / --fms-dropdown-*
  • 图标:Lucide(@lucide/vue)

实现方案

总体策略

在现有 Dropdown 组件上做增量增强,不改动 Popover 与定位逻辑;新增 DropdownItem.vue 子组件承载声明式写法;在 DropdownMenu.vue 与 dropdown.vue 中分别支持 item.onClick 与 #header 插槽。所有增强均为「可选能力」,未使用时不改变既有渲染与行为。

关键技术决策

  1. item.onClick 优先级:在 menu.vue 普通项点击处改为「先执行 item.onClick?.(item),再 selectItem(item)(继续 emit 全局 click)」。这样既支持项自带回调,又保留全局 @click 分发,互不冲突;项回调里可执行 close() 关闭(selectItem 内部已关闭)。
  2. DropdownItem 子组件:新建 DropdownItem.vue,仅用于「声明语义」——它不作为独立渲染节点,而是被 Dropdown 在 #default 默认插槽中收集其 props/插槽内容并转为 items;或直接由 DropdownMenu 消费。为最小化复杂度并避免与现有 items 递归体系冲突,采用「DropdownItem 仅暴露 props + 提供 default 插槽内容,Dropdown 在运行时通过 useSlots().default 读取子节点(VNode)还原成 items」的方案,复用 DropdownMenu 渲染。这样既获得 el-dropdown-item 的写法,又不重复一套渲染逻辑。
  • 权衡:直接把 DropdownItem 写成独立可点击 DOM 会与现有键盘导航/activeIndex provide 体系割裂,需大量重构。采用「VNode 收集转 items」更符合现有架构、改动最小、风险最低。
  • 取舍:DropdownItem 用法与 items 用法二选一(同一次 Dropdown 内不要混用,混用时以 items 优先或合并,计划以 items 优先、默认插槽 fallback)。
  1. #header 插槽:在 dropdown.vue 的 Popover #content 内、<DropdownMenu> 上方插入 <slot name="header" />(仅当 items != null 时显示,#menu 逃生舱模式仍由用户自行决定 header 是否出现)。头部区加 .dropdown-header 样式(padding、与下方列表之间 1px 分隔线、不可点击、user-select:none)。

性能与可靠性

  • 渲染开销:DropdownItem 的 VNode 收集仅在挂载时执行一次(非热路径),无性能问题;items 递归渲染与现有一致。
  • 键盘导航:enabledIndices 已跳过 divider/label/disabled,新增的 header 不参与索引,无需改动;DropdownItem 转换出的项若带 disabled 自动纳入跳过逻辑。
  • 向后兼容:所有新 props/插槽均默认空/可选;现有 AppTopbar 改法只在「使用 #header」时调整结构,未用到的 Dropdown 实例零影响。

实现注意事项

  • 复用现有 CSS 变量与 .dropdown-* 样式体系,不引入新 token;header 分隔线复用 .dropdown-divider 视觉或新建 .dropdown-header 独立样式(建议独立,避免 margin 冲突)。
  • DropdownItem 的 divider 属性转换为 { divider: true };disabled/danger/icon/key 直接映射;默认插槽内容映射为 label(支持富内容时允许 label 为 VNode/函数?本期仅支持文本 label,富内容走 #header 或 #menu 逃生舱,保持 YAGNI)。
  • AppTopbar 改造:删除 userMenuItems 里 { type:'label', label: org },改为在 #header 插槽内渲染「头像 + 名字 + 组织」;trigger 保留头像 + 名字按钮(或精简)。注意 scoped 样式 .fms-avatar/.fms-user-name 需在 AppTopbar 内保留(头部在浮层内,仍属该组件 scoped 作用域通过 slot 传递)。
  • 保持 0 lint:新增 SFC 遵循现有 ESLint 规则(defineProps 默认值、无未使用变量)。

架构设计

flowchart TD
  A[Dropdown.vue 主组件] -->|items 数组| B[DropdownMenu.vue 递归渲染]
  A -->|default 插槽含 DropdownItem| C[收集 VNode 转 items]
  C --> B
  A -->|#header 插槽| D[富头部区 dropdown-header]
  A -->|#menu 逃生舱| E[完全自定义内容]
  B -->|普通项点击| F[执行 item.onClick 再 selectItem emit click]
  A --> G[Popover 浮层/定位/关闭]

组件关系不变,仅在 Dropdown 入口增加「VNode→items」「header 插槽」两路输入,DropdownMenu 增加「onClick 优先」分支。

目录结构

fms-vue/src/components/ui/dropdown/
├── dropdown.vue      # [MODIFY] 新增 #header 插槽(Popover #content 内,DropdownMenu 上方);支持从 default 插槽收集 DropdownItem VNode 转换为 items(fallback 于未传 items 时);onClick 透传由 menu.vue 处理。
├── menu.vue          # [MODIFY] 普通项点击逻辑改为:先 item.onClick?.(item),再 selectItem(item);其余 divider/label/children 渲染不变。
├── DropdownItem.vue  # [NEW] 声明式菜单项子组件。仅承载 props(key/icon/danger/disabled/divider/label) 与默认插槽,不独立渲染交互,由 Dropdown 收集为 items 项。
└── index.scss        # [MODIFY] 新增 .dropdown-header 样式(padding、底部 1px 分隔、user-select:none、非交互)。

fms-vue/src/layouts/components/
└── AppTopbar.vue     # [MODIFY] 用户下拉:移除 userMenuItems 中的 type:'label' 组织行;新增 #header 插槽渲染「头像+名字+组织」富头部(复用现有 .fms-avatar/.fms-user-name 样式,新增组织文本样式);trigger 保持头像+名字按钮。

关键代码结构(接口级)

// DropdownItem.vue props(声明式项,仅用于被 Dropdown 收集)
defineProps<{
  key?: string | number
  icon?: Component
  label?: string
  danger?: boolean
  disabled?: boolean
  divider?: boolean
}>()

// menu.vue 普通项点击(增强后)
@click="!item.disabled && onItemClick(item)"
// onItemClick: item.onClick?.(item); selectItem(item)

Agent Extensions

SubAgent

  • code-explorer
  • 用途:在实施前深入核对 Dropdown/Popover/AppTopbar 的所有引用点与 slot/props 用法,确认改动无遗漏调用方。
  • 预期结果:产出受影响文件清单与调用关系,确保增强不破坏现有使用 Dropdown 的其他页面(如主题色板 Dropdown 用 #menu 逃生舱)。