useList:Vue3通用列表管理Hook,轻松搞定分页与筛选
在前端开发中,编写列表页面时最令人头疼的就是那些重复的分页、筛选和加载状态管理逻辑。每次都要手动处理loading状态、维护分页参数、判断数据是追加还是替换……长期如此确实让人崩溃。为了解决这些痛点,我们封装了 useList 这个通用Hook,专门用于统一管理列表页面的各种状态与请求,让开发者更专注于业务逻辑。

核心功能:一句话帮你省掉大量重复代码
这个Hook到底能帮你减少多少重复工作?简单列举几条核心能力:
- 响应式状态管理:通过
reactive将筛选条件和分页配置包裹起来,修改一处即可全局联动,无需手动同步刷新。 - 数据请求封装:loading加载状态、错误捕获、数据赋值一气呵成,组件中再也不用手写 try catch 了。
- 追加/替换模式:借助
isPush参数,既能支持常规分页(翻页直接替换数据),也能轻松实现滚动加载(持续追加数据)。 - 筛选重置:保存初始筛选条件,一键恢复至最原始状态,避免用户手动逐一清空。
详细代码与核心实现
直接上代码,关键位置已添加注释,仔细阅读即可轻松掌握:
import { reactive, ref, toRaw } from 'vue'export interface IPage {
pageNum: number
pageSize: number
total?: number
}export interface GetListFnArgsType {
pagination: IPage
filter: T
}
export default function useListextends Record<string, any>>(
filterOption: U,
getListFn: (args: GetListFnArgsType) => Promise<InResult<{ records: Array, total: number }>>, // InResult 类型定义请阅读uni.request 二次封装这篇文章
pageOption?: IPage,
) {
const list = ref<Array>([])
const loading = ref(false)
const filter = reactive({ ...filterOption })
const pagination = reactive<IPage>({
...pageOption,
pageSize: pageOption?.pageSize || 10,
pageNum: pageOption?.pageNum || 1,
}) // 请求列表数据
const loadData = async (curPage = pagination.pageNum, isPush = true) => {
pagination.pageNum = curPage
loading.value = true const req = { pagination, filter: toRaw(filter) as U }
try {
const res = await getListFn(req as GetListFnArgsType)
loading.value = false
if (!isPush) {
list.value = res.data.records as Array<any>
}
else {
list.value = [...list.value, ...res.data.records as Array<any>]
}
pagination.total = res.data.total
}
catch (error) {
loading.value = false
console.log(error)
}
} // 重置筛选
const filterReset = () => {
Object.assign(filter, filterOption)
loadData(1, false)
} // 搜索
const filterSearch = () => {
console.log(121212)
return loadData(1, false)
} // 分页请求
const loadDataPage = (page: number, isPush = true) => {
return loadData(page, isPush)
} // 分页大小改变
const loadDataPageSize = (pageSize: number) => {
pagination.pageSize = pageSize
loadData(1, false)
} return {
list,
loading,
filter,
pagination,
loadData,
loadDataPage,
loadDataPageSize,
filterReset,
filterSearch,
}
}
类型定义:清晰的接口设计
IPage——分页参数基础接口
分页参数的基础接口,包含常规的三件套:
interface IPage {
pageNum: number // 当前页码
pageSize: number // 每页条数
total?: number // 总条数(由响应数据填充)
}
GetListFnArgsType——请求函数入参类型
将分页和筛选条件整合在一起,作为请求函数的参数类型:
interface GetListFnArgsType {
pagination: IPage // 分页信息
filter: T // 筛选条件
}
返回值一览:开箱即用的属性和方法
调用Hook之后,你将获得以下可直接使用的属性和方法:
| 属性 | 类型 | 说明 |
|---|---|---|
list | Ref | 当前列表数据(响应式) |
loading | Ref | 数据加载状态 |
filter | Reactive | 筛选条件对象(响应式,直接绑定表单) |
pagination | Reactive | 分页信息(响应式,自动更新总条数) |
loadData | (curPage?: number, isPush?: boolean) => Promise | 加载数据核心方法 |
loadDataPage | (page: number, isPush?: boolean) => Promise | 指定页码加载数据(分页切换时使用) |
loadDataPageSize | (pageSize: number) => void | 修改每页条数并重新加载第一页 |
filterReset | () => void | 重置筛选条件为初始值,并刷新第一页 |
filterSearch | () => Promise | 搜索(强制从第一页开始加载) |
方法详解:一看就会的实用操作
loadData(curPage, isPush)
核心加载方法。需要指定加载哪一页,以及是否采用追加模式:
- curPage:默认值为当前分页的页码,通常无需手动传入。
- isPush:当为
true时,新数据追加到现有列表末尾(适用于滚动加载);为false则直接覆盖替换(适用于普通翻页)。
loadDataPage(page, isPush)
直接调用 loadData,是快速封装,方便在模板中绑定事件。
loadDataPageSize(pageSize)
用户切换每页显示条数时,先更新分页配置,然后立即请求第一页数据。
filterReset()
重置筛选条件至初始状态,并重新加载第一页。注意是直接覆盖,而非合并。
filterSearch()
搜索按钮点击时,先将页码重置为1,再发起请求。这样用户修改筛选条件后点击搜索,结果永远从第一页开始展示。
使用示例:真实项目中的完美落地
下面是一个实际项目中的案例,假设我们管理一个“事件列表”,筛选条件包括关键词、状态、起止时间:
import type { IEvent } from '@/api/task/type'
import { getEventList } from '@/api/task'
import useList, { type GetListFnArgsType } from '@/hooks/uesList.hook'const filterOption = {
keyword: '',
status: '',
startTime: '',
endTime: '',
}async function getListData(params: GetListFnArgsType<typeof filterOption>) {
const { pagination, filter } = params
const req = {
params: {
pageNo: pagination.pageNum,
pageSize: pagination.pageSize,
},
data: {
startTime: filter.startTime,
endTime: filter.endTime,
},
} const res = await getEventList(req)
return res /**
* 这里假设接口返回的数据格式不是 records 和 total
* 你可以在 getListData 中根据实际情况调整 records 和 total 的赋值,如:
* const res = await getEventList(req);
* return {data: { records: res.data.list, total: res.data.totalSize }}
*/
}const { list, filter, pagination, filterSearch, loadDataPage } = useList<IEvent, typeof filterOption>(filterOption, getListData, { pageNum: 1, pageSize: 20 })
在模板中,配合自定义列表组件 cus-list 即可快速搭建页面。将筛选条件绑定到 filter,分页事件绑定到 loadDataPage,代码变得极其简洁:
<template>
<cus-list v-model:keyword="filter.keyword" :refresh-func="filterSearch" :load-func="loadDataPage" :pagination="pagination" @search="filterSearch" @clear="filterSearch">
<template #serach-left>
<van-dropdown-menu style="--dropdown-menu-title-active-text-color: var(--uni-color-primary);--dropdown-menu-background-color: transparent;--dropdown-menu-title-text-color: var(--uni-text-color); --dropdown-menu-option-active-color: var(--uni-color-primary)" custom-class="min-w-[100rpx]">
<van-dropdown-item v-model:value="filter.siteId" :options="siteIdOptions" @change="onSiteChange" />
van-dropdown-menu>
template> <template #search-filter="{ data }">
<view class="box-border h-full w-full p-2">
<view class="flex items-center gap-2">
<view>开始时间:view>
<cus-date-picker v-model="filter.startTime" type="date" />
view>
<view class="mt-2 flex items-center gap-2">
<view>结束时间:view>
<cus-date-picker v-model="filter.endTime" type="date" />
view>
<view class="mt-5 flex gap-2">
<van-button size="small" block class="flex-1" @click="onFilterReset(data)">
重置
van-button>
<van-button type="primary" block size="small" class="flex-1" @click="onFilter(data)">
确定
van-button>
view>
view>
template> <template #default>
<task-item v-for="item in list" :key="item.id" :record="item" />
template>
cus-list>
template>
整个数据流链路跑下来,数据绑定和事件处理都与Hook完全对齐。代码量减少了一大半,所有逻辑集中在同一处维护,后期修改维护也十分清爽。
