Vant3 是 Vue3 生态里使用率很高的移动端组件库,其中 TreeSelect 组件专门解决多级分类选择场景。它把一级分类放在左侧导航栏,二级及更深层级的子分类显示在右侧内容区,交互直观,适合电商、内容发布等需要选择分类的业务。不过这个组件的文档相对精简,很多细节需要结合源码和实际项目经验才能掌握。下面先把基础用法和容易被忽略的数据结构讲明白。

基础用法与数据格式
要使用 Vant3 的 TreeSelect,首先在项目中安装 Vant3 并引入组件与样式。Vue3 的组合式 API 下,通常通过 import { TreeSelect } from 'vant'; 注册局部组件。核心属性有三个:items 用来传入分类树数据,active-id 控制右侧选中的分类 id,main-active-index 控制左侧高亮的一级分类索引。
items 是一个数组,每个一级分类对象需要包含 text 字段作为显示文字,以及 children 数组存放子分类。子分类对象则通常需要 text 和 id,因为选中态依赖 id 来匹配。如果希望支持三级分类,可以在子分类里继续嵌套 children,组件会递归渲染。这里有一个常见的误解:一级分类可以不写 id,但如果写了,也要保证不重复。右侧子分类的 id 必须全局唯一,否则点击一个分类时会出现多个高亮。
<template>
<van-tree-select
:items="items"
:active-id="activeId"
:main-active-index="mainActiveIndex"
@click-item="onClickItem"
@click-nav="onClickNav"
/>
</template>
<script setup>
import { ref } from 'vue';
import { TreeSelect as VanTreeSelect } from 'vant';
import 'vant/lib/index.css';
const items = ref([
{
text: '服装',
children: [
{ text: '男装', id: 1 },
{ text: '女装', id: 2 },
],
},
{
text: '数码',
children: [
{ text: '手机', id: 3 },
{ text: '电脑', id: 4 },
],
},
]);
const activeId = ref(1);
const mainActiveIndex = ref(0);
function onClickItem(data) {
activeId.value = data.id;
}
function onClickNav(index) {
mainActiveIndex.value = index;
}
</script>
上面这段代码展示了最基本的两级分类选择。点击左侧导航时,click-nav 回调会收到当前索引,把它赋给 mainActiveIndex 就能切换高亮。点击右侧子项时,click-item 回调的参数是整个节点对象,因此不仅拿到 id,还能拿到 text 等其他字段。如果业务需要多选,可以给组件传入 max 属性,并把 active-id 改成数组类型,此时回调里需要手动维护数组的增删逻辑。
另外,Vant3 的 TreeSelect 默认高度会根据内容撑开,但移动端屏幕有限,建议在容器外层或样式里限制高度,让左右两侧可以独立滚动。官方默认样式已经做了基本处理,但如果内容区子项很多,需要适当调整 padding 和高度。
异步加载分类数据与动态更新
真实项目里,分类数据几乎都来自后端接口,很少写死在组件里。接口返回的数据结构不一定和 TreeSelect 要求的 text、children 字段一致,所以拿到数据后要先做一次转换。比较常见的情况是后端返回扁平列表,每个节点带有 id、name 和 parentId,前端需要根据 parentId 构建成树。下面是一个通用转换函数,可以放在 onMounted 中调用。
import { ref, onMounted } from 'vue';
const items = ref([]);
const activeId = ref(0);
const mainActiveIndex = ref(0);
async function loadCategories() {
const res = await fetch('/api/categories');
const flatList = await res.json();
// 假设后端返回:{ id, name, parentId, sort }
const map = new Map();
const tree = [];
flatList.forEach(item => {
map.set(item.id, { ...item, text: item.name, children: [] });
});
flatList.forEach(item => {
const node = map.get(item.id);
if (item.parentId === 0) {
tree.push(node);
} else {
const parent = map.get(item.parentId);
if (parent) parent.children.push(node);
}
});
items.value = tree;
// 设置默认选中第一个一级分类的第一个子项
if (tree.length && tree[0].children) {
mainActiveIndex.value = 0;
activeId.value = tree[0].children[0]?.id ?? 0;
}
}
onMounted(loadCategories);
这个转换过程有两个关键点:一是确保每个节点都有 children 数组,即使为空也不会导致组件渲染异常;二是注意 id 的类型。Vant3 的 TreeSelect 在比较选中态时使用严格相等,如果后端返回字符串 '1',而 activeId 初始值是数字 1,就会出现点击后不高亮的问题。最稳妥的做法是在转换时统一使用 Number(item.id) 或保持接口返回类型一致。
当分类数据发生变化,例如用户切换了业务线,items 被重新赋值后,组件不会自动帮你重置选中状态。如果没有手动处理,可能出现右侧高亮了一个不存在的 id,或者左侧导航停留在旧索引。可以通过 watch 监听 items 变化,重置 activeId 和 mainActiveIndex。如果是一次性加载的场景,也可以直接在数据赋值后手动设置一次默认值。
import { watch } from 'vue';
watch(items, (newItems) => {
if (newItems.length && newItems[0].children) {
mainActiveIndex.value = 0;
activeId.value = newItems[0].children[0]?.id ?? 0;
}
});
还有一点需要留意,Vant3 的 TreeSelect 没有内置懒加载能力,也就是说所有层级需要一次性传给组件。如果分类树非常庞大,建议后端提供按需加载接口,由前端控制只请求当前展示层级的子节点,或者把不常用的三级分类收进弹层,避免首屏渲染压力过大。
常见问题解答与避坑指南
实际开发中,围绕 TreeSelect 的问题大多集中在数据格式、事件绑定、搜索过滤和性能优化这几个方向。下面把高频问题拆开讲,并给出对应的解决方案。
问题一:分类数据不显示或右侧空白。 这种情况大概率是 items 结构不符合要求。一级节点必须包含 text 字段,而且 children 必须是数组。如果后端给的是 name 而不是 text,需要先做字段映射,不能直接把原始数据塞进去。另一个隐蔽的原因是父级节点的 children 为 undefined,组件内部尝试遍历时可能报错或渲染空白。建议在转换时统一补齐 children: []。
问题二:选中值不更新或回显失败。 先检查 active-id 是否绑定了响应式变量,以及点击回调里有没有更新它。然后是类型问题,前面已经提到严格相等比较,数字和字符串混用是常见坑。如果业务要求回显历史选择,例如编辑页面初始加载时,要把后端保存的 id 赋给 activeId,同时根据该 id 找到对应的父级节点索引,设置 mainActiveIndex,否则右侧虽然高亮了,左侧导航还停留在第一项。
问题三:需要搜索分类,但组件没有搜索框。 TreeSelect 本身只负责展示和选择,搜索功能需要额外实现。通常是在顶部放一个 van-search 组件,监听输入关键字,对分类树做递归过滤。过滤逻辑要保证保留匹配项的父级链路,否则会出现子项虽然匹配但父级被过滤掉,导致左侧导航缺失。下面是一个过滤函数示例:
function filterTree(tree, keyword) {
if (!keyword) return tree;
return tree.reduce((result, node) => {
const textMatch = node.text.includes(keyword);
const filteredChildren = node.children ? filterTree(node.children, keyword) : [];
if (textMatch || filteredChildren.length) {
result.push({ ...node, children: filteredChildren.length ? filteredChildren : node.children });
}
return result;
}, []);
}
使用时用计算属性包装一下,把过滤结果传给 items。注意搜索期间选中状态可能会因为节点被过滤掉而失效,可以在清除搜索后重新设置默认值。
问题四:大量节点渲染卡顿。 如果一级分类有几十个,每个下面又有几百个子项,首屏渲染会明显变慢。Vant3 的 TreeSelect 没有虚拟滚动,所以需要从数据源头上控制规模。可以把二级分类做成分页加载,或者后端只返回当前一级分类下的子集,前端切换一级时再请求。另外尽量避免每次输入搜索都触发整棵树的 deep clone 或频繁 setData,使用 Object.freeze 处理不会变的数据也能降低响应式开销。
样式定制方面,Vant3 通过 CSS 变量统一管理主题。例如想改变选中项颜色和导航高度,可以在全局样式或组件作用域样式里写:
.van-tree-select {
--van-tree-select-item-active-color: #1989fa;
--van-tree-select-nav-height: 44px;
}
如果需要调整内容区的滚动行为,可以给 .van-tree-select__content 设置 max-height 和 overflow-y: auto。不过更推荐使用 Vant 提供的 height 属性或外层容器控制,避免破坏组件内部布局。
总体来看,Vant3 TreeSelect 是一个易用但细节较多的组件。遇到问题时,先确认数据格式是否符合 text 和 children 的要求,再排查 id 类型和响应式更新,最后才考虑样式和性能。按照这个顺序,大部分异常都能快速定位。
Vant3 TreeSelect分类选择常见问题修改时间:2026-09-28 04:05:03