项目中我们用的Tree组件一般都是父子节点关联的,但是产品提出了个需求,没办法使用组件自带的逻辑了,只能自定义逻辑进行管理选中节点。需求如下:
1. 父节点选中所有子节点也要选中
2. 父节点取消勾选子节点也要全部取消
3. 子节点勾选所有父节点都要勾选
4. 子节点取消父节点不取消
效果如下:
首先我们看一下antd tree这个组件需要什么:
<a-tree
v-if="authTreeList && authTreeList.length > 0"
v-model="checkedAuthKeys"
checkable
:expanded-keys="expandedKeys"
:auto-expand-parent="autoExpandParent"
:tree-data="authTreeList"
@check="onCheck"
checkStrictly
/>
- v-moel对应是选中的id集
- checkable添加复选框功能
- expanded-keys展开指定的树节点
- auto-expand-parent是否自动展开父节点
- tree-data树型数据
- check选中复选框触发
- checkStrictly节点选择完全受控(父子节点选中状态不再关联)
首先后端返回的权限数据是这样的:
checked是否选中,pId代表父节点,所以我们首先要把数据转换成树形数据以供tree组件使用
/**
* 获取树状结构
* 通过定义map,key为当前对象id,value为该对象
* 遍历集合,得到对象顶级节点放到集合中返回
* 不是顶级的就是当前对象得子节点,将对象放到该节点下
*/
export const toTree = (nodes) => {
let result = [];
//如果值是 Array,则为true; 否则为false。
if (!Array.isArray(nodes)) {
return result;
}
//深拷贝,否则会影响原数组
let node = JSON.parse(JSON.stringify(nodes));
//根据父节点进行拼接子节点,
node.forEach((item) => delete item.children); //已经有的话就删掉
//把每一项的引用放入map对象里
let map = {};
node.forEach((item) => (map[item.id] = item));
let newNode = [];
node.forEach((dt) => {
let parents = map[dt.pId];
if (parents) {
//如果 map[dt.pId] 有值 则 parents 为 dt 的父级
//判断 parents 里有无child 如果没有则创建 如果有则直接把 dt push到children里
if (!parents.children) {
parents.children = [];
}
parents.children.push(dt);
} else {
newNode.push(dt);
}
});
return newNode;
};
转换后的格式如下:
我们再继续抽离选中的id集:
const resData = res.data //后端返回的数据
let checkedAuthKeys = new Set()
for (let i = 0; i < resData.length; i++) {
item = resData[i]
if (item['checked']) {
checkedAuthKeys.add(item['id'])
} else {
if (checkedAuthKeys.has(item['id']))
checkedAuthKeys.delete(item['id'])
}
}
}
处理后:
分别赋值两个data给v-model和auto-expand-parent就可以了,这样展现应该没问题了,下面我们来控制一下节点选择时的逻辑。
大概讲一下如何实现,首先在默认进入时保留一个选中的id集,然后在选择的时候比对本次与上一次的差异,向下查找该节点下所有子节点的id,然后向上查找所有父节点的id,判断本次操作是否勾选来决定子节点集是合并还是移除。
onCheck(checkedKeys, e) {
const beforeCheckedAuthKeys = this.beforeCheckedAuthKeys //上一次选中的ids 如果没有就是初始进入选中的集合
// console.log('上一次选中ids', beforeCheckedAuthKeys)
// console.log('选中ids', checkedKeys.checked)
const changeNodes = getArrDifference(checkedKeys.checked, beforeCheckedAuthKeys) //比对这次选中与上次选中的差异
// console.log('改变的节点', changeNodes);
let checkedList = [];
for (let i = 0; i < changeNodes.length; i++) {
let itemNode = searchTreeNodes(this.authTreeList, changeNodes[i]) //查找该节点数据 authTreeList为树型数据
let childrenNodesIds = searchTreeNodesAllId(itemNode.children) //向下查找所有子节点的ids
checkedList = [...new Set([...checkedList, ...childrenNodesIds])] //合并去重
}
// console.log('该节点下所有ids', checkedList)
//获取选中的节点的所有父节点
let parentNodes = changeNodes
parentNodes.forEach(
(item) =>
(parentNodes = parentNodes.concat(getParentIdList(item, this.authTreeList)))
)
// console.log('选中的所有父节点', parentNodes)
if (e.checked) { // 勾选
this.checkedAuthKeys = [...new Set([...checkedKeys.checked, ...checkedList, ...parentNodes])]
} else { // 取消勾选
if(checkedList && checkedList.length) {
//如果取消勾选需要移除该节点下所有子节点ids
this.checkedAuthKeys = checkedKeys.checked.filter(item => checkedList.indexOf(item) == -1)
} else {
this.checkedAuthKeys = this.checkedAuthKeys.checked
}
}
this.beforeCheckedAuthKeys = this.checkedAuthKeys //本次选择处理结束 赋值当前选择供下次使用
console.log('选中', this.checkedAuthKeys)
},
以上通用的函数我都封装到js里了 :
/**
* 比较两个数组差异
*/
export function getArrDifference(arr1 = [], arr2 = []) {
return arr1.concat(arr2).filter((v, i, arr) => {
return arr.indexOf(v) === arr.lastIndexOf(v);
})
}
/**
* 根据id查找节点 输出节点
* @param nodes 树
* @param searchKey id
*/
export function searchTreeNodes(nodes, searchKey) {
for (let _i = 0; _i < nodes.length; _i++) {
if (nodes[_i].id == searchKey) {
return nodes[_i]
} else {
if (nodes[_i].children && nodes[_i].children.length > 0) {
let res = searchTreeNodes(nodes[_i].children, searchKey);
if (res) {
return res
}
}
}
}
return null
}
/**
* 获取该节点下子孙节点的id
* @param data 节点
* @param arr 返回数组
*/
export function searchTreeNodesAllId(data = [], arr = []) {
Object.keys(data).forEach((key) => {
arr.push(data[key].id)
if (data[key].children && data[key].children.length) searchTreeNodesAllId(data[key].children, arr)
})
return arr
}
/**
* 通过当前节点id,获取树状结构所有的祖先节点id,包含当前节点id
* @param {String|Number} code 当前节点id
* @param {Array} tree 树状数组
* @returns {Array} 所有祖先id,包含当前code
*/
export const getParentIdList = (code, tree) => {
let arr = []; //要返回的数组
for (let i = 0; i < tree.length; i++) {
let item = tree[i];
arr = [];
arr.push(item.id); //保存当前节点id
if (code == item.id) {
//判断当前id是否是默认id
return arr; //是则退出循环、返回数据
} else {
//否则进入下面判断,判断当前节点是否有子节点数据
if (item.children && item.children.length > 0) {
//合并子节点返回的数据
arr = arr.concat(getParentIdList(code, item.children));
if (arr.includes(code)) {
//如果当前数据中已包含默认节点,则退出循环、返回数据
return arr;
}
}
}
}
}
这样就大功告成了,当然可以做到更细致,比如控制半选状态,v-model它是一个有checked
和halfChecked
属性的对象,我们可以处理选中的ids赋值给checked对象、半选的ids赋值给halfChecked对象,这样就能做到复现一个Tree的复选框,并且我们还可以控制其中的关联。