轉(zhuǎn)載請注明出處,點擊此處 查看更多精彩內(nèi)容
el-select 是 element-ui 組件庫提供的下拉選擇菜單組件。
在項目中,我們展示到 el-select 的數(shù)據(jù)通常是從服務(wù)端獲取的,如果服務(wù)端的查詢較慢或者數(shù)據(jù)量過大,就會導(dǎo)致在前端的顯示很慢,特別是在網(wǎng)絡(luò)不好的時候更是如此。
所以,分頁展示就是一種較好的交互體驗了,可惜的是 el-select 組件并沒有提供分頁的功能。
本著不重復(fù)造輪子(懶)的原則,在網(wǎng)上逛了一圈,發(fā)現(xiàn)現(xiàn)有實現(xiàn)方案基本都是基于 el-select 封裝了新的組件,這可能導(dǎo)致 el-select 組件的部分功能不可用,并且不是很靈活。
算啦,動手做一個吧。
實現(xiàn)效果

實現(xiàn)思路
- 自定義一個組件
ElSelectLoading.vue,由用戶自行插入到 el-select 組件菜單的底部。 - 使用 IntersectionObserver 監(jiān)聽當(dāng)前組件是否出現(xiàn)在可見范圍,可見時觸發(fā)加載數(shù)據(jù)的事件。
- 用戶監(jiān)聽事件加載新數(shù)據(jù),對 el-select 的功能沒有影響。
這個思路也適用于其他的列表監(jiān)聽滾動觸底加載更多數(shù)據(jù)。
實現(xiàn)代碼
<!-- 監(jiān)聽 el-select 的滾動,并提供觸底加載數(shù)據(jù)的回調(diào) -->
<template>
<el-option ref="el" class="el-select-loading" value="">
<template v-if="hasMore">
<el-icon class="el-select-loading__icon"><Loading /></el-icon>
<span class="el-select-loading__tips">{{ loadingText || "正在加載" }}</span>
</template>
<template v-else>{{ noMoreText || "到底了~" }}</template>
</el-option>
</template>
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";
import { ElOption } from "element-plus";
interface Props {
// 當(dāng)前頁碼
page: number;
// 是否加載中,用來過濾重復(fù)的加載
loading: boolean;
// 加載中的提示文案
loadingText?: string;
// 是否有更多數(shù)據(jù)可加載
hasMore: boolean;
// 沒有更多數(shù)據(jù)的提示文案
noMoreText?: string;
}
const props = defineProps<Props>();
interface Emits {
(event: "loadMore", data: number): any;
}
const emit = defineEmits<Emits>();
const el = ref<typeof ElOption>();
const observer = ref<IntersectionObserver>();
// 組件加載成功,監(jiān)聽滾動
onMounted(() => {
if (!el.value) {
return;
}
const callback: IntersectionObserverCallback = (entries) => {
if (props.loading || !props.hasMore || !entries[0].isIntersecting) {
return;
}
emit("loadMore", props.page + 1);
};
const options: IntersectionObserverInit = {
root: el.value.$el.parentElement?.parentElement,
rootMargin: "0px 0px 0px 0px",
};
observer.value = new IntersectionObserver(callback, options);
observer.value.observe(el.value.$el);
});
// 組件卸載成功,取消滾動監(jiān)聽
onUnmounted(() => {
if (!el.value) {
return;
}
observer.value?.unobserve(el.value.$el);
});
</script>
<style lang="scss" scoped>
.el-select-loading {
display: flex;
align-items: center;
justify-content: center;
cursor: initial;
pointer-events: none;
color: var(--el-color-info);
font-size: 12px;
&__icon {
font-size: 16px;
animation: rotate 1.5s linear infinite;
}
&__tips {
margin-left: 6px;
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
}
</style>
為什么根組件使用 el-option 而不是 div 或其他標(biāo)簽?
這是因為 el-select 在內(nèi)部沒有任何 el-option 的時候不會渲染菜單浮層,如果使用 div,組件可能會沒有機會渲染。
Props:
| 參數(shù)名稱 | 說明 | 類型 | 默認(rèn)值 |
|---|---|---|---|
| page | 當(dāng)前頁碼 | number | - |
| loading | 是否加載中,用來過濾重復(fù)的加載 | boolean | - |
| loadingText | 加載中的提示文案 | string | 正在加載 |
| hasMore | 是否有更多數(shù)據(jù)可加載 | boolean | - |
| noMoreText | 沒有更多數(shù)據(jù)的提示文案 | string | 到底了~ |
Emits:
| 事件名稱 | 說明 | 回調(diào)參數(shù) |
|---|---|---|
| loadMore | 觸底可加載數(shù)據(jù)時觸發(fā) | (newPage: number) |
使用示例
<template>
<el-select placeholder="請選擇" v-model="selectValue">
<el-option
v-for="item in selectOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
<ElSelectLoading
:page="page"
:loading="loading"
:hasMore="hasMore"
@loadMore="handleLoadMore"
/>
</el-select>
</template>
<script setup lang="ts">
import { ref } from "vue";
import ElSelectLoading from "@/components/ElSelectLoading.vue";
const page = ref(0);
const loading = ref(false);
const hasMore = ref(true);
const selectValue = ref<number>();
const selectOptions = ref<any[]>([]);
/**
* 加載數(shù)據(jù)列表
*/
const loadDataList = async (newPage: number) => {
try {
loading.value = true;
const res = await pageRequest();
const list = res.data.list || [];
if (newPage === 1) {
selectOptions.value = [];
}
selectOptions.value.push(...list);
hasMore.value = selectOptions.value.length < res.data.total;
page.value = newPage;
} catch (err) {
console.error(err);
} finally {
loading.value = false;
}
};
/**
* 加載更多數(shù)據(jù)
*/
const handleLoadMore = async (newPage: number) => {
await loadDataList(newPage);
};
</script>
<style lang="scss" scoped></style>
觀察代碼可以發(fā)現(xiàn),在菜單底部插入了 ElSelectLoading 組件,并在加載數(shù)據(jù)時更新對應(yīng)的狀態(tài)。
注意: 每次
loadMore事件回調(diào)的新頁碼參數(shù)都是由組件props.page + 1得到的,因此,
page參數(shù)的值應(yīng)該由 0 開始。page.value的更新應(yīng)該放在數(shù)據(jù)加載成功后,以防加載失敗后重新加載時頁碼錯誤。
如果項目中有多個功能需要分頁加載,也可以自行基于 el-select 和 ElSelectLoading 做封裝。
分頁時數(shù)據(jù)回顯問題的解決方案
默認(rèn)情況下要回顯的數(shù)據(jù)在菜單里不存在時 el-select 會把 value 展示出來,在分頁加載中這種情況是很常見的,對用戶很不友好,需要處理一下。
以下方案都建立在回顯時已拿到選中項的
value和label值的前提下。
方案一:模擬回顯
如果是單選的話,我們可以用 absolute 定位元素覆蓋到 el-select 組件上模擬回顯 label 值,可以完美回顯。
多選的話模擬起來很麻煩,要考慮高度、刪除等問題,建議不要用 el-select 組件了,或者看一下方案二吧。
方案二:手動處理數(shù)據(jù)
根據(jù)要回顯的 value 和 label 組建一個列表并插入到第一頁,后續(xù)分頁加載時從列表中刪除重復(fù)數(shù)據(jù)。
該方案的缺點也很明顯:
- 回顯時會把原本分散的選中數(shù)據(jù)集中到最前面,有點違反直覺,如果原列表有排序的話,還會導(dǎo)致順序混亂。
- 從后續(xù)分頁中刪除已回顯的重復(fù)數(shù)據(jù)后,本頁加載到的有效數(shù)據(jù)量會小于
pagesSize,甚至出現(xiàn)為 0 的情況。
回顯方案總結(jié)
綜上所述,在列表篩選項等不需要回顯的場景或者【單選+回顯】場景下使用分頁加載是比較合適的,【多選+回顯】的情況不建議使用 el-select 分頁加載,可以考慮用 dialog + table 去做(需要和產(chǎn)品經(jīng)理 battle 一下),或者去找一找有沒有完善的帶回顯功能的分頁下拉菜單組件。
如果大家有好的回顯方案,也可以到評論區(qū)分享一下。