news 2026/9/13 6:43:29

在 Refine 中使用 ThemedLayout 搭建 Ant Design 管理后台布局

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
在 Refine 中使用 ThemedLayout 搭建 Ant Design 管理后台布局

在 Refine 中使用 ThemedLayout 搭建 Ant Design 管理后台布局

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

本文讲解 Refine 内置的<ThemedLayout>布局组件:它以 Ant Design 的Layout/Sider为基础,提供 Header、Sider、Title、Footer、OffLayoutArea 五个可插拔区域,并内置响应式适配与主题联动。读完本文,你将掌握<ThemedLayout>的接入方式、全部 Props 的配置要点、如何用swizzle弹出源码深度定制,以及如何通过useThemedLayoutContext在任意页面控制侧边栏的折叠状态。

什么是 ThemedLayout

<ThemedLayout>是 Refine 与 Ant Design 集成包@refinedev/antd提供的开箱即用布局组件。它基于 Ant Design 的<Layout><Sider>组件来定义页面的整体骨架,同时把页面拆分为五个可独立替换或定制的小节:

  • <ThemedHeader>:显示在页面顶部,可展示当前用户的姓名与头像。
  • <ThemedSider>:显示在页面左侧,根据 resources 配置自动生成菜单项。
  • <ThemedTitle>:显示在<ThemedSider>顶部,包含图标与文字。
  • <Footer>:显示在页面底部,Refine 不提供默认实现,需自行传入。
  • <OffLayoutArea>:渲染在主布局之外,可放在页面任意位置,同时仍属于整体布局的一部分。

由于这五个区域都通过 Props 注入,使用<ThemedLayout>可以在多个页面或站点分区之间保持一致的视觉与结构,同时提升代码的可维护性与复用性。

从源码实现看(packages/antd/src/components/themedLayout/index.tsx),<ThemedLayout>的核心组装逻辑非常清晰:

  • 通过Grid.useBreakpoint()获取响应式断点,并根据屏幕尺寸动态调整内容区 padding(大屏 24px,小屏 12px);
  • 外部容器AntdLayout设置minHeight: "100vh",保证页面始终撑满视口;
  • 内部再嵌套一层AntdLayout承载 Header 与 Content;
  • 整个布局用ThemedLayoutContextProvider包裹,为 Sider 折叠状态提供全局上下文。

组件的 Props 类型定义在 packages/antd/src/components/themedLayout/types.ts 中,其中RefineThemedLayoutSiderProps@refinedev/ui-types的基础上额外扩展了fixed属性。

快速上手:接入 ThemedLayout

下面是一个完整的可用示例:使用@refinedev/react-router作为路由、@refinedev/simple-rest作为数据提供者,并用RefineThemes.Blue配置 Ant Design 主题。这是<ThemedLayout>最常见的接入形态——把它作为路由布局组件包裹<Outlet />

import { Refine } from "@refinedev/core"; import { ThemedLayout, RefineThemes } from "@refinedev/antd"; import { ConfigProvider } from "antd"; import { AntdInferencer } from "@refinedev/inferencer/antd"; import routerProvider from "@refinedev/react-router"; import { BrowserRouter, Routes, Route, Outlet } from "react-router"; import dataProvider from "@refinedev/simple-rest"; import { authProvider } from "./authProvider"; const API_URL = "https://api.fake-rest.refine.dev"; const App: React.FC = () => { return ( <BrowserRouter> <ConfigProvider theme={RefineThemes.Blue}> <Refine routerProvider={routerProvider} dataProvider={dataProvider(API_URL)} authProvider={authProvider} resources={[ { name: "samples", list: "/samples", }, ]} > <Routes> <Route element={ <ThemedLayout> <Outlet /> </ThemedLayout> } > <Route path="/samples" element={<AntdInferencer />} /> </Route> </Routes> </Refine> </ConfigProvider> </BrowserRouter> ); };

其中authProvider提供登录态与用户身份信息,供 Header 展示姓名和头像:

const authProvider = { login: async () => ({ success: true, redirectTo: "/", }), logout: async () => ({ success: true, redirectTo: "/login", }), onError: async (error) => { console.error(error); return { error }; }, check: async () => ({ authenticated: true, }), getIdentity: async () => ({ id: 1, name: "Jane Doe", avatar: "https://unsplash.com/photos/IWLOvomUmWU/download?force=true&w=640", }), };

<ThemedLayout>是响应式的:在平板等小屏场景下,Sider 会切换为 Ant Design 的<Drawer>抽屉形式;在大屏下则使用标准的<Sider>。这一判断逻辑位于 Sider 源码中(sider/index.tsx),通过Grid.useBreakpoint()lg断点判断:!breakpoint.lg即视为移动端。

上述示例使用的是 React Router,仓库中的 examples/auth-antd/src/App.tsx 就是同样模式的生产级参考实现。其他框架下思路一致:Next.js 中放在src/app/layout.tsx作为页面布局,Remix 中则放在路由布局文件(如app/routes/_protected.tsx)内包裹<Outlet />

Sider:默认侧边栏与自定义替换

<ThemedLayout>中,侧边栏默认由<ThemedSider>渲染。该组件通过useMenuhook 根据<Refine>中声明的resources自动生成菜单项,因此只要声明了 resources,Sider 就会自动生成对应的导航菜单

如果你需要完全替换侧边栏,可以通过Siderprop 传入自定义组件:

import { Refine } from "@refinedev/core"; import { ThemedLayout } from "@refinedev/antd"; import { CustomSider } from "./CustomSider"; const App: React.FC = () => { return ( <Refine // ... > <ThemedLayout Sider={() => <CustomSider />} > {/* ... */} </ThemedLayout> </Refine> ); };

除了整体替换,也可以保留默认<ThemedSider>,通过它的 Props 或swizzle进行局部定制。例如用renderTitle定制菜单渲染逻辑和顶部标题:

import { Refine } from "@refinedev/core"; import { ThemedLayout, ThemedSider } from "@refinedev/antd"; import { CustomTitle } from "./CustomTitle"; const App: React.FC = () => { return ( <Refine // ... > <ThemedLayout Sider={() => ( <ThemedSider Title={({ collapsed }) => <CustomTitle collapsed={collapsed} />} render={({ items, logout, collapsed }) => { return ( <> <div>My Custom Element</div> {items} {logout} </> ); }} /> )} > {/* ... */} </ThemedLayout> </Refine> ); };

还可以通过fixed让侧边栏固定(吸顶悬浮),该属性可选,默认false

import { Refine } from "@refinedev/core"; import { ThemedLayout, ThemedSider } from "@refinedev/antd"; const App: React.FC = () => { return ( <Refine // ... > <ThemedLayout Sider={() => <ThemedSider fixed />} > {/* ... */} </ThemedLayout> </Refine> ); };

fixed的实现细节可以从源码确认(sider/index.tsx):当fixedtrue时,Sider 会获得position: fixed; top: 0; height: 100vh; zIndex: 999的样式,同时在其前方渲染一个占位 div(展开时 200px、折叠时 80px)避免内容被遮挡。

Sider Props 一览

Prop类型说明
TitleReact.FC渲染在侧边栏顶部的组件(即ThemedTitle或其替换件)
renderSiderRenderFunction自定义侧边栏内部菜单项与其他元素的渲染函数
metaRecord<string, any>创建菜单项路由时使用的元数据
fixedboolean侧边栏是否固定
activeItemDisabledboolean点击当前激活菜单项时是否禁用跳转(避免重复刷新页面),默认false
onSiderCollapsed(collapsed: boolean) => void侧边栏折叠/展开时的回调
siderItemsAreCollapsedboolean嵌套菜单项默认是展开还是折叠,默认true(折叠)

其中render的函数签名(SiderRenderFunction)如下:

type SiderRenderFunction = (props: { items: JSX.Element[]; logout: React.ReactNode; dashboard: React.ReactNode; collapsed: boolean; }) => React.ReactNode;

值得说明的是,activeItemDisabled在源码中通过给激活菜单项的链接设置pointerEvents: "none"实现(sider/index.tsx),而siderItemsAreCollapsed控制defaultOpenKeys的生成:为false时所有菜单项的 key 都会被加入默认展开列表(sider/index.tsx)。

控制初始折叠状态:initialSiderCollapsed

initialSiderCollapsed用于设置<ThemedSider>的初始折叠状态:

  • true:侧边栏默认折叠;
  • false:侧边栏默认展开(这也是默认值)。
<ThemedLayout initialSiderCollapsed={true} > {/* ... */} </ThemedLayout>

从上下文实现看(contexts/themedLayoutContext/index.tsx),该值作为useState的初始值注入,即只影响首屏渲染,后续折叠状态由用户交互驱动。

监听折叠变化:onSiderCollapsed

onSiderCollapsed会在<ThemedSider>collapsed状态变化时被触发。最常见的用途是把折叠状态持久化到localStorage,下次进入页面时再通过initialSiderCollapsed恢复:

const MyLayout = () => { const onSiderCollapse = (collapsed: boolean) => { localStorage.setItem("siderCollapsed", collapsed); }; const initialSiderCollapsed = Boolean(localStorage.getItem("siderCollapsed")); return ( <ThemedLayout initialSiderCollapsed={initialSiderCollapsed} onSiderCollapsed={onSiderCollapse} > {/* ... */} </ThemedLayout> ); };

源码层面,ThemedLayoutContextProvidersetSiderCollapsed内部先更新内部 state,再调用onSiderCollapsed回调(contexts/themedLayoutContext/index.tsx),因此回调一定能在状态变更后同步收到最新的collapsed值。

Header:用户信息展示与自定义

<ThemedLayout>的头部默认由<ThemedHeader>渲染,它使用useGetIdentityhook 获取当前用户信息,并在头部右侧展示用户名与头像。

需要说明的是,<ThemedHeader>只有在getIdentity返回了nameavatar时才渲染(源码见 header/index.tsx),否则返回null,顶部不会出现空白占位。

替换默认 Header 同样简单:

import { Refine } from "@refinedev/core"; import { ThemedLayout } from "@refinedev/antd"; import { CustomHeader } from "./CustomHeader"; const App: React.FC = () => { return ( <Refine // ... > <ThemedLayout Header={() => <CustomHeader />} > {/* ... */} </ThemedLayout> </Refine> ); };

也可以保留默认实现并让它吸顶(sticky):

import { Refine } from "@refinedev/core"; import { ThemedLayout, ThemedHeader, } from "@refinedev/antd"; const App: React.FC = () => { return ( <Refine // ... > <ThemedLayout Header={() => <ThemedHeader sticky />} > {/* ... */} </ThemedLayout> </Refine> ); };

sticky在源码中对应position: sticky; top: 0; zIndex: 1(header/index.tsx),滚动页面时头部会始终保持在视口顶部。

Title:品牌标识定制

<ThemedLayout>顶部的标题默认由<ThemedTitle>渲染,包含图标与文字,并整体包裹在一个指向/的链接中。可以通过Titleprop 传入自定义实现,例如根据折叠状态切换大小图标:

import { Refine } from "@refinedev/core"; import { ThemedLayout, ThemedTitle } from "@refinedev/antd"; import { MyLargeIcon, MySmallIcon } from "./MyIcon"; const App: React.FC = () => { return ( <Refine // ... > <ThemedLayout Title={({ collapsed }) => ( <ThemedTitle // collapsed 表示侧边栏是否折叠 collapsed={collapsed} icon={collapsed ? <MySmallIcon /> : <MyLargeIcon />} text="My Project" /> )} > {/* ... */} </ThemedLayout> </Refine> ); };

<ThemedTitle>还有一个实用特性:当没有显式传入icontext时,它会回退读取useRefineOptions()中配置的默认图标与文案(title/index.tsx),这让你可以在 Refine 全局配置中统一定义品牌标识,各处的<ThemedTitle>自动生效。另外,折叠状态下文字会被隐藏({!collapsed && ...}),只保留 24px 的图标。

Footer:自定义页脚

Refine 不提供默认的 Footer 组件,但你可以通过Footerprop 传入任意内容。下面的示例使用 Ant Design 的Layout.Footer实现一个居中显示的自定义页脚:

import { Refine } from "@refinedev/core"; import { ThemedLayout } from "@refinedev/antd"; import { Layout } from "antd"; const App: React.FC = () => { return ( <Refine // ... > <ThemedLayout Footer={() => ( <Layout.Footer style={{ textAlign: "center", color: "#fff", backgroundColor: "#7dbcea", }} > My Custom Footer </Layout.Footer> )} > {/* ... */} </ThemedLayout> </Refine> ); };

配合路由使用时,效果与前面的基础示例相同,只是在<ThemedLayout>上多传一个Footer渲染函数即可。从布局源码看,Footer被放置在内容区之后、最内层AntdLayout的末尾(index.tsx),因此它天然位于整个页面内容的底部。

OffLayoutArea:布局之外的内容区

OffLayoutArea用于渲染在主布局容器之外、但仍属于整体布局一部分的内容,典型场景是悬浮按钮、反馈入口等。Refine 同样不提供默认实现,需要自行传入:

import { Refine } from "@refinedev/core"; import { ThemedLayout } from "@refinedev/antd"; import { Button } from "antd"; const App: React.FC = () => { return ( <Refine // ... > <ThemedLayout OffLayoutArea={() => ( <Button type="primary" size="small" onClick={() => alert("Off layout are clicked")} style={{ position: "fixed", left: "8px", bottom: "8px", zIndex: 1000, }} > Send us Feedback </Button> )} > {/* ... */} </ThemedLayout> </Refine> ); };

在上面的示例中,反馈按钮通过position: fixed固定在页面左下角,即便它被声明在<ThemedLayout>内部,实际渲染位置也独立于常规布局流(源码见 index.tsx)。

使用 swizzle 深度自定义

🚨 该功能依赖@refine/cli,请先确保项目已安装。

swizzle命令可以把<ThemedLayout>的源码“弹出”到你的项目src目录,之后就可以直接修改源码实现任意定制。整个过程如下:

首先运行命令并选择要弹出的包:

> npm run refine swizzle ? Which package do you want to swizzle? (Use arrow keys or type to search) Data Provider ◯ @refinedev/simple-rest UI Framework ◉ @refinedev/antd

Refine CLI 只会列出当前项目已安装的包。接着选择要弹出的组件:

? Which component do you want to swizzle? ◯ TagField ◯ TextField ◯ UrlField Other ◯ Breadcrumb ❯◉ ThemedLayout Pages ◯ ErrorPage ◯ AuthPage (Move up and down to reveal more choices)

选择ThemedLayout后,CLI 会在项目中生成如下文件:

Successfully swizzled Themed Layout Files created: - src/components/themedLayout/sider.tsx - src/components/themedLayout/header.tsx - src/components/themedLayout/title.tsx - src/components/themedLayout/index.tsx Warning: If you want to change the default layout; You should pass layout related components to the <ThemedLayout/> component's props.

之后就可以从本地路径导入这些组件,并自由组合:

import { Refine } from "@refinedev/core"; import { ThemedLayout } from "components/themedLayout"; import { ThemedHeader } from "components/themedLayout/header"; import { ThemedSider } from "components/themedLayout/sider"; import { ThemedTitle } from "components/themedLayout/title"; const App = () => { return ( <Refine /* ... */ > <ThemedLayout Header={ThemedHeader} Sider={ThemedSider} Title={ThemedTitle} > /* ... */ </ThemedLayout> </Refine> ); };

:::simple Good to know

  • Refine CLI 会根据你使用的框架决定生成目录:例如使用 Remix 时,路径会是app/components/layout
  • 如果目标目录已存在同名文件,swizzle 命令不会覆盖它。

:::

这一文件结构与@refinedev/antd包内的源码布局一一对应——仓库中的 packages/antd/src/components/themedLayout 目录同样由sider/header/title/index.tsxtypes.ts组成,并且配有对应测试(如 index.spec.tsx 复用@refinedev/ui-testslayoutLayoutTests套件验证基础布局行为)。因此 swizzle 弹出的实际上就是这份经过测试的官方实现。

用 useThemedLayoutContext 控制折叠

useThemedLayoutContexthook 用于在任意位置折叠/展开 Sider,包括移动端的抽屉。你可以在任何页面中调用它,例如在 Dashboard 页面放两个按钮来控制侧边栏:

import { Refine } from "@refinedev/core"; import { ThemedLayout, RefineThemes, useThemedLayoutContext, } from "@refinedev/antd"; import { ConfigProvider, Button, Space } from "antd"; import { AntdInferencer } from "@refinedev/inferencer/antd"; import routerProvider from "@refinedev/react-router"; import { BrowserRouter, Routes, Route, Outlet } from "react-router"; import dataProvider from "@refinedev/simple-rest"; import { authProvider } from "./authProvider"; const API_URL = "https://api.fake-rest.refine.dev"; const DashboardPage = () => { const { siderCollapsed, setSiderCollapsed, mobileSiderOpen, setMobileSiderOpen, } = useThemedLayoutContext(); return ( <Space style={{ paddingTop: 30 }}> <Button type="primary" onClick={() => setMobileSiderOpen(!mobileSiderOpen)} > toggle mobile sider </Button> <Button type="primary" onClick={() => setSiderCollapsed(!siderCollapsed)}> toggle collapse of sider </Button> </Space> ); }; const App: React.FC = () => { return ( <BrowserRouter> <ConfigProvider theme={RefineThemes.Blue}> <Refine routerProvider={routerProvider} dataProvider={dataProvider(API_URL)} authProvider={authProvider} resources={[ { name: "dashboard", list: "/", }, { name: "samples", list: "/samples", }, ]} > <Routes> <Route element={ <ThemedLayout> <Outlet /> </ThemedLayout> } > <Route path="/" element={<DashboardPage />} /> <Route path="/samples" element={<AntdInferencer />} /> </Route> </Routes> </Refine> </ConfigProvider> </BrowserRouter> ); };

这个 hook 的实现非常轻量(hooks/useThemedLayoutContext/index.ts):它只是从ThemedLayoutContext中取出mobileSiderOpensiderCollapsedsetMobileSiderOpensetSiderCollapsed四个值并返回。Context 的默认值在 contexts/themedLayoutContext/index.tsx 中定义:siderCollapsedmobileSiderOpen初始均为false。也正因折叠状态由 Context 管理,页面组件才能跨层级与 Sider 交互。

FAQ:如何持久化 Sider 的折叠状态

问题:刷新页面后如何恢复上次的折叠状态?

方案:将initialSiderCollapsed的初始值来自localStoragecookie,同时用onSiderCollapsed把变化写回去。以下是三种路由框架下的写法。

React Router(src/App.tsx):

import { useState } from "react"; import { Refine } from "@refinedev/core"; import { BrowserRouter, Routes, Route, Outlet } from "react-router"; import { ThemedLayout } from "@refinedev/antd"; const App: React.FC = () => { // 该值可从 localStorage 或 cookie 中读取,实现跨会话持久化 const [initialSiderCollapsed, setInitialSiderCollapsed] = useState(true); return ( <BrowserRouter> <Refine // ... > {/* ... */} <Routes> <Route element={ <ThemedLayout initialSiderCollapsed={initialSiderCollapsed}> <Outlet /> </ThemedLayout> } > {/* ... */} </Route> </Routes> </Refine> </BrowserRouter> ); }; export default App;

Next.js(pages/_app.tsx):

import { useState } from "react"; import { Refine } from "@refinedev/core"; import { ThemedLayout } from "@refinedev/antd"; import type { AppProps } from "next/app"; import type { NextPage } from "next"; function MyApp({ Component, pageProps }: AppProps): JSX.Element { // 该值可从 localStorage 或 cookie 中读取,实现跨会话持久化 const [initialSiderCollapsed, setInitialSiderCollapsed] = useState(true); const renderComponent = () => { if (Component.noLayout) { return <Component {...pageProps} />; } return ( <ThemedLayout initialSiderCollapsed={initialSiderCollapsed}> <Component {...pageProps} /> </ThemedLayout> ); }; return ( <Refine // ... > {/* ... */} {renderComponent()} </Refine> ); } export default MyApp;

Remix(app/routes/_layout.tsx):

import { useState } from "react"; import { Outlet } from "@remix-run/react"; import { ThemedLayout } from "@refinedev/antd"; export default function BaseLayout() { // 该值可从 localStorage 或 cookie 中读取,实现跨会话持久化 const [initialSiderCollapsed, setInitialSiderCollapsed] = useState(true); return ( <ThemedLayout initialSiderCollapsed={initialSiderCollapsed}> <Outlet /> </ThemedLayout> ); }

总结

<ThemedLayout>是 Refine 与 Ant Design 集成的核心布局组件,它把「侧边栏菜单自动生成」「用户身份展示」「响应式抽屉」「主题联动」等高频能力内置化,同时通过SiderHeaderTitleFooterOffLayoutArea五个 Props 保持高度可定制性。结合initialSiderCollapsed/onSiderCollapsed可轻松实现折叠状态持久化,useThemedLayoutContext让你在任意页面掌控折叠行为,而swizzle则把完整源码交给开发者自由改造。对于需要快速搭建管理后台、且希望布局风格统一可维护的项目,这是一个开箱即用且可深度定制的起点。

【免费下载链接】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/13 6:42:46

支付宝当面付实战:Spring Boot集成扫码支付与回调处理

简介&#xff1a;支付宝当面付完整代码面向需要集成扫码支付功能的移动端或服务端开发者&#xff0c;是一套可直接参考落地的Java示例项目。压缩包共92个文件&#xff0c;以75个xml配置、9个java源码、2个properties配置为主体&#xff0c;辅以mvnw构建脚本、jar依赖和README说…

作者头像 李华
网站建设 2026/9/13 6:42:28

C语言基础概念与编程实践全解析

1. C语言基础概念全景解析作为一门诞生于1972年的经典编程语言&#xff0c;C语言至今仍是计算机科学教育的基石。在真正开始编写第一个"Hello World"程序之前&#xff0c;我们需要建立对基础概念的完整认知框架。这些概念就像建筑的地基&#xff0c;决定了后续代码的…

作者头像 李华
网站建设 2026/9/13 6:42:02

用AI写论文乱编文献、PPT翻车?2026开题到答辩选型指南直接抄作业

每年开学季&#xff0c;后台都会收到同一类私信&#xff1a;“学长&#xff0c;写论文到底用哪个AI&#xff1f;”“ChatGPT写的东西导师一眼就看出来了怎么办&#xff1f;”“答辩PPT有没有一键生成的&#xff0c;我真的排不动版了……” 先说一个被很多人忽略的真相&#xf…

作者头像 李华
网站建设 2026/9/13 6:41:57

C++课程设计实战:打地鼠游戏的状态机与实现

简介&#xff1a;这是面向高校C课程设计和期末大作业的实用项目资源&#xff0c;提供基于Qt框架的打地鼠游戏完整工程。代码涵盖随机生成地鼠、鼠标点击判定、计时计分、胜利与结束界面等核心模块&#xff0c;既可直接编译运行作为验收成果&#xff0c;也适合作为学习游戏循环、…

作者头像 李华
网站建设 2026/9/13 6:41:00

提示词工程实战指南:10个技巧让你的大模型输出质量翻倍

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

作者头像 李华
网站建设 2026/9/13 6:40:35

激光聚变装置光路系统设计与关键技术解析

1. 激光聚变装置的光路系统解析在加州利弗莫尔实验室的地下设施中&#xff0c;部署着当今世界最复杂的光学工程系统之一。这个由192路高能激光束构成的巨型装置&#xff0c;通过精确控制每束激光的传播路径&#xff0c;最终将兆焦耳级别的能量聚焦到毫米级的靶丸上。这套光路系…

作者头像 李华