平台工程

美工改稿 V4 四屏落地 · 禁止项是待办不是护栏

入档:2026-08-05 来源:PB Arena(pb.tiaozhuxiansheng.com)接收美工「浅色对战 V4」改稿并落地四屏,2 个提交(9f4f766 四屏改稿 / b734374 更新报告),已部署上线并线上验证 状态:已上线;insight 1/2/3/5 为首次发现 ⚠️,insight 4 是 美工改稿全站落地_跨稿重复才是规范_v1 insight 5 的二次验证并扩展形态

事实记录(不可修改区)

一句话总结

改稿落地里每一样「看起来已经对了」的东西都得亲手核一遍——包名不等于范围、规范的禁止项里藏着待办、代码里写了的 CSS 不等于生效过、稿子上画着的组件不等于它真的会动。这一轮四处全中。

五条可复用 insight

1. 成套交付包的范围以文件时间戳为准,不以包名为准 ⚠️首次

包名是「PB-Arena-全套UI改稿_浅色对战_V4」,30 张 PNG,读起来就是”全站重做”。ls -l 按 mtime 一分组才发现:只有 8 张是 2026-07-31 的新稿,另外 22 张 mtime 停在 2026-07-24,是上一轮 V2 原样重发

如果按包名理解,后果不是”多做一点”,而是把 22 张已经落地过的旧稿再逐张对一遍——而且因为它们确实和当前代码一致,对完还会得出”没什么要改的”这种毫无信息量的结论,白烧一轮。

同一份包里还有个更隐蔽的:两份规范 md,其中 UI视觉规范_简洁低疲劳V2.md 是上一轮那份一字未改的重发。如果不核,很容易把它当成”V4 也重新定义了全站基线”。

判据:拿到成套交付物,第一个动作是按 mtime / 内容哈希分组算出「这一轮真正变了什么」,这一步排在读规范之前。包名、目录名、交付说明都是当事人的描述,不是事实来源。

一般化:外包交付、第三方 SDK 更新包、数据集「全量重发」都是同一形态——声称的范围和实际的 diff 之间没有任何强制约束

2. 规范的否定条款是待办,肯定条款才是待核实 ⚠️首次

这条是 美工改稿全站落地_跨稿重复才是规范_v1 insight 2 的镜像面,两条必须配着用。

那一篇的教训是:规范写「移动端长列表优先两列图库」,我就当成待办报了”还没做”,实际早就有了——肯定条款不能当待办清单,要先回读实现

这一轮反过来。V4 禁止项写着「科技网格、瞄准镜、扫描线、霓虹、发光」,我第一反应读成”以后别加这些”。实际去搜,匹配大厅里就有:

.radar::before { ... animation: scan 3s linear infinite; }
@keyframes scan { to { transform: rotate(360deg); } }

一条绕圈的扫描线,.radar 整块加上三个同心圆环,精确命中禁止项。它不是”以后别加”,是”现在就得下掉”。

判据:读规范时把肯定句和否定句拆开处理——

条款形态正确动作跳过的后果
肯定句「要有 X」先回读实现,确认到底缺不缺把已有功能误报成缺失,交付进度是虚的
否定句「不要 X」去实现里搜有没有违例违例原样留在线上,而你以为自己遵守了规范

两种都跳过,就等于没读规范——只是抄了一遍色值。

3. 按下标匹配的全局选择器,在组件被复用第二处时静默失效 ⚠️首次

比赛房间底部那条「01 出图 — 02 上传 — 03 投票」时间轴,从来没有高亮过当前阶段。定位到:

$$(".phase-step").forEach((step, index) => {
  step.classList.toggle("active", index === currentIndex);   // currentIndex ∈ {0,1,2}
});

.phase-step 全页有 6 个:官方直播面板 3 个 + 比赛房间 3 个。而官方直播面板在文档里排前面,于是下标 0/1/2 落到了那一组(此时它是 hidden),比赛房间自己那三个拿到的是 3/4/5,永远匹配不上

值得记的不是这个 bug 本身,而是它为什么能活这么久:

是 V4 把它提为核心元素(规范原文:「创作 / 上传 / 投票状态沿一条连续时间轴表达」)才暴露出来。

判据:凡是 querySelectorAll(...) + 数组下标做匹配的代码,都隐含一个前提——「全页只有这一组」。这个前提在组件被复用第二次时失效,且不产生任何可观测信号。写这类代码时作用域必须锚到容器 id($$("#phaseTrack .phase-step")),不能只锚类名。

附带一层,同样值钱:改版是既有 bug 的显影剂。 一个原本不显眼的元素被提为主视觉时,先假设它从来没被真正验证过——它此前不显眼,正意味着没人盯着它看过。

4. 同权重后置胜出,让「写了」和「生效过」之间没有任何信号(二次验证 + 新形态)

.event-detail-dialog 写的是 width: min(620px, calc(100vw - 40px)),而基础样式 .dialog { width: min(460px, 100%) } 在文件后面,权重同为 (0,1,0) → 后置胜出,弹窗实际一直是 460px

上一篇 insight 5 已经记过这个机制(padding: 0 吃掉后续的 padding-left),但那次是我刚写的代码当场没生效,改完就发现了。这次是新形态:

判据(扩展):组件专用规则必须比基础规则更具体(.dialog.event-detail-dialog),不能靠「写在前面」。同权重靠源码顺序决胜意味着——代码里写了一个值 ≠ 这个值生效过,而且这种失效不产生任何可观测信号,只能靠对着设计稿量。

推论:基础组件类(.dialog / .card / .btn)上任何带具体数值的声明,都是在给所有变体设默认值兼天花板。 要么它写得足够弱(让变体自然覆盖),要么变体必须提权;指望源码顺序是最脆的一种。

5. Windows 上 headless Chrome 的窄屏截图会被最小窗宽夹住,截出假溢出 ⚠️首次

移动端校对时用 chrome --headless=new --window-size=430,2700 --screenshot,截出来的图到处横向溢出:卡片右边被切、1,289 ON 少半截、阶段轴第三格不见了。我据此判断”移动端布局崩了”,改了一轮 CSS。

真相是:Windows 会把浏览器窗口宽度夹到约 500px,页面按 ~500px 布局,截图再裁到 430px 输出。页面本身 scrollWidth === 430,从来没有溢出过

解法是把被测页面套进 iframe,让 iframe 而不是窗口来定宽:

<iframe id="f" src="/target.html" style="width:430px;height:2700px;border:0"></iframe>
<script>
  // 在 iframe 内直接量,而不是看截图
  var d = f.contentDocument, vw = d.documentElement.clientWidth;
  d.querySelectorAll("*").forEach(el => {
    var r = el.getBoundingClientRect();
    if (r.right > vw + 1 && r.width > 0) bad.push(el.className);
  });
  out.textContent = "scrollW=" + d.documentElement.scrollWidth + "\n" + (bad.join("\n") || "NO OVERFLOW");
</script>

宿主窗口开 520+(避开夹取),iframe 给 430 —— 媒体查询在 iframe 里按 430 求值,截出来的就是真的。

判据:响应式验证不能信「我给浏览器传了多宽」,要信「页面自己报了多宽」。全站文字截断体检_检测工具本身要先被证伪_v1 同族:那篇是判据常数拍错把正常元素判成缺陷,这篇是承载判据的容器尺寸就是错的——工具链的每一环都要先被证伪。

这一条已作为形态五补进 Windows下编码与DPI的所见非真相

顺手教训

下次改进

关联文档

类型/平台工程