Material UI Checkout 模板:从零搭建响应式三步结账流程的完整拆解
Material UI 官方提供了一套免费可复用的 Checkout(结账)模板,它位于仓库的 docs/data/material/getting-started/templates/checkout 目录中,实现了一个带配送地址、支付信息、订单确认三个步骤的完整结账流程,并内置亮色/暗色主题切换与移动端适配。本文基于该模板的源码逐层拆解其组件结构、状态驱动逻辑与主题配置,读完你可以直接把这套模板复制进自己的项目,并清楚知道每一处布局、主题变量和设计 token 背后的实现依据,从而按需改造成自己的电商结账页。
模板定位与使用方式
Checkout 是 Material UI 免费 React 模板集合中的一员,同系列模板还包括 Dashboard、Marketing Page、Sign In/Sign Up、Blog 等(见 模板总览文档)。所有免费模板都自带一套自定义主题和 Material Design 2 默认主题,且同时支持亮色与暗色模式;布局的各个区块通过独立文件划分,方便你把某个页面区块(比如侧栏商品清单)抽出来复用到其他页面。
按照 官方使用说明,接入步骤只有三步:
- 把
checkout和shared-theme两个文件夹复制进你的项目,或放进官方 示例项目(如 Vite、Next.js 示例工程)之一; - 确认项目已安装模板依赖:
@mui/material、@emotion/styled、@emotion/react; - 导入
Checkout组件并渲染。
模板目录结构如下(TypeScript 版本):
docs/data/material/getting-started/templates/
├── checkout/
│ ├── Checkout.tsx # 模板入口,负责步骤流程与整体布局
│ ├── components/
│ │ ├── AddressForm.tsx # 第一步:配送地址表单
│ │ ├── PaymentForm.tsx # 第二步:支付方式选择与表单
│ │ ├── Review.tsx # 第三步:订单复核
│ │ ├── Info.tsx # 商品与总价清单(桌面端侧栏)
│ │ ├── InfoMobile.tsx # 移动端清单(Drawer 抽屉)
│ │ └── SitemarkIcon.tsx # 站点图标占位
│ └── README.md
└── shared-theme/ # 所有模板共享的主题配置
├── AppTheme.tsx # 主题 Provider 封装
├── themePrimitives.ts # 设计 token:调色板、字体、阴影
├── ColorModeIconDropdown.tsx # 亮/暗模式切换下拉
└── customizations/ # 按组件类别拆分的 style overrides
整体布局:两栏 Grid 与双 Stepper 的响应式策略
入口组件 Checkout.tsx 承担了“布局骨架 + 流程控制器”双重职责。它用 @mui/material/Grid 构建了两栏容器:
- 左栏(
size={{ xs: 12, sm: 5, lg: 4 }}):在md及以上断点显示,渲染站点图标和 Info 组件(商品列表 + 总价);在xs小屏下整栏display: none; - 右栏(
size={{ sm: 12, md: 7, lg: 8 }}):主流程区域,包含步骤指示器、当前步骤表单、导航按钮,以及仅在小屏显示的商品摘要Card。
一个值得注意的响应式细节是:容器高度使用了 calc(100dvh - var(--template-frame-height, 0px))。其中 --template-frame-height 是文档站预览框架注入的 CSS 变量,在你自己的项目里该变量不存在,var(..., 0px) 的兜底值会使其退化为 100dvh,因此这段代码可以原样保留而无需修改。
模板渲染了两个 Stepper(步骤条):
// 桌面端水平步骤条:md 断点以上显示
<Stepper
id="desktop-stepper"
activeStep={activeStep}
sx={{ width: '100%', height: 40 }}
>
{steps.map((label) => (
<Step sx={{ ':first-child': { pl: 0 }, ':last-child': { pr: 0 } }} key={label}>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
// 移动端步骤条:sm~md 断点显示,使用 alternativeLabel 让标签居中于圆点
<Stepper
id="mobile-stepper"
activeStep={activeStep}
alternativeLabel
sx={{ display: { sm: 'flex', md: 'none' } }}
>
通过 display: { xs: 'none', md: 'flex' }(桌面版)与 display: { sm: 'flex', md: 'none' }(移动版)这对断点互补的显示规则,同一组 steps 数据在任意屏幕宽度下恰好只渲染一个步骤条。移动版额外设置了 '.MuiStepConnector-root': { top: { xs: 6, sm: 12 } } 来校正 alternativeLabel 模式下连接线的垂直位置。
三步流程的状态机:steps、activeStep 与 getStepContent
模板的核心流程逻辑极其精简,全部围绕一个数字状态展开:
const steps = ['Shipping address', 'Payment details', 'Review your order'];
function getStepContent(step: number) {
switch (step) {
case 0:
return <AddressForm />;
case 1:
return <PaymentForm />;
case 2:
return <Review />;
default:
throw new Error('Unknown step');
}
}
export default function Checkout(props: { disableCustomTheme?: boolean }) {
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => setActiveStep(activeStep + 1);
const handleBack = () => setActiveStep(activeStep - 1);
// ...
}
流程规则如下:
activeStep从 0 开始,Next按钮触发activeStep + 1,Previous按钮触发activeStep - 1;- 当
activeStep === steps.length(即 3)时,不再渲染任何表单,而是渲染一个完成态:Thank you for your order!、模拟订单号#140396和一个Go to my orders按钮——这就是模板内置的“下单成功”页面,无需路由即可演示完整闭环; - 导航按钮的样式也随断点变化:桌面端
Previous是variant="text"的幽灵按钮,移动端则换成variant="outlined"的全宽按钮(fullWidth),Next/Place order按钮在xs下width: '100%'、sm以上fit-content;最后一步按钮文案从Next自动切换为Place order。
另外,总价展示与步骤联动:进入“Review your order”步骤(activeStep >= 2)时,侧栏与移动端摘要里的总价从 $134.98 变为 $144.97(差额即 Review.tsx 中展示的 $9.99 运费),用来模拟“运费在复核阶段加入”的真实电商逻辑。
disableCustomTheme 属性是文档站专用的开关:置为 true 时 AppTheme 不创建自定义主题、直接透传 children,以便文档能在“默认主题”与“自定义主题”两种皮肤下截图对比。按源码注释说明,在自己项目里可以忽略或删除它。
步骤一 AddressForm:语义化表单网格
AddressForm.tsx 展示了一个“无状态、纯展示型”表单的写法。它用 Grid container spacing={3} 做 12 列网格,并通过 styled 包装出一个纵向的 FormGrid:
const FormGrid = styled(Grid)(() => ({
display: 'flex',
flexDirection: 'column',
}));
字段排布遵循地址表单的常见约定:
| 字段 | 断点宽度 | autoComplete |
|---|---|---|
| First name / Last name | md: 6(两列并排) |
given-name / family-name |
| Address line 1 / line 2 | xs: 12(整行) |
shipping address-line1/2 |
| City / State | xs: 6 |
address-level2 / address-level1 |
| Zip / Country | xs: 6 |
shipping postal-code / shipping country-name |
每个输入框都是 size="small" 的 OutlinedInput,并配套 FormLabel(通过 htmlFor 与 id 关联,保证可访问性)。所有字段都设置了标准的 autoComplete 令牌(如 shipping address-line1),让浏览器密码管理器能正确识别并自动填充。末尾还有一个 FormControlLabel + Checkbox 选项“Use this address for payment details”,演示跨步骤数据复用的交互占位。
步骤二 PaymentForm:受控输入 + 卡号格式化
PaymentForm.tsx 是模板中交互最密集的一步,包含三个技术要点。
1. 支付方式选择卡片。 用 RadioGroup(aria-label="Payment options")包裹两个 Card,分别代表信用卡和银行转账。Card 本身被 styled 扩展出一个 selected 属性:
const Card = styled(MuiCard)<{ selected?: boolean }>(({ theme }) => ({
border: '1px solid',
borderColor: (theme.vars || theme).palette.divider,
'&:hover': {
background: 'linear-gradient(to bottom right, hsla(210, 100%, 97%, 0.5) 25%, ...)',
// 暗色模式使用 theme.applyStyles('dark', {...}) 单独定义
},
variants: [
{
props: ({ selected }) => selected,
style: { borderColor: (theme.vars || theme).palette.primary.light },
},
],
}));
这里体现了 v6/v7 的两处新写法:styled 支持 variants 数组按 props 派生样式;(theme.vars || theme).palette.divider 的写法则兼容了 CSS 变量主题(theme.vars 下取值是 CSS 变量引用)。选中态由 selected={paymentType === 'creditCard'} 驱动,卡片内部图标颜色也随之切换为 primary.main。
2. 输入格式化逻辑。 三个受控输入框各自有 onChange 处理器,用正则完成纯客户端格式化:
// 卡号:只保留数字,每 4 位插入一个空格,最长 16 位
const value = event.target.value.replace(/\D/g, '');
const formattedValue = value.replace(/(\d{4})(?=\d)/g, '$1 ');
if (value.length <= 16) setCardNumber(formattedValue);
// 有效期:自动插入 “/”,最长 4 位数字(MM/YY)
const formattedValue = value.replace(/(\d{2})(?=\d{2})/, '$1/');
// CVV:只保留数字,最长 3 位
3. 按支付方式切换内容。 选择信用卡时渲染 PaymentContainer(一个固定高度、渐变背景、带阴影的“卡片视觉容器”,高度随断点从 300px 到 375px 变化);选择银行转账时则渲染 Alert severity="warning" 提示“Your order will be processed once we receive the funds.”加一组银行信息(Bank / Account number / Routing number),演示了同一表单容器内的条件渲染模式。
步骤三 Review:订单摘要与明细
Review.tsx 用 List + ListItemText 展示三段金额(Products $134.98、Shipping $9.99、Total $144.97),再用 Stack 的 divider={<Divider flexItem />} 特性在“Shipment details”和“Payment details”两块之间自动插入分隔线。注意此处展示的都是静态演示数据(const addresses = [...]、const payments = [...]),接入真实业务时需要替换为前两步表单收集到的状态。
商品信息:桌面端 Info 与移动端 Drawer
Info.tsx 接收唯一的 totalPrice: string prop,渲染“Total + 商品列表”。商品数据是一个 4 项的静态数组(Professional plan $15.00、Dedicated support Free、Hardware $69.99、Landing page template $49.99),用 List disablePadding 渲染,每项由 ListItemText(名称 + 描述)与右对齐价格组成——把商品数据与展示分离,正是模板“区块可复用”设计思路的体现。
移动端侧栏被隐藏后,商品清单改由 InfoMobile.tsx 承接:一个小屏 Card 里放“Selected products + 总价”,点击 View details 按钮打开一个 anchor="top" 的 Drawer,Drawer 内部复用同一个 Info 组件。Drawer 的 paper 上设置了 top: 'var(--template-frame-height, 0px)',同样是文档站框架兼容写法。
主题体系:shared-theme 的设计 token 与 CSS 变量
所有模板共享 shared-theme 目录。AppTheme 组件是主题注入点:
export default function AppTheme(props: AppThemeProps) {
const { children, disableCustomTheme, themeComponents } = props;
const theme = React.useMemo(() => {
return disableCustomTheme
? {}
: createTheme({
cssVariables: {
colorSchemeSelector: 'data-mui-color-scheme',
cssVarPrefix: 'template',
},
colorSchemes,
typography,
shadows,
shape,
components: {
...inputsCustomizations,
...dataDisplayCustomizations,
...feedbackCustomizations,
...navigationCustomizations,
...surfacesCustomizations,
...themeComponents,
},
});
}, [disableCustomTheme, themeComponents]);
if (disableCustomTheme) {
return <React.Fragment>{children}</React.Fragment>;
}
return (
<ThemeProvider theme={theme} disableTransitionOnChange>
{children}
</ThemeProvider>
);
}
几个关键配置:
cssVariables: { colorSchemeSelector: 'data-mui-color-scheme', cssVarPrefix: 'template' }:启用 CSS 变量模式,亮/暗切换通过给元素打data-mui-color-scheme属性完成,无需重渲染;前缀template保证生成的 CSS 变量名(如--template-palette-primary-main)不会与宿主应用冲突。colorSchemes:v6 引入的双色方案对象,light与dark各自携带完整的 palette(primary 用品牌蓝brand色阶、divider、background、text、action,以及一个自定义的baseShadow字段),切换颜色模式时只替换变量值。React.useMemo+disableTransitionOnChange:主题对象按 props 记忆化;ThemeProvider关闭切换过渡,避免模式切换时组件样式闪动。themeComponents扩展点:允许调用方在不修改模板文件的前提下追加组件级 style overrides,且排在五个内置 customization 之后(后者优先)。
themePrimitives.ts 里定义了全部设计 token:
brand/gray/green/orange/red五个 HSL 色阶(50~900),例如brand.main = hsl(210, 98%, 42%);文件顶部还通过declare module '@mui/material/styles'扩展了Palette类型,注册了baseShadow: string,让自定义 palette 字段获得类型支持;typography:fontFamily: 'Inter, sans-serif',h1 48px(letterSpacing: -0.5)到 caption 12px,全部经defaultTheme.typography.pxToRem转成 rem;shape.borderRadius: 8统一圆角;shadows是一个小技巧——它复制默认阴影数组,把shadows[1]替换成var(--template-palette-baseShadow),这样每个层级 1 的阴影都跟随颜色模式变化(暗色模式使用更重、更明显的 hsla 阴影),而 2 级及以上保持默认值。
组件级样式按类别拆在 customizations/ 下(inputs.tsx、dataDisplay.tsx、feedback.tsx、navigation.tsx、surfaces.tsx),AppTheme 把它们展开合并进 components 字段——这种“按组件类别分文件”的组织方式也是模板推荐的主题拆分粒度。
亮色/暗色模式:ColorModeIconDropdown 与 CssBaseline
模板右上角悬浮一个模式切换入口,由 ColorModeIconDropdown.tsx 实现。它基于 @mui/material/styles 的 useColorScheme hook:
const { mode, systemMode, setMode } = useColorScheme();
// setMode('system' | 'light' | 'dark')
useColorScheme 返回当前生效模式、跟随系统的实际模式(systemMode)以及 setMode 写入函数;mode 为 null 时(系统尚未水合,防止 SSR 闪烁),组件渲染一个占位 Box 而非按钮,等 hydration 完成后才出现图标。切换菜单提供 System / Light / Dark 三个选项,配合 CssBaseline enableColorScheme(在 Checkout.tsx 顶层渲染)完成全局的暗色基线:CssBaseline 负责归零浏览器默认样式并引入 @mui/material 的基础色板 CSS 变量,enableColorScheme 则让变量随 data-mui-color-scheme 属性联动。
把模板接入你的项目
结合前文,落地时的建议路径是:
- 复制文件:把 checkout 与 shared-theme 两个目录拷入项目(例如官方 Vite 示例 或 Next.js 示例 的
src下),保持相对导入../shared-theme/AppTheme的路径关系不变; - 依赖检查:安装
@mui/material、@emotion/styled、@emotion/react(模板中用到的@mui/icons-material也需一并安装,Checkout.tsx引用了ChevronLeftRounded、ChevronRightRounded等图标); - 渲染入口:
<Checkout />即可,它自带AppTheme包裹,不需要在外层再套ThemeProvider;若宿主应用已有自己的主题,可传disableCustomTheme关闭模板主题,通过themeComponents只追加增量样式; - 替换演示数据:
Info.tsx的商品数组、Review.tsx的地址/卡号常量、Checkout.tsx中硬编码的两个总价,都是接入真实订单状态的替换点;activeStep目前是无校验的直接加减,接入表单校验(如 react-hook-form)后应在handleNext中做拦截。
需要说明的是,--template-frame-height 变量、disableCustomTheme 属性以及组件里的 data-screenshot 标记均为文档站预览框架服务,源码注释已明确“可以在自己的项目中忽略或删除”,不影响模板在生产应用中直接运行。
小结
Material UI 的 Checkout 模板用约 20 个文件演示了一条完整的“布局 + 流程 + 主题”最佳实践链:两栏 Grid 与双 Stepper 的断点互补策略、activeStep 单状态驱动的步骤机、styled + variants + theme.applyStyles('dark') 的组件级双模式样式、以及 CSS 变量主题(cssVarPrefix: 'template')+ colorSchemes 的 token 化配色。它既可以直接复制使用,也是学习 Material UI v7 新版主题 API 的一个高密度样本;同一目录下还有 Dashboard、Marketing Page 等模板,均采用相同的 shared-theme 主题体系,可按同样的方式拆解复用。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00