本章技術棧:Vue-Router + VueX + Tabs + 動態菜單渲染
背景:上週寫了一篇基於 Iframe實現單頁面多tab切換界面無刷新 的功能,現在SPA盛行的時代,感覺Iframe實現SPA有點Low了(不過基於傳統多頁面實現SPA也是無奈之舉),所以最近想着基於VUE實現多tab功能,隨便也實現了菜單欄動態渲染、路由管理、狀態管理等項目框架基礎功能。這樣看來,這都可以用作一般中小型web後臺管理系統的框架基本骨架了。基於這套骨架後面我會持續增加登錄(Token)、TTP請求(axios)、用戶、角色、權限管理等。還會封裝一些分頁、上傳等組建,打造一款真正的開箱即用的後臺管理系統框架,讓開發者更多的關注項目需求,提高開發效率,節省開發成本。
先上效果圖
讀完這篇文章你能收穫什麼?
- 可以基於這套骨架搭建屬於自己的後臺管理系統框架
- 可以在自己的Vue項目中加入Tabs功能
- 瞭解Vue-Router、VueX基本用法
- 瞭解Element-ui中Menu、Tabs等組建基本用法
源碼註釋很詳細:https://github.com/xuqiaoba/vue-frame
核心代碼:
- 左側菜單欄渲染:
<template>
<el-menu
:default-active="activeItem"
class="el-menu-vertical-demo"
background-color="#545c64"
text-color="#fff"
router
:unique-opened='true'
@select="clickMenuItem"
>
<template v-for="(item,index) in menu">
<el-submenu v-if="item.hasChilder" :index="item.index" :key="index">
<template slot="title">
<i class="el-icon-document"></i>
<span>{{item.name}}</span>
</template>
<template v-for="(v,i) in item.children">
<el-menu-item :index="v.index" :key="i">{{v.name}}</el-menu-item>
</template>
</el-submenu>
<template v-else>
<el-menu-item :key="index" :index="item.index" >
<template slot="title">
<i class="el-icon-location"></i>
<span>{{item.name}}</span>
</template>
</el-menu-item>
</template>
</template>
</el-menu>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
data () {
return {
currFatherIndex: ''
}
},
mounted () {
this.getMenu()
},
methods: {
...mapActions('menu', {
getMenu: 'getMenu',
clickMenuItem: 'clickMenuItem'
})
},
computed: {
...mapState('menu', {
menu: 'menu',
activeItem: 'activeItem'
})
}
}
</script>
<style scoped>
.el-menu > ul,
.el-menu {
height: 100%;
}
.el-aside {
height: 100%;
}
</style>
- 右側Tabs組件:
<template>
<!-- 參考element-ui中Tabs組件 -->
<el-tabs :value="activeItem" @tab-remove="tabRemove" class='content-body' @tab-click="tabClick">
<el-tab-pane v-for="item in tabs" :label="item.label" :key="item.index" :name="item.index" :closable="item.closable">
</el-tab-pane>
</el-tabs>
</template>
<script>
import { mapActions, mapState, mapMutations } from 'vuex'
export default {
computed: {
...mapState('menu', {
tabs: 'tabs',
activeItem: 'activeItem'
})
},
created () {
console.log(this.tabs)
},
methods: {
...mapActions('menu', {
closeTab: 'closeTab'
}),
...mapMutations('menu', {
switchTab: 'switchTab'
}),
tabClick (e) {
this.switchTab(e.name)
this.$router.push({ path: e.name })
},
tabRemove (e) {
let t = this
// 異步阻塞一下,否則activeItem還是當前關閉tab的值
setTimeout(function () {
t.$router.push({ path: t.activeItem })
}, 1)
this.closeTab(e)
}
}
}
</script>
<style scoped>
.content-body {
height: 40px !important;
}
</style>
- VueX狀態管理:
import Store from './store'
// 菜單列表,可通過後臺返回,返回格式類似就行,還可增加icon圖標等字段
const menumap = [
{ name: '首頁', hasChilder: false, index: 'index', children: [] },
{ name: '菜單一', hasChilder: false, index: 'one', children: [] },
{ name: '菜單二', hasChilder: false, index: 'two', children: [] },
{
name: '菜單三',
hasChilder: true,
index: 'three',
children: [
{ name: '子菜單3-1', hasChilder: false, index: 'three3-1' },
{ name: '子菜單3-2', hasChilder: false, index: 'three3-2' }
]
},
{
name: '菜單四',
hasChilder: true,
index: 'four',
children: [
{ name: '子菜單4-1', hasChilder: false, index: 'four4-1' },
{ name: '子菜單4-2', hasChilder: false, index: 'four4-2' }
]
}
]
Store.registerModule('menu', {
namespaced: true,
state: {
menu: [],
// 默認tabs裏面有‘首頁’,且沒有closable屬性,不能刪除
tabs: [
{
label: '首頁',
index: 'index'
}
],
activeItem: 'index' // 默認選中首頁
},
getters: {
},
mutations: {
initMenu (state, menu) {
state.menu = menu
},
initTabs (state, tabs) {
state.tabs = tabs
},
addTab (state, tab) {
state.tabs.push(tab)
},
switchTab (state, nowIndex) {
state.activeItem = nowIndex
}
},
actions: {
getMenu (context) {
// 查詢所有菜單(如果做權限管理,即根據不同用戶(角色),顯示不同的菜單,可以在後臺就判斷權限,然後對應有權限的菜單列表)
context.commit('initMenu', menumap)
},
clickMenuItem (context, index) {
console.log(index)
// ‘index’一直固定在tabs裏面,所有不需要‘addTab’
if (index !== 'index') {
// find()函數用來查找目標元素,找到就返回該元素,找不到返回undefined。
var tab = context.state.tabs.find(f => f.index === index)
// tabs裏面沒有,就去總菜單去查詢
if (!tab) {
let menu = {}
// 首先查詢一級菜單
menu = context.state.menu.find(f => f.index === index)
// 如果當前menu不在一級菜單,則查詢所有二級菜單
if (!menu) {
// flat()用於將嵌套的數組“拉平”,變成一維數組。該方法返回一個新數組,對原數據沒有影響
menu = context.state.menu.map(a => a.children).flat().find(f => f.index === index)
}
let newTab = {
label: menu.name,
index: menu.index,
closable: true
}
context.commit('addTab', newTab)
}
}
context.commit('switchTab', index)
},
closeTab (context, index) {
console.log(index)
// findIndex()函數也是查找目標元素,找到就返回元素的位置,找不到就返回-1。
let indexNum = context.state.tabs.findIndex(f => f.index === index)
console.log(indexNum)
// 當前選中菜單
let activeItem = context.state.activeItem
// 除移除的tab之外的所有tabs
let newTabs = context.state.tabs.filter(f => f.index !== index)
// 重新初始化tabs
context.commit('initTabs', newTabs)
// 如果刪除的tab正好是當前選中的tab,則執行“switchTab”,改變activeItem
if (activeItem === index) {
// 如果indexNum===0,則表示switchTab到index(但是‘首頁’不可能被刪除,所以如果indexNum不可能爲0),否則跳轉到刪除tab的上一個tab
context.commit('switchTab', indexNum === 0 ? 'index' : newTabs[indexNum - 1].index)
}
}
}
})
- Vue-Router路由管理:
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
redirect: 'index',
component: () => import('../views/Home.vue'),
children: [
{
path: 'index',
name: 'index',
component: () => import('../views/Index.vue')
},
{
path: 'one',
name: 'one',
component: () => import('../views/One.vue')
},
{
path: 'two',
name: 'two',
component: () => import('../views/Two.vue')
},
{
path: 'three3-1',
name: 'three3-1',
component: () => import('../views/three/Three3-1.vue')
},
{
path: 'three3-2',
name: 'three3-2',
component: () => import('../views/three/Three3-2.vue')
},
{
path: 'four4-1',
name: 'four4-1',
component: () => import('../views/four/Four4-1.vue')
},
{
path: 'four4-2',
name: 'four4-2',
component: () => import('../views/four/Four4-2.vue')
}
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
// 解決路由下的界面,點擊該路由多次時
// 報錯:NavigationDuplicated{_name: "NavigationDuplicated", name: "NavigationDuplicated", message: "Navigating to current location ("/one") is not allowed", stack: "Error↵ at new NavigationDuplicated (webpack-int…e_modules/element-ui/lib/mixins/emitter.js:29:22)"}
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push (location) {
return originalPush.call(this, location).catch(err => err)
}
export default router
未完待續......
歡迎交流,歡迎 Star (^_^)
經驗總結,代碼加工廠!