有一种需求是单独选中父节点,而子节点不选中的情况,如图:
所以父节点要有三种状态可控制,点击父节点,先出现半选状态,再点击是全选状态, 之后点击就是全不选状态,所以有那么几种逻辑:
1. 父节点选中为半选状态,子节点不选中;
2. 父节点选中为勾选状态,子节点全部勾选;
3. 父节点取消选中,子节点全部取消勾选;
4. 子节点全部勾选,子节点最近一级的父节点是全选状态(再上一级根据旗下的子节点是否全选进行判断,以此类推...);
5. 子节点全部取消勾选,父节点不取消,呈半选状态。
梳理一下代码的大概逻辑:
先使用checkStrictly使父子节点状态不再受控,checkedKeys属性有两个参数可以用来控制全选和半选状态(checked是所有勾选中的id,halfChecked是半选的id),通过setCheckedKeys来控制状态即可。
打印出的参数就是这样:
接下来贴代码:
注:菜单树的数据我没有贴出来,涉及公司机密,数据就是树形数据形式。
<Tree
checkable
onCheck={onCheck}
treeData={treeInfo}
checkedKeys={checkedKeys}
checkStrictly
/>
const [checkedKeys,setCheckedKeys] = useState([]); //控制状态
const onCheck = (checkedKeys, event) => {
const { checked, halfChecked, children, key } = event.node; // 从参数解构出需要用到的数据
const {
checked: selectChecked, // 把checked存进selectChecked
halfChecked: halfSelectChecked, // 把halfChecked存进halfSelectChecked
} = checkedKeys;
// 开始判断有无子节点(children)的node
if(children && children.length > 0){
// 有children
if(!checked && !halfChecked){
// 半选
const nodes = getFathersById(event.node.key, treeInfo, 'id').filter(
v => v !== event.node.key,
); // 获取当前节点的所有父节点且过滤自身
const remindData = selectChecked.filter(function (v) {
return nodes.indexOf(v) === -1;
}); // 两个数组取交集 相同的部分取出来
halfSelectChecked.push(key);
setCheckedKeys({
checked: remindData.filter(v => v !== key), // 把该key从全选里面去掉
halfChecked: [...halfSelectChecked, ...nodes],
});
} else if(!checked && halfChecked){
// 全选
const childNodesFirst = getChildNode(treeInfo, event.node.key, []);
const nodes = getFathersById(event.node.key, treeInfo, 'id').filter(
v => v !== event.node.key,
); // 获取当前节点的所有父节点且过滤自身
let selectChecked1 = selectChecked;
let halfSelectChecked1 = halfSelectChecked;
let isSame;
nodes?.map(item => {
// 获取item下的所有子节点且包括item
const childNodes = getChildNode(treeInfo, item, []);
// 子节点全选时,点击全选状态变成空状态的交互
const selectChecked2 = selectChecked1;
isSame = includes(
[...childNodesFirst, ...selectChecked1],
childNodes.filter(v => v !== item),
); // 比较两个数组是否相同,后面的数组有一个没有在前面数组里面就false
selectChecked2.push(item);
selectChecked1 = isSame ? selectChecked2 : selectChecked1.filter(v => v
!== item);
halfSelectChecked1 = halfSelectChecked;
halfSelectChecked1.push(item);
return item;
});
setCheckedKeys({
checked: selectChecked1,
halfChecked: isSame ? halfSelectChecked1 : halfSelectChecked
});
// 父节点全选
setCheckedKeys({
checked: [...childNodesFirst, ...selectChecked1],
halfChecked: [...halfSelectChecked],
});
} else {
// 全不选(空)
const nodes = getFathersById(event.node.key, treeInfo, 'id').filter(
v => v !== event.node.key,
); // 获取当前节点的所有父节点且过滤自身
const childNodes = getChildNode(treeInfo, event.node.key, []);
const remindData = selectChecked.filter(function (v) {
return childNodes.indexOf(v) === -1;
}); // 两个数组取交集 相同的部分取出来
const remindData2 = remindData.filter(function (v) {
return nodes.indexOf(v) === -1;
});
// 把叶子节点的最近一级的父节点的半选去掉
const remindHalfData = halfSelectChecked.filter(function (v) {
return childNodes.indexOf(v) === -1;
});
setCheckedKeys({
checked: [...remindData2],
halfChecked: [...remindHalfData, ...nodes], // 第一次在全选状态点击二级节点时,父节点变成空状态不是半选状态
});
}
} else {
// 无children
const nodes = getFathersById(event.node.key, treeInfo, 'id').filter(
v => v !== event.node.key,
); // 获取当前节点的所有父节点且过滤自身
let selectChecked1 = [...new Set(selectChecked)];
let halfSelectChecked1 = halfSelectChecked;
let isSame;
let selectChecked2 = []; // 叶子节点全选反向选择的变量
nodes?.map(item => {
// 获取item下的所有子节点且包括item
const childNodes = getChildNode(treeInfo, item, []);
// 子节点全选时,点击全选状态变成空状态的交互
// const selectChecked2 = [...selectChecked1, ...nodes]; 这种会导致所有父节点都变成全选状态,但是实际爷节点是半选状态的时候,不需要变成全选
isSame = includes(
[...selectChecked, ...nodes], // 爷节点在孙子节点全选时没有变成全选状态
childNodes.filter(v => v !== item),
); // 比较两个数组是否相同,后面的数组有一个没有在前面数组里面就false
// 子节点全选时只把当前父节点变成全选,爷节点不需要变成全选
selectChecked2 = isSame ? [...selectChecked1, ...[item]] : selectChecked1.filter(v => v !== item);
selectChecked1 = selectChecked2;
halfSelectChecked1 = halfSelectChecked;
halfSelectChecked1.push(item);
return item;
});
setCheckedKeys({
checked: selectChecked1,
halfChecked: isSame ? halfSelectChecked1 : halfSelectChecked
});
}
}
上面有些用到的方法单独封装到一个文件里面进行调用:
/*
* 深度遍历树
* 一个递归方法
* @params id:当前节点的id
* @params data:原始的菜单树数据
* @params id:当前遍历节点的父节点id,初始为null(id)
*/
export const getFathersById = (id, data, prop = 'id') => {
const arr = [];
const rev = (data, IDS) => {
for(let i = 0, {length} = data; i < length; i++){
const node = data[i];
if (node[prop] === IDS) {
arr.unshift(node[prop]);
return true;
} else if (node.children && node.children.length) {
if(rev(node.children, IDS)){
arr.unshift(node[prop]);
return true;
}
}
}
return false;
};
rev(data, id);
return arr;
};
/*
* @Description:获取指定节点下的所有子节点
* @params nodes:原始的菜单树数据
* @params item:指定节点的id
* @params arr:用来接收值的空数组
*/
export const getChildNode = (nodes, item, arr) => {
for(const el of nodes){
if(el.id === item){
arr.push(el.id);
if(el.children){
childsNodeDeepWay(el.children, arr);
}
} else if (el.children) {
getChildNode(el.children, item, arr);
}
}
return arr;
};
const childsNodeDeepWay = (nodes, arr) => {
if(nodes){
nodes.forEach(ele => {
arr.push(ele.id);
if(ele.children){
childsNodeDeepWay(ele.children, arr);
}
});
}
};
/*
* @Description:比较两个数组是否相同,如果后一个数组里面有一个没有在前面一个数组的就为false
* @params arr1:数据相对更多的一个数组
* @params arr2:数据相对更少的一个数组
*/
export function includes(arr1, arr2){
return arr2.every(val => arr1.includes(val));
}
然后就是树组件的回显,我的项目是后台会把选中的数据返回一个字段,比如:isChecked: true,
通过把树形数据扁平化之后把所有isChecked: true的id放到一个空的数组里面。
// 放到useEffect里监听,我这里就不贴其余代码了
const arr = []; // 放所有isChecked: true的id
const getArr = _.cloneDeep(res?.data); // res?.data就是调接口拿到的数据
flatTree(getArr)?.filter(i => {
if(i.isChecked) {
arr.push(i.id);
}
return i;
}); // flatTree是一个扁平化的方法
// 勾选框回显
const checked = [];
const halfChecked = [];
arr.forEach(item => {
const value = getChildNode(getArr, item, []); // 获取item的所有子节点
const isSame = includes(arr, value); // 判断两个数组是否有交集
if (isSame) {
checked.push(item); // true即勾选
} else {
halfChecked.push(item); // false即半选
}
});
setCheckedKeys({ checked, halfChecked });
扁平化树形数据的方法:
/*
* 把树形数据转成一维数组---扁平化处理
* @param {any[]} treeData 原始数据
* @returns {any[]} 未获取到返回[]
*/
function flatTree(treeData: any[]) {
let result: any[] = [];
treeData?.forEach(ele => {
// 先克隆一份数据作为第一层级的填充
const arr = JSON.parse(JSON.stringify(ele));
delete arr?.children;
result.push(arr);
if (ele?.children && ele?.children?.length > 0) {
result = result.concat(flatTree(ele.children));
}
});
return result;
}
这样问题差不多就解决了,当然代码还有很多优化的空间,欢迎大家多多指教!