--- title: Tree 树形控件 description: 层级数据展示与操作:选中、勾选联动、过滤、拖拽排序、懒加载、右键菜单 --- # Tree 树形控件 `Tree` 用于展示带层级的数据(组织架构、目录、菜单、权限),并支持在树上直接做操作。 ## 何时使用 - 数据天然有父子关系(目录、分类、组织)时; - 需要「勾选一批节点交给后端」时(权限分配、批量操作); - 需要拖拽调整层级或顺序时; - 只是展示扁平的一列选项时,用 [Select](/ui/form/select) 或 [Checkbox](/ui/form/checkbox) 更简单。 ## 基础用法 `treeData` 是嵌套结构,默认按 `{ key, title, children }` 取值;选中值用 `v-model:selected-keys`。 ## 勾选与半选 `checkable` 开启复选框;**默认是父子联动**(选中父节点自动勾选全部子孙,部分选中时父节点显示半选)。`checkStrictly` 关闭联动,父子互不影响。 `check` 事件的载荷是 `{ checked, halfChecked }`,`checked` 已包含联动推导出的子孙节点,可以直接提交给后端。 ## 展开状态 | 场景 | 用法 | | --- | --- | | 初始全展开 | `default-expand-all` | | 初始展开指定项(并自动补全祖先) | `default-expanded-keys` + `default-expand-parent`(默认开) | | 完全受控 | 传 `expanded-keys` + `v-model:expanded-keys` | | 选中/勾选后自动展开祖先 | `auto-expand-parent` | ## 过滤 `filter` 传关键词:命中的节点与它的**祖先链**会显示,命中文字高亮,命中路径自动展开(这个自动展开不会写进你的 `expandedKeys`)。 `#title` 插槽存在时,内置高亮不生效——因为标题完全由你渲染。需要自带高亮时请在自己的插槽里处理。 ## 懒加载 `lazy` 模式下,展开一个「没有 children 且未加载过」的节点会触发 `load`;你请求完把 children 写回数据,并把 key 记入 `loaded-keys`(组件据此停止显示加载图标、并判断是否还有子节点)。 ## 拖拽排序 `draggable` 开启拖拽。落点按行的上 25% / 中 50% / 下 25% 分成 `before` / `inside` / `after`,你需要在 `drop` 事件里自己重排数据——组件**不会**直接修改 `tree-data`。 `externalDragKey` 可以把「树外拖入」的来源(如字段库)映射成拖拽键,从而让树内节点成为落点(用于「把字段拖进目录」这类场景)。 ## 右键菜单 `contextMenu` 传数组或 `(node, key) => items`:节点上右键收到该节点,空白处右键收到 `null`。 ## 自定义字段与渲染 `fieldNames` 映射后端字段;节点自身的 `icon` / `disabled` / `disableCheckbox` / `draggable: false` / `isLeaf` 可以逐节点覆盖行为。 ## 键盘操作 树是标准的 `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」,组件卸载时统一清理。