app.routes.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import { ActivatedRouteSnapshot, Route, Routes } from '@angular/router';
  2. import { authGuard } from './core/auth.guard';
  3. /**
  4. * 顶层路由表(ADR-004:双端双套路由树)
  5. *
  6. * 结构:
  7. * '' 入口分流页(UA/视口一次性判定 → replaceUrl 跳端)
  8. * login 登录(两端共用同一组件,形态自适应)
  9. * desktop/** 桌面端(懒加载,DesktopShell 包裹)
  10. * mobile/** 移动端(懒加载,MobileShell 包裹)
  11. * 旧路径 按当前端前缀重定向(FR-16;判定逻辑与 DeviceService.normalize 同源)
  12. * ** 兜底 → 入口页
  13. *
  14. * ⚠️ `redirectTo` 使用 Angular 的 `RedirectFunction`(形参是 route snapshot)。
  15. * 刻意不用「仅挂 canActivate 的 redirectTo 路由」写法 —— 存量为此踩过 NG04014。
  16. */
  17. type RedirectData = Pick<ActivatedRouteSnapshot, 'url'>;
  18. type RedirectFn = (data: RedirectData) => string;
  19. const LEGACY_SIMPLE = [
  20. 'messages',
  21. 'tabs',
  22. 'workbench',
  23. 'deliverables',
  24. 'mine',
  25. 'contacts',
  26. 'projects',
  27. 'events',
  28. ] as const;
  29. /** snapshot.url 是「相对当前路由的剩余段」;顶层路由下即为完整路径 */
  30. const legacyRedirect: RedirectFn = (d) => normalize(`/${segmentsOf(d).join('/')}`);
  31. export const routes: Routes = [
  32. {
  33. path: '',
  34. pathMatch: 'full',
  35. loadComponent: () => import('./pages/entry/entry').then((m) => m.EntryComponent),
  36. },
  37. {
  38. // 登录页分端(2026-11-03):移动端已按 Demo 重建独立形态;
  39. // 桌面端仍用原共用组件。两端登录逻辑共用 FmodeApi / SessionService,仅版式不同。
  40. path: 'login',
  41. loadComponent: () =>
  42. detectMode() === 'mobile'
  43. ? import('./mobile/pages/login/login').then((m) => m.MbLoginPage)
  44. : import('./pages/login/login').then((m) => m.LoginComponent),
  45. },
  46. {
  47. path: 'desktop',
  48. canActivate: [authGuard],
  49. loadChildren: () => import('./desktop/desktop.routes').then((m) => m.desktopRoutes),
  50. },
  51. {
  52. path: 'mobile',
  53. canActivate: [authGuard],
  54. loadChildren: () => import('./mobile/mobile.routes').then((m) => m.mobileRoutes),
  55. },
  56. // ---- 旧路径重定向(FR-16 AC-16.2)----
  57. ...LEGACY_SIMPLE.map((p): Route => ({ path: p, redirectTo: legacyRedirect })),
  58. { path: 'chat/:id', redirectTo: legacyRedirect },
  59. { path: 'chat', redirectTo: (): string => normalize('/chat') },
  60. { path: 'agent/:id', redirectTo: legacyRedirect },
  61. {
  62. // FR-11:语音通话暂不实现 → 落数字生命详情页,由详情页提示「暂未开放」
  63. path: 'call/:id',
  64. /*
  65. * 通话不做(FR-11)→ 落**容器设置页**(2026-11-04 恢复)。
  66. * 用户要从「某容器的来电」跳到该容器的设置,而不是笼统的会话列表。
  67. */
  68. redirectTo: (d): string => normalize(`/agent/${segmentsOf(d).pop() ?? ''}`),
  69. },
  70. { path: '**', redirectTo: (): string => normalize('') },
  71. ];
  72. function segmentsOf(d: RedirectData): string[] {
  73. return d.url.map((s) => s.path);
  74. }
  75. /**
  76. * 旧路径 → 当前端路径。
  77. * 独立成函数(而非注入 DeviceService)是为了让 `redirectTo` 保持**函数式**——
  78. * 存量为此踩过 NG04014 的坑,此处刻意规避。
  79. */
  80. function normalize(url: string): string {
  81. // 惰性读取,避免在路由定义期触碰 localStorage
  82. const forced = readModeOverride();
  83. const mode = forced ?? detectMode();
  84. const clean = url.split('?')[0];
  85. const [head, id] = clean.split('/').filter(Boolean);
  86. switch (head) {
  87. case 'messages':
  88. case 'tabs':
  89. return mode === 'mobile' ? '/mobile/messages' : '/desktop/chat';
  90. case 'chat':
  91. return id ? `/${mode}/chat/${id}` : `/${mode}/chat`;
  92. case 'agent':
  93. return `/${mode}/agent/${id ?? ''}`.replace(/\/$/, '');
  94. case 'call':
  95. // 通话暂不实现 → 落详情页
  96. return id ? `/${mode}/agent/${id}` : `/${mode}/chat`;
  97. // 首页已下线(2026-11-02):旧 /home 落到会话入口
  98. case 'home':
  99. return mode === 'mobile' ? '/mobile/messages' : '/desktop/chat';
  100. case 'workbench':
  101. case 'deliverables':
  102. case 'mine':
  103. case 'contacts':
  104. case 'projects':
  105. case 'events':
  106. return `/${mode}/${head}`;
  107. default:
  108. return mode === 'mobile' ? '/mobile/messages' : '/desktop/chat';
  109. }
  110. }
  111. function readModeOverride(): 'desktop' | 'mobile' | null {
  112. try {
  113. const v = localStorage.getItem('fmode_ui_mode_override');
  114. return v === 'desktop' || v === 'mobile' ? v : null;
  115. } catch {
  116. return null;
  117. }
  118. }
  119. function detectMode(): 'desktop' | 'mobile' {
  120. if (typeof navigator === 'undefined') return 'desktop';
  121. const ua = navigator.userAgent;
  122. if (/iPad|Tablet|PlayBook|Silk/i.test(ua)) return 'desktop';
  123. if (/Android/i.test(ua) && !/Mobile/i.test(ua)) return 'desktop';
  124. if (/iPhone|iPod|Android.*Mobile|Windows Phone|BlackBerry|webOS|Opera Mini/i.test(ua)) {
  125. return 'mobile';
  126. }
  127. if (typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches) {
  128. return typeof window !== 'undefined' && window.innerWidth < 768 ? 'mobile' : 'desktop';
  129. }
  130. return 'desktop';
  131. }