feat(h5): apply second customer UI feedback and homepage refinements

This commit is contained in:
xsl
2026-10-12 14:35:39 +08:00
parent abb1c4c1de
commit fe9623a1d7
16 changed files with 1963 additions and 1082 deletions
@@ -0,0 +1,65 @@
# 12 日第二批:明确项修改
依据用户提供的 `12 日第二批.docx`(正文与 6 张截图)、随后补充的首页高清批注图及最新 `UI外发版10.09.psd`。原第 3 张首页截图仅 73×160 像素,未猜测;收到高清图后补齐下面两项。
## 已修改
### 首页高清批注
- 将三行口号中的中文「以我为始 自有表现」独立为 700 字重,英文继续使用 400 字重,保留三行断句与位置。PSD 对应中文 `0/4` 为 Demibold、英文 `0/5` 为 Regular,确实有粗细区别。
- 放大底部「肌肤状态检测」到 24 CSS px、700 字重,与原始 Logo 约 26 CSS px 的可见高度对齐;Logo 保持原图宽高比。Logo 与文字均固定 CSS px,不再因屏宽缩放 Logo 而让二者比例失衡。
- 首页继续使用系统字体表达字重区别,不提前下载约 5 MB 的完整 MiSans 字体;不改动或子集化字体。系统字体字形的细微差别仍依手机系统而定。
- 在 320、375、390、414、480 五种屏宽验证:品牌行不溢出,口号仍三行,登录状态不遮挡口号,首屏不新增字体或检测引擎请求。
### 拍照展示
- 去掉人脸外轮廓的白色描边,以及原来无脸呼吸/合格变绿的轮廓描边;保留明暗镂空、五官虚线、扫描提示和「保持不动」反馈。
- 视频由 85% 缩小预览恢复为铺满视口,避免外侧矩形取景边界;镜像仍保留。屏幕人脸中心坐标的缩放系数同步为 1,距离、姿态、光线判定阈值不变。
- 对 PSD 导出的折线轮廓做最大 2 CSS px 的局部圆角化,使用二次曲线、不过冲、不改变轮廓坐标系。镂空与扫描裁切使用同一份平滑轮廓。原始 `CameraGeometry` 不修改。
- 原始视频抓帧、上传图片、报告图片处理与评分/推荐逻辑不变。
### 准备捕捉提醒
原来仅在补光抓拍瞬间请求 30 ms 震动,改为首次满足检测条件、开始「保持不动」时请求 `[70, 40, 70]` 两次短震动。连续合格 450 ms 后仍按原流程自动抓拍,不增加倒计时。抖动重试时最多每两秒请求一次,每步重置。
实时相机的手动快门模式仍在点击抓拍时请求提醒;自动模式不会在抓拍瞬间再重复震动。
API 不存在、返回 false 或抛出异常时安全跳过,不能因震动失败卡住拍摄。此处没有接入未经验证的微信私有接口,也不声称所有 iPhone/微信环境均可震动。振动接口存在浏览器、用户激活和系统设置限制,实效必须在手机微信上测试;参见 [MDN Navigator.vibrate](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/vibrate)。保留原有文字、语音和补光反馈。
### 字重
直接读取 PSD 字体元数据,明确区分而非将所有文字一起加粗:
| 位置 | PSD 图层 | 原始字体 |
| --- | --- | --- |
| 协议弹窗标题 | `1/3/0` | MiSans-Demibold |
| 拍照引导弹窗标题 | `4/7` | MiSans-Demibold |
| 正/右/左拍照步骤 | `5/3`、`6/2`、`7/2` | MiSans-Demibold |
| 综合评分、肌龄 | `8/12/2`、`8/12/3` | MiSans-Demibold |
| 三维度数字 | `8/12/7/2`、`8/12/7/9`、`8/12/7/13` | MiSans-Demibold |
| 报告标题、三维度大标题 | `8/13`、`8/9/17`、`8/10/20`、`8/11/17` | MiSans-Semibold |
| 护理建议、产品区大标题 | `8/2/0/1/0`、`8/1/3` | MiSans-Semibold |
项目原来只有 Regular、Semibold,数字和部分提示还使用 Regular 字重。现从[小米官方字体包](https://hyperos.mi.com/font/zh/download/)增加完整的原始 Demibold 文件(4994364 字节),未转换、未子集化、未修改字形;SHA-256 为 `7afe3737efaf6a137db0ef4857fbe4033a8802756485ebc7f008e96a4bdedcf2`。保留字体归属声明。
Demibold 使用独立 CSS 字体族,仅应用于本轮明确需要它的位置,避免影响原有正文、Tab 和其他 Semibold 元素。报告大标题按原始 Semibold 字形显示,不能承诺所有原本已经正确加载 Semibold 的标题都会额外变粗。
字体样式注册提前到打开协议/拍照引导弹窗时,解决首次协议弹窗尚未注册项目字体的问题。继续 `font-display: swap`,不阻塞交互;登录、首页和账号恢复仍使用系统字体,不在首屏请求完整字体或模型。新增完整字体约 5 MB,在需要的页面下载并缓存;弱网下载完成前仍可能短暂显示回退字体。固定字号与产品两行断句保持不变。
## 验证与状态
```bash
node h5/test/customer-feedback-second.js
H5_ARTIFACT_DIR=/tmp/juzi-feedback-second-final-20261012 node h5/test/customer-feedback-second.mjs
node h5/test/customer-feedback.js
H5_ARTIFACT_DIR=/tmp/juzi-feedback-second-regression node h5/test/customer-feedback.mjs
node h5/test/recommendation.js
node h5/test/analyze-timeout.js
node h5/test/login-performance.mjs
node h5/test/e2e-browser.mjs
H5_ARTIFACT_DIR=/tmp/juzi-feedback-second-ui-20261012 node h5/test/ui-20261009.mjs
```
专项测试覆盖三种轮廓的有界曲线和无原始数据修改、异常输入回退、震动缺失/拒绝/抛错容错、抓拍前触发及频率限制、原有 450 ms 自动抓拍时机;浏览器核验实际渲染字体(不仅是 CSS 数值)、五种屏宽的全屏预览与固定字号、三种状态均无轮廓描边、五官引导保留、登录首屏无大字体/模型请求。专项浏览器测试拦截会话并使用本地合成视频流,不写入后端账号、报告,也不调用收费检测。
已完成本地开发及验证,首页高清图的两处批注也已补齐;本轮尚未提交 Git、推送或部署公网。真实手机震动实效仍待确认。
+20 -26
View File
@@ -3,25 +3,25 @@
"1": "线上闭环测试(不含真实微信授权)",
"2": "app.js",
"3": "skin-score.js",
"4": "k",
"5": "f",
"4": "customer-feedback-second.js",
"5": "n",
"6": ".runAndExtendWithFaceDetection",
"7": ".run",
"7": "k",
"8": "admin.js",
"9": "__awaiter",
"10": "SsdMobilenetv1",
"10": "mn",
"11": "lib/accounts.js",
"12": "server.js",
"13": "files",
"14": "AgeGenderNet",
"15": "extractFaceTensors",
"14": ".draw",
"15": "extractFaces",
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
"17": "extractFaces",
"17": "FaceLandmarks68",
"18": "1'me 在线肌肤测试 H5",
"19": "head-pose.js",
"20": "build_fixtures.py",
"21": "disposeUnusedWeightTensors",
"22": ".loadFromDisk",
"21": "AgeGenderNet",
"22": "extractAllFacesAndComputeResults",
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
"25": "mobile-select.iife.min.js",
@@ -35,11 +35,11 @@
"33": "build-fixture.py",
"34": "smoke-test.js",
"35": "customer-feedback.js",
"36": "ao",
"36": "actualFont",
"37": "UI 外发版 10.09 图片资源",
"38": "FaceLandmark68Net",
"39": "UI交互界面调整",
"40": ".predictAgeAndGender",
"40": "extractFaceTensors",
"41": "NeuralNetwork",
"42": "export_psd_assets.py",
"43": "normalize",
@@ -47,7 +47,7 @@
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
"47": "recommendation.js",
"48": "report-persistence.mjs",
"48": "已修改",
"49": "mobile-select/README.md",
"50": "call_skinanalyze_3view.sh",
"51": "第二轮2反馈.md",
@@ -65,37 +65,31 @@
"63": "e2e-browser.mjs",
"64": "account-client.js",
"65": "微信登录接入与公网部署说明",
"66": "TinyYolov2Base",
"66": "report-persistence.mjs",
"67": "ui-20261009.mjs",
"68": "analyze-timeout.js",
"69": "misans/README.md",
"70": ".draw",
"71": "initialize",
"70": ".forwardInput",
"71": ".toString",
"72": "build_report_copy.py",
"73": "camera-geometry.js",
"74": "report-copy.js",
"75": "prepare_h5_assets.py",
"76": ".detect",
"77": ".runAndExtendWithFaceDetections",
"78": "n",
"78": "t",
"79": "登录反馈与加载优化(2026-10-12)",
"80": "线上微信登录部署记录",
"81": "wechat-production.mjs",
"82": "test/accounts.js",
"82": "wechat-browser.mjs",
"83": "10.09 新版 H5 实施与验收",
"84": "微信登录联调:2026-10-12",
"85": "FaceLandmarks",
"86": "isValidNumber",
"85": ".predictAgeAndGender",
"86": "LabeledFaceDescriptors",
"87": "产品推荐规则:肤龄基准与边界调整",
"88": "gn",
"89": "傅艳NEW与项思炼的会话 2026年10月12日.md",
"90": "customer-feedback.mjs",
"91": "FaceDetection",
"92": "客户反馈修改(2026-10-12)",
"93": "TinyFaceFeatureExtractor",
"94": "DetectSingleFaceLandmarksTask",
"95": "PredictAllFaceExpressionsTask",
"97": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
"98": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
"99": "PredictSingleFaceExpressionsTask"
"98": "PredictSingleFaceExpressionsWithFaceAlignmentTask"
}
+1 -1
View File
@@ -1 +1 @@
{"0": "04a103a1199997a0", "1": "cb6ea50bd4e4a5ca", "2": "b663df3416204401", "3": "1f737ca8edb978f2", "4": "377a4f12232bbcdc", "5": "f074a081a584c171", "6": "5a9014c4a7f87699", "7": "6cda159cdef28f96", "8": "1ecf47531e1d507b", "9": "a16154b22fd8e2b3", "10": "d0c91dec45eac5ad", "11": "ae1e487145595eb9", "12": "53594b653ad48743", "13": "7e3b558a587a4ebe", "14": "d0f5c6e307b85cb1", "15": "0e3ea8dc60f6e10f", "16": "95add6a975c0cb72", "17": "0d185c03b2040000", "18": "38bb0b185ceed116", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "5d94cf57d5d1a6fb", "22": "be2f6d3dcfc9aabd", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "83b0fd3b14363e4d", "27": "eab427ce9a5219e9", "28": "b992b3cc0a0fa9be", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "c4fa292c3dfb0d0e", "36": "a3859fc25c864a81", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "a2b13160223b5a98", "41": "d96493a5f110bd4c", "42": "ffd960136fde00ad", "43": "57726d26805c136b", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "e042d7fe7351bcb9", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "da0f3cd0568e4331", "56": "8c9a3fc5edff7e44", "57": "f2a40b5f1b29f669", "58": "7471c1ff7f7420f0", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "8394d2bc925a408a", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "74ae017de0ffd36b", "66": "6b96cb56e55af88c", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "bf75bfc99d4f9e34", "71": "eb3733e409dc7411", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "c7c04904801db5b4", "77": "429cd62f9137881c", "78": "1ce63692525fa555", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "4bc370be8afd7db1", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "4c7d02c634649745", "86": "1c27e5f448dc4f9c", "87": "e5fa7717989eda80", "88": "a3e6d368559999ec", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "287d60115d9acda8", "92": "9509aca4f2e8a012", "93": "3befc14a32607829", "94": "23f1d406f6c779ae", "95": "e800d832c255899a", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "cb5ac3401283c378"}
{"0": "00c97d478a8f90eb", "1": "cb6ea50bd4e4a5ca", "2": "00705ebb76871f0f", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "93c69860c6f6563c", "6": "5a9014c4a7f87699", "7": "188dd5e33354e35d", "8": "1ecf47531e1d507b", "9": "64a6935321c48bb6", "10": "9ac0935bc0d2c1fd", "11": "ed0d13cfa8571d24", "12": "53594b653ad48743", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "72d3264e772df1cb", "16": "95add6a975c0cb72", "17": "15606ce5d8eec23a", "18": "38bb0b185ceed116", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "224bdcd6528fddbf", "22": "ebf7b8a48916906e", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "83b0fd3b14363e4d", "27": "eab427ce9a5219e9", "28": "9c6f14efe476acf4", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "c4fa292c3dfb0d0e", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "c6e2efd58425d13d", "41": "d96493a5f110bd4c", "42": "ffd960136fde00ad", "43": "5b04375ec4594cb1", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "1e2c13775a52fdea", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "54d7329fed675057", "56": "8c9a3fc5edff7e44", "57": "f2a40b5f1b29f669", "58": "07d28ed3afe8ac95", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "2d008ec4a8df7849", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "74ae017de0ffd36b", "66": "e042d7fe7351bcb9", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "880a74c6d255e5cc", "71": "cd6f168abba94fea", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "68250611c66f576f", "77": "429cd62f9137881c", "78": "f4aae1bece83f9e0", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "2b037735e1f5b0f6", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "a2b13160223b5a98", "86": "f8b38aeaaab11ec4", "87": "e5fa7717989eda80", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "92": "fced244d93677d90", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54"}
+97 -116
View File
@@ -1,16 +1,16 @@
# Graph Report - juzi (2026-10-12)
## Corpus Check
- 74 files · ~4,387,581 words
- 77 files · ~4,389,710 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 1255 nodes · 2651 edges · 99 communities (78 shown, 21 thin omitted)
- 1280 nodes · 2677 edges · 93 communities (76 shown, 17 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 181 edges (avg confidence: 0.53)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `3193a4b6`
- Built from commit: `abb1c4c1`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -19,25 +19,25 @@
- 线上闭环测试(不含真实微信授权)
- app.js
- skin-score.js
- k
- f
- customer-feedback-second.js
- n
- .runAndExtendWithFaceDetection
- .run
- k
- admin.js
- __awaiter
- SsdMobilenetv1
- mn
- lib/accounts.js
- server.js
- files
- AgeGenderNet
- extractFaceTensors
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
- .draw
- extractFaces
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
- FaceLandmarks68
- 1'me 在线肌肤测试 H5
- head-pose.js
- build_fixtures.py
- disposeUnusedWeightTensors
- .loadFromDisk
- AgeGenderNet
- extractAllFacesAndComputeResults
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
- mobile-select.iife.min.js
@@ -50,11 +50,10 @@
- FaceMatcher
- build-fixture.py
- customer-feedback.js
- ao
- UI 外发版 10.09 图片资源
- FaceLandmark68Net
- UI交互界面调整
- .predictAgeAndGender
- extractFaceTensors
- NeuralNetwork
- export_psd_assets.py
- normalize
@@ -62,7 +61,7 @@
- PredictAllAgeAndGenderWithFaceAlignmentTask
- PredictAllFaceExpressionsWithFaceAlignmentTask
- recommendation.js
- report-persistence.mjs
- 已修改
- mobile-select/README.md
- call_skinanalyze_3view.sh
- 护理建议(全量文本,按需求摘抄)
@@ -77,35 +76,29 @@
- e2e-browser.mjs
- account-client.js
- 微信登录接入与公网部署说明
- TinyYolov2Base
- report-persistence.mjs
- ui-20261009.mjs
- analyze-timeout.js
- misans/README.md
- .draw
- initialize
- .forwardInput
- .toString
- build_report_copy.py
- .detect
- .runAndExtendWithFaceDetections
- n
- t
- 登录反馈与加载优化(2026-10-12)
- 线上微信登录部署记录
- wechat-production.mjs
- test/accounts.js
- wechat-browser.mjs
- 10.09 新版 H5 实施与验收
- 微信登录联调:2026-10-12
- FaceLandmarks
- isValidNumber
- .predictAgeAndGender
- LabeledFaceDescriptors
- 产品推荐规则:肤龄基准与边界调整
- gn
- 傅艳NEW与项思炼的会话 2026年10月12日.md
- FaceDetection
- 客户反馈修改(2026-10-12)
- TinyFaceFeatureExtractor
- DetectSingleFaceLandmarksTask
- PredictAllFaceExpressionsTask
- PredictSingleAgeAndGenderWithFaceAlignmentTask
- PredictSingleFaceExpressionsWithFaceAlignmentTask
- PredictSingleFaceExpressionsTask
## God Nodes (most connected - your core abstractions)
1. `__awaiter()` - 59 edges
@@ -134,11 +127,11 @@
## Import Cycles
- None detected.
## Communities (99 total, 21 thin omitted)
## Communities (93 total, 17 thin omitted)
### Community 0 - "face-api.js"
Cohesion: 0.03
Nodes (20): cn(), dr(), en(), ff(), ge(), ia(), na(), nt() (+12 more)
Cohesion: 0.02
Nodes (43): ce(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), Dimensions(), dr() (+35 more)
### Community 1 - "线上闭环测试(不含真实微信授权)"
Cohesion: 0.25
@@ -146,43 +139,43 @@ Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测
### Community 2 - "app.js"
Cohesion: 0.05
Nodes (80): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), checkStill(), closeLoginSuccess() (+72 more)
Nodes (81): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), captureHaptic(), checkStill() (+73 more)
### Community 3 - "skin-score.js"
Cohesion: 0.12
Nodes (45): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+37 more)
### Community 4 - "k"
Cohesion: 0.13
Nodes (19): b(), ba(), ca(), cl(), da(), fa(), k(), ll() (+11 more)
### Community 4 - "customer-feedback-second.js"
Cohesion: 0.17
Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more)
### Community 5 - "f"
Cohesion: 0.21
Nodes (23): a(), cf(), d(), drawContour(), dt(), ea(), f(), g() (+15 more)
### Community 5 - "n"
Cohesion: 0.17
Nodes (39): a(), ae(), be(), bf(), cf(), d(), de(), drawContour() (+31 more)
### Community 6 - ".runAndExtendWithFaceDetection"
Cohesion: 0.33
Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectSingleFaceTask()
### Community 7 - ".run"
Cohesion: 0.22
Nodes (3): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor()
### Community 7 - "k"
Cohesion: 0.18
Nodes (11): b(), gn(), k(), kn(), ne(), tt(), wo(), Yt() (+3 more)
### Community 8 - "admin.js"
Cohesion: 0.21
Nodes (28): ageText(), api(), bind(), closeDetail(), collectFilters(), dimsHtml(), doRetry(), dSec() (+20 more)
### Community 9 - "__awaiter"
Cohesion: 0.15
Nodes (13): __awaiter(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow() (+5 more)
Cohesion: 0.14
Nodes (13): __awaiter(), bufferToImage(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator() (+5 more)
### Community 10 - "SsdMobilenetv1"
### Community 10 - "mn"
Cohesion: 0.17
Nodes (10): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+2 more)
Nodes (11): Lu(), mn(), Point(), Pu(), Uu(), vn(), Vu(), wl() (+3 more)
### Community 11 - "lib/accounts.js"
Cohesion: 0.08
Nodes (31): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+23 more)
Cohesion: 0.07
Nodes (34): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+26 more)
### Community 12 - "server.js"
Cohesion: 0.08
@@ -192,21 +185,17 @@ Nodes (51): accounts, appendRecord(), callFacepp(), CRED, crypto, csvCell(), DAT
Cohesion: 0.12
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
### Community 14 - "AgeGenderNet"
Cohesion: 0.12
Nodes (5): AgeGenderNet(), extractParams$1(), FaceExpressionNet(), FaceLandmark68TinyNet(), FaceProcessor()
### Community 15 - "extractFaceTensors"
Cohesion: 0.25
Nodes (3): DetectAllFaceLandmarksTask(), extractFaceTensors(), isTensor3D()
### Community 15 - "extractFaces"
Cohesion: 0.21
Nodes (17): awaitMediaLoaded(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv(), getMediaDimensions() (+9 more)
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
Cohesion: 0.15
Nodes (12): 1. 基准与尺寸换算, 2. 色彩规范, 3. 字体规范, 4. 组件切图(assets/svg/), 5. 图片素材(assets/images/,共 15 张), 6. 页面清单(design/ 下有整屏 PNG 预览), 7. 目录结构, 8. 还原建议 (+4 more)
### Community 17 - "extractFaces"
Cohesion: 0.14
Nodes (19): awaitMediaLoaded(), Box(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow() (+11 more)
### Community 17 - "FaceLandmarks68"
Cohesion: 0.19
Nodes (3): FaceLandmarks5(), FaceLandmarks68(), getCenterPoint()
### Community 18 - "1'me 在线肌肤测试 H5"
Cohesion: 0.13
@@ -220,13 +209,13 @@ Nodes (8): assertPoints(), check(), clamp(), dist(), estimate(), makeFrontal(),
Cohesion: 0.40
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
### Community 21 - "disposeUnusedWeightTensors"
Cohesion: 0.09
Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more)
### Community 21 - "AgeGenderNet"
Cohesion: 0.05
Nodes (28): AgeGenderNet(), disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1() (+20 more)
### Community 22 - ".loadFromDisk"
Cohesion: 0.20
Nodes (5): ComposableTask(), getModelUris(), Gf(), Kf(), TinyFaceDetector()
### Community 22 - "extractAllFacesAndComputeResults"
Cohesion: 0.12
Nodes (8): ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllFaceExpressionsTask(), PredictSingleFaceExpressionsTask()
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
Cohesion: 0.20
@@ -249,8 +238,8 @@ Cohesion: 0.30
Nodes (11): ndarray, Path, gen_countdown(), main(), normalize_clip(), 倒计时单独按字生成再拼接,避免「三、二、一」整句被读成播报腔、与提示音气质漂移。 与提示音共用同一 speaker + instruct;句间留约…, 裁掉过长首尾静音,保留 ≤150ms 边沿,避免提示显迟钝。, 响度归一 −16 LUFS + 转 MP3 48k CBR mono 22050Hz。 (+3 more)
### Community 28 - "C"
Cohesion: 0.14
Nodes (22): bf(), Bo(), C(), cd(), co(), eo(), fo(), Ft() (+14 more)
Cohesion: 0.10
Nodes (22): ao(), Bo(), C(), cd(), co(), Do(), eo(), fo() (+14 more)
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
Cohesion: 0.29
@@ -268,10 +257,6 @@ Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height}
Cohesion: 0.11
Nodes (17): alternate, assert, crop, details, excellent, fs, model, modelBefore (+9 more)
### Community 36 - "ao"
Cohesion: 0.40
Nodes (5): ao(), Do(), io(), so(), To()
### Community 37 - "UI 外发版 10.09 图片资源"
Cohesion: 0.40
Nodes (4): UI 外发版 10.09 图片资源, 前端素材对应, 文件位置, 重新导出
@@ -280,17 +265,17 @@ Nodes (4): UI 外发版 10.09 图片资源, 前端素材对应, 文件位置,
Cohesion: 0.40
Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设计**
### Community 40 - ".predictAgeAndGender"
Cohesion: 0.28
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
### Community 40 - "extractFaceTensors"
Cohesion: 0.08
Nodes (10): bgrToRgbTensor(), Box(), DetectAllFaceLandmarksTask(), DetectSingleFaceLandmarksTask(), extendWithFaceLandmarks(), extractFaceTensors(), FaceLandmarks(), isTensor3D() (+2 more)
### Community 42 - "export_psd_assets.py"
Cohesion: 0.70
Nodes (4): bounds(), layer_at(), main(), save_image()
### Community 43 - "normalize"
Cohesion: 0.13
Nodes (15): conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), denseBlock3(), denseBlock4(), depthwiseSeparableConv() (+7 more)
Cohesion: 0.07
Nodes (27): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+19 more)
### Community 44 - "start-local.sh"
Cohesion: 0.50
@@ -300,9 +285,9 @@ Nodes (3): HOST, PORT, start-local.sh script
Cohesion: 0.38
Nodes (6): assert, check(), missing, raw, SS, UI
### Community 48 - "report-persistence.mjs"
Cohesion: 0.29
Nodes (6): child, dataRoot, fx, h5, ReportUI, require
### Community 48 - "已修改"
Cohesion: 0.25
Nodes (7): 12 日第二批:明确项修改, 准备捕捉提醒, 字重, 已修改, 拍照展示, 首页高清批注, 验证与状态
### Community 53 - "护理建议(全量文本,按需求摘抄)"
Cohesion: 0.10
@@ -313,8 +298,8 @@ Cohesion: 0.11
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
### Community 55 - "i"
Cohesion: 0.12
Nodes (19): bn(), dn(), ee(), Ef(), et(), fn(), i(), je() (+11 more)
Cohesion: 0.08
Nodes (31): ba(), bn(), ca(), cl(), da(), dn(), ee(), et() (+23 more)
### Community 56 - "report-ui.js"
Cohesion: 0.50
@@ -325,12 +310,12 @@ Cohesion: 0.60
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
### Community 58 - ".dispose"
Cohesion: 0.18
Nodes (12): extractBoundingBoxes(), FaceRecognitionNet(), getSizesForScale(), nonMaxSuppression(), normalize$1(), pyramidDown(), rescaleAndNormalize(), scale() (+4 more)
Cohesion: 0.22
Nodes (9): FaceRecognitionNet(), getSizesForScale(), nonMaxSuppression(), normalize$1(), rescaleAndNormalize(), scale(), stage1(), stage2() (+1 more)
### Community 61 - ".extractWeights"
Cohesion: 0.09
Nodes (25): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+17 more)
Nodes (26): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+18 more)
### Community 62 - "提交前 Review(2026-10-11)"
Cohesion: 0.40
@@ -348,29 +333,33 @@ Nodes (5): browser(), init(), read(), rememberKind(), request()
Cohesion: 0.22
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 本机开发环境限制(不代表线上状态)
### Community 66 - "TinyYolov2Base"
Cohesion: 0.18
Nodes (13): convLayer(), convWithBatchNorm(), depthwiseSeparableConv$1(), fullyConnectedLayer(), leaky(), ONet(), PNet(), prelu() (+5 more)
### Community 66 - "report-persistence.mjs"
Cohesion: 0.29
Nodes (6): child, dataRoot, fx, h5, ReportUI, require
### Community 68 - "analyze-timeout.js"
Cohesion: 0.20
Nodes (7): assert, end, fs, path, source, start, vm
### Community 71 - "initialize"
Cohesion: 0.29
Nodes (7): createBrowserEnv(), createFileSystem(), createNodejsEnv(), initialize(), isBrowser(), monkeyPatch(), setEnv()
### Community 70 - ".forwardInput"
Cohesion: 0.22
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
### Community 71 - ".toString"
Cohesion: 0.33
Nodes (5): FaceMatch(), la(), pr(), pt(), vt()
### Community 76 - ".detect"
Cohesion: 0.18
Nodes (6): computeReshapedDimensions(), iou(), isEven(), NetInput(), nonMaxSuppression$1(), TinyYolov2()
Cohesion: 0.15
Nodes (6): iou(), NetInput(), nonMaxSuppression$1(), SsdMobilenetv1(), TinyYolov2(), TinyYolov2Base()
### Community 77 - ".runAndExtendWithFaceDetections"
Cohesion: 0.15
Nodes (11): DetectAllFacesTask(), DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), extendWithFaceDetection(), FaceExpressions(), isWithFaceDetection() (+3 more)
### Community 78 - "n"
Cohesion: 0.11
Nodes (36): ae(), be(), bi(), ce(), de(), e(), er(), FaceMatch() (+28 more)
### Community 78 - "t"
Cohesion: 0.14
Nodes (14): bi(), Ef(), er(), ia(), Ie(), j(), Kt(), Mf() (+6 more)
### Community 79 - "登录反馈与加载优化(2026-10-12)"
Cohesion: 0.25
@@ -384,9 +373,9 @@ Nodes (6): 初次部署时的凭据查找记录(现已由用户提供), 初
Cohesion: 0.60
Nodes (4): api(), base, callback(), start()
### Community 82 - "test/accounts.js"
### Community 82 - "wechat-browser.mjs"
Cohesion: 0.22
Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
Nodes (6): createAccounts, h5, ReportUI, require, root, SkinScore
### Community 83 - "10.09 新版 H5 实施与验收"
Cohesion: 0.29
@@ -396,40 +385,32 @@ Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义
Cohesion: 0.33
Nodes (6): 后续真机确认与体验优化, 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
### Community 85 - "FaceLandmarks"
Cohesion: 0.07
Nodes (15): extendWithFaceLandmarks(), FaceLandmark68NetBase(), FaceLandmarks(), FaceLandmarks5(), FaceLandmarks68(), getCenterPoint(), isDimensions(), minBbox() (+7 more)
### Community 86 - "isValidNumber"
Cohesion: 0.33
Nodes (5): bgrToRgbTensor(), Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender()
### Community 85 - ".predictAgeAndGender"
Cohesion: 0.28
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
### Community 87 - "产品推荐规则:肤龄基准与边界调整"
Cohesion: 0.33
Nodes (6): 产品推荐规则:肤龄基准与边界调整, 决策顺序, 后台与前端一致性, 实施与验证范围, 已明确的要求, 已确认的三个条件
### Community 88 - "gn"
Cohesion: 0.40
Nodes (5): gn(), kn(), tt(), wo(), z()
### Community 92 - "客户反馈修改(2026-10-12)"
Cohesion: 0.40
Nodes (4): 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证
Cohesion: 0.33
Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证
## Knowledge Gaps
- **220 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+215 more)
- **236 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+231 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **21 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **17 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `r()` connect `n` to `face-api.js`, `k`, `f`, `normalize`, `server.js`, `i`, `C`?**
- **Why does `r()` connect `n` to `face-api.js`, `.toString`, `mn`, `normalize`, `server.js`, `t`, `i`, `C`?**
_High betweenness centrality (0.050) - this node is a cross-community bridge._
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `f`, `lib/accounts.js`, `n`, `gn`?**
_High betweenness centrality (0.050) - this node is a cross-community bridge._
- **Why does `renderReport()` connect `app.js` to `k`, `n`?**
_High betweenness centrality (0.038) - this node is a cross-community bridge._
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `n`, `lib/accounts.js`, `t`, `i`?**
_High betweenness centrality (0.043) - this node is a cross-community bridge._
- **Why does `renderReport()` connect `app.js` to `t`, `k`?**
_High betweenness centrality (0.041) - this node is a cross-community bridge._
- **Are the 24 inferred relationships involving `n()` (e.g. with `newRecordId()` and `ae()`) actually correct?**
_`n()` has 24 INFERRED edges - model-reasoned connections that need verification._
- **Are the 27 inferred relationships involving `r()` (e.g. with `handleExport()` and `handleRecordsQuery()`) actually correct?**
@@ -437,4 +418,4 @@ _Questions this graph is uniquely positioned to answer:_
- **Are the 7 inferred relationships involving `t()` (e.g. with `renderReport()` and `e()`) actually correct?**
_`t()` has 7 INFERRED edges - model-reasoned connections that need verification._
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
_220 weakly-connected nodes found - possible documentation gaps or missing edges._
_236 weakly-connected nodes found - possible documentation gaps or missing edges._
File diff suppressed because one or more lines are too long
+1476 -907
View File
File diff suppressed because it is too large Load Diff
+23 -8
View File
@@ -40,8 +40,8 @@
"semantic_hash": ""
},
"h5/js/app.js": {
"mtime": 1791780010.867014,
"ast_hash": "da06fb14a0750f9aeaac3cf1cdc474df",
"mtime": 1791786351.8390813,
"ast_hash": "7bfa025f6ac5b97e62075cd762a1eee0",
"semantic_hash": ""
},
"h5/js/head-pose.js": {
@@ -140,8 +140,8 @@
"semantic_hash": ""
},
"h5/index.html": {
"mtime": 1791779513.7549675,
"ast_hash": "b0d1eaae730873cfb06dab32e997fa29",
"mtime": 1791786653.9070883,
"ast_hash": "39e41fec33bce3bf22520b9a3128b2ec",
"semantic_hash": ""
},
"h5/vendor/faceapi/README.md": {
@@ -1295,8 +1295,8 @@
"semantic_hash": ""
},
"h5/assets/fonts/misans/README.md": {
"mtime": 1791646269.9287534,
"ast_hash": "9f471a9be8b8ba08e9af70d95f73958b",
"mtime": 1791785802.8870926,
"ast_hash": "7d94c6ffc6ff797d644b2765243c9a59",
"semantic_hash": ""
},
"reference/AI\u6d4b\u80a4\u62a5\u544a\u9875\u8bdd\u672f\u4fee\u6539.md": {
@@ -1665,13 +1665,28 @@
"semantic_hash": ""
},
"docs/\u5ba2\u6237\u53cd\u9988\u4fee\u6539-2026-10-12.md": {
"mtime": 1791780101.1150675,
"ast_hash": "4b0cf0511f31cb1c1ea310fe72b3b808",
"mtime": 1791782301.2510374,
"ast_hash": "2602a202e227a624108b7749f43175be",
"semantic_hash": ""
},
"design/ui-20261009/verification/02-login-success.png": {
"mtime": 1791780103.7670736,
"ast_hash": "6e06a46e74f50f9f9a4ee94bb34bc3de",
"semantic_hash": ""
},
"h5/test/customer-feedback-second.js": {
"mtime": 1791786351.8390813,
"ast_hash": "ca63c07bb8ee8eb77267b9d08e5cb3d8",
"semantic_hash": ""
},
"h5/test/customer-feedback-second.mjs": {
"mtime": 1791786653.9070883,
"ast_hash": "4dbb7749ae94be10910e3dbf8b090c72",
"semantic_hash": ""
},
"docs/\u5ba2\u6237\u53cd\u9988\u7b2c\u4e8c\u6279\u4fee\u6539-2026-10-12.md": {
"mtime": 1791786653.9070883,
"ast_hash": "0cb17a2e1c909816642fa31d3a416f1e",
"semantic_hash": ""
}
}
Binary file not shown.
+1
View File
@@ -6,6 +6,7 @@
- 下载包:https://hyperos.mi.com/font-download/MiSans.zip
- 下载日期:2026-10-10
- 文件:官方原始 MiSans-Regular.woff2、MiSans-Semibold.woff2,未修改、未子集化。
- 2026-10-12 按最新 PSD 补充官方原始 MiSans-Demibold.woff2,未修改、未子集化;报告主标题使用 Semibold,拍照/协议弹窗标题及报告数字使用 Demibold。保留按需加载,不用于登录首屏。
- 许可协议:上述官方下载页的「许可协议」;此处字体随本应用使用,不作为独立字库产品分发。
页面「我的报告」底部包含字体使用声明。请保持声明,单独再分发或改造字体前需另行确认授权。
+3 -1
View File
@@ -1,3 +1,5 @@
/* Original MiSans files, unchanged; never needed for login/session restoration. */
/* Original MiSans files, unchanged; never needed for login/session restoration.
PSD Semibold titles use 600; PSD Demibold titles/numbers use 700. */
@font-face { font-family: MiSans; src: url(../assets/fonts/misans/MiSans-Regular.woff2) format('woff2'); font-weight: 100 500; font-display: swap; }
@font-face { font-family: MiSans; src: url(../assets/fonts/misans/MiSans-Semibold.woff2) format('woff2'); font-weight: 600 900; font-display: swap; }
@font-face { font-family: "MiSans Demibold"; src: url(../assets/fonts/misans/MiSans-Demibold.woff2) format('woff2'); font-weight: 700; font-display: swap; }
+7 -8
View File
@@ -207,15 +207,16 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
#s-cam { background: #000; }
/* 相机未就绪/降级时的静态态:白底经蒙版 85% 压暗 → 框外深灰、轮廓内白 */
.cam-bg { position: absolute; inset: 0; background: #fff; }
/* 实时画面整体缩小 15%:脸离镜头近一点也能装进取景轮廓(检测距离阈值按同一比例放宽) */
/* 实时画面铺满视口,避免缩小预览产生矩形取景边界。送检仍抓原始视频帧。 */
#cam-video {
position: absolute; inset: 0;
width: 100%; height: 100%;
object-fit: cover;
transform: scale(.85);
transform: none;
border: 0; outline: none; box-shadow: none;
display: none; z-index: 0;
}
#cam-video.mirror { transform: scale(-.85, .85); } /* 前置镜头镜像预览 */
#cam-video.mirror { transform: scaleX(-1); } /* 前置镜头镜像预览 */
#s-cam.cam-live #cam-video { display: block; }
#s-cam.cam-live .cam-bg { display: none; }
/* 纵向定位按 WebView 口径:Figma 帧 812 高扣掉顶部 86px 微信导航栏 = 726 高视口 */
@@ -255,7 +256,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
}
.cam-fill.on { opacity: 1; transition: none; }
/* 取景蒙版:SVG mask 镂空 + 轮廓线框,按 data-step 切换轮廓;框外 85% 黑 */
/* 取景蒙版:SVG 镂空,无外轮廓描边;按 data-step 切换轮廓,框外 85% 黑 */
.cam-mask {
position: absolute; inset: 0;
width: 100%; height: 100%;
@@ -267,10 +268,8 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
.cam-mask[data-step="2"] [data-shape="2"] { display: block; }
.cam-mask.flip { transform: scaleX(-1); } /* 后置镜头预览不镜像:侧脸轮廓随之翻转 */
.cam-dim-rect { fill: #000; fill-opacity: .85; }
.cam-line path { fill: none; stroke: rgba(255,255,255,.85); stroke-width: 2.5; }
@keyframes maskBreath { 0%,100% { stroke-opacity: .3; } 50% { stroke-opacity: .95; } }
.cam-mask.no-face .cam-line path { animation: maskBreath 1.8s ease-in-out infinite; }
.cam-mask.ok .cam-line path { stroke: #34C759; stroke-opacity: 1; animation: none; }
.cam-line { display: none; }
.cam-line path { fill: none; stroke: none; }
/* 动态捕捉扫描带:自上而下扫过轮廓内 */
.cam-scan { display: none; }
.cam-mask.scanning .cam-scan { display: block; }
+11 -2
View File
@@ -19,10 +19,14 @@
.login-message { min-height: calc(28 * var(--u)); max-width: 84%; color: #eee; text-align: center; font-size: 10px; line-height: 1.5; }
.splash-bg, .guide-bg { background-image: url(../assets/img/ui-20261009/bg-splash-h5.webp); }
.splash-slogan { top: 7.2%; font-size: 23px; line-height: 1.16; letter-spacing: 0; font-weight: 400; }
.splash-slogan-en { font-weight: 400; }
.splash-slogan-zh { font-weight: 700; white-space: nowrap; }
.splash-bottom { bottom: 6.5%; gap: calc(9 * var(--u)); }
.brand-row { margin-bottom: calc(7 * var(--u)); gap: calc(5 * var(--u)); }
.brand-logo { width: calc(48 * var(--u)); }
.brand-text { font-size: 19px; letter-spacing: 0; }
/* Match visible CJK glyph height to the logo, not the font's line box.
Both stay fixed in CSS px so narrow screens retain the same proportions. */
.brand-logo { width: auto; height: 26px; flex-shrink: 0; }
.brand-text { font-size: 24px; line-height: 1.2; font-weight: 700; letter-spacing: 0; white-space: nowrap; }
.btn-history { background: #fff; }
.profile-bg { background: url(../assets/img/ui-20261009/bg-profile-h5.webp) center/cover no-repeat; }
.skin-desc { max-width: calc(150 * var(--u)); font-size: 11.5px; }
@@ -84,6 +88,11 @@
#p-privacy { background: linear-gradient(#0008,#0008),url(../assets/img/ui-20261009/bg-splash-h5.webp) center/cover; }
#p-photoguide .popup { padding: calc(62 * var(--u)) calc(30 * var(--u)) calc(40 * var(--u)); }
.popup-title-center { text-align: left; margin: 0 0 calc(18 * var(--u)); }
/* PSD uses Semibold for report headings, Demibold for capture/consent/numbers. */
.report-title, .dimension-title, .card-heading, .prod-title, .page-title { font-weight: 600; }
.popup-title, .cam-step, .stat-value b, .stat-value i, .dim-score, .dim-score b, .care-summary {
font-family: "MiSans Demibold", var(--font); font-weight: 700;
}
.guide-description { font-size: 12px; line-height: 1.6; margin-bottom: calc(35 * var(--u)); }
.cam-feature-lines { fill: none; stroke: #fff; stroke-width: 2.5; stroke-dasharray: 5 5; filter: drop-shadow(0 1px 1px #000); }
#s-cam:not(.cam-live) .cam-feature-lines { stroke: #111; filter: none; }
+2 -2
View File
@@ -42,7 +42,7 @@
<section class="screen" id="s-splash">
<div class="splash-bg"></div>
<button class="account-status account-status-home" data-action="switch-login" aria-label="切换登录方式"><span data-auth-status></span><small>切换 ›</small></button>
<p class="splash-slogan">ONE ME,<br>FROM THE INSIDE OUT<br>以我为始 自有表现</p>
<p class="splash-slogan"><span class="splash-slogan-en">ONE ME,<br>FROM THE INSIDE OUT</span><br><span class="splash-slogan-zh">以我为始 自有表现</span></p>
<div class="splash-bottom">
<div class="brand-row">
<img src="assets/img/ui-20261009/brand-logo.png" alt="1'me" class="brand-logo">
@@ -107,7 +107,7 @@
<section class="screen" id="s-cam">
<div class="cam-bg" id="cam-bg"></div>
<p class="cam-step" id="cam-step">1/3<br>请正对屏幕</p>
<!-- 取景蒙版:SVG mask 镂空(框外 85% 黑,对齐 Figma 图层不透明度) + 轮廓线框;data-step 切换三种轮廓
<!-- 取景蒙版:SVG mask 镂空(框外 85% 黑),无外轮廓描边;data-step 切换三种轮廓
轮廓由 Figma 拍照示意图(design/assets/images/*-拍照示意.png,alpha 镂空区)逐像素描摹生成,坐标为 Figma 375×812 整帧;viewBox 只取 y 86–812(帧顶 86px 是微信导航栏,不属于 H5 视口)
0=正脸(含耳部) 1=左脸出镜(用户头向自己右侧转,镜像预览中耳在画面左、鼻朝画面右) 2=右脸出镜(头向自己左侧转,耳在画面右、鼻朝画面左)
data-step 取 CAM_STEPS[].mask(拍摄顺序 正→右→左,对齐 Figma) -->
+55 -7
View File
@@ -4,10 +4,43 @@
var $ = function (sel, root) { return (root || document).querySelector(sel); };
/* camera-outline-pure-begin */
// Round the exported polygon's corners without changing its position/size.
// Quadratic control points stay within the original corner's convex hull.
function smoothCameraOutline(outline) {
if (typeof outline !== 'string' || !/^M[\d.,\sL+-]+Z$/i.test(outline)) return outline;
var numbers = outline.match(/[-+]?\d*\.?\d+/g);
if (!numbers) return outline;
var values = numbers.map(Number), points = [];
if (values.length < 6 || values.length % 2 || !values.every(isFinite)) return outline;
for (var i = 0; i < values.length; i += 2) {
var p = [values[i], values[i + 1]], last = points[points.length - 1];
if (!last || Math.hypot(p[0] - last[0], p[1] - last[1]) > .001) points.push(p);
}
if (points.length > 1 && Math.hypot(points[0][0] - points[points.length - 1][0], points[0][1] - points[points.length - 1][1]) < .001) points.pop();
if (points.length < 3) return outline;
var corners = points.map(function (p, j) {
var prev = points[(j + points.length - 1) % points.length], next = points[(j + 1) % points.length];
var before = Math.hypot(prev[0] - p[0], prev[1] - p[1]), after = Math.hypot(next[0] - p[0], next[1] - p[1]);
var radius = Math.min(2, before * .35, after * .35);
return { p: p, enter: [p[0] + (prev[0] - p[0]) * radius / before, p[1] + (prev[1] - p[1]) * radius / before],
exit: [p[0] + (next[0] - p[0]) * radius / after, p[1] + (next[1] - p[1]) * radius / after] };
});
function xy(p) { return p.map(function (n) { return n.toFixed(3); }).join(','); }
var path = 'M' + xy(corners[0].exit);
for (var k = 1; k <= corners.length; k++) {
var c = corners[k % corners.length];
path += 'L' + xy(c.enter) + 'Q' + xy(c.p) + ' ' + xy(c.exit);
}
return path + 'Z';
}
/* camera-outline-pure-end */
// The PSD geometry uses the same 375 × 812 coordinate space as the capture mask.
(window.CameraGeometry || []).forEach(function (geometry, index) {
var outline = smoothCameraOutline(geometry.outline);
document.querySelectorAll('#cam-mask path[data-shape="' + index + '"]').forEach(function (path) {
path.setAttribute('d', geometry.outline);
path.setAttribute('d', outline);
});
var group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.setAttribute('data-shape', index);
@@ -107,7 +140,10 @@
}
/* ─── 弹窗管理 ─── */
function openOverlay(id) { $('#' + id).classList.add('show'); }
function openOverlay(id) {
if (id === 'p-privacy' || id === 'p-photoguide') loadAppFont();
$('#' + id).classList.add('show');
}
function closeOverlay(id) { $('#' + id).classList.remove('show'); }
// 点遮罩空白处关闭(与各自关闭按钮同行为:隐私→回启动页、拍照引导→留在档案页)
@@ -581,8 +617,8 @@
var MISS_FRAMES_MAX = 3; // 连续漏检 ≥3 帧(~450ms)才报「未检测到人脸」,容忍单帧抖动
var OK_HOLD_MS = 450; // 条件连续合格时长:滤掉一闪而过的合格帧(防运动模糊),达标立即抓拍
var NO_PROGRESS_MS = 12000; // 单步超过该时长仍未抓拍→浮出调整提示
var PREVIEW_SCALE = 0.85; // 与 CSS #cam-video 的 scale 一致:预览缩小 15%
// 人脸框高/视频高。原标定(取景框≈42% 屏高)为 0.26/0.58;预览缩小 15% 后,同样装满轮廓需要脸在原始帧里大 1/0.85 倍
var PREVIEW_SCALE = 1; // 与 CSS #cam-video 铺满预览一致,镜像只改变方向
// 人脸框高/原始视频高;保留现有距离判定阈值。
var FACE_NEAR = 0.30, FACE_FAR = 0.68;
var CENTER_TOL = 0.18; // 人脸中心偏离取景中线超过屏宽 18% → 提示移入取景框
var LUMA_DARK = 65, LUMA_BRIGHT = 235; // 脸部平均亮度(0–255):过暗/过曝提示调整光线
@@ -611,6 +647,7 @@
}
var detRunning = false, detTimer = null; // 检测循环
var okSince = 0; // 当前连续 ok 起点(0=未在累积)
var readyHapticAt = 0; // 抖动重试时最多每两秒提醒一次
var nudgeTimer = null; // 12s 无进展提示
var capturing = false; // 白屏补光抓帧中(暂停姿态判定)
var lastHint = ''; // 提示文案去重
@@ -636,7 +673,7 @@
$('#cam-mask defs').appendChild(clip);
})();
function setMask(cls) { // '' 静态白框 | 'no-face' 呼吸动画 | 'ok' 品牌绿定格
function setMask(cls) { // State also drives readiness hints; no visible outline stroke.
var m = $('#cam-mask');
m.classList.toggle('no-face', cls === 'no-face');
m.classList.toggle('ok', cls === 'ok');
@@ -664,6 +701,7 @@
// 每步进入/回退时的检测状态机重置(蒙版/提示/无进展计时全部归位)
function resetDetectState() {
okSince = 0;
readyHapticAt = 0;
missFrames = 0;
oppFrames = 0;
lastHint = '';
@@ -737,6 +775,13 @@
} catch (e) { return null; }
}
var missFrames = 0; // 连续漏检帧计数:单帧抖动不报「未检测到人脸」
/* camera-haptic-pure-begin */
function captureHaptic(nav) {
// Unsupported/blocked vibration must never interrupt automatic capture.
try { return typeof nav.vibrate === 'function' && nav.vibrate([70, 40, 70]) !== false; }
catch (e) { return false; }
}
/* camera-haptic-pure-end */
function onDetect(d) {
var hint = '', ok = false;
missFrames = d ? 0 : missFrames + 1;
@@ -784,7 +829,10 @@
// 合格时「保持不动」只显示不播报:随即抓拍,「拍摄完成」会截断它
setHint(ok ? '保持不动' : hint, ok, ok);
if (!ok) { okSince = 0; return; }
if (!okSince) okSince = Date.now();
if (!okSince) {
okSince = Date.now();
if (!readyHapticAt || okSince - readyHapticAt >= 2000) { readyHapticAt = okSince; captureHaptic(navigator); }
}
else if (Date.now() - okSince >= OK_HOLD_MS) { okSince = 0; doCapture(); }
}
@@ -858,12 +906,12 @@
if (!camLive) return;
shutterLock = true;
capturing = true;
if (isManualMode()) captureHaptic(navigator); // Manual shutter has no readiness callback.
clearTimeout(nudgeTimer);
hideNudge();
var seq = camSeq;
var fill = $('#cam-fill');
fill.classList.add('on'); // .on 无过渡,瞬间点亮
if (navigator.vibrate) navigator.vibrate(30);
setTimeout(function () {
var shot = captureFromVideo();
setTimeout(function () {
+69
View File
@@ -0,0 +1,69 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const path = require('node:path');
const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
function extract(name) { return source.split('/* ' + name + '-begin */')[1].split('/* ' + name + '-end */')[0]; }
const context = {};
vm.runInNewContext(extract('camera-outline-pure') + extract('camera-haptic-pure'), context);
const geometry = { window: {} };
vm.runInNewContext(fs.readFileSync(path.join(__dirname, '../js/camera-geometry.js'), 'utf8'), geometry);
const original = JSON.stringify(geometry.window.CameraGeometry);
for (const row of geometry.window.CameraGeometry) {
const rounded = context.smoothCameraOutline(row.outline);
assert(rounded.includes('Q') && rounded.endsWith('Z'));
assert(!/NaN|Infinity/.test(rounded));
// Curve endpoints/control points remain within the exported contour bounds.
const oldNumbers = row.outline.match(/[-+]?\d*\.?\d+/g).map(Number);
const newNumbers = rounded.match(/[-+]?\d*\.?\d+/g).map(Number);
for (let axis = 0; axis < 2; axis++) {
const oldAxis = oldNumbers.filter((_, i) => i % 2 === axis), newAxis = newNumbers.filter((_, i) => i % 2 === axis);
assert(Math.min(...newAxis) >= Math.min(...oldAxis) - .001);
assert(Math.max(...newAxis) <= Math.max(...oldAxis) + .001);
assert(Math.abs(Math.min(...newAxis) - Math.min(...oldAxis)) <= 2);
assert(Math.abs(Math.max(...newAxis) - Math.max(...oldAxis)) <= 2);
}
}
assert.equal(JSON.stringify(geometry.window.CameraGeometry), original);
for (const value of [null, '', 'M--Z', 'M1,2L1,2Z', 'M0,0L0,0L0,0Z', 'M0,0Q1,2 3,4Z']) assert.equal(context.smoothCameraOutline(value), value);
for (const nav of [null, {}, { vibrate: false }, { vibrate: () => false }, { vibrate: () => { throw Error('denied'); } }]) assert.equal(context.captureHaptic(nav), false);
let pattern;
assert(context.captureHaptic({ vibrate: p => { pattern = Array.from(p); return true; } }));
assert.deepEqual(pattern, [70, 40, 70]);
// Exercise the actual readiness state machine; only camera/pose inputs and clock
// are stubs. No detector thresholds, hold time or capture sequence are changed.
let now = 10000, buzzes = 0, captures = 0;
const state = {
missFrames: 0, MISS_FRAMES_MAX: 3, camVideo: { videoHeight: 600 }, FACE_NEAR: .30, FACE_FAR: .68,
faceOffsetX: () => 0, CENTER_TOL: .18, CAM_STEPS: [{ target: 'front' }], camStep: 0,
HeadPose: { check: () => ({ ok: true }) }, sideYawFlip: false, faceLuma: () => 100,
LUMA_DARK: 65, LUMA_BRIGHT: 235, window: {}, setMask: () => {}, setHint: () => {},
okSince: 0, readyHapticAt: 0, OK_HOLD_MS: 450, Date: { now: () => now },
navigator: { vibrate: () => { buzzes++; return true; } }, doCapture: () => { captures++; }
};
vm.runInNewContext(extract('camera-haptic-pure') + source.slice(source.indexOf(' function onDetect(d)'), source.indexOf(' /* ─── 拍照流程')), state);
const face = { detection: { box: { height: 250 } }, landmarks: { positions: [] } };
state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 0);
now += 200; state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 0);
now += 250; state.onDetect(face); assert.equal(buzzes, 1); assert.equal(captures, 1);
now += 100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
now += 100; state.onDetect(face); assert.equal(buzzes, 1, 'jitter must not repeatedly buzz');
now += 2100; state.onDetect({ ...face, detection: { box: { height: 20 } } });
state.onDetect(face); assert.equal(buzzes, 2);
state.navigator.vibrate = () => { throw Error('blocked'); };
now += 450; state.onDetect(face); assert.equal(captures, 2, 'vibration failure must not prevent capture');
for (const manual of [true, false]) {
let manualBuzzes = 0, scheduled = [];
const shutter = { shutterLock: false, camLive: true, capturing: false, camSeq: 1,
isManualMode: () => manual, navigator: { vibrate: () => { manualBuzzes++; throw Error('blocked'); } },
clearTimeout: () => {}, nudgeTimer: null, hideNudge: () => {},
$: () => ({ classList: { add: () => {} } }), setTimeout: (_, ms) => scheduled.push(ms) };
vm.runInNewContext(extract('camera-haptic-pure') + source.slice(source.indexOf(' function doCapture()'), source.indexOf(' // 三张拍完:按视角重排')), shutter);
shutter.doCapture();
assert.equal(manualBuzzes, manual ? 1 : 0, 'auto must not double buzz; manual must retain its cue');
assert(shutter.shutterLock && shutter.capturing);
assert.deepEqual(scheduled, [300], 'blocked haptic must still schedule camera capture');
}
console.log('PASS: three bounded rounded contours, invalid outline fallback, no geometry mutation; haptic missing/false/exception safety, pre-capture cue, jitter throttling, unchanged 450ms auto-capture');
+129
View File
@@ -0,0 +1,129 @@
// Browser-local session/preview fixtures. No account/report writes or paid calls.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { execSync } from 'node:child_process';
import { pathToFileURL } from 'node:url';
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
const base = process.env.H5_BASE_URL || 'http://127.0.0.1:8080/';
const artifacts = process.env.H5_ARTIFACT_DIR;
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true });
const errors = [], requests = [];
page.on('pageerror', e => errors.push(e.message));
page.on('request', r => requests.push(new URL(r.url()).pathname));
await page.route('**/api/auth/session', route => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok: true, user: { id: 'second-batch-fixture', kind: 'browser' } }) }));
await page.goto(base);
await page.waitForSelector('#s-splash.active');
await page.waitForLoadState('networkidle');
assert(!requests.some(p => /\.woff2$|faceapi/.test(p)), 'entry must not download complete fonts or face engine');
for (const width of [320, 375, 390, 414, 480]) {
await page.setViewportSize({ width, height: 726 });
const home = await page.evaluate(() => {
const en = getComputedStyle(document.querySelector('.splash-slogan-en'));
const zh = getComputedStyle(document.querySelector('.splash-slogan-zh'));
const text = document.querySelector('#s-splash .brand-text');
const style = getComputedStyle(text);
const logo = document.querySelector('#s-splash .brand-logo').getBoundingClientRect();
const row = document.querySelector('#s-splash .brand-row').getBoundingClientRect();
const slogan = document.querySelector('.splash-slogan').getBoundingClientRect();
const badge = document.querySelector('.account-status-home').getBoundingClientRect();
const ctx = document.createElement('canvas').getContext('2d');
ctx.font = `${style.fontWeight} ${style.fontSize} ${style.fontFamily}`;
const metrics = ctx.measureText(text.textContent);
return { en: en.fontWeight, zh: zh.fontWeight, size: style.fontSize, weight: style.fontWeight,
logoHeight: logo.height, inkHeight: metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent,
logoRatio: logo.width / logo.height, rowX: row.x, rowRight: row.right,
badgeBottom: badge.bottom, sloganTop: slogan.top,
sloganLines: slogan.height / parseFloat(zh.lineHeight), overflow: document.documentElement.scrollWidth > innerWidth };
});
assert.equal(home.en, '400'); assert.equal(home.zh, '700');
assert.equal(home.size, '24px'); assert.equal(home.weight, '700');
assert.equal(home.logoHeight, 26);
assert(Math.abs(home.inkHeight - home.logoHeight) <= 4, 'logo/text optical height mismatch: ' + JSON.stringify(home));
assert(Math.abs(home.logoRatio - 132 / 59) < .01, 'logo must retain original aspect ratio');
assert(home.rowX >= 0 && home.rowRight <= width && !home.overflow, 'brand row overflows at ' + width);
assert(home.badgeBottom <= home.sloganTop, 'status badge overlaps slogan at ' + width);
assert(Math.abs(home.sloganLines - 3) < .1, 'slogan must retain three lines at ' + width);
}
await page.setViewportSize({ width: 375, height: 726 });
if (artifacts) {
fs.mkdirSync(artifacts, { recursive: true });
await page.screenshot({ path: path.join(artifacts, '00-home-clarified.png'), animations: 'disabled' });
}
await page.waitForLoadState('networkidle');
assert(!requests.some(p => /\.woff2$|faceapi/.test(p)), 'home typography must not add full fonts or face engine');
await page.click('[data-action="open-privacy"]');
await page.waitForSelector('link[href="css/fonts.css"]', { state: 'attached' });
await page.evaluate(async () => { await document.fonts.load('700 23px "MiSans Demibold"', "欢迎体验肌肤状态评估服务"); });
await page.waitForLoadState('networkidle');
assert(requests.some(p => /MiSans-Demibold\.woff2$/.test(p)), 'consent must load PSD Demibold before profile');
assert.equal(await page.locator('#p-privacy .popup-title').evaluate(e => getComputedStyle(e).fontWeight), '700');
const cdp = await page.context().newCDPSession(page);
await cdp.send('DOM.enable'); await cdp.send('CSS.enable');
async function actualFont(selector, expected) {
const { root } = await cdp.send('DOM.getDocument');
const { nodeId } = await cdp.send('DOM.querySelector', { nodeId: root.nodeId, selector });
const { fonts } = await cdp.send('CSS.getPlatformFontsForNode', { nodeId });
assert(fonts.some(f => f.isCustomFont && new RegExp(expected, 'i').test(f.postScriptName || f.familyName)), selector + ': ' + JSON.stringify(fonts));
}
await actualFont('#p-privacy .popup-title', 'Demibold');
if (artifacts) {
fs.mkdirSync(artifacts, { recursive: true });
await page.screenshot({ path: path.join(artifacts, '01-consent-demibold.png'), animations: 'disabled' });
}
await page.click('[data-action="close-privacy"]');
// Synthetic local stream only, to inspect full-bleed framing without camera permission.
await page.evaluate(async () => {
document.querySelectorAll('.screen').forEach(s => s.classList.toggle('active', s.id === 's-cam'));
document.querySelector('#s-cam').classList.add('cam-live');
const canvas = document.createElement('canvas'); canvas.width = 375; canvas.height = 726;
const ctx = canvas.getContext('2d'); ctx.fillStyle = '#587581'; ctx.fillRect(0, 0, canvas.width, canvas.height);
const video = document.querySelector('#cam-video'); video.srcObject = canvas.captureStream(5); await video.play();
});
await page.waitForTimeout(400); // Let the existing screen-entry translate animation finish.
await page.evaluate(async () => { await document.fonts.load('700 23px "MiSans Demibold"', '请正脸出镜'); });
await actualFont('#cam-step', 'Demibold');
for (const [step, label] of [['0', '1/3<br>请正脸出镜'], ['2', '2/3<br>请右脸出镜'], ['1', '3/3<br>请左脸出镜']]) {
await page.evaluate(({ step, label }) => { document.querySelector('#cam-mask').dataset.step = step; document.querySelector('#cam-step').innerHTML = label; }, { step, label });
for (const width of [320, 375, 390, 414, 480]) {
await page.setViewportSize({ width, height: 726 });
const box = await page.locator('#cam-video').boundingBox();
assert(Math.abs(box.width - width) < 1 && Math.abs(box.height - 726) < 1, 'preview must fill viewport');
assert(Math.abs(box.x) < 1 && Math.abs(box.y) < 1);
assert.equal(await page.locator('#cam-video').evaluate(e => getComputedStyle(e).borderWidth), '0px');
for (const state of ['', 'no-face', 'ok']) {
await page.locator('#cam-mask').evaluate((e, state) => { e.classList.remove('no-face', 'ok'); if (state) e.classList.add(state); }, state);
assert.equal(await page.locator('.cam-line').evaluate(e => getComputedStyle(e).display), 'none');
}
assert(await page.locator('#cam-cut path[data-shape="' + step + '"]').evaluate(e => e.getAttribute('d').includes('Q')));
assert.equal(await page.locator('#cam-step').evaluate(e => getComputedStyle(e).fontSize), '21.5px');
assert.equal(await page.locator('.cam-feature-lines[data-shape="' + step + '"]').evaluate(e => getComputedStyle(e).display), 'block');
}
if (artifacts) {
await page.setViewportSize({ width: 375, height: 726 });
await page.screenshot({ path: path.join(artifacts, '02-camera-' + step + '.png'), animations: 'disabled' });
}
}
await page.goto(base + '?mock=1&screen=report');
await page.waitForFunction(() => document.querySelector('#stat-score b').textContent !== '--');
await page.evaluate(async () => { await document.fonts.load('700 40px "MiSans Demibold"', '0123456789'); await document.fonts.load('600 23px MiSans', '护理建议您的肌肤状态表现紧塑力'); });
await page.waitForLoadState('networkidle');
for (const selector of ['#stat-score b', '#stat-age b', '.dim-score b']) await actualFont(selector, 'Demibold');
for (const selector of ['#s-report .report-title', '.dimension-title', '.card-heading']) await actualFont(selector, 'Semibold');
for (const width of [320, 375, 390, 414, 480]) {
await page.setViewportSize({ width, height: 844 });
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
assert.equal(await page.locator('.card-heading').first().evaluate(e => getComputedStyle(e).fontSize), '23px');
}
if (artifacts) {
await page.setViewportSize({ width: 375, height: 726 });
await page.locator('#s-report').evaluate(e => { e.scrollTop = 0; });
await page.screenshot({ path: path.join(artifacts, '03-report-demibold-numbers.png'), animations: 'disabled' });
await page.locator('.card-care').scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(artifacts, '04-care-semibold.png'), animations: 'disabled' });
}
assert.deepEqual(errors, []);
console.log('PASS: heavier Chinese slogan, matched logo/text optical height, three lines/no overlap in five widths, no fonts/engine at entry; early consent Demibold; actual PSD font glyphs for headings/numbers; full-bleed camera, no outline in any state, three rounded contours/retained guides; fixed fonts/no overflow in five widths; no JS errors');
} finally { await browser.close(); }