react 高效高质量搭建后台系统 系列 —— 系统布局

其他章节请看:

react 高效高质量搭建后台系统 系列

系统布局

前面我们用脚手架搭建了项目,并实现了登录模块,登录模块所依赖的请求数据antd(ui框架和样式)也已完成。

本篇将完成系统布局。比如导航区、头部区域、主体区域、页脚。

最终效果如下:

react 高效高质量搭建后台系统 系列 —— 系统布局

spug 中系统布局的分析

spug 登录成功后进入系统,页面分为三大块:左侧导航、头部和主体区域。如下图所示:

react 高效高质量搭建后台系统 系列 —— 系统布局

Tip:spug 将版权部分也放在主体区域内。

切换左侧导航,主体内容会跟着变化,头部区域不变。例如从工作台切换到 Dashboard,就像这样:

react 高效高质量搭建后台系统 系列 —— 系统布局

入口

登录成功后,进入系统。也就是进入 Layout 组件。

// App.js class App extends Component {    render() {      return (        <Switch>          <Route path="/" exact component={Login} />          {/* 系统登录后进入 Layout 组件 */}          <Route component={Layout} />        </Switch>      );    } } 

Layout下index.js渲染的代码如下:

  return (     <Layout>       {/* 左侧区域,对 antd 中 Sider 的封装 */}       <Sider collapsed={collapsed}/>       <Layout style={{height: '100vh'}}>         {/* 顶部区域, 对 antd 中 Layout.Header 的封装*/}         <Header collapsed={collapsed} toggle={() => setCollapsed(!collapsed)}/>         <Layout.Content className={styles.content}>           <Switch>             {Routes}             <Route component={NotFound}/>           </Switch>           <Footer/>         </Layout.Content>       </Layout>     </Layout> 

这里主要用到 antd 的 Layout 布局组件。请看 antd 中 Layout 的示例,和 spug 中的代码和效果几乎相同:

react 高效高质量搭建后台系统 系列 —— 系统布局

Tip

  1. 这里的 Sider 和 Header 都不是 antd 中的原始组件,已被封装,挪出成一个单独的组件。
  2. <Footer/> 总是在视口底部,受父元素 flex 的影响。请看下图:

react 高效高质量搭建后台系统 系列 —— 系统布局

Layout 中 index.js 完整代码如下:

// spugsrclayoutindex.js  import React, { useState, useEffect } from 'react'; import { Switch, Route } from 'react-router-dom'; import { Layout, message } from 'antd'; import { NotFound } from 'components'; import Sider from './Sider'; import Header from './Header'; import Footer from './Footer' /* 对象数组。就像这样:  [   { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },   ...   {     icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [       { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmIndex },       { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmContact },       { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmGroup },     ]   },   ... ] */ import routes from '../routes'; import { hasPermission, isMobile } from 'libs'; import styles from './layout.module.less';  // 将 routes 中有权限的路由提取到 Routes 中 function initRoutes(Routes, routes) {   for (let route of routes) {     // 叶子节点才有 component。如果没有child则属于叶子节点     if (route.component) {       // 如果不需要权限,或有权限则放入 Routes       if (!route.auth || hasPermission(route.auth)) {         Routes.push(<Route exact key={route.path} path={route.path} component={route.component}/>)       }     } else if (route.child) {       initRoutes(Routes, route.child)     }   } }  export default function () {   // 侧边栏收起状态。这里设置为展开   const [collapsed, setCollapsed] = useState(false)   // 路由,默认是空数组   const [Routes, setRoutes] = useState([]);    // 组件挂载后执行。相当于 componentDidMount()   useEffect(() => {      if (isMobile) {       setCollapsed(true);       message.warn('检测到您在移动设备上访问,请使用横屏模式。', 5)     }     // 注:重新声明一个变量 Routes,比上文的 Routes 作用域更小范围     const Routes = [];     initRoutes(Routes, routes);     // console.log('Routes', Routes)     // console.log('Routes', JSON.stringify(Routes))     setRoutes(Routes)   }, [])     return (     // 此处 Layout 是 antd 布局组件。和官方用法相同:     /*     <Layout>       <Sider>Sider</Sider>       <Layout>         <Header>Header</Header>         <Content>Content</Content>         <Footer>Footer</Footer>       </Layout>     </Layout>     */     <Layout>              {/* 左侧区域,对 antd 中 Sider 的封装 */}       <Sider collapsed={collapsed}/>       {/* 内容高度不够,版权信息在底部;内容高度太高,则需要滚动才可查看全部内容; */}       <Layout style={{height: '100vh'}}>         {/* 顶部区域, 对 antd 中 Layout.Header 的封装*/}         <Header collapsed={collapsed} toggle={() => setCollapsed(!collapsed)}/>         <Layout.Content className={styles.content}>           {/* 只渲染第一个路径匹配的组件。类似 if...else。参考:https://www.cnblogs.com/pengjiali/p/16045481.html#Switch */}           <Switch>             {/* 路由数组。里面每项类似这样:<Route exact key={route.path} path='/home' component={HomeComponent}/> */}             {Routes}             {/* 没有匹配则进入 NotFound */}             <Route component={NotFound}/>           </Switch>           {/* 系统底部展示。例如版权、官网、文档链接、仓库链接*/}           {/* 父元素采用 flex 布局,当主体内容不多时,版权这部分信息也会置于底部 */}           <Footer/>         </Layout.Content>       </Layout>     </Layout>   ) } 

左侧导航

左侧导航封装在 Sider(spugsrclayoutSider.js) 组件中。

利用的是 antd 中的 Menu 组件。就像这样:

// <4.20.0 可用,>=4.20.0 时不推荐 <Menu>     <Menu.Item>菜单项一</Menu.Item>     <Menu.Item>菜单项二</Menu.Item>     <Menu.SubMenu title="子菜单">         <Menu.Item>子菜单项</Menu.Item>     </Menu.SubMenu> </Menu>; 

完整代码如下:

// spugsrclayoutSider.js  import React, { useState } from 'react'; import { Layout, Menu } from 'antd'; import { hasPermission, history } from 'libs'; import styles from './layout.module.less'; /* 对象数组。就像这样:  [   { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },   ...   {     icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [       { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmIndex },       { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmContact },       { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmGroup },     ]   },   ... ] */ import menus from '../routes'; import logo from './spug.png' // 当前选中的菜单项 key 数组 let selectedKey = window.location.pathname; /* 初始化菜单映射。如果输入不存在的路径,那么菜单则无需选中  { /home: 1,                   // 一级菜单 /dashboard: 1,              // 一级菜单 ... /alarm/alarm: "报警中心",   // 二级菜单 /alarm/contact: "报警中心", // 二级菜单 /alarm/group: "报警中心",   // 二级菜单 ... } */ const OpenKeysMap = {};  for (let item of menus) {   if (item.child) {     for (let sub of item.child) {       // child 中的节点值为 item.title       if (sub.title) OpenKeysMap[sub.path] = item.title     }   } else if (item.title) {     // 一级节点的值是 1     OpenKeysMap[item.path] = 1   } }  export default function Sider(props) {   // openKeys	当前展开的 SubMenu 菜单项 key 数组 string[]   // const [openKeys, setOpenKeys] = useState([]);    // 根据路由返回菜单项或子菜单。没有权限或没有 title 返回 null   function makeMenu(menu) {     // 如果没有权限     if (menu.auth && !hasPermission(menu.auth)) return null;     // 没有 title 返回 null     if (!menu.title) return null;     // 如果有 child 则调用 _makeSubMenu;没有 child 则调用 _makeItem     return menu.child ? _makeSubMenu(menu) : _makeItem(menu)   }    // 返回子菜单   function _makeSubMenu(menu) {     return (       <Menu.SubMenu key={menu.title} title={<span>{menu.icon}<span>{menu.title}</span></span>}>         {menu.child.map(menu => makeMenu(menu))}       </Menu.SubMenu>     )   }    // 返回菜单项   function _makeItem(menu) {     return (       <Menu.Item key={menu.path}>         {menu.icon}         <span>{menu.title}</span>       </Menu.Item>     )   }   // window.location.pathname 返回当前页面的路径或文件名   // 例如 https://demo.spug.cc/host?name=pjl 返回 /host   const tmp = window.location.pathname;   const openKey = OpenKeysMap[tmp];   // 如果是不存在的路径(例如 /host9999),菜单则无需选中   if (openKey) {     // 当前选中的菜单项 key 数组。     selectedKey = tmp;     // 更新子菜单。`openKey 不是1` && `侧边栏展开` &&      // if (openKey !== 1 && !props.collapsed && !openKeys.includes(openKey)) {     //   setOpenKeys([...openKeys, openKey])     // }   }   // 下面的className都仅仅让样式好看点,对功能没有影响。   return (     // Sider:侧边栏,自带默认样式及基本功能,其下可嵌套任何元素,只能放在 Layout 中。     // collapsed - 当前收起状态。这里设置为默认展开     <Layout.Sider width={208} collapsed={props.collapsed} className={styles.sider}>       {/* 图标 */}       <div className={styles.logo}>         <img src={logo} alt="Logo" style={{ height: '30px' }} />       </div>       <div className={styles.menus} style={{ height: `${document.body.clientHeight - 64}px` }}>         {/* 导航菜单。使用的是`缩起内嵌菜单` */}         <Menu           theme="dark"           mode="inline"           className={styles.menus}           // 当前选中的菜单项 key 数组           selectedKeys={[selectedKey]}           // openKeys	当前展开的 SubMenu 菜单项 key 数组 string[]           // openKeys={openKeys}           // onOpenChange - SubMenu 展开/关闭的回调           // onOpenChange={setOpenKeys}           // 路由切换。点击哪个导航,url和路由就会切换到该路劲           onSelect={menu => history.push(menu.key)}>           {/* 数组中的 null 会被忽略 */}           {menus.map(menu => makeMenu(menu))}         </Menu>       </div>     </Layout.Sider>   ) } 

代码简析:

  • 模块返回一个侧边栏 <Layout.Sider>,里面使用菜单组件 Menu,Menu 中的 openKeys 和 onOpenChange 的逻辑有点凌乱,这里将其注释,对于切换菜单没有影响
  • menus 来自路由(routes.js),菜单中的内容由 makeMenu() 返回
  • 侧边栏默认展开,由父组件传入的 collapsed 决定
  • OpenKeysMap 其中一个作用是,当你输入的路径不在菜单中,菜单项则无需选中

头部

头部组件比较简单,分为三块:左侧导航伸缩控制区、通知区和用户区。

点击用户区个人中心,主体区域路由会跳转。效果如下图所示:

react 高效高质量搭建后台系统 系列 —— 系统布局

完整代码:

// spugsrclayoutHeader.js  import React from 'react'; import { Link } from 'react-router-dom'; import { Layout, Dropdown, Menu, Avatar } from 'antd'; import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LogoutOutlined } from '@ant-design/icons'; import Notification from './Notification'; import styles from './layout.module.less'; import http from '../libs/http'; import history from '../libs/history'; import avatar from './avatar.png';  export default function (props) {   // 退出   function handleLogout() {     // 跳转到登录页     history.push('/');     // 告诉后端退出登录     http.get('/api/account/logout/')   }     const UserMenu = (     <Menu>       <Menu.Item>         {/* 路由跳转。主体区域对应路由是 `{ path: '/welcome/info', component: WelcomeInfo },` */}         <Link to="/welcome/info">           <UserOutlined style={{marginRight: 10}}/>个人中心         </Link>       </Menu.Item>       <Menu.Divider/>       <Menu.Item onClick={handleLogout}>         <LogoutOutlined style={{marginRight: 10}}/>退出登录       </Menu.Item>     </Menu>   );    return (     <Layout.Header className={styles.header}>       {/* 收缩左侧导航按钮 */}       <div className={styles.left}>         {/* 点击触发父组件的 toggle 方法 */}         <div className={styles.trigger} onClick={props.toggle}>           {/* 根据父组件的 collapsed 属性显示对应图标*/}           {props.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}         </div>       </div>       {/* 通知 */}       <Notification/>       {/* 用户区域 */}       <div className={styles.right}>         <Dropdown overlay={UserMenu} style={{background: '#000'}}>           <span className={styles.action}>             <Avatar size="small" src={avatar} style={{marginRight: 8}}/>             {/* 登录后设置过的昵称 */}             {localStorage.getItem('nickname')}           </span>         </Dropdown>       </div>     </Layout.Header>   ) } 

主体区域

主体区域更简单,就是一个组件(根据自己需求自行完成)。如果需要面包屑,自行加上即可。有无面包屑导航的效果如下图所示:

react 高效高质量搭建后台系统 系列 —— 系统布局

主页(/home) 代码可以浏览下:

// spugsrcpageshomeindex.js  function HomeIndex() {   return (     <div>       {/* 面包屑 */}       <Breadcrumb>         <Breadcrumb.Item>首页</Breadcrumb.Item>         <Breadcrumb.Item>工作台</Breadcrumb.Item>       </Breadcrumb>        <Row gutter={12}>         <Col span={16}>           <NavIndex />         </Col>         <Col span={8}>           <Row gutter={[12, 12]}>             <Col span={24}>               <TodoIndex />             </Col>             <Col span={24}>               <NoticeIndex />             </Col>           </Row>         </Col>       </Row>     </div>   ) }  export default HomeIndex 

myspug 系统布局的实现

入口

在 App.js 中引入 Layout 组件,之前我们是一个占位组件:

// myspugsrcApp.js -import HelloWorld from './HelloWord' +import Layout from './layout'  import { Switch, Route } from 'react-router-dom';   // 定义一个类组件 class App extends Component {        <Switch>          <Route path="/" exact component={Login} />          {/* 没有匹配则进入 Layout */} -        <Route component={HelloWorld} /> +        <Route component={Layout} />        </Switch>      ); } 

Layout 中 index.js 代码如下:

// myspugsrclayoutindex.js  import React, { useState, useEffect } from 'react'; import { Switch, Route } from 'react-router-dom'; import { Layout, message } from 'antd'; // 404 对应的组件 /*  //  myspugsrccompomentsindex.js import NotFound from './NotFound';  export {     NotFound, }  */ import { NotFound } from '@/components'; // 侧边栏 import Sider from './Sider'; // 头部 import Header from './Header'; // 页脚。例如版权 import Footer from './Footer'  /* 引入路由。对象数组,就像这样:  [   { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },   ...   {     icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [       { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmIndex },       { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmContact },       { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmGroup },     ]   },   ... ] */ import routes from '../routes'; // hasPermission - 权限判断。本篇忽略,这里直接返回 true; isMobile - 是否是手机 /* export function hasPermission(strCode) {     return true } // 基于检测用户代理字符串的浏览器标识是不可靠的,不推荐使用,因为用户代理字符串是用户可配置的 export const isMobile = /Android|iPhone/i.test(navigator.userAgent)  */ import { hasPermission, isMobile } from '@/libs';  // 布局样式,直接拷贝 spug 中的样式即可 import styles from './layout.module.less';  // 将 routes 中有权限的路由提取到 Routes 中 function initRoutes(Routes, routes) {   for (let route of routes) {     // 叶子节点才有 component。没有 child 则属于叶子节点     if (route.component) {       // 如果不需要权限,或有权限则放入 Routes       if (!route.auth || hasPermission(route.auth)) {         Routes.push(<Route exact key={route.path} path={route.path} component={route.component} />)       }     } else if (route.child) {       initRoutes(Routes, route.child)     }   } }  export default function () {   // 侧边栏收缩状态。默认展开   const [collapsed, setCollapsed] = useState(false)   // 路由,默认是空数组   const [Routes, setRoutes] = useState([]);    // 组件挂载后执行。相当于 componentDidMount()   useEffect(() => {     if (isMobile) {       // 手机查看时导航栏收起       setCollapsed(true);       message.warn('检测到您在移动设备上访问,请使用横屏模式。', 5)     }      // 注:重新声明一个变量 Routes,比上文(useState 中的 Routes)的 Routes 作用域更小范围     const Routes = [];     initRoutes(Routes, routes);     setRoutes(Routes)   }, [])    return (     // 此处 Layout 是 antd 布局组件。和官方用法相同:     /*     <Layout>       <Sider>Sider</Sider>       <Layout>         <Header>Header</Header>         <Content>Content</Content>         <Footer>Footer</Footer>       </Layout>     </Layout>     */     <Layout>        {/* 左侧区域,对 antd 中 Sider 的封装 */}       <Sider collapsed={collapsed} />       {/* 内容高度不够,版权信息在底部;内容高度太高,则需要滚动才可查看全部内容; */}       <Layout style={{ height: '100vh' }}>         {/* 顶部区域, 对 antd 中 Layout.Header 的封装*/}         <Header collapsed={collapsed} toggle={() => setCollapsed(!collapsed)} />         <Layout.Content className={styles.content}>           {/* 只渲染第一个路径匹配的组件*/}           <Switch>             {/* 路由数组。里面每项类似这样:<Route exact key={route.path} path='/home' component={HomeComponent}/> */}             {Routes}             {/* 没有匹配则进入 NotFound */}             <Route component={NotFound} />           </Switch>           {/* 系统底部展示。例如版权、官网、文档链接、仓库链接*/}           <Footer />         </Layout.Content>       </Layout>     </Layout>   ) } 

在 routes.js 中定义3个路由,其中报警中心里面有三个子菜单,用同一个组件做占位:

// myspugsrcroutes.js  import React from 'react'; import {     DesktopOutlined,     AlertOutlined, } from '@ant-design/icons'; /* export default function HomeIndex() {     return <div>我是主页</div> } */ import HomeIndex from './pages/home'; // 占位效果 /* export default function AlarmCenter() {     return <div>报警中心占位符 - {window.location.pathname}</div> } */ import AlarmCenter from './pages/alarm/alarm'; // 个人中心 /* export default function HomeIndex() {     return <div>我是个人中心</div> } */ import WelcomeInfo from './pages/welcome/info';  export default [     { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },     {         icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [           { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmCenter },           { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmCenter },           { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmCenter },         ]       },     { path: '/welcome/info', component: WelcomeInfo }, ] 

Tip: <Footer> 组件直接拷贝 spug 中的

NotFound 代码如下:

// myspugsrccompomentsNotFound.js import React from 'react'; // 拷贝 spug 中的内容 import styles from './index.module.less';  export default function NotFound() {     return (         <div className={styles.notFound}>             <div className={styles.imgBlock}>                 <div className={styles.img} />             </div>             <div>                 <h1 className={styles.title}>404</h1>                 <div className={styles.desc}>抱歉,你访问的页面不存在</div>             </div>         </div>     ) } 

左侧导航

// myspugsrclayoutSider.js  import React, { useState } from 'react'; import { Layout, Menu } from 'antd'; import { hasPermission, history } from '@/libs'; import styles from './layout.module.less'; /* 对象数组。就像这样:  [   { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },   ...   {     icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [       { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmIndex },       { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmContact },       { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmGroup },     ]   },   ... ] */ import menus from '../routes';  import logo from './spug.png'  let selectedKey = window.location.pathname; /* 菜单映射。如果输入不存在的路径,那么菜单就不需要选中  { /home: 1,                   // 一级菜单 /dashboard: 1,              // 一级菜单 ... /alarm/alarm: "报警中心",   // 二级菜单 /alarm/contact: "报警中心", // 二级菜单 /alarm/group: "报警中心",   // 二级菜单 ... } */ const OpenKeysMap = {};  for (let item of menus) {   if (item.child) {     for (let sub of item.child) {       // child 中的节点值为 item.title       if (sub.title) OpenKeysMap[sub.path] = item.title     }   } else if (item.title) {     // 一级节点的值是 1     OpenKeysMap[item.path] = 1   } }  export default function Sider(props) {   // 根据路由返回菜单项或子菜单。没有权限或没有 title 返回 null   function makeMenu(menu) {     // 如果没有权限     if (menu.auth && !hasPermission(menu.auth)) return null;     // 没有 title 返回 null     if (!menu.title) return null;     // 如果有 child 则调用 _makeSubMenu;没有 child 则调用 _makeItem     return menu.child ? _makeSubMenu(menu) : _makeItem(menu)   }    // 返回子菜单   function _makeSubMenu(menu) {     return (       <Menu.SubMenu key={menu.title} title={<span>{menu.icon}<span>{menu.title}</span></span>}>         {menu.child.map(menu => makeMenu(menu))}       </Menu.SubMenu>     )   }    // 返回菜单项   function _makeItem(menu) {     return (       <Menu.Item key={menu.path}>         {menu.icon}         <span>{menu.title}</span>       </Menu.Item>     )   }   // window.location.pathname 返回当前页面的路径或文件名   // 例如 https://demo.spug.cc/host?name=pjl 返回 /host   const tmp = window.location.pathname;   const openKey = OpenKeysMap[tmp];   // 如果是不存在的路径(例如 /host9999),菜单则无需选中   if (openKey) {     // 当前选中的菜单项 key 数组。     selectedKey = tmp;   }   // 下面的className都仅仅让样式好看点,对功能没有影响。   return (     // Sider:侧边栏,自带默认样式及基本功能,其下可嵌套任何元素,只能放在 Layout 中。     // collapsed - 当前收起状态。这里设置为默认展开     <Layout.Sider width={208} collapsed={props.collapsed} className={styles.sider}>       {/* 图标 */}       <div className={styles.logo}>         <img src={logo} alt="Logo" style={{ height: '30px' }} />       </div>       <div className={styles.menus} style={{ height: `${document.body.clientHeight - 64}px` }}>         {/* 导航菜单。使用的是`缩起内嵌菜单` */}         <Menu           theme="dark"           mode="inline"           className={styles.menus}           // 当前选中的菜单项 key 数组           selectedKeys={[selectedKey]}           // 路由切换。点击哪个导航,url和路由就会切换到该路劲           onSelect={menu => history.push(menu.key)}>           {/* 数组中的 null 会被忽略 */}           {menus.map(menu => makeMenu(menu))}         </Menu>       </div>     </Layout.Sider>   ) } 

头部

Tip通知暂不实现

代码如下:

// myspugsrclayoutHeader.js  import React from 'react'; import { Link } from 'react-router-dom'; import { Layout, Dropdown, Menu, Avatar } from 'antd'; import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LogoutOutlined } from '@ant-design/icons'; //  `通知`暂不实现 //  import Notification from './Notification'; import styles from './layout.module.less'; import http from '../libs/http'; import history from '../libs/history'; import avatar from './avatar.png';  export default function (props) {   // 退出   function handleLogout() {     // 跳转到登录页     history.push('/');     // 告诉后端退出登录     http.get('/api/account/logout/')   }    const UserMenu = (     <Menu>       <Menu.Item>         {/* 路由跳转。主体区域对应路由是 `{ path: '/welcome/info', component: WelcomeInfo },` */}         <Link to="/welcome/info">           <UserOutlined style={{ marginRight: 10 }} />个人中心         </Link>       </Menu.Item>       <Menu.Divider />       <Menu.Item onClick={handleLogout}>         <LogoutOutlined style={{ marginRight: 10 }} />退出登录       </Menu.Item>     </Menu>   );    return (     <Layout.Header className={styles.header}>       {/* 收缩左侧导航按钮 */}       <div className={styles.left}>         {/* 点击触发父组件的 toggle 方法 */}         <div className={styles.trigger} onClick={props.toggle}>           {/* 根据父组件的 collapsed 属性显示对应图标*/}           {props.collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}         </div>       </div>       {/* 通知 */}       <div>通知 todo</div>       {/* <Notification/> */}       {/* 用户区域 */}       <div className={styles.right}>         <Dropdown overlay={UserMenu} style={{ background: '#000' }}>           <span className={styles.action}>             <Avatar size="small" src={avatar} style={{ marginRight: 8 }} />             {/* 登录后设置过的昵称 */}             {localStorage.getItem('nickname')}           </span>         </Dropdown>       </div>     </Layout.Header>   ) } 

less 模块化样式的配置

Tip: 样式模块化的更多介绍请看 这里

目前 myspug 支持 index.module.css:

// 支持 import helloWorld from './index.module.css'  export default function HelloWorld() {     return <div className={helloWorld.title}>hello world!</div> } 

却不支持 .module.less 这种模块化的写法:

// 不支持 import helloWorld from './index.module.less'  export default function HelloWorld() {     return <div className={helloWorld.title}>hello world!</div> } 

你会发现 div 元素上的 class 是空的。

使其支持费了一些波折:

  • 参考 spugconfig-overrides.js 添加 addLessLoader() 报错,修改 addLessLoader 新语法也报错,将 less、less-loader更新至与 spug 中相同版本不行,安装 postCss 报新错
  • 使用 antd 中自定义主题的方式成功跑起来,但按钮总是绿色

最终解决方法如下:

 // config-overrides.js -const { override, fixBabelImports, addWebpackAlias } = require('customize-cra'); +const { override, fixBabelImports, addWebpackAlias, addLessLoader, adjustStyleLoaders } = require('customize-cra');  const path = require('path')  module.exports = override(      fixBabelImports('import', {      module.exports = override(      // 增加别名。避免 ../../ 相对路劲引入 libs/http      addWebpackAlias({          '@': path.resolve(__dirname, './src') -    }) +    }), +    // 解决 +    addLessLoader({ +        lessOptions: { +            javascriptEnabled: true, +            localIdentName: '[local]--[hash:base64:5]' +        } +    }), +    // 网友`阖湖丶`的介绍,解决:ValidationError: Invalid options object. PostCSS Loader has been initialized... +    adjustStyleLoaders(({ use: [, , postcss] }) => { +        const postcssOptions = postcss.options; +        postcss.options = { postcssOptions }; +    }),  ); 

效果验证

最终效果:

react 高效高质量搭建后台系统 系列 —— 系统布局

react 高效高质量搭建后台系统 系列 —— 系统布局

  • 登录成功默认进入主页
  • 点击报警历史,url 切换为 /alarm/alarm,菜单选中项更新,同时主体区域显示对应信息
  • 鼠标移至管理员,点击个人中心,url切换,菜单选中项不变,同时主体区域显示对应信息
  • 对于不存在的 url ,内容区域会显示 404 的效果,同时菜单选中项会清空

其他章节请看:

react 高效高质量搭建后台系统 系列

发表评论

评论已关闭。

相关文章