动态tab水平菜单,这个需求很常见,特别是对于后台管理系统来讲;javascript
实现的思路有点绕,有更好的姿式请留言,谢谢阅读。java
只有一个时候是不容许关闭,因此也不会显示关闭的按钮,关闭其余也不会影响惟一的react
tag
换行mobx
& mobx-react
react-router-dom v4
styled-components
react 16.4.x
antd 3.8.x
为了保持后台的风格一致化,直接基于antd
的基础上封装一下chrome
实现的思路基本是同样的(哪怕是本身把组件都写了)数组
mobx
来维护打开的菜单数据,数据用数组来维护
tab
展现页面内容,同时关联侧边栏的菜单tab
自身能够关闭,注意规避只有一个的时候不显示关闭按钮,高亮的tab
的时候(tab
和路由匹配的状况),再次渲染组件url
之外的的全部tab
有兴趣的自行拓展,具体idea
以下缓存
icon
,这样把icon
同步到水平菜单就比较好看了,目前水平都是直接写死Model
咱们要考虑这么几点安全
item
的的组key
,和子key
,子name
以及访问的url
action
,删除的action
思路有了.剩下就是东西的出炉了,先构建model
,其实就是mobx
数据结构antd
import { observable, action, computed, toJS } from 'mobx';
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
currentUrl; // 当前访问的信息
@observable
urlHistory; // 访问过的路由信息
constructor() {
this.currentUrl = {};
this.urlHistory = [];
}
// 当前访问的信息
@action
addRoute = values => {
// 赋值
this.currentUrl = values;
// 如果数组为0
if (this.urlHistory.length === 0) {
// 则追加到数组中
this.urlHistory.push(this.currentUrl);
} else {
findObj(toJS(this.urlHistory), values)
? null
: this.urlHistory.push(this.currentUrl);
}
};
// 设置index为高亮路由
@action
setIndex = index => {
this.currentUrl = toJS(this.urlHistory[index]);
};
// 关闭单一路由
@action
closeCurrentTag = index => {
// 当历史集合长度大于一才重置,不然只剩下一个确定保留额
this.urlHistory.splice(index, 1);
this.currentUrl = toJS(this.urlHistory[this.urlHistory.length - 1]);
};
// 关闭除了当前url的其余全部路由
@action
closeOtherTag = route => {
if (this.urlHistory.length > 1) {
this.urlHistory = [this.currentUrl];
} else {
return false;
}
};
// 获取当前激活的item,也就是访问的路由信息
@computed
get activeRoute() {
return toJS(this.currentUrl);
}
// 获取当前的访问历史集合
@computed
get historyCollection() {
return toJS(this.urlHistory);
}
}
const RouterState = new RouterStateModel();
export default RouterState;
复制代码
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: ['0'],
rootSubmenuKeys: groupKey,
itemName: ''
};
}
setDefaultActiveItem = ({ location, rstat } = this.props) => {
RouterTree.map(item => {
if (item.pathname) {
// 作一些事情,这里只有二级菜单
}
// 由于菜单只有二级,简单的作个遍历就能够了
if (item.children && item.children.length > 0) {
item.children.map(childitem => {
// 为何要用match是由于 url有可能带参数等,全等就不能够了
// 如果match不到会返回null
if (location.pathname.match(childitem.path)) {
this.setState({
openKeys: [item.key],
selectedKeys: [childitem.key]
});
// 设置title
document.title = childitem.text;
// 调用mobx方法,缓存初始化的路由访问
rstat.addRoute({
groupKey: item.key,
childKey: childitem.key,
childText: childitem.text,
pathname: childitem.path
});
}
});
}
});
};
getSnapshotBeforeUpdate(prevProps, prevState) {
const { location, match } = prevProps;
// 重定向的时候用到
if (!prevState.openKeys[0] && match.path === '/') {
let snapshop = '';
RouterTree.map(item => {
if (item.pathname) {
// 作一些事情,这里只有二级菜单
}
// 由于菜单只有二级,简单的作个遍历就能够了
if (item.children && item.children.length > 0) {
return item.children.map(childitem => {
// 为何要用match是由于 url有可能带参数等,全等就不能够了
// 如果match不到会返回null
if (location.pathname.match(childitem.path)) {
snapshop = {
openKeys: [item.key],
selectedKeys: [childitem.key]
};
}
});
}
});
if (snapshop) {
return snapshop;
}
}
return null;
}
componentDidMount = () => {
// 设置菜单的默认值
this.setDefaultActiveItem();
};
componentDidUpdate = (prevProps, prevState, snapshot) => {
if (snapshot) {
this.setState(snapshot);
}
if (prevProps.location.pathname !== this.props.location.pathname) {
this.setState({
openKeys: [this.props.rstat.activeRoute.groupKey],
selectedKeys: [this.props.rstat.activeRoute.childKey]
});
}
};
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, activeRoute) => {
// 拿到路由相关的信息
const { history, location } = this.props;
// 判断咱们传入的静态路由表的路径是否和路由信息匹配
// 不匹配则容许跳转,反之打断函数
if (location.pathname === itemurl) {
return;
} else {
// 调用mobx方法,缓存路由访问
this.props.rstat.addRoute({
pathname: itemurl,
...activeRoute
});
history.push(itemurl);
}
};
render() {
const { openKeys, selectedKeys } = this.state;
const { collapsed, onCollapse } = 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={() => {
// 设置高亮的item
this.setState({ selectedKeys: [menuItem.key] });
// 设置文档标题
document.title = menuItem.text;
this.gotoUrl(menuItem.path, {
groupKey: item.key,
childKey: menuItem.key,
childText: menuItem.text
});
}}>
{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;
复制代码
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() {
const { rstat } = this.props;
const { closeTagIcon } = this.state;
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; 复制代码
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={() => {
rstat.setIndex(index);
if (tag.pathname === location.pathname) {
return;
} else {
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;
复制代码
import React from 'react';
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'));
// APP 管理
const USERLIST = asyncComponent(() => import('pages/AppManage/UserList'));
// 安全中心
const REPORT = asyncComponent(() => import('pages/Safety/Report'));
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: Analyze
},
{
key: '18',
text: '新建活动',
path: '/active/add',
component: Analyze
}
]
},
{
key: 'g3',
title: {
icon: 'scan',
text: '电影专栏'
},
exact: true,
path: '/active',
children: [
{
key: '22',
text: '电影大全',
path: '/active/list',
component: Analyze
}
]
},
{
key: 'g4',
title: {
icon: 'apple-o',
text: 'APP管理'
},
exact: true,
path: '/appmanage',
children: [
{
key: '29',
text: '移动交互',
path: '/appmanage/interaction',
component: Analyze
},
{
key: '30',
text: '用户列表',
path: '/appmanage/userlist',
component: USERLIST
},
{
key: '31',
text: '用户协议',
path: '/platform/license',
component: Analyze
},
{
key: '32',
text: '帮助中心',
path: '/platform/help',
component: Analyze
}
]
},
{
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: Analyze
}
]
},
{
key: 'g6',
title: {
icon: 'user',
text: '系统设置'
},
exact: true,
path: '/user',
children: [
{
key: '43',
text: '我的设置',
path: '/user/setting',
component: Analyze
},
{
key: '44',
text: '用户列表',
path: '/user/list',
component: Analyze
}
]
}
];
export const groupKey = RouterTree.map(item => item.key);
export default RouterTree;
复制代码
为何不作那种带两个箭头(能够往前日后),自我感受意义不大,水平菜单的宽度无论是pad
上仍是pc
上,数据结构
默认一行最起码能够打开五个tab
, 通常人的注意力都集中在几个常见的页面上,react-router
假如你须要更多呢?这里也考虑到了,直接换行,用的flex
布局,有不对之处请留言,会及时修正,谢谢阅读