Skip to content

useTable 函数

useTable 是一个用于管理表格数据、列和加载状态的 Vue Hook。它提供了一种灵活的方式来处理数据获取、分页、列可见性等常见表格功能。

本指南将介绍如何使用最新的 useTable,以及其针对 Naive UI 的封装 useNaiveTableuseNaivePaginatedTable

快速对比

  • useTable:不绑定任何 UI 库,仅处理请求、数据转换、列配置与列显隐(checks)管理。
  • useNaiveTable:在 useTable 基础上,适配 Naive UI 的列定义,提供 scrollX,并在 i18n 切换时自动刷新列。
  • useNaivePaginatedTable:在 useNaiveTable 基础上集成分页(PaginationProps)、移动端分页 mobilePaginationgetDataByPage 等。
  • useTableOperate:围绕“新增/编辑/批量删除/单删”的通用 UI 状态和回调封装。
  • defaultTransform:把后端统一分页结构转换为 PaginationData<T>

useTable

useTable 只关注“数据驱动”,不关心具体表格组件/UI 库,它提供了灵活的选项来配置数据获取、转换和列管理。

函数签名

typescript
export default function useTable<ResponseData, ApiData, Column, Pagination extends boolean>(
  options: UseTableOptions<ResponseData, ApiData, Column, Pagination>
);

UseTableOptions 接口

typescript
export interface UseTableOptions<ResponseData, ApiData, Column, Pagination extends boolean> {
  /**
   * 获取表格数据的 API 函数
   */
  api: () => Promise<ResponseData>;
  /**
   * 是否启用分页
   */
  pagination?: Pagination;
  /**
   * 将 API 响应转换为表格数据的函数
   */
  transform: Transform<ResponseData, ApiData, Pagination>;
  /**
   * 列定义的工厂函数
   */
  columns: () => Column[];
  /**
   * 获取列检查项的函数
   */
  getColumnChecks: (columns: Column[]) => TableColumnCheck[];
  /**
   * 根据检查项获取最终列的函数
   */
  getColumns: (columns: Column[], checks: TableColumnCheck[]) => Column[];
  /**
   * 数据获取完成后的回调函数
   */
  onFetched?: (data: GetApiData<ApiData, Pagination>) => void | Promise<void>;
  /**
   * 是否立即获取数据
   *
   * @default true
   */
  immediate?: boolean;
}

返回值

ts
{
    loading: Ref<boolean, boolean>;
    empty: Ref<boolean, boolean>;
    data: Ref<ApiData[], ApiData[]>;
    columns: ComputedRef<Column[]>;
    columnChecks: Ref<TableColumnCheck[], TableColumnCheck[]>;
    reloadColumns: () => void;
    getData: () => Promise<void>;
}

说明

  • columnChecks 决定列的显示与隐藏;columns() 是“工厂函数”(每次取值都会重新生成列定义)。
  • reloadColumns 在不丢失“勾选状态”的前提下,按当前工厂函数输出重建列(例如语言切换后标题变化时调用)。
  • paginationtrue 时,transform 返回 PaginationData<ApiData>useTable 会把其中的 data 用作表格数据。

使用示例

typescript
import { useTable } from '@sa/hooks';
import type { UseTableOptions } from '@sa/hooks';
import type { PaginationData } from '@sa/hooks';
import type { DataTableColumns } from 'naive-ui';

interface User {
  id: number;
  name: string;
  email: string;
}

interface UserResponse {
  data: User[];
  total: number;
}

const { loading, data, columns, getData } = useTable<UserResponse, User, DataTableColumns<User>, false>({
  api: fetchUsers, // 一个返回 Promise<UserResponse> 的函数
  transform: response => response.data,
  columns: () => [
    {
      key: 'id',
      title: 'ID'
    },
    {
      key: 'name',
      title: 'Name'
    },
    {
      key: 'email',
      title: 'Email'
    }
  ],
  getColumnChecks: cols =>
    cols.map(col => ({ key: col.key as string, title: col.title!, checked: true, visible: true })),
  getColumns: (cols, checks) => cols.filter(col => checks.find(c => c.key === col.key)?.checked)
});

// 获取数据
getData();

useNaiveTable

useNaiveTable 是对 useTable 的 Naive UI 版本封装,使用 NaiveUI.TableColumn<T>,并提供横向滚动宽度 scrollX,内置 i18n 变更时的列刷新。

额外选项:

  • getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean
    • 控制列是否出现在“列显隐面板”(例如 selection/expand 列可选择不显示在面板中)。

额外返回:

  • scrollX: ComputedRef<number>(根据 width/minWidth 汇总,便于 NDataTable 横向滚动)。

说明:

  • 不再需要你提供 getColumnChecksgetColumns,封装内部已适配 Naive UI 的列显隐处理。
  • 内部会为 selection/expand 这类无 key 的列生成内部 key 以参与显隐控制。

函数签名

typescript
export function useNaiveTable<ResponseData, ApiData>(options: UseNaiveTableOptions<ResponseData, ApiData, false>);

UseNaiveTableOptions 接口

typescript
export type UseNaiveTableOptions<ResponseData, ApiData, Pagination extends boolean> = Omit<
  UseTableOptions<ResponseData, ApiData, NaiveUI.TableColumn<ApiData>, Pagination>,
  'pagination' | 'getColumnChecks' | 'getColumns'
> & {
  /**
   * get column visible
   *
   * @param column
   *
   * @default true
   *
   * @returns true if the column is visible, false otherwise
   */
  getColumnVisible?: (column: NaiveUI.TableColumn<ApiData>) => boolean;
};

使用示例

typescript
import { useNaiveTable } from '@/hooks/common/table';

/** get user list */
function fetchGetUserList(params?: Api.SystemManage.UserSearchParams) {
  return request<Api.SystemManage.UserList>({
    url: '/systemManage/getUserList',
    method: 'get',
    params
  });
}

const searchParams: Api.SystemManage.UserSearchParams = reactive({
  current: 1,
  size: 999,
  status: null,
  userName: null,
  userGender: null,
  nickName: null,
  userPhone: null,
  userEmail: null
});

const { loading, data, columns, getData, scrollX } = useNaiveTable({
  api: () => fetchGetUserList(),
  transform: response => {
    const { data: list, error } = response;

    if (!error) {
      return list.records;
    }

    return [];
  },
  columns
});

// 获取数据
getData();

注意:fetchGetUserList 需要明确的返回类型,useNaiveTable 可以不传递泛型参数,直接推导出类型。

useNaivePaginatedTable

useNaivePaginatedTable 是针对需要分页的 Naive UI DataTable 组件的封装。

函数签名

typescript
export function useNaivePaginatedTable<ResponseData, ApiData>(
  options: UseNaivePaginatedTableOptions<ResponseData, ApiData>
);

UseNaivePaginatedTableOptions 接口

typescript
type UseNaivePaginatedTableOptions<ResponseData, ApiData> = UseNaiveTableOptions<ResponseData, ApiData, true> & {
  paginationProps?: Omit<PaginationProps, 'page' | 'pageSize' | 'itemCount'>;
  /**
   * whether to show the total count of the table
   *
   * @default true
   */
  showTotal?: boolean;
  onPaginationParamsChange?: (params: PaginationParams) => void | Promise<void>;
};

使用示例

typescript
import { defaultTransform, useNaivePaginatedTable } from '@/hooks/common/table';

/** get role list */
function fetchGetRoleList(params?: Api.SystemManage.RoleSearchParams) {
  return request<Api.SystemManage.RoleList>({
    url: '/systemManage/getRoleList',
    method: 'get',
    params
  });
}

const searchParams: Api.SystemManage.RoleSearchParams = reactive({
  current: 1,
  size: 10,
  roleName: null,
  roleCode: null,
  status: null
});

const { loading, data, columns, pagination, getDataByPage } = useNaivePaginatedTable({
  api: fetchGetRoleList,
  transform: response => defaultTransform(response),
  onPaginationParamsChange: ({ page, pageSize }) => {
    // 把分页参数同步到搜索参数(关键)
    searchParams.current = page;
    searchParams.size = pageSize;
  },
  columns: () => [
    {
      type: 'selection',
      align: 'center',
      width: 48
    },
    {
      key: 'index',
      title: $t('common.index'),
      width: 64,
      align: 'center',
      render: (_, index) => index + 1
    },
    {
      key: 'roleName',
      title: $t('page.manage.role.roleName'),
      align: 'center',
      minWidth: 120
    }
    // ...其他列
  ]
});

// 常用的增删改查辅助状态和方法
const {
  drawerVisible,
  operateType,
  editingData,
  handleAdd,
  handleEdit,
  checkedRowKeys,
  onBatchDeleted,
  onDeleted
  // ...其他方法
} = useTableOperate(data, 'id', getData);

useTableOperate(表格操作辅助)

封装“新增/编辑/批量删除/单删”的 UI 状态与回调,配合抽屉/弹窗使用。

签名:

ts
export function useTableOperate<TableData>(
  data: Ref<TableData[]>,
  idKey: keyof TableData,
  getData: () => Promise<void>
);

入参:

typescript

{
  data: Ref<TableData[], TableData[]>, // 表格当前数据(编辑时根据 `id` 定位行数据)。
  idKey: keyof TableData, // 主键字段名(如 `id`)。
  getData: () => Promise<void> // 删除后的刷新函数。
}

返回:

ts
{
    drawerVisible: Ref<boolean>;
    openDrawer: () => void;
    closeDrawer: () => void;
    operateType: ShallowRef<NaiveUI.TableOperateType>;
    handleAdd: () => void;
    editingData: ShallowRef<TableData | null>;
    handleEdit: (id: TableData[keyof TableData]) => void;
    checkedRowKeys: ShallowRef<string[]>;
    onBatchDeleted: () => Promise<void>; // (批量删除成功后调用:清空勾选 + 刷新)
    onDeleted: () => Promise<void>; // (单项删除成功后调用:刷新)
}

说明:

  • handleEdit(id) 会在 data 中查找该行并打开抽屉,将行数据赋给 editingData
  • 删除类操作成功后调用 onBatchDeleted/onDeleted 即可处理状态并刷新。

defaultTransform(统一分页数据转换)

如果接口返回结构为:

  • FlatResponseData<any, Api.Common.PaginatingQueryRecord<ApiData>>
  • data 通常包含:recordscurrentsizetotal

可直接使用 defaultTransform 将其转换为 PaginationData<ApiData>

如果接口返回的结构有其他差异,可自行编写类似于 defaultTransform 的转换函数传给 transform 参数,请务必明确返回类型。

列显隐与横向滚动

  • 列显隐面板:columnChecks 即“可见列”集合,结合 v-model:columns 绑定到你的“列表头操作”组件(如 TableHeaderOperation)。
  • Naive 适配中 selection/expand 列也会参与显隐(内部有稳定 key)。
  • 横向滚动:scrollX = ∑(column.width || column.minWidth || 120)。建议为列设置 width/minWidth,否则采用默认最小宽度 120。

i18n 与列刷新

  • useNaiveTable/useNaivePaginatedTable 内部会监听 appStore.locale 并触发 reloadColumns(),保证标题 $t(...) 在语言切换后即时更新。
  • useTable 场景若使用 i18n,需要自行调用 reloadColumns()

与示例页面对照

以“用户管理/角色管理”页面为例(src/views/manage/user/index.vuesrc/views/manage/role/index.vue):

  • 通过 useNaivePaginatedTable 提供 columnscolumnChecksdataloadinggetDatagetDataByPagemobilePaginationscrollX
  • TableHeaderOperation 中使用 v-model:columns="columnChecks" 控制“列显隐”。
  • NDataTable
    • 绑定 :columns:data:loading:scroll-x="scrollX"
    • remote
    • :row-key="row => row.id"
    • :pagination="mobilePagination"
  • useTableOperate 管理新增/编辑抽屉与删除后的刷新。

常见问题与最佳实践

  • 何时使用 getDataByPage(1)?当筛选条件变化时,从第一页拉取。
  • 忘记同步页码?务必在 onPaginationParamsChange 中把 page/pageSize 同步回搜索参数。
  • immediate 默认 true:初始化会拉一次数据;如不需要,可传 immediate: false 并手动调用 getData()
  • NDataTablerow-key 必须稳定,确保选择、展开等功能正常。
  • 移动端优化:使用 mobilePagination,小屏更合适的分页展示。

根据 MIT 许可证发布