動態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
佈局,有不對之處請留言,會及時修正,謝謝閱讀