工作中经常需要用到下拉框滚动分页加载,但是elementPlus没有,只能自己封装一个
功能:支持分页、远程搜索、配置分页、配置搜索参数、配置其他参数、配置下拉框的值等
注意:如果没有对el-select加class区分,一个页面引入多个下拉框分页组件,会出现滚动加载没有效果,是因为多个组件同时监听了同一个滚动事件,导致事件处理混乱,加上 :popper-class="popperClass"做区分可解决这个问题
1.下拉框分页组件
<template>
<el-select
v-model="selectedValue"
filterable
remote
reserve-keyword
:remote-method="remoteMethod"
:loading="loading"
@clear="handleClear"
@change="handleValueChange"
v-bind="$attrs"
:popper-class="popperClass"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="valueKey ? item :item.value"
></el-option>
<div v-if="hasMore" v-loading="loadingMore" class="infinite-loading">
<span>加载中...</span>
</div>
<div v-else-if="options.length > 0" class="no-more-data">
<span>没有更多数据了</span>
</div>
</el-select>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted,watch } from 'vue';
import { ElSelect, ElOption } from 'element-plus';
interface OptionItem {
value: string | number;
label: string;
}
const props = defineProps({
modelValue: {
type: [String, Number, Array,Object],
default: '',
},
// 统一的数据获取函数
fetchData: {
type: Function,
required: true,
},
// 每页大小
pageSize: {
type: Number,
default: 50,
},
// 搜索
keywordKey: {
type: String,
default: 'keyword',
},
valueKey:{
type: Boolean,
default: true, //下拉框值,是item 或者 item.value
},
//分页对应的字段
pageOrSizeKey:{
type:Object,
default:{
page:'page',
size:'size'
}
},
//其他搜索参数
otherSearchParams:{
type:Object,
default:{}
}
});
const popperClass = `popperClass${getRandom4DigitInt()}`
const selectedValue = ref(props.modelValue);
const options = ref<OptionItem[]>([]);
const loading = ref(false);
const loadingMore = ref(false);
const hasMore = ref(true);
const currentPage = ref(1);
const keyword = ref('');
const scrollContainer = ref<HTMLElement | null>(null);
function getRandom4DigitInt() {
return Math.floor(Math.random() * 9000) + 1000;
}
const emit = defineEmits(['update:modelValue', 'change']);
watch(() => props.modelValue, (newVal) => {
// 只有当新值与当前值不同时才更新
if (newVal !== selectedValue.value) {
selectedValue.value = newVal;
emit('update:modelValue', newVal);
}
}, { immediate: true ,deep: true});
// 监听值变化
const handleValueChange = (value: string | number | Array<string | number>) => {
selectedValue.value = value;
emit('update:modelValue', value);
emit('change', value);
};
// 统一加载数据方法
const loadData = async (reset = false) => {
if (reset) {
currentPage.value = 1;
hasMore.value = true;
}
if (!hasMore.value) return;
const loadingRef = reset ? loading : loadingMore;
loadingRef.value = true;
try {
const params = {
[props.pageOrSizeKey.page]: currentPage.value,
[props.pageOrSizeKey.size]: props.pageSize,
...props.otherSearchParams
};
// 如果有搜索关键词,加入参数
if (keyword.value) {
params[props.keywordKey] = keyword.value;
}
const result = await props.fetchData(params);
if (reset) {
options.value = result.list;
} else {
options.value = [...options.value, ...result.list];
}
hasMore.value = result.totalPages > currentPage.value;
currentPage.value += 1;
} catch (error) {
console.error('加载数据失败:', error);
} finally {
loadingRef.value = false;
}
};
// 远程搜索方法
const remoteMethod = (query: string) => {
keyword.value = query;
loadData(true);
};
// 处理清空
const handleClear = () => {
keyword.value = '';
loadData(true);
};
// 滚动加载
const handleScroll = () => {
if (!scrollContainer.value) return;
const { scrollTop, scrollHeight, clientHeight } = scrollContainer.value;
const isBottom = scrollHeight - (scrollTop + clientHeight) < 50;
if (isBottom && !loadingMore.value && hasMore.value) {
loadData();
}
};
// 初始化滚动监听
const initScrollListener = () => {
const el = document.querySelector(
`.${popperClass} .el-select-dropdown .el-select-dropdown__wrap`,
);
if (el) {
scrollContainer.value = el as HTMLElement;
scrollContainer.value.addEventListener('scroll', handleScroll);
}
};
// 移除滚动监听
const removeScrollListener = () => {
if (scrollContainer.value) {
scrollContainer.value.removeEventListener('scroll', handleScroll);
}
};
onMounted(() => {
setTimeout(initScrollListener, 300);
});
onUnmounted(() => {
removeScrollListener();
});
</script>
<style scoped>
.infinite-loading,
.no-more-data {
padding: 8px 0;
font-size: 12px;
color: var(--el-text-color-secondary);
text-align: center;
}
.no-more-data {
padding: 16px 0;
}
</style>
2.实现
<template>
<div>
<infinite-scroll-select
v-model="selectedValue"
:fetchData="searchData"
placeholder="请选择"
:keywordKey="'name'"
@change="handleChange"
clearable
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import InfiniteScrollSelect from './selectPage.vue';
import { getPageApi } from '#/api/index';
const selectedValue = ref('');
// 模拟搜索数据
const searchData = async (params: {
page: number;
size: number;
keyword: string;
}) => {
const res = await getPageApi(params); //后端接口
const list = res.data.content.map((item) => ({
label: item.name,
value: item.code,
}));
return {
list,
totalPages: res.data.totalPages, //总页数
};
};
const handleChange = (value: string | number) => {
console.log('选中值变化:', value);
console.log(selectedValue.value)
};
</script>
733

被折叠的 条评论
为什么被折叠?



