一:前言

在业务的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 的封装过程以及实战案例~

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com