# 27: 前端执行票 —— 监测点台账分母渲染 undefined Type: task Status: resolved Assignee: ASUS Label: ready-for-agent Category: frontend-execution ## 来源 票 18 真实浏览器取证(2026-10-10 15:57,`DATA_SOURCE.REAL` 运行监测模式)。 ## 现象 监测点台账面板在真实空态下渲染: > 已列出 0 个点位 · 本端点序列共 **undefined** 条(含未关联设备) 分母取到了 `undefined` 并被**直接渲染到界面**。界面出现字面量 `undefined`,违反「缺失不冒充值」的既有纪律(`domain/fields.js` 与 `sectionState.js` 都要求缺失回占位符而非直接呈现)。 注意本条与「面板无数据」是两回事:面板处于真实空态是**正确**的(13 个地图对象坐标全部 `UNVERIFIED`、按契约不得上图),但把 `undefined` 显示给用户是**缺陷**。 ## 范围 1. 定位台账分母的取值链:确认为何在真实响应形状下得到 `undefined`(响应缺该字段,还是前端取错路径)。 2. 缺失一律回占位符或如实 `0`,不得渲染 `undefined`/`null`/`NaN` 等字面量。 3. 同类面板一并自查(其余读数位是否也存在缺失直出风险)。 ## 验收 浏览器实测:监测点台账在真实空态下显示可读文案(不含 `undefined`);有数据时计数正确;契约测试覆盖缺失字段场景。 ## 关联 发现于票 18;本票是从 [票 22](22-22.md) 复核中分离出的**真实**前端缺陷(票 22 本身已撤销)。 ## 进度 - 2026-10-13 **诊断完成(本票无 `needs-triage`,按阶段闸门仍先做诊断)**:根因定位并离线复现。 - 2026-10-13 实施 + 契约测试 + 同类自查。 ### 诊断:根因(已复现,非推测) 取值链:`fetchMonitoringDistribution()` → `toMonitoringDistribution()` → `monitoringPointLedgerPanel()`。 1. **真实空态下 `section.data` 是 `null`**。`services/monitoringApi.js` 的 `isEmpty` 判据是 「点位 0 且未关联设备 0 且 `pagination.total === 0`」——真实空态三者都成立,于是 `sectionFromResult()` 走 `isEmpty` 分支回 `section(EMPTY, { mode, freshness })`, **不带 `data`**(`domain/sectionState.js` 第 141–143 行)。store 照搬 (`stores/monitoringStore.js` 第 184 行 `{ ...distribution, loaded: true }`),外壳再传 `section.data`(`CockpitShell.vue` 第 593 行)——台账视图拿到的就是 `null`。 2. **视图层的守卫写错了类型**。`domain/monitoringPanels.js` 第 750–753 行: ```js pageNote: pagination.total === null ? '' : `… 本端点序列共 ${pagination.total} 条 …` ``` `data` 为 `null` 时 `current = {}`,`pagination = {}`,所以 `pagination.total` 是 **`undefined`**(不是 `null`)——守卫 `=== null` 不成立,模板串把 `undefined` 直出。 作者意图本就是「没有 total 就不出这句」,**缺陷只是守卫漏了 `undefined` 这一态**。 3. **渲染无条件**:`components/PanelBlocks.vue` 第 44 行 `
` 在 `hasContent` 分支**之前**,所以空态/失败态也照画——这正是用户在空态看到那句话的原因。 **复现(离线,2026-10-13)**: ``` A EMPTY pageNote = "已列出 0 个点位 · 本端点序列共 undefined 条(含未关联设备)" C OK pageNote = "已列出 1 个点位 · 本端点序列共 8 条(含未关联设备) · 还有下一页" ``` **同类自查(范围第 3 条)**:对 `domain/panelViews.js` 注册的**全部 16 块面板**,喂 `null` / `{}` / 缺 `pagination` / `total: null` 四种载荷 × `EMPTY/SUCCESS/STALE/FAILED` 四态,递归扫描视图对象里每个字符串是否含 `undefined`/`NaN`/`null` 字面量—— **唯一命中就是本票的台账 `pageNote`**,其余读数位(在线率、处置率、`valueText` 系列、 `coordinate.js` 汇总)都走了占位符或 `numberish` 收口。守卫已固化为回归脚本 [`verify/t27-ledger-undefined.mjs`](verify/t27-ledger-undefined.mjs)。 ### 实施 源码一处(`src/modules/cockpit/domain/monitoringPanels.js`,+8/−2):把台账分母先过 `numberish()` 收口,再判 `total === null`。 ```js const total = numberish(pagination.total) … pageNote: total === null ? '' : `已列出 ${cards.length} 个点位 · 本端点序列共 ${total} 条…` ``` 为什么用 `numberish` 而不是补一个 `pagination.total === undefined` 判断:仓库已有 「数字/数字串 → 数字,认不出回 null」的**唯一口径**(`domain/panelBlocks.js` 第 112 行), 后端 BigDecimal 在线上是字符串(既有事实),补类型判断会同时漏掉 `''`/`NaN`/字符串等 其余缺失形态,等于把同一条纪律再抄一份。**缺失时整句不出**是作者原意,不是新增文案。 **未改**的两处,如实记录理由: - `components/PanelBlocks.vue` 第 44 行的渲染位置(`hasContent` 之前)**不改**:空态 不画这句话是由 `pageNote` 为空保证的,与渲染位置无关;挪动它会改变失败态/过期态的 其它既有行为,超出本票范围。 - 探针在**绕过适配层直接喂裸行**时另抓到两处(`management` 的 `key = "UNASSIGNED|undefined"`、`operationOverview` 对 `...item.statusReasons` 抛 `not iterable`),但两处都**经真实适配层不可达**——`toManagementInformation()` 已把 `industry` 归一为 `''`(第 202 行),`toAbnormalEquipment()` 已把 `statusReasons` 归一为数组(第 311 行)。本票是「真实响应渲染」缺陷票,不为合成形状扩张范围; 已在此记录,供将来若改适配层时回看。 ### 验收自查(对抗式) | 验收标准 | 结果 | | --- | --- | | 真实空态显示可读文案(不含 `undefined`) | ✅ 端到端走真实消费链(`fetchMonitoringDistribution` → `sectionFromResult` → 视图),`pageNote === ''`;修复前实测为 `…共 undefined 条…` | | 有数据时计数正确 | ✅ 同一脚本钉住 `已列出 1 个点位 · 本端点序列共 8 条(含未关联设备) · 还有下一页`,且 `hasMore === true` | | 契约测试覆盖缺失字段场景 | ✅ 缺 `pagination` / `total: null` / `data: null` / `{}` 四形态,另加 BigDecimal 字符串分母(`'13'` → `共 13 条`,不被误判为缺失) | | 同类面板一并自查(范围 3) | ✅ 16 块面板 × 5 载荷 × 4 态普查,零泄漏 | | 变异测试(守卫有牙) | ✅ 把守卫改回 `pagination.total === null` 后 **3 项转红**(含端到端与普查),还原后 8/8 全绿 | **证据**:`node .scratch/cockpit-real-data-wayfinding/verify/t27-ledger-undefined.mjs` → **8/8 passed**。 ### 真机验收(2026-10-13,用户明确授权后执行) 环境:既有 dev server `http://127.0.0.1:8000/#/cockpit`(**本会话未新起服务**,该进程已在运行), 运行时配置 `public/cockpit/config.json` 的 `dataSource: "real"` → `http://192.168.110.96:8301/api/cockpit/v1` (实测 HTTP 200)。外壳状态栏自报「配置就绪 · 数据源 real」。 **如何造出真实空态**:全域 6 个监测点的行业均为未分区,故把全局行业筛选切到「燃气」后, `monitoringPoints`/`unassociatedEquipment` 与 `pagination.total` 同时为 0 —— 正是 `fetchMonitoringDistribution()` 的 `isEmpty` 成立、`section.data` 回 `null` 的现场。 | 步骤 | 观察 | 判定 | | --- | --- | --- | | 修复版 + 真实空态 | 台账面板:`监测点台账 · 暂无数据` / 「无数据 当前范围内没有记录」/「该面板当前没有数据」;**无 `[data-panel-page-note]` 元素**,整页无 `undefined` | ✅ 验收第 1 条通过 | | 修复版 + 有数据(全域) | `已列出 6 个点位 · 本端点序列共 15 条(含未关联设备)` | ✅ 验收第 2 条通过 | | **反证**:把守卫临时改回 `pagination.total === null`(HMR 重载) | 同一空态下台账**复现票面原文**:`已列出 0 个点位 · 本端点序列共 undefined 条(含未关联设备)` | ✅ 证明该空态确实是本缺陷的触发条件,且修复是它的直接原因 | 反证截图(缺陷版,右下角台账可见 `undefined`)与正证截图(修复版,同一空态无该句)均已由 本会话取得;**随后已把源码还原为修复版并复跑脚本 8/8**。 **为什么必须做这一步**:离线脚本证明的是「喂 `null` 时不再直出」,但票面验收点名的是 「真实空态下的画面」。反证把两者接上:同一真实空态、同一份代码路径,只改守卫一处, `undefined` 出现↔消失——这才排除「离线绿但真机另有别的缺失源」的可能。 **回归**:`t15-time-district.mjs` 11/11 通过。`t18-monitoring-panels.mjs` 与 `t20-overview-panels.mjs`/`t05-adapter.mjs` 存在**与本票无关的既存失败**,已逐一核实: t18 因 `filterOptions.js` 在 `HEAD` 上已无 `REGION_MAP_GAP`/`hasRegionMapping`/`regionQuery` 导出而**模块加载即失败**(语法级,与本票改动无因果关系);t20 的 6 项与 t05 的 1 项 全部落在 `overviewStore.js`,**把我这处改动还原后失败项一字不差**(已对照跑过), 故非本票引入。这三条既存红是**别的会话**留下的,需要各自的票去收,不并入本票。 **并发写入说明(如实记录)**:本会话进行期间,另一会话的提交 `3c69814`(票 24)把本票的 `monitoringPanels.js` 改动与 `a6fbbea`(票 24 票据收口)把本票的票据与 `t27` 脚本一并扫进了 它们自己的提交。已核对:`git show HEAD:…monitoringPanels.js` 与 `git show HEAD:…t27-ledger-undefined.mjs` 的内容与本会话交付**逐字一致**,脚本在已提交版本上 复跑 8/8 通过,故不影响本票结论。 Status: resolved Type: task Blocked by: Labels: ready-for-agent, frontend-execution, wayfinder:task ## Comments ### 2026-10-13 —— 已关闭(含真机反证) 诊断 → 修复 → 离线回归 8/8 → 16 块面板普查 → 变异验证 → **真实 `DATA_SOURCE.REAL` 浏览器正证 + 反证** 全部完成。票面三条验收均通过,详见上方「验收自查」与「真机验收」。 ## Answer **结论:已修复并关闭。** 台账分母在真实空态下直出 `undefined` 的根因是**守卫类型错配**: `fetchMonitoringDistribution()` 的 `isEmpty` 在真实空态成立 → `sectionFromResult()` 走空分支 **不带 `data`** → 视图拿到 `null`,`pagination` 为 `{}`,`total` 是 **`undefined`**(不是 `null`), 而守卫只写了 `pagination.total === null`,模板串把 `undefined` 直出。 **交付物**: - 源码:`src/modules/cockpit/domain/monitoringPanels.js`(+8/−2)——分母先过 `numberish()` 收口再判空;缺失时整句不出。 - 回归:[`verify/t27-ledger-undefined.mjs`](verify/t27-ledger-undefined.mjs) —— 8/8,含端到端空态、四种缺失形态、有数据计数、BigDecimal 字符串分母、全 16 块面板普查。 **验收对照**:票面三条全部通过(真实空态无 `undefined`;有数据计数正确;契约测试覆盖缺失字段), 且真实 `DATA_SOURCE.REAL` 环境取得**正证 + 反证**成对证据:同一空态下只改守卫一处, `undefined` 出现↔消失。变异验证 3 项转红证明守卫有牙。 **方法论沉淀**:「缺失值不直出」的守卫要按**整个缺失域**(`undefined`/`''`/`NaN`/字符串)收口, 只判一种形态等于没判;离线绿必须配一次真机反证,否则证明不了「真实空态」就是缺陷的触发条件。 **未纳入本票**(如实记录):探针绕过适配层直接喂裸行时另见两处合成形状问题 (`management` 的 `UNASSIGNED|undefined`、`operationOverview` 对 `...statusReasons` 抛错), 经真实适配层均不可达(`toManagementInformation()`/`toAbnormalEquipment()` 已归一), 故不为合成形状扩张范围,留档备查。