经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » React » 查看文章
react 高效高质量搭建后台系统 系列 —— 系统布局
来源:cnblogs  作者:彭加李  时间:2023/2/1 9:37:40  对本文有异议

其他章节请看:

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

系统布局

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

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

最终效果如下:

spug 中系统布局的分析

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

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

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

入口

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

  1. // App.js
  2. class App extends Component {
  3. render() {
  4. return (
  5. <Switch>
  6. <Route path="/" exact component={Login} />
  7. {/* 系统登录后进入 Layout 组件 */}
  8. <Route component={Layout} />
  9. </Switch>
  10. );
  11. }
  12. }

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

  1. return (
  2. <Layout>
  3. {/* 左侧区域,对 antd 中 Sider 的封装 */}
  4. <Sider collapsed={collapsed}/>
  5. <Layout style={{height: '100vh'}}>
  6. {/* 顶部区域, 对 antd 中 Layout.Header 的封装*/}
  7. <Header collapsed={collapsed} toggle={() => setCollapsed(!collapsed)}/>
  8. <Layout.Content className={styles.content}>
  9. <Switch>
  10. {Routes}
  11. <Route component={NotFound}/>
  12. </Switch>
  13. <Footer/>
  14. </Layout.Content>
  15. </Layout>
  16. </Layout>

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

Tip

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

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

  1. // spug\src\layout\index.js
  2. import React, { useState, useEffect } from 'react';
  3. import { Switch, Route } from 'react-router-dom';
  4. import { Layout, message } from 'antd';
  5. import { NotFound } from 'components';
  6. import Sider from './Sider';
  7. import Header from './Header';
  8. import Footer from './Footer'
  9. /*
  10. 对象数组。就像这样:
  11. [
  12. { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },
  13. ...
  14. {
  15. icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [
  16. { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmIndex },
  17. { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmContact },
  18. { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmGroup },
  19. ]
  20. },
  21. ...
  22. ]
  23. */
  24. import routes from '../routes';
  25. import { hasPermission, isMobile } from 'libs';
  26. import styles from './layout.module.less';
  27. // 将 routes 中有权限的路由提取到 Routes 中
  28. function initRoutes(Routes, routes) {
  29. for (let route of routes) {
  30. // 叶子节点才有 component。如果没有child则属于叶子节点
  31. if (route.component) {
  32. // 如果不需要权限,或有权限则放入 Routes
  33. if (!route.auth || hasPermission(route.auth)) {
  34. Routes.push(<Route exact key={route.path} path={route.path} component={route.component}/>)
  35. }
  36. } else if (route.child) {
  37. initRoutes(Routes, route.child)
  38. }
  39. }
  40. }
  41. export default function () {
  42. // 侧边栏收起状态。这里设置为展开
  43. const [collapsed, setCollapsed] = useState(false)
  44. // 路由,默认是空数组
  45. const [Routes, setRoutes] = useState([]);
  46. // 组件挂载后执行。相当于 componentDidMount()
  47. useEffect(() => {
  48. if (isMobile) {
  49. setCollapsed(true);
  50. message.warn('检测到您在移动设备上访问,请使用横屏模式。', 5)
  51. }
  52. // 注:重新声明一个变量 Routes,比上文的 Routes 作用域更小范围
  53. const Routes = [];
  54. initRoutes(Routes, routes);
  55. // console.log('Routes', Routes)
  56. // console.log('Routes', JSON.stringify(Routes))
  57. setRoutes(Routes)
  58. }, [])
  59. return (
  60. // 此处 Layout 是 antd 布局组件。和官方用法相同:
  61. /*
  62. <Layout>
  63. <Sider>Sider</Sider>
  64. <Layout>
  65. <Header>Header</Header>
  66. <Content>Content</Content>
  67. <Footer>Footer</Footer>
  68. </Layout>
  69. </Layout>
  70. */
  71. <Layout>
  72. {/* 左侧区域,对 antd 中 Sider 的封装 */}
  73. <Sider collapsed={collapsed}/>
  74. {/* 内容高度不够,版权信息在底部;内容高度太高,则需要滚动才可查看全部内容; */}
  75. <Layout style={{height: '100vh'}}>
  76. {/* 顶部区域, 对 antd 中 Layout.Header 的封装*/}
  77. <Header collapsed={collapsed} toggle={() => setCollapsed(!collapsed)}/>
  78. <Layout.Content className={styles.content}>
  79. {/* 只渲染第一个路径匹配的组件。类似 if...else。参考:https://www.cnblogs.com/pengjiali/p/16045481.html#Switch */}
  80. <Switch>
  81. {/* 路由数组。里面每项类似这样:<Route exact key={route.path} path='/home' component={HomeComponent}/> */}
  82. {Routes}
  83. {/* 没有匹配则进入 NotFound */}
  84. <Route component={NotFound}/>
  85. </Switch>
  86. {/* 系统底部展示。例如版权、官网、文档链接、仓库链接*/}
  87. {/* 父元素采用 flex 布局,当主体内容不多时,版权这部分信息也会置于底部 */}
  88. <Footer/>
  89. </Layout.Content>
  90. </Layout>
  91. </Layout>
  92. )
  93. }

左侧导航

左侧导航封装在 Sider(spug\src\layout\Sider.js) 组件中。

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

  1. // <4.20.0 可用,>=4.20.0 时不推荐
  2. <Menu>
  3. <Menu.Item>菜单项一</Menu.Item>
  4. <Menu.Item>菜单项二</Menu.Item>
  5. <Menu.SubMenu title="子菜单">
  6. <Menu.Item>子菜单项</Menu.Item>
  7. </Menu.SubMenu>
  8. </Menu>;

完整代码如下:

  1. // spug\src\layout\Sider.js
  2. import React, { useState } from 'react';
  3. import { Layout, Menu } from 'antd';
  4. import { hasPermission, history } from 'libs';
  5. import styles from './layout.module.less';
  6. /*
  7. 对象数组。就像这样:
  8. [
  9. { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },
  10. ...
  11. {
  12. icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [
  13. { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmIndex },
  14. { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmContact },
  15. { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmGroup },
  16. ]
  17. },
  18. ...
  19. ]
  20. */
  21. import menus from '../routes';
  22. import logo from './spug.png'
  23. // 当前选中的菜单项 key 数组
  24. let selectedKey = window.location.pathname;
  25. /*
  26. 初始化菜单映射。如果输入不存在的路径,那么菜单则无需选中
  27. {
  28. /home: 1, // 一级菜单
  29. /dashboard: 1, // 一级菜单
  30. ...
  31. /alarm/alarm: "报警中心", // 二级菜单
  32. /alarm/contact: "报警中心", // 二级菜单
  33. /alarm/group: "报警中心", // 二级菜单
  34. ...
  35. }
  36. */
  37. const OpenKeysMap = {};
  38. for (let item of menus) {
  39. if (item.child) {
  40. for (let sub of item.child) {
  41. // child 中的节点值为 item.title
  42. if (sub.title) OpenKeysMap[sub.path] = item.title
  43. }
  44. } else if (item.title) {
  45. // 一级节点的值是 1
  46. OpenKeysMap[item.path] = 1
  47. }
  48. }
  49. export default function Sider(props) {
  50. // openKeys 当前展开的 SubMenu 菜单项 key 数组 string[]
  51. // const [openKeys, setOpenKeys] = useState([]);
  52. // 根据路由返回菜单项或子菜单。没有权限或没有 title 返回 null
  53. function makeMenu(menu) {
  54. // 如果没有权限
  55. if (menu.auth && !hasPermission(menu.auth)) return null;
  56. // 没有 title 返回 null
  57. if (!menu.title) return null;
  58. // 如果有 child 则调用 _makeSubMenu;没有 child 则调用 _makeItem
  59. return menu.child ? _makeSubMenu(menu) : _makeItem(menu)
  60. }
  61. // 返回子菜单
  62. function _makeSubMenu(menu) {
  63. return (
  64. <Menu.SubMenu key={menu.title} title={<span>{menu.icon}<span>{menu.title}</span></span>}>
  65. {menu.child.map(menu => makeMenu(menu))}
  66. </Menu.SubMenu>
  67. )
  68. }
  69. // 返回菜单项
  70. function _makeItem(menu) {
  71. return (
  72. <Menu.Item key={menu.path}>
  73. {menu.icon}
  74. <span>{menu.title}</span>
  75. </Menu.Item>
  76. )
  77. }
  78. // window.location.pathname 返回当前页面的路径或文件名
  79. // 例如 https://demo.spug.cc/host?name=pjl 返回 /host
  80. const tmp = window.location.pathname;
  81. const openKey = OpenKeysMap[tmp];
  82. // 如果是不存在的路径(例如 /host9999),菜单则无需选中
  83. if (openKey) {
  84. // 当前选中的菜单项 key 数组。
  85. selectedKey = tmp;
  86. // 更新子菜单。`openKey 不是1` && `侧边栏展开` &&
  87. // if (openKey !== 1 && !props.collapsed && !openKeys.includes(openKey)) {
  88. // setOpenKeys([...openKeys, openKey])
  89. // }
  90. }
  91. // 下面的className都仅仅让样式好看点,对功能没有影响。
  92. return (
  93. // Sider:侧边栏,自带默认样式及基本功能,其下可嵌套任何元素,只能放在 Layout 中。
  94. // collapsed - 当前收起状态。这里设置为默认展开
  95. <Layout.Sider width={208} collapsed={props.collapsed} className={styles.sider}>
  96. {/* 图标 */}
  97. <div className={styles.logo}>
  98. <img src={logo} alt="Logo" style={{ height: '30px' }} />
  99. </div>
  100. <div className={styles.menus} style={{ height: `${document.body.clientHeight - 64}px` }}>
  101. {/* 导航菜单。使用的是`缩起内嵌菜单` */}
  102. <Menu
  103. theme="dark"
  104. mode="inline"
  105. className={styles.menus}
  106. // 当前选中的菜单项 key 数组
  107. selectedKeys={[selectedKey]}
  108. // openKeys 当前展开的 SubMenu 菜单项 key 数组 string[]
  109. // openKeys={openKeys}
  110. // onOpenChange - SubMenu 展开/关闭的回调
  111. // onOpenChange={setOpenKeys}
  112. // 路由切换。点击哪个导航,url和路由就会切换到该路劲
  113. onSelect={menu => history.push(menu.key)}>
  114. {/* 数组中的 null 会被忽略 */}
  115. {menus.map(menu => makeMenu(menu))}
  116. </Menu>
  117. </div>
  118. </Layout.Sider>
  119. )
  120. }

代码简析:

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

头部

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

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

完整代码:

  1. // spug\src\layout\Header.js
  2. import React from 'react';
  3. import { Link } from 'react-router-dom';
  4. import { Layout, Dropdown, Menu, Avatar } from 'antd';
  5. import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LogoutOutlined } from '@ant-design/icons';
  6. import Notification from './Notification';
  7. import styles from './layout.module.less';
  8. import http from '../libs/http';
  9. import history from '../libs/history';
  10. import avatar from './avatar.png';
  11. export default function (props) {
  12. // 退出
  13. function handleLogout() {
  14. // 跳转到登录页
  15. history.push('/');
  16. // 告诉后端退出登录
  17. http.get('/api/account/logout/')
  18. }
  19. const UserMenu = (
  20. <Menu>
  21. <Menu.Item>
  22. {/* 路由跳转。主体区域对应路由是 `{ path: '/welcome/info', component: WelcomeInfo },` */}
  23. <Link to="/welcome/info">
  24. <UserOutlined style={{marginRight: 10}}/>个人中心
  25. </Link>
  26. </Menu.Item>
  27. <Menu.Divider/>
  28. <Menu.Item onClick={handleLogout}>
  29. <LogoutOutlined style={{marginRight: 10}}/>退出登录
  30. </Menu.Item>
  31. </Menu>
  32. );
  33. return (
  34. <Layout.Header className={styles.header}>
  35. {/* 收缩左侧导航按钮 */}
  36. <div className={styles.left}>
  37. {/* 点击触发父组件的 toggle 方法 */}
  38. <div className={styles.trigger} onClick={props.toggle}>
  39. {/* 根据父组件的 collapsed 属性显示对应图标*/}
  40. {props.collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
  41. </div>
  42. </div>
  43. {/* 通知 */}
  44. <Notification/>
  45. {/* 用户区域 */}
  46. <div className={styles.right}>
  47. <Dropdown overlay={UserMenu} style={{background: '#000'}}>
  48. <span className={styles.action}>
  49. <Avatar size="small" src={avatar} style={{marginRight: 8}}/>
  50. {/* 登录后设置过的昵称 */}
  51. {localStorage.getItem('nickname')}
  52. </span>
  53. </Dropdown>
  54. </div>
  55. </Layout.Header>
  56. )
  57. }

主体区域

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

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

  1. // spug\src\pages\home\index.js
  2. function HomeIndex() {
  3. return (
  4. <div>
  5. {/* 面包屑 */}
  6. <Breadcrumb>
  7. <Breadcrumb.Item>首页</Breadcrumb.Item>
  8. <Breadcrumb.Item>工作台</Breadcrumb.Item>
  9. </Breadcrumb>
  10. <Row gutter={12}>
  11. <Col span={16}>
  12. <NavIndex />
  13. </Col>
  14. <Col span={8}>
  15. <Row gutter={[12, 12]}>
  16. <Col span={24}>
  17. <TodoIndex />
  18. </Col>
  19. <Col span={24}>
  20. <NoticeIndex />
  21. </Col>
  22. </Row>
  23. </Col>
  24. </Row>
  25. </div>
  26. )
  27. }
  28. export default HomeIndex

myspug 系统布局的实现

入口

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

  1. // myspug\src\App.js
  2. -import HelloWorld from './HelloWord'
  3. +import Layout from './layout'
  4. import { Switch, Route } from 'react-router-dom';
  5. // 定义一个类组件
  6. class App extends Component {
  7. <Switch>
  8. <Route path="/" exact component={Login} />
  9. {/* 没有匹配则进入 Layout */}
  10. - <Route component={HelloWorld} />
  11. + <Route component={Layout} />
  12. </Switch>
  13. );
  14. }

Layout 中 index.js 代码如下:

  1. // myspug\src\layout\index.js
  2. import React, { useState, useEffect } from 'react';
  3. import { Switch, Route } from 'react-router-dom';
  4. import { Layout, message } from 'antd';
  5. // 404 对应的组件
  6. /*
  7. // myspug\src\compoments\index.js
  8. import NotFound from './NotFound';
  9. export {
  10. NotFound,
  11. }
  12. */
  13. import { NotFound } from '@/components';
  14. // 侧边栏
  15. import Sider from './Sider';
  16. // 头部
  17. import Header from './Header';
  18. // 页脚。例如版权
  19. import Footer from './Footer'
  20. /*
  21. 引入路由。对象数组,就像这样:
  22. [
  23. { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },
  24. ...
  25. {
  26. icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [
  27. { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmIndex },
  28. { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmContact },
  29. { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmGroup },
  30. ]
  31. },
  32. ...
  33. ]
  34. */
  35. import routes from '../routes';
  36. // hasPermission - 权限判断。本篇忽略,这里直接返回 true; isMobile - 是否是手机
  37. /*
  38. export function hasPermission(strCode) {
  39. return true
  40. }
  41. // 基于检测用户代理字符串的浏览器标识是不可靠的,不推荐使用,因为用户代理字符串是用户可配置的
  42. export const isMobile = /Android|iPhone/i.test(navigator.userAgent)
  43. */
  44. import { hasPermission, isMobile } from '@/libs';
  45. // 布局样式,直接拷贝 spug 中的样式即可
  46. import styles from './layout.module.less';
  47. // 将 routes 中有权限的路由提取到 Routes 中
  48. function initRoutes(Routes, routes) {
  49. for (let route of routes) {
  50. // 叶子节点才有 component。没有 child 则属于叶子节点
  51. if (route.component) {
  52. // 如果不需要权限,或有权限则放入 Routes
  53. if (!route.auth || hasPermission(route.auth)) {
  54. Routes.push(<Route exact key={route.path} path={route.path} component={route.component} />)
  55. }
  56. } else if (route.child) {
  57. initRoutes(Routes, route.child)
  58. }
  59. }
  60. }
  61. export default function () {
  62. // 侧边栏收缩状态。默认展开
  63. const [collapsed, setCollapsed] = useState(false)
  64. // 路由,默认是空数组
  65. const [Routes, setRoutes] = useState([]);
  66. // 组件挂载后执行。相当于 componentDidMount()
  67. useEffect(() => {
  68. if (isMobile) {
  69. // 手机查看时导航栏收起
  70. setCollapsed(true);
  71. message.warn('检测到您在移动设备上访问,请使用横屏模式。', 5)
  72. }
  73. // 注:重新声明一个变量 Routes,比上文(useState 中的 Routes)的 Routes 作用域更小范围
  74. const Routes = [];
  75. initRoutes(Routes, routes);
  76. setRoutes(Routes)
  77. }, [])
  78. return (
  79. // 此处 Layout 是 antd 布局组件。和官方用法相同:
  80. /*
  81. <Layout>
  82. <Sider>Sider</Sider>
  83. <Layout>
  84. <Header>Header</Header>
  85. <Content>Content</Content>
  86. <Footer>Footer</Footer>
  87. </Layout>
  88. </Layout>
  89. */
  90. <Layout>
  91. {/* 左侧区域,对 antd 中 Sider 的封装 */}
  92. <Sider collapsed={collapsed} />
  93. {/* 内容高度不够,版权信息在底部;内容高度太高,则需要滚动才可查看全部内容; */}
  94. <Layout style={{ height: '100vh' }}>
  95. {/* 顶部区域, 对 antd 中 Layout.Header 的封装*/}
  96. <Header collapsed={collapsed} toggle={() => setCollapsed(!collapsed)} />
  97. <Layout.Content className={styles.content}>
  98. {/* 只渲染第一个路径匹配的组件*/}
  99. <Switch>
  100. {/* 路由数组。里面每项类似这样:<Route exact key={route.path} path='/home' component={HomeComponent}/> */}
  101. {Routes}
  102. {/* 没有匹配则进入 NotFound */}
  103. <Route component={NotFound} />
  104. </Switch>
  105. {/* 系统底部展示。例如版权、官网、文档链接、仓库链接*/}
  106. <Footer />
  107. </Layout.Content>
  108. </Layout>
  109. </Layout>
  110. )
  111. }

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

  1. // myspug\src\routes.js
  2. import React from 'react';
  3. import {
  4. DesktopOutlined,
  5. AlertOutlined,
  6. } from '@ant-design/icons';
  7. /*
  8. export default function HomeIndex() {
  9. return <div>我是主页</div>
  10. }
  11. */
  12. import HomeIndex from './pages/home';
  13. // 占位效果
  14. /*
  15. export default function AlarmCenter() {
  16. return <div>报警中心占位符 - {window.location.pathname}</div>
  17. }
  18. */
  19. import AlarmCenter from './pages/alarm/alarm';
  20. // 个人中心
  21. /*
  22. export default function HomeIndex() {
  23. return <div>我是个人中心</div>
  24. }
  25. */
  26. import WelcomeInfo from './pages/welcome/info';
  27. export default [
  28. { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },
  29. {
  30. icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [
  31. { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmCenter },
  32. { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmCenter },
  33. { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmCenter },
  34. ]
  35. },
  36. { path: '/welcome/info', component: WelcomeInfo },
  37. ]

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

NotFound 代码如下:

  1. // myspug\src\compoments\NotFound.js
  2. import React from 'react';
  3. // 拷贝 spug 中的内容
  4. import styles from './index.module.less';
  5. export default function NotFound() {
  6. return (
  7. <div className={styles.notFound}>
  8. <div className={styles.imgBlock}>
  9. <div className={styles.img} />
  10. </div>
  11. <div>
  12. <h1 className={styles.title}>404</h1>
  13. <div className={styles.desc}>抱歉,你访问的页面不存在</div>
  14. </div>
  15. </div>
  16. )
  17. }

左侧导航

  1. // myspug\src\layout\Sider.js
  2. import React, { useState } from 'react';
  3. import { Layout, Menu } from 'antd';
  4. import { hasPermission, history } from '@/libs';
  5. import styles from './layout.module.less';
  6. /*
  7. 对象数组。就像这样:
  8. [
  9. { icon: <DesktopOutlined />, title: '工作台', path: '/home', component: HomeIndex },
  10. ...
  11. {
  12. icon: <AlertOutlined />, title: '报警中心', auth: 'alarm.alarm.view|alarm.contact.view|alarm.group.view', child: [
  13. { title: '报警历史', auth: 'alarm.alarm.view', path: '/alarm/alarm', component: AlarmIndex },
  14. { title: '报警联系人', auth: 'alarm.contact.view', path: '/alarm/contact', component: AlarmContact },
  15. { title: '报警联系组', auth: 'alarm.group.view', path: '/alarm/group', component: AlarmGroup },
  16. ]
  17. },
  18. ...
  19. ]
  20. */
  21. import menus from '../routes';
  22. import logo from './spug.png'
  23. let selectedKey = window.location.pathname;
  24. /*
  25. 菜单映射。如果输入不存在的路径,那么菜单就不需要选中
  26. {
  27. /home: 1, // 一级菜单
  28. /dashboard: 1, // 一级菜单
  29. ...
  30. /alarm/alarm: "报警中心", // 二级菜单
  31. /alarm/contact: "报警中心", // 二级菜单
  32. /alarm/group: "报警中心", // 二级菜单
  33. ...
  34. }
  35. */
  36. const OpenKeysMap = {};
  37. for (let item of menus) {
  38. if (item.child) {
  39. for (let sub of item.child) {
  40. // child 中的节点值为 item.title
  41. if (sub.title) OpenKeysMap[sub.path] = item.title
  42. }
  43. } else if (item.title) {
  44. // 一级节点的值是 1
  45. OpenKeysMap[item.path] = 1
  46. }
  47. }
  48. export default function Sider(props) {
  49. // 根据路由返回菜单项或子菜单。没有权限或没有 title 返回 null
  50. function makeMenu(menu) {
  51. // 如果没有权限
  52. if (menu.auth && !hasPermission(menu.auth)) return null;
  53. // 没有 title 返回 null
  54. if (!menu.title) return null;
  55. // 如果有 child 则调用 _makeSubMenu;没有 child 则调用 _makeItem
  56. return menu.child ? _makeSubMenu(menu) : _makeItem(menu)
  57. }
  58. // 返回子菜单
  59. function _makeSubMenu(menu) {
  60. return (
  61. <Menu.SubMenu key={menu.title} title={<span>{menu.icon}<span>{menu.title}</span></span>}>
  62. {menu.child.map(menu => makeMenu(menu))}
  63. </Menu.SubMenu>
  64. )
  65. }
  66. // 返回菜单项
  67. function _makeItem(menu) {
  68. return (
  69. <Menu.Item key={menu.path}>
  70. {menu.icon}
  71. <span>{menu.title}</span>
  72. </Menu.Item>
  73. )
  74. }
  75. // window.location.pathname 返回当前页面的路径或文件名
  76. // 例如 https://demo.spug.cc/host?name=pjl 返回 /host
  77. const tmp = window.location.pathname;
  78. const openKey = OpenKeysMap[tmp];
  79. // 如果是不存在的路径(例如 /host9999),菜单则无需选中
  80. if (openKey) {
  81. // 当前选中的菜单项 key 数组。
  82. selectedKey = tmp;
  83. }
  84. // 下面的className都仅仅让样式好看点,对功能没有影响。
  85. return (
  86. // Sider:侧边栏,自带默认样式及基本功能,其下可嵌套任何元素,只能放在 Layout 中。
  87. // collapsed - 当前收起状态。这里设置为默认展开
  88. <Layout.Sider width={208} collapsed={props.collapsed} className={styles.sider}>
  89. {/* 图标 */}
  90. <div className={styles.logo}>
  91. <img src={logo} alt="Logo" style={{ height: '30px' }} />
  92. </div>
  93. <div className={styles.menus} style={{ height: `${document.body.clientHeight - 64}px` }}>
  94. {/* 导航菜单。使用的是`缩起内嵌菜单` */}
  95. <Menu
  96. theme="dark"
  97. mode="inline"
  98. className={styles.menus}
  99. // 当前选中的菜单项 key 数组
  100. selectedKeys={[selectedKey]}
  101. // 路由切换。点击哪个导航,url和路由就会切换到该路劲
  102. onSelect={menu => history.push(menu.key)}>
  103. {/* 数组中的 null 会被忽略 */}
  104. {menus.map(menu => makeMenu(menu))}
  105. </Menu>
  106. </div>
  107. </Layout.Sider>
  108. )
  109. }

头部

Tip通知暂不实现

代码如下:

  1. // myspug\src\layout\Header.js
  2. import React from 'react';
  3. import { Link } from 'react-router-dom';
  4. import { Layout, Dropdown, Menu, Avatar } from 'antd';
  5. import { MenuFoldOutlined, MenuUnfoldOutlined, UserOutlined, LogoutOutlined } from '@ant-design/icons';
  6. // `通知`暂不实现
  7. // import Notification from './Notification';
  8. import styles from './layout.module.less';
  9. import http from '../libs/http';
  10. import history from '../libs/history';
  11. import avatar from './avatar.png';
  12. export default function (props) {
  13. // 退出
  14. function handleLogout() {
  15. // 跳转到登录页
  16. history.push('/');
  17. // 告诉后端退出登录
  18. http.get('/api/account/logout/')
  19. }
  20. const UserMenu = (
  21. <Menu>
  22. <Menu.Item>
  23. {/* 路由跳转。主体区域对应路由是 `{ path: '/welcome/info', component: WelcomeInfo },` */}
  24. <Link to="/welcome/info">
  25. <UserOutlined style={{ marginRight: 10 }} />个人中心
  26. </Link>
  27. </Menu.Item>
  28. <Menu.Divider />
  29. <Menu.Item onClick={handleLogout}>
  30. <LogoutOutlined style={{ marginRight: 10 }} />退出登录
  31. </Menu.Item>
  32. </Menu>
  33. );
  34. return (
  35. <Layout.Header className={styles.header}>
  36. {/* 收缩左侧导航按钮 */}
  37. <div className={styles.left}>
  38. {/* 点击触发父组件的 toggle 方法 */}
  39. <div className={styles.trigger} onClick={props.toggle}>
  40. {/* 根据父组件的 collapsed 属性显示对应图标*/}
  41. {props.collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
  42. </div>
  43. </div>
  44. {/* 通知 */}
  45. <div>通知 todo</div>
  46. {/* <Notification/> */}
  47. {/* 用户区域 */}
  48. <div className={styles.right}>
  49. <Dropdown overlay={UserMenu} style={{ background: '#000' }}>
  50. <span className={styles.action}>
  51. <Avatar size="small" src={avatar} style={{ marginRight: 8 }} />
  52. {/* 登录后设置过的昵称 */}
  53. {localStorage.getItem('nickname')}
  54. </span>
  55. </Dropdown>
  56. </div>
  57. </Layout.Header>
  58. )
  59. }

less 模块化样式的配置

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

目前 myspug 支持 index.module.css:

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

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

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

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

使其支持费了一些波折:

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

最终解决方法如下:

  1. // config-overrides.js
  2. -const { override, fixBabelImports, addWebpackAlias } = require('customize-cra');
  3. +const { override, fixBabelImports, addWebpackAlias, addLessLoader, adjustStyleLoaders } = require('customize-cra');
  4. const path = require('path')
  5. module.exports = override(
  6. fixBabelImports('import', {
  7. module.exports = override(
  8. // 增加别名。避免 ../../ 相对路劲引入 libs/http
  9. addWebpackAlias({
  10. '@': path.resolve(__dirname, './src')
  11. - })
  12. + }),
  13. + // 解决
  14. + addLessLoader({
  15. + lessOptions: {
  16. + javascriptEnabled: true,
  17. + localIdentName: '[local]--[hash:base64:5]'
  18. + }
  19. + }),
  20. + // 网友`阖湖丶`的介绍,解决:ValidationError: Invalid options object. PostCSS Loader has been initialized...
  21. + adjustStyleLoaders(({ use: [, , postcss] }) => {
  22. + const postcssOptions = postcss.options;
  23. + postcss.options = { postcssOptions };
  24. + }),
  25. );

效果验证

最终效果:

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

其他章节请看:

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

原文链接:https://www.cnblogs.com/pengjiali/p/17079747.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号