其他章节请看:

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

系统布局

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

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

最终效果如下:

spug 中系统布局的分析

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

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

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

入口

登录成功后,进入系统。也就是进入 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 中的代码和效果几乎相同:

Tip

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

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

// spug\src\layout\index.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(spug\src\layout\Sider.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>;

完整代码如下:

// spug\src\layout\Sider.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 其中一个作用是,当你输入的路径不在菜单中,菜单项则无需选中

头部

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

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

完整代码:

// spug\src\layout\Header.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>
)
}

主体区域

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

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

// spug\src\pages\home\index.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 组件,之前我们是一个占位组件:

// myspug\src\App.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 代码如下:

// myspug\src\layout\index.js

import React, { useState, useEffect } from 'react';
import { Switch, Route } from 'react-router-dom';
import { Layout, message } from 'antd';
// 404 对应的组件
/* // myspug\src\compoments\index.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个路由,其中报警中心里面有三个子菜单,用同一个组件做占位:

// myspug\src\routes.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 代码如下:

// myspug\src\compoments\NotFound.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>
)
}

左侧导航

// myspug\src\layout\Sider.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通知暂不实现

代码如下:

// myspug\src\layout\Header.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 是空的。

使其支持费了一些波折:

  • 参考 spug\config-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 };
+ }),
);

效果验证

最终效果:

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

其他章节请看:

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

react 高效高质量搭建后台系统 系列 —— 系统布局的更多相关文章

  1. 使用vue1.0+es6+vue-cli+webpack+iview-ui+jQuery 撸一套高质量的后台管理系统

    首先按照vue.js官网的指令安装: 1.本地安装好node.js 2.根据官方命令行工具 详情 这样一个官方的脚手架工具就已经搭建好了:但是有一点需要注意的是由于现在按照官方的搭建方法是搭建vue2 ...

  2. 编写高质量的Python代码系列(八)之部署

    Python提供了一些工具,使我们可以把软件部署到不同的环境中.它也提供了一些模块,令开发者可以把程序编写的更加健壮.本章讲解如何使用Python调试.优化并测试程序,以提升其质量与性能. 第五十四条 ...

  3. 编写高质量的Python代码系列(一)之用Pythonic方式来思考

    Python开发者用Pythonic这个形容词来描述具有特定风格的代码.这种风格是大家在使用Python语言进行编程并相互协作的过程中逐渐形成的习惯.那么,如何以改风格完成常见的Python编程工作呢 ...

  4. nodejs 从helloworld到高质量的后台服务server的一点思考

    ---恢复内容开始--- 新公司用的nodejs作为app和网站的后台服务server,所以最近对nodejs一直在学习,加上之前简单的学习了一点,看了两天后台接口源码,所以就直接上手干活了,下面是我 ...

  5. 编写高质量的Python代码系列(七)之协作开发

    如果多个人要开发同一个Python程序,那就得仔细商量代码的写法了.即使你是一个人开发,也需要理解其他人所写的模块.本节讲解多人协作开发Python程序时所用的标准工具及最佳做法. 第四十九条:为每个 ...

  6. 编写高质量的Python代码系列(六)之内置模块

    Python预装了许多写程序时会用到的重要模块.这些标准软件包与通常意义上的Python语言联系得非常精密,我们可以将其当成语言规范的一部分.本节将会讲解基本的内置模块. 第四十二条:用functoo ...

  7. 编写高质量的Python代码系列(五)之并发与并行

    用Python可以很容易就能写出并发程序,这种程序可以在同一时间做许多间不同的事情.我们也可以通过系统调用.子进程(subprocess)及C语言扩展来实现并行处理. 第三十六条: 用subproce ...

  8. 编写高质量的Python代码系列(四)之元类及属性

    元类(metaclass)及动态属性(dynamic attribute)都是很强大的Python特性,然后他们也可能导致及其古怪.及其突然的行为.本节讲解这些机制的常见用法,以确保各位程序员写出来的 ...

  9. 编写高质量的Python代码系列(三)之类与继承

    用Python进行编程时,通常需要编写心累,并定义这些类应该如何通过其接口及继承体系与外界交互.本节讲解如何使用类和继承来表达对象所以更具备的行为. 第二十二条:尽量用辅助类来维护程序的状态,而不要用 ...

  10. 编写高质量的Python代码系列(二)之函数

    Python中的函数具备多种特性,这可以简化编程工作.Python函数的某些性质与其他编程语言中的函数相似,但也有性质是Python独有的.本节将介绍如何用函数来表达亿图.提升可复用程度,并减少Bug ...

随机推荐

  1. [Pyhton] SimPy 离散事件模拟框架详解 —— 以一个简单的汽车充电排队模拟为例

    目录 一.背景知识 二.SimPy 讲解 2.1 SimPy 概述 2.2 基本概念 2.3 一个汽车开开停停的例子 2.4 在走走停停过程中增加充电过程(过程交互) 2.5 共享资源 三.后续 参考 ...

  2. HTTP(s) API 经验总结

    目录 参考文章 First URL HTTP(s)-Header 请求方式 参考文章 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Overvie ...

  3. 前端常见loading动画

    loading动画是前端页面加载时必不可少的元素,好看合适的加载动画会极大的提升用户体验与系统的交互效果.下面为大家提供几种简单的加载动画效果,如果帮助到你了请点赞评论. 1.无限循环的圆圈 < ...

  4. 云原生之旅 - 14)遵循 GitOps 实践的好工具 ArgoCD

    前言 Argo CD 是一款基于 kubernetes 的声明式的Gitops 持续部署工具. 应用程序定义.配置和环境都是声明式的,并受版本控制 应用程序部署和生命周期管理都是自动化的.可审计的,并 ...

  5. [排序算法] 双向冒泡排序 (C++)

    前言 本文章是建立在冒泡排序的基础上写的,如还有对 冒泡排序 不了解的童鞋,可以看看这里哦~ 冒泡排序 C++ 双向冒泡排序原理 双向冒泡排序 的基本思想与 冒泡排序还是一样的.冒泡排序 每次将相邻的 ...

  6. Pinely Round 1 (Div. 1 + Div. 2)

    比赛链接 A 题意 构造两个长为 \(n\) 排列,使得两排列有长为 \(a\) 公共前缀和长为 \(b\) 的公共后缀. 题解 知识点:构造. 注意到,当 \(a+b\leq n-2\) 时,中间段 ...

  7. ES系列二之常见问题解决

    上篇ES系列一之java端API操作结束后本以为就相安无事了,但生产的问题是层出不穷的:下面我就再记录下近几周遇到的问题以及解决方案: 一 更新ES信息报错 报错信息如下: Use Elasticse ...

  8. 关于windows上开启远程桌面连接不上的问题解决

    解决办法 启用远程桌面连接(计算机-属性-远程设置) 选择第二个:允许运行任意版本远程桌面 关闭防火墙(或者给远程桌面添加端口3389放行也可以) 创建用户以及密码 这里注意,一定要建立密码,只有账号 ...

  9. APACHE快速安装流程梳理

    操作参考教程:https://www.cnblogs.com/haw2106/p/9839655.html 快速安装开始: [环境配置1] yum -y install gcc gcc-c++ wge ...

  10. 【Java框架】SSM-Spring总结:IOC、DI、AOP、JDBC、事务管理、实际案例

    〇.概述 1.常用资料 2.组成 一.控制反转与依赖注入 (一)Spring概述 1.介绍 以IOC和AOP为内核的框架 通过IOC实现控制,使用spring创建对象,与DI描述同一个概念 DI是对象 ...