Files
Docker/uniapp/youjiala/src/router/nav-guard.ts
T
2026-10-13 10:47:39 +08:00

136 lines
4.8 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.
/**
* 全局导航守卫(对应旧 WebVue 的 setupLoginGuard + catch-all 404)
*
* uni-app 没有 router.beforeEach,导航拦截靠 uni.addInterceptor:
* invoke 钩子返回 false 时本次导航被静默丢弃(uni-h5 的 queue 见 res === false 直接返回空 thenable,
* 既不执行原调用也不产生 promise rejection),所以取消导航不会给调用方留下未捕获异常。
*
* 覆盖范围(两条链路都要,缺一漏一半):
* 1. 站内跳转:本文件的 addInterceptor,拦住 navigateTo/redirectTo/reLaunch/switchTab;
* 2. 冷启动直达:H5 地址栏输入 / 分享链接 / 小程序编译进某个页面,不经过任何导航 API,
* 拦截器拦不到,必须由页面自己调用 composables/useAuthGuard(页面级 onShow 校验)。
*/
import type { ErrorPageName, PageName } from "@/router/routes";
import { getJwtInfo } from "@/common/http.interceptor";
import { canVisitPage } from "@/permission/permission-context";
import {
errorPageUrl,
isAuthFree,
normalizePath,
pageUrl,
resolvePageName,
} from "@/router/routes";
/** 需要拦的导航 API(switchTab 目前没有 tabBar 页,留着以免将来漏) */
const NAV_APIS = ["navigateTo", "redirectTo", "reLaunch", "switchTab"];
/** 是否已登录:只认请求层的 AccessToken(令牌唯一来源,与 store 口径一致) */
function isLoggedIn(): boolean {
return !!getJwtInfo()?.AccessToken;
}
/**
* 判定目标地址该被拦成哪个错误页;返回 null 表示放行
*
* 顺序即语义:地址不存在(404) -> 没登录(401) -> 没权限(403)
* 403 必须排在 401 之后:未登录时权限档案多半也是空的,先给 401 才谈得上"去登录"
*/
function evaluate(url: string): ErrorPageName | null {
const name: PageName | null = resolvePageName(url);
if (!name) {
return "error404";
}
if (isAuthFree(name)) {
return null;
}
if (!isLoggedIn()) {
return "error401";
}
if (!canVisitPage(name)) {
return "error403";
}
return null;
}
/** 跳错误页;redirectTo 保住返回栈("返回上一页"还有内容),页面栈为空时退化成 reLaunch */
function jumpToError(blocked: ErrorPageName, from: string): void {
const url = errorPageUrl(blocked, from);
uni.redirectTo({
url,
fail: () => {
uni.reLaunch({ url });
},
});
}
/**
* 冷启动/页面级校验:当前页面对象已由 uni 解析好,按真实路径判定
* 返回 false 表示已被拦走,调用方(useAuthGuard)不必再做后续初始化
*/
export function ensureCurrentPageAuthorized(): boolean {
const pages = getCurrentPages();
const current = pages[pages.length - 1];
if (!current) {
// 页面栈还没建立(启动极早期),交给页面自己的 useAuthGuard 再判一次
return true;
}
const path = `/${current.route}`;
const blocked = evaluate(path);
if (!blocked) {
return true;
}
// getCurrentPages 只会给出真实路径,而 ?from= 是给用户看的:换成当前端的地址形态
const name = resolvePageName(path);
jumpToError(blocked, name ? pageUrl(name) : path);
return false;
}
/** 注册导航拦截器;在 App.vue 的 onLaunch 调用,确保首次站内跳转就被管住 */
export function setupNavGuard(): void {
NAV_APIS.forEach((api) => {
uni.addInterceptor(api, {
invoke(args: { url?: string }) {
const url = args?.url ?? "";
const blocked = evaluate(url);
if (!blocked) {
return true;
}
jumpToError(blocked, url);
// 取消本次导航
return false;
},
});
});
}
/**
* H5 专属:地址栏直接访问一个不存在的地址时,uni 路由表里没有对应页面,
* 结果是白屏 + 一条 vue-router "No match found",且这条路径不经过导航 API,拦截器也管不到,
* 只能在启动时按 location.pathname 兜底跳 404。
*
* 前提:manifest 的 h5.router.base 为 "/";改成子路径部署时这里要先剥掉 base 前缀。
*/
export function setupUnknownPathFallback(): void {
// #ifdef H5
if (resolvePageName(window.location.pathname)) {
return;
}
const from = `${window.location.pathname}${window.location.search}`;
// 延到下一个任务:此时 uni 已完成首屏路由解析,reLaunch 才作用在真实页面栈上
setTimeout(() => {
uni.reLaunch({ url: errorPageUrl("error404", from) });
}, 0);
// #endif
}
/**
* 供页面把用户送回登录页(带 redirect,登录成功后回到原地址;缺省回 dashboard)
* redirect 与 from 一样不做预编码,理由见 routes.ts 的 errorPageUrl
*/
export function goLoginWithRedirect(from?: string): void {
const target = from && resolvePageName(from) ? from : pageUrl("dashboard");
uni.reLaunch({
url: `${pageUrl("login")}?redirect=${normalizePath(target)}`,
});
}