跳转到内容

usePaging

usePaging 是一个用于处理分页数据的通用钩子,它提供了分页数据的获取、重置、刷新等功能。该钩子特别适用于需要分页加载数据的场景,例如表格、列表等。

通过 usePaging,你可以轻松管理分页状态,包括当前页码、每页数量、数据列表、加载状态等。它还支持追加模式和参数过滤,使得分页数据的处理更加灵活。

vue
<template>
  <div>
    <ul>
      <li v-for="item in pager.list" :key="item.id">{{ item.name }}</li>
    </ul>
    <button @click="getList" :disabled="pager.loading">加载更多</button>
    <button @click="resetPage">重置页码</button>
    <button @click="resetParams">重置参数</button>
    <button @click="refresh">刷新</button>
  </div>
</template>

<script setup>
import { usePaging } from './paging';

const { pager, getList, resetPage, resetParams, refresh } = usePaging({
  fetchFu: fetchData,
  initialFetch: true,
});
</script>

代码:

ts
/**
 * usePaging 分页钩子函数
 * 依赖于 lodash-es 的 cloneDeep 方法
 */
// @ts-ignore 这里我并没有下载 lodash-es 所以用 @ts-ignore 去除警告,真实代码需去除这行
import { cloneDeep } from 'lodash-es'
import { reactive, isRef, isProxy, toRaw } from 'vue'
import type { Reactive, Ref } from 'vue'

export type PagingParams = {
  page_no?: number
  page_size?: number
}

export type Params = Record<string, any>

export interface Pager<
  Item = any,
  Extra extends Record<string, any> = {},
> {
  /**
   * 当前页码
   */
  page_no: number
  /**
   * 每页数量
   */
  page_size: number
  /**
   * 是否加载中
   */
  loading: boolean
  /**
   * 数据总数
   */
  count: number
  /**
   * 数据列表
   */
  list: Item[]
  /**
   * 额外数据
   */
  extra: Extra
  /**
   * 是否还有更多
   */
  more: boolean
  /**
   * 是否已经完成(对 pager.more 的取反),适用于上拉加载等场景
   */
  finished: boolean
}

export interface PagingOptions {
  /**
   * 初始页数
   */
  page_no?: number
  /**
   * 初始每页数量
   */
  page_size?: number
  /**
   * 请求方法
   */
  fetchFu: (params: Params) => Promise<any>
  /**
   * 请求参数
   */
  params?: Params
  /**
   * 是否为追加模式
   */
  append?: boolean
  /**
   * pager.loading 是否默认为 true
   */
  initialLoading?: boolean
  /**
   * 是否在组件实例化时请求
   */
  initialFetch?: boolean
  /**
   * 过滤参数,例如:[void 0] 当请求参数为 undefined 时,则过滤掉这个参数
   */
  filterValues?: any[]
  /**
   * 自定义参数过滤函数
   */
  filterFn?: (params: Params) => Params
}

export const usePaging = <
  Item = any,
  Extra extends Record<string, any> = {},
>({
  page_no = 1,
  page_size = 15,
  fetchFu,
  params = {},
  append = false,
  initialLoading = false,
  initialFetch = false,
  filterValues = [void 0],
  filterFn = (p) => p,
}: PagingOptions) => {
  // 分页数据
  const pager = reactive<Pager<Item, Extra>>({
    page_no,
    page_size,
    loading: initialLoading,
    count: 0,
    list: [],
    extra: {} as Extra,
    more: true,
    finished: false,
  })

  // 记录初始参数
  const initParams: Params = Object.freeze(cloneDeep(getRaw(params)))
  // 记录初始 pager
  const initPager = Object.freeze(cloneDeep(pager))

  let setLastOutdate: () => void = () => {}

  /**
   * 请求分页接口
   */
  const getList = async () => {
    setLastOutdate()
    let outdated = false
    setLastOutdate = () => {
      outdated = true
    }

    pager.loading = true

    return fetchFu({
      page_no: pager.page_no,
      page_size: pager.page_size,
      ...filterFn(filterParams(params, filterValues)),
    })
      .then((res: any) => {
        if (outdated) {
          // 保证数据是最新的,如果请求已经过期,则不处理返回数据
          return Promise.resolve(res)
        }

        pager.count = res?.count
        pager.extra = res?.extra
        pager.more = pager.page_no * pager.page_size < pager.count
        pager.finished = !pager.more

        if (append) {
          pager.list.push(...res?.list)
        } else {
          pager.list = res?.list
        }
        return Promise.resolve(res)
      })
      .finally(() => {
        pager.loading = false
      })
  }
  /**
   * 重置页码
   */
  const resetPage = () => {
    pager.page_no = initPager.page_no
    getList()
  }
  /**
   * 重置参数
   */
  const resetParams = () => {
    if (isRef(params)) {
      params.value = cloneDeep(initParams)
    } else {
      // 清空 params
      for (const key in params) {
        delete params[key]
      }
      Object.assign(params, cloneDeep(initParams))
    }
    getList()
  }
  /**
   * 重置 pager
   */
  const resetPager = () => {
    Object.assign(pager, cloneDeep(initPager))
  }
  /**
   * 刷新
   */
  const refresh = () => {
    resetPager()
    getList()
  }

  if (initialFetch) {
    getList()
  }

  return {
    pager,
    getList,
    resetPage,
    resetParams,
    resetPager,
    refresh,
  }
}

// 获取原始数据(辅助函数)
const getRaw = <T>(value: Ref<T> | Reactive<T> | T): T => {
  if (isRef(value)) {
    if (isProxy(value.value)) {
      return toRaw(value.value) as T
    }
    return value.value as T
  } else if (isProxy(value)) {
    return toRaw(value) as T
  } else {
    return value as T
  }
}

// 过滤参数(辅助函数)
const filterParams = (params: Params, values: any[] = []) => {
  const _params = Object.assign({}, params)
  // 过滤参数
  for (const key in _params) {
    if (values.includes(_params[key])) {
      delete _params[key]
    }
  }
  return _params
}