文章为公司内部提供的分享文章,业务场景描述部分会涉及到内部框架 yt-org
以及 yt-admin
等陌生词汇,读者请手动忽略,或直接读 实现思路 及 实现代码部分。
首先提一下我们基于 el-cascader
二次封装的机构选择组件 yt-org
, 接触过营销平台的小伙伴都知道, 这个组件用起来是相当舒适的。没接触过没关系,我大概说一下功能:
yt-admin
中v-model
等 el-cascader 所提供所有功能众所周知,我们在使用类似树结构组件的多选时,会有一个是否 强关联 的属性,它是这样子的:
组件相当完美,然而需求更奇葩。 我在泰隆这边行方用到场景是 需要非强关联,支持能选父也能选择子,本来不开启强关联的多选完全满足场景,但业务提了需求: 要在此基础上支持勾选父,也选中所有子节点,省得一个个勾选。 what f ? 大概说下我的实现方式:改造 yt-org , 在勾选事件change
回调中搞事情:
yt-org
的value值和el-cascader
的vmodel值(currValue)分开el-cascader
的change回调参数里拿到改变后的数组,将此数组与value数组进行长度比较,可以得知是当前操作是选中还是取消这一套思路走下路,业务功能实现了,可是遇到了性能上的问题:当我当前勾选项的所有子节点较多时(这边机构总共大概有3500个子节点),勾选的显示会非常慢(9s左右)。你可以想象,我选择机构,点了根节点,反应了9s才勾选上是什么操作…
后来翻看 el-cascader 源码,是在加载完成后,回显时做了较多的比对操作,所以会很慢。
到这里由于el-cascade的性能限制,yt-org已经不适用我的场景了。于是决定自己写一个适用场景,反应快一点的树选择组件来完成功能。
因为是数据操作,所以组件实现中大量应用递归,其中包括了vue 组件的递归。
show
和 showItem
属性来对其进行控制。同理, checked
用于表示该节点选中与否。渲染子节点。 这一步主要用到了组件的递归。 先循环出treeData
的第一层,注意 用 v-show
控制该节点显示与否用于条件过滤; 然后调用递归显示children
。 这里我还用到了 el-collapse-transition
这个el过度动画。
子节点递归调用自身时需要注意层级关系,我们要用一个标志位进行层级标记。
实现子节点伸缩切换:点击节点时,判断该节点下是否有子节点,若有,将节点 show
取反来实现是否显示子节点;同时别忘了将该节点作为选中
勾选操作:在节点自定义的 checkbox 上绑定点击事件,若点击了checkbox选框,则: 若当前节点是选中状态(selected
为 true
),则只需设置selected
为false
即可, 若当前节点是未选中状态(selected
为 fasle
),则不仅需要将该节点selected
为true
, 还要递归子节点,将所有子节点全部勾选.
展开/收缩按钮: 递归渲染树数据,将节点的 show
设为true
或false
。 在这一步,如果是展开操作,考虑到性能,只递归三层,即限制最大展开层数为3, 避免几千条一次性加载导致的卡顿
全选/反选按钮: 递归渲染树数据,将节点的 selected
设为true
或false
。这一步最简单。
实现搜索:在头部 el-input 的keydown.enter.native
事件中做过滤: 还是递归渲染树数据,判断节点的 label
是否包含输入的值, 若有,则设置该节点的 showItem
为 true
, 并注意:若该条符合搜索条件,则他所有父节点都要应该显示出来,因此,在这里要进行反向递归,找到该节点所有父节点,并将其全部的 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>