# 27: 前端执行票 —— 监测点台账分母渲染 undefined Type: task Status: claimed 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**。 **未做的验证(如实声明)**:按 `AGENTS.md`「修改代码后不要自动运行构建或调试命令」, **未**运行 `npm run build`/`dev`,**未**做真实浏览器走查。因此本票的浏览器实测项 (真实 `DATA_SOURCE.REAL` 环境下的画面取证)**尚未执行**——离线端到端已复现同一消费链 并锁定语义,但按票面验收第 1 条的字面要求(浏览器实测),需一次真机确认才能宣称完全达标。 **回归**:`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`,**把我这处改动还原后失败项一字不差**(已对照跑过), 故非本票引入。这三条既存红是**别的会话**留下的,需要各自的票去收,不并入本票。 Status: claimed Type: task Blocked by: Labels: ready-for-agent, frontend-execution, wayfinder:task ## Comments ### 2026-10-13 —— 实施与离线验收完成,剩一项真机取证 源码修复、回归脚本、同类普查、变异验证全部完成(详见上方「实施」「验收自查」)。 `node .scratch/cockpit-real-data-wayfinding/verify/t27-ledger-undefined.mjs` → **8/8 passed**; 把守卫改回缺陷原样则 **3 项转红**(守卫有牙)。 **未关闭的理由**:票面验收第 1 条的字面要求是「**浏览器实测**:监测点台账在真实空态下 显示可读文案(不含 `undefined`)」。按 `AGENTS.md`「在修改本项目代码后,不要自动运行构建 或调试命令」,本会话**未**启 `yarn dev`、**未**做真实浏览器走查,故该项**尚未执行**。 离线端到端已复现并锁死同一条消费链,但我不把「离线绿」冒充成「浏览器实测通过」。 **下一步(二选一)**: 1. 授权本会话启动 `yarn dev` + 浏览器,在 `DATA_SOURCE.REAL` 运行监测模式下取证 `[data-panel-page-note]` 不含 `undefined`,然后关闭本票; 2. 或按既有惯例由「票 18 前端真实接口联调与阶段收口」的真机窗口一并复跑后关闭。 若选 1,请在回复中明确授权(项目规则要求用户在当前对话中显式要求才可跑构建/调试命令)。 ## Answer