首页
/ Material UI Checkout 模板:从零搭建响应式三步结账流程的完整拆解

Material UI Checkout 模板:从零搭建响应式三步结账流程的完整拆解

2026-09-06 13:01:20作者:薛曦旖Francesca

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 默认主题,且同时支持亮色与暗色模式;布局的各个区块通过独立文件划分,方便你把某个页面区块(比如侧栏商品清单)抽出来复用到其他页面。

按照 官方使用说明,接入步骤只有三步:

  1. checkoutshared-theme 两个文件夹复制进你的项目,或放进官方 示例项目(如 Vite、Next.js 示例工程)之一;
  2. 确认项目已安装模板依赖:@mui/material@emotion/styled@emotion/react
  3. 导入 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 + 1Previous 按钮触发 activeStep - 1
  • activeStep === steps.length(即 3)时,不再渲染任何表单,而是渲染一个完成态:Thank you for your order!、模拟订单号 #140396 和一个 Go to my orders 按钮——这就是模板内置的“下单成功”页面,无需路由即可演示完整闭环;
  • 导航按钮的样式也随断点变化:桌面端 Previousvariant="text" 的幽灵按钮,移动端则换成 variant="outlined" 的全宽按钮(fullWidth),Next/Place order 按钮在 xswidth: '100%'sm 以上 fit-content;最后一步按钮文案从 Next 自动切换为 Place order

另外,总价展示与步骤联动:进入“Review your order”步骤(activeStep >= 2)时,侧栏与移动端摘要里的总价从 $134.98 变为 $144.97(差额即 Review.tsx 中展示的 $9.99 运费),用来模拟“运费在复核阶段加入”的真实电商逻辑。

disableCustomTheme 属性是文档站专用的开关:置为 trueAppTheme 不创建自定义主题、直接透传 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(通过 htmlForid 关联,保证可访问性)。所有字段都设置了标准的 autoComplete 令牌(如 shipping address-line1),让浏览器密码管理器能正确识别并自动填充。末尾还有一个 FormControlLabel + Checkbox 选项“Use this address for payment details”,演示跨步骤数据复用的交互占位。

步骤二 PaymentForm:受控输入 + 卡号格式化

PaymentForm.tsx 是模板中交互最密集的一步,包含三个技术要点。

1. 支付方式选择卡片。RadioGrouparia-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.tsxList + ListItemText 展示三段金额(Products $134.98、Shipping $9.99、Total $144.97),再用 Stackdivider={<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 引入的双色方案对象,lightdark 各自携带完整的 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 字段获得类型支持;
  • typographyfontFamily: '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.tsxdataDisplay.tsxfeedback.tsxnavigation.tsxsurfaces.tsx),AppTheme 把它们展开合并进 components 字段——这种“按组件类别分文件”的组织方式也是模板推荐的主题拆分粒度。

亮色/暗色模式:ColorModeIconDropdown 与 CssBaseline

模板右上角悬浮一个模式切换入口,由 ColorModeIconDropdown.tsx 实现。它基于 @mui/material/stylesuseColorScheme hook:

const { mode, systemMode, setMode } = useColorScheme();
// setMode('system' | 'light' | 'dark')

useColorScheme 返回当前生效模式、跟随系统的实际模式(systemMode)以及 setMode 写入函数;modenull 时(系统尚未水合,防止 SSR 闪烁),组件渲染一个占位 Box 而非按钮,等 hydration 完成后才出现图标。切换菜单提供 System / Light / Dark 三个选项,配合 CssBaseline enableColorScheme(在 Checkout.tsx 顶层渲染)完成全局的暗色基线:CssBaseline 负责归零浏览器默认样式并引入 @mui/material 的基础色板 CSS 变量,enableColorScheme 则让变量随 data-mui-color-scheme 属性联动。

把模板接入你的项目

结合前文,落地时的建议路径是:

  1. 复制文件:把 checkoutshared-theme 两个目录拷入项目(例如官方 Vite 示例Next.js 示例src 下),保持相对导入 ../shared-theme/AppTheme 的路径关系不变;
  2. 依赖检查:安装 @mui/material@emotion/styled@emotion/react(模板中用到的 @mui/icons-material 也需一并安装,Checkout.tsx 引用了 ChevronLeftRoundedChevronRightRounded 等图标);
  3. 渲染入口<Checkout /> 即可,它自带 AppTheme 包裹,不需要在外层再套 ThemeProvider;若宿主应用已有自己的主题,可传 disableCustomTheme 关闭模板主题,通过 themeComponents 只追加增量样式;
  4. 替换演示数据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 主题体系,可按同样的方式拆解复用。

登录后查看全文
热门项目推荐
相关项目推荐