Lzh on GitHub

Tree

树形视图小部件显示项目的层次列表,可以展开或折叠以显示或隐藏其子项目,例如在文件系统导航器中。
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { TreeItem, TreeRoot } from 'reka-ui'

const items = [
  {
    title: 'composables',
    icon: 'lucide:folder',
    children: [
      { title: 'useAuth.ts', icon: 'vscode-icons:file-type-typescript' },
      { title: 'useUser.ts', icon: 'vscode-icons:file-type-typescript' },
    ],
  },
  {
    title: 'components',
    icon: 'lucide:folder',
    children: [
      {
        title: 'Home',
        icon: 'lucide:folder',
        children: [
          { title: 'Card.vue', icon: 'vscode-icons:file-type-vue' },
          { title: 'Button.vue', icon: 'vscode-icons:file-type-vue' },
        ],
      },
    ],
  },
  { title: 'app.vue', icon: 'vscode-icons:file-type-vue' },
  { title: 'nuxt.config.ts', icon: 'vscode-icons:file-type-nuxt' },
]
</script>

<template>
  <TreeRoot
    v-slot="{ flattenItems }"
    class="list-none select-none w-56 bg-white text-stone-700 rounded-lg border shadow-sm p-2 text-sm font-medium"
    :items="items"
    :get-key="(item) => item.title"
    :default-expanded="['components']"
  >
    <h2 class="font-semibold text-sm text-stone-400 px-2 pt-1 pb-3">
      Directory Structure
    </h2>
    <TreeItem
      v-for="item in flattenItems"
      v-slot="{ isExpanded }"
      :key="item._id"
      :style="{ 'padding-left': `${item.level - 0.5}rem` }"
      v-bind="item.bind"
      class="flex items-center py-1 px-2 my-0.5 rounded outline-none focus:ring-grass8 focus:ring-2 data-[selected]:bg-grass4"
    >
      <template v-if="item.hasChildren">
        <Icon
          v-if="!isExpanded"
          icon="lucide:folder"
          class="h-4 w-4"
        />
        <Icon
          v-else
          icon="lucide:folder-open"
          class="h-4 w-4"
        />
      </template>
      <Icon
        v-else
        :icon="item.value.icon || 'lucide:file'"
        class="h-4 w-4"
      />
      <div class="pl-2">
        {{ item.value.title }}
      </div>
    </TreeItem>
  </TreeRoot>
</template>
  • 数据项完全自定义,不受约束,但默认可能需要包含 children 子节点。
  • 树中子项的缩进通过 level 缩进层级和样式来实现

功能特点

  • 可控或不可控。
  • 焦点完全管理。
  • 完整的键盘导航。
  • 支持从右到左的方向。
  • 支持多选。
  • 不同的选择行为。

安装

从命令行安装组件。

$ npm add reka-ui

结构

导入所有部分并将其组合在一起。

<script setup>
  import { TreeItem, TreeRoot, TreeVirtualizer } from 'reka-ui'
</script>

<template>
  <TreeRoot>
    <TreeItem />

    <!-- or with virtual -->
    <TreeVirtualizer>
      <TreeItem />
    </TreeVirtualizer>
  </TreeRoot>
</template>

API 参考

Root

包含树的所有部分。

Prop默认值类型描述
as'ul'AsTag | Component此组件应渲染为的元素或组件。可以通过 asChild 覆盖。
asChildfalseboolean将默认渲染的元素更改为作为子元素传递的元素,合并它们的 props 和行为。有关详细信息,请阅读我们的组合指南
bubbleSelectfalseboolean当为 true 时,选择子节点将更新父节点状态。
defaultExpanded[]string[]树首次渲染时展开的值。当您不需要控制展开树的状态时使用。
defaultValueundefinedRecord<string, any> | Record<string, any>[]树首次渲染时的值。当您不需要控制树的状态时使用。
dirundefined'ltr' | 'rtl'列表框(如果适用)的阅读方向。如果省略,则从 ConfigProvider 全局继承或假定为从左到右 (LTR) 阅读模式。
disabledfalseboolean当为 true 时,阻止用户与树交互。
expanded[]string[]展开项的受控值。可以与 v-model:expanded 绑定。
getChildrenval.children((val: Record<string, any>) => Record<string, any>[])此函数将传递每个项目的索引,并应返回该项目的子项列表。
getKey*((val: Record<string, any>): string)此函数将传递每个项目的索引,并应返回该项目的唯一键。
items[]Record<string, any>[]项目列表。
modelValueundefinedRecord<string, any> | Record<string, any>[]树的受控值。可以与 v-model 绑定。
multiplefalseboolean是否可以选择多个选项。
propagateSelectfalseboolean当为 true 时,选择父节点将选择后代。
selectionBehavior'toggle''toggle' | 'replace'集合中多选应如何表现。

触发事件 (Emit)

事件Payload描述
update:expanded[val: string[]]
update:modelValue[val: Record<string, any> | Record<string, any>[]]切换值更改时调用的事件处理程序。

默认插槽

插槽参数插槽参数类型
flattenItemsFlattenedItem<Record<string, any>>[]
modelValueRecord<string, any> | Record<string, any>[]
expandedstring[]

Item

Item 组件。

Prop默认值类型描述
as'li'AsTag | Component此组件应渲染为的元素或组件。可以通过 asChild 覆盖。
asChildboolean将默认渲染的元素更改为作为子元素传递的元素,合并它们的 props 和行为。有关详细信息,请阅读我们的组合指南
level*number深度层级。
value*Record<string, any>赋予此项目的值。

触发事件 (Emit)

事件Payload描述
select[event: SelectEvent<Record<string, any>>]选择项目时调用的事件处理程序。可以通过调用 event.preventDefault 来阻止。
toggle[event: ToggleEvent<Record<string, any>>]切换项目时调用的事件处理程序。可以通过调用 event.preventDefault 来阻止。

默认插槽

插槽参数插槽参数类型
isExpandedboolean
isSelectedboolean
isIndeterminateboolean | undefined
handleToggle(): void
handleSelect(): void

数据属性

属性
[data-indent]Number(树节点的深度层级)
[data-expanded]展开时存在
[data-selected]选中时存在

Virtualizer

用于实现列表虚拟化的虚拟容器。

Prop默认值类型描述
estimateSize-number每个项目的估计大小(像素)。
overscan-number在可见区域之外渲染的项目数量。
textContent-((item: Record<string, any>) => string)每个项目的文本内容,用于实现类型前瞻功能。

默认插槽

插槽参数插槽参数类型
itemFlattenedItem<Record<string, any>>
virtualizerVirtualizer<Element | Window, Element>
virtualItemVirtualItem

示例

选择多个项目

Tree 组件允许您选择多个项目。您可以通过提供一个值数组而不是单个值并设置 multiple="true" 来启用此功能。

<script setup lang="ts">
  import { TreeRoot } from 'reka-ui'
  import { ref } from 'vue'

  const people = [
    { id: 1, name: 'Durward Reynolds' },
    { id: 2, name: 'Kenton Towne' },
    { id: 3, name: 'Therese Wunsch' },
    { id: 4, name: 'Benedict Kessler' },
    { id: 5, name: 'Katelyn Rohan' },
  ]
  const selectedPeople = ref([people[0], people[1]])
</script>

<template>
  <TreeRoot
    v-model="selectedPeople"
    multiple
  >
    ...
  </TreeRoot>
</template>

虚拟列表

渲染长列表项可能会减慢应用程序速度,因此使用虚拟化将显著提高性能。

有关虚拟化的更多一般信息,请参阅虚拟化指南

<script setup lang="ts">
  import { TreeItem, TreeRoot, TreeVirtualizer } from 'reka-ui'
  import { ref } from 'vue'
</script>

<template>
  <TreeRoot :items>
    <TreeVirtualizer
      v-slot="{ item }"
      :text-content="(opt) => opt.name"
    >
      <TreeItem v-bind="item.bind">
        {{ person.name }}
      </TreeItem>
    </TreeVirtualizer>
  </TreeRoot>
</template>

带复选框

一些 Tree 组件可能希望显示 toggled/indeterminate 复选框。我们可以通过使用一些 props 和 preventDefault 事件来更改 Tree 组件的行为。

我们将 propagateSelect 设置为 true,因为我们希望父复选框选择/取消选择其后代。然后,我们添加一个触发 select 事件的复选框。

<script setup lang="ts">
  import { TreeItem, TreeRoot } from 'reka-ui'
  import { ref } from 'vue'
</script>

<template>
  <TreeRoot
    v-slot="{ flattenItems }"
    :items
    multiple
    propagate-select
  >
    <TreeItem
      v-for="item in flattenItems"
      :key="item._id"
      v-bind="item.bind"
      v-slot="{ handleSelect, isSelected, isIndeterminate }"
      @select="(event) => {
        if (event.detail.originalEvent.type === 'click')
          event.preventDefault()
      }"
      @toggle="(event) => {
        if (event.detail.originalEvent.type === 'keydown')
          event.preventDefault()
      }"
    >
      <Icon
        v-if="item.hasChildren"
        icon="radix-icons:chevron-down"
      />
      <button
        tabindex="-1"
        @click.stop
        @change="handleSelect"
      >
        <Icon
          v-if="isSelected"
          icon="radix-icons:check"
        />
        <Icon
          v-else-if="isIndeterminate"
          icon="radix-icons:dash"
        />
        <Icon
          v-else
          icon="radix-icons:box"
        />
      </button>
      <div class="pl-2">
        {{ item.value.title }}
      </div>
    </TreeItem>
  </TreeRoot>
</template>

嵌套树节点

默认示例显示扁平的树项和节点,这使得虚拟化和拖放等自定义功能更容易实现。但是,您也可以将其构建为具有嵌套 DOM 节点。

Tree.vue 中:

Tree.vue
<script setup lang="ts">
  import { TreeItem } from 'reka-ui'

  interface TreeNode {
    title: string
    icon: string
    children?: TreeNode[]
  }

  withDefaults(defineProps<{
    treeItems: TreeNode[]
    level?: number
  }>(), { level: 0 })
</script>

<template>
  <li
    v-for="tree in treeItems"
    :key="tree.title"
  >
    <TreeItem
      v-slot="{ isExpanded }"
      as-child
      :level="level"
      :value="tree"
    >
      <button></button>
      <ul v-if="isExpanded && tree.children">
        <!-- 递归调用组件 -->
        <Tree
          :tree-items="tree.children"
          :level="level + 1"
        />
      </ul>
    </TreeItem>
  </li>
</template>

CustomTree.vue 中:

CustomTree.vue
<template>
  <TreeRoot
    :items="items"
    :get-key="(item) => item.title"
  >
    <Tree :tree-items="items" />
  </TreeRoot>
</template>

自定义子节点 schema

默认情况下,<TreeRoot /> 要求您通过为每个节点传递 children 列表来提供节点子节点列表。您可以通过提供 getChildren prop 来覆盖此设置。

如果节点没有任何 children 节点,getChildren 应该返回 undefined 而不是空数组。
<script setup lang="ts">
  import { TreeRoot } from 'reka-ui'
  import { ref } from 'vue'

  interface FileNode {
    title: string
    icon: string
  }

  interface DirectoryNode {
    title: string
    icon: string
    directories?: DirectoryNode[]
    files?: FileNode[]
  }
</script>

<template>
  <TreeRoot
    :items="items"
    :get-key="(item) => item.title"
    :get-children="(item) => (!item.files) ? item.directories : (!item.directories) ? item.files : [...item.directories, ...item.files]"
  >
    ...
  </TreeRoot>
</template>

get-children prop 告诉 TreeRoot 组件 如何从一个父节点中获取其子节点列表。这是构建树形结构的关键。当 TreeRoot 组件需要渲染某个节点下的子节点时,它会调用这个函数,并传入该父节点的数据。

函数逻辑详解:

  • (item) => { ... }: 接收一个 item 参数,它可能是 DirectoryNodeFileNode
  • (!item.files) ? item.directories : ...: 如果 item 没有 files 属性(或者 item.filesnull/undefined),则返回 item.directories。这表示当前 item 是一个只包含子目录的目录。
  • (!item.directories) ? item.files : ...:
    • 这是上一个条件不满足时执行的(即 itemfiles 属性)。
    • 如果 item 没有 directories 属性,则返回 item.files。这表示当前 item 是一个只包含文件的目录。
  • ...item.directories, ...item.files:
    • 这是前面两个条件都不满足时执行的(即 item 同时有 filesdirectories 属性)。
    • 它使用展开运算符 (...) 将 item.directoriesitem.files 两个数组合并成一个新数组并返回。这表示当前 item 是一个既包含子目录又包含文件的目录,并且希望将它们都作为子节点渲染。

总结 get-children 作用:这个函数根据 DirectoryNode 的定义,灵活地告诉 TreeRoot 组件如何遍历并获取一个目录下的所有子目录和子文件,以构建完整的树形层级。

可拖拽/可排序树

对于更复杂的拖拽 Tree 组件,在此示例中,我们将使用 pragmatic-drag-and-drop 作为处理 dnd 的核心包。

Stackblitz 演示

可访问性

遵循 Tree WAI-ARIA 设计模式

键盘交互

按键描述
Enter当焦点位于 TreeItem 上时,选择聚焦的项目。
ArrowDown当焦点位于 TreeItem 上时,将焦点移动到下一个项目。
ArrowUp当焦点位于 TreeItem 上时,将焦点移动到上一个项目。
ArrowRight当焦点位于关闭的 TreeItem(节点)上时,打开节点但不移动焦点。当焦点位于打开的节点上时,将焦点移动到第一个子节点。当焦点位于末端节点上时,不执行任何操作。
ArrowLeft当焦点位于打开的 TreeItem(节点)上时,关闭节点。当焦点位于子节点上且该子节点是末端节点或关闭节点时,将焦点移动到其父节点。当焦点位于根节点上且该根节点是末端节点或关闭节点时,不执行任何操作。
Home PageUp将焦点移动到第一个 TreeItem
End PageDown将焦点移动到最后一个 TreeItem