|
@@ -1,133 +1,158 @@
|
|
|
import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core';
|
|
import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core';
|
|
|
import { Router } from '@angular/router';
|
|
import { Router } from '@angular/router';
|
|
|
-import { UiAvatar, UiMSectionHeader } from '../../../ui';
|
|
|
|
|
-import { StatePanelComponent } from '../../../components/feedback/state-panel/state-panel';
|
|
|
|
|
-import { EventsService } from '../../../core/events.service';
|
|
|
|
|
-import { LiveService } from '../../../core/live.service';
|
|
|
|
|
|
|
+import { UiMButton, UiMCell, UiMCellGroup, UiMEmpty, UiMNoticeBar, UiMSectionHeader, UiMSkeleton } from '../../../ui';
|
|
|
|
|
+import { MAvatarComponent } from '../../components/m-avatar/m-avatar';
|
|
|
import { DeliverablesService } from '../../../core/deliverables.service';
|
|
import { DeliverablesService } from '../../../core/deliverables.service';
|
|
|
-import { TasksService } from '../../../core/tasks.service';
|
|
|
|
|
import { DeviceService } from '../../../core/device.service';
|
|
import { DeviceService } from '../../../core/device.service';
|
|
|
-import { SessionService } from '../../../core/session.service';
|
|
|
|
|
-import { fmtTime } from '../../../core/time';
|
|
|
|
|
|
|
+import { EventsService } from '../../../core/events.service';
|
|
|
import { isMatrixReady } from '../../../core/format';
|
|
import { isMatrixReady } from '../../../core/format';
|
|
|
|
|
+import { LiveService } from '../../../core/live.service';
|
|
|
|
|
+import { SessionService } from '../../../core/session.service';
|
|
|
|
|
+import { TasksService } from '../../../core/tasks.service';
|
|
|
|
|
+import { fmtRelTime } from '../../../core/time';
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 移动·聚合首页(SPEC FR-19)
|
|
* 移动·聚合首页(SPEC FR-19)
|
|
|
*
|
|
*
|
|
|
* ① 事件提醒条 ② 最新交付成果 ×3 ③ 最近会话 ×6,各带「全部」入口
|
|
* ① 事件提醒条 ② 最新交付成果 ×3 ③ 最近会话 ×6,各带「全部」入口
|
|
|
*
|
|
*
|
|
|
- * 🔴 AC-19.1 **计数与列表条数一致**(修正存量「显示 20 却只有 6 行」的误导)
|
|
|
|
|
- * 🔴 AC-19.2 **每个区块独立三态**,错误态**不得伪装成「暂无交付成果」**
|
|
|
|
|
- * 🔴 AC-19.3 输入/字号 ≥16px(由全局与组件保证)
|
|
|
|
|
- * 🔴 AC-19.4 「全部」分别落到 /mobile/events、/mobile/deliverables、/mobile/messages
|
|
|
|
|
|
|
+ * 实现纪律(用户要求:**不要出现 mobile 端却有 PC 端组件样式**):
|
|
|
|
|
+ * · 区块标题用 `ui-m-section-header`(移动件,非桌面风格)
|
|
|
|
|
+ * · 列表行用 `ui-m-cell`(移动列表标准件)
|
|
|
|
|
+ * · 空/加载用 `ui-m-empty` / `ui-m-skeleton`(移动件)
|
|
|
|
|
+ * · 头像用 `app-m-avatar`(移动尺寸 44px;底座无 ui-m-avatar)
|
|
|
|
|
+ * · 触达区 ≥44px、安全区由外壳处理
|
|
|
|
|
+ *
|
|
|
|
|
+ * AC:19.1 计数与列表条数一致 / 19.2 每区块独立三态 / 19.3 输入 ≥16px / 19.4 全部入口落点
|
|
|
*/
|
|
*/
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-mb-home-page',
|
|
selector: 'app-mb-home-page',
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
- imports: [StatePanelComponent, UiAvatar, UiMSectionHeader],
|
|
|
|
|
|
|
+ imports: [UiMButton, UiMCell, UiMCellGroup, UiMEmpty, UiMNoticeBar, UiMSectionHeader, UiMSkeleton, MAvatarComponent],
|
|
|
template: `
|
|
template: `
|
|
|
<div class="m-page">
|
|
<div class="m-page">
|
|
|
|
|
+ <!-- 顶部问候 -->
|
|
|
<header class="m-head">
|
|
<header class="m-head">
|
|
|
- <div>
|
|
|
|
|
- <h1>{{ greeting() }},{{ meName() }}</h1>
|
|
|
|
|
- <p>这是你与数字生命的今日进展。</p>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <h1>{{ greeting() }},{{ meName() }}</h1>
|
|
|
|
|
+ <p>这是你与数字生命的今日进展。</p>
|
|
|
</header>
|
|
</header>
|
|
|
|
|
|
|
|
- <!-- ① 事件提醒条 -->
|
|
|
|
|
- <section class="block" [attr.data-block]="'events'">
|
|
|
|
|
- <ui-m-section-header title="需要关注" actionText="全部" (action)="go('/events')" />
|
|
|
|
|
|
|
+ <!-- ① 需要关注 -->
|
|
|
|
|
+ <section class="block" data-block="events">
|
|
|
|
|
+ <ui-m-section-header
|
|
|
|
|
+ title="需要关注"
|
|
|
|
|
+ [actionText]="'全部'"
|
|
|
|
|
+ (action)="go('/events')"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
@switch (eventsState()) {
|
|
@switch (eventsState()) {
|
|
|
@case ('loading') {
|
|
@case ('loading') {
|
|
|
- <app-state-panel state="loading" loadingText="加载中…" />
|
|
|
|
|
|
|
+ <ui-m-skeleton variant="rect" height="64px" />
|
|
|
}
|
|
}
|
|
|
@case ('error') {
|
|
@case ('error') {
|
|
|
- <app-state-panel state="error" [message]="events.error()" (retry)="reload()" />
|
|
|
|
|
|
|
+ <div class="err-hold">
|
|
|
|
|
+ <ui-m-empty icon="fa-solid fa-triangle-exclamation" title="加载失败" [description]="events.error() ?? ''" />
|
|
|
|
|
+ <ui-m-button variant="secondary" block (click)="reload()">重新加载</ui-m-button>
|
|
|
|
|
+ </div>
|
|
|
}
|
|
}
|
|
|
@case ('empty') {
|
|
@case ('empty') {
|
|
|
- <p class="block-empty">暂无需要关注的事件。</p>
|
|
|
|
|
|
|
+ <ui-m-empty title="暂无需要关注的事件" />
|
|
|
}
|
|
}
|
|
|
@default {
|
|
@default {
|
|
|
- <button type="button" class="alert-bar" (click)="openEvent()">
|
|
|
|
|
- <span class="alert-icon" aria-hidden="true"><i class="fa-solid fa-triangle-exclamation"></i></span>
|
|
|
|
|
- <span class="alert-body">
|
|
|
|
|
- <strong>{{ topAlert()?.title }}</strong>
|
|
|
|
|
- <small>{{ topAlert()?.description }}</small>
|
|
|
|
|
- </span>
|
|
|
|
|
- <i class="fa-solid fa-chevron-right" aria-hidden="true"></i>
|
|
|
|
|
- </button>
|
|
|
|
|
|
|
+ <ui-m-notice-bar
|
|
|
|
|
+ type="warning"
|
|
|
|
|
+ leftIcon="fa-solid fa-triangle-exclamation"
|
|
|
|
|
+ [wrap]="true"
|
|
|
|
|
+ [text]="topAlert()?.title ?? ''"
|
|
|
|
|
+ (click)="openEvent()"
|
|
|
|
|
+ />
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
- <!-- ② 最新交付成果(计数与条数一致) -->
|
|
|
|
|
- <section class="block" [attr.data-block]="'deliverables'">
|
|
|
|
|
|
|
+ <!-- ② 最新交付成果(计数与条数一致:全部=N,列表=3) -->
|
|
|
|
|
+ <section class="block" data-block="deliverables">
|
|
|
<ui-m-section-header
|
|
<ui-m-section-header
|
|
|
title="最新交付成果"
|
|
title="最新交付成果"
|
|
|
[actionText]="'全部 ' + deliverables.all().length"
|
|
[actionText]="'全部 ' + deliverables.all().length"
|
|
|
(action)="go('/deliverables')"
|
|
(action)="go('/deliverables')"
|
|
|
/>
|
|
/>
|
|
|
|
|
+
|
|
|
@switch (deliverables.state()) {
|
|
@switch (deliverables.state()) {
|
|
|
@case ('loading') {
|
|
@case ('loading') {
|
|
|
- <app-state-panel state="loading" loadingText="加载中…" />
|
|
|
|
|
|
|
+ <div class="sk-group">
|
|
|
|
|
+ <ui-m-skeleton variant="rect" height="64px" />
|
|
|
|
|
+ <ui-m-skeleton variant="rect" height="64px" />
|
|
|
|
|
+ </div>
|
|
|
}
|
|
}
|
|
|
@case ('error') {
|
|
@case ('error') {
|
|
|
- <!-- AC-19.2:不得伪造成「暂无交付成果」 -->
|
|
|
|
|
- <app-state-panel
|
|
|
|
|
- state="error"
|
|
|
|
|
- title="成果加载失败"
|
|
|
|
|
- [message]="deliverables.error()"
|
|
|
|
|
- (retry)="reload()"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <!-- AC-19.2:不得伪装成「暂无交付成果」 -->
|
|
|
|
|
+ <div class="err-hold">
|
|
|
|
|
+ <ui-m-empty icon="fa-solid fa-triangle-exclamation" title="成果加载失败" [description]="deliverables.error() ?? ''" />
|
|
|
|
|
+ <ui-m-button variant="secondary" block (click)="reload()">重新加载</ui-m-button>
|
|
|
|
|
+ </div>
|
|
|
}
|
|
}
|
|
|
@case ('empty') {
|
|
@case ('empty') {
|
|
|
- <p class="block-empty">暂无交付成果。</p>
|
|
|
|
|
|
|
+ <ui-m-empty title="暂无交付成果" />
|
|
|
}
|
|
}
|
|
|
@default {
|
|
@default {
|
|
|
- @for (d of recentDeliverables(); track d.id) {
|
|
|
|
|
- <button type="button" class="row" (click)="openDeliverable(d.id)">
|
|
|
|
|
- <span class="row-icon" aria-hidden="true"><i class="fa-solid fa-file-lines"></i></span>
|
|
|
|
|
- <span class="row-body">
|
|
|
|
|
- <strong class="truncate">{{ d.title }}</strong>
|
|
|
|
|
- <small class="truncate">{{ d.projectName }} · {{ d.fileType }}</small>
|
|
|
|
|
- </span>
|
|
|
|
|
- <time>{{ time(d.deliveredAt) }}</time>
|
|
|
|
|
- </button>
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ <ui-m-cell-group>
|
|
|
|
|
+ @for (d of recentDeliverables(); track d.id) {
|
|
|
|
|
+ <ui-m-cell
|
|
|
|
|
+ [title]="d.title"
|
|
|
|
|
+ [label]="(d.projectName ?? '') + ' · ' + (d.fileType ?? 'FILE')"
|
|
|
|
|
+ icon="fa-solid fa-file-lines"
|
|
|
|
|
+ [isLink]="true"
|
|
|
|
|
+ (click)="openDeliverable(d.id)"
|
|
|
|
|
+ />
|
|
|
|
|
+ }
|
|
|
|
|
+ </ui-m-cell-group>
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
<!-- ③ 最近会话 -->
|
|
<!-- ③ 最近会话 -->
|
|
|
- <section class="block" [attr.data-block]="'conversations'">
|
|
|
|
|
|
|
+ <section class="block" data-block="conversations">
|
|
|
<ui-m-section-header
|
|
<ui-m-section-header
|
|
|
title="最近会话"
|
|
title="最近会话"
|
|
|
[actionText]="'全部 ' + readyAgents().length"
|
|
[actionText]="'全部 ' + readyAgents().length"
|
|
|
(action)="go('/messages')"
|
|
(action)="go('/messages')"
|
|
|
/>
|
|
/>
|
|
|
|
|
+
|
|
|
@switch (liveState()) {
|
|
@switch (liveState()) {
|
|
|
@case ('loading') {
|
|
@case ('loading') {
|
|
|
- <app-state-panel state="loading" loadingText="加载中…" />
|
|
|
|
|
|
|
+ <div class="sk-group">
|
|
|
|
|
+ <ui-m-skeleton variant="rect" height="64px" />
|
|
|
|
|
+ <ui-m-skeleton variant="rect" height="64px" />
|
|
|
|
|
+ </div>
|
|
|
}
|
|
}
|
|
|
@case ('error') {
|
|
@case ('error') {
|
|
|
- <app-state-panel state="error" [message]="live.error()" (retry)="reload()" />
|
|
|
|
|
|
|
+ <div class="err-hold">
|
|
|
|
|
+ <ui-m-empty icon="fa-solid fa-triangle-exclamation" title="会话加载失败" [description]="live.error() ?? ''" />
|
|
|
|
|
+ <ui-m-button variant="secondary" block (click)="reload()">重新加载</ui-m-button>
|
|
|
|
|
+ </div>
|
|
|
}
|
|
}
|
|
|
@case ('empty') {
|
|
@case ('empty') {
|
|
|
- <p class="block-empty">暂无会话。</p>
|
|
|
|
|
|
|
+ <ui-m-empty title="暂无会话" />
|
|
|
}
|
|
}
|
|
|
@default {
|
|
@default {
|
|
|
- @for (a of recentAgents(); track a.id) {
|
|
|
|
|
- <button type="button" class="row" (click)="openChat(a.id)">
|
|
|
|
|
- <ui-avatar [name]="a.name" size="sm" />
|
|
|
|
|
- <span class="row-body">
|
|
|
|
|
- <strong class="truncate">{{ a.name }}</strong>
|
|
|
|
|
- <small class="truncate">{{ a.currentTask || a.specialty }}</small>
|
|
|
|
|
- </span>
|
|
|
|
|
- @if (a.unread > 0) {
|
|
|
|
|
- <span class="unread">{{ a.unread }}</span>
|
|
|
|
|
- }
|
|
|
|
|
- </button>
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ <div class="conv-list">
|
|
|
|
|
+ @for (a of recentAgents(); track a.id) {
|
|
|
|
|
+ <button type="button" class="conv-row" (click)="openChat(a.id)">
|
|
|
|
|
+ <app-m-avatar [name]="a.name" />
|
|
|
|
|
+ <span class="conv-body">
|
|
|
|
|
+ <span class="conv-line">
|
|
|
|
|
+ <strong class="ellipsis">{{ a.name }}</strong>
|
|
|
|
|
+ <time>{{ rel(a.updatedAt) }}</time>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <span class="conv-preview ellipsis">{{ a.currentTask || a.specialty }}</span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ @if (a.unread > 0) {
|
|
|
|
|
+ <span class="unread">{{ a.unread }}</span>
|
|
|
|
|
+ }
|
|
|
|
|
+ </button>
|
|
|
|
|
+ }
|
|
|
|
|
+ </div>
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
</section>
|
|
</section>
|
|
@@ -135,9 +160,14 @@ import { isMatrixReady } from '../../../core/format';
|
|
|
`,
|
|
`,
|
|
|
styles: [
|
|
styles: [
|
|
|
`
|
|
`
|
|
|
|
|
+ :host {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
.m-page {
|
|
.m-page {
|
|
|
padding: var(--sp-5) var(--mob-gutter) var(--sp-6);
|
|
padding: var(--sp-5) var(--mob-gutter) var(--sp-6);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
.m-head {
|
|
.m-head {
|
|
|
margin-bottom: var(--sp-5);
|
|
margin-bottom: var(--sp-5);
|
|
|
}
|
|
}
|
|
@@ -152,128 +182,62 @@ import { isMatrixReady } from '../../../core/format';
|
|
|
font-size: var(--fs-13);
|
|
font-size: var(--fs-13);
|
|
|
color: var(--muted);
|
|
color: var(--muted);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
.block {
|
|
.block {
|
|
|
margin-bottom: var(--sp-6);
|
|
margin-bottom: var(--sp-6);
|
|
|
}
|
|
}
|
|
|
- .block-head {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
- gap: var(--sp-2);
|
|
|
|
|
- margin-bottom: var(--sp-3);
|
|
|
|
|
- }
|
|
|
|
|
- .block-head h2 {
|
|
|
|
|
- margin: 0;
|
|
|
|
|
- font-size: var(--fs-15);
|
|
|
|
|
- color: var(--ink);
|
|
|
|
|
- }
|
|
|
|
|
- .more {
|
|
|
|
|
- display: inline-flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: var(--sp-1);
|
|
|
|
|
- min-height: var(--touch-min);
|
|
|
|
|
- padding: 0 var(--sp-2);
|
|
|
|
|
- border: 0;
|
|
|
|
|
- background: transparent;
|
|
|
|
|
- color: var(--primary);
|
|
|
|
|
- font-size: var(--fs-13);
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- }
|
|
|
|
|
- .count {
|
|
|
|
|
- font-size: var(--fs-11);
|
|
|
|
|
- color: var(--muted);
|
|
|
|
|
- }
|
|
|
|
|
- .block-empty {
|
|
|
|
|
- margin: 0;
|
|
|
|
|
- padding: var(--sp-4);
|
|
|
|
|
- border-radius: var(--r-md);
|
|
|
|
|
- background: var(--surface-2);
|
|
|
|
|
- font-size: var(--fs-13);
|
|
|
|
|
- color: var(--muted);
|
|
|
|
|
- text-align: center;
|
|
|
|
|
- }
|
|
|
|
|
- .alert-bar {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: var(--sp-3);
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- min-height: 64px;
|
|
|
|
|
- padding: var(--sp-3);
|
|
|
|
|
- border: 0;
|
|
|
|
|
- border-radius: var(--r-md);
|
|
|
|
|
- background: var(--danger-subtle);
|
|
|
|
|
- color: var(--danger);
|
|
|
|
|
- text-align: left;
|
|
|
|
|
- cursor: pointer;
|
|
|
|
|
- }
|
|
|
|
|
- .alert-icon {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ .sk-group,
|
|
|
|
|
+ .err-hold {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
- place-items: center;
|
|
|
|
|
- width: 32px;
|
|
|
|
|
- height: 32px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- border-radius: var(--r-sm);
|
|
|
|
|
- background: var(--surface);
|
|
|
|
|
- font-size: var(--fs-15);
|
|
|
|
|
|
|
+ gap: var(--sp-2);
|
|
|
}
|
|
}
|
|
|
- .alert-body {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /* 会话行:移动端标准 64px 行高,触达区达标 */
|
|
|
|
|
+ .conv-list {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
- gap: 2px;
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- min-width: 0;
|
|
|
|
|
- }
|
|
|
|
|
- .alert-body strong {
|
|
|
|
|
- font-size: var(--fs-13);
|
|
|
|
|
- }
|
|
|
|
|
- .alert-body small {
|
|
|
|
|
- font-size: var(--fs-11);
|
|
|
|
|
- opacity: 0.85;
|
|
|
|
|
|
|
+ gap: var(--sp-2);
|
|
|
}
|
|
}
|
|
|
- .row {
|
|
|
|
|
|
|
+ .conv-row {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
gap: var(--sp-3);
|
|
gap: var(--sp-3);
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
min-height: 64px;
|
|
min-height: 64px;
|
|
|
padding: var(--sp-2) var(--sp-3);
|
|
padding: var(--sp-2) var(--sp-3);
|
|
|
- margin-bottom: var(--sp-2);
|
|
|
|
|
border: 1px solid var(--edge);
|
|
border: 1px solid var(--edge);
|
|
|
border-radius: var(--r-md);
|
|
border-radius: var(--r-md);
|
|
|
background: var(--surface);
|
|
background: var(--surface);
|
|
|
text-align: left;
|
|
text-align: left;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
}
|
|
}
|
|
|
- .row:hover {
|
|
|
|
|
|
|
+ .conv-row:active {
|
|
|
background: var(--surface-hover);
|
|
background: var(--surface-hover);
|
|
|
}
|
|
}
|
|
|
- .row-icon {
|
|
|
|
|
- display: grid;
|
|
|
|
|
- place-items: center;
|
|
|
|
|
- width: 32px;
|
|
|
|
|
- height: 32px;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
- border-radius: var(--r-sm);
|
|
|
|
|
- background: var(--info-subtle);
|
|
|
|
|
- color: var(--info);
|
|
|
|
|
- }
|
|
|
|
|
- .row-body {
|
|
|
|
|
|
|
+ .conv-body {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
gap: 2px;
|
|
gap: 2px;
|
|
|
flex: 1;
|
|
flex: 1;
|
|
|
min-width: 0;
|
|
min-width: 0;
|
|
|
}
|
|
}
|
|
|
- .row-body strong {
|
|
|
|
|
- font-size: var(--fs-13);
|
|
|
|
|
|
|
+ .conv-line {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: var(--sp-2);
|
|
|
|
|
+ }
|
|
|
|
|
+ .conv-line strong {
|
|
|
|
|
+ font-size: var(--fs-15);
|
|
|
color: var(--ink);
|
|
color: var(--ink);
|
|
|
}
|
|
}
|
|
|
- .row-body small {
|
|
|
|
|
|
|
+ .conv-line time {
|
|
|
font-size: var(--fs-11);
|
|
font-size: var(--fs-11);
|
|
|
color: var(--muted);
|
|
color: var(--muted);
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
- .row time {
|
|
|
|
|
- font-size: var(--fs-11);
|
|
|
|
|
|
|
+ .conv-preview {
|
|
|
|
|
+ font-size: var(--fs-12);
|
|
|
color: var(--muted);
|
|
color: var(--muted);
|
|
|
- flex-shrink: 0;
|
|
|
|
|
}
|
|
}
|
|
|
.unread {
|
|
.unread {
|
|
|
min-width: 18px;
|
|
min-width: 18px;
|
|
@@ -284,8 +248,10 @@ import { isMatrixReady } from '../../../core/format';
|
|
|
font-size: var(--fs-11);
|
|
font-size: var(--fs-11);
|
|
|
line-height: 18px;
|
|
line-height: 18px;
|
|
|
text-align: center;
|
|
text-align: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
}
|
|
}
|
|
|
- .truncate {
|
|
|
|
|
|
|
+
|
|
|
|
|
+ .ellipsis {
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
@@ -302,20 +268,17 @@ export class MbHomePage implements OnInit {
|
|
|
private readonly router = inject(Router);
|
|
private readonly router = inject(Router);
|
|
|
private readonly device = inject(DeviceService);
|
|
private readonly device = inject(DeviceService);
|
|
|
|
|
|
|
|
- readonly time = fmtTime;
|
|
|
|
|
|
|
+ readonly rel = fmtRelTime;
|
|
|
|
|
|
|
|
- /** 各区块独立三态(AC-19.2) */
|
|
|
|
|
readonly eventsState = computed(() => this.events.state());
|
|
readonly eventsState = computed(() => this.events.state());
|
|
|
readonly liveState = computed(() => this.live.state());
|
|
readonly liveState = computed(() => this.live.state());
|
|
|
-
|
|
|
|
|
readonly meName = computed(() => this.session.me()?.name ?? '');
|
|
readonly meName = computed(() => this.session.me()?.name ?? '');
|
|
|
-
|
|
|
|
|
readonly topAlert = computed(() => this.events.filtered().find((e) => e.category === 'alert'));
|
|
readonly topAlert = computed(() => this.events.filtered().find((e) => e.category === 'alert'));
|
|
|
|
|
|
|
|
- /** 取 3 条 —— 与「全部」计数分开表达,避免「显示 N 却只有 M 行」的误导 */
|
|
|
|
|
|
|
+ /** AC-19.1:列表取 3 条,与「全部 N」分开表达,避免「显示 N 却只有 M 行」的误导 */
|
|
|
readonly recentDeliverables = computed(() => this.deliverables.all().slice(0, 3));
|
|
readonly recentDeliverables = computed(() => this.deliverables.all().slice(0, 3));
|
|
|
- readonly recentAgents = computed(() => this.readyAgents().slice(0, 6));
|
|
|
|
|
readonly readyAgents = computed(() => this.live.agents().filter((a) => isMatrixReady(a)));
|
|
readonly readyAgents = computed(() => this.live.agents().filter((a) => isMatrixReady(a)));
|
|
|
|
|
+ readonly recentAgents = computed(() => this.readyAgents().slice(0, 6));
|
|
|
|
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
async ngOnInit(): Promise<void> {
|
|
|
await this.reload();
|
|
await this.reload();
|
|
@@ -353,12 +316,8 @@ export class MbHomePage implements OnInit {
|
|
|
|
|
|
|
|
openEvent(): void {
|
|
openEvent(): void {
|
|
|
const top = this.topAlert();
|
|
const top = this.topAlert();
|
|
|
- if (top?.taskId) {
|
|
|
|
|
- void this.router.navigate(['/', this.device.mode(), 'tasks', top.taskId]);
|
|
|
|
|
- } else if (top?.agentId) {
|
|
|
|
|
- void this.router.navigate(['/', this.device.mode(), 'agent', top.agentId]);
|
|
|
|
|
- } else {
|
|
|
|
|
- this.go('/events');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (top?.taskId) void this.router.navigate(['/', this.device.mode(), 'tasks', top.taskId]);
|
|
|
|
|
+ else if (top?.agentId) void this.router.navigate(['/', this.device.mode(), 'agent', top.agentId]);
|
|
|
|
|
+ else this.go('/events');
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|