Vue3.5 企业级管理系统实战(二十一):菜单权限
有了菜单及角色管理后,我们还需要根据用户访问的token,去获取用户信息,根据用户的角色信息,拉取所有的菜单权限,进而生成左侧菜单树数据。
1 增加获取用户信息 api
在 src/api/user.ts 中,添加获取用户信息的 api(getUserInfo),代码如下:
//src/api/user.ts
import request from "@/api/config/request";
// 从 "./type" 模块中导入 ApiResponse 类型,用于定义接口响应数据的结构
import type { ApiResponse } from "./type";
import type { IRole } from "./role";/*** 定义用户登录所需的数据结构* @interface IUserLoginData* @property {string} username - 用户登录使用的用户名* @property {string} password - 用户登录使用的密码*/
export interface IUserLoginData {username: string;password: string;
}/*** 定义登录接口响应的数据结构* @interface ILoginResponseData* @property {string} token - 登录成功后返回的令牌,用于后续请求的身份验证*/
export interface ILoginResponseData {token: string;
}/*** 登录接口* @param {IUserLoginData} data - 用户登录所需的数据,包含用户名和密码* @returns {Promise<ApiResponse<ILoginResponseData>>} - 返回一个 Promise 对象,该对象解析为包含登录响应数据的 ApiResponse 类型*/
export const login = (data: IUserLoginData
): Promise<ApiResponse<ILoginResponseData>> => {return request.post("/4642164-4292760-default/287017559", data);
};
//test
export const logout_error = (): Promise<ApiResponse<ILoginResponseData>> => {return request.post("/4642164-4292760-default/auth/401");
};//个人中心接口
export interface Profile {id: number;username: string;email: string;mobile: string;isSuper: boolean;status: boolean;avatar: string;description: string;roles: IRole[];roleIds?: number[]; // 修改用户的时候,后端接受只要id
}export interface IUsers {users: Profile[];count: number;
}// 查询参数
export interface IUserQuery {pageNum?: number;pageSize?: number;mobile?: string;status?: boolean;username?: string;flag: number;
}// 获取用户列表的接口
export const getUsers = (params: IUserQuery): Promise<ApiResponse<IUsers>> => {const {pageNum = 0,pageSize = 10,username = "",status,mobile = "",flag} = params;return request.get("http://127.0.0.1:4523/m1/4642164-4292760-default/user", {params: {pageNum,pageSize,username,status,mobile,flag}});
};// 删除用户
export const removeUser = (id: number): Promise<ApiResponse> => {return request.delete(`http://127.0.0.1:4523/m1/4642164-4292760-default/user/`);
};// 添加用户
export const addUser = (data: Profile): Promise<ApiResponse> => {return request.post("http://127.0.0.1:4523/m1/4642164-4292760-default/user",data);
};// 编辑用户
export const updateUser = (id: number, data: Profile): Promise<ApiResponse> => {return request.put(`http://127.0.0.1:4523/m1/4642164-4292760-default/user`,data);
};// 获取用户信息
export const getUserInfo = (): Promise<ApiResponse<Profile>> => {return request.post("/auth/info");
};
2 修改用户 Store
在 src/stores/user.ts 中,增加获取当前用户信息的方法 getUserInfo,代码如下:
//src/stores/user.ts
import type { IUserLoginData, IUserQuery, IUsers, Profile } from "@/api/user";
import {login as loginApi,getUsers as getUsersApi, // 获取用户addUser as addUserApi,removeUser as removeUserApi,updateUser as updateUserApi,getUserInfo as getUserInfoApi
} from "@/api/user";
import { setToken, removeToken } from "@/utils/auth";
import { useTagsView } from "./tagsView";
import type { IRole } from "@/api/role";/*** 用户信息查询参数类型,继承自Profile并添加分页参数*/
export type IProfileQuery = Profile & {pageNum?: number;pageSize?: number;
};/*** 用户状态管理*/
export const useUserStore = defineStore("user", () => {// 状态管理const state = reactive({token: "", // 用户令牌users: [] as IUsers["users"], // 用户列表count: 0, // 用户总数roles: [] as IRole[], // 用户角色列表userInfo: {} as Profile // 当前用户信息});// 引用标签视图模块const tagsViewStore = useTagsView();/*** 获取当前用户信息*/const getUserInfo = async () => {const res = await getUserInfoApi();if (res.code === 0) {// 解构响应数据,分离角色和用户信息const { roles, ...info } = res.data;state.roles = roles; // 存储角色信息state.userInfo = info as Profile; // 存储用户信息}};/*** 用户登录* @param userInfo - 包含用户名和密码的登录信息*/const login = async (userInfo: IUserLoginData) => {try {const { username, password } = userInfo;// 调用登录API,用户名去除首尾空格const response = await loginApi({ username: username.trim(), password });const { data } = response;state.token = data.token; // 存储令牌setToken(data.token); // 保存令牌到本地存储} catch (e) {return Promise.reject(e); // 登录失败时返回错误}};/*** 用户注销*/const logout = () => {state.token = ""; // 清空令牌removeToken(); // 移除本地存储的令牌tagsViewStore.delAllView(); // 清除所有标签视图};/*** 获取全部用户列表* @param params - 查询参数,包含分页和筛选条件*/const getAllUsers = async (params: IUserQuery) => {const res = await getUsersApi(params);const { data } = res;state.users = data.users; // 更新用户列表state.count = data.count; // 更新用户总数};/*** 添加新用户* @param data - 用户信息,包含分页参数(用于添加成功后刷新列表)*/const addUser = async (data: IProfileQuery) => {// 分离分页参数和用户信息const { pageSize, pageNum, ...params } = data;const res = await addUserApi(params);if (res.code === 0) {// 添加成功后刷新用户列表getAllUsers({pageSize,pageNum});}};/*** 删除用户* @param data - 包含用户ID和分页信息(用于删除成功后刷新列表)*/const removeUser = async (data: IProfileQuery) => {const { pageSize, pageNum, id } = data;const res = await removeUserApi(id);if (res.code === 0) {// 删除成功后刷新用户列表getAllUsers({pageSize,pageNum});}};/*** 编辑用户信息* @param data - 用户信息,包含分页参数(用于编辑成功后刷新列表)*/const editUser = async (data: IProfileQuery) => {// 分离分页参数和用户信息const { pageSize, pageNum, ...params } = data;const res = await updateUserApi(params.id, params);if (res.code === 0) {// 编辑成功后刷新用户列表getAllUsers({pageSize,pageNum});}};// 导出可访问的方法和状态return {login,state,logout,getAllUsers,editUser,removeUser,addUser,getUserInfo};
});
3 新增 permission Store
新增 src/stores/permission.ts,代码如下:
//src/stores/permission.ts
import type { RouteRecordRaw } from "vue-router";
import { useUserStore } from "./user";
import { asyncRoutes } from "@/router";
import { useMenuStore } from "./menu";
import type { MenuData } from "@/api/menu";
import path from "path-browserify";/*** 递归生成路由配置* @param routes - 原始路由配置* @param routesPath - 需要保留的路由路径数组* @param basePath - 基础路径,用于解析子路由* @returns 过滤后的路由配置*/
function generateRoutes(routes: RouteRecordRaw[],routesPath: string[],basePath = "/"
) {const routerData: RouteRecordRaw[] = [];routes.forEach((route) => {// 解析当前路由的完整路径const routePath = path.resolve(basePath, route.path);// 递归处理子路由if (route.children) {route.children = generateRoutes(route.children, routesPath, routePath);}// 路由过滤条件:// 1. 当前路由路径在允许列表中// 2. 或者当前路由有子路由if (routesPath.includes(routePath) ||(route.children && route.children.length >= 1)) {routerData.push(route);}});return routerData;
}/*** 根据菜单数据过滤异步路由* @param menus - 菜单数据* @param routes - 原始异步路由配置* @returns 过滤后的路由配置*/
function filterAsyncRoutes(menus: MenuData[], routes: RouteRecordRaw[]) {// 提取菜单中的路径信息const routesPath = menus.map((item) => item.path);// 调用递归生成函数return generateRoutes(routes, routesPath);
}/*** 权限管理模块*/
export const usePermissionStore = defineStore("permission", () => {const userStore = useUserStore();const menuStore = useMenuStore();// 存储最终生成的可访问路由let accessMenuRoutes: RouteRecordRaw[] = [];/*** 生成可访问的路由配置* @returns 基于用户角色的可访问路由配置*/const generateRoutes = async () => {// 计算属性:获取用户角色名称列表const rolesNames = computed(() =>userStore.state.roles.map((item) => item.name));// 计算属性:获取用户角色ID列表const roleIds = computed(() =>userStore.state.roles.map((item) => item.id));// 超级管理员角色处理if (rolesNames.value.includes("super_admin")) {// 超级管理员拥有全部路由访问权限accessMenuRoutes = asyncRoutes;// 获取全部菜单列表await menuStore.getAllMenuListByAdmin();return accessMenuRoutes;} else {// 普通角色处理:根据角色ID获取对应菜单await menuStore.getMenuListByRoles(roleIds.value);// 获取当前用户权限下的菜单列表const menus = menuStore.state.authMenuList;// 根据菜单权限过滤路由accessMenuRoutes = filterAsyncRoutes(menus, asyncRoutes);return accessMenuRoutes;}};return {generateRoutes};
});
4 修改 permission.ts
修改 src/permission.ts,代码如下:
//src/permission.ts
// 路由鉴权配置 - 控制用户访问权限和页面导航逻辑
import router from "@/router";
import NProgress from "nprogress"; // 进度条插件
import "nprogress/nprogress.css"; // 进度条样式
import { getToken } from "./utils/auth"; // 获取token工具函数
import { useUserStore } from "./stores/user"; // 用户状态管理
import { usePermissionStore } from "./stores/permission"; // 权限状态管理// 配置进度条选项,不显示旋转加载图标
NProgress.configure({ showSpinner: false });// 白名单路由 - 无需登录即可访问的页面
const whiteList = ["/login"];/*** 全局前置守卫 - 路由切换前的权限校验* 1. 检查Token有效性* 2. 判断用户权限* 3. 动态生成路由*/
router.beforeEach(async (to, from) => {// 开始进度条NProgress.start();// 获取本地Tokenconst hasToken = getToken();const userStore = useUserStore();const permissionStore = usePermissionStore();// 情况1:已登录状态if (hasToken) {// 已登录但访问登录页,重定向到首页if (to.path === "/login") {NProgress.done(); // 结束进度条return {path: "/",replace: true // 替换历史记录,禁止回退};} else {// 已登录且访问非登录页,校验用户权限(有可能token是伪造的,无效的)try {// 检查是否已有用户角色信息const hasRoles = userStore.state.roles.length > 0;// 已有角色信息,直接放行if (hasRoles) {NProgress.done();return true;}// 没有角色信息,重新获取用户信息await userStore.getUserInfo();// 根据用户角色动态生成可访问的路由配置const routes = await permissionStore.generateRoutes();// 动态添加路由到路由器routes.forEach((route) => router.addRoute(route));// 确保路由添加完成,重新访问目标路径return router.push({ path: to.path, replace: true });} catch (error) {// 获取用户信息失败,可能Token过期或无效console.error("获取用户信息失败:", error);// 清除用户状态并跳转到登录页userStore.logout();NProgress.done();// 携带当前路径作为重定向参数return `/login?redirect=${to.path}`;}}}// 情况2:未登录状态else {// 访问白名单页面,直接放行if (whiteList.includes(to.path)) {NProgress.done();return true;}// 非白名单页面,重定向到登录页并记录原始路径return {path: "/login",query: {redirect: to.path,...to.query // 保留原路径的查询参数}};}
});
5 修改路由配置
修改 src/router/index.ts,代码如下:
//src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import Layout from "@/layout/index.vue";
export const constantRoutes: RouteRecordRaw[] = [{path: "/",component: Layout,redirect: "/dashboard",children: [{path: "dashboard",name: "dashboard",component: () => import("@/views/dashboard/index.vue"),meta: {icon: "ant-design:bank-outlined",title: "dashboard",affix: true, // 固定在tagsViews中noCache: true // 不需要缓存}}]},{path: "/redirect",component: Layout,meta: {hidden: true},// 当跳转到 /redirect/a/b/c/d?query=1children: [{path: "/redirect/:path(.*)",component: () => import("@/views/redirect/index.vue")}]},{path: "/login",name: "Login",meta: {hidden: true},component: () => import("@/views/login/index.vue")}
];
export const asyncRoutes: RouteRecordRaw[] = [{path: "/documentation",component: Layout,redirect: "/documentation/index",children: [{path: "index",name: "documentation",component: () => import("@/views/documentation/index.vue"),meta: {icon: "ant-design:database-filled",title: "documentation"}}]},{path: "/guide",component: Layout,redirect: "/guide/index",children: [{path: "index",name: "guide",component: () => import("@/views/guide/index.vue"),meta: {icon: "ant-design:car-twotone",title: "guide"}}]},{path: "/system",component: Layout,redirect: "/system/menu",meta: {icon: "ant-design:unlock-filled",title: "system",alwaysShow: true// breadcrumb: false// 作为父文件夹一直显示},children: [{path: "menu",name: "menu",component: () => import("@/views/system/menu/index.vue"),meta: {icon: "ant-design:unlock-filled",title: "menu"}},{path: "role",name: "role",component: () => import("@/views/system/role/index.vue"),meta: {icon: "ant-design:unlock-filled",title: "role"}},{path: "user",name: "user",component: () => import("@/views/system/user/index.vue"),meta: {icon: "ant-design:unlock-filled",title: "user"}}]},{path: "/external-link",component: Layout,children: [{path: "http://www.baidu.com",redirect: "/",meta: {icon: "ant-design:link-outlined",title: "link Baidu"}}]}
];
// 需要根据用户赋予的权限来动态添加异步路由
export const routes = [...constantRoutes];
export default createRouter({routes, // 路由表history: createWebHistory() // 路由模式
});
以上就是菜单权限相关内容。
下一篇将继续探讨 动态菜单的实现,敬请期待~
相关文章:
Vue3.5 企业级管理系统实战(二十一):菜单权限
有了菜单及角色管理后,我们还需要根据用户访问的token,去获取用户信息,根据用户的角色信息,拉取所有的菜单权限,进而生成左侧菜单树数据。 1 增加获取用户信息 api 在 src/api/user.ts 中,添加获取用户信…...
kafka幂等生产者和事务生产者区别
#作者:张桐瑞 文章目录 消息交付可靠性保障什么是幂等性(Idempotence)?幂等性Producer事务事务型Producer 消息交付可靠性保障 所谓的消息交付可靠性保障,是指Kafka对Producer和Consumer要处理的消息提供什么样的承诺…...

【HarmonyOS Next之旅】DevEco Studio使用指南(二十九) -> 开发云数据库
目录 1 -> 开发流程 2 -> 创建对象类型 3 -> 添加数据条目 3.1 -> 手动创建数据条目文件 3.2 -> 自动生成数据条目文件 4 -> 部署云数据库 1 -> 开发流程 云数据库是一款端云协同的数据库产品,提供端云数据的协同管理、统一的数据模型和…...

批量导出CAD属性块信息生成到excel——CAD C#二次开发(插件实现)
本插件可实现批量导出文件夹内大量dwg文件的指定块名的属性信息到excel,效果如下: 插件界面: dll插件如下: 使用方法: 1、获取此dll插件。 2、cad命令行输入netload ,加载此dll(要求AutoCAD&…...
可视化大屏如何制作
超详细!手把手教你制作可视化大屏 在当今数字化时代,数据犹如一座蕴藏无尽价值的宝藏,而可视化大屏则是开启这座宝藏大门、让数据价值得以充分展现的关键钥匙。无论是企业运营监控、数据分析展示,还是项目成果汇报,可视…...

Goreplay最新版本的安装和简单使用
一:概述 Gor 是一个开源工具,用于捕获实时 HTTP 流量并将其重放到测试环境中,以便使用真实数据持续测试您的系统。它可用于提高对代码部署、配置更改和基础设施更改的信心。简单易用。 项目地址:buger/goreplay: GoReplay is an …...

Android Studio 解决报错 not support JCEF 记录
问题:Android Studio 安装Markdown插件后,报错not support JCEF不能预览markdown文件。 原因:Android Studio不是新装,之前没留意IDE自带的版本是不支持JCEF的。 解决办法: 在菜单栏选中Help→Find Actionÿ…...
SMT高速贴片机核心技术深度剖析
内容概要 在智能制造升级背景下,SMT高速贴片机的性能直接影响电子产品的生产效率和可靠性。本文将从微米级贴装精度的实现机制出发,探讨高速运动控制与精准定位的协同优化方案,同时分析视觉系统在多类型元件识别中的动态补偿策略。针对消费电…...

sigmastar实现SD卡升级
参考文章:http://wx.comake.online/doc/DD22dk2f3zx-SSD21X-SSD22X/customer/development/software/Px/zh/sys/P3/usb%20&%20sd%20update.html#21-sd 1、构建SD卡升级包 在project下make image完成后使用make_sd_upgrade_sigmastar.sh脚本打包SD卡升级包。 ./make_sd_up…...

kafka学习笔记(三、消费者Consumer使用教程——配置参数大全及性能调优)
本章主要介绍kafka consumer的配置参数及性能调优的点,其kafka的从零开始的安装到生产者,消费者的详解介绍、源码及分析及原理解析请到博主kafka专栏 。 1.消费者Consumer配置参数 配置参数默认值含义bootstrap.servers无(必填)…...
yarn、pnpm、npm
非常好,这样从“问题驱动 → 工具诞生 → 优化演进”的角度来讲,更清晰易懂。下面我按时间线和动机,把 npm → yarn → pnpm 的演变脉络讲清楚。 🧩 一、npm 为什么一开始不够好? 早期(npm v4 及之前&…...
JVM——Truffle:语言实现框架
引入 在编程语言的实现领域,传统的编译器和解释器设计往往面临着复杂性和性能优化的双重挑战。尤其是对于动态语言,解释器的效率问题一直是一个难以突破的瓶颈。而 Truffle 框架的出现,为这一难题提供了全新的解决方案。Truffle 是一个高性能…...
C++ STL vector容器详解:从原理到实践
引言 亲爱的小伙伴们,今天我要和大家分享一个C编程中的"神器"——vector容器!作为STL(标准模板库)中最常用的容器之一,vector就像是一个"超级数组",既有数组的高效随机访问特性&#…...
视频压制(Video Encoding/Compression)
视频压制(Video Encoding/Compression) 视频压制是指通过特定的算法和技术,将原始视频文件转换为更小体积或更适合传播的格式的过程。其核心目的是在尽量保持画质的前提下,减少视频的文件大小,或适配不同播放设备、网络环境的需求…...

【论文笔记】Transcoders Find Interpretable LLM Feature Circuits
Abstract 机制可解释性(mechanistic interpretability)的核心目标是路径分析(circuit analysis):在模型中找出与特定行为或能力对应的稀疏子图。 然而,MLP 子层使得在基于 Transformer 的语言模型中进行细粒度的路径分析变得困难。具体而言,…...
音视频融合中的语音分离技术实现
音视频融合中的语音分离技术实现 一、任务概述 语音分离是音频信号处理的核心任务,旨在从混合音频中分离出目标语音。音视频融合技术通过结合视觉信息(如嘴唇运动)显著提升分离效果。本方案将实现一个基于深度学习的音视频融合语音分离系统。 二、系统架构 #mermaid-svg-3…...

每天总结一个html标签——a标签
文章目录 一、定义与使用说明二、支持的属性三、支持的事件四、默认样式五、常见用法1. 文本链接2. 图片链接3. 导航栏 在前端开发中,a标签(锚点标签)是最常用的HTML标签之一,主要用于创建超链接,实现页面间的跳转或下…...
在Babylon.js中创建3D文字:简单而强大的方法
引言 在3D场景中添加文字是许多WebGL项目的常见需求。Babylon.js提供了多种创建3D文字的方法,其中使用TextBlock结合平面网格是一种简单而高效的方式。本文将介绍如何使用Babylon.js的GUI系统在3D空间中创建美观的文字效果。 方法概述 Babylon.js的GUI系统允许我…...
CSS 渐变完全指南:从基础概念到实战案例(线性渐变/径向渐变/重复渐变)
一、什么是 CSS 渐变? 渐变是网页设计中常用的视觉效果,指两种或多种颜色之间的平滑过渡。CSS 提供了强大的渐变功能,无需依赖图片即可创建复杂的色彩过渡效果,主要分为线性渐变和径向渐变两大类。 二、线性渐变(Line…...
初识Docker:容器化技术的入门指南
初识Docker:容器化技术的入门指南 一、Docker是什么:容器化技术的核心概念二、Docker的核心优势2.1 环境一致性2.2 高效部署与快速迭代2.3 资源利用率高 三、Docker的安装与基本使用3.1 安装Docker3.2 Docker基本概念3.3 第一个Docker容器体验 四、Docke…...

android binder(1)基本原理
一、IPC 进程间通信(IPC,Inter-Process Communication)机制,用于解决不同进程间的数据交互问题。 不同进程之间用户地址空间的变量和函数是不能相互访问的,但是不同进程的内核地址空间是相同和共享的,我们可…...

行业分析---小米汽车2025第一季度财报
1 背景 最近几年是新能源汽车的淘汰赛,前短时间比亚迪再次开始了降价,导致一片上市车企的股价大跌,足见车圈现在的敏感度。因此笔者会一直跟踪新势力车企的财报状况,对之前财报分析感兴趣的读者朋友可以参考以下博客:…...

边缘计算网关支撑医院供暖系统高效运维的本地化计算与边缘决策
一、项目背景 医院作为人员密集的特殊场所,对供暖系统的稳定性和高效性有着极高的要求。其供暖换热站传统的人工现场监控方式存在诸多弊端,如人员值守成本高、数据记录不及时不准确、故障发现和处理滞后、能耗难以有效监测和控制等,难以满足…...
GO环境配置
Go 语言环境安装指南(Windows 版) 以下是在 Windows 系统上安装 Go 语言环境的完整步骤: 准备工作 操作系统要求:Windows 7 或更高版本(推荐 Windows 10/11)系统架构:64位(…...
`docker run`、`docker start`、`docker exec` 区别
🧠 先给你一句话理解: docker run ≈ docker create docker start docker exec(第一次) ✅ 三者的区别一览表 命令作用类比真实生活常用场景docker run创建 启动 执行命令(一次性)你买了一台新电脑&am…...

简单了解string类的特性及使用(C++)
string的特性 string类不属于STL,它属于标准库 但由于它具有数据结构的特性,所以从归类的角度,可以将string类归类到容器里面去 在C标准库中,std::string 是一个特化的类型,实际上是 std::basic_string 的别名。std…...

FastAPI+Pyomo实现线性回归解决饮食问题
之前在 FastAPI介绍-CSDN博客 中介绍过FastAPI,在 Pyomo中线性规划接口的使用-CSDN博客 中使用Pyomo解决饮食问题,这里将两者组合,即FastAPI在服务器端启动,通过Pyomo实现线性回归;客户端通过浏览器获取饮食的最优解。…...

16.FreeRTOS
目录 第1章 FreeRTOS 实时操作系统 1.1 认识实时操作系统 1.1.1 裸机的概念 1.1.2 操作系统的概念 1.2 操作系统的分类 1.3 常见的操作系统 1.4 认识实时操作系统 1.4.1 可剥夺型内核与不可剥夺型内核 1.4.2 嵌入式操作系统的作用 1.4.3 嵌入式操作系统的发展 1.4.4…...

Redis最佳实践——购物车优化详解
Redis在电商购物车高并发读写场景下的优化实践 一、购物车业务场景分析 典型操作特征 读/写比例 ≈ 8:2高峰QPS可达10万单用户最大商品数500操作类型:增删改查、全选/反选、数量修改 技术挑战 高并发下的数据一致性海量数据存储与快速访问实时价格计算与库存校验分…...

【计算机网络】传输层UDP协议
🔥个人主页🔥:孤寂大仙V 🌈收录专栏🌈:计算机网络 🌹往期回顾🌹: 【计算机网络】应用层协议Http——构建Http服务服务器 🔖流水不争,争的是滔滔不…...