一、问题描述
表单中某下拉框,由于数据过多,选择的时候会因为数据量过大导致页面卡顿,于是对于el-select进行二次封装,实现虚拟滚动。
二、实现如下:
看起来是加载了全部数据,实际上只加载了自己设定的10条(可修改)数据。
安装
npm install vue-virtual-scroll-list --save
代码:
VirtualSelect.vue
<template><div><el-selectpopper-class="virtualselect"class="virtual-select-custom-style":value="defaultValue"filterable:filter-method="filterMethod"default-first-optionclearable:placeholder="placeholder":disabled="disabled":multiple="isMultiple":allow-create="allowCreate"@visible-change="visibleChange"v-on="$listeners"@clear="clearChange"><virtual-listref="virtualList"class="virtualselect-list":data-key="value":data-sources="selectArr":data-component="itemComponent":keeps="keepsParams":extra-props="{label: label,value: value,isRight: isRight,isConcat: isConcat,concatSymbol: concatSymbol}"></virtual-list></el-select></div>
</template>
<script>const validatenull = (val) => {if (typeof val === 'boolean') {return false}if (typeof val === 'number') {return false}if (val instanceof Array) {if (val.length===0) return true} else if (val instanceof Object) {if (JSON.stringify(val) === '{}') return true} else {if (val==='null' || val===null || val==='undefined' || val===undefined || val==='') return truereturn false}return false}import virtualList from 'vue-virtual-scroll-list'import ElOptionNode from './elOptionNode.vue'export default {name: 'VirtualSelect',components: {'virtual-list': virtualList},model: {prop: 'bindValue',event: 'change'},props: {// 数组list: {type: Array,default() {return []}},// 显示名称label: {type: String,default: ''},// 标识value: {type: String,default: ''},// 是否拼接label | valueisConcat: {type: Boolean,default: false},// 拼接label、value符号concatSymbol: {type: String,default: ' | '},// 显示右边isRight: {type: Boolean,default: false},// 加载条数keepsParams: {type: Number,default: 10},// 绑定的默认值bindValue: {type: [String, Array],default() {if (typeof this.bindValue === 'string') return ''return []}},// 是否多选isMultiple: {type: Boolean,default: false},// 输入框占位文本placeholder: {type: String,default: '请选择'},// 是否禁用disabled: {type: Boolean,default: false},// 是否允许创建条目allowCreate: {type: Boolean,default: false}},data() {return {itemComponent: ElOptionNode,selectArr: [],defaultValue: null // 绑定的默认值}},watch: {'list'() {this.init()},bindValue: {handler(val, oldVal) {this.defaultValue = this.bindValueif (validatenull(val)) this.clearChange()this.init()},immediate: false,deep: true}},mounted() {this.defaultValue = this.bindValuethis.init()},methods: {init() {if (!this.defaultValue || this.defaultValue?.length === 0) {this.selectArr = this.list} else {// 回显问题// 由于只渲染固定keepsParams(10)条数据,当默认数据处于10条之外,在回显的时候会显示异常// 解决方法:遍历所有数据,将对应回显的那一条数据放在第一条this.selectArr = JSON.parse(JSON.stringify(this.list))let obj = {}if (typeof this.defaultValue === 'string' && !this.isMultiple) {if (this.allowCreate) {const arr = this.selectArr.filter(val => {return val[this.value] === this.defaultValue})if (arr.length === 0) {const item = {}// item[this.value] = `Create-${this.defaultValue}`item[this.value] = this.defaultValueitem[this.label] = this.defaultValueitem.allowCreate = truethis.selectArr.push(item)this.$emit('selChange', item)} else {this.$emit('selChange', arr[0])}}// 单选for (let i = 0; i < this.selectArr.length; i++) {const element = this.selectArr[i]if (element[this.value]?.toLowerCase() === this.defaultValue?.toLowerCase()) {obj = elementthis.selectArr?.splice(i, 1)break}}this.selectArr?.unshift(obj)} else if (this.isMultiple) {if (this.allowCreate) {this.defaultValue.map(v => {const arr = this.selectArr.filter(val => {return val[this.value] === v})if (arr?.length === 0) {const item = {}// item[this.value] = `Create-${v}`item[this.value] = vitem[this.label] = vitem.allowCreate = truethis.selectArr.push(item)this.$emit('selChange', item)} else {this.$emit('selChange', arr[0])}})}// 多选for (let i = 0; i < this.selectArr.length; i++) {const element = this.selectArr[i]this.defaultValue?.map(val => {if (element[this.value]?.toLowerCase() === val?.toLowerCase()) {obj = elementthis.selectArr?.splice(i, 1)this.selectArr?.unshift(obj)}})}}}},// 搜索filterMethod(query) {if (!validatenull(query?.trim())) {this.$refs.virtualList.scrollToIndex(0) // 滚动到顶部setTimeout(() => {this.selectArr = this.list.filter(item => {return this.isRight || this.isConcat? (item[this.label].trim()?.toLowerCase()?.indexOf(query?.trim()?.toLowerCase()) > -1 || item[this.value]?.toLowerCase()?.indexOf(query?.trim()?.toLowerCase()) > -1): item[this.label]?.toLowerCase()?.indexOf(query?.trim()?.toLowerCase()) > -1})}, 100)} else {setTimeout(() => {this.init()}, 100)}},visibleChange(bool) {if (!bool) {this.$refs.virtualList.reset()this.init()}this.$emit('visible-change', bool)},clearChange() {if (typeof this.defaultValue === 'string') {this.defaultValue = ''} else if (this.isMultiple) {this.defaultValue = []}this.visibleChange(false)}}}
</script>
<style scoped>
/* .virtual-select-custom-style ::v-deep .el-select-dropdown__item {// 设置最大宽度,超出省略号,鼠标悬浮显示// options 需写 :title="source[label]"width: 250px;display: inline-block;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;
} */
.virtualselect {max-height:245px;overflow-y:auto;/* // 设置最大高度&-list {max-height:245px;overflow-y:auto;} */
}
.virtualselect-list {max-height:245px;overflow-y:auto;
}
.el-select {width: 100%;
}
::-webkit-scrollbar {width: 6px;height: 6px;background-color: transparent;cursor: pointer;margin-right: 5px;
}
::-webkit-scrollbar-thumb {background-color: rgba(144,147,153,.3) !important;border-radius: 3px !important;
}
::-webkit-scrollbar-thumb:hover{background-color: rgba(144,147,153,.5) !important;
}
::-webkit-scrollbar-track {background-color: transparent !important;border-radius: 3px !important;-webkit-box-shadow: none !important;
}
::v-deep .el-select__tags {flex-wrap: unset;overflow: auto;
}
</style>
<style>
.virtualselect .el-select-dropdown__item{display: block;max-width: 350px;overflow: visible;
}
</style>
elOptionNode.vue
<template><el-option:key="label+value":label="concatString(source[label], source[value])":value="source[value]":disabled="source.disabled":title="concatString(source[label], source[value])"><span>{{ concatString(source[label], source[value]) }}</span><spanv-if="isRight"style="float:right;color:#939393">{{ source[value] }}</span></el-option>
</template>
<script>export default {name: 'ElOptionNode',props: {// 每一行的索引index: {type: Number,default: 0},// 每一行的内容source: {type: Object,default() {return {}}},// 需要显示的名称label: {type: String,default: ''},// 绑定的值value: {type: String,default: ''},// 是否拼接label | valueisConcat: {type: Boolean,default: false},// 拼接label、value符号concatSymbol: {type: String,default: ' | '},// 右侧是否显示绑定的值isRight: {type: Boolean,default() {return false}}},methods: {concatString(a, b) {a = a || ''b = b || ''if (this.isConcat) {return a + ((a && b) ? this.concatSymbol : '') + b}return a}}}
</script>
demo.vue
<template><div><div style="width: 300px;"><virtual-selectv-model="selectValue":list="selectValueOptions"label="name"value="code":placeholder="'请选择(单选)'":keeps-params="10":is-concat="true":concat-symbol="' || '":is-multiple="false":disabled="false":allow-create="false"@change="changeVal"@visible-change="visibleChange"@clear="clearChange"@blur="blurChange"@focus="focusChange"/><virtual-selectv-model="selectValue2":list="selectValueOptions"label="name"value="code"placeholder="请选择(多选)":keeps-params="10":is-concat="false":is-multiple="true":is-right="true":disabled="false":allow-create="true"@change="changeVal"@remove-tag="removeTag"@visible-change="visibleChange"@clear="clearChange"@focus="focusChange"/></div></div>
</template><script>
import VirtualSelect from '@/components/VirtualSelect/VirtualSelect.vue'
export default {name: 'VirtualSelectDemo',components: { VirtualSelect },data () {return {selectValue: null,selectValue2: null,selectValueOptions: []}},created() {this.getList(10000, 1000).then(res => {this.selectValueOptions = res})},methods: {changeVal(val) {console.log(val, 'changeVal val')},removeTag(tag) {console.log(tag, 'removeTag tag')},visibleChange(bool) {console.log(bool, 'visibleChange bool')},clearChange(val) {console.log(val, 'clearChange val')},blurChange(val) {console.log(val, 'blurChange val')},focusChange(val) {console.log(val, 'focusChange val')},getList(num = 10000, time) {return new Promise((resolve, reject) => {setTimeout(() => {const tempArr = []let count = numwhile (count--) {const index = num - counttempArr.push({code: `${index}$${Math.random().toString(16).substring(9)}`,index,name: `测试数据-${index}`,value: index})}resolve(tempArr)}, time)})},}
}
</script>
<style scoped>
</style>
参考:
封装el-select,实现虚拟滚动,可单选、多选、搜索查询、创建条目