使用 HTML + JavaScript 实现级联选择器

文章目录

一、级联选择器

级联选择器是一种重要的用户界面组件,特别适用于处理具有层级关系的数据,如地区选择、分类选择等场景。通过级联选择器,用户可以从上到下逐级选择,每级选择都会影响下一级的选项内容,从而有效缩小选择范围,提升用户体验。本文将介绍如何使用 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>
相关推荐
无知就要求知2 小时前
golang实现ftp功能简单又实用
java·前端·golang
哥本哈士奇2 小时前
使用Gradio构建AI前端 - RAG召回测试
前端·人工智能
codingFunTime2 小时前
vue3 snapdom 导出图片和pdf
前端·javascript·pdf
成为大佬先秃头2 小时前
渐进式JavaScript框架:Vue 组件
前端·javascript·vue.js
赵庆明老师2 小时前
uniapp 微信小程序页面JS模板
javascript·微信小程序·uni-app
程序员勾践2 小时前
前端仅传path路径给后端,避免攻击
前端
登山人在路上2 小时前
Vue 2 中响应式失效的常见情况
开发语言·前端·javascript
董世昌412 小时前
创建对象的方法有哪些?
开发语言·前端
问道飞鱼2 小时前
【前端知识】前端项目不同构建模式的差异
前端·webpack·构建·开发模式·生产模式