游乐游手机版
首页/前端开发/文章详情

useList通用列表管理Hook实现与使用

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

useList:Vue3通用列表管理Hook,轻松搞定分页与筛选

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

useList 通用列表管理hook

核心功能:一句话帮你省掉大量重复代码

这个Hook到底能帮你减少多少重复工作?简单列举几条核心能力:

  1. 响应式状态管理:通过 reactive 将筛选条件和分页配置包裹起来,修改一处即可全局联动,无需手动同步刷新。
  2. 数据请求封装:loading加载状态、错误捕获、数据赋值一气呵成,组件中再也不用手写 try catch 了。
  3. 追加/替换模式:借助 isPush 参数,既能支持常规分页(翻页直接替换数据),也能轻松实现滚动加载(持续追加数据)。
  4. 筛选重置:保存初始筛选条件,一键恢复至最原始状态,避免用户手动逐一清空。

详细代码与核心实现

直接上代码,关键位置已添加注释,仔细阅读即可轻松掌握:

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之后,你将获得以下可直接使用的属性和方法:

属性类型说明
listRef>当前列表数据(响应式)
loadingRef数据加载状态
filterReactive筛选条件对象(响应式,直接绑定表单)
paginationReactive分页信息(响应式,自动更新总条数)
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完全对齐。代码量减少了一大半,所有逻辑集中在同一处维护,后期修改维护也十分清爽。

来源:https://juejin.cn/post/7644115167692816427
上一篇GoWind Admin基于Element Plus重塑中后台CRUD开发范式 下一篇用MCP与一句话生成完整CRUD页面
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
checked表单属性与CSS变量实现换肤原理
前端开发 · 2026-07-02

checked表单属性与CSS变量实现换肤原理

先聊一个有意思的现象:不需要编写任何 JavaScript,仅靠一个 :checked 伪类,就能驱动整个主题切换系统。听起来很神奇,但原理其实并不复杂——核心在于,:checked 是浏览器原生状态的实时镜像,而不是 JS 模拟出来的开关。 用户点击 ,或者用键盘空格键选中它,状态更新的那一刻,C

HTML meta标签页面定时跳转实现
前端开发 · 2026-07-02

HTML meta标签页面定时跳转实现

说到前端开发中最简洁的页面跳转方式,meta http-equiv= "refresh " 绝对算得上一个经典方案。不过别看它结构简单,格式上稍有疏忽,页面就可能原地卡死,或者直接跳到一个错误地址。下面把几个最容易踩坑的细节彻底讲清楚,帮你避开这些常见陷阱。 使用 http-equiv= "refresh

Cypress跨测试用例状态传递的不推荐但可选方案
前端开发 · 2026-07-02

Cypress跨测试用例状态传递的不推荐但可选方案

Cypress 默认的设计哲学很干脆:每个测试用例都必须是独立小王国,谁也不靠谁。这意味着 it() 执行前,浏览器上下文会被“一键还原”——页面状态、LocalStorage、Cookies 统统清空,强制维护测试隔离。这一规则让很多新手头疼:明明前一个测试已经创建了员工,后一个测试怎么就没法直接

全面深度解析HTML主体main标签唯一性原则与使用规范
前端开发 · 2026-07-02

全面深度解析HTML主体main标签唯一性原则与使用规范

在进行前端无障碍审计时,不少开发者会遇到一个奇怪的场景:浏览器不报错,但Lighthouse却直接标红“duplicate-main”。这其实是语义层与渲染层之间的根本差异。 为什么浏览器不报错但 Lighthouse 直接标红 duplicate-main 关键原因就在于:`main` 是语义锚点

HTML main标签在文档结构中的唯一性详解
前端开发 · 2026-07-02

HTML main标签在文档结构中的唯一性详解

先做一个快速检测:打开你最近开发的一个页面,按下 Ctrl+F 搜索 。如果搜索结果里出现2个以上,那这篇文章建议你认真读完。 本期要聊的主题,是HTML标签中一个看似简单、实际极易踩坑的核心知识点:main标签的唯一性。很多开发者知道这个标签的存在,但真正写到项目里,尤其是用了React、Vue这