news 2026/4/25 9:46:47

Vue3+Vant4实战:手把手教你封装一个带搜索和全选的移动端树形选择器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3+Vant4实战:手把手教你封装一个带搜索和全选的移动端树形选择器

Vue3+Vant4实战:构建企业级移动端树形选择组件

在移动端H5开发中,组织架构选择、多级分类筛选等场景对交互体验提出了极高要求。传统的下拉选择器难以应对复杂层级数据的展示与操作,这正是我们需要构建一个功能完备的树形选择组件的原因。本文将带你从零开始,基于Vue3的组合式API和Vant4的移动端组件库,打造一个支持多选/单选关键词搜索父子联动全选功能的企业级树形选择器。

1. 组件架构设计与基础搭建

1.1 技术选型与设计思路

我们选择Vue3+Vant4的组合主要基于以下考量:

  • Vant4:专为移动端优化的UI组件库,提供Popup、Field、Checkbox等高质量基础组件
  • Vue3 Composition API:更好的逻辑复用和组织,特别适合复杂交互组件
  • TypeScript支持:Vue3原生TS支持为组件提供类型安全

组件核心功能模块划分:

├── TreeSelect (主入口) │ ├── SearchBar (搜索模块) │ ├── Tree (递归树形结构) │ ├── ControlBar (全选/状态切换) │ └── ConfirmButton (确认操作)

1.2 基础组件结构搭建

首先创建主组件框架,使用Vant的Popup作为容器:

<template> <van-field v-model="displayText" is-link readonly :placeholder="placeholder" @click="showPopup" /> <van-popup v-model:show="isVisible" position="bottom" round :style="{ height: '70vh' }" > <div class="tree-select-container"> <!-- 搜索区域 --> <van-search v-model="searchKeyword" placeholder="请输入关键词搜索" @search="handleSearch" /> <!-- 控制栏 --> <div class="control-bar"> <van-checkbox v-if="multiple" v-model="isAllSelected" @click="toggleSelectAll" > 全选 </van-checkbox> </div> <!-- 树形内容 --> <div class="tree-wrapper"> <Tree ref="treeRef" :nodes="filteredData" :multiple="multiple" @change="handleSelectionChange" /> </div> <!-- 确认按钮 --> <van-button v-if="multiple" type="primary" block @click="confirmSelection" > 确定 </van-button> </div> </van-popup> </template>

2. 递归树形组件的实现

2.1 树节点数据结构设计

良好的数据结构是树形组件的基础,我们采用以下格式:

interface TreeNode { id: string | number label: string children?: TreeNode[] parentId?: string | number | null checked?: boolean expanded?: boolean visible?: boolean disabled?: boolean }

2.2 递归组件核心实现

创建Tree.vue组件处理递归渲染:

<template> <div class="tree-node" v-for="node in visibleNodes" :key="node.id"> <div class="node-content" :style="{ paddingLeft: `${depth * 20}px` }"> <!-- 选择控件 --> <van-checkbox v-if="multiple" v-model="node.checked" @click.stop="toggleCheck(node)" /> <van-radio v-else :name="node.id" :model-value="selectedId" @click.stop="selectNode(node)" /> <!-- 节点标签 --> <span class="node-label">{{ node.label }}</span> <!-- 展开/收起图标 --> <van-icon v-if="hasChildren(node)" :name="node.expanded ? 'arrow-up' : 'arrow-down'" @click.stop="toggleExpand(node)" /> </div> <!-- 递归子节点 --> <Tree v-if="node.expanded && hasChildren(node)" :nodes="node.children" :depth="depth + 1" :multiple="multiple" @change="$emit('change', $event)" /> </div> </template> <script setup lang="ts"> const props = defineProps({ nodes: { type: Array as PropType<TreeNode[]>, required: true }, depth: { type: Number, default: 0 }, multiple: { type: Boolean, default: true }, selectedId: { type: [String, Number], default: null } }) const emit = defineEmits(['change']) const hasChildren = (node: TreeNode) => { return node.children && node.children.length > 0 } const toggleExpand = (node: TreeNode) => { node.expanded = !node.expanded } const toggleCheck = (node: TreeNode) => { node.checked = !node.checked // 处理父子联动逻辑 updateChildNodes(node, node.checked) updateParentNodes(node) emitSelectionChange() } const emitSelectionChange = () => { const selectedNodes = flattenTree(props.nodes) .filter(node => node.checked) emit('change', selectedNodes) } </script>

3. 核心功能实现细节

3.1 父子联动选择逻辑

实现父子节点间的联动选择是树形组件的关键:

// 更新所有子节点的选中状态 const updateChildNodes = (node: TreeNode, checked: boolean) => { if (node.children) { node.children.forEach(child => { child.checked = checked updateChildNodes(child, checked) }) } } // 更新父节点的选中状态 const updateParentNodes = (node: TreeNode) => { if (!node.parentId) return const parent = findNodeById(props.nodes, node.parentId) if (!parent) return const allChildrenChecked = parent.children?.every(child => child.checked) const someChildrenChecked = parent.children?.some(child => child.checked) parent.checked = allChildrenChecked ? true : someChildrenChecked ? null // 半选状态 : false updateParentNodes(parent) } // 辅助函数:根据ID查找节点 const findNodeById = (nodes: TreeNode[], id: string | number): TreeNode | null => { for (const node of nodes) { if (node.id === id) return node if (node.children) { const found = findNodeById(node.children, id) if (found) return found } } return null }

3.2 关键词搜索与筛选

实现高效的树形搜索需要考虑性能和平滑体验:

const searchKeyword = ref('') const searchTimeout = ref<NodeJS.Timeout>() const handleSearch = () => { clearTimeout(searchTimeout.value) searchTimeout.value = setTimeout(() => { filterTreeNodes() }, 300) } const filterTreeNodes = () => { if (!searchKeyword.value) { resetNodeVisibility(props.nodes) return } const keyword = searchKeyword.value.toLowerCase() // 先隐藏所有节点 setAllNodesVisibility(props.nodes, false) // 显示匹配节点及其祖先 props.nodes.forEach(node => { if (node.label.toLowerCase().includes(keyword)) { showNodeAndAncestors(node) } if (node.children) { searchInChildren(node.children, keyword) } }) } const showNodeAndAncestors = (node: TreeNode) => { node.visible = true if (node.parentId) { const parent = findNodeById(props.nodes, node.parentId) if (parent) { parent.expanded = true showNodeAndAncestors(parent) } } } const searchInChildren = (nodes: TreeNode[], keyword: string) => { nodes.forEach(node => { if (node.label.toLowerCase().includes(keyword)) { showNodeAndAncestors(node) } if (node.children) { searchInChildren(node.children, keyword) } }) }

4. 性能优化与体验提升

4.1 大数据量优化策略

当处理大型组织架构时,我们需要特别关注性能:

虚拟滚动实现方案

<template> <RecycleScroller class="tree-scroller" :items="flattenedVisibleNodes" :item-size="50" key-field="id" > <template #default="{ item }"> <TreeNode :node="item" :depth="item.depth" @toggle="handleToggle" /> </template> </RecycleScroller> </template> <script setup> import { RecycleScroller } from 'vue-virtual-scroller' import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' const flattenedVisibleNodes = computed(() => { const result: Array<TreeNode & { depth: number }> = [] flattenVisibleNodes(props.nodes, result, 0) return result }) const flattenVisibleNodes = ( nodes: TreeNode[], result: Array<TreeNode & { depth: number }>, depth: number ) => { nodes.forEach(node => { if (node.visible !== false) { result.push({ ...node, depth }) if (node.expanded && node.children) { flattenVisibleNodes(node.children, result, depth + 1) } } }) } </script>

4.2 动画与交互优化

提升移动端体验的关键细节:

/* 平滑展开动画 */ .tree-node { transition: all 0.3s ease; .node-content { display: flex; align-items: center; padding: 12px 16px; .node-label { flex: 1; margin: 0 12px; transition: color 0.2s; } &:active { background-color: #f5f5f5; } } } /* 半选状态样式 */ .van-checkbox--indeterminate .van-checkbox__icon { background-color: var(--van-primary-color); border-color: var(--van-primary-color); &::before { content: ''; width: 50%; height: 2px; background-color: white; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); } }

5. 完整组件集成与API设计

5.1 组件Props与Events设计

完善的API设计让组件更易用:

interface TreeSelectProps { modelValue: Array<string | number> // 选中的节点ID treeData: TreeNode[] // 树形数据 placeholder?: string multiple?: boolean // 是否多选 searchable?: boolean // 是否可搜索 checkStrictly?: boolean // 是否严格模式(不联动) expandAll?: boolean // 默认展开所有节点 showCheckbox?: boolean // 是否显示复选框 defaultExpandLevel?: number // 默认展开层级 } interface TreeSelectEmits { (e: 'update:modelValue', value: Array<string | number>): void (e: 'change', nodes: TreeNode[]): void (e: 'search', keyword: string): void (e: 'expand-change', node: TreeNode, isExpanded: boolean): void }

5.2 组件方法与使用示例

暴露实用方法并展示典型用法:

<script setup> const treeSelectRef = ref() // 暴露的方法 defineExpose({ expandAll: () => treeSelectRef.value.expandAll(), collapseAll: () => treeSelectRef.value.collapseAll(), getSelectedNodes: () => treeSelectRef.value.getSelectedNodes(), filter: (keyword: string) => treeSelectRef.value.filter(keyword) }) // 使用示例 const selectedDepartments = ref([]) const departmentTree = ref([]) fetch('/api/departments').then(res => { departmentTree.value = res.data }) </script> <template> <TreeSelect v-model="selectedDepartments" :tree-data="departmentTree" placeholder="请选择部门" multiple searchable @change="handleDepartmentChange" /> </template>

在实际项目中使用时,我们还需要考虑与后端API的对接、错误处理、空状态展示等细节。一个健壮的树形选择组件应该能够处理各种边界情况,如异步加载子节点、节点禁用状态、自定义节点渲染等需求。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 9:43:36

EfficientNetV2深度解析:从渐进式训练到Fused-MBConv的架构革新

1. EfficientNetV2的诞生背景与核心目标 2019年EfficientNetV1的问世让业界看到了复合缩放&#xff08;Compound Scaling&#xff09;的威力——通过统一缩放网络深度、宽度和分辨率三个维度&#xff0c;用更少的参数实现了更高的准确率。但当我们真正把V1模型部署到生产环境时…

作者头像 李华
网站建设 2026/4/25 9:40:19

LFM2.5-1.2B-Instruct效果展示:LNG接收站操作规程问答准确性

LFM2.5-1.2B-Instruct效果展示&#xff1a;LNG接收站操作规程问答准确性 1. 模型能力概览 LFM2.5-1.2B-Instruct是一个1.2B参数量的轻量级指令微调大语言模型&#xff0c;专为边缘设备和低资源服务器设计。这个模型在保持较小体积的同时&#xff0c;展现出令人印象深刻的专业…

作者头像 李华