# 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。