vue3笔记(23-2)el-tree在父子节点的联动

el-tree 在父子节点的联动上有点小麻烦,在刚开始设计时我并没有仔细研究 API,导致出现了好多问题,本篇是踩坑记录。
另有全选/全不选,全部展开/收起操作,本篇也做一个记录。

菜单权限设计

业务需求:在菜单权限设计中,新增权限状态下,选择子级菜单,对应的父级菜单也应该被赋予权限。
编辑权限状态下,打开权限设置时,应将已有权限做一个勾选。

父子节点默认非严格关联

check-strictly 属性表示在“显示复选框”的情况下,是否“严格的遵循父子不互相关联”的做法,默认为 false。此时,选择父级菜单,子级菜单会全部选中;选择子级菜单,父级前会有“-”标记:
父子非严格不互相关联选择父级菜单
父子非严格不互相关联选择子级菜单
当值设置为 true 时,选择父级菜单,子级菜单不会被选中:
父子严格不互相关联

方案一:非严格关联

在提交选择时,应该将子级菜单对应的父级也加入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
getAllNodes(treeRef.value!.getCheckedNodes(false, false))

const getAllNodes = (arr: Array) => {
let list = []
let pidList = []
arr.forEach((item) => {
list.push(item.id)
if (item.pid !== 0 && list.indexOf(item.pid) == -1) {
list.push(item.pid)
pidList.push(item.pid)
}
})
// menuTree 是树状图的数据源
if (state.menuTree && state.menuTree.length > 0) {
state.menuTree.forEach((sys) => {
if (sys.children && sys.children.length > 0) {
sys.children.forEach((i) => {
pidList.forEach((pid) => {
if (i.level !== 0 && i.id == pid) {
list.push(i.pid)
}
})
})
}
})
}
return list
}

提交时完美实现。
但是出现了一个之前欠考虑的问题,就是在编辑状态下(即新增时已有勾选,初始化时后端返回已勾选数组,需要前端在打开时勾选),因为父子非严格关联,导致父节点勾选时,对应所有子节点都被勾选。

方案二:严格关联

使用 changeCheck 方法,在节点被勾选时,将其所有父节点勾选。level 为组件自带属性,可以基于此进行节点递归。

1
2
3
4
5
6
7
8
9
<el-tree
ref="treeRef"
:data="menuTree"
show-checkbox
node-key="id"
:props="defaultProps"
@check="changeCheck"
:check-strictly="true"
/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const changeCheck = (node) => {
let thisNode = treeRef.value!.getNode(node.id) // 获取当前节点
const keys = treeRef.value!.getCheckedKeys() // 获取已勾选节点的key值
if (thisNode.checked) { // 当前节点若被选中
for (let i = thisNode.level; i > 1; i--) { // 判断是否有父级节点
if (!thisNode.parent.checked) {
// 父级节点未被选中,则将父节点替换成当前节点,往上继续查询,并将此节点key存入keys数组
thisNode = thisNode.parent
keys.push(thisNode.data.id)
}
}
}
treeRef.value!.setCheckedKeys(keys) // 将所有keys数组的节点全选
}

父子严格不互相关联
这个有个新问题就是,用户在操作时,可能将父节点取消勾选,在提交时就只剩下子节点了,所以需要在提交时再进行一次校验,将未加入(用户手动取消)的父节点再次加入。

1
2
3
4
5
6
7
8
9
10
const handleAddRoleMenu = () => {
getAllNodes(treeRef.value!.getCheckedNodes(false, false))
const nodeList = treeRef.value!.getCheckedKeys(false)
}

const getAllNodes = (arr: Array) => {
arr.forEach((item) => {
changeCheck(item)
})
}

提交时保证了所有被选择节点,及其父节点都被加入。

全选/全不选

1
2
3
4
5
6
<el-switch
v-model="changeSelectV"
:active-value="true"
:inactive-value="false"
@change="changeSelect"
/>
1
2
3
4
5
6
7
const changeSelect = () => {
if (changeSelectV.value == true) {
treeRef.value!.setCheckedNodes(state.menuTree)
} else {
treeRef.value!.setCheckedNodes([])
}
}

当tree 设置了check-strictly,通过 setCheckedNodes 全选方法失效,父子不关联,只能选中一级父节点,只能用遍历的方式选中节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const changeSelect = () => {
if (changeSelectV.value == true) {
travelNodes(state.menuTree)
} else {
treeRef.value!.setCheckedNodes([])
}
}

const travelNodes = (tmpRoot) => {
if (Array.isArray(tmpRoot)) {
tmpRoot.forEach((item) => {
treeRef.value!.setChecked(item.id, true, true)
if (item.children && item.children.length > 0) {
travelNodes(item.children)
}
})
}
}

全部展开/收起

1
2
3
4
5
6
<el-switch
v-model="changeCollapseV"
:active-value="true"
:inactive-value="false"
@change="changeCollapse"
/>
1
2
3
4
5
6
7
8
9
10
11
12
const changeCollapse = () => {
travelExpend(state.menuTree, changeCollapseV.value)
}

const travelExpend = (branch: Array, expend: boolean) => {
branch.forEach((item) => {
treeRef.value!.store.nodesMap[item.id].expanded = expend
if (item.children) {
travelExpend(item.children, expend)
}
})
}