# 14: 事件详情与设备定位 **What to build:** 点击事件后打开事件详情,并通过设备编码定位设备。地图高亮设备和关联管线,返回时保留原列表上下文。 Blocked by: 13 Status: resolved Labels: ready-for-agent - [x] 事件详情包含编码、设备编码、指标、阈值、实际值、等级、时间和状态。 - [x] 点击事件可定位关联设备。—— 真机实测:相机真的飞到设备点位。 - [x] 地图高亮设备和关联管线。—— **设备侧已完成并真机实测**(真机 `data-map-emphasis=1`、点位数不变;强调已有实体、零新建);**关联管线**的接缝已就绪并按**设备码**匹配,等票 22 的管线图层落地即生效(见下文分工)。 - [x] 返回路径保留列表位置和筛选条件。—— 真机实测:返回后 12 行逐位相同、族不变。 - [x] 无有效坐标时事件仍可查看并显示无地图位置原因。—— 逻辑闸门 5 项(含走查样例 E4);真机 D 组未跑成,**已由维护者人工验收通过**。 - [x] 有事件定位与返回状态单测。 ## 进度:100%(已关闭) **2026-09-24 维护者人工验收通过**,票 14 关闭。代码 + 逻辑闸门(44/44)+ 真机 DOM 走查 A/B/C/E 组全绿; D 组(无坐标样例)在真机上没跑成(`@puppeteer/browsers` 空目录致走查脚本起不来,运行时替代操作把浏览器宿主弄卡), 其路径由逻辑闸门 E4/E1/E2/D2/D4 覆盖,**并由维护者人工验收补上最后一段确认**。 ### 真机走查结果(本次实测,从 DOM 读,不读 store) - **A 详情真的画出来**:真实鼠标点第 1 行 `WRN-1001` → 详情卡出现、`data-event-detail-code` = 所点那一行;9 行字段全部从 DOM 读到(名称=燃气浓度预警、设备编码=EQ-002、 指标=GAS_CONCENTRATION、实际值=0.9 %LEL、等级=1级、时间、状态=进行中、位置=城区A); 列表 12 行逐位不变;点位数 2→2。 - **B 定位真动**:点「定位关联设备」→ `locateResult=ok`、`data-map-emphasis=1`、 **相机中心真的从家视角 (110.39028164, 28.4436787) 飞到设备点位 (110.142, 28.453)**、 点位数 2→2(只高亮已有实体)。 - **C/E 返回 + 还原**:点「返回事件列表」→ 详情收起、12 行逐位相同、族仍是「预警处置」、 **`data-map-emphasis` 从 1 回到 0**(这正是对抗式自查抓到的"收起不还原"那个 bug 的回归点)。 - **D 未跑成**:要换 `event-detail-no-coordinate` 样例得让 mock 重新注册(场景在注册时被闭包捕获), 尝试「改全局键 + 切 hash 触发重挂载」把页面弄成空白,随后浏览器宿主卡死(截图/a11y/导航全超时; 同一时刻两个 dev server 仍 9–38 ms 正常响应,故非服务问题)。**D 的等价证据留在逻辑闸门 E4** (直连 mock:12 行仍可看、`hasLocation=false`、位置「城区A · 坐标未经校验」、定位 `blocked`), 以及 E1/E2/D2/D4。**没有把 D 说成跑过了。** > 一个副产品结论(写给以后要跑走查的人):本机 `@puppeteer/browsers` 目录是**空的**, > 所以 `t14-event-detail-walkthrough.mjs`(以及 t28)**起不来**,只能改用内建浏览器手工驱动; > 而内建浏览器窗口**一旦被遮挡/隐藏**,`requestAnimationFrame` 归零(实测 0 fps), > `cameraControl` 的阻尼飞行就永远推进不了 —— 首轮我把「相机没动」误读成产品缺陷, > 实为**标签页不可见**(`document.hidden=true`)。跑相机类走查前先确认标签页可见。 ## 交付物 - `domain/eventDetail.js`(新):详情视图(8 项字段 + 阈值半开区间 + 缺失不伪装零值)、 位置视图(「无地图位置原因」沿用票 07 的同一张中文词表)、 定位计划(**只认后端 `renderable`**)、高亮编码。 - `domain/mapEmphasis.js`(新):强调**判定**的纯函数 + `EMPHASIS_STYLE`。 抽出来的理由是它能脱浏览器断言(引擎层 `import 'cesium/...widgets.css'`,node 里加载不了)。 - `components/EventDetailCard.vue`(新):详情卡(9 行字段 + 定位反馈 + 定位按钮 + 返回)。只画不判。 - `components/EventListPane.vue`:事件行可点、可键盘回车打开详情;卡片挂在列表上方。 - `stores/eventStore.js`:`selectedEvent` / `locate` / `openEvent` / `closeEvent` / `locateEvent` (**打开与返回都不取数**;定位注入点 `{ objects, mapReady, focus, highlight }` 供单测)。 - `services/cesiumEngine.js`:`highlightEntities()` —— 只改**已存在实体**的 `billboard.scale` / `polyline.width`,零 `entities.add`;`emphasisBase`(WeakMap)记住初值, **传空数组 = 还原全部**;额外扫描票 22 的 `cockpit-pipelines` 数据源。 - `services/mapService.js`:`highlightMapObject(codes)`(设备码 → 点位码解析,只读 `markerPlan`) + 宿主 `data-map-emphasis` 可观察信号(销毁时一并清)。 - `domain/mapSummary.js` / `domain/pointStatus.js` / `mocks/contractMocks.js`:地图对象与标记计划带 `deviceCodes`(事件→设备的关联键;管线也按设备关联),样例与默认页数不变。 - `verify/t14-event-detail.mjs`(新,44 项**逻辑闸门**)、 `verify/t14-event-detail-walkthrough.mjs`(新,真机走查,**待执行**)。 - mock 新样例 `event-detail-no-coordinate`(事件照常给全,`coordinateValidation.renderable=false`): 让「无地图位置」这条路径在真机上真的走得通(逻辑闸门 E4 已证明它有效)。 ## 验收自查(逐条对着验收原句) 1. **8 项字段**:A1/A2/A3 —— 编码/设备编码/指标/阈值/实际值/等级(色键+文字)/时间/状态齐全; 缺失走占位符「—」、**真实的 0 照实显示**;阈值只给下界时显示 `≥ x`(不编一个上界)。 2. **点击定位**:列表行 → `openEvent` → 详情;`locateEvent` 按**设备编码**回联点位并下发相机目标 (C1/C3;没有点位码时也能按 `deviceCodes` 命中)。 3. **高亮设备与关联管线**:C2(编码含点位码 + 设备码)、F5(按设备码**一次命中两个**实体: 点位标记 + 关联管线)、F1/F2(零新建实体 —— `planPointMarkers` 逐位不变、`highlightEntities` 无 `entities.add`)。 4. **返回保留上下文**:G1/G2/G5 —— 返回不触发任何重取,行序、族、分区对象引用全原样 (H6 守住「`closeEvent` 零取数」这条构造性保证)。 5. **无坐标仍可看**:E1/E2/E4 + D2/D4 —— 详情 8 项照旧,位置一栏给原因(票 07 的词表), 定位如实报「不可上图」。 6. **单测**:t14 逻辑闸门 **44/44** + 两次变异测试确认守卫有牙(见下)。 ## 验证证据 - `node .scratch/cockpit-shell-panel-layout/verify/t14-event-detail.mjs` → **44/44**(八段:详情字段、 等级/状态同表、定位计划、四种定位结果、无坐标路径、高亮判定与还原、store 上下文、静态守卫)。 - **变异测试(守卫确实会变红,不是假绿)**: ① 在引擎里恢复旧写法「目标为空就提前 return」→ **F7 红**(还原路径断掉); ② 把判定改成「目标为空 = 全亮」→ **F6 红**。 - 回归全绿(全部逻辑闸门):t01、t04、t05(227)**、t06、t07、t08、t09、t10(57)、t11(61)、 t12、t13(49)、t17(57)、t18(67)、**t14(44)**、t21-terrain、t21-view-modes、t29、t30。 另有 t28 属 puppeteer 走查,在本机因缺 `@puppeteer/browsers` 起不来(环境问题,与本票无关)。 - 按项目规则**未跑** `yarn build` / `yarn dev`(AGENTS.md:改完代码不自动跑构建与调试命令, 另见「真实启动路径是 `yarn dev`」)。 ### 真机走查(待执行,一条命令) node .scratch/cockpit-shell-panel-layout/verify/t14-event-detail-walkthrough.mjs A 点行 → 详情真的从 DOM 画出来(8 项字段 + 编码=所点那一行);B 点定位 → 相机真动、 `data-map-emphasis > 0`、**点位数不变**;C 返回 → 详情收起、**行码逐位相同、族不变**; D 用 `event-detail-no-coordinate` 样例 → 详情完整 + 位置写出原因 + 定位 `blocked`; E 收起后 `data-map-emphasis` 回到 **0**(高亮真的还原)。 **本次(2026-09-24)A / B / C / E 已在真机上逐项实测通过**(证据见上文「真机走查结果」), **D 未跑成**(换样例的运行时操作把页面弄白、随后宿主卡死),其等价证据在逻辑闸门 E4/E1/E2/D2/D4。 ## 与票 22(Cesium 管线渲染)的分工 本票交付「事件 → 设备 → 点位标记」的完整闭环,并给管线留了一条**按设备码**的现成接缝: `highlightEntities()` 已经扫描数据源 `cockpit-pipelines`,命中属性 `pipelineCode` / `deviceCode`。 票 22 只要把管线实体放进该数据源、带上这两个属性之一,「高亮关联管线」即自动生效 (**不需要回头改本票**)。在此之前管线层扫到 0 个 —— 如实报「关联设备尚未出现在地图上」, 不伪造成成功(G4)。 ## 对抗式自查抓到的两个真 bug 1. **详情把列表行的 `level` 当成等级数字**:列表行上 `level` 已是算好的 `{tone,label}`, 详情再喂一次 `eventLevelTag()` → 恒判「等级未知」、`status` 恒为「—」(逻辑闸门第一版 在 A1/B1/E1 抓红)。改为**优先直接沿用行上那一份**(同表由"取同一份数据"保证)。 —— 又一次「测试写得像样,才看得见"能加载 ≠ 能渲染"」。 2. **收起详情不还原高亮**:`highlightEntities` 原先在目标为空时**提前 return**,一趟实体都没走, 被放大的标记会一直胖着、管线宽度也回不去。现改为**每个实体都过判定**, 未命中写回 `emphasisBase` 里记下的原值。走查 E1 就盯这一条。 ## 移交给后续票(本票不追认) 1. **管线侧强调的运行时断言**:票 22 落地后,`data-map-emphasis` 的命中数会从「1(点位)」 变成「≥2(点位 + 管线)」,届时应补一条真机断言。本票的判定已按**设备码**匹配,无需改判定。 2. **D 组的 DOM 层补测(可选)**:待 puppeteer 可用后跑 `node .scratch/cockpit-shell-panel-layout/verify/t14-event-detail-walkthrough.mjs` (本机 `@puppeteer/browsers` 是空目录故起不来)。逻辑闸门 E4 已覆盖同一条路径。 3. **视觉终稿**:详情卡沿用现有 token 与本票局部样式;设计公司终稿替换时只改 `styles/tokens.css`。 ## Answer **事件详情与设备定位落地:点事件行开详情(8 项字段 + 位置/原因)、按设备编码回联点位并定位、 只强调**已存在**的地图实体(设备点位 + 关联管线)、返回不丢列表上下文。** - **详情是列表行的纯函数投影**(`domain/eventDetail.js`):等级/状态**直接沿用行上那一份** (「列表与详情一致」是取同一份数据的结构保证,不是两边各算一遍),阈值支持半开区间、 缺失值走占位符而不伪装零值、`0` 是有效数据照实显示。 - **定位复用票 17/18 立的那条接缝**(`domain/mapLocate.js` 的四结果 + 只认后端 `renderable`), 不新造第二套判定或结果码;设备码优先回联、无点位码也能命中(`deviceCodes`)。 - **高亮不新建实体**(票 13 的硬纪律):`highlightEntities()` 只改已存在实体的 `billboard.scale` / `polyline.width`,零 `entities.add`;`emphasisBase`(WeakMap)记住初值, **空目标 = 全部还原**(收起详情后标记不会一直胖着)。判定抽到 `domain/mapEmphasis.js`, 因此能脱浏览器断言(引擎层 `import widgets.css`,node 里加载不了)。 - **返回保留上下文是构造性的**:列表状态全在 store,收起详情只清选择与高亮,**零取数** (H6 守住这条,否则「返回」会把列表滚回第 1 页)。 - **无坐标路径用票 07 的同一张词表**:新增 mock 样例 `event-detail-no-coordinate` 让这条路真走得通。 **验证**:逻辑闸门 `verify/t14-event-detail.mjs` **44/44**(八段:详情字段 / 同表 / 定位计划 / 四结果 / 无坐标 / 高亮判定与还原 / store 上下文 / 静态守卫)+ **两个变异测试确认守卫有牙**; 真机 DOM 走查 A/B/C/E 组逐项通过(详情真画出、相机真从家视角飞到设备点位、返回后 12 行逐位相同、 `data-map-emphasis` 由 1 回到 0);全量回归绿(t01/t04/t05(227)/t06/t07/t08/t09/t10(57)/t11(61)/t12/ t13(49)/t17(57)/t18(67)/t21×2/t29/t30)。**2026-09-24 维护者人工验收通过**。 **两个只有对抗式自查才暴露的真 bug 已修**:① 详情把列表行的 `level`(已是 `{tone,label}`) 当等级数字再喂一次判定 → 恒判「等级未知」、状态恒「—」;② `highlightEntities` 目标为空时提前 `return` → 收起详情不还原高亮(走查 E1 专盯这条)。 **边界**:事件不产生独立地图标记(只回联已有实体);本票不判断预警真伪/误报(后端契约没有该字段); 「高亮关联管线」的实际可见性等票 22 的管线图层落地(接缝已就绪、无需回头改本票)。