React antd 树组件自定义父子节点交互逻辑(包括全选,半选,全不选三种状态)使用checkStrictly使父子节点状态不再受控

文章详细阐述了如何在React应用中使用Tree组件,通过checkStrictly属性和checkedKeys状态来控制父节点的半选、全选和全不选状态。当点击节点时,根据节点是否有子节点以及子节点的状态,调整父节点和子节点的选中状态。同时,文章还介绍了扁平化树形数据的方法,用于回显选中状态。
摘要由CSDN通过智能技术生成

有一种需求是单独选中父节点,而子节点不选中的情况,如图:

所以父节点要有三种状态可控制,点击父节点,先出现半选状态,再点击是全选状态, 之后点击就是全不选状态,所以有那么几种逻辑:

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;
}

这样问题差不多就解决了,当然代码还有很多优化的空间,欢迎大家多多指教! 

  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值