本章技术栈:Vue-Router + VueX + Tabs + 动态菜单渲染
背景:上周写了一篇基于 Iframe实现单页面多tab切换界面无刷新 的功能,现在SPA盛行的时代,感觉Iframe实现SPA有点Low了(不过基于传统多页面实现SPA也是无奈之举),所以最近想着基于VUE实现多tab功能,随便也实现了菜单栏动态渲染、路由管理、状态管理等项目框架基础功能。这样看来,这都可以用作一般中小型web后台管理系统的框架基本骨架了。基于这套骨架后面我会持续增加登录(Token)、HTTP请求(axios)、用户、角色、权限管理等。还会封装一些分页、上传等组建,打造一款真正的开箱即用的后台管理系统框架,让开发者更多的关注项目需求,提高开发效率,节省开发成本。
先上效果图
读完这篇文章你能收获什么?
- 可以基于这套骨架搭建属于自己的后台管理系统框架
- 可以在自己的Vue项目中加入Tabs功能
- 了解Vue-Router、VueX基本用法
- 了解Element-ui中Menu、Tabs等组建基本用法
- 浏览器前进后退时监听路由变化,更新当前tabs(10月25日晚新增)
- 利用<keep-alive>动态缓存指定组件(10月25日晚新增)
源码注释很详细,详情请戳:https://gitbook.cn/gitchat/activity/5db1a2469fc75b4c0af61ee4
核心代码:
- 左侧菜单栏渲染:
<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
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) {
if (index !== 'index') {
var tab = context.state.tabs.find(f => f.index === index)
if (!tab) {
let menu = {}
menu = context.state.menu.find(f => f.index === index)
if (!menu) {
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) {
let indexNum = context.state.tabs.findIndex(f => f.index === index)
let activeItem = context.state.activeItem
let newTabs = context.state.tabs.filter(f => f.index !== index)
context.commit('initTabs', newTabs)
if (activeItem === index) {
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
})
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push (location) {
return originalPush.call(this, location).catch(err => err)
}
export default router
10月29日晚,更新以下内容:
- 增加登录界面,路由守卫,储存 token;
- 封装 Axios,实现http请求拦截和响应拦截;
- 加入highcharts和拖拽组件,实现推拽图表功能;
- 增加样式,美化界面;
整体效果图
目前整体功能包括:
1.登录(路由守卫);
2.封装 Axios 请求;
3.Tabs 动态管理,切换界面;
4.动态渲染菜单;
5.利用 <keep-alive>
; 动态缓存指定组件;
6.Vue-Router 路由管理;
7.VueX 状态管理;
8.图表拖拽效果;
特点:结构简单,通用性强,适合用作中小型 Web 后台管理系统框架,开箱即用。
- Axios封装
import axios from 'axios'
import { Message, Loading } from 'element-ui'
import router from '../router/index'
let loading // 定义loading变量
function startLoading () { // 使用Element loading-start 方法
loading = Loading.service({
lock: true,
text: '加载中...',
background: 'rgba(0, 0, 0, 0.7)'
})
}
function endLoading () { // 使用Element loading-close 方法
loading.close()
}
// 请求拦截 设置统一header
axios.interceptors.request.use(config => {
// 加载
startLoading()
if (localStorage.eleToken) { config.headers.Authorization = localStorage.eleToken }
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截 token过期处理
axios.interceptors.response.use(response => {
endLoading()
if (response.data.success) {
return response.data.message
} else {
Message.error(response.data.message)
return Promise.reject(response.data.message)
}
}, error => {
// 错误提醒
endLoading()
// Message.error(error.response.message)
const { status } = error.response
if (status === '401') { //状态吗根据后台返回而定
Message.error('token值无效,请重新登录')
// 清除token
localStorage.removeItem('eleToken')
// 页面跳转
router.push('/login')
} else {
Message.error('系统错误')
}
return Promise.reject(error)
})
export default axios
源码注释很详细,详情请戳:https://gitbook.cn/gitchat/activity/5db1a2469fc75b4c0af61ee4
未完待续......
欢迎交流,欢迎 Star (^_^)
经验总结,代码加工厂!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。