手撸一个自定义关联方式的多选树组件

目录

    • 提示
    • 业务场景
    • 实现思路
      • 功能拆分
      • 实现逻辑
    • 代码实现
      • 树的实现
      • 逻辑组件
      • 预览

提示

文章为公司内部提供的分享文章,业务场景描述部分会涉及到内部框架 yt-org 以及 yt-admin 等陌生词汇,读者请手动忽略,或直接读 实现思路 及 实现代码部分。

业务场景

首先提一下我们基于 el-cascader 二次封装的机构选择组件 yt-org , 接触过营销平台的小伙伴都知道, 这个组件用起来是相当舒适的。没接触过没关系,我大概说一下功能:

  • 该组件为全局组件, 集成在 yt-admin
  • 第一次调用时会请求接口一次性拿到所有数据, 并存起来, 再次使用不会去做请求
  • 支持单/多选及 v-model 等 el-cascader 所提供所有功能

大概长这样:
手撸一个自定义关联方式的多选树组件_第1张图片

众所周知,我们在使用类似树结构组件的多选时,会有一个是否 强关联 的属性,它是这样子的:

  • 不开启强关联时,可以选择任一节点
  • 开始强关联时,勾选/取消父节点,所有子节点都会被勾选/取消,但最终value值里面只有所有子节点的

组件相当完美,然而需求更奇葩。 我在泰隆这边行方用到场景是 需要非强关联,支持能选父也能选择子,本来不开启强关联的多选完全满足场景,但业务提了需求: 要在此基础上支持勾选父,也选中所有子节点,省得一个个勾选。 what f ? 大概说下我的实现方式:改造 yt-org , 在勾选事件change回调中搞事情:

  • yt-org的value值和el-cascader 的vmodel值(currValue)分开
  • 深度立即监听value值,如有改变,立即赋给currValue
  • el-cascader的change回调参数里拿到改变后的数组,将此数组与value数组进行长度比较,可以得知是当前操作是选中还是取消
  • 如果是选中操作,再比对两数组,找出勾选项,再从树数据中找到勾选项,并递归该节点找到下属所有id,加入change回调参数里,$emit 加入后的数组,如果是取消操作, 直接 $emit change回调参数数组不用做处理。

这一套思路走下路,业务功能实现了,可是遇到了性能上的问题:当我当前勾选项的所有子节点较多时(这边机构总共大概有3500个子节点),勾选的显示会非常慢(9s左右)。你可以想象,我选择机构,点了根节点,反应了9s才勾选上是什么操作…

后来翻看 el-cascader 源码,是在加载完成后,回显时做了较多的比对操作,所以会很慢。

到这里由于el-cascade的性能限制,yt-org已经不适用我的场景了。于是决定自己写一个适用场景,反应快一点的树选择组件来完成功能。

实现思路

因为是数据操作,所以组件实现中大量应用递归,其中包括了vue 组件的递归。

功能拆分

  • 组件分弹窗和表单层,表单上以勾选数量 + 按钮的形式, 点击按钮打开弹窗
  • 弹窗内部通过树数据进行树节点渲染,点击节点展开折叠,并提供关键字搜索框, 展开,折叠,全选,反选按钮

实现逻辑

  • 我这边拿到的是标准的{ label: ‘’, value: ‘’, children: [] } 格式的树数据, 首先进行树节点渲染。为了控制 点击时子节点隐藏显示 以及 搜索时自身是否显示,我先在拿到数据的时候走一遍递归, 在每项中添加 showshowItem 属性来对其进行控制。同理, checked 用于表示该节点选中与否。
  • 渲染子节点。 这一步主要用到了组件的递归。 先循环出treeData的第一层,注意 用 v-show 控制该节点显示与否用于条件过滤; 然后调用递归显示children。 这里我还用到了 el-collapse-transition 这个el过度动画。

  • 子节点递归调用自身时需要注意层级关系,我们要用一个标志位进行层级标记。

  • 实现子节点伸缩切换:点击节点时,判断该节点下是否有子节点,若有,将节点 show 取反来实现是否显示子节点;同时别忘了将该节点作为选中

  • 勾选操作:在节点自定义的 checkbox 上绑定点击事件,若点击了checkbox选框,则: 若当前节点是选中状态(selectedtrue),则只需设置selectedfalse即可, 若当前节点是未选中状态(selectedfasle),则不仅需要将该节点selectedtrue, 还要递归子节点,将所有子节点全部勾选.

  • 展开/收缩按钮: 递归渲染树数据,将节点的 show 设为truefalse。 在这一步,如果是展开操作,考虑到性能,只递归三层,即限制最大展开层数为3, 避免几千条一次性加载导致的卡顿

  • 全选/反选按钮: 递归渲染树数据,将节点的 selected 设为truefalse。这一步最简单。

  • 实现搜索:在头部 el-input 的keydown.enter.native事件中做过滤: 还是递归渲染树数据,判断节点的 label 是否包含输入的值, 若有,则设置该节点的 showItemtrue, 并注意:若该条符合搜索条件,则他所有父节点都要应该显示出来,因此,在这里要进行反向递归,找到该节点所有父节点,并将其全部的 show, showItem 设置为true。 最后,考虑下性能:如果用户手贱输入 '' 空字符串直接搜索,那所有节点都会展示,因此还要在这里针对这种情况做下处理,若用户输入空,则只给展示三层。

ok,大致思路走下来如此,下面贴代码实现,我会在代码里尽量详细的做下注释。

代码实现

树的实现

tree.vue

<template>
    <ul class="menu-tree">
        
  • <div v-show="item.showItem" :class="{'itemTree':true,'active':actId == item.value}" @click="selectItem(item)"> <div :style="transform"> <i class="el-icon-caret-right" v-if="item.children && item.children.length === 0" style="opacity:0">i> <i class="el-icon-caret-right" v-if="item.children && item.children.length > 0 && !item.show" >i> <i class="el-icon-caret-bottom" v-if="item.children && item.children.length > 0 && item.show">i> <i class="selectBox" :class="{'checkName ':item.selected}" v-if="showCheckbox" @click.stop="checkItem(item)"> <i>i> i> {{item.label}} {{ item.children && item.children.length ? `(${item.children.length})` : '' }} div> div> <el-collapse-transition> Tree> el-collapse-transition> li> ul> template> <script> export default { name: 'Tree', props: { // 渲染树数据 menus: { type: Array, default: () => [] }, // 节点层级 depth: { type: [Number, String], default: 0 }, // 当前id actId: { type: [Number, String], default: '' }, showCheckbox: { type: Boolean, default: false } }, data () { return {} }, methods: { // 将selectItem方法暴露出去 selectItem (item) { this.$emit('selectItem', item) }, // 将checkItem方法暴露出去 checkItem (item) { this.$emit('checkItem', item) } }, computed: { // 通过传过来的depth计算下级目录的偏移量,这里我用的transform transform () { return 'transform:translateX(' + this.depth * 10 + 'px)' } } } script>