其他章节请看:

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. 三十三、HPA实现自动扩缩容

    通过HPA实现业务应用的动态扩缩容 HPA控制器介绍 当系统资源过高的时候,我们可以使用如下命令来实现 Pod 的扩缩容功能 $ kubectl -n luffy scale deployment m ...

  2. Kubeadm部署k8s单点master

    Kubeadm部署k8s单点master 1.环境准备: 主机名 IP 说明 宿主机系统 master 10.0.0.17 Kubernetes集群的master节点 CentOS 7.9 node1 ...

  3. ES6 学习笔记(六)基本类型String

    字符串String 1.字面量 需要注意的地方: 由单引号或双引号括起来的字符序列. 单双引号可以嵌套,由最外围引号定界字符串 字符串字面量可以拆分成数行,每行必须以反斜线(\)结束,且反斜线都不计入 ...

  4. Dubbo-时间轮设计

    前言 Dubbo源码阅读分享系列文章,欢迎大家关注点赞 SPI实现部分 Dubbo-SPI机制 Dubbo-Adaptive实现原理 Dubbo-Activate实现原理 Dubbo SPI-Wrap ...

  5. 2022-11-05 Acwing每日一题

    本系列所有题目均为Acwing课的内容,发表博客既是为了学习总结,加深自己的印象,同时也是为了以后回过头来看时,不会感叹虚度光阴罢了,因此如果出现错误,欢迎大家能够指出错误,我会认真改正的.同时也希望 ...

  6. CodeQL(1)

    前言 开始学习使用CodeQL,做一些笔记,可供参考的资料还是比较少的,一个是官方文档,但是Google翻译过来,总觉得怪怪的,另一个就是别人的一个资源整合,其中可供参考的也不是很多,大多也是官方文档 ...

  7. 注册IBMlinuxone并使用xshell登陆

    登陆地址:https://linuxone.cloud.marist.edu/#/login 注册地址:https://linuxone.cloud.marist.edu/#/register?fla ...

  8. 【Java SE进阶】Day06 线程、同步

    一.线程 1.多线程原理 流程图 内存图解说明 创建线程的方式 继承Thread类 实现 Runnable接口 2.继承Thead类 3.实现Runnable接口 实现接口,重写run方法 最终均需要 ...

  9. echarts map地图中绘制浙江省市区县乡镇多级联动边界下钻的最新geojson数据文件获取和更新

    目录 ECharts Map地图的显示 GeoJSON数据文件获取 在ECharts中绘制浙江省的数据 ECharts Map地图的显示 ECharts支持地理坐标显示,专门提供了一个geo组件,在s ...

  10. Java工厂模式的最佳实践?

    "Simplicity is prerequisite for reliability." - Edsger Dijkstra "简单是可靠的前提条件." -- ...