功能点:
- 点击节点前的箭头,可以手动展开或折叠该节点的子节点。
- 在搜索框中输入关键词,匹配的节点及其父节点会自动展开。
- 清空搜索框后,恢复到用户手动控制的展开状态。
- 勾选节点时仍然遵守 "最多勾选 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;