前提:
(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 數據。

