| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- import { MAvatarComponent } from '../../components/m-avatar/m-avatar';
- import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core';
- import { Router } from '@angular/router';
- import { UiMCell, UiMSearch, UiMTabs } from '../../../ui';
- import { StatePanelComponent } from '../../../components/feedback/state-panel/state-panel';
- import { StatusBadgeComponent } from '../../../components/domain/status-badge/status-badge';
- import { Agent } from '../../../core/api/models';
- import { ContactsService } from '../../../core/contacts.service';
- import { LiveService } from '../../../core/live.service';
- import { DeviceService } from '../../../core/device.service';
- import { isMatrixReady } from '../../../core/format';
- /**
- * 移动·通讯录(SPEC FR-03)
- * 三 Tab + 搜索 + 同事展开;行操作触达 ≥44px。
- */
- @Component({
- selector: 'app-mb-contacts-page',
- changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [StatePanelComponent, StatusBadgeComponent, UiMSearch, MAvatarComponent, UiMCell, UiMTabs],
- template: `
- <div class="m-page">
- <header class="m-head">
- <h1>通讯录</h1>
- </header>
- <ui-m-search placeholder="搜索名称或专长" [value]="contacts.keyword()" (valueChange)="contacts.keyword.set($event)" />
- <ui-m-tabs [items]="tabItems" [active]="tabIndex()" (activeChange)="onTabIndex($event)" />
- @switch (contacts.state()) {
- @case ('loading') {
- <app-state-panel state="loading" loadingText="正在加载…" />
- }
- @case ('error') {
- <app-state-panel state="error" [message]="contacts.error()" (retry)="reload()" />
- }
- @case ('empty') {
- <app-state-panel state="empty" title="暂无数字生命" [retryable]="false" />
- }
- @default {
- @if (contacts.isEmpty()) {
- <app-state-panel state="empty" title="没有匹配的结果" [retryable]="false" />
- } @else {
- @if (contacts.tab() !== 'people') {
- <h2 class="section-title">AI 数字生命</h2>
- <div class="rows">
- @for (a of contacts.visibleAgents(); track a.id) {
- <button type="button" class="row" (click)="open(a)">
- <app-m-avatar [name]="a.name" />
- <span class="row-body">
- <strong class="truncate">{{ a.name }}</strong>
- <small class="truncate">{{ a.specialty }}</small>
- </span>
- <app-status-badge [agent]="a" />
- </button>
- }
- </div>
- }
- @if (contacts.tab() !== 'ai') {
- <h2 class="section-title">同事</h2>
- <div class="rows">
- @for (p of contacts.visiblePeople(); track p.id) {
- <ui-m-cell
- [title]="p.name"
- [label]="(p.agents?.length || 0) + ' 位数字生命'"
- icon="fa-solid fa-user"
- [isLink]="true"
- (click)="contacts.toggleColleague(p.id)"
- />
- @if (contacts.expandedColleague() === p.id) {
- @for (a of contacts.agentsOfPerson(p); track a.id) {
- <button type="button" class="row child" (click)="open(a)">
- <app-m-avatar [name]="a.name" size="sm" />
- <span class="row-body">
- <strong class="truncate">{{ a.name }}</strong>
- <small class="truncate">{{ a.specialty }}</small>
- </span>
- <app-status-badge [agent]="a" />
- </button>
- }
- }
- }
- </div>
- }
- }
- }
- }
- </div>
- `,
- styles: [
- `
- .m-page {
- padding: var(--sp-5) var(--mob-gutter) var(--sp-6);
- }
- .m-head h1 {
- margin: 0 0 var(--sp-4);
- font-size: var(--fs-26);
- color: var(--ink);
- }
- .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);
- font-size: var(--fs-17);
- }
- .m-tabs {
- display: flex;
- gap: var(--sp-1);
- padding: var(--sp-1);
- margin-bottom: var(--sp-4);
- 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;
- }
- .section-title {
- margin: 0 0 var(--sp-3);
- font-size: var(--fs-13);
- color: var(--ink-2);
- }
- .section-title:not(:first-of-type) {
- margin-top: var(--sp-5);
- }
- .rows {
- display: grid;
- gap: var(--sp-2);
- }
- .row {
- display: flex;
- align-items: center;
- gap: var(--sp-3);
- width: 100%;
- min-height: 64px;
- 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;
- }
- .row:active {
- background: var(--surface-hover);
- }
- .row.child {
- margin-left: var(--sp-4);
- width: calc(100% - var(--sp-4));
- background: var(--surface-2);
- }
- .row-body {
- display: grid;
- gap: 2px;
- flex: 1;
- min-width: 0;
- }
- .row-body strong {
- font-size: var(--fs-15);
- color: var(--ink);
- }
- .row-body small {
- font-size: var(--fs-12);
- color: var(--muted);
- }
- .truncate {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- `,
- ],
- })
- export class MbContactsPage implements OnInit {
- readonly contacts = inject(ContactsService);
- private readonly live = inject(LiveService);
- private readonly router = inject(Router);
- private readonly device = inject(DeviceService);
- readonly tabs = [
- { key: 'all' as const, label: '全部' },
- { key: 'ai' as const, label: 'AI' },
- { key: 'people' as const, label: '同事' },
- ];
- readonly tabItems = [{ label: '全部' }, { label: 'AI 数字生命' }, { label: '同事' }];
- readonly tabIndex = computed(() => ({ all: 0, ai: 1, people: 2 })[this.contacts.tab()]);
- onTabIndex(i: number): void {
- this.contacts.setTab((['all', 'ai', 'people'] as const)[i] ?? 'all');
- }
- readonly counts = computed(() => {
- const c = this.contacts.counts();
- return { all: c.all, ai: c.ai, people: c.people };
- });
- async ngOnInit(): Promise<void> {
- await this.contacts.ensureLoaded();
- }
- async reload(): Promise<void> {
- await this.live.refresh();
- }
- onSearch(e: Event): void {
- this.contacts.keyword.set((e.target as HTMLInputElement).value);
- }
- open(a: Agent): void {
- void this.router.navigate(
- isMatrixReady(a)
- ? ['/', this.device.mode(), 'chat', a.id]
- : ['/', this.device.mode(), 'agent', a.id],
- );
- }
- }
|