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 显示给用户是缺陷。
undefined(响应缺该字段,还是前端取错路径)。0,不得渲染 undefined/null/NaN 等字面量。浏览器实测:监测点台账在真实空态下显示可读文案(不含 undefined);有数据时计数正确;契约测试覆盖缺失字段场景。
发现于票 18;本票是从 票 22 复核中分离出的真实前端缺陷(票 22 本身已撤销)。
needs-triage,按阶段闸门仍先做诊断):根因定位并离线复现。取值链:fetchMonitoringDistribution() → toMonitoringDistribution() → monitoringPointLedgerPanel()。
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。视图层的守卫写错了类型。domain/monitoringPanels.js 第 750–753 行:
pageNote: pagination.total === null ? '' : `… 本端点序列共 ${pagination.total} 条 …`
data 为 null 时 current = {},pagination = {},所以 pagination.total 是
undefined(不是 null)——守卫 === null 不成立,模板串把 undefined 直出。
作者意图本就是「没有 total 就不出这句」,缺陷只是守卫漏了 undefined 这一态。
渲染无条件:components/PanelBlocks.vue 第 44 行 <p v-if="view.pageNote"> 在
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。
源码一处(src/modules/cockpit/domain/monitoringPanels.js,+8/−2):把台账分母先过
numberish() 收口,再判 total === null。
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。
环境:既有 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
诊断 → 修复 → 离线回归 8/8 → 16 块面板普查 → 变异验证 → 真实 DATA_SOURCE.REAL 浏览器正证 + 反证
全部完成。票面三条验收均通过,详见上方「验收自查」与「真机验收」。
结论:已修复并关闭。 台账分母在真实空态下直出 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 —— 8/8,含端到端空态、四种缺失形态、有数据计数、BigDecimal 字符串分母、全 16 块面板普查。验收对照:票面三条全部通过(真实空态无 undefined;有数据计数正确;契约测试覆盖缺失字段),
且真实 DATA_SOURCE.REAL 环境取得正证 + 反证成对证据:同一空态下只改守卫一处,
undefined 出现↔消失。变异验证 3 项转红证明守卫有牙。
方法论沉淀:「缺失值不直出」的守卫要按整个缺失域(undefined/''/NaN/字符串)收口,
只判一种形态等于没判;离线绿必须配一次真机反证,否则证明不了「真实空态」就是缺陷的触发条件。
未纳入本票(如实记录):探针绕过适配层直接喂裸行时另见两处合成形状问题
(management 的 UNASSIGNED|undefined、operationOverview 对 ...statusReasons 抛错),
经真实适配层均不可达(toManagementInformation()/toAbnormalEquipment() 已归一),
故不为合成形状扩张范围,留档备查。