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

前言

動態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同步到水平菜單就比較好看了,目前水平都是直接寫死
  • 加上水波紋動效,目前沒有..就是MD風格點一下擴散那種
  • 拖拽,這樣能夠擺出更符合本身使用習慣的水平菜單
  • 固定額外不被消除的標籤,相似chrome的固定,不會給關閉全部幹掉

代碼實現

RouterStateModel.js(mobx狀態維護)

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;


複製代碼

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 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;



複製代碼

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
@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; 複製代碼

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
@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;

複製代碼

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

假如你須要更多呢?這裏也考慮到了,直接換行,用的flex佈局,有不對之處請留言,會及時修正,謝謝閱讀

相關文章
相關標籤/搜索