路由权限概览
和原先vue2版本的流程相似
约定数据格式
和后端确认数据格式
{
children: [ // 二级路由
{
children: null, // 三级路由,children里有内容说明有下级,需要继续递归
icon: null, // 图标
id: 32, // 菜单id
isShow: 1, // 是否展示
name: "网站管理", // 菜单名字
open: null,
orderNum: 0, // 排序
parentId: 1, //
parentName: "权限管理",
perms: "",
type: 1, // 类型 0是目录,1是菜单
url: "/authority/subsite" // 菜单地址
}
],
icon: "system",
id: 1,
isShow: 1,
name: "权限管理",
open: null,
orderNum: 0,
parentId: 0,
parentName: null,
perms: null,
type: 0, // 类型 0是目录,1是菜单
url: "/authority" // 目录地址
}
复制代码
路由设计
路由搭配vuex,sessionStorage,NProgress食用
1. 加入progress,实现导航进度条效果
配置
// 简单配置
NProgress.inc(2);
NProgress.configure({ easing: 'ease', speed: 500, showSpinner: false }); // 动作
复制代码
使用
// 导航跳转开始
router.beforeEach(async (to, from, next) => {
NProgress.start();
})
// 导航跳转结束
router.afterEach((to, from) => {
NProgress.done();
});
复制代码
2. 配置router
1. 配置默认路由
登录页面肯定是默认加载的,所以放在默认路由里面,前端直接获取
有不需要权限的页面,都可以放在这
// 默认路由,登录页面肯定是不需要
export const routes = [
{
path: '/login',
name:'login',
component: () => import('@/views/login_page/index.vue'),
meta:{
title:'登录界面',
show:true
}
},
{
path: '/vuex',
name:'vuex',
component: () => import('@/views/vuex_page/index.vue'),
meta:{
title:'测试界面2',
show:true
}
},
];
复制代码
2. 权限页面控制
主要逻辑就和上面的流程图一样
router.beforeEach(async (to, from, next) => {
if (to.meta.title) {
document.title = to.meta.title
}
// 获取缓存的登录状态
let hasLogin = sessionStorage.getItem("token")
if (hasLogin) { // 有登录状态
console.log('state', to, from)
// 判断vuex中是否有存入路由数据
if (store.state.menuList.length === 0) {
// 进入到这一步用户已经登录过,但是又刷新了浏览器,导致路由清空了,所以要在vuex中重新请求路由
let res = await store.dispatch('getMenuList')
// code 不为200 时候,说明路由接口报错,需要重新登录
if (res !== 200) {
// 清除storage缓存
clearLoginInfo()
// 跳转登录页面
next({
path: '/login',
replace: true
})
// element报错提示
ElMessageBox.alert('菜单获取失败,请重新登录', '警告', {
confirmButtonText: '确定',
callback: () => {
}
})
} else { // 有路由数据
// 通过addRoute动态加入路由信息
store.state.menuList.map((item) => {
router.addRoute(item);
})
// router.addRoutes是异步的,使用 next({ ...to, replace: true })重新载入
next({...to, replace: true })
}
}
if (to.name === 'login') { // 已经登录过,但是访问的是login,强制跳转至首页
// 这里设置的是vuex中首个导航栏的path地址
next({
path: router.getRoutes()[0].path,
})
} else { // 正常跳转
if (to.matched.length === 0) { // 未匹配到路由, 强制首页
next({
path: router.getRoutes()[0].path,
})
// 导航状态自动更新到首个菜单
sessionStorage.setItem('activeIndex', router.getRoutes()[0].meta.menuId)
} else { // 正常跳转✔
next()
}
}
} else { // 没有登录,则强制跳转登录页
// 没有登录想访问其他页面,跳转至
if (to.name !== 'login') {
next({
path: '/login',
})
} else {
next()
}
}
});
复制代码
3. router页面整体配置
import { createRouter, createWebHistory } from 'vue-router'
import { ElMessageBox } from 'element-plus';
import NProgress from 'nprogress';
import store from '../store'
import { clearLoginInfo } from "../utils/common";
// 简单配置
NProgress.inc(2);
NProgress.configure({ easing: 'ease', speed: 500, showSpinner: false }); // 动作
// 默认路由
export const routes = [
{
path: '/login',
name:'login',
component: () => import('@/views/login_page/index.vue'),
meta:{
title:'登录界面',
show:true
}
},
{
path: '/vuex',
name:'vuex',
component: () => import('@/views/vuex_page/index.vue'),
meta:{
title:'测试界面2',
show:true
}
},
];
export const router = createRouter({
// 使用 hash 模式构建路由( url中带 # 号的那种)
// history: createWebHashHistory(),
// 使用 history 模式构建路由 ( url 中没有 # 号,但生产环境需要特殊配置)
history: createWebHistory(),
// history: createWebHashHistory(),
routes:routes
});
router.beforeEach(async (to, from, next) => {
NProgress.start();
if (to.meta.title) {
document.title = to.meta.title
}
// 获取缓存的登录状态
let hasLogin = sessionStorage.getItem("token")
// console.log('state', hasLogin, to, from)
if (hasLogin) { // 有登录状态
console.log('state', to, from)
// 判断vuex中是否有存入路由数据
if (store.state.menuList.length === 0) {
// 进入到这一步用户已经登录过,但是又刷新了浏览器,导致路由清空了,所以要在vuex中重新请求路由
let res = await store.dispatch('getMenuList')
// code 不为200 时候,说明路由接口报错,需要重新登录
if (res !== 200) {
clearLoginInfo()
next({
path: '/login',
replace: true
})
// 报错
ElMessageBox.alert('菜单获取失败,请重新登录', '警告', {
confirmButtonText: '确定',
callback: () => {
// clearLoginInfo()
// 跳转登录页
}
})
} else { // 有路由数据
store.state.menuList.map((item) => {
router.addRoute(item);
})
// router.addRoutes是异步的,使用 next({ ...to, replace: true })重新载入
next({...to, replace: true })
}
}
if (to.name === 'login') { // 已经登录过,但是访问的是login,强制跳转至首页
// 这里设置的是vuex中首个导航栏的path地址
next({
path: router.getRoutes()[0].path,
})
} else { // 正常跳转
if (to.matched.length === 0) { // 未匹配到路由, 强制首页
next({
path: router.getRoutes()[0].path,
})
sessionStorage.setItem('activeIndex', router.getRoutes()[0].meta.menuId)
} else {
next()
}
// console.log('333', router.getRoutes())
}
} else { // 没有登录,则强制跳转登录页
// 没有登录想访问其他页面,跳转至
if (to.name !== 'login') {
next({
path: '/login',
})
} else {
next()
}
}
});
router.afterEach((to, from) => {
NProgress.done();
console.log(to, from, 3333)
// 刷新后重新定向导航按钮展示
if (sessionStorage.getItem('activeIndex') !== to.meta.menuId) {
sessionStorage.setItem('activeIndex', to.meta.menuId)
}
});
export default router
复制代码
vuex配置
这里的逻辑是每次刷新或者跳转路由,都需要判断页面是否还有权限,菜单是否正常返回
import { createStore, createLogger } from 'vuex'
import HttpAxios from '../utils/httpTool'
const debug = process.env.NODE_ENV !== 'production'
const store = createStore({
state: {
count: 0,
collapse: false,
menuList: []
},
getters: {
menuList: state => state.menuList
},
mutations: {
setMenuList (state, value) {
// 存入数据
state.menuList = value;
}
},
actions: {
// 获取动态路由
async getMenuList({commit}) {
// 通过接口获取路由数据
let url='/sys/menu/nav';
let requestData = {};
let res = await HttpAxios.axiosGet(url, requestData); // 这里使用axios,晚点出一篇axios配置到原型链的文章
if (res.code === 200 && res.data.menus.length > 0) { // 成功
// 生成路由信息
let menus = needRoutes(res.data.menus)
,
commit('setMenuList', menus)
return res.code
} else { // 失败
return 500
}
}
},
modules: {
},
strict: debug,
plugins: debug ? [createLogger()] : []
});
// 生成路由数据
let needRoutes = (data) => {
// 判断是否是数组
if (!Array.isArray(data)) {
return new TypeError('arr must be an array.');
}
let arr = formatComponent(data)
return arr;
}
// 递归函数,用来对组件和路由关联,进行异步渲染
let formatComponent = (data) => {
data.map((obj) => {
if (obj.url && /\S/.test(obj.url)) {
if (obj.type === 1) { // 菜单逻辑
obj.url = obj.url.replace(/^\//, '')
const component = obj.url
// 把后台返回的路由参数,拼接路径,路由对应的就是页面地址
obj.component = () => import(`@/views/${component}/index.vue`)
// 通过正则来处理路由
obj.path = '/' + obj.url.replace('/', '-').match(/(\S*)-/)[1] + '/' + obj.url.replace('/', '-')
obj.title = obj.name
let name = obj.path.split('/')
obj.name = name[name.length-1]
obj.meta = {
menuId: obj.id.toString(),
title: obj.title,
isDynamic: true,
show: obj.isShow === 1 ? true: false,
isTab: true,
}
} else if (obj.type === 0) { // 目录逻辑
obj.component = () => import('@/views/home_page/index.vue')
obj.path = obj.url
obj.meta = {
menuId: obj.id.toString(),
title: obj.name,
isDynamic: true,
show: false,
isTab: true,
}
}
}
if (obj.children && obj.children.length > 0) { // children有长度,说明有子路由,进行递归
return formatComponent(obj.children)
}
})
return data
}
export default store
复制代码
路由实现效果
以我这边设计的数据格式
{
children: [ // 二级路由
{
children: null, // 三级路由,children里有内容说明有下级,需要继续递归
icon: null, // 图标
id: 32, // 菜单id
isShow: 1, // 是否展示
name: "网站管理", // 菜单名字
open: null,
orderNum: 0, // 排序
parentId: 1, //
parentName: "权限管理",
perms: "",
type: 1, // 类型 0是目录,1是菜单
url: "/authority/subsite" // 菜单地址
}
],
icon: "system",
id: 1,
isShow: 1,
name: "权限管理",
open: null,
orderNum: 0,
parentId: 0,
parentName: null,
perms: null,
type: 0, // 类型 0是目录,1是菜单
url: "/authority" // 目录地址
}
复制代码
当前有两层路由,那么二级路由是”/authority/subsite”
经过formatComponent函数处理之后,一级路由是/authority,二级路由就是/authority/authority-subsite
动态加入router之后
element-plus配置主页
1.page页面设置
为了方便前端页面构建,和配合路由设计
前端的页面结构是目录->菜单->子菜单,我这里暂时是二级菜单,没有涉及三级菜单
1. app.vue页面配置, 这里用了transition配置,和vue2还是有点不一样的
<template>
<div id="app">
<router-view v-slot="{ Component }">
<transition name="slide-fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</div>
</template>
复制代码
2. 设置一个主页
新建一个homepage页面,配置一下,我这里配置的首页就是element官方的导航栏样式
1.在el-main里面配置的就是路由切换后的内容页
2.nav-menu组件是左侧导航
3.nav-header组件是头部展示
复制代码
<template>
<div id="homePage">
<el-container style="height: 100%">
<el-aside height="100%" style="transition: all .3s" :style="{width:collapse?56+'px':256+'px'}">
<nav-menu></nav-menu>
</el-aside>
<el-container>
<el-header style="padding: 0;height: 120px">
<nav-header></nav-header>
</el-header>
<el-main style="padding: 20px;width: 100%;height: 100%;background: #EBEDF2">
<router-view v-slot="{ Component }">
<transition name="mode-fade" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</el-main>
</el-container>
</el-container>
</div>
</template>
复制代码
这里讲下nav-menu这个组件,这个组件需要用到一个子组件menu,menu这个组件是需要递归的
refRouter这个字段,就是存入vuex中的导航数据
<template>
<div id="navMenu">
<el-menu
router
class="el-menu-vertical-demo"
:collapse="collapse"
:default-active="activeIndex"
el-menu
background-color="#2A354E"
text-color="#fff"
active-text-color="#1890FF"
@select="handleSelect"
@open="handleOpen"
@close="handleClose"
>
<Menu :routerList="refRouter"></Menu>
</el-menu>
</div>
</template>
复制代码
menu组件, 这里的v-if判断就是对应之前数据格式定义,
// 1.当路由是目录的时候,如果有children,并且children有长度
<el-submenu v-if="item.children && item.children.length > 0" :key="item.path" :index="item.meta.menuId">
<template #title style="padding-left:10px" v-if="!item.meta.show">
<i class="el-icon-menu"></i>
<span>{{ item.meta.title}}</span>
</template>
<!-- 如果有子级数据使用递归组件 -->
<Menu :routerList="item.children"></Menu>
</el-submenu>
复制代码
// 路由是菜单,show这个字段就是用来判断是否展示菜单
<el-menu-item v-if="!item.children && item.meta.show" :route="item.path" :index="item.meta.menuId" :key="item.path">
<i class="el-icon-menu"></i>
<span>{{item.meta.title}}</span>
</el-menu-item>
复制代码
整个页面如下
<template>
<template v-for="item in routerList">
<el-submenu v-if="item.children && item.children.length > 0" :key="item.path" :index="item.meta.menuId">
<template #title style="padding-left:10px" v-if="!item.meta.show">
<i class="el-icon-menu"></i>
<span>{{ item.meta.title}}</span>
</template>
<!-- 如果有子级数据使用递归组件 -->
<Menu :routerList="item.children"></Menu>
</el-submenu>
<el-menu-item v-if="!item.children && item.meta.show" :route="item.path" :index="item.meta.menuId" :key="item.path">
<i class="el-icon-menu"></i>
<span>{{item.meta.title}}</span>
</el-menu-item>
</template>
</template>
复制代码
目录结构如下
功能权限概览
考虑到需要精细到页面的权限控制,所以会在页面进入时,请求接口的时候做一个判断
1.权限主要是涉及数据的增删改
2.控制方式第一层,通过后台给的权限标识,控制按钮的展示
3.控制方式第二层,后台改写逻辑,没有分配权限的请求,直接过滤
复制代码
权限的标识,是在获取菜单的时候一起获取的,获取之后直接存入缓存,这里可以直接看之前vuex的一步
// 缓存权限配置到storage
sessionStorage.setItem('permissions', JSON.stringify(res.data.authorities))
复制代码
权限标识可以根据实际业务来,进行涉及,需要做的一个逻辑就是在按钮处进行判断
我这里直接写一个公共函数,用来判断是否有权限标识,如果有则返回true,没有则返回false
export function isAuth (key) {
return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
}
复制代码
那我在页面中使用的时候,可以直接加入这个条件, 我这里是直接加入原型链,在main.js 中直接引入
这里说下,vue3的原型链使用方式和vue2是不一样的
// 权限控制
import {isAuth} from "./utils/common";
app.config.globalProperties.isAuth = isAuth
复制代码
最后在页面就可以直接用了
<el-button v-if="isAuth('sys:web:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button>
复制代码