為 el-select 組件添加滾動加載更多的功能

轉(zhuǎn)載請注明出處,點擊此處 查看更多精彩內(nèi)容

el-selectelement-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 得到的,因此,

  1. page 參數(shù)的值應(yīng)該由 0 開始。
  2. page.value 的更新應(yīng)該放在數(shù)據(jù)加載成功后,以防加載失敗后重新加載時頁碼錯誤。

如果項目中有多個功能需要分頁加載,也可以自行基于 el-selectElSelectLoading 做封裝。

分頁時數(shù)據(jù)回顯問題的解決方案

默認(rèn)情況下要回顯的數(shù)據(jù)在菜單里不存在時 el-select 會把 value 展示出來,在分頁加載中這種情況是很常見的,對用戶很不友好,需要處理一下。

以下方案都建立在回顯時已拿到選中項的 valuelabel 值的前提下。

方案一:模擬回顯

如果是單選的話,我們可以用 absolute 定位元素覆蓋到 el-select 組件上模擬回顯 label 值,可以完美回顯。

多選的話模擬起來很麻煩,要考慮高度、刪除等問題,建議不要用 el-select 組件了,或者看一下方案二吧。

方案二:手動處理數(shù)據(jù)

根據(jù)要回顯的 valuelabel 組建一個列表并插入到第一頁,后續(xù)分頁加載時從列表中刪除重復(fù)數(shù)據(jù)。

該方案的缺點也很明顯:

  1. 回顯時會把原本分散的選中數(shù)據(jù)集中到最前面,有點違反直覺,如果原列表有排序的話,還會導(dǎo)致順序混亂。
  2. 從后續(xù)分頁中刪除已回顯的重復(fù)數(shù)據(jù)后,本頁加載到的有效數(shù)據(jù)量會小于 pagesSize,甚至出現(xiàn)為 0 的情況。

回顯方案總結(jié)

綜上所述,在列表篩選項等不需要回顯的場景或者【單選+回顯】場景下使用分頁加載是比較合適的,【多選+回顯】的情況不建議使用 el-select 分頁加載,可以考慮用 dialog + table 去做(需要和產(chǎn)品經(jīng)理 battle 一下),或者去找一找有沒有完善的帶回顯功能的分頁下拉菜單組件。

如果大家有好的回顯方案,也可以到評論區(qū)分享一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容