Tree

View source
基于 Nuxt UI Tree 的树形组件,补齐搜索、懒加载、工具栏、复选框与父子策略。

简介

MTree 在 Nuxt UI Tree 之上做薄壳封装,透传其全部 props、事件与插槽,并补齐多项增强能力:搜索过滤与高亮、异步懒加载、工具栏(展开/折叠切换、三态全选)、复选框多选与父子策略(级联 / 互不关联)、键绑定(v-model:selectedKeys)以及命令式 API 与选中分类。树形数据的归一化、过滤、遍历等运算复用 @movk/coreTree 工具方法。

基于 Nuxt UI 的 Tree 组件构建,原生 props 与插槽完全透传

用法

传入 items 渲染层级结构,节点 defaultExpanded 控制初始展开,v-model 绑定选中节点:

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultExpanded: true,
    children: [
      { label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
      { label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
    ]
  },
  {
    label: 'composables',
    icon: 'i-lucide-folder',
    children: [
      { label: 'useAuth.ts', icon: 'i-vscode-icons-file-type-typescript' },
      { label: 'useUser.ts', icon: 'i-vscode-icons-file-type-typescript' }
    ]
  }
]

const selected = ref<TreeItem>()
</script>

<template>
  <MTree v-model="selected" :items="items" />
</template>

defaultExpanded 默认展开

defaultExpanded 作用:以策略推导初始展开的父节点,缺省回退节点上的 defaultExpanded 标记。传 true 展开全部父级、传 number 仅展开 depth 小于该值的父级、传函数按节点与深度自定义:

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

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

<template>
  <MTree :items="items" :default-expanded="1" />
</template>

searchable 搜索过滤

searchable 作用:在顶部渲染搜索框,按关键字剪枝并保留命中节点的祖先链;highlight 默认开启,高亮命中文本,命中后自动展开。filter 可自定义匹配谓词,search 支持 v-model:search 双向绑定关键字:

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

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

<template>
  <MTree :items="items" searchable />
</template>

checkable 复选框级联

checkable 作用:在节点前渲染复选框,内部启用 multiple 与父子级联、子级半选冒泡,v-model 收集选中节点数组:

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: '技术中心',
    defaultExpanded: true,
    children: [
      { label: '前端组', children: [{ label: '组件库' }, { label: '可视化' }] },
      { label: '后端组' }
    ]
  },
  { label: '产品中心', children: [{ label: '交互设计' }] }
]

const checked = ref<TreeItem[]>([])
</script>

<template>
  <MTree v-model="checked" :items="items" checkable />
</template>
checkable 等价于 multiple + strategy(默认 cascade)的语法糖。仅需多选而不渲染复选框时改用 multiple

multiple 多选

multiple 作用:开启多选但不渲染复选框,点击节点累加选中,v-model 收集选中节点数组:

已选:无

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: '技术中心',
    defaultExpanded: true,
    children: [
      { label: '前端组', children: [{ label: '组件库' }, { label: '可视化' }] },
      { label: '后端组' }
    ]
  },
  { label: '产品中心', children: [{ label: '交互设计' }] }
]

const selected = ref<TreeItem[]>([])
</script>

<template>
  <div class="space-y-3">
    <MTree v-model="selected" :items="items" multiple />
    <p class="text-sm text-muted">
      已选:{{ selected.map(node => node.label).join('、') || '无' }}
    </p>
  </div>
</template>

strategy 父子策略

strategy 作用:控制多选 / checkable 下的父子勾选关系。cascade(默认)父子级联且子级全选时回填父级,isolated 父子互不关联、半选不冒泡:

cascade(默认)

isolated

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: '技术中心',
    defaultExpanded: true,
    children: [
      { label: '前端组', children: [{ label: '组件库' }, { label: '可视化' }] },
      { label: '后端组', children: [{ label: '网关' }, { label: '存储' }] }
    ]
  }
]

const cascade = ref<TreeItem[]>([])
const isolated = ref<TreeItem[]>([])
</script>

<template>
  <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
    <div class="space-y-2">
      <p class="text-sm font-medium">
        cascade(默认)
      </p>
      <MTree v-model="cascade" :items="items" checkable />
    </div>
    <div class="space-y-2">
      <p class="text-sm font-medium">
        isolated
      </p>
      <MTree v-model="isolated" :items="items" checkable strategy="isolated" />
    </div>
  </div>
</template>

selectedKeys 键绑定

selectedKeys 作用:以节点 key 数组双向绑定选中,适合从后端回显或与路由同步。v-model:selectedKeysv-model 互通,键由 getKey / labelKey 派生:

selectedKeys:app.vue

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultExpanded: true,
    children: [
      { label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
      { label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
    ]
  }
]

const selectedKeys = ref<string[]>(['app.vue'])
</script>

<template>
  <div class="space-y-3">
    <MTree v-model:selected-keys="selectedKeys" :items="items" checkable />
    <p class="text-sm text-muted">
      selectedKeys:{{ selectedKeys.join('、') || '无' }}
    </p>
  </div>
</template>

toolbar 工具栏

toolbar 作用:渲染顶部工具栏,提供展开 / 折叠切换按钮;searchable 时内嵌可清除的搜索框,checkable 时附带三态全选复选框与选中计数:

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: '技术中心',
    children: [
      { label: '前端组', children: [{ label: '组件库' }, { label: '可视化' }] },
      { label: '后端组', children: [{ label: '网关' }, { label: '存储' }] }
    ]
  },
  { label: '产品中心', children: [{ label: '交互设计' }, { label: '用户研究' }] }
]

const checked = ref<TreeItem[]>([])
</script>

<template>
  <MTree v-model="checked" :items="items" toolbar searchable checkable />
</template>
工具栏的全选计数按叶子计:级联下选中父级会带上子级 key,按叶子统计可避免重复计数。

lazy 异步懒加载

lazy 作用:配合 loadChildren,展开未加载的父节点时拉取子节点并显示加载态;节点 isLeaf 标记为叶子,不渲染展开占位:

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  { label: '区域 A' },
  { label: '区域 B' },
  { label: '直辖节点', isLeaf: true }
]

let seq = 0
function loadChildren(node: TreeItem): Promise<TreeItem[]> {
  return new Promise(resolve => setTimeout(() => {
    seq += 1
    resolve([
      { label: `${node.label} / 子节点 ${seq}-1` },
      { label: `${node.label} / 子节点 ${seq}-2`, isLeaf: true }
    ])
  }, 800))
}
</script>

<template>
  <MTree :items="items" lazy :load-children="loadChildren" />
</template>

childrenKey 字段映射

childrenKey 作用:将后端的子节点字段归一化为 childrenlabelKey 指定展示字段,无需预先改造数据结构:

<script setup lang="ts">
const items = [
  {
    name: '技术中心',
    nodes: [
      { name: '前端组', nodes: [{ name: '组件库' }, { name: '可视化' }] },
      { name: '后端组' }
    ]
  },
  { name: '产品中心', nodes: [{ name: '交互设计' }] }
]
</script>

<template>
  <MTree :items="items" children-key="nodes" label-key="name" />
</template>

virtualize 虚拟滚动

virtualize 作用:透传 Nuxt UI Tree 的虚拟化能力,仅渲染可视区节点,适配大数据量树:

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = Array.from({ length: 60 }, (_, group) => ({
  label: `分组 ${group + 1}`,
  defaultExpanded: group === 0,
  children: Array.from({ length: 30 }, (_, leaf) => ({ label: `节点 ${group + 1}-${leaf + 1}` }))
}))
</script>

<template>
  <MTree :items="items" :virtualize="true" class="max-h-72 w-md" />
</template>

示例

自定义节点

通过透传的 item-trailing 等插槽自定义节点内容,未覆盖的插槽仍由 Nuxt UI Tree 默认渲染:

<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: 'app',
    icon: 'i-lucide-folder',
    defaultExpanded: true,
    children: [
      { label: 'app.vue', icon: 'i-vscode-icons-file-type-vue' },
      { label: 'nuxt.config.ts', icon: 'i-vscode-icons-file-type-nuxt' }
    ]
  }
]
</script>

<template>
  <MTree :items="items">
    <template #item-trailing="{ item }">
      <UBadge v-if="!item.children" label="file" color="neutral" variant="subtle" size="sm" />
    </template>
  </MTree>
</template>

自定义工具栏

toolbar-leading / toolbar-trailing 在默认工具栏首尾追加内容;需要完全接管时改用 #toolbar 插槽,其作用域暴露 toggleExpandselectAllclearselectionSummary 等方法与状态:

部门
0 项
<script setup lang="ts">
import type { TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: '技术中心',
    children: [
      { label: '前端组', children: [{ label: '组件库' }, { label: '可视化' }] },
      { label: '后端组', children: [{ label: '网关' }, { label: '存储' }] }
    ]
  },
  { label: '产品中心', children: [{ label: '交互设计' }, { label: '用户研究' }] }
]

const checked = ref<TreeItem[]>([])
</script>

<template>
  <MTree v-model="checked" :items="items" toolbar searchable checkable>
    <template #toolbar-leading>
      <UBadge label="部门" color="neutral" variant="subtle" size="sm" />
    </template>
    <template #toolbar-trailing>
      <UBadge :label="`${checked.length} 项`" color="primary" variant="subtle" size="sm" />
    </template>
  </MTree>
</template>

命令式控制

通过 useTemplateRef 拿到组件实例,调用 expandToDepthcollapseAllselectAllclearSelection 等方法控制树:

<script setup lang="ts">
import type { TreeExposed, TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: '技术中心',
    children: [
      { label: '前端组', children: [{ label: '组件库' }, { label: '可视化' }] },
      { label: '后端组', children: [{ label: '网关' }, { label: '存储' }] }
    ]
  },
  { label: '产品中心', children: [{ label: '交互设计' }, { label: '用户研究' }] }
]

const checked = ref<TreeItem[]>([])
const tree = useTemplateRef<TreeExposed>('tree')
</script>

<template>
  <div class="space-y-3">
    <div class="flex flex-wrap gap-2">
      <UButton size="xs" label="展开到第 2 层" @click="tree?.expandToDepth(2)" />
      <UButton size="xs" label="收起全部" color="neutral" variant="subtle" @click="tree?.collapseAll()" />
      <UButton size="xs" label="全选" @click="tree?.selectAll()" />
      <UButton size="xs" label="清空" color="neutral" variant="subtle" @click="tree?.clearSelection()" />
    </div>
    <MTree ref="tree" v-model="checked" :items="items" checkable />
  </div>
</template>

选中结果分类

实例的 treeSelection 反应式回传选中分类:leaves(选中叶子)、parents(满选父级)、halfSelected(半选父级)、strictlyChecked(剔除随父级联的子节点):

叶子(leaves):无
满选父级(parents):无
半选父级(halfSelected):无
<script setup lang="ts">
import type { TreeExposed, TreeItem } from '@movk/nuxt'

const items: TreeItem[] = [
  {
    label: '技术中心',
    defaultExpanded: true,
    children: [
      { label: '前端组', children: [{ label: '组件库' }, { label: '可视化' }] },
      { label: '后端组', children: [{ label: '网关' }, { label: '存储' }] }
    ]
  }
]

const checked = ref<TreeItem[]>([])
const tree = useTemplateRef<TreeExposed>('tree')

const labels = (nodes?: TreeItem[]) => (nodes ?? []).map(node => node.label).join('') || ''
</script>

<template>
  <div class="space-y-3">
    <MTree ref="tree" v-model="checked" :items="items" checkable />
    <dl class="text-sm text-muted space-y-1">
      <div>叶子(leaves):{{ labels(tree?.treeSelection.leaves) }}</div>
      <div>满选父级(parents):{{ labels(tree?.treeSelection.parents) }}</div>
      <div>半选父级(halfSelected):{{ labels(tree?.treeSelection.halfSelected) }}</div>
    </dl>
  </div>
</template>

API

Props

Prop Default Type
as'ul'any

The element or component this component should render as.

itemsT

树数据源,按 childrenKey 解析层级

childrenKey'children'string

取子节点数组的字段名,归一化为 UTree 的 children

labelKey'label'keyof Extract<NestedItem<T>, object> & string | DotPathKeys<Extract<NestedItem<T>, object>>

展示字段名

getKey(val: T[number]) => string

自定义节点 key,缺省取 labelKey 字段值

defaultExpandednumber | false | true | (node: T[number], depth: number): boolean

初始展开策略,缺省回退节点上的 defaultExpanded 标记

  • true 展开全部父级
  • number 展开 depth 小于该值的父级
  • 函数 按节点与深度自定义
selectedKeysstring[]

选中节点 key 列表,可用 v-model:selectedKeys 双向绑定

multipleM

开启多选

strategy'cascade'"cascade" | "isolated"

多选 / checkable 下的父子勾选策略

  • 'cascade' 父子级联(propagateSelect + bubbleSelect)
  • 'isolated' 父子互不关联
size'md'"md" | "xs" | "sm" | "lg" | "xl"

尺寸

color'primary'"primary" | "secondary" | "info" | "success" | "warning" | "error" | "important" | "neutral"

主色

filterTreeFilter<T[number]>

自定义匹配谓词,缺省按 labelKey 文本不区分大小写包含匹配

loadChildrenTreeLoadChildren<T[number]>

懒加载回调,展开未加载的父节点时调用

checkableM

渲染复选框并启用多选(multiple + strategy,默认 cascade)

trailingIconappConfig.ui.icons.chevronDownany

The icon displayed on the right side of a parent node.

expandedIconappConfig.ui.icons.folderOpenany

The icon displayed when a parent node is expanded.

collapsedIconappConfig.ui.icons.folderany

The icon displayed when a parent node is collapsed.

modelValueM extends true ? T[number][] : T[number]

The controlled value of the Tree. Can be bind as v-model.

defaultValueM extends true ? T[number][] : T[number]

The value of the Tree when initially rendered. Use when you do not need to control the state of the Tree.

virtualizefalseboolean | { overscan?: number; estimateSize?: number | ((index: number) => number); }

Enable virtualization for large lists. Note: when enabled, the tree structure is flattened like if nested was set to false.

onSelect(e: SelectEvent<T[number]>, item: T[number]) => void
onToggle(e: ToggleEvent<T[number]>, item: T[number]) => void
expanded[]string[]

The controlled value of the expanded item. Can be binded with v-model.

selectionBehavior"replace" | "toggle"

How multiple selection should behave in the collection.

search''string
propagateSelectboolean

选中父节点时级联选中子节点。cascade 策略下默认开启,显式 true 可在 isolated 下强制开启;关闭级联请用 strategy='isolated'

bubbleSelectboolean

子节点全部选中时回填父节点。cascade 策略下默认开启,显式 true 可在 isolated 下强制开启;关闭级联请用 strategy='isolated'

searchableboolean

开启顶部搜索过滤

highlighttrueboolean

高亮命中文本,仅在 searchable 时生效

lazyboolean

开启异步懒加载子节点

toolbarboolean

开启顶部工具栏(展开/折叠,checkable 时附带全选/清空)

nestedtrueboolean

Use nested DOM structure (children inside parents) vs flattened structure (all items at same level). When virtualize is enabled, this is automatically set to false.

disabledboolean

When true, prevents the user from interacting with tree

uiRecord<string, ClassNameValue> & { root?: SlotClass; item?: SlotClass; listWithChildren?: SlotClass; itemWithChildren?: SlotClass; link?: SlotClass; linkLeadingIcon?: SlotClass; linkLabel?: SlotClass; linkTrailing?: SlotClass; linkTrailingIcon?: SlotClass; container?: SlotClass; toolbar?: SlotClass; toolbarButton?: SlotClass; search?: SlotClass; checkbox?: SlotClass; highlight?: SlotClass; loading?: SlotClass; loadingIcon?: SlotClass; empty?: SlotClass; }

Emits

Event Type
update:modelValue[val: M extends true ? T[number][] : T[number]]
update:expanded[val: string[]]
update:search[value: string]
change[payload: { value: M extends true ? T[number][] : T[number]; keys: string[]; selection: TreeSelectionResult<T[number]>; }]
update:selectedKeys[value: string[]]
除透传 Nuxt UI Tree 的 update:modelValueupdate:expanded 外,MTree 额外提供:
  • update:search:搜索关键字变化时触发,支持 v-model:search
  • update:selectedKeys:选中 key 列表变化时触发,支持 v-model:selectedKeys
  • change:选中变化时触发,载荷为 { value, keys, selection }keysgetKey/labelKey 派生,selection 为选中结果分类。

Slots

Slot Type
item-wrapper{ item: T[number]; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean; handleSelect: () => void; handleToggle: () => void; ui: { root: (props?: Record<string, any>) => string; item: (props?: Record<string, any>) => string; listWithChildren: (props?: Record<string, any>) => string; itemWithChildren: (props?: Record<string, any>) => string; link: (props?: Record<string, any>) => string; linkLeadingIcon: (props?: Record<string, any>) => string; linkLabel: (props?: Record<string, any>) => string; linkTrailing: (props?: Record<string, any>) => string; linkTrailingIcon: (props?: Record<string, any>) => string; }; }
item{ item: T[number]; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean; handleSelect: () => void; handleToggle: () => void; ui: { root: (props?: Record<string, any>) => string; item: (props?: Record<string, any>) => string; listWithChildren: (props?: Record<string, any>) => string; itemWithChildren: (props?: Record<string, any>) => string; link: (props?: Record<string, any>) => string; linkLeadingIcon: (props?: Record<string, any>) => string; linkLabel: (props?: Record<string, any>) => string; linkTrailing: (props?: Record<string, any>) => string; linkTrailingIcon: (props?: Record<string, any>) => string; }; }
item-leading{ item: T[number]; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean; handleSelect: () => void; handleToggle: () => void; ui: { root: (props?: Record<string, any>) => string; item: (props?: Record<string, any>) => string; listWithChildren: (props?: Record<string, any>) => string; itemWithChildren: (props?: Record<string, any>) => string; link: (props?: Record<string, any>) => string; linkLeadingIcon: (props?: Record<string, any>) => string; linkLabel: (props?: Record<string, any>) => string; linkTrailing: (props?: Record<string, any>) => string; linkTrailingIcon: (props?: Record<string, any>) => string; }; }
item-label{ item: T[number]; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean; handleSelect: () => void; handleToggle: () => void; ui: { root: (props?: Record<string, any>) => string; item: (props?: Record<string, any>) => string; listWithChildren: (props?: Record<string, any>) => string; itemWithChildren: (props?: Record<string, any>) => string; link: (props?: Record<string, any>) => string; linkLeadingIcon: (props?: Record<string, any>) => string; linkLabel: (props?: Record<string, any>) => string; linkTrailing: (props?: Record<string, any>) => string; linkTrailingIcon: (props?: Record<string, any>) => string; }; }
item-trailing{ item: T[number]; index: number; level: number; expanded: boolean; selected: boolean; indeterminate: boolean; handleSelect: () => void; handleToggle: () => void; ui: { root: (props?: Record<string, any>) => string; item: (props?: Record<string, any>) => string; listWithChildren: (props?: Record<string, any>) => string; itemWithChildren: (props?: Record<string, any>) => string; link: (props?: Record<string, any>) => string; linkLeadingIcon: (props?: Record<string, any>) => string; linkLabel: (props?: Record<string, any>) => string; linkTrailing: (props?: Record<string, any>) => string; linkTrailingIcon: (props?: Record<string, any>) => string; }; }
toolbar{ expandAll: () => void; collapseAll: () => void; toggleExpand: () => void; allExpanded: boolean; selectAll: () => void; clear: () => void; search: string; selectionSummary: TreeSelectionSummary; }
toolbar-leadingany

默认工具栏起始处追加内容

toolbar-trailingany

默认工具栏末尾追加内容

emptyany
loading{ node: T[number]; }

Expose

您可以通过 useTemplateRef 访问该类型化组件实例。

NameType
expandAll()void

展开全部可展开节点

collapseAll()void

收起全部节点

expandToDepth(depth)void

展开到指定层级,depth=0 收起全部

selectAll()void

选中全部可选节点

clearSelection()void

清空选中

treeSelectionTreeSelectionResult

当前选中结果分类(selected / leaves / parents / halfSelected / strictlyChecked

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    tree: {
      slots: {
        container: 'flex flex-col gap-2 min-h-0',
        toolbar: 'flex items-center gap-1.5',
        toolbarButton: 'shrink-0',
        search: 'flex-1 min-w-0',
        checkbox: 'shrink-0 me-1.5',
        highlight: 'rounded-[2px] bg-primary/15 text-primary',
        loading: 'flex items-center gap-1.5 text-sm text-muted',
        loadingIcon: 'size-4 shrink-0 animate-spin',
        empty: 'py-6 text-center text-sm text-muted'
      }
    }
  }
})

Changelog

No recent changes
Copyright © 2025 - 2026 YiXuan - MIT License