react+antd实现一个支持多种类型及可新增编辑搜索的下拉框

背景描述

开发过程中会有一种情况为产品需要一个对下拉框数据源进行快捷搜索/带icon/更改的入口 例:快捷进入数据源的编辑/新增进行更改数据

实现过程

下方为组件代码

复制代码
import { PlusOutlined, SettingOutlined } from '@ant-design/icons';
import { Button, Input, Select, TreeSelect } from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import { useIntl } from 'umi';

const TreeSelectWithAddEdit = ({
  treeData,
  onAdd,
  onEdit,
  addText = '新增',
  addFlag = true,
  addDisabled = false,
  // 新增:模式切换属性,可选 'tree' | 'flat'
  mode = 'tree',
  // 新增:平铺模式下是否支持多选
  flatMultiple = true,
  ...restProps
}) => {
  const [processedTreeData, setProcessedTreeData] = useState([]);
  const [searchValue, setSearchValue] = useState('');
  const { formatMessage: t } = useIntl();

  // 处理原始数据,添加自定义标题
  useEffect(() => {
    const processData = (data) => {
      return data.map((node) => {
        const processedNode = {
          ...node,
          // 保存原始标题用于搜索
          originalTitle: node.title || node.label || node.value,
          // 生成平铺模式的key(包含父节点路径,避免重复)
          flatKey: node.flatKey || node.value,
        };

        // 如果有子节点,递归处理
        if (node.children && node.children.length > 0) {
          processedNode.children = processData(node.children);
        }

        // 创建自定义标题
        processedNode.title = (
          <div
            style={{
              display: 'flex',
              justifyContent: 'space-between',
              alignItems: 'center',
              width: '100%',
            }}
          >
            <div style={{ display: 'flex', alignItems: 'center' }}>
              {processedNode.icon && processedNode.icon}&nbsp;
              <div
                title={processedNode.originalTitle}
                style={{
                  whiteSpace: 'nowrap', // 强制文本单行显示
                  overflow: 'hidden', // 隐藏溢出内容
                  textOverflow: 'ellipsis', // 溢出部分显示省略号
                  maxWidth: '330px', // 或固定宽度如 200px
                }}
              >
                {processedNode.originalTitle}
              </div>
            </div>

            {/* 1不显示 2禁用 3显示 */}
            {processedNode.edit && processedNode.edit != 1 && (
              <SettingOutlined
                style={{
                  cursor: processedNode.edit != 2 ? 'pointer' : 'not-allowed',
                  color: processedNode.edit != 2 ? '#000' : '#999',
                  fontSize: '14px',
                  marginLeft: 8,
                  flexShrink: 0,
                  marginRight: 2,
                }}
                onClick={(e) => {
                  if (processedNode.edit == 2) return;
                  e.stopPropagation();
                  if (typeof onEdit === 'function') {
                    onEdit(processedNode);
                  }
                }}
              />
            )}
          </div>
        );

        return processedNode;
      });
    };

    if (treeData && treeData.length > 0) {
      setProcessedTreeData(processData(treeData));
    } else {
      setProcessedTreeData([]);
    }
  }, [treeData, onEdit]);

  // 将树形数据扁平化为一维数组(递归遍历所有节点)
  const flattenTreeData = useMemo(() => {
    const flatten = (data, parentKey = '') => {
      let result = [];
      data.forEach((node) => {
        const flatNode = {
          ...node,
          flatKey: parentKey ? `${parentKey}-${node.value}` : node.value,
          // 平铺模式下的显示标题(可添加层级前缀,如 ├─ )
          flatTitle: node.originalTitle,
        };
        result.push(flatNode);

        // 递归处理子节点
        if (node.children && node.children.length > 0) {
          result = [...result, ...flatten(node.children, flatNode.flatKey)];
        }
      });
      return result;
    };
    return flatten(processedTreeData);
  }, [processedTreeData]);

  // 过滤数据(同时支持树形和平铺)
  const filterData = useMemo(() => {
    if (!searchValue) {
      return mode === 'tree' ? processedTreeData : flattenTreeData;
    }

    // 树形模式过滤
    if (mode === 'tree') {
      const filterTree = (data, keyword) => {
        return data
          .map((node) => {
            const filteredChildren = node.children ? filterTree(node.children, keyword) : [];
            const isMatch = node.originalTitle?.toLowerCase().includes(keyword.toLowerCase());
            const hasMatchingChild = filteredChildren.length > 0;

            if (isMatch || hasMatchingChild) {
              return { ...node, children: filteredChildren };
            }
            return null;
          })
          .filter(Boolean);
      };
      return filterTree(processedTreeData, searchValue);
    }

    // 平铺模式过滤
    return flattenTreeData.filter((node) =>
      node.originalTitle?.toLowerCase().includes(searchValue.toLowerCase()),
    );
  }, [searchValue, processedTreeData, flattenTreeData, mode]);

  // 点击新增按钮
  const handleAddClick = () => {
    if (typeof onAdd === 'function') {
      onAdd();
    }
  };

  // 自定义下拉内容(通用)
  const renderDropdownContent = (menu) => (
    <div style={{ padding: 8, minWidth: 200 }}>
      {/* 搜索框 + 新增按钮 */}
      <div style={{ display: 'flex', marginBottom: 8, gap: 8 }}>
        <Input.Search
          placeholder={t({ id: 'input.placeholder' })}
          style={{ flex: 1 }}
          value={searchValue}
          onChange={(e) => setSearchValue(e.target.value)}
          onSearch={(value) => setSearchValue(value)}
          allowClear
        />
        {addFlag && (
          <Button
            type="primary"
            icon={<PlusOutlined />}
            onClick={handleAddClick}
            disabled={addDisabled}
          >
            {addText}
          </Button>
        )}
      </div>
      {/* 内容区域 */}
      <div style={{ maxHeight: 300, overflow: 'auto' }}>{menu}</div>
    </div>
  );

  // 渲染树形模式组件
  const renderTreeMode = () => {
    const dropdownRender = (menu) =>
      renderDropdownContent(
        React.cloneElement(menu, {
          treeData: filterData,
          filterTreeNode: () => true, // 禁用内部过滤
        }),
      );

    return (
      <TreeSelect
        {...restProps}
        treeData={filterData}
        dropdownRender={dropdownRender}
        placeholder={t({ id: 'select.placeholder' })}
        showSearch={false}
        treeCheckable
        allowClear
        style={{ width: '100%' }}
        filterTreeNode={false}
      />
    );
  };

  // 渲染平铺模式组件
  const renderFlatMode = () => {
    // 处理平铺选项
    const flatOptions = filterData.map((node) => ({
      label: node.title || node.flatTitle,
      value: node.flatKey || node.value,
      // 透传原始节点数据
      nodeData: node,
    }));

    // 自定义Select下拉内容
    const dropdownRender = (menu) => renderDropdownContent(menu);

    return (
      <Select
        {...restProps}
        options={flatOptions}
        dropdownRender={dropdownRender}
        placeholder={t({ id: 'select.placeholder' })}
        showSearch={false}
        mode={flatMultiple ? 'multiple' : undefined}
        allowClear
        style={{ width: '100%' }}
        // 自定义选项渲染(保留编辑按钮功能)
        optionLabelProp="label"
      />
    );
  };

  // 根据模式渲染对应组件
  if (mode === 'tree') {
    return renderTreeMode();
  }

  if (mode === 'flat') {
    return renderFlatMode();
  }

  return renderTreeMode(); // 默认树形
};

export default TreeSelectWithAddEdit;

下方为调用示例

复制代码
//平铺展示数据类型为
{
      title: item.name,
      value: item.id,
      key: item.id,
      edit: 3,//可选
      icon: <DecryptIcon />,//可选
}
//树状数据源加工函数
  const processUrlDataToTree = (rawData = []) => {
    // 直接map遍历每个分组,修改字段名
    return rawData.map((groupItem, index) => {
      const { urlType, count, data = [] } = groupItem;
      const formatUrlTypeName = () => {
        const urlTypeName = CATEGORY_MAP[urlType] || { zh: urlType, en: urlType };
        return locale === 'zh-CN' ? urlTypeName.zh : urlTypeName.en;
      };
      // 构建父节点
      return {
        // 父节点标题:自定义命名 + 数量
        title: `${formatUrlTypeName()} (${count})`,
        // 父节点value/key:自定义规则(保证唯一即可)
        value: urlType,
        key: urlType,
        // 子节点:map修改字段名
        children: data.map((child) => ({
          title: child.urlName,
          value: child.id,
          key: child.id,
          edit: urlType === 'custom' ? 2 : 1,
          icon: <UrlClassification style={{ marginRight: 2 }} />,
        })),
      };
    });
  };



<TreeSelectWithAddEdit
  treeData={treeData}
  mode="flat"//平铺展示非树状
  onAdd={handleAdd} // 传入新增触发方法
  onEdit={handleEdit} // 传入编辑/配置触发方法
  flatMultiple={false}//是否多选
/>
相关推荐
摘星编程2 小时前
用React Native开发OpenHarmony应用:Loading加载状态组件
javascript·react native·react.js
aesthetician2 小时前
Spotify 网页版前端技术全面解析
前端
咩图2 小时前
Sketchup软件二次开发+Ruby+VisualStudioCode
java·前端·ruby
Можно2 小时前
从零开始:Vue 框架安装全指南
前端·javascript·vue.js
阿蒙Amon2 小时前
TypeScript学习-第9章:类型断言与类型缩小
javascript·学习·typescript
福大大架构师每日一题2 小时前
agno v2.4.7发布!新增Else条件分支、AWS Bedrock重排器、HITL等重大升级全解析
javascript·云计算·aws
.清和.2 小时前
【js】Javascript事件循环机制
开发语言·javascript·ecmascript
蜗牛攻城狮2 小时前
CSS中的 `dvh` 与 `vh`: 深入理解视口单位
前端·css
心柠2 小时前
原型和原型链
开发语言·javascript·ecmascript