树形结构对比之预更新

198 阅读6分钟

大家好,我之前分享了两棵树形结构数据的对比,但因为该文章是聚焦于树形结构的对比,就没有编写预更新(对比预览)部分。还是有不少同学对这部分是比较感兴趣的,那么就跟随这篇文章看看识别出对比后,怎么进行预更新吧。

因为本文与两棵树形结构数据的对比拓展篇,有强关联关系,请先阅读对比篇文章后再来阅读本文效果更佳。

构建视图

既然是预更新,那就得看到效果,不能像上一篇只写 js 代码,我们需要先把两颗树画出来。

这里用的 ui 框架是 element-plus,代码比较简单这里就不过多赘述,具体的一些参数大家可以自行去 element-plus 官网查阅。

  <div id="tree">
    <!--左侧树-->
    <div class="tree-con">
      <el-tree :data="data.leftTree" default-expand-all>
        <template #default="{ data }">
          <span v-if="!data.status">{{ data.title }}</span>
        </template>
      </el-tree>
    </div>
    <!--右侧树-->
    <div class="tree-con" style="border: 1px solid #f00">
      <el-tree
        :props="{ class: customNodeClass }"
        :data="data.rightTree"
        @check-change="checkChange"
        show-checkbox
        default-expand-all
      >
        <template #default="{ data }">
          <span v-if="!data.status">{{ data.title }}</span>
        </template>
      </el-tree>
    </div>
  </div>

大家这会去看页面可能什么也看不到甚至还有报错,因为树结构的数据我们还没写。既然 ui 用了 element-plus 那说明这个 demo 是用 vue3 写的,如果不会也不用紧张,这个 demo 用到 vue3 特性的地方不多,总体跟 js 的代码是很像的。

我们把树数据定义出来,还得定义个 customNodeClass 方法供 element-plus 的 tree 使用展示样式:

const data = reactive({
  leftTree: [
    {
      pid: 0,
      id: 1,
      level: 1,
      key: '1-1',
      title: '第一层1-左侧',
    },
    {
      pid: 0,
      id: 2,
      level: 1,
      key: '1-2',
      title: '第一层2',
    },
    {
      pid: 0,
      id: 3,
      level: 1,
      key: '1-3',
      title: '第一层3',
    },
  ],
  rightTree: [
    {
      pid: 0,
      id: 1,
      level: 1,
      key: '1-1',
      title: '第一层1-右侧',
      children: [
        {
          pid: 1,
          id: 11,
          level: 2,
          key: '2-11',
          title: '第二层11',
          children: [
            {
              pid: 11,
              id: 111,
              level: 3,
              key: '3-111',
              title: '第三层111',
            },
          ],
        },
      ],
    },
    {
      pid: 0,
      id: 2,
      level: 1,
      key: '1-2',
      title: '第一层2',
    },
    {
      pid: 0,
      id: 4,
      level: 1,
      key: '1-4',
      title: '第一层4',
      children: [
        {
          pid: 4,
          id: 41,
          level: 2,
          key: '2-41',
          title: '第二层41',
        },
      ],
    },
  ],
});


/**
 * 树节点自定义的类名
 * @data {object} 数据
 * */
const customNodeClass = (data) => {
  if (data.class) return 'change-color';
  return null;
};

既然是树,那必然要有 id 和 pid,这里我们再把 children 定义出来,这组数据就直接包含了增删改的情况了,大家通过对比篇文章的代码就能看到效果。 我们现在有了视图页面,加点样式让大家看的更直观点。

<style lang="less">
#tree {
  padding-top: 50px;
  display: flex;
  align-items: center;
  background-color: #fff;

  .tree-con {
    width: 200px;
    border: 1px solid #000;
    margin-left: 10px;
  }
}

.el-button {
  margin: 10px;
}

.change-color {
  color: #f00;

  .el-checkbox__inner {
    border-color: #f00;
  }
}

.el-tree-node__expand-icon.expanded {
  visibility: hidden;
}
</style>

视图效果:

image.png图 1 对比

第一层包含了增加和修改,第二层两边是一样的所以不可选,第三层是删除的情况,第四层是完全新增。

实现

对比的效果出来了,因为是勾选数据更新而非全量更新,那我们就得识别出勾选的数据,也就是将要预更新到左侧的数据,其实也就是复选框被勾选的时候获取勾选的数据。

获取勾选的数据

我们在 html 部分其实就已经提前定义了复选框的勾选方法 @check-change="checkChange",element-plus 已经 提供了 check-change 方法,每次点击复选框都会触发它,我们定义 checkChange 方法接收下即可:

const pickList = reactive([]); //勾选的数据
/**
 * 当复选框被点击的时候触发
 * @data {object} 勾选/取消勾选的数据
 * @bool {boolean} 是否选中
 * */
const checkChange = (data, bool) => {
  if (bool) {
    //选中
    pickList.push(data);
  } else {
    //取消选中
    let index = pickList.indexOf(data);
    pickList.splice(index, 1); //将取消选中数据从勾选数组里移除
  }
};

数据处理

触发按钮

拿到要更新的数据后,我们还需要个按钮来触发何时进行预更新:

<el-button type="primary" @click="preview">预览</el-button>

展开树结构

我们在按钮上定义 preview 方法,在 js 里编写方法内容。但这个 preview 方法该怎么写呢?树结构是有子节点的,通常的遍历只能遍历这一层没法遍历到下一层,这时候就需要我们对树进行特殊处理,也就是展开铺平树结构数据。

/**
 * 展开平铺树结构数据
 * @data {array} 树结构数据
 * */
const flattenTree = (data) => {
  data = JSON.parse(JSON.stringify(data));
  let res = [];

  //遍历树
  while (data.length) {
    let node = data.shift(); //每次循环截取树数组首条数据
    if (node.children && node.children.length) {
      data = data.concat(node.children);
    }
    delete node.children;
    res.push(node);
  }
  return res;
};

const flatLeft = flattenTree(data.leftTree); //平铺的树

flatLeft 就是我们平铺展开的树,我们拿到后再来试试能否顺利进行预更新。

我们遍历 pickList 将选择的数据与平铺树数据通过唯一值 key 进行对比:

/**
 * 预览对比效果
 * */
const preview = () => {
  pickList.map((item) => {
    let i = flatLeft.findIndex((v) => item.key === v.key); //通过唯一值 key 进行对比,拿到要预更新的数据的下标
    flatLeft.splice(i, 1, item); //数据替换
  });

  data.leftTree = flatLeft;
};

平铺数组转树结构

点击按钮,我们发现预更新后数据变得很奇怪,因为我们的目标数据是树结构数据,拿平铺的数据去赋值树结构数据当然会有问题,所以我们还缺少一将平铺数组转树结构的步骤。

/**
 * 平铺数组转树结构数据
 * @items {array} 平铺数组
 * */
const arrayToTree = (items) => {
  let res = [];
  let getChildren = (res, pid) => {
    for (const i of items) {
      if (i.pid === pid) {
        const newItem = { ...i, children: [] };
        res.push(newItem);
        getChildren(newItem.children, newItem.id);
      }
    }
  };
  getChildren(res, 0);
  return res;
};

最后修改下 preview 方法,在给 data.leftTree 赋值前将平铺的数组转为树:

/**
 * 预览对比效果
 * */
const preview = () => {
  pickList.map((item) => {
    let i = flatLeft.findIndex((v) => item.key === v.key);
    flatLeft.splice(i, 1, item);
  });

  data.leftTree = arrayToTree(flatLeft);
};

效果

image.png图 2 效果

完整代码

对比部分的代码请看对比篇文章。

<template>
  <div id="tree">
    <!--左侧树-->
    <div class="tree-con">
      <el-tree :data="data.leftTree" default-expand-all>
        <template #default="{ data }">
          <span v-if="!data.status">{{ data.title }}</span>
        </template>
      </el-tree>
    </div>
    <!--右侧树-->
    <div class="tree-con" style="border: 1px solid #f00">
      <el-tree
        :props="{ class: customNodeClass }"
        :data="data.rightTree"
        @check-change="checkChange"
        show-checkbox
        default-expand-all
      >
        <template #default="{ data }">
          <span v-if="!data.status">{{ data.title }}</span>
        </template>
      </el-tree>
    </div>
  </div>

  <el-button type="primary" @click="preview">预览</el-button>
</template>

<script setup>
const data = reactive({
  leftTree: [
    {
      pid: 0,
      id: 1,
      level: 1,
      key: '1-1',
      title: '第一层1-左侧',
    },
    {
      pid: 0,
      id: 2,
      level: 1,
      key: '1-2',
      title: '第一层2',
    },
    {
      pid: 0,
      id: 3,
      level: 1,
      key: '1-3',
      title: '第一层3',
    },
  ],
  rightTree: [
    {
      pid: 0,
      id: 1,
      level: 1,
      key: '1-1',
      title: '第一层1-右侧',
      children: [
        {
          pid: 1,
          id: 11,
          level: 2,
          key: '2-11',
          title: '第二层11',
          children: [
            {
              pid: 11,
              id: 111,
              level: 3,
              key: '3-111',
              title: '第三层111',
            },
          ],
        },
      ],
    },
    {
      pid: 0,
      id: 2,
      level: 1,
      key: '1-2',
      title: '第一层2',
    },
    {
      pid: 0,
      id: 4,
      level: 1,
      key: '1-4',
      title: '第一层4',
      children: [
        {
          pid: 4,
          id: 41,
          level: 2,
          key: '2-41',
          title: '第二层41',
        },
      ],
    },
  ],
});

/**
 * 树节点自定义的类名
 * @data {object} 数据
 * */
const customNodeClass = (data) => {
  if (data.class) return 'change-color';
  return null;
};

/**
 * 当复选框被点击的时候触发
 * @data {object} 勾选/取消勾选的数据
 * @bool {boolean} 是否选中
 * */
const checkChange = (data, bool) => {
  if (bool) {
    //选中
    pickList.push(data);
  } else {
    //取消选中
    let index = pickList.indexOf(data);
    pickList.splice(index, 1); //将取消选中数据从勾选数组里移除
  }
};

/**
 * 展开平铺树结构数据
 * @data {array} 树结构数据
 * */
const flattenTree = (data) => {
  data = JSON.parse(JSON.stringify(data));
  let res = [];

  //遍历树
  while (data.length) {
    let node = data.shift(); //每次循环截取树数组首条数据
    if (node.children && node.children.length) {
      data = data.concat(node.children);
    }
    delete node.children;
    res.push(node);
  }
  return res;
};

/**
 * 平铺数组转树结构数据
 * @items {array} 平铺数组
 * */
const arrayToTree = (items) => {
  let res = [];
  let getChildren = (res, pid) => {
    for (const i of items) {
      if (i.pid === pid) {
        const newItem = { ...i, children: [] };
        res.push(newItem);
        getChildren(newItem.children, newItem.id);
      }
    }
  };
  getChildren(res, 0);
  return res;
};

/**
 * 预览对比效果
 * */
const preview = () => {
  pickList.map((item) => {
    let i = flatLeft.findIndex((v) => item.key === v.key);
    flatLeft.splice(i, 1, item);
  });

  data.leftTree = arrayToTree(flatLeft);
};

const pickList = reactive([]); //勾选的数据
const flatLeft = flattenTree(data.leftTree);
</script>

<style lang="less">
#tree {
  padding-top: 50px;
  display: flex;
  align-items: center;
  background-color: #fff;

  .tree-con {
    width: 200px;
    border: 1px solid #000;
    margin-left: 10px;
  }
}

.el-button {
  margin: 10px;
}

.change-color {
  color: #f00;

  .el-checkbox__inner {
    border-color: #f00;
  }
}

.el-tree-node__expand-icon.expanded {
  visibility: hidden;
}
</style>