diff --git a/src/layouts/components/Menu/MixTopMenu.vue b/src/layouts/components/Menu/MixTopMenu.vue index 0709f53b..a3b2620e 100644 --- a/src/layouts/components/Menu/MixTopMenu.vue +++ b/src/layouts/components/Menu/MixTopMenu.vue @@ -102,12 +102,12 @@ const updateMenuState = (topMenuPath: string, skipNavigation = false) => { // 不相同才更新,避免重复操作 if (topMenuPath !== appStore.activeTopMenuPath) { appStore.activeTopMenu(topMenuPath); // 设置激活的顶部菜单 - permissionStore.updateSideMenu(topMenuPath); // 更新左侧菜单 + permissionStore.setMixLayoutSideMenus(topMenuPath); // 设置混合布局左侧菜单 } // 如果是点击菜单且状态已变更,才进行导航 if (!skipNavigation) { - navigateToFirstLeftMenu(permissionStore.sideMenuRoutes); // 跳转到左侧第一个菜单 + navigateToFirstLeftMenu(permissionStore.mixLayoutSideMenus); // 跳转到左侧第一个菜单 } }; @@ -145,7 +145,7 @@ onMounted(() => { ? useRoute().path.match(/^\/[^/]+/)?.[0] || "/" : "/"; appStore.activeTopMenu(currentTopMenuPath); // 设置激活的顶部菜单 - permissionStore.updateSideMenu(currentTopMenuPath); // 更新左侧菜单 + permissionStore.setMixLayoutSideMenus(currentTopMenuPath); // 设置混合布局左侧菜单 }); // 监听路由变化,同步更新顶部菜单和左侧菜单的激活状态 diff --git a/src/layouts/composables/useLayoutMenu.ts b/src/layouts/composables/useLayoutMenu.ts index 850c707c..cebe15fa 100644 --- a/src/layouts/composables/useLayoutMenu.ts +++ b/src/layouts/composables/useLayoutMenu.ts @@ -1,4 +1,3 @@ -import { computed, watch } from "vue"; import { useRoute } from "vue-router"; import { useAppStore, usePermissionStore } from "@/store"; @@ -17,7 +16,7 @@ export function useLayoutMenu() { const routes = computed(() => permissionStore.routes); // 混合布局左侧菜单路由 - const sideMenuRoutes = computed(() => permissionStore.sideMenuRoutes); + const sideMenuRoutes = computed(() => permissionStore.mixLayoutSideMenus); // 当前激活的菜单 const activeMenu = computed(() => { diff --git a/src/layouts/views/MixLayout.vue b/src/layouts/views/MixLayout.vue index 5756ec20..7f9c111f 100644 --- a/src/layouts/views/MixLayout.vue +++ b/src/layouts/views/MixLayout.vue @@ -142,7 +142,7 @@ watch( const permissionStore = usePermissionStore(); appStore.activeTopMenu(topMenuPath); - permissionStore.updateSideMenu(topMenuPath); + permissionStore.setMixLayoutSideMenus(topMenuPath); } }, { immediate: true } diff --git a/src/plugins/permission.ts b/src/plugins/permission.ts index 361d0bb1..2890b6df 100644 --- a/src/plugins/permission.ts +++ b/src/plugins/permission.ts @@ -1,169 +1,87 @@ -import type { NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router"; +import type { RouteRecordRaw } from "vue-router"; import NProgress from "@/utils/nprogress"; import { Auth } from "@/utils/auth"; import router from "@/router"; import { usePermissionStore, useUserStore } from "@/store"; import { ROLE_ROOT } from "@/constants"; -// 路由生成锁,防止重复生成 -let isGeneratingRoutes = false; - export function setupPermission() { - // 白名单路由 - const whiteList = ["/login"]; + const whiteList = ["/login"]; // 无需登录的页面 router.beforeEach(async (to, from, next) => { NProgress.start(); - const isLoggedIn = Auth.isLoggedIn(); + try { + const isLoggedIn = Auth.isLoggedIn(); - if (isLoggedIn) { - // 如果已登录但访问登录页,重定向到首页 + // 未登录处理 + if (!isLoggedIn) { + if (whiteList.includes(to.path)) { + next(); + } else { + next(`/login?redirect=${encodeURIComponent(to.fullPath)}`); + NProgress.done(); + } + return; + } + + // 已登录且访问登录页,重定向到首页 if (to.path === "/login") { next({ path: "/" }); return; } - // 处理已登录用户的路由访问 - await handleAuthenticatedUser(to, from, next); - } else { - console.log("❌ User not logged in"); + // 已登录用户的正常访问 + const permissionStore = usePermissionStore(); + const userStore = useUserStore(); - // 未登录用户的处理 - if (whiteList.includes(to.path)) { - next(); - } else { - redirectToLogin(to, next); - NProgress.done(); + // 确保用户信息已加载 + if (!userStore.userInfo.username) { + await userStore.getUserInfo(); } + + // 确保动态路由已生成 + if (!permissionStore.isDynamicRoutesGenerated) { + const dynamicRoutes = await permissionStore.generateRoutes(); + dynamicRoutes.forEach((route: RouteRecordRaw) => { + router.addRoute(route); + }); + // 路由刚生成,重新导航 + next({ ...to, replace: true }); + return; + } + + // 检查路由是否存在 + if (to.matched.length === 0) { + next("/404"); + return; + } + + // 设置页面标题 + const title = (to.params.title as string) || (to.query.title as string); + if (title) { + to.meta.title = title; + } + + next(); + } catch (error) { + console.error("❌ Route guard error:", error); + // 出错时清理状态并重定向到登录页 + try { + await useUserStore().resetAllState(); + } catch (resetError) { + console.error("❌ Failed to reset user state:", resetError); + } + next("/login"); + NProgress.done(); } }); - // 后置守卫,确保进度条关闭 router.afterEach(() => { NProgress.done(); }); } -/** - * 处理已登录用户的路由访问 - */ -async function handleAuthenticatedUser( - to: RouteLocationNormalized, - from: RouteLocationNormalized, - next: NavigationGuardNext -) { - const permissionStore = usePermissionStore(); - const userStore = useUserStore(); - - try { - // 检查用户信息是否存在 - if (!userStore.userInfo.username) { - await userStore.getUserInfo(); - } - - // 检查路由是否已生成 - if (!permissionStore.routesLoaded) { - // 防止重复生成路由 - if (isGeneratingRoutes) { - console.log("⏳ Routes already generating, waiting..."); - // 等待当前路由生成完成 - await waitForRoutesGeneration(permissionStore); - } else { - await generateAndAddRoutes(permissionStore); - } - - // 路由生成完成后,重新导航到目标路由 - next({ ...to, replace: true }); - return; - } - - // 路由已加载,检查路由是否存在 - if (to.matched.length === 0) { - next("/404"); - return; - } - - // 动态设置页面标题 - const title = (to.params.title as string) || (to.query.title as string); - if (title) { - to.meta.title = title; - } - - next(); - } catch (error) { - console.error("❌ Route guard error:", error); - - // 出错时重置状态并重定向到登录页 - await resetUserStateAndRedirect(to, next); - } -} - -/** - * 生成并添加动态路由 - */ -async function generateAndAddRoutes(permissionStore: any) { - isGeneratingRoutes = true; - - try { - const dynamicRoutes = await permissionStore.generateRoutes(); - - // 添加路由到路由器 - dynamicRoutes.forEach((route: RouteRecordRaw) => { - router.addRoute(route); - }); - } finally { - isGeneratingRoutes = false; - } -} - -/** - * 等待路由生成完成 - */ -async function waitForRoutesGeneration(permissionStore: any): Promise { - return new Promise((resolve) => { - const checkInterval = setInterval(() => { - if (!isGeneratingRoutes && permissionStore.routesLoaded) { - clearInterval(checkInterval); - resolve(); - } - }, 50); - - // 超时保护,最多等待5秒 - setTimeout(() => { - clearInterval(checkInterval); - resolve(); - }, 5000); - }); -} - -/** - * 重置用户状态并重定向到登录页 - */ -async function resetUserStateAndRedirect(to: RouteLocationNormalized, next: NavigationGuardNext) { - try { - await useUserStore().resetAllState(); - redirectToLogin(to, next); - } catch (resetError) { - console.error("❌ Failed to reset user state:", resetError); - // 强制跳转到登录页 - next("/login"); - } finally { - NProgress.done(); - } -} - -/** - * 重定向到登录页 - */ -function redirectToLogin(to: RouteLocationNormalized, next: NavigationGuardNext) { - const params = new URLSearchParams(to.query as Record); - const queryString = params.toString(); - const redirect = queryString ? `${to.path}?${queryString}` : to.path; - - next(`/login?redirect=${encodeURIComponent(redirect)}`); -} - /** 判断是否有权限 */ export function hasAuth(value: string | string[], type: "button" | "role" = "button") { const { roles, perms } = useUserStore().userInfo; diff --git a/src/store/modules/permission.store.ts b/src/store/modules/permission.store.ts index 920adc25..315a9254 100644 --- a/src/store/modules/permission.store.ts +++ b/src/store/modules/permission.store.ts @@ -8,78 +8,64 @@ const modules = import.meta.glob("../../views/**/**.vue"); const Layout = () => import("@/layouts/index.vue"); export const usePermissionStore = defineStore("permission", () => { - // 存储所有路由,包括静态路由和动态路由 + // 所有路由(静态路由 + 动态路由) const routes = ref([]); - // 混合模式左侧菜单路由 - const sideMenuRoutes = ref([]); - // 路由是否加载完成 - const routesLoaded = ref(false); + // 混合布局的左侧菜单路由 + const mixLayoutSideMenus = ref([]); + // 动态路由是否已生成 + const isDynamicRoutesGenerated = ref(false); /** - * 获取后台动态路由数据,解析并注册到全局路由 - * - * @returns Promise 解析后的动态路由列表 + * 生成动态路由 */ - function generateRoutes() { - return new Promise((resolve, reject) => { - MenuAPI.getRoutes() - .then((data) => { - const dynamicRoutes = parseDynamicRoutes(data); + async function generateRoutes(): Promise { + try { + const data = await MenuAPI.getRoutes(); + const dynamicRoutes = parseDynamicRoutes(data); - routes.value = [...constantRoutes, ...dynamicRoutes]; - routesLoaded.value = true; + routes.value = [...constantRoutes, ...dynamicRoutes]; + isDynamicRoutesGenerated.value = true; - resolve(dynamicRoutes); - }) - .catch((error) => { - console.error("❌ Failed to generate routes:", error); - - // 即使失败也要设置状态,避免无限重试 - routesLoaded.value = false; - - reject(error); - }); - }); + return dynamicRoutes; + } catch (error) { + console.error("❌ Failed to generate routes:", error); + isDynamicRoutesGenerated.value = false; + throw error; + } } /** - * 根据父菜单路径设置侧边菜单 - * - * @param parentPath 父菜单的路径,用于查找对应的菜单项 + * 设置混合布局的左侧菜单 */ - const updateSideMenu = (parentPath: string) => { - const matchedItem = routes.value.find((item) => item.path === parentPath); - if (matchedItem && matchedItem.children) { - sideMenuRoutes.value = matchedItem.children; - } + const setMixLayoutSideMenus = (parentPath: string) => { + const parentMenu = routes.value.find((item) => item.path === parentPath); + mixLayoutSideMenus.value = parentMenu?.children || []; }; /** - * 重置路由 + * 重置路由状态 */ const resetRouter = () => { - // 创建常量路由名称集合,用于O(1)时间复杂度的查找 + // 移除动态路由 const constantRouteNames = new Set(constantRoutes.map((route) => route.name).filter(Boolean)); - - // 从 router 实例中移除动态路由 routes.value.forEach((route) => { if (route.name && !constantRouteNames.has(route.name)) { router.removeRoute(route.name); } }); - // 重置为仅包含常量路由 + // 重置状态 routes.value = [...constantRoutes]; - sideMenuRoutes.value = []; - routesLoaded.value = false; + mixLayoutSideMenus.value = []; + isDynamicRoutesGenerated.value = false; }; return { routes, - sideMenuRoutes, - routesLoaded, + mixLayoutSideMenus, + isDynamicRoutesGenerated, generateRoutes, - updateSideMenu, + setMixLayoutSideMenus, resetRouter, }; }); diff --git a/src/views/login/components/Login.vue b/src/views/login/components/Login.vue index 0c41ff0f..d55e6291 100644 --- a/src/views/login/components/Login.vue +++ b/src/views/login/components/Login.vue @@ -111,8 +111,6 @@