前提:
(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%; width: 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); width: 100%; max-width: 200px; background-color: #263238; text-align: left; right: 0; } .header-logo { background-color: #17b3a3; text-align: center; height: 50px; line-height: 50px; width: 200px; font-size: 24px; color: #fff; font-weight: bold; margin-bottom: 0; cursor: pointer; } .el-submenu .el-menu-item { max-width: 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; width: 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 数据。