Vue3.5 企业级管理系统实战(二十一):菜单权限

[复制链接]
发表于 2025-7-3 08:53:52 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
有了菜单及脚色管理后,我们还须要根据用户访问的token,去获取用户信息,根据用户的脚色信息,拉取全部的菜单权限,进而生成左侧菜单树数据。
1 增长获取用户信息 api

在 src/api/user.ts 中,添加获取用户信息的 api(getUserInfo),代码如下:
  1. //src/api/user.ts
  2. import request from "@/api/config/request";
  3. // 从 "./type" 模块中导入 ApiResponse 类型,用于定义接口响应数据的结构
  4. import type { ApiResponse } from "./type";
  5. import type { IRole } from "./role";
  6. /**
  7. * 定义用户登录所需的数据结构
  8. * @interface IUserLoginData
  9. * @property {string} username - 用户登录使用的用户名
  10. * @property {string} password - 用户登录使用的密码
  11. */
  12. export interface IUserLoginData {
  13.   username: string;
  14.   password: string;
  15. }
  16. /**
  17. * 定义登录接口响应的数据结构
  18. * @interface ILoginResponseData
  19. * @property {string} token - 登录成功后返回的令牌,用于后续请求的身份验证
  20. */
  21. export interface ILoginResponseData {
  22.   token: string;
  23. }
  24. /**
  25. * 登录接口
  26. * @param {IUserLoginData} data - 用户登录所需的数据,包含用户名和密码
  27. * @returns {Promise<ApiResponse<ILoginResponseData>>} - 返回一个 Promise 对象,该对象解析为包含登录响应数据的 ApiResponse 类型
  28. */
  29. export const login = (
  30.   data: IUserLoginData
  31. ): Promise<ApiResponse<ILoginResponseData>> => {
  32.   return request.post("/4642164-4292760-default/287017559", data);
  33. };
  34. //test
  35. export const logout_error = (): Promise<ApiResponse<ILoginResponseData>> => {
  36.   return request.post("/4642164-4292760-default/auth/401");
  37. };
  38. //个人中心接口
  39. export interface Profile {
  40.   id: number;
  41.   username: string;
  42.   email: string;
  43.   mobile: string;
  44.   isSuper: boolean;
  45.   status: boolean;
  46.   avatar: string;
  47.   description: string;
  48.   roles: IRole[];
  49.   roleIds?: number[]; // 修改用户的时候,后端接受只要id
  50. }
  51. export interface IUsers {
  52.   users: Profile[];
  53.   count: number;
  54. }
  55. // 查询参数
  56. export interface IUserQuery {
  57.   pageNum?: number;
  58.   pageSize?: number;
  59.   mobile?: string;
  60.   status?: boolean;
  61.   username?: string;
  62.   flag: number;
  63. }
  64. // 获取用户列表的接口
  65. export const getUsers = (params: IUserQuery): Promise<ApiResponse<IUsers>> => {
  66.   const {
  67.     pageNum = 0,
  68.     pageSize = 10,
  69.     username = "",
  70.     status,
  71.     mobile = "",
  72.     flag
  73.   } = params;
  74.   return request.get("http://127.0.0.1:4523/m1/4642164-4292760-default/user", {
  75.     params: {
  76.       pageNum,
  77.       pageSize,
  78.       username,
  79.       status,
  80.       mobile,
  81.       flag
  82.     }
  83.   });
  84. };
  85. // 删除用户
  86. export const removeUser = (id: number): Promise<ApiResponse> => {
  87.   return request.delete(
  88.     `http://127.0.0.1:4523/m1/4642164-4292760-default/user/`
  89.   );
  90. };
  91. // 添加用户
  92. export const addUser = (data: Profile): Promise<ApiResponse> => {
  93.   return request.post(
  94.     "http://127.0.0.1:4523/m1/4642164-4292760-default/user",
  95.     data
  96.   );
  97. };
  98. // 编辑用户
  99. export const updateUser = (id: number, data: Profile): Promise<ApiResponse> => {
  100.   return request.put(
  101.     `http://127.0.0.1:4523/m1/4642164-4292760-default/user`,
  102.     data
  103.   );
  104. };
  105. // 获取用户信息
  106. export const getUserInfo = (): Promise<ApiResponse<Profile>> => {
  107.   return request.post("/auth/info");
  108. };
复制代码
2 修改用户 Store

在 src/stores/user.ts 中,增长获取当前用户信息的方法 getUserInfo,代码如下:
  1. //src/stores/user.ts
  2. import type { IUserLoginData, IUserQuery, IUsers, Profile } from "@/api/user";
  3. import {
  4.   login as loginApi,
  5.   getUsers as getUsersApi, // 获取用户
  6.   addUser as addUserApi,
  7.   removeUser as removeUserApi,
  8.   updateUser as updateUserApi,
  9.   getUserInfo as getUserInfoApi
  10. } from "@/api/user";
  11. import { setToken, removeToken } from "@/utils/auth";
  12. import { useTagsView } from "./tagsView";
  13. import type { IRole } from "@/api/role";
  14. /**
  15. * 用户信息查询参数类型,继承自Profile并添加分页参数
  16. */
  17. export type IProfileQuery = Profile & {
  18.   pageNum?: number;
  19.   pageSize?: number;
  20. };
  21. /**
  22. * 用户状态管理
  23. */
  24. export const useUserStore = defineStore("user", () => {
  25.   // 状态管理
  26.   const state = reactive({
  27.     token: "", // 用户令牌
  28.     users: [] as IUsers["users"], // 用户列表
  29.     count: 0, // 用户总数
  30.     roles: [] as IRole[], // 用户角色列表
  31.     userInfo: {} as Profile // 当前用户信息
  32.   });
  33.   // 引用标签视图模块
  34.   const tagsViewStore = useTagsView();
  35.   /**
  36.    * 获取当前用户信息
  37.    */
  38.   const getUserInfo = async () => {
  39.     const res = await getUserInfoApi();
  40.     if (res.code === 0) {
  41.       // 解构响应数据,分离角色和用户信息
  42.       const { roles, ...info } = res.data;
  43.       state.roles = roles; // 存储角色信息
  44.       state.userInfo = info as Profile; // 存储用户信息
  45.     }
  46.   };
  47.   /**
  48.    * 用户登录
  49.    * @param userInfo - 包含用户名和密码的登录信息
  50.    */
  51.   const login = async (userInfo: IUserLoginData) => {
  52.     try {
  53.       const { username, password } = userInfo;
  54.       // 调用登录API,用户名去除首尾空格
  55.       const response = await loginApi({ username: username.trim(), password });
  56.       const { data } = response;
  57.       state.token = data.token; // 存储令牌
  58.       setToken(data.token); // 保存令牌到本地存储
  59.     } catch (e) {
  60.       return Promise.reject(e); // 登录失败时返回错误
  61.     }
  62.   };
  63.   /**
  64.    * 用户注销
  65.    */
  66.   const logout = () => {
  67.     state.token = ""; // 清空令牌
  68.     removeToken(); // 移除本地存储的令牌
  69.     tagsViewStore.delAllView(); // 清除所有标签视图
  70.   };
  71.   /**
  72.    * 获取全部用户列表
  73.    * @param params - 查询参数,包含分页和筛选条件
  74.    */
  75.   const getAllUsers = async (params: IUserQuery) => {
  76.     const res = await getUsersApi(params);
  77.     const { data } = res;
  78.     state.users = data.users; // 更新用户列表
  79.     state.count = data.count; // 更新用户总数
  80.   };
  81.   /**
  82.    * 添加新用户
  83.    * @param data - 用户信息,包含分页参数(用于添加成功后刷新列表)
  84.    */
  85.   const addUser = async (data: IProfileQuery) => {
  86.     // 分离分页参数和用户信息
  87.     const { pageSize, pageNum, ...params } = data;
  88.     const res = await addUserApi(params);
  89.     if (res.code === 0) {
  90.       // 添加成功后刷新用户列表
  91.       getAllUsers({
  92.         pageSize,
  93.         pageNum
  94.       });
  95.     }
  96.   };
  97.   /**
  98.    * 删除用户
  99.    * @param data - 包含用户ID和分页信息(用于删除成功后刷新列表)
  100.    */
  101.   const removeUser = async (data: IProfileQuery) => {
  102.     const { pageSize, pageNum, id } = data;
  103.     const res = await removeUserApi(id);
  104.     if (res.code === 0) {
  105.       // 删除成功后刷新用户列表
  106.       getAllUsers({
  107.         pageSize,
  108.         pageNum
  109.       });
  110.     }
  111.   };
  112.   /**
  113.    * 编辑用户信息
  114.    * @param data - 用户信息,包含分页参数(用于编辑成功后刷新列表)
  115.    */
  116.   const editUser = async (data: IProfileQuery) => {
  117.     // 分离分页参数和用户信息
  118.     const { pageSize, pageNum, ...params } = data;
  119.     const res = await updateUserApi(params.id, params);
  120.     if (res.code === 0) {
  121.       // 编辑成功后刷新用户列表
  122.       getAllUsers({
  123.         pageSize,
  124.         pageNum
  125.       });
  126.     }
  127.   };
  128.   // 导出可访问的方法和状态
  129.   return {
  130.     login,
  131.     state,
  132.     logout,
  133.     getAllUsers,
  134.     editUser,
  135.     removeUser,
  136.     addUser,
  137.     getUserInfo
  138.   };
  139. });
复制代码
3 新增 permission Store 

新增 src/stores/permission.ts,代码如下:
  1. //src/stores/permission.ts
  2. import type { RouteRecordRaw } from "vue-router";
  3. import { useUserStore } from "./user";
  4. import { asyncRoutes } from "@/router";
  5. import { useMenuStore } from "./menu";
  6. import type { MenuData } from "@/api/menu";
  7. import path from "path-browserify";
  8. /**
  9. * 递归生成路由配置
  10. * @param routes - 原始路由配置
  11. * @param routesPath - 需要保留的路由路径数组
  12. * @param basePath - 基础路径,用于解析子路由
  13. * @returns 过滤后的路由配置
  14. */
  15. function generateRoutes(
  16.   routes: RouteRecordRaw[],
  17.   routesPath: string[],
  18.   basePath = "/"
  19. ) {
  20.   const routerData: RouteRecordRaw[] = [];
  21.   routes.forEach((route) => {
  22.     // 解析当前路由的完整路径
  23.     const routePath = path.resolve(basePath, route.path);
  24.     // 递归处理子路由
  25.     if (route.children) {
  26.       route.children = generateRoutes(route.children, routesPath, routePath);
  27.     }
  28.     // 路由过滤条件:
  29.     // 1. 当前路由路径在允许列表中
  30.     // 2. 或者当前路由有子路由
  31.     if (
  32.       routesPath.includes(routePath) ||
  33.       (route.children && route.children.length >= 1)
  34.     ) {
  35.       routerData.push(route);
  36.     }
  37.   });
  38.   return routerData;
  39. }
  40. /**
  41. * 根据菜单数据过滤异步路由
  42. * @param menus - 菜单数据
  43. * @param routes - 原始异步路由配置
  44. * @returns 过滤后的路由配置
  45. */
  46. function filterAsyncRoutes(menus: MenuData[], routes: RouteRecordRaw[]) {
  47.   // 提取菜单中的路径信息
  48.   const routesPath = menus.map((item) => item.path);
  49.   // 调用递归生成函数
  50.   return generateRoutes(routes, routesPath);
  51. }
  52. /**
  53. * 权限管理模块
  54. */
  55. export const usePermissionStore = defineStore("permission", () => {
  56.   const userStore = useUserStore();
  57.   const menuStore = useMenuStore();
  58.   // 存储最终生成的可访问路由
  59.   let accessMenuRoutes: RouteRecordRaw[] = [];
  60.   /**
  61.    * 生成可访问的路由配置
  62.    * @returns 基于用户角色的可访问路由配置
  63.    */
  64.   const generateRoutes = async () => {
  65.     // 计算属性:获取用户角色名称列表
  66.     const rolesNames = computed(() =>
  67.       userStore.state.roles.map((item) => item.name)
  68.     );
  69.     // 计算属性:获取用户角色ID列表
  70.     const roleIds = computed(() =>
  71.       userStore.state.roles.map((item) => item.id)
  72.     );
  73.     // 超级管理员角色处理
  74.     if (rolesNames.value.includes("super_admin")) {
  75.       // 超级管理员拥有全部路由访问权限
  76.       accessMenuRoutes = asyncRoutes;
  77.       // 获取全部菜单列表
  78.       await menuStore.getAllMenuListByAdmin();
  79.       return accessMenuRoutes;
  80.     } else {
  81.       // 普通角色处理:根据角色ID获取对应菜单
  82.       await menuStore.getMenuListByRoles(roleIds.value);
  83.       // 获取当前用户权限下的菜单列表
  84.       const menus = menuStore.state.authMenuList;
  85.       // 根据菜单权限过滤路由
  86.       accessMenuRoutes = filterAsyncRoutes(menus, asyncRoutes);
  87.       return accessMenuRoutes;
  88.     }
  89.   };
  90.   return {
  91.     generateRoutes
  92.   };
  93. });
复制代码
4 修改 permission.ts

修改 src/permission.ts,代码如下:
  1. //src/permission.ts
  2. // 路由鉴权配置 - 控制用户访问权限和页面导航逻辑
  3. import router from "@/router";
  4. import NProgress from "nprogress"; // 进度条插件
  5. import "nprogress/nprogress.css"; // 进度条样式
  6. import { getToken } from "./utils/auth"; // 获取token工具函数
  7. import { useUserStore } from "./stores/user"; // 用户状态管理
  8. import { usePermissionStore } from "./stores/permission"; // 权限状态管理
  9. // 配置进度条选项,不显示旋转加载图标
  10. NProgress.configure({ showSpinner: false });
  11. // 白名单路由 - 无需登录即可访问的页面
  12. const whiteList = ["/login"];
  13. /**
  14. * 全局前置守卫 - 路由切换前的权限校验
  15. * 1. 检查Token有效性
  16. * 2. 判断用户权限
  17. * 3. 动态生成路由
  18. */
  19. router.beforeEach(async (to, from) => {
  20.   // 开始进度条
  21.   NProgress.start();
  22.   // 获取本地Token
  23.   const hasToken = getToken();
  24.   const userStore = useUserStore();
  25.   const permissionStore = usePermissionStore();
  26.   // 情况1:已登录状态
  27.   if (hasToken) {
  28.     // 已登录但访问登录页,重定向到首页
  29.     if (to.path === "/login") {
  30.       NProgress.done(); // 结束进度条
  31.       return {
  32.         path: "/",
  33.         replace: true // 替换历史记录,禁止回退
  34.       };
  35.     } else {
  36.       // 已登录且访问非登录页,校验用户权限(有可能token是伪造的,无效的)
  37.       try {
  38.         // 检查是否已有用户角色信息
  39.         const hasRoles = userStore.state.roles.length > 0;
  40.         // 已有角色信息,直接放行
  41.         if (hasRoles) {
  42.           NProgress.done();
  43.           return true;
  44.         }
  45.         // 没有角色信息,重新获取用户信息
  46.         await userStore.getUserInfo();
  47.         // 根据用户角色动态生成可访问的路由配置
  48.         const routes = await permissionStore.generateRoutes();
  49.         // 动态添加路由到路由器
  50.         routes.forEach((route) => router.addRoute(route));
  51.         // 确保路由添加完成,重新访问目标路径
  52.         return router.push({ path: to.path, replace: true });
  53.       } catch (error) {
  54.         // 获取用户信息失败,可能Token过期或无效
  55.         console.error("获取用户信息失败:", error);
  56.         // 清除用户状态并跳转到登录页
  57.         userStore.logout();
  58.         NProgress.done();
  59.         // 携带当前路径作为重定向参数
  60.         return `/login?redirect=${to.path}`;
  61.       }
  62.     }
  63.   }
  64.   // 情况2:未登录状态
  65.   else {
  66.     // 访问白名单页面,直接放行
  67.     if (whiteList.includes(to.path)) {
  68.       NProgress.done();
  69.       return true;
  70.     }
  71.     // 非白名单页面,重定向到登录页并记录原始路径
  72.     return {
  73.       path: "/login",
  74.       query: {
  75.         redirect: to.path,
  76.         ...to.query // 保留原路径的查询参数
  77.       }
  78.     };
  79.   }
  80. });
复制代码
5 修改路由配置

修改 src/router/index.ts,代码如下:
  1. //src/router/index.ts
  2. import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
  3. import Layout from "@/layout/index.vue";
  4. export const constantRoutes: RouteRecordRaw[] = [
  5.   {
  6.     path: "/",
  7.     component: Layout,
  8.     redirect: "/dashboard",
  9.     children: [
  10.       {
  11.         path: "dashboard",
  12.         name: "dashboard",
  13.         component: () => import("@/views/dashboard/index.vue"),
  14.         meta: {
  15.           icon: "ant-design:bank-outlined",
  16.           title: "dashboard",
  17.           affix: true, // 固定在tagsViews中
  18.           noCache: true //   不需要缓存
  19.         }
  20.       }
  21.     ]
  22.   },
  23.   {
  24.     path: "/redirect",
  25.     component: Layout,
  26.     meta: {
  27.       hidden: true
  28.     },
  29.     // 当跳转到  /redirect/a/b/c/d?query=1
  30.     children: [
  31.       {
  32.         path: "/redirect/:path(.*)",
  33.         component: () => import("@/views/redirect/index.vue")
  34.       }
  35.     ]
  36.   },
  37.   {
  38.     path: "/login",
  39.     name: "Login",
  40.     meta: {
  41.       hidden: true
  42.     },
  43.     component: () => import("@/views/login/index.vue")
  44.   }
  45. ];
  46. export const asyncRoutes: RouteRecordRaw[] = [
  47.   {
  48.     path: "/documentation",
  49.     component: Layout,
  50.     redirect: "/documentation/index",
  51.     children: [
  52.       {
  53.         path: "index",
  54.         name: "documentation",
  55.         component: () => import("@/views/documentation/index.vue"),
  56.         meta: {
  57.           icon: "ant-design:database-filled",
  58.           title: "documentation"
  59.         }
  60.       }
  61.     ]
  62.   },
  63.   {
  64.     path: "/guide",
  65.     component: Layout,
  66.     redirect: "/guide/index",
  67.     children: [
  68.       {
  69.         path: "index",
  70.         name: "guide",
  71.         component: () => import("@/views/guide/index.vue"),
  72.         meta: {
  73.           icon: "ant-design:car-twotone",
  74.           title: "guide"
  75.         }
  76.       }
  77.     ]
  78.   },
  79.   {
  80.     path: "/system",
  81.     component: Layout,
  82.     redirect: "/system/menu",
  83.     meta: {
  84.       icon: "ant-design:unlock-filled",
  85.       title: "system",
  86.       alwaysShow: true
  87.       // breadcrumb: false
  88.       // 作为父文件夹一直显示
  89.     },
  90.     children: [
  91.       {
  92.         path: "menu",
  93.         name: "menu",
  94.         component: () => import("@/views/system/menu/index.vue"),
  95.         meta: {
  96.           icon: "ant-design:unlock-filled",
  97.           title: "menu"
  98.         }
  99.       },
  100.       {
  101.         path: "role",
  102.         name: "role",
  103.         component: () => import("@/views/system/role/index.vue"),
  104.         meta: {
  105.           icon: "ant-design:unlock-filled",
  106.           title: "role"
  107.         }
  108.       },
  109.       {
  110.         path: "user",
  111.         name: "user",
  112.         component: () => import("@/views/system/user/index.vue"),
  113.         meta: {
  114.           icon: "ant-design:unlock-filled",
  115.           title: "user"
  116.         }
  117.       }
  118.     ]
  119.   },
  120.   {
  121.     path: "/external-link",
  122.     component: Layout,
  123.     children: [
  124.       {
  125.         path: "http://www.baidu.com",
  126.         redirect: "/",
  127.         meta: {
  128.           icon: "ant-design:link-outlined",
  129.           title: "link Baidu"
  130.         }
  131.       }
  132.     ]
  133.   }
  134. ];
  135. // 需要根据用户赋予的权限来动态添加异步路由
  136. export const routes = [...constantRoutes];
  137. export default createRouter({
  138.   routes, // 路由表
  139.   history: createWebHistory() //  路由模式
  140. });
复制代码
以上就是菜单权限相关内容。

下一篇将继承探讨 动态菜单的实现,敬请期待~ 


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表