场景
element-plus中有虚拟化选择器el-select-v2实现滚动加载下拉框,但是在一些比较老的项目中使用的是el-ement版本的UI,除了单独引入el-select-v2进行局部引用外,我们还可以自己手动封装一个滚动分页下拉
代码如下
<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:*ele.scrollHeight - ele.scrollTop === ele.clientHeight; */
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 () {
// this.getOptionData();
},
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 = this.getListFormat(data);
// this.list = this.list.concat(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>
不足之处
该手动封装的组件只可以向下滚动请求(page+1),当向上滚动到当前page的第一条时无法再page-1进行请求,所以size需要写大一点,或者感兴趣的小伙伴可以在此基础上进行优化,欢迎给出建议