Files
workspace/code/fms/.codebuddy/plans/fms-tree-component_16ac753e(未完成).md
T
2026-08-10 22:34:33 +08:00

265 lines
10 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 + Tailwind v4 + Vue 3 Composition API)从零封装 FmsTree 组件,参考旧系统 FmsTree 的核心能力(虚拟根节点、右键菜单、i18n、展开/选择),并确保与模块管理页面等场景兼容。
design:
architecture:
framework: vue
component: shadcn
styleKeywords:
- shadcn-vue
- new-york
- Minimalism
- Clean
- Semantic Design
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: add-t-function
content: 在 stores/app.ts 中添加 t() 翻译函数,支持从 DB 的 b_i18n 表获取翻译文本
status: pending
- id: create-use-tree-i18n
content: 创建 composables/useTreeI18n.ts,提供递归树节点翻译能力
status: pending
dependencies:
- add-t-function
- id: create-fms-tree-node
content: 创建 components/FmsTreeNode.vue 递归节点组件,基于 Collapsible + ContextMenu 实现展开折叠、选中高亮、右键菜单、键盘导航
status: pending
- id: create-fms-tree
content: 创建 components/FmsTree.vue 根组件,实现虚拟根节点包裹、props 定义、v-model 双向绑定、事件转发和 slot 透传
status: pending
dependencies:
- create-fms-tree-node
- create-use-tree-i18n
- id: verify-and-format
content: 验证组件在模块管理页面中可正常渲染,运行 pnpm fmt:check 检查格式
status: pending
dependencies:
- create-fms-tree
---
## 用户需求
在新系统 fms-vue 中封装一个可复用的 FmsTree 树组件,替代旧系统 fms-vue2 中的 FmsTree.vue。新项目使用 shadcn-vue (reka-ui + Tailwind v4) 技术栈,不能使用 antdv-next。
## 产品概述
FmsTree 是一个通用的树形展示组件,为模块管理、数据分类、文件目录等场景提供统一的树交互体验。
## 核心功能
- **树形结构展示**:递归渲染树节点,支持无限层级嵌套
- **展开/折叠**:点击箭头图标展开或折叠子节点,支持 v-model:expandedKeys 受控模式
- **节点选中**:点击节点高亮选中,支持 v-model:selectedKeys 受控模式,支持单选/多选
- **虚拟根节点**:自动在 treeData 外层包裹虚拟根节点(如 全部分类),确保首次加载时树处于展开可交互状态
- **右键菜单**:通过 contextMenu prop 传入菜单生成函数,右键节点弹出自定义菜单项
- **标题插槽**:支持 #title slot 自定义节点标题渲染
- ~~i18n 翻译(本次不做,后续按需添加)~~
- **键盘导航**:支持上下箭头切换节点、左右箭头展开/折叠、Enter 选中
- **样式定制**:遵循 shadcn-vue 语义颜色体系,支持 Tailwind class 透传
## 技术栈
- **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 等)
- **@lucide/vue**:ChevronRight 图标(展开箭头)
- **clsx + tailwind-merge**:cn() 工具函数
- **@vueuse/core**:useDebounceFn(可选,用于搜索防抖场景)
## 实现方案
### 核心策略
由于 reka-ui 没有 Tree/TreeView 原语,采用 **Collapsible 递归嵌套** 的方式自建树结构。每个树节点如果包含 children,则用它自己的 `CollapsibleRoot` 包裹;叶子节点则为普通 div。这样天然获得展开/折叠动画和 ARIA 属性。
右键菜单通过 reka-ui 的 `ContextMenuRoot` + `ContextMenuTrigger` 包裹每个节点行实现,无需全局 Menu 管理器。
虚拟根节点在组件内部用 `computed` 生成,确保外部调用者只需传入原始 treeData。
### 架构设计
```
FmsTree.vue (根容器)
├── 虚拟根节点 computed (包裹 treeData,defaultOpen)
│ └── FmsTreeNode.vue (递归节点,v-for children)
│ ├── CollapsibleRoot (当 node.children 存在时)
│ │ ├── ContextMenuRoot (当 contextMenu prop 存在时)
│ │ │ ├── ContextMenuTrigger
│ │ │ │ └── 节点行 (缩进 + 箭头 + title slot)
│ │ │ └── ContextMenuContent → ContextMenuItem v-for
│ │ └── CollapsibleContent
│ │ └── FmsTreeNode (递归)
│ └── 无 children 时:纯节点行 (带 ContextMenu)
└── 透传 slots (#title, #icon 等)
```
### 组件文件拆分
```
src/
├── components/
│ ├── FmsTree.vue # [NEW] 根组件:props 定义、虚拟根节点、右键菜单 orchestration、事件转发
│ └── FmsTreeNode.vue # [NEW] 递归节点:Collapsible 逻辑、缩进计算、选中高亮、键盘导航
├── composables/
│ └── useTreeI18n.ts # [NEW] 树节点 i18n 翻译 composable
└── stores/
└── app.ts # [MODIFY] 添加 t() 翻译函数
```
### 数据流
```
外部调用者
│ treeData (TreeNode[]) + expandedKeys + selectedKeys
▼
FmsTree.vue
│ computed: effectiveTreeData (包裹虚拟根节点)
│ computed: localizedData (i18n 翻译)
▼
FmsTreeNode.vue (递归)
│ CollapsibleRoot.open = expandedKeys.includes(node.key)
│ 节点行 class = selectedKeys.includes(node.key) ? 'bg-accent' : ''
│ @click → emit('select', keys, node)
│ @rightclick → ContextMenu 弹出
▼
事件向上冒泡 → FmsTree emit select / menuClick / update:expandedKeys / update:selectedKeys
```
## 实现细节
### Props 设计
```ts
// FmsTree.vue props
interface FmsTreeProps {
treeData: TreeNode[] // 原始树数据
expandedKeys?: string[] // 受控展开 keys
selectedKeys?: string[] // 受控选中 keys
multiple?: boolean // 是否多选,默认 false
contextMenu?: (node: TreeNode) => { items: ContextMenuItem[], onClick?: (payload: { key: string, node: TreeNode }) => void }
rootKey?: string // 虚拟根节点 key,默认 '__fms_root__'
rootTitle?: string // 虚拟根节点标题,默认 '全部分类'
showRoot?: boolean // 是否显示虚拟根节点,默认 true
indent?: number // 每层缩进像素,默认 24
}
```
### 事件设计
```ts
// FmsTree.vue emits
emit('update:expandedKeys', keys: string[])
emit('update:selectedKeys', keys: string[])
emit('select', keys: string[], info: { node: TreeNode, selected: boolean })
emit('menuClick', payload: { key: string, node: TreeNode })
```
### TreeNode 数据类型
```ts
interface TreeNode {
key: string // 唯一标识(bigint id 序列化为 string)
title: string // 显示标题
children?: TreeNode[] // 子节点
b_id?: string // 业务 ID
b_name?: string // 业务名称
b_code?: string // 业务编码
b_i18n?: string // 国际化 key
b_module_type?: string // 模块类型: directory | page | data
[key: string]: unknown // 其他业务字段透传
}
```
### 性能考虑
- 递归节点组件使用 `v-for` 渲染子节点,Vue 3 的 diff 算法已做优化
- `expandedKeys` 和 `selectedKeys` 通过 props 受控,避免组件内部状态与外部不同步
- i18n 翻译仅在节点 title 通过 `b_i18n` key 查找时触发,不影响渲染性能
- 不实现虚拟滚动(项目当前无海量树数据场景,如有需要后续可基于 ScrollArea 和 `@tanstack/virtual` 添加)
- 避免不必要的 re-render:FmsTreeNode 使用 `props.key` 作为显式 key,确保 Vue 复用节点 DOM
### 与旧系统的差异
| 旧系统 FmsTree | 新系统 FmsTree |
| --- | --- |
| 基于 antdv `<a-tree>` | 基于 reka-ui Collapsible + ContextMenu |
| 透传 a-tree attrs | 仅暴露明确设计的 props(不盲目透传) |
| select 行为修正用 selectSuppress 标志 | 用 CollapsibleRoot 的 v-model:open 受控,消除副作用 |
| 虚拟滚动 + 拖拽互斥逻辑 | 暂不实现,按需后续添加 |
| localizeNodes 递归翻译 | 在 composable useTreeI18n 中处理 |
### 日志和错误处理
- 组件为纯 UI 组件,不产生日志
- TreeNode.key 缺失时给出 console.warn 提示
- 右键菜单 contextMenu prop 未传入时,节点不响应右键(不报错)
### 向后兼容
- `FmsTree.vue` 是新文件,不影响现有代码
- `app.ts` 的 t() 函数为增量添加,现有功能不变
## 设计风格
遵循 shadcn-vue new-york 风格:简洁、中性、语义化。树组件作为列表型控件,采用极简设计,无多余装饰。
## 页面布局(树组件内)
每个树节点行从左侧到右侧依次为:展开箭头(16px lucide 图标)→ 缩进空白(每层 24px)→ 标题内容。悬停时显示浅背景色,选中时显示 accent 背景色。
### 节点行样式
- 高度 32px,flex items-center,rounded-sm
- 默认:transparent 背景,text-foreground
- 悬停:bg-accent/50 背景(非选中态时)
- 选中:bg-accent text-accent-foreground 背景 + 文字色
- 缩进:每层级 padding-left: 24px(通过 style binding 计算)
- 展开箭头:ChevronRight 图标,展开时 rotate-90 transition-transform
- 间距:节点行之间无 gap,紧密排列
### 右键菜单样式
- 复用项目已有的 ContextMenu 组件样式(bg-popover、border、shadow-md、rounded-md)
- 菜单项:h-8、px-2、text-sm、gap-2,hover 时 bg-accent
### 交互
- 点击箭头:仅展开/折叠,不选中
- 点击行:选中节点
- 右键行:弹出 ContextMenu
- 键盘:ArrowUp/Down 切换焦点,ArrowRight/Left 展开/折叠,Enter 选中
## Agent Extensions
### Skill
- **shadcn-vue**
- 目的:验证 ContextMenu 和 Collapsible 组件的 API,确保封装正确使用 reka-ui 原语
- 预期结果:确认 ContextMenu 的 Root/Trigger/Content/Item 用法、Collapsible 的 Root/Trigger/Content 用法