fix: 🐛 修复登录成功路由无法跳转问题和相关代码优化

This commit is contained in:
Ray.Hao
2025-08-04 15:04:11 +08:00
parent 4a224bbd0e
commit 0179b5c470
7 changed files with 96 additions and 221 deletions

View File

@@ -102,12 +102,12 @@ const updateMenuState = (topMenuPath: string, skipNavigation = false) => {
// 不相同才更新,避免重复操作 // 不相同才更新,避免重复操作
if (topMenuPath !== appStore.activeTopMenuPath) { if (topMenuPath !== appStore.activeTopMenuPath) {
appStore.activeTopMenu(topMenuPath); // 设置激活的顶部菜单 appStore.activeTopMenu(topMenuPath); // 设置激活的顶部菜单
permissionStore.updateSideMenu(topMenuPath); // 更新左侧菜单 permissionStore.setMixLayoutSideMenus(topMenuPath); // 设置混合布局左侧菜单
} }
// 如果是点击菜单且状态已变更,才进行导航 // 如果是点击菜单且状态已变更,才进行导航
if (!skipNavigation) { if (!skipNavigation) {
navigateToFirstLeftMenu(permissionStore.sideMenuRoutes); // 跳转到左侧第一个菜单 navigateToFirstLeftMenu(permissionStore.mixLayoutSideMenus); // 跳转到左侧第一个菜单
} }
}; };
@@ -145,7 +145,7 @@ onMounted(() => {
? useRoute().path.match(/^\/[^/]+/)?.[0] || "/" ? useRoute().path.match(/^\/[^/]+/)?.[0] || "/"
: "/"; : "/";
appStore.activeTopMenu(currentTopMenuPath); // 设置激活的顶部菜单 appStore.activeTopMenu(currentTopMenuPath); // 设置激活的顶部菜单
permissionStore.updateSideMenu(currentTopMenuPath); // 更新左侧菜单 permissionStore.setMixLayoutSideMenus(currentTopMenuPath); // 设置混合布局左侧菜单
}); });
// 监听路由变化,同步更新顶部菜单和左侧菜单的激活状态 // 监听路由变化,同步更新顶部菜单和左侧菜单的激活状态

View File

@@ -1,4 +1,3 @@
import { computed, watch } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { useAppStore, usePermissionStore } from "@/store"; import { useAppStore, usePermissionStore } from "@/store";
@@ -17,7 +16,7 @@ export function useLayoutMenu() {
const routes = computed(() => permissionStore.routes); const routes = computed(() => permissionStore.routes);
// 混合布局左侧菜单路由 // 混合布局左侧菜单路由
const sideMenuRoutes = computed(() => permissionStore.sideMenuRoutes); const sideMenuRoutes = computed(() => permissionStore.mixLayoutSideMenus);
// 当前激活的菜单 // 当前激活的菜单
const activeMenu = computed(() => { const activeMenu = computed(() => {

View File

@@ -142,7 +142,7 @@ watch(
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
appStore.activeTopMenu(topMenuPath); appStore.activeTopMenu(topMenuPath);
permissionStore.updateSideMenu(topMenuPath); permissionStore.setMixLayoutSideMenus(topMenuPath);
} }
}, },
{ immediate: true } { immediate: true }

View File

@@ -1,90 +1,63 @@
import type { NavigationGuardNext, RouteLocationNormalized, RouteRecordRaw } from "vue-router"; import type { RouteRecordRaw } from "vue-router";
import NProgress from "@/utils/nprogress"; import NProgress from "@/utils/nprogress";
import { Auth } from "@/utils/auth"; import { Auth } from "@/utils/auth";
import router from "@/router"; import router from "@/router";
import { usePermissionStore, useUserStore } from "@/store"; import { usePermissionStore, useUserStore } from "@/store";
import { ROLE_ROOT } from "@/constants"; import { ROLE_ROOT } from "@/constants";
// 路由生成锁,防止重复生成
let isGeneratingRoutes = false;
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 {
const isLoggedIn = Auth.isLoggedIn(); 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") { if (to.path === "/login") {
next({ path: "/" }); next({ path: "/" });
return; return;
} }
// 处理已登录用户的路由访问 // 已登录用户的正常访问
await handleAuthenticatedUser(to, from, next);
} else {
console.log("❌ User not logged in");
// 未登录用户的处理
if (whiteList.includes(to.path)) {
next();
} else {
redirectToLogin(to, next);
NProgress.done();
}
}
});
// 后置守卫,确保进度条关闭
router.afterEach(() => {
NProgress.done();
});
}
/**
* 处理已登录用户的路由访问
*/
async function handleAuthenticatedUser(
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardNext
) {
const permissionStore = usePermissionStore(); const permissionStore = usePermissionStore();
const userStore = useUserStore(); const userStore = useUserStore();
try { // 确保用户信息已加载
// 检查用户信息是否存在
if (!userStore.userInfo.username) { if (!userStore.userInfo.username) {
await userStore.getUserInfo(); await userStore.getUserInfo();
} }
// 检查路由是否已生成 // 确保动态路由已生成
if (!permissionStore.routesLoaded) { if (!permissionStore.isDynamicRoutesGenerated) {
// 防止重复生成路由 const dynamicRoutes = await permissionStore.generateRoutes();
if (isGeneratingRoutes) { dynamicRoutes.forEach((route: RouteRecordRaw) => {
console.log("⏳ Routes already generating, waiting..."); router.addRoute(route);
// 等待当前路由生成完成 });
await waitForRoutesGeneration(permissionStore); // 路由刚生成,重新导航
} else {
await generateAndAddRoutes(permissionStore);
}
// 路由生成完成后,重新导航到目标路由
next({ ...to, replace: true }); next({ ...to, replace: true });
return; return;
} }
// 路由已加载,检查路由是否存在 // 检查路由是否存在
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;
@@ -93,75 +66,20 @@ async function handleAuthenticatedUser(
next(); next();
} catch (error) { } catch (error) {
console.error("❌ Route guard error:", 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<void> {
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 { try {
await useUserStore().resetAllState(); await useUserStore().resetAllState();
redirectToLogin(to, next);
} catch (resetError) { } catch (resetError) {
console.error("❌ Failed to reset user state:", resetError); console.error("❌ Failed to reset user state:", resetError);
// 强制跳转到登录页 }
next("/login"); next("/login");
} finally {
NProgress.done(); NProgress.done();
} }
} });
/** router.afterEach(() => {
* 重定向到登录页 NProgress.done();
*/ });
function redirectToLogin(to: RouteLocationNormalized, next: NavigationGuardNext) {
const params = new URLSearchParams(to.query as Record<string, string>);
const queryString = params.toString();
const redirect = queryString ? `${to.path}?${queryString}` : to.path;
next(`/login?redirect=${encodeURIComponent(redirect)}`);
} }
/** 判断是否有权限 */ /** 判断是否有权限 */

View File

@@ -8,78 +8,64 @@ 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", () => {
// 存储所有路由,包括静态路由动态路由 // 所有路由静态路由 + 动态路由
const routes = ref<RouteRecordRaw[]>([]); const routes = ref<RouteRecordRaw[]>([]);
// 混合模式左侧菜单路由 // 混合布局的左侧菜单路由
const sideMenuRoutes = ref<RouteRecordRaw[]>([]); const mixLayoutSideMenus = ref<RouteRecordRaw[]>([]);
// 路由是否加载完 // 动态路由是否已生
const routesLoaded = ref(false); const isDynamicRoutesGenerated = ref(false);
/** /**
* 获取后台动态路由数据,解析并注册到全局路由 * 生成动态路由
*
* @returns Promise<RouteRecordRaw[]> 解析后的动态路由列表
*/ */
function generateRoutes() { async function generateRoutes(): Promise<RouteRecordRaw[]> {
return new Promise<RouteRecordRaw[]>((resolve, reject) => { try {
MenuAPI.getRoutes() const data = await MenuAPI.getRoutes();
.then((data) => {
const dynamicRoutes = parseDynamicRoutes(data); const dynamicRoutes = parseDynamicRoutes(data);
routes.value = [...constantRoutes, ...dynamicRoutes]; routes.value = [...constantRoutes, ...dynamicRoutes];
routesLoaded.value = true; isDynamicRoutesGenerated.value = true;
resolve(dynamicRoutes); return dynamicRoutes;
}) } catch (error) {
.catch((error) => {
console.error("❌ Failed to generate routes:", error); console.error("❌ Failed to generate routes:", error);
isDynamicRoutesGenerated.value = false;
// 即使失败也要设置状态,避免无限重试 throw error;
routesLoaded.value = false; }
reject(error);
});
});
} }
/** /**
* 根据父菜单路径设置侧边菜单 * 设置混合布局的左侧菜单
*
* @param parentPath 父菜单的路径,用于查找对应的菜单项
*/ */
const updateSideMenu = (parentPath: string) => { const setMixLayoutSideMenus = (parentPath: string) => {
const matchedItem = routes.value.find((item) => item.path === parentPath); const parentMenu = routes.value.find((item) => item.path === parentPath);
if (matchedItem && matchedItem.children) { mixLayoutSideMenus.value = parentMenu?.children || [];
sideMenuRoutes.value = matchedItem.children;
}
}; };
/** /**
* 重置路由 * 重置路由状态
*/ */
const resetRouter = () => { const resetRouter = () => {
// 创建常量路由名称集合用于O(1)时间复杂度的查找 // 移除动态路由
const constantRouteNames = new Set(constantRoutes.map((route) => route.name).filter(Boolean)); const constantRouteNames = new Set(constantRoutes.map((route) => route.name).filter(Boolean));
// 从 router 实例中移除动态路由
routes.value.forEach((route) => { routes.value.forEach((route) => {
if (route.name && !constantRouteNames.has(route.name)) { if (route.name && !constantRouteNames.has(route.name)) {
router.removeRoute(route.name); router.removeRoute(route.name);
} }
}); });
// 重置为仅包含常量路由 // 重置状态
routes.value = [...constantRoutes]; routes.value = [...constantRoutes];
sideMenuRoutes.value = []; mixLayoutSideMenus.value = [];
routesLoaded.value = false; isDynamicRoutesGenerated.value = false;
}; };
return { return {
routes, routes,
sideMenuRoutes, mixLayoutSideMenus,
routesLoaded, isDynamicRoutesGenerated,
generateRoutes, generateRoutes,
updateSideMenu, setMixLayoutSideMenus,
resetRouter, resetRouter,
}; };
}); });

View File

@@ -111,8 +111,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from "element-plus"; import type { FormInstance } from "element-plus";
import { LocationQuery, RouteLocationRaw, useRoute } from "vue-router";
import { useI18n } from "vue-i18n";
import AuthAPI, { type LoginFormData } from "@/api/auth.api"; import AuthAPI, { type LoginFormData } from "@/api/auth.api";
import router from "@/router"; import router from "@/router";
import { useUserStore } from "@/store"; import { useUserStore } from "@/store";
@@ -202,10 +200,11 @@ async function handleLoginSubmit() {
// 3. 获取用户信息(包含用户角色,用于路由生成) // 3. 获取用户信息(包含用户角色,用于路由生成)
await userStore.getUserInfo(); await userStore.getUserInfo();
// 4. 登录成功,让路由守卫处理跳转逻辑 // 4. 登录成功,简单跳转,让路由守卫处理后续逻辑
const redirect = resolveRedirectTarget(route.query); const redirectPath = (route.query.redirect as string) || "/";
await router.replace(redirect); // 使用push而不是replace避免与路由守卫冲突
await router.push(decodeURIComponent(redirectPath));
} catch (error) { } catch (error) {
// 5. 统一错误处理 // 5. 统一错误处理
getCaptcha(); // 刷新验证码 getCaptcha(); // 刷新验证码
@@ -215,32 +214,6 @@ async function handleLoginSubmit() {
} }
} }
/**
* 解析重定向目标
*
* @param query 路由查询参数
* @returns 标准化后的路由地址
*/
function resolveRedirectTarget(query: LocationQuery): RouteLocationRaw {
// 默认跳转路径
const defaultPath = "/";
// 获取原始重定向路径
const rawRedirect = (query.redirect as string) || defaultPath;
try {
// 6. 使用Vue Router解析路径
const resolved = router.resolve(rawRedirect);
return {
path: resolved.path,
query: resolved.query,
};
} catch {
// 7. 异常处理:返回安全路径
return { path: defaultPath };
}
}
// 检查输入大小写 // 检查输入大小写
function checkCapsLock(event: KeyboardEvent) { function checkCapsLock(event: KeyboardEvent) {
// 防止浏览器密码自动填充时报错 // 防止浏览器密码自动填充时报错

View File

@@ -34,7 +34,6 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
preprocessorOptions: { preprocessorOptions: {
// 定义全局 SCSS 变量 // 定义全局 SCSS 变量
scss: { scss: {
api: "modern-compiler",
additionalData: `@use "@/styles/variables.scss" as *;`, additionalData: `@use "@/styles/variables.scss" as *;`,
}, },
}, },