/**
* 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} · 统一管理平台` : '统一管理平台';
});
}