发布于 更新于 约 8 分钟读完 · 3,189 字

本文目录 11 节
  1. 事实记录(不可修改区)
  2. 一句话总结
  3. 五条可复用 insight
  4. 1. 跨稿重复出现的才是规范,单稿出现的是那一页的特例 ⚠️首次
  5. 2. 「规范要求 X」不等于「实现缺 X」,下结论前必须回读实现 ⚠️首次
  6. 3. 给数据加派生字段前,先数清有几条取数路径 ⚠️首次
  7. 4. 宁可显示”没有”,也不要显示一个宽松解析出来的错值 ⚠️首次
  8. 5. 要「没有 padding」就别写 padding: 0 ⚠️首次
  9. 顺手教训
  10. 下次改进
  11. 关联文档

美工改稿全站落地 · 跨稿重复才是规范

入档:2026-07-26 来源:PB Arena(pb.tiaozhuxiansheng.com)接收美工「简洁低疲劳 V2」改稿并全站落地的会话,5 个提交(76a5b72 / b88027c / 6fdc155 / e839394 / 4d5ab9a),已部署上线并线上验证 状态:已上线;五条 insight 均为本次首次发现 ⚠️,尚未跨项目二次验证

事实记录(不可修改区)

  • 交付物:美工给的 31MB zip,内含 30 张 PNG(公共端桌面 10 + 移动 10 + 后台桌面 10)+ 1 份 UI视觉规范_简洁低疲劳V2.md
  • 规范要点(原文):暖白 #FAFAF8 / 炭黑 #171717 / 珊瑚红 #FF5A52 占屏 5%–8%;普通控件圆角 6–8px、弹窗与主要面板 10–14px;取消渐变、光晕、厚阴影、玻璃拟态;列表/排行榜/人员管理/操作流水统一行式结构;多个统计卡片合并为一条细线分隔的指标栏
  • 改动范围:全程只动 pb-arena-optimized/ 下 4 个文件(index.html / styles.css / app.js / api.js),后端零改动(git diff --stat f510fe5..HEAD -- pb-arena-server 为空)
  • 落地顺序:先 token 层全局改(1 个提交),再逐页对稿(4 个提交)
  • 客观事实(线上,不加解释):部署后 styles.css?v=20260726l 生效、824 条规则全部解析、--bg 为 #fafaf8、正文字体 Noto Sans SC、控制台无报错;7 场真实赛事全部走通新增的派生字段(startsAt: 1784989920000 → 22:32 / 7月25日 周六)
  • 本机限制:借来的 Windows 机器没装 Node,后端测试跑不了、本地起不了服务,全程靠 file:// 静态快照 + 演示数据验证
  • 未验证项(已如实交回作者):运营后台十个面板的新版式只量了计算值,没有登录后实际点过

一句话总结

设计稿是比规范文字更可靠的事实来源,但单张稿子只能证明”这一页长这样”——只有跨多张稿重复出现的处理,才够格当成全局规范去改代码。

五条可复用 insight

1. 跨稿重复出现的才是规范,单稿出现的是那一页的特例 ⚠️首次

规范文字说「减少卡片嵌套」,这句话落到代码上可以是任何幅度。真正让我敢把后台 .admin-card 的白底描边和 30px 内边距整个删掉的,是翻到第 6 张后台稿时发现十张里没有一张有卡片框——内容一律直接落在页面底色上、靠细线分组。这时它才从”某一页的画法”升格成”后台的规范”。

反面例子在同一次里:历史图库的桌面稿无边框、移动稿有边框。这个差异只在这一对稿子里出现,没有第二处佐证,于是按”响应式差异”照做(宽屏去框、窄屏收回成卡片),没有外推成”图库类一律无框”。

判据:一个处理跨 ≥3 张不同页面的稿子重复出现 → 当规范,可以全局改;只在 1–2 张里出现 → 当该页特例,就地实现,不要提取。 提早提取的代价是全局改错、还得逐页打补丁改回来。

2. 「规范要求 X」不等于「实现缺 X」,下结论前必须回读实现 ⚠️首次

我看到规范写「移动端长列表优先两列图库」「比赛房间用三段式进度轨」,就在交付报告里写成了”这两项还没做”。实际去查:.history-grid 在窄屏本来就是 repeat(2, 1fr),.phase-track 连编号圆圈带连接线也早就在——两项都已存在,差的只是配色和字重细节。

这个错误的机制很典型:读规范时脑子里默认”规范提到的都是待办”,而规范其实是对成品的完整描述,里面既有要改的也有已经对的。我用规范当了待办清单。

正确顺序是:先读实现 → 再对稿 → 最后才下”缺什么”的结论。 跳过第一步就会把”已有但不够好”误报成”没有”,报给作者的进度是虚的。这次是我在下一轮自己查代码时发现并当场纠正的,但如果没有下一轮,这个虚报就留在交付记录里了。

3. 给数据加派生字段前,先数清有几条取数路径 ⚠️首次

需求是把开赛时间拆成”大字时钟 + 日期说明”。我在 normalizeOfficialEvent() 里加了派生逻辑,本地四条演示数据全部正确,看起来收工了。

实际上 getOfficialEvents() 有两条分支:

const backendEvents = await tryBackend("/api/official-events?...");
if (backendEvents) {
  return backendEvents.map(...);   // ← 后端路径,故意不走 normalize
}
// ↓ 演示数据路径,走 normalize
return store.officialEvents.map(normalizeOfficialEvent)...

后端路径是有意绕开 normalize 的——因为 normalize 会用本地算法覆盖后端已经算好的 currentRoundTopic 等字段。于是只加在 normalize 里的结果是:本地演示全绿,生产环境根本拿不到新字段。而生产恰恰是这个需求唯一真正服务的场景。

定位方式:改一个派生函数前,先 grep 它被谁调用,然后逐个问”不调用它的分支,是不是也需要这个字段”。 修法不是把后端路径也塞进 normalize(会引入它刻意规避的副作用),而是抽一个只做派生的小函数,两条路径各贴各的。

这条与 ⚠️ 赛事状态机到点不切换_写入方不等于推进方_v1 的「写入方 ≠ 推进方」同族但形态不同:那篇是字段没人推进,这篇是派生逻辑只挂在一条取数路径上。共同点是”看起来齐了”的表象来自你正好在看的那条路径。

4. 宁可显示”没有”,也不要显示一个宽松解析出来的错值 ⚠️首次

要从开赛时间里摘出时钟。第一反应是 new Date(raw) 兜底——但演示数据里有 "06.21" 和 "05.18 - 05.25" 这种运营写的说明文本,V8 的宽松日期解析能把它们解析成一个日期,于是界面会渲染出一个煞有介事的 00:00。用户看到的是一个错的、但完全不像出错的值。

正解是显式白名单 + 逐级降级 + 兜底留空:

// 只认后端与建赛表单统一产出的 YYYY-MM-DD HH:mm
const ISO_MINUTE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})/;

取值优先级定成:后端时间戳 → 统一格式字符串 → 说明文本结尾的时钟 → 都不成立时返回空,界面显示「开赛时间待公布」。

判据:凡是要把自由文本变成用户可见的结构化值,兜底分支必须是”什么都不显示”,不能是”尽力猜一个”。 猜错的值不会报错、不会告警,只会安静地骗人。

5. 要「没有 padding」就别写 padding: 0 ⚠️首次

去卡片化时,.admin-card 原本是 padding: 30px; border: 1px solid; background: white。我改成 .admin-card { padding: 0 },同时给分栏容器加了 .admin-grid > * + * { padding-left: 44px; border-left: 1px solid }。

结果:竖分隔线出来了,44px 的内边距没有。两条选择器权重同为 (0,1,0),而 .admin-card 写在后面 —— 后置同权重胜出,padding: 0 精确地吃掉了 padding-left。

解法不是提权重(.admin-grid > .admin-card + .admin-card 之类会越写越脆),而是干脆删掉那条声明:要”没有 padding”,让它保持默认的 0 就行,别显式写。显式写等于占位,会挡住后面所有想给它加内边距的规则。

一般化:重置类声明(padding: 0 / margin: 0 / border: 0)写在通用类上时,等于宣告”这个属性归我管”;如果别的规则还要在这个属性上做文章,就别写它。

顺手教训

  • 版本号要覆盖”会一起变的那一组文件”,不只是被改的那个 —— ⚠️ 赛事状态机到点不切换_写入方不等于推进方_v1 已记过「改 CSS 必须同步改 ?v=」,本次是它的扩展面:这次连 app.js 里的 DOM 结构一起改了,而 app.js 压根没有版本号。只 bust CSS 的后果是老用户拿到新样式配旧结构——卡片样式已经删了,DOM 还在按旧结构渲染,比不更新更难看。给三个文件挂上同一个版本号才算完。二次验证并扩展。
  • 登录态 UI 无法行为验证时的退路,以及必须如实标注 —— 后台十个面板要登录才能看到,本机又没 Node 起不了服务。退路是用 JS 往容器里注入样例 DOM,再用 getComputedStyle 量栅格列数、边框宽度、计算色值,确认结构和配色对。但这只能证明规则算对了,不能证明真实数据下好看,交付时必须写成”这是量出来的,不是看到的”,并把需要人工点一遍的部分明确交回去。同族见 ⚠️ Claude预览环境不派发滚动事件_滚动类功能无法行为验证_v1。
  • 无头 Chrome 截图是视口截图,不会自动截全页 —— 做进度图 PNG 时,--screenshot 只截 --window-size 那么大,内容更长就被切。解法是两趟:先在页面里塞一行 document.title = "H" + document.documentElement.scrollHeight,用 --dump-dom 把高度读出来,再按精确高度截第二趟。比截一张超高图再裁干净。
  • 别默认本机的图像库能用 —— 原计划用 Pillow 裁掉底部空白,ImageChops.difference 处理 2160×3462 直接 access violation 崩掉(而 Image.open + load 是好的)。与其排查这台机器的 Pillow,不如换成上面那条”让页面自报高度”的路子——绕开比修好快,尤其在借来的机器上。

下次改进

  • 接到成套设计稿,先整体翻一遍数出”哪些处理跨多张重复”,再动第一行代码;别看一张改一张。
  • 交付进度前,凡是要写”某项还没做”,先回代码里搜一遍确认它真的不存在。
  • 改派生函数前先 grep 调用方,把每条取数路径列出来再动手。
  • 改前端结构时,把所有会一起变的静态资源版本号当成一个原子操作一起改。

关联文档

类型/平台工程