React 16.x折腾记 - (3) 结合Mobx实现一个比较靠谱的动态tab水平菜单,同时关联侧边栏

简介: 动态tab水平菜单,这个需求很常见,特别是对于后台管理系统来说实现的思路有点绕,有更好的姿势请留言,谢谢阅读。


前言


动态tab水平菜单,这个需求很常见,特别是对于后台管理系统来说

实现的思路有点绕,有更好的姿势请留言,谢谢阅读。


效果如下


  • 关联展示




  • 单个删除和删除其他的标签


只有一个时候是不允许关闭,所以也不会显示关闭的按钮,关闭其他也不会影响唯一的





  • tag换行



基础环境


  • 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';
// 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;


目录
相关文章
|
7月前
|
前端开发
Vue3/React 动态设置 ant-design/icons 图标
Vue3/React 动态设置 ant-design/icons 图标
468 1
|
7月前
|
前端开发 JavaScript
使用 MobX 优化 React 代码
使用 MobX 优化 React 代码
95 0
|
缓存 前端开发 API
React + MobX 快速上手2
React + MobX 快速上手
|
2天前
|
缓存 前端开发 UED
React 侧边栏组件 Sidebar
本文介绍了如何使用React创建交互式侧边栏组件,涵盖基础结构、状态管理、样式设计等方面。通过`useState`钩子控制侧边栏的展开与收起,并利用CSS实现动画效果。同时,文章还探讨了响应式设计、性能优化、可访问性和路由集成等常见问题及解决方案,帮助开发者构建高效、美观且易于维护的侧边栏组件,提升Web应用的用户体验。
19 8
|
2月前
|
前端开发 JavaScript
深入理解前端状态管理:React、Redux 和 MobX
【10月更文挑战第7天】深入理解前端状态管理:React、Redux 和 MobX
81 0
|
4月前
|
存储 JavaScript 前端开发
探索React状态管理:Redux的严格与功能、MobX的简洁与直观、Context API的原生与易用——详细对比及应用案例分析
【8月更文挑战第31天】在React开发中,状态管理对于构建大型应用至关重要。本文将探讨三种主流状态管理方案:Redux、MobX和Context API。Redux采用单一存储模型,提供预测性状态更新;MobX利用装饰器语法,使状态修改更直观;Context API则允许跨组件状态共享,无需第三方库。每种方案各具特色,适用于不同场景,选择合适的工具能让React应用更加高效有序。
100 0
|
7月前
|
存储 前端开发 数据可视化
构建基于React的动态数据可视化应用
【5月更文挑战第27天】构建基于React的动态数据可视化应用,通过Create React App快速搭建环境,使用Recharts等库封装组件。在`useState`和`useEffect` Hooks管理状态,处理动态数据。优化性能,添加交互功能,实现响应式设计,确保可访问性,打造高性能、用户体验佳的可视化应用。
|
7月前
|
前端开发 JavaScript 安全
【亮剑】探讨了在React TypeScript应用中如何通过道具(props)传递CSS样式,以实现模块化、主题化和动态样式
【4月更文挑战第30天】本文探讨了在React TypeScript应用中如何通过道具(props)传递CSS样式,以实现模块化、主题化和动态样式。文章分为三部分:首先解释了样式传递的必要性,包括模块化、主题化和动态样式以及TypeScript集成。接着介绍了内联样式的基本用法和最佳实践,展示了一个使用内联样式自定义按钮颜色的例子。最后,讨论了使用CSS模块和TypeScript接口处理复杂样式的方案,强调了它们在组织和重用样式方面的优势。结合TypeScript,确保了样式的正确性和可维护性,为开发者提供了灵活的样式管理策略。
80 0
|
7月前
|
前端开发 JavaScript 安全
如何在React项目中动态插入HTML内容
如何在React项目中动态插入HTML内容
260 0
|
7月前
|
前端开发
React动态标签名称
React动态标签名称
78 0