Appearance
分页查询封装
本节目标:理解“分页查询封装”的核心思路,并能把它用于实际开发或面试表达。 import { reactive, ref, computed, watch, type Ref } from 'vue' /**
- 分页请求基础参数约束
- 约定:后端使用 page / page_size 作为分页字段 / export interface PaginatedParams { page: number page_size: number // 其他任意筛选 / 排序字段 [key: string]: unknown } /*
- 标准化分页返回结构
- 你可以在 fetcher 里把后端的结构适配成这个 / export interface PaginatedResult<T> { data: T[] total: number current_page: number per_page: number } /*
- usePaginatedTable 选项 / export interface UsePaginatedTableOptions<TItem, P extends PaginatedParams> { fetcher: (params: P) => Promise<PaginatedResult<TItem>> initialParams: P immediate?: boolean } /*
- 核心:通用分页 + 加载更多 + 条件变化重置分页的状态机 */ export function usePaginatedTable<TItem, P extends PaginatedParams>( options: UsePaginatedTableOptions<TItem, P>, ) { const { fetcher, initialParams, immediate = true } = options const params = reactive<P>({ ...(initialParams as P), }) const dataSource: Ref<TItem[]> = ref<TItem[]>([]) const total = ref(0) const loading = ref(false) const error = ref<Error | null>(null) const hasMore = ref(false) const lastPageSize = ref(initialParams.page_size) const refreshKey = ref(0) // 从 params 中剥离分页字段,得到“查询身份” const queryKey = computed(() => { const { page, page_size, ...rest } = params return JSON.stringify({ ...rest, rk: refreshKey.value, }) }) // 真正发请求并根据 append 决定是追加还是替换 const execute = async (append: boolean) => { loading.value = true error.value = null try { // 复制一份 params,避免外部在请求中途修改导致混乱 const query = { ...(params as P) } const result = await fetcher(query) total.value = result.total lastPageSize.value = result.per_page const list = Array.isArray(result.data) ? result.data : [] if (append) { dataSource.value = [...dataSource.value, ...list] } else { dataSource.value = list } // 根据总数和每页数计算是否还有更多 const pageCount = result.per_page > 0 ? Math.ceil(result.total / result.per_page) : 0 hasMore.value = pageCount > 0 && result.current_page < pageCount } catch (err) { error.value = err instanceof Error ? err : new Error(String(err)) } finally { loading.value = false } } // 核心 watch,统一处理 // 1)查询身份变化 => 回到第一页 // 2)同一查询身份翻页 => 追加数据(加载更多) watch( () => [queryKey.value, params.page] as const, async ([newKey, newPage], [oldKey, oldPage] = [undefined, undefined], onInvalidate) => { // 若 immediate=false,则首次不自动请求 if (!immediate && oldKey === undefined && oldPage === undefined) { return } // 查询条件变了但页码不是 1,先把页码重置为 1,再由下一次 watch 触发请求 if (newKey !== oldKey && newPage !== 1) { params.page = 1 return } // 在“同一查询身份下,页码变化”时,视为加载更多 const append = newKey === oldKey && newPage !== oldPage let cancelled = false onInvalidate(() => { cancelled = true }) // 非追加场景先清空数据,避免旧数据闪烁 if (!append) { dataSource.value = [] total.value = 0 } await execute(append) // 如果在请求过程中依赖已失效,则丢弃结果 if (cancelled) return }, { immediate, flush: 'post' }, ) // 仅在还有更多且未加载中时,才增加页码 const loadMore = () => { if (!hasMore.value || loading.value) return params.page += 1 } // 部分更新参数,只有真的变化才触发 watch const updateParams = (partial: Partial<P>) => { let changed = false for (const key in partial) { if (Object.prototype.hasOwnProperty.call(partial, key)) { if (partial[key] !== params[key]) { changed = true } } } if (!changed) return Object.assign(params, partial) } // 保持当前筛选条件,重新请求第一页 const refresh = () => { dataSource.value = [] hasMore.value = false params.page = 1 refreshKey.value += 1 } // 重置为初始参数(保留初始分页大小),并重新请求 const reset = () => { Object.assign(params, { ...(initialParams as P), }) params.page = initialParams.page params.page_size = initialParams.page_size dataSource.value = [] hasMore.value = false refreshKey.value += 1 } return { // state params, dataSource, total, loading, error, hasMore, lastPageSize, // actions loadMore, updateParams, refresh, reset, } }
