一、项目环境
- 前端技术栈:Vue-Cli
- 前端软体:WebStorm 2020.3
- 前端样式: Bootstrap; Element-UI
二、文章主题
- 内容概述:为将Project07与SpringSecurity/Shiro+jwt整合,主要参考了MarkerHub_前后端分离后台管理系统进行改写。本文是关于该视频P16内容的学习整理。VueX相关内容参考:不良人_VueX
- 项目源码:shoppingProject01_pub : version7.1
注:目前前端界面已初步搭建完成,运行前端项目后,可供参考的地址分别为登录展示及内容展示。
已实现的前端页面如图1所示:
(1) 左侧为导航栏,期望用户有三种角色(admin,vip,normal),不同角色进入商城时左侧应具有不同的导航栏显示,如admin可以看到“商品管理、用户管理、订单查询”,而其他角色不行。
(2) 为此,需要动态获取左侧导航栏的条目并对各条目进行动态路由绑定。如此,不具有访问权限的用户左侧将不会显示对应条目,在地址栏输入受限路由地址也不会跳转到相应的界面。
4. WebStorm项目相关目录结构如图2所示
三、实现过程
Step1:mock.js模拟后端发送过来的数据
Mock.mock('/menu/nav','get',()=> {
let nav = [
{
title: '商品',
name: 'item', // 用于高亮
icon: 'el-icon-s-shop',
component: '',
path: '',
children: [
{
title: '商品列表',
name: 'itemList',
icon: 'el-icon-s-goods',
component: 'sys/item/itemList',
path: '/itemList',
},
{
title: '会员福利',
name: 'itemVip',
icon: 'el-icon-s-management',
component: 'sys/item/itemVip',
path: '/itemVip',
},
{
title: '商品管理',
name: 'itemContro',
icon: 'el-icon-s-finance',
component: 'sys/item/itemContro',
path: '/itemContro',
}
]
},
{
title: '用户',
name: 'user',
icon: 'el-icon-s-custom',
component: '',
path: '',
children: [
{
title: '用户排行榜',
name: 'userRank',
icon: 'el-icon-s-check',
component: 'sys/user/userRank',
path: '/userRank',
},
{
title: '用户管理',
name: 'userContro',
icon: 'el-icon-s-finance',
component: 'sys/user/userContro',
path: '/userContro',
}
]
},
{
title: '订单',
name: 'order',
icon: 'el-icon-s-finance',
component: '',
path: '',
children: [
{
title: '订单查询',
name: 'orderList',
icon: 'el-icon-s-finance',
component: 'sys/orderList',
path: 'orderList',
}
]
}
]
let authoritys = []
Result.data = {
nav: nav,
authoritys: authoritys
}
return Result
})
// 即发送的是导航栏信息和用户权限信息
Step2:动态路由绑定
router–>index.js 中硬绑定的路由内容如下(注释内容):
注: 该文件的主要目的是当用户点击左侧导航栏条目时,小页面可以跳转到相应的自定义页面,由下面讨论的SideMenu.vue文件可知,用户点击左侧导航栏点击时点的是path路径,而当前文件的目的就是将path路径与文件夹下相应的自定义页面进行绑定(即path与component的绑定)
import Vue from 'vue'
import Router from 'vue-router'
// 这里先是引用全局axios而非自定义axios
import axios from "axios";
import store from "../store"
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Home',
component: () => import('../views/Home'),
children: [
{
path: '/userCenter',
name: 'UserCenter',
component: () => import('../views/user/userCenter')
},
{
path: '/index',
name: 'Index',
component: () => import('../views/Index')
},
// {
// path: '/itemContro',
// name: 'ItemContro',
// component: () => import('../views/sys/item/itemContro')
// },
// {
// path: '/itemList',
// name: 'ItemList',
// component: () => import('../views/sys/item/itemList')
// },
// {
// path: '/itemVip',
// name: 'ItemVip',
// component: () => import('../views/sys/item/itemVip')
// },
// {
// path: '/userContro',
// name: 'UserContro',
// component: () => import('../views/sys/user/userContro')
// },
// {
// path: '/userRank',
// name: 'UserRank',
// component: () => import('../views/sys/user/userRank')
// },
// {
// path: '/orderList',
// name: 'OrderList',
// component: () => import('../views/sys/orderList')
// }
]},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login')
},
{
path: '/mailReg',
name: 'MailReg',
component: () => import('../views/user/mailReg')
},
{
path: '/nickReg',
name: 'NickReg',
component: () => import('../views/user/nickReg')
}
]
})
在router --> index.js中增加如下方法:
// 在路由加载之前的判定(因为我希望不同角色的用户看到不同的导航栏)
router.beforeEach((to,from,next)=>{
// 这一步是为了判断是否之前已经加载了路由,避免重复加载,store中的menus中的hasRoute方法之后进行讨论
let hasRoute = store.state.menus.hasRoute
if ( !hasRoute ) {
axios.get("/menu/nav",{
headers: {
Authorization: localStorage.getItem("token") // 这里是因为我要集成jwt和SpirngSecurity,token信息在用户登录后必携带于请求头
}
}).then(res => {
console.log(res.data.data)
// 拿到menuList,存在store中。store是vuex的内容,用于管理全局的数据、更新数据,方便各组件进行调用和修改
store.commit("setMenuList",res.data.data.nav)
// 拿到用户权限(目前不做处理)
store.commit("setPermList",res.data.data.authoritys)
// 动态绑定路由
// 方法是新建一个newRoutes,之后加入到始祖路由router中
let newRoutes = router.options.routes
res.data.data.nav.forEach(menu => { // 见上方mock.js中发送过来的信息,menu是父条目,父条目没有对应的路由
if ( menu.children ) {
menu.children.forEach(e => { // 对于父条目中的每一个子条目
// 转成路由, 定义方法 menuToRoute,将子条目中的路由抽取出来形成真实的路由(主要是component转换如' @/views/sys/item/itemList.vue ')
// 意思是 src目录下的views目录下的sys目录下的item目录下的itemList组件
let route = menuToRoute(e)
// 把路由添加到路由管理中
if (route) {
newRoutes[0].children.push(route) // 获取的每一个route都会在始祖routes的第一个route的children中填充
}
})
}
})
console.log("newRoutes")
console.log(newRoutes)
router.addRoutes(newRoutes) // 始祖route中加入获取到的所有路由
hasRoute = true // 这个flag为了下次页面刷新时,不用重复进行动态路由绑定
store.commit("changeRouteStatus",hasRoute) // flag加到公共store的changeRouteStatus方法中
})
}
next() // 放行,始祖路由可以开始工作了
})
// 导航转成路由
const menuToRoute = (menu) => {
if ( !menu.component ) { // 像主条目没有route,这里就直接返回空值
return null
}
let route = { // 获取到了一个完整的route
name: menu.name,
path: menu.path,
meta: { // 这里是加载但在本文件下不需要的信息
icon: menu.icon,
title: menu.title
}
}
route.component = () => import('@/views/'+ menu.component +'.vue') // 形成真实的route中的component
return route
}
Step3:动态获取导航栏
在views==>inclu==>SideMenu.vue中的代码如下:
注:该文件的主要目的是展示左侧导航栏信息
<template>
<el-menu
default-active="2"
class="el-menu-vertical-demo"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b">
<router-link to="/index">
<el-menu-item index="1-1">
<template slot="title">
<i class="el-icon-s-home"></i>
<span slot="title">首页</span>
</template>
</el-menu-item>
</router-link>
<el-submenu :index="menu.name" v-for="menu in menuList">
<template slot="title">
<i :class="menu.icon"></i>
<span>{{ menu.title }}</span> <!-- 当前父条目的标题 -->
</template>
<router-link :to="thing.path" v-for="thing in menu.children"> <!-- 与router合作,点击时切换小页面 -->
<el-menu-item :index="thing.name"> <!-- 当前子条目的name,name的作用是点它时点亮 -->
<template slot="title">
<i :class="thing.icon"></i>
<span slot="title">{{ thing.title }}</span> <!-- 当前子条目的标题 -->
</template>
</el-menu-item>
</router-link>
</el-submenu>
</el-menu>
</template>
<script>
export default {
name: "SideMenu",
data() {
return {
}
},
computed: { // 为什么在computed获取menuList?
menuList: { // 因为computed中可以持续的获取当前页面中的menuList信息进行渲染,防止页面渲染完成后才获取到数据导致页面无法显示数据
get() {
return this.$store.state.menus.menuList // 接下来我们讨论store
}
}
},
methods: {
}
}
</script>
<style scoped>
.el-menu-vertical-demo {
height: 100%;
}
</style>
Step4:Vuex中的存储
在 store–>index.js中的代码如下:
import Vue from 'vue'
import Vuex from 'vuex'
import menus from "./modules/menus"
Vue.use(Vuex); // 配置内置vue注册vuex状态管理
const store = new Vuex.Store({
state: {
token: '' // 存储jwt令牌信息(本次不讨论)
},
mutations: {
SET_TOKEN: (state, token) => { // 设置jwt令牌信息的方法(本次不讨论)
state.token = token
localStorage.setItem("token",token)
},
resetState: (state) => { // 用户logout时清楚令牌信息(本次不讨论)
state.token = ''
}
},
actions:{
},
modules:{
menus // 主要看这里,加载了menus模块,该文件也import了modules文件夹下的menus
}
});
export default store;
在 store–>modules–>menus.js中的代码如下:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex); // 配置内置vue注册vuex状态管理
export default{
state: {
menuList: [], // menuList,由mock.js发到前端,在router->index.js中获取并存储(router->index.js也用它绑定动态路由了),在SideMenu使用
permList: [],
hasRoute: false // 是否已动态绑定路由的判决flag,在router->index.js中使用
},
mutations: {
setMenuList(state,menus) { // 获取menus,存储到当前的menuList中
state.menuList = menus
},
setPermList(state,perms) {
state.permList = perms
},
changeRouteStatus(state,hasRoute) { // 获取flag,存储到当前hasRoute中,
state.hasRoute = hasRoute // 此外 sessionStorage.setItem("hasRoute",hasRoute)防止页面刷新时动态绑定的路由丢失
sessionStorage.setItem("hasRoute",hasRoute)
}
},
actions:{
}
}
四、前端按钮权限细粒度管理
Step1: src目录下,定义文件globalFun.js实现全局皆可调用,代码如下:
import Vue from "vue"
Vue.mixin({
methods: {
hasAuth(perm) {
var authority = this.$store.state.menus.permList
return authority.indexOf(perm) > -1
}
}
})
Step2:在main.js中注册globalFun.js,如图3所示
Step3:使用方法
在itemVip.vue组件中添加如下v-if判断条件:
此时,前端界面如图5所示:
若在mock.js中添加如下权限编码段:
Mock.mock('/menu/nav','get',()=> {
let nav = [...]
let authoritys = ['sys:user:vipbutton','sys:user:adminbutton'] // 这里
Result.data = {
nav: nav,
authoritys: authoritys
}
return Result
})
此时,前端界面如图6所示: