跳转到内容

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 方法
 */
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 type ExtraType = Record<string, any>

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

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

export const usePaging = <
  T = any,
  Item = any,
  Extra extends ExtraType = {},
>(
  options: PagingOptions<T>,
) => {
  const {
    page_no = 1,
    page_size = 15,
    fetchFu,
    params = {},
    append = false,
    initialLoading = false,
    initialFetch = false,
    filterValues = [void 0],
    filterFn = (p) => p,
    fetchList = (res: any) => res.list || [],
    fetchCount = (res: any) => res.count || 0,
    fetchExtra = (res: any) => res.extra || {},
  } = options

  // 分页数据
  const pager = reactive<Pager<Item, Extra>>({
    page_no,
    page_size,
    loading: initialLoading,
    count: 0,
    list: [],
    extra: {} as Extra,
    more: true,
    finished: false,
  })

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

  let setLastOutdate: (() => void) | undefined

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

    pager.loading = true

    try {
      const res = await fetchFu({
        page_no: pager.page_no,
        page_size: pager.page_size,
        ...filterFn(filterParams(params, filterValues)),
      })

      // 保证数据是最新的,如果请求已经过期,则不处理返回数据
      if (outdated) return

      const list = fetchList(res)
      const count = fetchCount(res)
      const extra = fetchExtra(res)

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

      if (append) {
        pager.list.push(...list)
      } else {
        pager.list = list
      }
    } catch (error) {
      throw error
    } finally {
      pager.loading = false
    }
  }
  /** 重置页码 */
  const resetPage = () => {
    pager.page_no = _pager.page_no
    getList()
  }
  /** 重置参数 */
  const resetParams = () => {
    if (isRef(params)) {
      params.value = cloneDeep(__params)
    } else {
      // 清空 params
      for (const key in params) {
        delete params[key]
      }
      Object.assign(params, cloneDeep(__params))
    }
    getList()
  }
  /** 重置 pager */
  const resetPager = () => {
    Object.assign(pager, cloneDeep(_pager))
    getList()
  }

  if (initialFetch) {
    getList()
  }

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

// 获取原始数据(辅助函数)
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
}