# 17: 基础设施模式面板 **What to build:** 在基础设施一级模式下同时展示基础档案、运行信息、管理信息、应急保障四个二级面板,并与中央地图保持联动。 Blocked by: 06, 08 Status: resolved Labels: ready-for-agent - [x] 基础档案面板展示设施规模、行业分类、区域分布和档案摘要。 - [x] 运行信息面板展示供应、输送、承载能力和指标占位。 - [x] 管理信息面板展示管理单位、权属单位和责任区域。 - [x] 应急保障面板展示危险源、防护目标和应急资源。 - [x] 四个面板支持独立展开/收起。 - [x] 点击面板中的可定位对象可联动地图。 - [x] 有面板数据适配和联动单测。 ## 进度:100% **已完成并关闭(2026-09-18)。** 逻辑闸门 **57/57**、真机走查 **28/28**; 回归:15 个逻辑闸门全绿 + 4 个既有真机走查全绿(t01 20/20、t09 28/28、t10 28/28、t11 29/29)。 ### 交付物 - `domain/infrastructurePanels.js`(新):四块面板的**契约适配 + 视图计划**(纯函数)。 四个 `to*()` 把后端四份面板级聚合读模型翻成领域对象;四个 `*Panel()` 把领域对象翻成 组件可直接画的块(`kpi` / `bars` / `rows` / `list`)。 - `domain/mapLocate.js`(新):面板可定位对象 → 地图定位的**共享接缝**。三种"点不动"分得开 (不在已加载范围 / 后端判定不可上图 / 地图未就绪)。票 18 的台账与票 19 的事件定位复用同一条。 - `services/infrastructureApi.js`(新):四个端点的唯一出口,每块各自成败。 - `stores/infrastructureStore.js`(新):四块分区状态 + 按行业取数 + 定位联动。 - `components/InfrastructurePanel.vue`(新):四块面板的**共享渲染层**,按 `block.kind` 分派。 - `domain/filterOptions.js`:新增 `INDUSTRY_CODES` / `industryQuery()` / `industryLabel()` (前端行业键 → 后端正式行业码)。 - `domain/sectionState.js`:`BACKEND_TO_SECTION` 认下基础设施三切片子分区用的 `READY` / `UNAVAILABLE` (翻译同义词,不是放宽判定;认不出的取值仍一律落 `FAILED`)。 - `mocks/contractMocks.js`:四块面板的契约化样例 + `infrastructure-partial` 场景。 ### 五条值得记下的判断 **1. 「责任区域」在后端契约里根本不存在,面板如实标缺口而不拿行业冒充。** 逐类读过 `CockpitInfrastructureManagementInformationResponse`/`Snapshot`/`Service`: `SourceRelation` 只有 `objectCode/objectKind/industry/organizationCode/organizationName/ownershipType`, 响应是 `organizations` + `ownershipDistribution` + `mapFilter` + `excluded` —— **全链路没有 region/area**。 对比 `base-archives` 有 `regionDistribution`、`emergency-support` 有 `area`,所以不是漏读。 处理:`MANAGEMENT_REGION_GAP` 常量 + 界面一条缺口说明(指向票 25 的 E 组),**不拿 industry 冒充区域**。 这条修正了票 25 表格里「17 基础设施面板:D/E/F 契约已交付,可对接」的说法 —— E 交付了, 但票 17 验收点名的第三项(责任区域)不在该契约内。 **2. 「可定位对象联动地图」是一条共享接缝,不是每个面板各写一套。** 后端这些聚合切片**只给点位编码、不给经纬度**(`operating-information` 与 `emergency-support` 的 `pointCodes`),坐标在 `mapStore` 已加载的地图摘要里。所以联动本质是「回联 + 判定」: 按编码找对象 → 过票 07 的 `isRenderable()`(**只认后端 `renderable`,前端绝不重算经纬度**) → 票 03 的 `setCameraFocus()` 移动相机 → 票 10 的 `openPoint()` 打开同一张点位详情卡。 三种失败必须分得开(否则值班人员不知道是自己点错了还是数据有问题): `missing`(不在已加载分页里)/ `blocked`(后端判定不可上图,原因用票 07 同一张词表)/ `unavailable`(地图未就绪)。 `archive` 与 `management` **没有可定位对象**(前者是聚合分布口径、后端不返回逐设施坐标; 后者只到机构维度),如实说明而不是造假按钮。 **3. 行业筛选只接行业一维,范围/区域不接。** CONTEXT.md 定义行业筛选是作用于整个驾驶舱的全局条件,所以本票把它落到四个端点的 `industry` 入参上。 但**井盖在后端正式行业枚举里没有契约位**(票 25 第八轮已记:库里另有 `manhole_base`,正式枚举只有三个), 所以 `manhole` 按「不传参」处理并在面板上如实说明,**不拿一个后端不认的字符串去换 400**。 > **票 18 更正(2026-09-28):这条判断的依据已被后端推翻。** > `MANHOLE` 现已是**第四个正式行业**,且全部 11 个 cockpit 服务都收(含 `map-snapshot`)—— > `git show HEAD` 可见 `CockpitMapSnapshotService` 第 39 行为 > `Arrays.asList("GAS","WATER_SUPPLY","DRAINAGE","MANHOLE")`,认不出的行业仍 400 但文案已改为 > 「must be GAS, WATER_SUPPLY, DRAINAGE or MANHOLE」。 > 因此 `manhole` **现在正常下发**(`domain/filterOptions.js` 的 `INDUSTRY_CODES` 已加 `manhole: 'MANHOLE'`, > 该文件由本票与票 18 共用)。**本票闸门 `t17-infrastructure-panels.mjs` 的 E1/E2 断言已同步更新** —— > 它原来断言的 `industryQuery('manhole') === ''` 守的正是这条已被推翻的假设。 > 本票其余结论不受影响(它交付的四块面板与坐标准入/定位接缝都未变,逻辑闸门仍 57/57)。 范围/区域不接:archive 是 `region(CN/CB/…)`、emergency 是 `area(CN/TC/…)`,与 `filterOptions` 的 `south/north/taichang` 对不上,那需要先裁定一套映射 —— 属票 18 的域,本票不预支。 **4. 后端 `Distribution` 是三类维度共用结构,一律读 `code` 会全部落空。** `CockpitInfrastructureEmergencySupportService` 的三个构造函数把值分别填在 `industry` / `area` / `category` 上,`code` 一律是 `null`。所以适配层必须**按维度给取键字段**。 这条只有对着后端源码逐行读才会发现,光看演示载荷(那几行确实是 `{ industry: '…', count: n }`) 容易当成 `code`。已写进 B7b 断言,并做了变异测试确认它抓得到(改回 `code` 即红)。 **5. BigDecimal 在线上是字符串,`Number()` 直吃会把真值变成假 0。** 演示载荷里是 `"0.32"`、`"1.2500"`,所以适配层用 `numberish` 宽进严出:数字与数字串都认, 真 0 是有效读数,认不出的回 `null` → 界面画占位符。变异测试确认:把 `numberish` 换成 `Number()` 会打红 4 条(含「能力基准缺失不显示虚假 0 负荷」)。 ### 验证 **逻辑闸门 54/54**(`verify/t17-infrastructure-panels.mjs`,七段): node .scratch/cockpit-shell-panel-layout/verify/t17-infrastructure-panels.mjs - **A 面板映射**(2):四块与 ADR 0003 目录一致;不越界接其它模式的面板。 - **B 契约适配**(13):BigDecimal 字符串、真 0 vs 缺值、三类能力、管理单位+权属、应急三段、 共用 Distribution 按维度取键、哨兵值(`UNKNOWN`/`EMPTY`)翻人话而不泄漏。 - **C 视图层**(13):验收点名的画面元素逐项在场;缺值占位符;色键必带文字(票 06); 未取数说"正在取数"而不是"没有数据"(整段缺失也算"还没有",不算"加载失败")。 - **D 分区独立**(6):四块各自成败、一块坏不遮其它三块 + 对抗式反证(全成功场景全绿, 故"一块坏"有信息量)、空/过期分得开、子分区 `UNAVAILABLE` ≠ `EMPTY`。 - **E 行业筛选**(3):前端键→后端码;`all`/`manhole` 不传参;端到端请求串里真的带上了码。 - **F 地图联动**(7):三种"点不动"分得开;坐标准入只认后端 `renderable`(越界坐标只要后端说 可上图就放行、合法坐标只要后端说不可上图就拦下);store 只在成功那一次动相机+开详情。 - **G 静态守卫**(10):组件不拼路径、不认后端裸字段、零后端代号;domain 不发请求/不 import service; store 零裸字段;api 零凭据;零写操作;四块走同一接缝;**行业筛选真的重取**而不只是记在 store 里。 **对抗式自查(变异测试,五条都确认守卫有牙)**: ① 把 `isRenderable()` 换成前端自算经纬度范围(票 07 明令禁止的第二套判据)→ 打红 4 条; ② Distribution 改回一律读 `code` → 打红 B7b;③ `numberish` 换成 `Number()` → 打红 4 条; ④ 去掉"未取数 → 正在取数"分支(首帧就会谎报"没有数据")→ 打红 C7; ⑤ 摘掉行业跟随(筛选只记在 store、不重取)→ 打红 G10。 五次变异后均已还原并复跑至全绿。 **回归 14 个既有闸门 + 本票闸门全绿**(exit=0): t01 / t04 / t05 **206** / t06 / t07 39 / t08 / t09 38 / t10 57 / t11 61 / t12 / t21 / t21-view / t29 / t30(既有),t17 **54**(本票)。 - **t05 的静态守卫面已扩到本票新增的四个文件**(组件 + store + 两个 domain)—— 不补进去,新代码就是检查不到的荒地(票 11 的成例)。t05 因此由 194 → **206** 项。 - **t06 的 C1 断言放宽了一处**(原为 `` 字面匹配): 外壳现在按 panelId 分派,占位组件前面出现了 `v-else`。放宽的是「前面不许有指令」, 保留的是它真正要守的两条契约(占位文本未变 + 占位组件仍带着 `panelId` 绑定被真实使用)。 改完做了变异测试:把占位组件从外壳里摘掉,C1 与 C5 仍然双红。 ### 真机走查(28/28,2026-09-18) node .scratch/cockpit-shell-panel-layout/verify/t17-infrastructure-panels-walkthrough.mjs 六组,面板内容一律**从 DOM 读**(证明界面真画出来了,而不是 store 里有值): - **A 正常场景(13 项)**:切到基础设施模式后四块面板全 `success` 且各有真内容 (档案 `设施总数=5处 / 里程=12.4km / 规模=中型`、运行信息 `供应=0.32 MPa / 输送=1200.5 m3/h / 承载=800 m3` = 验收点名的三类能力、管理信息 3 家管理单位 + 责任区域缺口说明、应急保障三段 `危险源=1 / 防护目标=1 / 应急资源=2`); 本模式 0 块占位(四块全是真面板),切到总体概览仍是 4 块占位(票 18/19/20 的域); **两栏可见文本零后端代号**(查 26 个状态码与枚举值);无页面错误。 - **B 定位联动(5 项,验收第 6 条)**:面板给出 6 个可定位对象;点 `MP-002` 后 **相机中心真的落在 `110.14200,28.45300`**(目标 `110.142,28.453`)、**打开了同一张点位详情卡** (`level=point pointCode=MP-002`)、成功时不给多余文字、**地图标记数不变(2 → 2)**(面板不往地图上加对象)。 - **C 不可上图(1 项)**:点 `MP-003`(后端判 UNVERIFIED)→ 给出 `坐标未经校验`、**相机没动、 详情卡仍停在 MP-002**(没被换成被拒的点位)。 - **D 展开收起(1 项)**:档案面板 `414 → 42px`(收起真的缩回标题栏高度,不是只藏内容)。 - **E 失败隔离(5 项)**:四端点全坏时四块都说「加载失败(其它面板不受影响)」且无页面报错; **只坏管理信息一个端点时,恰好 1 坏 3 好**,档案 KPI 3 个与应急条目 4 条照常画出(验收第 3 条的形态)。 - **F 分段失败与空(3 项)**:`infrastructure-partial` 下防护目标段「该段数据未取到」且 0 条, 危险源 1 条、应急资源 2 条照常画出(**一段坏了不遮盖其它段**);空场景四块都是明确占位说明且不伪造 0。 **走查逮到的真问题(这层证据的价值所在,三条都不是纯逻辑能发现的)**: 1. **英文枚举值直接上屏(真泄漏,产品缺陷)**:`规模=medium` —— 后端 `scale` 是**数据源透传的自由字符串** (契约手册与 `CockpitContractDictionaryTest` 都只把它当值,后端测试断言的就是 `"medium"`), 我的 `CODE_TEXT` 漏了这三个词,于是英文枚举印到了大屏上。**已修**:补 `large/medium/small → 大型/中型/小型`, 并加两条断言(逐值 + 归纳版「登记过的码翻译结果不得等于原码」)。 **我的 A9 守卫当时只查状态码、不查枚举值**,所以纯逻辑全绿而真机露馅 —— 守卫面已按此加宽到 26 个词。 2. **组件里一段死代码**:`data-item-state` 挂在 `v-else-if` 上,而与本契约里 `stateText` 与 `rows` 互斥, 那个分支**永远不执行**,段状态实际走的是空列表分支(文案能显示但挂在错误的节点上)。 已改成 `stateText` 优先且不挂 `v-else-if`,并补 `D6b` 把两者的互斥性钉住。 3. **我自己走查脚本的两条错断言**:A8 以为基础设施模式有 12 块占位(其实该模式 4 块全是真面板, 应为 0;已改为「本模式 0 占位 + 切到总体概览 4 占位」两步);C1 断言「没有详情卡」, 而 B 组刚打开过卡片,必然假(已改为「卡片没被换成被拒的那个点位」)。 ### 回归(含既有真机走查) **逻辑闸门 15 个全绿**(exit=0): t01 / t04 / t05 **206** / t06 / t07 39 / t08 / t09 38 / t10 57 / t11 61 / t12 / t21 / t21-view / t29 / t30(既有),t17 **57**(本票)。 **既有真机走查 4 个全绿**:t01 **20/20**、t09 28/28、t10 28/28、t11 29/29。 - **t05 的静态守卫面已扩到本票新增的四个文件**(组件 + store + 两个 domain)—— 不补进去,新代码就是检查不到的荒地(票 11 的成例)。t05 因此由 194 → **206** 项。 - **t06 的 C1 断言放宽了一处**(原为 `` 字面匹配): 外壳现在按 panelId 分派,占位组件前面出现了 `v-else`。放宽的是「前面不许有指令」, 保留的是它真正要守的两条契约。改完做了变异测试:把占位组件从外壳里摘掉,C1 与 C5 仍然双红。 - **t01 的 A5 断言修正了一处根因性缺陷**(走查逮到):它原本用 `body.split('·')[1]` **从占位文案里反推 panelId**,而本票把基础设施四块换成真实面板后占位组件不再渲染, panelId 解析成空、A5 假红。断言钉在了"面板体长什么样"上 —— 那本是实现细节 (占位或真实内容都该通过)。已改为读外壳新给的稳定属性 **`data-panel-id`** (`CockpitPanel` 新增 `panelId` prop,渲染为 `data-panel-id`), A5 与四个既有走查随即全部恢复(t01 19/20 → **20/20**)。 ### 变异测试(对抗式自查,七条都确认守卫有牙) ① `isRenderable()` 换成前端自算经纬度范围(票 07 明令禁止的第二套判据)→ 打红 4 条; ② Distribution 改回一律读 `code` → 打红 B7b;③ `numberish` 换成 `Number()` → 打红 4 条; ④ 去掉"未取数 → 正在取数"分支(首帧就会谎报"没有数据")→ 打红 C7; ⑤ 摘掉行业跟随(筛选只记在 store、不重取)→ 打红 G10; ⑥ 撤掉 `scale` 直译(复现真机逮到的英文上屏)→ 打红 B4b/B4c; ⑦ 把占位组件从外壳摘掉 → t06 的 C1/C5 双红。 七次变异后均已还原并复跑至全绿。 ## Answer **基础设施模式四块面板落地:各接一个后端**面板级聚合读模型**端点、四块各自成败 (一块坏了其余三块照常画出真实内容),契约适配与视图计划收在一层纯函数里由四块共用; 「点面板里的可定位对象联动地图」做成一条共享接缝(按点位编码回联已加载的地图对象 → 只认后端 `renderable` 的坐标准入 → 移动相机 + 打开与地图同源的两级详情卡), 三种"点不动"分得开且不假装成功。** 三条边界如实划清,不发明业务: ① **「责任区域」在后端管理信息契约里没有字段**(逐类读过响应类与快照类),面板以缺口说明呈现, 不拿行业冒充区域 —— 这条修正了票 25「17 的 D/E/F 已交付可对接」的说法; ② **行业筛选只接行业一维**(`gas/water/drainage` 1:1 映到后端码;`all` 不传参; **井盖在后端正式行业枚举里没有契约位**,同样不传参并如实说明),范围/区域归票 18; ③ 基础档案与管理信息**没有可定位对象**(前者是聚合分布口径、后端不返回逐设施坐标), 如实说明而不是造假按钮。 票面七条验收全部达成(逐条见正文勾选)。收尾时走查逮到并修掉一处**真泄漏** (后端透传的自由字符串 `scale` 未翻译,英文枚举 `medium` 直接印上大屏)与组件里一段**死代码**, 两者都只有真机走查能发现 —— 这也是本票坚持跑走查的原因。 ## Comments ### 2026-09-18 认领与阶段判定 已认领。本票无 `needs-triage` 标签,`Blocked by: 06, 08` 且两张前置票均已 `resolved`, 故不经过诊断阶段,直接进入实施。 ### 2026-09-18 实施前的五条澄清(维护者裁定,均按建议) 实施前就把五处"不该猜"的地方提出来并拿到裁定:① 责任区域契约缺口的呈现方式(如实说明,不冒充); ② 可定位对象联动地图的接缝与语义;③ 是否跟随全局行业筛选(只接行业,范围归票 18); ④ 是否为 18/19/20 立共享接缝(立);⑤ 自查范围(跑逻辑脚本、写但不跑走查)。 ### 2026-09-18 维护者同意后执行真机走查 维护者明确「同意执行真机走查」后执行:首轮 **23/26**(三条失败:两条是我自己走查脚本的错断言, 一条暴露出组件里的死代码),修完复跑 **27/27**;走查日志里读出 `规模=medium`, 顺藤查实是 `scale` 未翻译的**真泄漏**并修掉,扩宽泄漏守卫后末轮 **28/28**。 期间 t01 的 A5 因我改了面板内容而假红(它从占位文案反推 panelId),已改为读稳定的 `data-panel-id`, t01 随之恢复 20/20。