element-ui的Tree树组件使用技巧

2021-09-24

1,前言


最近这段时间在做一个新的模块,其中有一个三层的树结构,产品经理提出了一个很古怪的需求,整的我只能自己控制树的交互,写完之后,感觉对这个组件的用法有了不一样的了解,故而写下来。

2,需求


  • 如果上级节点勾选了,则底下所有节点也勾选
  • 如果是一个个勾选子级节点,直至勾选满所有子级,则该父级节点不能勾选,只能算选中状态
  • 已勾选的节点不能展开,如果是展开了再勾选的,要自动收缩回去

遇见问题:

问题1:后端数据不友好,无唯一key值(有重复key),导致Tree组件无唯一的key

问题2:后端数据不友好,第一层第二层的字段和第三层的字段不一致(第一层字段是dept_id,子集字段是children,第二层子集字段是porjs,第三层字段又是porj_id

问题3:不能使用check-strictly,也就是Tree组件自带的父子关联,只能手动控制checkbox的选中状态

问题4:提交给后端的数据,如果一级二级节点被勾选,则不用传递其下层结构,如果不是被勾选,则需要传递其下层结构

如图:
父子不互相关联
不过还好这个树结构只有三层,办法还是有的。(如果是未知层级就难了)

3,解决思路


问题1:无唯一key

这个好办,接口请求到数据之后,深拷贝一份,遍历一下,给id手动添加字符来使它们变成唯一的,最后提交的时候去掉前面添加的字符

// 将所有id根据层级加上壹,贰,叁
handlePushLabel(data) {
  try {
    data.forEach(item1 => {
      item1.dept_id += '壹'
      if (item1.children && item1.children.length > 0) {
        item1.children.forEach(item2 => {
          item2.dept_id += '贰'
          item2.parent_id += '壹'
          if (item2.children.length > 0) {
            item2.children.forEach(item3 => {
              item3.dept_id += '叁'
              item3.parent_id += '贰'
            })
          }
        })
      }
    })
    return data
  } catch (error) {
    console.warn(error)
  }
}
// 将数据的key恢复为原来的
treeList.forEach(item1 => {
  item1.dept_id = item1.dept_id.replace('壹', '')
  if (item1.children.length > 0) {
    item1.children.forEach(item2 => {
      item2.dept_id = item2.dept_id.replace('贰', '')
      item2.parent_id = item2.parent_id.replace('壹', '')
      if (item2.children.length > 0) {
        item2.children.forEach(item3 => {
          item3.dept_id = item3.dept_id.replace('叁', '')
          item3.parent_id = item3.parent_id.replace('贰', '')
        })
      }
    })
  }
})

问题2:第一层第二层的字段和第三层的字段不一致

这个也好办,最好的办法是后端调整成一样的,但是如果碰见博主这样的无法沟通的后端,只能前端自己转换字段了,这里采用的是forEach遍历,然后使用map替换对象键名。

// 将树数据的projs字段和proj_id和proj_name改名
handleChangeKey(data) {
  try {
    const tree = data
    tree.forEach(item => {
      if (item.children) {
        const arr = item.children
        // 将projs字段转为children
        item.children = arr.map(item1 => {
          if (item1.projs.length > 0) {
            const obj = item1.projs
            const parent_id = item1.dept_id
            // 将proj_id字段转为dept_id 将proj_name字段转为dept_name
            // 并添加depth深度和父节点id
            item1.projs = obj.map(item2 => {
              return {
                dept_id: item2.proj_id,
                dept_name: item2.proj_name,
                depth: 3,
                parent_id
              }
            })
          }
          return {
            dept_id: item1.dept_id,
            dept_name: item1.dept_name,
            depth: item1.depth,
            parent_id: item1.parent_id,
            children: item1.projs
          }
        })
      }
    })
    return this.handlePushLabel(tree)
  } catch (error) {
    console.warn(error)
  }
}

问题3:不能使用check-strictly

这个就比较繁琐了,不能使用Tree自带的勾选父子关联(原因看需求2),只能自己手写一二三级节点的勾选逻辑。这样的话,二级和三级节点需要有个parent_id字段,也就是其父级的id,且有一个depth字段,代表其深度1,2,3

<el-tree
  @check-change="handleTreeClick"
  :data="treeList"
  show-checkbox
  :default-expand-all="false"
  :check-strictly="true"
  @node-expand="handleTreeOpen"
  node-key="dept_id"
  ref="tree"
  highlight-current
  :props="defaultProps"
/>

Tree组件加上ref属性,设置check-strictlytrue,利用@check-change监听节点勾选,利用@node-expand监听节点展开收起,设置node-key为每个节点的id

思路是:通过@check-change的时间回调,拿到第一个参数data,这个data里包含该节点的数据,通过这个数据可以拿到depth判断他是第几层节点,还可以拿到parent_id找到它的上级节点。根据这个区分一二三级节点,然后通过获取到的id,使用this.$refs.tree.getNode(id)可以获取到节点Node。设置节点Nodecheckedtrue,则该节点会变成勾选状态。设置它的indeterminatetrue,则会变成选中状态,设置expandedtrue,则是展开状态。也可以通过this.$refs.tree.setChecked(id, true)来设置选中。

问题4:提交给后端的数据

这个就是坑了,需要先把之前改变的key变回去,还有子级的键名改回去,然后根据是勾选还是只是单纯的选中来拼接数据。在这里用到了getCheckedNodes来获取目前被选中的节点所组成的数组,也用到了getHalfCheckedNodes获取半选中的节点所组成的数组。

4,完整代码


export default {
  // 将树数据的projs字段和proj_id和proj_name改名
  handleChangeKey(data) {
    try {
      const tree = data
      tree.forEach(item => {
        if (item.children) {
          const arr = item.children
          // 将projs字段转为children
          item.children = arr.map(item1 => {
            if (item1.projs.length > 0) {
              const obj = item1.projs
              const parent_id = item1.dept_id
              // 将proj_id字段转为dept_id 将proj_name字段转为dept_name
              // 并添加depth深度和父节点id
              item1.projs = obj.map(item2 => {
                return {
                  dept_id: item2.proj_id,
                  dept_name: item2.proj_name,
                  depth: 3,
                  parent_id
                }
              })
            }
            return {
              dept_id: item1.dept_id,
              dept_name: item1.dept_name,
              depth: item1.depth,
              parent_id: item1.parent_id,
              children: item1.projs
            }
          })
        }
      })
      return this.handlePushLabel(tree)
    } catch (error) {
      console.warn(error)
    }
  },
  // 将所有id根据层级加上壹,贰,叁
  handlePushLabel(data) {
    try {
      data.forEach(item1 => {
        item1.dept_id += '壹'
        if (item1.children && item1.children.length > 0) {
          item1.children.forEach(item2 => {
            item2.dept_id += '贰'
            item2.parent_id += '壹'
            if (item2.children.length > 0) {
              item2.children.forEach(item3 => {
                item3.dept_id += '叁'
                item3.parent_id += '贰'
              })
            }
          })
        }
      })
      return data
    } catch (error) {
      console.warn(error)
    }
  },
  /**
  * 树的选中状态发生变化时
  * @param {Object} data 该节点的数据
  * @param {Object} on 节点本身是否被选中
  * @param {Object} child 节点的子树中是否有被选中的节点
  */
  handleTreeClick(data, on, child) {
    try {
      this.form.tree = data
      if (data.depth === 1) {
        this.handleOneNode(on, data)
      } else if (data.depth === 2) {
        this.handleTwoNode(on, data)
      } else if (data.depth === 3) {
        this.handleThreeNode(on, data)
      }
    } catch (error) {
      console.warn(error)
    }
  },
  /**
  * 一级节点处理
  * @param {Boolean} on 是否被选中
  * @param {Object} data 当前节点的数据
  */
  handleOneNode(on, data) {
    try {
      const tree = this.$refs.tree
      // 如果当前节点未被选中且为半选状态
      const node = tree.getNode(data.dept_id)
      if (node.indeterminate && !node.checked) return
      // 如果当前节点被选中则不能展开
      if (node.checked && node.expanded) node.expanded = false
      // 勾选所有下级
      let arr = []
      if (data.children.length > 0) {
        data.children.forEach(item => {
          // 筛选出所有的下级key
          arr.push(item.dept_id)
          if (item.children.length > 0) {
            item.children.forEach(child => {
              // 筛选出所有的下下级key
              arr.push(child.dept_id)
            })
          }
        })
      }
      // 选中or取消
      if (on) {
        arr.forEach(dept => {
          tree.setChecked(dept, true)
        })
      } else {
        arr.forEach(dept => {
          tree.setChecked(dept, false)
        })
      }
    } catch (error) {
      console.warn(error)
    }
  },
  /**
  * 二级节点处理
  * @param {Boolean} on 是否被选中
  * @param {Object} data 当前节点的数据
  */
  handleTwoNode(on, data) {
    try {
      const tree = this.$refs.tree
      const node = tree.getNode(data.dept_id)
      // 如果当前是半选
      if (node.indeterminate && !node.checked) return
      // 如果当前节点被选中则不能展开
      if (node.checked && node.expanded) node.expanded = false
      // 上级节点
      const parentNode = tree.getNode(data.parent_id)
      // 勾选所有下级
      let arr = []
      if (data.children.length > 0) {
        data.children.forEach(item => {
          // 筛选出所有的下级key
          arr.push(item.dept_id)
        })
      }
      // 选中or取消
      if (on) {
        arr.forEach(dept => {
          tree.setChecked(dept, true)
        })
        // 如果上级节点不是被勾选则让上级节点半勾选
        if (!parentNode.checked) {
          parentNode.indeterminate = true
        }
      } else {
        // 先取消所有下级勾选
        arr.forEach(dept => {
          tree.setChecked(dept, false)
        })
        // 如果上级节点被勾选则让上级节点半勾选
        if (parentNode.checked) {
          parentNode.indeterminate = true
          // 如果上级是半选,则循环判断下级是否还存在勾选的,来决定上级是否需要去掉半选
        } else if (parentNode.indeterminate) {
          const parentData = parentNode.data || []
          let bool = true
          const children = parentData.children
          const childArr = []
          // 筛选出所有兄弟节点的key
          if (children && children.length > 0) {
            children.forEach(childItem => {
              childArr.push(childItem.dept_id)
            })
          }
          // 循环判断
          if (childArr.length > 0) {
            for (let i of childArr) {
              let thisNode = tree.getNode(i)
              // 如果有一个是勾选或者半选
              if (thisNode.checked || thisNode.indeterminate) {
                bool = false
              }
            }
          }
          if (bool) {
            parentNode.indeterminate = false
          }
        }
      }
    } catch (error) {
      console.warn(error)
    }
  },
  /**
  * 三级节点处理
  * @param {Boolean} on 是否被选中
  * @param {Object} data 当前节点的数据
  */
  handleThreeNode(on, data) {
    try {
      // 1,如果勾选了,上级节点没选,则把上级节点和上上级改为半选
      // 2,如果取消了,上级节点如果是勾选,则把上级节点和上上级改为半选
      const tree = this.$refs.tree
      // 上级节点
      console.log(data)
      const parentNode = tree.getNode(data.parent_id)
      const forefathersKey = parentNode.data.parent_id
      // 祖先节点
      console.log(parentNode)
      console.log(forefathersKey)
      const forefathersNode = tree.getNode(forefathersKey)
      console.log(forefathersNode)
      // 如果当前节点被勾选
      if (on) {
        // 如果上级节点未被勾选,则让他半选
        if (!parentNode.checked) {
          parentNode.indeterminate = true
        }
        // 如果祖先节点未被勾选,则让他半选
        if (!forefathersNode.checked) {
          forefathersNode.indeterminate = true
        }
        // 如果当前节点是被取消勾选
      } else {
        const parentArr = []
        const forefathersArr = []
        const parentData = parentNode.data
        const forefathersData = forefathersNode.data
        let parentBool = true
        let forefathersBool = true
        // 筛选出所有兄弟key,如果有勾选的则代表上级不需要去除勾选
        if (parentData.children.length > 0) {
          parentData.children.forEach(parent => {
            parentArr.push(parent.dept_id)
          })
          for (let i of parentArr) {
            let thisNode = tree.getNode(i)
            if (thisNode.checked) {
              parentBool = false
            }
          }
        }
        // 为tree则代表没有三级节点被勾选,此时上级去除勾选
        if (parentBool) {
          parentNode.checked = false
          parentNode.indeterminate = false
        } else {
          parentNode.indeterminate = true
        }
        // 筛选出所有上级的兄弟key,如果有勾选的则代表上级不需要去除勾选
        if (forefathersData.children.length > 0) {
          forefathersData.children.forEach(parent => {
            forefathersArr.push(parent.dept_id)
          })
          for (let i of forefathersArr) {
            let thisNode = tree.getNode(i)
            if (thisNode.checked || thisNode.indeterminate) {
              forefathersBool = false
            }
          }
        }
        if (forefathersBool) {
          forefathersNode.indeterminate = false
        }
      }
    } catch (error) {
      console.warn(error)
    }
  },
  /**
  * 树被展开时
  * @param {Object} data 该节点的数据
  * @param {Object} node 节点对应的Node
  * @param {Object} ref 节点组件
  */
  handleTreeOpen(data, node) {
    // 如果节点被选中则不让展开
    if (node.checked) {
      Tip.warn('当前层级已被全选,无法展开!')
      node.expanded = false
    }
  },
  // 拼接出需要的树数据
  handleJoinTree() {
    try {
      const tree = this.$refs.tree
      const treeList = _.cloneDeep(this.treeList)
      // 被选中的节点
      const onItem = tree.getCheckedNodes()
      // 半选中的节点
      const halfItem = tree.getHalfCheckedNodes()
      const oneArr = []
      const twoArr = []
      const threeArr = []
      const oneArr_ = []
      const twoArr_ = []
      const threeArr_ = []
      // 节点分层
      if (onItem.length > 0) {
        onItem.forEach(item => {
          switch (item.depth) {
            case 1:
              oneArr.push(item.dept_id)
              break
            case 2:
              twoArr.push(item.dept_id)
              break
            case 3:
              threeArr.push(item.dept_id)
              break
          }
        })
      }
      if (halfItem.length > 0) {
        halfItem.forEach(item => {
          switch (item.depth) {
            case 1:
              oneArr_.push(item.dept_id)
              break
            case 2:
              twoArr_.push(item.dept_id)
              break
            case 3:
              threeArr_.push(item.dept_id)
              break
          }
        })
      }
      const oneList = this.handlejoinOne(treeList, oneArr, oneArr_)
      const twoList = this.handlejoinTwo(treeList, twoArr, twoArr_)
      const threeList = this.handlejoinThree(treeList, threeArr, threeArr_)
      // 将第二层拼进第一层
      oneList.forEach(item => {
        twoList.forEach(item2 => {
          if (item2.parent_id === item.dept_id) {
            if (!item.isOn) {
              item.children.push(item2)
            }
          }
        })
      })
      // 将第三层拼进第二层
      oneList.forEach(child1 => {
        if (child1.children.length > 0) {
          child1.children.forEach(child2 => {
            threeList.forEach(child3 => {
              if (child3.parent_id === child2.dept_id) {
                if (!child2.isOn) {
                  child2.children.push(child3)
                }
              }
            })
          })
        }
      })
      return oneList
    } catch (error) {
      console.warn(error)
    }
  },
  // 返回第一层
  handlejoinOne(treeList, oneArr, oneArr_) {
    try {
      // 找出第一层节点
      const oneList = []
      treeList.forEach(item => {
        for (let i of oneArr) {
          if (item.dept_id === i) {
            oneList.push({
              dept_id: item.dept_id,
              children: [],
              isOn: true,
              name: item.dept_name
            })
          }
        }
        for (let i of oneArr_) {
          if (item.dept_id === i) {
            oneList.push({
              dept_id: item.dept_id,
              children: [],
              isOn: false,
              name: item.dept_name
            })
          }
        }
      })
      return oneList
    } catch (error) {
      console.warn(error)
    }
  },
  // 返回第二层
  handlejoinTwo(treeList, twoArr, twoArr_) {
    try {
      const twoList = []
      treeList.forEach(item => {
        if (item.children.length > 0) {
          item.children.forEach(item2 => {
            for (let i of twoArr) {
              if (item2.dept_id === i) {
                twoList.push({
                  dept_id: item2.dept_id,
                  children: [],
                  isOn: true,
                  parent_id: item2.parent_id,
                  name: item2.dept_name
                })
              }
            }
            for (let i of twoArr_) {
              if (item2.dept_id === i) {
                twoList.push({
                  dept_id: item2.dept_id,
                  children: [],
                  isOn: false,
                  parent_id: item2.parent_id,
                  name: item2.dept_name
                })
              }
            }
          })
        }
      })
      return twoList
    } catch (error) {
      console.warn(error)
    }
  },
  // 返回第三层
  handlejoinThree(treeList, threeArr, threeArr_) {
    try {
      const threeList = []
      treeList.forEach(item => {
        if (item.children.length > 0) {
          item.children.forEach(item2 => {
            if (item2.children.length > 0) {
              item2.children.forEach(item3 => {
                for (let i of threeArr) {
                  if (item3.dept_id === i) {
                    threeList.push({
                      dept_id: item3.dept_id,
                      isOn: true,
                      parent_id: item3.parent_id,
                      name: item3.dept_name
                    })
                  }
                }
                for (let i of threeArr_) {
                  if (item3.dept_id === i) {
                    threeList.push({
                      dept_id: item3.dept_id,
                      isOn: false,
                      parent_id: item3.parent_id,
                      name: item3.dept_name
                    })
                  }
                }
              })
            }
          })
        }
      })
      return threeList
    } catch (error) {
      console.warn(error)
    }
  },
  // 将数据的key恢复为原来的
  handleRestoreKey() {
    try {
      const treeList = this.handleJoinTree()
      // 去掉id后面的壹 贰 叁
      treeList.forEach(item1 => {
        item1.dept_id = item1.dept_id.replace('壹', '')
        if (item1.children.length > 0) {
          item1.children.forEach(item2 => {
            item2.dept_id = item2.dept_id.replace('贰', '')
            item2.parent_id = item2.parent_id.replace('壹', '')
            if (item2.children.length > 0) {
              item2.children.forEach(item3 => {
                item3.dept_id = item3.dept_id.replace('叁', '')
                item3.parent_id = item3.parent_id.replace('贰', '')
              })
            }
          })
        }
      })
      // 将dept_id字段转为proj_id将dept_name字段转为proj_name,将children转为projs
      treeList.forEach(child1 => {
        if (child1.children.length > 0) {
          const childObj = child1.children.map(item => {
            let returnObj = {}
            if (item.children.length > 0) {
              const obj = item.children
              obj.children = obj.map(child2 => {
                return {
                  proj_id: child2.dept_id,
                  proj_name: child2.name
                }
              })
              returnObj = {
                dept_id: item.dept_id,
                dept_name: item.name,
                projs: obj.children
              }
            } else {
              returnObj = {
                projs: [],
                dept_id: item.dept_id,
                isOn: true,
                name: item.name
              }
            }
            return returnObj
          })
          child1.children = childObj
        }
      })
      console.log(treeList)
      return treeList
    } catch (error) {
      console.warn(error)
    }
  },
  // 详情设置树勾选
  handleSetTree(list) {
    try {
      console.log(list)
      const one = []
      const two = []
      const three = []
      if (list.length > 0) {
        // 第一层
        list.forEach(item => {
          let child = item.children || ''
          let obj = { id: item.dept_id + '壹', isOn: true }
          if (child && child.length > 0) {
            obj.isOn = false
          }
          one.push(obj)
        })
        // 第二层
        list.forEach(item1 => {
          let child1 = item1.children || ''
          if (child1 && child1.length > 0) {
            child1.forEach(item2 => {
              let child2 = item2.projs || ''
              let obj = { id: item2.dept_id + '贰', isOn: true }
              if (child2 && child2.length > 0) {
                obj.isOn = false
              }
              two.push(obj)
            })
          }
        })
        // 第二层
        list.forEach(item1 => {
          let child1 = item1.children || ''
          if (child1 && child1.length > 0) {
            child1.forEach(item2 => {
              let child2 = item2.projs || ''
              if (child2 && child2.length > 0) {
                child2.forEach(item3 => {
                  let obj = { id: item3.proj_id + '叁', isOn: true }
                  three.push(obj)
                })
              }
            })
          }
        })
        const tree = this.$refs.tree
        // 勾选第一层
        if (one && one.length > 0) {
          one.forEach(item => {
            let node = tree.getNode(item.id)
            if (item.isOn) {
              node.checked = true
              this.handleOneNode(true, node.data)
            } else {
              node.indeterminate = true
            }
          })
        }
        // 勾选第二层
        if (two && two.length > 0) {
          two.forEach(item => {
            let node = tree.getNode(item.id)
            if (item.isOn) {
              node.checked = true
              this.handleTwoNode(true, node.data)
            } else {
              node.indeterminate = true
            }
          })
        }
        // 勾选第三层
        if (three && three.length > 0) {
          three.forEach(item => {
            let node = tree.getNode(item.id)
            node.checked = true
          })
        }
      }
    } catch (error) {
      console.warn(error)
    }
  }
}

获取转换后的结构:

this.treeList = this.handleChangeKey(data)

提交转换后的结构:

const treeList = this.handleRestoreKey()

5,总结


如果你有用到Tree组件,且产品出的需求不咋地,可以看看Tree常用这些方法技巧;

  • 获取指定ID的节点:this.$refs.tree.getNode(id)

  • 返回目前半选中的节点所组成的数组:this.$refs.tree.getHalfCheckedNodes()

  • 返回目前被选中的节点所组成的数组:this.$refs.tree.getCheckedNodes()

  • 通过 key / data 设置某个节点的勾选状态:this.$refs.tree.setChecked(id, true)


如果看了觉得有帮助的,我是@鹏多多,欢迎 点赞 关注 评论;END


PS:在本页按F12,在console中输入document.querySelectorAll('.diggit')[0].click(),有惊喜哦


面向百度编程

公众号

weixinQRcode.png

往期文章

个人主页