前言
简化了代码逻辑和代码量,重写了一遍,执行逻辑和上个版本有所差异;
效果图
功能点
在上个版本的功能的基础上梳理,剔除一些BUG,基本都会触发联动
- 重定向
- 关闭单一标签/关闭其他标签
- 动态追加标签
- 浏览器的前进后退功能
- 同子域的,菜单会保持展开
依赖 :antd
/styled-components
/mobx
/mobx-react
/react
实现思路
- 把遍历匹配的扔到状态里面去匹配,可以减少挺多代码量
- 从布局容器触发匹配(这样初始化就能让动态菜单正常)
- 借助
getDerivedStateFromProps
和getSnapshotBeforeUpdate
这类React 16.3+
的特性实现侧边栏联动 - 动态菜单只操作mobx共享状态
代码
布局缓存活动路由的关键代码
// 路由容器那个组件 // 注入mobx状态,这样活动路由每次都能正确响应 // 减少一些不必要的渲染,update需要做一些判断..同样的路由不作处理 componentDidMount = () => { this.props.rstat.searchRoute(location.pathname); }; componentDidUpdate(prevProps, prevState) { if ( this.props.rstat.activeRoute.pathname !== this.props.location.pathname ) { this.props.rstat.searchRoute(this.props.location.pathname); } }
侧边栏(Sidebar.js)
import React, { Component } from 'react'; import { withRouter } from 'react-router-dom'; import { observer, inject } from 'mobx-react'; // antd import { Layout, Menu, Icon } from 'antd'; const { Sider } = Layout; const { SubMenu, Item } = Menu; import RouterTree, { groupKey } from 'router'; // Logo组件 import Logo from 'pages/Layout/Logo'; @inject('rstat') @withRouter @observer class Sidebar extends Component { constructor(props) { super(props); this.state = { openKeys: [''], selectedKeys: [''], rootSubmenuKeys: groupKey }; } static getDerivedStateFromProps(nextProps, prevState) { const { groupKey, childKey } = nextProps.rstat.activeRoute; if ( !prevState.openKeys[0] || (groupKey !== prevState.openKeys[0] && childKey !== prevState.selectedKeys[0]) ) { return { openKeys: [groupKey], selectedKeys: [childKey] }; } return null; } getSnapshotBeforeUpdate(prevProps, prevState) { const { openKeys, selectedKeys } = prevState; const { groupKey, childKey } = prevProps.rstat.activeRoute; if (openKeys[0] !== groupKey || selectedKeys[0] !== childKey) { return { openKeys: [groupKey], selectedKeys: [childKey] }; } return null; } componentDidUpdate = (prevProps, prevState, snapshot) => { if (snapshot) { this.setState(snapshot); } }; OpenChange = openKeys => { const latestOpenKey = openKeys.find( key => this.state.openKeys.indexOf(key) === -1 ); if (this.state.rootSubmenuKeys.indexOf(latestOpenKey) === -1) { this.setState({ openKeys }); } else { this.setState({ openKeys: latestOpenKey ? [latestOpenKey] : [...openKeys] }); } }; // 路由跳转 gotoUrl = itemurl => { const { history, location } = this.props; if (location.pathname === itemurl) { return; } else { this.props.rstat.searchRoute(itemurl) history.push(itemurl); } }; render() { const { openKeys, selectedKeys } = this.state; const { collapsed, onCollapse, rstat, history } = this.props; const SiderTree = RouterTree.map(item => ( <SubMenu key={item.key} title={ <span> <Icon type={item.title.icon} /> <span>{item.title.text}</span> </span> }> {item.children && item.children.map(menuItem => ( <Item key={menuItem.key} onClick={() => this.gotoUrl(menuItem.path)}> {menuItem.text} </Item> ))} </SubMenu> )); return ( <Sider collapsible breakpoint="lg" collapsed={collapsed} onCollapse={onCollapse} trigger={collapsed}> <Logo collapsed={collapsed} /> <Menu subMenuOpenDelay={0.3} theme="dark" openKeys={openKeys} selectedKeys={selectedKeys} mode="inline" onOpenChange={this.OpenChange}> {SiderTree} </Menu> </Sider> ); } } export default Sidebar;
Mobx Model(联动共享状态)
import { observable, action, computed, toJS } from 'mobx'; import RouterTree from 'router'; // 这个是自己维护的静态路由表 function findObj(array, obj) { for (let i = 0, j = array.length; i < j; i++) { if (array[i].childKey === obj.childKey) { return true; } } return false; } class RouterStateModel { @observable currentRouteInfo; // 当前访问的信息 @observable routerCollection; // 访问过的路由信息 constructor() { this.currentRouteInfo = {}; this.routerCollection = []; } // 当前访问的信息 @action addRoute = values => { // 赋值 this.currentRouteInfo = values; // 若是数组为0 if (this.routerCollection.length === 0) { // 则追加到数组中 this.routerCollection.push(this.currentRouteInfo); } else { findObj(this.routerCollection, values) ? null : this.routerCollection.push(this.currentRouteInfo); } }; // 设置index为高亮路由 @action setIndex = index => { this.currentRouteInfo = this.routerCollection[index]; }; // 查询路由匹配 @action searchRoute(path) { RouterTree.map(item => { if (item.pathname) { // 做一些事情,这里只有二级菜单 } // 因为菜单只有二级,简单的做个遍历就可以了 if (item.children && item.children.length > 0) { item.children.map(childitem => { // 为什么要用match是因为 url有可能带参数等,全等就不可以了 // 若是match不到会返回null if (path.match(childitem.path)) { // 设置title document.title = childitem.text; this.addRoute({ groupKey: item.key, childKey: childitem.key, childText: childitem.text, pathname: childitem.path }); } }); } }); } // 关闭单一路由 @action closeCurrentTag = index => { this.routerCollection.splice(index, 1); this.currentRouteInfo = this.routerCollection[ this.routerCollection.length - 1 ]; }; // 关闭除了当前url的其他所有路由 @action closeOtherTag = route => { if (this.routerCollection.length > 1) { this.routerCollection = [this.currentRouteInfo]; } else { return false; } }; // 获取当前激活的item,也就是访问的路由信息 @computed get activeRoute() { return toJS(this.currentRouteInfo); } // 获取当前的访问历史集合 @computed get historyCollection() { return toJS(this.routerCollection); } } const RouterState = new RouterStateModel(); export default RouterState;
静态路由表(router/index.js)
import asyncComponent from 'components/asyncComponent/asyncComponent'; // 数据分析 const Monitor = asyncComponent(() => import('pages/DashBoard/Monitor')); const Analyze = asyncComponent(() => import('pages/DashBoard/Analyze')); // 音频管理 const VoiceList = asyncComponent(() => import('pages/AudioManage/VoiceList')); const CallVoice = asyncComponent(() => import('pages/AudioManage/CallVoice')); const PrivateChat = asyncComponent(() => import('pages/AudioManage/PrivateChat') ); const Topic = asyncComponent(() => import('pages/AudioManage/Topic')); // 活动中心 const ActiveList = asyncComponent(() => import('pages/ActivityCenter/ActiveList') ); // APP 管理 const USERLIST = asyncComponent(() => import('pages/AppManage/UserList')); const ApkSetting = asyncComponent(() => import('pages/AppManage/ApkSetting')); const LicenseList = asyncComponent(() => import('pages/AppManage/LicenseList')); const QaList = asyncComponent(() => import('pages/AppManage/QaList')); // 安全中心 const REPORT = asyncComponent(() => import('pages/Safety/Report')); const BroadCast = asyncComponent(() => import('pages/Safety/BroadCast')); // 电影频道 const MovieList = asyncComponent(() => import('pages/Movie/MovieList')); // 后台管理 const UserSetting = asyncComponent(() => import('pages/AdminSetting/UserSetting') ); const RouterTree = [ { key: 'g0', title: { icon: 'dashboard', text: '数据分析' }, exact: true, path: '/dashboard', children: [ { key: '1', text: '数据监控', path: '/dashboard/monitor', component: Monitor }, { key: '2', text: '数据分析', path: '/dashboard/analyze', component: Analyze } ] }, { key: 'g1', title: { icon: 'play-circle', text: '音频管理' }, exact: true, path: '/voice', children: [ { key: '8', text: '声兮列表', path: '/voice/sxlist', component: VoiceList }, { key: '9', text: '回声列表', path: '/voice/calllist', component: CallVoice }, { key: '10', text: '私聊列表', path: '/voice/privatechat', component: PrivateChat } // { // key: '11', // text: '热门话题', // path: '/voice/topcis', // component: Topic // } ] }, { key: 'g2', title: { icon: 'schedule', text: '活动中心' }, exact: true, path: '/active', children: [ { key: '17', text: '活动列表', path: '/active/list', component: ActiveList } ] }, { key: 'g3', title: { icon: 'scan', text: '电影专栏' }, exact: true, path: '/active', children: [ { key: '22', text: '电影大全', path: '/movie/list', component: MovieList } ] }, { key: 'g4', title: { icon: 'apple-o', text: 'APP管理' }, exact: true, path: '/appmanage', children: [ { key: '29', text: 'Apk设置', path: '/appmanage/apksetting', component: ApkSetting }, { key: '30', text: '用户列表', path: '/appmanage/userlist', component: USERLIST }, { key: '31', text: '用户协议', path: '/platform/license', component: LicenseList }, { key: '32', text: '帮助中心', path: '/platform/help', component: QaList } ] }, { key: 'g5', title: { icon: 'safety', text: '安全中心' }, exact: true, path: '/safety', children: [ { key: '36', text: '举报处理', path: '/safety/report', component: REPORT }, { key: '37', text: '广播中心', path: '/safety/broadcast', component: BroadCast } ] }, { key: 'g6', title: { icon: 'user', text: '后台设置' }, exact: true, path: '/user', children: [ { key: '43', text: '个人设置', path: '/admin/setting', component: UserSetting } ] } ]; export const groupKey = RouterTree.map(item => item.key); export default RouterTree;
动态菜单
DynamicTabMenu.js
import React, { Component } from 'react'; import styled from 'styled-components'; import { withRouter } from 'react-router-dom'; import { observer, inject } from 'mobx-react'; import { Button, Popover } from 'antd'; import TagList from './TagList'; const DynamicTabMenuCSS = styled.div` box-shadow: 0px 1px 1px -1px rgba(0, 0, 0, 0.2), 0px 1px 1px 0px rgba(0, 0, 0, 0.14), 0px 1px 3px 0px rgba(0, 0, 0, 0.12); width: 100%; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; background-color: #fff; .tag-menu { flex: 1; } .operator { padding: 0 15px; flex-shrink: 1; } `; @inject('rstat') @withRouter @observer class DynamicTabMenu extends Component { constructor(props) { super(props); this.state = { closeTagIcon: false // 控制关闭所有标签的状态 }; } // 关闭其他标签 closeOtherTagFunc = () => { this.props.rstat.closeOtherTag(); }; render() { return ( <DynamicTabMenuCSS> <div className="tag-menu"> <TagList /> </div> <div className="operator" onClick={this.closeOtherTagFunc} onMouseEnter={() => { this.setState({ closeTagIcon: true }); }} onMouseLeave={() => { this.setState({ closeTagIcon: false }); }}> <Popover placement="bottom" title="关闭标签" content={'只会保留当前访问的标签'} trigger="hover"> <Button type="dashed" shape="circle" icon="close" /> </Popover> </div> </DynamicTabMenuCSS> ); } } export default DynamicTabMenu;
TagList.js
import React, { Component } from 'react'; import { withRouter } from 'react-router-dom'; import { observer, inject } from 'mobx-react'; import { Icon, Menu } from 'antd'; @inject('rstat') @withRouter @observer class TagList extends Component { constructor(props) { super(props); this.state = { showCloseIcon: false, // 控制自身关闭icon currentIndex: '' // 当前的索引 }; } render() { const { rstat, history, location } = this.props; const { showCloseIcon, currentIndex } = this.state; return ( <Menu selectedKeys={[rstat.activeRoute.childKey]} mode="horizontal"> {rstat.historyCollection && rstat.historyCollection.map((tag, index) => ( <Menu.Item key={tag.childKey} onMouseEnter={() => { this.setState({ showCloseIcon: true, currentIndex: tag.childKey }); }} onMouseLeave={() => { this.setState({ showCloseIcon: false }); }} onClick={() => { if (tag.pathname === location.pathname) { return; } else { rstat.setIndex(index) history.push(tag.pathname); } }}> <span> <Icon type="tag-o" style={{ padding: '0 0 0 10px' }} /> {tag.childText} </span> {showCloseIcon && rstat.historyCollection.length > 1 && currentIndex === tag.childKey ? ( <Icon type="close-circle" style={{ position: 'absolute', top: 0, right: -20, fontSize: 24 }} onClick={event => { event.stopPropagation(); rstat.closeCurrentTag(index); history.push( rstat.activeRoute.pathname ); }} /> ) : null} </Menu.Item> ))} </Menu> ); } } export default TagList;