You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中实现带复选框的多级下拉选择组件方法咨询

实现思路与代码示例

完全可行,不用依赖现成库自己就能实现,核心是处理层级复选框的联动状态和下拉展开逻辑,给你一套落地的实现方案:

1. 定义层级数据结构

先把菜单数据组织成嵌套结构,每个节点需要包含以下核心字段:

  • id: 唯一标识
  • label: 显示文本
  • children: 子节点数组(可选)
  • checked: 是否选中
  • indeterminate: 是否半选(父节点专属状态,子节点部分选中时触发)

示例数据:

const menuData = [
  {
    id: 1,
    label: "父选项1",
    checked: false,
    indeterminate: false,
    children: [
      { id: 11, label: "子选项1-1", checked: false },
      { id: 12, label: "子选项1-2", checked: false },
    ],
  },
  {
    id: 2,
    label: "父选项2",
    checked: false,
    indeterminate: false,
    children: [
      {
        id: 21,
        label: "父选项2-1",
        checked: false,
        indeterminate: false,
        children: [{ id: 211, label: "子选项2-1-1", checked: false }],
      },
    ],
  },
];

2. 核心组件实现

拆成两个组件,逻辑更清晰:下拉容器负责展开收起和状态管理,树形节点负责单个选项的渲染与递归。

下拉容器组件(NestedCheckboxDropdown)

import { useState } from "react";
import TreeCheckbox from "./TreeCheckbox";

const NestedCheckboxDropdown = ({ data }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [menuData, setMenuData] = useState(data);

  // 递归更新节点选中状态,处理父子联动
  const updateNodeCheck = (nodeId, checked, nodes) => {
    return nodes.map(node => {
      if (node.id === nodeId) {
        const updatedNode = { ...node, checked };
        // 父节点选中时同步所有子节点状态
        if (updatedNode.children) {
          updatedNode.children = updateNodeCheck(nodeId, checked, updatedNode.children);
        }
        return updatedNode;
      }
      // 递归处理子节点
      if (node.children) {
        const updatedChildren = updateNodeCheck(nodeId, checked, node.children);
        // 更新父节点的选中/半选状态
        const allChecked = updatedChildren.every(child => child.checked);
        const someChecked = updatedChildren.some(child => child.checked);
        return {
          ...node,
          children: updatedChildren,
          checked: allChecked,
          indeterminate: someChecked && !allChecked,
        };
      }
      return node;
    });
  };

  const handleCheckChange = (nodeId, checked) => {
    setMenuData(prev => updateNodeCheck(nodeId, checked, prev));
  };

  // 统计选中数量,显示在触发按钮上
  const countChecked = (nodes) => {
    let total = 0;
    nodes.forEach(node => {
      if (node.checked) total++;
      if (node.children) total += countChecked(node.children);
    });
    return total;
  };

  return (
    <div className="dropdown-container">
      <button className="dropdown-trigger" onClick={() => setIsOpen(!isOpen)}>
        选择选项 ({countChecked(menuData)})
      </button>
      {isOpen && (
        <div className="dropdown-menu">
          {menuData.map(node => (
            <TreeCheckbox
              key={node.id}
              node={node}
              onCheckChange={handleCheckChange}
            />
          ))}
        </div>
      )}
    </div>
  );
};

export default NestedCheckboxDropdown;

树形复选框节点组件(TreeCheckbox)

import { useState } from "react";

const TreeCheckbox = ({ node, onCheckChange }) => {
  const [isExpanded, setIsExpanded] = useState(true); // 默认展开子节点

  const handleCheck = (e) => {
    onCheckChange(node.id, e.target.checked);
  };

  return (
    <div className="tree-node">
      <div className="node-header">
        {/* 只有有子节点时显示展开/折叠按钮 */}
        {node.children && (
          <button
            className="expand-btn"
            onClick={() => setIsExpanded(!isExpanded)}
          >
            {isExpanded ? "▼" : "▶"}
          </button>
        )}
        <input
          type="checkbox"
          checked={node.checked}
          indeterminate={node.indeterminate}
          onChange={handleCheck}
        />
        <span>{node.label}</span>
      </div>
      {/* 递归渲染子节点,缩进区分层级 */}
      {node.children && isExpanded && (
        <div className="node-children">
          {node.children.map(child => (
            <TreeCheckbox
              key={child.id}
              node={child}
              onCheckChange={onCheckChange}
            />
          ))}
        </div>
      )}
    </div>
  );
};

export default TreeCheckbox;

3. 基础样式(CSS)

给组件加些样式,让它看起来像标准下拉菜单:

.dropdown-container {
  position: relative;
  width: 220px;
}

.dropdown-trigger {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  background: #fff;
  cursor: pointer;
  text-align: left;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.dropdown-trigger::after {
  content: "▼";
  font-size: 12px;
}

.dropdown-menu {
  position: absolute;
  top: calc(100% + 2px);
  left: 0;
  right: 0;
  border: 1px solid #e0e0e0;
  border-radius: 0 0 4px 4px;
  background: #fff;
  max-height: 320px;
  overflow-y: auto;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  z-index: 100;
}

.tree-node {
  padding: 4px 8px;
}

.node-header {
  display: flex;
  align-items: center;
  gap: 6px;
  cursor: pointer;
  padding: 4px 0;
}

.expand-btn {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  font-size: 12px;
  width: 16px;
  text-align: center;
}

.node-children {
  margin-left: 24px;
}

关键逻辑说明

  • 父子联动:父节点选中时自动勾选所有子节点;子节点部分选中时,父节点切换为半选状态;子节点全部选中时,父节点自动选中。
  • 层级缩进:通过margin-left区分不同层级的选项,视觉上更清晰。
  • 状态同步:递归遍历更新节点状态,确保整个树形结构的状态一致性。

你可以根据需求调整样式、默认展开状态,或者添加搜索、禁用选项等扩展功能。

内容的提问来源于stack exchange,提问作者desmond_lost

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 16:23:16