# 前端执行票 —— 工单堆叠图语义间隔与饼图交互 Label: ready-for-agent Category: frontend-execution ## 来源 承接票37收口时维护者反馈第1、2条。 ## 范围 1. 调查工单横向堆叠统计图中部分类别间的小间隔(待接单/已接单、已结案/已取消):确认是浏览器像素舍入、条段绘制策略还是有意表达语义边界;不得把不明原因的视觉现象直接当成设计。 2. 若为设计语义,补充图上说明/图例提示;若为绘制缺陷,修复为连续、可解释的条段。 3. 为饼图/环图补齐交互:悬停放大或高亮、共享详情浮层、键盘焦点/可访问名称;交互风格与已验收横条统计保持一致。 4. 验收:不同统计块的颜色、间隔含义明确;饼图扇区 hover/focus 有明显反馈并展示类别与数量;不改变统计口径。 ## 非目标 - 不改变后端统计字段与工单状态口径。 - 不重做票37已收口的共享省略组件。 ## 进度 - 2026-10-15 上午~傍晚:三轮(接力取整 → 整楔命中区 → shallowRef+放大档),每轮一次驳回,详见下方历史与本票 Comments。 - 2026-10-16(**第四轮根因**,用户反馈:hover 停留几秒、鼠标未移出,放大与联动高亮**自己消失**):外壳时钟每秒 `clockText` 变化重渲染 + `:view="panelViewOf(panelId)"` 是模板方法调用 → 每帧重算出**全新 block 对象**;第三轮的 `owner === toRaw(block)` 身份比较在下一帧静默失效——浮层存的是标量所以还在,高亮却掉了,症状完全吻合。修复=联动键改 `chartScope(index, block)`=**视图名|块序号|块类型|块标题**的稳定标量串(数据刷新前后不变,仍区分并排同类图与块内不同行)。批量替换时漏掉 `clearChartHover` 6 处(前缀不含 set),mouseleave 仍传 block → 高亮清不掉,被 t43 P1 假红逮出修正;16 个调用点全部统一。 - 2026-10-16 09:21(第四轮完成态走查,用户授权延续,real):**hover 持续 3.5 秒跨 3+ 时钟 tick**,饼图「报警」/环形图「排水」的放大+图例联动+浮层全程保持(页面时钟 09:20:23→09:21:41 跨多次重渲染)——不再自灭。t43 扩到 23/23(P8 标量 scope 守卫 + P8b 运行时探针:对象身份键在新对象下查不回、scope 键查得回;变异 M8 退化回对象身份 → P8 打红);11 组离线回归全绿。 Status: resolved Type: task Blocked by: Labels: ready-for-agent ## 诊断结论(范围 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:饼图/环图交互(第三轮最终形态 —— 整楔命中区 + shallowRef 联动 + 明显放大档) > 第一轮把 tabindex/hover 挂在可见细环带上(46/25、42/26),真实指针落在圆心孔/环带外全部不触发; > 第二轮改整楔命中区后浮层触发修好了,但联动状态 `chartHover = ref(...)` 被 Vue 深层代理, > `owner === block` 恒 false,`.is-hovered` 从未生效——扇区放大/图例高亮全部落空(第三轮驳回)。 > 第三轮:`shallowRef` 存联动状态(身份保持原始对象,整体替换即触发渲染);放大档加强到 > scale 1.1 + brightness 1.35 + 白描边(原 1.05/1.06 在 64px 图上仅 ~1.5px 位移,不够"明显")。 - **整楔命中区(`hitPath`)**:`statusPie.js` 的 `hitWedge` 与 `panelBlocks.js` 的 `donutBlock` 为每个扇区额外产出一条**从圆心出发的整楔**(内半径 0)路径。渲染层把它画在可见扇区之上(`fill: transparent`),**悬停/聚焦/键盘全部挂命中楔**——热区与视觉扇区同角度同边界,圆心孔不再是死区。 - **扇区 ⇄ 图例双向联动(`chartHover = shallowRef`)**:任一侧悬停/聚焦,可见扇区加 `.is-hovered`(放大+提亮+描边)、对应图例行加 `.is-hovered`(底色+文字提亮),同一个共享详情浮层(票 37 的 `showChartTip`)。按 `owner=块 + key=行` 记账:两块同类图并排不串亮;清除比对 owner+key 防闪烁。 - **键盘可达**:命中楔 `tabindex="0"` + `role="img"` + 每片 `aria-label`(`类别:数量`);`:focus-visible` 画白色描边。外层 `` `role="group"`(img 会藏子树使名称不可达)。 - **详情入口只留一个**:扇区不带原生 ``(票 37 趋势点成例)。 - **图例标注与数值成对(维护者反馈②)**:饼图行去 `justify-content: space-between`、环形计数去 `margin-left: auto`、标签 `flex: 0 1 auto`(只缩不涨——`1 1 auto` 会把数值顶回行尾)。环形中心读数 `pointer-events: none` 让位给命中楔。 ## 验收对照(票面四条) 1. **不同统计块的颜色、间隔含义明确** —— 间隔缺陷已消除(连续条段),颜色沿用票 37 的 `barTone` 八态八色相,未新增含义不明的视觉元素。 2. **饼图扇区 hover/focus 有明显反馈并展示类别与数量** —— 第三轮以**运行时证据**判(不再以浮层出现代判):悬停/聚焦任一侧后,可见扇区 `class` 真带 `is-hovered` 且计算样式 transform/filter 变化、对应图例行同亮;浏览器正反证见「进度」与 Comments。 3. **不改变统计口径** —— `total`/`start`/`ratio` 保持精确值;闸门 S4 钉住这一点。 4. **零依赖逻辑闸门** —— [t43-stacked-gap-and-pie-interactions.mjs](../verify/t43-stacked-gap-and-pie-interactions.mjs) 第三轮扩到 **22 条**(新增 P9 shallowRef 静态守卫 + P9c 运行时身份探针)。 ## 验证与对抗式自查 **闸门 t43(第三轮 22/22,全离线)**:S 组堆叠连续(接力构造、旧缺陷实证复现 + 新公式缝=0、宽度和=100、0 计数行保留、口径不变、渲染层消费接力值);P 组交互(P0 两图扇区带 `hitPath`、P1 命中楔承载 tabindex/aria-label/hover/focus、P1b 可见扇区被动化、P2 无原生 title、P3 svg role=group、P4 `.is-hovered` 放大档 + 命中楔焦点描边、P5 图例⇄扇区双向、P6 数值紧跟标签、P7 中心读数让位、P8 联动记账结构、**P9 联动状态必须 shallowRef + P9c Vue 运行时身份探针(ref 深层代理会破坏 owner===block,shallowRef 不会)**);D 组 `donutBlock` 形状与可点接缝不回归。 **变异测试累计 7 处打红**:M1 独立取整→S 组红;M2 删 tabindex→P1 红;M3 恢复 ``→P2 红;M4 图例删 setChartHover→P5 红;M5 恢复 margin-left:auto→P6 红;M6 标签改回生长(同时暴露 CSS 守卫必须先剥注释)→P6 红;**M7 `shallowRef` 改回 `ref`→P9 红(本轮根因的反向锁定)**。 **回归(离线 11 组全绿)**:t43、t50 30/30、t42、t15、t23、t27、t16-stores、t16-alarm-order、t22、t19、t09。`@vue/compiler-sfc` 结构校验全过。 **既有红闸门归因**:`t20-overview-panels` 60/66 的 6 条红与票 42 记录的 HEAD 基线同 6 条,与本票 diff 零交集。 **教训留档**:① 归因实验禁用 `git stash push -- <文件>`(本工作树会写坏文件),用票 42 的 HEAD worktree 对照法;② 带孔/细环 SVG 图形的 hover 断言必须打在**用户视角位置**(圆心、环带外);③ **联动类交互的验收判据必须是"被联动元素的状态本身"(class/计算样式),不是旁证(浮层)**;④ 把对象存进深层 `ref` 再用 `===` 比身份,会被 Vue 代理拆穿——跨响应式边界比身份要用 `shallowRef`/`toRaw` 或稳定标量键。 ## 遗留与下一步 - **入库状态(2026-10-16 更新)**:第一轮 hunks 已随并行票 37 批次 `a40aedc` 推送 `origin/master`;第二/三/四轮 hunks(`domain/panelBlocks.js`、`domain/statusPie.js`、`components/PanelBlocks.vue`、`verify/t43-*.mjs`)经**独立临时 index 的 plumbing 提交**为 **`7f71992`** 并推送 `origin/master`。工作树混有并行票 38/47 在制品,故只摘本票 hunks(`PanelBlocks.vue` 13 处逐个复核归属,剔除票 38 的维度切换与 KPI `wide` 布局共 6 处),未碰他人暂存区与工作树。 - **相邻票边界**:票 44 的「条段之间有明确分割」是有意装饰,与本票消除的舍入缝无关,勿混用。 ## Answer **(第三轮最终)** 双向联动的真正根因不在几何、不在 CSS,而在**响应式身份比较**:`chartHover = ref(null)` 存入 `{owner: block}` 后被 Vue 深层代理,读回的 owner 是代理对象,与模板 v-for 拿到的原始 block `===` 恒 false → `.is-hovered` 永不生效(扇区不放大、图例不亮),而浮层不依赖这个比较所以照常弹出——一个根因同时解释「只出详情」与「联动失败」。修复=`shallowRef` + 两侧 `toRaw()` 比较;放大档加到 scale(1.1)+brightness(1.35)+白描边(原 1.05/1.06 在 64–72px 图上仅 1.5–2px 位移,不够"明显")。**验收判据本轮改为"被联动元素自身的状态"**(class/视觉放大),不再以浮层出现代判:真实浏览器(real)悬停扇区→扇区放大+描边+图例亮、悬停图例→扇区放大+描边(双向成立)、环形图与键盘 focus 同法通过;变异 M7(shallowRef 改回 ref)被 P9 打红,P9c 在真 Vue 里钉住机制本身。t43 22/22、11 组回归全绿。第二/三轮 hunks 已于 2026-10-16 随第四轮一并提交(`7f71992`,plumbing 限定提交)并推送 `origin/master`。 ## Comments ### local — 2026-10-15T03:55:21.458Z 诊断+实施完成(2026-10-15):① 间隔根因=绘制缺陷——堆叠段 left/width 各自独立取整露出底色缝(mock 报警向量 [0,1,2,1,1,3,1,0] 实测「待验收→已结案」边界 1 个百分点,反证非语义设计:CSS 无任何分隔声明、露缝与否随计数变化)。修复=domain 单累加器接力取整(startPct/widthPct),连续由构造保证;total/start/ratio 口径原样保留。② 饼/环交互补齐:扇区 tabindex=0 + 每片 aria-label(类别:数量)、focus/blur 接同一共享浮层、:focus-visible 与 :hover 同档放大+描边、外层 svg role=img→group(img 会藏子树使名称不可达)、删原生 <title> 收敛单一详情入口(票 37 趋势点成例)、环形图图例悬停与饼图对齐。验证:t43 新闸门 13/13、变异 M1/M2/M3 全部打红、10 组离线回归全绿(t50 30/30)、@vue/compiler-sfc 结构校验 9 项全过。t20 60/66 的 6 条红=票 42 记录的 HEAD 既有失败,与本票零交集。事故如实记录:归因实验的 git stash 往返写坏了 panelBlocks.js,已按本会话完整读取的原文+本票编辑重建(619=604+15 行、导出 29 符号齐、EllipsisText 15 用点、CRLF 归一),全部闸门在重建文件上复跑转绿。浏览器走查已获用户授权,但被并行会话在制品 mapService.js 的半完成语法错误阻断(Vite 模块图编译失败),用户指示等其修复后重试。改动未提交(混有并行在制品,按票 33/36/42 裁剪纪律单独摘取)。 ### local — 2026-10-15T07:06:33.257Z 票 43 重开轮(2026-10-15 下午,维护者复验反馈两条):① 饼/环图交互没成功——只有悬停图例才出详情,扇区本身不触发;要求扇区与图例都触发、效果对齐已验收横条统计。② 图例标注与数值隔整行宽,没有关联感。诊断:可见扇区是细环带(饼 46/25、环 42/26),SVG 只有 painted 区接收指针——真实鼠标落在中心孔/环带外全部不触发;上轮合成 hover 恰好打在楔形 painted 区上造成假绿(教训:hover 断言必须打在包围盒中心=此前不可达的孔位)。第一轮修复:statusPie/donutBlock 每扇区新增 hitPath=到圆心的整楔(inner=0),渲染层画透明命中层承载 hover/focus/键盘,可见扇区改被动 aria-hidden;扇区⇄图例双向联动(chartHover 按 块+行key 记账,任一侧悬停两侧同亮+同一浮层),放大从 :hover 改 .is-hovered 驱动(事件在命中层上,扇区 :hover 收不到);环形中心读数 pointer-events:none 让位;图例数值紧跟标签(饼行去 space-between、环计数去 margin-left:auto、标签 flex:0 1 auto 只缩不涨——第一轮我写成 1 1 auto 把数值又顶回行尾,被变异 M6 逮住:CSS 守卫必须先剥注释,否则注释里的期望值骗过 [^}]* 匹配)。t43 闸门扩到 20 条全绿、变异 M4/M5/M6 打红;11 组离线回归全绿;SFC 结构校验 6 项全过。真实浏览器:环图中心悬停已触发联动(截图证实),但完成态走查再次被并行在制品阻断(这次是 pipeModels.js:3 行尾多一个引号,Vite 编译不出模块图,页面报 __vnode 级联),等待其修复后补完三条判据再关票。 ### local — 2026-10-15T07:33:56.424Z 第二轮收口(2026-10-15 15:33 完成态走查补齐):环形图中心悬停已实证——总览「行业构成·设施」(真实 5520,四片)中心点触发共享浮层「窨井 3」+色块(中心点被最后一片命中楔覆盖属预期:圆心对所有楔都命中,顶层者胜),图例数值成对(排水 3166/供水 1652/燃气 699/窨井 3 紧跟标签)。加上此前饼图中心悬停「无关联设备 8」、图例悬停「无设备 6」、Tab 逐片聚焦「报警 1→无设备 6」,两条反馈的全部判据闭环。t43 20/20 + 变异 6 处打红 + 11 组回归全绿 + SFC 6/6。关闭。第二轮 hunks(panelBlocks.js/statusPie.js/PanelBlocks.vue/t43 脚本)未提交,混有并行票 45/46/47 在制品,入库单独裁剪。 ### local — 2026-10-15T08:17:04.691Z 维护者再次复验驳回:双向联动未实现,扇区仅显示详情而没有放大/高亮。本轮重开,必须验证运行时高亮条件及实际视觉效果,不能再用浮层出现或源码存在代替验收。 ### local — 2026-10-15T09:47:14.501Z 第三轮收口(2026-10-15 17:45):根因=chartHover 用 ref 存对象被 Vue 深层代理,owner===block 恒 false → is-hovered 永不生效(扇区不放大、图例不亮),浮层不依赖该比较所以照常出——一个根因同时解释两条驳回(已用真 Vue 探针 P9c 钉死机制)。修复=shallowRef + 两侧 toRaw;放大档 1.05/1.06 → 1.1 + brightness 1.35 + 白描边(小图上原档只有 1.5–2px 位移,不够"明显")。真实浏览器(real,用户授权)以**被联动元素自身状态**为判据:① 悬停饼图「报警」扇区→红片外顶放大+白描边、图例行「报警 1」亮;② 反向悬停图例「无关联设备 8」→橙片放大+描边;③ 环形图悬停「排水」扇区与键盘 focus「供水」命中楔同档反馈。正反证:变异 M7(shallowRef 改回 ref)P9 转红。t43 22/22、11 组离线回归全绿。摘 needs-triage、关闭。第二/三轮 hunks(PanelBlocks.vue、panelBlocks.js、statusPie.js、t43 脚本)未提交,混有并行票 45/46/47 在制品,入库单独裁剪。 ### local — 2026-10-16T01:22:14.470Z 维护者复验(第四轮反馈):扇区放大已生效,但 hover 停留几秒、鼠标未移出时,放大与图例联动**自己消失**。重开诊断。 ### local — 2026-10-16T01:22:32.543Z 第四轮收口(2026-10-16 09:21):根因=联动键用了**对象身份**(owner === toRaw(block)),而外壳时钟每秒重渲染 + `:view="panelViewOf(panelId)"` 是模板方法调用 → 每帧重算出全新 block 对象,hover 未移出时下一次 tick 的比较就静默失效——这正是「停留一会自己消失、浮层还在」(浮层存标量)。修复=联动键换成**跨重算稳定的标量 scope**(`chartScope(index, block)`=视图名|块序号|块类型|块标题),数据刷新前后不变;批量替换时曾漏掉 clearChartHover 的 6 处调用(前缀不含 set),mouseleave 还传 block 导致高亮永远清不掉,被 t43 P1 假红逮出并修正——全部 16 个调用点已统一 chartScope。运行时判据(用户授权,real):**hover 持续 3.5s 跨 3+ 时钟 tick**,饼图「报警」与环形图「排水」的高亮/放大/浮层全程保持(时钟 09:20:23→09:21:41 跨多次重渲染),两类图均不再自灭。t43 扩到 23/23(P8 标量 scope 静态守卫 + P8b 运行时探针:对象身份键在新对象下查不回、scope 键查得回)、变异 M8(chartScope 退化回对象身份)打红;11 组离线回归全绿。第四轮 hunks 并入本票未提交批次(PanelBlocks.vue/t43 脚本),混有并行在制品,入库单独裁剪。