• SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(五):引入 vue-router 进行路由管理、模块化封装 axios 请求、使用 iframe 标签嵌套页面


    (1) 相关博文地址:

    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(一):搭建基本环境:https://www.cnblogs.com/l-y-h/p/12930895.html
    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(二):引入 element-ui 定义基本页面显示:https://www.cnblogs.com/l-y-h/p/12935300.html
    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(三):引入 js-cookie、axios、mock 封装请求处理以及返回结果:https://www.cnblogs.com/l-y-h/p/12955001.html
    SpringBoot + Vue + ElementUI 实现后台管理系统模板 -- 前端篇(四):引入 vuex 进行状态管理、引入 vue-i18n 进行国际化管理:https://www.cnblogs.com/l-y-h/p/12963576.html

    (2)代码地址:

    https://github.com/lyh-man/admin-vue-template.git

    一、引入 vue-router 进行路由管理

    1、简单了解下

      之前在 搭建基本页面时,已经简单使用过,这里再深入了解一下。
    (1)文件格式如下
      由于创建项目时,指定了 router,所以 vue-cli 自动生成了 router 文件夹以及相关的 js 文件。

      

    (2)手动引入 router(可选操作)。

      若初始化项目时未指定 router,可以自己手动添加 router。

    【router 中文文档:】
        https://router.vuejs.org/zh/
    
    【router 使用参考:】
        https://www.cnblogs.com/l-y-h/p/11661874.html

    2、项目中使用

    (1)简介
      此项目是单页面应用,通过 vue-router 将 各个组件(components)映射到指定位置,实现页面切换的效果。
      之前定义基本页面时,已经简单应用了 router。

    (2)代码如下:
      根据路径可以进行路由匹配,也可根据 name 属性去定位路由。
    其中:
      component 采用路由懒加载的形式( () => import() ),路由被访问时再加载。
      path: '/' 表示项目根路径。
      redirect 表示跳转到另一个路由。
      name: "Login" 表示路由名,可以根据 name 定位路由。
      path: "*" 表示全匹配,一般写在路由规则的最后一个(用于路径不存在时跳转到一个指定页面)。

    【基本路由:】
        https://www.cnblogs.com/l-y-h/p/12935300.html#_label1_8
    
    
    【路由规则:】
    
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Home from '../views/Home.vue'
    
    Vue.use(VueRouter)
    
    const routes = [{
            path: '/',
            redirect: {
                name: "Login"
            }
        },
        {
            path: '/404',
            name: '404',
            component: () => import('@/components/common/404.vue')
        },
        {
            path: '/Login',
            name: 'Login',
            component: () => import('@/components/common/Login.vue')
        },
        {
            path: '/Home',
            name: 'Home',
            component: () => import('@/views/Home.vue'),
            redirect: {
                name: 'HomePage'
            },
            children: [{
                    path: '/Home/Page',
                    name: 'HomePage',
                    component: () => import('@/views/menu/HomePage.vue')
                },
                {
                    path: '/Home/Demo/Echarts',
                    name: 'Echarts',
                    component: () => import('@/views/menu/Echarts.vue')
                },
                {
                    path: '/Home/Demo/Ueditor',
                    name: 'Ueditor',
                    component: () => import('@/views/menu/Ueditor.vue')
                }
            ]
        },
        {
            path: "*",
            redirect: {
                name: '404'
            }
        }
    ]
    
    const router = new VueRouter({
        // routes 用于定义 路由跳转 规则
        routes,
        // mode 用于去除地址中的 #
        mode: 'history',
        // scrollBehavior 用于定义路由切换时,页面滚动。
        scrollBehavior: () => ({
            y: 0
        })
    })
    
    // 解决相同路径跳转报错
    const routerPush = VueRouter.prototype.push;
    VueRouter.prototype.push = function push(location, onResolve, onReject) {
        if (onResolve || onReject)
            return routerPush.call(this, location, onResolve, onReject)
        return routerPush.call(this, location).catch(error => error)
    };
    
    export default router

    3、导航守卫、路由元信息

    (1)简介
      导航守卫适用于 路由变化时。即当路由变化时,会触发导航守卫。
      路由元信息 可以用于定义路由独有的信息(meta)。
    注:
      同一个组件切换时,参数改变不会触发导航守卫(复用组件)。可以通过 watch 监听 $route 对象的变化来定义导航守卫,或者 直接使用 beforeRouteUpdate 来进行导航守卫(组件内守卫)。

    (2)全局前置守卫(beforeEach)
      使用 beforeEach 可以定义一个全局前置守卫,路由跳转前会触发。
    其有三个参数:
      to:一个路由对象,表示即将进入的 目标路由对象。
      from:一个路由对象,表示当前路由 离开时的路由对象。
      next:一个方法(不能少,确保路由能够跳转出去。
        next() 表示执行下一个守卫规则,若所有规则执行完毕,则结束并跳转到指定路由。
        next({path: ''}) 或者 next({name: ''}) 表示指定路径跳转。

    const router = new VueRouter({ ... })
    
    router.beforeEach((to, from, next) => {
      // ...
    })

    (3)路由元信息
      定义路由规则时,可以通过 meta 指定路由的元信息。
      通过 router对象.meta 可以获取到某个 router对象 的 meta 信息,并根据其进行处理。

    const router = new VueRouter({
      routes: [
        {
          path: '/foo',
          component: Foo,
          children: [
            {
              path: 'bar',
              component: Bar,
              meta: { requiresAuth: true }
            }
          ]
        }
      ]
    })

    (4)项目中使用
      进入主页面后,当 token 过期或不存在时,需要跳转到登录页面重新登录。
      使用 导航守卫,每次路由跳转前,确定 token 是否存在。可以使用 beforeEach 定义全局守卫,也可以使用 beforeEnter 为某个路由定义独有守卫。
      此处演示使用 beforeEach 定义全局守卫。

    Step1:
      修改 Login.vue 登录逻辑,保存 token 值。
      之前将 cookie 相关的操作保存在 /http/auth.js 中,需要引入该 js。

    import { setToken } from '@/http/auth.js'
    
    dataFormSubmit() {
        // TODO:登录代码逻辑待完善
        // alert("登录代码逻辑未完善")
        this.$http({
            url: '/auth/token',
            method: 'get'
        }).then(response => {
            this.$message({
                message: this.$t("login.signInSuccess"),
                type: 'success'
            })
               // 保存 token 值
            setToken(response.data.token)
            this.updateName(this.dataForm.userName)
            console.log(response)
            this.$router.push({
                name: 'Home'
            })
        })
    },

    Step2:
      修改路由。
      定义路由元信息(meta)。meta 用于定义路由元信息,其中 isRouter 用于指示是否开启路由守卫(true 表示开启)。

    {
        path: '/Home',
        name: 'Home',
        component: () => import('@/views/Home.vue'),
        redirect: {
            name: 'HomePage'
        },
        children: [{
                path: '/Home/Page',
                name: 'HomePage',
                component: () => import('@/views/menu/HomePage.vue'),
                meta: {
                    isRouter: true
                }
            },
            {
                path: '/Home/Demo/Echarts',
                name: 'Echarts',
                component: () => import('@/views/menu/Echarts.vue'),
                meta: {
                    isRouter: true
                }
            },
            {
                path: '/Home/Demo/Ueditor',
                name: 'Ueditor',
                component: () => import('@/views/menu/Ueditor.vue'),
                meta: {
                    isRouter: true
                }
            }
        ]
    }

     

    Step3:
      添加全局守卫(beforeEach)。
      当 isRouter 为 true 时,才会去校验 token,token 校验失败则跳转到 Login 页面重新登录。

    // 添加全局路由导航守卫
    router.beforeEach((to, from, next) => {
        // 当开启导航守卫时,验证 token 是否存在。
        if (to.meta.isRouter) {
            // 获取 token 值
            let token = getToken()
            console.log(token)
            // token 不存在时,跳转到 登录页面
            if (!token || !/S/.test(token)) {
                next({name: 'Login'})
            }
        }
        next()
    })

    Step4:
      完整 router 如下:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Home from '../views/Home.vue'
    import { getToken } from '@/http/auth.js'
    
    Vue.use(VueRouter)
    
    // 定义路由跳转规则
    // component 采用 路由懒加载形式
    // 此项目中,均采用 name 方式指定路由进行跳转
    // meta 用于定义路由元信息,其中 isRouter 用于指示是否开启路由守卫(true 表示开启)。
    const routes = [{
            path: '/',
            redirect: {
                name: "Login"
            }
        },
        {
            path: '/404',
            name: '404',
            component: () => import('@/components/common/404.vue')
        },
        {
            path: '/Login',
            name: 'Login',
            component: () => import('@/components/common/Login.vue')
        },
        {
            path: '/Home',
            name: 'Home',
            component: () => import('@/views/Home.vue'),
            redirect: {
                name: 'HomePage'
            },
            children: [{
                    path: '/Home/Page',
                    name: 'HomePage',
                    component: () => import('@/views/menu/HomePage.vue'),
                    meta: {
                        isRouter: true
                    }
                },
                {
                    path: '/Home/Demo/Echarts',
                    name: 'Echarts',
                    component: () => import('@/views/menu/Echarts.vue'),
                    meta: {
                        isRouter: true
                    }
                },
                {
                    path: '/Home/Demo/Ueditor',
                    name: 'Ueditor',
                    component: () => import('@/views/menu/Ueditor.vue'),
                    meta: {
                        isRouter: true
                    }
                }
            ]
        },
        // 路由匹配失败时,跳转到 404 页面
        {
            path: "*",
            redirect: {
                name: '404'
            }
        }
    ]
    
    // 创建一个 router 实例
    const router = new VueRouter({
        // routes 用于定义 路由跳转 规则
        routes,
        // mode 用于去除地址中的 #
        mode: 'history',
        // scrollBehavior 用于定义路由切换时,页面滚动。
        scrollBehavior: () => ({
            y: 0
        })
    })
    
    // 添加全局路由导航守卫
    router.beforeEach((to, from, next) => {
        // 当开启导航守卫时,验证 token 是否存在。
        if (to.meta.isRouter) {
            // 获取 token 值
            let token = getToken()
            console.log(token)
            // token 不存在时,跳转到 登录页面
            if (!token || !/S/.test(token)) {
                next({name: 'Login'})
            }
        }
        next()
    })
    
    // 解决相同路径跳转报错
    const routerPush = VueRouter.prototype.push;
    VueRouter.prototype.push = function push(location, onResolve, onReject) {
        if (onResolve || onReject)
            return routerPush.call(this, location, onResolve, onReject)
        return routerPush.call(this, location).catch(error => error)
    };
    
    export default router

    Step5:
      测试一下。

      手动模拟 token 失效。token 失效后,点击菜单栏,路由会跳转到 登录界面。

    二、模块化封装 axios 请求

    1、简介

      前面封装了 axios,并在 main.js 中全局挂载,所以在组件中可以使用 $http 进行访问。
      但是每次请求的相关处理都会写在各个组件中,代码看上去不太美观,且不易维护。
      所以可以将请求根据功能、模块进行划分,并写在固定位置,在组件中引入这些模块即可。

    2、代码实现

    (1)Step1:
      按照功能将请求进行模块划分。
    比如:

      登录、登出的请求为 login.js,用户信息相关的请求为 user.js,菜单相关的请求为 menu.js。

    (2)Step2:
      由于之前封装了 httpRequest.js,所以引入 该 js,对请求进行处理。
    此处以 login.js 为例。

    import http from '@/http/httpRequest.js'
    
    export function getToken() {
        return http({
            url: '/auth/token',
            method: 'get'
        })
    }

    (3)Step3:
      定义一个 http.js,引入 login.js 模块。

    import * as login from './modules/login.js'
    import * as user from './modules/menu.js'
    
    export default {
        login,
        user
    }

    (4)Step4:
      在 main.js 中全局挂载。

    import http from '@/http/http.js'
    
    Vue.prototype.$http = http

    (5)Step5:
      修改 Login.vue 的登录逻辑,通过全局挂载的 $http 调用 login 模块的 getToken 方法。

    dataFormSubmit() {
        // TODO:登录代码逻辑待完善
        // alert("登录代码逻辑未完善")
        this.$http.login.getToken().then(response => {
            this.$message({
                message: this.$t("login.signInSuccess"),
                type: 'success'
            })
            // 保存 token
            setToken(response.data.token)
            this.updateName(this.dataForm.userName)
            console.log(response)
            this.$router.push({
                name: 'Home'
            })
        })
    }

    (6)页面显示:

    三、使用 iframe 标签嵌套页面

    1、简单了解一下

    (1)什么是 iframe?
      iframe 标签会创建一个行内框架(包含另一个文档的内联框架)。
      简单地理解:页面中嵌套另一个页面。

    (2)使用场景?
      有的项目需求,需要在当前页面中显示外部网页,比如访问百度、查看接口文档等,此时就可以使用 iframe 标签,嵌套一个页面。

    (3)简单使用一下
      如下,简单使用一下 iframe

    <template>
        <el-main class="content">
            <el-card class="card" shadow="hover">
                <!-- <keep-alive>
                    <router-view />
                </keep-alive> -->
                <iframe src="https://www.baidu.com/" frameborder="0" width="100%" height="700px"></iframe>
            </el-card>
        </el-main>
    </template>

    2、项目中使用

    (1)实现效果
      每点击一个菜单项,在内容区会显示一个标签页,
      点击不同的标签页,会跳转到相应的组件,并显示不同的内容。
      若是自身模块,则使用 router-view 显示,若是外部网页,则使用 iframe 显示。

    (2)思路:
      由于涉及到组件间数据的交互,所以使用 vuex 维护状态。侧边栏(Aside.vue)选中菜单项时,相关数据被修改,而 内容区(Content.vue)根据 相关数据进行展示。
    需要维护的数据:
      需要一个数组,用于保存点击的菜单项(标签属性、url、标题等)。
      需要两个字符串,一个用于保存当前菜单选中项,一个保存当前标签选中项。

      由于菜单内容的显示通过路由跳转完成,不同的菜单需要不同的显示效果,所以可以使用 router 的 meta,定义相关路由元信息。
    路由元信息:
      isTab: 表示可以显示为标签页。
      iframeUrl : 表示 url,其中 以 http 或者 https 开头的 url 使用 iframe 标签展示。

    (3)实现
    Step1:
      在路由中添加一个路由元信息,并新增一个路由用于测试 iframe 使用(Baidu)。
    其中:
      isTab 用于表示是否显示为标签页(true 表示显示)
      iframeUrl 用于表示 url,使用 http 或者 https 开头的 url 使用 iframe 标签展示

    meta: {
        isTab: true,
         iframeUrl: 'https://www.baidu.com/'
    }
    
    
    {
        path: '/Home',
        name: 'Home',
        component: () => import('@/views/Home.vue'),
        redirect: {
            name: 'HomePage'
        },
        children: [{
                path: '/Home/Page',
                name: 'HomePage',
                component: () => import('@/views/menu/HomePage.vue'),
                meta: {
                    isRouter: true
                }
            },
            {
                path: '/Home/Demo/Echarts',
                name: 'Echarts',
                component: () => import('@/views/menu/Echarts.vue'),
                meta: {
                    isRouter: true,
                    isTab: true
                }
            },
            {
                path: '/Home/Demo/Ueditor',
                name: 'Ueditor',
                component: () => import('@/views/menu/Ueditor.vue'),
                meta: {
                    isRouter: true,
                    isTab: true
                }
            },
            {
                path: '/Home/Demo/Baidu',
                name: 'Baidu',
                meta: {
                    isRouter: true,
                    isTab: true,
                    iframeUrl: 'https://www.baidu.com/'
                }
            }
        ]
    }

     

    Step2:
      使用 vuex 维护几个必要的状态。
    其中:
      menuActiveName 表示侧边栏选中的菜单项的名
      mainTabs 表示标签页数据,数组
      mainTabsActiveName 表示标签页中选中的标签名

    如下,在 common.js 中进行相关定义。

    export default {
        // 开启命名空间(防止各模块间命名冲突),访问时需要使用 模块名 + 方法名
        namespaced: true,
        // 管理数据(状态)
        state: {
            // 用于保存语言设置(国际化),默认为中文
            language: 'zh',
            // 表示侧边栏选中的菜单项的名
            menuActiveName: '',
            // 表示标签页数据,数组
            mainTabs: [],
            // 表示标签页中选中的标签名
            mainTabsActiveName: ''
        },
        // 更改 state(同步)
        mutations: {
            updateLanguage(state, data) {
                state.language = data
            },
            updateMenuActiveName(state, name) {
                state.menuActiveName = name
            },
            updateMainTabs(state, tabs) {
                state.mainTabs = tabs
            },
            updateMainTabsActiveName(state, name) {
                state.mainTabsActiveName = name
            },
        },
        // 异步触发 mutations
        actions: {
            updateLanguage({commit, state}, data) {
                commit("updateLanguage", data)
            },
            updateMenuActiveName({commit, state}, name) {
                commit("updateMenuActiveName", name)
            },
            updateMainTabs({commit, state}, tabs) {
                commit("updateMainTabs", tabs)
            },
            updateMainTabsActiveName({commit, state}, name) {
                commit("updateMainTabsActiveName", name)
            }
        }
    }

    Step3:
      在侧边栏中,引入 menuActiveName 、 mainTabs、mainTabsActiveName 以及其相关的修改方法。对其进行操作。

    import {mapState, mapActions} from 'vuex'
    
    export default {
        computed: {
            ...mapState('common', ['menuActiveName', 'mainTabs'])
        },
        methods: {
            ...mapActions('common', ['updateMenuActiveName', 'updateMainTabs', 'updateMainTabsActiveName'])
        }
    }

    Step4:
      监视 $route 的变化,路由发生变化后,就会触发。
      每次点击 菜单项,均会触发 路由的跳转,所以监听 $route 的变化,变化时可以进行相关操作。
    如下:
      监视路由的变化,路由发生改变后,侧边栏菜单项选中状态需要修改到选中位置。
      根据路由元信息判断,如果可以显示为标签页,则处理标签页相关规则,否则直接跳过。
      标签页规则:
        使用 数组 保存标签页信息,如果当前选中的菜单项 未保存在 数组中,则向数组中添加该标签信息并修改当前选中的标签页名,若已存在,则直接修改当前选中的标签页名。
      标签页信息:
        name 表示标签名
        params、query 表示参数(路由需要的参数)
        type 表示显示类型, iframe 表示使用 iframe 标签显示
        iframeUrl 表示 url,默认为 ‘’

    watch: {
        // 监视路由的变化,每次点击菜单项时会触发
        $route(route) {
            // 路由变化时,修改当前选中的菜单项
            this.updateMenuActiveName(route.name)
            // 是否显示标签页
            if (route.meta.isTab) {
                // 判断当前标签页数组中是否存在当前选中的标签,根据标签名匹配
                let tab = this.mainTabs.filter(item => item.name === route.name)[0]
                // 若当前标签页数组不存在该标签,则向数组中添加标签
                if (!tab) {
                    // 设置标签页数据
                    tab = {
                        name: route.name,
                        params: route.params,
                        query: route.query,
                        type: isURL(route.meta.iframeUrl) ? 'iframe' : 'module',
                        iframeUrl: route.meta.iframeUrl || ''
                    }
                    // 将数据保存到标签页数组中
                    this.updateMainTabs(this.mainTabs.concat(tab))
                }
                // 保存标签页中当前选中的标签名
                this.updateMainTabsActiveName(route.name)
            }
        }
    }

      

      上面的 isURL 是封装的一个方法,此处抽取到一个公用 js 中。

    /**
     * URL地址
     * @param {*} s
     */
    export function isURL (s) {
      return /^http[s]?://.*/.test(s)
    }

    当然使用时需要引入该 js。

    import {isURL} from '@/utils/validate.js'

    Step5:
      添加一个菜单项(Baidu),将上面几步整合。
    完整的 Aside.vue 如下:

    <template>
        <div>
            <!-- 系统 Logo -->
            <el-aside class="header-logo" :width="asideWidth">
                <div @click="$router.push({ name: 'Home' })">
                    <a v-if="foldAside">{{language.adminCenter}}</a>
                    <a v-else>{{language.admin}}</a>
                </div>
            </el-aside>
            <el-aside class="aside" :width="asideWidth" :class='"icon-size-" + iconSize'>
                <el-scrollbar style="height: 100%;  100%;">
                    <!--
                            default-active 表示当前选中的菜单,默认为 HomePage。
                            collapse 表示是否折叠菜单,仅 mode 为 vertical(默认)可用。
                            collapseTransition 表示是否开启折叠动画,默认为 true。
                            background-color 表示背景颜色。
                            text-color 表示字体颜色。
                        -->
                    <el-menu :default-active="menuActiveName || 'HomePage'" :collapse="!foldAside" :collapseTransition="false"
                     background-color="#263238" text-color="#8a979e">
                        <el-menu-item index="HomePage" @click="$router.push({ name: 'Home' })">
                            <i class="el-icon-s-home"></i>
                            <span slot="title">{{language.homePage}}</span>
                        </el-menu-item>
                        <el-submenu index="demo">
                            <template slot="title">
                                <i class="el-icon-star-off"></i>
                                <span>demo</span>
                            </template>
                            <el-menu-item index="Echarts" @click="$router.push({ name: 'Echarts' })">
                                <i class="el-icon-s-data"></i>
                                <span slot="title">echarts</span>
                            </el-menu-item>
                            <el-menu-item index="Ueditor" @click="$router.push({ name: 'Ueditor' })">
                                <i class="el-icon-document"></i>
                                <span slot="title">ueditor</span>
                            </el-menu-item>
                            <el-menu-item index="Baidu" @click="$router.push({ name: 'Baidu' })">
                                <i class="el-icon-document"></i>
                                <span slot="title">baidu</span>
                            </el-menu-item>
                        </el-submenu>
                    </el-menu>
                </el-scrollbar>
            </el-aside>
        </div>
    </template>
    
    <script>
        import {mapState, mapActions} from 'vuex'
        import {isURL} from '@/utils/validate.js'
        export default {
            name: 'Aside',
            props: ['foldAside'],
            data() {
                return {
                    // 保存当前选中的菜单
                    // menuActiveName: 'home',
                    // 保存当前侧边栏的宽度
                    asideWidth: '200px',
                    // 用于拼接当前图标的 class 样式
                    iconSize: 'true'
                }
            },
            computed: {
                ...mapState('common', ['menuActiveName', 'mainTabs']),
                // 国际化
                language() {
                    return {
                        adminCenter: this.$t("aside.adminCenter"),
                        admin: this.$t("aside.admin"),
                        homePage: this.$t("aside.homePage")
                    }
                }
            },
            methods: {
                ...mapActions('common', ['updateMenuActiveName', 'updateMainTabs', 'updateMainTabsActiveName'])
            },
            watch: {
                // 监视是否折叠侧边栏,折叠则宽度为 64px。
                foldAside(val) {
                    this.asideWidth = val ? '200px' : '64px'
                    this.iconSize = val
                },
                // 监视路由的变化,每次点击菜单项时会触发
                $route(route) {
                    // 路由变化时,修改当前选中的菜单项
                    this.updateMenuActiveName(route.name)
                    // 是否显示标签页
                    if (route.meta.isTab) {
                        // 判断当前标签页数组中是否存在当前选中的标签,根据标签名匹配
                        let tab = this.mainTabs.filter(item => item.name === route.name)[0]
                        // 若当前标签页数组不存在该标签,则向数组中添加标签
                        if (!tab) {
                            // 设置标签页数据
                            tab = {
                                name: route.name,
                                params: route.params,
                                query: route.query,
                                type: isURL(route.meta.iframeUrl) ? 'iframe' : 'module',
                                iframeUrl: route.meta.iframeUrl || ''
                            }
                            // 将数据保存到标签页数组中
                            this.updateMainTabs(this.mainTabs.concat(tab))
                        }
                        // 保存标签页中当前选中的标签名
                        this.updateMainTabsActiveName(route.name)
                    }
                }
            }
        }
    </script>
    
    <style>
        .aside {
            margin-bottom: 0;
            height: 100%;
            max-height: calc(100% - 50px);
             100%;
            max- 200px;
            background-color: #263238;
            text-align: left;
            right: 0;
        }
    
        .header-logo {
            background-color: #17b3a3;
            text-align: center;
            height: 50px;
            line-height: 50px;
             200px;
            font-size: 24px;
            color: #fff;
            font-weight: bold;
            margin-bottom: 0;
            cursor: pointer;
        }
        .el-submenu .el-menu-item {
            max- 200px !important;
        }
        .el-scrollbar__wrap {
            overflow-x: hidden !important;
        }
        .icon-size-false I {
            font-size: 30px !important;
        }
        .icon-size-true I {
            font-size: 18px !important;
        }
    </style>

    Step6:
      修改内容区,用于显示不同的页面。
    如下:
      定义一个 Tab.vue 组件,当路由元信息 isTab 为 true 时(即可以显示为标签页),则显示标签页,否则不显示标签页。

    <template>
        <el-main class="content">
            <Tab v-if="$route.meta.isTab"></Tab>
            <el-card v-else class="card" shadow="hover">
                <keep-alive>
                    <router-view />
                </keep-alive>
            </el-card>
        </el-main>
    </template>
    
    <script>
        import Tab from '@/views/home/Tab.vue'
        export default {
            name: 'Content',
            components:{
                Tab
            }
        }
    </script>
    
    <style>
        .content {
            background-color: #f1f4f5;
        }
    
        .card {
            height: 100%;
        }
    </style>

    Step7:
      现在只需要完善 Tab.vue 组件,即可实现想要的效果了。

      Tab 组件中需要引入 mainTabs 、mainTabsActiveName 以及其相关修改方法。
    其中:
      mainTabs 用于展示当前标签列表,可以使用 v-for 进行遍历展示。
      mainTabsActiveName 用于显示当前标签选中项。

    import { mapState, mapActions } from 'vuex'
    export default {
        computed: {
            ...mapState('common', ['mainTabs']),
            mainTabsActiveName: {
                get() {
                    return this.$store.state.common.mainTabsActiveName
                },
                set(val) {
                    this.updateMainTabsActiveName(val)
                }
            }
        },
        methods: {
            ...mapActions('common', ['updateMainTabs', 'updateMainTabsActiveName'])
        }
    }

     

    Step8:
      给 Tab.vue 组件引入基本页面,
      使用 v-for 遍历 mainTabs 数组,如果 标签中 type 为 iframe,则使用 iframe 进行展示,否则使用 router-view 展示。根据 mainTabsActiveName 选中标签页。

    <template>
        <!--
            el-tabs 用于显示标签页,
            其中:
                v-model 绑定当前选中的 标签
                :closable = true 表示当前标签可以关闭
                @tab-click 绑定标签选中事件
                @tab-remove 绑定标签删除事件
        -->
        <el-tabs v-model="mainTabsActiveName" class="tab" :closable="true" @tab-click="selectedTabHandle" @tab-remove="removeTabHandle">
            <!-- 循环遍历标签数组,用于生成标签列表 -->
            <el-tab-pane v-for="item in mainTabs" :key="item.name" :label="item.name" :name="item.name">
                <el-card class="card" shadow="hover">
                    <!-- 以 http 或者 https 开头的地址,均使用 iframe 进行展示 -->
                    <iframe v-if="item.type === 'iframe'" :src="item.iframeUrl" width="100%" height="650px" frameborder="0" scrolling="yes">
                    </iframe>
                    <!-- 自身组件模块路由跳转,使用 router-view 表示 -->
                    <keep-alive v-else>
                        <router-view v-if="item.name === mainTabsActiveName" />
                    </keep-alive>
                </el-card>
            </el-tab-pane>
            <!-- 定义下拉框,用于操作标签列表 -->
            <el-dropdown class="dropdown-tool" :show-timeout="0">
                <i class="el-icon-arrow-down"></i>
                <el-dropdown-menu slot="dropdown">
                    <el-dropdown-item @click.native="closeCurrentTabsHandle">关闭当前标签页</el-dropdown-item>
                    <el-dropdown-item @click.native="closeOtherTabsHandle">关闭其它标签页</el-dropdown-item>
                    <el-dropdown-item @click.native="closeAllTabsHandle">关闭全部标签页</el-dropdown-item>
                    <el-dropdown-item @click.native="refreshCurrentTabs">刷新当前标签页</el-dropdown-item>
                </el-dropdown-menu>
            </el-dropdown>
        </el-tabs>
    </template>
    <style scoped="scoped">
        .tab {
            background-color: #fff;
            margin: -15px -20px 10px -20px;
            padding: 0 10px 0 10px;
            height: 40px;
        }
    
        .dropdown-tool {
            float: left;
            position: fixed !important;
            right: 0;
             40px;
            height: 40px;
            line-height: 40px;
            top: 55px;
            background-color: #f1f4f5;
        }
    
        .card {
            height: 650px;
        }
    </style>

    Step9:
      定义 Tab.vue 相关方法:
        selectedTabHandle 标签选中事件,选中标签后触发。
        removeTabHandle 标签移除事件,删除标签后触发。
        closeCurrentTabsHandle 关闭当前标签。
        closeOtherTabsHandle 关闭其他标签。
        closeAllTabsHandle 关闭所有标签。
        refreshCurrentTabs 刷新当前选中的标签。

    【selectedTabHandle:】
    选中事件处理很简单,首先找到选中的标签页,然后路由跳转即可,
    由于 Aside.vue 中,已经监听了 $route,所以路由一变化,就会进行相关处理(修改 vuex 的三个值)。
    注:
        选中已选中的标签时,由于是同一个路由,路由($route)不变化,
        若想实现变化,可以见后面的 refreshCurrentTabs 方法处理。
    
    // 处理标签选中事件
    selectedTabHandle(tab) {
        // 选择某个标签,标签存在于标签数组时,则跳转到相应的路由(根据名字跳转)
        tab = this.mainTabs.filter(item => item.name === tab.name)[0]
        if (tab) {
            // 已经在 Aside.vue 中使用 watch 监视了 $route,所以一旦路由变化,其就可以感知到,从而维护 vuex 状态。
            this.$router.push({
                name: tab.name,
                query: tab.query,
                params: tab.params
            })
        }
    }
    
    【removeTabHandle】
    移除事件,只要从 标签列表 中找到选中的标签移除即可。
    若标签列表没有数据,则跳转到首页。
    若移除的标签是当前选中的标签,则移除后跳转到最后一个标签页。
    
    // 处理标签删除事件
    removeTabHandle(tabName) {
        // 从 mainTabs 中删除标签即可
        this.updateMainTabs(this.mainTabs.filter(item => item.name !== tabName))
        // 如果当前 mainTabs 中仍有值,则进行当前选中标签逻辑处理
        if (this.mainTabs.length > 0) {
            // 如果删除的是当前选中的标签,则默认选择最后一个标签
            let tab = this.mainTabs[this.mainTabs.length - 1]
            if (tabName === this.mainTabsActiveName) {
                this.$router.push({
                    name: tab.name,
                    query: tab.query,
                    params: tab.params
                })
            }
        } else {
            // 如果当前 mainTabs 中没有值,则跳转到 HomePage 主页面
            this.updateMainTabsActiveName('')
            this.$router.push({name: 'HomePage'})
        }
    }
    
    【closeCurrentTabsHandle、closeOtherTabsHandle、closeAllTabsHandle】
    直接操作 标签列表 mainTabs 即可。
    关闭所有列表后,需要跳转到首页。
    
    // 关闭当前标签
    closeCurrentTabsHandle() {
        this.removeTabHandle(this.mainTabsActiveName)
    },
    // 关闭其他标签
    closeOtherTabsHandle() {
        this.updateMainTabs(this.mainTabs.filter(item => item.name === this.mainTabsActiveName))
    },
    // 关闭所有标签
    closeAllTabsHandle() {
        // 清空 mainTabs 数组,并跳转到 主页面
        this.updateMainTabs([])
        // 如果当前 mainTabs 中没有值,则跳转到 HomePage 主页面
        this.updateMainTabsActiveName('')
        this.$router.push({name: 'HomePage'})
    }
    
    【refreshCurrentTabs:】
    由于同一个路由跳转时, $route 不会变化,即 watch 失效。
    想要实现刷新效果,可以先移除标签,再添加标签,并重新跳转。
    
    // 刷新当前选中的标签
    refreshCurrentTabs() {
        // 用于保存当前标签数组
        let tabs = []
        Object.assign(tabs, this.mainTabs)
        // 保存当前选中的标签
        let tab = this.mainTabs.filter(item => item.name === this.mainTabsActiveName)[0]
        // 先移除标签
        this.removeTabHandle(tab.name)
        this.$nextTick(() => {
            // 移除渲染后,再重新添加标签数组,并跳转路由
            this.updateMainTabs(tabs)
            this.$router.push({
                name: tab.name,
                query: tab.query,
                params: tab.params
            })
        })
    }

    注:
      想要同一个路由跳转不报错,在 route 中需要定义如下代码。

    // 解决相同路径跳转报错
    const routerPush = VueRouter.prototype.push;
    VueRouter.prototype.push = function push(location, onResolve, onReject) {
        if (onResolve || onReject)
            return routerPush.call(this, location, onResolve, onReject)
        return routerPush.call(this, location).catch(error => error)
    }

    Step10:
      实现国际化。
    如下,代码需要实现国际化。

    <el-dropdown class="dropdown-tool" :show-timeout="0">
        <i class="el-icon-arrow-down"></i>
        <el-dropdown-menu slot="dropdown">
            <el-dropdown-item @click.native="closeCurrentTabsHandle">关闭当前标签页</el-dropdown-item>
            <el-dropdown-item @click.native="closeOtherTabsHandle">关闭其它标签页</el-dropdown-item>
            <el-dropdown-item @click.native="closeAllTabsHandle">关闭全部标签页</el-dropdown-item>
            <el-dropdown-item @click.native="refreshCurrentTabs">刷新当前标签页</el-dropdown-item>
        </el-dropdown-menu>
    </el-dropdown>

    修改 zh.json、en.json。

    【zh.json】
    "tab": {
        "closeCurrentTabs": "关闭当前标签页",
        "closeOtherTabs": "关闭其它标签页",
        "closeAllTabs": "关闭全部标签页",
        "refreshCurrentTabs": "刷新当前标签页"
    }
    
    【en.json】
    "tab": {
        "closeCurrentTabs": "Close Current Tabs",
        "closeOtherTabs": "Close Other Tabs",
        "closeAllTabs": "Close All Tabs",
        "refreshCurrentTabs": "Refresh Current Tabs"
    }

    修改Tab.vue

    <!-- 定义下拉框,用于操作标签列表 -->
    <el-dropdown class="dropdown-tool" :show-timeout="0">
        <i class="el-icon-arrow-down"></i>
        <el-dropdown-menu slot="dropdown">
            <el-dropdown-item @click.native="closeCurrentTabsHandle">{{$t("tab.closeCurrentTabs")}}</el-dropdown-item>
            <el-dropdown-item @click.native="closeOtherTabsHandle">{{$t("tab.closeOtherTabs")}}</el-dropdown-item>
            <el-dropdown-item @click.native="closeAllTabsHandle">{{$t("tab.closeAllTabs")}}</el-dropdown-item>
            <el-dropdown-item @click.native="refreshCurrentTabs">{{$t("tab.refreshCurrentTabs")}}</el-dropdown-item>
        </el-dropdown-menu>
    </el-dropdown>

      

    3、页面刷新时保存 state 数据

    【页面刷新时,如何保持原有vuex中的state信息】
        https://www.cnblogs.com/l-y-h/p/11722007.html

      由于 使用 vuex 维护了数据,页面一刷新,state 数据会变化,就会出现很诡异的效果。
    如下图:
      选中了标签后,但是一刷新页面,数据相关效果就会变得很奇怪。

    解决方法:
      在页面刷新之前,将 state 信息保存,页面刷新后,再将该值赋给 state。

    在 Home.vue 中添加如下代码:
      使用 localStorage 保存 state 信息(也可以使用 sessionStorage)。

    created() {
        //在页面加载时读取localStorage里的状态信息
        if (localStorage.getItem("store") ) {
            this.$store.replaceState(Object.assign({}, this.$store.state,JSON.parse(localStorage.getItem("store"))))
        }
    
        //在页面刷新时将vuex里的信息保存到localStorage里
        window.addEventListener("beforeunload",()=>{
            localStorage.setItem("store",JSON.stringify(this.$store.state))
        })
    }

      当然,为了防止登录时获取到上一个用户保存的 state 值,需要在 登录时将其移除。
    如下:
      在 vuex 中定义一个重置数据的方法,并在登录页面创建时调用。

    // 更改 state(同步)
    mutations: {
        resetState(state) {
            let stateTemp = {
                language: 'zh',
                menuActiveName: '',
                mainTabs: [],
                mainTabsActiveName: ''
            }
            Object.assign(state, stateTemp)
        }
    },
    // 异步触发 mutations
    actions: {
        resetState({commit, state}) {
            commit("resetState")
        }
    }

    在 登录页面 引入并调用。

    ...mapActions('common', {resetState: "resetState"})
    
    created() {
        // 进入画面前,移除主页面保存的 state 信息
        localStorage.removeItem("store")
        this.resetState()
    }

    完整效果:
      主页面中,页面一刷新,state 就会保存在 localStorage 中,
      进入登录界面后,会移除掉 localStorage 中的 state 数据,并重置 state 数据。

  • 相关阅读:
    数据结构实用C语言基础
    第二个星期的C语言
    hello world!
    代码规范
    调用有道词典翻译api
    PO,POJO,BO,DTO,VO的区别
    切换Eclipse语言包
    URI和URL区别
    get和post
    Eclipse快捷键
  • 原文地址:https://www.cnblogs.com/huoyz/p/14378959.html
Copyright © 2020-2023  润新知