element-ui的Tree樹組件使用技巧

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

往期文章

個人主頁

Tags: