vue3 封装一个ListView触底加载更多

ListView.vue

<template>
  <div class="list-view-container" ref="containerRef">
    <div class="list-view" ref="listViewRef" @scroll="onScroll">
      <div class="list" :style="{ 'padding-top': top + 'px', gap: gap + 'px' }">
        <slot></slot>

        <div v-if="loading" class="icon-loading">
          <div class="balls">
            <div></div>
            <div></div>
            <div></div>
          </div>
        </div>

        <div
          v-if="!loading && !hasMore && canScroll"
          class="no-more-text"
        >
          <span>{{ noMoreText }}</span>
        </div>
      </div>

      <div
        class="list-scroll-to-top"
        v-if="showScrollToTop && showScrollToTopBtn"
        @click="scrollToTop"
      >
        <i class="icon-arrow-up">Top</i>
      </div>
    </div>

    <div v-if="!loading && !hasMore && showEmpty" class="list-empty-view">
      <slot name="emptyView">
          <img src="@/assets/common/empty.svg" alt="" />
          <span>{{ emptyText }}</span>
      </slot>
    </div>
  </div>
</template>

<script setup>
import {
  ref,
  watchEffect,
  defineProps,
  defineEmits,
  onMounted,
  onActivated,
  nextTick,
  computed,
} from "vue";

const props = defineProps({
  emptyText: String,
  fetchData: {
    type: Function,
    required: true,
  },
  pageSize: {
    type: Number,
    default: 20,
  },
  initPage: {
    type: Number,
    default: 0,
  },
  showScrollToTopBtn: {
    type: Boolean,
    default: false,
  },
  top: {
    type: Number,
    default: 0,
  },
  gap: {
    type: Number,
    default: 0,
  },
  showEmpty: {
    type: Boolean,
    default: true,
  },
});

const emits = defineEmits(["error"]);

const noMoreText = "No more text"

// ref
const listViewRef = ref();
const containerRef = ref();

const positionScrollTop = ref(0);
const listHeight = ref(0);

const page = ref(props.initPage);
const hasMore = ref(true);
const loading = ref(false);
const showScrollToTop = ref(false);

const canScroll = computed(() => {
  if (!listViewRef.value) return false;
  return listViewRef.value?.scrollHeight > listViewRef.value.clientHeight;
})

const fetchMoreData = async () => {
  if (loading.value || !hasMore.value) {
    return;
  }

  loading.value = true;

  try {
    const newData = await props.fetchData(page.value, props.pageSize);

    if (newData.length < props.pageSize) {
      hasMore.value = false;
    }

    page.value += 1;
  } catch (error) {
    emits("error");
  } finally {
    loading.value = false;
    checkContentHeight();
  }
};

const onScroll = () => {
  if (!listViewRef.value) return;
  
  const scrollTop = listViewRef.value.scrollTop;
  const scrollHeight = listViewRef.value.scrollHeight;
  const clientHeight = listViewRef.value.clientHeight;

  // 底部距离
  const bottomDistance = 100;

  if (scrollTop + clientHeight + bottomDistance >= scrollHeight) {
    fetchMoreData();
  }

  // 滚动到顶部按钮的显示和隐藏
  showScrollToTop.value = scrollTop > listHeight.value;

  // 记录容器高度
  positionScrollTop.value = listViewRef.value?.scrollTop;
};

// 检测内容高度是否小于容器高度,如果小于则继续加载
const checkContentHeight = async () => {
  // 执行顺序放到任务队列的末尾,并等待下一次 DOM 更新循环结束之后再执行
  await nextTick();

  const scrollHeight = containerRef.value?.scrollHeight;
  listHeight.value = listViewRef.value?.offsetHeight;

  if (listHeight.value > 0 && scrollHeight > listHeight.value) {
    fetchMoreData();
  }
};

const scrollToTop = () => {
  if (!listViewRef.value) {
    return;
  }
  listViewRef.value.scrollTo({ top: 0, behavior: "smooth" });
};

// 监听 items 变化,重新计算容器高度
watchEffect(() => {
  listHeight.value = listViewRef.value?.offsetHeight;
});

onMounted(() => {
  // 初始化数据
  fetchMoreData();
});

onActivated(() => {
  if (listViewRef.value)
    listViewRef.value.scrollTop = positionScrollTop.value;
});
</script>

<style lang="less" scoped>
.list-view-container {
  display: flex;
  flex-direction: column;
  height: 100%;

  .list-empty-view {
    width: 100%;
    height: 100%;

    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: 32px;

    img {
      width: 300px;
      opacity: 0.8;
    }

    span {
      font-family: "HarmonyOS_Bold";
      font-size: 14px;
      color: rgba(242, 242, 242, 0.6);
    }
  }
}

.list-view {
  width: 100%;

  color: rgba(242, 242, 242, 0.6);

  overflow-x: hidden;
  overflow-y: scroll;

  /* Firefox IE */
  scrollbar-width: none;
  -ms-overflow-style: none;

  &::-webkit-scrollbar {
    width: 8px;
  }

  &::-webkit-scrollbar-thumb {
    background: rgba(242, 242, 242, 0.1);
    border-radius: 46px;

    &:hover {
      background: rgba(242, 242, 242, 0.2);
    }
  }

  &::-webkit-scrollbar-track {
    margin-top: 10px;
    margin-bottom: 10px;
  }
}

.list {
  display: flex;
  flex-direction: column;
  padding-right: 6px;

  .icon-loading {
    margin: 20px 0px;
    display: flex;
    justify-content: center;
  }
}

.no-more-text {
  display: flex;
  justify-content: center;
  padding: 20px 0;
}

.list-scroll-to-top {
  width: 50px;
  height: 50px;
  position: fixed;
  bottom: 96px;
  right: 54px;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #f2f2f2;
  background: #232626;
  border-radius: 50%;
  cursor: pointer;
  opacity: 1;
  transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);

  &:hover {
    background: #3a3d3c;
  }
}

.balls {
  width: 3.5em;
  height: 28px;

  display: flex;
  flex-flow: row nowrap;
  align-items: center;
  justify-content: space-between;

  transform: scale(0.8);
}

.balls div {
  width: 0.7em;
  height: 0.7em;
  border-radius: 50%;
  background-color: #0bb764ca;
  transform: translateY(-100%);
  animation: loading 0.6s ease-in-out alternate infinite;
}

.balls div:nth-of-type(1) {
  animation-delay: -0.4s;
}

.balls div:nth-of-type(2) {
  animation-delay: -0.2s;
}

@keyframes loading {
  from {
    transform: translateY(-30%);
  }
  to {
    transform: translateY(30%);
  }
}
</style>

使用

    <list-view
      :fetch-data="fetchData"
      :pageSize="pageSize"
      :initPage="initPage"
      :gap="45"
      :top="24"
      :showEmpty="items.length == 0"
      :emptyText="emptyText"
    >

      ...

    </list-view>

<script setup>
const fetchData = (pageNum, pageSize) => {
  return new Promise((resolve) => {
    ...
  });
};
</script>

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
`ag-Grid-Vue` 是一个基于 Vue.js 的 `ag-Grid` 的封装组件,它是 `ag-Grid`(一个功能强大的数据表格和网格组件库)在 Vue 框架中的实现。滚动条触底加载更多(Infinite Scroll or Virtual Scrolling)是 ag-Grid 提供的一种功能,用于在用户滚动到表格底部自动加载更多行,而不是一次性加载所有数据,从而提高性能和用户体验。 在 ag-Grid-Vue 中实现滚动条触底加载更多的步骤通常包括: 1. 配置列:在 ag-Grid 的列定义中启用 `infiniteScroll` 属性,并设置 `scrollThreshold` 参数,指定当滚动到底部多少距离时触发加载。例如: ```html <ag-grid-vue :columnDefs="columnDefs" :infiniteScroll="true" scrollThreshold="100"></ag-grid-vue> ``` 这里 `100` 表示当滚动条距离底部小于100px时开始加载更多数据。 2. 数据分页:你需要确保数据源是分页的,可以使用 ag-Grid 的 `gridApi.getRowNode` 方法获取当前可视区域的行号范围,然后从后向前逐页加载。 3. 加载事件处理:在 Vue 组件中监听 `onGridReady` 或其他适当的事件,当滚动到底部时触发加载更多数据的请求,并更新数据源。示例: ```javascript mounted() { this.gridApi.addEventListener('scroll', this.handleScroll); }, methods: { handleScroll(params) { if (params.isEnd && params.isLastRenderedPage) { // 加载更多数据 this.loadMoreData(); } }, loadMoreData() { // 假设你有一个接口 `loadNextPage` 用于请求更多数据 this.loadNextPage().then(() => { // 更新行数,告诉 ag-Grid 加载完成 this.gridApi.sizeColumnsToFit(); }); } } ``` 4. 更新视图:加载新数据后,你需要将其添加到现有数据的末尾,并调用 `gridApi.sizeColumnsToFit()` 来调整列宽以适应新的内容。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值