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; 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'; }