自定义ElementUI风格树形组件,详解递归组件的使用及事件数据传递,视图更新等问题

组件视图样式

当我们做多级菜单或者权限列表管理的时候,大多会采用树形结构来实现,有的朋友为了省事,不想多费脑力,多费时间,直接几层for循环就做了个差不多的树形组件,更省事的朋友直接拿ElementUI中的Tree树形组件来完成需求,这时,稍微有点要求的产品就来要求了:不行,这组件样式都是竖着的,要是权限列表多了,这还了得?改。
这里笔者仿照ElementUI的风格,自定义了个简单的树形组件,先看样式:
自定义ElementUI风格树形组件,详解递归组件的使用及事件数据传递,视图更新等问题_第1张图片
自定义ElementUI风格树形组件,详解递归组件的使用及事件数据传递,视图更新等问题_第2张图片

递归组件说明

什么是递归组件?简单的说就是组件自己调用自己。有些朋友可能就懵了,哎呦卧槽,组件还能调用自己?这么牛X?对,就是这么牛X。
咱们先不谈如何实现,先看工作中会碰到的一种需求。
看如下示例数据

list: [
    {
      id: "1",
      label: "一级菜单1",
      checked: false,
      children: [
        {
          id: "11",
          label: "二级菜单1",
          checked: false,
          children: [
            {
              id: "111",
              label: "三级菜单1",
              checked: false,
            },
          ],
        },
        {
          id: "12",
          label: "二级菜单2",
          checked: false,
          children: [
            {
              id: "121",
              label: "三级菜单1",
              checked: false,
            },
            {
              id: "122",
              label: "三级菜单2",
              checked: false,
            },
          ],
        },
      ],
    },
  ],

按照以上数据来实现视图,很多朋友一看这有啥难度,然后直接上手写代码,最终写了如下代码:

  <div class="example">
    <div v-for="item in list" :key="item.id" class="example-item">
      <el-checkbox v-model="item.checked">{{item.label}}</el-checkbox>
      <div v-for="it in item.children" :key="it.id" class="example-item">
        <el-checkbox v-model="it.checked">{{it.label}}</el-checkbox>
        <div v-for="i in it.children" :key="i.id" class="example-item">
          <el-checkbox v-model="it.checked">{{i.label}}</el-checkbox>
        </div>
      </div>
    </div>
  </div>

这么简答的需求我要是做不出来,那简直是侮辱我80的智商。这时,有朋友瞪着眼睛多看了两眼,咦?我瞅着怎么这代码结构好像有规律可循?这要是多几层数据,我岂不是要继续重复嵌套下去?咱是有水平的程序员,怎么能总是干这种重复的事情呢?

于是,递归的思想冒出来了,我可以把上述代码的基本结构抽离出来,成一个组件,然后让这个组件嵌套调用自己,这样无论有多少层数据,我都不用担心了。

请看如下:
创建ExmapleItem.vue

<template>
	<div class="example-item">
	  <el-checkbox v-model="item.checked">{{item.label}}</el-checkbox>
	  <div v-if="item.children">
	    <example-item v-for="it in item.children" :key="it.id" :item="it"></example-item>
	  </div>
	</div>
</template>
<script>
export default {
	name: "example-item",
	props:{
		item: {
			type: Object,
		}
	}
}
</script>

上述示例代码是递归组件的一个子项,咱们要实现上述list数据内容,还需要另外创建一个组件,在这个组件引入递归组件,这么使用:

<example-item v-for="item in list" :key="item.id" :item="item"></example-item>

以上简单示例就实现了一个简单的递归组件方案,这里需要重点说明的一点就是,在组件内部要加上name名称,该名称写的是什么,递归调用的组件名称就是什么,注意递归组件内部数据的传递参数要保持一致。

自定义CTree树形组件

上述示例看明白了,递归组件也明白了,那么,就可以进行自定义树形组件的开发工作。

不想看下面内容的朋友,可以直接点此下载完整内容。

新建一个CTree文件夹,在内部创建两个文件index.vue和CTreeItem.vue。

index.vue

<template>
  <div class="c-tree-component">
    <c-tree-item v-for="item in tree" :key="item.value" :treeObj="item" :size="size"></c-tree-item>
  </div>
</template>
<script>
import treeStore from "./tree-store";
import CTreeItem from "./CTreeItem.vue";

export default {
  name: "CTree",
  data() {
    return {
      tree: [],
    };
  },
  props: {
    data: {
      type: Array,
      default: [],
    },
    size: {
      type: String,
      default: "normal", // small 小
    },
    mode: {
      type: String,
      default: "normal", // auth 权限模式,此模式下,子组件被选中,父组件一定被选中
    },
    expanded: {
      type: Boolean,
      default: true,
    },
  },
  mounted() {
    this.tree = this.initData(this.data);

    // 组件事件监听,监听子组件状态变化,获取和更改父组件数据
    treeStore.$on("tree-change", (res) => {
      // res当前点击项的数据

      // 如果当前模式为auth,执行auth模式组件选择方式
      if (this.mode === "auth") {
        let result = this.reInitParentCheck(this.tree, res.value);
        this.tree = result.data;
      } else {
        // 拿到更新后的部分数据,normal模式下,子组件的check随父组件变化
        res = this.updateSonCheck(res, res.checked);

        // 更新数据,同步显示组件样式
        this.tree = this.updateData(this.tree, res);

        // 更新数据,更新所有checkBox状态
        this.tree = this.reInitCheck(this.tree);
      }
      // console.log(this.tree);

      // 向外部引用父组件传递选中的value值
      let result = this.getAllValue(this.tree);
      this.$emit("change", result);
      // console.log(result);
    });
  },
  methods: {
    /**
     * 采用递归方式重新修改data数据
     * 方便组件进行选中,展开等操作
     */
    initData(e) {
      let temp = [];
      e.forEach((ele) => {
        if (ele.children && ele.children.length > 0) {
          ele.children = this.initData(ele.children);
        }
        temp.push({
          ...ele,
          checked: false, // 是否选中
          expanded: this.expanded, // 是否展开
          indeterminate: false, // checkBox的中间态
        });
      });
      return temp;
    },
    /**
     * 更新全部数据内容
     */
    updateData(e, target) {
      let isFind = false;
      e.forEach((ele) => {
        if (ele.value === target.value) {
          ele = target;
          isFind = true;
        }
      });
      if (!isFind && e.children && e.children.length > 0) {
        e.children = this.updateData(e.children, target);
      }
      return e;
    },
    /**
     * 更新数据的check状态
     * 判断是否有children,如果有,全部更新为父级数据的check状态
     */
    updateSonCheck(e, checked) {
      if (e.children && e.children.length > 0) {
        e.children.forEach((i) => {
          i.checked = checked;
          if (i.children && i.children.length > 0) {
            i = this.updateSonCheck(i, checked);
          }
        });
      }
      e.checked = checked;
      return e;
    },
    /**
     * 更新全部数据的视图check状态
     * none all half
     */
    reInitCheck(e) {
      e.forEach((ele) => {
        if (ele.children && ele.children.length > 0) {
          ele.children = this.reInitCheck(ele.children);
          let res = "";
          res = this.getNodeStatus(ele.children);
          if (res === "none") {
            ele.checked = false;
            ele.indeterminate = false;
          } else if (res === "all") {
            ele.checked = true;
            ele.indeterminate = false;
          } else if (res === "half") {
            ele.checked = false;
            ele.indeterminate = true;
          }
        }
      });
      return e;
    },
    /**
     * 更新全部父级数据的视图check状态
     * mode为auth模式时,调用此方法
     * 根据当前级别所有项check状态更新父组件check状态
     * 父组件check不影响子组件,子组件check必定更新父组件check
     */
    reInitParentCheck(e, current) {
      let isChecked = false;
      for (let i = 0; i < e.length; i++) {
        let ele = e[i];
        // 如果遍历选项与当前操作项的value相等,将所有子项置为同样状态
        if (ele.value === current) {
          // 更新所有子项数据
          ele = this.updateSonCheck(ele, ele.checked);
          // 如果当前操作项check为true,将标记isChecked置为true,表示当前级别有选中项
          if (ele.checked) {
            isChecked = true;
            continue;
          }
        }
        if (ele.children && ele.children.length > 0) {
          let res = this.reInitParentCheck(ele.children, current);
          ele.children = res.data;
          ele.checked = res.isChecked;
        }
        if (ele.checked) {
          isChecked = true;
        }
      }
      return {
        data: e,
        isChecked: isChecked,
      };
    },
    /**
     * 获取每一节点的状态,用于更新checkBox中间态
     * 如果该节点所有同级checkBox都被选中,返回all
     * 0个选中,返回none
     * 其余情况返回half
     */
    getNodeStatus(e) {
      let checkedNum = 0;
      e.forEach((ele) => {
        if (ele.checked) {
          ++checkedNum;
        }
      });
      if (checkedNum === 0) {
        return "none";
      } else if (checkedNum === e.length) {
        return "all";
      } else {
        return "half";
      }
    },
    /**
     * 获取所有被选中项的value
     */
    getAllValue(e) {
      let temp = [];
      e.forEach((ele) => {
        if (ele.checked) {
          temp.push(ele.value);
        }
        if (ele.children && ele.children.length > 0) {
          let res = this.getAllValue(ele.children);
          temp = temp.concat(res);
        }
      });
      return temp;
    },
  },
  components: {
    CTreeItem,
  },
};
</script>

CTreeItem.vue

<template>
  <div class="c-tree" @click.stop="_handleClick">
    <div class="c-tree-level" :style="{height:size=='small'?'26px':'40px'}">
      <i class="el-icon-caret-right c-tree-level-icon"></i>
      <el-checkbox @click.native.stop v-model="obj.checked" :indeterminate="obj.indeterminate" @change="_handleChange"></el-checkbox>
      <span>{{obj.label}}</span>
    </div>
    <el-collapse-transition>
      <div v-if="obj.children" v-show="obj.expanded" class="c-tree-level-child" :class="{'level-last':!obj.children[0].children}">
        <c-tree-item v-for="it in obj.children" :key="it.value" :treeObj="it" :size="size"></c-tree-item>
      </div>
    </el-collapse-transition>
  </div>
</template>
<script>
import treeStore from "./tree-store";

export default {
  name: "CTreeItem",
  data() {
    return {
      obj: {
        children: [],
        checked: false,
        expanded: false,
      },
    };
  },
  props: {
    treeObj: {
      type: Object,
    },
    size: {
      type: String,
      default: "normal", // small 小
    },
  },
  watch: {
    "treeObj.checked": function (e) {
      this.obj = this.treeObj;
    },
  },
  created() {
    this.obj = this.treeObj;
  },
  methods: {
    /**
     * 点击操作,控制展开和收缩,并更新父组件数据
     */
    _handleClick() {
      // 修改当前点击项的收缩展开状态
      this.obj.expanded = !this.obj.expanded;
      this._handleChange();
    },
    /**
     * checkBox数据修改监听
     * 更新局部组件视图,将更新的组件状态传给父组件
     */
    _handleChange() {
      this.$nextTick(() => {
        treeStore.$emit("tree-change", this.obj);
      });
    },
  },
};
</script>
<style lang="scss" scoped>
.c-tree {
  &-level {
    display: flex;
    align-items: center;
    cursor: pointer;
    height: 40px;
    &:hover,
    &.focus {
      background-color: #f5f7fa;
    }
    &.expanded {
      .c-tree-level-icon {
        transform: rotate(90deg);
      }
    }
    &-icon {
      color: #c0c4cc;
      font-size: 12px;
      transition: 0.3s;
      transform: rotate(0deg);
      padding: 6px;
    }
    .el-checkbox {
      margin-right: 8px;
    }
    &-child {
      margin-left: 24px;
      &.level-last {
        display: flex;
        align-items: center;
        .c-tree-level {
          padding-right: 12px;
          margin-right: 24px;
          &-icon {
            color: transparent;
          }
        }
      }
    }
  }
}
</style>

递归组件事件传递

在递归组件中,寻常使用的this.$emit()就无法生效,我们这里可以采用eventBus方案来实现,有朋友可能疑惑:eventBus是个啥玩意?你就当它是一种解决方案就可以,方法很简单,另外创建一个Vue实例,让emit和on绑定同一个实例就可以在递归组件内部发送事件了。
如上述代码中的tree-store.js.

import Vue from 'vue';
export default new Vue();

外部按照正常组件引用方式,引入index.vue组件就可以。

<c-tree :data="tree" mode="auth" :expanded="true"></c-tree>

本组件提供了两种mode,默认“normal”,checkBox有中间态,子组件不全部选中情况下,父组件check状态为false,“auth”为权限列表选择模式,一旦子组件有选中项,父组件一定为选中状态。

expanded控制是否展开。

size:默认normal,间隔较大,如上图所示,“small”表示各级之间间隔较小,表现形式如同ElementUI中的tree组件。

直接使用本案例的朋友这里要注意一点,外部传入数据可以按照如下方式传入,label表示名称,value表示key或者id等:
示例参数:

tree: [
        {
          label: "一级1",
          value: "level_1",
          children: [
            {
              label: "二级1",
              value: "level_1_1",
              children: [
                {
                  label: "三级1",
                  value: "level_1_1_1",
                },
                {
                  label: "三级2",
                  value: "level_1_1_2",
                },
              ],
            },
            {
              label: "二级2",
              value: "level_1_2",
              children: [
                {
                  label: "三级2-1",
                  value: "level_1_2_1",
                },
                {
                  label: "三级2-2",
                  value: "level_1_2_2",
                },
                {
                  label: "三级2-3",
                  value: "level_1_2_3",
                },
              ],
            },
            {
              label: "二级3",
              value: "level_1_3",
              children: [
                {
                  label: "三级3-1",
                  value: "level_1_3_1",
                },
                {
                  label: "三级3-2",
                  value: "level_1_3_2",
                },
                {
                  label: "三级3-3",
                  value: "level_1_3_3",
                },
                {
                  label: "三级3-4",
                  value: "level_1_3_4",
                },
              ],
            },
          ],
        },
        {
          label: "一级2",
          value: "level_2",
          children: [
            {
              label: "二级2-1",
              value: "level_2_1",
              children: [
                {
                  label: "三级2-1-1",
                  value: "level_2_1_1",
                },
              ],
            },
          ],
        },
      ],

el-collapse-transition不生效问题

细心的朋友会发现,上述代码中用到了el-collapse-transition标签,这是ElementUI提供的内置过度动画中的一个标签,用来进行展开折叠操作。

有朋友自己写组件用到了此标签,引用后发现不生效,并没有展开折叠的动效。

解决方法:
1.用一个单独的简单示例操作此标签,查看是否生效,如果生效,说明问题出在你引用此标签的组件内。
2.检查组件的最外层div是否有v-if操作,如果有,干掉,再次检查是否生效。

el-collapse-transition标签的外层,最好不要有v-if操作,尤其是当你采用了局部视图更新方案时,此标签会因为视图更新失效。

点此下完源码资源


欢迎关注本博主:小圣贤君,有问题可以留言哦~

你可能感兴趣的:(vue,树形选择组件,tree组件,递归组件,el-collapse不生效,多级列表组件)