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

news2025/6/4 13:27:59

有了菜单及角色管理后,我们还需要根据用户访问的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();

  // 获取本地Token
  const 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=1
    children: [
      {
        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() //  路由模式
});

以上就是菜单权限相关内容。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2396816.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【HarmonyOS Next之旅】DevEco Studio使用指南(二十九) -> 开发云数据库

目录 1 -> 开发流程 2 -> 创建对象类型 3 -> 添加数据条目 3.1 -> 手动创建数据条目文件 3.2 -> 自动生成数据条目文件 4 -> 部署云数据库 1 -> 开发流程 云数据库是一款端云协同的数据库产品&#xff0c;提供端云数据的协同管理、统一的数据模型和…

批量导出CAD属性块信息生成到excel——CAD C#二次开发(插件实现)

本插件可实现批量导出文件夹内大量dwg文件的指定块名的属性信息到excel&#xff0c;效果如下&#xff1a; 插件界面&#xff1a; dll插件如下&#xff1a; 使用方法&#xff1a; 1、获取此dll插件。 2、cad命令行输入netload &#xff0c;加载此dll&#xff08;要求AutoCAD&…

Goreplay最新版本的安装和简单使用

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

Android Studio 解决报错 not support JCEF 记录

问题&#xff1a;Android Studio 安装Markdown插件后&#xff0c;报错not support JCEF不能预览markdown文件。 原因&#xff1a;Android Studio不是新装&#xff0c;之前没留意IDE自带的版本是不支持JCEF的。 解决办法&#xff1a; 在菜单栏选中Help→Find Action&#xff…

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的配置参数及性能调优的点&#xff0c;其kafka的从零开始的安装到生产者&#xff0c;消费者的详解介绍、源码及分析及原理解析请到博主kafka专栏 。 1.消费者Consumer配置参数 配置参数默认值含义bootstrap.servers无&#xff08;必填&#xff09;…

【论文笔记】Transcoders Find Interpretable LLM Feature Circuits

Abstract 机制可解释性(mechanistic interpretability)的核心目标是路径分析(circuit analysis)&#xff1a;在模型中找出与特定行为或能力对应的稀疏子图。 然而&#xff0c;MLP 子层使得在基于 Transformer 的语言模型中进行细粒度的路径分析变得困难。具体而言&#xff0c;…

每天总结一个html标签——a标签

文章目录 一、定义与使用说明二、支持的属性三、支持的事件四、默认样式五、常见用法1. 文本链接2. 图片链接3. 导航栏 在前端开发中&#xff0c;a标签&#xff08;锚点标签&#xff09;是最常用的HTML标签之一&#xff0c;主要用于创建超链接&#xff0c;实现页面间的跳转或下…

android binder(1)基本原理

一、IPC 进程间通信&#xff08;IPC&#xff0c;Inter-Process Communication&#xff09;机制&#xff0c;用于解决不同进程间的数据交互问题。 不同进程之间用户地址空间的变量和函数是不能相互访问的&#xff0c;但是不同进程的内核地址空间是相同和共享的&#xff0c;我们可…

行业分析---小米汽车2025第一季度财报

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

边缘计算网关支撑医院供暖系统高效运维的本地化计算与边缘决策

一、项目背景 医院作为人员密集的特殊场所&#xff0c;对供暖系统的稳定性和高效性有着极高的要求。其供暖换热站传统的人工现场监控方式存在诸多弊端&#xff0c;如人员值守成本高、数据记录不及时不准确、故障发现和处理滞后、能耗难以有效监测和控制等&#xff0c;难以满足…

简单了解string类的特性及使用(C++)

string的特性 string类不属于STL&#xff0c;它属于标准库 但由于它具有数据结构的特性&#xff0c;所以从归类的角度&#xff0c;可以将string类归类到容器里面去 在C标准库中&#xff0c;std::string 是一个特化的类型&#xff0c;实际上是 std::basic_string 的别名。std…

FastAPI+Pyomo实现线性回归解决饮食问题

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

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操作类型&#xff1a;增删改查、全选/反选、数量修改 技术挑战 高并发下的数据一致性海量数据存储与快速访问实时价格计算与库存校验分…

【计算机网络】传输层UDP协议

&#x1f525;个人主页&#x1f525;&#xff1a;孤寂大仙V &#x1f308;收录专栏&#x1f308;&#xff1a;计算机网络 &#x1f339;往期回顾&#x1f339;&#xff1a; 【计算机网络】应用层协议Http——构建Http服务服务器 &#x1f516;流水不争&#xff0c;争的是滔滔不…

安全漏洞修复导致SpringBoot2.7与Springfox不兼容

项目基于 springboot2.5.2 实现的&#xff0c;用 springfox-swagger2 生成与前端对接的 API 文档&#xff1b;pom.xml 中依赖如下 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId>&l…

从法律层面剖析危化品证书:两证一证背后的安全逻辑

《安全生产法》第 24 条明确规定&#xff0c;危化品单位主要负责人和安全管理人员 “必须考核合格方可上岗”。这并非仅仅是行政要求&#xff0c;而是通过法律来筑牢安全防线。在某危化品仓库爆炸事故中&#xff0c;由于负责人未持证&#xff0c;导致事故责任升级&#xff0c;企…

深入理解复数加法与乘法:MATLAB演示

在学习复数的过程中&#xff0c;复数加法与乘法是两个非常基础且重要的概念。复数的加法和乘法操作与我们常见的实数运算有所不同&#xff0c;它们不仅涉及到数值的大小&#xff0c;还有方向和相位的变化。在这篇博客中&#xff0c;我们将通过MATLAB演示来帮助大家更好地理解复…

【设计模式-3.6】结构型——桥接模式

说明&#xff1a;本文介绍结构型设计模式之一的桥接模式 定义 桥接模式&#xff08;Bridge Pattern&#xff09;又叫作桥梁模式、接口&#xff08;Interface&#xff09;模式或柄体&#xff08;Handle and Body&#xff09;模式&#xff0c;指将抽象部分与具体实现部分分离&a…