Files
cn-rdms-web/src/store/modules/route/index.ts
hongawen 387eb41412 fix(auth): 修复令牌过期处理和会话失效通知机制
- 移除 VITE_SERVICE_LOGOUT_CODES 中的 1002023000 状态码
- 将 VITE_SERVICE_EXPIRED_TOKEN_CODES 从 1002023001 改为 1002023000
- 修改 fetchRefreshToken 函数使用 params 传递 refreshToken 并设置 skipAuth
- 添加 skipAuth 配置选项避免给公开接口带上过期 access 头
- 实现 notifySessionExpired 函数确保并发请求只弹一次会话失效提示
- 在登录成功后复位会话失效标志以支持下次正常提示
- 更新 handleExpiredRequest 使用 refreshTokenPromise 替代 refreshTokenFn
2026-05-18 08:29:51 +08:00

374 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { computed, nextTick, ref, shallowRef } from 'vue';
import type { RouteRecordRaw } from 'vue-router';
import { defineStore } from 'pinia';
import { useBoolean } from '@sa/hooks';
import type { CustomRoute, ElegantConstRoute, LastLevelRouteKey, RouteKey, RouteMap } from '@elegant-router/types';
import { fetchGetUserRoutes, fetchIsRouteExist } from '@/service/api';
import { SetupStoreId } from '@/enum';
import { getRouteName, getRoutePath } from '@/router/elegant/transform';
import { getGlobalRouter } from '@/router/instance';
import { useAuthStore } from '../auth';
import { useDictStore } from '../dict';
import { useTabStore } from '../tab';
import {
filterAuthRoutesByRoles,
getBreadcrumbsByRoute,
getCacheRouteNames,
getGlobalMenusByAuthRoutes,
getSelectedMenuKeyPathByKey,
isRouteExistByRouteName,
sortRoutesByOrder,
transformMenuToSearchMenus,
updateLocaleOfGlobalMenus
} from './shared';
type RouteModule = typeof import('@/router/routes');
async function loadRouteModule(): Promise<RouteModule> {
return import('@/router/routes');
}
function createRootRoute(redirect: string): CustomRoute {
return {
name: 'root',
path: '/',
redirect,
meta: {
title: 'root',
constant: true
}
};
}
export const useRouteStore = defineStore(SetupStoreId.Route, () => {
const authStore = useAuthStore();
const dictStore = useDictStore();
const tabStore = useTabStore();
const { bool: isInitConstantRoute, setBool: setIsInitConstantRoute } = useBoolean();
const { bool: isInitAuthRoute, setBool: setIsInitAuthRoute } = useBoolean();
/**
* 权限路由模式
*
* 建议开发环境先使用 static生产环境再切换到 dynamic。
* 在 static 模式下,权限路由由前端结合 "@elegant-router/vue" 自动生成。
*/
const authRouteMode = ref(import.meta.env.VITE_AUTH_ROUTE_MODE);
/** 首页路由 key */
const routeHome = ref(import.meta.env.VITE_ROUTE_HOME);
/**
* 设置首页路由
*
* @param routeKey 路由 key
*/
function setRouteHome(routeKey: LastLevelRouteKey) {
routeHome.value = routeKey;
}
/** 常量路由 */
const constantRoutes = shallowRef<ElegantConstRoute[]>([]);
function addConstantRoutes(routes: ElegantConstRoute[]) {
const constantRoutesMap = new Map<string, ElegantConstRoute>([]);
routes.forEach(route => {
constantRoutesMap.set(route.name, route);
});
constantRoutes.value = Array.from(constantRoutesMap.values());
}
/** 权限路由 */
const authRoutes = shallowRef<ElegantConstRoute[]>([]);
function addAuthRoutes(routes: ElegantConstRoute[]) {
const authRoutesMap = new Map<string, ElegantConstRoute>([]);
routes.forEach(route => {
authRoutesMap.set(route.name, route);
});
authRoutes.value = Array.from(authRoutesMap.values());
}
const removeRouteFns: (() => void)[] = [];
/** 全局菜单 */
const menus = ref<App.Global.Menu[]>([]);
const searchMenus = computed(() => transformMenuToSearchMenus(menus.value));
/** 生成全局菜单 */
function getGlobalMenus(routes: ElegantConstRoute[]) {
menus.value = getGlobalMenusByAuthRoutes(routes);
}
/** 根据当前语言更新全局菜单文案 */
function updateGlobalMenusByLocale() {
menus.value = updateLocaleOfGlobalMenus(menus.value);
}
/** 需要缓存的路由 */
const cacheRoutes = ref<RouteKey[]>([]);
/**
* 排除缓存的路由
*
* 用于重置指定路由缓存
*/
const excludeCacheRoutes = ref<RouteKey[]>([]);
/**
* 提取需要缓存的路由
*
* @param routes Vue 路由
*/
function getCacheRoutes(routes: RouteRecordRaw[]) {
cacheRoutes.value = getCacheRouteNames(routes);
}
/**
* 重置路由缓存
*
* @default router.currentRoute.value.name 当前路由名
* @param routeKey
*/
async function resetRouteCache(routeKey?: RouteKey) {
const routeName = routeKey || (getGlobalRouter().currentRoute.value.name as RouteKey);
excludeCacheRoutes.value.push(routeName);
await nextTick();
excludeCacheRoutes.value = [];
}
/** 全局面包屑 */
const breadcrumbs = computed(() => getBreadcrumbsByRoute(getGlobalRouter().currentRoute.value, menus.value));
/** 重置 store */
async function resetStore() {
// setup store 没有内置 $reset需要显式重置内部状态。
// 否则 isInitConstantRoute / isInitAuthRoute 一直停在 true导致下面 initConstantRoute 早返,
// 路由被 resetVueRoutes 摘掉后无法重新注册,菜单和导航都会失效。
setIsInitConstantRoute(false);
setIsInitAuthRoute(false);
constantRoutes.value = [];
authRoutes.value = [];
menus.value = [];
cacheRoutes.value = [];
excludeCacheRoutes.value = [];
resetVueRoutes();
// 重置后需要重新初始化常量路由
await initConstantRoute();
}
/** 重置已注册到 Vue Router 的路由 */
function resetVueRoutes() {
removeRouteFns.forEach(fn => fn());
removeRouteFns.length = 0;
}
/** 初始化常量路由 */
async function initConstantRoute() {
if (isInitConstantRoute.value) return;
const { createStaticRoutes } = await loadRouteModule();
const staticRoute = createStaticRoutes();
addConstantRoutes(staticRoute.constantRoutes);
await handleConstantAndAuthRoutes();
setIsInitConstantRoute(true);
tabStore.initHomeTab();
}
/** 初始化权限路由 */
async function initAuthRoute() {
// 先确保用户信息已经初始化
if (!authStore.userInfo.userId) {
await authStore.initUserInfo();
}
await dictStore.initDictCache();
if (authRouteMode.value === 'static') {
await initStaticAuthRoute();
} else {
await initDynamicAuthRoute();
}
tabStore.initHomeTab();
}
/** 初始化静态权限路由 */
async function initStaticAuthRoute() {
const { createStaticRoutes } = await loadRouteModule();
const { authRoutes: staticAuthRoutes } = createStaticRoutes();
if (authStore.isStaticSuper) {
addAuthRoutes(staticAuthRoutes);
} else {
const filteredAuthRoutes = filterAuthRoutesByRoles(staticAuthRoutes, authStore.userInfo.roles);
addAuthRoutes(filteredAuthRoutes);
}
await handleConstantAndAuthRoutes();
setIsInitAuthRoute(true);
}
/** 初始化动态权限路由 */
async function initDynamicAuthRoute() {
const { data, error } = await fetchGetUserRoutes();
if (!error) {
const { routes, home } = data;
addAuthRoutes(routes);
await handleConstantAndAuthRoutes();
setRouteHome(home);
await handleUpdateRootRouteRedirect(home);
setIsInitAuthRoute(true);
} else {
// 获取用户路由失败时,重置认证状态
authStore.resetStore();
}
}
/** 统一处理常量路由和权限路由 */
async function handleConstantAndAuthRoutes() {
const { getAuthVueRoutes } = await loadRouteModule();
// 常量路由优先:动态权限路由中与常量路由 name 重复的项剔除,避免菜单出现重复入口(如 workbench
const constantRouteNames = new Set(constantRoutes.value.map(route => route.name));
const dedupedAuthRoutes = authRoutes.value.filter(route => !constantRouteNames.has(route.name));
const allRoutes = [...constantRoutes.value, ...dedupedAuthRoutes];
const sortRoutes = sortRoutesByOrder(allRoutes);
const vueRoutes = getAuthVueRoutes(sortRoutes);
resetVueRoutes();
addRoutesToVueRouter(vueRoutes);
getGlobalMenus(sortRoutes);
getCacheRoutes(vueRoutes);
}
/**
* 将路由注册到 Vue Router
*
* @param routes Vue 路由
*/
function addRoutesToVueRouter(routes: RouteRecordRaw[]) {
routes.forEach(route => {
const removeFn = getGlobalRouter().addRoute(route);
addRemoveRouteFn(removeFn);
});
}
/**
* 记录移除路由的方法
*
* @param fn
*/
function addRemoveRouteFn(fn: () => void) {
removeRouteFns.push(fn);
}
/**
* 在动态权限路由模式下更新根路由的重定向
*
* @param redirectKey 重定向目标路由 key
*/
async function handleUpdateRootRouteRedirect(redirectKey: LastLevelRouteKey) {
const redirect = getRoutePath(redirectKey);
if (redirect) {
const { getAuthVueRoutes } = await loadRouteModule();
const rootRoute = createRootRoute(redirect);
const router = getGlobalRouter();
router.removeRoute(rootRoute.name);
const [rootVueRoute] = getAuthVueRoutes([rootRoute]);
router.addRoute(rootVueRoute);
}
}
/**
* 判断权限路由是否存在
*
* @param routePath 路由路径
*/
async function getIsAuthRouteExist(routePath: RouteMap[RouteKey]) {
const routeName = getRouteName(routePath);
if (!routeName) {
return false;
}
if (authRouteMode.value === 'static') {
const { createStaticRoutes } = await loadRouteModule();
const { authRoutes: staticAuthRoutes } = createStaticRoutes();
return isRouteExistByRouteName(routeName, staticAuthRoutes);
}
const { data } = await fetchIsRouteExist(routeName);
return data;
}
/**
* 获取当前选中菜单的 key 路径
*
* @param selectedKey 当前选中的菜单 key
*/
function getSelectedMenuKeyPath(selectedKey: string) {
return getSelectedMenuKeyPathByKey(selectedKey, menus.value);
}
async function onRouteSwitchWhenLoggedIn() {
await authStore.initUserInfo();
}
async function onRouteSwitchWhenNotLoggedIn() {
// 这里可以放未登录情况下也需要执行的全局初始化逻辑
}
return {
resetStore,
routeHome,
authRoutes,
menus,
searchMenus,
updateGlobalMenusByLocale,
cacheRoutes,
excludeCacheRoutes,
resetRouteCache,
breadcrumbs,
initConstantRoute,
isInitConstantRoute,
initAuthRoute,
isInitAuthRoute,
setIsInitAuthRoute,
getIsAuthRouteExist,
getSelectedMenuKeyPath,
onRouteSwitchWhenLoggedIn,
onRouteSwitchWhenNotLoggedIn
};
});