Files
vue-ts-example/src/components/UseIntersectionObserverInfiniteLoading.vue
严浩 dbcc28fb42
Some checks failed
/ build-and-deploy-to-vercel (push) Failing after 39s
/ playwright (push) Successful in 2m1s
/ depcheck (push) Successful in 1m30s
feat: 优化 UseIntersectionObserverInfiniteLoading 组件的加载逻辑和模板结构
2024-12-11 15:48:35 +08:00

166 lines
4.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
function checkIsVisible(el: Element, root: Element | null = null) {
if (!el) return false;
const elRect = el.getBoundingClientRect();
const rootRect = root
? root.getBoundingClientRect()
: { top: 0, left: 0, bottom: window.innerHeight, right: window.innerWidth };
return (
elRect.bottom >= rootRect.top &&
elRect.top <= rootRect.bottom &&
elRect.right >= rootRect.left &&
elRect.left <= rootRect.right
);
}
</script>
<script setup lang="ts">
/**
* @example
*
* ```ts
* const list = ref<Record<string, never>[]>([]);
* const loadData = async (page: number) => { ... };
* ```
*
* ```vue
* <UseIntersectionObserverInfiniteLoading :async-load="loadData" ref="infiniteLoading" />
* ```
*/
const props = defineProps<{
asyncLoad: (page: number) => Promise<{ hasMore: boolean }>;
}>();
defineSlots<{
// 加载中
loading(): unknown;
// 加载完成(还有更多)
loadMore(props: { load: () => void }): unknown;
// 加载完成(没有更多了)
complete(): unknown;
// 加载失败
error(props: { retry: () => void }): unknown;
}>();
defineExpose({
refresh: () => {
currentPage = 0;
state.value = '';
return load('refresh');
},
});
const target = ref(null);
let currentPage = 0;
const state = ref<'' | 'loading' | 'loaded' | 'complete' | 'error'>('');
const load = async (why?: string) => {
console.group('load');
console.debug(`why :>> `, why);
console.groupEnd();
if (state.value === 'loading') return;
state.value = 'loading';
try {
const { hasMore } = await props.asyncLoad(++currentPage);
state.value = hasMore ? 'loaded' : 'complete';
if (hasMore && checkIsVisible(target.value!)) {
await nextTick();
await new Promise((resolve) => setTimeout(resolve, 300));
load('visible after load');
}
if (!hasMore) {
pause();
}
} catch (error) {
currentPage--;
state.value = 'error';
}
};
const { pause, resume /* ,isSupported, isActive */ } = useIntersectionObserver(
target,
([entry]) => {
if (entry?.isIntersecting) {
load('visible in observer');
}
},
{
immediate: false,
root: undefined,
rootMargin: '0px',
// 数值形式单个值表示目标元素可见部分与整个目标元素的比例。例如threshold: 0.5
// 目标元素可见比例达到 50% 时触发回调。
// 数组形式多个值表示多个可见比例触发点。例如threshold: [0, 0.25, 0.5, 0.75, 1.0]。
// 在目标元素可见部分从 0% 增加到 100% 时,每达到一个阈值都会触发回调。
threshold: 0,
},
);
onMounted(() => {
resume();
});
</script>
<template>
<div class="infinite-loading" ref="target">
<div v-if="state === 'complete'" class="infinite-loading__complete">
<slot name="complete">
<span>没有更多了</span>
</slot>
</div>
<template v-else>
<div v-show="state == 'loading'" class="infinite-loading__loading">
<slot name="loading">
<span> 加载中... </span>
</slot>
</div>
<!-- 如果 isSupportedloaded 状态应该永远不会出现 -->
<div
v-show="state === 'loaded' || state === ''"
class="infinite-loading__loaded"
@click="
() => {
!$slots.loadMore && load('click loadMore');
}
"
>
<slot name="loadMore" :load>
<span> 加载更多 </span>
</slot>
</div>
<div
v-if="state === 'error'"
class="infinite-loading__error"
@click="
() => {
!$slots.error && load('click error');
}
"
>
<slot name="error" :retry="load">
<span> 加载失败点击重试 </span>
</slot>
</div>
</template>
</div>
</template>
<style>
.infinite-loading__loading,
.infinite-loading__loaded,
.infinite-loading__complete,
.infinite-loading__error {
display: flex;
justify-content: center;
align-items: center;
min-height: 40px;
color: #666;
}
.infinite-loading__loaded,
.infinite-loading__error {
cursor: pointer;
}
</style>