前言
动态tab水平菜单,这个需求很常见,特别是对于后台管理系统来说;
因为当我们侧边栏层级多了,你要找到一个子菜单,必须找,展开,点击.
而有了这个,我们就能节省不少时间,体验上来说也会改善不少
实现的思路有点绕,有更好的姿势请留言,谢谢阅读..
效果如下
- 关联展示
- 单个删除和删除其他的标签
只有一个时候是不允许关闭,所以也不会显示关闭的按钮,关闭其他也不会影响唯一的
- 多
tag
换行
基础环境
mobx
/ react-router-dom
/ styled-components
/ react
/ antd
为了保持后台的风格一致化,直接基于antd
的基础上封装一下
实现的思路基本是一样的(哪怕是自己把组件都写了)
实现思路
思路
- 用
mobx
来维护打开的菜单数据,数据用数组来维护- 考虑追加,移除过程的去重
- 数据及行为的设计
- 结合路由进行响应
目标
- 点击
tab
展示页面内容,同时关联侧边栏的菜单 tab
自身可以关闭,注意规避只有一个的时候不显示关闭按钮,高亮的- 杜绝重复点击
tab
的时候(tab
和路由匹配的情况),再次渲染组件 - 一键关闭除当前
url
意外的的所有tab
可拓展的方向
有兴趣的自行拓展,具体idea
如下
- 比如快速跳转到第一个或者最后一个的快捷菜单等
- 给侧边栏的子菜单都带上
icon
,这样把icon
同步到水平菜单就比较好看了,目前水平都是直接写死 - 加上水波纹动效,目前没有..就是MD风格点一下扩散那种
- 拖拽,这样可以摆出更符合自己使用习惯的水平菜单
- 固定额外不被消除的标签,类似chrome的固定,不会给关闭所有干掉
代码实现
Model我们要考虑这么几点
- 侧边栏
item
的的组key
,和子key
,子name
以及访问的url
- 追加的
action
,删除的action
- 只读的历史集合,只读的当前路由对象集合
思路有了.剩下就是东西的出炉了,先构建model
,其实就是mobx
数据结构
RouterStateModel.js
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;
复制代码
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 { sidebarData, groupKey } from 'pages/Layout/SidebarData';
// Logo组件
import Logo from 'pages/Layout/Logo';
import { rstat } from 'store/RouterStateModel';
@inject('rstat')
@withRouter
@observer
class Sidebar extends Component {
constructor(props) {
super(props);
// 初始化置空可以在遍历不到的时候应用默认值
this.state = {
openKeys: [''],
selectedKeys: [''],
rootSubmenuKeys: groupKey,
itemName: ''
};
}
setDefaultActiveItem = ({ location, rstat } = this.props) => {
sidebarData.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
});
}
});
}
});
};
componentDidMount = () => {
// 设置菜单的默认值
this.setDefaultActiveItem();
};
componentDidUpdate = (prevProps, prevState) => {
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 SideTree = sidebarData.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}>
{SideTree}
</Menu>
</Sider>
);
}
}
export default Sidebar;
复制代码
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 { sidebarData } from 'pages/Layout/SidebarData';
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;
.tab-menu {
flex: 1;
}
.operator {
flex-shrink: 1;
}
`;
@inject('rstat')
@withRouter
@observer
class DynamicTabMenu extends Component {
constructor(props) {
super(props);
this.state = {
closeOtherTag: false // 控制关闭所有标签的状态
};
}
initTagMenu = ({ location } = this.props) => {
sidebarData.map(item => {
// 因为菜单只有二级,简单的做个遍历就可以了
if (item.children && item.children.length > 0) {
item.children.map(childitem => {
// 为什么要用match是因为 url有可能带参数等,全等就不可以了
// 若是match不到会返回null
if (location.pathname.match(childitem.path)) {
this.setState({
currentTag: childitem.key
});
}
});
}
});
};
componentDidMount = () => {
// 设置tag高亮
this.initTagMenu();
};
// 关闭其他标签
closeOtherTagFunc = () => {
this.props.rstat.closeOtherTag();
};
render() {
const { rstate } = this.props;
const { closeOtherTag } = this.state;
return (
<DynamicTabMenuCSS>
<div className="tab-menu">
<TagList />
</div>
<div
className="operator"
onClick={this.closeOtherTagFunc}
onMouseEnter={() => {
this.setState({
closeOtherTag: true
});
}}
onMouseLeave={() => {
this.setState({
closeOtherTag: 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, Button } from 'antd';
@inject('rstat')
@withRouter
@observer
class TagList extends Component {
constructor(props) {
super(props);
this.state = {
showCloseIcon: false, // 控制自身关闭icon
currentIndex: '', // 当前的索引
selectedKeys: ''
};
}
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
});
}}>
<span
onClick={() => {
rstat.setIndex(index);
if (tag.pathname === location.pathname) {
return;
} else {
history.push(tag.pathname);
}
}}>
<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={{
padding: '0 0 0 10px',
position: 'absolute',
top: 0,
right: 0,
marginRight: -12,
zIndex: 999,
fontSize: 24
}}
onClick={() => {
rstat.closeCurrentTag(index);
history.push(
rstat.activeRoute.pathname
);
}}
/>
) : null}
</Menu.Item>
))}
</Menu>
);
}
}
export default TagList;
复制代码
总结
为什么不做那种带两个箭头,可以往前或者往后的..
因为感觉意义不大,水平菜单的宽度不管是pad
上还是pc
上,
默认一行最起码可以打开五个tab
, 一般人的注意力都集中在几个常见的页面上
假如你需要更多呢?这里也考虑到了,直接换行,用的flex
布局...
有不对之处请留言,会及时修正,谢谢阅读