【配置化 CRUD 02】 useTable Hooks:列表查询、分页封装
一:前言
在业务的CRUD开发体系中 ,几乎所有业务模块都需要实现列表查询与分页功能,你是不是经常会看到类似代码:
// 1.每个页面都要单独引入接口
import { getListApi } from '@/api/user'
// 2. 定义分页列表参数(每个列表页都要写一遍)
const current = ref(1)
const pageSize = ref(10)
const total = ref(0)
const tableData = ref([])
const loading = ref(false)
const searchParams = ref({})
// 3. 重复拼接请求参数(分页+搜索)
const getListData = async () => {
loading.value = true
try {
const params = {
current: current.value,
pageSize: pageSize.value,
...searchParams.value
}
const res = await getListApi(params)
// 重复解析响应数据(每个页面可能还要适配不同后端格式)
tableData.value = res.data.list
total.value = res.data.total
}catch (error) {
console.error('列表查询失败:', error) }
finally {
loading.value = false
}
}
// 4. 重复编写分页联动监听
watch([current, pageSize], async () => { await getListData() },
{ immediate: true })
...
类似这样的代码,每新增一个列表页就需要几乎完整复制一遍,基于此,我们针对性封装 useTable Hooks,延续配置化 CRUD体系「通用逻辑内聚、业务逻辑外溢」的设计原则,将列表查询请求、分页参数管理、加载状态控制、查询与分页联动等通用逻辑全部抽离,支持自定义请求接口、灵活配置分页参数...,可联动专栏前一期的搜索重置组件,这样在实现列表页时,无需关注底层的查询与分页逻辑,只需传入简单配置,即可快速实现列表查询与分页功能,专注于业务本身的表单配置与数据展示。
文章指路(一起食用,味道更加):
1.动态表单深度实践: juejin.cn/post/757946… ;
2.搜索重置组件:juejin.cn/post/760056… ;
二:useTable hooks实现
首先明确useTable Hooks的实现目标,所有代码实现均围绕目标展开:
1.冗余逻辑抽离:将分页参数定义、列表查询请求、分页联动、搜索联动、表格操作等通用逻辑全部内聚;
2.配置化适配:支持通过入参灵活配置请求接口、响应数据格式、分页参数、默认参数等,适配不同业务模块、不同后端接口规范;
3.联动兼容:支持与前一期封装的搜索重置组件联动,同时适配表格组件,提供实例注册、选中行获取、表格列设置等方法,实现全流程联动;
...
接下来我们开始实现:
2.1.useTable Hooks Props类型约束
interface UseTableConfig<T = any> {
// 请求Url 或函数
getListApi: string | Function
// 返回数据格式配置 -- 可指定对应列表和总数的对应字段
response?: {
list: string
total?: string
}
// 接收默认参数, 如 pageType:"testPage",不参与响应式更新!
initParams?: Object
// 搜索组件默认参数,注意跟搜索组件初始值保持一致,不参与响应式更新!
searchInitParams?: Object
// 针对table的相关配置
tableObject?: Object
}
2.2 数据状态:统一维护表格、搜索组件、初始参数等相关状态
// 表格组件实例引用,用于调用表格内部方法
const tableRef = ref(null)
// 接收搜索组件传递的实时参数,参与响应式更新!
const searchParamObj = ref({})
// 表格相关
const tableObjects = ref({
current: 1, // 默认当前页为1
pageSize: 10, // 默认页大小为10
pageSizeOptions: ['5', '10', '15', '20'], // 默认分页大小可选值
total: 0, // 数据总数,初始为0
tableData: [], // 表格渲染数据,初始为空数组
// 允许外部传入覆盖默认配置
...tableObject,
loading: false, // 表格加载状态,控制加载动画
})
// 利用`computed`计算属性自动组装请求参数
const parmsObj = computed(() => {
return {
current: tableObjects.value.current, // 分页-当前页
pageSize: tableObjects.value.pageSize, // 分页-页大小
...initParams, // 固定参数(不响应式)
...searchInitParams, // 搜索默认参数(不响应式)
...searchParamObj.value, // 搜索实时参数(响应式)
}
})
2.3 核心方法:封装查询、表格操作与搜索联动逻辑
2.3.1 register:注册表格ref
// 注册表格ref
const register = (ref: any) => {
tableRef.value = ref
}
// 获取表格实例
const getTable = () => {
return tableRef.value
}
2.3.2 getListData : 获取列表数据
const getListData = async () => {
tableObjects.value.loading = true
console.log('getListData::列表调用参数:', parmsObj.value)
try {
const res = await Http({
method,
url:getListApi,
params:parmsObj.value
})
tableObjects.value.total = res[response?.total || 'total']
tableObjects.value.tableData = res[response?.list || 'list']
tableObjects.value.loading = false
} catch (error) {
message.error(error)
}
}
2.3.3 setSearchParams : 搜索联动
const setSearchParams = (data: any, getList = true, isReset = false) => {
Object.assign(searchParamObj.value, data)
if (isReset) {
// 重置
searchParamObj.value = {}
}
// 是否获取数据
getList && getListData()
}
2.3.4 setRowChecked : 设置列选择
const setRowChecked = (row: any) => {
const table = getTable() as any
table && table.toggleRowSelection(row, true)
}
...
2.4 联动监听:分页参数变化自动触发查询
// 监听当前页变化,自动刷新列表
watch(() => tableObjects.value.current, async () => {
getListData()
}, { immediate: true, deep: true })
// 监听页大小变化,自动刷新列表
watch(() => tableObjects.value.pageSize, async () => {
getListData()
}, { immediate: true, deep: true })
2.5 API统一暴露:对外提供简洁易用的调用接口
return {
// 核心方法集合,供外部调用
methods: {
getListData, // 列表查询
getTable, // 获取表格实例
setRowChecked, // 列选择
setSearchParams, // 设置查询参数
},
tableRef, // 表格实例引用(可选暴露,供外部手动操作)
register, // 表格实例注册方法(表格组件必须调用)
tableObject: tableObjects.value, // 分页与表格核心状态(响应式,供表格组件绑定)
}
2.6 完整代码
import { ref, watch, computed } from 'vue'
interface UseTableConfig<T = any> {
getListApi: string | Function
// 请求方式
method?: 'get' | 'post'
//返回数据格式配置 -- 可指定对应列表和总数的取值
response?: {
list: string
total?: string
}
initParams?: Object //接收默认参数, 如 pageType:"testPage",不参与响应式更新!
searchInitParams?: Object //搜索组件默认参数,注意跟搜索组件初始值保持一致,不参与响应式更新!
tableObject?: Object // 针对table的相关配置
}
export const useTable = (
config: UseTableConfig
) => {
const { method = 'get',getListApi, response = {}, initParams = {}, tableObject = {}, searchInitParams = {} } = config
const tableRef = ref(null)
// 接收搜索组件传递的实时参数,参与响应式更新!
const searchParamObj = ref({})
//表格配置
const tableObjects = ref({
current: 1, // 当前页
pageSize: 10, // 页限制
pageSizeOptions: ['5', '10', '15', '20'], // 分页配置
total: 0, // 总数
tableData: [], // 表格数据
...tableObject,
loading: false, //表格加载状态
})
const parmsObj = computed(() => {
return {
current: tableObjects.value.current,
pageSize: tableObjects.value.pageSize,
...initParams, // 固定限制参数
...searchInitParams, // 搜索默认参数(不响应式)
...searchParamObj.value, // 搜索实时参数(响应式)
}
})
const getListData = async () => {
tableObjects.value.loading = true
console.log('getListData::列表调用参数:', parmsObj.value)
try {
const res = await Http({
method,
url:getListApi,
params:parmsObj.value
})
tableObjects.value.total = res[response?.total || 'total']
tableObjects.value.tableData = res[response?.list || 'list']
tableObjects.value.loading = false
} catch (error) {
message.error(error)
}
}
//统一拿到table的Ref
const getTable = () => {
return tableRef.value
}
//获取表格选中的列
const getSelections = async () => {
const table = getTable() as any
return table.getSelectionRows()
}
//从外界直接传入的表格列参数
const setcolumns = (columns: any) => {
const table = getTable() as any
table && table?.setcolumns(columns)
}
//设置列选择
const setRowChecked = (row: any) => {
const table = getTable() as any
table && table.toggleRowSelection(row, true)
}
//注册表格ref
const register = (ref: any) => {
tableRef.value = ref
}
// 支持从外面设置搜索参数,并查询
const setSearchParams = (data: any, getList = true, isReset = false) => {
Object.assign(searchParamObj.value, data)
if (isReset) {
searchParamObj.value = {}
}
getList && getListData()
}
//检测到分页变化 重新获取数据
watch(() => tableObjects.value.current, async () => {
getListData()
},
{ immediate: true, deep: true })
//检测到分页变化 重新获取数据
watch(() => tableObjects.value.pageSize, async () => {
getListData()
}, { immediate: true, deep: true })
return {
methods: {
getListData,
getTable,
getSelections,
setcolumns,
setRowChecked,
setSearchParams,
},
tableRef,
register,
tableObject: tableObjects.value,
}
}
三: 实战使用(useTable结合搜索、表格组件)
页面引入Hooks使用:
const { methods, register, tableObject } = useTable({
getListApi: 'test/list',
initParams: {
pageType:"testPage"
},
searchInitParams:{
age:10,
name:"初始化名字"
}
})
const { getListData, setSearchParams } = methods
getListData()
搜索组件联动:search和reset方法改造
const searchColumn = [
{
label: '姓名',
prop: 'name',
initValue: '初始化名字',
component: 'Input',
componentProps: {
onInput: (e: any) => {
console.log('姓名输入框输入事件', e)
searchRef.value?.setSchemas([
{
prop: 'age',
path: 'componentProps.placeholder',
value: `请输入${e}的年龄`
}
])
}
}
},
{
label: '年龄',
prop: 'age',
component: 'Input',
initValue: 10,
formItemProps: {
rules:[
{
required: true,
message: '请输入年龄',
trigger: 'blur'
}
]
}
},
{
label: '上学阶段',
prop: 'jieduan',
component: 'Select',
componentProps: {
options: [
{
label: '幼儿园',
value: 1
},
{
label: '其他阶段',
value: 2
}
]
}
},
]
// 改动前:
<Search
ref="searchRef"
:schema="searchColumn"
@search="(params) => console.log('点击查询:',{params})"
:showReset="false"
:isVaildSearch="true"
>
</Search>
// 改动后:
<Search
ref="searchRef"
:schema="searchColumn"
@search="setSearchParams"
:showReset="false"
:isVaildSearch="true"
>
</Search>
表格组件支持:提供register方法、及相关props接收
const tablecolumns = [
{
label: '姓名',
prop: 'name',
minWidth: '140px',
},
{
label: '年龄',
prop: 'age',
minWidth: '140px',
},
{
label: '上学阶段',
prop: 'jieduan',
minWidth: '140px',
}
]
<Comtable
v-model:current="tableObject.current"
v-model:pageSize="tableObject.pageSize"
:rowKey="'id'"
:columns="tablecolumns"
:dataList="tableObject.tableData"
:pagination="tableObject"
:loading="tableObject.loading"
@register="register"
>
<template #operation="{ row }">
<el-button
type="primary"
size="small"
link
@click="editClick(row)"
>{{$t('Edit')}}</el-button
>
</template>
</Comtable>
运行截图:
初始状态:
输入搜索:
分页变化:
以上就是useTable Hooks 的封装过程以及实战案例~