效果:
子节点选中时候 displayedKeys 选中子节点和对应的父节点 id 数据回填的时候 会把父id过滤避免tree组件由于父id选中导致样式异常(父id选中之后默认会将全部子节点选中,但是这个时候选中的key子节点没有全部选中)
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { Tree } from 'antd';
/**
* LinkedMultiSelectTree 组件
*
* @param {Object} props - 组件的配置项
* @param {Array} props.treeData - 树形数据,格式为 { key, title, children? }
* @param {Array} [props.value=[]] - 受控的选中节点 key 数组
* @param {Function} [props.onChange] - 选中状态变化时的回调函数,接收新的选中节点的 key 数组
* @param {Boolean} [props.parentLinkage=true] - 是否启用父节点联动,默认开启
* @param {Function} [props.onDisplayedKeysChange] - 展示的节点(选中的节点和半选中的节点)变化时的回调函数
*
* @returns {React.Element} 返回一个树形组件
*
* @example
* const treeData = [
* { key: '1', title: 'Node 1', children: [{ key: '1-1', title: 'Node 1-1' }] },
* { key: '2', title: 'Node 2' }
* ];
*
* const [selectedKeys, setSelectedKeys] = useState([]); // 不关联父节点的数据
* const [displayedKeys, setDisplayedKeys] = useState([]); // 关联父节点的数据
*
* const handleChange = (checkedKeys) => {
* console.log('选中的节点:', checkedKeys);
* };
*
* // 处理显示的权限键变化
* const handleDisplayedKeysChange = useCallback((keys) => {
* setDisplayedKeys(keys);
* }, []);
*
* <LinkedMultiSelectTree
* treeData={treeData} // 权限树数据
* value={selectedKeys} // 当前选中的权限
* onChange={setSelectedKeys} // 权限选中变化
* onDisplayedKeysChange={handleDisplayedKeysChange} // 显示的权限键变化
* />
*/
const LinkedMultiSelectTree = ({
treeData,
value = [],
onChange,
parentLinkage = true, // 是否开启父节点联动,默认开启
onDisplayedKeysChange,
}) => {
const { checkedKeys, indeterminateKeys, handleSelect } = useTreeCheckbox({
treeData,
value,
onChange,
parentLinkage,
});
// 使用 useRef 来存储上一次的 displayedKeys
const prevDisplayedKeysRef = useRef([]);
const displayedKeys = useMemo(() => {
return parentLinkage
? Array.from(new Set([...checkedKeys, ...indeterminateKeys]))
: checkedKeys;
}, [checkedKeys, indeterminateKeys, parentLinkage]);
useEffect(() => {
if (onDisplayedKeysChange && JSON.stringify(prevDisplayedKeysRef.current) !== JSON.stringify(displayedKeys)) {
onDisplayedKeysChange(displayedKeys);
prevDisplayedKeysRef.current = displayedKeys;
}
}, [displayedKeys]);
return (
<Tree
checkable
treeData={treeData}
indeterminateKeys={indeterminateKeys}
checkedKeys={checkedKeys}
onCheck={handleSelect}
/>
);
}
/**
* 递归获取所有叶子节点的 key
* @param {Array} treeData - 树形数据
* @returns {Array} - 叶子节点的 key 数组
*/
const getLeafKeys = (treeData) => {
const keys = [];
const traverse = (data) => {
data.forEach((item) => {
if (item.children && item.children.length > 0) {
traverse(item.children);
} else {
keys.push(item.key);
}
});
};
traverse(treeData);
return keys;
};
/**
* @param {Object} params - 配置参数
* @param {Array} params.treeData - 树形数据
* @param {Array} params.value - 选中的节点 key 数组(受控)
* @param {Function} params.onChange - 选中状态变化的回调函数
* @param {Boolean} params.parentLinkage - 是否启用父节点联动
* @returns {Object} - 多选框的状态和操作函数
*/
export const useTreeCheckbox = ({ treeData, value, onChange, parentLinkage }) => {
const [indeterminateKeys, setIndeterminateKeys] = useState([]); // 半选中的节点
// 使用 useMemo 计算叶子节点的 key,避免重复计算
const leafKeys = useMemo(() => getLeafKeys(treeData), [treeData]);
/**
* 获取选中节点往上的父节点和祖父节点,直到根节点
* @param {Array} treeData - 树形数据
* @param {Array} selectedKeys - 选中的节点 key 数组
* @returns {Array} - 包含选中节点的父节点和祖先节点的 key 数组
*/
const getParentKeys = (treeData, selectedKeys) => {
const parentKeys = new Set(); // 使用 Set 去重
/**
* 遍历树形结构查找父节点
* @param {Array} nodes - 当前节点的数据
* @param {Array} parentKeyPath - 当前节点的父节点路径
*/
const traverse = (nodes, parentKeyPath = []) => {
nodes.forEach((node) => {
const currentKeyPath = [...parentKeyPath, node.key]; // 当前节点的 key 路径(包含祖先节点)
// 如果当前节点是选中的节点,保存其所有祖先节点
if (selectedKeys.includes(node.key)) {
currentKeyPath.forEach((key) => parentKeys.add(key)); // 保存当前节点和所有父节点
}
// 如果当前节点有子节点,继续递归查找
if (node.children) {
traverse(node.children, currentKeyPath);
}
});
};
traverse(treeData); // 从根节点开始遍历
return Array.from(parentKeys); // 将 Set 转换为数组并返回
};
/**
* 处理选择框变化
* @param {Array} newCheckedKeys - 当前选中的节点 key 数组
* @param {Object} info - 包含半选中节点 key 和其他信息
*/
const handleSelect = (newCheckedKeys, info) => {
const halfCheckedKeys = info.halfCheckedKeys || [];
setIndeterminateKeys(halfCheckedKeys);
// 调用外部回调,通知父组件状态变化
if (onChange) {
onChange(newCheckedKeys, info);
}
};
// 处理回填值
const processedCheckedKeys = useMemo(() => {
const selectedKeys = value.filter((key) => leafKeys.includes(key)); // 只选中叶子节点
// 计算哪些父节点需要半选中
const newIndeterminateKeys = parentLinkage ? getParentKeys(treeData, selectedKeys) : [];
// 更新 indeterminateKeys
setIndeterminateKeys(newIndeterminateKeys);
return selectedKeys; // 返回选中的叶子节点
}, [value, parentLinkage, leafKeys, treeData]);
return {
checkedKeys: processedCheckedKeys, // 回填时过滤的选中节点
indeterminateKeys, // 半选中的节点
leafKeys, // 所有叶子节点
handleSelect, // 选中操作的处理函数
};
};
export default LinkedMultiSelectTree