其他章节请看:

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. Linux的挖矿木马病毒清除(kswapd0进程)

    1.top查看资源使用情况 看到这些进程一直在变化,但是,主要是由于kswapd0进程在作怪,占据了99%以上的CUP,查找资料后,发现它就是挖矿进程 2.排查kswapd0进程 执行命令netsta ...

  2. Mockito使用方法(Kotlin)

    一.为什么要使用Mockito 1.实际案例 1.1 遇到的问题 对于经常维护的项目,经常遇到一个实际问题:需求不停改变,导致架构经常需要修改某些概念的定义. 对于某些十分基础又十分常用的概念,常常牵 ...

  3. Linux系统命令基础

    Linux系统命令基础 前面咱们已经成功安装了Linux系统--centos7,那么现在奔向Linux命令行的世界. Linux命令格式 1.一般情况下,[参数]是可选的,一些情况下[文件或路径]也是 ...

  4. 初识Rasp——Openrasp代码分析

    初识Rasp--Openrasp代码分析 @author:Drag0nf1y 本文首发于奇安信安全社区,现转载到个人博客. 原文链接: https://forum.butian.net/share/1 ...

  5. while、for循环结合else

    """1.while else,当while循环正常结束时,才走else里的代码块,也就是没有被break打断的情况下2.此处只是不被break打断,也就是遇到break ...

  6. 云原生之旅 - 9)云原生时代网关的后起之秀Envoy Proxy 和基于Envoy 的 Emissary Ingress

    前言 前一篇文章讲述了基于Nginx代理的Kuberenetes Ingress Nginx[云原生时代的网关 Ingress Nginx]这次给大家介绍下基于Envoy的 Emissary Ingr ...

  7. 【笔记】CF1607F Robot on the Board 2 及相关

    题目传送门 记忆化搜索 首先,这题 \(10000\) 组 \(2000\times 2000\) 的数据直接爆搜肯定会超时.想到,如果一个点的答案已经被更新过,之后走到这个点能再多走的点也就确定了, ...

  8. Vue3 企业级优雅实战 - 组件库框架 - 6 搭建example环境

    该系列已更新文章: 分享一个实用的 vite + vue3 组件库脚手架工具,提升开发效率 开箱即用 yyg-cli 脚手架:快速创建 vue3 组件库和vue3 全家桶项目 Vue3 企业级优雅实战 ...

  9. 2022春每日一题:Day 11

    题目:高斯消元法 高斯消元法是一个模板,下面简单介绍其内容以及实现方法. 高斯消元是求一个求多元一次方程组的解的算法. 就是形式如下的关于x1,x2...xn的方程组的解. a11x1 + a12x2 ...

  10. 推荐三个实用的 Go 开发工具

    孙悟空在花果山称王的时候,特意去了一趟东海,在那里淘到了如意金箍棒.因为身为一个山大王,怎么能没有一件趁手的兵器呢? 作为程序员的我们也一样,除了我们的傍身武器 Ctrl C + V 之外,还要不停的 ...