解决vue2版本,el-select下拉框数据量过多,el-select进行分页请求懒加载,滚动防抖加载,支持关键字过滤及其他el-select属性

时间:2025-03-23 18:58:00
<template> <!--分页远程请求数据(辅以虚拟化列表)--> <el-select ref="elSelectRef" v-model="valueModel" v-loadData="loadData" :disabled="disabled" filterable :filter-method="filterMethod" class="w-full" :popper-class="`more-select-dropdown ${loading && 'loading'}`" :clearable="clearable" :placeholder="placeholder" clear="filterMethod" change="onChange" visible-change="visibleChange" :size="size" > <li class="start" :style="{ height: startHeight + 'px' }" /> <!--选项--> <!-- <template v-for="(item, index) in resultList"> --> <el-option v-for="(item, index) in resultList" @click.native="changedWay && changedWay(changedParams, valueModel)" :key="item[valueKey] + `'${index}'`" :label="item[labelKey]" :value="item[valueKey]" /> <!-- </template> --> <li class="end" :style="{ height: endHeight + 'px' }" /> </el-select> </template> <script> import Vue from 'vue' import { debounce } from 'lodash-es' // 选项每行高度 const OPTION_LINE_HEIGHT = 34 // 展示多少行 const VIEW_LENGTH = 50 Vue.directive('loadData', { bind (el, binding) { // 获取element-ui定义好的scroll盒子 const SELECTWRAP_DOM = el.querySelector( '.el-select-dropdown .el-select-dropdown_wrap' ) SELECTWRAP_DOM.addEventListener('scroll', function () { /** * scrollHeight获取元素内容高度(只读) *scrollTop获取或者设置元素的偏移值常用于,计算滚动条的位置,当一个元素的容器没有产生垂直方向的滚动条,那它的scrolTop的值默认为0O.*clientHeight读取元素的可见高度(只读) *如果元素滚动到底,下面等式返回true,没有则返回false:* - === ; */ const condition = parseInt(this.scrollHeight - this.scrollTop) <= this.clientHeight // 监听下拉框是否滚动到底部,滚动到底部就加载下一页数据 binding.value(this.scrollTop, condition) }) } }) export default { props: { // 父组件双向绑定值 value: { type: [Array, String, Number], default: '' }, // 默认选项label值 defaultValueLabel: { type: String, default: '' }, // 默认一次加载大小值 size: { type: String, default: '' }, // 是否禁用 disabled: { type: Boolean, default: false }, // 是否可以多选 // multiple: { // type: Boolean, // default: false // }, // 是否可以清空 clearable: { type: Boolean, default: true }, // 远程搜索时,输入框搜索值的键名 searchKey: { type: String, default: 'keyword' }, // 获取下拉框值得方法名 request: { type: Function, default: null }, // 选中下拉框值后根据选中值立即调用的函数 changedWay: { type: Function, default: null }, // 选中下拉框值后根据选中值立即调用的函数的参数 changedParams: { type: Number, default: undefined }, // 获取下拉框值时默认参数 getListParams: { type: Object, default: () => ({}) }, // 是否格式化数据 formateData: { type: Boolean, default: false }, // 下拉框值的格式 getListFormat: { type: Function, default: data => { return data.map(item => ({ label: item, value: item })) } }, // 默认作为value的键值 valueKey: { type: String, default: 'id' }, // 默认作为label的键值 labelKey: { type: String, default: 'name' }, placeholder: { type: String, default: '' } }, data () { return { OPTION_LINE_HEIGHT, list: [], // activeValue: ", searchText: '', pageNum: 1, pageSize: 100, loading: false, // 是否加载完所有数据 finished: false, // 当前可视区域第一条(虚拟化) currentIndex: 0, // 是否已经加载 isCreated: false, // 是否已经重置,用于减少重置次数 hasReset: false } }, computed: { valueModel: { get () { return this.value }, set (val) { this.$emit('input', val) } }, // 虚拟化隐藏的上条数 hiddenLength () { return Math.max(this.currentIndex - VIEW_LENGTH, 0) }, // 最终展示的选项列表(虚拟化) resultList () { // 当前值可能不在分页列表里,因此手动放入 // 搜索时则不需要 // 虚拟化 return (this.valueModel && !this.searchText && !this.list.some(item => item[this.valueKey] === this.valueModel) ? [ { [this.labelKey]: this.defaultValueLabel, [this.valueKey]: this.valueModel }, ...this.list ] : this.list ).slice(this.hiddenLength, this.currentIndex + VIEW_LENGTH) }, // 填充虚拟化的上高度 startHeight () { return this.hiddenLength * OPTION_LINE_HEIGHT }, // 填充虚拟化的下高度 endHeight () { return ( Math.max( this.list.length - this.resultList.length - this.hiddenLength, 0 ) * OPTION_LINE_HEIGHT ) } }, watch: { value (val) { val && this.resetCurrentIndex() // 设置重置标识 this.hasReset = true } }, created () { // (); }, methods: { getOptionData () { if (!this.request || this.finished) return this.loading = true const params = { ...this.getListParams, [this.searchKey]: this.searchText, page: this.pageNum, size: this.pageSize } this.request(params) .then(res => { if (!res) return // let { list: data, total } = res // 对数据进行格式化 // data = (data); // = (data) this.list = res.data if (this.formateData) { this.list = this.getListFormat(this.list) } // 判断(当list长度小于size或等于返回列表总数为最后一页) if ( this.list.length < (this.pageSize || 0) || this.list.length === res.total ) { this.finished = true } this.pageNum += 1 }) .finally(() => { this.loading = false }) }, /** *搜索事件(防抖处理) */ filterMethod: debounce(function (searchText = '') { if (searchText === this.searchText) return this.list = [] this.pageNum = 1 this.finished = false this.searchText = searchText this.getOptionData() }, 500), /** *加载数据(防抖处理) */ loadData (scrollTop, condition) { this.currentIndex = Math.round(scrollTop / OPTION_LINE_HEIGHT) if (!condition) return this.getOptionData() }, /** *选值变化 */ onChange (val) { // 更新对应文本值 const info = this.list.find(it => it[this.valueKey] === val) || {} const itemLabel = info[this.labelKey] itemLabel && this.$emit('update:defaultValueLabel', itemLabel) this.$emit('change', val) this.$emit('change-info', info) }, /** *重置可视区域 */ resetCurrentlndex () { const currentlndex = this.list.findIndex( item => item.value === this.valueModel ) this.currentlndex = Math.max(currentlndex, 0) }, /** *下拉框出现/隐藏 * param visible是否出现 */ visibleChange (visible) { // 下拉框隐藏 if (!visible) { // 清除搜素值 if (this.searchText) { this.filterMethod() } // 未重置则重置 if (!this.hasReset) { this.resetCurrentlndex() } else { // 清除重置标识 this.hasReset = false if (!this.isCreated) { this.getOptionData() this.isCreated = true } } } }, /** *清空重置选项部分 */ clearOptions () { this.list = [] this.pageNum = 1 this.finished = false this.isCreated = false }, /** *清空重置 */ clear () { this.valueModel = '' this.clearOptions() } } } </script> <style lang="scss"> //更多数据的加载 .more-select-dropdown.loading::after { content: "加载中"; width: 100%; text-align: center; line-height: 34px; height: 34px; position: absolute; bottom: 0; background: #fff; font-size: 14px; color: #999; } </style>