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