Refine 教程实践:Ant Design CRUD 视图组件(List/Create/Edit/Show)在 React Router 项目中的落地
2026/9/13 8:08:46 网站建设 项目流程

Refine 教程实践:Ant Design CRUD 视图组件(List/Create/Edit/Show)在 React Router 项目中的落地

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

本篇指南基于 Refine 官方教程中 "CRUD Components" 一节展开,聚焦 Refine 的 Ant Design 集成(@refinedev/antd)提供的四个 CRUD 视图包装组件——<List /><Create /><Edit /><Show />。读完本文,你将掌握如何在基于 React Router 的 Ant Design 后台应用中用这四个组件替换自制的页面头部,自动获得带 i18n 支持的多语言标题、面包屑、返回按钮、创建/编辑/删除/刷新等操作按钮,并能结合useForm返回的saveButtonProps统一处理表单提交。

为什么需要 CRUD 视图组件

在教程的前置步骤中,我们已经完成了页面的基础重构(移除了自制的<Header />组件,同时也就移除了原本指向/products/create路由的导航链接)。此时如果只是继续手写页面,就要为每个页面重复实现:页面标题、面包屑、返回按钮、右上角的操作按钮(新建、编辑、删除、刷新)、底部保存按钮……

Refine 的 Ant Design 集成为此提供了四个"布局型"包装组件。它们的定位非常明确:本身不包含任何数据逻辑,只负责提供一致的页面结构与默认功能。文档原话指出,这些组件不是使用 Refine 的必要条件,但它们能让你在不为这些通用功能写一行代码的前提下,构建出一个风格统一的界面。

这四个组件在源码中分别位于:

  • List 实现
  • Create 实现
  • Edit 实现
  • Show 实现
  • 各自的 Props 类型定义在 crud/types.ts,而所有 UI 集成共用的基础类型则集中在 ui-types 的 crud.tsx

下面按教程顺序,逐一看这四种视图如何接入。

List 视图:自动获得"新建"入口

<List />是列表页的包装组件,提供带 i18n 支持的页面头部,以及一个"创建新记录"的导航按钮。你可以在传入children之外,通过 props 进一步定制它。

教程中一个值得注意的点:之前手动移除<Header />时,我们把/products/create的导航链接也一并移除了。现在只需把<Table />外层包上<List />,"新建"按钮就会自动出现,无需额外代码。

更新src/pages/products/list.tsx

import { useMany, getDefaultFilter } from "@refinedev/core"; import { useTable, EditButton, ShowButton, getDefaultSortOrder, FilterDropdown, useSelect, // 关键:引入 List List, } from "@refinedev/antd"; import { Table, Space, Input, Select } from "antd"; export const ListProducts = () => { const { tableProps, sorters, filters } = useTable({ sorters: { initial: [{ field: "id", order: "asc" }] }, filters: { initial: [ { field: "name", operator: "contains", value: "" }, { field: "category.id", operator: "in", value: [1, 2] }, ], }, syncWithLocation: true, }); const { result: categories, query: { isLoading }, } = useMany({ resource: "categories", ids: tableProps?.dataSource?.map((product) => product.category?.id) ?? [], }); const { selectProps } = useSelect({ resource: "categories", }); return ( <List> <Table {...tableProps} rowKey="id"> {/* 表格列定义,与教程前文一致 */} </Table> </List> ); };

源码视角:List 的默认行为从哪里来

从 List 组件源码 可以看到它的实现细节:

  1. 标题的 i18n 机制:默认标题通过translate(${identifier}.titles.list, ...)生成——先尝试查找当前语言的products.titles.list这类翻译键,找不到时回退为基于资源名生成的可读名称(如 "Products",并自动取复数形式)。翻译键中的identifier来自useResourceParams(),默认从 URL 的:resource路由参数读取。
  2. Create 按钮的显隐逻辑isCreateButtonVisible = canCreate ?? (!!resource?.create || !!createButtonPropsFromProps)。也就是说,只要资源定义中注册了create路由,"新建"按钮就会默认出现在页头右侧;你可以显式传canCreate={false}关闭它,或通过createButtonProps定制按钮本身(如hideTextsize)。
  3. 面包屑:默认渲染内置的<Breadcrumb />,但优先读取Refine组件options.breadcrumb中设置的全局面包屑,也支持通过breadcrumbprop 局部覆盖。
  4. 页头按钮可完全替换headerButtons支持传节点或函数,函数形式可以拿到{ defaultButtons, createButtonProps }上下文,方便在保留默认按钮的基础上追加自定义按钮。

Create 视图:页头 + 底部 SaveButton 的默认布局

<Create />是创建页的包装组件:带 i18n 标题、返回列表的导航、返回按钮、面包屑,并且页脚自带一个<SaveButton />——你可以把useForm返回的saveButtonProps传给它来提交表单。

更新src/pages/products/create.tsx

import { useForm, useSelect, Create } from "@refinedev/antd"; import { Form, Input, Select, InputNumber } from "antd"; export const CreateProduct = () => { const { formProps, saveButtonProps } = useForm({ refineCoreProps: { // 保存成功后重定向到编辑页 redirect: "edit", }, }); const { selectProps } = useSelect({ resource: "categories", }); return ( <Create saveButtonProps={saveButtonProps}> <Form {...formProps} layout="vertical"> <Form.Item label="Name" name="name"> <Input /> </Form.Item> <Form.Item label="Description" name="description"> <Input.TextArea /> </Form.Item> <Form.Item label="Material" name="material"> <Input /> </Form.Item> <Form.Item label="Category" name={["category", "id"]}> <Select {...selectProps} /> </Form.Item> <Form.Item label="Price" name="price"> <InputNumber step="0.01" stringMode /> </Form.Item> </Form> </Create> ); };

源码视角:saveButtonProps 为什么能"接住"表单

从 Create 组件源码 可以看到默认页脚按钮的组装方式:

const saveButtonProps: SaveButtonProps = { ...(isLoading ? { disabled: true } : {}), ...saveButtonPropsFromProps, // useForm 返回的 saveButtonProps 在这里展开 htmlType: "submit", };

三个要点:

  • htmlType: "submit"是强制追加的(放在展开之后),因此<SaveButton />天然是一个提交按钮,点击它会触发<Form {...formProps}>onFinish,即useForm内部的 mutation 提交逻辑。这就是教程所说的"用saveButtonProps提供与手写<SaveButton />相同的功能"的底层原因。
  • 加载态自动联动:传入isLoading时保存按钮会自动disabled,防止请求进行中重复提交。
  • 内容区被Card+Spin包裹children渲染在一个无边框Card内,Spin负责加载遮罩;页脚按钮区通过Cardactions插槽右对齐展示。

useForm中的refineCoreProps.redirect: "edit"表示创建成功后跳转到该记录的编辑页,这是useForm@refinedev/core)的redirect配置能力,教程在这里首次使用它。

Edit 视图:比 Create 多出的刷新与删除按钮

<Edit />的设计与用法同<Create />一致,但额外在页头包含<RefreshButton />和页脚的<DeleteButton />

更新src/pages/products/edit.tsx

import { useForm, useSelect, Edit } from "@refinedev/antd"; import { Form, Input, Select, InputNumber } from "antd"; export const EditProduct = () => { const { formProps, saveButtonProps, query } = useForm({ refineCoreProps: { // 保存成功后跳转到详情页 redirect: "show", }, }); const { selectProps } = useSelect({ resource: "categories", // 用当前记录已有的分类作为下拉默认值 defaultValue: query?.result?.category?.id, }); return ( <Edit saveButtonProps={saveButtonProps}> <Form {...formProps} layout="vertical"> <Form.Item label="Name" name="name"> <Input /> </Form.Item> <Form.Item label="Description" name="description"> <Input.TextArea /> </Form.Item> <Form.Item label="Material" name="material"> <Input /> </Form.Item> <Form.Item label="Category" name={["category", "id"]}> <Select {...selectProps} /> </Form.Item> <Form.Item label="Price" name="price"> <InputNumber step="0.01" stringMode /> </Form.Item> </Form> </Edit> ); };

教程在此处给出了一条提示(tip):我们删掉了<EditProduct />里手写的<SaveButton />,改用saveButtonProps这个 prop,让<Edit />组件本身来承载同样的保存功能——这与 Create 视图的做法完全一致。

源码视角:Edit 的默认按钮组合

从 Edit 组件源码 可以确认它的默认按钮布局:

  • 页头默认按钮{hasList && <ListButton />} + <RefreshButton />(若传了autoSaveProps还会多一个AutoSaveIndicator自动保存指示器)。hasListresource?.list && !recordItemId决定,即资源定义了 list 路由且当前不是按recordItemId做内联编辑时才显示"返回列表"按钮。
  • 页脚默认按钮{isDeleteButtonVisible && <DeleteButton />} + <SaveButton />。删除按钮的显隐逻辑为canDelete ?? (resource?.meta?.canDelete || deleteButtonPropsFromProps),即默认跟随资源定义。
  • 删除成功后自动回列表:源码中给DeleteButton注入了onSuccess: () => go({ to: goListPath })goListPathuseToPath({ resource, action: "list" })计算,所以不需要手动处理删除后的跳转。
  • recordItemId支持非 URL 场景const id = recordItemId ?? idFromParams,意味着你可以不用路由参数、而是通过recordItemIdprop 为任意一条记录渲染编辑页(例如可编辑表格中的行)。
  • 其他相关 props:mutationMode决定删除等 mutation 的乐观/悲观策略(默认读取useMutationMode()的上下文值);dataProviderName可指定非默认的数据提供者。

Show 视图:一整套只读页操作按钮

<Show />是详情页的包装组件,提供带 i18n 的标题、返回列表/编辑记录的导航、刷新按钮、删除按钮、返回按钮和面包屑。

更新src/pages/products/show.tsx

import { useShow, useOne } from "@refinedev/core"; import { TextField, NumberField, MarkdownField, Show } from "@refinedev/antd"; import { Typography } from "antd"; export const ShowProduct = () => { const { result: product, query: { isLoading } } = useShow(); // 按需加载所属分类,仅当 product 存在时发起请求 const { data: category, query: { isLoading: categoryIsLoading } } = useOne({ resource: "categories", id: product?.category.id || "", queryOptions: { enabled: !!product, }, }); return ( <Show isLoading={isLoading}> <Typography.Title level={5}>Id</Typography.Title> <TextField value={product?.id} /> <Typography.Title level={5}>Name</Typography.Title> <TextField value={product?.name} /> <Typography.Title level={5}>Description</Typography.Title> <MarkdownField value={product?.description} /> <Typography.Title level={5}>Material</Typography.Title> <TextField value={product?.material} /> <Typography.Title level={5}>Category</Typography.Title> <TextField value={categoryIsLoading ? "Loading..." : category?.title} /> <Typography.Title level={5}>Price</Typography.Title> <NumberField value={product?.price} /> </Show> ); };

这里体现了 Show 页的两类核心用法:

  1. isLoading透传:把useShow()query.isLoading传给<Show isLoading={...}>,源码中它会同时驱动内容区的Spin遮罩和页头/页脚所有默认按钮的disabled状态,避免数据未就绪时误操作。
  2. 只读字段组件TextFieldNumberFieldMarkdownField同样来自@refinedev/antd,它们只是展示层组件,与视图包装组件无逻辑耦合;useOne+queryOptions.enabled: !!product是"依赖主记录再拉取关联记录"的标准模式。

从 Show 组件源码 确认的默认页头按钮为:<ListButton />(资源有 list 路由时)+<EditButton type="primary" />canEdit ?? !!resource?.edit)+<DeleteButton />(受canDelete控制)+<RefreshButton />,与文档描述的"编辑、刷新、删除、返回列表"能力一一对应;删除成功后的回列表跳转逻辑与 Edit 相同。

四个组件的通用 Props 一览

四个组件共享一套 props 约定,统一定义在 ui-types 的 crud.tsx(RefineCrudListProps/RefineCrudCreateProps/RefineCrudEditProps/RefineCrudShowProps),Ant Design 的 types.ts 再把其中的泛型参数具体化为 antd 的CardSpacePageHeader等类型。常用项如下:

Prop适用组件说明默认值(摘自类型注释)
resource全部资源名,用于数据交互与路由从 URL 的:resource读取
title全部页头标题基于资源名生成的List/Create/Edit/Show {resource}并走 i18n
breadcrumb全部自定义面包屑内置<Breadcrumb />(优先全局配置)
wrapperProps/headerProps/contentProps全部分别透传给外层容器、页头、内容包裹层-
headerButtons/footerButtons各不同替换默认按钮,支持函数式渲染List:<CreateButton />;Create/Edit:<SaveButton />
canCreate/canDelete/canEditList / Edit、Show / Show控制对应默认按钮的显隐跟随资源定义
isLoadingCreate/Edit/Show加载态,联动 Spin 与按钮禁用false
saveButtonPropsCreate/Edit透传给默认<SaveButton />-
recordItemIdEdit/Show不走 URL 参数时指定记录 id取 URL 的:id
goBackCreate/Edit/Show自定义左上角返回图标内置返回箭头

需要强调的一致性设计:所有按钮替换点(headerButtons/footerButtons)都支持ActionButtonRenderer形态——既可以直接传 React 节点,也可以传一个接收{ defaultButtons, xxxButtonProps }上下文的函数,既能完全接管,也能在默认按钮旁边安全地追加自定义按钮。

小结与后续

到这里,教程示例应用已经做到了"零手写头部/按钮代码"地获得:一致的页面标题(自动 i18n)、面包屑、返回按钮、创建/编辑/删除/刷新入口、表单保存按钮以及加载态联动。这正是 Refine 视图组件的设计意图——把 CRUD 页面的"骨架"标准化,让你把精力集中在数据与业务字段上。

按教程的进度,下一步是学习如何处理通知(notifications)并将其与 Ant Design 的通知系统集成,从而为 mutation 的成功/失败提供统一反馈。若希望深入了解这四个组件之外的能力,可以继续阅读@refinedev/antd集成文档中的按钮组件(EditButtonShowButtonDeleteButton等,位于 buttons 目录)与表格组件(useTableFilterDropdown等,位于 table 目录)。

【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询