React 折騰記 – (3) 結合Mobx實現一個比較靠譜的動態tab水平選單,同時關聯側邊欄

CRPER發表於2018-08-12

前言

動態tab水平選單,這個需求很常見,特別是對於後臺管理系統來說;

因為當我們側邊欄層級多了,你要找到一個子選單,必須找,展開,點選.

而有了這個,我們就能節省不少時間,體驗上來說也會改善不少

實現的思路有點繞,有更好的姿勢請留言,謝謝閱讀..


效果如下

  • 關聯展示
React 折騰記 – (3) 結合Mobx實現一個比較靠譜的動態tab水平選單,同時關聯側邊欄
React 折騰記 – (3) 結合Mobx實現一個比較靠譜的動態tab水平選單,同時關聯側邊欄
  • 單個刪除和刪除其他的標籤

只有一個時候是不允許關閉,所以也不會顯示關閉的按鈕,關閉其他也不會影響唯一的

React 折騰記 – (3) 結合Mobx實現一個比較靠譜的動態tab水平選單,同時關聯側邊欄
React 折騰記 – (3) 結合Mobx實現一個比較靠譜的動態tab水平選單,同時關聯側邊欄
  • tag換行
React 折騰記 – (3) 結合Mobx實現一個比較靠譜的動態tab水平選單,同時關聯側邊欄

基礎環境

  • mobx &
    mobx-react
  • react-router-dom v4
  • styled-components
  • react 16.4.x
  • antd 3.8.x

為了保持後臺的風格一致化,直接基於antd的基礎上封裝一下

實現的思路基本是一樣的(哪怕是自己把元件都寫了)


實現思路

思路

  • mobx來維護開啟的選單資料,資料用陣列來維護
    • 考慮追加,移除過程的去重
    • 資料及行為的設計
  • 結合路由進行響應

目標

  • 點選tab展示頁面內容,同時關聯側邊欄的選單
  • tab自身可以關閉,注意規避只有一個的時候不顯示關閉按鈕,高亮的
  • 杜絕重複點選tab的時候(tab和路由匹配的情況),再次渲染元件
  • 一鍵關閉除當前url以外的的所有tab
  • 重定向的時候也會自動展開側邊欄(路由表存在匹配的情況)

可擴充的方向

有興趣的自行擴充,具體idea如下

  • 比如快速跳轉到第一個或者最後一個的快捷選單等
  • 給側邊欄的子選單都帶上icon,這樣把icon同步到水平選單就比較好看了,目前水平都是直接寫死
  • 加上水波紋動效,目前沒有..就是MD風格點一下擴散那種
  • 拖拽,這樣可以擺出更符合自己使用習慣的水平選單
  • 固定額外不被消除的標籤,類似chrome的固定,不會給關閉所有幹掉

程式碼實現

RouterStateModel.js(mobx狀態維護)

Model我們要考慮這麼幾點

  • 側邊欄item的的組key,和子key,子name以及訪問的url
  • 追加的action,刪除的action
  • 只讀的歷史集合,只讀的當前路由物件集合

思路有了.剩下就是東西的出爐了,先構建model,其實就是mobx資料結構

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';
// antdimport {
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@observerclass 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;
複製程式碼

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@observerclass 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;
複製程式碼

TagList(標籤頁)

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@observerclass 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;
複製程式碼

RouterTree

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, 一般人的注意力都集中在幾個常見的頁面上

假如你需要更多呢?這裡也考慮到了,直接換行,用的flex佈局…

有不對之處請留言,會及時修正,謝謝閱讀

來源:https://juejin.im/post/5b6d395fe51d4517c564f2d2

相關文章