vue实现el-menu与el-tabs联动,通过点击menu菜单来动态添加tabs选项页


先上效果图

 达成这样的效果,其实根本在于,通过开启el-menu的route属性,点击menu菜单后会进行路径的跳转,但是你跳转的路由地址必须写为根组件的子路由地址,否则会直接进行整个页面的跳转,失去了这样的效果。

然后看一下el-tabs,这里放一下elementUI的官方文档

 子元素都是可以通过for遍历循环出来的,所以到了这里,基本的思路应该就是通过watch来监听$route,根据跳转的路径,在遍历tabs的数组里面进行判断,来动态的增加或者进行路由跳转。

同时,这里面有多个组件之间的通信,我们可以使用vuex,在这里做个最基本的应用。

下面开始上代码。

首先,我的页面结构布局是 左侧的菜单,右侧的头部和主体内容,直接使用element的Container布局容器

<el-container>

  <el-aside width="200px">Aside</el-aside>

  <el-container>

    <el-header>Header</el-header>

     <el-main>Main</el-main>

   </el-container>

</el-container>

先来看看左侧的Menu的代码:

<template>
    <div class="menu">
        <el-menu
            :default-active="String(activeNav)"
            class="el-menu-vertical-demo"
            @open="handleOpen"
            @close="handleClose"
            :collapse="getToggle"
            :collapse-transition="false"
            unique-opened
            router
        >
            <div class="menu_logo">
                <img :src="logoImg" alt="" />
                <span v-show="!getToggle">{{ menuTitle }}</span>
            </div>
            <fragment v-for="item in menuList" :key="item.key">
                <!-- 一级菜单 -->
                <el-submenu
                    v-if="item.children && item.children.length"
                    :index="item.path"
                    :key="item.key"
                >
                    <template slot="title"
                        ><i class="icon iconfont" v-html="item.icon"></i
                        ><span>{{ item.title }}</span></template
                    >

                    <!-- 二级菜单 -->
                    <template v-for="itemChild in item.children">
                        <el-submenu
                            v-if="
                                itemChild.children && itemChild.children.length
                            "
                            :index="itemChild.path"
                            :key="itemChild.key"
                        >
                            <template slot="title"
                                ><i
                                    class="icon iconfont"
                                    v-html="itemChild.icon"
                                ></i
                                ><span>{{ itemChild.title }}</span></template
                            >

                            <!-- 三级菜单 -->
                            <el-menu-item
                                v-for="itemChild_Child in itemChild.children"
                                :index="itemChild_Child.path"
                                :key="itemChild_Child.key"
                            >
                                <i
                                    class="icon iconfont"
                                    v-html="itemChild_Child.icon"
                                ></i
                                ><span slot="title">{{
                                    itemChild_Child.title
                                }}</span></el-menu-item
                            >
                        </el-submenu>

                        <el-menu-item
                            v-else
                            :index="itemChild.path"
                            :key="itemChild.key"
                            ><i
                                class="icon iconfont"
                                v-html="itemChild.icon"
                            ></i
                            ><span slot="title">{{
                                itemChild.title
                            }}</span></el-menu-item
                        >
                    </template>
                </el-submenu>

                <el-menu-item v-else :index="item.path" :key="item.key"
                    ><i class="icon iconfont" v-html="item.icon"></i
                    ><span slot="title">{{ item.title }}</span></el-menu-item
                >
            </fragment>
        </el-menu>
    </div>
</template>

<script>
import sunLogo from '@/assets/image/sun-logo.png';

import { mapGetters, mapState } from 'vuex';

export default {
    name: 'Menu',
    props: {
        menuTitle: {
            type: String,
            default: () => {
                return '';
            }
        }
    },
    data() {
        return {
            logoImg: sunLogo
        };
    },
    created() {},
    mounted() {},
    computed: {
        activeNav() {
            // 获取当前激活的导航
            return this.$route.path;
        },
        ...mapGetters(['getToggle']),
        ...mapState(['menuList'])
    },
    methods: {
        handleOpen(key, keyPath) {},
        handleClose(key, keyPath) {}
    }
};
</script>

<style lang="less" scoped>
@import '~@/assets/css/common.less';

.menu {
    ::v-deep .el-menu {
        border-right: none;
        background-color: transparent;

        .menu_logo {
            text-align: center;
            padding-top: 1.5rem;
            margin-bottom: 1.5rem;
            img {
                width: 2.83rem;
                height: 2.83rem;
                vertical-align: middle;
                margin-right: 0.5rem;
            }
            span {
                font-size: 1.5rem;
                font-family: PingFangSC-Semibold, PingFang SC;
                font-weight: 600;
                color: #ffffff;
                height: 2.08rem;
                line-height: 2.08rem;
            }
        }

        .el-submenu__title,
        .el-menu-item {
            font-size: 1.17rem;
            height: 4rem;
            line-height: 4rem;
            color: @theme-white;
            transition: none;
            .icon {
                margin-right: 0.33rem;
                color: @theme-white;
            }
        }
        .el-submenu__title i {
            color: @theme-white;
        }
        .el-submenu .el-menu-item {
            height: 40px;
            line-height: 40px;
        }
        .el-menu-item.is-active {
            background-color: @theme-white;
            color: @theme-text-color-primary;
            border-top-left-radius: 25px;
            border-bottom-left-radius: 25px;
            position: relative;

            .icon {
                color: @theme-text-color-primary !important;
            }

            &::before {
                content: '';
                width: 0;
                height: 0;
                border: 4px solid transparent;
                border-bottom-color: @theme-white;
                border-right-color: @theme-white;
                position: absolute;
                top: -8px;
                right: 0;
            }
            &::after {
                content: '';
                width: 0;
                height: 0;
                border: 4px solid transparent;
                border-top-color: @theme-white;
                border-right-color: @theme-white;
                position: absolute;
                bottom: -8px;
                right: 0;
            }
        }
        .el-menu--inline {
            padding-left: 1.27rem;
        }
        .el-menu-item:focus,
        .el-menu-item:hover,
        .el-submenu__title:hover {
            background-color: @theme-white;
            border-top-left-radius: 25px;
            border-bottom-left-radius: 25px;
            color: @theme-text-color-primary;
            position: relative;
            .icon {
                color: @theme-text-color-primary !important;
            }
            i {
                color: @theme-text-color-primary !important;
            }
            &::before {
                content: '';
                width: 0;
                height: 0;
                border: 4px solid transparent;
                border-bottom-color: @theme-white;
                border-right-color: @theme-white;
                position: absolute;
                top: -8px;
                right: 0;
            }
            &::after {
                content: '';
                width: 0;
                height: 0;
                border: 4px solid transparent;
                border-top-color: @theme-white;
                border-right-color: @theme-white;
                position: absolute;
                bottom: -8px;
                right: 0;
            }
        }
    }
}
</style>
 
其中,fragment标签是用来替代div使折叠菜单生效的,可以使用 npm i vue-fragment -D 下载包后,在main.js里面  import Fragment from 'vue-fragment'  来使用。
可以看到,是启用了route属性的,而 :default-active="String(activeNav)"  则是来根据跳转的菜单来进行当前选中的激活。
这个 getToggle 则是用来收缩菜单的, 也是放在了 vuex 里面,可以先暂时不考虑这个。 
这个 menuList 我则是放在了vuex里面,里面的数据为:
menuList: [
            {
                key: 0,
                title: '首页',
                icon: '&#xe603;',
                path: '/index'
            },
            {
                key: 1,
                title: '人员管理',
                icon: '&#xe789;',
                path: '/person_manage'
            },
            {
                key: 2,
                title: '事物管理',
                icon: '&#xe7c1;',
                path: 'drug_case_manage',
                children: [
                    {
                        key: '2-1',
                        title: '物件管理',
                        icon: '&#xe7c8;',
                        path: '/case_manage'
                    },
                    {
                        key: '2-2',
                        title: '物件研判',
                        icon: '&#xe7c9;',
                        path: '/person_involved'
                    }
                ]
            },
            {
                key: 3,
                title: '一键搜',
                icon: '&#xe79d;',
                path: '/search'
            }
        ]
其中的icon是我本地使用的,这个可以不用管,接下来看下router.js里面的路由配置。
这是我router文件夹下面的路由配置
    {
        path: '/',
        redirect: '/home'
    },
    {
        path: '/home',
        name: 'Home',
        component: () => import(/* webpackChunkName: "home" */ '@/views/Home'),
        redirect: '/guide',
        children: [
            {
                // 引导页
                path: '/guide',
                name: 'Guide',
                meta: {
                    name: 'guide',
                    title: '欢迎使用',
                    comp: 'guide'
                },
                component: () => import('@/views/Guide/guide')
            },
            {
                // 首页
                path: '/index',
                name: 'Index',
                meta: {
                    name: 'index',
                    title: '首页',
                    comp: 'Index'
                },
                component: () => import('@/views/index/Index')
            },
            {
                path: '/search',
                name: 'Search',
                component: () => import('@/views/OneClickSearch/GoSearch.vue')
            },
            {
                // 人员管理
                path: '/person_manage',
                name: 'PersonManage',
                meta: {
                    name: 'PersonManage',
                    title: '人员管理',
                    comp: 'manage'
                },
                component: () => import('@/views/PersonManage/manage')
            },
            {
                path: '/case_manage',
                name: 'CaseManage',
                meta: {
                    name: 'CaseManage',
                    title: '物品管理',
                    comp: 'CaseManage'
                },
                component: () => import('@/views/CaseManage/manage')
            },
            {
                path: '/person_involved',
                name: 'involved',
                meta: {
                    name: 'involved',
                    title: '物件研判',
                    comp: 'involved'
                },
                component: () => import('@/views/CaseManage/involved')
            },
            {
                path: '/search',
                name: '一键搜',
                meta: {
                    name: '一键搜',
                    title: '一键搜',
                    comp: 'Search'
                },
                component: () => import('@/views/OneClickSearch/GoSearch')
            }
        ]
    }
其中meta是自定义的属性值,可以在监听 $route 时,监听到定义的属性值。
将这些页面设为home根路径下面的子路径,并将home页面重定向到欢迎页,这样打开时直接显示的就是欢迎页面。
接下来就是tabs组件了,主要的逻辑功能都在这一部分。
先上代码:
<template>
    <div>
        <el-tabs
            v-model="activeIndex"
            type="card"
            @tab-remove="removeTab"
            @tab-click="clickTab"
        >
            <el-tab-pane
                :key="item.name"
                v-for="item in openTab"
                :label="item.title"
                :name="item.name"
                :closable="item.closable"
            >
                <router-view></router-view>
            </el-tab-pane>
        </el-tabs>
    </div>
</template>
 
其中的 activeIndex 就是tabs当前选中的,我们默认为欢迎页面,然后通过循环 openTab 这个数组来进行动态的添加或删除,而这个 closable 属性则是可以手动设置为是否可以关掉tabs的选项卡,我们在里面放一个 router-view 来进行子页面的跳转。
data 里面的则是:
data() {
        return {
            activeIndex: '/guide',
            openTab: [
                {
                    title: '引导页',
                    name: '/guide',
                    closable: false
                }
            ]
        };
    }
这是初始数据,默认设置欢迎页,然后重点就是通过watch来监听 $route ,代码如下:
    watch: {
        $route(to, form) {
            var flag = false;
            // 当前页面菜单已打开
            for (let i = 0; i < this.openTab.length; i++) {
                if (to.path == this.openTab[i].name) {
                    this.activeIndex = this.openTab[i].name;
                    flag = true;
                    break;
                }
            }
            // 打开新的页面
            if (!flag) {
                let obj = {
                    title: to.meta.title,
                    name: to.path,
                    closable: true
                };
                this.activeIndex = to.path;
                this.openTab.push(obj);
            }
        }
    }
我们加个自定义的 flag 属性来进行往 openTab 这个数组里面push数据的判断依据,同时,通过methods里面的 clickTab 方法来进行tabs页面的切换
clickTab(tab) {
            this.activeIndex = tab.paneName;
            this.$router.push({ path: this.activeIndex });
}
接下来是 removeTab 方法,关闭打开的tabs标签页
removeTab(target) {
            // 删除的是当前选中的页面
            if (this.activeIndex === target) {
                this.openTab.forEach((item, index) => {
                    if (item.name == target) {
                        let nextTab = item[index + 1] || item[index - 1];
                        if (nextTab) {
                            this.activeIndex = nextTab.name;
                        }
                    }
                });
            }
            var i = 0;
            this.openTab.forEach((item, index) => {
                if (item.name == target) {
                    // eslint-disable-next-line no-return-assign
                    return (i = index);
                }
            });
            this.openTab.splice(i, 1);

            // 更新路由
            this.$router.push({ path: this.openTab[this.openTab.length - 1].name });
}
到这里,基本的页面跳转以及切换就差不多了,但是我们还需要再补充一下,当我们打开几个页面时,进行刷新,就会发现重置了,这时需要在 mounted 里面添加如下代码:
mounted() {
        // 监听页面加载前
        window.addEventListener('beforeunload', e => {
            sessionStorage.setItem(
                'openTab',
                JSON.stringify({
                    openTab: this.openTab.filter(
                        item => item.name != '/guide'
                    ),
                    openTabPath: this.openTab.filter(
                        item => item.name !== '/guide'
                    ),
                    currActiveTabs: this.activeIndex
                })
            );
        });
}
同时,在created里面进行读取本地 sessionStorage 来进行判断:
created() {
        const sessionTab = JSON.parse(window.sessionStorage.getItem('openTab')) || '';
        if (sessionTab) {
            if (sessionTab.openTab.length != 0 && sessionTab.openTabPath.length != 0) {
                for (let i = 0; i < sessionTab.openTab.length; i++) {
                    this.openTab.push({
                        title: sessionTab.openTab[i].title,
                        name: sessionTab.openTab[i].name,
                        closable: true
                    });
                }
                this.activeIndex = sessionTab.currActiveTabs;
                this.$router.push({ path: this.activeIndex });
            }
        }
    }
这样,在打开几个页面后,刷新页面之前,保留已打开的页面,刷新之后,还是显示之前打开过的页面,至此,基本的流程已经结束。
我把这些功能都单独拿出去了,所以就贴上了基本的代码,主要的逻辑流程就是这些,其中可能会有些问题,我们还是需要自己来一一解决。
框架完整代码可以在码云上面去看    https://gitee.com/chenkai777/vue_demo
 
 
 
 

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM