鄙人做服务端的,想学学前端,顺便用vue + element ui搭了一个管理员后台。
在这里参考了官网提供的管理员后台模板vue-elemetn-admin
https://panjiachen.github.io/vue-element-admin-site/zh/guide/
https://github.com/PanJiaChen/vue-element-admin
官网的,自然更好看,功能更强大, 我做的只能算是超级阉割版,主要是为了学习。
我做这个管理员后台还有一个原因,权限问题。
https://juejin.cn/post/6844903478880370701
注意:这篇文章还是要一点基础才能看得懂的
先说说官方的权限与左侧菜单的思路。菜单与路由关联的,路由常亮加路由变量(登录后根据用户角色计算出的),菜单项与用户角色挂钩,拥有这个角色的用户才能看到这个菜单。
这里是维护了一个全量的异步路由表,需要权限判断的菜单项都与角色关联,用户登录成功后根据用户角色过滤这个全量的路由表,再动态挂在到路由上。这里就出现了一个比较严重的问题:正常情况下,一个平台或者系统不可能只有一两个权限或者角色,根据官方的这种思路,新增角色的话,就需要修改这个全量的异步路由表(JS里面),这样就显得不灵活了,而且不满足需要。所以我就自己做了一个管理员后台,从服务端加载用户有权限的路由,再异步挂载。
先说说我用户、角色、权限的实现思路,这篇文章不包括服务端相关内容,所有的数据我都是在js里面写死的,到时候对接服务端的时候需要稍作修改。
一个用户有多个角色,一个角色有多个权限,用户与权限不直接关联。权限与路由绑定,角色授权权限,用户授权角色,这样就能获取到用户拥有的权限了。
这里一样的保留了登录,可以跟oauth2的密码模式很好的结合。
首先简单看一下相关的版本,主要是vue3 + element-plus
{
"name": "front-end",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"element-plus": "^1.0.2-beta.44",
"js-cookie": "^2.2.1",
"nprogress": "^0.2.0",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "^4.5.13",
"@vue/cli-plugin-vuex": "^4.5.13",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
基本布局与说明
首先看一下布局
最外层容器
左侧el-aside:左侧菜单栏
上方el-header:收缩、展开按钮,面包屑导航,搜索,头像下拉
主要,el-main:选项卡
再看代码结构,没有什么花里胡哨的,我是从零开始搭建的
注意layout文件夹
dashboard:首页
NavBreadcrumb:面包屑导航
NavHeader:头
NavMenu:左侧菜单
NavTab:选项卡
左侧菜单
首先,服务端会生成一个树,需要还用这个解析树生成路由对象并且添加到异步路由。后台维护的component是字符串,这里需要解析为组件。
由于这个树可能会有很多层级,所以把el-menu-item单独抽出来生成一个组件,可以递归遍历树。
<template>
<!-- 还有子菜单 -->
<el-submenu v-if="item && item.children && item.children.length > 0" :index="parent === '' ? item.path : parent + '/' + item.path">
<template #title>
<i v-if="item.meta && item.meta.icon" :class="item.meta.icon" />
<span>{{ item.meta.title }}</span>
</template>
<NavMenuItem v-for="temp in item.children" :key="temp.name" :item="temp" :parent="parent === '' ? item.path : parent + '/' + item.path" />
</el-submenu>
<!-- 显示和隐藏只针对最后一个菜单生效, hidden = true表示隐藏, 不显示 -->
<el-menu-item
v-else-if="item.meta.hidden === undefined || !item.meta.hidden"
:index="parent === '' ? item.path : parent + '/' + item.path"
>
<template #title>
<i v-if="item.meta.icon" :class="item.meta.icon" />
<span>{{ item.meta.title }}</span>
</template>
</el-menu-item>
</template>
<script>
export default {
name: 'NavMenuItem',
props: {
item: {
required: false,
type: Object,
default: () => {}
},
// 这个表示父路劲
parent: {
required: false,
type: String,
default: '' // 这个值为空字符串表示一级路由
},
key: {
required: false,
type: String,
default: ''
}
}
}
</script>
<style scoped>
</style>
使用组件
<NavMenuItem v-for="router in routers" :key="router.name" :item="router" />
这里遇到过一个bug,就是在menu-item外面包一层div,会出现收缩的时候效果不理想,F12调试会发现,左侧菜单内容与正常的菜单内容不一样。
头
展开、搜索
这是个状态保存在vuex里面,左侧菜单根据这个状态展开还是搜索
这个按钮
<!-- 展开 -->
<el-button v-if="isCollapse" size="small" icon="el-icon-s-unfold" class="btn-folde" @click="fold(!isCollapse)" />
<!-- 折叠 -->
<el-button v-else size="small" icon="el-icon-s-fold" class="btn-folde" @c
lick="fold(!isCollapse)" />
isCollapse 直接从状态里面获得
computed: {
isCollapse() {
return this.$store.getters.isCollapse
}
},
methods: {
fold(isCollapse) {
// 更新状态
this.$store.dispatch('navMenu/toggleCollapse', isCollapse)
}
}
面包屑导航
这里是只读的,这样简单,充当一个展示效果
<template>
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item v-for="item in activeItems" :key="item">{{ item }}</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
import { analyseBreadcreumb } from '../../store/modules/permission'
export default {
name: 'NavBreadcrumb',
props: {
max: {
type: Number,
default: 8
}
},
computed: {
// 面包屑数组
activeItems() {
// 解析生成面包屑
return analyseBreadcreumb(this.$router.getRoutes(), this.$store.getters.activeItem)
}
}
}
</script>
<style scoped>
</style>
搜索
<el-autocomplete
v-model="searchKeyword"
class="inline-input"
placeholder="请输入内容"
prefix-icon="el-icon-search"
size="small"
:fetch-suggestions="querySearch"
@select="handleSelect"
@keyup.enter="handleSelect"
/>
下拉
<el-dropdown style="height: 50px">
<div class="avatar">
<el-avatar :size="40" hape="square" :src="src" />
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/dashboard">
<el-dropdown-item>首页</el-dropdown-item>
</router-link>
<el-dropdown-item>个人资料</el-dropdown-item>
<el-dropdown-item @click.native="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
选项卡
这里需要注意,选项卡头部要定格在最上方,不能与内容一起滚动,这里是用css样式控制的。
全部源码
代码里面备注写的很多了,应该能看懂的。
- vue.config.js
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir)
}
module.exports = {
publicPath: '/',
devServer: {
port: 9527,
open: false
},
configureWebpack: {
// 设置标题
name: 'Admin Template',
resolve: {
alias: {
'@': resolve('src')
}
}
}
}
- package.json
{
"name": "front-end",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"core-js": "^3.6.5",
"element-plus": "^1.0.2-beta.44",
"js-cookie": "^2.2.1",
"nprogress": "^0.2.0",
"vue": "^3.0.0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "^4.5.13",
"@vue/cli-plugin-vuex": "^4.5.13",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-eslint": "^10.1.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
- src/layout/index.vue
<template>
<div class="common-layout">
<el-container>
<el-aside width="auto">
<NavMenu />
</el-aside>
<el-container>
<el-header height="50px">
<NavHeader />
<!--<el-radio-group v-model="isCollapse" style="margin-bottom: 20px;">
<el-radio-button :label="false">展开</el-radio-button>
<el-radio-button :label="true">收起</el-radio-button>
</el-radio-group>-->
</el-header>
<el-main>
<NavTab />
</el-main>
</el-container>
</el-container>
</div>
</template>
<script>
import NavMenu from './NavMenu'
import NavHeader from './NavHeader'
import NavTab from './NavTab/index'
export default {
name: 'Layout',
components: {
NavTab,
NavMenu,
NavHeader
}/*,
computed: {
isCollapse: {
get() {
return this.$store.getters.isCollapse
},
set(value) {
this.$store.commit('navMenu/TOGGLE_COLLAPSE', value)
}
}
}*/
}
</script>
<style scoped>
.common-layout,
.el-container {
height: 100%;
}
.el-header {
padding: 0;
border-bottom: 1px solid #e5e5e5;
}
.el-aside::-webkit-scrollbar{
width: 3px;
background-color: #F5F5F5;
}
/*定义滚动条轨道 内阴影+圆角*/
.el-aside::-webkit-scrollbar-track {
border-radius: 8px;
background-color: #F5F5F5;
}
/*定义滑块 内阴影+圆角*/
.el-aside::-webkit-scrollbar-thumb{
border-radius: 10px;
box-shadow: inset 0 0 6px rgba(0, 0, 0, .1);
background-color: #c8c8c8;
}
.el-main {
padding: 0;
overflow-y: hidden;
}
</style>
- src/layout/NavBreadcrumb/index.vue
<template>
<el-breadcrumb separator-class="el-icon-arrow-right">
<el-breadcrumb-item v-for="item in activeItems" :key="item">{{ item }}</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script>
import { analyseBreadcreumb } from '../../store/modules/permission'
export default {
name: 'NavBreadcrumb',
props: {
max: {
type: Number,
default: 8
}
},
computed: {
// 面包屑数组
activeItems() {
// 解析生成面包屑
return analyseBreadcreumb(this.$router.getRoutes(), this.$store.getters.activeItem)
}
}
}
</script>
<style scoped>
</style>
- src/layout/NavHeader/index.vue
<template>
<div class="header-contrainer">
<el-row>
<el-col :span="1">
<!-- 展开 -->
<el-button v-if="isCollapse" size="small" icon="el-icon-s-unfold" class="btn-folde" @click="fold(!isCollapse)" />
<!-- 折叠 -->
<el-button v-else size="small" icon="el-icon-s-fold" class="btn-folde" @click="fold(!isCollapse)" />
</el-col>
<el-col :span="18">
<NavBreadcrumb :max="8" />
</el-col>
<el-col :span="4">
<el-autocomplete
v-model="searchKeyword"
class="inline-input"
placeholder="请输入内容"
prefix-icon="el-icon-search"
size="small"
:fetch-suggestions="querySearch"
@select="handleSelect"
@keyup.enter="handleSelect"
/>
</el-col>
<el-col :span="1">
<el-dropdown style="height: 50px">
<div class="avatar">
<el-avatar :size="40" hape="square" :src="src" />
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/dashboard">
<el-dropdown-item>首页</el-dropdown-item>
</router-link>
<el-dropdown-item>个人资料</el-dropdown-item>
<el-dropdown-item @click.native="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-col>
</el-row>
</div>
</template>
<script>
import NavBreadcrumb from '../NavBreadcrumb/index'
export default {
name: 'NavHeader',
components: { NavBreadcrumb },
data() {
return {
collapseIcon: '',
searchKeyword: ''
}
},
computed: {
isCollapse() {
return this.$store.getters.isCollapse
},
src() {
// TODO 头像路径
return require('@/assets/default-avatar.jpeg')
}
},
methods: {
fold(isCollapse) {
this.$store.dispatch('navMenu/toggleCollapse', isCollapse)
},
// 退出登录
logout() {
this.$store.dispatch('user/resetToken')
.then(() => {
// 清空状态路由
this.$store.dispatch('permission/clearRouter')
// 清空选项卡
this.$store.dispatch('tabItem/resetTabs')
// 跳转到登录页面,直接跳转刷新
location.href = '/login'
}).catch((error) => {
console.log('logout error', error)
})
},
handleSelect() {
if (this.searchKeyword && this.searchKeyword !== '') {
const splits = this.searchKeyword.split(':')
if (splits.length > 1) {
this.$router.push({ path: splits[1] })
}
}
},
// 过滤
createFilter(queryString) {
return item => item.value.match(queryString)
},
// 过滤根,只保留叶子节点, routers这个是路由数组,这里需要转换一下
getLeaves() {
// 路由
const routers = this.$router.getRoutes()
const result = []
// 报名单
const writelist = ['/', '/login']
routers.forEach(router => {
if (!router.children || router.children.length === 0 || writelist.indexOf(router.path) === -1) {
result.push({
value: (router.meta && router.meta.title ? router.meta.title : '') + ':' + router.path,
title: router.meta.title
})
}
})
return result
},
// 关键字过滤
querySearch: function(queryString, cb) {
const filterRoutes = this.getLeaves()
if (queryString && queryString !== '') {
cb(filterRoutes.filter(this.createFilter(queryString)))
} else {
if (filterRoutes.length > 10) {
cb(filterRoutes.slice(0, 10))
} else {
cb(filterRoutes)
}
}
}
}
}
</script>
<style scoped>
.btn-folde {
font-size: 24px;
border: none;
height: 50px;
line-height: 50px;
padding: 0;
width: 50px;
padding-top: 2px;
}
.header-contrainer {
height: 50px;
line-height: 50px;
padding: 0 20px;
}
.el-breadcrumb {
line-height: 50px;
}
/* 头像 */
.avatar {
height: 50px;
width: 50px;
}
.avatar {
padding: 5px;
}
.router-link-active, a {
text-decoration: none;
}
</style>
- src/layout/NavMenu/index.vue
<template>
<div class="nav-menu-contrainer">
<el-menu
unique-opened
class="el-menu-vertical-demo"
:collapse="isCollapse"
background-color="#545c64"
text-color="#FFFFFF"
router
:default-active="$route.path"
>
<el-submenu index="1">
<template #title>
<i class="el-icon-alarm-clock" />
<span>一级菜单</span>
</template>
<el-menu-item index="2-1">二级菜单1</el-menu-item>
<el-menu-item index="2-2">二级菜单2</el-menu-item>
<el-submenu index="2-3">
<template #title>二级菜单3</template>
<el-menu-item index="2-3-1">三级菜单1</el-menu-item>
<el-menu-item index="2-3-2">三级菜单2</el-menu-item>
<el-submenu index="2-3-2">
<template #title>三级带单3</template>
<el-menu-item index="2-3-2-1">四级菜单1</el-menu-item>
<el-menu-item index="2-3-2-2">四级菜单2</el-menu-item>
<el-menu-item index="2-3-2-3">四级菜单3</el-menu-item>
</el-submenu>
</el-submenu>
</el-submenu>
<el-submenu index="2">
<template #title>
<i class="el-icon-alarm-clock" />
<span>Nav2</span>
</template>
<el-menu-item index="/system/demo/manage">nav2-1</el-menu-item>
<el-menu-item index="/system/demo/add">nav2-2</el-menu-item>
</el-submenu>
<NavMenuItem v-for="router in routers" :key="router.name" :item="router" />
</el-menu>
</div>
</template>
<script>
import NavMenuItem from './NavMenuItem'
export default {
name: 'NavMenu',
components: {
NavMenuItem
},
data() {
return {}
},
computed: {
isCollapse() {
return this.$store.getters.isCollapse
},
routers() {
console.log('vuex route', this.$store.getters.routers)
return this.$store.getters.routers
}
}
}
</script>
<style scoped>
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
.el-aside .nav-menu-contrainer{
height: 100%;
}
.el-menu {
height: 100%;
}
</style>
- src/layout/NavMenu/NavMenuItem/index.vue
<template>
<!-- 还有子菜单 -->
<el-submenu v-if="item && item.children && item.children.length > 0" :index="parent === '' ? item.path : parent + '/' + item.path">
<template #title>
<i v-if="item.meta && item.meta.icon" :class="item.meta.icon" />
<span>{{ item.meta.title }}</span>
</template>
<NavMenuItem v-for="temp in item.children" :key="temp.name" :item="temp" :parent="parent === '' ? item.path : parent + '/' + item.path" />
</el-submenu>
<!-- 显示和隐藏只针对最后一个菜单生效, hidden = true表示隐藏, 不显示 -->
<el-menu-item
v-else-if="item.meta.hidden === undefined || !item.meta.hidden"
:index="parent === '' ? item.path : parent + '/' + item.path"
>
<template #title>
<i v-if="item.meta.icon" :class="item.meta.icon" />
<span>{{ item.meta.title }}</span>
</template>
</el-menu-item>
</template>
<script>
export default {
name: 'NavMenuItem',
props: {
item: {
required: false,
type: Object,
default: () => {}
},
// 这个表示父路劲
parent: {
required: false,
type: String,
default: '' // 这个值为空字符串表示一级路由
},
key: {
required: false,
type: String,
default: ''
}
}
}
</script>
<style scoped>
</style>
- src/layout/NavTab/index.vue
<template>
<div class="content-contrainer">
<el-tabs v-model="activeItem" type="card" @tab-remove="removeTab" @tab-click="tabClick">
<el-tab-pane v-for="(item, index) in openTabs" :key="item.path" :label="item.title" :name="item.path" :closable="index !== 0">
<router-view />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
export default {
name: 'NavTab',
computed: {
openTabs() {
return this.$store.getters.openTabs
},
// 选中的选项卡
activeItem: {
get() {
return this.$store.getters.activeItem
},
set(value) {
this.$store.dispatch('tabItem/setActive', value)
}
}
},
watch: {
$route(to, from) {
// 添加选项卡
this.$store.dispatch('tabItem/addItem', { path: to.path, title: to.meta.title, name: to.name }).then(result => {
// 跳转
this.$router.push({ path: result })
})
}
},
mounted() {
// 刷新时以当前路由做为tab加入tabs
// 当前路由不是首页时,添加首页以及另一页到store里,并设置激活状态
// 当当前路由是首页时,添加首页到store,并设置激活状态
if (this.$route.path !== '/' && this.$route.path !== '/dashboard') {
this.$store.dispatch('tabItem/addItem', { path: '/dashboard', title: '首页', name: 'dashboard' })
this.$store.dispatch('tabItem/addItem', { path: this.$route.path, title: this.$route.meta.title, name: this.$route.name })
this.$store.dispatch('tabItem/setActive', this.$route.path)
} else {
this.$store.dispatch('tabItem/addItem', { path: '/dashboard', title: '首页', name: 'dashboard' })
this.$store.dispatch('tabItem/setActive', '/dashboard')
}
},
methods: {
// 删除选项卡
removeTab(targetName) {
// 当前路径
const currentPage = this.$route.path
// 删除
this.$store.dispatch('tabItem/deleteItem', targetName).then((result) => {
// 删除的是当前选项卡
if (currentPage === targetName) {
// result是删除选项卡之后返回的上一个选项卡的路径
this.$router.push({ path: result })
}
})
},
// 切换
tabClick() {
this.$router.push({ path: this.activeItem })
}
}
}
</script>
<style>
.content-contrainer {
height: 100%;
overflow-y: auto;
}
.content-contrainer .el-tabs--card>.el-tabs__header {
background-color: rgb(255 255 255);
position: absolute;
width: calc(100% - 201px);
opacity: 1;
z-index: 1;
}
.content-contrainer .el-tabs__content {
height: 100%;
padding-top: 44px;
}
</style>
- src/store/index.js
import { createStore } from 'vuex'
import navMenu from './modules/navMenu'
import permission from './modules/permission'
import user from './modules/user'
import tabItem from './modules/tabItem'
import getters from './getters'
export default createStore({
state: {
},
mutations: {
},
actions: {
},
modules: {
navMenu,
permission,
user,
tabItem
},
getters
})
- src/store/getters.js
const getters = {
isCollapse: state => state.navMenu.isCollapse, // 左侧菜单是否展开
routers: state => state.permission.routers, // 路由
openTabs: state => state.tabItem.openTabs, // 选项卡
activeItem: state => state.tabItem.activeItem // 当前选中菜单和选项卡,路径
}
export default getters
- src/modules/navMenu.js
const state = {
isCollapse: false
}
const mutations = {
TOGGLE_COLLAPSE: (state, isCollapse) => {
state.isCollapse = isCollapse
}
}
const actions = {
toggleCollapse({ commit }, isCollapse) {
commit('TOGGLE_COLLAPSE', isCollapse)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
- src/store/modules/permission.js
/*
* 权限、路由相关
*/
import Layout from '@/layout'
const state = {
routers: []
}
const mutations = {
// 设置路由
SET_ROUTER: (state, routers) => {
state.routers = routers
}
}
const actions = {
// 清空路由
clearRouter: ({ commit }) => {
commit('SET_ROUTER', [])
},
// 生成路由
generateRoute({ commit }) {
return new Promise((resolve, reject) => {
// TODO 自定义父子树比较组装复杂,由服务端生成
const asyncRoute = [
{
name: 'SystemManage', // 唯一名称,与选项卡name对应
title: '系统管理', // 菜单标题,与选项卡标题对应@deprecated
path: '/system', // 路径
// redirect: '/dashboard', // 重定向路径,TODO 不需要重定向,只有最后一个路由才会调转
// component: '../../layout', // 对应组件路径,为空表示一级路由,以都放在src下面,记得以"/"开始,TODO 这里还需要解析一次生成对应的组件
meta: {
hidden: false, // 是否隐藏,添加页面、编辑页面等,默认不隐藏
icon: 'el-icon-plus', // 图标,针对一级路由生效
title: '系统管理' // 选项卡和菜单展示名称
},
// 子路由
children: [
{
name: 'DemoManage',
title: '测试管理',
path: 'demo/manage',
component: '/demo/manage/index',
meta: {
title: '系统管理'
}
},
{
name: 'DemoAdd',
title: '测试添加',
path: 'demo/add',
component: '/demo/add/index',
meta: {
hidden: true,
title: '测试添加'
}
},
{
name: 'DemoEdit',
title: '测试编辑',
path: 'demo/edit',
component: '/demo/edit/index',
meta: {
hidden: true,
title: '测试编辑'
}
}
]
},
{
name: 'nested',
path: '/nested',
meta: {
icon: 'el-icon-plus',
title: '一级嵌套路由'
},
children: [
{
name: 'nested1',
path: 'nested1',
component: '/nested/index-1',
meta: {
title: '一级嵌套路由1'
},
children: [
{
name: 'nested1-1',
path: 'nested1-1',
component: '/nested/nested/index-1-1',
meta: {
title: '二级嵌套路由1'
}
},
{
name: 'nested1-2',
path: 'nested1-2',
component: '/nested/nested/index-1-2',
meta: {
title: '二级嵌套路由2',
hidden: false
}
}
]
},
{
name: 'nested2',
path: 'nested2',
component: '/nested/index-2',
meta: {
title: '一级嵌套路由2'
}
}
]
}
]
// 状态保存
commit('SET_ROUTER', asyncRoute)
// todo 解析
resolve(analyseRoute(asyncRoute))
})
}
}
/**
* 导入vue组件
* @param file
* @returns {function(): *}
* @private
*/
function _import(file) {
return () => import('@/views' + file + '.vue')
}
/**
* 路由树生成路由对象,component由字符串变为组件
* @param routes
*/
function analyseRoute(routes) {
// 最终生成的路由
const result = []
// 递归
recurrenceRoute(result, routes)
return result
}
/**
* 递归
* @param result 返回结果
* @param routes 数组
*/
export function recurrenceRoute(result, routes) {
routes.forEach(route => {
const temp = {
name: route.name,
path: route.path,
meta: {},
component: route.component ? _import(route.component) : Layout
}
if (route.meta) {
if (route.meta.icon) {
temp.meta.icon = route.meta.icon
}
if (route.meta.hidden !== undefined) {
temp.meta.hidden = route.meta.hidden
}
if (route.meta.title) {
temp.meta.title = route.meta.title
}
}
if (route.children) {
temp.children = []
recurrenceRoute(temp.children, route.children)
}
result.push(temp)
})
}
/**
* 解析面包屑数组
* @param routers 路由直接获取
* @param activeMenu 当前激活的菜单,路径
*/
export function analyseBreadcreumb(routers, activeMenu) {
// 面包屑导航
const array = []
while (activeMenu && activeMenu.length > 0) {
// 查找路由
const route = searchByActiveMenu(routers, activeMenu)
if (route && route.meta && route.meta.title) {
array.unshift(route.meta.title)
}
activeMenu = activeMenu.substr(0, activeMenu.lastIndexOf('/'))
}
return array
}
/**
* 检索第一条符合条件的路由
* @param routers
* @param activeMenu
*/
function searchByActiveMenu(routers, activeMenu) {
for (let i = 0; i < routers.length; i++) {
if (routers[i].path === activeMenu) {
return routers[i]
}
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
- src/store/modules/tabItem.js
/*
* 选项卡
*/
const state = {
openTabs: [], // 选项卡已有数据,【首页】为第一条
activeItem: '' // 当选选中的选项卡,路径
}
const mutations = {
/**
* 添加选项卡,如果选项卡存在(根据路径确定),则选中该选项卡,不存在则添加
* {
* name: '', 名称
* path: '/dashboard', # 路由访问路径, 这个绝对路径
* title: '' # 标题,选项卡展示的那个
* }
* @param state
* @param item
* @constructor
*/
ADD_ITEM: (state, item) => {
// 已有选项卡名称
const pathes = state.openTabs.map(tab => tab.path)
// 判断选项卡是否已存在
if (pathes.indexOf(item.path) === -1) {
// 选项卡不存在,添加
state.openTabs.push(item)
}
// 选中菜单
state.activeItem = item.path
},
// 设置当前选中的选项卡名称
SET_ACTIVE_ITEM: (state, activeItem) => {
state.activeItem = activeItem
},
// 根据index删除选项卡
DELETE_ITEM: (state, index) => {
// 删除
state.openTabs.splice(index, 1)
},
// 重置选项卡
RESET_TABS: (state) => {
state.openTabs = []
state.activeMenu = ''
}
}
const actions = {
// 添加选项卡
addItem({ commit }, item) {
return new Promise(resolve => {
commit('ADD_ITEM', item)
resolve(item.path)
})
},
// 设置选项卡选中
setActive({ commit }, activeItem) {
commit('SET_ACTIVE_ITEM', activeItem)
},
// 删除选项卡,根据名称删除
deleteItem({ commit, state }, path) {
return new Promise((resolve, reject) => {
for (let i = 0; i < state.openTabs.length; i++) {
if (state.openTabs[i].path === path) {
// 删除选项卡
commit('DELETE_ITEM', i)
resolve(state.openTabs[i - 1].path)
}
}
})
},
// 重置选项卡
resetTabs({ commit }) {
commit('RESET_TABS')
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
- src/store/modules/user.js
/*
* 用户基本操作
*/
import { removeToken, setToken } from '../../utils/auth'
const state = {
// 头像
avatar: '',
// 用户名
username: ''
}
const mutations = {
SET_AVATAR: (state, avatar) => {
state.avatar = avatar
},
SET_USERNAME: (state, username) => {
state.username = username
}
}
const actions = {
// 登录操作, username + password,登录成功获取access_token并保存在cookie里面
login({ commit }, userinfo) {
const { username, password } = userinfo
return new Promise((resolve, reject) => {
// TODO 登录查询
console.log('login username', username)
console.log('login password', password)
if (username !== 'admin') {
reject('用户名密码错误')
} else {
setToken('this_is_admin_token')
console.log('设置token')
resolve()
}
})
},
// 重置token,主要是删除cookie里面的token
resetToken({ commit }) {
return new Promise(resolve => {
// 清空cookie里面token
removeToken()
// 清空头像
commit('SET_AVATAR', '')
// 清空用户名
commit('SET_USERNAME', '')
resolve()
})
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
- src/utils/auth.js
/**
* cookie存取用户登录token
*/
import Cookies from 'js-cookie'
const accessToken = 'access_token'
export function getToken() {
return Cookies.get(accessToken)
}
export function setToken(token) {
return Cookies.set(accessToken, token)
}
export function removeToken() {
return Cookies.remove(accessToken)
}
- src/views/login/index.vue
<template>
<div class="login-contrainer">
<div class="login-form-contrainer">
<div class="form-header-tip">
<span>欢迎您</span>
</div>
<div class="login-form">
<el-form ref="form" :model="form" :rules="rules" label-width="auto">
<el-form-item prop="username">
<el-input v-model="form.username" prefix-icon="el-icon-user" placeholder="用户名" clearable />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" prefix-icon="el-icon-s-goods" show-password placeholder="密码" clearable @keyup.enter="login('form')" />
</el-form-item>
<el-button class="btn-login" type="primary" @click.native.prevent="login('form')">登录</el-button>
</el-form>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Login',
data() {
return {
form: {
username: 'admin',
password: '123456'
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
},
redirect: ''
}
},
created() {
// 设置重定向路由
this.redirect = this.getRedirectUrl(this.$route.fullPath)
},
methods: {
login(forName) {
this.$refs[forName].validate(valid => {
if (valid) {
this.$store.dispatch('user/login', this.form)
.then(() => {
// 跳转
this.$router.push({ path: this.redirect || '/' })
})
.catch((error) => {
this.$message.error(error)
})
} else {
return false
}
})
},
// 解析重定向路由
getRedirectUrl(fullPath) {
const urlParams = fullPath.substr(fullPath.lastIndexOf('?') + 1)
const keyPairs = urlParams.split('&')
keyPairs.forEach(keyPair => {
const params = keyPair.split('=')
if (params[0] === 'redirect') {
this.redirect = params[1]
}
})
return '/dashboard'
}
}
}
</script>
<style scoped>
html, body, #app, .login-contrainer{
height: 100%;
}
.login-contrainer {
background-size: 100% 100%;
background: url('../../assets/login-bg-img.jpeg') no-repeat;
position: relative;
}
.login-form-contrainer {
border-radius: 3px;
position: absolute;
width: 500px;
height: 300px;
top: 50%;
left: 50%;
/* 用transform向左(上)平移它自己宽度(高度)的50%,也就达到居中效果了 */
transform: translate(-50%, -50%);
background-color: #FFFFFF;
box-shadow: 3px 3px 10px #888888;
}
.form-header-tip{
font-size: 30px;
font-weight: bold;
text-align: center;
height: 100px;
line-height: 100px;
}
.login-form {
padding: 0 30px;
}
.btn-login {
width: 100%;
}
</style>
由于遇到的问题很多,这里就没有意义描述了,这里贴出了全部源码
有需要的联系我的163邮箱,见url地址
或者到csdn下载https://download.csdn.net/download/admin_15082037343/18785160