在 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进行局部定制。例如用render与Title定制菜单渲染逻辑和顶部标题:
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):当fixed为true时,Sider 会获得position: fixed; top: 0; height: 100vh; zIndex: 999的样式,同时在其前方渲染一个占位 div(展开时 200px、折叠时 80px)避免内容被遮挡。
Sider Props 一览
| Prop | 类型 | 说明 |
|---|---|---|
Title | React.FC | 渲染在侧边栏顶部的组件(即ThemedTitle或其替换件) |
render | SiderRenderFunction | 自定义侧边栏内部菜单项与其他元素的渲染函数 |
meta | Record<string, any> | 创建菜单项路由时使用的元数据 |
fixed | boolean | 侧边栏是否固定 |
activeItemDisabled | boolean | 点击当前激活菜单项时是否禁用跳转(避免重复刷新页面),默认false |
onSiderCollapsed | (collapsed: boolean) => void | 侧边栏折叠/展开时的回调 |
siderItemsAreCollapsed | boolean | 嵌套菜单项默认是展开还是折叠,默认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> ); };源码层面,ThemedLayoutContextProvider在setSiderCollapsed内部先更新内部 state,再调用onSiderCollapsed回调(contexts/themedLayoutContext/index.tsx),因此回调一定能在状态变更后同步收到最新的collapsed值。
Header:用户信息展示与自定义
<ThemedLayout>的头部默认由<ThemedHeader>渲染,它使用useGetIdentityhook 获取当前用户信息,并在头部右侧展示用户名与头像。
需要说明的是,<ThemedHeader>只有在getIdentity返回了name或avatar时才渲染(源码见 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>还有一个实用特性:当没有显式传入icon和text时,它会回退读取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/antdRefine 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.tsx、types.ts组成,并且配有对应测试(如 index.spec.tsx 复用@refinedev/ui-tests的layoutLayoutTests套件验证基础布局行为)。因此 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中取出mobileSiderOpen、siderCollapsed、setMobileSiderOpen、setSiderCollapsed四个值并返回。Context 的默认值在 contexts/themedLayoutContext/index.tsx 中定义:siderCollapsed与mobileSiderOpen初始均为false。也正因折叠状态由 Context 管理,页面组件才能跨层级与 Sider 交互。
FAQ:如何持久化 Sider 的折叠状态
问题:刷新页面后如何恢复上次的折叠状态?
方案:将initialSiderCollapsed的初始值来自localStorage或cookie,同时用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 集成的核心布局组件,它把「侧边栏菜单自动生成」「用户身份展示」「响应式抽屉」「主题联动」等高频能力内置化,同时通过Sider、Header、Title、Footer、OffLayoutArea五个 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),仅供参考