背景
需求
现在就有这么一个需求,实现一个权限配置功能,功能描述如下:
-
遍历一颗拥有权限节点的树形结构的数据
- 当勾选父级时,会将所有子级全部勾选上,反之,亦然
- 当子级别中不全被勾选时,父级别都是半勾选状态
- 当子级别未被勾选时,父级别都是非选中☑️状态
-
实现收缩折叠功能
其实也不难,只要把思路理清楚,将逻辑分步实现就可以了。可能有人会说,你做出来了,才说不难;确实,刚开始的时候我也脑瓜子疼,但是不难又怎来的进步,否极泰来不就是那么回事吗,好像说得有点夸张了。
最终的实现结果如下:
涉及到两个比较关键的概念,就是递归遍历和对象地址引用这两个,其中地址引用是数据交互层面用到的思想,在这里不多加阐述。我主要想说的是树形结构数据定位父子元素方法封装,这个才是核心要素。
树形结构
数据构造
如下,我们大体会拿到类似下面的一个树形结构数据,每一层中的每一项,最好包含唯一的id,pid的话可有可无,如果有,我们还可以进一步优化代码。
/**
* 树形结构
*/
const resData = [
{
title: "系统管理",
id: 1,
pid: 0,
isMenu: true,
children: [
{
title: "角色分配",
id: 11,
pid: 1,
isMenu: true,
children: [
{
title: "角色分配1",
id: 111,
pid: 11,
isMenu: true,
children: [
{ title: "删除", isMenu: false, id: 1111, pid: 111 },
{ title: "详情", isMenu: false, id: 1112, pid: 111 },
{ title: "添加", isMenu: false, id: 1113, pid: 111 },
],
},
{ title: "角色分配2", isMenu: true, id: 112, pid: 11 },
{ title: "角色分配3", isMenu: true, id: 113, pid: 11 },
],
},
{
title: "总部分配",
id: 12,
pid: 1,
isMenu: true,
children: [
{ title: "总部分配1", isMenu: true, id: 121, pid: 12 },
{ title: "总部分配2", isMenu: true, id: 122, pid: 12 },
{ title: "总部分配3", isMenu: true, id: 123, pid: 12 },
],
},
{
title: "权限分配",
isMenu: true,
id: 13,
pid: 1,
},
],
},
{
title: "活动管理",
id: 2,
pid: 0,
isMenu: true,
children: [
{ title: "活动1", isMenu: true, id: 21, pid: 2 },
{ title: "活动2", isMenu: true, id: 22, pid: 2 },
{ title: "活动3", isMenu: true, id: 23, pid: 2 },
],
},
{
title: "积分管理",
isMenu: true,
id: 3,
pid: 0,
},
];
思路
数据加装
在动手开发之前,我们先讲步骤分解,我们要实现背景中所提到的功能,需要这个树形数据给我们提供哪些属性,当然这些属性的设置,完全可以不依赖于后端开发人员;
关键词提取: 半选中、选中、显示隐藏用于收缩展开效果实现、层级(可选)
有些额外需求就是,要你区分是菜单还是按钮就得加多一个属性
按照关键词的内容,加装属性
element.checked = false;
element.preChecked = false;
element.expand= true;
element.isShow = true;
初始化数据如下:
// 第一步转换数据格式
this.switchData(resData, 0);
// 第二部绑定数据
this.constructData = resData;
// 第三步扁平化数据
this.flatConstructData = this.platFn(resData);
/**
* @desc 转换数据格式,设定checked选中、preChecked部分选中、level层级
*/
switchData(data, level) {
if (data && data.length > 0) {
data.forEach((element) => {
element.checked = false;
element.preChecked = false;
element.expand= true;
element.isShow = true;
if (element.isMenu) {
element.level = level + 1;
if (
element.hasOwnProperty("children") &&
element.children.length > 0
) {
// 如果最后子集有一个不是菜单,也是到头了
if (!element.children[0].isMenu) {
element.children.map((v) => {
v.level = element.level + 1;
});
element.end = true;
element.row = 1;
} else {
element.row = 0;
}
this.switchData(element.children, element.level);
} else {
element.children = [];
// 如果没有了子集,那也到头了
element.end = true;
element.row = 1;
}
}
});
}
},
收缩折叠事件
就一个递归方法、按照状态值(expand)传递下去即可,直到chilidren:[]
isExpandClick(row) {
row.expand = !row.expand
this.setExpandChild(row.children, row.expand)
},
setExpandChild(child, status) {
if(child.length > 0 && child[0].isMenu) {
child.forEach((item)=> {
item.isShow = status
if(item.isMenu && item.children.length > 0) {
this.setExpandChild(item.children)
}
})
}
},
选中按钮事件
- 当选中的时候,需要做哪些步骤,我们需要理清楚这些,
- 当勾选按钮的时候,我们需要判断同级别其他按钮是否也完全勾选,然后往上判定设置
- 当勾选按钮的时候,我们需要判断子级是否也完全勾选,然后往下判定和设置
分析得出,我们需要实现几个关键的功能
- 通过传入当前节点某个值(如id),查询所有父级节点---方法封装
- 通过当前节点对象,拿到子级别所有数据,方便属性的交互设置,这个可以通过扁平化数据解决
附上代码,环境依赖vue2、ant-design-vue组件,所以看不到效果,主要理解思想就好
点击script部分
总结
核心方法如下
通过id值查找该元素
方法一:递归
/*
* @desc:这种递归方式,是先从内深入去执行,先从第一个元素,不断解刨children去寻找,
* 最后从最后一个开始向刚开始的方向去寻找
*/
function getChidlren(id) {
let hasFound = false, // 表示是否有找到id值
result = null;
// 通过递归实现
let fn = function (data) {
if (Array.isArray(data) && !hasFound) { // 判断是否是数组并且没有的情况下,
data.forEach(item => {
if (item.id === id) { // 数据循环每个子项,并且判断子项下边是否有id值
result = item; // 返回的结果等于每一项
hasFound = true; // 并且找到id值
} else if (item.children) {
fn(item.children); // 递归调用下边的子项
}
})
}
}
fn(data); // 调用一下
return result;
}
console.log(getChidlren(3));
方法二:树状转扁平化(递归)、再利用find
/*
* @输入参数 list: 需要扁平化的树形结构数组,默认按children字段扁平展开
* @输出:返回别扁平化的数组
*/
function platFn(list) {
let res = []
res = list.concat(...list.map(item => {
if (item.children instanceof Array && item.children.length > 0) {
return platFn(item.children)
}
return null
}).filter(o => o instanceof Array && o.length > 0))
return res
}
var platList= platFn(treeList)
console.log(platList)
/**
*@desc: find查找
*
*/
console.log('id: 32==>', platList.find(item => item.id == 32))
通过传入当前节点某个值(如id),查询所有父级节点
返回一个由上到下的数组列表
export function getParent(data2, nodeId2) {
var arrRes = [];
if (data2.length == 0) {
if (!!nodeId2) {
arrRes.unshift(data2)
}
return arrRes;
}
let rev = (data, nodeId) => {
for (var i = 0, length = data.length; i < length; i++) {
let node = data[i];
if (node.id == nodeId) {
arrRes.unshift(node)
// 查找到当前id,继续追随父级id
rev(data2, node.pid); // 注意这里是传入的tree,不要写成data了,不然遍历的时候一直都是node.children,不是从最顶层开始遍历的
break;
}
else { // 如果当前节点没有对应id,则追溯该子类是否有匹配项
if (!!node.children) {
rev(node.children, nodeId);
}
}
}
return arrRes;
};
arrRes = rev(data2, nodeId2);
return arrRes;
}
调用时,传入数据和当前节点code(数据为树型结构)
let newLevel = getParent(this.info, row.id)
通过递归json树根据子id查父id
//传入参数:需要遍历的json,需要匹配的id
findPnodeId(data,nodeId){
//设置结果
let result;
if (!data) {
return;//如果data传空,直接返回
}
for (var i = 0; i < data.children.length; i++) {
let item = data.children[i];
if (item.nodeId == nodeId) {
result=data.nodeId;
//找到id相等的则返回父id
return result;
} else if (item.children && item.children.length > 0) {
//如果有子集,则把子集作为参数重新执行本方法
result= findPnodeId(item, nodeId);
//关键,千万不要直接return本方法,不然即使没有返回值也会将返回return,导致最外层循环中断,直接返回undefined,要有返回值才return才对
if(result){
return result;
}
}
}
//如果执行循环中都没有return,则在此return
return result;
}