import { MAvatarComponent } from '../../components/m-avatar/m-avatar'; import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core'; import { Router } from '@angular/router'; import { UiMSearch } from '../../../ui'; import { StatePanelComponent } from '../../../components/feedback/state-panel/state-panel'; import { LiveService } from '../../../core/live.service'; import { DeviceService } from '../../../core/device.service'; import { isMatrixReady } from '../../../core/format'; import { fmtRelTime } from '../../../core/time'; /** * 移动·会话列表(SPEC FR-04 移动形态) * * 列表头 + 搜索 + 全部/未读 Tab + 会话行。 * 返回列表时**保持滚动位置**(AC-05.2 同源要求,由 Router 的滚动恢复 + 路径缓存保证)。 */ @Component({ selector: 'app-mb-messages-page', changeDetection: ChangeDetectionStrategy.OnPush, imports: [StatePanelComponent, UiMSearch, MAvatarComponent], template: `

会话

与数字生命一对一沟通。

@switch (live.state()) { @case ('loading') { } @case ('error') { } @case ('empty') { } @default { @if (filtered().length === 0) { } @else {
@for (c of filtered(); track c.id) { }
} } }

消息经 Matrix 通道加密传输

`, styles: [ ` .m-page { padding: var(--sp-5) var(--mob-gutter) var(--sp-6); } .m-head { margin-bottom: var(--sp-4); } .m-head h1 { margin: 0; font-size: var(--fs-26); color: var(--ink); } .m-head p { margin: var(--sp-1) 0 0; font-size: var(--fs-13); color: var(--muted); } .m-search { display: flex; align-items: center; gap: var(--sp-2); min-height: 46px; padding: 0 var(--sp-3); margin-bottom: var(--sp-3); border: 1px solid var(--edge); border-radius: var(--r-md); background: var(--surface); color: var(--muted); } .m-search input { flex: 1; min-width: 0; border: 0; background: transparent; color: var(--ink); /* ≥16px,iOS 聚焦不放大(AC-19.3 同源要求) */ font-size: var(--fs-17); } .m-tabs { display: flex; gap: var(--sp-1); padding: var(--sp-1); margin-bottom: var(--sp-3); border-radius: var(--r-md); background: var(--surface-2); } .m-tab { flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-1); min-height: var(--touch-min); border: 0; border-radius: var(--r-sm); background: transparent; color: var(--ink-2); font-size: var(--fs-13); cursor: pointer; } .m-tab.active { background: var(--surface); color: var(--ink); font-weight: 600; box-shadow: var(--shadow-soft); } .tab-count { font-size: var(--fs-11); opacity: 0.7; } .chat-list { display: grid; gap: var(--sp-2); } .chat-row { display: flex; align-items: center; gap: var(--sp-3); width: 100%; min-height: 68px; padding: var(--sp-2) var(--sp-3); border: 1px solid var(--edge); border-radius: var(--r-md); background: var(--surface); text-align: left; cursor: pointer; } .chat-row:active { background: var(--surface-hover); } .chat-body { display: grid; gap: 2px; flex: 1; min-width: 0; } .chat-line { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); } .chat-line strong { font-size: var(--fs-15); color: var(--ink); } .chat-line time { font-size: var(--fs-11); color: var(--muted); flex-shrink: 0; } .chat-preview { font-size: var(--fs-12); color: var(--muted); } .unread { min-width: 18px; padding: 0 5px; border-radius: var(--r-tag); background: var(--danger); color: var(--on-danger); font-size: var(--fs-11); line-height: 18px; text-align: center; flex-shrink: 0; } .foot-note { display: flex; align-items: center; justify-content: center; gap: var(--sp-1); margin-top: var(--sp-5); font-size: var(--fs-11); color: var(--muted); } .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } `, ], }) export class MbMessagesPage implements OnInit { readonly live = inject(LiveService); private readonly router = inject(Router); private readonly device = inject(DeviceService); readonly tab = signal<'all' | 'unread'>('all'); readonly keyword = signal(''); readonly rel = fmtRelTime; readonly list = computed(() => this.live.agents().filter((a) => isMatrixReady(a))); readonly unreadCount = computed(() => this.list().filter((a) => a.unread > 0).length, ); readonly filtered = computed(() => { const q = this.keyword().trim().toLowerCase(); return this.list().filter((a) => { if (this.tab() === 'unread' && a.unread === 0) return false; if (!q) return true; return ( a.name.toLowerCase().includes(q) || (a.specialty ?? '').toLowerCase().includes(q) ); }); }); async ngOnInit(): Promise { await this.live.ensureLoaded(); } async reload(): Promise { await this.live.refresh(); } onSearch(e: Event): void { this.keyword.set((e.target as HTMLInputElement).value); } open(id: string): void { void this.router.navigate(['/', this.device.mode(), 'chat', id]); } }