根据登录角色的不同,用户拥有的菜单权限、按钮权限不同;
其中,超级管理员具有所有的菜单、按钮权限,其他角色不定。
通常后台系统包括的路由有:
index.ts
// 引入创建路由实例的方法,和创建 hash(createWebHashHistory) / history(createWebHistory) 模式的方法
import { createRouter, createWebHashHistory } from "vue-router"
import { constantRoute } from "./routes.ts"
let router = createRouter({
// 配置路由模式:hash
history: createWebHashHistory(),
// 配置路由
routes: constantRoute,
//路由切换时,页面滚动到顶部左侧
scrollBehavior() {
return {
top: 0,
left: 0
}
}
});
export default router;
routes.ts
//静态路由(所有用户都可以访问的路由:登录、首页、数据大屏、404)
export const constantRoute = [
{
path: "/login",
name: "Login",
component: () => import("@/views/login/index.vue"),
meta: {
title: "登录", // 菜单标题
hidden: true, //是否隐藏菜单
icon: "Notebook" //菜单图标
}
},
{
path: "/",
name: "Layout",
component: () => import("@/layout/index.vue"),
redirect: "/home",
meta: {
title: "",
icon: ""
},
children: [
{
path: "/home",
name: "Home",
component: () => import("@/views/home/index.vue"),
meta: {
title: "首页",
icon: "HomeFilled"
}
}
]
},
{
path: "/404",
name: "404",
component: () => import("@/views/404/index.vue"),
meta: {
title: "404",
hidden: true,
icon: "CirclePlusFilled"
}
},
{
path: "/screen",
name: "Screen",
component: () => import("@/views/screen/index.vue"),
meta: {
title: "数据大屏",
icon: "Platform"
}
},
]
// 异步路由(需要根据用户角色过滤的路由:权限管理、商品管理)
export const asyncRoute = [
{
path: "/acl",
name: "Acl",
component: () => import("@/layout/index.vue"),
meta: {
title: "权限管理",
icon: "Lock"
},
redirect: "/acl/user",
children: [
{
path: "/acl/user",
name: "User",
component: () => import("@/views/acl/user/index.vue"),
meta: {
title: "用户管理",
icon: "User"
}
},
{
path: "/acl/role",
name: "Role",
component: () => import("@/views/acl/role/index.vue"),
meta: {
title: "角色管理",
icon: "UserFilled"
}
},
{
path: "/acl/permission",
name: "Permission",
component: () => import("@/views/acl/permission/index.vue"),
meta: {
title: "菜单管理",
icon: "Grid"
}
}
]
},
{
path: "/product",
name: "Product",
component: () => import("@/layout/index.vue"),
meta: {
title: "商品管理",
icon: "Goods"
},
redirect: "/product/trademark",
children: [
{
path: "/product/trademark",
name: "Trademark",
component: () => import("@/views/product/trademark/index.vue"),
meta: {
title: "品牌管理",
icon: "ShoppingTrolley"
}
},
{
path: "/product/attr",
name: "Attr",
component: () => import("@/views/product/attr/index.vue"),
meta: {
title: "属性管理",
icon: "ChatLineSquare"
}
},
{
path: "/product/spu",
name: "Spu",
component: () => import("@/views/product/spu/index.vue"),
meta: {
title: "SPU管理",
icon: "CollectionTag"
}
},
{
path: "/product/sku",
name: "Sku",
component: () => import("@/views/product/sku/index.vue"),
meta: {
title: "SKU管理",
icon: "Shop"
}
}
]
}
]
// 任意路由(匹配不上静态路由和异步路由时,跳转到404)
export const anyRoute = [
{
// 路由都匹配不上时,匹配这个任意路由,重定向到 404
path: "/:pathMatch(.*)*",
redirect: "/404",
name: "Any",
meta: {
title: "其他路由",
hidden: true,
icon: "Open"
}
}
]
permission.ts
// 要在main.ts中引入该文件,在切换路由时才会运行
//引入路由全局守卫
// import "@/router/permission"
/**
* 路由里面需处理以下3个问题
* 1、任意路由切换实现进度条业务 --nprogress
* 2、路由鉴权(路由组件访问权限的设置)
* 全部路由组件:登录|404|任意路由|首页|数据大屏|权限管理(三个子路由)|商品管理(四个子路由)
* 路由鉴权份两种情况(用户是否登录,可以通过 token 判断):
* 1)用户未登录:可以访问登录路由(/login),其余路由不能访问
* 2)用户登录成功:除了登录路由,其余路由都可访问
*/
// 引入路由
import router from "@/router";
// 引入页面进度条方法及样式
import NProgress from "nprogress"
NProgress.configure({ showSpinner: false });
import 'nprogress/nprogress.css'
// 获取用户的store信息,里面包含 token、username
import useUserStore from "@/store/modules/user";
// 配置路由全局前置、后置守卫
router.beforeEach(async (to, from, next) => {
let userStore = useUserStore()
// to:要跳转到的页面
// from:从哪个页面来
// next:是否放行
NProgress.start();
let token = userStore.token;
let username = userStore.username;
if(token) {
//如果存在 token,表示用户已登录
//登录状态下,如果访问 login 页面,重定向到 home 页面
if(to.path === "/login") {
next({ path: "/home" });
} else {
// 登录状态下访问其他页面时
// 如果存在 username,那么路由正常跳转
if(username) {
next();
} else {
// 如果没有 usernam,那么先获取用户信息,成功后路由再继续跳转
try {
await userStore.userInfo()
// console.log(to)
// userStore.userInfo() 方法里面会进行异步路由处理,异步路由页面刷新时,
// 有可能获取到用户信息,异步路由还没加载完毕,可能会出现空白效果
// next 方法里面加 {...to} 是保证异步路由加载完成以后进行页面路由跳转
next({...to});
} catch(error) {
// 如果获取用户信息失败,那么退出登录,清除用户信息,跳转到登录页面
// 获取用户信息失败的原因:1、token 失效,2、用户手动修改本地 token
await userStore.logout();
next({ path: "/login" });
}
}
}
} else {
//如果没有token表示用户未登录,未登录状态只能访问 login 页面
if(to.path === "/login") {
next();
} else {
// 如果访问其他页面,因为没有token重定向了登录页面,标记 redirect 属性,登录成功以后,跳到原页面
next({ path: "/login", query: { redirect: to.path } })
}
}
})
router.afterEach((to, from, next) => {
NProgress.done();
})
// 引入路由
import { constantRoute, asyncRoute, anyRoute } from "@/router/routes"
import router from "@/router"
// 深复制
import { cloneDeep } from "lodash";
const filterAsyncRouter = (asyncRoute:any, routes:any ) => {
return asyncRoute.filter((item:any) => {
if(routes.includes(item.name)) {
if(item.children && item.children.length > 0) {
item.children = filterAsyncRouter(item.children, routes)
}
return true;
}
})
}
async userInfo() {
let result:userResponseData = await reqUserInfo();
if(result.code === 200) {
this.username = result.data.name;
this.avatar = result.data.avatar;
// 获取用户信息成功后,处理异步路由
let userAsyncRoute = filterAsyncRouter(cloneDeep(asyncRoute), result.data.routes);
this.menuRouters = [...constantRoute, ...userAsyncRoute, ...anyRoute];
[...userAsyncRoute, ...anyRoute].forEach((route) => {
router.addRoute(route)
})
// 获取注册过的所有路由
console.log(router.getRoutes())
return "ok"
} else {
return Promise.reject("获取用户信息失败!")
}
},
可能遇到的问题(假设异步路由有:1,2,3):
1、切换不同角色的用户如,从 A(拥有1、3) 切换到 B (拥有 1、2、3),因为 user.ts 在处理异步路由时,是用 item.children = filterAsyncRouter(item.children, routes) 会改变 asyncRoute ,会导致B没有 2 的权限,所以要用深复制 deepClone
2、用户登录时,如果要重定向到异步路由页面,可能会出现白屏,是因为登录成功获取用户信息后,异步路由可能还没加载完毕。所以跳转时要用 next({…to});
3、切换用户时可能会保留上一个用户的路由,所以要在退出登录时删除一下路由
buttons= ["Uer.add","Uer.edit"]
,存在缓存中main.js
import { createApp } from 'vue'
import App from '@/App.vue'
let app = createApp(App)
import { isHasButton } from "./directive/has"
isHasButton(app);
app.mount('#app')
directive/has
import useUserStore from "@/store/modules/user"
export const isHasButton = (app:any) => {
// 自定义指令,判断按钮是否有操作权限
app.directive('has', {
// 使用这个指令的dom在挂载时会执行一次
mounted(el:any,options:any) {
// 在所有权限按钮中匹配不到指定绑定的权限时,将元素删除
if(!useUserStore().buttons.includes(options.value)) {
// el.parentElement.removeChild(el)
el.remove()
}
},
})
}
index.vue
<el-button v-has="'btn.Spu.add'" type="primary" icon="Plus" :disabled="categoryStore.c3Id?false:true"
@click="addOrEditSpu('add')">添加SPUel-button>