fix: 🐛 修复登录成功路由无法跳转问题和相关代码优化
This commit is contained in:
@@ -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); // 设置混合布局左侧菜单
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听路由变化,同步更新顶部菜单和左侧菜单的激活状态
|
// 监听路由变化,同步更新顶部菜单和左侧菜单的激活状态
|
||||||
|
|||||||
@@ -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(() => {
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -1,169 +1,87 @@
|
|||||||
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();
|
||||||
|
|
||||||
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") {
|
if (to.path === "/login") {
|
||||||
next({ path: "/" });
|
next({ path: "/" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理已登录用户的路由访问
|
// 已登录用户的正常访问
|
||||||
await handleAuthenticatedUser(to, from, next);
|
const permissionStore = usePermissionStore();
|
||||||
} else {
|
const userStore = useUserStore();
|
||||||
console.log("❌ User not logged in");
|
|
||||||
|
|
||||||
// 未登录用户的处理
|
// 确保用户信息已加载
|
||||||
if (whiteList.includes(to.path)) {
|
if (!userStore.userInfo.username) {
|
||||||
next();
|
await userStore.getUserInfo();
|
||||||
} else {
|
|
||||||
redirectToLogin(to, next);
|
|
||||||
NProgress.done();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保动态路由已生成
|
||||||
|
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(() => {
|
router.afterEach(() => {
|
||||||
NProgress.done();
|
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<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 {
|
|
||||||
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<string, string>);
|
|
||||||
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") {
|
export function hasAuth(value: string | string[], type: "button" | "role" = "button") {
|
||||||
const { roles, perms } = useUserStore().userInfo;
|
const { roles, perms } = useUserStore().userInfo;
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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) {
|
||||||
// 防止浏览器密码自动填充时报错
|
// 防止浏览器密码自动填充时报错
|
||||||
|
|||||||
@@ -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 *;`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user