轉(zhuǎn)載請(qǐng)注明出處轿塔,點(diǎn)擊此處 查看更多精彩內(nèi)容
el-select 是 element-ui 組件庫(kù)提供的下拉選擇菜單組件。
在項(xiàng)目中,我們展示到 el-select 的數(shù)據(jù)通常是從服務(wù)端獲取的票腰,如果服務(wù)端的查詢較慢或者數(shù)據(jù)量過(guò)大姜凄,就會(huì)導(dǎo)致在前端的顯示很慢找默,特別是在網(wǎng)絡(luò)不好的時(shí)候更是如此。
所以崩掘,分頁(yè)展示就是一種較好的交互體驗(yàn)了七嫌,可惜的是 el-select 組件并沒(méi)有提供分頁(yè)的功能。
本著不重復(fù)造輪子(懶)的原則苞慢,在網(wǎng)上逛了一圈诵原,發(fā)現(xiàn)現(xiàn)有實(shí)現(xiàn)方案基本都是基于 el-select 封裝了新的組件,這可能導(dǎo)致 el-select 組件的部分功能不可用挽放,并且不是很靈活绍赛。
算啦,動(dòng)手做一個(gè)吧辑畦。
實(shí)現(xiàn)效果
實(shí)現(xiàn)思路
- 自定義一個(gè)組件
ElSelectLoading.vue
吗蚌,由用戶自行插入到 el-select 組件菜單的底部。 - 使用 IntersectionObserver 監(jiān)聽(tīng)當(dāng)前組件是否出現(xiàn)在可見(jiàn)范圍纯出,可見(jiàn)時(shí)觸發(fā)加載數(shù)據(jù)的事件蚯妇。
- 用戶監(jiān)聽(tīng)事件加載新數(shù)據(jù),對(duì) el-select 的功能沒(méi)有影響暂筝。
這個(gè)思路也適用于其他的列表監(jiān)聽(tīng)滾動(dòng)觸底加載更多數(shù)據(jù)箩言。
實(shí)現(xiàn)代碼
<!-- 監(jiān)聽(tīng) el-select 的滾動(dòng),并提供觸底加載數(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)前頁(yè)碼
page: number;
// 是否加載中焕襟,用來(lái)過(guò)濾重復(fù)的加載
loading: boolean;
// 加載中的提示文案
loadingText?: string;
// 是否有更多數(shù)據(jù)可加載
hasMore: boolean;
// 沒(méi)有更多數(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)聽(tīng)滾動(dòng)
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);
});
// 組件卸載成功,取消滾動(dòng)監(jiān)聽(tīng)
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)簽鸵赖?
這是因?yàn)?el-select 在內(nèi)部沒(méi)有任何 el-option
的時(shí)候不會(huì)渲染菜單浮層务漩,如果使用 div
拄衰,組件可能會(huì)沒(méi)有機(jī)會(huì)渲染。
Props:
參數(shù)名稱 | 說(shuō)明 | 類型 | 默認(rèn)值 |
---|---|---|---|
page | 當(dāng)前頁(yè)碼 | number | - |
loading | 是否加載中菲饼,用來(lái)過(guò)濾重復(fù)的加載 | boolean | - |
loadingText | 加載中的提示文案 | string | 正在加載 |
hasMore | 是否有更多數(shù)據(jù)可加載 | boolean | - |
noMoreText | 沒(méi)有更多數(shù)據(jù)的提示文案 | string | 到底了~ |
Emits:
事件名稱 | 說(shuō)明 | 回調(diào)參數(shù) |
---|---|---|
loadMore | 觸底可加載數(shù)據(jù)時(shí)觸發(fā) | (newPage: number) |
使用示例
<template>
<el-select placeholder="請(qǐng)選擇" 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ù)時(shí)更新對(duì)應(yīng)的狀態(tài)宏悦。
注意: 每次
loadMore
事件回調(diào)的新頁(yè)碼參數(shù)都是由組件props.page + 1
得到的镐确,因此,
page
參數(shù)的值應(yīng)該由 0 開(kāi)始饼煞。page.value
的更新應(yīng)該放在數(shù)據(jù)加載成功后源葫,以防加載失敗后重新加載時(shí)頁(yè)碼錯(cuò)誤。
如果項(xiàng)目中有多個(gè)功能需要分頁(yè)加載砖瞧,也可以自行基于 el-select 和 ElSelectLoading
做封裝息堂。
分頁(yè)時(shí)數(shù)據(jù)回顯問(wèn)題的解決方案
默認(rèn)情況下要回顯的數(shù)據(jù)在菜單里不存在時(shí) el-select 會(huì)把 value
展示出來(lái),在分頁(yè)加載中這種情況是很常見(jiàn)的块促,對(duì)用戶很不友好荣堰,需要處理一下。
以下方案都建立在回顯時(shí)已拿到選中項(xiàng)的
value
和label
值的前提下竭翠。
方案一:模擬回顯
如果是單選的話振坚,我們可以用 absolute
定位元素覆蓋到 el-select 組件上模擬回顯 label
值,可以完美回顯斋扰。
多選的話模擬起來(lái)很麻煩渡八,要考慮高度、刪除等問(wèn)題传货,建議不要用 el-select 組件了屎鳍,或者看一下方案二吧。
方案二:手動(dòng)處理數(shù)據(jù)
根據(jù)要回顯的 value
和 label
組建一個(gè)列表并插入到第一頁(yè)问裕,后續(xù)分頁(yè)加載時(shí)從列表中刪除重復(fù)數(shù)據(jù)逮壁。
該方案的缺點(diǎn)也很明顯:
- 回顯時(shí)會(huì)把原本分散的選中數(shù)據(jù)集中到最前面,有點(diǎn)違反直覺(jué)粮宛,如果原列表有排序的話貌踏,還會(huì)導(dǎo)致順序混亂。
- 從后續(xù)分頁(yè)中刪除已回顯的重復(fù)數(shù)據(jù)后窟勃,本頁(yè)加載到的有效數(shù)據(jù)量會(huì)小于
pagesSize
,甚至出現(xiàn)為 0 的情況逗堵。
回顯方案總結(jié)
綜上所述秉氧,在列表篩選項(xiàng)等不需要回顯的場(chǎng)景或者【單選+回顯】場(chǎng)景下使用分頁(yè)加載是比較合適的,【多選+回顯】的情況不建議使用 el-select 分頁(yè)加載蜒秤,可以考慮用 dialog + table
去做(需要和產(chǎn)品經(jīng)理 battle 一下)汁咏,或者去找一找有沒(méi)有完善的帶回顯功能的分頁(yè)下拉菜單組件亚斋。
如果大家有好的回顯方案,也可以到評(píng)論區(qū)分享一下攘滩。