当前位置: 首页 > news >正文

用 React + TypeScript + Antd 打造一个动态加载的树形穿梭选择组件

作为一名前端工程师,我时常需要在项目中处理复杂的选择场景,比如组织架构选择、权限分配等。普通的下拉框已经无法满足需求,而 Ant Design 的 TreeSelect 组件虽然强大,但如何让它支持动态加载数据、树形结构选择,还能与穿梭框的效果结合起来?这篇文章将带你基于 React、TypeScript 和 Antd,打造一个功能强大且易用的 TransferComSelect 组件,解决这些痛点。

为什么需要这个组件?

在实际业务中,我们经常会遇到以下需求:

  • 树形数据展示: 需要展示层级结构,比如公司部门树。
  • 动态加载: 数据量大时,按需加载子节点以提升性能。
  • 多选与穿梭: 支持多选,并清晰展示已选内容。
  • 灵活性: 可切换“只选本级”或“包含子级”的选择模式。

基于这些需求,我们将实现一个结合 TreeSelect 的树形选择组件,支持动态加载和自定义交互。

核心代码实现

以下是 TransferComSelect 组件的核心代码,我会逐步拆解它的实现逻辑:

.switch {
  display: flex;
  background-color: #fff;
  padding: 5px 10px;
  width: 100%;
  margin-bottom: 5px;

  >span {
    margin-right: 5px;
  }
}

.tree {
  height: 300px;
  overflow-y: auto;
  scrollbar-width: none;
  /* Firefox */
  -ms-overflow-style: none;
}

.tree::-webkit-scrollbar {
  display: none;
}
import React, { useState, useEffect, useCallback } from 'react';
import { TreeSelect, Switch, Spin, Empty } from 'antd';
import {debounce} from 'lodash'; // 引入 lodash 的防抖函数
import  './index.less';

interface TreeDataNode {
  key: string;
  value: string;
  title: string;
  children?: TreeDataNode[];
  isLeaf?: boolean;
}

interface TransferComSelectProps {
  loadDataFetch: (params: { parentId: string }) => Promise<any>;
  treeData?: TreeDataNode[];
  type?: number;
  selectedData?: {id:string}[];
  handleChange?: (data: { selectedIds: string[]; type: number }) => void;
  className?: string;
  style?: React.CSSProperties;
  placeholder?: string; // 新增:自定义 placeholder
  switchText?: { onlyThisLevel: string; on: string; off: string }; // 新增:开关文案
  emptyText?: string; // 新增:空状态文案
}

const TransferComSelect: React.FC<TransferComSelectProps> = (props) => {
  const {
    loadDataFetch,
    treeData = [],
    type = 0,
    selectedData = [],
    handleChange,
    className,
    style,
    placeholder = '请输入', // 默认值
    switchText = { onlyThisLevel: '只选本级', on: '开', off: '关' }, // 默认文案
    emptyText = '暂无数据', // 默认空状态文案
  } = props;

  const [sch, setSch] = useState(true);
  const [targetKeys, setTargetKeys] = useState<string[]>([]);
  const [treeDataSource, setTreeDataSource] = useState<TreeDataNode[]>(treeData);
  const [loading, setLoading] = useState(false);

    // 更新树数据源
    const updateTreeDataSource = (data: TreeDataNode[], key: string, children: TreeDataNode[]) => {
      return data.forEach((node) => {
        if (node.key === key) {
          node.children = children;
        } else if (node.children) {
          updateTreeDataSource(node.children, key, children);
        }
      });
    };

  // 动态加载子节点数据
  const loadData = (treeNode: any) => {
    return new Promise((resolve) => {
      setLoading(true);
      loadDataFetch({ parentId: treeNode.id }).then((children: any) => {
        const newChildren = children.map((i: any) => ({
          ...i,
          isLeaf: i?.leaf,
          key: i?.id,
          value: i?.id,
        }));
        updateTreeDataSource(treeDataSource, treeNode.props.eventKey, newChildren);
        setTreeDataSource([...treeDataSource]);
        setLoading(false);
        resolve();
      }).catch(() => setLoading(false));
    });
  };



  // 处理选择变化
  const onChange = (newValue: any[]) => {
      setTargetKeys(newValue.map((i) => (i?.value||i)));
  };

  // 同步外部传入的已选数据
  useEffect(() => {
    if (selectedData?.length > 0) {
      setTargetKeys(selectedData.map((i) => i?.id));
    }
  }, [selectedData]);

  // 通知父组件选择结果
  useEffect(() => {
    if (handleChange) {
      handleChange({
        selectedIds: targetKeys,
        type,
      });
    }
  }, [targetKeys, handleChange, type]);

  // 新增:防抖搜索函数
  const filterTreeNode = useCallback(
    debounce((inputValue: string, treeNode: any) => {
      return treeNode.title.toLowerCase().includes(inputValue.toLowerCase());
    }, 300), // 300ms 防抖
    []
  );

  const { SHOW_ALL } = TreeSelect;
  const tProps = {
    treeData: treeDataSource,
    value: targetKeys,
    onChange,
    treeCheckStrictly: sch,
    treeCheckable: true,
    showCheckedStrategy: SHOW_ALL,
    placeholder,
    loadData,
    showSearch: true,
    filterTreeNode,
    dropdownRender: (node: any) => (
      <>
        <div className='switch'>
          <span>{switchText.onlyThisLevel}</span>
          <Switch
            checkedChildren={switchText.on}
            unCheckedChildren={switchText.off}
            checked={sch}
            onChange={setSch}
          />
        </div>
        <Spin spinning={loading}>
          {treeDataSource.length === 0 && !loading ? (
            <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={emptyText} />
          ) : (
            node
          )}
        </Spin>
      </>
    ),
    className,
    style: { width: '100%', ...style },
  };

  return <TreeSelect {...tProps} />;
};

export default TransferComSelect;

如何使用这个组件?

该组件已集成到 react-nexlif 开源库中。 具体文档可参考使用文档。你可以通过以下方式引入并使用:

pnpm add react-nexlif


import React from 'react';
import {TransferComSelect} from 'react-nexlif';

const App: React.FC = () => {
  const initialTreeData = [
    { key: '1', value: '1', title: 'Department A',id:'1' },
    { key: '2', value: '2', title: 'Department B',id:'2' },
  ];

  const fetchData = ({ parentId }: { parentId: string }) => {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve([
          { id: `${parentId}-1`, title: `Sub ${parentId}-1`, leaf: true },
          { id: `${parentId}-2`, title: `Sub ${parentId}-2`, leaf: true },
        ]);
      }, 1000);
    });
  };

  const handleChange = (data: { list: { orgId: string }[]; type: number }) => {
    console.log('Selected:', data);
  };

  return (
      <div>
      <h1>TransferComSelect   预览</h1>
    <TransferComSelect
      treeData={initialTreeData}
      loadDataFetch={fetchData}
      type={0}
      selectedData={[{ id: '1' }]}
      handleChange={handleChange}
      classtitle="custom-tree-select"
      style={{ border: '1px solid #1890ff', borderRadius: 4 }}
      placeholder="Please input" // 自定义 placeholder
      switchText={{ onlyThisLevel: 'Only this level', on: 'On', off: 'Off' }} // 英文文案
      emptyText="No data available" // 自定义空状态文案
    />
        </div>
  );
};

export default App;
代码亮点解析
  1. 动态加载数据: 通过 loadData 函数实现按需加载子节点,结合 loadDataFetch 接口请求数据,优化性能。
  2. 类型安全: 使用 TypeScript 定义 TreeDataNode 和 TransferComSelectProps,确保传入参数和数据结构清晰可控。
  3. 灵活选择模式: 通过 treeCheckStrictly 和 Switch 组件实现“只选本级”与“包含子级”的切换。
  4. 自定义下拉: 使用 dropdownRender 在下拉菜单中添加开关,增强交互体验。
  5. 状态同步: 通过 useEffect 将选择结果同步给父组件,保持数据一致性。
  6. 加载状态提示
    • 新增 loading 状态,在 loadData 中通过 setLoading 控制。
    • 使用 Antd 的 Spin 组件包裹下拉内容,数据加载时显示旋转动画,直观反馈用户。
  7. 搜索功能
    • 添加 showSearch: true 启用 TreeSelect 的搜索功能。
    • 实现 filterTreeNode 函数,支持对节点标题的模糊匹配,用户输入时快速定位。
  8. 样式定制
    • 新增 className 和 style 属性,允许调用者通过 CSS 类或内联样式调整组件外观。
    • 默认宽度保持 100%,通过 ...style 合并用户传入的样式,确保灵活性。
  • 搜索防抖
    • 使用 lodash/debounce 包装 filterTreeNode,设置 300ms 延迟。
    • 通过 useCallback 确保函数引用稳定,避免不必要的重新渲染。
    • 用户快速输入时,只在停止输入 300ms 后触发搜索,显著减少性能开销。
  • 空状态提示
    • 在 dropdownRender 中检查 treeDataSource.length === 0 且 !loading。
    • 使用 Antd 的 Empty 组件显示空状态,文案通过 emptyText 自定义。
    • 当数据为空时,用户会看到清晰的“暂无数据”提示,而不是空白。
  • 多语言支持
    • 新增 placeholder、switchText 和 emptyText 属性,将所有文案提取为 props。
    • 默认提供中文文案,调用者可传入英文或其他语言,轻松实现国际化。
    • 比如,switchText 支持开关的标签和状态文案自定义。
总结

通过 React、TypeScript 和 Antd 的 TreeSelect,我们实现了一个支持动态加载、多选切换的树形穿梭选择组件。它不仅满足了复杂业务场景的需求,还保持了代码的可读性和扩展性。

如果你在项目中也有类似需求,不妨试试这个组件!复制代码,稍作调整就能跑起来。有什么优化建议或使用心得,欢迎留言交流~

相关文章:

  • 深入理解指针(5)(C语言版)
  • Go 语言规范学习(7)
  • 使用FastAPI和google gemini打造一个多语言翻译网站
  • JWT在线解密/JWT在线解码 - 加菲工具
  • 蓝桥杯专项复习——二分查找、二分答案
  • C++自定义迭代器
  • 【学习笔记】计算机网络(六)
  • [GESP202503 四级] 二阶矩阵
  • 初始ARM
  • 8.3链表专题:LeetCode 143. 重排链表
  • elementui的默认样式修改
  • 常见集合篇(二)数组、ArrayList与链表:原理、源码及业务场景深度解析
  • 【C语言】字符函数的易错点及其模拟实现
  • SQL在线格式化 - 加菲工具
  • WINDOWS 2019 2022 服务器安装了更新补丁 自动重启 分析
  • 第6章 与学习相关的技巧(鱼书)
  • (二)机器学习---常见任务及算法概述
  • 3.31 代码随想录第三十一天打卡
  • 第十章 VGA显示圆
  • # 使用 OpenCV 和神经网络实现图像风格化
  • 广告设计要学哪些软件/东莞百度推广排名优化
  • 杭州哪里做网站好/小程序设计
  • 网站制作源码版权/专注于品牌营销服务
  • 徐州市鼓楼区建设局网站/百度论坛首页官网
  • 网站底部素材/windows优化大师在哪里
  • vue做门户网站/西安做网站哪家好