Refine v5 中 Ant Design<Show>组件完全指南:从布局到源码级解析
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
<Show>是 Refine v5 的 Ant Design 集成包(@refinedev/antd)中用于展示单条记录详情页的布局组件。它本身不包含任何业务逻辑,却能在不写一行额外代码的情况下提供页面标题、返回按钮、刷新按钮、面包屑以及可选的编辑/删除入口。本文以该组件的官方文档为主体,结合仓库内@refinedev/antd包的源码实现与测试用例,系统讲解<Show>的全部属性、用法与底层工作原理,帮助你快速构建生产可用的详情展示页。
<Show>是什么:一个"无逻辑"的页面布局
按官方文档的定义:
<Show>provides us a layout for displaying the page. It does not contain any logic but adds extra functionalities like a refresh button or giving title to the page.
也就是说,<Show>只负责"搭架子":它渲染出一个标准的详情页外壳(页头 + 卡片内容区),并顺带提供刷新按钮、页面标题等增强功能。真正"取数据"的工作由 Refine 核心的useShowHook 完成,两者配合使用。
从源码实现看(packages/antd/src/components/crud/show/index.tsx),Show接收title、canEdit、canDelete、deleteButtonProps、isLoading、resource、recordItemId、dataProviderName、breadcrumb、contentProps、headerProps、wrapperProps、headerButtons、footerButtons、footerButtonProps、headerButtonProps、goBack等属性,内部通过useResourceParams、useToPath、useBack、useGo、useTranslate、useRefineContext等 Refine 核心 Hook 完成资源解析、路径跳转与国际化,最终渲染结构为:
<div wrapperProps> <PageHeader backIcon={goBack} onBack={...} title={title 或 "Show xxx"} extra={headerButtons} breadcrumb={Breadcrumb} {...headerProps} > <Spin spinning={isLoading}> <Card variant="borderless" actions={footerButtons} {...contentProps}> {children} </Card> </Spin> </PageHeader> </div>可以看到:外层是普通<div>,页头用的是 Ant Design Pro 的PageHeader,内容区是 Ant Design 的Card,加载态由Spin包裹。
基础用法:与useShow组合展示帖子详情
下面是一个完整的帖子详情页示例。它先用useShow<IPost>()从当前路由解析出的resource与id拉取数据,再用useOne<ICategory>()关联查询分类名称,最后把数据交给<Show>渲染:
import { Show, MarkdownField } from "@refinedev/antd"; import { Typography } from "antd"; import { useShow, useOne } from "@refinedev/core"; const { Title, Text } = Typography; interface ICategory { id: number; title: string; } interface IPost { id: number; title: string; content: string; status: "published" | "draft" | "rejected"; category: { id: number }; } const PostShow: React.FC = () => { const { result: post, query: { isLoading }, } = useShow<IPost>(); const { data: categoryData, isLoading: categoryIsLoading } = useOne<ICategory>({ resource: "categories", id: post?.category.id || "", queryOptions: { enabled: !!post, }, }); return ( <Show isLoading={isLoading}> <Title level={5}>Id</Title> <Text>{record?.id}</Text> <Title level={5}>Title</Title> <Text>{record?.title}</Text> <Title level={5}>Category</Title> <Text>{categoryIsLoading ? "Loading..." : categoryData?.data.title}</Text> <Title level={5}>Content</Title> <MarkdownField value={record?.content} /> </Show> ); };配套的路由配置中需要把show路径注册到资源上,例如:
<Refine resources={[ { name: "posts", list: "/posts", show: "/posts/show/:id", }, ]} > <Routes> <Route path="/posts" element={...}> <Route index element={ <RefineAntd.ShowButton recordItemId="123">Show Item 123</RefineAntd.ShowButton> } /> <Route path="show/:id" element={<PostShow />} /> </Route> </Routes> </Refine>其中MarkdownField用于把 Markdown 内容渲染为富文本(实现在 packages/antd/src/components/fields/markdown/index.tsx),ShowButton则是用于跳转到详情页的按钮组件(见 packages/antd/src/components/buttons/show/index.tsx)。
补充:
<Show>组件是可被 swizzle 定制的。通过Refine CLI的 swizzle 命令,你可以把组件源码复制到自己的项目中直接修改,官方文档对此有专门说明(Refine CLI)。
属性详解(Properties)
title:自定义页头标题
title允许你在<Show>内部添加标题。如果不传该属性,组件会默认使用 "Show" 前缀加上资源的单数形式名称。例如对posts资源,默认标题就是 "Show post"。
import { Show } from "@refinedev/antd"; const PostShow: React.FC = () => { return ( <Show title="Custom Title"> <p>Rest of your page here</p> </Show> ); };从源码(index.tsx)可以看到默认标题的完整生成逻辑:优先取title;否则用useTranslate查找${identifier}.titles.show的国际化词条;再回退到`Show ${getUserFriendlyName(resource?.meta?.label ?? identifier, "singular")}`,即把资源名转成用户友好形式(如posts→post)。
resource:指定自定义资源
<Show>默认从路由中读取resource信息。如果你需要在非标准路由上使用它,可以显式传入resource属性:
import { Show } from "@refinedev/antd"; const CustomPage: React.FC = () => { return ( <Show resource="posts"> <p>Rest of your page here</p> </Show> ); };注意,该属性最终会传入useResourceParams({ resource: resourceFromProps }),因此它也支持传资源对象而不仅是字符串。当你存在多个同名资源时,可以改用identifier传值——identifier仅作为资源匹配的主键,而数据提供者(data provider)的方法调用仍会使用<Refine/>组件中定义的资源name。更多细节可参考<Refine/>组件的identifier文档。
canDelete与canEdit:控制删除/编辑按钮
这两个布尔属性用于在<Show>内部添加删除与编辑按钮:
- 点击删除按钮会执行 data provider 提供的
useDelete(即deleteOne方法); - 点击编辑按钮会把用户重定向到该记录的编辑页。
典型用法是结合usePermissions做权限控制——只有管理员才显示操作按钮:
import { Show } from "@refinedev/antd"; import { usePermissions } from "@refinedev/core"; const PostShow: React.FC = () => { const { data: permissionsData } = usePermissions(); return ( <Show canDelete={permissionsData?.includes("admin")} canEdit={permissionsData?.includes("admin")} > <p>Rest of your page here</p> </Show> ); };源码中的判定逻辑(index.tsx)值得注意:
const hasList = resource?.list && !recordItemId; const isDeleteButtonVisible = canDelete ?? (resource?.meta?.canDelete || deleteButtonPropsFromProps); const isEditButtonVisible = canEdit ?? !!resource?.edit;即:canDelete优先采用显式传入的值,否则回退到资源meta.canDelete,甚至只要传了deleteButtonProps也会让删除按钮显示;canEdit优先采用显式值,否则回退到资源是否配置了edit路径。这些分支在测试文件 packages/antd/src/components/crud/show/index.spec.tsx 中都有对应的用例覆盖(例如资源canEdit: false但组件传canEdit={true}时按钮仍然渲染)。
deleteButtonProps:定制删除按钮
如果资源具备删除能力,并且你想调整删除按钮的外观或行为,可以传入deleteButtonProps:
import { Show } from "@refinedev/antd"; import { usePermissions } from "@refinedev/core"; const PostShow: React.FC = () => { const { data: permissionsData } = usePermissions(); return ( <Show canDelete={permissionsData?.includes("admin")} deleteButtonProps={{ size: "small" }} canEdit={permissionsData?.includes("admin")} > <p>Rest of your page here</p> </Show> ); };源码在组装删除按钮属性时(index.tsx)会自动补充recordItemId、onSuccess跳回列表页等默认行为,然后与你传入的deleteButtonProps做浅合并:
const deleteButtonProps: DeleteButtonProps | undefined = isDeleteButtonVisible ? { ...(isLoading ? { disabled: true } : {}), resource: identifier, recordItemId: id, onSuccess: () => { go({ to: goListPath }); }, dataProviderName, ...deleteButtonPropsFromProps, } : undefined;类型上它是DeleteButtonProps(可参考 packages/antd/src/components/buttons/types.ts),即 Ant DesignButton的属性与 Refine 删除按钮能力的并集。
recordItemId:在自定义页面/弹窗中显式指定 id
<Show>默认从路由读取id信息。当组件被用在自定义页面、Modal 或 Drawer 中,无法从 URL 读取 id 时,就需要用recordItemId显式传入:
import { Show, useModalForm } from "@refinedev/antd"; import { Modal, Button } from "antd"; const PostShow: React.FC = () => { const { modalProps, id, show } = useModalForm({ action: "show", }); return ( <div> <Button onClick={() => show()}>Show Button</Button> <Modal {...modalProps}> <Show recordItemId={id}> <p>Rest of your page here</p> </Show> </Modal> </div> ); };源码中的取值逻辑是const id = recordItemId ?? idFromParams;(index.tsx),recordItemId优先级高于路由参数。同时文档特别提示:<Show>需要id信息才能让<RefreshButton>正常工作,因为刷新按钮会携带recordItemId重新触发数据查询。
dataProviderName:指定多数据提供者中的某一个
不指定时,Refine 会使用默认的 data provider。如果应用配置了多个 data provider,可以通过dataProviderName指定使用哪一个:
import { Refine } from "@refinedev/core"; import dataProvider from "@refinedev/simple-rest"; import { Show } from "@refinedev/antd"; const PostShow = () => { return <Show dataProviderName="other">...</Show>; }; export const App: React.FC = () => { return ( <Refine dataProvider={{ default: dataProvider("https://api.fake-rest.refine.dev/"), other: dataProvider("https://other-api.fake-rest.refine.dev/"), }} > {/* ... */} </Refine> ); };源码中该属性会被透传给删除按钮与刷新按钮(见 index.tsx 与 L114-L119),保证删除与刷新请求也走同一个 data provider。
goBack:定制或禁用返回按钮
通过goBack属性可以自定义返回按钮,也可以传入false/空值来禁用:
import { Show } from "@refinedev/antd"; import { Button } from "antd"; const PostShow: React.FC = () => { const BackButton = () => <Button>←</Button>; return ( <Show goBack={<BackButton />}> <p>Rest of your page here</p> </Show> ); };源码中的goBack会被直接作为PageHeader的backIcon(index.tsx),而onBack只有在当前action不是list且已定义时才绑定useBack()返回的跳转函数。因此有一个重要细节:如果路由中没有:action参数、或 action 是list,即使传了goBack也不会显示返回按钮(因为onBack为 undefined 时 Ant Design 的 PageHeader 默认不渲染返回图标)。
此时可以通过headerProps覆盖onBack来强制启用:
import { useBack } from "@refinedev/core"; import { Show } from "@refinedev/antd"; import { Button } from "antd"; const PostShow: React.FC = () => { const back = useBack(); const BackButton = () => <Button>←</Button>; return ( <Show goBack={<BackButton />} headerProps={{ onBack: back }}> <p>Rest of your page here</p> </Show> ); };goBack的默认值是<ArrowLeft />图标,类型为ReactNode(见文档末尾的 API Reference 表格)。
isLoading:加载态
由于<Show>内部使用 Ant Design 的Card组件,isLoading可以直接传入。为true时,内容区会被Spin包裹显示加载动画:
import { Show } from "@refinedev/antd"; const PostShow: React.FC = () => { return ( <Show isLoading={true}> <p>Rest of your page here</p> </Show> ); };源码中isLoading的默认值是false(index.tsx),并且它同时会禁用编辑/删除/刷新按钮(...(isLoading ? { disabled: true } : {})),避免加载过程中触发重复操作。实践中通常直接透传useShow返回的query.isLoading。
breadcrumb:定制或禁用面包屑
breadcrumb属性用于定制面包屑;不传时默认使用@refinedev/antd包导出的Breadcrumb组件。传false可完全禁用:
import { Show, Breadcrumb } from "@refinedev/antd"; const PostShow: React.FC = () => { return ( <Show breadcrumb={ <div style={{ padding: "3px 6px", border: "2px dashed cornflowerblue", }} > <Breadcrumb /> </div> } > <p>Rest of your page here</p> </Show> ); };源码的优先级逻辑是(index.tsx):组件级breadcrumb优先;若为undefined则回退到<Refine/>全局配置的options.breadcrumb;都未提供时才渲染默认<Breadcrumb />。Breadcrumb组件的详细用法见 Breadcrumb 文档。
wrapperProps:定制最外层容器
@refinedev/antd的 wrapper 元素就是普通的<div/>,因此wrapperProps可以接收<div/>能接收的一切属性(className、style、id、事件等):
import { Show } from "@refinedev/antd"; const PostShow: React.FC = () => { return ( <Show wrapperProps={{ style: { backgroundColor: "cornflowerblue", padding: "16px", }, }} > <p>Rest of your page here</p> </Show> ); };headerProps:定制页头
headerProps用于定制PageHeader。除了样式,还可以设置subTitle等 PageHeader 专属属性(其类型为PageHeaderProps,参考 packages/antd/src/components/crud/types.ts):
import { Show } from "@refinedev/antd"; const PostShow: React.FC = () => { return ( <Show headerProps={{ subTitle: "This is a subtitle", style: { backgroundColor: "cornflowerblue", padding: "16px", }, }} > <p>Rest of your page here</p> </Show> ); };源码通过{...(headerProps ?? {})}展开到PageHeader上,因此你可以在其中覆盖onBack、title等任何 PageHeader 属性(这正是上一节"强制显示返回按钮"技巧的原理)。更多属性可参考 ProComponents PageHeader 文档。
contentProps:定制内容卡片
contentProps用于定制包裹内容的Card(类型为CardProps):
import { Show } from "@refinedev/antd"; const PostShow: React.FC = () => { return ( <Show contentProps={{ style: { backgroundColor: "cornflowerblue", padding: "16px", }, }} > <p>Rest of your page here</p> </Show> ); };注意源码中Card使用variant="borderless"的无边框样式(index.tsx),contentProps会展开到该Card上,因此你也可以通过它覆盖actions(即页脚按钮区)。
headerButtons:定制页头按钮
默认情况下,<Show/>的页头包含四个按钮:
<ListButton>—— 返回列表页<EditButton>—— 跳转编辑页<DeleteButton>—— 删除当前记录<RefreshButton>—— 刷新数据
headerButtons接受React.ReactNode或渲染函数,渲染函数的签名是:
({ defaultButtons, listButtonProps, editButtonProps, deleteButtonProps, refreshButtonProps }) => React.ReactNode方式一:保留默认按钮并追加自定义按钮:
import { Show } from "@refinedev/antd"; import { Button } from "antd"; const PostShow: React.FC = () => { return ( <Show headerButtons={({ defaultButtons }) => ( <> {defaultButtons} <Button type="primary">Custom Button</Button> </> )} > <p>Rest of your page here</p> </Show> ); };方式二:完全自定义按钮组,同时利用渲染函数提供的默认 props 复用各按钮的默认值:
import { Show, ListButton, EditButton, DeleteButton, RefreshButton, } from "@refinedev/antd"; import { Button } from "antd"; const PostShow: React.FC = () => { return ( <Show headerButtons={({ deleteButtonProps, editButtonProps, listButtonProps, refreshButtonProps, }) => ( <> <Button type="primary">Custom Button</Button> {listButtonProps && ( <ListButton {...listButtonProps} meta={{ foo: "bar" }} /> )} {editButtonProps && ( <EditButton {...editButtonProps} meta={{ foo: "bar" }} /> )} {deleteButtonProps && ( <DeleteButton {...deleteButtonProps} meta={{ foo: "bar" }} /> )} <RefreshButton {...refreshButtonProps} meta={{ foo: "bar" }} /> </> )} > <p>Rest of your page here</p> </Show> ); };有几个按条件渲染的规则需要记住(这些行为同样在 packages/antd/src/components/crud/show/index.spec.tsx 的测试中验证过):
- 如果资源没有定义
list,<ListButton>不会渲染,listButtonProps为undefined; - 如果
canDelete为false,<DeleteButton>不会渲染,deleteButtonProps为undefined; - 如果
canEdit为false,<EditButton>不会渲染,editButtonProps为undefined; <RefreshButton>始终渲染。
所以在自定义渲染函数里,务必用条件判断包裹listButtonProps、editButtonProps、deleteButtonProps(如上例),避免解构到 undefined。各按钮的详细文档:ListButton、RefreshButton、EditButton、DeleteButton。
headerButtonProps:定制页头按钮容器
页头按钮默认被包裹在一个 Ant DesignSpace组件里(源码 index.tsx)。headerButtonProps类型为SpaceProps,用于定制这个容器:
import { Show } from "@refinedev/antd"; import { Button } from "antd"; const PostShow: React.FC = () => { return ( <Show headerButtonProps={{ style: { backgroundColor: "cornflowerblue", padding: "16px", }, }} headerButtons={<Button type="primary">Custom Button</Button>} > <p>Rest of your page here</p> </Show> ); };footerButtons:定制页脚按钮
footerButtons用于定制页脚按钮,同样接受ReactNode或渲染函数({ defaultButtons }) => React.ReactNode:
import { Show } from "@refinedev/antd"; import { Button } from "antd"; const PostShow: React.FC = () => { return ( <Show footerButtons={({ defaultButtons }) => ( <> {defaultButtons} <Button type="primary">Custom Button</Button> </> )} > <p>Rest of your page here</p> </Show> ); };从源码看(index.tsx),页脚按钮会作为Card的actions数组中的唯一一项渲染(Card 的 actions 本身是一个数组,这里把整个Space作为一个 action),渲染函数的defaultButtons固定为null。
footerButtonProps:定制页脚按钮容器
footerButtonProps用于定制页脚按钮的Space容器:
import { Show } from "@refinedev/antd"; import { Button } from "antd"; const PostShow: React.FC = () => { return ( <Show footerButtons={({ defaultButtons }) => ( <> {defaultButtons} <Button type="primary">Custom Button</Button> </> )} footerButtonProps={{ style: { float: "right", marginRight: 24, backgroundColor: "cornflowerblue", padding: "16px", }, }} > <p>Rest of your page here</p> </Show> ); };源码中的按钮组装逻辑
把上述属性串起来,<Show>的核心工作就是在渲染前计算好四个按钮的 props(index.tsx):
| 按钮 | 生成条件 | 关键 props |
|---|---|---|
ListButton | resource?.list存在且未传recordItemId | resource: identifier |
EditButton | canEdit ?? !!resource?.edit为真 | type: "primary"、recordItemId: id,加载中时disabled |
DeleteButton | canDelete ?? resource?.meta?.canDelete ?? deleteButtonProps为真 | recordItemId: id、dataProviderName、删除成功回调go({ to: goListPath }) |
RefreshButton | 始终渲染 | recordItemId: id、dataProviderName,加载中时disabled |
hasList = resource?.list && !recordItemId这个条件很有意思:当你在 Modal 里用recordItemId展示详情时,列表按钮会被自动隐藏(因为此时跳回列表页没有意义),该行为在 index.spec.tsx 中有专门的测试用例"should render optional recordItemId with resource prop, not render list button"。
测试保障:共享 CRUD 测试套件
@refinedev/antd的<Show>不仅有自己的测试(packages/antd/src/components/crud/show/index.spec.tsx),还通过crudShowTests.bind(this)(Show)继承了@refinedev/ui-tests包中的共享 CRUD 测试套件(packages/ui-tests/src/tests/crud/show.tsx)。这套共享用例覆盖了"渲染 children"、"默认渲染编辑/删除按钮"、"按钮可见性与权限联动"等跨 UI 框架的通用行为,确保 Ant Design、MUI、Mantine、Chakra UI 等不同适配层对<Show>的语义保持一致。测试中还通过RefineButtonTestIds.DeleteButton等 test id 断言按钮的存在与禁用状态。
API Reference
Properties
| 属性 | 类型 | 默认值 |
|---|---|---|
title | ReactNode | "Show" + 资源单数名 |
resource | string \| Resource | 从路由解析 |
recordItemId | BaseKey | 路由中的:id |
dataProviderName | string | 默认 data provider |
canDelete | boolean | resource?.meta?.canDelete |
canEdit | boolean | !!resource?.edit |
deleteButtonProps | DeleteButtonProps | — |
goBack | ReactNode | <ArrowLeft /> |
isLoading | boolean | false |
breadcrumb | ReactNode \| false | 全局 breadcrumb 或<Breadcrumb /> |
wrapperProps | div的 HTML 属性 | — |
headerProps | PageHeaderProps | — |
contentProps | CardProps | — |
headerButtons | ReactNode \| render function | ListButton、EditButton、DeleteButton、RefreshButton |
headerButtonProps | SpaceProps | — |
footerButtons | ReactNode \| render function | — |
footerButtonProps | SpaceProps | — |
其中部分类型的底层定义可以进一步查看 packages/antd/src/components/crud/types.ts,ShowProps是由RefineCrudShowProps泛型实例化而来,分别对应SpaceProps(按钮容器)、HTMLAttributes<HTMLDivElement>(wrapper)、PageHeaderProps(页头)与CardProps(内容卡)。
小结
<Show>的设计哲学是"布局与逻辑分离":数据获取交给useShow/useOne,页面骨架与操作入口交给<Show>。通过title、goBack、breadcrumb、headerButtons、footerButtons等属性,你可以在零业务代码的前提下获得一个带标题、面包屑、返回/刷新/编辑/删除按钮的完整详情页;遇到 Modal、Drawer、自定义页面等特殊场景时,recordItemId、resource、dataProviderName又能保证组件依然正确工作。理解其源码中按钮可见性的判定顺序(props 优先、资源 meta 兜底)与id的解析优先级(recordItemId ?? 路由id),将帮助你在实际项目中精准地定制和排查详情页行为。
【免费下载链接】refineA React Framework for building internal tools, admin panels, dashboards & B2B apps with unmatched flexibility.项目地址: https://gitcode.com/GitHub_Trending/re/refine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考