级联选择器是一种重要的用户界面组件,特别适用于处理具有层级关系的数据,如地区选择、分类选择等场景。通过级联选择器,用户可以从上到下逐级选择,每级选择都会影响下一级的选项内容,从而有效缩小选择范围,提升用户体验。本文将介绍如何使用 HTML、CSS 和 JavaScript 实现一个可配置的级联选择器。效果演示
本系统实现了一个三级级联选择器,用户可以从省、市、区三个层级依次选择。
页面结构
页面主要包含以下区域:级联选择器容器(cascadeContainer)、选择信息展示区(selectedInfo)。<div id="cascadeContainer" class="cascade-container"></div><div id="selectedInfo" class="selected-info"></div>
核心功能实现
渲染级联容器
renderCascade 函数根据配置的级数动态创建选择器容器,并初始化第一级数据。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; }}
绑定级联事件
bindCascadeEvents 函数为每个选择器绑定 change 事件,当用户选择某个选项时触发级联更新逻辑。事件处理函数会根据当前选择的层级和值,动态加载下一级选项。function bindCascadeEvents() { cascadeState.selectElements.forEach((select, index) => { select.addEventListener('change', (e) => { handleCascadeChange(index, e.target.value); }); });}
处理级联变化
当用户在某级选择器选中值时,handleCascadeChange 会保存该值、清空并禁用后续所有级联选择器,若有值则立即加载下一级数据并刷新信息展示区。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();}
加载下级数据
loadNextLevel 函数根据当前选择路径,从原始数据中找到对应的下级数据,并填充到下一个选择器中。该函数会逐级遍历数据结构,直到找到当前选择路径对应的下级选项。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<!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 14px; 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: 14px; flex-wrap: wrap; } .cascade-level { flex: 1; min-width: 120px; } .selected-info { margin-top: 20px; padding: 14px; 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); </script></body></html>
阅读原文:https://mp.weixin.qq.com/s/21_m4lX3BJeiglfetl5lEg
该文章在 2025/12/25 9:33:22 编辑过