news 2026/9/12 3:02:37

Refine v5 + Mantine 高级表格实战:基于 @refinedev/react-table 实现筛选、排序、行内编辑与批量删除

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Refine v5 + Mantine 高级表格实战:基于 @refinedev/react-table 实现筛选、排序、行内编辑与批量删除

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-tableuseTable内部同时调用了 Refine Core 的useTableCore(负责数据请求、分页、筛选、排序的"服务端"状态)和 TanStack 的useReactTable(负责表格 UI 的"客户端"状态),并用useEffect把两边状态双向同步:

  • 表格的pagination变化 → 调用setCurrentPage更新 Refine 当前页(见 index.ts);
  • 表格的sorting变化 → 转换为CrudSorting并调用setSorters(见 index.ts);
  • 表格的columnFilters变化 → 通过columnFiltersToCrudFilters转换为CrudFilter[]并调用setFilters(见 index.ts)。

关键点在于manualSortingmanualFiltering的取值:当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"),并开启了syncWithLocationwarnWhenUnsavedChanges两个全局选项(见 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 集成(含ListuseFormSaveButton等);
  • @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: falseenableColumnFilter: false
id主键列默认排序/筛选
title标题列,参与筛选meta: { filterOperator: "contains" }
category.id分类外键列,展示时映射为分类标题enableColumnFilter: falsecell中通过table.options.meta读取分类数据
createdAt创建时间列cellDateField渲染,format="LLL"
actions行内编辑/删除按钮enableColumnFilter: falseenableSorting: 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(中性灰);
  • 升序ascIconChevronDown
  • 降序descIconChevronUp

由于适配器默认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(), });

这里getRowIdid的字符串形式作为行唯一标识,是 TanStack 行选择(row selection)能正确工作的前提。表格渲染时:

  • 表头遍历getHeaderGroups(),对每个 header 渲染flexRender(header.column.columnDef.header, header.getContext()),并附加ColumnSorterColumnFilter
  • 表体遍历getRowModel().rows,每个单元格通过flexRender(cell.column.columnDef.cell, cell.getContext())渲染;
  • 底部用 MantinePagination组件对接 Refine 的分页状态:total={pageCount}page={current}onChange={setCurrent}

关联数据:分类标题的两种获取方式

由于IPost.category只存了id,表格展示时需要把分类 id 映射为分类标题。示例展示了两种数据获取方式:

  1. 行渲染用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 }, }));
  2. 行内编辑下拉用useSelect:行内编辑的分类下拉框需要完整分类列表,因此用useSelectresource: "categories"pagination: { mode: "server" })拉取,并把selectProps展开到 MantineSelect上。

行展开:Markdown 内容预览

selection列的第二格是一个展开按钮,点击调用row.toggleExpanded();当row.getIsExpanded()为 true 时,在数据行下方插入一个colSpan等于可见列数的<tr>,用@uiw/react-md-editorpreview="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等于useFormid,则渲染renderEditRow(row)——一个包含TextInput(标题)、Select(分类)、SaveButtonCancel按钮的编辑行;Cancel按钮通过setId(undefined)退出编辑态(见 list.tsx)。触发编辑入口是actions列里的EditButton

<EditButton hideText onClick={() => { setId(getValue() as number); }} />

这里EditButton不再走路由跳转,而是把行 id 写入useFormid,实现"就地编辑"。配合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 的服务端数据流无缝衔接:

  1. 列排序:TanStack 排序状态 →CrudSorting→ data provider 查询参数;
  2. 列筛选:列meta.filterOperator+ 筛选值 →CrudFilter[]→ 服务端过滤;
  3. 行内编辑useForm(action: "edit")+setId就地切换编辑行,无需路由跳转;
  4. 批量删除: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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 3:02:05

打造STM32舵机库:STS3215串口帧协议与半双工总线控制

简介&#xff1a;针对飞特舵机STS3215的控制库文件&#xff0c;面向机器人、无人机及其他自动化设备开发者&#xff0c;意在简化舵机驱动开发与底层通信管理。压缩包共4个文件&#xff0c;包含2个C源码和2个头文件&#xff1a;源码负责协议解析、指令发送等功能实现&#xff0c…

作者头像 李华
网站建设 2026/9/12 2:59:44

MLflow+BentoML实现模型全生命周期管理:从实验到容器化部署

我做了三年多算法&#xff0c;也带过模型上线的小组&#xff0c;最大的感受是&#xff1a;模型本身从来不是瓶颈&#xff0c;瓶颈在于“模型从训练完到真正提供服务”这段路。多少人遇到过这种场景&#xff1a;同事把一个模型文件起名叫model_v2_final_0815(2).pkl放进网盘&…

作者头像 李华
网站建设 2026/9/12 2:59:18

电力市场联合清算:MISOCP优化模型与应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 2:59:17

研究问题怎么提?把问句按描述、关系、因果、机制四个层位逐层拆解

研究问题提不明确&#xff0c;卡点常不在「不会写」&#xff0c;而在手里那句问句站错了层位。下面把研究问题拆成描述、关系、因果、机制四个层位&#xff0c;先认出问句在哪一层&#xff0c;再给往上走还是停下的判据&#xff0c;让问题层次站稳、问题拆解有落点。免费智能大…

作者头像 李华
网站建设 2026/9/12 2:59:10

无人机降落伞Simulink建模:从阻力方程到Stateflow状态机

简介&#xff1a;面向高校电子信息工程、计算机及数学类专业学生&#xff0c;这份无人机降落伞 Simulink 模型资源专注于无人机应急降落伞系统的建模与仿真&#xff0c;可直接用于课程设计、期末大作业或毕业设计中的控制与运动仿真环节。包体共15个文件&#xff0c;包含由 .sl…

作者头像 李华