其他章节请看:
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 :
这里的 Sider 和 Header 都不是 antd 中的原始组件,已被封装,挪出成一个单独的组件。 <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 高效高质量搭建后台系统 系列 —— 系统布局的详细内容...