| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- import { ActivatedRouteSnapshot, Route, Routes } from '@angular/router';
- import { authGuard } from './core/auth.guard';
- /**
- * 顶层路由表(ADR-004:双端双套路由树)
- *
- * 结构:
- * '' 入口分流页(UA/视口一次性判定 → replaceUrl 跳端)
- * login 登录(两端共用同一组件,形态自适应)
- * desktop/** 桌面端(懒加载,DesktopShell 包裹)
- * mobile/** 移动端(懒加载,MobileShell 包裹)
- * 旧路径 按当前端前缀重定向(FR-16;判定逻辑与 DeviceService.normalize 同源)
- * ** 兜底 → 入口页
- *
- * ⚠️ `redirectTo` 使用 Angular 的 `RedirectFunction`(形参是 route snapshot)。
- * 刻意不用「仅挂 canActivate 的 redirectTo 路由」写法 —— 存量为此踩过 NG04014。
- */
- type RedirectData = Pick<ActivatedRouteSnapshot, 'url'>;
- type RedirectFn = (data: RedirectData) => string;
- const LEGACY_SIMPLE = [
- 'messages',
- 'tabs',
- 'workbench',
- 'deliverables',
- 'mine',
- 'contacts',
- 'projects',
- 'events',
- ] as const;
- /** snapshot.url 是「相对当前路由的剩余段」;顶层路由下即为完整路径 */
- const legacyRedirect: RedirectFn = (d) => normalize(`/${segmentsOf(d).join('/')}`);
- export const routes: Routes = [
- {
- path: '',
- pathMatch: 'full',
- loadComponent: () => import('./pages/entry/entry').then((m) => m.EntryComponent),
- },
- {
- // 登录页分端(2026-11-03):移动端已按 Demo 重建独立形态;
- // 桌面端仍用原共用组件。两端登录逻辑共用 FmodeApi / SessionService,仅版式不同。
- path: 'login',
- loadComponent: () =>
- detectMode() === 'mobile'
- ? import('./mobile/pages/login/login').then((m) => m.MbLoginPage)
- : import('./pages/login/login').then((m) => m.LoginComponent),
- },
- {
- path: 'desktop',
- canActivate: [authGuard],
- loadChildren: () => import('./desktop/desktop.routes').then((m) => m.desktopRoutes),
- },
- {
- path: 'mobile',
- canActivate: [authGuard],
- loadChildren: () => import('./mobile/mobile.routes').then((m) => m.mobileRoutes),
- },
- // ---- 旧路径重定向(FR-16 AC-16.2)----
- ...LEGACY_SIMPLE.map((p): Route => ({ path: p, redirectTo: legacyRedirect })),
- { path: 'chat/:id', redirectTo: legacyRedirect },
- { path: 'chat', redirectTo: (): string => normalize('/chat') },
- { path: 'agent/:id', redirectTo: legacyRedirect },
- {
- // FR-11:语音通话暂不实现 → 落数字生命详情页,由详情页提示「暂未开放」
- path: 'call/:id',
- /*
- * 通话不做(FR-11)→ 落**容器设置页**(2026-11-04 恢复)。
- * 用户要从「某容器的来电」跳到该容器的设置,而不是笼统的会话列表。
- */
- redirectTo: (d): string => normalize(`/agent/${segmentsOf(d).pop() ?? ''}`),
- },
- { path: '**', redirectTo: (): string => normalize('') },
- ];
- function segmentsOf(d: RedirectData): string[] {
- return d.url.map((s) => s.path);
- }
- /**
- * 旧路径 → 当前端路径。
- * 独立成函数(而非注入 DeviceService)是为了让 `redirectTo` 保持**函数式**——
- * 存量为此踩过 NG04014 的坑,此处刻意规避。
- */
- function normalize(url: string): string {
- // 惰性读取,避免在路由定义期触碰 localStorage
- const forced = readModeOverride();
- const mode = forced ?? detectMode();
- const clean = url.split('?')[0];
- const [head, id] = clean.split('/').filter(Boolean);
- switch (head) {
- case 'messages':
- case 'tabs':
- return mode === 'mobile' ? '/mobile/messages' : '/desktop/chat';
- case 'chat':
- return id ? `/${mode}/chat/${id}` : `/${mode}/chat`;
- case 'agent':
- return `/${mode}/agent/${id ?? ''}`.replace(/\/$/, '');
- case 'call':
- // 通话暂不实现 → 落详情页
- return id ? `/${mode}/agent/${id}` : `/${mode}/chat`;
- // 首页已下线(2026-11-02):旧 /home 落到会话入口
- case 'home':
- return mode === 'mobile' ? '/mobile/messages' : '/desktop/chat';
- case 'workbench':
- case 'deliverables':
- case 'mine':
- case 'contacts':
- case 'projects':
- case 'events':
- return `/${mode}/${head}`;
- default:
- return mode === 'mobile' ? '/mobile/messages' : '/desktop/chat';
- }
- }
- function readModeOverride(): 'desktop' | 'mobile' | null {
- try {
- const v = localStorage.getItem('fmode_ui_mode_override');
- return v === 'desktop' || v === 'mobile' ? v : null;
- } catch {
- return null;
- }
- }
- function detectMode(): 'desktop' | 'mobile' {
- if (typeof navigator === 'undefined') return 'desktop';
- const ua = navigator.userAgent;
- if (/iPad|Tablet|PlayBook|Silk/i.test(ua)) return 'desktop';
- if (/Android/i.test(ua) && !/Mobile/i.test(ua)) return 'desktop';
- if (/iPhone|iPod|Android.*Mobile|Windows Phone|BlackBerry|webOS|Opera Mini/i.test(ua)) {
- return 'mobile';
- }
- if (typeof matchMedia === 'function' && matchMedia('(pointer: coarse)').matches) {
- return typeof window !== 'undefined' && window.innerWidth < 768 ? 'mobile' : 'desktop';
- }
- return 'desktop';
- }
|