Prechádzať zdrojové kódy

feat(cockpit): 票43/44 图表语义与分布排序、地图服务与宿主验收页增量

- 同步票 43 工单堆叠图间隔与饼图交互票据记录、票 44 分布排序闸门脚本
- mapService 增量与 qiankun 宿主验收页调整
ChenJZ 5 dní pred
rodič
commit
bd45f27c14

+ 57 - 2
.scratch/cockpit-real-data-wayfinding/issues/43-untitled.md

@@ -23,15 +23,70 @@ Category: frontend-execution
 
 ## 进度
 
-<!-- 做到哪一步了,在这里一行一行记 -->
+- 2026-10-15:认领(`Status: claimed`)→ 诊断(范围 1)→ 实施(范围 2/3)→ 离线闸门 + 变异测试 + 回归 → 收口。
+- 2026-10-15:真实浏览器走查未执行(`AGENTS.md` 项目规则:不自动运行 dev/build/截图);走查入口 `http://127.0.0.1:8000/#/cockpit`,判据见「验收对照」第 2 条。
+- 2026-10-15:**改动未提交**(工作树混有票 37/38/39/40/45/46 等并行在制品,入库须按票 33/36/42 的裁剪纪律单独摘取本票 hunks)。
 
 Status: claimed
 Type: task
 Blocked by:
 Labels: ready-for-agent, wayfinder:task
 
-## Comments
+## 诊断结论(范围 1)
+
+**小间隔=绘制缺陷(像素舍入),不是设计语义。** 判定过程没有把视觉现象当设计:
+
+- **机制**:`components/PanelBlocks.vue` 的堆叠段样式原先对 `left` 与 `width` **各自独立取整** —— `left: Math.round(row.start*100)%`、`width: Math.round(row.ratio*100)%`。相邻段之间露出的底色缝 = `round(nextStart*100) − (round(start*100) + round(ratio*100))`,该值可为 ±1 个百分点(取决于两段各自的小数部分)。
+- **复现**:用 mock 报警族工单向量 `[0,1,2,1,1,3,1,0]`(`mocks/contractMocks.js:2419` `alarmWorkOrderStates`,total=9)逐段复算,「待验收→已结案」边界 `left+width=55` 而下一段 `left=56`,**实测 1 个百分点缝**,与维护者所见一致(预警效率/报警族两处堆叠条同一算法,同一缺陷)。
+- **反证「非有意设计」**:CSS 里 `.panel-stacked__seg` 无 `margin`、条容器无 gap/分隔声明,分段色由 `barTone` 逐态给(八态八色相)——若真要表达语义边界,应当是显式的分隔样式,而不是随数据总数随机出现在某些边界上的缝。**同一对类别是否露缝取决于计数**(待派单/待接单边界 0→0 不露,待验收/已结案露 1),这正是舍入噪声的特征,不是语义设计的特征。
+- **结论**:走票面第 2 条的**「绘制缺陷」分支** → 修复为连续、可解释的条段(不加「语义间隔」说明,因为不存在这种语义)。
+
+## 实施(范围 2/3)
+
+### 交付 1:堆叠条段边界改「接力取整」,连续由构造保证
+
+- **`domain/panelBlocks.js` `stackedBlock()`** 每行新增 `startPct` / `widthPct`:由**同一个累加器**取整接力(`endPct = round(acc/total*100)`,下一段 `startPct = 上一段 endPct`),末段终点恒为 100。**无缝不是运气舍入的结果,而是代码结构保证**(每段起点定义上就是上一段终点)。
+- **`components/PanelBlocks.vue`** 堆叠段样式改消费这两个整数百分比,删除段内的 `Math.round(...)` 独立取整。
+- **口径不变(票面非目标)**:`total` / `start` / `ratio` 原样保留未取整的精确占比,只改**绘制边界**;后端统计字段与工单状态码一字未动。
+
+### 交付 2:饼图/环图交互补齐(悬停放大已有,本票补键盘与可达性)
+
+- **扇区可键盘聚焦**:饼图与环形图的每个扇区 `tabindex="0"` + `role="img"` + **每片自己的可访问名称** `` :aria-label="`${slice.label}:${slice.count}`" ``(类别与数量都在名称里,读屏可达)。
+- **focus 与 hover 走同一条路**:`@focus`/`@blur` 接**同一个**共享详情浮层 `showChartTip/hideChartTip`(票 37 走查扩展立的浮层),CSS `:focus-visible` 与 `:hover` **同一档放大**(饼 1.05 / 环 1.06)+ 增亮 + 一圈白色描边让焦点位置可见。交互风格与已验收横条统计一致(横条用 `:hover`/`:focus-visible` 同档反馈,见 `.panel-bar--button`)。
+- **外层 `<svg>` 由 `role="img"` 改 `role="group"`**:`img` 会把整棵子树对辅助技术隐藏,扇区的名称与焦点根本读不到 —— 这是「可访问名称」真正生效的前提。
+- **详情入口只留一个**:删除扇区原生 `<title>`(原生提示有 ~1s 延迟、样式不可控,且与自绘浮层双重叠加)。这与票 37 给趋势点定的成例一致(「趋势点改用无延迟的 `aria-label` 详情入口,避免与自绘浮层叠加原生 `title`」)。
+- **环形图图例悬停补齐**:此前饼图图例行接了浮层、环形图图例行漏接(同款交互分家)。现两处一致。
+
+## 验收对照(票面四条)
+
+1. **不同统计块的颜色、间隔含义明确** —— 间隔缺陷已消除(连续条段),颜色沿用票 37 的 `barTone` 八态八色相,未新增含义不明的视觉元素。
+2. **饼图扇区 hover/focus 有明显反馈并展示类别与数量** —— hover/focus 同档放大 + 描边 + 共享浮层显示「类别:数量」;键盘可达(`tabindex` + `aria-label`)。**真实浏览器走查未执行**(项目规则),走查判据:Tab 进入扇区应见描边与浮层、四类扇区逐一可读、堆叠条各类别边界不再露底色缝。
+3. **不改变统计口径** —— `total`/`start`/`ratio` 保持精确值;闸门 S4 钉住这一点。
+4. **零依赖逻辑闸门** —— 新增 [t43-stacked-gap-and-pie-interactions.mjs](../verify/t43-stacked-gap-and-pie-interactions.mjs) **13/13**。
 
+## 验证与对抗式自查
+
+**新闸门 t43(13/13,全离线)**:S 组堆叠连续(接力构造、**旧缺陷实证复现 + 新公式缝=0**、宽度和=100、0 计数行保留、口径不变、渲染层消费接力值);P 组交互(两图扇区 tabindex/aria-label/focus-blur、原生 title 已清、svg role=group、`:focus-visible` 与 `:hover` 同档、环形图图例浮层对齐);D 组 `donutBlock` 形状与可点接缝不回归。
+
+**变异测试证明守卫有牙(3 处全部打红)**:
+- M1 domain 退回旧的独立取整 → **S1/S2/S3 红**(S2 显示缝重新变 1)。
+- M2 只删饼图扇区 `tabindex` → **P1-pie 红**(证明逐元素正则用 `(?:(?!</path>)[\s\S])*?` 锚定,不会从环形图那片借断言假绿)。
+- M3 恢复扇区原生 `<title>` → **P2 红**。
+
+**回归(离线可跑的 10 组全绿)**:t43 13/13、t50 **30/30**(此前被跳过的 pinia store 断言本轮环境已恢复并实跑)、t42、t15 11/11、t23、t27、t16-stores、t16-alarm-order、t22、t19 16/16。另用仓库自带 `@vue/compiler-sfc` 对 `PanelBlocks.vue` 做**结构校验**(parse + compileTemplate 零错误,票 43 的 9 处接线在编译产物中逐条命中)。
+
+**既有红闸门归因**:`t20-overview-panels` **60/66**,6 条红为 B4/C2/D2b/D3/E2/F3(片区端点、载荷引用、store 裸字段),与票 42 收口时记录的 HEAD 基线**同 6 条**,且与本票 diff(堆叠/饼/环)零交集 —— 非本票引入。
+
+**事故与修复(如实记录)**:归因实验期间用 `git stash push -- <两个文件>` 往返,**`domain/panelBlocks.js` 被写坏**(内容变成 `.vue` 的模板、后被压成单行)。已恢复并逐层验证:① 从票 37 会话留下的 dangling stash blob 取回一份 js,核对后发现它是**票 37 之前的旧版**(旧图标名、无 `KPI_ICON_COLORS`、`barsBlock` 无 `hint`)——**没有采用**,否则会回退并行在制品;② 按本会话内已完整读过的原文(604 行)+ 本票那一处编辑重建,逐段核对;③ 结构校验:619 行 = 604 + 本票 15 行,导出符号 29 个齐全,`EllipsisText` 用点 15 处、`text-overflow` 声明 0 处(票 37 的单一口径未被破坏),`role` 用法逐处核对(2 个 svg group、2 个扇区 img、趋势/堆叠的 `role="img"` 原样保留);④ 行尾改回仓库的 CRLF(避免整文件行尾噪声污染维护者的裁剪提交);⑤ 上述全部闸门在重建后的文件上复跑转绿。`git stash list` 已空,未遗留 stash。
+
+## 遗留与下一步
+
+- **未提交**:本票 hunks = `domain/panelBlocks.js`(`stackedBlock` 接力取整 + 注释)、`components/PanelBlocks.vue`(堆叠段样式、饼/环扇区 tabindex+aria-label+focus/blur、svg role、删 `<title>`、环形图图例浮层、`:focus-visible` CSS)、`verify/t43-stacked-gap-and-pie-interactions.mjs`(新闸门)。入库按票 33/36/42 的裁剪纪律单独摘取。
+- **浏览器走查**:待维护者明确要求后执行(项目规则禁止自动运行);判据见「验收对照」第 2 条。
+- **相邻票边界**:票 44 的「条段之间有明确分割」是**视觉密度优化**(有意加分割装饰),与本票「消除无意义舍入缝」不冲突但相邻 —— 若票 44 要加显式分隔,应作为**有意的、与计数无关的**固定分隔样式实现,不要复用本票消除的舍入缝。
+
+## Comments
 
 ## Answer
 
+**间隔=绘制缺陷(`left`/`width` 各自独立取整露出的底色缝),不是语义边界;已改为 domain 单累加器接力取整,连续由构造保证,统计口径未变。饼图/环图补齐键盘焦点(`tabindex` + 每片 `aria-label` 携带类别与数量)、focus 与 hover 同档放大 + 同一个共享详情浮层、外层 svg 改 `role="group"` 使名称可达、删原生 `<title>` 收敛为单一详情入口、环形图图例悬停与饼图对齐。t43 闸门 13/13 + 3 处变异全部打红;10 组离线回归全绿;t20 的 6 条红经票 42 HEAD 基线对照证实为既有失败。改动未提交(混有并行在制品);浏览器走查按项目规则未自动执行。**

+ 8 - 0
.scratch/cockpit-real-data-wayfinding/verify/t44-distribution-ranking.mjs

@@ -32,8 +32,10 @@ import {
     archivePanel,
     emergencyPanel,
     managementPanel,
+    operationPanel,
     toBaseArchives,
     toEmergencySupport,
+    toOperatingInformation,
 } from '../../../src/modules/cockpit/domain/infrastructurePanels.js'
 import {
     monitoringDistributionPanel,
@@ -442,6 +444,12 @@ function audit(name, view, expect) {
         sv(null),
     )
     audit('management', management, [])
+    // 运行信息:KPI + 指标清单(票 38 才把它图表化);现在长出分布块就是未声明的改动。
+    const operation = operationPanel(
+        toOperatingInformation({ industries: [{ code: 'GAS', label: '燃气', metrics: [] }] }),
+        sv(null),
+    )
+    audit('operation', operation, [])
 }
 
 // ================================================================ B. 并列稳定性 + 视觉密度守卫

+ 9 - 1
public/__qiankun-host-acceptance/index.html

@@ -58,7 +58,15 @@
           name: APP,
           entry: ENTRY,
           container: '#micro-container',
-          activeRule: function () { return true; }
+          activeRule: function () { return true; },
+          props: {
+            // 被嵌侧 lifecycle.js 只在收到 url 时切 memory history 并 push 该路由;
+            // 不传则停在宿主默认页,/cockpit 不会渲染。
+            url: '/cockpit',
+            onRouterInit: function (router) {
+              rec('routerInit', String(!!router));
+            }
+          }
         }], {
           beforeLoad: [function (app) { rec('beforeLoad', app.name); }],
           beforeMount: [function (app) { rec('beforeMount', app.name); }],

+ 21 - 1
src/modules/cockpit/services/mapService.js

@@ -2982,8 +2982,28 @@ export function getHoveredDevice() {
     return hoveredDeviceCode
 }
 
+const pipelinePickHandlers = new Set()
+
+/** 管线/构件点选订阅;回调只收到业务身份,不收到 Cesium 实体。 */
+export function onPipelinePick(handler) {
+    if (typeof handler !== 'function') return () => {}
+    pipelinePickHandlers.add(handler)
+    return () => pipelinePickHandlers.delete(handler)
+}
+
+function pickPipelineAt(clientX, clientY) {
+    if (!instance || !instance.engine || !instance.viewer || typeof instance.engine.pickPipeObjectAt !== 'function') {
+        return { target: 'none', pick: null }
+    }
+    const pick = instance.engine.pickPipeObjectAt(instance.viewer, clientX, clientY)
+    if (!pick) return { target: 'none', pick: null }
+    const result = { target: 'pipeline', pick }
+    for (const handler of [...pipelinePickHandlers]) handler(pick)
+    return result
+}
+
 /**
- * 点选链路(票 43):**先设备点、再监测点**(规格:正常单点设备点优先、监测点兜底)。
+ * 点选链路(票 43/23):**先设备点、再监测点**(规格:正常单点设备点优先、监测点兜底)。
  *
  * 优先级判定在 `domain/deviceCorrespondence.js` 的 `resolvePickOrder()`(可脱浏览器断言),
  * 本层只负责"两次引擎拾取 + 按结论派发",不自己写 if-else 顺序。