Vue.js 中动态构建多层级 Select 下拉菜单
在后台管理系统、商品分类选择等场景中,我们经常需要用到多层级的下拉选择功能,比如省份-城市-区县的三级联动,或者商品的一级分类-二级分类-三级分类选择。如果层级固定且数量少,我们可以手动编写多个并列的 <select> 标签,但如果层级不固定、数据是从后端动态获取的,就需要用更灵活的方式动态构建组件。
接下来我们就一步步实现这个功能,核心思路是:先定义好层级数据,根据当前选中的值动态渲染对应层级的 <select> 下拉框,每层级的选择变化都会触发下一级的更新,同时清理下一级之后的选中状态。
一、准备基础数据
首先我们需要准备多层级的结构化数据,通常后端返回的数据会是树形结构,每个节点包含自身的 id、名称,以及子节点列表。我们这里先模拟一份简单的商品分类数据:
// 模拟多层级分类数据
const categoryData = [
{
id: 1,
name: '数码产品',
children: [
{
id: 11,
name: '手机',
children: [
{ id: 111, name: '苹果', children: [] },
{ id: 112, name: '华为', children: [] },
{ id: 113, name: '小米', children: [] }
]
},
{
id: 12,
name: '电脑',
children: [
{ id: 121, name: '笔记本', children: [] },
{ id: 122, name: '台式机', children: [] }
]
}
]
},
{
id: 2,
name: '服装服饰',
children: [
{
id: 21,
name: '男装',
children: [
{ id: 211, name: '外套', children: [] },
{ id: 212, name: '裤子', children: [] }
]
},
{
id: 22,
name: '女装',
children: [
{ id: 221, name: '连衣裙', children: [] },
{ id: 222, name: '半身裙', children: [] }
]
}
]
}
];二、Vue 组件核心逻辑实现
我们的组件需要满足几个核心需求:
- 根据选中的路径动态渲染对应层级的 <select> 下拉框
- 每一层级的选项由上一层级的选中值决定
- 选中某一层级后,自动清空该层级之后所有层级的选中状态
- 支持获取最终选中的完整路径和最后一级的 id
下面是完整的 Vue 单文件组件实现代码:
<template>
<div class="multi-level-select">
<!-- 遍历当前需要展示的所有层级 -->
<div v-for="(level, index) in levelList" :key="index" class="select-item">
<select v-model="selectedValues[index]" @change="handleSelectChange(index)">
<option value="">请选择{{ levelNames[index] || `第${index + 1}级` }}</option>
<option
v-for="item in level.options"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</option>
</select>
</div>
<div class="selected-result" v-if="selectedValues.length > 0">
<p>当前选中路径:{{ selectedPath.join(' / ') }}</p>
<p>最后一级ID:{{ lastSelectedId }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'MultiLevelSelect',
props: {
// 树形结构的数据源
treeData: {
type: Array,
default: () => []
},
// 自定义层级名称,比如['一级分类', '二级分类', '三级分类']
levelNames: {
type: Array,
default: () => []
}
},
data() {
return {
// 存储每一层级选中的值,索引对应层级
selectedValues: [],
// 存储每一层级的选项列表
levelOptions: []
};
},
computed: {
// 计算当前需要展示的层级列表,包含每一层的选项
levelList() {
const list = [];
// 第一层级的选项始终是根节点的子节点
let currentOptions = this.treeData;
for (let i = 0; i <= this.selectedValues.length; i++) {
// 如果当前层级有选中值,就找下一级的选项
if (i < this.selectedValues.length && this.selectedValues[i]) {
const selectedId = this.selectedValues[i];
const selectedNode = currentOptions.find(item => item.id === selectedId);
list.push({
level: i,
options: currentOptions
});
// 更新下一级的选项为当前选中节点的子节点
currentOptions = selectedNode ? selectedNode.children : [];
} else {
// 没有选中值的话,只展示当前层级的选项,后续层级不再展示
list.push({
level: i,
options: currentOptions
});
break;
}
}
return list;
},
// 计算选中的路径名称
selectedPath() {
const path = [];
let currentNodes = this.treeData;
for (let i = 0; i < this.selectedValues.length; i++) {
const selectedId = this.selectedValues[i];
if (!selectedId) break;
const node = currentNodes.find(item => item.id === selectedId);
if (node) {
path.push(node.name);
currentNodes = node.children || [];
} else {
break;
}
}
return path;
},
// 最后一级选中的ID
lastSelectedId() {
const validValues = this.selectedValues.filter(item => item);
return validValues.length > 0 ? validValues[validValues.length - 1] : '';
}
},
methods: {
// 处理某一层级的选择变化
handleSelectChange(changeLevel) {
// 清空变化层级之后所有层级的选中值
this.selectedValues = this.selectedValues.slice(0, changeLevel + 1);
// 触发父组件的事件,传递选中的完整信息
this.$emit('change', {
path: this.selectedPath,
lastId: this.lastSelectedId,
selectedValues: [...this.selectedValues]
});
}
},
watch: {
// 监听数据源变化,重置选中状态
treeData: {
handler() {
this.selectedValues = [];
},
deep: true
}
}
};
</script>
<style scoped>
.multi-level-select {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.select-item select {
padding: 6px 12px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
min-width: 140px;
height: 36px;
}
.selected-result {
margin-top: 16px;
padding: 12px;
background-color: #f5f7fa;
border-radius: 4px;
font-size: 14px;
color: #606266;
}
.selected-result p {
margin: 4px 0;
}
</style>三、组件使用方式
在父组件中引入并注册该组件,传入对应的数据源即可使用,示例代码如下:
<template>
<div id="app">
<h2>商品分类选择</h2>
<multi-level-select
:tree-data="categoryData"
:level-names="['一级分类', '二级分类', '三级分类']"
@change="handleCategoryChange"
/>
</div>
</template>
<script>
import MultiLevelSelect from './MultiLevelSelect.vue';
// 引入之前模拟的categoryData
import { categoryData } from './mockData.js';
export default {
name: 'App',
components: {
MultiLevelSelect
},
data() {
return {
categoryData
};
},
methods: {
handleCategoryChange(selectedInfo) {
console.log('选中信息:', selectedInfo);
// 可以在这里处理选中后的逻辑,比如提交表单、请求下级数据等
}
}
};
</script>四、实现说明
1. 组件的层级渲染逻辑通过 levelList 计算属性实现,它会根据当前 selectedValues 中存储的选中值,动态计算每一层需要展示的选项列表,不需要手动维护多个 <select> 标签。
2. 当某一层级的选择发生变化时,handleSelectChange 方法会截断 selectedValues 数组,只保留到变化层级及之前的选中值,这样就会自动清空后续所有层级的选中状态,同时触发重新计算 levelList,实现下一级选项的更新。
3. 如果需要支持默认选中值,只需要在组件初始化时给 selectedValues 赋值对应的 id 数组即可,比如默认选中“数码产品-手机-华为”,可以设置 selectedValues: [1, 11, 112]。
4. 如果后端返回的数据不是标准的树形结构,比如是扁平化的列表,每个节点包含 pid 字段表示父级 id,也可以先在前端做数据转换,把扁平数据转成树形结构再传入组件,不会影响组件的核心逻辑。
五、适用场景
这个组件可以适配大部分多层级下拉选择的场景,比如:
- 地区选择:省份-城市-区县-街道四级联动
- 商品分类选择:不限层级深度的分类选择
- 组织架构选择:公司-部门-小组的多层级选择
- 菜单权限选择:多级菜单的勾选场景
如果需要根据实际业务调整样式或者增加功能,比如禁用某一层级、支持搜索选项等,都可以在现有组件的基础上扩展,核心的动态构建逻辑不需要做大的改动。