树形选择处理

2,289 阅读2分钟

需求

在树状结构的数据显示中,未知层数。均存在选择框,需要做勾选逻辑的处理。规则如下

  • 勾选有三种状态,全勾、半勾、不勾。
  • 当某个节点勾选并且有子节点时,将当前节点勾上,并且将其所有的子节点全部勾选。
  • 当某个节点勾选并且是作为子节点时,若兄弟节点均勾选上,则将父节点全勾;若兄弟节点只勾选一部分,则父节点半勾。
  • 以上两条规则除了考虑勾选逻辑,同时需要考虑取消勾选的逻辑。

分析

当前已有的数据结构list如下,checked作为勾选标志,Y为全勾,P为半勾,N为不勾。每个节点均有可能存在子节点,也可能为空。

[{
    id: 1,
    parentId: null,
    checked: 'P',
    children: [{
        id: 4,
        parentId: 1,
        checked: 'Y',
        children: [...],
    }, {
        id: 5,
        parentId: 1,
        checked: 'P',
        children: [...],
    }],
}, {
    id: 2,
    parentId: null,
    checked: 'Y'
    children: [...],
}, {
    id: 3,
    parentId: null,
    checked: 'N'
    children: [...],
}]

这里有个特别的地方是需要对父节点的内容做操作,而由于目前的数据结构从子节点去获取父节点其实是比较困难的,所以添加一个map的数据结构,用来方便子节点对父节点的获取。map的数据结果如下。

Map() {
    1: {
        id: 1,
        parentId: null,
        checked: 'P',
        children: [...],
    },
    2: {
        id: 2,
        parentId: null,
        checked: 'Y'
        children: [...],
    },
    3: {
        id: 3,
        parentId: null,
        checked: 'N'
        children: [...],
    },
    4: {
        id: 4,
        parentId: 1,
        checked: 'Y',
        children: [...],
    },
    ...
}

因为在生成map的过程不做克隆操作,而是直接赋值地址空间,所以map中value指向的对象和list中指向的对象是相同的,并没有占用多余的空间。这一个生成的过程需要使用递归。

实现

实现关键的部分,首先是通过list去生成一个map的对象。

map = new Map();
getMap(list){
    if(list && list.length>0){
        list.forEach(item => {
            map.set(item.id, item);
            getMap(item.children); // 递归调用
        })   
    }
}

勾选的时候,调用的是 handleCheck 方法。

// 对子节点做处理
handleChildren(record, checked){
    record.checked = checked ? 'Y' : 'N'; // 对当前节点赋值,再遍历其子节点
    if(record.children && record.children.length > 0){
        record.children.forEach(item => handleChildren(item, checked)); // 递归调用
    }
}

// 对父节点的处理
handleParent(record){
    // 看所有子节点的状态。
    let checkAll = true;
    let uncheckAll = true;
    const lastChecked = record.checked; // 记录原来的状态
    record.children.forEach(item => {
        // 当不全勾选时,则 checkAll 为false
        if(item.checked === 'P' || item.checked === 'N') 
            checkAll = false;
        // 当至少有一个勾选的时,uncheckAll为false
        if(item.checked === 'P' || item.checked === 'Y') 
            uncheckAll = false;
    });
    if(checkAll){ // 全选则为 Y
        record.checked = 'Y';
    } else if(uncheckAll){ // 全不选则为 N
        record.checked = 'N';
    } else { // 不全选则为 P
        record.checked = 'P';
    }
    
    if(record.parentId && lastChecked!==record.checked){
        // 父节点存在并且当前节点的状态改变了,才需要继续往父节点操作。
        const parentNode = map.get(record.parentId);
        handleParent(parentNode); // 递归调用
    }
}

// 勾选的调用方法
handleCheck(id, checked){
    const record = map.get(id); // 获取当前操作的对象
    // 先对子节点做遍历并勾选
    handleChildren(record, checked);
    
    // 然后处理对父节点的影响
    if(record.parentId) { // 存在则处理
        const parentNode = map.get(record.parentId);
        handleParent(parentNode);
    }
    
    // 最后统一做界面更新,设置state
    // ...
}