Vue.js 实现多级联动下拉选择框
多级联动下拉选择框是前端开发中常见的交互组件,比如省市区选择、商品分类筛选等场景都会用到。它的核心逻辑是上级选项发生变化时,下级选项要根据上级选中的值动态更新,同时清空已经选中的下级内容。本文将介绍如何使用 Vue.js 实现一套通用的多级联动下拉选择框,支持任意层级的联动配置。
实现思路
要实现多级联动,首先需要明确几个核心点:
- 联动的层级和每一层的数据源需要可配置,避免硬编码导致组件无法复用
- 每一级的选中值需要实时响应,当上级值变化时,自动触发下级数据的更新
- 需要维护一个选中值的数组,数组长度和联动层级一致,方便后续获取完整的选中结果
- 初始化时需要加载第一级的数据,后续层级的数据根据上级选中值动态请求或读取本地数据
基础数据准备
这里以省市区三级联动为例,我们先准备基础的模拟数据。实际开发中可以将数据替换为接口请求返回的内容,本文为了方便演示,使用本地静态数据。
// 模拟省市区数据
const regionData = [
{
id: 1,
name: '北京市',
children: [
{
id: 11,
name: '北京市',
children: [
{ id: 111, name: '东城区' },
{ id: 112, name: '西城区' },
{ id: 113, name: '朝阳区' }
]
}
]
},
{
id: 2,
name: '广东省',
children: [
{
id: 21,
name: '广州市',
children: [
{ id: 211, name: '天河区' },
{ id: 212, name: '越秀区' },
{ id: 213, name: '白云区' }
]
},
{
id: 22,
name: '深圳市',
children: [
{ id: 221, name: '福田区' },
{ id: 222, name: '南山区' },
{ id: 223, name: '宝安区' }
]
}
]
}
];
// 联动配置项,每一级对应一个配置,label为下拉框的提示文本,data为当前层级的数据源
const levelConfig = [
{ label: '请选择省份', data: regionData },
{ label: '请选择城市', data: [] },
{ label: '请选择区域', data: [] }
];组件实现
接下来我们基于 Vue 3 的 Composition API 实现联动组件,组件支持传入自定义的数据和配置,能够适配不同的联动场景。
<template>
<div class="multi-level-linkage">
<div class="level-item" v-for="(level, index) in levelList" :key="index">
<label class="level-label">{{ level.label }}</label>
<select
class="level-select"
v-model="selectedValues[index]"
@change="handleLevelChange(index)"
:disabled="index > 0 && !selectedValues[index - 1]"
>
<option value="">{{ level.label }}</option>
<option
v-for="item in level.data"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</option>
</select>
</div>
<div class="result-area" v-if="selectedValues.filter(v => v).length === levelList.length">
当前选中结果:{{ selectedLabels.join(' / ') }}
</div>
</div>
</template>
<script setup>
import { ref, reactive, watch, onMounted } from 'vue';
// 定义组件入参,支持自定义层级配置和初始数据
const props = defineProps({
// 层级配置,格式为[{label: '提示文本', data: []}]
levelConfig: {
type: Array,
required: true
}
});
// 选中的值数组,长度和层级一致,初始为空
const selectedValues = ref(props.levelConfig.map(() => ''));
// 选中的文本标签数组,用于展示结果
const selectedLabels = ref([]);
// 处理后的层级列表,包含每一层的数据和配置
const levelList = reactive(props.levelConfig.map(item => ({ ...item })));
// 根据父级选中值获取下一级的数据
const getNextLevelData = (parentId, currentLevelIndex) => {
// 如果是第一级,直接返回初始配置的第一级数据
if (currentLevelIndex === 0) {
return props.levelConfig[0].data;
}
// 查找父级对应的数据项
let parentData = props.levelConfig[0].data;
for (let i = 1; i <= currentLevelIndex - 1; i++) {
const parentId = selectedValues.value[i - 1];
const target = parentData.find(item => item.id === parentId);
if (!target || !target.children) return [];
parentData = target.children;
}
const currentParent = parentData.find(item => item.id === parentId);
return currentParent ? (currentParent.children || []) : [];
};
// 处理层级变化事件
const handleLevelChange = (changedIndex) => {
// 清空当前变化层级之后的所有选中值
for (let i = changedIndex + 1; i < selectedValues.value.length; i++) {
selectedValues.value[i] = '';
levelList[i].data = [];
}
// 如果当前层级选中了值,加载下一级的数据
if (selectedValues.value[changedIndex]) {
const nextIndex = changedIndex + 1;
if (nextIndex < levelList.length) {
levelList[nextIndex].data = getNextLevelData(selectedValues.value[changedIndex], changedIndex);
}
}
// 更新选中的文本标签
updateSelectedLabels();
};
// 更新选中的文本标签
const updateSelectedLabels = () => {
selectedLabels.value = selectedValues.value.map((val, index) => {
if (!val) return '';
const target = levelList[index].data.find(item => item.id === val);
return target ? target.name : '';
}).filter(v => v);
};
// 初始化第一级数据
onMounted(() => {
levelList[0].data = props.levelConfig[0].data;
});
</script>
<style scoped>
.multi-level-linkage {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 400px;
padding: 20px;
border: 1px solid #e5e6eb;
border-radius: 8px;
}
.level-item {
display: flex;
align-items: center;
gap: 10px;
}
.level-label {
width: 100px;
text-align: right;
color: #333;
font-size: 14px;
}
.level-select {
flex: 1;
height: 36px;
padding: 0 10px;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 14px;
color: #606266;
outline: none;
}
.level-select:disabled {
background-color: #f5f7fa;
cursor: not-allowed;
}
.level-select:focus {
border-color: #409eff;
}
.result-area {
margin-top: 16px;
padding: 12px;
background-color: #f0f9eb;
border-radius: 4px;
color: #67c23a;
font-size: 14px;
}
</style>组件使用
完成组件开发后,使用方式非常简单,只需要传入对应的层级配置即可,以下是使用示例:
<template>
<div class="app-container">
<h3>省市区三级联动示例</h3>
<MultiLevelLinkage :level-config="levelConfig" />
</div>
</template>
<script setup>
import MultiLevelLinkage from './MultiLevelLinkage.vue';
import { reactive } from 'vue';
// 联动配置,和之前准备的数据对应
const levelConfig = reactive([
{ label: '请选择省份', data: regionData },
{ label: '请选择城市', data: [] },
{ label: '请选择区域', data: [] }
]);
// 引入之前定义的regionData
import { regionData } from './data.js';
</script>功能扩展
上述实现是基础版本,实际使用中可以根据需求扩展更多功能:
- 支持异步加载数据:将 getNextLevelData 方法改为接口请求,在请求完成后再赋值给下一级的 data,加载时可以添加 loading 状态提示
- 支持设置默认值:在组件初始化时,传入默认的选中值数组,自动触发各级数据加载,回显选中状态
- 支持任意层级:目前的实现支持任意长度的 levelConfig,不需要修改组件逻辑,只需要配置对应的层级即可
- 添加事件回调:当选中值变化时,通过 emit 向外抛出选中结果,方便父组件处理后续业务逻辑
通过 Vue 的响应式特性,我们只需要维护好选中值数组和每一级的数据源,就能很方便地实现多级联动效果,组件的可复用性也很高,适用于大部分需要联动选择的业务场景。