Refine v5 + Mantine 高级表格实战:基于 @refinedev/react-table 实现筛选、排序、行内编辑与批量删除
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
本指南基于 Refine 官方示例 table-mantine-advanced,以
@refinedev/react-table适配器为技术底座,完整讲解如何在 Mantine 项目中搭建一张"生产级"高级数据表格:列筛选(Column Filtering)、列排序(Column Sorting)、行展开预览、行内编辑(Inline Edit)以及批量选择删除(Bulk Delete)。读完本文,你将掌握@refinedev/react-table与 TanStack Table(React Table)的组合模式,并能在自己的 Refine v5 项目中直接复用这些能力。
背景:从基础表格到高级表格
Refine 官方文档将 Mantine 表格示例分为两个层级:
- 基础示例:演示如何用
@refinedev/react-table适配器 + MantineTable组件渲染一个带服务端分页的基础表格; - 高级示例:在基础示例之上,把删除、编辑和筛选三种能力同时放进同一张表格中。
本篇文章聚焦的正是高级示例。它说明了一个核心观点:Refine 允许你借助@refinedev/react-table适配器,直接使用 TanStack Table(原 React Table)的全部特性,同时管理服务端数据操作(分页、排序、筛选都走 Refine data provider 打到后端 API),形成"前端表格交互 + 服务端数据流"的双向同步。
适配器的职责划分
从源码 packages/react-table/src/useTable/index.ts 可以看到useTable返回的对象被明确拆成两部分:
export type UseTableReturnType<TData extends BaseRecord = BaseRecord, ...> = { reactTable: Table<TData>; // TanStack Table 实例 refineCore: useTableReturnTypeCore<TData, TError>; // Refine 核心数据流 };也就是说,@refinedev/react-table的useTable内部同时调用了 Refine Core 的useTableCore(负责数据请求、分页、筛选、排序的"服务端"状态)和 TanStack 的useReactTable(负责表格 UI 的"客户端"状态),并用useEffect把两边状态双向同步:
- 表格的
pagination变化 → 调用setCurrentPage更新 Refine 当前页(见 index.ts); - 表格的
sorting变化 → 转换为CrudSorting并调用setSorters(见 index.ts); - 表格的
columnFilters变化 → 通过columnFiltersToCrudFilters转换为CrudFilter[]并调用setFilters(见 index.ts)。
关键点在于manualSorting与manualFiltering的取值:当refineCoreProps.filters?.mode/sorters?.mode为"server"(默认值)时,适配器会关闭 TanStack 的客户端排序/筛选行模型,把状态交给 Refine 走服务端请求;反之若设为"client",则启用getSortedRowModel/getFilteredRowModel做纯前端处理。这正是"高级表格"里筛选、排序能打到 API 的底层原理。
示例工程速览
示例代码位于 examples/table-mantine-advanced,关键文件结构如下:
examples/table-mantine-advanced/ ├── src/ │ ├── App.tsx # Refine 根组件、路由与主题 │ ├── main.tsx │ ├── components/ │ │ ├── index.ts │ │ └── table/ │ │ ├── columnFilter.tsx # 列筛选弹层组件 │ │ ├── columnSorter.tsx # 列排序图标组件 │ │ └── index.ts │ ├── interfaces/ │ │ └── index.d.ts # IPost / ICategory 类型 │ └── pages/ │ ├── index.ts │ └── posts/ │ ├── index.ts │ └── list.tsx # 高级表格主实现 ├── package.json └── vite.config.ts示例使用@refinedev/simple-rest作为 data provider,数据源为https://api.fake-rest.refine.dev(见 App.tsx)。路由上只注册了一个posts资源(list: "/posts"),并开启了syncWithLocation与warnWhenUnsavedChanges两个全局选项(见 App.tsx):
<Refine routerProvider={routerProvider} dataProvider={dataProvider(API_URL)} notificationProvider={useNotificationProvider} resources={[{ name: "posts", list: "/posts" }]} options={{ syncWithLocation: true, warnWhenUnsavedChanges: true, }} >核心依赖(见 package.json):
@refinedev/core^5.0.12:Refine 核心;@refinedev/react-table^6.0.1:TanStack Table 适配器;@refinedev/mantine^3.0.2:Mantine UI 集成(含List、useForm、SaveButton等);@tanstack/react-table^8.2.6:表格引擎;@uiw/react-md-editor:用于 Markdown 内容的编辑与预览。
数据模型与列定义
示例数据类型定义在 interfaces/index.d.ts:
export interface ICategory { id: number; title: string; } export interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; category: { id: number }; }在 list.tsx 中,列定义通过React.useMemo生成,使用 TanStack Table v8 的ColumnDef<IPost>[]类型。各列要点:
| 列 id | 说明 | 关键配置 |
|---|---|---|
selection | 复选框 + 展开按钮 + 批量删除按钮 | enableSorting: false、enableColumnFilter: false |
id | 主键列 | 默认排序/筛选 |
title | 标题列,参与筛选 | meta: { filterOperator: "contains" } |
category.id | 分类外键列,展示时映射为分类标题 | enableColumnFilter: false,cell中通过table.options.meta读取分类数据 |
createdAt | 创建时间列 | cell用DateField渲染,format="LLL" |
actions | 行内编辑/删除按钮 | enableColumnFilter: false、enableSorting: false |
meta.filterOperator是连接 TanStack 列筛选与 RefineCrudFilter的关键:columnFiltersToCrudFilters会读取列的meta.filterOperator,把"标题包含某关键词"这类语义转换为{"field": "title", "operator": "contains", "value": ...}的服务端筛选条件。不配置时默认使用"eq"。
列排序与列筛选组件
高级表格的表头把排序、筛选做成独立的可复用组件,放在 components/table 下。
ColumnSorter:一键排序
columnSorter.tsx 只做一件事:根据column.getCanSort()判断该列是否可排序,然后渲染一个ActionIcon,点击触发column.getToggleSortingHandler()。图标根据column.getIsSorted()状态切换:
- 未排序 →
IconSelector(中性灰); - 升序
asc→IconChevronDown; - 降序
desc→IconChevronUp。
由于适配器默认manualSorting: true(服务端模式),点击排序图标后,useTable的同步useEffect会把排序状态转换为CrudSorting并写入 Refine 核心,最终由 data provider 生成?sort=title&order=asc之类的查询参数请求后端;同时若非首页则会自动setCurrentPage(1)回到第一页(见 index.ts)。
ColumnFilter:弹层筛选
columnFilter.tsx 是一个 MantineMenu弹层:
- 若
column.getCanFilter()为 false,直接返回null; - 打开时把
column.getFilterValue()存入本地state,在弹层内通过TextInput修改; - 点击"对勾"保存时调用
column.setFilterValue(state.value),点击"叉号"清空时调用column.setFilterValue(undefined); - 若列定义了自定义的
meta.filterElement组件,则优先渲染该组件(renderFilterElement分支),否则回退到TextInput。
保存筛选后,适配器通过columnFiltersToCrudFilters把列筛选转成CrudFilter[]并同步给 Refine 核心,从而触发携带筛选参数的服务端查询。这解释了示例中title列配置filterOperator: "contains"后,输入关键词即能实现"模糊查询"的原因。
主列表页:组装高级表格
主实现集中在 list.tsx。下面按功能拆解。
useTable 的接入
const { reactTable: { setOptions, getAllColumns, getHeaderGroups, getRowModel, resetRowSelection, }, refineCore: { tableQuery: { data: tableData }, setCurrentPage: setCurrent, pageCount, currentPage: current, }, } = useTable<IPost>({ columns, getRowId: (originalRow) => originalRow.id.toString(), });这里getRowId用id的字符串形式作为行唯一标识,是 TanStack 行选择(row selection)能正确工作的前提。表格渲染时:
- 表头遍历
getHeaderGroups(),对每个 header 渲染flexRender(header.column.columnDef.header, header.getContext()),并附加ColumnSorter与ColumnFilter; - 表体遍历
getRowModel().rows,每个单元格通过flexRender(cell.column.columnDef.cell, cell.getContext())渲染; - 底部用 Mantine
Pagination组件对接 Refine 的分页状态:total={pageCount}、page={current}、onChange={setCurrent}。
关联数据:分类标题的两种获取方式
由于IPost.category只存了id,表格展示时需要把分类 id 映射为分类标题。示例展示了两种数据获取方式:
- 行渲染用
useMany:从当前页tableData.data收集category.id集合,调用useMany批量拉取(见 list.tsx),再通过setOptions把结果塞进table.options.meta,供category.id列的cell读取:const categoryIds = tableData?.data?.map((item) => item.category.id) ?? []; const { result: categoriesData } = useMany<ICategory>({ resource: "categories", ids: categoryIds, queryOptions: { enabled: categoryIds.length > 0 }, }); setOptions((prev) => ({ ...prev, meta: { ...prev.meta, categoriesData }, })); - 行内编辑下拉用
useSelect:行内编辑的分类下拉框需要完整分类列表,因此用useSelect(resource: "categories"、pagination: { mode: "server" })拉取,并把selectProps展开到 MantineSelect上。
行展开:Markdown 内容预览
selection列的第二格是一个展开按钮,点击调用row.toggleExpanded();当row.getIsExpanded()为 true 时,在数据行下方插入一个colSpan等于可见列数的<tr>,用@uiw/react-md-editor以preview="preview"模式渲染row.original.content(见 list.tsx)。这样不必进入详情页即可预览长文本内容。
行内编辑:useForm 驱动
行内编辑通过 Refine Mantine 的useForm实现:
const { refineCore: { id, setId }, getInputProps, saveButtonProps, } = useForm<IPost>({ refineCoreProps: { redirect: false, // 保存后不跳转 action: "edit", // 复用编辑动作 }, initialValues: { title: "", category: { id: "" }, content: "" }, });在表体渲染时,若当前行的id等于useForm的id,则渲染renderEditRow(row)——一个包含TextInput(标题)、Select(分类)、SaveButton与Cancel按钮的编辑行;Cancel按钮通过setId(undefined)退出编辑态(见 list.tsx)。触发编辑入口是actions列里的EditButton:
<EditButton hideText onClick={() => { setId(getValue() as number); }} />这里EditButton不再走路由跳转,而是把行 id 写入useForm的id,实现"就地编辑"。配合App.tsx里的warnWhenUnsavedChanges: true,未保存时切换行会得到未保存变更提醒。
批量删除:useDeleteMany
表头复选框与行复选框通过 TanStack 的行选择 API 联动:
<Checkbox checked={table.getIsAllRowsSelected()} indeterminate={table.getIsSomeRowsSelected()} onChange={table.getToggleAllRowsSelectedHandler()} />当有部分行被选中时,表头出现红色Delete按钮,点击后取出table.getSelectedRowModel().flatRows的 id 列表,交给useDeleteMany:
const { mutate } = useDeleteMany<IPost>(); const deleteSelectedItems = (ids: number[]) => { mutate( { resource: "posts", ids }, { onSuccess: () => { resetRowSelection(); } }, ); };删除成功后调用resetRowSelection()清空选择状态(见 list.tsx)。useDeleteMany会按 data provider 的deleteMany实现发出批量删除请求,并自动刷新列表数据。
运行示例
在仓库根目录使用 pnpm 安装依赖并启动开发服务器:
pnpm install pnpm --filter table-mantine-advanced dev示例本身也在package.json中提供了dev/build/start脚本(refine dev/refine build/refine start)。Node 版本要求>=20。数据来自公开的 fake REST API(https://api.fake-rest.refine.dev),无需本地后端即可体验完整的服务端分页、排序与筛选。
小结
通过@refinedev/react-table适配器,Refine 把 TanStack Table 的客户端交互能力与 Refine 的服务端数据流无缝衔接:
- 列排序:TanStack 排序状态 →
CrudSorting→ data provider 查询参数; - 列筛选:列
meta.filterOperator+ 筛选值 →CrudFilter[]→ 服务端过滤; - 行内编辑:
useForm(action: "edit")+setId就地切换编辑行,无需路由跳转; - 批量删除:TanStack 行选择 +
useDeleteMany+resetRowSelection形成完整闭环。
高级示例的全部实现细节都可在 examples/table-mantine-advanced 中查看,适配器的状态同步逻辑则见 packages/react-table/src/useTable/index.ts。以此为模板,你可以快速为自己的管理后台构建带筛选、排序、行内编辑和批量操作的 Mantine 数据表格。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考