react ant design树穿梭框实现搜索并展开到子节点、同级节点选择及同时选择数量限制功能

发布于:2025-03-26 ⋅ 阅读:(35) ⋅ 点赞:(0)

 功能点:

  • 点击节点前的箭头,可以手动展开或折叠该节点的子节点。
  • 在搜索框中输入关键词,匹配的节点及其父节点会自动展开。
  • 清空搜索框后,恢复到用户手动控制的展开状态。
  • 勾选节点时仍然遵守 "最多勾选 6 个节点" 和 "只能选择同级节点" 的限制。
import { Transfer, Tree, Input, message } from 'antd';
import React, { useState } from 'react';

// 判断是否已选中
const isChecked = (selectedKeys, eventKey) => selectedKeys.includes(eventKey);

// 生成树结构,并标记禁用状态
const generateTree = (treeNodes = [], checkedKeys = []) =>
  treeNodes.map(({ children, ...props }) => ({
    ...props,
    disabled: checkedKeys.includes(props.key),
    children: generateTree(children, checkedKeys),
  }));

// 搜索功能:过滤树节点并展开到匹配节点
const filterTreeData = (treeData, searchValue) => {
  const loop = (data) =>
    data
      .map((item) => {
        const match = item.title.toLowerCase().includes(searchValue.toLowerCase());
        const children = item.children ? loop(item.children) : [];
        return {
          ...item,
          children,
          isMatch: match || children.some((child) => child.isMatch),
        };
      })
      .filter((item) => item.isMatch);
  return loop(treeData);
};

// 自定义 TreeTransfer 组件
const TreeTransfer = ({ dataSource, targetKeys, ...restProps }) => {
  const transferDataSource = [];
  const [searchValue, setSearchValue] = useState('');
  const [expandedKeys, setExpandedKeys] = useState([]); // 控制展开的节点

  // 展平树结构为一维数组
  function flatten(list = []) {
    list.forEach((item) => {
      transferDataSource.push(item);
      flatten(item.children);
    });
  }
  flatten(dataSource);

  // 动态计算需要展开的节点(用于搜索)
  const calculateExpandedKeys = (treeData) => {
    const keys = [];
    const loop = (data) =>
      data.forEach((item) => {
        if (item.isMatch) {
          keys.push(item.key);
          if (item.children) loop(item.children);
        }
      });
    loop(treeData);
    return keys;
  };

  // 过滤后的树数据
  const filteredTreeData = filterTreeData(dataSource, searchValue);
  const autoExpandedKeys = calculateExpandedKeys(filteredTreeData);

  // 更新展开状态
  const handleExpand = (newExpandedKeys) => {
    setExpandedKeys(newExpandedKeys);
  };

  return (
    <Transfer
      {...restProps}
      targetKeys={targetKeys}
      dataSource={transferDataSource}
      className="tree-transfer"
      render={(item) => item.title}
      showSelectAll={false}
    >
      {({ direction, onItemSelect, selectedKeys }) => {
        if (direction === 'left') {
          const checkedKeys = [...selectedKeys, ...targetKeys];

          return (
            <>
              {/* 搜索框 */}
              <Input
                placeholder="搜索节点"
                value={searchValue}
                onChange={(e) => {
                  setSearchValue(e.target.value);
                  setExpandedKeys(autoExpandedKeys); // 搜索时自动展开匹配节点
                }}
                style={{ marginBottom: 16 }}
              />
              <Tree
                blockNode
                checkable
                checkStrictly
                defaultExpandAll
                checkedKeys={checkedKeys}
                treeData={filteredTreeData}
                expandedKeys={searchValue ? autoExpandedKeys : expandedKeys} // 根据搜索状态决定展开哪些节点
                onExpand={handleExpand} // 手动控制展开/折叠
                onCheck={(_, { node }) => {
                  const { key, parentKey } = node;

                  // 检查是否在同一层级
                  const isSameLevel = checkedKeys.every((k) => {
                    const checkedNode = transferDataSource.find((item) => item.key === k);
                    return !checkedNode || checkedNode.parentKey === parentKey;
                  });

                  // 检查是否超过最大勾选数量限制
                  const isWithinLimit = checkedKeys.length < 6 || checkedKeys.includes(key);

                  if (isSameLevel && isWithinLimit) {
                    onItemSelect(key, !isChecked(checkedKeys, key));
                  } else {
                    message.warn(
                      !isSameLevel
                        ? '只能选择同级节点'
                        : '最多只能同时勾选 6 个节点'
                    );
                  }
                }}
                onSelect={(_, { node }) => {
                  const { key, parentKey } = node;

                  // 检查是否在同一层级
                  const isSameLevel = checkedKeys.every((k) => {
                    const checkedNode = transferDataSource.find((item) => item.key === k);
                    return !checkedNode || checkedNode.parentKey === parentKey;
                  });

                  // 检查是否超过最大勾选数量限制
                  const isWithinLimit = checkedKeys.length < 6 || checkedKeys.includes(key);

                  if (isSameLevel && isWithinLimit) {
                    onItemSelect(key, !isChecked(checkedKeys, key));
                  } else {
                    message.warn(
                      !isSameLevel
                        ? '只能选择同级节点'
                        : '最多只能同时勾选 6 个节点'
                    );
                  }
                }}
              />
            </>
          );
        }
      }}
    </Transfer>
  );
};

// 测试数据
const treeData = [
  {
    key: '0-0',
    title: 'Node 0-0',
    parentKey: null, // 根节点没有父节点
  },
  {
    key: '0-1',
    title: 'Node 0-1',
    parentKey: null,
    children: [
      {
        key: '0-1-0',
        title: 'Node 0-1-0',
        parentKey: '0-1', // 父节点是 '0-1'
      },
      {
        key: '0-1-1',
        title: 'Node 0-1-1',
        parentKey: '0-1', // 父节点是 '0-1'
      },
    ],
  },
  {
    key: '0-2',
    title: 'Node 0-3',
    parentKey: null,
  },
];

// 主应用组件
const App = () => {
  const [targetKeys, setTargetKeys] = useState([]);

  const onChange = (keys) => {
    setTargetKeys(keys);
  };

  return <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={onChange} />;
};

export default App;