背景描述
开发过程中会有一种情况为产品需要一个对下拉框数据源进行快捷搜索/带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}
<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}//是否多选
/>