React Material UI Mini Variant与Responsive Drawer适配移动端开发咨询
实现方案
核心思路是通过MUI内置的响应式hook监听屏幕尺寸,动态切换抽屉的交互模式:
第一步:新增依赖导入
在代码顶部导入需要的hooks:
import { useTheme } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery';
第二步:组件内新增响应式判断逻辑
在SideMenuLayout组件内const classes = useStyles();后面添加以下代码:
const theme = useTheme(); // 以sm断点(默认600px)作为移动端分界,可按需调整为xs/md等 const isMobile = useMediaQuery(theme.breakpoints.down('sm')); // 可选优化:屏幕切换为移动端时自动关闭抽屉 useEffect(() => { if (isMobile) setDrawerOpen(false); }, [isMobile, setDrawerOpen]);
第三步:调整组件属性适配响应式逻辑
修改AppBar样式逻辑
将AppBar的className修改为仅非移动端时才触发偏移:
<AppBar position="absolute" className={clsx(classes.appBar, !isMobile && drawerOpen && classes.appBarShift)} >
修改菜单按钮显示逻辑
将菜单按钮的className修改为仅非移动端展开时才隐藏:
<IconButton edge="start" color="inherit" aria-label="open drawer" onClick={handleDrawerOpen} className={clsx(classes.menuButton, !isMobile && drawerOpen && classes.menuButtonHidden)} > <MenuIcon /> </IconButton>
修改Drawer组件配置
动态切换抽屉类型,移动端使用临时抽屉模式:
<Drawer variant={isMobile ? 'temporary' : 'permanent'} onClose={isMobile ? handleDrawerClose : undefined} classes={{ paper: clsx(classes.drawerPaper, !isMobile && !drawerOpen && classes.drawerPaperClose), }} open={drawerOpen} >
完整修改后代码
import React, {useState, useEffect} from 'react'; import clsx from 'clsx'; import { makeStyles, useTheme } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import CssBaseline from '@material-ui/core/CssBaseline'; import Drawer from '@material-ui/core/Drawer'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import List from '@material-ui/core/List'; import Typography from '@material-ui/core/Typography'; import Divider from '@material-ui/core/Divider'; import IconButton from '@material-ui/core/IconButton'; import MenuIcon from '@material-ui/icons/Menu'; import MenuOpenIcon from '@material-ui/icons/MenuOpen'; import { MainListItems } from './listItems'; import AccountMenu from '../../components/AccountMenu' import ChangePasswordDialog from './ChangePasswordDialog'; const drawerWidth = 240; const useStyles = makeStyles((theme) => ({ root: { display: 'flex', }, toolbar: { paddingRight: 60, }, toolbarIcon: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', padding: '0 8px', ...theme.mixins.toolbar, }, appBar: { zIndex: theme.zIndex.drawer + 1, transition: theme.transitions.create(['width', 'margin'], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), }, appBarShift: { marginLeft: drawerWidth, width: `calc(100% - ${drawerWidth}px)`, transition: theme.transitions.create(['width', 'margin'], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, }), }, menuButton: { marginRight: 36, }, menuButtonHidden: { display: 'none', }, mainTitle: { flexGrow: 1, marginLeft: 12 }, title: { flexGrow: 1, }, drawerPaper: { position: 'relative', whiteSpace: 'nowrap', width: drawerWidth, transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, }), // 适配移动端临时抽屉的层级和定位 [theme.breakpoints.down('sm')]: { position: 'fixed', } }, drawerPaperClose: { overflowX: 'hidden', transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), width: theme.spacing.unit * 7, [theme.breakpoints.up('sm')]: { width: theme.spacing.unit * 9, }, }, appBarSpacer: theme.mixins.toolbar, content: { flexGrow: 1, height: '100vh', overflow: 'auto', }, container: { paddingTop: theme.spacing(4), paddingBottom: theme.spacing(4), }, paper: { padding: theme.spacing(2), display: 'flex', overflow: 'auto', flexDirection: 'column', }, fixedHeight: { height: 240, }, })); export default function SideMenuLayout({ drawerOpen, setDrawerOpen, children, title }) { const classes = useStyles(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down('sm')); useEffect(() => { if (isMobile) setDrawerOpen(false); }, [isMobile, setDrawerOpen]); const handleDrawerOpen = () => { setDrawerOpen(true); }; const handleDrawerClose = () => { setDrawerOpen(false); }; const [changePasswordDialogOpen, setChangePasswordDialogOpen] = useState(false) return ( <div className={classes.root}> <CssBaseline /> <AppBar position="absolute" className={clsx(classes.appBar, !isMobile && drawerOpen && classes.appBarShift)}> <Toolbar className={classes.toolbar}> <IconButton edge="start" color="inherit" aria-label="open drawer" onClick={handleDrawerOpen} className={clsx(classes.menuButton, !isMobile && drawerOpen && classes.menuButtonHidden)} > <MenuIcon /> </IconButton> <Typography component="h1" variant="h6" color="inherit" noWrap className={classes.title}> {drawerOpen ? title : `MyTitle - ${title}`} </Typography> <AccountMenu onChangePassword={() => setChangePasswordDialogOpen(true)} /> </Toolbar> </AppBar> <Drawer variant={isMobile ? 'temporary' : 'permanent'} onClose={isMobile ? handleDrawerClose : undefined} classes={{ paper: clsx(classes.drawerPaper, !isMobile && !drawerOpen && classes.drawerPaperClose), }} open={drawerOpen} > <div className={classes.toolbarIcon}> <Typography component="h1" variant="h6" color="inherit" noWrap className={classes.mainTitle}> MyTitle </Typography> <IconButton onClick={handleDrawerClose}> <MenuOpenIcon /> </IconButton> </div> <Divider /> <List><MainListItems /></List> </Drawer> <main className={classes.content}> <div className={classes.appBarSpacer} /> {children} </main> <ChangePasswordDialog isOpen={changePasswordDialogOpen} setOpen={setChangePasswordDialogOpen} /> </div> ); }
关键逻辑说明
- 移动端抽屉为临时悬浮模式,点击外部区域自动关闭,不会挤压主内容区域
- 桌面端完全保留原有的Mini Variant抽屉的收缩/展开交互逻辑
- 可通过调整
theme.breakpoints.down('sm')的断点值,自定义响应式切换的触发宽度
内容的提问来源于stack exchange,提问作者Son Blake
相关产品推荐
相关产品推荐

