前提: (1) 相關博文地址: SpringBoot + Vue + ElementUI 實現後臺管理系統模板 -- 前端篇(一):搭建基本環境:https://www.cnblogs.com/l-y-h/p/12930895.html SpringBoot + Vue + ElementUI 實現 ...
前提:
(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(() => { // 移除渲染後,再重新添加標