# 08: 地图摘要分页与快照 **What to build:** 接入地图摘要分页契约,渐进加载监测点和未关联设备。同一批加载使用相同快照标记,刷新时切换到新快照。 Blocked by: 05 Status: resolved Labels: ready-for-agent - [x] 地图先显示框架,再按分页批次增加摘要点位。 - [x] 分页参数和快照参数由适配层管理。 - [x] 同一次加载保持快照一致。 - [x] 监测点与未关联设备都能进入地图摘要状态。 - [x] 加载中、完成、失败和快照过期状态可演示。 - [x] 有分页与快照合并单测。 ## 进度:100% **已完成并关闭(2026-09-18)。** 逻辑闸门 **44/44**、真机走查 **16/16**,回归 t01/t03/t04/t05(179/179)/t06/t21/t28(7/7)/t29/t30 全部通过、t05 走查 10/10。 ### 交付 **领域层**(`domain/mapSummary.js` 追加,纯函数 + 注入取数,可脱浏览器断言) - `MAP_SUMMARY_PHASE` 六态:`idle / loading / complete / empty / failed / expired`——验收第 5 条的四态全部可演示,`EMPTY`(后端说总数 0)与 `COMPLETE`(确实加载完)严格分开。 - `mergeMapPage(prev, page)`:跨页合并的核心。三条语义照抄后端实现(`CockpitMapSnapshotService`)而不是演示页的乐观假设:**分页窗口是「监测点 + 未关联设备」一条总序列**(先排满监测点再排设备,跨页横跨两类);按 `kind+code` 去重(重放/重叠切片不得在地图上画两遍,不同 kind 同名编码不合并、无编码脏对象不吞掉);`prev = null` 即换快照,旧对象不残留。 - `createMapSummaryLoader({ fetchPage, pageSize })`:渐进加载状态机。**第 1 页不带 `snapshotAt`、第 2 页起带第 1 页锁定的标记**(后端 `validatePagination` 的硬要求);响应快照与锁定值不一致立即进 `expired`,绝不把两个快照的点拼在一张图上;失败保持 page 不动 = 重试同一页不跳页;完成态是终态;取数函数抛错收敛成 `failed` 不卡 loading。 - `summaryView(state)`:六态 → 中文文案 + 色键,内部原因码不上界面(票 05 同一条纪律)。 - `toMapBatch` 补 `objects` 字段:批次对外就是一条「监测点在前、设备在后」的合并序列,地图不必各写一套分支。 **服务层**(`services/cockpitApi.js`、`services/mapApi.js` 不动语义) - `REQUEST_REASON.SNAPSHOT_EXPIRED`(`snapshot-expired`):后端把快照失效放在 **400 响应体 `AjaxResult.data.category`**(`CockpitErrorCategory` 大写枚举),不在 HTTP 状态码里——只看状态码会把它和「分页参数不合法」混成一个原因。`readBusinessReason()` 做枚举→词汇映射,只认白名单这一个取值,其它(含 `INVALID_REQUEST`)一律回笼统 `unavailable`。 - 分区与装载器的词汇翻译只在 `stores/mapStore.js` 一处:`fetchMapSnapshot` 回的是票 05 的 `section()`(**没有 `ok` 字段**),装载器吃 `{ok, data, reason}`——直接塞会恒判失败(逻辑闸门第一轮抓到的接口形状坑),翻译时 `success/empty → ok:true`,其余失败原因原样上提。 **store 与界面** - `stores/mapStore.js`(ADR 0004 的 `mapStore`):装载器唯一实例。对外只有 `advance()`(取下一批)/ `refresh()`(换新快照、从第 1 页重来)/ `view`(文案色键)/ `settled`(完成或空)——**组件与页面不出现 `page`/`pageSize`/`snapshotAt`**(静态断言守着)。演示页大小取 2(mock 三页翻完 5 个对象,渐进可感)。 - `CockpitShell.vue`:筛选条右区新增「地图摘要」指示器——实时文案(加载中·第 N 页·已上图 M 个 / 加载完成·N/M / 快照已失效·请刷新…)+ 一个动作按钮(未完 = 「加载下一页」,完成 = 「刷新快照」),`data-map-summary-phase` 供走查断言。 - `MapCanvas.vue`:装载状态写进宿主 `dataset`(`data-map-summary[-loaded|-total|-snapshot|-page]`),底部状态灯随态变色(加载中橙呼吸/完成绿/失败失效红);DEV 把手新增 `__cockpitMap.mapSummary.{advance,refresh,getState}` 供人工演示与走查驱动。 - `detailStore.js`:地图摘要分区改由 `mapStore` 供货(同一端点只拉一份、只持一个快照)。**分区映射刻意不把「还没翻完」写成 `STALE`**——票 16 词汇里 STALE 是「有值但过期」,分页未完的数据是新鲜的,说成"已过期"是对值班人员撒谎;取到一部分就是 `SUCCESS`,「翻到第几页」由地图摘要指示器与地图点位数体现。真异常三种如实报:`FAILED` / `STALE+retained`(快照作废)/ `EMPTY`。 **mock**(`mocks/contractMocks.js`、`mocks/index.js`) - `/map-snapshot` 从「只给一页假分页」改成**按后端真实语义分页**:窗口横跨两类对象、第 2 页起不带快照或快照不一致抛 `SNAPSHOT_EXPIRED`、`complete` 只在末尾为真。走查开关新增 `window.__COCKPIT_MOCK_MAP__ = { pageSize, snapshotAt, expireAfterPage }`——`expireAfterPage: 1` 即演示「取完第 1 页后后端换快照」。 - t05 的 C1 断言随之更新(旧断言写的是「一次拿到全部」):第 1 页只是总序列的一段(2 个对象、`complete:false`),并补「第 3 页带着第 1 页快照请求、结果一致且 complete」的断言。 ### 验证证据 - **纯逻辑**:`node .scratch/cockpit-shell-panel-layout/verify/t08-map-summary.mjs` → **44/44**。八段:A 批次合并(跨页拼接/去重/跨 kind 不合并/换快照清空/长跑护栏 maxBatches+maxObjects);B 演示态词(六态中文文案、零原因码泄漏);C 装载器状态机(同批同快照、渐进、完成即终态、刷新换快照、过期不拼接、失败重试同页、空快照、参数由调用方给);D 适配层(查询串带 `page/pageSize/snapshotAt`、400 里的 `SNAPSHOT_EXPIRED` 与 `INVALID_REQUEST` 分别归类);E 取数抛错收敛;F mock 真实分页(切片、跨两类、缺快照即失效);G 装载器→store→分区状态(部分翻完 = success 而非 stale、未取到不改写分区、刷新回第 1 页、过期保留已上图);H 静态守卫(8 个界面文件零分页/快照参数、参数只在 mapStore 适配处拼、外壳不直接翻页)。 - **真机走查**:`node .scratch/cockpit-shell-panel-layout/verify/t08-map-summary-walkthrough.mjs` → **16/16**(`yarn dev` @ localhost:8000,服务由维护者开启)。逐条:首屏引擎 ready + 第 1 批已在图上且 `loaded < total`(框架先于全量);逐批推进 2→4→5、终态 complete、批次数 3;**整个加载过程快照标记唯一**;完成后按钮切「刷新快照」;刷新回第 1 页(loaded 5→2,旧快照的点不残留);失败场景分区板 `failed` 而地图宿主与引擎保留;快照过期场景显示「已失效·请刷新」且保留已上图 2 个;空快照显示「没有可上图对象」不误报失败;**1680/1280 两种宽度下筛选条零横向溢出**、零页面级报错。 - **回归**:t01 / t03 / t04 / t05(179/179,含更新后的 C1 三断言)/ t06 / t21×2 / t29 / t30 逻辑脚本全通过;t28 走查 7/7(外壳填满、无滚动条);t05 走查 10/10(共享分区板、401 只标受影响分区、地图骨架不牵连)。 ### 走查抓到并修掉的问题(对抗式自查的实证) 1. **走查脚本自身假绿(第一轮 11/11 是假的)**:`check()` 签名多了一个 `desc` 参数,调用点却是三参——证据字符串被当成"通过"吃掉,所有断言恒真。修签名后立刻暴露三个真失败(起点判定、按钮态时序、刷新等待),逐个修脚本并复核产品行为后才得真 16/16。教训:**走查脚本也要对得了账,绿不够,得知道绿的是什么。** 2. **「没翻完」误标为「已过期」(设计修正)**:最初把部分翻完的地图摘要分区映射成 `STALE`,走查与复盘发现这既违反票 16 的词汇(STALE=有值但过期),又与地图上的真实进度自相矛盾。改为部分翻完 = `SUCCESS`,进度归地图摘要指示器。 3. **1280 宽筛选条溢出 +202px(布局回归)**:新增控件后 flex 项不许收缩。给 `__scope-left/right` 补 `min-width:0`、摘要块与分区板显式 `flex-shrink:1`,走查守卫 F1 实测两种宽度 `scopeOverflow: 0`。 4. **console.log 的 `%` 格式串吞证据**:模板串里的 `%` 走了 console 格式化分支,证据被吞——改用字符串拼接输出。 ### 边界与遗留 - 本票交的是「摘要数据进图」的数据面:点位的**视觉渲染**(图标、点位聚合、选中)归票 22(Cesium 管线渲染),坐标校验与「无地图位置」归票 07(`coordinate` 字段已随对象带出,`summarizeCoordinateValidation` 即其计数依据)。 - 增量更新(WebSocket 推送换快照)归票 15;「手动刷新」按钮的全局语义归票 16,本票的 `refresh()` 是它可复用的入口。 - 演示页大小 2 是 mock/演示值;真实部署按后端默认 500、上限 1000(`mapStore.MAP_SUMMARY_PAGE_SIZE` 一处可调)。 - 本票改动**未提交**(工作区另有票 04/05/21/27/29/30 会话的在途改动,见 map.md)。