vue + elementui 开发横向树形表格权限组件

2,935 阅读3分钟

功能介绍

基于 Vue.js 的递归组件知识,来开发一个不常见的树形table控件。
本节要实现的 Tree 组件具有以下功能:

image.png

  • 节点可以无限延伸(递归)
  • 节点可以选中,选中父节点,它的所有子节点也全部被选中,同样,反选父节点,其所有子节点也取消选择;
  • 节点的半选状态
  • 同一级所有子节点选中时,它的父级也自动选中,一直递归判断到根节点。
  • 父级节点支持展开收缩。

API

table-Tree 是典型的数据驱动型组件,所以节点的配置就是一个 data,里面描述了所有节点的信息,数据如下:

{
    "name": "权限管理",
    "id": 19,
    "children": [
        {
            "id": 58,
            "name": "新增角色",
        }
    ]
}

每个节点的配置(props:data)描述如下:

  • name:节点标题
  • checked:是否选中该节点。开启后,该节点的 Checkbox 将选中;
  • children:子节点属性数组
  • expand:节点展开收缩
  • indeterminate:节点半选

入口 treeTable.vue

src/components 中新建目录 tree,并在 tree 下创建两个组件 treeTable.vuesubTable.vue。treeTable.vue 是组件的入口,用于接收和处理数据,并将数据传递给 subTable.vue;subTable.vue 就是一个递归组件,它构成了每一个节点,一个多选框、节点标题以及递归的下一级节点。

tree.vue 主要做的事情:

  • 克隆数据 (JSON.parse(JSON.stringify()))
  • 加入showRow属性终止递归

传统的tree组件递归终止是根据children,如果一个节点没有 children 字段,那它就是最后一个节点 这里面情况不同,我们递归的最后一级始终是这个

image.png 这就要去给元数据加shouRow属性

// 递归的终止条件 用于判断当前项的子级的子级是否存在,存在为true,继续递归,不存在就终止

methods: {
    // 加入showRow属性,用于递归组件
    showRow (list) {
      list.forEach(item => {
        item.showRow = this.hasThreeChild(item.children)
        if (item.children && item.children.length !== 0) {
          this.showRow(item.children);
        }
      })
    },
    hasThreeChild (list) {
      return list.some(item => item.children.length !== 0)
    },
}

递归组件 subTable.vue

subTable.vue 是组件 treeTable 的核心,而一个 subTable 节点包含 4 个部分:

  • 半选
  • 全选
  • 节点标题
  • 递归子节点

先看subTable结构

 <template>
  <div class="permission-subTable">
    <div v-if="!menu.showRow" class="content-item">
      <div class="cell">
        <span v-if="menu.children && menu.children.length" class="table-expand" @click="handleExpand">
          <i v-if="menu.expand" class="el-icon-minus table-expand" />
          <i v-else class="el-icon-plus table-expand" />
        </span>
        <el-tooltip effect="dark" :content="menu.name " placement="right">
          <el-checkbox v-model="menu.checked" class="hasEllipsis" :indeterminate="menu.indeterminate" @change="(checked) => handleCheck(checked, menu)">
            {{ menu.name }}
          </el-checkbox>
        </el-tooltip>
      </div>
      <div v-show="menu.expand" class="last-content">
        <template v-if="menu.children && menu.children.length">
          <div v-for="child in menu.children" :key="child.id" class="last-child-item">
            <el-checkbox v-model="child.checked" @change="(checked) => handleCheck(checked, child)">
              {{ child.name }}
            </el-checkbox>
          </div>
        </template>
      </div>
      <div v-show="!menu.expand" style="flex:1;border: 0.5px solid #dcdfe6;" />
    </div>
    <div v-else class="table">
      <div class="title">
        <el-tooltip effect="dark" :content="menu.name " placement="right">
          <el-checkbox v-model="menu.checked" class="hasEllipsis" :indeterminate="menu.indeterminate" @change="(checked) => handleCheck(checked, menu)">
            {{ menu.name }}
          </el-checkbox>
        </el-tooltip>
      </div>
      <div class="content">
        <SubTable v-for="item in menu.children" :key="item.id" :menu="item" />
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'SubTable',
  props: {
    menu: {
      type: Object,
      default: () => ({}),
    },
  },
  data () {
    return {
    };
  },
  watch: {
    'menu.children': {
      handler (data) {
        if (data) {
          if (!data.length) return;
          const checkedAll = !data.some(item => !item.checked);
          const menu = this.menu;
          this.$set(menu, 'checked', checkedAll);
          const isIndeterminate = data.filter(item => (item.indeterminate));
          if (isIndeterminate.length) {
            this.$set(menu, 'indeterminate', true);
          } else {
            const checkChild = data.filter(item => (item.checked));
            const indeterminate = checkChild.length < data.length && checkChild.length > 0;
            this.$set(menu, 'indeterminate', indeterminate);
          }

        }
      },
      deep: true,
      immediate: true,
    },
  },
  mounted () {
  },
  methods: {
    handleExpand () {
      this.$set(this.menu, 'expand', !this.menu.expand);
    },
    handleCheck (checked, item) {
      this.checkChild(checked, item);
    },
    checkChild (checked, data) {
      if (data.children && data.children.length) {
        data.children.forEach(item => {
          this.checkChild(checked, item);
        });
      } else {
        data.checked = checked;
      }
    },
  }
};
</script>

<style scoped lang="scss">
.table {
  display: flex;
  overflow: auto;
}
.title {
  width: 158px;
  display: flex;
  align-items: center;
  border: 0.5px solid #dcdfe6;
  padding: 0 4px;
}
.content {
  flex: 1;
}
.item-content {
  display: flex;
}
.cell {
  width: 158px;
  border: 0.5px solid #dcdfe6;
  display: flex;
  align-items: center;
  justify-content: left;
  padding: 0 4px;
  .table-expand {
    cursor: pointer;
    margin-right: 2px;
  }
}

.last-content {
  display: flex;
  flex-wrap: wrap;
  flex: 1;
  border: 0.5px solid #dcdfe6;
  max-height: 200px;
  overflow: auto;
  padding: 0 12px;
}
.last-child-item {
  flex: 40%;
}
.content-item {
  display: flex;
}
</style>
<style lang="scss">
.permission-subTable {
  .hasEllipsis {
    .el-checkbox__label {
      width: 120px;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
      vertical-align: middle;
    }
  }
}
</style>

updateTreeDown 只是向下修改了所有的数据,因为当前节点的数据里,是包含其所有子节点数据的,通过递归遍历可以轻松修改

第 2 个逻辑相对复杂, 一个节点,除了手动选中(或反选),还有就是第 2 种逻辑的被动选中(或反选),也就是说,如果这个节点的所有直属子节点(就是它的第一级子节点)都选中(或反选)时,这个节点就自动被选中(或反选),递归地,可以一级一级响应上去。有了这个思路,我们就可以通过 watch 来监听当前节点的子节点是否都选中,进而修改当前的 checked 字段:

watch: {
    'menu.children': {
      handler (data) {
        if (data) {
          if (!data.length) return;
          const checkedAll = !data.some(item => !item.checked);
          const menu = this.menu;
          this.$set(menu, 'checked', checkedAll);
          const isIndeterminate = data.filter(item => (item.indeterminate));
          if (isIndeterminate.length) {
            this.$set(menu, 'indeterminate', true);
          } else {
            const checkChild = data.filter(item => (item.checked));
            const indeterminate = checkChild.length < data.length && checkChild.length > 0;
            this.$set(menu, 'indeterminate', indeterminate);
          }

        }
      },
      deep: true,
      immediate: true,
    },
  },

在 watch 中,监听了 data.children 的改变,并且是深度监听的。这段代码的意思是,当 data.children 中的数据的某个字段发生变化时(这里当然是指 checked 字段),也就是说它的某个子节点被选中(或反选)了,这时执行绑定的句柄 handler 中的逻辑。const checkedAll = !data.some(item => !item.checked); 也是一个巧妙的缩写,checkedAll 最终返回结果就是当前子节点是否都被选中了。

这里非常巧妙地利用了递归的特性,因为 SubTable.vue 是一个递归组件,那每一个组件里都会有 watch 监听 menu.children,要知道,当前的节点有两个”身份“,它既是下属节点的父节点,同时也是上级节点的子节点,它作为下属节点的父节点被修改的同时,也会触发上级节点中的 watch 监听函数。这就是递归

注意

文章参考于树形组件-tree 只知道他的github地址 所以贴一下

github.com/icarusion/v…

功能是如何去写传统的树形结构