136 lines
4.8 KiB
TypeScript
136 lines
4.8 KiB
TypeScript
/**
|
||
* 全局导航守卫(对应旧 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)}`,
|
||
});
|
||
}
|