实现思路:
1: 获取列表数据,计算出总数total
2: 根据行高,视口高度,计算出需要显示的数量
3: 监听滚动区域,计算需要位移的距离,添加节流控制避免卡顿
4: 显示列表渲染
直接贴代码
虚拟列表组件:
<template>
<el-select
v-model="value"
class="chooseSelect"
:popper-class="popperClass"
:filterable="filterable"
:clearable="clearable"
:filter-method="filterMethod"
:disabled="disabled"
:multiple="selectAttributes.multiple || false"
:multiple-limit="selectAttributes.multipleLimit || 0"
:placeholder="placeholderValue"
@visible-change="handleVisibleChange"
@change="changeValue"
>
<el-option
v-for="(item, index) in optionsData"
:key="index"
:label="item.label"
:value="item"
></el-option>
</el-select>
</template>
<script>
import { _throttle } from '@/utils/public'
/**
* 长列表select
*/
export default {
props: {
// 绑定值
model: {
type: [String, Number, Boolean],
default: ''
},
// 下拉数据
options: {
type: [Array, Object],
default: () => ([])
},
// 支持过滤
filterable: {
type: Boolean,
default: false
},
// 支持清除
clearable: {
type: Boolean,
default: false
},
// 占位符
placeholder: {
type: String,
default: '请选择'
},
// 是否禁用
disabled: {
type: Boolean,
default: false
},
// 传入为Array时,需要定义标签和value的name [labelName, valueName]
labelValue: {
type: Array,
default: () => (['label', 'value'])
},
// el-select配置项
selectAttributes: {
type: Object,
default: () => ({})
}
},
computed: {
popperClass() {
return 'popperClass' + Date.now()
}
},
watch: {
options: {
immediate: true,
handler(val) {
this.filterOptions = this.handleOption(val)
this.modelToValue(this.model)
},
deep: true
},
filterOptions: {
handler() {
this.init()
},
deep: true
},
model: {
immediate: true,
handler(val) {
this.modelToValue(val)
}
}
},
data() {
return {
value: null,
// 过滤后源数据
filterOptions: [],
// 下拉框数据
optionsData: [],
// 可视区域的高度250(element默认,修改的话需要对应修改样式)
viewHeight: 250,
// 当前行的默认高度30(element默认,修改的话需要对应修改样式)
rowHeight: 30,
startIndex: 0,
endIndex: 0,
// 滚动容器
scrollView: null,
// 占位容器
placeholderView: null,
// 是否注册监听
isSignIn: false,
// 当前选中的item
cacheItem: {},
placeholderValue: ''
}
},
mounted() {
this.placeholderValue = this.placeholder
},
beforeDestroy() {
if (this.scrollView && this.isSignIn) {
this.scrollView.removeEventListener('scroll', this.handleScroll)
}
},
methods: {
// 用于 初始化回显
modelToValue(val) {
const options = this.handleOption(this.options) || []
const item = options.find(item => val === item.value) || {}
this.value = item.label || this.value || null
this.cacheItem = item
},
init() {
const { filterOptions, rowHeight, viewHeight } = this
// 滚动区域
const scrollView = document.querySelector(`.${this.popperClass} .el-scrollbar__wrap`)
// 虚拟列表占位区域
const placeholderView = document.querySelector(`.${this.popperClass} .el-select-dropdown__list`)
// 注册监听
if (scrollView && !this.isSignIn) {
this.scrollView = scrollView
this.isSignIn = true
this.scrollView.addEventListener('scroll', this.handleScroll)
}
if (placeholderView) {
this.placeholderView = placeholderView
// 所有数据的总条数
const total = filterOptions.length
if (total < Math.floor(viewHeight / rowHeight)) {
this.placeholderView.style.height = '100%'
} else {
this.placeholderView.style.height = `${total * rowHeight}px`
}
this.handleScroll()
}
},
// 滚动事件回调触发更新数据
updateOptions() {
const { filterOptions, viewHeight, rowHeight } = this
const total = filterOptions.length
// 可视区域的条数 向上取整
const limit = Math.ceil(viewHeight / rowHeight)
// 设置末位索引
this.endIndex = Math.min(this.startIndex + limit, total)
// 如果endIndex是最后一个,则重计算startIndex,避免后缀白条或被隐藏的情况
if (this.endIndex === total) {
this.startIndex = Math.max(this.endIndex - limit, 0)
}
this.optionsData = filterOptions.slice(this.startIndex, this.endIndex)
},
// 下拉框重新显示时归位
handleVisibleChange(val) {
// 当打开下拉框时,重置scrollView的paddingTop,避免白屏
if (this.placeholderView) {
this.$nextTick(() => {
this.placeholderView.style.paddingTop = '0px'
})
}
this.filterMethod('')
this.updateOptions()
// 处理filterable时value直接在input中
if (val && this.filterable) {
this.$nextTick(() => {
this.placeholderValue = this.value || this.placeholder
this.value = null
})
} else {
this.$nextTick(() => {
this.placeholderValue = this.placeholder
this.value = this.cacheItem.label
})
}
},
// 滚动监听
handleScroll: _throttle(function() {
const { rowHeight, startIndex } = this
const scrollTop = this.scrollView.scrollTop
// 计算当前滚动位置,获取当前开始的起始位置
const currentIndex = Math.floor(scrollTop / rowHeight)
// 根据滚动条获取当前索引与起始索引不相等时,将滚动的当前位置设置为起始位置
if (currentIndex !== startIndex) {
// 重新赋值
this.startIndex = Math.max(currentIndex, 0)
}
if (startIndex === 0) {
this.placeholderView.style.paddingTop = '0px'
} else {
this.placeholderView.style.paddingTop = `${startIndex * rowHeight}px`
}
this.updateOptions()
}, 20),
// 过滤数据
filterMethod(val) {
if (val) {
this.value = val
}
const options = this.handleOption(this.options)
this.filterOptions = options.filter(item => {
return item.label.includes(val)
})
},
// 处理Option类型问题
handleOption(options) {
const arr = []
if (options instanceof Array) {
options.forEach(item => {
arr.push({
label: item[this.labelValue[0]],
value: item[this.labelValue[1]],
item: item
})
})
} else if (options instanceof Object) {
// 对象类型 { 1: 'xxx' }转换为数组类型
for (const key in options) {
arr.push({
label: options[key],
value: key
})
}
}
return arr
},
changeValue(val) {
// 使用label,options改变时会导致只显示value
this.value = val.label
this.cacheItem = val || {}
this.$emit('update:model', val.value || null)
this.$emit('change', val)
}
}
}
</script>
<style lang="scss" scoped>
</style>
父组件调用:
<virtual-select
:options="upCompanyList"
:model.sync="editForm.parentId"
:label-value="['customName', 'id']"
filterable
clearable
placeholder="请选择上级公司"
@change="changeParent"
>
</virtual-select>
节流函数
// 节流
export function _throttle(fn, interval) {
let last
let timer
const intervalTimer = interval || 200
return function() {
const th = this
const args = arguments
const now = +new Date()
if (last && now - last < intervalTimer) {
clearTimeout(timer)
timer = setTimeout(function() {
last = now
fn.apply(th, args)
}, intervalTimer)
} else {
last = now
fn.apply(th, args)
}
}
}
效果图