uniapp分页列表组件实现

众所周知列表分页加载 是经常常见的功能,但在uniapp生态中我似乎没有找到合适的UI库和组件,没办法只能自己琢磨去写了,在这里我会将我的一些代码实现分享给大家

话不多说先看实战效果:

相信大家已经看到效果了哈,目前该组件支持下拉刷新、自动检查加载数据、数据为空时自动提示、加载错误重新加载、返回顶部等功能,也可继续拓展其他功能,如果对大家有帮助欢迎大家拿去使用!

源码地址

如果需要使用请注意以下几点

  1. 安装以下依赖包
css 复制代码
// 核心分页逻辑用到
pnpm i @vueuse/core
  1. 将相关静态资源下载到本地

资源地址

  1. 组件内部的 icon 使用的是 nutui-uniapp 的UI资源,大家也可选择使用其他的icon资源哈

组件源代码如下:

ts 复制代码
<script setup lang="ts">
import { onLoad } from "@dcloudio/uni-app"
import { computed, getCurrentInstance, ref, watch } from "vue"

import { useListPagination } from "@/hooks"
import { debounce } from "@/utils"

import type { IPaginationFetchDataFnParam, TPaginationDataItem, TPaginationFetchDataFnReturn } from "@/hooks"
import type { ScrollViewOnScrollEvent } from "@uni-helper/uni-app-types"

export interface IListProps {
/**
 * @description 是否启用数据为空时元素占位
 * @default true
 */
    emptyPlaceholder?: boolean
/**
 * @description 数据为空时的图片
 * @default '/static/images/List/empty.png'
 */
    emptyImage?: string
/**
 * @description 数据为空时的图片尺寸, 为数组时顺序为: [宽, 高]
 * @default ['345rpx', '344rpx']
 */
    emptyImageSize?: string | [string, string]
/**
 * @description 数据为空时的文本描述
 * @default '暂无数据'
 */
    emptyText?: string

/**
 * @description 是否启用下拉刷新功能
 * @default true
 */
    refresh?: boolean
/**
 * @description 下拉刷新背景颜色
 * @default '#ffffff'
 */
    refreshBackground?: string
/**
 * @description 是否自动加载数据
 * @default true
 */
    autoLoad?: boolean
/**
 * @description 滚动条与底部距离小于 offset 时触发 fetchDataFn 函数
 * @default 300
 */
    offset?: string | number
/**
 * @description 加载过程中的提示文案
 * @default '加载中...'
 */
    loadingText?: string
/**
 * @description 加载失败后的提示文案
 * @default '加载失败,点击重试'
 */
    errorText?: string
/**
 * @description 加载完成后的提示文案
 * @default '没有更多了'
 */
    finishedText?: string
/**
 * @description 是否点击 苹果(状态栏) 按钮(标题) 返回顶部
 * @default true
 */
    clickStatusBarBackTop?: boolean

/**
 * @description 是否启用 back-top 按钮
 * @default true
 */
    backTop?: boolean
/**
 * @description 距离页面右侧的距离
 * @default '25rpx'
 */
    backTopRight?: string
/**
 * @description 距离页面底部的距离
 * @default '100rpx'
 */
    backTopBottom?: string
/**
 * @description 滚动高度达到此参数值时才显示组件
 * @default 200
 */
    backTopOffset?: number
/**
 * @description 返回顶部时是否启用过渡动画
 * @default false
 */
    backTopTransition?: boolean
/**
 * @description 每页数据大小
 * @default 10
 */
    pageSize?: number
/**
 * @description 请求数据的方法
 * @param param 请求参数
 * @returns 数据返回值
 */
    fetchDataFn: (param: IPaginationFetchDataFnParam) => TPaginationFetchDataFnReturn<TPaginationDataItem>
}

// DATA: 定义 props & 给定默认值
const props = withDefaults(defineProps<IListProps>(), {
    /** 启用数据为空时的占位 */
    emptyPlaceholder: true,
    /** 数据为空时的默认图片 */
    emptyImage: "/static/image/List/empty.png",
    // 数据为空时的图片尺寸
    emptyImageSize: () => ["345rpx", "344rpx"],
    /** 数据为空时的文本描述 */
    emptyText: "暂无数据",

    /** 是否启用下拉刷新功能 */
    refresh: true,
    /** 下拉刷新背景颜色 */
    refreshBackground: "#ffffff",
    /** 是否自动加载数据 */
    autoLoad: true,
    /** 滚动距离触发加载的默认值 */
    offset: 300,
    /** 加载中的提示文案 */
    loadingText: "加载中...",
    /** 加载失败的提示文案 */
    errorText: "加载失败,点击重试",
    /** 加载完成的提示文案 */
    finishedText: "没有更多了",
    /** 是否点击 苹果(状态栏) 按钮(标题) 返回顶部 */
    clickStatusBarBackTop: true,

    /** 启用 back-top 按钮 */
    backTop: true,
    /** 距离右侧的距离 */
    backTopRight: "25rpx",
    /** 距离底部的距离 */
    backTopBottom: "100rpx",
    /** 滚动到此高度时显示 back-top */
    backTopOffset: 200,
    /** 返回顶部时是否启用过渡动画 */
    backTopTransition: true,
    /** 每页数据大小 */
    pageSize: 10
})

// HOOKS: 使用列表分页器
const listPagination = useListPagination<TPaginationDataItem>({
    pageSize: props.pageSize,
    fetchDataFn: param => props.fetchDataFn(param)
})

/** 列表分页器返回值 */
const { currentLoadStatus, currentTotalData, currentTotalSize, totalSize, finished, refreshing, next, clearRefresh } =
    listPagination

/** STATIC: 当前实例 */
const instance = getCurrentInstance()

/** STATIC: selectorQuery 对象 */
const selectorQuery = uni.createSelectorQuery().in(instance?.proxy)

/** STATIC: scroll-view 类名 */
const scrollViewClassName = "list__scroll-view"

/** STATIC: scroll-view 内容 类名 */
const scrollViewContentClassName = "list__scroll-view__content"

/** STATIC: scroll-view 元素 */
const scrollViewElement = selectorQuery.select(`.${scrollViewClassName}`)

/** STATIC: scroll-view 内容 */
const scrollViewContentElement = selectorQuery.select(`.${scrollViewContentClassName}`)

/** STATIC: scroll-view 高度 */
let scrollViewHeight: number

// FUN: 获取元素高度
const getElementHeight = (element: UniApp.NodesRef): Promise<number> => {

    return new Promise(resolve => {

        element
            .boundingClientRect(rect => {
                // @ts-ignore
                resolve(rect.height)

            })
            .exec()

    })

}

/** FUN: 自动加载数据 */
const autoLoad = async() => {
    // 如果不自动加载数据,直接返回
    if (!props.autoLoad) {

        return

    }

    // scrollView 高度为空时,获取 scrollView 高度
    if (!scrollViewHeight) {

        scrollViewHeight = await getElementHeight(scrollViewElement)

    }

    // 记录 scroll-view 主体高度
    const _scrollViewContentHeight = await getElementHeight(scrollViewContentElement)

    // 如果 scroll-view 主体高度小于或等于 scroll-view 高度,执行 next()
    _scrollViewContentHeight <= scrollViewHeight && next()

}

// LIFECYCLE: 初次挂载完成后
onLoad(() => {

    autoLoad()

})

/** WATCH: 监听数据变化 注: 只有加载数据成功并且有值时才会发生变化 */
watch(currentTotalSize, () => {

    autoLoad()

})

/** COMPUTED: 是否显示空元素占位 */
const showEmpty = computed(() => props.emptyPlaceholder && finished.value && totalSize.value <= 0)

/** COMPUTED: 是否显示空元素占位图片样式 */
const emptyImageStyle = computed(() => {
    // 默认图片尺寸
    let size = ["0rpx", "0rpx"]
    if (Array.isArray(props.emptyImageSize) && props.emptyImageSize.length === 2) {

        size = props.emptyImageSize

    }
    else if (typeof props.emptyImageSize === "string") {

        size = [props.emptyImageSize, props.emptyImageSize]

    }
    return {
        width: size[0],
        height: size[1]
    }

})

/** COMPUTED: 底部提示图标 */
const bottomIcon = computed(() => {
    // 根据当前加载状态返回对应图标
    switch (currentLoadStatus.value) {

    case "loading":
        return "loading1"
    case "fail":
        return "refresh2"
    default:
        return ""

    }

})

/** COMPUTED: 底部提示文案 */
const bottomText = computed(() => {
    // 如果正在刷新
    if (refreshing.value) {

        return ""

    }

    // 如果所有数据已经加载完成 && 总数据量大于 0
    if (finished.value && totalSize.value > 0) {

        return props.finishedText

    }

    // 根据当前加载状态返回对应文案
    switch (currentLoadStatus.value) {

    case "loading":
        return props.loadingText
    case "fail":
        return props.errorText
    default:
        return ""

    }

})

/** COMPUTED: back-top 样式 */
const backTopStyle = computed(() => {

    return {
        right: props.backTopRight,
        bottom: props.backTopBottom
    }

})

/** REF: 是否显示 back-top */
const showBackTop = ref(false)

// EVENT: 滚动事件
const onScroll = debounce((e: ScrollViewOnScrollEvent) => {

    const {
        detail: { scrollTop: _scrollTop }
    } = e

    // 是否显示 back-top
    showBackTop.value = _scrollTop >= props.backTopOffset

}, 100)

// EVENT: 点击底部提示文案
const onClickBottomText = () => {
    // 如果当前加载状态是失败
    currentLoadStatus.value === "fail" && next()

}

/** REF: 滚动量 */
const scrollTop = ref(1)

// EVENT: 点击 back-top
const onClickBackTop = () => {

    scrollTop.value = 0
    // 踩坑:uni-app 中 scroll-view 的 scrollTop 属性只有在变化时才会触发滚动
    const _delay = setTimeout(() => {
        // 重置滚动量
        scrollTop.value = 1
        clearTimeout(_delay)

    }, 10)

}

/** EXPOSE: 导出列表分页器状态信息 */
defineExpose({
    listPagination
})
</script>

<script lang="ts">
export default {
    options: {
        // 虚拟化组件节点,使组件外部样式能够直接作用到组件内部的第一层节点
        // eslint-disable-next-line padded-blocks
        virtualHost: true,
        // 允许父组件样式穿透到子组件
        // eslint-disable-next-line padded-blocks
        styleIsolation: "shared"
    }
}
</script>

<template>
    <view class="list">
        <scroll-view
            :class="scrollViewClassName"
            :scroll-y="true"
            :enable-flex="true"
            :scroll-top="scrollTop"
            :refresher-triggered="refreshing"
            :lower-threshold="props.offset"
            :scroll-with-animation="props.backTopTransition"
            :refresher-enabled="props.refresh"
            :refresher-background="props.refreshBackground"
            :enable-back-to-top="props.clickStatusBarBackTop"
            @scroll="onScroll"
            @scrolltolower="next"
            @refresherrefresh="clearRefresh"
        >
            <view :class="scrollViewContentClassName">
                <!-- 用于存放列表之上的其他内容 -->
                <slot name="top"></slot>

                <view v-if="showEmpty" class="list__scroll-view__content__empty-box">
                    <image
                        class="list__scroll-view__content__empty-box__img"
                        :style="emptyImageStyle"
                        :src="props.emptyImage"
                        mode="scaleToFill"
                    />

                    <view class="list__scroll-view__content__empty-box__text">{{ props.emptyText }}</view>
                </view>

                <view v-else class="list__scroll-view__content__main">
                    <view class="list__scroll-view__content__main__item" v-for="(item, index) in currentTotalData" :key="index">
                        <slot name="default" :item="item" :index="index"></slot>
                    </view>
                </view>

                <view v-if="bottomText" class="list__scroll-view__content__bottom-box" @click="onClickBottomText">
                    <nut-icon v-if="bottomIcon" :name="bottomIcon" custom-color="#808089" size="30rpx" />

                    <view class="list__scroll-view__content__bottom-box__text">{{ bottomText }}</view>
                </view>
            </view>
        </scroll-view>

        <view
            v-if="props.backTop"
            class="list__back-top"
            :class="{ 'list__back-top--activation': showBackTop }"
            :style="backTopStyle"
            @click="onClickBackTop"
        >
            <image class="list__back-top__img" src="/static/image/List/back-top.png" mode="scaleToFill" />
        </view>
    </view>
</template>

<style lang="scss" scoped>
.list {
    position: relative;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    overflow: hidden;

    &__scroll-view {
        position: relative;
        box-sizing: border-box;
        width: 100%;
        height: 100%;

        &__content {
            position: relative;
            box-sizing: border-box;
            width: 100%;

            &__empty-box {
                display: flex;
                flex-direction: column;
                gap: 20rpx 0;
                align-items: center;
                justify-content: center;
                width: 100%;
                height: fit-content;
                padding: 30rpx 0;
                overflow: hidden;

                &__img {
                    display: block;
                }

                &__text {
                    width: 100%;
                    color: #808089;
                    font-size: 27rpx;
                    text-align: center;
                    @include text-ellipsis-mixin;
                }
            }

            &__main {
                position: relative;
                box-sizing: border-box;
                width: 100%;
            }

            &__bottom-box {
                display: flex;
                gap: 0 10rpx;
                align-items: center;
                justify-content: center;
                width: 100%;
                height: fit-content;
                padding: 30rpx 0;

                &__text {
                    color: #808089;
                    font-size: 27rpx;
                }
            }
        }
    }

    &__back-top {
        position: absolute;
        display: flex;
        align-items: center;
        justify-content: center;
        width: 84rpx;
        height: 84rpx;
        overflow: hidden;
        background: #29d446;
        border-radius: 50%;
        box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.12);
        transform: scale(0);
        transition: transform 0.3s;

        &--activation {
            transform: scale(1);
        }

        &__img {
            display: block;
            width: 25rpx;
            height: 32rpx;
        }
    }
}
</style>

核心分页逻辑源代码如下:

ts 复制代码
import { useOffsetPagination } from "@vueuse/core"
import { ref, computed } from "vue"

import type { UseOffsetPaginationReturn } from "@vueuse/core"
import type { ComputedRef, Ref } from "vue"

/** 分页数据项 */
type TPaginationDataItem = string | number | Record<string, any> | any

/** 分页数据映射 */
type TPaginationDataMap<T> = Map<number, T[]>

/** 分页请求数据函数参数 */
interface IPaginationFetchDataFnParam {
    /** 当前页码 */
    currentPage: number
    /** 当前页码大小 */
    currentPageSize: number
    /** 总页数 */
    pageCount: number
    /** 是否为第一页 */
    isFirstPage: boolean
    /** 是否为最后一页 */
    isLastPage: boolean
}

/** 分页请求数据函数返回结果类型 */
interface IPaginationFetchDataFnResult<T extends TPaginationDataItem> {
    /** 当前页码的数据 */
    currentPageData: T[]
    /** 数据总大小 */
    totalSize: number
}

/** 分页请求数据函数返回类型 */
type TPaginationFetchDataFnReturn<T extends TPaginationDataItem> = Promise<IPaginationFetchDataFnResult<T> | void>

/** 分页配置 */
interface IUsePaginationOptions<T extends TPaginationDataItem> {
    /**
     * @description 页码
     * @default 1
     */
    page?: number
    /**
     * @description 每页显示的项目数
     * @default 10
     */
    pageSize?: number
    /**
     * @description 是否在加载失败时使用之前加载的数据
     * @default false
     */
    usePreviousDataOnFail?: boolean
    /**
     * 请求数据的函数
     *
     * @param param 请求参数
     * @returns 返回 当前页的数据列表 和 数据总数
     */
    fetchDataFn: (param: IPaginationFetchDataFnParam) => TPaginationFetchDataFnReturn<T>
}

/** 分页加载状态 */
type TPaginationLoadStatus = "loading" | "success" | "fail"
/** 分页刷新参数 */
type TPaginationRefreshParam = number | `${number}`

/** 分页返回结果 */
interface IUsePaginationReturn<T extends TPaginationDataItem> extends UseOffsetPaginationReturn {
    /** 当前加载状态 */
    currentLoadStatus: Ref<TPaginationLoadStatus>
    /** 是否初始化成功 */
    initialized: Ref<boolean>
    /** 是否正在刷新 */
    refreshing: Ref<boolean>
    /** 当前页的数据(计算属性) */
    currentPageData: ComputedRef<T[]>
    /** 当前已加载的总数据(计算属性) */
    currentTotalData: ComputedRef<T[]>
    /** 当前已加载的总数据量(计算属性) */
    currentTotalSize: ComputedRef<number>
    /** 是否所有数据已加载完成(计算属性) */
    finished: ComputedRef<boolean>
    /** 数据总数量 */
    totalSize: Ref<number>
    /** 当前所有页的数据映射 */
    currentTotalDataMap: Ref<TPaginationDataMap<T>>
    /**
     * 加载指定页码数据
     *
     * @param page 目标页码 默认: 当前页码
     */
    load: (page?: TPaginationRefreshParam) => Promise<void>
    /**
     * 刷新单页数据
     *
     * @param page 目标页码 默认: 当前页码
     */
    refresh: (page?: TPaginationRefreshParam) => Promise<void>
    /** 上一页 */
    prev: () => Promise<void>
    /** 下一页 */
    next: () => Promise<void>
}

/**
 * 分页器
 *
 * @author dyb
 * @date 04/09/2024/  18:05:57
 * @export
 * @template T 分页数据类型
 * @param {IUsePaginationOptions<T>} options 分页配置
 * @returns {*}  {IUsePaginationReturn<T>} 分页返回结果
 */
const usePagination = <T extends TPaginationDataItem>(options: IUsePaginationOptions<T>): IUsePaginationReturn<T> => {

    const { page = 1, pageSize = 10, usePreviousDataOnFail = false, fetchDataFn } = options

    /** REF: 是否初始化成功 */
    const initialized = ref<boolean>(false)
    /** REF: 是否正在刷新数据 */
    const refreshing = ref<boolean>(false)
    /** REF: 数据总数 */
    const totalSize = ref<number>(0)
    /** REF: 加载状态 */
    const currentLoadStatus = ref<TPaginationLoadStatus>("success")
    /** REF: 当前所有页的数据映射 */
    const currentTotalDataMap: Ref<TPaginationDataMap<T>> = ref(new Map())

    /** COMPUTED: 当前页的数据 */
    const currentPageData = computed<T[]>(() => currentTotalDataMap.value.get(currentPage.value) || [])

    /** COMPUTED: 当前所有页的数据 */
    const currentTotalData = computed<T[]>(() => Array.from(currentTotalDataMap.value.values()).flat())

    /** COMPUTED: 当前已加载的总数据量 */
    const currentTotalSize = computed<number>(() => currentTotalData.value.length)

    /** COMPUTED: 是否已加载完成 */
    const finished = computed<boolean>(() => initialized.value && currentTotalSize.value === totalSize.value)

    // 初始化分页器
    const {
        currentPage,
        currentPageSize,
        pageCount,
        isFirstPage,
        isLastPage,
        prev: offsetPaginationPrev,
        next: offsetPaginationNext
    } = useOffsetPagination({
        total: totalSize,
        page,
        pageSize
    })

    /**
     * 检查页码是否有效
     *
     * @param {TPaginationRefreshParam} page 目标页码
     * @returns {*}  {boolean} 是否有效
     */
    const _isPageValid = (page: TPaginationRefreshParam): boolean => {

        // 强制设置数字类型
        page = Number(page)
        if (isNaN(page)) {

            console.error(`_isPageValid() =>> page不是数字 page: ${page}`)
            return false

        }

        if (page < 1 || page > pageCount.value) {

            console.error(`_isPageValid() =>> page超出范围 page: ${page} pageCount: ${pageCount.value}`)
            return false

        }
        return true

    }

    /**
     * 加载指定页码数据
     *
     * @author dyb-dev
     * @date 06/09/2024/  21:42:48
     * @param {TPaginationRefreshParam} [page] 目标页码 默认: 当前页码
     */
    const load = async(page: TPaginationRefreshParam = currentPage.value) => {

        // 页码无效时
        if (!_isPageValid(page)) {

            return

        }

        currentPage.value = <number>page

        // 取消数据的响应性
        // 解决:同时加载第一页和第二页的数据,当前页码为第二页,第二页的数据先返回,第一页的数据后返回,这个时候 currentTotalDataMap.value.set 由于 currentPage 数据的响应性,会将第一页的请求结果数据覆盖第二页的数据,造成数据错乱
        const _param: IPaginationFetchDataFnParam = {
            currentPage: currentPage.value,
            currentPageSize: currentPageSize.value,
            pageCount: pageCount.value,
            isFirstPage: isFirstPage.value,
            isLastPage: isLastPage.value
        }

        try {

            currentLoadStatus.value = "loading"

            const _result = await fetchDataFn(_param)

            // 当异步同时加载不同页码的数据时,只对当前页码的数据进行处理
            // 解决:同时加载第一页和第二页的数据,当前页码为第二页,第二页的数据先返回,第一页的数据后返回,`currentLoadStatus` 状态可能会设置错误
            if (currentPage.value !== _param.currentPage) {

                console.warn(
                    `load() =>> 当前页码已变更取消数据更新操作 加载前页码: ${_param.currentPage} 加载后页码: ${currentPage.value}`
                )
                return

            }

            /** 当前页码的数据 */
            const _currentPageData = _result?.currentPageData
            /** 数据总大小 */
            const _totalSize = Number(_result?.totalSize)

            if (!Array.isArray(_currentPageData)) {

                throw `fetchDataFn() 返回的数据不是数组 currentPageData: ${_currentPageData}`

            }

            if (isNaN(_totalSize)) {

                throw `fetchDataFn() 返回的数据总数不是数字 _totalSize: ${_totalSize}`

            }

            /** 当前页码的数据长度 */
            const _currentPageDataLength = _currentPageData.length

            if (_totalSize < _currentPageDataLength) {

                throw `fetchDataFn() 数据总数小于当前页数据长度 totalSize: ${_totalSize} _currentPageDataLength: ${_currentPageDataLength}`

            }

            if (_currentPageDataLength <= 0 && _totalSize > 0) {

                throw `fetchDataFn() 当前数据为空,但总数据不为空 totalSize: ${_totalSize} _currentPageDataLength: ${_currentPageDataLength}`

            }

            // 更新数据
            currentTotalDataMap.value.set(_param.currentPage, _currentPageData)
            totalSize.value = _totalSize

            // 设置初始化完成状态
            initialized.value = true
            currentLoadStatus.value = "success"

        }
        catch (error) {

            console.error(`load() =>> currentPage: ${_param.currentPage} ${error}`)

            // 不需要使用之前加载的数据时
            if (!usePreviousDataOnFail) {

                // 更新数据
                currentTotalDataMap.value.set(_param.currentPage, [])

            }
            currentLoadStatus.value = "fail"

        }

    }

    /**
     * 刷新数据
     *
     * @author dyb-dev
     * @date 06/09/2024/  22:11:55
     * @param {TPaginationRefreshParam} [page=currentPage.value] 目标页码 默认: 当前页码
     */
    const refresh = async(page: TPaginationRefreshParam = currentPage.value) => {

        // 页码无效时
        if (!_isPageValid(page)) {

            return

        }

        // 更新刷新状态
        refreshing.value = true

        await load(page)

        // 更新刷新状态
        refreshing.value = false

    }

    /**
     * 上一页/下一页
     *
     * @author dyb-dev
     * @date 06/09/2024/  20:46:51
     * @param {()=> void} method 上一页/下一页
     */
    const _change = async(method: () => void) => {

        try {

            // 未初始化成功
            if (!initialized.value) {

                throw `初始化未完成 initialized: ${initialized.value}`

            }
            method()

            await load()

        }
        catch (error) {

            throw `_change() =>> ${error}`

        }

    }

    /**
     * 上一页
     *
     * @author dyb
     * @date 05/09/2024/  00:58:19
     */
    const prev = async() => {

        try {

            await _change(offsetPaginationPrev)

        }
        catch (error) {

            console.error("prev() =>>", error)

        }

    }

    /**
     * 下一页
     *
     * @author dyb
     * @date 05/09/2024/  00:58:09
     */
    const next = async() => {

        try {

            await _change(offsetPaginationNext)

        }
        catch (error) {

            console.error("next() =>>", error)

        }

    }

    return {
        initialized,
        refreshing,
        finished,

        isFirstPage,
        isLastPage,
        currentLoadStatus,
        currentPage,
        currentPageSize,
        currentPageData,
        currentTotalSize,
        currentTotalData,
        currentTotalDataMap,
        pageCount,
        totalSize,

        load,
        refresh,
        prev,
        next
    }

}

/** 列表分页配置,继承自 IUsePaginationOptions */
interface IUseListPaginationOptions<T extends TPaginationDataItem>
    extends Omit<IUsePaginationOptions<T>, "usePreviousDataOnFail"> {}

/** 列表分页返回结果,继承自 IUsePaginationReturn */
interface IUseListPaginationReturn<T extends TPaginationDataItem> extends Omit<IUsePaginationReturn<T>, "refresh" | "prev"> {
    /** 清空所有数据并刷新首页 */
    clearRefresh: () => Promise<void>
    /** 初始化分页器 */
    initialize: () => Promise<void>
}

/**
 * 列表分页器
 *
 * @author dyb-dev
 * @date 05/09/2024/  14:19:07
 * @template T 列表分页数据类型
 * @param {IUseListPaginationOptions<T>} options 列表分页配置
 * @returns {*}  {IUseListPaginationReturn<T>} 列表分页返回结果
 */
const useListPagination = <T extends TPaginationDataItem>(options: IUseListPaginationOptions<T>): IUseListPaginationReturn<T> => {

    // 调用 usePagination 并传入参数
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
    const { refresh, prev, ..._pagination } = usePagination(options)

    /**
     * 重置分页状态
     *
     * @author dyb-dev
     * @date 16/11/2024/  00:53:21
     */
    const _reset = () => {

        _pagination.currentTotalDataMap.value.clear()
        _pagination.totalSize.value = 0
        _pagination.initialized.value = false

    }

    /**
     * 初始化分页器
     *
     * @author dyb-dev
     * @date 16/11/2024/  00:53:54
     */
    const initialize = async() => {

        _reset()
        await _pagination.load(1)

    }

    /**
     * 清空所有数据并刷新首页
     *
     * @author dyb-dev
     * @date 06/09/2024/  22:24:56
     */
    const clearRefresh = async() => {

        _reset()
        await refresh(1)

    }

    /**
     * 列表分页的下一页逻辑
     *
     * @author dyb
     * @date 05/09/2024/  13:16:57
     */
    const next = async() => {

        try {

            const { initialized, currentLoadStatus, currentPage, finished } = _pagination

            if (!initialized.value) {

                console.warn("next() =>>", `初始化未完成 initialized: ${initialized.value} 取消执行下一页 优先执行初始化`)
                await initialize()
                return

            }
            if (finished.value) {

                console.warn("next() =>>", `已经加载完所有数据,无法继续加载下一页 currentPage: ${currentPage.value}`)
                return

            }
            if (currentLoadStatus.value === "loading") {

                throw `当前页码数据加载中,取消继续加载下一页的数据 currentPage: ${currentPage.value} currentLoadStatus: ${currentLoadStatus.value}`

            }
            if (currentLoadStatus.value === "fail") {

                console.warn(
                    "next() =>>",
                    `当前页码数据加载失败,取消继续加载下一页的数据,重新加载当前页数据  currentPage: ${currentPage.value} currentLoadStatus: ${currentLoadStatus.value}`
                )

                // 加载失败,重新加载当前页码数据
                await _pagination.load()
                return

            }
            await _pagination.next()

        }
        catch (error) {

            console.error("next() =>>", error)

        }

    }

    return {
        ..._pagination,
        initialize,
        clearRefresh,
        next
    }

}

export type {
    IUseListPaginationOptions,
    IUseListPaginationReturn,
    TPaginationLoadStatus,
    TPaginationDataItem,
    TPaginationDataMap,
    IPaginationFetchDataFnParam,
    IPaginationFetchDataFnResult,
    TPaginationFetchDataFnReturn,
    TPaginationRefreshParam,
    IUsePaginationOptions,
    IUsePaginationReturn
}

export { useListPagination, usePagination }

本文到这里就结束啦,希望能够帮助到大家!

相关推荐
Cachel wood13 分钟前
Vue.js前端框架教程8:Vue消息提示ElMessage和ElMessageBox
linux·前端·javascript·vue.js·前端框架·ecmascript
桃园码工2 小时前
4_使用 HTML5 Canvas API (3) --[HTML5 API 学习之旅]
前端·html5·canvas
桃园码工2 小时前
9_HTML5 SVG (5) --[HTML5 API 学习之旅]
前端·html5·svg
人才程序员2 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
m0_548514772 小时前
前端三大主流框架:React、Vue、Angular
前端·vue.js·react.js
m0_748232393 小时前
单页面应用 (SPA):现代 Web 开发的全新视角
前端
孤留光乩3 小时前
从零搭建纯前端飞机大战游戏(附源码)
前端·javascript·游戏·html·css3
伊泽瑞尔.3 小时前
el-tabs标签过多
前端·javascript·vue.js
2401_854391083 小时前
智能挂号系统设计典范:SSM 结合 Vue 在医院的应用实现
前端·javascript·vue.js
觉醒的程序猿3 小时前
vue2设置拖拽选中时间区域
开发语言·前端·javascript