<template>
|
<scroll-view
|
:scroll-y="true"
|
:refresher-background="'transparent'"
|
:lowerThreshold="100"
|
@scrolltolower="lower"
|
:refresherEnabled="refresherEnabled"
|
:refresherTriggered="state.triggered"
|
@refresherrefresh="onRefresherRefresh"
|
:show-scrollbar="false"
|
:style="scrollViewStyle"
|
:enable-back-to-top="true"
|
:class="[scrollViewClassName]"
|
@scroll="onScroll"
|
:scroll-top="scrollDistance"
|
enhanced
|
:scroll-with-animation="true"
|
v-bind="{ ...$attrs }"
|
>
|
<slot name="header"></slot>
|
<LoadingLayout :loading="isLoading" :error="isError" :loadError="() => refetch?.()">
|
<NoData v-if="hasNoData" />
|
<div
|
:class="['infinite-list-inner', { noShowMoreText: !showMoreText, hasPaddingTop }]"
|
v-else
|
>
|
<slot name="extra" />
|
<slot v-if="$slots.default" />
|
<template v-else>
|
<template v-for="(group, index) in listData.pages" :key="index">
|
<template v-for="(item, i) in group.data" :key="i">
|
<slot
|
name="renderItem"
|
:item="item"
|
:groupIndex="index"
|
:itemIndex="i"
|
:index="findDataIndex(item)"
|
/>
|
</template>
|
</template>
|
</template>
|
</div>
|
<div
|
v-if="!hasNoData && showMoreText && listData?.pages?.length > 0 && !hasMore"
|
class="loading-more-tips"
|
>
|
{{ noMoreText }}
|
</div>
|
<div v-if="isFetching && hasMore && enabledLoadingMore" class="infiniting-tips">
|
<Loading class="infiniting-tips-icon"></Loading>数据加载中...
|
</div>
|
</LoadingLayout>
|
</scroll-view>
|
<div class="back-top-wrapper" @click.stop="backToTop" v-show="oldScrollDistance > 100">
|
<img class="back-top-img" :src="IconBackTop" />
|
<div class="back-top-text">返回顶部</div>
|
</div>
|
</template>
|
|
<script lang="ts" setup generic="T">
|
import { VNode, CSSProperties, computed, reactive } from 'vue';
|
import NoData from '../NoData/NoData.vue';
|
import LoadingLayout from '../Layout/LoadingLayout.vue';
|
import { FetchNextPageOptions } from '@tanstack/vue-query';
|
import { Loading } from '@nutui/icons-vue-taro';
|
import { useScrollDistance } from 'senin-mini/hooks';
|
import IconBackTop from '../../assets/icon-back-top.png';
|
|
defineOptions({
|
name: 'InfiniteLoading',
|
});
|
|
type Page = {
|
data?: T[];
|
pageModel?: {
|
rows?: number;
|
page?: number;
|
totalCount?: number;
|
totalPage?: number;
|
};
|
[key: string]: any;
|
};
|
|
type TData = {
|
pages: Page[];
|
};
|
|
type Props = {
|
// list?: TData;
|
listData?: TData;
|
flattenListData?: T[];
|
renderItem?: (item: T, index: number) => VNode;
|
refresherEnabled?: boolean;
|
hasMore?: boolean;
|
isLoading?: boolean;
|
isError?: boolean;
|
isFetching?: boolean;
|
isFetchingNextPage?: boolean;
|
refetch?: (options?: any) => Promise<any>;
|
scrollViewStyle?: string | CSSProperties;
|
scrollViewClassName?: string;
|
fetchNextPage?: (options?: FetchNextPageOptions) => Promise<any>;
|
showMoreText?: boolean;
|
hasPaddingTop?: boolean;
|
noMoreText?: string;
|
enabledLoadingMore?: boolean;
|
customNoData?: boolean;
|
};
|
|
const props = withDefaults(defineProps<Props>(), {
|
renderItem: () => () => null,
|
refresherEnabled: true,
|
showMoreText: true,
|
hasPaddingTop: false,
|
noMoreText: '没有更多内容了~',
|
enabledLoadingMore: true,
|
customNoData: undefined,
|
});
|
|
const emit = defineEmits<{
|
(e: 'refresh', done: () => any): void;
|
(e: 'loadMore'): void;
|
}>();
|
|
const hasNoData = computed(() => {
|
if (props.customNoData !== undefined) return props.customNoData;
|
if (props.listData?.pages?.length) {
|
return props.listData?.pages[0].data.length === 0;
|
}
|
return true;
|
});
|
|
const state = reactive({
|
triggered: false,
|
});
|
|
function lower() {
|
if (props.hasMore && props.enabledLoadingMore) {
|
props.fetchNextPage?.();
|
emit('loadMore');
|
}
|
}
|
|
// eslint-disable-next-line no-unused-vars
|
async function onRefresherRefresh() {
|
try {
|
// 正处于刷新状态
|
if (state.triggered) return;
|
state.triggered = true;
|
|
await props.refetch?.();
|
|
state.triggered = false;
|
} catch (error) {}
|
}
|
|
const { onScroll, scrollDistance, oldScrollDistance, setScrollDistance } = useScrollDistance();
|
|
function backToTop() {
|
setScrollDistance(0);
|
}
|
|
const scrollToBottom = (dis = 300) => {
|
setScrollDistance(scrollDistance.value + dis);
|
};
|
|
function findDataIndex(item: T) {
|
return props.flattenListData?.findIndex((data) => data === item);
|
}
|
|
defineExpose({ backToTop, scrollToBottom });
|
</script>
|