基于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
。后面说要支持更多层,发现当初这样子已经无法实现,当初写的太呆了,
于是重新写了一版,通过这次对递归的使用也有了一些理解,因为以前很少使用这个,也算是学习到了,记录一下。
全部源码放到github
上了,传送门