一步一步構(gòu)件react后臺(tái)系統(tǒng) 9 之 tab切換
先看看效果
可以看到, 在面包屑下面多了個(gè)tab欄

思路
之前看到別人的后臺(tái)系統(tǒng), 有一個(gè)tab, 來(lái)記錄所有跳轉(zhuǎn)過(guò)的頁(yè)面, 我就在想, 該怎么做?
然后看了一下別人的代碼。
原理是這樣的。
做一個(gè)tab, 然后點(diǎn)擊導(dǎo)航的時(shí)候, 我們的props就會(huì)發(fā)生改變, componentWillReceiveProps 方法就會(huì)被調(diào)用, 然后就在里面監(jiān)聽(tīng), 判斷當(dāng)前頁(yè)面在不在tab面板里面, 如果不在,則拿到當(dāng)前頁(yè)面路徑到路由表里面進(jìn)行尋找, 找到對(duì)應(yīng)的對(duì)象并提取出來(lái), 然后加入到所有tabs數(shù)組里面, 并在組件里面進(jìn)行渲染。
在路由配置里面, 有component屬性的, 里面保存著組件, 直接把這個(gè)渲染在tab的面板里面就可以了。
開(kāi)始動(dòng)手
- 先貼出components/tabs.js完整代碼
import React, { Component } from 'react';
import { Tabs } from 'antd';
import { filterData, deleObj, deepFlatten, removeArrItem } from '@/utils/index.js'
import { main as mainConfig } from '@/router/index'
import {connect} from "react-redux";
import {withRouter} from "react-router-dom";
import {crumbsMap} from "@/reducer/connect";
const TabPane = Tabs.TabPane;
class MyTabs extends Component {
constructor(props) {
super(props)
this.state = {
currentPage: {},
openPages: [],
routerConfig: deepFlatten(mainConfig),
mode: 'top',
}
}
handleModeChange = () => {
let mode = 'left';
switch (this.state.mode) {
case 'top':
mode = 'left';
break;
case 'left' :
mode = 'right';
break;
case 'right':
mode = 'bottom';
break;
default:
mode = 'top'
}
this.setState({ mode });
}
onEdit = (targetKey, action) => {
if (action === 'remove') {
this.removeTabs(targetKey)
}
}
removeTabs = (targetKey) => {
let openPages = removeArrItem(this.state.openPages, function(item){
return item.path === targetKey
})
this.setState({
openPages
})
}
onTabClick = (activeKey) => {
}
componentDidMount () {
}
// props 更新時(shí)調(diào)用
componentWillReceiveProps (nextProps) {
this.addRouteToAllTabPans(nextProps)
}
addRouteToAllTabPans (props) {
if (props.location.pathname !== this.state.currentPage.path && props.location.pathname !=='/index') {
let {openPages} = this.state
let isHasRoute = openPages.some(item =>item.path === props.location.pathname)
if (!isHasRoute) {
let currentRoute = this.getCurrentRoute(props.location.pathname)
if (currentRoute) {
openPages.push(currentRoute)
this.setState({
openPages,
currentPage: currentRoute
})
}
}
}
}
getCurrentRoute (path) {
return this.state.routerConfig.filter(item => {
if (item.path === path) return item
})[0]
}
render() {
let { location, getRouterConfig, routerConfig, routes } = this.props
return (
<div>
<Tabs
hideAdd
defaultActiveKey={this.state.currentPage.path}
type="editable-card"
animated={true}
onEdit={this.onEdit}
onTabClick={this.onTabClick}
tabBarGutter={5}
hideAdd={true}
tabPosition={this.state.mode}
tabBarExtraContent={<span onClick={this.handleModeChange}>{this.state.mode}</span>}
>
{this.state.openPages.map(page => {
return <TabPane forceRender tab={page.name} closable={page.closable} key={page.path}>
<page.component routes={routes}></page.component>
</TabPane>
})}
</Tabs>
</div>
);
}
}
export default connect(crumbsMap.mapStateToProps, crumbsMap.mapDispatchToProps)(withRouter(MyTabs))
- 查看細(xì)節(jié)
- props 改變的時(shí)候, 會(huì)觸發(fā)
// props 更新時(shí)調(diào)用
componentWillReceiveProps (nextProps) {
this.addRouteToAllTabPans(nextProps)
}
- 判斷 點(diǎn)擊的是當(dāng)前tab頁(yè)面, 則不做反應(yīng), 如果點(diǎn)擊的是index頁(yè)面, 同樣不做反應(yīng)。
- 使用some判斷當(dāng)前路徑是否已經(jīng)存在 openPages(tabs數(shù)組)里面,
- 如果不存在, 從路由表里面提取出改路徑的路由對(duì)象, 并添加到openPages里面。 同時(shí)修改 currentPage當(dāng)前路由對(duì)象
addRouteToAllTabPans (props) {
if (props.location.pathname !== this.state.currentPage.path && props.location.pathname !=='/index') {
let {openPages} = this.state
let isHasRoute = openPages.some(item =>item.path === props.location.pathname)
if (!isHasRoute) {
let currentRoute = this.getCurrentRoute(props.location.pathname)
if (currentRoute) {
openPages.push(currentRoute)
this.setState({
openPages,
currentPage: currentRoute
})
}
}
}
}
- 在這里, 就是循環(huán)渲染了。
- <page.component routes={routes}></page.component> 這個(gè)就是路由對(duì)象的組件, 直接在TabPane里面渲染出來(lái)。
{this.state.openPages.map(page => {
return <TabPane forceRender tab={page.name} closable={page.closable} key={page.path}>
<page.component routes={routes}></page.component>
</TabPane>
})}
大概完成思路就是這樣。
完善
這里我把切換功能放在了header上面。 可以根據(jù)自己的選擇需要或不需要tab
- 修改components/header.js
因?yàn)閙enu的點(diǎn)擊事件放在了menu標(biāo)簽上面, 而點(diǎn)擊的時(shí)候, 可以拿到menuItem中的key
因此, 我們進(jìn)行判斷, 如果是tabs, 我們就觸發(fā)事件
handleClick = (e) => {
if (e.key === 'tabs') {
this.changeTabs()
} else {
this.setState({
current: e.key,
});
}
}
切換 tabs狀態(tài)
changeTabs = (obj) => {
this.props.toggleTabs({
tabs: !this.props.headerData.tabs
})
}
我們的tabs需要在header組件與 index組件里面使用, 因此不能單純的只是放在當(dāng)前頁(yè)面, 需要用redux進(jìn)行管理。
- 修改 reducer/connect
修改 mapLogout,
export const mapLogout = {
mapStateToProps: (state) => {
return { headerData: { }, ...state.slidecollapsed}
},
mapDispatchToProps: (dispatch) => {
return {onSlidecollapsed: () => dispatch(action_slidecollapsed), getRouterConfig: () => {
return dispatch(routerConfig)
}, toggleSlide: () => {
dispatch({type: action_slidecollapsed.type})
},
onLogout: (data) => {
return dispatch(fetchPosts('/logout', action_slidecollapsed.type, 'logoutData', data))
},
toggleTabs: (data) => { // 添加切換tab的函數(shù)
console.log(data);
dispatch(receive(action_slidecollapsed.type, 'headerData', {
...data
}))
}
}
}
}
修改rudexs.js
以前的數(shù)據(jù), 是用redux里面控制, 現(xiàn)在我們要改成傳參的形式。
const slidecollapsedFuc = (state = { slidecollapsed: false }, action) => {
switch (action.type) {
case SLIDECOLLAPSED:
return Object.assign({}, state, action)
default:
return state
}
}
components/header
添加 changeTabs 方法 。 render 里面引入 headerData 數(shù)據(jù), 然后添加MenuItem標(biāo)簽
changeTabs = (obj) => {
this.props.toggleTabs({
tabs: !this.props.headerData.tabs
})
}
let { slidecollapsed, headerData, toggleSlide, toggleTabs } = this.props
let { tabs } = headerData
<Menu.Item key="tabs">
<Icon type="notification" /> {tabs ? '隱藏tabs' : '顯示tabs'}
</Menu.Item>
這樣, 點(diǎn)擊頭部的'隱藏tabs'按鈕的時(shí)候,就會(huì)進(jìn)行切換,文字。
- 修改views/index/index
添加引入
添加方法和屬性
添加this.props.headerData
判斷tabs 進(jìn)行顯示隱藏。
import MyTabs from '@/components/tabs.js'
import MySlider from '@/components/slider'
import { connect } from 'react-redux'
import { mapIndex } from '@/reducer/connect'
state = {
currentPage: '',
openPages: []
}
onEdit = (targetKey, action) => {
this[action](targetKey);
}
onTabClick = (activeKey) => {
// if (activeKey !== this.state.currentPage && activeKey === 'home') {
// this.props.history.push('/app/home');
// return;
// }
// if (activeKey !== this.state.currentPage) {
// this.props.history.push(MenuToRouter[activeKey]);
// }
}
let { routes, headerData } = this.props
console.log(this.props)
let { tabs } = headerData
{ tabs &&
<MyTabs routes={routes}></MyTabs>
}
{!tabs &&
<MyMain routes={routes}></MyMain>
}
export default connect(mapIndex.mapStateToProps)(Index);
- redcer/connect
添加 mapIndex方法。
export const mapIndex = {
mapStateToProps: (state) => {
return { headerData: { }, ...state.slidecollapsed}
},
mapDispatchToProps: (dispatch) => {
return {
toggleTabs: (data) => {
console.log(data);
dispatch(receive(action_slidecollapsed.type, 'headerData', {
...data
}))
}
}
}
}
- utils/index 內(nèi)添加方法
先從面包屑組件里面提取 deepFlatten 到公共方法, 然后 添加removeArrItem
export const deepFlatten = arr => [].concat(...arr.map(v => Array.isArray(v) ? deepFlatten(v) : ( typeof v === 'object' ? (Array.isArray(v.routes) ? deepFlatten(v.routes.concat(deleObj(v, 'routes'))) : v) : v )));
// 刪除數(shù)組某項(xiàng)元素
export const removeArrItem = (arr, validFunx) => {
arr.splice(arr.findIndex(item => validFunx(item)), 1)
return arr
}