如何实现一个无限滚动列表,并分页加载?

列表中的数据是变化的,所以在实现无限滚动的同时,也要请求新的数据,这样的无限滚动怎么实现呢?现在的开发环境是vue3的,从1滚动到最后一个后,再从1开始滚动,自动滚动,不需要滚动条!当数据不足5条的时候不需要滚动
image.png

阅读 1.4k
avatarAI BotBETA
### 实现无限滚动列表并分页加载(Vue 3)

要在 Vue 3 中实现一个无限滚动列表并分页加载数据,同时考虑数据的变化和循环滚动,你可以按照以下步骤进行:

1. **设置基本结构**:
   - 使用 Vue 3 的组合式 API。
   - 创建一个用于存储数据的数组 `items`。
   - 创建一个当前页码 `currentPage` 和每页显示的项目数量 `pageSize`。

2. **实现滚动监听**:
   - 使用 `onMounted` 和 `onUnmounted` 生命周期钩子来添加和移除滚动事件监听器。
   - 在滚动事件处理函数中,检查是否滚动到底部,如果是,则加载更多数据。

3. **数据请求**:
   - 使用一个函数 `fetchData` 来模拟从服务器获取数据。
   - 每次滚动到底部时,调用 `fetchData` 并更新 `items` 数组。

4. **处理数据变化**:
   - 在数据加载后,合并新数据到 `items` 数组中。
   - 如果有必要,处理数据去重或更新逻辑。

5. **实现循环滚动**:
   - 在滚动到底部并加载新数据后,如果数据数量足够多,可以通过调整滚动位置来实现循环效果。
   - 例如,可以在加载新数据后,立即将滚动位置重置到顶部。

### 示例代码

<template>
<div @scroll="handleScroll" class="infinite-scroll-container" ref="scrollContainer">

<ul>
  <li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>

</div>
</template>

<script>
import { ref, onMounted, onUnmounted, reactive } from 'vue';

export default {
setup() {

const items = reactive([]);
const currentPage = ref(1);
const pageSize = 10;
const scrollContainer = ref(null);

const fetchData = async (page) => {
  // 模拟从服务器获取数据
  const newData = Array.from({ length: pageSize }, (_, i) => ({
    id: (page - 1) * pageSize + i + 1,
    name: `Item ${(page - 1) * pageSize + i + 1}`,
  }));
  return newData;
};

const loadMoreData = async () => {
  const newItems = await fetchData(currentPage.value);
  items.push(...newItems);
  currentPage.value += 1;
  
  // 循环滚动:重置滚动位置到顶部
  if (scrollContainer.value) {
    scrollContainer.value.scrollTop = 0;
  }
};

const handleScroll = () => {
  const bottomOfWindow =
    scrollContainer.value.scrollHeight - scrollContainer.value.scrollTop ===
    scrollContainer.value.clientHeight;
  if (bottomOfWindow) {
    loadMoreData();
  }
};

onMounted(() => {
  // 初始加载第一页数据
  loadMoreData();
  window.addEventListener('scroll', handleScroll);
});

onUnmounted(() => {
  window.removeEventListener('scroll', handleScroll);
});

return {
  items,
  scrollContainer,
};

},
};
</script>

<style>
.infinite-scroll-container {
height: 300px; / 设置容器高度以启用滚动 /
overflow-y: auto;
border: 1px solid #ccc;
}
</style>


### 解释
- **模板部分**:使用 `v-for` 指令渲染列表项,并在 `div` 容器上监听滚动事件。
- **脚本部分**:
  - 使用 `reactive` 管理 `items` 数组。
  - 使用 `ref` 管理当前页码、每页大小和滚动容器引用。
  - `fetchData` 函数模拟数据获取。
  - `loadMoreData` 函数加载更多数据并处理循环滚动逻辑。
  - `handleScroll` 函数检查是否滚动到底部并调用 `loadMoreData`。
  - 在 `onMounted` 中初始加载数据并添加滚动事件监听器,在 `onUnmounted` 中移除监听器。
- **样式部分**:设置容器高度和滚动条样式。

这个示例提供了一个基本框架,你可以根据实际需求进行扩展和优化,比如处理数据更新、去重、错误处理等。
4 个回答
<template>
  <div class="card-list">
    <div class="card-item" v-for="(item, index) in list">
        {{ item }}
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from "vue";

const rbTimer = ref(null);
const totalList = ref([
  1,
  2,
  3,
  4,
  5,
  6,
  7,
  8,
  9,
  10,
  11,
  12,
  13,
  14,
  15,
  16,
  17,
  18,
  19,
]);
const list = ref([1, 2, 3, 4, 5]);
const pageSize = 5; // 每页显示的条数
let currentPage = 1; // 当前页码

onMounted(() => {
  const cardList = document.getElementsByClassName("card-list")[0];
  if (totalList.value.length > pageSize) {
    rbTimer.value = scroll(cardList);
  }
});

const scroll = (tableDom) => {
  let isScroll = true;
  tableDom.addEventListener("mouseover", () => {
    isScroll = false;
  });
  tableDom.addEventListener("mouseout", () => {
    isScroll = true;
  });
  return setInterval(() => {
    if (isScroll) {
      tableDom.scrollTop += 3; // 设置滚动速度
      if (
        Math.abs(tableDom.clientHeight + tableDom.scrollTop - tableDom.scrollHeight) <= 2
      ) {
        if (list.value.length === totalList.value.length) {
          tableDom.scrollTop = 0;
        }
        setTimeout(loadNextPage, 500); // 延迟500毫秒加载下一页数据
      }
    }
  }, 100);
};

const loadNextPage = () => {
  if (currentPage * pageSize < totalList.value.length) {
    currentPage++;
    list.value =[...list.value, ...totalList.value.slice(
      (currentPage - 1) * pageSize,
      currentPage * pageSize
    )];
  }
};
</script>

<style>
.card-list {
  height: 100px;
  overflow-y: auto;
  overflow: scroll;
}

.card-list::-webkit-scrollbar{
  width: 0;
  height: 0;
}

.card-item{
  line-height: 40px;
}

.card-item:nth-child(odd) {
  height: 40px;
  background: #20a6ff;
}

.card-item:nth-child(even) {
  height: 40px;
  background:#19daf9;
}
</style>

vue-virtual-scroller或者
或者自定义:

<template>
  <div class="scroll-container" ref="scrollContainer">
    <div class="scroll-list" :style="{ transform: `translateX(${scrollOffset}px)` }">
      <div
        v-for="item in displayedItems"
        :key="item.id"
        class="scroll-item"
      >
        {{ item.name }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';

const scrollContainer = ref(null);
const items = ref([]); 
const pageSize = 10;
const currentPage = ref(1);
const isLoading = ref(false);
const scrollOffset = ref(0); // 控制滚动位置
const scrollSpeed = 2; // 滚动速度,单位:像素/帧
const scrollInterval = ref(null);
const minItemsToScroll = 5; // 数据少于多少条不滚动

// 异步请求数据
const fetchData = async (page) => {
  isLoading.value = true;
  return new Promise((resolve) => {
    setTimeout(() => {
      const newData = Array.from({ length: pageSize }, (_, i) => ({
        id: `item-${(page - 1) * pageSize + i + 1}`,
        name: `Item ${ (page - 1) * pageSize + i + 1} - Page ${page}`,
      }));
      resolve(newData);
      isLoading.value = false;
    }, 500); 
  });
};

const displayedItems = computed(() => {
  if (items.value.length < minItemsToScroll) {
    return items.value;
  }
  const cloneCount = 2; 
  const startClone = items.value.slice(0, cloneCount);
  const endClone = items.value.slice(-cloneCount);
  return [...endClone, ...items.value, ...startClone];
});

const startAutoScroll = () => {
  if (items.value.length < minItemsToScroll || scrollInterval.value) {
    return;
  }
  scrollOffset.value = 0; // 初始化滚动位置
  const containerWidth = scrollContainer.value.offsetWidth;
  const listWidth = scrollContainer.value.querySelector('.scroll-list').offsetWidth;

  scrollInterval.value = setInterval(() => {
    scrollOffset.value -= scrollSpeed;
    if (Math.abs(scrollOffset.value) > listWidth - containerWidth) {
      // 滚动到末尾,跳回开头
      scrollOffset.value = 0;
    }
  }, 16); // 16ms 约等于 60帧
};

const stopAutoScroll = () => {
  if (scrollInterval.value) {
    clearInterval(scrollInterval.value);
    scrollInterval.value = null;
  }
};

const loadMoreData = async () => {
  if (isLoading.value) return;
  currentPage.value++;
  const newData = await fetchData(currentPage.value);
  items.value = [...items.value, ...newData];
};

watch(items, (newItems) => {
  if (newItems.length >= minItemsToScroll && !scrollInterval.value) {
    startAutoScroll();
  } else if (newItems.length < minItemsToScroll) {
    stopAutoScroll();
  }
});

onMounted(async () => {
  const initialData = await fetchData(currentPage.value);
  items.value = initialData;
});

onUnmounted(() => {
  stopAutoScroll();
});
</script>

<style scoped>
.scroll-container {
  width: 100%;
  overflow: hidden; /* 隐藏滚动条 */
}

.scroll-list {
  display: flex;
  white-space: nowrap;
  transition: transform 0.3s ease-out; /* 加过渡效果 */
}

.scroll-item {
  flex-shrink: 0;
  padding: 10px;
  border: 1px solid #ccc;
  margin-right: 10px;
}
</style>

可以考虑采用swiper插件
示例代码

<template>
  <swiper
    :modules="modules"
    :slides-per-view="3"
    :space-between="50"
    direction="vertical"
    :pagination="true"
    :navigation="true"
    :speed="5000"
    :loop="true"
    :autoplay="{ delay: 0, disableOnInteraction: false,pauseOnMouseEnter: true }"
    @swiper="onSwiper"
    @slideChange="onSlideChange"
  >
    <swiper-slide v-for="(slide, index) in slides" :key="index">{{ slide }}</swiper-slide>
  </swiper>
</template>

<script>
// 引入必要的组件和样式
import { Swiper, SwiperSlide } from 'swiper/vue';
import 'swiper/css';
import 'swiper/css/pagination';
import 'swiper/css/navigation';

// 正确地从 'swiper/modules' 导入需要的 Swiper 模块
import { Pagination, Navigation, Autoplay } from 'swiper/modules';

export default {
  name: 'YourComponentName',
  components: {
    Swiper,
    SwiperSlide,
  },
  setup() {
    const slides = ['Slide 1', 'Slide 2', 'Slide 3', 'Slide 4', 'Slide 5'];

    // 将要用到的 Swiper 模块
    const modules = [Pagination, Navigation, Autoplay];

    return {
      slides,
      modules,
      onSwiper(swiper) {
        console.log('Swiper instance:', swiper);
      },
      onSlideChange() {
        console.log('Slide changed');
      }
    };
  }
};
</script>

<style scoped>
/* 在这里添加自定义样式 */
.swiper {
  width: 100%;
  height: 100%;
}
.swiper-slide {
  text-align: center;
  font-size: 18px;
  background: #fff;

  /* Center slide text vertically */
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

swiper官网地址:https://swiperjs.com/vue?spm=5176.28103460.0.0.7d815d27dNyISH

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏