Material-UI 反馈组件 #

概述 #

MUI 提供了丰富的反馈组件,用于向用户提供操作反馈和状态提示。

text
反馈组件体系
│
├── Dialog        对话框
├── Snackbar      消息提示
├── Alert         警告提示
├── Progress      进度指示器
├── Backdrop      背景遮罩
├── Skeleton      骨架屏
└── Popover       弹出框

Dialog 对话框 #

基础对话框 #

jsx
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material';

function AlertDialog() {
  const [open, setOpen] = useState(false);

  const handleClickOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);

  return (
    <>
      <Button variant="outlined" onClick={handleClickOpen}>
        打开对话框
      </Button>
      <Dialog open={open} onClose={handleClose}>
        <DialogTitle>确认操作</DialogTitle>
        <DialogContent>
          <DialogContentText>
            你确定要执行此操作吗?此操作无法撤销。
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose}>取消</Button>
          <Button onClick={handleClose} autoFocus>
            确认
          </Button>
        </DialogActions>
      </Dialog>
    </>
  );
}

表单对话框 #

jsx
function FormDialog() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button variant="outlined" onClick={() => setOpen(true)}>
        添加联系人
      </Button>
      <Dialog open={open} onClose={() => setOpen(false)}>
        <DialogTitle>添加新联系人</DialogTitle>
        <DialogContent>
          <DialogContentText>
            请输入联系人的姓名和邮箱地址。
          </DialogContentText>
          <TextField
            autoFocus
            margin="dense"
            label="姓名"
            type="text"
            fullWidth
            variant="standard"
          />
          <TextField
            margin="dense"
            label="邮箱地址"
            type="email"
            fullWidth
            variant="standard"
          />
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setOpen(false)}>取消</Button>
          <Button onClick={() => setOpen(false)}>添加</Button>
        </DialogActions>
      </Dialog>
    </>
  );
}

确认对话框 #

jsx
function ConfirmationDialog() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button variant="outlined" color="error" onClick={() => setOpen(true)}>
        删除项目
      </Button>
      <Dialog open={open} onClose={() => setOpen(false)}>
        <DialogTitle>确认删除</DialogTitle>
        <DialogContent>
          <DialogContentText>
            此操作将永久删除该项目,无法恢复。确定要继续吗?
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setOpen(false)}>取消</Button>
          <Button onClick={() => setOpen(false)} color="error" autoFocus>
            删除
          </Button>
        </DialogActions>
      </Dialog>
    </>
  );
}

全屏对话框 #

jsx
import { useTheme, useMediaQuery } from '@mui/material';

function FullScreenDialog() {
  const [open, setOpen] = useState(false);
  const theme = useTheme();
  const fullScreen = useMediaQuery(theme.breakpoints.down('md'));

  return (
    <Dialog fullScreen={fullScreen} open={open} onClose={() => setOpen(false)}>
      <DialogTitle>全屏对话框</DialogTitle>
      <DialogContent>
        <DialogContentText>内容区域</DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button onClick={() => setOpen(false)}>关闭</Button>
      </DialogActions>
    </Dialog>
  );
}

滚动对话框 #

jsx
function ScrollDialog() {
  const [open, setOpen] = useState(false);
  const descriptionElementRef = useRef(null);

  useEffect(() => {
    if (open) {
      const { current: descriptionElement } = descriptionElementRef;
      if (descriptionElement !== null) {
        descriptionElement.focus();
      }
    }
  }, [open]);

  return (
    <Dialog
      open={open}
      onClose={() => setOpen(false)}
      scroll="paper"
      aria-labelledby="scroll-dialog-title"
      aria-describedby="scroll-dialog-description"
    >
      <DialogTitle id="scroll-dialog-title">滚动对话框</DialogTitle>
      <DialogContent dividers>
        <DialogContentText id="scroll-dialog-description" ref={descriptionElementRef} tabIndex={-1}>
          {[...new Array(50)]
            .map(
              () => `Cras mattis consectetur purus sit amet fermentum.
Cras justo odio, dapibus ac facilisis in, egestas eget quam.
Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
Praesent commodo cursus magna, vel scelerisque nisl consectetur et.`,
            )
            .join('\n')}
        </DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button onClick={() => setOpen(false)}>取消</Button>
        <Button onClick={() => setOpen(false)}>订阅</Button>
      </DialogActions>
    </Dialog>
  );
}

可拖动对话框 #

jsx
import Draggable from 'react-draggable';

function PaperComponent(props) {
  return (
    <Draggable handle="#draggable-dialog-title" cancel={'[class*="MuiDialogContent-root"]'}>
      <Paper {...props} />
    </Draggable>
  );
}

function DraggableDialog() {
  const [open, setOpen] = useState(false);

  return (
    <Dialog open={open} onClose={() => setOpen(false)} PaperComponent={PaperComponent}>
      <DialogTitle style={{ cursor: 'move' }} id="draggable-dialog-title">
        可拖动对话框
      </DialogTitle>
      <DialogContent>
        <DialogContentText>点击标题栏可以拖动对话框</DialogContentText>
      </DialogContent>
      <DialogActions>
        <Button autoFocus onClick={() => setOpen(false)}>
          取消
        </Button>
        <Button onClick={() => setOpen(false)}>订阅</Button>
      </DialogActions>
    </Dialog>
  );
}

Snackbar 消息提示 #

基础用法 #

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

function SimpleSnackbar() {
  const [open, setOpen] = useState(false);

  const handleClick = () => setOpen(true);
  const handleClose = (event, reason) => {
    if (reason === 'clickaway') return;
    setOpen(false);
  };

  return (
    <>
      <Button onClick={handleClick}>显示消息</Button>
      <Snackbar
        open={open}
        autoHideDuration={3000}
        onClose={handleClose}
        message="操作成功!"
      />
    </>
  );
}

带动作的消息 #

jsx
function SnackbarWithAction() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)}>显示消息</Button>
      <Snackbar
        open={open}
        autoHideDuration={5000}
        onClose={() => setOpen(false)}
        message="文件已删除"
        action={
          <Button color="secondary" size="small" onClick={() => setOpen(false)}>
            撤销
          </Button>
        }
      />
    </>
  );
}

位置 #

jsx
<Snackbar
  open={open}
  anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
  message="顶部居中"
/>

<Snackbar
  open={open}
  anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
  message="右下角"
/>

配合 Alert 使用 #

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

function CustomizedSnackbars() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button variant="outlined" onClick={() => setOpen(true)}>
        显示成功消息
      </Button>
      <Snackbar open={open} autoHideDuration={3000} onClose={() => setOpen(false)}>
        <Alert onClose={() => setOpen(false)} severity="success" sx={{ width: '100%' }}>
          操作成功完成!
        </Alert>
      </Snackbar>
    </>
  );
}

Alert 警告提示 #

基础用法 #

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

<Stack sx={{ width: '100%' }} spacing={2}>
  <Alert severity="error">这是一个错误提示</Alert>
  <Alert severity="warning">这是一个警告提示</Alert>
  <Alert severity="info">这是一个信息提示</Alert>
  <Alert severity="success">这是一个成功提示</Alert>
</Stack>

带标题 #

jsx
<Stack sx={{ width: '100%' }} spacing={2}>
  <Alert severity="error">
    <AlertTitle>错误</AlertTitle>
    这是一个错误提示 — <strong>请检查输入!</strong>
  </Alert>
  <Alert severity="warning">
    <AlertTitle>警告</AlertTitle>
    这是一个警告提示 — <strong>请注意!</strong>
  </Alert>
  <Alert severity="info">
    <AlertTitle>信息</AlertTitle>
    这是一个信息提示 — <strong>仅供参考!</strong>
  </Alert>
  <Alert severity="success">
    <AlertTitle>成功</AlertTitle>
    这是一个成功提示 — <strong>操作完成!</strong>
  </Alert>
</Stack>

可关闭 #

jsx
<Alert onClose={() => {}}>这是一个可关闭的提示</Alert>

带图标 #

jsx
<Alert icon={<CheckIcon fontSize="inherit" />} severity="success">
  自定义图标
</Alert>

<Alert icon={false} severity="success">
  无图标
</Alert>

变体 #

jsx
<Stack spacing={2}>
  <Alert variant="outlined" severity="error">Outlined 错误</Alert>
  <Alert variant="filled" severity="error">Filled 错误</Alert>
  <Alert variant="standard" severity="error">Standard 错误</Alert>
</Stack>

带动作 #

jsx
<Alert
  severity="info"
  action={
    <Button color="inherit" size="small">
      撤销
    </Button>
  }
>
  这是一条带动作的提示
</Alert>

Progress 进度指示器 #

线性进度条 #

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

<Stack spacing={2} sx={{ width: '100%' }}>
  <LinearProgress />
  <LinearProgress color="secondary" />
  <LinearProgress color="success" />
  <LinearProgress variant="determinate" value={50} />
  <LinearProgress variant="buffer" value={50} valueBuffer={75} />
  <LinearProgress variant="query" />
</Stack>

圆形进度条 #

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

<Stack spacing={2} direction="row">
  <CircularProgress />
  <CircularProgress color="secondary" />
  <CircularProgress color="success" />
  <CircularProgress variant="determinate" value={75} />
  <CircularProgress size={60} />
  <CircularProgress thickness={5} />
</Stack>

带标签的进度条 #

jsx
function LinearProgressWithLabel(props) {
  return (
    <Box sx={{ display: 'flex', alignItems: 'center' }}>
      <Box sx={{ width: '100%', mr: 1 }}>
        <LinearProgress variant="determinate" {...props} />
      </Box>
      <Box sx={{ minWidth: 35 }}>
        <Typography variant="body2" color="text.secondary">{`${Math.round(props.value)}%`}</Typography>
      </Box>
    </Box>
  );
}

function CircularProgressWithLabel(props) {
  return (
    <Box sx={{ position: 'relative', display: 'inline-flex' }}>
      <CircularProgress variant="determinate" {...props} />
      <Box
        sx={{
          top: 0,
          left: 0,
          bottom: 0,
          right: 0,
          position: 'absolute',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <Typography variant="caption" component="div" color="text.secondary">
          {`${Math.round(props.value)}%`}
        </Typography>
      </Box>
    </Box>
  );
}

加载状态 #

jsx
function LoadingButton() {
  const [loading, setLoading] = useState(false);

  return (
    <Box sx={{ display: 'flex', alignItems: 'center' }}>
      <Box sx={{ m: 1, position: 'relative' }}>
        <Button variant="contained" disabled={loading}>
          提交
        </Button>
        {loading && (
          <CircularProgress
            size={24}
            sx={{
              color: 'primary.main',
              position: 'absolute',
              top: '50%',
              left: '50%',
              marginTop: '-12px',
              marginLeft: '-12px',
            }}
          />
        )}
      </Box>
    </Box>
  );
}

Backdrop 背景遮罩 #

基础用法 #

jsx
import { Backdrop, CircularProgress, Button } from '@mui/material';

function SimpleBackdrop() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)}>显示遮罩</Button>
      <Backdrop sx={{ color: '#fff', zIndex: (theme) => theme.zIndex.drawer + 1 }} open={open} onClick={() => setOpen(false)}>
        <CircularProgress color="inherit" />
      </Backdrop>
    </>
  );
}

自定义内容 #

jsx
<Backdrop open={open} onClick={() => setOpen(false)}>
  <Box sx={{ textAlign: 'center', color: 'white' }}>
    <CircularProgress color="inherit" />
    <Typography sx={{ mt: 2 }}>加载中...</Typography>
  </Box>
</Backdrop>

Skeleton 骨架屏 #

基础用法 #

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

<Stack spacing={1}>
  <Skeleton variant="text" />
  <Skeleton variant="circular" width={40} height={40} />
  <Skeleton variant="rectangular" width={210} height={118} />
</Stack>

动画 #

jsx
<Stack spacing={1}>
  <Skeleton animation="pulse" />
  <Skeleton animation="wave" />
  <Skeleton animation={false} />
</Stack>

卡片骨架屏 #

jsx
function SkeletonCard() {
  return (
    <Card sx={{ maxWidth: 345, m: 2 }}>
      <Skeleton variant="rectangular" height={140} />
      <CardContent>
        <Skeleton animation="wave" height={10} style={{ marginBottom: 6 }} />
        <Skeleton animation="wave" height={10} width="80%" />
      </CardContent>
    </Card>
  );
}

列表骨架屏 #

jsx
function SkeletonList() {
  return (
    <List>
      {[1, 2, 3].map((item) => (
        <ListItem key={item}>
          <ListItemAvatar>
            <Skeleton variant="circular" width={40} height={40} />
          </ListItemAvatar>
          <ListItemText
            primary={<Skeleton animation="wave" height={10} width="80%" />}
            secondary={<Skeleton animation="wave" height={10} width="40%" />}
          />
        </ListItem>
      ))}
    </List>
  );
}

Popover 弹出框 #

基础用法 #

jsx
import { Popover, Typography, Button } from '@mui/material';

function BasicPopover() {
  const [anchorEl, setAnchorEl] = useState(null);

  const handleClick = (event) => setAnchorEl(event.currentTarget);
  const handleClose = () => setAnchorEl(null);

  const open = Boolean(anchorEl);

  return (
    <>
      <Button onClick={handleClick}>打开弹出框</Button>
      <Popover
        open={open}
        anchorEl={anchorEl}
        onClose={handleClose}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
      >
        <Typography sx={{ p: 2 }}>弹出框内容</Typography>
      </Popover>
    </>
  );
}

锚点位置 #

jsx
<Popover
  open={open}
  anchorEl={anchorEl}
  onClose={handleClose}
  anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
  transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
  <Typography sx={{ p: 2 }}>内容</Typography>
</Popover>

悬停弹出框 #

jsx
function MouseOverPopover() {
  const [anchorEl, setAnchorEl] = useState(null);

  const handlePopoverOpen = (event) => setAnchorEl(event.currentTarget);
  const handlePopoverClose = () => setAnchorEl(null);

  const open = Boolean(anchorEl);

  return (
    <>
      <Typography
        aria-owns={open ? 'mouse-over-popover' : undefined}
        aria-haspopup="true"
        onMouseEnter={handlePopoverOpen}
        onMouseLeave={handlePopoverClose}
      >
        悬停显示弹出框
      </Typography>
      <Popover
        id="mouse-over-popover"
        sx={{ pointerEvents: 'none' }}
        open={open}
        anchorEl={anchorEl}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
        transformOrigin={{ vertical: 'top', horizontal: 'left' }}
        onClose={handlePopoverClose}
        disableRestoreFocus
      >
        <Typography sx={{ p: 1 }}>弹出框内容</Typography>
      </Popover>
    </>
  );
}

实战示例:完整反馈系统 #

jsx
import { createContext, useContext, useState } from 'react';
import { Snackbar, Alert, Slide } from '@mui/material';

const FeedbackContext = createContext();

function SlideTransition(props) {
  return <Slide {...props} direction="up" />;
}

export function FeedbackProvider({ children }) {
  const [notification, setNotification] = useState({
    open: false,
    message: '',
    severity: 'info',
  });

  const showNotification = (message, severity = 'info') => {
    setNotification({ open: true, message, severity });
  };

  const handleClose = () => {
    setNotification({ ...notification, open: false });
  };

  return (
    <FeedbackContext.Provider value={{ showNotification }}>
      {children}
      <Snackbar
        open={notification.open}
        autoHideDuration={3000}
        onClose={handleClose}
        TransitionComponent={SlideTransition}
        anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
      >
        <Alert onClose={handleClose} severity={notification.severity} sx={{ width: '100%' }}>
          {notification.message}
        </Alert>
      </Snackbar>
    </FeedbackContext.Provider>
  );
}

export function useFeedback() {
  return useContext(FeedbackContext);
}

下一步 #

继续学习 导航组件,了解 AppBar、Drawer、Tabs 等导航组件!

最后更新:2026-03-28