# 3b · 实测记录(③段界面交互) > 落点:`执行会话/目标-根据1a需求文档生成MCN短视频整合营销-d85687/ui/3b-实测记录.md` > 判据源:`stage-delivery/SKILL.md` §3b/§供给;`references/execution-runbook.md` §3.5、§5.3、§六;`references/layouts-tooling.md` §1、§5.5 > 交付物:`ui/mcn-workbench.html`(单文件自包含原型)+ `ui/DESIGN.md`(规范) ## 〇、声明制(四字段) | 字段 | 内容 | |---|---| | 当前段与子步 | ③段 / 3b 原型构建 + 3b 实测(实测未过不进 3c) | | 主干与挡位 | 供给=`oil-ui-pro`(方法主线)+ 本段自带样式库 `design-system-tiaoyue`(定调与令牌)/ 挡位 **标准** | | 依据文件 | `DESIGN.md`(本段契约)、`prd/2b-界面布局.md`(冻结骨架)、`prd/2a-产品功能.md`(F1–F14) | | 产出落点 | `ui/mcn-workbench.html`、`ui/_tools/`(三个可复跑脚本)、`ui/_gate_shots/`(23 张视口帧) | ## 一、怎么复跑(三条命令,全部只读原型) ```bash PY="E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" cd "…/ui" $PY _tools/cap.py --mode dom --route selftest --viewport 1440x900 --out _scratch/st.html # 取自检输出 $PY _tools/mutate_test.py # 变异对照 $PY _tools/check_skeleton.py # 骨架冻结闸门 ``` `cap.py` 对外只收 `--viewport`;它内部按模式补偏移量(原因见 §五 的通道口径一节)。`_scratch/` 是跑出来的中间件、目录不存在时脚本会自建,看完随手删掉不影响任何结论;交付目录里只留脚本、截图与四份文档。 ## 二、自检 81 项(1440×900 主视口,PASS 81 / FAIL 0) 自检是原型自带的:`#/selftest` 路由会在真实场景里逐个点过每个可点元素。全文落盘如下,⛔ 不是摘要。 ```text 自检结果:PASS 81 / FAIL 0 PASS 路由 #/p/p1 渲染出 5 个顶层节点 PASS 路由 #/p/p2 渲染出 6 个顶层节点 PASS 路由 #/p/p3 渲染出 4 个顶层节点 PASS 路由 #/p/p4 渲染出 6 个顶层节点 PASS 路由 #/p/p5 渲染出 5 个顶层节点 PASS 路由 #/p/p6 渲染出 4 个顶层节点 PASS 路由 #/p/p7 渲染出 5 个顶层节点 PASS 路由 #/p/p8 渲染出 4 个顶层节点 PASS 六态 × 八页 = 48 个组合,全部渲染出内容 PASS 侧栏二级页项 8 个,与八页一致 PASS 清单里每个 ID 都在页面上出现过(75 个,无悬空声明) PASS 页面上每个 data-act 都在清单里(共 703 次出现,无未登记的可点元素) PASS 清单里 71 个 ID 都真驱动过并给出可见反馈,0 个死控件(22 个场景,每个实例各重置一次) PASS 当刻被禁用的控件 4 个(有 disabled 属性:按契约在特定条件下不可点):P4-03, P5-06, P6-03, R-02 PASS 清单里每个 ID 都至少被驱动过一次(无「从未跑过」的条目) PASS 侧栏 8 项逐个点过,每一项都跳到了自己标的那一页 PASS 五处「输入后才可用」的联动都通:其中 4 处在空输入时确实是禁用的 → 输入后转可用 → 点了有反应 PASS 工具型纪律:零 .eyebrow / 零 .lead / 零 .hero PASS 零 @keyframes(工具型不做入场编排) PASS 骨架冻结:八页 37 个板块的数量与顺序与 ②段 2b「板块清单」逐位一致,⛔ 未增删挪 PASS 结构 ①:八页各一个 h1,标题逐级不跳;每个板块都有文字标题 PASS 结构 ②:字号 12/14/16/18/24px 共 5 档,相邻差 2/2/2/6,正文档 14–16 在用 PASS 结构 ④:逐页查过,首屏同屏主档按钮 ≤1 个;本页实底按钮 1 种 PASS 交互态与可读性::hover / :focus-visible 都在且没被 outline:none 抹掉;tabular-nums、prefers-reduced-motion、防溢出三件套齐全 PASS 阴影:面板/卡片都用多层叠层,无单层死黑重阴影 PASS 间距:flex/grid 的 gap 取值全部落在登记刻度内(9 个刻度) PASS 浮层「随手记」中性关闭出口 = 1(右上 ×)|另有决策型关闭 1 个(R-03);遮罩是「点层外」通道,不计作出口 PASS 浮层「随手记」打开后焦点在层内(当前=X-close) PASS 浮层「随手记」连按 8 次 Tab,焦点始终锁在层内 PASS 浮层「随手记」Esc 能关 PASS 浮层「随手记」关掉后焦点回到触发件 G-06 PASS 浮层「随手记」点遮罩(层外空白)能关 PASS 浮层「全局检索」中性关闭出口 = 1(右上 ×)|另有决策型关闭 0 个;遮罩是「点层外」通道,不计作出口 PASS 浮层「全局检索」打开后焦点在层内(当前=X-close) PASS 浮层「全局检索」连按 8 次 Tab,焦点始终锁在层内 PASS 浮层「全局检索」Esc 能关 PASS 浮层「全局检索」关掉后焦点回到触发件 G-10 PASS 浮层「全局检索」点遮罩(层外空白)能关 PASS 浮层「两处假设说明」中性关闭出口 = 1(右上 ×)|另有决策型关闭 0 个;遮罩是「点层外」通道,不计作出口 PASS 浮层「两处假设说明」打开后焦点在层内(当前=X-close) PASS 浮层「两处假设说明」连按 8 次 Tab,焦点始终锁在层内 PASS 浮层「两处假设说明」Esc 能关 PASS 浮层「两处假设说明」关掉后焦点回到触发件 G-08 PASS 浮层「两处假设说明」点遮罩(层外空白)能关 PASS 浮层「资产选择器」中性关闭出口 = 1(右上 ×)|另有决策型关闭 0 个;遮罩是「点层外」通道,不计作出口 PASS 浮层「资产选择器」打开后焦点在层内(当前=X-close) PASS 浮层「资产选择器」连按 8 次 Tab,焦点始终锁在层内 PASS 浮层「资产选择器」Esc 能关 PASS 浮层「资产选择器」关掉后焦点回到触发件 P4-04 PASS 浮层「资产选择器」点遮罩(层外空白)能关 PASS 浮层「版本列表」中性关闭出口 = 1(右上 ×)|另有决策型关闭 0 个;遮罩是「点层外」通道,不计作出口 PASS 浮层「版本列表」打开后焦点在层内(当前=X-close) PASS 浮层「版本列表」连按 8 次 Tab,焦点始终锁在层内 PASS 浮层「版本列表」Esc 能关 PASS 浮层「版本列表」关掉后焦点回到触发件 P4-07 PASS 浮层「版本列表」点遮罩(层外空白)能关 PASS 浮层「发布二次确认」中性关闭出口 = 1(右上 ×)|另有决策型关闭 1 个(P5-10);遮罩是「点层外」通道,不计作出口 PASS 浮层「发布二次确认」打开后焦点在层内(当前=X-close) PASS 浮层「发布二次确认」连按 8 次 Tab,焦点始终锁在层内 PASS 浮层「发布二次确认」Esc 能关 PASS 浮层「发布二次确认」关掉后焦点回到触发件 P5-08 PASS 浮层「发布二次确认」点遮罩(层外空白)能关 PASS 浮层「核销二次确认」中性关闭出口 = 1(右上 ×)|另有决策型关闭 1 个(CHECKOUT-CANCEL);遮罩是「点层外」通道,不计作出口 PASS 浮层「核销二次确认」打开后焦点在层内(当前=X-close) PASS 浮层「核销二次确认」连按 8 次 Tab,焦点始终锁在层内 PASS 浮层「核销二次确认」Esc 能关 PASS 浮层「核销二次确认」关掉后焦点回到触发件 P8-03 PASS 浮层「核销二次确认」点遮罩(层外空白)能关 --- 数值取证(布局视口 1440 × 900 ;window.innerWidth 1440)--- PASS 对比度 --warn 对 --surface = 5.93:1(契约 ≥4.5:1) PASS 对比度 --accent-text 对 --surface = 4.76:1(契约 ≥4.5:1) PASS 对比度 --ink-faint 对 --surface = 3.55:1(契约 ≥3:1) PASS 强调色填充面积占比:八页最高 创作台 = 0.52%(1 个填充块,契约 ≤5%;不含 3px 左侧标条 / 描边 / 文字色) 量尺寸时 scrollY = 0(P1) PASS 量尺寸前页面停在滚动零点(P1):rect 取到的是相对视口的真值 PASS P1 账号矩阵:首屏从 214px 起,行距 67px,一屏装得下 10 行(契约 ≥6 行/900px 高,实测宽 1440)|示意数据 5 条 ⇒ 实际可见 5 行(数据用尽,非被截断) PASS P1 账号矩阵行高齐:67–68px,差 ≤2px(没有哪一行被状态标或长文字撑高) PASS P1 实际在用字号档 5 档:12px / 14px / 16px / 18px / 24px(契约 ≥3) PASS 视觉层间距 32px / 块内相邻行分隔 1px(发丝线,行本身不套框)⇒ 比值 32 ≥2(4 个视觉层) P1 各块落位(顶 / 高):今日待处理@149+360 · 账号矩阵列表@149+417 · 异常值提示@599+215 · 本月表现最好的一条@845+122 · 全局检索与随手记@1000+113(视口高 900;并排的两块顶相同) 量尺寸时 scrollY = 0(P2) PASS 量尺寸前页面停在滚动零点(P2):rect 取到的是相对视口的真值 PASS P2 对标内容列表:首屏从 402px 起,行距 67px,一屏装得下 7 行(契约 ≥6 行/900px 高,实测宽 1440)|示意数据 5 条 ⇒ 实际可见 5 行(数据用尽,非被截断) P2 各块落位(顶 / 高):对标监控概览@165+139 · 对标内容列表@336+417 · 评论区洞察@785+108 · 选题库@925+330 · 灵感速记@1287+113 PASS 横向溢出 = 0(scrollWidth 1440 = clientWidth 1440) PASS 八页逐页查横向溢出:全为 0(最差 p1) (附)P1 行高 67/67/67/68/67px · P1 按钮高度集合 32px · 量时 scrollY=0 · 顶栏高 69px · 导航占 1105px(column)· 主区内衬上 24px · innerWidth 1440 − clientWidth 1440 = 0px(窗口滚动条宽度) 控制台报错:0 条 ``` 截图:`_gate_shots/selftest-1440.png`。 ## 三、变异对照 13 / 13(证明上面那串 PASS 不是恒绿) 一个永远 PASS 的自检等于没有自检。做法:复制原型 → 注入一处**已知的假病** → 再跑一次自检 → 要求它报红、**且红在预期那一条上**。 ```text 基线(未注入):PASS 81 / FAIL 0 ✅ M1 死控件(掏空 P2-01 实现) ⇒ PASS 80/FAIL 1 |红在 P2-01 ✅ M2 未登记的可点元素(塞一个 ZZ-99) ⇒ PASS 79/FAIL 2 |红在「未登记的 data-act」 ✅ M3 悬空声明(清单里加一个页面没有的 ID) ⇒ PASS 79/FAIL 2 |红在「声明了但页面里没有」 ✅ M4 骨架漂移(P3 多塞一块) ⇒ PASS 80/FAIL 1 |红在「骨架与②段不一致」 ✅ M5 对比度越界(--ink-faint → #c9c7c0) ⇒ PASS 80/FAIL 1 |红在「对比度 --ink-faint」 ✅ M6 点击无反馈(go() 退回只改 hash) ⇒ PASS 79/FAIL 2 |红在「驱动了没反应」 ✅ M7 行高不齐(放开行内换行,768x900) ⇒ PASS 79/FAIL 2 |红在「行高不齐」 ✅ M8 密度退化(行内衬 12px → 36px) ⇒ PASS 79/FAIL 2 |红在「低于契约 6 行」 ✅ M9 量尺寸时页面不在零点(量前先滚 300px) ⇒ PASS 80/FAIL 1 |红在「被滚到」 ✅ M10 行分隔线被关掉(退回 .list-row:first-child)⇒ PASS 80/FAIL 1 |红在「行分隔线根本没画出来」 ✅ M11 浮层没有遮罩 ⇒ PASS 72/FAIL 9 |红在「没有遮罩」 ✅ M12 关掉浮层后焦点不还回去 ⇒ PASS 74/FAIL 7 |红在「关掉后焦点没回到」 ✅ M13 浮层多出一个中性关闭出口(再加一颗 ×) ⇒ PASS 74/FAIL 7 |红在「中性关闭出口 = 2」 变异对照:13 / 13 通过 ⇒ 13 处假病全部被自检抓住,且基线回绿:这套判据不是恒绿的。 ``` ⚠️ **M9 换过一次**。原来那版是「拿掉 `applyScene` 末尾的滚动复位」,但实测它**已经观测不到差别**了:重绘 `#view` 时浏览器把滚动自然夹回 0,量出来的数确实干净 ⇒ 那条变异变成了一条抓不到东西的假变异。改成直接注入「量之前把页面滚下去」,也就是这条守卫真正声称要拦的那件事。 ## 四、骨架冻结闸门(正向 + 双向变异) 把②段 `prd/2b-界面布局.md` §二 当权威读,与原型 `FROZEN` 表逐页逐位比(数量相等、顺序相等、名字互为前缀)。 ```text 正向:PASS 骨架冻结:八页 37 个板块,②段 2b-界面布局.md §二 与原型 FROZEN 表逐页逐位一致 反向变异 1(改原型 p3 多一块):p3 板块数 ②段 4 ≠ 原型 5 反向变异 2(改②段副本 p1 第 2 块名):p1 第 2 块 ②段「账号矩阵一览」≠ 原型「账号矩阵列表」 ``` 两条反向变异改的都是**副本**(`_probe/2b-mut.md`),权威件 `prd/2b-界面布局.md` 全程只读。为了事后能复核这件事,这里钉一条指纹: ```text prd/2b-界面布局.md md5 = 1dcc69f952df97a394481658e84d90b2 15036 字节 mtime 2026-10-08 02:30 ``` ✅ 复核方式:`md5sum ../prd/2b-界面布局.md`,与上面比。一致 ⇒ ③段确实没动过冻结骨架。 (这条指纹是删 `_probe/` 之前补登的 —— 备份删掉后,只剩这一个数能证明骨架没被污染,所以先写进正文再清临时件。) ## 五、多视口密度核对(契约:一屏可见行数 ≥6 / 900px 高,`layouts-tooling.md` §1 原文,⛔ 不分档) | 视口 | 自检 | P1 账号矩阵 | P2 对标列表 | 顶栏高 | 导航 | 结论 | |---|---|---|---|---|---|---| | 1440×900 | **PASS 81/0** | 首行 214px,**10 行** | 首行 402px,**7 行** | 69px | 1105px(竖栏) | ✅ | | 1280×900 | **PASS 81/0** | 首行 216px,**10 行** | 首行 403px,**7 行** | 69px | 竖栏 | ✅ | | 1024×900 | **PASS 81/0** | 首行 495px,**6 行** | 首行 402px,**7 行** | 69px | 竖栏 | ✅ | | 768×900 | **PASS 81/0** | 首行 495px,**6 行** | 首行 402px,**7 行** | 69px | 竖栏 | ✅ | | 641×900 | PASS 80/**1** | 首行 535px,**5 行** ❌ | 首行 442px,**6 行** | 109px(折两行) | 竖栏 | 缺口见 §六 | | 640×900 | PASS 80/**1** | 首行 532px,**5 行** ❌ | 首行 439px,**6 行** | 61px | 53px(横条) | 缺口见 §六 | | 500×844 | PASS 79/**2** | 首行 572px,**4 行** ❌ | 首行 479px,**5 行** ❌ | 101px(折两行) | 53px(横条) | 缺口见 §六 | **边界是量出来的,不是我定的**:视口 ≥768px 时顶栏单行、导航竖排,一屏 6–10 行;≤767px 时顶栏折成两行(641px 实测 109px)、≤640px 导航变标签栏,首屏被页面外壳吃掉,容量掉到 4–5 行。行本身没变肥 —— 行高 67px、各行差 ≤2px、块内分隔 1px,这几条在任何视口都 PASS。 > 反事实(把 P1 前两块强制落回单列,1440×900):`账号矩阵列表@530+413` ⇒ 首行顶 530px、容量 `floor((900−530)/67) = 5 行`,够不着 6 ⇒ **并排是密度判据逼出来的,不是审美选择**。 ### 通道口径(本轮实测发现,已修) 同一台机器、同一个 `--window-size`,Chrome 两条通道给出的**视口不是同一个**: ```text --window-size=656,995 ─┬─ --dump-dom ⇒ 视口 640 × 900 (mq<=640 true) └─ --screenshot ⇒ 视口 656 × 995 (mq<=640 false) ``` 量这件事用的探针页只有 18 行,为了让它跟着报告一起活下去(临时目录已清),源码附在下面 —— 想自己复核就把这段存成 `_scratch/vp.html`,两种模式各跑一次同一尺寸,读第 1、3 行: ```html vp
…
``` ⇒ 截图通道原样等于视口,dump-dom 通道要补回被扣掉的 16 / 95。`cap.py` 已按模式分开算,并且**每次截图后直接从 PNG 的 IHDR 读真实像素跟目标视口比**,不等就当场报红 —— 这条守卫是补回来的,因为本轮之前所有截图都比标称大 16×95(「1440×900」实际是 1456×995)。 ## 六、本轮修掉的真缺陷(每条:现象 → 根因 → 证据) | # | 现象 | 根因 | 证据 | |---|---|---|---| | 1 | 整份设计系统在屏幕上失效 | 样式表顶部注释里 `--t-*/--raw-*` 的 `*/` 提前闭掉注释,吞掉整个 `:root` 令牌块 | 修复后对比度取证才量得出来 | | 2 | 全表行分隔线一直没画出来 | `.list-row:first-child{border-top:0}`:每行都包在 `
  • ` 里,于是**每行都是自己 `
  • ` 的 first-child**,`borderTopStyle` 全是 `none` | 变异 M10;另修了判据除数为 0 时恒 PASS(比值 `Infinity`) | | 3 | 浮层不浮 | `#layer-root` 与 `.layer` 都没有定位样式,浮层渲染在文档流末尾,得滚到页尾才看得见 | 改为遮罩 `fixed inset:0` + 层本体 `fixed` 居中 | | 4 | 行高随文字长短乱跳(66/82/100px) | 状态标放在操作列把 `auto` 列撑宽,只有那一行折行 | 状态标移到名称旁;再加「行内每段不许换行 + 省略号」;判据新增「行高齐」 | | 5 | 自检在窄视口给出假 PASS「装得下 10 行」 | 驱动几百次点击后浏览器为聚焦元素自动滚动,`rect.top` 变负,容量公式虚高 | 加「量尺寸时页面在滚动零点」守卫 + 变异 M9 | | 6 | 手机导航「既不是一条、也没滚动」,占 161px | `.sidebar` 只给了 `flex-direction:row`,可导航分组挂在 `