zyc 890cb9ab67
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m2s
chore(core/qa): function-audit toolchain + parity/audit reports + pixel-perfect skill
- qa/function-audit: playwright 行为审计工具(audit.mjs/verify-modals.mjs/pages.json)
  + 18 页审计产出(*.audit.md/json、summary、运行日志)
- qa/visual-parity: 调试/测量辅助脚本(_dbg*.mjs/_measure.mjs/_off.mjs)
- core/还原度核对报告.md: 18 页 pixelmatch 核对结果(含 vite 代理陈旧坑记录)
- core/还原与接口待办.md: 逐页还原度/真实数据/交互接入待办总表
- .claude/skills/pixel-perfect-react: 像素级还原 React 的 SKILL 文档
- frontend/public/_devlogin.html: 临时本地登录辅助页(可删)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 09:41:30 +08:00

12 KiB

name, description
name description
pixel-perfect-react 把 HTML 设计稿像素级精准还原成 React/TSX 组件。当任务涉及"还原页面 / 页面还原度 / HTML 转 React / 设计稿转组件 / exact 页面 React 化 / 精确到 px / 像素对齐 / visual parity / 还原度太粗糙 / 对齐设计稿",或要把 public/exact/*.html 转成 src/routes 下真 React 组件、或修一个已 React 化但还原不准的页面时使用。强制"逐字转写而非重新发挥"+ pixelmatch 像素 diff 闭环,把 diffPixels 逼到趋近 0。

像素级 HTML → React 还原

路径约定:本文路径相对 AirShelf 仓库根(即本 .claude 的上一级)。前端工程在 core/frontend/,设计 SSoT 在 电商AI平台/design.md,设计镜像在 core/frontend/public/exact/

核心原则:转写,不是重画(Transcribe, don't reinterpret)

React 文件 = 源 HTML 的忠实转写。只有「数据」和「事件」变,结构 / 类名 / 内联样式 / 文案逐字保留。

还原度变糙的 99% 根因:AI 把 HTML"读懂后重写"成自以为更干净 / 更语义化的结构和类名,于是 restraint.css 里靠这些类名和结构生效的精确样式全部失配——间距、字号、圆角、颜色就这么一点点漂走了。禁止重画,只许转写。


十条铁律(每条都对应一种"变糙"的根因)

  1. 先读设计规范 — 涉及任何样式,先 Read 电商AI平台/design.md(§0 协作铁律、§2 token、§8 Don't List)。这是 CLAUDE.md 的硬性要求。
  2. 类名逐字保留 — 源 HTML 每个 class 原样搬到 className禁止改名 / 合并 / 拆分 / 换成 Tailwind utility / 换成内联样式。
  3. DOM 结构 1:1 — 嵌套层级、兄弟顺序、看似多余的空 <div>、装饰 <span class="corner-tr">/corner-bl 全部保留。restraint.css 大量用后代选择器和 :nth-child,结构一动样式就错位
  4. 内联样式精确搬运style="width: 33%"style={{ width: "33%" }},数值一个字符都不改;kebab-case → camelCase。禁止"约等于"(padding 14 写成 16、宽 33% 写成 35% 都算错)。
  5. 文案 / Mono 装饰逐字// 05.14[ 200 OK ][ /v1 ]LIVE、占位文本全部原样保留,禁止改写 / 翻译 / 精简 / 补全。这些是品牌签名。
  6. 内联 SVG 属性级保留viewBox / path d / fill / stroke-width 逐字抄,禁止重画图标或换 lucide 顶替(除非源 HTML 本就用 lucide)。
  7. 加载同一套 CSS + 字体 — React 入口必须 import 同一份 restraint.css / styles.css / design-restraint.css;禁止在组件里用内联 <style> 或新 class 重定义 .btn .pill .input .modal .drawer 等共享类(design.md 铁律 #3)。要变体回 restraint.css 加。
  8. 只换数据,不动壳 — 把写死的示例数据替换成 props / state / .map(),周围 markup 一个标签都不动。循环渲染时,卡片 / 行内部的完整结构要原样保留。
  9. 核对"作用域 CSS 复刻"是否逐行忠实 — 当源页面有页面级内联 <style>(登录/向导这类),它往往被复刻进共享 CSS 的一个作用域块(如 .auth-exact-page .divider {...})。这份复刻本身也可能抄错:实战见过 .divider 被多加 height:1px; background,把 OR 分隔塌成一条线——你组件转写得再准也白搭。逐行 diff 作用域 CSS 块 vs 源 <style>,多一行少一行都要揪出来。 尤其当你"补回一个缺失元素"后样式不对,先怀疑这里,而不是怀疑组件。
  10. 警惕"旧全局 CSS 泄漏" — 同名类(.balance-banner .pay-row .recharge-card .pane .stage-script .video-thumb 等)在共享 styles.css 里常有一份旧的全局版本。你新写的 .page-x .foo 即使特异性更高,也只覆盖你显式声明的属性;你没声明的属性(如 margin-top/max-height/border-top)会回退到旧全局值,造成神秘高差、grid align-items:stretch 失效、元素被顶偏。症状:diff 图里某块以下全部"重影"(垂直错位累积)。定位:量 getBoundingClientRect().height 对比设计稿镜像,逐元素找多出来的 px;再 grep styles.css 找同名旧全局规则。根治:确认该旧全局类只此页用(grep -rl 组件),直接从 styles.css 删掉那段旧全局规则(你的 scoped 版已自洽);删不干净时在 scoped 规则里显式把泄漏属性清零。实战:pipeline 删掉 1245-1342 旧全局块,顺手把 stage-1 卡了很久的 pane-h 换行刀刃残差从 25237px→999px——旧全局 .stage-script/.video-thumb max-height 一直在暗中泄漏。

标准流程

1. 定位三件套

  • 设计源(SSoT):电商AI平台/<page>.html,或其镜像 core/frontend/public/exact/<page>.html
  • 共享样式:public/exact/assets/restraint.css + src/styles.css + src/design-restraint.css
  • 目标文件:src/routes/<page>.tsx(接主 SPA 的真组件)

2. 截设计稿基线(这是像素目标)

确保前端在跑(cd core/frontend && npm run dev),然后在目标视口打开设计稿截图:视口 1440x900deviceScaleFactor=1colorScheme: lightreducedMotion: reduce、等 document.fonts.ready。(下面第 5 步的 compare-page.mjs 已自动做这些。)

3. 逐字转写 HTML → JSX(对照铁律 2-6)

机械转换,别动脑"优化":

  • classclassName,forhtmlFor,空元素自闭合(<br><br />)
  • style="a: x; b: y"style={{ a: "x", b: "y" }},值不变,kebab → camel
  • <!-- 注释 -->{/* 注释 */}(可删,但别碰可见文案)
  • 内联 onclick → React handler,只接交互,不动结构
  • tabindextabIndex,stroke-widthstrokeWidth 等 SVG 属性驼峰化
  • CSS 自定义属性内联(如 mock-media 的 --mock-media-url)→ style={{ ["--mock-media-url"]: "url(...)" } as CSSProperties}

4. 接数据,保持壳不变

示例数据 → props / state / map唯一允许变的就是数据出处和事件,DOM 一律不动。

5. 像素 diff 闭环(仓库已有 pixelmatch 工具,别自己造)

cd core/qa/visual-parity
node compare-page.mjs \
  --source "http://127.0.0.1:5173/exact/<page>.html" \
  --target "http://127.0.0.1:5173/<真 React 路由>" \
  --name <page> --viewport 1440x900 --token <登录token>
  • output/<page>.report.jsondiffPixels / diffRatio
  • 打开 output/<page>.diff.png:红色高亮处就是 drift。逐个定位是哪个元素的间距 / 字号 / 字重 / 颜色 / 圆角错了 → 回组件改 → 再 diff。
  • diffPixels 每轮必须下降,目标趋近 0。 上轮没降就是改错了方向。

⚠️ 登录态陷阱:真 React 业务页(/dashboard /products 等)有登录门禁。compare-page.mjs --token <token> 会给 source + target 同时注入登录态。token 取法:curl -s -X POST http://127.0.0.1:8010/api/auth/login/ -H 'Content-Type: application/json' -d '{"username":"<演示账号>","password":"<密码>"}'(注意字段是 username 不是 email)。源页也要带同一 ?product_id=/?project_id= 等 query,两边同数据才是公平结构 diff。

6. 逐项自检(对照 design.md §8 Don't List)

  • 全场 8px 圆角(>12px 直接判错;pill / dot 999 例外)
  • 全场只有一个橙色 accent;hover 用 alpha 不换 hue
  • 无裸 hex,颜色全部用 design.md §2.1 的 token
  • 字重只有 400 / 500 / 600(700 仅 Ctrl K 徽标)
  • 用 inside-border(box-shadow: inset)而非真 border(hover 不抖)
  • Mono 装饰在位(// xx [ 200 OK ])
  • 只有主 CTA 有阴影,其他场景无阴影
  • 没动基础 token(--heat --background-base --border-faint)

实战补遗(2026-06 全站还原沉淀 · 比铁律更具体的坑)

这套页面的"真实感"不是单层数据——镜像视觉 = 三层叠加,要像素对齐必须三层都复刻。诊断时先用 Playwright 把镜像(带 ?id + token)的真实 DOM 抓出来(可见元素数 / 计数 / 每个 thumb 的 computed backgroundImage / 步进器 dot 类名),照着复刻,比盲读 HTML 快得多。

  1. 三层叠加:① public/exact/assets/api-bridge.jsrenderPageX 只 hydrate 少量真字段(且 setField 仅在值非空时覆盖,否则留 mock 默认值——React 必须照抄"真值‖mock默认"回退);② 页面自带内联 <script> 跑默认筛选/排序(如 product-detail 素材默认 status filter「通过」永远生效→只显示 pass 卡而非全部);③ shell.js 加载 assets/mock-media.js,对所有 .placeholder 按"上下文文本"正则塞 mock 图。
  2. mock-media 映射:.placeholder 命中即加 .has-mock-media 类(共享 CSS 里该类 background-image:var(--mock-media-url) + 把 .ph-frame 透明)。React 要给对应元素手动加 has-mock-media + 内联 --mock-media-url。映射:面膜/补水/玻尿酸→product-mask;平台/套图→scene-tabletop;办公→scene-office;床头/卧室→scene-bedroom;林夕/主播/女性→person-linxi;视频封面 mask→cover-mask-v3 / final→cover-mask-final。特例:#ed-canvas 这类非 .placeholder 元素,mock-media 只设内联 backgroundImage(size 默认 auto 平铺,别加 cover)。
  3. 运行时会改静态 HTML——别照抄静态:products 静态 result-meta 里有 grid/list 切换器,但镜像 api-bridge 运行时把它删了(只剩计数 span);照静态加上去会让该行变高、把整块网格下推几 px → 全红。以 Playwright 抓到的实时 DOM 为准,不是静态文件。
  4. 默认 tab/pane 对齐镜像而非"全部":library 镜像默认 active=人物(0 资产→空态),不是"全部";pipeline 默认可见 pane=脚本(stage1),但步进器 active=项目真实阶段(二者解耦)。
  5. 步进器(pipeline)严格复刻 activateStage:activeDot = 已导航?所看阶段:项目阶段;completed=max(项目阶段-1, activeDot-1);dot i===activeDot active / i<=completed done;line n<=completed done
  6. chip caret 别自己写宽高:给 .chip .caretwidth/margin 会让每个 chip 宽 +2px、多个 chip 累计错位文字重影。交给全局 restraint .chip 管。(projects 这一删 1494→182px)
  7. 作用域写法:用 CSS 嵌套 .xxx-page { ... },@keyframes 提到顶层。.app.xxx-page 全屏页用 & > main 子选择器。共享类(tabs/chip/toolbar/search-inline/result-meta/empty-filter/pill/btn/.stat/.prog)走全局 design-restraint(它是 restraint.css 的端口,在 styles.css 之后加载→覆盖旧全局),页面专属类才 scope。

反模式(出现任一即判"变糙",必须重做)

  • 把 HTML 结构"优化"成更少的 div / 更语义化的标签
  • 自创 class 名,或用 Tailwind / 内联样式替代 restraint 共享类
  • 内联样式凭感觉约等于(padding 14 → 16width 33% → 35%)
  • 删改 Mono 装饰 / 占位文案 / 标点
  • 重画 SVG 图标,或拿相近图标顶替
  • 在组件里 <style> 重定义共享类
  • 不截图不 diff,肉眼"差不多"就提交
  • 改了基础 token,影响全站
  • 照抄静态 HTML 的运行时被改部分(view-tog、默认 tab、注入的 mock 图)

验收门槛

diffPixels 逐轮下降且 diff.png 无业务性红块为通过。字体抗锯齿 + 共享 shell 残差(crumb mock 名 / nav 徽标 / bell 计数)等非业务差异允许残留,但必须在 report 旁单独记一行说明。视频真实生成不在还原阶段触发。

一页话总结

读 design.md → 截设计稿基线 → 逐字转写(类名/结构/内联样式/文案/SVG 全保真)→ 只换数据(三层叠加都复刻)→ pixelmatch diff 到趋近 0 → 对 §8 自检。 任何"我觉得这样更好"的改写,都是还原度变糙的源头。