| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151 |
- /**
- * 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):内容区 <router-view/> 渲染 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<typeof buildRouter>): 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} · 统一管理平台` : '统一管理平台';
- });
- }
|