guard.ts 6.2 KB
import type { Router } from 'vue-router';

import { nextTick } from 'vue';

import { LOGIN_PATH } from '@vben/constants';
import { preferences } from '@vben/preferences';
import { useAccessStore, useTabbarStore, useUserStore } from '@vben/stores';
import { startProgress, stopProgress } from '@vben/utils';

import { accessRoutes, coreRouteNames } from '#/router/routes';
import { useAuthStore } from '#/store';

import { generateAccess } from './access';

function findFirstAccessiblePath(routes: any[]): string {
  for (const route of routes) {
    if (route?.meta?.hideInMenu) {
      continue;
    }
    if (
      typeof route?.redirect === 'string' &&
      route.redirect &&
      route.redirect !== 'noRedirect'
    ) {
      return route.redirect;
    }
    if (Array.isArray(route?.children) && route.children.length > 0) {
      const childPath = findFirstAccessiblePath(route.children);
      if (childPath) {
        return childPath;
      }
    }
    if (typeof route?.path === 'string' && route.path) {
      return route.path;
    }
  }
  return '';
}

/**
 * 通用守卫配置
 * @param router
 */
function setupCommonGuard(router: Router) {
  // 记录已经加载的页面
  const loadedPaths = new Set<string>();

  router.beforeEach((to) => {
    to.meta.loaded = loadedPaths.has(to.path);

    // 页面加载进度条
    if (!to.meta.loaded && preferences.transition.progress) {
      startProgress();
    }
    return true;
  });

  router.afterEach((to) => {
    // 记录页面是否加载,如果已经加载,后续的页面切换动画等效果不在重复执行

    loadedPaths.add(to.path);

    // 关闭页面加载进度条
    if (preferences.transition.progress) {
      stopProgress();
    }
  });
}

/**
 * 权限访问守卫配置
 * @param router
 */
function setupAccessGuard(router: Router) {
  router.beforeEach(async (to, from) => {
    const accessStore = useAccessStore();
    const userStore = useUserStore();
    const authStore = useAuthStore();

    // 基本路由,这些路由不需要进入权限拦截
    if (coreRouteNames.includes(to.name as string)) {
      if (to.path === LOGIN_PATH && accessStore.accessToken) {
        return decodeURIComponent(
          (to.query?.redirect as string) ||
            userStore.userInfo?.homePath ||
            preferences.app.defaultHomePath,
        );
      }
      return true;
    }

    // accessToken 检查
    if (!accessStore.accessToken) {
      // 明确声明忽略权限访问权限,则可以访问
      if (to.meta.ignoreAccess) {
        return true;
      }

      // 没有访问权限,跳转登录页面
      if (to.fullPath !== LOGIN_PATH) {
        return {
          path: LOGIN_PATH,
          // 如不需要,直接删除 query
          query:
            to.fullPath === preferences.app.defaultHomePath
              ? {}
              : { redirect: encodeURIComponent(to.fullPath) },
          // 携带当前跳转的页面,登录后重新跳转该页面
          replace: true,
        };
      }
      return to;
    }

    // 是否已经生成过动态路由
    if (accessStore.isAccessChecked) {
      return true;
    }

    // 生成路由表
    // 当前登录用户拥有的角色标识列表
    const userInfo = userStore.userInfo || (await authStore.fetchUserInfo());
    const userRoles = userInfo.roles ?? [];

    // 生成菜单和路由
    const { accessibleMenus, accessibleRoutes } = await generateAccess({
      roles: userRoles,
      router,
      // 则会在菜单中显示,但是访问会被重定向到403
      routes: accessRoutes,
    });

    // 保存菜单信息和路由信息
    accessStore.setAccessMenus(accessibleMenus);
    accessStore.setAccessRoutes(accessibleRoutes);
    accessStore.setIsAccessChecked(true);
    let redirectPath = (from.query.redirect ??
      (to.path === preferences.app.defaultHomePath
        ? userInfo.homePath || preferences.app.defaultHomePath
        : to.fullPath)) as string;
    const decodedRedirectPath = decodeURIComponent(redirectPath);
    const firstAccessiblePath = findFirstAccessiblePath(accessibleMenus as any[]);
    if (firstAccessiblePath && userInfo.homePath !== firstAccessiblePath) {
      userStore.setUserInfo({
        ...userInfo,
        homePath: firstAccessiblePath,
      });
    }
    if (
      decodedRedirectPath === preferences.app.defaultHomePath &&
      firstAccessiblePath &&
      firstAccessiblePath !== preferences.app.defaultHomePath
    ) {
      redirectPath = firstAccessiblePath;
    }

    return {
      ...router.resolve(decodeURIComponent(redirectPath)),
      replace: true,
    };
  });
}

/**
 * 任意路由切换前先卸载 RouterView,避免报表/Vxe/ECharts 卸载时报 parentNode 为 null,
 * 导致 router.push 后 URL 已变但主区域仍显示上一页。
 */
function setupRouteViewRemountGuard(router: Router) {
  function restoreRouteView() {
    const tabbarStore = useTabbarStore();
    if (tabbarStore.renderRouteView === false) {
      tabbarStore.renderRouteView = true;
    }
  }

  router.beforeEach(async (to, from) => {
    if (from.matched.length > 0 && to.path !== from.path) {
      const tabbarStore = useTabbarStore();
      tabbarStore.renderRouteView = false;
      window.setTimeout(restoreRouteView, 1200);
      await nextTick();
      await new Promise<void>((resolve) => {
        requestAnimationFrame(() => resolve());
      });
      await nextTick();
    }
    return true;
  });

  router.onError(() => {
    restoreRouteView();
  });

  router.afterEach(async (to) => {
    const tabbarStore = useTabbarStore();
    if (tabbarStore.renderRouteView === false) {
      await nextTick();
      await new Promise<void>((resolve) => {
        requestAnimationFrame(() => resolve());
      });
      await nextTick();
      tabbarStore.renderRouteView = true;
    }
    // 导航完成后再次等待一帧,避免报表 RangePicker 卸载异常时新页未挂上
    if (to.matched.length > 0) {
      await nextTick();
    }
  });
}

/**
 * 项目守卫配置
 * @param router
 */
function createRouterGuard(router: Router) {
  /** 通用 */
  setupCommonGuard(router);
  /** 权限访问 */
  setupAccessGuard(router);
  /** 路由切换前强制卸载主内容区 */
  setupRouteViewRemountGuard(router);
}

export { createRouterGuard };