# 11: 设备详情聚合与分区状态 **What to build:** 通过设备详情聚合契约加载设备档案、最新指标、预警、报警、工单状态和最新图片。各分区可独立成功、失败、为空或过期。 Blocked by: 10 Status: resolved Labels: ready-for-agent - [x] 设备详情按设备编码加载。 - [x] 档案、指标、预警、报警、工单和图片分区独立渲染。 - [x] 任一分区失败时不遮盖其他成功分区。 - [x] 空数据、字段缺失和过期状态均有明确 UI。 - [x] 最新图片只展示后端指定的一张。 - [x] 有分区状态映射单测。 ## 进度:100% **已完成并关闭(2026-09-18)。** 逻辑闸门 **61/61**、真机走查 **29/29**;回归 12 个既有逻辑闸门全绿。 ### 交付物 - `domain/equipmentDetailView.js`(新):六个分区的**界面视图**(纯函数)。 面板渲染计划 `EQUIPMENT_SECTION_PANELS`(档案 / 指标 / 预警 / 报警 / 工单 / 图片)、 占位符与空/失败/不可用措辞、档案行 / 指标行 / 事件行 / 工单行 / 图片视图, 外加三个判据:`sectionHasContent`(过期算有值)、`equipmentDetailMatches`(这份载荷是不是当前这台)、 `aggregateUnavailable`(整包失败,见下)。 - `components/EquipmentDetailSections.vue`(新):六块分区各带自己的状态标签与内容, 一块坏了缩成一行提示、其余照常画。 - `components/PointDetailCard.vue`:第二级由「待接入」占位换成真分区容器。 - `stores/detailStore.js`:`loadEquipmentAggregate`(按编码取数)+ `deviceLoading`, 并把**进设备级**的两个入口(`selectDevice` / `openDevice`)改成自己负责取数。 - `mocks/contractMocks.js`:新增 `equipment-partial` 场景(档案/工单/图片各自 FAILED, 指标/预警/报警 SUCCESS)。原先那段分区级失败样例挂在 `failed` 分支下**永远走不到** (`failed` 在注册层就抛错),所以「任一分区失败不遮盖其它分区」在 mock 上一直无法演示。 ### 三条值得记下的判断 **1. 票 10 的「进第二级不取数」是延期,不是契约。** 票 10 票面原文写明「设备详情聚合的加载是票 11 的契约」, 所以本票把取数收进 `selectDevice` / `openDevice` 这两个**唯一**的进级入口: 「进了设备级却没取数」在结构上不可能发生,调用方不必记得多调一次。 竞态收口沿用票 10 的点位级同款(token 丢弃迟到响应),并追加两条: 已答过的同一台不再重复取(返回卡片列表再点回来不重拉), 失败/不可用**不算已答**(留重试路径,否则永远没有恢复路径)。 **2. 「整包失败」与「分区级失败」必须分开,否则界面对值班人员撒谎。** 聚合端点整体读不到时 `data` 为 null、六块一起没数据,若照常渲染六块, 每块都会说「该分区加载失败(其它分区不受影响)」—— 那句话在只有一块坏时是真的, 在六块一起坏时是**假的**。所以整包失败走一句实话 + 「重新加载」入口, 分区级失败(`aggregateState: PARTIAL`、各分区自带 state、`data` 在)才逐块渲染。 这是本票验收第 3 条的反面,专门写进了 `aggregateUnavailable` 的注释与单测。 (附带发现:失败分区 `data` 为 null、带不出设备编码,所以该判据**刻意不比对编码**, 靠 store 的 token 纪律保证「失败态必定属于当前那台」;D11 把这个前提钉住了。) **3. 不把接口词汇漏到大屏上。** 后端 `stateReason`(`SAMPLE_OUTDATED` 这类) 与字段缺失清单(`archive.deviceName` 这类路径)都是接口契约词汇。 票 05 的真机走查已经逮到过一次同类泄漏,所以:认得出的原因翻人话、 认不出的**回空串**;缺失只报**条数**,字段路径进 `title`(排查拿得到,值班人员看不到)。 两条都有单测 + 组件静态守卫(F4b/F4c),并且**都做了变异测试**确认守卫有牙 (F4b 第一版只查引号字面量,会放过模板里的裸文本,已修)。 ### 验证 逻辑闸门 **61/61**(`verify/t11-equipment-detail.mjs`,六段:分区映射、视图层、store 取数与竞态、 分区独立、图片单值、静态守卫)。真机走查 **29/29**(`verify/t11-equipment-detail-walkthrough.mjs`, 五组见下)。回归 12 个既有逻辑闸门全绿 (t01 / t04 / t05 188 / t06 / t07 39 / t08 / t09 38 / t10 57 / t21 / t21-view / t29 / t30)。 **t05 的静态守卫面已扩到本票新增的两个组件与一个 domain** —— 不补进去,新代码就是检查不到的荒地。**t10 的 C4 按本票改写了**:它原本断言 「进第二级不取数」,那正是票 10 延期给票 11 的接缝,本票接手后该断言不再成立(已改为断言按编码取数)。 ### 真机走查(29/29,2026-09-18) node .scratch/cockpit-shell-panel-layout/verify/t11-equipment-detail-walkthrough.mjs 五组,都是**从 DOM 读**分区内容(证明界面真画出来了,而不是 store 里有值): - **A 成功态(12 项)**:选卡片进设备级且表头编码 = 选中设备;六块分区按序画出、各带自己的状态标签; 档案 14 行含设备名;指标 2 条含**真 0 读数**(`0m³/h` 没被当成缺失);预警/报警各 1 条、等级 `2 级`/`1 级`; 工单 `WO-2026090200001=处理中 · 故障维修`;图片分区**恰好 1 张** ``;打开详情不改动地图标记数 (票 10 的常驻语义不回退);整卡可见文本零内部代号零字段路径;无页面错误。 - **B 分区级部分失败(6 项)**:档案/工单/图片三块 failed 且措辞明确,**指标 2 条 / 预警 1 条 / 报警 1 条 照常画出真实内容**,浮层没白屏也没自动关。B5 专门断言「恰好 3 坏 3 好」, 否则 B3「好块仍在」会变成恒真。 - **C 空数据(3 项)**:六块全 `empty`、各自说「当前没有数据」,且**没有伪造的 0 / 空行 / 空图**。 - **D 过期(4 项)**:指标块 `stale`(`retained=true`)**仍画出 2 条读数**,说的是「数据已过期」而非「无数据」, 且不牵连档案块(照常 success 14 行)。 - **E 整包失败(4 项)**:说一句实话(六块都没数据)而不是逐块谎报「其它分区不受影响」; 给出「重新加载」入口;浮层仍在、表头仍带设备编码。 场景注入用 `__COCKPIT_MOCK_ONLY__` 把异常样例**只**施加在聚合端点,地图与点位摘要保持 success —— 否则全局 `empty` 会把地图清空、没有可点的标记,走查会退化成「点不到东西」的假失败。 ## Answer **设备详情六个分区按设备编码加载:取数收进 `selectDevice`/`openDevice` 两个唯一进级入口 (「进了设备级却没取数」结构上不可能发生),竞态由 token 纪律收口(已答过的同一台不再重取、 失败仍可重试、迟到响应丢弃);分区视图是纯函数层,前端只画后端给的那一张图、只报缺失条数、 把接口代号翻成人话;「整包失败」与「分区级失败」严格分开 —— 前者说一句实话并给重试入口, 后者才逐块渲染,所以「任一分区失败不遮盖其它分区」在真机上是可验证的形态而不是口号。** ## Comments ### 2026-09-18 认领与阶段判定 已认领(`Status: claimed`)。本票无 `needs-triage` 标签、`Blocked by: 10` 且票 10 已 resolved, 故不经过诊断阶段,直接进入实施。 ### 2026-09-29 跨票同步(由票 20 触发,非缺陷) 票 20 按后端权威枚举 `WorkOrderStatusEnum`(0–7)对齐了工单状态字典(维护者 2026-09-29 裁定), 本票交付面的**行为未变**,但两处断言/样例里的**码值**随之更新(属于"同一份业务词表的消费者跟着改"): - `verify/t11-equipment-detail.mjs` 的 `B7-work-order-state-from-dictionary`: 入参码 4 → **3**(都要落在「处理中」上;旧码在新字典里是「待验收」); - 断言措辞「按七态字典补」→「按状态字典(后端 0–7)补」。 本票逻辑闸门复跑 **61/61 通过**,无功能回退。