08-map-summary-pagination.md 9.1 KB

08: 地图摘要分页与快照

What to build: 接入地图摘要分页契约,渐进加载监测点和未关联设备。同一批加载使用相同快照标记,刷新时切换到新快照。

Blocked by: 05

Status: resolved Labels: ready-for-agent

  • 地图先显示框架,再按分页批次增加摘要点位。
  • 分页参数和快照参数由适配层管理。
  • 同一次加载保持快照一致。
  • 监测点与未关联设备都能进入地图摘要状态。
  • 加载中、完成、失败和快照过期状态可演示。
  • 有分页与快照合并单测。

进度: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)。