无限滚动通用组件
代码
点击查看代码
vue
<!-- InfiniteScroll.vue -->
<template>
<div
ref="scrollContainer"
class="infinite-scroll-container"
:class="{ 'position-full': positionFull }"
@scroll="handleScroll"
>
<slot></slot>
<div class="loading-tip">
<span v-if="loading">{{ loadingText }}</span>
<span v-if="finished">{{ finishedText }}</span>
<span v-if="emptied">{{ emptiedText }}</span>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
interface Props {
loading?: boolean // 是否正在加载
finished?: boolean // 是否全部加载完成
emptied?: boolean // 是否为空数据
threshold?: number // 触发加载的滚动阈值(像素)
loadingText?: string // 加载中的提示文字
finishedText?: string // 加载完成的提示文字
emptiedText?: string // 空数据的提示文字
positionFull?: boolean // 是否使用定位撑满父容器
checkOnInit?: boolean // 新增初始化检查开关
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
finished: false,
threshold: 0,
loadingText: '加载中...',
finishedText: '没有更多了',
emptiedText: '暂无数据',
positionFull: false,
checkOnInit: true, // 默认开启初始化检查
})
const emit = defineEmits(['load'])
const scrollContainer = ref<HTMLElement | null>(null)
// 核心检查方法
const checkLoadMore = async (forceCheck = false) => {
if (props.loading || props.finished) return
await nextTick() // 等待DOM更新
const el = scrollContainer.value
if (!el) return
// 强制检查或自动检查
if (forceCheck || shouldAutoCheck(el)) {
emit('load')
}
}
// 判断是否需要自动加载
const shouldAutoCheck = (el: HTMLElement) => {
const scrollBottom = el.scrollTop + el.clientHeight
const shouldLoad = scrollBottom >= el.scrollHeight - props.threshold
return shouldLoad
}
// 滚动处理
const handleScroll = () => {
checkLoadMore()
}
// 兼容性处理:ResizeObserver Polyfill
const initResizeObserver = () => {
if (!scrollContainer.value) return
if (typeof ResizeObserver !== 'undefined') {
let lastHeight = scrollContainer.value.clientHeight
const observer = new ResizeObserver(() => {
// 判断高度是否变化
if (
scrollContainer.value &&
scrollContainer.value.clientHeight !== lastHeight
) {
lastHeight = scrollContainer.value.clientHeight
handleScroll()
}
})
observer.observe(scrollContainer.value)
return observer
}
}
// 初始化检查
onMounted(() => {
const observer = initResizeObserver()
if (props.checkOnInit) {
checkLoadMore(true)
}
// 清理
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
})
})
watch(
() => [props.loading, props.finished],
() => {
if (!props.loading && !props.finished) {
// 加载完成且还有更多数据时,再次检查
checkLoadMore()
}
},
)
defineExpose({ checkLoadMore })
</script>
<style lang="scss" scoped>
.infinite-scroll-container {
overflow-y: auto;
height: 100%;
position: relative;
&.position-full {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.loading-tip {
padding: 12px 0;
text-align: center;
color: #999;
font-size: 0.9em;
}
</style>
示例
@ep_src_vue_generic_component_infinite_scroll_Example1(./Example1.vue)
vue
<template>
<div style="height: 300px">
<InfiniteScroll
:loading="loading"
:finished="finished"
@load="loadMore"
>
<!-- 你的内容 -->
<div v-for="item in list" :key="item.id">{{ item.content }}</div>
</InfiniteScroll>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import InfiniteScroll from './InfiniteScroll.vue'
interface ListItem {
id: number
content: string
}
const list = ref<ListItem[]>([])
const loading = ref(false)
const finished = ref(false)
let page = 1
const loadMore = async () => {
console.log('loadMore')
if (loading.value || finished.value) return
loading.value = true
try {
// 模拟 API 调用
const newData = await fetchData(page)
if (newData.length) {
list.value.push(...newData)
page++
} else {
finished.value = true
}
} finally {
loading.value = false
}
}
let count = 1
// 模拟 API
const fetchData = async (page: number) => {
return new Promise<ListItem[]>((resolve) => {
setTimeout(() => {
const data = Array.from({ length: 10 }, () => {
const n = count++
return {
id: n,
content: `Item ${n}`,
}
})
if (count >= 100) {
finished.value = true
}
resolve(data)
}, random(600, 300))
})
}
const random = (max: number, min = 0) => {
return Math.floor(Math.random() * (max - min + 1)) + min
}
</script>