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(),有驚喜哦


公眾號

往期文章

個人主頁