Files
workspace/code/fms/.codebuddy/plans/fms-tree-component_8d7b2eee.md
T
2026-08-10 22:34:33 +08:00

218 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: fms-tree-component
overview: 基于 shadcn-vue 模式(reka-ui Collapsible + ContextMenu + Tailwind v4 + Vue 3 Composition API)从零封装 FmsTree/FmsTreeNode 组件,含虚拟根节点、右键菜单、展开折叠、节点选中、键盘导航。本次不做多语言。
design:
architecture:
framework: vue
component: shadcn
styleKeywords:
- shadcn-vue
- new-york
- Minimalism
- Clean
fontSystem:
fontFamily: Inter
heading:
size: 14px
weight: 500
subheading:
size: 12px
weight: 400
body:
size: 13px
weight: 400
colorSystem:
primary:
- var(--primary)
- var(--accent)
background:
- var(--background)
- var(--accent)
- transparent
text:
- var(--foreground)
- var(--muted-foreground)
- var(--accent-foreground)
functional:
- var(--border)
- var(--ring)
todos:
- id: create-fms-tree-node
content: 创建 components/FmsTreeNode.vue 递归节点组件,基于 Collapsible + ContextMenu 实现展开折叠、选中高亮、右键菜单、键盘导航
status: completed
- id: create-fms-tree
content: 创建 components/FmsTree.vue 根组件,实现虚拟根节点包裹、props 定义、v-model 双向绑定、事件转发和 slot 透传
status: completed
dependencies:
- create-fms-tree-node
- id: verify-and-format
content: 验证组件可正常渲染,运行 pnpm fmt:check 检查格式
status: completed
dependencies:
- create-fms-tree
---
## 产品概述
在新系统 fms-vue 中封装一个可复用的 FmsTree 树组件,基于 shadcn-vue(reka-ui + Tailwind v4)技术栈,为模块管理等场景提供统一的树交互体验。本次不做多语言、拖拽、虚拟滚动。
## 核心功能
- 树形结构展示:递归渲染节点,支持无限层级嵌套
- 展开/折叠:点击箭头图标展开或折叠子节点,支持 v-model:expandedKeys 受控模式
- 节点选中:点击节点高亮选中,支持 v-model:selectedKeys,单选模式
- 虚拟根节点:treeData 外层自动包裹虚拟根节点(如 全部分类),确保首次加载时树处于展开可交互状态
- 右键菜单:通过 contextMenu prop 传入菜单项生成函数,右键节点弹出 ContextMenu
- 标题插槽:支持 #title slot 自定义节点标题渲染
- 键盘导航:ArrowUp/Down 切换焦点,ArrowRight/Left 展开折叠,Enter 选中
## 技术栈
- Vue 3 Composition API + TypeScript(`<script setup lang="ts">`)
- reka-ui:CollapsibleRoot / CollapsibleTrigger / CollapsibleContent(展开/折叠)
- reka-ui:ContextMenuRoot / ContextMenuTrigger / ContextMenuContent / ContextMenuItem(右键菜单)
- Tailwind v4:语义颜色(bg-accent、text-muted-foreground、bg-popover 等)
- @lucide/vue:ChevronRight 图标(展开箭头)
- clsx + tailwind-merge:cn() 工具函数
## 实现方案
### 核心策略
由于 reka-ui 没有 Tree 原语,采用 **Collapsible 递归嵌套** 自建树结构。每个有 children 的节点用 `Collapsible` 包裹,叶子节点为普通 div。右键菜单通过 `ContextMenu` 包裹每个节点行实现。虚拟根节点在 `FmsTree.vue` 中用 `computed` 生成。
### 架构设计
```
FmsTree.vue(根容器)
├── computed:effectiveTreeData(包裹虚拟根节点)
│ └── FmsTreeNode.vue(递归节点,v-for children)
│ ├── Collapsible(当 node.children 存在)
│ │ ├── ContextMenu(包裹节点行)
│ │ │ ├── ContextMenuTrigger
│ │ │ │ └── 节点行(缩进 + ChevronRight箭头 + title slot)
│ │ │ └── ContextMenuContent → ContextMenuItem
│ │ └── CollapsibleContent
│ │ └── FmsTreeNode(递归子节点)
│ └── 无 children 时:纯节点行(ContextMenu 包裹)
```
### 组件文件拆分
```
src/
└── components/
├── FmsTree.vue # [NEW] 根组件:props/v-model定义、虚拟根节点、事件转发
└── FmsTreeNode.vue # [NEW] 递归节点:Collapsible逻辑、缩进、选中高亮、键盘导航
```
### 数据流
外部调用者传入 treeData + expandedKeys + selectedKeys → FmsTree 包裹虚拟根节点 → FmsTreeNode 递归渲染,CollapsibleRoot.open 受 expandedKeys 控制,节点行 class 受 selectedKeys 控制,事件向上冒泡(select / menuClick / update:expandedKeys / update:selectedKeys)。
## 实现细节
### Props 设计(FmsTree.vue)
```ts
interface TreeNode {
key: string
title: string
children?: TreeNode[]
[key: string]: unknown
}
// Props
treeData: TreeNode[]
expandedKeys?: string[]
selectedKeys?: string[]
contextMenu?: (node: TreeNode) => { key: string; label: string }[]
rootKey?: string // 默认 '__fms_root__'
rootTitle?: string // 默认 '全部分类'
showRoot?: boolean // 默认 true
indent?: number // 默认 24
```
### Events
```ts
emit('update:expandedKeys', keys: string[])
emit('update:selectedKeys', keys: string[])
emit('select', keys: string[], info: { node: TreeNode })
emit('menuClick', payload: { key: string; node: TreeNode })
```
### 组件关键实现要点
**FmsTreeNode.vue(递归节点)**:
- 接收 props:node、expandedKeys、selectedKeys、indent(从 depth 计算)、contextMenu
- CollapsibleRoot 的 v-model:open 根据 expandedKeys.includes(node.key) 受控
- 节点行的 padding-left 由 `depth * indent` 计算
- ChevronRight 图标通过 CSS `rotate-90` transition 表示展开状态
- 选中态 class:`selectedKeys.includes(node.key) ? 'bg-accent text-accent-foreground' : ''`
- 悬停态 class:`hover:bg-accent/50`(非选中时)
- 键盘导航:外层容器 @keydown 处理 ArrowUp/Down(focus 相邻节点)、ArrowRight(展开)/ArrowLeft(折叠)、Enter(选中)
- 递归渲染:CollapsibleContent 内 `<FmsTreeNode>` 自引用 v-for children
**FmsTree.vue(根组件)**:
- effectiveTreeData computed:包裹虚拟根节点 `[{ key: rootKey, title: rootTitle, children: treeData }]`
- expandedKeys 首次为空时自动添加 rootKey
- 传递给 FmsTreeNode 时 depth 从 0 开始(虚拟根 level 由 showRoot 控制是否显示)
- 透传 #title slot 到 FmsTreeNode
### 与旧系统差异
| 旧系统 FmsTree | 新系统 FmsTree |
| --- | --- |
| 基于 antdv `<a-tree>` | 基于 reka-ui Collapsible + ContextMenu |
| 透传 a-tree attrs | 仅暴露明确设计的 props |
| select 行为修正用 selectSuppress 标志 | CollapsibleRoot v-model:open 受控,无副作用 |
| 虚拟滚动 + 拖拽互斥 | 暂不实现 |
| localizeNodes 递归翻译 | 本次不做 |
### 性能考虑
- Vue 3 v-for diff 已做优化,递归组件用显式 key 确保 DOM 复用
- expandedKeys / selectedKeys 通过 props 受控,避免内外状态不同步
- 不实现虚拟滚动(当前无海量数据场景)
### 日志与错误处理
- TreeNode.key 缺失时 console.warn 提示
- contextMenu prop 未传入时节点不响应右键(不报错)
## 设计风格
遵循 shadcn-vue new-york 风格:简洁、中性、语义化。树组件作为列表型控件,采用极简设计。
## 节点行样式
- 高度 32px,flex items-center,rounded-sm
- 默认:transparent 背景,text-foreground
- 悬停:bg-accent/50 背景
- 选中:bg-accent text-accent-foreground
- 缩进:每层级 padding-left: 24px
- 展开箭头:ChevronRight 图标,展开时 rotate-90,transition-transform duration-200
- 节点间无 gap,紧密排列
## 右键菜单
复用项目已有 ContextMenu 组件样式:bg-popover、border、shadow-md、rounded-md,菜单项 h-8 px-2 text-sm
## 交互
- 点击箭头:仅展开/折叠,不选中
- 点击行:选中节点
- 右键行:弹出 ContextMenu
- 键盘:ArrowUp/Down 切换焦点,ArrowRight/Left 展开/折叠,Enter 选中
## Agent Extensions
### Skill
- **shadcn-vue**
- 目的:验证 Collapsible 和 ContextMenu 组件的 reka-ui API(v-model:open、Trigger/Content/Root 用法),确保封装正确
- 预期结果:确认 CollapsibleRoot 的 open/defaultOpen props、ContextMenuRoot 的 modal 等关键 API 用法