名称:原子设计模板 描述:用于在没有真实内容的情况下创建页面布局。模板使用生物体、分子和原子定义页面的骨架结构。 允许工具:
- Bash
- 读取
- 写入
- 编辑
- Glob
- Grep
原子设计:模板
掌握模板的创建 - 页面级布局,定义内容结构而无需实际内容。模板建立页面将使用的骨架结构。
什么是模板?
模板是页面级对象,将组件放置到布局中,并阐明设计的基础内容结构。它们是:
- 由生物体组成:将生物体排列到页面布局中
- 内容无关:使用占位内容,而非真实数据
- 结构性:定义内容类型将出现的位置
- 可重用:同一模板可用于多个页面
- 响应式:处理所有视口大小
常见模板类型
营销模板
- 落地页面布局
- 首页布局
- 产品展示布局
- 关于/公司布局
应用模板
- 仪表板布局
- 设置页面布局
- 个人资料页面布局
- 列表/详情页面布局
内容模板
- 博客文章布局
- 文章布局
- 文档布局
- 帮助中心布局
电子商务模板
- 产品列表布局
- 产品详情布局
- 结账布局
- 订单确认布局
MainLayout 模板示例
完整实现
// templates/MainLayout/MainLayout.tsx
import React from 'react';
import { Header, type HeaderProps } from '@/components/organisms/Header';
import { Footer, type FooterProps } from '@/components/organisms/Footer';
import styles from './MainLayout.module.css';
export interface MainLayoutProps {
/** 头部配置 */
headerProps: HeaderProps;
/** 底部配置 */
footerProps: FooterProps;
/** 主要内容 */
children: React.ReactNode;
/** 显示面包屑 */
showBreadcrumbs?: boolean;
/** 面包屑组件 */
breadcrumbs?: React.ReactNode;
/** 最大内容宽度 */
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
/** 页面背景颜色 */
background?: 'white' | 'gray' | 'primary';
}
export const MainLayout: React.FC<MainLayoutProps> = ({
headerProps,
footerProps,
children,
showBreadcrumbs = false,
breadcrumbs,
maxWidth = 'lg',
background = 'white',
}) => {
return (
<div className={`${styles.layout} ${styles[`bg-${background}`]}`}>
<Header {...headerProps} />
<main className={styles.main}>
{showBreadcrumbs && breadcrumbs && (
<div className={styles.breadcrumbs}>{breadcrumbs}</div>
)}
<div className={`${styles.content} ${styles[`max-${maxWidth}`]}`}>
{children}
</div>
</main>
<Footer {...footerProps} />
</div>
);
};
MainLayout.displayName = 'MainLayout';
/* templates/MainLayout/MainLayout.module.css */
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
}
.breadcrumbs {
padding: 16px 24px;
background-color: var(--color-neutral-50);
border-bottom: 1px solid var(--color-neutral-200);
}
.content {
flex: 1;
margin: 0 auto;
padding: 24px;
width: 100%;
}
/* 最大宽度变体 */
.max-sm {
max-width: 640px;
}
.max-md {
max-width: 768px;
}
.max-lg {
max-width: 1024px;
}
.max-xl {
max-width: 1280px;
}
.max-full {
max-width: 100%;
}
/* 背景变体 */
.bg-white {
background-color: var(--color-white);
}
.bg-gray {
background-color: var(--color-neutral-50);
}
.bg-primary {
background-color: var(--color-primary-50);
}
/* 响应式调整 */
@media (max-width: 768px) {
.content {
padding: 16px;
}
}
DashboardLayout 模板示例
// templates/DashboardLayout/DashboardLayout.tsx
import React, { useState } from 'react';
import { Header } from '@/components/organisms/Header';
import { Sidebar, type SidebarProps } from '@/components/organisms/Sidebar';
import styles from './DashboardLayout.module.css';
export interface DashboardLayoutProps {
/** 头部属性 */
headerProps: {
logo: React.ReactNode;
user?: { name: string; email: string; avatar?: string };
onLogout?: () => void;
};
/** 侧边栏属性 */
sidebarProps: SidebarProps;
/** 主要内容 */
children: React.ReactNode;
/** 页面标题 */
pageTitle?: string;
/** 页面描述 */
pageDescription?: string;
/** 页面操作(按钮等) */
pageActions?: React.ReactNode;
/** 侧边栏初始折叠 */
sidebarCollapsed?: boolean;
}
export const DashboardLayout: React.FC<DashboardLayoutProps> = ({
headerProps,
sidebarProps,
children,
pageTitle,
pageDescription,
pageActions,
sidebarCollapsed = false,
}) => {
const [isCollapsed, setIsCollapsed] = useState(sidebarCollapsed);
return (
<div className={styles.layout}>
{/* 顶部头部 */}
<Header
logo={headerProps.logo}
navigation={[]}
user={headerProps.user}
onLogout={headerProps.onLogout}
showSearch={false}
/>
<div className={styles.body}>
{/* 侧边栏 */}
<Sidebar
{...sidebarProps}
isCollapsed={isCollapsed}
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
/>
{/* 主要内容区域 */}
<main className={styles.main}>
{/* 页面头部 */}
{(pageTitle || pageActions) && (
<header className={styles.pageHeader}>
<div className={styles.titleSection}>
{pageTitle && <h1 className={styles.pageTitle}>{pageTitle}</h1>}
{pageDescription && (
<p className={styles.pageDescription}>{pageDescription}</p>
)}
</div>
{pageActions && (
<div className={styles.pageActions}>{pageActions}</div>
)}
</header>
)}
{/* 页面内容 */}
<div className={styles.content}>{children}</div>
</main>
</div>
</div>
);
};
DashboardLayout.displayName = 'DashboardLayout';
/* templates/DashboardLayout/DashboardLayout.module.css */
.layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.body {
display: flex;
flex: 1;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
overflow-x: hidden;
background-color: var(--color-neutral-50);
}
.pageHeader {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 24px;
padding: 24px;
background-color: var(--color-white);
border-bottom: 1px solid var(--color-neutral-200);
}
.titleSection {
flex: 1;
}
.pageTitle {
margin: 0;
font-size: 24px;
font-weight: 600;
color: var(--color-neutral-900);
}
.pageDescription {
margin: 4px 0 0;
font-size: 14px;
color: var(--color-neutral-500);
}
.pageActions {
display: flex;
gap: 12px;
flex-shrink: 0;
}
.content {
flex: 1;
padding: 24px;
overflow-y: auto;
}
@media (max-width: 768px) {
.pageHeader {
flex-direction: column;
align-items: stretch;
}
.pageActions {
margin-top: 16px;
}
.content {
padding: 16px;
}
}
AuthLayout 模板示例
// templates/AuthLayout/AuthLayout.tsx
import React from 'react';
import styles from './AuthLayout.module.css';
export interface AuthLayoutProps {
/** 徽标元素 */
logo: React.ReactNode;
/** 页面标题 */
title: string;
/** 页面副标题 */
subtitle?: string;
/** 表单内容 */
children: React.ReactNode;
/** 底部内容(链接等) */
footer?: React.ReactNode;
/** 背景图片URL */
backgroundImage?: string;
/** 显示装饰性侧面板 */
showSidePanel?: boolean;
/** 侧面板内容 */
sidePanelContent?: React.ReactNode;
}
export const AuthLayout: React.FC<AuthLayoutProps> = ({
logo,
title,
subtitle,
children,
footer,
backgroundImage,
showSidePanel = false,
sidePanelContent,
}) => {
return (
<div className={styles.layout}>
{/* 侧面板(可选) */}
{showSidePanel && (
<div
className={styles.sidePanel}
style={
backgroundImage
? { backgroundImage: `url(${backgroundImage})` }
: undefined
}
>
<div className={styles.sidePanelContent}>{sidePanelContent}</div>
</div>
)}
{/* 主要内容 */}
<div className={styles.main}>
<div className={styles.container}>
{/* 徽标 */}
<div className={styles.logo}>{logo}</div>
{/* 头部 */}
<header className={styles.header}>
<h1 className={styles.title}>{title}</h1>
{subtitle && <p className={styles.subtitle}>{subtitle}</p>}
</header>
{/* 表单内容 */}
<div className={styles.content}>{children}</div>
{/* 底部 */}
{footer && <footer className={styles.footer}>{footer}</footer>}
</div>
</div>
</div>
);
};
AuthLayout.displayName = 'AuthLayout';
/* templates/AuthLayout/AuthLayout.module.css */
.layout {
display: flex;
min-height: 100vh;
}
.sidePanel {
display: none;
width: 50%;
background-color: var(--color-primary-600);
background-size: cover;
background-position: center;
position: relative;
}
@media (min-width: 1024px) {
.sidePanel {
display: flex;
align-items: center;
justify-content: center;
}
}
.sidePanelContent {
padding: 48px;
color: var(--color-white);
text-align: center;
}
.main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background-color: var(--color-neutral-50);
}
.container {
width: 100%;
max-width: 400px;
}
.logo {
text-align: center;
margin-bottom: 32px;
}
.header {
text-align: center;
margin-bottom: 32px;
}
.title {
margin: 0;
font-size: 28px;
font-weight: 700;
color: var(--color-neutral-900);
}
.subtitle {
margin: 8px 0 0;
font-size: 16px;
color: var(--color-neutral-500);
}
.content {
background-color: var(--color-white);
padding: 32px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.footer {
margin-top: 24px;
text-align: center;
font-size: 14px;
color: var(--color-neutral-500);
}
.footer a {
color: var(--color-primary-500);
text-decoration: none;
}
.footer a:hover {
text-decoration: underline;
}
ProductListingLayout 模板示例
// templates/ProductListingLayout/ProductListingLayout.tsx
import React from 'react';
import { MainLayout, type MainLayoutProps } from '../MainLayout';
import styles from './ProductListingLayout.module.css';
export interface ProductListingLayoutProps {
/** 主要布局属性 */
layoutProps: Omit<MainLayoutProps, 'children'>;
/** 类别标题 */
categoryTitle: string;
/** 类别描述 */
categoryDescription?: string;
/** 产品数量 */
productCount: number;
/** 过滤侧边栏内容 */
filters: React.ReactNode;
/** 排序/视图控制 */
controls: React.ReactNode;
/** 产品网格内容 */
products: React.ReactNode;
/** 分页内容 */
pagination?: React.ReactNode;
/** 在移动设备上显示过滤器 */
mobileFiltersOpen?: boolean;
/** 切换移动过滤器 */
onToggleMobileFilters?: () => void;
}
export const ProductListingLayout: React.FC<ProductListingLayoutProps> = ({
layoutProps,
categoryTitle,
categoryDescription,
productCount,
filters,
controls,
products,
pagination,
mobileFiltersOpen = false,
onToggleMobileFilters,
}) => {
return (
<MainLayout {...layoutProps} maxWidth="xl">
{/* 类别头部 */}
<header className={styles.header}>
<div className={styles.titleSection}>
<h1 className={styles.title}>{categoryTitle}</h1>
{categoryDescription && (
<p className={styles.description}>{categoryDescription}</p>
)}
<span className={styles.count}>{productCount} 产品</span>
</div>
</header>
<div className={styles.body}>
{/* 桌面过滤器 */}
<aside className={styles.sidebar}>
<div className={styles.sidebarContent}>{filters}</div>
</aside>
{/* 移动过滤器覆盖层 */}
{mobileFiltersOpen && (
<div className={styles.mobileFilters}>
<div className={styles.mobileFiltersHeader}>
<h2>过滤器</h2>
<button onClick={onToggleMobileFilters}>关闭</button>
</div>
<div className={styles.mobileFiltersContent}>{filters}</div>
</div>
)}
{/* 主要内容 */}
<div className={styles.main}>
{/* 控制栏 */}
<div className={styles.controls}>
<button
className={styles.mobileFilterButton}
onClick={onToggleMobileFilters}
>
过滤器
</button>
{controls}
</div>
{/* 产品网格 */}
<div className={styles.products}>{products}</div>
{/* 分页 */}
{pagination && (
<div className={styles.pagination}>{pagination}</div>
)}
</div>
</div>
</MainLayout>
);
};
ProductListingLayout.displayName = 'ProductListingLayout';
BlogPostLayout 模板示例
// templates/BlogPostLayout/BlogPostLayout.tsx
import React from 'react';
import { MainLayout, type MainLayoutProps } from '../MainLayout';
import { Avatar } from '@/components/atoms/Avatar';
import { Text } from '@/components/atoms/Typography';
import styles from './BlogPostLayout.module.css';
export interface Author {
name: string;
avatar?: string;
bio?: string;
}
export interface BlogPostLayoutProps {
/** 主要布局属性 */
layoutProps: Omit<MainLayoutProps, 'children'>;
/** 文章标题 */
title: string;
/** 文章副标题 */
subtitle?: string;
/** 特色图片 */
featuredImage?: string;
/** 作者信息 */
author: Author;
/** 发布日期 */
publishedAt: string;
/** 阅读时间 */
readingTime?: string;
/** 文章类别/标签 */
tags?: React.ReactNode;
/** 主要文章内容 */
children: React.ReactNode;
/** 目录 */
tableOfContents?: React.ReactNode;
/** 作者简介卡片 */
showAuthorBio?: boolean;
/** 相关文章 */
relatedPosts?: React.ReactNode;
/** 评论部分 */
comments?: React.ReactNode;
/** 社交分享按钮 */
shareButtons?: React.ReactNode;
}
export const BlogPostLayout: React.FC<BlogPostLayoutProps> = ({
layoutProps,
title,
subtitle,
featuredImage,
author,
publishedAt,
readingTime,
tags,
children,
tableOfContents,
showAuthorBio = true,
relatedPosts,
comments,
shareButtons,
}) => {
const formattedDate = new Date(publishedAt).toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
return (
<MainLayout {...layoutProps} maxWidth="md">
<article className={styles.article}>
{/* 文章头部 */}
<header className={styles.header}>
{tags && <div className={styles.tags}>{tags}</div>}
<h1 className={styles.title}>{title}</h1>
{subtitle && <p className={styles.subtitle}>{subtitle}</p>}
{/* 作者和元数据 */}
<div className={styles.meta}>
<div className={styles.author}>
<Avatar
src={author.avatar}
alt={author.name}
initials={author.name.slice(0, 2).toUpperCase()}
size="md"
/>
<div className={styles.authorInfo}>
<Text weight="semibold">{author.name}</Text>
<Text size="sm" color="muted">
{formattedDate}
{readingTime && ` · ${readingTime}`}
</Text>
</div>
</div>
{shareButtons && (
<div className={styles.share}>{shareButtons}</div>
)}
</div>
</header>
{/* 特色图片 */}
{featuredImage && (
<figure className={styles.featuredImage}>
<img src={featuredImage} alt={title} />
</figure>
)}
{/* 内容与可选目录 */}
<div className={styles.contentWrapper}>
{/* 目录(桌面) */}
{tableOfContents && (
<aside className={styles.toc}>
<div className={styles.tocContent}>{tableOfContents}</div>
</aside>
)}
{/* 主要内容 */}
<div className={styles.content}>{children}</div>
</div>
{/* 作者简介 */}
{showAuthorBio && (
<footer className={styles.authorBio}>
<Avatar
src={author.avatar}
alt={author.name}
initials={author.name.slice(0, 2).toUpperCase()}
size="lg"
/>
<div>
<Text weight="semibold" size="lg">
{author.name}
</Text>
{author.bio && <Text color="muted">{author.bio}</Text>}
</div>
</footer>
)}
{/* 分享(底部) */}
{shareButtons && (
<div className={styles.bottomShare}>{shareButtons}</div>
)}
</article>
{/* 相关文章 */}
{relatedPosts && (
<section className={styles.relatedPosts}>
<h2>相关文章</h2>
{relatedPosts}
</section>
)}
{/* 评论 */}
{comments && (
<section className={styles.comments}>{comments}</section>
)}
</MainLayout>
);
};
BlogPostLayout.displayName = 'BlogPostLayout';
TwoColumnLayout 模板示例
// templates/TwoColumnLayout/TwoColumnLayout.tsx
import React from 'react';
import styles from './TwoColumnLayout.module.css';
export interface TwoColumnLayoutProps {
/** 左列(通常为主要内容) */
main: React.ReactNode;
/** 右列(通常为侧边栏) */
sidebar: React.ReactNode;
/** 侧边栏位置 */
sidebarPosition?: 'left' | 'right';
/** 侧边栏宽度 */
sidebarWidth?: 'narrow' | 'medium' | 'wide';
/** 粘性侧边栏 */
stickySidebar?: boolean;
/** 在移动设备上反转(先显示侧边栏) */
reverseMobile?: boolean;
/** 列之间的间隙 */
gap?: 'sm' | 'md' | 'lg';
}
export const TwoColumnLayout: React.FC<TwoColumnLayoutProps> = ({
main,
sidebar,
sidebarPosition = 'right',
sidebarWidth = 'medium',
stickySidebar = false,
reverseMobile = false,
gap = 'md',
}) => {
const layoutClass = [
styles.layout,
styles[`sidebar-${sidebarPosition}`],
styles[`width-${sidebarWidth}`],
styles[`gap-${gap}`],
reverseMobile && styles.reverseMobile,
]
.filter(Boolean)
.join(' ');
const sidebarClass = [
styles.sidebar,
stickySidebar && styles.sticky,
]
.filter(Boolean)
.join(' ');
return (
<div className={layoutClass}>
<main className={styles.main}>{main}</main>
<aside className={sidebarClass}>{sidebar}</aside>
</div>
);
};
TwoColumnLayout.displayName = 'TwoColumnLayout';
最佳实践
1. 使用占位内容
// 好:模板带有占位内容槽
const ProductDetailLayout = ({
productGallery, // 画廊组件的占位符
productInfo, // 产品详情的占位符
productTabs, // 标签的占位符
relatedProducts, // 推荐产品的占位符
}) => (
<div>
<section>{productGallery}</section>
<section>{productInfo}</section>
<section>{productTabs}</section>
<section>{relatedProducts}</section>
</div>
);
// 坏:模板带有硬编码内容
const ProductDetailLayout = ({ product }) => (
<div>
<ProductGallery images={product.images} /> {/* 太具体 */}
<h1>{product.name}</h1> {/* 真实内容 */}
<p>{product.description}</p>
</div>
);
2. 定义清晰的内容区域
// 好:清晰、命名的内容槽
interface PageTemplateProps {
header: React.ReactNode;
hero?: React.ReactNode;
main: React.ReactNode;
sidebar?: React.ReactNode;
footer: React.ReactNode;
}
// 坏:仅通用子元素
interface PageTemplateProps {
children: React.ReactNode;
}
3. 处理响应式布局
// 好:内置响应式考虑
const DashboardLayout = ({ sidebar, main }) => (
<div className={styles.layout}>
<aside className={styles.sidebar}>{sidebar}</aside>
<main className={styles.main}>{main}</main>
</div>
);
// CSS 处理响应式行为
// .sidebar { @media (max-width: 768px) { display: none; } }
4. 保持模板简洁
// 好:模板仅排列生物体
const MainLayout = ({ header, main, footer }) => (
<div className={styles.layout}>
<div className={styles.header}>{header}</div>
<div className={styles.main}>{main}</div>
<div className={styles.footer}>{footer}</div>
</div>
);
// 坏:模板带有业务逻辑
const MainLayout = ({ userId }) => {
const user = useUser(userId); // 获取数据
const isAdmin = user?.role === 'admin'; // 业务逻辑
return (
<div>
<Header user={user} showAdmin={isAdmin} />
{/* ... */}
</div>
);
};
要避免的反模式
1. 模板带有真实内容
// 坏:硬编码真实内容
const HomepageLayout = () => (
<div>
<h1>欢迎来到我们的商店</h1> {/* 真实内容! */}
<p>购买我们的最新系列...</p> {/* 真实内容! */}
</div>
);
// 好:内容作为属性/子元素传递
const HomepageLayout = ({ heroTitle, heroDescription }) => (
<div>
<h1>{heroTitle}</h1>
<p>{heroDescription}</p>
</div>
);
2. 过度嵌套的模板
// 坏:模板包含模板
const AppLayout = () => (
<BaseLayout>
<AuthLayout>
<DashboardLayout>
{/* 嵌套过多 */}
</DashboardLayout>
</AuthLayout>
</BaseLayout>
);
// 好:直接选择适当的模板
const AppPage = () => (
<DashboardLayout>
{/* 内容 */}
</DashboardLayout>
);
3. 模板属性过多
// 坏:太多配置选项
interface LayoutProps {
showHeader: boolean;
showFooter: boolean;
showSidebar: boolean;
sidebarPosition: 'left' | 'right';
headerVariant: 'default' | 'minimal' | 'transparent';
footerVariant: 'default' | 'minimal';
maxWidth: 'sm' | 'md' | 'lg' | 'xl';
// ... 20 更多属性
}
// 好:创建单独的模板
const FullPageLayout = ({ ... }) => { ... };
const MinimalLayout = ({ ... }) => { ... };
const SidebarLayout = ({ ... }) => { ... };
模板组合模式
嵌套布局
// 所有页面的基础布局
const BaseLayout = ({ children }) => (
<div className="base">
<SkipLink />
{children}
</div>
);
// 营销布局扩展基础
const MarketingLayout = ({ children }) => (
<BaseLayout>
<MarketingHeader />
<main>{children}</main>
<MarketingFooter />
</BaseLayout>
);
// 应用布局扩展基础
const AppLayout = ({ children }) => (
<BaseLayout>
<AppHeader />
<main>{children}</main>
</BaseLayout>
);
基于槽的布局
interface SlotLayoutProps {
slots: {
header?: React.ReactNode;
sidebar?: React.ReactNode;
main: React.ReactNode;
footer?: React.ReactNode;
};
}
const SlotLayout: React.FC<SlotLayoutProps> = ({ slots }) => (
<div className={styles.layout}>
{slots.header && <header>{slots.header}</header>}
<div className={styles.body}>
{slots.sidebar && <aside>{slots.sidebar}</aside>}
<main>{slots.main}</main>
</div>
{slots.footer && <footer>{slots.footer}</footer>}
</div>
);
何时使用此技能
- 创建页面结构模式
- 构建可重用的布局组件
- 建立一致的页面架构
- 设置响应式框架
- 定义内容槽模式
相关技能
atomic-design-fundamentals- 核心方法论概述atomic-design-organisms- 构建复杂生物体atomic-design-integration- 框架特定模式