refactor(permission): ♻️ 优化路由权限模块,简化命名和职责分离
This commit is contained in:
@@ -322,7 +322,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { hasAuth } from "@/plugins/permission";
|
import { hasPerm } from "@/utils/auth";
|
||||||
import { useDateFormat, useThrottleFn } from "@vueuse/core";
|
import { useDateFormat, useThrottleFn } from "@vueuse/core";
|
||||||
import {
|
import {
|
||||||
genFileId,
|
genFileId,
|
||||||
@@ -387,7 +387,7 @@ function hasButtonPerm(action: string): boolean {
|
|||||||
const perm = getButtonPerm(action);
|
const perm = getButtonPerm(action);
|
||||||
// 如果没有设置权限标识,则默认具有权限
|
// 如果没有设置权限标识,则默认具有权限
|
||||||
if (!perm) return true;
|
if (!perm) return true;
|
||||||
return hasAuth(perm);
|
return hasPerm(perm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建工具栏按钮
|
// 创建工具栏按钮
|
||||||
|
|||||||
@@ -2,16 +2,14 @@ import type { RouteRecordRaw } from "vue-router";
|
|||||||
import NProgress from "@/utils/nprogress";
|
import NProgress from "@/utils/nprogress";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
import { usePermissionStore, useUserStore } from "@/store";
|
import { usePermissionStore, useUserStore } from "@/store";
|
||||||
import { ROLE_ROOT } from "@/constants";
|
|
||||||
|
|
||||||
export function setupPermission() {
|
export function setupPermission() {
|
||||||
const whiteList = ["/login"]; // 无需登录的页面
|
const whiteList = ["/login"];
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
NProgress.start();
|
NProgress.start();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用 store 暴露的登录态,便于后续扩展(如基于过期时间等)
|
|
||||||
const isLoggedIn = useUserStore().isLoggedIn();
|
const isLoggedIn = useUserStore().isLoggedIn();
|
||||||
|
|
||||||
// 未登录处理
|
// 未登录处理
|
||||||
@@ -25,18 +23,17 @@ export function setupPermission() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已登录且访问登录页,重定向到首页
|
// 已登录登录页重定向
|
||||||
if (to.path === "/login") {
|
if (to.path === "/login") {
|
||||||
next({ path: "/" });
|
next({ path: "/" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已登录用户的正常访问
|
|
||||||
const permissionStore = usePermissionStore();
|
const permissionStore = usePermissionStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
// 路由未生成则生成
|
// 动态路由生成
|
||||||
if (!permissionStore.isDynamicRoutesGenerated) {
|
if (!permissionStore.isRouteGenerated) {
|
||||||
if (!userStore.userInfo?.roles?.length) {
|
if (!userStore.userInfo?.roles?.length) {
|
||||||
await userStore.getUserInfo();
|
await userStore.getUserInfo();
|
||||||
}
|
}
|
||||||
@@ -50,13 +47,13 @@ export function setupPermission() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查路由是否存在
|
// 路由404检查
|
||||||
if (to.matched.length === 0) {
|
if (to.matched.length === 0) {
|
||||||
next("/404");
|
next("/404");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置页面标题
|
// 动态标题设置
|
||||||
const title = (to.params.title as string) || (to.query.title as string);
|
const title = (to.params.title as string) || (to.query.title as string);
|
||||||
if (title) {
|
if (title) {
|
||||||
to.meta.title = title;
|
to.meta.title = title;
|
||||||
@@ -64,13 +61,9 @@ export function setupPermission() {
|
|||||||
|
|
||||||
next();
|
next();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Route guard error:", error);
|
// 错误处理:重置状态并跳转登录
|
||||||
// 出错时清理状态并重定向到登录页
|
console.error("Route guard error:", error);
|
||||||
try {
|
await useUserStore().resetAllState();
|
||||||
await useUserStore().resetAllState();
|
|
||||||
} catch (resetError) {
|
|
||||||
console.error("❌ Failed to reset user state:", resetError);
|
|
||||||
}
|
|
||||||
next("/login");
|
next("/login");
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
}
|
}
|
||||||
@@ -80,18 +73,3 @@ export function setupPermission() {
|
|||||||
NProgress.done();
|
NProgress.done();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断是否有权限 */
|
|
||||||
export function hasAuth(value: string | string[], type: "button" | "role" = "button") {
|
|
||||||
const { roles, perms } = useUserStore().userInfo;
|
|
||||||
|
|
||||||
// 超级管理员 拥有所有权限
|
|
||||||
if (type === "button" && roles.includes(ROLE_ROOT)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auths = type === "button" ? perms : roles;
|
|
||||||
return typeof value === "string"
|
|
||||||
? auths.includes(value)
|
|
||||||
: value.some((perm) => auths.includes(perm));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import router from "@/router";
|
|||||||
|
|
||||||
import MenuAPI, { type RouteVO } from "@/api/system/menu-api";
|
import MenuAPI, { type RouteVO } from "@/api/system/menu-api";
|
||||||
const modules = import.meta.glob("../../views/**/**.vue");
|
const modules = import.meta.glob("../../views/**/**.vue");
|
||||||
const Layout = () => import("@/layouts/index.vue");
|
const Layout = () => import("../../layouts/index.vue");
|
||||||
|
|
||||||
export const usePermissionStore = defineStore("permission", () => {
|
export const usePermissionStore = defineStore("permission", () => {
|
||||||
// 所有路由(静态路由 + 动态路由)
|
// 所有路由(静态路由 + 动态路由)
|
||||||
@@ -13,42 +13,34 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
// 混合布局的左侧菜单路由
|
// 混合布局的左侧菜单路由
|
||||||
const mixLayoutSideMenus = ref<RouteRecordRaw[]>([]);
|
const mixLayoutSideMenus = ref<RouteRecordRaw[]>([]);
|
||||||
// 动态路由是否已生成
|
// 动态路由是否已生成
|
||||||
const isDynamicRoutesGenerated = ref(false);
|
const isRouteGenerated = ref(false);
|
||||||
|
|
||||||
/**
|
/** 生成动态路由 */
|
||||||
* 生成动态路由
|
|
||||||
*/
|
|
||||||
async function generateRoutes(): Promise<RouteRecordRaw[]> {
|
async function generateRoutes(): Promise<RouteRecordRaw[]> {
|
||||||
try {
|
try {
|
||||||
const data = await MenuAPI.getRoutes(); // 获取当前登录人拥有的菜单路由
|
const data = await MenuAPI.getRoutes(); // 获取当前登录人的菜单路由
|
||||||
const processRouteList = processRoutes(data); // 处理后的路由数据
|
const dynamicRoutes = transformRoutes(data);
|
||||||
const dynamicRoutes = parseDynamicRoutes(processRouteList);
|
|
||||||
|
|
||||||
routes.value = [...constantRoutes, ...dynamicRoutes];
|
routes.value = [...constantRoutes, ...dynamicRoutes];
|
||||||
|
isRouteGenerated.value = true;
|
||||||
isDynamicRoutesGenerated.value = true;
|
|
||||||
|
|
||||||
return dynamicRoutes;
|
return dynamicRoutes;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Failed to generate routes:", error);
|
// 路由生成失败,重置状态
|
||||||
isDynamicRoutesGenerated.value = false;
|
isRouteGenerated.value = false;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 设置混合布局左侧菜单 */
|
||||||
* 设置混合布局的左侧菜单
|
|
||||||
*/
|
|
||||||
const setMixLayoutSideMenus = (parentPath: string) => {
|
const setMixLayoutSideMenus = (parentPath: string) => {
|
||||||
const parentMenu = routes.value.find((item) => item.path === parentPath);
|
const parentMenu = routes.value.find((item) => item.path === parentPath);
|
||||||
mixLayoutSideMenus.value = parentMenu?.children || [];
|
mixLayoutSideMenus.value = parentMenu?.children || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/** 重置路由状态 */
|
||||||
* 重置路由状态
|
|
||||||
*/
|
|
||||||
const resetRouter = () => {
|
const resetRouter = () => {
|
||||||
// 移除动态路由
|
// 移除动态添加的路由
|
||||||
const constantRouteNames = new Set(constantRoutes.map((route) => route.name).filter(Boolean));
|
const constantRouteNames = new Set(constantRoutes.map((route) => route.name).filter(Boolean));
|
||||||
routes.value.forEach((route) => {
|
routes.value.forEach((route) => {
|
||||||
if (route.name && !constantRouteNames.has(route.name)) {
|
if (route.name && !constantRouteNames.has(route.name)) {
|
||||||
@@ -56,16 +48,16 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重置状态
|
// 重置所有状态
|
||||||
routes.value = [...constantRoutes];
|
routes.value = [...constantRoutes];
|
||||||
mixLayoutSideMenus.value = [];
|
mixLayoutSideMenus.value = [];
|
||||||
isDynamicRoutesGenerated.value = false;
|
isRouteGenerated.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
routes,
|
routes,
|
||||||
mixLayoutSideMenus,
|
mixLayoutSideMenus,
|
||||||
isDynamicRoutesGenerated,
|
isRouteGenerated,
|
||||||
generateRoutes,
|
generateRoutes,
|
||||||
setMixLayoutSideMenus,
|
setMixLayoutSideMenus,
|
||||||
resetRouter,
|
resetRouter,
|
||||||
@@ -73,63 +65,40 @@ export const usePermissionStore = defineStore("permission", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析后端返回的路由数据并转换为 Vue Router 兼容的路由配置
|
* 转换后端路由数据为Vue Router配置
|
||||||
*
|
* 处理组件路径映射和Layout层级嵌套
|
||||||
* @param rawRoutes 后端返回的原始路由数据
|
|
||||||
* @returns 解析后的路由集合
|
|
||||||
*/
|
*/
|
||||||
const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => {
|
const transformRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteRecordRaw[] => {
|
||||||
const parsedRoutes: RouteRecordRaw[] = [];
|
return routes.map((route) => {
|
||||||
|
const { component, children, ...args } = route;
|
||||||
|
|
||||||
rawRoutes.forEach((route) => {
|
// 处理组件:顶层或非Layout保留组件,中间层Layout设为undefined
|
||||||
const normalizedRoute = { ...route } as RouteRecordRaw;
|
const processedComponent = isTopLevel || component !== "Layout" ? component : undefined;
|
||||||
|
|
||||||
if (!normalizedRoute.component) {
|
const normalizedRoute = { ...args } as RouteRecordRaw;
|
||||||
// 如果没有组件,则将组件设置为 undefined 防止404 例如(多级菜单的父菜单)
|
|
||||||
|
if (!processedComponent) {
|
||||||
|
// 多级菜单的父级菜单,不需要组件
|
||||||
normalizedRoute.component = undefined;
|
normalizedRoute.component = undefined;
|
||||||
} else {
|
} else {
|
||||||
// 处理组件路径
|
// 动态导入组件,Layout特殊处理,找不到组件时返回404
|
||||||
normalizedRoute.component =
|
normalizedRoute.component =
|
||||||
normalizedRoute.component?.toString() === "Layout"
|
processedComponent === "Layout"
|
||||||
? Layout
|
? Layout
|
||||||
: modules[`../../views/${normalizedRoute.component}.vue`] ||
|
: modules[`../../views/${processedComponent}.vue`] ||
|
||||||
modules[`../../views/error/404.vue`]; // 找不到页面时,返回404页面
|
modules[`../../views/error/404.vue`];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 递归解析子路由
|
// 递归处理子路由
|
||||||
if (normalizedRoute.children) {
|
if (children && children.length > 0) {
|
||||||
normalizedRoute.children = parseDynamicRoutes(route.children);
|
normalizedRoute.children = transformRoutes(children, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedRoutes.push(normalizedRoute);
|
return normalizedRoute;
|
||||||
});
|
|
||||||
|
|
||||||
return parsedRoutes;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 路由处理函数
|
|
||||||
* - 去除中间层路由 `component: Layout` 的 `component` 属性
|
|
||||||
* @param routes 路由数组
|
|
||||||
* @param isTopLevel 是否是顶层路由
|
|
||||||
*/
|
|
||||||
const processRoutes = (routes: RouteVO[], isTopLevel: boolean = true): RouteVO[] => {
|
|
||||||
return routes.map(({ component, children, ...args }) => {
|
|
||||||
return {
|
|
||||||
...args,
|
|
||||||
component: isTopLevel || component !== "Layout" ? component : undefined,
|
|
||||||
// 递归处理children,标记为非顶层 todo 原样返回 children(undefined)
|
|
||||||
children: children && children.length > 0 ? processRoutes(children, false) : children,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/** 非组件环境使用权限store */
|
||||||
* 导出此hook函数用于在非组件环境(如其他store、工具函数等)中获取权限store实例
|
|
||||||
*
|
|
||||||
* 在组件中可直接使用usePermissionStore(),但在组件外部需要传入store实例
|
|
||||||
* 此函数简化了这个过程,避免每次都手动传入store参数
|
|
||||||
*/
|
|
||||||
export function usePermissionStoreHook() {
|
export function usePermissionStoreHook() {
|
||||||
return usePermissionStore(store);
|
return usePermissionStore(store);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Storage } from "./storage";
|
import { Storage } from "./storage";
|
||||||
import { AUTH_KEYS } from "@/constants";
|
import { AUTH_KEYS, ROLE_ROOT } from "@/constants";
|
||||||
import { useUserStoreHook } from "@/store/modules/user-store";
|
import { useUserStoreHook } from "@/store/modules/user-store";
|
||||||
import router from "@/router";
|
import router from "@/router";
|
||||||
|
|
||||||
@@ -44,6 +44,23 @@ export const AuthStorage = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限判断
|
||||||
|
*/
|
||||||
|
export function hasPerm(value: string | string[], type: "button" | "role" = "button"): boolean {
|
||||||
|
const { roles, perms } = useUserStoreHook().userInfo;
|
||||||
|
|
||||||
|
// 超级管理员拥有所有权限
|
||||||
|
if (type === "button" && roles.includes(ROLE_ROOT)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auths = type === "button" ? perms : roles;
|
||||||
|
return typeof value === "string"
|
||||||
|
? auths.includes(value)
|
||||||
|
: value.some((perm) => auths.includes(perm));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重定向到登录页面
|
* 重定向到登录页面
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user