一、前言
不管你是开发一款APP或者是微信小程序等其他的应用程序,都需要搭配一个后台管理系统,那么在后台管理系统中有一个比较重要的模块那就是用户权限管理
了,接下来我们来实现这个功能。
二、技术栈
前端:前端我用的是VUE3
、Element-Plus
、Vite
、Pinia
等,在这里就不一一列出来了
后端:node.js
、接口我是用node写的(不出意外的话,讲不到后端的接口)
数据库:MySQL
介绍就到这里吧!
三、数据库
首先是我们的数据库,我会尽量写的详细。
1、用户表
我这里就只列出重要的字段了
可以根据自己的需要扩展其他字段如:手机号、性别、邮箱、创建时间等等
字段名 | 类型 | 必填 | 注释 |
---|---|---|---|
id | int | 是 | 用户ID |
username | varchar | 是 | 用户名称 |
password | varchar | 是 | 用户密码 |
roleIds | varchar | 否 | 角色ID(可多选) |
isFullFunc | int | 是 | 是否全功能(1:是、2:否)默认:2 |
2、角色表
字段名 | 类型 | 必填 | 注释 |
---|---|---|---|
id | int | 是 | 角色ID |
roleName | varchar | 是 | 角色名称 |
roleRemark | varchar | 是 | 角色备注 |
menus | varchar | 否 | 菜单ID |
menuPowers | varchar | 否 | 菜单权限ID |
roleStatus | int | 是 | 角色状态(1:开启、2:禁用)默认:1 |
3、菜单表
字段名 | 类型 | 必填 | 注释 |
---|---|---|---|
id | int | 是 | 菜单ID |
menuName | varchar | 是 | 菜单名称 |
menuPath | varchar | 是 | 菜单路径 |
menuType | int | 是 | 菜单类型(1:目录、2:页面) |
menuStatus | int | 是 | 菜单类型(1:开启、2:禁用)默认:1 |
parentId | int | 否 | 上级菜单ID |
parentName | varchar | 否 | 上级菜单名称 |
4、菜单权限表
字段名 | 类型 | 必填 | 注释 |
---|---|---|---|
id | int | 是 | 菜单权限ID |
menuPowerName | varchar | 是 | 菜单权限名称 |
menuPowerMark | varchar | 是 | 菜单权限标识 |
menuId | int | 是 | 所属菜单 |
menuName | int | 是 | 所属菜单名称 |
流程图
图片有点潦草了
大致就是角色
授权我们的菜单
和按钮
的权限,然后用户可以绑定多个角色
,嗯…对就是这样。
前端代码
首先是四个表单的增删改查,这些代码我就不贴出来了,没用。
核心代码
在我们项目中的permission.ts
也就是你写路由全局守卫的地方
import router from '@/router';
import usePermissionStore from '@/store/modules/permission';
import {
useUserStoreHook } from '@/store/modules/user'; // 注意自己路径
const whiteList = ['/login']; // 白名单路由
const userStore = useUserStoreHook()
const permissionStore = usePermissionStore()
// 路由全局守卫
router.beforeEach(async (to, from, next) => {
NProgress.start();// 这个是页面加载时进度条
if (Cookies.get('Token')) {
// 登录成功,跳转到首页
if (to.path === '/login') {
next({
path: '/' }); // 首页路径
NProgress.done();
}else{
// 确定用户是否已通过getInfo获得其权限角色
const hasGetUserInfo = userStore.roles.length > 0;
if (hasGetUserInfo) {
if (to.matched.length === 0) {
from.name ? next({
name: from.name as any }) : next('/401');
} else {
next();
}
} else {
try {
let roles: any = [];
roles = await userStore.getUserInfo() // 返回的是用户授权的菜单列表
const accessRoutes: any = await permissionStore.generateRoutes(roles); // 通过用户授权的菜单列表过滤路由
accessRoutes.forEach((route: any) => {
router.addRoute(route); // 添加路由
});
next();
} catch (error) {
// 移除 token 并跳转登录页
await removeToken(); // 这个方法我就不贴出来了,就是移除你登录时存的Token
next(`/login`); // 跳转登陆页
NProgress.done();
}
}
}
}else{
// 未登录时可以访问白名单页面
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
// 否则跳转登陆页面
next(`/login`); // 跳转登陆页
NProgress.done();
}
}
});
router.afterEach(() => {
NProgress.done();
});
store/index.ts
import type {
App } from 'vue';
import {
createPinia } from "pinia";
const store = createPinia();
// 全局挂载store
export function setupStore(app: App<Element>) {
app.use(store);
}
export {
store };
// 需要在main.ts挂载
// main.ts 直接写这里了
import {
setupStore } from '@/store';
const app = createApp(App);
setupStore(app);
store/modules/types.ts
import {
RouteLocationNormalized, RouteRecordRaw } from 'vue-router';
export interface PermissionState {
routes: RouteRecordRaw[];
addRoutes: RouteRecordRaw[];
}
export interface UserState {
roles: string[];
power: string[];
isFullFunc: Number;
}
store/modules/user.ts
import {
defineStore } from 'pinia';
import {
UserState } from './types';
import {
store } from '@/store';
const useUserStore = defineStore({
id:'user',
state: (): UserState => ({
roles: [],
power: [],
isFullFunc: 2
}),
actions:{
getUserInfo() {
return new Promise((resolve, reject) => {
userPower().then(res => {
// 请求接口
if (res.data.isFullFunc == 1) {
// 等等 1 代表用户全功能
this.isFullFunc = res.data.isFullFunc
resolve(['admin']) // 随便返回个什么,不是空就行
} else {
this.roles = res.data.menuData // 菜单权限
this.power = res.data.menuPowerData // 菜单按钮权限
resolve(res.data.menuData) // 返回菜单权限
}
}).catch(res => {
reject(res)
})
});
},
}
})
export default useUserStore;
// 非setup
export function useUserStoreHook() {
return useUserStore(store);
}
// 这里是接口返回的数据格式
/**
res:{
data:{
isFullFunc:1,
menuData:[
{
{
"id": 1,
"menuName": "首页",
"menuPath": "/",
"menuType": 2,
"menuStatus": 1,
"parentId": 0,
"parentName": null,
"createDate": 1682435267868
}
...等等
}
],
menuPowerData:[
{
{
"id": 1,
"menuPowerName": "用户管理 - 新增",
"menuPowerMark": "USER-ADD",
"menuId": 3,
"menuName": "用户管理",
"createDate": 1682472332893
}
...等等
}
]
}
}
*/
store/modules/permission.ts
// 路由处理的逻辑写的不是很好,后续优化了会更新
import {
PermissionState } from './types';
import {
RouteRecordRaw } from 'vue-router';
import {
defineStore } from 'pinia';
import {
store } from '@/store';
import {
constantRoutes } from '@/router';
const filterAsyncRoutes = (routes: RouteRecordRaw[], roles: string[]) => {
// routes 所有路由
// roles 授权的路由
const res: RouteRecordRaw[] = [];
if (roles[0] == 'admin') {
// 表示全功能,添加所有的路由
routes.forEach(route => {
const tmp = {
...route } as any;
res.push(tmp);
});
} else {
routes.forEach((route: any) => {
// 添加不存在在菜单权限中的路由
const tmp = {
...route } as any;
if (tmp.path == '/redirect' || tmp.path == '/login' || tmp.path == '/') {
res.push(tmp)
}
roles.forEach((item: any) => {
if (item.menuPath != '/') {
if (route.path === item.menuPath) {
// 一级路由
let data = {
path: route.path,
component: route.component,
redirect: route.redirect,
meta: route.meta,
children: [],
}
// 子路由
route.children.forEach((tab: any) => {
roles.forEach((item2: any) => {
if (item2.menuPath === tab.path) {
data.children.push(tab)
}
})
})
res.push(data)
}
}
})
});
}
return res;
};
const usePermissionStore = defineStore({
id: 'permission',
state: (): PermissionState => ({
routes: [],
addRoutes: [],
}),
actions: {
setRoutes(routes: RouteRecordRaw[]) {
this.addRoutes = routes;
this.routes = routes;
},
generateRoutes(roles: string[]) {
return new Promise((resolve, reject) => {
if (roles[0] == 'admin') {
const asyncRoutes = constantRoutes;
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
this.setRoutes(accessedRoutes);
resolve(accessedRoutes);
} else {
const asyncRoutes = constantRoutes;
const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
this.setRoutes(accessedRoutes);
resolve(accessedRoutes);
}
});
},
},
});
export default usePermissionStore;
// 非setup
export function usePermissionStoreHook() {
return usePermissionStore(store);
}
到这里我们项目的菜单权限就实现了。
接下来实现我们的按钮权限
创建一个directives.ts
文件
import type {
App } from "vue";
import {
useUserStoreHook } from '@/store/modules/user';
const userStore = useUserStoreHook()
// 判断单个按钮权限
const hasPermission = (userPermission: any) => {
let permissionList = userStore.$state.power // 获取权限时存的菜单按钮的权限
return permissionList.some((i: any) => i.menuPowerMark == userPermission)
};
export function setAllDirectives(app: App) {
// 注册自定义指令
app.directive('has', {
mounted(el, binding, vnode, prevVnode) {
if (userStore.$state.isFullFunc == 1) {
return true
} else {
if (!hasPermission(binding.value)) {
el.parentNode.removeChild(el); // 无权限时删除dom元素
}
}
},
})
}
// 同样需要在main.ts中引入
// main.ts
import {
setAllDirectives } from '@/utils/directives' // 导入全局自定义指令
setAllDirectives(app) // 全局注册自定义指令
自定义指令的使用
<el-button type="primary" @click="roleAdd" v-has="'ROLE-ADD'">新增</el-button>
<!-- v-has 直接传我们的按钮标识就好咯 -->
表单就是这样子的。
结尾!
代码仅提供逻辑,直接搬代码可能会有问题哦。
谢谢观看~