Vue: 如何实现树状收展及勾选联动功能-超实用干货 a

4,977 阅读6分钟

背景

需求

现在就有这么一个需求,实现一个权限配置功能,功能描述如下:

  1. 遍历一颗拥有权限节点的树形结构的数据

    • 当勾选父级时,会将所有子级全部勾选上,反之,亦然
    • 当子级别中不全被勾选时,父级别都是半勾选状态
    • 当子级别未被勾选时,父级别都是非选中☑️状态
  2. 实现收缩折叠功能

其实也不难,只要把思路理清楚,将逻辑分步实现就可以了。可能有人会说,你做出来了,才说不难;确实,刚开始的时候我也脑瓜子疼,但是不难又怎来的进步,否极泰来不就是那么回事吗,好像说得有点夸张了。

最终的实现结果如下:

涉及到两个比较关键的概念,就是递归遍历和对象地址引用这两个,其中地址引用是数据交互层面用到的思想,在这里不多加阐述。我主要想说的是树形结构数据定位父子元素方法封装,这个才是核心要素。

树形结构

数据构造

如下,我们大体会拿到类似下面的一个树形结构数据,每一层中的每一项,最好包含唯一的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;
}