在工作中开发的树形递归组件

2,073 阅读3分钟

最近的工作主要内容就是开发组件,这里记录一下经常开发的递归组件。

一、前言 转眼间,从学校到职场已经快一年的时间了。加入掘金到现在也有两年多的时间。从只会看别人的文章到现在终于能写下自己的第一篇文章了。工作中的时常会感觉到迷茫和焦虑,希望在职场的自己仍能保持对学习的热情和对技术的热爱!

二、组件演示

介绍

最近在参与开发项目组的类BI工具,不得不说真的是学习到太多东西啦!因为BI工具主要涉及到数据处理与操作。有时候的过滤条件比较复杂,需要根据不同的业务需求与筛选条件需求自定义过滤条件。

演示

用户可以自己组合且条件或者或条件。

三、功能点梳理 开发这样的组件的时候需要提前梳理好功能点。我常常会使用印象笔记将每个功能点梳理出来,然后对功能点进行排序,最后依次完成组件的功能(这样开发效率真的很高)。
组件的功能如下:

  • 添加第一条筛选条件是一个普通的数组结构。
  • 从添加第二条筛选条件开始构造树结构。
  • 选同级的两条筛选条件进行合并操作,并生成条件
  • 合并之后下级的子节点合并再次生成条件(该条件与父级的条件互斥,如果与父节点条件相同就没有意义的哈)
  • 点击“切换”图标,会取消当前的条件选项,也会取消当前的层级

四、实现

4.1从文件结构说起

4.2编码的核心其实在操作树结构
  • 客户端需要的数据结构为:
 textdata = [
    {
      Group: "group1",
      GroupRel: "and",
      children: [
        {
          Group: "group2",
          GroupRel: "or",
        },

        {
          Field: "",
          FieldDataType: "",
          check: false,
          edit: false,
          isshow: true,
          index: 2,
          keyword: "",
          operator: ""
        },
        {
          Field: "",
          FieldDataType: "",
          check: false,
          edit: false,
          isshow: true,
          index: 1,
          keyword: "",
          operator: ""
        }
      ]
    }
  ]
  • 服务端需要的数据结构为:
{
Field: "birthday"
FieldDataType: "date"
Group: "group1"
GroupAndOr: "或"
IsFirst: true
Keyword: ""
Operator: ""
},
{
AndOr: "且"
Field: "age"
FieldDataType: "number"
Group: "group1"
GroupAndOr: "或"
Keyword: ""
Operator: ""
},
{
Field: "name"
FieldDataType: "string"
Group: "group2"
GroupAndOr: "或"
IsFirst: true
Keyword: "11"
Operator: "包含"
}

4.3用js的方式渲染html页面

判断树的节点是否存在孩子,有孩子节点就递归遍历孩子节点,否则直接渲染当前筛选条件。

<div>
    <ng-container *ngFor="let item of ccfilterObj">
      <div class="filtertopbox" *ngIf="item.children">
       <div class="group-name">
          <div class="group-name--name">
              <span>{{item.GroupRel}}</span>
              <span (click)="filterChangeFilter(item)">
                <i class="iconfont iconqiehuan"></i>
              </span>
          </div>
        </div>
        <div class="group-con">
        <!--递归-->
          <app-myfilter [ccfilterObj]="item.children" (deleteEvent)="deleteFun($event)"
            (checkChange)="checkChangeFun($event)" (changeGroup)=" filterChangeFilter($event)" (groupSwitchEvent)="switchChangeFun($event)"></app-myfilter>
        </div>
      </div>
      <div class="filter-single" *ngIf="!item.children">
      <!--渲染组件内容-->
      </div>
    </ng-container>
</div>
4.4添加筛选条件(构造树节点以及他的父级)
  • 创建父节点
  createFF(rela = "或", isopen = false) {
    return {
      Group: `group${++this.myMaxIndex}`,
      GroupRel: rela,
      isopen: isopen,
      index: ++this.myMaxIndex,
      children: []
    }
  }
},
  • 创建子节点
 //构造项{当前项,父项,}
  addNewItem(index) {
    let { objO, objF } = this.findObjByIndeAndFF(this.ccfilterObj, index, this.ccfilterObj);
    // let maxIndex = this.findMaxIndex(this.ccfilterObj);
    let addObj = {
      index: ++this.myMaxIndex,
      Field: "",
      operator: "",
      keyword: "",
      isshow: true,
      FieldDataType: "str",
      edit: false,
      check: false
    }
    return { objO, objF, addObj }
  }
  • 将子节点添加到父节点的children中
//当前添加项
  addfun() {
    let myindex = this.ccfilterObj[0] && this.ccfilterObj[0].index;
    let { objO, addObj, objF } = this.addNewItem(myindex || 0);
    return { objO, addObj, objF } 
  }
//增加过滤条件
  addFilterFun() {
    this.cleckFun(this.ccfilterObj);
    let { objO, addObj, objF }  = this.addfun();
    if (!objO || !objO.children) { //第一项,普通数组结构
      objF.push(addObj)
    } else {//大于一项,数组结构,但是只有一个元素,且元素对象为树结构
      let temparr = this.findSwitchObj(this.ccfilterObj);
      temparr.forEach((item) => {
        let { objO, addObj, objF }  = this.addfun();
        item.children.push(addObj);
      })
    }
    if (this.ccfilterObj.length > 1) {//替换第一项为数组结构
      let obgFF = this.createFF("或", true);
      obgFF.children = JSON.parse(JSON.stringify(this.ccfilterObj));
      this.ccfilterObj.splice(0, this.ccfilterObj.length, obgFF);
    }
  }
  
   //寻找switch为true的项
  findSwitchObj(arr, res = []) {
    for (let i = 0; i < arr.length; i++) {
      if (arr[i].isopen) {
        res.push(arr[i]);
      }
      if (arr[i].children) {
        this.findSwitchObj(arr[i].children, res);
      }
    }
    return res;
  }
4.5合并筛选条件(注意合并之后的条件与父级条件相斥)
  mergeFun() {
    //找出当前选中项
    let resCheck = this.findObjByCheck(this.ccfilterObj);
    let resCheckInfo = resCheck.map((item) => {
      return this.findObjByIndeAndFF(this.ccfilterObj, item.index, this.ccfilterObj);
    })
    let objFF = resCheckInfo[0].objF;//选中项的父项
    let ppObj = this.createFF(objFF.GroupRel == "或" ? "且" : "或", false);//为选中项创新的父级项
    let minIndex = Math.min.apply(null, resCheckInfo.map((item) => {
      return item.objF.children.findIndex(
        (idtem) => {
          return idtem.index == item.objO.index
        }
      )
    }))
    //删除
    resCheckInfo.forEach((it) => {
      it.objO.check = false;
      ppObj.children.push(it.objO);
      it.objF.children.splice(
        it.objF.children.findIndex(
          (item) => item.index == it.objO.index
        ),
        1
      );
    });
    objFF.children.splice(minIndex, 0, ppObj);
    this.ismergeDisable = true;
  }
4.6取消合并
 changerel(item) {
    item.GroupRel = item.GroupRel == "且" ? "或" : "且";
    this.dealCancel(this.ccfilterObj);
  }
  dealCancel(arr) {
    for (let item of arr) {
      if (item.children) {
        let fRel = item.GroupRel;
        let equalG = item.children.filter(
          it => it.Group && it.GroupRel == fRel
        );
        if (equalG.length != 0) {
          equalG.reverse().forEach(it => {
            let data = it.children;
            let ind = item.children.findIndex(
              ite => ite.index == it.index
            );
            item.children.splice(ind, 1);
            data.reverse().forEach(e =>
              item.children.splice(ind, 0, e)
            );
          });
        } else {
          let noEqualG = item.children.filter(
            it => it.Group && it.GroupRel != fRel
          );
          noEqualG.forEach(it => this.dealCut([it]));
        }
      }
    }
  }

五、处理树结构常用的递归工具函数

1.获取任意节点的父节点以及当前节点

 //根据index找到当前项,并且返回他的父级
  findObjByIndeAndFF(arr, index, objF) {
    let obj = { objO: null, objF: objF };
    for (let i = 0; i < arr.length; i++) {
      if (arr[i].index == index) {
        obj.objO = arr[i];
        break;
      } else if (arr[i].children) {
        obj = this.findObjByIndeAndFF(arr[i].children, index, arr[i]);
        if (obj.objO) break;
        obj.objF = objF;
      }
    }
    return obj;
  }

2.获取树节点中的最大层级树

  //获取最大层级树
  getMaxDeep(node) {
    if (!node.children || node.children.length === 0) {
      return 1
    }
    const maxChildrenDepth = [...node.children].map(v => this.getMaxDeep(v))
    return 1 + Math.max(...maxChildrenDepth)
  }

3.取出树结构中的每一项

//将树结构每一项拿出来
  makeTreeToArr(arr, res = []) {
    for (let i = 0; i < arr.length; i++) {
      if (arr[i].children) {
        let tempp = JSON.parse(JSON.stringify(arr[i]));
        delete tempp.children;
        res.push(tempp);
        this.makeTreeToArr(arr[i].children, res);
      } else {
        res.push(arr[i])
      }
    }
    return res;
  }

4.将同一层级的数据取出来并归类

  //将树结构转化为二维数组
  toDoubleArr(arr) {
    let deeparr = new Set(arr.map((item) => {
      return item.deep;
    }))
    let deeparrD = [...deeparr];
    let res = [];
    for (let i = 0; i < deeparrD.length; i++) {
      let temparr = []
      for (let j = 0; j < arr.length; j++) {
        if (arr[j].deep == deeparrD[i]) {
          temparr.push(arr[j]);
        }
      }
      res.push(temparr);
    }
    return res;
  }