最近在跟一个中烟的项目,我们单位是中烟的承接单位,碰到了一个树形结构的下拉框,卡顿比较严重,还有树形结构的穿梭框也是,这个树形结构是后端一次性给前端了,经过ant-design-vue中的树形组件渲染后,页面的dom元素会有很多,每一个dom元素上产生的交互事件就会卡顿,如下图所示
一开始我的想法就是改变dom渲染的数量,但是因为它是下拉框,我现在的实力做不到用户在使用的时候很平顺,可以滑动
之后就想到了用虚拟列表去做,封装成一个组件,为什么要封装成组件呢?因为ant-design-vue的form表单内部就维护了一个form表单,得使用它的v-decorator来做数据绑定,不然会很卡。
解决方案:自己做一个树形结构的下拉多选框,其中下拉框中的数据进行虚拟列表处理,input框要实现多选的标签化。所以下拉树中绑定的数据要和input框中的数据实时同步,我们这边的产品还要在标签数量超过15个的时候,展示剩余多少项。
针对input框:
// 这里的话大家可以用input的一个vue插件
// http://www.vue-tags-input.com/#/start
npm install @johmun/vue-tags-input
import VueTagsInput from '@johmun/vue-tags-input';
input框的话可以用2个input来进行切换,使用v-show减少开销,不必重新生成dom,具体的一些操作大家可以根据需求看文档来实现,我之后会把封装的组件代码都放上来。
针对下拉框的虚拟列表:
针对虚拟列表的插件有很多,vue本身也提供了虚拟列表的实现,我一般常用的就是vue-virtual-scroll-list.
针对树形结构github上也有做了封装的,名字叫ctree,链接:https://github.com/wsfe/vue-tree/tree/2.x
npm install @wsfe/ctree
import {CTreeSearch} from "../../node_modules/@wsfe/ctree"
Vue.component('CTreeSearch', CTreeSearch)
@import '~@wsfe/ctree/dist/ctree.css';
最终的实现效果:
页面的虚拟dom,我这边是始终渲染200个dom元素,可以看到右边的dom元素改变
<template>
<div class="meetingTree" v-click-outside="onClickOutside">
<div @mouseenter="tagsinputMouseEnter" @mouseleave="tagsinputMouseLeave">
<vue-tags-input
v-show="isTag"
:tags="enjoyTags"
v-model="enjoyTagsInput"
placeholder="请选择参会人员"
ref="vuetagsinput"
@before-deleting-tag="deleteTagHandle"
@focus="enjoyMeetingOpenHandle"
@input="vuetagsInputHandle"
:add-only-from-autocomplete="true"
/>
<vue-tags-input
v-show="!isTag"
:tags="enjoyTagsCopy"
v-model="enjoyTagsCopyInput"
@before-deleting-tag="deleteTagHandle"
placeholder="请选择参会人员"
@focus="enjoyMeetingOpenHandle"
@input="vuetagsInputHandle"
:add-only-from-autocomplete="true"
ref="vuetagsinputcopy"
/>
<a-icon v-show="tagsinputFlag" class="close-icon" type="close-circle" @click="clearTagsHandle"/>
</div>
<div class="ctree-search">
<div :style="{ ...treeStyle }" v-show="ctreeSearchFlag">
<CTree-Search
:data="personSelectData"
:showCheckAll="false"
v-model="conferee_uuids"
:showCheckedButton="false"
:renderNodeAmount="200"
:bufferNodeAmount="200"
default-expand-all
checkable
searchPlaceholder="请选择参会人员"
@check="ctreeCheck"
@uncheck="ctreeUncheck"
@checked-change="checkedChange"
ref="ctreeSearch"
>
<template slot="footer">
<div class="footer-box">
已选{{this.enjoyTags.length}}项
</div>
</template>
</CTree-Search>
</div>
</div>
</div>
</template>
<script>
import VueTagsInput from '@johmun/vue-tags-input';
export default {
name: 'LjMeetingSelect',
props:{
personSelectData:{
type:Array,
default:[],
},
originTreeData:{
type:Array,
default:[],
},
// 展开和隐藏,emit父组件改变父组件的isTag
isTag:{
type:Boolean,
default:true,
},
addGroupData:{
type:Array,
default:function(){
return [];
}
}
},
data() {
return {
conferee_uuids:[],
enjoyTags:[], // 参会人员的tags,数量变多后,隐藏起来
enjoyTagsCopy:[], // 参会人员tags的副本,用于展示所有的tags
tagsParents:[], // 删除tags时父类的id
tagsinputFlag:false,
enjoyTagsInput:"",
enjoyTagsCopyInput:"",
firstFlag:true,
ctreeSearchFlag:false,
}
},
components:{VueTagsInput},
computed: {
treeStyle() {
const { treeHeight = 300, localScroll: overflow } = this
let height = treeHeight + 'px'
return {
height,
overflow,
}
},
},
watch:{
enjoyTags(newval,oldval){
let deppArrayCopy=JSON.parse(JSON.stringify(this.enjoyTags));
this.enjoyTagsCopy=deppArrayCopy // 这两组数据在前15个时始终相等
if(newval.length>15){
if(this.firstFlag){
this.firstIsTag()
}
this.isTagChange(true)
deppArrayCopy.splice(15,this.enjoyTags.length-15)
let data={
text:"共计"+this.enjoyTags.length+"项"
}
this.enjoyTagsCopy.push(data)
}else{
this.isTagChange(false)
}
},
// 群组选择的监听,需要往uuids和tags中添加数据,不能重复
addGroupData(newval){
// 选中对应的节点
this.$refs.ctreeSearch.setCheckedKeys(newval[0],true)
}
},
methods: {
// 勾选的时候触发
ctreeCheck(value){
this.changeSearch()
// this.deeploopCheck(value)
},
// 取消勾选的时候触发
ctreeUncheck(value){
this.changeSearch()
// this.deeploopUncheck(value)
},
checkedChange(value){
this.enjoyTags=[]
value.map((value)=>{
if(value.isLeaf){
let data={
text:value.title,
id:value.id
}
this.enjoyTags.push(data)
}
})
},
// 递归找到所选择的节点信息并增加进enjoyTags
deeploopCheck(value){
if(value.isLeaf){
let data={
text:value.title,
id:value.id
}
this.enjoyTags.push(data)
}else{
value.children.map((item)=>{
this.deeploopCheck(item)
})
}
},
// 递归找到所有不选择的所有节点,在enjoyTags中删除
deeploopUncheck(value){
if(value.isLeaf){
this.enjoyTags=this.enjoyTags.filter((item)=>{
return item.id != value.id
})
}else{
value.children.map((item)=>{
this.deeploopUncheck(item)
})
}
},
// 递归找到父id
loopParentId(id,array){
array.map((item)=>{
if(item.id===id){
this.loopParentId(item.pId,array)
this.tagsParents.push(id)
}
})
},
// 删除tags标签中的数据
deleteTagHandle(value){
if(value.tag.id){
console.log("删除")
this.tagsParents=[]
// 目录层级超过2层的删不掉,因为层级数太多,过滤不了
this.loopParentId(value.tag.id,this.originTreeData) // id 过滤出所有父类id
// 对conferee_uuid中的值进行删除,并且对enjoysTags进行删除
// 1.对conferee_uuid进行删除
this.conferee_uuids=this.conferee_uuids.filter((item)=>{
// return item != (deleteId && parentId)
if(!this.tagsParents.includes(item)){
return item
}
})
// 2.对enjoysTags进行删除
this.enjoyTags.splice(value.index,1)
}else{
console.log("未进入删除")
}
},
// 子传父的调用方法
toChangeTree(){
let checkNodes=this.$refs.ctreeSearch.getCheckedNodes()
let idvalue=[]
let namevalue=[]
checkNodes.map(item=>{
idvalue.push(item.id)
// 非叶子节点不放入
if(item.isLeaf){
namevalue.push(item.title)
}
})
let data=[idvalue,namevalue]
this.$emit("treeChange",data)
},
// 给父组件传递值
// toSaveData(){
// this.$emit("uuidChange",this.conferee_uuids)
// },
isTagChange(value){
this.$emit("isTagChange",value)
},
// 第一次超出15个tags
firstIsTag(){
this.firstFlag=false
this.$emit("firstIsTag",true)
},
tagsinputMouseEnter(){
if(this.conferee_uuids.length>0){
this.tagsinputFlag=true
}
},
tagsinputMouseLeave(){
this.tagsinputFlag=false
},
// 清空数据
clearTagsHandle(){
this.enjoyTags=[]
this.enjoyTagsCopy=[]
this.$refs.ctreeSearch.clearChecked()
// 同样需要调用父组件treechange方法改变表单数据
this.toChangeTree()
},
enjoyMeetingOpenHandle(){
this.ctreeSearchFlag=true
},
// 触发点击外部div
onClickOutside (event) {
this.ctreeSearchFlag=false
// 在这里传值并且提交表单
this.toChangeTree()
// this.toSaveData()
},
// tagsinput框搜索触发
vuetagsInputHandle(value){
this.debounce(this.$refs.ctreeSearch.search(value),500)
},
//防抖
debounce(fn, delay) {
let timer = null; //借助闭包
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(fn, delay); // 简化写法
};
},
changeSearch(){
this.$refs.vuetagsinput.newTag=""
this.$refs.vuetagsinputcopy.newTag=""
this.$refs.ctreeSearch.search()
}
},
}
</script>
<style scoped lang="less">
@import '~@wsfe/ctree/dist/ctree.css';
.meetingTree{
position:relative;
.ctree-search{
position: absolute;
z-index: 1;
background: #ffffff;
width: 100%;
}
.close-icon{
position: absolute;
right: 5px;
top: 5px;
cursor:pointer;
}
}
/deep/.ctree-tree-search__search{
display:none !important;
}
/deep/.ctree-tree__block-area {
margin-top: 10px !important;
}
/deep/.ctree-tree-node__checkbox {
position: relative !important;
}
/deep/.vue-tags-input{
max-width:none !important;
}
/deep/.ti-tag{
background-color:#fafafa !important;
color:#000000 !important;
font-size:14px !important;
border: 1px solid #e8e8e8 !important;
}
</style>
大家有需要的可以直接复制粘贴这个组件,页面引入即可。
很长时间不更新了,之后会慢慢继续分享所学,因为之前出于个人规划脱产了一段时间。
之后会更新一下C语言的算法,再之后会发布一些node和vue3碰到的问题和解决方案