背景
公司業務有個角色權限設置的需求,數據可能有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
。后面說要支持更多層,發現當初這樣子已經無法實現,當初寫的太呆了,
於是重新寫了一版,通過這次對遞歸的使用也有了一些理解,因為以前很少使用這個,也算是學習到了,記錄一下。
全部源碼放到github
上了,傳送門