/** * index.ts —— 路由表 + 守卫链(README §3.3 路由与鉴权 / §3.4 守卫链顺序)。 * * 守卫链顺序(全局 beforeEach,按序短路): * ① 登录态:login.isLoggedIn(有 access 且未过期,提前 30s 判过期); * ② 刷新:access 过期但有 refresh → 静默 refresh(refreshAccessToken 单例)→ 成功放行 / 失败跳登录; * ③ 白名单:登录页/SSO 回调等公开路由直接放行(在 Auth 守卫之前,对齐后端"公开路由先注册"纪律); * ④ 作用域:路由 meta.scopeRequired(默认 true)且 scope 未就绪 → 跳作用域选择页。 * * 路由 path 与后端端点对齐(`/{端}/{模块}/{资源}`,README §3.1);市场角色登录入口暂禁用—— * 不注册任何市场端路由(后续启用时单独拍板,勿自行加)。 */ import { useLogin } from '@/store/login'; import { useScope } from '@/store/scope'; import { refreshAccessToken } from '@/api/http'; import type { RouteRecordRaw } from 'vue-router'; /** 路由 meta 扩展(业务路由可声明的守卫参数) */ declare module 'vue-router' { interface RouteMeta { /** 是否需要登录(默认 true;登录页/SSO 回调设 false) */ public?: boolean; /** 是否要求作用域就绪(默认 true;登录页/作用域选择页设 false) */ scopeRequired?: boolean; /** 路由标题(document.title 同步用,中文) */ title?: string; } } /** * 路由表(createRouter 的 routes 入参;vite define 注入为 VueRouter.createRouter)。 * * 套壳模型(嵌套路由,README §2 layout 壳): * - 顶层路由**不套壳**:/login(登录,公开)/ /org/scope(作用域选择,登录后全屏过渡页); * - **layout 父路由**(path '/',component = views/layout):内容区 渲染 children; * 业务域页面全部作为其 children(相对 path),统一带侧边菜单 + 顶栏骨架; * - 访问 '/' 自动 redirect 到首个业务页 /org/institutions(scope 未就绪时守卫会拦到 /org/scope)。 * * 入参:无。 * * 出参: * - RouteRecordRaw[]:/login / /org/scope / layout(children: org/institutions) / 兜底 404。 * * 异常场景: * - 路径未匹配:通配路由落到 404(本样例 redirect 到登录页,避免白屏)。 */ export const routes: RouteRecordRaw[] = [ { path: '/login', name: 'Login', component: () => import('@/views/login/index.vue'), meta: { public: true, scopeRequired: false, title: '登录' }, }, { path: '/org/scope', name: 'ScopeSelect', component: () => import('@/views/org/scope-select.vue'), meta: { scopeRequired: false, title: '选择机构' }, }, { // layout 父路由:套壳(侧边菜单 + 顶栏 + 内容区 router-view)。业务域路由嵌套其下。 path: '/', component: () => import('@/views/layout/index.vue'), redirect: '/org/institutions', children: [ { path: 'org/institutions', name: 'InstitutionList', component: () => import('@/views/org/institution/list.vue'), meta: { title: '机构管理' }, }, ], }, { path: '/:pathMatch(.*)*', redirect: '/login', }, ]; /** * 创建路由实例(history 模式;生产由 nginx 按 location 回退 index.html)。 * * 入参:无。 * * 出参: * - 路由实例:main.ts 装配用。 * * 异常场景: * - 非法 URL(带未编码字符):createWebHistory 内部兜底,不抛。 */ export function buildRouter() { return createRouter({ history: createWebHistory(), routes, }); } /** * 全局前置守卫(挂载后生效;README §3.3 守卫链)。 * * 入参(由 vue-router 注入): * - to:目标路由(含 meta)。 * - from:来源路由(本样例未使用,保留签名完整性)。 * * 出参: * - 省略返回值 = 放行;返回 false / 跳转 = 拦截。 * * 异常场景: * - 守卫内 pinia 未激活(极端早启动):useLogin 抛错 → 由全局 Recover 思路兜底(本仓为纯前端, * 表现为控制台报错 + 页面不跳转;main.ts 保证 app.use(pinia) 早于 router.push 首帧)。 */ export function setupGuards(router: ReturnType): void { router.beforeEach(async (to, _from) => { const login = useLogin(); const scope = useScope(); // ③ 白名单(放在登录判断前:未登录访问公开页直接放行) if (to.meta.public) { return true; } // ① 登录态 if (!login.isLoggedIn) { // 两步登录①→② 过渡态:票据在手(pendingTicket)但未签 token,允许进入作用域选择页 // (scope-select);未签发前刷新会丢失过渡态,需回登录页重走①(守卫拦截自然兜底)。 if (to.name === 'ScopeSelect' && login.pendingTicket) { return true; } // ② 有 refresh 则静默刷新(单例;并发多路由切换只刷一次) const refreshed = await refreshAccessToken(); if (refreshed && login.accessToken) { login.hydrate(); return true; } // 刷新失败:清本地态(refresh 失败时 http 层已 clear,此处幂等兜底)后跳登录 return { name: 'Login' }; } // ④ 作用域就绪 if (to.meta.scopeRequired !== false && !scope.ready) { return { name: 'ScopeSelect' }; } return true; }); // 路由标题同步(document.title 中文,README §1.4 默认简体中文) router.afterEach((to) => { document.title = to.meta.title ? `${to.meta.title} · 统一管理平台` : '统一管理平台'; }); }