基於elementui checkbox樹形多級聯動
背景
公司業務有個角色許可權設定的需求,資料可能有5到6層的許可權,本來是想直接使用elementui
的el-tree
元件的,奈何ui
難以修改,要做成公司想要的樣子,只好自己寫了。
資料結構
後臺返回的資料結構是這樣的:
介面許可權資料
{ code: 0, msg: null, data: [ { applicationModule: 'xxx', menuTreeList: [ { id: 40000, parentId: -1, children: [ { id: 40005, parentId: 40000, children: [], name: 'xxx', label: 'xxx', }, { id: 40002, parentId: 40000, children: [ { id: 40004, parentId: 40002, children: [ { id: 40006, parentId: 40004, children: [], name: 'xxx', label: 'xxx', }, { id: 40007, parentId: 40004, children: [], name: 'xxx', label: 'xxx', }, ], name: 'xxx', label: 'xxx', }, { id: 40003, parentId: 40002, children: [], name: 'xxx', label: 'xxx', }, ], name: 'xxx', label: 'xxx', }, { id: 40001, parentId: 40000, children: [ { id: 40012, parentId: 40001, children: [], name: 'xxx', label: 'xxx', }, { id: 40009, parentId: 40001, children: [ { id: 40015, parentId: 40009, children: [], name: 'xxx', label: 'xxx', }, { id: 40017, parentId: 40009, children: [], name: 'xxx', label: 'xxx', }, { id: 40016, parentId: 40009, children: [], name: 'xxx', label: 'xxx', }, ], name: 'xxx', label: 'xxx', }, { id: 40014, parentId: 40001, children: [ { id: 40021, parentId: 40014, children: [], name: 'xxx', label: 'xxx', }, { id: 40020, parentId: 40014, children: [], name: 'xxx', label: 'xxx', }, ], name: 'xxx', label: 'xxx', }, { id: 40011, parentId: 40001, children: [], name: 'xxx', label: 'xxx', }, { id: 40008, parentId: 40001, children: [], icon: null, name: 'xxx', label: 'xxx', }, { id: 40013, parentId: 40001, children: [ { id: 40018, parentId: 40013, children: [], name: 'xxx', label: 'xxx', }, { id: 40019, parentId: 40013, children: [], name: 'xxx', label: 'xxx', }, ], name: 'xxx', label: 'xxx', }, { id: 40010, parentId: 40001, children: [], name: 'xxx', label: 'xxx', }, ], name: 'xxx', label: 'xxx', }, ], name: 'xxx', label: 'xxx', }, ], }, ], }
後臺會返回一個數組,每個陣列物件對應一個選單,許可權資料都在menuTreeList
數組裡。
許可權選擇的ui
大概的樣子:
拆分元件
父元件
- 引入封裝好的元件
checkboxTree
,將需要的資料傳入。
<checkboxTree ref="checkTreeRef" :role-list="tableData"></checkboxTree>
- 編輯回顯時,呼叫子元件的方法
this.$refs.checkTreeRef.refurbishTreeCheckStatus(res.data, this.tableData)
- 將後臺返回的資料重新設定一下,給予初始的選中以及半選狀態
this.tableData = this.$refs.checkTreeRef.formatTreeData(res.data)
- 儲存許可權時,拿到所有已選擇許可權的
roleId
params.menuIds = this.$refs.checkTreeRef.returnAllCheckIds(this.tableData)
checkboxTree
元件
html
部分,寫第一級的許可權
<template> <div> <template v-for="item in roleList"> <template v-for="treeData in item.menuTreeList"> <div :key="treeData.id"> <p class="check-group"> <el-checkbox v-model="treeData.mychecked" :indeterminate="treeData.isIndeterminate" @change="handleCheckAllChange({ val: treeData, checked: $event })"> {{ treeData.name }} </el-checkbox> </p> <checkboxTreeRender :tree-data="treeData" @handle-check-all-change="handleCheckAllChange"></checkboxTreeRender> </div> </template> </template> </div> </template>
點選任何checkbox
,都會進入到handleCheckAllChange
方法,再通過findChildren
和findParent
方法不斷遞迴設定整個資料的選中以及半選狀態,程式碼如下:
handleCheckAllChange(data) {
let { val, checked } = data
if (val.children.length > 0) {
// 處理下級
this.findChildren(val.children, checked)
} else {
// 處理本級
val.children.forEach((v) => {
v.mychecked = checked
})
}
if (val.parentId !== -1) {
// 處理上級
this.findParent(this.roleList, val.parentId)
}
val.isIndeterminate = false
},
// 設定子級
findChildren(list, checked) {
list.forEach((child) => {
child.mychecked = checked
child.isIndeterminate = false
if (child.children.length > 0) {
this.findChildren(child.children, checked)
}
})
},
// 設定這一整條線
findParent(list, parentId) {
list.forEach((k) => {
if (k.menuTreeList) {
k.menuTreeList.forEach((child) => {
this.handleList(child, parentId)
})
} else {
this.handleList(k, parentId)
}
})
},
// 設定這一整條線具體方法
handleList(child, parentId) {
let parentCheckedLength = 0
let parentIndeterminateLength = 0
if (child.id === parentId) {
child.children.forEach((children) => {
if (children.isIndeterminate) {
parentIndeterminateLength++
} else if (children.mychecked) {
parentCheckedLength++
}
})
child.mychecked = parentCheckedLength === child.children.length
child.isIndeterminate = (parentIndeterminateLength > 0 || parentCheckedLength > 0) && parentCheckedLength < child.children.length
if (child.parentId !== -1) {
this.findParent(this.roleList, child.parentId)
}
} else if (child.children.length > 0) {
this.findParent(child.children, parentId)
}
},
這是主要checkbox
選擇互動的聯動邏輯,下面是一些工具方法,主要是用於業務儲存時需要傳遞許可權id
,以及初始拿到後臺資料時需要format
一下,程式碼如下:
const returnCheckTree = (data, checkArr = []) => {
data.forEach((v) => {
if (v.mychecked || v.isIndeterminate) {
!checkArr.includes(v.id) && checkArr.push(v.id)
}
if (v.children && v.children.length) {
returnCheckTree(v.children, checkArr)
}
})
return checkArr
}
const fmtTreeData = (data) => {
data.forEach((v) => {
v.mychecked = false
v.isIndeterminate = false
if (v.children && v.children.length > 0) {
fmtTreeData(v.children)
}
})
return data
}
// 返回所有已選或許可權的role
returnAllCheckIds(currentData) {
let roleIds = []
currentData.forEach((k) => {
roleIds = [...returnCheckTree(k.menuTreeList), ...roleIds]
})
return roleIds.join(',')
},
// 初始化樹狀資料
formatTreeData(currentData) {
currentData.forEach((k) => {
fmtTreeData(k.menuTreeList)
})
return currentData
},
最後,編輯角色時需要回顯角色許可權,後臺返回給我的資料結構和全部許可權是一致的,只是只會返回已經選擇的許可權資料,當然,對我來說,什麼結構都無所謂,因為我這種做法,實際上是要遞迴把所有許可權id
丟到一個數組裡面,
我的思路是先拿到所有的許可權id
陣列放到roleIds
裡,然後將所有許可權id
在roleIds
裡的物件設定為已選,再重新去設定半選,當前物件是已選,但children
物件的已選比children
的長度少,說明當前物件是半選。程式碼如下:
const returnEditRoleTreeIds = (data, checkArr = []) => {
data.forEach((v) => {
!checkArr.includes(v.id) && checkArr.push(v.id)
if (v.children && v.children.length) {
returnEditRoleTreeIds(v.children, checkArr)
}
})
return checkArr
}
// 編輯時回顯許可權資料
refurbishTreeCheckStatus(checkData, allData) {
let roleIds = []
let firstLevelIds = []
let notFirstLevelIds = []
checkData.forEach((k) => {
roleIds = [...returnEditRoleTreeIds(k.menuTreeList), ...roleIds]
})
allData.forEach((k) => {
this.setTreeCheckStatus(k.menuTreeList, roleIds)
})
allData.forEach((k) => {
this.setTreeIndeterminateStatus(k.menuTreeList)
})
},
// 所有已選擇的role全部設定為已選
setTreeCheckStatus(data, roleIds = []) {
data.forEach((v) => {
if (roleIds.includes(v.id)) {
v.mychecked = true
}
if (v.children && v.children.length) {
this.setTreeCheckStatus(v.children, roleIds)
}
})
},
// 重新遞迴設定半選狀態
setTreeIndeterminateStatus(data) {
data.forEach((v) => {
let parentCheckedLength = 0
let parentIndeterminateLength = 0
v.children.forEach((children) => {
if (children.isIndeterminate) {
parentIndeterminateLength++
} else if (children.mychecked) {
parentCheckedLength++
}
})
v.isIndeterminate = (parentIndeterminateLength > 0 || parentCheckedLength > 0) && parentCheckedLength < v.children.length
if (v.children && v.children.length) {
this.setTreeIndeterminateStatus(v.children)
}
})
},
應該不是最好的思路,各位有更好的建議可以在評論區告訴我。
checkboxTreeRender
元件
這個元件主要是遞迴元件,去渲染樹形dom
結構。
<template>
<div>
<div v-if="treeData.children && treeData.children.length" style="padding-left: 24px">
<div v-for="childrenData in treeData.children" :key="childrenData.id" :style="returnStyle(childrenData.children)">
<el-checkbox
v-model="childrenData.mychecked"
style="margin-bottom: 15px"
:indeterminate="childrenData.isIndeterminate"
:label="childrenData.id"
@change="handleCheckAllChange({ val: childrenData, checked: $event })"
>
{{ childrenData.name }}
</el-checkbox>
<checkboxTreeRender :tree-data="childrenData" @handle-check-all-change="handleCheckAllChange"></checkboxTreeRender>
</div>
</div>
</div>
</template>
接收一個數據物件
props: {
treeData: {
type: Object,
default: function () {
return {}
},
},
},
以及將checkbox
變化的方法拋給父元件去處理,這個元件只負責渲染
returnStyle(child) {
const premise = child && child.length
return {
display: premise ? '' : 'inline-block',
marginRight: premise ? '' : '30px',
}
},
handleCheckAllChange(data) {
this.$emit('handle-check-all-change', data)
},
至此,一個基於elementui
的多層checkbox
樹形聯動元件就寫好了。
結語
最開始需求是說最多隻有三層結構,所以我就寫了一版寫死的三層聯動的邏輯,使用了checkboxGroup
,只需要在checkboxGroup
上進行監聽就能拿到下面所有選擇的checkbox
。後面說要支援更多層,發現當初這樣子已經無法實現,當初寫的太呆了,
於是重新寫了一版,通過這次對遞迴的使用也有了一些理解,因為以前很少使用這個,也算是學習到了,記錄一下。