This commit is contained in:
oneao committed 2026-10-09 17:32:14 +08:00
1 parent e99a9fb274
commit 0be0b0767a
788 files changed
+112023 -14941

No files matched your search

@@ -0,0 +1,198 @@
---
title: Tree 树形控件
description: 层级数据展示与操作:选中、勾选联动、过滤、拖拽排序、懒加载、右键菜单
---
# Tree 树形控件
`Tree` 用于展示带层级的数据(组织架构、目录、菜单、权限),并支持在树上直接做操作。
## 何时使用
- 数据天然有父子关系(目录、分类、组织)时;
- 需要「勾选一批节点交给后端」时(权限分配、批量操作);
- 需要拖拽调整层级或顺序时;
- 只是展示扁平的一列选项时,用 [Select](/ui/form/select) 或 [Checkbox](/ui/form/checkbox) 更简单。
## 基础用法
`treeData` 是嵌套结构,默认按 `{ key, title, children }` 取值;选中值用 `v-model:selected-keys`。
<demo vue="tree/basic.vue" />
## 勾选与半选
`checkable` 开启复选框;**默认是父子联动**(选中父节点自动勾选全部子孙,部分选中时父节点显示半选)。`checkStrictly` 关闭联动,父子互不影响。
<demo vue="tree/checkable.vue" />
`check` 事件的载荷是 `{ checked, halfChecked }`,`checked` 已包含联动推导出的子孙节点,可以直接提交给后端。
## 展开状态
| 场景 | 用法 |
| --- | --- |
| 初始全展开 | `default-expand-all` |
| 初始展开指定项(并自动补全祖先) | `default-expanded-keys` + `default-expand-parent`(默认开) |
| 完全受控 | 传 `expanded-keys` + `v-model:expanded-keys` |
| 选中/勾选后自动展开祖先 | `auto-expand-parent` |
<demo vue="tree/expand.vue" />
## 过滤
`filter` 传关键词:命中的节点与它的**祖先链**会显示,命中文字高亮,命中路径自动展开(这个自动展开不会写进你的 `expandedKeys`)。
<demo vue="tree/filter.vue" />
`#title` 插槽存在时,内置高亮不生效——因为标题完全由你渲染。需要自带高亮时请在自己的插槽里处理。
## 懒加载
`lazy` 模式下,展开一个「没有 children 且未加载过」的节点会触发 `load`;你请求完把 children 写回数据,并把 key 记入 `loaded-keys`(组件据此停止显示加载图标、并判断是否还有子节点)。
<demo vue="tree/lazy.vue" />
## 拖拽排序
`draggable` 开启拖拽。落点按行的上 25% / 中 50% / 下 25% 分成 `before` / `inside` / `after`,你需要在 `drop` 事件里自己重排数据——组件**不会**直接修改 `tree-data`。
<demo vue="tree/draggable.vue" />
`externalDragKey` 可以把「树外拖入」的来源(如字段库)映射成拖拽键,从而让树内节点成为落点(用于「把字段拖进目录」这类场景)。
## 右键菜单
`contextMenu` 传数组或 `(node, key) => items`:节点上右键收到该节点,空白处右键收到 `null`。
<demo vue="tree/context-menu.vue" />
## 自定义字段与渲染
`fieldNames` 映射后端字段;节点自身的 `icon` / `disabled` / `disableCheckbox` / `draggable: false` / `isLeaf` 可以逐节点覆盖行为。
<demo vue="tree/custom.vue" />
## 键盘操作
树是标准的 `role="tree"`(节点 `role="treeitem"`),采用 roving tabindex:
| 按键 | 行为 |
| --- | --- |
| `↑` / `↓` | 在可见节点间移动(自动跳过被折叠的子树) |
| `→` | 展开当前节点;已展开则进入第一个子节点 |
| `←` | 收起当前节点;已收起则跳到父节点 |
| `Enter` | 选中当前节点 |
| `Space` | 勾选(无复选框时等同选中) |
## API
### Props
| 名称 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| `treeData` | `TreeNodeData[]` | `[]` | 树数据(嵌套结构) |
| `fieldNames` | `TreeFieldNames` | `{ key: 'key', title: 'title', children: 'children', icon: 'icon' }` | 字段名映射 |
| `checkable` | `boolean` | `false` | 显示复选框 |
| `selectable` | `boolean` | `true` | 是否允许选中(`false` 时行点击不选中) |
| `multiple` | `boolean` | `false` | 多选:`Ctrl/Cmd + 点击`切换、普通点击单选替换、再点唯一项取消 |
| `checkStrictly` | `boolean` | `false` | 勾选严格模式:父子不联动 |
| `disabled` | `boolean` | `false` | 整树禁用(展开 / 选中 / 勾选 / 拖拽 / 右键全部短路) |
| `lazy` | `boolean` | `false` | 异步加载模式;首次展开未加载节点时触发 `load` |
| `draggable` | `boolean` | `false` | 允许拖拽排序 |
| `externalDragKey` | `(dataTransfer, event) => TreeKey \| null` | — | 把树外拖入源映射成拖拽键,返回 `null` 表示不识别 |
| `filter` | `string` | `''` | 过滤关键词(匹配 title,大小写不敏感) |
| `blockNode` | `boolean` | `false` | 节点占满整行(便于显示 hover 底色) |
| `defaultExpandAll` | `boolean` | `false` | 初始展开全部(仅非受控初始化生效) |
| `defaultExpandParent` | `boolean` | `true` | 初始补全 `defaultExpandedKeys` 的祖先路径 |
| `autoExpandParent` | `boolean` | `false` | 选中 / 勾选后自动展开其祖先路径 |
| `contextMenu` | `TreeContextMenuItem[] \| ((node, key) => TreeContextMenuItem[])` | — | 右键菜单数据源(`node` 为 `null` 表示空白处) |
| `expandedKeys` | `TreeKey[]` | — | 受控展开键(**传了即受控**) |
| `selectedKeys` | `TreeKey[]` | — | 受控选中键 |
| `checkedKeys` | `TreeKey[] \| { checked: TreeKey[] }` | — | 受控勾选键(两种形态都接受) |
| `loadedKeys` | `TreeKey[]` | — | 受控已加载键(懒加载用) |
| `defaultExpandedKeys` | `TreeKey[]` | `[]` | 非受控初始展开键 |
| `defaultSelectedKeys` | `TreeKey[]` | `[]` | 非受控初始选中键 |
| `defaultCheckedKeys` | `TreeKey[]` | `[]` | 非受控初始勾选键 |
| `defaultLoadedKeys` | `TreeKey[]` | `[]` | 非受控初始已加载键 |
### Events
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| `select` | `(keys, { node, selectedKeys, nativeEvent })` | 选中项变化 |
| `check` | `({ checked, halfChecked }, { node, checked })` | 勾选变化。第二个参数的 `checked` 表示本次是「勾上」还是「取消」 |
| `expand` | `(expandedKeys, { node, expanded })` | 展开 / 收起 |
| `load` | `(node)` | 懒加载:首次展开未加载节点 |
| `drop` | `({ dragKey, dropKey, dropPosition, node, event })` | 拖拽落下。`dropPosition` 为 `'before' \| 'inside' \| 'after'` |
| `dragstart` / `dragend` | `({ node, event })` | 拖拽开始 / 结束 |
| `contextmenu` | `({ node, key, event })` | 右键(空白处 `node` 为 `null`) |
| `contextMenuClick` | `({ item, node, key, event })` | 点击右键菜单项 |
| `update:expandedKeys` | `(keys)` | 展开键变化(受控 / 非受控都会发出) |
| `update:selectedKeys` | `(keys)` | 选中键变化 |
| `update:checkedKeys` | `(keys)` | 勾选键变化 |
| `update:loadedKeys` | `(keys)` | 已加载键变化 |
### Slots
| 名称 | 参数 | 说明 |
| --- | --- | --- |
| `title` | `{ node, expanded, selected }` | 自定义节点标题(提供后内置高亮失效) |
| `empty` | — | 无数据时的内容,默认取语言包的「暂无数据」 |
### 类型定义
```ts
export type TreeKey = string | number
export type DropPosition = 'before' | 'inside' | 'after'
export interface TreeFieldNames {
key?: string
title?: string
children?: string
icon?: string
}
export interface TreeNodeData {
key?: TreeKey
title?: string
children?: TreeNodeData[]
icon?: Component | VNode
disabled?: boolean
disableCheckbox?: boolean
draggable?: boolean
isLeaf?: boolean
}
export interface TreeContextMenuItem {
key: string | number
label?: string
icon?: Component | VNode
disabled?: boolean
danger?: boolean
divider?: boolean
}
```
### 样式变量
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `--g3-tree-node-height` | `28px` | 节点行高 |
| `--g3-tree-node-gap` | `6px` | 相邻节点行之间的竖向间距 |
| `--g3-tree-indent` | `18px` | 每层缩进量 |
| `--g3-tree-icon-size` | `16px` | 图标尺寸 |
| `--g3-tree-switcher-size` | `22px` | 展开箭头点击区 |
| `--g3-tree-gutter-x` | `4px` | 节点行左右留白 |
| `--g3-tree-title-padding-x` | `6px` | 标题内边距 |
| `--g3-tree-drop-line-size` | `2px` | 拖拽落点指示线粗细 |
## 实现说明
- **勾选只存显式键**:非严格模式下 `checkedKeys` 保存的是「用户直接点过的节点」,实际的勾选与半选状态由 `conductCheck()` 从数据推导(父选中 ⇒ 子孙选中;子全选中 ⇒ 父选中;部分选中 ⇒ 父半选)。点击时还会把该节点的**祖先从显式集合里删掉**,否则「取消一个子节点」会被父节点残留的显式条目瞬间拉回勾选;
- **性能**:`buildFlat()` 一次 DFS 建好父键 / 层级 / 子键 / 根键四张索引表(`computed` 缓存,`treeData` 变才重算),勾选联动、过滤、拖拽判断都基于索引表,不重复递归;
- **过滤与展开分离**:过滤命中的自动展开只作用于「有效展开集合」,不写回用户的 `expandedKeys`——关闭过滤后树的展开状态回到用户原样;
- **键盘可见顺序**:`visibleKeys` 是按当前展开状态 DFS 出来的顺序(遇到折叠就截断),与过滤结果取交集,因此 `↑ ↓` 只在「用户真正看得到的节点」间移动;
- **拖拽**:`drop` 之前先快照落点位置再清理视觉状态(否则读到的永远是 `null`);内部拖拽会校验 `isDescendant`,不允许把节点拖进自己的子树;
- **层级缩进**:不是嵌套 padding,而是节点行内联 `--g3-tree-level` + CSS `calc(gutter + indent * level)`,拖拽指示线复用同一个计算,因此深层节点的落点线也能精确对齐;
- **右键菜单**:Teleport 到配置容器、按鼠标坐标定位并用 `computePosition` 做视口翻转,关闭时机是「文档 mousedown(捕获)+ Esc + 滚动 + resize」,组件卸载时统一清理。