|
|
@@ -1,138 +1,52 @@
|
|
|
-import { MBackBarComponent } from '../../components/m-back-bar/m-back-bar';
|
|
|
-import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core';
|
|
|
+import { ChangeDetectionStrategy, Component, computed, inject, OnInit } from '@angular/core';
|
|
|
+import { ActivatedRoute, Router } from '@angular/router';
|
|
|
import { MFilterRowComponent } from '../../components/m-filter-row/m-filter-row';
|
|
|
+import { MBackBarComponent } from '../../components/m-back-bar/m-back-bar';
|
|
|
import { MStatePanelComponent } from '../../components/m-state-panel/m-state-panel';
|
|
|
import { MStatusBadgeComponent } from '../../components/m-status-badge/m-status-badge';
|
|
|
-import { UiMButton, UiMCellGroup, UiMDropdownMenu } from '../../../ui';
|
|
|
-import { ActivatedRoute, Router } from '@angular/router';
|
|
|
-import { } from '../../../components/domain/filter-chips/filter-chips';
|
|
|
+import { UiMDropdownMenu } from '../../../ui';
|
|
|
+import { LiveService } from '../../../core/live.service';
|
|
|
import { TaskStatus, TASK_STATUS_TEXT } from '../../../core/api/models';
|
|
|
import { TasksService } from '../../../core/tasks.service';
|
|
|
-import { LiveService } from '../../../core/live.service';
|
|
|
-import { DeviceService } from '../../../core/device.service';
|
|
|
-import { fmtDateTime, fmtDuration, fmtTime } from '../../../core/time';
|
|
|
-import { fmtSize } from '../../../core/format';
|
|
|
+import { fmtDuration, fmtTime } from '../../../core/time';
|
|
|
|
|
|
-/**
|
|
|
- * 移动·任务看板(SPEC FR-07b 移动形态)
|
|
|
- *
|
|
|
- * 列表 ↔ 详情同页切换(AC-07b.4 双端同一服务)。
|
|
|
- * **一期只读**:无任何派发/评论入口(AC-07b.6)。
|
|
|
- */
|
|
|
+/** 移动任务列表:固定工具区,只有列表区域滚动。详情使用 /tasks/:id 独立路由。 */
|
|
|
@Component({
|
|
|
selector: 'app-mb-tasks-page',
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
- imports: [MBackBarComponent, MStatePanelComponent, MStatusBadgeComponent, UiMButton, UiMCellGroup, UiMDropdownMenu, MFilterRowComponent],
|
|
|
+ imports: [MBackBarComponent, MStatePanelComponent, MStatusBadgeComponent, UiMDropdownMenu, MFilterRowComponent],
|
|
|
template: `
|
|
|
- <div class="m-page">
|
|
|
- @if (detail(); as t) {
|
|
|
- <app-m-back-bar
|
|
|
- [title]="t.title"
|
|
|
- subtitle="任务详情"
|
|
|
- fallbackPath="/tasks"
|
|
|
- backLabel="返回任务看板"
|
|
|
- (back)="backToList()"
|
|
|
- >
|
|
|
- <span barActions class="readonly-chip">
|
|
|
- <i class="fa-solid fa-lock" aria-hidden="true"></i>只读
|
|
|
- </span>
|
|
|
- </app-m-back-bar>
|
|
|
-
|
|
|
- <article class="detail">
|
|
|
- <div class="detail-top">
|
|
|
- <span class="mono muted">{{ t.id }}</span>
|
|
|
- <app-m-status-badge [task]="t" />
|
|
|
- </div>
|
|
|
- <h1>{{ t.title }}</h1>
|
|
|
-
|
|
|
- @if (t.status === 'blocked') {
|
|
|
- <div class="block-banner" role="alert">
|
|
|
- <i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
|
|
|
- <div>
|
|
|
- <strong>已阻塞 {{ dur(t.blockedHours) }} · 连续失败 {{ t.consecutiveFailures || 0 }} 次</strong>
|
|
|
- @if (t.blockKind) {
|
|
|
- <p class="mono">{{ t.blockKind }}</p>
|
|
|
- }
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- }
|
|
|
-
|
|
|
- <dl class="facts">
|
|
|
- <div><dt>数字生命</dt><dd>{{ agentName(t.agentId) }}</dd></div>
|
|
|
- <div><dt>负责人</dt><dd>{{ t.owner || '—' }}</dd></div>
|
|
|
- <div><dt>最近更新</dt><dd>{{ dt(t.updatedAt) }}</dd></div>
|
|
|
- <div><dt>分支</dt><dd class="mono">{{ t.branchName || '—' }}</dd></div>
|
|
|
- </dl>
|
|
|
-
|
|
|
- @if (t.body) {
|
|
|
- <h2 class="section-title">任务说明</h2>
|
|
|
- <pre class="markdown">{{ t.body }}</pre>
|
|
|
- }
|
|
|
-
|
|
|
- <h2 class="section-title">运行流水 <span class="count">{{ t.runs.length }}</span></h2>
|
|
|
- @if (t.runs.length === 0) {
|
|
|
- <p class="empty-inline">任务尚未运行,暂无记录。</p>
|
|
|
- } @else {
|
|
|
- @for (r of t.runs; track r.id) {
|
|
|
- <div class="run-row">
|
|
|
- <span class="run-node" [class.warn]="r.status === 'blocked'" [class.ok]="r.status === 'done'">
|
|
|
- <i class="fa-solid" [class]="r.status === 'done' ? 'fa-check' : r.status === 'blocked' ? 'fa-triangle-exclamation' : 'fa-clock'" aria-hidden="true"></i>
|
|
|
- </span>
|
|
|
- <span class="run-body">
|
|
|
- <strong>{{ r.title || r.stepKey || '运行' }}</strong>
|
|
|
- @if (r.detail || r.summary) {
|
|
|
- <small>{{ r.detail || r.summary }}</small>
|
|
|
- }
|
|
|
- </span>
|
|
|
- <time>{{ time(r.endedAt || r.startedAt) }}</time>
|
|
|
- </div>
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- <ui-m-cell-group title="附件">
|
|
|
- <span class="count">{{ t.attachments.length }}</span>
|
|
|
- </ui-m-cell-group>
|
|
|
- @if (t.attachments.length === 0) {
|
|
|
- <p class="empty-inline">暂无附件。</p>
|
|
|
- } @else {
|
|
|
- @for (att of t.attachments; track att.id) {
|
|
|
- <div class="attach-row">
|
|
|
- <i class="fa-solid fa-file-lines" aria-hidden="true"></i>
|
|
|
- <span class="truncate">{{ att.filename }}</span>
|
|
|
- <small>{{ size(att.size) }}</small>
|
|
|
- </div>
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- <h2 class="section-title">历史评论 <span class="readonly-chip">只读</span></h2>
|
|
|
- @if (t.comments.length === 0) {
|
|
|
- <p class="empty-inline">暂无历史评论。</p>
|
|
|
- } @else {
|
|
|
- @for (c of t.comments; track c.id) {
|
|
|
- <p class="comment">{{ c.author ? c.author + ' · ' : '' }}{{ c.body }}</p>
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- <ui-m-button variant="secondary" (click)="openChat(t.agentId)">
|
|
|
- <i class="fa-solid fa-comment" aria-hidden="true"></i>进入会话
|
|
|
- </ui-m-button>
|
|
|
- <p class="foot-note">任务看板仅供查看,指令通过一对一会话沟通。</p>
|
|
|
- </article>
|
|
|
- } @else {
|
|
|
- <header class="m-head">
|
|
|
- <h1>任务看板</h1>
|
|
|
- <span class="readonly-chip"><i class="fa-solid fa-lock" aria-hidden="true"></i>只读</span>
|
|
|
- </header>
|
|
|
-
|
|
|
+ <div class="m-page m-page--fixed">
|
|
|
+ <app-m-back-bar
|
|
|
+ title="任务看板"
|
|
|
+ [subtitle]="activeAgentName() || '只读任务视图'"
|
|
|
+ fallbackPath="/workbench"
|
|
|
+ backLabel="返回工作台"
|
|
|
+ >
|
|
|
+ <span barActions class="readonly-chip"><i class="fa-solid fa-lock" aria-hidden="true"></i>只读</span>
|
|
|
+ </app-m-back-bar>
|
|
|
+
|
|
|
+ <div class="toolbar">
|
|
|
+ <div class="agent-picker-hold">
|
|
|
<ui-m-dropdown-menu [menus]="agentMenus()" (change)="onAgentMenuChange($event)" />
|
|
|
+ </div>
|
|
|
|
|
|
+ <div class="filter-hold">
|
|
|
<app-m-filter-row
|
|
|
ariaLabel="任务状态"
|
|
|
[items]="statusFilterItems()"
|
|
|
[active]="tasks.statusFilter()"
|
|
|
(select)="onStatus($event)"
|
|
|
/>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
+ <div class="list-summary">
|
|
|
+ <strong>任务列表</strong>
|
|
|
+ <span>{{ tasks.filtered().length }} 项</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="task-scroll">
|
|
|
@switch (tasks.state()) {
|
|
|
@case ('loading') {
|
|
|
<app-m-state-panel state="loading" loadingText="正在加载任务…" />
|
|
|
@@ -144,7 +58,7 @@ import { fmtSize } from '../../../core/format';
|
|
|
<app-m-state-panel
|
|
|
state="empty"
|
|
|
title="暂无任务数据"
|
|
|
- message="接入云函数后此处会显示真实任务。"
|
|
|
+ message="任务数据来自容器 kanban,接入后会显示真实任务。"
|
|
|
[retryable]="false"
|
|
|
/>
|
|
|
}
|
|
|
@@ -181,274 +95,94 @@ import { fmtSize } from '../../../core/format';
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
+ </div>
|
|
|
</div>
|
|
|
`,
|
|
|
styles: [
|
|
|
`
|
|
|
+ :host { display: block; height: 100%; }
|
|
|
.m-page {
|
|
|
- padding: var(--sp-5) var(--mob-gutter) var(--sp-6);
|
|
|
- }
|
|
|
- .m-head {
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: var(--sp-2);
|
|
|
- margin-bottom: var(--sp-4);
|
|
|
- }
|
|
|
- .m-head h1 {
|
|
|
- margin: 0;
|
|
|
- font-size: var(--fs-26);
|
|
|
- color: var(--ink);
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ min-height: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ box-sizing: border-box;
|
|
|
+ padding: 0 0 calc(var(--sp-3) + var(--mob-safe-bottom));
|
|
|
+ background: var(--canvas);
|
|
|
}
|
|
|
+ .toolbar { flex-shrink: 0; margin-inline: var(--mob-gutter); padding-top: var(--sp-3); }
|
|
|
.readonly-chip {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
gap: var(--sp-1);
|
|
|
+ min-height: 28px;
|
|
|
padding: 0 var(--sp-2);
|
|
|
border-radius: var(--r-sm);
|
|
|
background: var(--surface-hover);
|
|
|
color: var(--muted);
|
|
|
font-size: var(--fs-11);
|
|
|
- line-height: 24px;
|
|
|
}
|
|
|
- .agent-picker {
|
|
|
+ .agent-picker-hold,
|
|
|
+ .filter-hold,
|
|
|
+ .list-summary { flex-shrink: 0; }
|
|
|
+ .agent-picker-hold { margin-bottom: var(--sp-2); }
|
|
|
+ .filter-hold { margin-bottom: var(--sp-2); }
|
|
|
+ .list-summary {
|
|
|
+ margin-inline: var(--mob-gutter);
|
|
|
display: flex;
|
|
|
- align-items: center;
|
|
|
+ align-items: baseline;
|
|
|
+ justify-content: space-between;
|
|
|
gap: var(--sp-2);
|
|
|
- margin-bottom: var(--sp-3);
|
|
|
- font-size: var(--fs-13);
|
|
|
+ padding: var(--sp-1) 0 var(--sp-2);
|
|
|
color: var(--ink-2);
|
|
|
+ font-size: var(--fs-12);
|
|
|
}
|
|
|
- .agent-picker select {
|
|
|
+ .list-summary strong { color: var(--ink); font-size: var(--fs-13); }
|
|
|
+ .list-summary span { color: var(--muted); }
|
|
|
+ .task-scroll {
|
|
|
flex: 1;
|
|
|
- min-height: var(--touch-min);
|
|
|
- padding: 0 var(--sp-3);
|
|
|
- border: 1px solid var(--edge);
|
|
|
- border-radius: var(--r-md);
|
|
|
- background: var(--surface);
|
|
|
- color: var(--ink);
|
|
|
- font-size: var(--fs-17);
|
|
|
+ min-height: 0;
|
|
|
+ margin-inline: var(--mob-gutter);
|
|
|
+ overflow-y: auto;
|
|
|
+ overscroll-behavior: contain;
|
|
|
+ padding-bottom: calc(var(--sp-5) + var(--mob-safe-bottom));
|
|
|
}
|
|
|
.rows {
|
|
|
display: grid;
|
|
|
- gap: var(--sp-2);
|
|
|
- margin-top: var(--sp-4);
|
|
|
+ gap: 1px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid var(--edge);
|
|
|
+ border-radius: var(--r-md);
|
|
|
+ background: var(--edge);
|
|
|
}
|
|
|
.row {
|
|
|
display: grid;
|
|
|
gap: var(--sp-1);
|
|
|
width: 100%;
|
|
|
+ min-height: 86px;
|
|
|
padding: var(--sp-3);
|
|
|
- border: 1px solid var(--edge);
|
|
|
- border-radius: var(--r-md);
|
|
|
- background: var(--surface);
|
|
|
- text-align: left;
|
|
|
- cursor: pointer;
|
|
|
- }
|
|
|
- .row.blocked {
|
|
|
- border-left: 3px solid var(--danger);
|
|
|
- }
|
|
|
- .row-top {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: var(--sp-2);
|
|
|
- }
|
|
|
- .row strong {
|
|
|
- font-size: var(--fs-15);
|
|
|
- color: var(--ink);
|
|
|
- }
|
|
|
- .row-meta {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- font-size: var(--fs-11);
|
|
|
- color: var(--muted);
|
|
|
- }
|
|
|
- .blocked-note {
|
|
|
- display: inline-flex;
|
|
|
- align-items: center;
|
|
|
- gap: var(--sp-1);
|
|
|
- font-size: var(--fs-11);
|
|
|
- color: var(--danger);
|
|
|
- }
|
|
|
- .detail h1 {
|
|
|
- margin: var(--sp-2) 0 var(--sp-4);
|
|
|
- font-size: var(--fs-20);
|
|
|
- color: var(--ink);
|
|
|
- }
|
|
|
- .detail-top {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- justify-content: space-between;
|
|
|
- gap: var(--sp-2);
|
|
|
- }
|
|
|
- .mono {
|
|
|
- font-family: var(--mono, monospace);
|
|
|
- }
|
|
|
- .muted {
|
|
|
- color: var(--muted);
|
|
|
- }
|
|
|
- .block-banner {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- gap: var(--sp-3);
|
|
|
- padding: var(--sp-3);
|
|
|
- margin-bottom: var(--sp-4);
|
|
|
- border-radius: var(--r-md);
|
|
|
- background: var(--danger-subtle);
|
|
|
- color: var(--danger);
|
|
|
- }
|
|
|
- .block-banner strong {
|
|
|
- font-size: var(--fs-13);
|
|
|
- }
|
|
|
- .block-banner p {
|
|
|
- margin: 2px 0 0;
|
|
|
- font-size: var(--fs-11);
|
|
|
- }
|
|
|
- .facts {
|
|
|
- display: grid;
|
|
|
- grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
|
|
- gap: var(--sp-3);
|
|
|
- margin: 0 0 var(--sp-4);
|
|
|
- padding: var(--sp-4);
|
|
|
- border-radius: var(--r-md);
|
|
|
- background: var(--surface-2);
|
|
|
- }
|
|
|
- .facts > div {
|
|
|
- display: grid;
|
|
|
- gap: 2px;
|
|
|
- }
|
|
|
- .facts dt {
|
|
|
- font-size: var(--fs-11);
|
|
|
- color: var(--muted);
|
|
|
- }
|
|
|
- .facts dd {
|
|
|
- margin: 0;
|
|
|
- font-size: var(--fs-13);
|
|
|
- color: var(--ink);
|
|
|
- overflow-wrap: anywhere;
|
|
|
- }
|
|
|
- .section-title {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: var(--sp-2);
|
|
|
- margin: var(--sp-5) 0 var(--sp-3);
|
|
|
- font-size: var(--fs-13);
|
|
|
- color: var(--ink-2);
|
|
|
- }
|
|
|
- .count {
|
|
|
- font-size: var(--fs-11);
|
|
|
- color: var(--muted);
|
|
|
- }
|
|
|
- .markdown {
|
|
|
- margin: 0;
|
|
|
- padding: var(--sp-4);
|
|
|
- border-radius: var(--r-md);
|
|
|
- background: var(--surface-2);
|
|
|
- color: var(--ink);
|
|
|
- font-family: inherit;
|
|
|
- font-size: var(--fs-13);
|
|
|
- line-height: 1.8;
|
|
|
- white-space: pre-wrap;
|
|
|
- overflow-wrap: anywhere;
|
|
|
- }
|
|
|
- .run-row {
|
|
|
- display: flex;
|
|
|
- align-items: flex-start;
|
|
|
- gap: var(--sp-3);
|
|
|
- padding: var(--sp-2) 0;
|
|
|
- border-bottom: 1px solid var(--edge);
|
|
|
- }
|
|
|
- .run-node {
|
|
|
- display: grid;
|
|
|
- place-items: center;
|
|
|
- width: 24px;
|
|
|
- height: 24px;
|
|
|
- flex-shrink: 0;
|
|
|
- border-radius: var(--r-tag);
|
|
|
- background: var(--info-subtle);
|
|
|
- color: var(--info);
|
|
|
- font-size: var(--fs-11);
|
|
|
- }
|
|
|
- .run-node.ok {
|
|
|
- background: var(--success-subtle);
|
|
|
- color: var(--success);
|
|
|
- }
|
|
|
- .run-node.warn {
|
|
|
- background: var(--danger-subtle);
|
|
|
- color: var(--danger);
|
|
|
- }
|
|
|
- .run-body {
|
|
|
- display: grid;
|
|
|
- gap: 2px;
|
|
|
- flex: 1;
|
|
|
- min-width: 0;
|
|
|
- }
|
|
|
- .run-body strong {
|
|
|
- font-size: var(--fs-13);
|
|
|
- color: var(--ink);
|
|
|
- }
|
|
|
- .run-body small {
|
|
|
- font-size: var(--fs-11);
|
|
|
- color: var(--muted);
|
|
|
- }
|
|
|
- .run-row time {
|
|
|
- font-size: var(--fs-11);
|
|
|
- color: var(--muted);
|
|
|
- flex-shrink: 0;
|
|
|
- }
|
|
|
- .attach-row {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: var(--sp-2);
|
|
|
- padding: var(--sp-2) 0;
|
|
|
- font-size: var(--fs-13);
|
|
|
- color: var(--ink);
|
|
|
+ border: 0;
|
|
|
border-bottom: 1px solid var(--edge);
|
|
|
- }
|
|
|
- .attach-row small {
|
|
|
- margin-left: auto;
|
|
|
- font-size: var(--fs-11);
|
|
|
- color: var(--muted);
|
|
|
- }
|
|
|
- .comment {
|
|
|
- margin: 0 0 var(--sp-2);
|
|
|
- padding: var(--sp-3);
|
|
|
- border-radius: var(--r-md);
|
|
|
- background: var(--surface-2);
|
|
|
- font-size: var(--fs-12);
|
|
|
- color: var(--ink-2);
|
|
|
- }
|
|
|
- .empty-inline {
|
|
|
- margin: 0;
|
|
|
- font-size: var(--fs-13);
|
|
|
- color: var(--muted);
|
|
|
- }
|
|
|
- .btn-outline {
|
|
|
- display: inline-flex;
|
|
|
- align-items: center;
|
|
|
- gap: var(--sp-2);
|
|
|
- min-height: var(--touch-min);
|
|
|
- margin-top: var(--sp-5);
|
|
|
- padding: 0 var(--sp-4);
|
|
|
- border: 1px solid var(--edge-strong);
|
|
|
- border-radius: var(--r-sm);
|
|
|
background: var(--surface);
|
|
|
color: var(--ink);
|
|
|
- font-size: var(--fs-13);
|
|
|
+ text-align: left;
|
|
|
cursor: pointer;
|
|
|
- }
|
|
|
- .foot-note {
|
|
|
- margin: var(--sp-3) 0 0;
|
|
|
- font-size: var(--fs-11);
|
|
|
- color: var(--muted);
|
|
|
- }
|
|
|
- .truncate {
|
|
|
- overflow: hidden;
|
|
|
- text-overflow: ellipsis;
|
|
|
- white-space: nowrap;
|
|
|
+ transition: background-color var(--motion-fast) var(--ease-standard), transform var(--motion-fast) var(--ease-standard);
|
|
|
+ }
|
|
|
+ .row:last-child { border-bottom: 0; }
|
|
|
+ .row:active { background: var(--surface-hover); transform: scale(0.995); }
|
|
|
+ .row.blocked { box-shadow: inset 3px 0 0 var(--danger); }
|
|
|
+ .row-top,
|
|
|
+ .row-meta { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); }
|
|
|
+ .row strong { font-size: var(--fs-15); }
|
|
|
+ .row-meta { font-size: var(--fs-11); color: var(--muted); }
|
|
|
+ .blocked-note { display: inline-flex; align-items: center; gap: var(--sp-1); font-size: var(--fs-11); color: var(--danger); }
|
|
|
+ .mono { font-family: var(--mono, monospace); }
|
|
|
+ .muted { color: var(--muted); }
|
|
|
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
+ @media (prefers-reduced-motion: reduce) {
|
|
|
+ .row { transition: none; }
|
|
|
}
|
|
|
`,
|
|
|
],
|
|
|
@@ -458,21 +192,11 @@ export class MbTasksPage implements OnInit {
|
|
|
readonly live = inject(LiveService);
|
|
|
private readonly route = inject(ActivatedRoute);
|
|
|
private readonly router = inject(Router);
|
|
|
- private readonly device = inject(DeviceService);
|
|
|
|
|
|
readonly time = fmtTime;
|
|
|
- readonly dt = fmtDateTime;
|
|
|
readonly dur = fmtDuration;
|
|
|
- readonly size = fmtSize;
|
|
|
-
|
|
|
- readonly detailId = signal<string | null>(null);
|
|
|
+ readonly activeAgentName = computed(() => this.live.agentById(this.tasks.agentId())?.name ?? '');
|
|
|
|
|
|
- readonly detail = computed(() => {
|
|
|
- const id = this.detailId();
|
|
|
- return id ? this.tasks.all().find((t) => t.id === id) : undefined;
|
|
|
- });
|
|
|
-
|
|
|
- /** 数字生命下拉(移动件 ui-m-dropdown-menu 的配置结构) */
|
|
|
readonly agentMenus = computed(() => [
|
|
|
{
|
|
|
name: '数字生命',
|
|
|
@@ -493,42 +217,26 @@ export class MbTasksPage implements OnInit {
|
|
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
|
await this.live.ensureLoaded();
|
|
|
- const routeId = this.route.snapshot.paramMap.get('id');
|
|
|
- const agentId = this.live.agents()[0]?.id ?? '';
|
|
|
+ const queryAgentId = this.route.snapshot.queryParamMap.get('agent');
|
|
|
+ const agentId = queryAgentId && this.live.agentById(queryAgentId)
|
|
|
+ ? queryAgentId
|
|
|
+ : this.live.agents()[0]?.id ?? '';
|
|
|
if (agentId) await this.tasks.loadFor(agentId);
|
|
|
- if (routeId) {
|
|
|
- // URL 传的是 taskId
|
|
|
- if (this.tasks.all().some((t) => t.id === routeId)) this.detailId.set(routeId);
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
- async reload(): Promise<void> {
|
|
|
- await this.tasks.loadFor(this.tasks.agentId());
|
|
|
- }
|
|
|
+ async reload(): Promise<void> { await this.tasks.loadFor(this.tasks.agentId()); }
|
|
|
|
|
|
onAgentMenuChange(e: { index: number; value: unknown }): void {
|
|
|
- void this.tasks.loadFor(String(e.value));
|
|
|
- this.tasks.selectFirstIfNeeded();
|
|
|
+ const agentId = String(e.value);
|
|
|
+ void this.tasks.loadFor(agentId);
|
|
|
+ void this.router.navigate(['/mobile', 'tasks'], { queryParams: { agent: agentId } });
|
|
|
}
|
|
|
|
|
|
- onStatus(key: string): void {
|
|
|
- this.tasks.setStatusFilter(key as TaskStatus | 'all');
|
|
|
- }
|
|
|
+ onStatus(key: string): void { this.tasks.setStatusFilter(key as TaskStatus | 'all'); }
|
|
|
|
|
|
openDetail(id: string): void {
|
|
|
- this.detailId.set(id);
|
|
|
- }
|
|
|
-
|
|
|
- /** 返回列表:清详情态(不重新加载,列表滚动位置保持) */
|
|
|
- backToList(): void {
|
|
|
- this.detailId.set(null);
|
|
|
- }
|
|
|
-
|
|
|
- agentName(id: string): string {
|
|
|
- return this.live.agentById(id)?.name ?? '数字生命';
|
|
|
- }
|
|
|
-
|
|
|
- openChat(agentId: string): void {
|
|
|
- void this.router.navigate(['/', this.device.mode(), 'chat', agentId]);
|
|
|
+ void this.router.navigate(['/mobile', 'tasks', id], {
|
|
|
+ queryParams: { agent: this.tasks.agentId() || null },
|
|
|
+ });
|
|
|
}
|
|
|
}
|