文章目录
一、级联选择器
级联选择器是一种重要的用户界面组件,特别适用于处理具有层级关系的数据,如地区选择、分类选择等场景。通过级联选择器,用户可以从上到下逐级选择,每级选择都会影响下一级的选项内容,从而有效缩小选择范围,提升用户体验。本文将介绍如何使用 HTML、CSS 和 JavaScript 实现一个可配置的级联选择器。
二、效果演示
本系统实现了一个三级级联选择器,用户可以从省、市、区三个层级依次选择。


三、系统分析
1、页面结构
页面主要包含以下区域:级联选择器容器(cascadeContainer)、选择信息展示区(selectedInfo)。
html
<div id="cascadeContainer" class="cascade-container"></div>
<div id="selectedInfo" class="selected-info"></div>
2、核心功能实现
2.1 渲染级联容器
renderCascade 函数根据配置的级数动态创建选择器容器,并初始化第一级数据。
javascript
function renderCascade(data) {
for (let i = 0; i < cascadeState.levels; i++) {
const levelDiv = document.createElement('div');
levelDiv.className = 'cascade-level';
const wrapper = document.createElement('div');
wrapper.className = 'select-wrapper';
const select = document.createElement('select');
select.id = `cascade-select-${i}`;
select.disabled = i > 0;
const option = document.createElement('option');
option.value = '';
option.textContent = cascadeState.placeholder;
select.appendChild(option);
wrapper.appendChild(select);
levelDiv.appendChild(wrapper);
cascadeContainer.appendChild(levelDiv);
cascadeState.selectElements.push(select);
}
// 初始化第一级数据
if (data.length > 0) {
populateSelect(0, data);
cascadeState.selectElements[0].disabled = false;
}
}
2.2 绑定级联事件
bindCascadeEvents 函数为每个选择器绑定 change 事件,当用户选择某个选项时触发级联更新逻辑。事件处理函数会根据当前选择的层级和值,动态加载下一级选项。
javascript
function bindCascadeEvents() {
cascadeState.selectElements.forEach((select, index) => {
select.addEventListener('change', (e) => {
handleCascadeChange(index, e.target.value);
});
});
}
2.3 处理级联变化
当用户在某级选择器选中值时,handleCascadeChange 会保存该值、清空并禁用后续所有级联选择器,若有值则立即加载下一级数据并刷新信息展示区。
javascript
function handleCascadeChange(level, value) {
cascadeState.selectedValues[level] = value;
// 清空后续选择
for (let i = level + 1; i < cascadeState.levels; i++) {
cascadeState.selectedValues[i] = '';
cascadeState.selectElements[i].innerHTML = `<option value="">${cascadeState.placeholder}</option>`;
cascadeState.selectElements[i].disabled = true;
}
if (value) {
loadNextLevel(level, value);
}
onCascadeSelect(cascadeState.selectedValues);
showSelectedInfo();
}
2.4 加载下级数据
loadNextLevel 函数根据当前选择路径,从原始数据中找到对应的下级数据,并填充到下一个选择器中。该函数会逐级遍历数据结构,直到找到当前选择路径对应的下级选项。
javascript
function loadNextLevel(currentLevel) {
if (currentLevel >= cascadeState.levels - 1) return;
const nextLevel = currentLevel + 1;
let currentData = regionData; // 使用全局数据
for (let i = 0; i <= currentLevel; i++) {
if (!cascadeState.selectedValues[i]) break;
const selectedData = findDataByValue(currentData, cascadeState.selectedValues[i]);
if (selectedData && selectedData[cascadeState.childrenKey]) {
currentData = selectedData[cascadeState.childrenKey];
} else {
return;
}
}
populateSelect(nextLevel, currentData);
cascadeState.selectElements[nextLevel].disabled = false;
}
四、完整代码
git地址:https://gitee.com/ironpro/hjdemo/blob/master/select-cascade/index.html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>级联选择器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #f5f5f5;
padding: 20px;
min-height: 100vh;
display: flex;
justify-content: center;
color: #333;
}
.container {
background: white;
padding: 20px;
width: 800px;
height: 400px;
box-shadow: 0 2px 10px rgba(0,0,0,0.05);
}
h1 {
color: #2c3e50;
font-size: 22px;
margin-bottom: 25px;
text-align: center;
border-bottom: 1px solid #eee;
padding-bottom: 15px;
}
.select-wrapper {
position: relative;
margin-bottom: 20px;
}
select {
width: 100%;
padding: 10px 12px;
border: 1px solid #ddd;
background: white;
font-size: 14px;
color: #2c3e50;
cursor: pointer;
appearance: none;
outline: none;
}
select:hover {
border-color: #888;
}
select:focus {
border-color: #3498db;
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.15);
}
.select-wrapper::after {
content: '▼';
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #999;
font-size: 10px;
pointer-events: none;
}
select:disabled {
background: #f8f9fa;
color: #95a5a6;
cursor: not-allowed;
}
.cascade-container {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.cascade-level {
flex: 1;
min-width: 120px;
}
.selected-info {
margin-top: 20px;
padding: 12px;
background: #e3f2fd;
font-size: 14px;
color: #1976d2;
display: none;
}
</style>
</head>
<body>
<div class="container">
<h1>级联选择器</h1>
<div class="main">
<div id="cascadeContainer" class="cascade-container"></div>
<div id="selectedInfo" class="selected-info"></div>
</div>
</div>
<script>
const cascadeContainer = document.getElementById('cascadeContainer');
const selectedInfo = document.getElementById('selectedInfo');
const cascadeState = {
containerId: 'cascadeContainer',
levels: 3,
placeholder: '请选择',
valueKey: 'value',
labelKey: 'label',
childrenKey: 'children',
selectElements: [],
selectedValues: [],
defaultValue: [] // 新增:默认值配置
};
// 地区数据
const regionData = [
{
value: "110000",
label: "北京市",
children: [{
value: "110100",
label: "北京市",
children: [
{ value: "110101", label: "东城区" },
{ value: "110102", label: "西城区" },
{ value: "110105", label: "朝阳区" },
]
}]
},
// ...
];
// 验证数据结构
function validateData(data) {
if (!Array.isArray(data)) return false;
for (const item of data) {
if (typeof item !== 'object' ||
!item.hasOwnProperty(cascadeState.valueKey) ||
!item.hasOwnProperty(cascadeState.labelKey)) {
return false;
}
}
return true;
}
// 渲染容器结构
function renderCascade(data) {
for (let i = 0; i < cascadeState.levels; i++) {
const levelDiv = document.createElement('div');
levelDiv.className = 'cascade-level';
const wrapper = document.createElement('div');
wrapper.className = 'select-wrapper';
const select = document.createElement('select');
select.id = `cascade-select-${i}`;
select.disabled = i > 0;
const option = document.createElement('option');
option.value = '';
option.textContent = cascadeState.placeholder;
select.appendChild(option);
wrapper.appendChild(select);
levelDiv.appendChild(wrapper);
cascadeContainer.appendChild(levelDiv);
cascadeState.selectElements.push(select);
}
// 初始化第一级数据
if (data.length > 0) {
populateSelect(0, data);
cascadeState.selectElements[0].disabled = false;
}
}
// 绑定事件
function bindCascadeEvents() {
cascadeState.selectElements.forEach((select, index) => {
select.addEventListener('change', (e) => {
handleCascadeChange(index, e.target.value);
});
});
}
// 处理选择变化
function handleCascadeChange(level, value) {
cascadeState.selectedValues[level] = value;
// 清空后续选择
for (let i = level + 1; i < cascadeState.levels; i++) {
cascadeState.selectedValues[i] = '';
cascadeState.selectElements[i].innerHTML = `<option value="">${cascadeState.placeholder}</option>`;
cascadeState.selectElements[i].disabled = true;
}
if (value) {
loadNextLevel(level, value);
}
onCascadeSelect(cascadeState.selectedValues);
showSelectedInfo();
}
// 加载下一级数据
function loadNextLevel(currentLevel) {
if (currentLevel >= cascadeState.levels - 1) return;
const nextLevel = currentLevel + 1;
let currentData = regionData; // 使用全局数据
for (let i = 0; i <= currentLevel; i++) {
if (!cascadeState.selectedValues[i]) break;
const selectedData = findDataByValue(currentData, cascadeState.selectedValues[i]);
if (selectedData && selectedData[cascadeState.childrenKey]) {
currentData = selectedData[cascadeState.childrenKey];
} else {
return;
}
}
populateSelect(nextLevel, currentData);
cascadeState.selectElements[nextLevel].disabled = false;
}
// 填充选择器选项
function populateSelect(level, data) {
const select = cascadeState.selectElements[level];
select.innerHTML = `<option value="">${cascadeState.placeholder}</option>`;
if (!data || !Array.isArray(data)) return;
data.forEach(item => {
const option = document.createElement('option');
option.value = item[cascadeState.valueKey];
option.textContent = item[cascadeState.labelKey];
select.appendChild(option);
});
}
// 根据值查找数据项
function findDataByValue(data, value) {
if (!data || !Array.isArray(data)) return null;
return data.find(item => item[cascadeState.valueKey] === value) || null;
}
// 显示选择信息
function showSelectedInfo() {
const hasSelection = cascadeState.selectedValues.some(v => v);
if (hasSelection) {
selectedInfo.style.display = 'block';
selectedInfo.innerHTML = `<strong>已选择:</strong>${cascadeState.selectedValues.filter(v => v).join(' > ')}`;
} else {
selectedInfo.style.display = 'none';
}
}
// 设置默认值
function setDefaultValue(defaultValues) {
if (!Array.isArray(defaultValues) || defaultValues.length === 0) return;
cascadeState.defaultValue = [...defaultValues];
for (let level = 0; level < defaultValues.length; level++) {
const value = defaultValues[level];
if (value && cascadeState.selectElements[level]) {
// 检查当前选项是否存在于当前级的数据中
const select = cascadeState.selectElements[level];
const optionExists = Array.from(select.options).some(option => option.value === value);
if (optionExists) {
select.value = value;
cascadeState.selectedValues[level] = value;
select.disabled = false;
if (level < defaultValues.length - 1) {
loadNextLevel(level);
}
} else {
break;
}
}
}
// 清空后续未设置默认值的层级
for (let level = defaultValues.length; level < cascadeState.levels; level++) {
if (cascadeState.selectElements[level]) {
cascadeState.selectElements[level].innerHTML = `<option value="">${cascadeState.placeholder}</option>`;
cascadeState.selectElements[level].disabled = true;
cascadeState.selectedValues[level] = '';
}
}
// 更新显示信息
showSelectedInfo();
onCascadeSelect(cascadeState.selectedValues);
}
// 初始化组件
function initCascade(data, defaultValues = []) {
if (!validateData(data)) {
console.error('数据结构不符合要求');
return;
}
renderCascade(data);
bindCascadeEvents();
// 设置默认值
if (Array.isArray(defaultValues) && defaultValues.length > 0) {
setDefaultValue(defaultValues);
}
}
// 获取选择值
function onCascadeSelect(values) {
console.log('选择值:', values);
}
// 初始化组件
initCascade(regionData);
// initCascade(regionData, ["440000", "440300", "440305"]);
</script>
</body>
</html>