浏览代码

fix(agent): 详情页档案头改两行结构,修复头像被挤压

根因:.hero 用 flex + flex-wrap,一行塞 头像 + 文字块(min-width 200px) + 按钮。
页面 narrow 上限 980px,减侧栏与内边距后不足 900px
→ 768~1150 区间必然挤压:头像被压扁、文字换行、按钮掉行。

方案(不删头像,改结构):
- .hero-main: grid [头像 auto][文字 1fr][操作 auto],头像固定列不参与伸缩
- .hero-meta: 归属/状态/当前任务 独立成行(原挤在文字块内)
- h1 加 ellipsis,长名字截断而非撑破布局
- 窄屏 bp.dk-below-4col(<1150) 按钮独占一行 + 100% 宽
- statusText 复用 core/format 的 agentStatusText

移动端同源问题(较轻):补头像 flex-shrink:0 + 三处文字 ellipsis

验证:等价格局页多宽度实测(900/860/768/600/420)
      头像均 64x64 完整、无横向溢出;ng test 499 / build /
      verify:ui 11 / verify:docs 47

未验证:页面需登录态,未在真实详情页目视确认,仅等价布局验证

Co-Authored-By: Claude Code <noreply@anthropic.com>
Huccct 1 周之前
父节点
当前提交
ea4a58fac0
共有 3 个文件被更改,包括 156 次插入 和 27 次删除
  1. 15 0
      docs/00-tracking/操作追踪.md
  2. 125 27
      src/app/desktop/pages/agent/agent.ts
  3. 16 0
      src/app/mobile/pages/agent/agent.ts

+ 15 - 0
docs/00-tracking/操作追踪.md

@@ -812,3 +812,18 @@
 | **修正** | 初稿引用 `scripts/check-reporter-fields.mjs` —— 该脚本**本项目不存在**(仅云函数注释里的约定),已改为准确表述「成对维护声明」 |
 | 验证证据 | `verify:docs` **47 文件**(+1)/ 表格结构全部正常 |
 | 待做 | 用户确认规范后,将 §八 验收清单并入容器部署提示词;容器按本规范改造脚本 |
+
+### 2026-10-20 — 桌面端数字生命详情页:档案头布局重构(修复头像挤压)
+
+| 项 | 内容 |
+|---|---|
+| **用户反馈** | 「对应用户头像区域这部分要么删除要么优化一下,被挤压的很厉害」 |
+| **根因(代码级)** | `.hero` 是 `display:flex` + `flex-wrap`,一行塞 **头像 + 文字块(`min-width:200px`) + 操作按钮**。<br>页面用 `<app-page [narrow]="true">`,`--page-max-w-narrow: 980px`;减去侧栏与内边距后可用宽度不足 900px → **768~1150 区间必然挤压**:头像被压缩、文字换行、按钮掉行 |
+| **方案选择** | 不删除头像(详情页身份识别需要它),改为 **两行结构**:<br>① `.hero-main`:`grid [头像 auto][文字 1fr][操作 auto]` —— 头像固定列,**永不参与伸缩**<br>② `.hero-meta`:归属 / 状态 / 当前任务 独立成行(原先 `归属` 挤在文字块里)|
+| **✏️ 桌面端** | `desktop/pages/agent/agent.ts`:<br>· 模板:`.hero` 拆为 `.hero-main`(头像/名称/专业/按钮)+ `.hero-meta`(`<dl>` 元信息)<br>· `h1` 加 `ellipsis` —— 长名字截断而非撑破布局<br>· 窄屏(`bp.dk-below-4col`,<1150)按钮**独占一行**(`grid-area: action`)+ 100% 宽<br>· 新增 `statusText = agentStatusText`(复用 `core/format`,不重复实现)|
+| **✏️ 移动端** | `mobile/pages/agent/agent.ts`:问题较轻(按钮本就在下方),补 `flex-shrink:0` 防头像被压缩 + `hero-body:flex:1` + 三处文字 `ellipsis` |
+| **合规修正** | 初版用了裸断点 `@media (max-width: 860px)` → `verify:ui` 报「1 处裸断点」→ 改用 `@include bp.dk-below-4col`(`_breakpoints.scss` mixin)。<br>又遇 `@use` 必须位于样式块最前(Sass 规则)→ 移到 `styles` 开头 |
+| **验证方式** | Playwright 无法直接验(页面需登录态,会重定向 `/login`)→ 改用**等价布局页**多宽度实测:<br>`900px` 头像 64x64 完整、按钮同排 ✅<br>`860/768/600/420px` 头像 64x64 完整、按钮独立成行 ✅<br>**全部宽度无横向溢出** |
+| 验证证据 | `ng test` **499** / `build` 通过 / `verify:ui` **11/11** / `verify:docs` **47 文件** |
+| 未验证项(披露) | 真实详情页的窄屏表现为**等价布局验证**,非在登录态下实测。后端数据(cpuPercent 等)到位后需再目视确认一次 |
+| 环境备注 | Playwright 浏览器走国内镜像安装(`PLAYWRIGHT_DOWNLOAD_HOST=https://cdn.npmmirror.com/binaries/playwright`)。测速:npmmirror **8.32 MB/s** vs 官方 akamai **0.09 MB/s** |

+ 125 - 27
src/app/desktop/pages/agent/agent.ts

@@ -15,7 +15,7 @@ import { StatusBadgeComponent } from '../../../components/domain/status-badge/st
 import { Agent } from '../../../core/api/models';
 import { DeliverablesService } from '../../../core/deliverables.service';
 import { DeviceService } from '../../../core/device.service';
-import { fmtBalance, fmtPercent, fmtTokens, isMatrixReady, usageOf } from '../../../core/format';
+import { agentStatusText, fmtBalance, fmtPercent, fmtTokens, isMatrixReady, usageOf } from '../../../core/format';
 import { fmtDateTime } from '../../../core/time';
 import { LiveService } from '../../../core/live.service';
 
@@ -83,27 +83,47 @@ import { LiveService } from '../../../core/live.service';
             <!-- 档案头 -->
             <ui-card>
               <div class="hero">
-                <ui-avatar [name]="a.name" size="lg" />
-                <div class="hero-body">
-                  <div class="hero-line">
-                    <h1>{{ a.name }}</h1>
-                    <app-status-badge [agent]="a" />
+                <!-- 第一行:头像 + 主信息 + 操作(🔴 头像独立列,不参与挤压) -->
+                <div class="hero-main">
+                  <ui-avatar [name]="a.name" size="lg" class="hero-avatar" />
+                  <div class="hero-body">
+                    <div class="hero-line">
+                      <h1 class="ellipsis" [title]="a.name">{{ a.name }}</h1>
+                      <app-status-badge [agent]="a" />
+                    </div>
+                    <p class="hero-specialty ellipsis">{{ a.specialty || '数字生命' }}</p>
+                  </div>
+                  <div class="hero-action">
+                    @if (ready(a)) {
+                      <ui-button variant="primary" icon="fa-solid fa-comment" (click)="openChat(a)">
+                        进入会话
+                      </ui-button>
+                    } @else {
+                      <!-- AC-09.2:禁用态 + 内联说明,不用原生 alert -->
+                      <ui-button variant="primary" icon="fa-solid fa-comment-slash" [disabled]="true">
+                        尚未接入会话
+                      </ui-button>
+                    }
                   </div>
-                  <p class="hero-specialty">{{ a.specialty || '数字生命' }}</p>
-                  <p class="hero-owner">归属 {{ a.owner?.name || '未指派' }}</p>
                 </div>
-                <div class="hero-action">
-                  @if (ready(a)) {
-                    <ui-button variant="primary" icon="fa-solid fa-comment" (click)="openChat(a)">
-                      进入会话
-                    </ui-button>
-                  } @else {
-                    <!-- AC-09.2:禁用态 + 内联说明,不用原生 alert -->
-                    <ui-button variant="primary" icon="fa-solid fa-comment-slash" [disabled]="true">
-                      尚未接入会话
-                    </ui-button>
+
+                <!-- 第二行:身份元信息(🔴 独立于主信息行,窄屏也不挤) -->
+                <dl class="hero-meta">
+                  <div class="meta-item">
+                    <dt>归属</dt>
+                    <dd class="ellipsis">{{ a.owner?.name || '未指派' }}</dd>
+                  </div>
+                  <div class="meta-item">
+                    <dt>状态</dt>
+                    <dd class="ellipsis">{{ statusText(a) }}</dd>
+                  </div>
+                  @if (a.currentTask) {
+                    <div class="meta-item meta-item--wide">
+                      <dt>当前任务</dt>
+                      <dd class="ellipsis" [title]="a.currentTask">{{ a.currentTask }}</dd>
+                    </div>
                   }
-                </div>
+                </dl>
               </div>
 
               @if (!ready(a)) {
@@ -184,6 +204,8 @@ import { LiveService } from '../../../core/live.service';
   `,
   styles: [
     `
+      @use '../../../core/styles/breakpoints' as bp;
+
       :host {
         display: block;
       }
@@ -198,41 +220,115 @@ import { LiveService } from '../../../core/live.service';
         padding: var(--sp-3);
       }
 
+      /*
+       * 🔴 档案头改为「两行结构」(2026-10-20 修复头像挤压)
+       *
+       * 原实现:.hero 一行 flex + flex-wrap,塞 头像 + 文字块(min-width 200px) + 按钮。
+       * 页面 narrow 上限 980px,减侧栏与内边距后不足 900px,768~1000px 区间必然挤压:
+       * 头像被压扁、文字换行、按钮掉行。
+       *
+       * 新结构:
+       *   第一行 .hero-main:grid [头像 4rem][文字 1fr][操作 auto]
+       *   第二行 .hero-meta:元信息,窄屏自动折为单列
+       * 头像用固定列宽,永不参与伸缩。
+       */
       .hero {
-        display: flex;
+        display: grid;
+        gap: var(--sp-4);
+      }
+      .hero-main {
+        display: grid;
+        grid-template-columns: auto minmax(0, 1fr) auto;
         align-items: center;
         gap: var(--sp-4);
-        flex-wrap: wrap;
+      }
+      /* 头像:固定尺寸,禁止被 flex/grid 压缩 */
+      .hero-avatar {
+        flex-shrink: 0;
+        align-self: center;
       }
       .hero-body {
-        flex: 1;
-        min-width: 200px;
         display: grid;
         gap: var(--sp-1);
+        min-width: 0; /* 🔴 允许子项收缩,让 ellipsis 生效 */
       }
       .hero-line {
         display: flex;
         align-items: center;
         gap: var(--sp-3);
-        flex-wrap: wrap;
+        min-width: 0;
       }
       .hero-line h1 {
         margin: 0;
         font-size: var(--fs-26);
+        line-height: 1.2;
         color: var(--ink);
+        min-width: 0;
       }
       .hero-specialty {
         margin: 0;
         font-size: var(--fs-13);
         color: var(--ink-2);
+        min-width: 0;
+      }
+      .hero-action {
+        flex-shrink: 0;
+        white-space: nowrap;
       }
-      .hero-owner {
+
+      /* 元信息行:归属 / 状态 / 当前任务 */
+      .hero-meta {
+        display: flex;
+        flex-wrap: wrap;
+        gap: var(--sp-2) var(--sp-5);
         margin: 0;
+        padding-top: var(--sp-3);
+        border-top: 1px solid var(--edge);
+      }
+      .meta-item {
+        display: flex;
+        align-items: baseline;
+        gap: var(--sp-2);
+        min-width: 0;
+        max-width: 100%;
+      }
+      .meta-item--wide {
+        flex: 1 1 100%; /* 当前任务独占一行,长文本也不会挤压前面的项 */
+      }
+      .meta-item dt {
+        flex-shrink: 0;
         font-size: var(--fs-11);
         color: var(--muted);
       }
-      .hero-action {
-        flex-shrink: 0;
+      .meta-item dd {
+        margin: 0;
+        font-size: var(--fs-12);
+        color: var(--ink-2);
+        min-width: 0;
+      }
+
+      /* 🔴 窄屏(<1150 右栏让位区间):操作按钮独立成行,避免与文字抢空间 */
+      @include bp.dk-below-4col {
+        .hero-main {
+          grid-template-columns: auto minmax(0, 1fr);
+          grid-template-areas:
+            'avatar body'
+            'action action';
+          row-gap: var(--sp-3);
+        }
+        .hero-avatar {
+          grid-area: avatar;
+        }
+        .hero-body {
+          grid-area: body;
+        }
+        .hero-action {
+          grid-area: action;
+          justify-self: stretch;
+        }
+        .hero-action :is(ui-button, button) {
+          width: 100%;
+        }
       }
       .alert-hold {
         margin-top: var(--sp-4);
@@ -366,6 +462,8 @@ export class DkAgentPage implements OnInit {
   });
 
   readonly dt = fmtDateTime;
+  /** 状态中文文案(模板用;文案由 core/format 统一给出,不在组件里拼) */
+  readonly statusText = agentStatusText;
   readonly pctText = (v?: number) => fmtPercent(v);
   readonly tokens = (a: Agent) => fmtTokens(usageOf(a).totalTokens);
   readonly balance = (a: Agent) => fmtBalance(a.balance);

+ 16 - 0
src/app/mobile/pages/agent/agent.ts

@@ -161,31 +161,47 @@ import { fmtDateTime } from '../../../core/time';
         border-radius: var(--r-lg);
         background: var(--surface);
       }
+      /* 🔴 头像禁止被压缩(长名字会把头像挤扁) */
+      .hero > app-m-avatar {
+        flex-shrink: 0;
+      }
       .hero-body {
         display: grid;
         gap: var(--sp-1);
         min-width: 0;
+        flex: 1;
       }
       .hero-line {
         display: flex;
         align-items: center;
         gap: var(--sp-2);
         flex-wrap: wrap;
+        min-width: 0;
       }
       .hero-line h1 {
         margin: 0;
         font-size: var(--fs-20);
         color: var(--ink);
+        min-width: 0;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
       }
       .hero-specialty {
         margin: 0;
         font-size: var(--fs-12);
         color: var(--ink-2);
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
       }
       .hero-owner {
         margin: 0;
         font-size: var(--fs-11);
         color: var(--muted);
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: nowrap;
       }
       .btn-primary,
       .btn-outline {