Refine Mantine ExportButton 组件详解:数据导出按钮的定制与 useExport 实战
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
<ExportButton>是 Refine 在 Mantine 适配层中提供的开箱即用导出按钮:它本质上是 Mantine<Button>为主体,结合packages/mantine与packages/core中的源码与测试,讲解该组件的用法、全部属性、底层实现原理,以及如何与useExport组合成完整的导出实战方案。
:::simple 本文速览
- 掌握
ExportButton的基本用法、hideText等核心属性; - 通过源码理解按钮的“纯展示”定位与默认文案/图标的来源;
- 结合
useExport的完整参数表,实现分页拉取、字段映射、自定义文件名与 CSV 下载; - 了解 Swizzle 定制入口与跨 UI 库共用的测试基座。
:::
ExportButton 是什么
<ExportButton>是一个 Mantine<Button>,带有默认的导出图标和默认文本 “Export”。它在数据层面只有展示价值(presentational),真正的导出逻辑由核心包提供的useExportHook 承担,两者通过onClick与loading串联起来。
这一“纯展示”定位在源码中有明确体现。Mantine 版实现 中,组件只做了三件事:
- 通过
useExportButton()获取本地化后的默认文案label; - 根据
hideText决定渲染ActionIcon(仅图标)还是Button(图标 + 文本); - 统一挂载
data-testid与className,供测试与样式覆盖使用。
import { ActionIcon, Button } from "@mantine/core"; import { IconFileExport } from "@tabler/icons-react"; export const ExportButton: React.FC<ExportButtonProps> = ({ hideText = false, children, loading = false, svgIconProps, ...rest }) => { const { label } = useExportButton(); // ... return hideText ? ( <ActionIcon loading={loading} aria-label={label} ...> <IconFileExport size={18} {...svgIconProps} /> </ActionIcon> ) : ( <Button variant="default" loading={loading} leftIcon={<IconFileExport size={18} />} ...> {children ?? label} </Button> ); };默认文案label由核心包的按钮 Hook 体系提供。packages/core/src/hooks/button/index.tsx 中,useExportButton与useSaveButton、useImportButton一样,统一收敛到useActionableButton({ type: "export" }),由 i18n 上下文给出 “Export” 之类的翻译,因此当你的应用配置了多语言时,按钮文字会自动随语言包切换。
基本用法:给列表页加上导出能力
导出按钮最常见的应用场景是列表页(List)头部。它本身不包含任何导出逻辑,需要配合@refinedev/core的useExportHook 使用:
import { useExport } from "@refinedev/core"; import { List, ExportButton } from "@refinedev/mantine"; import { Table, Pagination } from "@mantine/core"; import { useTable } from "@refinedev/react-table"; import { ColumnDef, flexRender } from "@tanstack/react-table"; const PostList: React.FC = () => { const columns = React.useMemo<ColumnDef<IPost>[]>( () => [ { id: "id", header: "ID", accessorKey: "id" }, { id: "title", header: "Title", accessorKey: "title" }, ], [], ); const { reactTable: { getHeaderGroups, getRowModel }, refineCore: { setCurrentPage, pageCount, currentPage }, } = useTable({ columns }); const { triggerExport, isLoading: exportLoading } = useExport<IPost>({ mapData: (item) => ({ id: item.id, post_title: item.title, }), pageSize: 10, maxItemCount: 50, }); return ( <List headerButtons={ <ExportButton loading={exportLoading} onClick={triggerExport} /> } > <Table> <thead> {getHeaderGroups().map((headerGroup) => ( <tr key={headerGroup.id}> {headerGroup.headers.map((header) => ( <th key={header.id}> {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} </th> ))} </tr> ))} </thead> <tbody> {getRowModel().rows.map((row) => ( <tr key={row.id}> {row.getVisibleCells().map((cell) => ( <td key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ))} </tr> ))} </tbody> </Table> <br /> <Pagination position="right" total={pageCount} page={currentPage} onChange={setCurrentPage} /> </List> ); }; interface IPost { id: number; title: string; }要点说明:
- 触发链路:点击按钮 → 调用
triggerExport→useExport内部通过 data provider 的getList分批拉取数据 → 用 papaparse 序列化为 CSV → 触发浏览器下载; - 加载态:
loading={exportLoading}让按钮在导出过程中显示 loading 状态并防止重复点击; - 位置:通过
List组件的headerButtons插槽放入列表页头部工具栏,这是 Refine 推荐的放法,也可以放到任意你想放置的位置。
Properties:ExportButton 的全部属性
hideText
hideText用于控制是否显示按钮文字。当为true时,只显示导出图标:
import { ExportButton } from "@refinedev/mantine"; const MyExportComponent = () => { return <ExportButton hideText />; };从源码可见,hideText模式下组件渲染的是 Mantine 的ActionIcon(图标按钮),并自动带上aria-label={label},保证无障碍可访问性;非隐藏模式下渲染Button,文字优先使用children,未传children时才回退到默认的label。
其余常用属性
ExportButton的属性类型定义为RefineExportButtonProps<ButtonProps, CommonButtonProps>(见 packages/mantine/src/components/buttons/types.ts),即除下面列出的自定义属性外,MantineButton的全部属性(variant、size、color、disabled、onClick等)均可直接透传:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
hideText | boolean | false | 为true时仅显示图标,隐藏 “Export” 文本 |
children | ReactNode | — | 自定义按钮文字,优先级高于默认文案 |
loading | boolean | false | 导出进行中显示加载态(源码中透传给Button/ActionIcon) |
svgIconProps | Omit<IconProps, "ref"> | — | 自定义IconFileExport图标的尺寸、颜色等属性 |
hidden | boolean | — | 由CommonButtonProps提供的通用隐藏开关 |
onClick | 事件回调 | — | 点击处理,通常绑定triggerExport |
此外,Mantine 版按钮内部使用mapButtonVariantToActionIconVariant将Button的 variant 映射为ActionIcon的 variant,因此在hideText与普通模式之间切换时,视觉风格可以保持一致。
底层原理:useExport 如何工作
虽然本文主角是ExportButton,但要写出可用的导出功能,理解 useExport 实现 是必要的。核心流程如下:
- 解析资源:通过
useResourceParams拿到当前资源名;若存在多个 data provider,用pickDataProvider选择正确的 provider。 - 分批拉取:以
pageSize(默认 20)为一批循环调用getList,pagination.mode固定为"server"以启用服务端分页,直到满足以下任一条件停止:- 已拉取数据量达到
maxItemCount; rawData.length === total(所有数据已取完)。
- 已拉取数据量达到
- 字段映射:对每条记录执行
mapData,用于重命名/裁剪导出字段(例如把item.title输出为post_title)。 - 序列化与下载:使用 papaparse 的
unparse生成 CSV(默认quotes: true、header: true),并通过downloadInBrowser触发浏览器下载;文件名中的空格会被替换为下划线,并支持 BOM 前缀(默认开启,便于 Excel 正确识别 UTF-8)。
useExport的完整可配置项如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
resource | string | 从路由读取 | 要导出的资源名 |
mapData | MapDataFn | (item) => item | 对每条记录执行的字段映射函数 |
sorters | CrudSort[] | — | 导出数据的排序规则 |
filters | CrudFilter[] | — | 导出数据的过滤规则 |
maxItemCount | number | — | 导出的最大记录数上限 |
pageSize | number | 20 | 每批拉取的条数(即getList的页大小) |
unparseConfig | UnparseConfig | { quotes: true, header: true } | papaparse 序列化配置 |
meta | MetaQuery | — | 传给 data provider 的元数据 |
dataProviderName | string | — | 多 data provider 时指定使用的 provider |
onError | (error) => void | — | 导出失败回调 |
download | boolean | true | 是否在浏览器触发下载 |
filename | string | 资源名-当前时间 | 自定义导出文件名(不含扩展名) |
useTextFile | boolean | false | 为true时导出.txt文本文件 |
useBom | boolean | true | 是否在文件头写入 BOM |
title | string | "My Generated Report" | 导出文件的标题文本 |
showTitle | boolean | false | 是否在文件首行输出标题 |
返回值为{ isLoading, triggerExport }:isLoading驱动按钮的 loading 态,triggerExport是异步函数,调用后返回生成的 CSV 字符串(或undefined)。
一个更贴近实战的自定义示例:
const { triggerExport, isLoading } = useExport<IPost>({ resource: "posts", filename: "posts_report", pageSize: 50, useTextFile: false, title: "Posts Report", showTitle: true, mapData: (item) => ({ id: item.id, title: item.title }), onError: (error) => console.error("Export failed:", error), }); return <ExportButton loading={isLoading} onClick={triggerExport} />;Swizzle:一键定制 ExportButton
如果你需要深度定制(例如更换图标、调整默认样式、增加业务逻辑),Refine 官方推荐使用 Swizzle 机制:运行 Refine CLI 的 swizzle 命令后,组件源码会被复制进你的项目src目录,此后你可以直接修改这份副本而不影响框架升级。
npm run refine swizzle @refinedev/mantine ExportButtonSwizzle 能力对应的文档元数据为swizzle: true(见 文档 frontmatter)。定制后的按钮依然可以依赖useExportButton获取本地化文案,从而在保持团队 i18n 一致性的前提下自由改版。
测试基座:跨 UI 库共用的导出按钮行为
Refine 的 UI 组件测试是跨适配层共用的。Mantine 版测试 直接委托给@refinedev/ui-tests中的buttonExportTests,它验证了四条核心行为(见 packages/ui-tests/src/tests/buttons/export.tsx):
- 无 props 时默认渲染出 “Export” 文本;
- 挂载了正确的
data-testid(RefineButtonTestIds.ExportButton); - 传入
children时优先渲染自定义文本; - 设置
hideText后不再出现 “Export” 文本。
这意味着你在 Ant Design、Material UI、Chakra UI 等任意适配层中使用对应导出按钮时,这些基础行为都是被同一套测试保障的,可以放心迁移。
小结
<ExportButton>是纯展示组件:图标 + “Export” 文案,默认文案来自 i18n,支持children覆盖与hideText图标模式;- 导出逻辑全部交给
useExport:它负责分页拉取、字段映射、papaparse 序列化与浏览器下载,并返回isLoading/triggerExport与按钮对接; - 合理使用参数:
pageSize控制批大小、maxItemCount防止超大导出、mapData裁剪字段、filename/useTextFile/useBom定制输出文件; - 需要深度定制时:优先使用 Refine CLI 的 Swizzle 将组件复制到项目内修改,保持升级兼容的同时实现自定义样式与逻辑。
掌握了这套“按钮 + Hook”的组合,你就能在 Refine 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),仅供参考