有了菜单及角色管理后,我们还需要根据用户访问的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() // 路由模式
});
以上就是菜单权限相关内容。
下一篇将继续探讨 动态菜单的实现,敬请期待~