Material-UI 表面组件 #

概述 #

表面组件是用于承载内容的容器,提供视觉层次和分组效果。

text
表面组件体系
│
├── Paper       纸张容器
├── Card        卡片
├── Accordion   手风琴
├── Collapse    折叠
└── Container   容器

Paper 纸张容器 #

基础用法 #

jsx
import { Paper, Box, Typography } from '@mui/material';

<Paper elevation={3}>
  <Box sx={{ p: 2 }}>
    <Typography>这是一个 Paper 组件</Typography>
  </Box>
</Paper>

elevation 阴影级别 #

jsx
import { Stack } from '@mui/material';

<Stack spacing={2} direction="row" sx={{ flexWrap: 'wrap' }}>
  {[0, 1, 2, 3, 4, 6, 8, 12, 16, 24].map((elevation) => (
    <Paper key={elevation} elevation={elevation} sx={{ p: 2, width: 100, height: 100, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <Typography>elevation={elevation}</Typography>
    </Paper>
  ))}
</Stack>

variant 变体 #

jsx
<Stack spacing={2}>
  <Paper variant="elevation" sx={{ p: 2 }}>
    elevation 变体(默认)
  </Paper>
  <Paper variant="outlined" sx={{ p: 2 }}>
    outlined 变体
  </Paper>
</Stack>

square 圆角 #

jsx
<Stack direction="row" spacing={2}>
  <Paper sx={{ p: 2 }}>圆角 Paper</Paper>
  <Paper square sx={{ p: 2 }}>
    无圆角 Paper
  </Paper>
</Stack>

圆形 Paper #

jsx
<Paper
  sx={{
    width: 100,
    height: 100,
    borderRadius: '50%',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
  }}
>
  圆形
</Paper>

实际应用 #

jsx
function PaperCard() {
  return (
    <Paper
      elevation={3}
      sx={{
        p: 2,
        margin: 'auto',
        maxWidth: 500,
        flexGrow: 1,
        backgroundColor: (theme) => theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
      }}
    >
      <Grid container spacing={2}>
        <Grid item>
          <ButtonBase sx={{ width: 128, height: 128 }}>
            <Img alt="complex" src="/static/images/grid/complex.jpg" />
          </ButtonBase>
        </Grid>
        <Grid item xs={12} sm container>
          <Grid item xs container direction="column" spacing={2}>
            <Grid item xs>
              <Typography gutterBottom variant="subtitle1" component="div">
                Standard license
              </Typography>
              <Typography variant="body2" gutterBottom>
                Full resolution 1920x1080 • JPEG
              </Typography>
              <Typography variant="body2" color="text.secondary">
                ID: 1030114
              </Typography>
            </Grid>
            <Grid item>
              <Typography sx={{ cursor: 'pointer' }} variant="body2">
                Remove
              </Typography>
            </Grid>
          </Grid>
          <Grid item>
            <Typography variant="subtitle1" component="div">
              $19.00
            </Typography>
          </Grid>
        </Grid>
      </Grid>
    </Paper>
  );
}

Card 卡片 #

基础卡片 #

jsx
import { Card, CardContent, Typography } from '@mui/material';

<Card sx={{ minWidth: 275 }}>
  <CardContent>
    <Typography sx={{ fontSize: 14 }} color="text.secondary" gutterBottom>
      单词卡片
    </Typography>
    <Typography variant="h5" component="div">
      be•nev•o•lent
    </Typography>
    <Typography sx={{ mb: 1.5 }} color="text.secondary">
      形容词
    </Typography>
    <Typography variant="body2">
      善良的,仁慈的
      <br />
      {'"她是一个善良的人"'}
    </Typography>
  </CardContent>
</Card>

Card 组件结构 #

text
Card 组件结构
│
├── CardHeader       卡片头部
│   ├── avatar       头像
│   ├── title        标题
│   ├── subheader    副标题
│   └── action       操作按钮
│
├── CardMedia        媒体内容
│
├── CardContent      卡片内容
│
├── CardActions      卡片操作
│
└── Collapse         可折叠内容

带媒体卡片 #

jsx
import { CardMedia, CardActions, Button } from '@mui/material';

<Card sx={{ maxWidth: 345 }}>
  <CardMedia
    sx={{ height: 140 }}
    image="/static/images/cards/contemplative-reptile.jpg"
    title="green iguana"
  />
  <CardContent>
    <Typography gutterBottom variant="h5" component="div">
      蜥蜴
    </Typography>
    <Typography variant="body2" color="text.secondary">
      蜥蜴是一类广泛分布的爬行动物,全球有超过6000种,分布于各大洲(南极洲除外)。
    </Typography>
  </CardContent>
  <CardActions>
    <Button size="small">分享</Button>
    <Button size="small">了解更多</Button>
  </CardActions>
</Card>

带头部卡片 #

jsx
import { CardHeader, Avatar, IconButton } from '@mui/material';
import { MoreVert as MoreVertIcon } from '@mui/icons-material';

<Card sx={{ maxWidth: 345 }}>
  <CardHeader
    avatar={
      <Avatar sx={{ bgcolor: 'red' }} aria-label="recipe">
        R
      </Avatar>
    }
    action={
      <IconButton aria-label="settings">
        <MoreVertIcon />
      </IconButton>
    }
    title="Shrimp and Chorizo Paella"
    subheader="September 14, 2016"
  />
  <CardMedia
    sx={{ height: 194 }}
    image="/static/images/cards/paella.jpg"
    title="Paella dish"
  />
  <CardContent>
    <Typography variant="body2" color="text.secondary">
      这是一道令人印象深刻的派对菜肴。
    </Typography>
  </CardContent>
</Card>

交互式卡片 #

jsx
import { Collapse, IconButton, CardActionArea } from '@mui/material';
import { ExpandMore as ExpandMoreIcon, Favorite as FavoriteIcon, Share as ShareIcon } from '@mui/icons-material';

function RecipeReviewCard() {
  const [expanded, setExpanded] = useState(false);

  return (
    <Card sx={{ maxWidth: 345 }}>
      <CardHeader
        avatar={<Avatar sx={{ bgcolor: 'red' }}>R</Avatar>}
        action={
          <IconButton aria-label="settings">
            <MoreVertIcon />
          </IconButton>
        }
        title="Shrimp and Chorizo Paella"
        subheader="September 14, 2016"
      />
      <CardMedia component="img" height="194" image="/static/images/cards/paella.jpg" alt="Paella dish" />
      <CardContent>
        <Typography variant="body2" color="text.secondary">
          这是一道令人印象深刻的派对菜肴。
        </Typography>
      </CardContent>
      <CardActions disableSpacing>
        <IconButton aria-label="add to favorites">
          <FavoriteIcon />
        </IconButton>
        <IconButton aria-label="share">
          <ShareIcon />
        </IconButton>
        <IconButton
          onClick={() => setExpanded(!expanded)}
          aria-expanded={expanded}
          aria-label="show more"
          sx={{ transform: !expanded ? 'rotate(0deg)' : 'rotate(180deg)', marginLeft: 'auto', transition: (theme) => theme.transitions.create('transform', { duration: theme.transitions.duration.shortest }) }}
        >
          <ExpandMoreIcon />
        </IconButton>
      </CardActions>
      <Collapse in={expanded} timeout="auto" unmountOnExit>
        <CardContent>
          <Typography paragraph>方法:</Typography>
          <Typography paragraph>热油,加入洋葱和辣椒...</Typography>
        </CardContent>
      </Collapse>
    </Card>
  );
}

可点击卡片 #

jsx
<Card sx={{ maxWidth: 345 }}>
  <CardActionArea>
    <CardMedia component="img" height="140" image="/static/images/cards/contemplative-reptile.jpg" alt="green iguana" />
    <CardContent>
      <Typography gutterBottom variant="h5" component="div">
        蜥蜴
      </Typography>
      <Typography variant="body2" color="text.secondary">
        蜥蜴是一类广泛分布的爬行动物。
      </Typography>
    </CardContent>
  </CardActionArea>
</Card>

卡片网格 #

jsx
function CardsGrid() {
  const cards = [1, 2, 3, 4, 5, 6];

  return (
    <Container sx={{ py: 8 }} maxWidth="md">
      <Grid container spacing={4}>
        {cards.map((card) => (
          <Grid item key={card} xs={12} sm={6} md={4}>
            <Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
              <CardMedia component="img" sx={{ pt: '56.25%' }} image="https://source.unsplash.com/random" alt="random" />
              <CardContent sx={{ flexGrow: 1 }}>
                <Typography gutterBottom variant="h5" component="h2">
                  Heading
                </Typography>
                <Typography>
                  This is a media card. You can use this section to describe the content.
                </Typography>
              </CardContent>
              <CardActions>
                <Button size="small">View</Button>
                <Button size="small">Edit</Button>
              </CardActions>
            </Card>
          </Grid>
        ))}
      </Grid>
    </Container>
  );
}

卡片变体 #

jsx
<Stack spacing={2}>
  <Card variant="elevation" sx={{ p: 2 }}>
    elevation 变体
  </Card>
  <Card variant="outlined" sx={{ p: 2 }}>
    outlined 变体
  </Card>
</Stack>

Accordion 手风琴 #

基础用法 #

jsx
import { Accordion, AccordionSummary, AccordionDetails, Typography } from '@mui/material';
import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';

<Stack spacing={1}>
  <Accordion>
    <AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="panel1a-content" id="panel1a-header">
      <Typography>手风琴 1</Typography>
    </AccordionSummary>
    <AccordionDetails>
      <Typography>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget.
      </Typography>
    </AccordionDetails>
  </Accordion>
  <Accordion>
    <AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls="panel2a-content" id="panel2a-header">
      <Typography>手风琴 2</Typography>
    </AccordionSummary>
    <AccordionDetails>
      <Typography>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortis eget.
      </Typography>
    </AccordionDetails>
  </Accordion>
</Stack>

默认展开 #

jsx
<Accordion defaultExpanded>
  <AccordionSummary expandIcon={<ExpandMoreIcon />}>
    <Typography>默认展开</Typography>
  </AccordionSummary>
  <AccordionDetails>
    <Typography>这个手风琴默认是展开状态</Typography>
  </AccordionDetails>
</Accordion>

禁用手风琴 #

jsx
<Accordion disabled>
  <AccordionSummary expandIcon={<ExpandMoreIcon />}>
    <Typography>禁用手风琴</Typography>
  </AccordionSummary>
  <AccordionDetails>
    <Typography>这个手风琴被禁用了</Typography>
  </AccordionDetails>
</Accordion>

受控手风琴 #

jsx
function ControlledAccordions() {
  const [expanded, setExpanded] = useState(false);

  const handleChange = (panel) => (event, isExpanded) => {
    setExpanded(isExpanded ? panel : false);
  };

  return (
    <div>
      <Accordion expanded={expanded === 'panel1'} onChange={handleChange('panel1')}>
        <AccordionSummary expandIcon={<ExpandMoreIcon />}>
          <Typography>手风琴 1</Typography>
        </AccordionSummary>
        <AccordionDetails>
          <Typography>内容 1</Typography>
        </AccordionDetails>
      </Accordion>
      <Accordion expanded={expanded === 'panel2'} onChange={handleChange('panel2')}>
        <AccordionSummary expandIcon={<ExpandMoreIcon />}>
          <Typography>手风琴 2</Typography>
        </AccordionSummary>
        <AccordionDetails>
          <Typography>内容 2</Typography>
        </AccordionDetails>
      </Accordion>
    </div>
  );
}

带图标 #

jsx
import { Settings as SettingsIcon, People as PeopleIcon, BarChart as BarChartIcon } from '@mui/icons-material';

<Accordion>
  <AccordionSummary expandIcon={<ExpandMoreIcon />}>
    <SettingsIcon sx={{ mr: 1 }} />
    <Typography>设置</Typography>
  </AccordionSummary>
  <AccordionDetails>
    <Typography>设置内容</Typography>
  </AccordionDetails>
</Accordion>

带操作按钮 #

jsx
<Accordion>
  <AccordionSummary expandIcon={<ExpandMoreIcon />}>
    <Typography sx={{ width: '33%', flexShrink: 0 }}>General settings</Typography>
    <Typography sx={{ color: 'text.secondary' }}>I am an accordion</Typography>
  </AccordionSummary>
  <AccordionDetails>
    <Typography>
      Nulla facilisi. Phasellus sollicitudin nulla et quam mattis feugiat.
    </Typography>
  </AccordionDetails>
  <AccordionActions>
    <Button>取消</Button>
    <Button>保存</Button>
  </AccordionActions>
</Accordion>

Collapse 折叠 #

基础用法 #

jsx
import { Collapse, Button, Paper } from '@mui/material';

function SimpleCollapse() {
  const [checked, setChecked] = useState(false);

  return (
    <Box>
      <Button onClick={() => setChecked(!checked)}>
        {checked ? '隐藏' : '显示'}
      </Button>
      <Collapse in={checked}>
        <Paper sx={{ p: 2, mt: 1 }}>
          <Typography>折叠内容</Typography>
        </Paper>
      </Collapse>
    </Box>
  );
}

带过渡效果 #

jsx
<Collapse in={checked} timeout={500}>
  <Paper sx={{ p: 2 }}>
    <Typography>带过渡效果的折叠内容</Typography>
  </Paper>
</Collapse>

折叠高度 #

jsx
<Collapse in={checked} collapsedSize={40}>
  <Paper sx={{ p: 2 }}>
    <Typography>
      这是一段很长的内容,当折叠时会显示部分内容。
      Lorem ipsum dolor sit amet, consectetur adipiscing elit.
    </Typography>
  </Paper>
</Collapse>

嵌套折叠 #

jsx
function NestedCollapse() {
  const [outerOpen, setOuterOpen] = useState(false);
  const [innerOpen, setInnerOpen] = useState(false);

  return (
    <Box>
      <Button onClick={() => setOuterOpen(!outerOpen)}>
        外层折叠
      </Button>
      <Collapse in={outerOpen}>
        <Paper sx={{ p: 2, m: 1 }}>
          <Typography>外层内容</Typography>
          <Button onClick={() => setInnerOpen(!innerOpen)}>
            内层折叠
          </Button>
          <Collapse in={innerOpen}>
            <Paper sx={{ p: 2, m: 1 }}>
              <Typography>内层内容</Typography>
            </Paper>
          </Collapse>
        </Paper>
      </Collapse>
    </Box>
  );
}

实战示例:FAQ 页面 #

jsx
function FAQPage() {
  const [expanded, setExpanded] = useState(false);

  const faqs = [
    {
      question: '如何开始使用?',
      answer: '首先安装依赖,然后按照文档进行配置...',
    },
    {
      question: '支持哪些浏览器?',
      answer: '支持所有现代浏览器,包括 Chrome、Firefox、Safari、Edge...',
    },
    {
      question: '如何自定义主题?',
      answer: '使用 createTheme 创建自定义主题,然后通过 ThemeProvider 应用...',
    },
  ];

  const handleChange = (panel) => (event, isExpanded) => {
    setExpanded(isExpanded ? panel : false);
  };

  return (
    <Container maxWidth="md" sx={{ py: 4 }}>
      <Typography variant="h4" gutterBottom>
        常见问题
      </Typography>
      <Stack spacing={2}>
        {faqs.map((faq, index) => (
          <Accordion
            key={index}
            expanded={expanded === `panel${index}`}
            onChange={handleChange(`panel${index}`)}
          >
            <AccordionSummary expandIcon={<ExpandMoreIcon />}>
              <Typography variant="h6">{faq.question}</Typography>
            </AccordionSummary>
            <AccordionDetails>
              <Typography>{faq.answer}</Typography>
            </AccordionDetails>
          </Accordion>
        ))}
      </Stack>
    </Container>
  );
}

实战示例:产品展示卡片 #

jsx
function ProductCard({ product }) {
  const [expanded, setExpanded] = useState(false);

  return (
    <Card sx={{ maxWidth: 345 }}>
      <CardMedia
        component="img"
        height="200"
        image={product.image}
        alt={product.name}
      />
      <CardContent>
        <Typography gutterBottom variant="h5" component="div">
          {product.name}
        </Typography>
        <Typography variant="h6" color="primary">
          ¥{product.price}
        </Typography>
        <Typography variant="body2" color="text.secondary" sx={{ mt: 1 }}>
          {product.description}
        </Typography>
      </CardContent>
      <CardActions>
        <Button size="small" color="primary">
          加入购物车
        </Button>
        <Button size="small" onClick={() => setExpanded(!expanded)}>
          {expanded ? '收起详情' : '查看详情'}
        </Button>
      </CardActions>
      <Collapse in={expanded} timeout="auto" unmountOnExit>
        <CardContent>
          <Typography paragraph>产品详情:</Typography>
          <Typography paragraph>{product.details}</Typography>
        </CardContent>
      </Collapse>
    </Card>
  );
}

下一步 #

继续学习 高级主题定制,深入了解 MUI 的主题系统!

最后更新:2026-03-28