要求:自定义搜索、自定义指令,滚动懒加载每次展示10条数据
在element-plus中,el-select的选项框是使用popper.js生成的,不在body中,无法直接获取,这时需要巧妙地使用popper-class属性来获取到dom并挂载到el-select进行监听来实现我们的需求。
下面是代码
<template>
<el-select
v-model="value"
:loading="loading"
loading-text="数据加载中..."
filterable
clearable
popper-class="event-select-poper"
v-el-select-loadmore="loadmore"
:placeholder="placeholder"
style="width: 100%"
:filter-method="filterOptions"
@change="handleEventsChange"
@visible-change="handleVisibleChange">
<el-option
v-for="(item, index) in options"
:key="index"
:label="item.eventTheme"
:value="item.logCode">
</el-option>
</el-select>
</template>
在vue3的setup语法糖中,以v开头的变量会被识别为指令,一般指令变量命名使用小驼峰语法
<script setup>
import { ref, reactive, watch, nextTick, onBeforeMount } from 'vue'
const props = defineProps({
code: {
type: [String, Number],
default: ''
},
placeholder: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
}
})
const emits = defineEmits(['change'])
let options = reactive([])
let optionCodes = ref([])
let loading = ref(false)
let value = ref('')
let allEvents = reactive([
{ eventTheme: '1', logCode: 1 },
{ eventTheme: '2', logCode: 2 },
{ eventTheme: '3', logCode: 3 },
{ eventTheme: '4', logCode: 4 },
{ eventTheme: '5', logCode: 5 },
{ eventTheme: '6', logCode: 6 },
{ eventTheme: '7', logCode: 7 },
{ eventTheme: '8', logCode: 8 },
{ eventTheme: '9', logCode: 9 },
{ eventTheme: '10', logCode: 10 },
{ eventTheme: '11', logCode: 11 },
{ eventTheme: '12', logCode: 12 },
{ eventTheme: '13', logCode: 13 },
{ eventTheme: '14', logCode: 14 },
{ eventTheme: '15', logCode: 15 },
{ eventTheme: '16', logCode: 16 },
])
let allFilterEvents = reactive([
{ eventTheme: '1', logCode: 1 },
{ eventTheme: '2', logCode: 2 },
{ eventTheme: '3', logCode: 3 },
{ eventTheme: '4', logCode: 4 },
{ eventTheme: '5', logCode: 5 },
{ eventTheme: '6', logCode: 6 },
{ eventTheme: '7', logCode: 7 },
{ eventTheme: '8', logCode: 8 },
{ eventTheme: '9', logCode: 9 },
{ eventTheme: '10', logCode: 10 },
{ eventTheme: '11', logCode: 11 },
{ eventTheme: '12', logCode: 12 },
{ eventTheme: '13', logCode: 13 },
{ eventTheme: '14', logCode: 14 },
{ eventTheme: '15', logCode: 15 },
{ eventTheme: '16', logCode: 16 },
])
let pageNum = ref(1)
let pageSize = ref(10)
// 自定义v-el-select-loadmore指令
const vElSelectLoadmore = {
beforeMount(el, binding) {
/**
* vue2时:
* el.querySelector('.el-select-dropdown .el-select-dropdown__wrap');
* vue3时:
* 在el-select给一个参数popper-class="event-select-poper"
* element-plus中el-select的选项是使用的popper.js生成的,无法直接获取
*/
const selectDom = document.querySelector('.event-select-poper .el-select-dropdown__wrap')
let loadMores = function() {
/**
* scrollHeight 获取元素内容高度(只读)
* scrollTop 获取或者设置元素的偏移值,常用于, 计算滚动条的位置, 当一个元素的容器没有产生垂直方向的滚动条, 那它的scrollTop的值默认为0.
* clientHeight 读取元素的可见高度(只读)
* 如果元素滚动到底, 下面等式返回true, 没有则返回false:
* ele.scrollHeight - ele.scrollTop === ele.clientHeight;
*/
// 判断是否到底
const isBase = this.scrollHeight - this.scrollTop <= this.clientHeight + 20
if (isBase) {
// 增加防抖
binding.value && binding.value()
}
}
// 将获取到的dom和函数挂载到el-select上,实例销毁时好处理
el.selectDomInfo = selectDom
el.selectLoadMore = loadMores
// 监听滚动事件
selectDom?.addEventListener('scroll', loadMores.bind(selectDom))
},
// 实例销毁
beforeUnmount(el) {
if (el.selectLoadMore) {
el.selectDomInfo.removeEventListener('scroll', el.selectLoadMore)
delete el.selectDomInfo
delete el.selectLoadMore
}
}
}
watch(() => allFilterEvents, () => {
let startIndex = pageNum.value * pageSize.value - pageSize.value
let endIndex = pageNum.value * pageSize.value
options = allFilterEvents.slice(startIndex, endIndex)
optionCodes.value = options.value.map(item => { return item.code })
}, {
immediate: true,
deep: true
})
watch(() => props.code, async (val) => {
// 加载完毕后尝试设置默认值
await setSelectedDefaultValue(val)
}, {
immediate: true,
deep: true
})
// 添加一个方法用于设置默认值
const setSelectedDefaultValue = async (defaultValue) => {
if (defaultValue) {
// 如果默认值不在当前options中,尝试加载更多直到找到
while (!optionCodes.value.includes(defaultValue)) {
await new Promise(resolve => setTimeout(resolve, 0)) // 异步等待,模拟加载延迟
if (pageNum.value * pageSize.value <= allEvents.value.length) {
await loadmore()
} else {
break // 防止无限循环
}
}
eventCode.value = defaultValue
}
}
onBeforeMount(async () => {
await loadEvents()
// 这里调用一次设置默认值
await setSelectedDefaultValue(props.code)
})
// 模拟懒加载
const loadmore = () => {
// 数据加载完成之后,不需要再执行懒加载
if (allEvents.length <= options.length) { return }
pageNum.value++
nextTick(() => {
loading.value = true
let startIndex = pageNum.value * pageSize.value - pageSize.value
let endIndex = pageNum.value * pageSize.value
options = [
...options,
...allFilterEvents.slice(startIndex, endIndex)
]
optionCodes.value = options.value.map(item => { return item.code })
loading.value = false
})
}
// 自定义过滤函数
const filterOptions = (query = '') => {
pageNum.value = 1
nextTick(() => {
if(query === ''){
// 搜索词为空时,显示全部
allFilterEvents = JSON.parse(JSON.stringify(allEvents))
await setSelectedDefaultValue(props.code)
}else{
// 搜索词不为空时,从所有关键字列表中筛选匹配项
let arr = allEvents.filter((item) => {
return item.includes(query)
})
$set(this, 'allFilterEvents', arr)
}
})
}
// 下拉框隐藏时,重置分页已经过滤得到的列表
const handleVisibleChange = (visible) => {
if(!visible){
pageNum.value = 1
nextTick(() => {
allFilterEvents = JSON.parse(JSON.stringify(allEvents))
})
}
}
// 获取事件列表
const loadEvents = () => {
loading.value = true
allFilterEvents = allEvents
// 调用接口
loading.value = false
}
const handleEventsChange = (val) => {
emits('change', val)
}
</script>