feat(h5): apply October 12 customer report feedback

This commit is contained in:
xsl
2026-10-12 13:15:09 +08:00
parent 3193a4b683
commit d7ae748e08
22 changed files with 2282 additions and 1027 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 317 KiB

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

After

Width:  |  Height:  |  Size: 428 KiB

+53
View File
@@ -0,0 +1,53 @@
# 客户反馈修改(2026-10-12)
依据:`feedback/傅艳NEW与项思炼的会话 2026年10月12日/傅艳NEW与项思炼的会话 2026年10月12日.md` 及其 6 张截图。本文仅处理本次客户反馈,不修改评分公式、肤龄显示规则或产品推荐规则。
## 逐项处理
| 客户反馈 | 处理 |
| --- | --- |
| 报告 Tab 同时支持滑动、点击 | 三组维度卡分别支持横向手势及点击;纵向滚动不切换,边界不循环。保留键盘方向键、Home/End 操作。 |
| 检测图脸部放大,同时保留点开放大 | 根据接口原始人脸矩形统一裁切照片、增强图和标记的展示区域,保留额头、下巴余量;七张检测图均处理,点开放大保留。无人脸矩形或矩形无效时展示原图。上传原图、检测输入、存档照片不裁切。 |
| 皱纹分区详细说明 | 展示 12 个真实分区的 9 项原始指标,缺失显示「未获取」,0 保留为 0。抬头纹和额头纹在现有数据中同属 `forehead_wrinkle_info`,合并为「抬头纹(额头纹)」,不虚构第 13 区。左右以本人为准。 |
| 平滑度图有框,但文字说无毛孔、黑头 | 原图错误使用色斑、痣、痤疮框。改为真实的毛孔、黑头增强图,并说明颜色及其不代表严重程度。缺图不绘制其他问题框。总体评分优秀但局部计数大于 0 时明确列出计数,不再写「无明显」。评分与档位本身不变。 |
| 黄色按钮色号不一致 | 最新 PSD 按钮矢量填色与导出像素均为 `#F5F078`,当前按钮也使用该色;正常按钮没有额外透明度/滤镜。傅艳已确认暂按 PSD 的 `#F5F078`,保持现有色值。屏幕色彩模式可能影响观感,不能据截图猜测新色。 |
| 产品名固定断句 | 三款副标题第一行分别为「以我本然有光蓄活」「以我自在有势挺括」「以我从容有态构塑」,第二行统一「双仓精华液」,独立块且不自动换行。 |
| 链接卡顿、拍摄提示不符 | 卡顿已由上一轮首屏延迟加载与静态缓存优化处理,本次复测无回退。拍摄提示按最新 PSD 改为「为使测试结果更准确,请关闭美颜/滤镜、并确保光线充足均匀。」 |
| 字号不随手机宽度缩放 | 原来按 `--u` 缩放的字号改为 375 宽设计基准的固定 CSS px;布局间距和图片仍适应屏宽。既有文字自动放大控制保留。不承诺不同屏幕的物理毫米大小或系统强制辅助缩放完全相同。 |
## 皱纹数据说明
12 区:抬头纹(额头纹)、左右法令纹、左右鱼尾纹、左右眼部细纹、左右嘴角纹、眉间纹、左右脸颊纹。
9 项:皱纹分、严重等级、深纹条数、浅纹条数、标准化长度、标准化深度、像素密度、面积比例、深纹比例。
数值来自接口原始字段,最多显示四位小数;比例保留接口原值,不擅自转成百分比或补单位。分区深浅纹条数仅在对应指标缺失时使用接口分区计数回退,其余缺失不推算。不对原始数据及评分模型作任何写入。
新生成报告由后端保存新版文案快照,后台与前端读取同一份内容。已有历史报告继续保留当时的文案和推荐快照,不批量重算、不修改历史记录;展示图片可依据已保存原图与矩形使用新的裁切方式。验收新增皱纹话术请生成新报告,不以旧快照作为新版话术验证依据。
## 验证
```bash
node h5/test/customer-feedback.js
H5_ARTIFACT_DIR=/tmp/juzi-feedback-customer-20261012 node h5/test/customer-feedback.mjs
node h5/test/recommendation.js
node h5/test/regression.js
node h5/test/accounts.js
node h5/test/report-persistence.mjs
node h5/test/ui-20261009.mjs
node h5/test/login-performance.mjs
node h5/test/wechat-browser.mjs
node h5/test/analyze-timeout.js
node h5/test/browser-failures.mjs
node h5/test/e2e-browser.mjs
```
专项验证包括真实浏览器触摸事件、三组独立切换、纵滚不误触、横滑不误开放大图、点开放大仍正常、七图裁切及边界容错、12 区真实指标和缺失值、部分增强图缺失、三款产品在 320/375/390/414/480 五种宽度下固定两行且无溢出。专项截图见上述临时目录。
登录性能复测:150 ms 网络延迟、约 2 Mbps 下载、CPU 四倍降速,冷登录可操作约 1636 ms、静态资源编码后 262618 字节;登录仍不加载人脸引擎、模型或完整字体。该结果是开发机模拟观测,不保证所有手机耗时。微信回归使用模拟授权上游,实际 HTTPS Cookie 和跨设备身份隔离均验证,不代表重新完成一次真人微信授权。
上述测试全部通过,包含 126 个产品推荐案例、86 条评分真值断言;UI 回归同时刷新了 `design/ui-20261009/verification/` 内的验收截图。已执行 `graphify update .` 更新代码关系图。
测试使用样例、隔离数据目录或只读演示页,不调用收费 Face++,不修改生产账号或历史报告。确认本地没有近 180 秒进行中的检测后,已重启本地 H5 服务以加载后端新版话术,健康检查正常。黄色按钮色值已确认,无其他待确认项。
提交仅包含代码、修改说明、代码关系图及验收截图,不包含客户聊天原文和附件。公网部署沿用既有 Git bundle 快进同步方式,不推送远程仓库,不迁移或覆盖 `h5/data`,不修改微信凭据、nginx 或后台访问保护。部署实测结果在完成后追加。
+23 -17
View File
@@ -4,17 +4,17 @@
"2": "app.js",
"3": "skin-score.js",
"4": "k",
"5": "r",
"6": "extractFaceTensors",
"7": "ComputeSingleFaceDescriptorTask",
"5": "f",
"6": ".runAndExtendWithFaceDetection",
"7": ".run",
"8": "admin.js",
"9": "__awaiter",
"10": ".forwardInput",
"10": "SsdMobilenetv1",
"11": "lib/accounts.js",
"12": "server.js",
"13": "files",
"14": "AgeGenderNet",
"15": ".runAndExtendWithFaceDetections",
"15": "extractFaceTensors",
"16": "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范",
"17": "extractFaces",
"18": "1'me 在线肌肤测试 H5",
@@ -34,15 +34,15 @@
"32": "FaceMatcher",
"33": "build-fixture.py",
"34": "smoke-test.js",
"35": "DetectSingleFaceLandmarksTask",
"35": "customer-feedback.js",
"36": "ao",
"37": "UI 外发版 10.09 图片资源",
"38": "FaceLandmark68Net",
"39": "UI交互界面调整",
"40": ".predictAgeAndGender",
"41": "test/accounts.js",
"41": "NeuralNetwork",
"42": "export_psd_assets.py",
"43": ".toBatchTensor",
"43": "normalize",
"44": "start-local.sh",
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
@@ -60,34 +60,40 @@
"58": ".dispose",
"59": "call_skinanalyze.sh",
"60": "report-fixture.js",
"61": "NeuralNetwork",
"61": ".extractWeights",
"62": "提交前 Review(2026-10-11)",
"63": "e2e-browser.mjs",
"64": "account-client.js",
"65": "微信登录接入与公网部署说明",
"66": "wechat-browser.mjs",
"66": "TinyYolov2Base",
"67": "ui-20261009.mjs",
"68": "analyze-timeout.js",
"69": "misans/README.md",
"70": ".draw",
"71": "production-loop.mjs",
"71": "initialize",
"72": "build_report_copy.py",
"73": "camera-geometry.js",
"74": "report-copy.js",
"75": "prepare_h5_assets.py",
"76": "TinyYolov2",
"77": "drawFaceExpressions",
"76": ".detect",
"77": ".runAndExtendWithFaceDetections",
"78": "n",
"79": "微信登录联调-2026-10-12.md",
"79": "登录反馈与加载优化(2026-10-12)",
"80": "线上微信登录部署记录",
"81": "wechat-production.mjs",
"82": "ComputeAllFaceDescriptorsTask",
"82": "test/accounts.js",
"83": "10.09 新版 H5 实施与验收",
"84": "微信登录联调:2026-10-12",
"85": "FaceLandmarks",
"86": "isValidNumber",
"87": "产品推荐规则:肤龄基准与边界调整",
"91": "login-performance.mjs",
"94": "Box",
"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",
+1 -1
View File
@@ -1 +1 @@
{"0": "bf880d08a452d7b9", "1": "cb6ea50bd4e4a5ca", "2": "e45f02a2d81f378b", "3": "1f737ca8edb978f2", "4": "68c8c4a53446cb29", "5": "d50faa21d10efe9c", "6": "21004e7ec716999e", "7": "7ac475ea9bc280f8", "8": "1ecf47531e1d507b", "9": "bb9def5316182bbf", "10": "880a74c6d255e5cc", "11": "4a490abd5e503318", "12": "53594b653ad48743", "13": "7e3b558a587a4ebe", "14": "d982c7c094e0d01c", "15": "4184c65898b38dd8", "16": "95add6a975c0cb72", "17": "d522c79a290badbf", "18": "38bb0b185ceed116", "19": "0a373ad2197fd1a6", "20": "aecf2fce44ba49c5", "21": "8835aeeaa165e9f9", "22": "af8f2189aee7bcc9", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "83b0fd3b14363e4d", "27": "eab427ce9a5219e9", "28": "08e85dab33772c23", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "23f1d406f6c779ae", "36": "a3859fc25c864a81", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "a2b13160223b5a98", "41": "4bc370be8afd7db1", "42": "ffd960136fde00ad", "43": "b434f4a4ecab270c", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "e042d7fe7351bcb9", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "ac5257538f52da80", "56": "91d7ab49459e0b92", "57": "f2a40b5f1b29f669", "58": "16bec50ccad83d20", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "5f4ba3be5e4fe3af", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "74ae017de0ffd36b", "66": "2b037735e1f5b0f6", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "bf75bfc99d4f9e34", "71": "4195478d5c528371", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "c96276581a8c98d4", "77": "d5ce16a18b491627", "78": "bff404f6e2fa3713", "79": "f31ac836426a58b6", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "3dd68cce72af5314", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "1d30b164aa092dc6", "87": "e5fa7717989eda80", "91": "59c31f858c7b6970", "94": "c6eac02ad7178f8e", "95": "e800d832c255899a", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "cb5ac3401283c378"}
{"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"}
+120 -95
View File
@@ -1,16 +1,16 @@
# Graph Report - juzi (2026-10-12)
## Corpus Check
- 70 files · ~4,212,892 words
- 74 files · ~4,387,581 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 1222 nodes · 2613 edges · 93 communities (72 shown, 21 thin omitted)
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 180 edges (avg confidence: 0.53)
- 1255 nodes · 2651 edges · 99 communities (78 shown, 21 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: `85790ef9`
- Built from commit: `3193a4b6`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -20,17 +20,17 @@
- app.js
- skin-score.js
- k
- r
- extractFaceTensors
- ComputeSingleFaceDescriptorTask
- f
- .runAndExtendWithFaceDetection
- .run
- admin.js
- __awaiter
- .forwardInput
- SsdMobilenetv1
- lib/accounts.js
- server.js
- files
- AgeGenderNet
- .runAndExtendWithFaceDetections
- extractFaceTensors
- 巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范
- extractFaces
- 1'me 在线肌肤测试 H5
@@ -49,15 +49,15 @@
- browser-failures.mjs
- FaceMatcher
- build-fixture.py
- DetectSingleFaceLandmarksTask
- customer-feedback.js
- ao
- UI 外发版 10.09 图片资源
- FaceLandmark68Net
- UI交互界面调整
- .predictAgeAndGender
- test/accounts.js
- NeuralNetwork
- export_psd_assets.py
- .toBatchTensor
- normalize
- start-local.sh
- PredictAllAgeAndGenderWithFaceAlignmentTask
- PredictAllFaceExpressionsWithFaceAlignmentTask
@@ -72,31 +72,36 @@
- regression.js
- .dispose
- call_skinanalyze.sh
- NeuralNetwork
- .extractWeights
- 提交前 Review(2026-10-11)
- e2e-browser.mjs
- account-client.js
- 微信登录接入与公网部署说明
- wechat-browser.mjs
- TinyYolov2Base
- ui-20261009.mjs
- analyze-timeout.js
- misans/README.md
- .draw
- production-loop.mjs
- initialize
- build_report_copy.py
- TinyYolov2
- drawFaceExpressions
- .detect
- .runAndExtendWithFaceDetections
- n
- 微信登录联调-2026-10-12.md
- 登录反馈与加载优化(2026-10-12)
- 线上微信登录部署记录
- wechat-production.mjs
- ComputeAllFaceDescriptorsTask
- test/accounts.js
- 10.09 新版 H5 实施与验收
- 微信登录联调:2026-10-12
- FaceLandmarks
- isValidNumber
- 产品推荐规则:肤龄基准与边界调整
- login-performance.mjs
- Box
- gn
- 傅艳NEW与项思炼的会话 2026年10月12日.md
- FaceDetection
- 客户反馈修改(2026-10-12)
- TinyFaceFeatureExtractor
- DetectSingleFaceLandmarksTask
- PredictAllFaceExpressionsTask
- PredictSingleAgeAndGenderWithFaceAlignmentTask
- PredictSingleFaceExpressionsWithFaceAlignmentTask
@@ -123,17 +128,17 @@
h5/js/app.js → h5/vendor/faceapi/face-api.js
- `vizCompose()` --indirect_call--> `rh()` [INFERRED]
h5/js/app.js → h5/vendor/faceapi/face-api.js
- `vizCompose()` --indirect_call--> `w()` [INFERRED]
- `vizCompose()` --indirect_call--> `scale()` [INFERRED]
h5/js/app.js → h5/vendor/faceapi/face-api.js
## Import Cycles
- None detected.
## Communities (93 total, 21 thin omitted)
## Communities (99 total, 21 thin omitted)
### Community 0 - "face-api.js"
Cohesion: 0.02
Nodes (29): cn(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), dr(), en(), FaceDetection(), ff() (+21 more)
Cohesion: 0.03
Nodes (20): cn(), dr(), en(), ff(), ge(), ia(), na(), nt() (+12 more)
### Community 1 - "线上闭环测试(不含真实微信授权)"
Cohesion: 0.25
@@ -141,39 +146,43 @@ Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测
### Community 2 - "app.js"
Cohesion: 0.05
Nodes (79): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), checkStill(), closeLoginSuccess() (+71 more)
Nodes (80): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), checkStill(), closeLoginSuccess() (+72 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.15
Nodes (15): b(), ba(), ca(), da(), fa(), k(), ne(), pa() (+7 more)
Cohesion: 0.13
Nodes (19): b(), ba(), ca(), cl(), da(), fa(), k(), ll() (+11 more)
### Community 5 - "r"
Cohesion: 0.14
Nodes (34): a(), ae(), bf(), cf(), d(), drawContour(), dt(), ea() (+26 more)
### Community 5 - "f"
Cohesion: 0.21
Nodes (23): a(), cf(), d(), drawContour(), dt(), ea(), f(), g() (+15 more)
### Community 6 - "extractFaceTensors"
Cohesion: 0.25
Nodes (4): DetectAllFaceLandmarksTask(), extractFaceTensors(), isTensor3D(), isTensor4D()
### 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 8 - "admin.js"
Cohesion: 0.21
Nodes (28): ageText(), api(), bind(), closeDetail(), collectFilters(), dimsHtml(), doRetry(), dSec() (+20 more)
### Community 9 - "__awaiter"
Cohesion: 0.14
Nodes (14): __awaiter(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), FaceRecognitionNet(), fetchImage(), fetchJson() (+6 more)
Cohesion: 0.15
Nodes (13): __awaiter(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow() (+5 more)
### Community 10 - ".forwardInput"
Cohesion: 0.22
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
### Community 10 - "SsdMobilenetv1"
Cohesion: 0.17
Nodes (10): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+2 more)
### Community 11 - "lib/accounts.js"
Cohesion: 0.17
Nodes (20): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+12 more)
Cohesion: 0.08
Nodes (31): atomic(), body(), cookie(), cookies(), crypto, fs, json(), login() (+23 more)
### Community 12 - "server.js"
Cohesion: 0.08
@@ -184,20 +193,20 @@ 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.10
Nodes (6): AgeGenderNet(), extractParams$1(), FaceExpressionNet(), FaceLandmark68NetBase(), FaceLandmark68TinyNet(), FaceProcessor()
Cohesion: 0.12
Nodes (5): AgeGenderNet(), extractParams$1(), FaceExpressionNet(), FaceLandmark68TinyNet(), FaceProcessor()
### Community 15 - ".runAndExtendWithFaceDetections"
Cohesion: 0.18
Nodes (8): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectAllFacesTask(), DetectSingleFaceTask(), extendWithFaceDetection(), resizeResults()
### Community 15 - "extractFaceTensors"
Cohesion: 0.25
Nodes (3): DetectAllFaceLandmarksTask(), extractFaceTensors(), isTensor3D()
### 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.25
Nodes (16): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+8 more)
Cohesion: 0.14
Nodes (19): awaitMediaLoaded(), Box(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow() (+11 more)
### Community 18 - "1'me 在线肌肤测试 H5"
Cohesion: 0.13
@@ -213,11 +222,11 @@ Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), p
### Community 21 - "disposeUnusedWeightTensors"
Cohesion: 0.09
Nodes (24): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+16 more)
Nodes (23): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+15 more)
### Community 22 - ".loadFromDisk"
Cohesion: 0.22
Nodes (4): ComposableTask(), getModelUris(), Gf(), TinyFaceDetector()
Cohesion: 0.20
Nodes (5): ComposableTask(), getModelUris(), Gf(), Kf(), TinyFaceDetector()
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
Cohesion: 0.20
@@ -240,8 +249,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.12
Nodes (28): Bo(), C(), cd(), cl(), co(), e(), eo(), fo() (+20 more)
Cohesion: 0.14
Nodes (22): bf(), Bo(), C(), cd(), co(), eo(), fo(), Ft() (+14 more)
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
Cohesion: 0.29
@@ -255,6 +264,10 @@ Nodes (5): assets, document_size, layers, pages, source
Cohesion: 0.60
Nodes (4): main(), 递归缩放 result 内全部坐标:{left,top,width,height} 矩形与 {x,y} 点,与原数据同为整数。, scale_coords(), to_data_url()
### Community 35 - "customer-feedback.js"
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()
@@ -271,17 +284,13 @@ Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设
Cohesion: 0.28
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
### Community 41 - "test/accounts.js"
Cohesion: 0.22
Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
### Community 42 - "export_psd_assets.py"
Cohesion: 0.70
Nodes (4): bounds(), layer_at(), main(), save_image()
### Community 43 - ".toBatchTensor"
Cohesion: 0.09
Nodes (28): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+20 more)
### Community 43 - "normalize"
Cohesion: 0.13
Nodes (15): conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), denseBlock3(), denseBlock4(), depthwiseSeparableConv() (+7 more)
### Community 44 - "start-local.sh"
Cohesion: 0.50
@@ -304,24 +313,24 @@ Cohesion: 0.11
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
### Community 55 - "i"
Cohesion: 0.10
Nodes (21): bn(), dn(), ee(), et(), fn(), i(), je(), ln() (+13 more)
Cohesion: 0.12
Nodes (19): bn(), dn(), ee(), Ef(), et(), fn(), i(), je() (+11 more)
### Community 56 - "report-ui.js"
Cohesion: 0.90
Nodes (4): build(), count(), fill(), numeric()
Cohesion: 0.50
Nodes (8): build(), count(), fill(), metricText(), numeric(), scalar(), wrinkleDetails(), wrinkleText()
### Community 57 - "regression.js"
Cohesion: 0.60
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
### Community 58 - ".dispose"
Cohesion: 0.14
Nodes (17): computeReshapedDimensions(), extractBoundingBoxes(), getSizesForScale(), iou(), isEven(), NetInput(), nonMaxSuppression(), nonMaxSuppression$1() (+9 more)
Cohesion: 0.18
Nodes (12): extractBoundingBoxes(), FaceRecognitionNet(), getSizesForScale(), nonMaxSuppression(), normalize$1(), pyramidDown(), rescaleAndNormalize(), scale() (+4 more)
### Community 61 - "NeuralNetwork"
Cohesion: 0.06
Nodes (28): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+20 more)
### Community 61 - ".extractWeights"
Cohesion: 0.09
Nodes (25): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+17 more)
### Community 62 - "提交前 Review(2026-10-11)"
Cohesion: 0.40
@@ -339,29 +348,33 @@ Nodes (5): browser(), init(), read(), rememberKind(), request()
Cohesion: 0.22
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 本机开发环境限制(不代表线上状态)
### Community 66 - "wechat-browser.mjs"
Cohesion: 0.22
Nodes (6): createAccounts, h5, ReportUI, require, root, SkinScore
### Community 66 - "TinyYolov2Base"
Cohesion: 0.18
Nodes (13): convLayer(), convWithBatchNorm(), depthwiseSeparableConv$1(), fullyConnectedLayer(), leaky(), ONet(), PNet(), prelu() (+5 more)
### Community 68 - "analyze-timeout.js"
Cohesion: 0.20
Nodes (7): assert, end, fs, path, source, start, vm
### Community 71 - "production-loop.mjs"
Cohesion: 0.50
Nodes (3): artifacts, errors, samples
### Community 71 - "initialize"
Cohesion: 0.29
Nodes (7): createBrowserEnv(), createFileSystem(), createNodejsEnv(), initialize(), isBrowser(), monkeyPatch(), setEnv()
### Community 77 - "drawFaceExpressions"
Cohesion: 0.22
Nodes (8): DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), FaceExpressions(), isWithFaceDetection(), isWithFaceExpressions(), isWithFaceLandmarks()
### Community 76 - ".detect"
Cohesion: 0.18
Nodes (6): computeReshapedDimensions(), iou(), isEven(), NetInput(), nonMaxSuppression$1(), TinyYolov2()
### Community 77 - ".runAndExtendWithFaceDetections"
Cohesion: 0.15
Nodes (11): DetectAllFacesTask(), DrawBox(), drawDetections(), drawFaceExpressions(), DrawFaceLandmarks(), extendWithFaceDetection(), FaceExpressions(), isWithFaceDetection() (+3 more)
### Community 78 - "n"
Cohesion: 0.10
Nodes (31): be(), bi(), ce(), de(), Ef(), er(), fe(), Ie() (+23 more)
Cohesion: 0.11
Nodes (36): ae(), be(), bi(), ce(), de(), e(), er(), FaceMatch() (+28 more)
### Community 79 - "微信登录联调-2026-10-12.md"
Cohesion: 0.29
Nodes (4): 体验变化, 慢的原因与处理, 登录反馈与加载优化(2026-10-12), 验证
### Community 79 - "登录反馈与加载优化(2026-10-12)"
Cohesion: 0.25
Nodes (5): 体验变化, 公网部署后复测, 慢的原因与处理, 登录反馈与加载优化(2026-10-12), 验证
### Community 80 - "线上微信登录部署记录"
Cohesion: 0.33
@@ -371,6 +384,10 @@ Nodes (6): 初次部署时的凭据查找记录(现已由用户提供), 初
Cohesion: 0.60
Nodes (4): api(), base, callback(), start()
### Community 82 - "test/accounts.js"
Cohesion: 0.22
Nodes (9): assert, create, fs, http, os, path, ReportUI, SkinScore (+1 more)
### Community 83 - "10.09 新版 H5 实施与验收"
Cohesion: 0.29
Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令
@@ -380,31 +397,39 @@ Cohesion: 0.33
Nodes (6): 后续真机确认与体验优化, 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
### Community 85 - "FaceLandmarks"
Cohesion: 0.08
Nodes (12): bgrToRgbTensor(), Dimensions(), extendWithFaceLandmarks(), FaceLandmarks(), FaceLandmarks5(), FaceLandmarks68(), getCenterPoint(), isValidNumber() (+4 more)
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 87 - "产品推荐规则:肤龄基准与边界调整"
Cohesion: 0.33
Nodes (6): 产品推荐规则:肤龄基准与边界调整, 决策顺序, 后台与前端一致性, 实施与验证范围, 已明确的要求, 已确认的三个条件
### Community 94 - "Box"
Cohesion: 0.20
Nodes (3): Box(), isDimensions(), ObjectDetection()
### 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), 皱纹数据说明, 逐项处理, 验证
## Knowledge Gaps
- **198 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+193 more)
- **220 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+215 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.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `r()` connect `r` to `face-api.js`, `.toBatchTensor`, `server.js`, `n`, `i`, `C`?**
_High betweenness centrality (0.063) - this node is a cross-community bridge._
- **Why does `r()` connect `n` to `face-api.js`, `k`, `f`, `normalize`, `server.js`, `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.051) - this node is a cross-community bridge._
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `r`, `lib/accounts.js`, `n`, `C`?**
_High betweenness centrality (0.047) - this node is a cross-community bridge._
_High betweenness centrality (0.038) - 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?**
@@ -412,4 +437,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?**
_198 weakly-connected nodes found - possible documentation gaps or missing edges._
_220 weakly-connected nodes found - possible documentation gaps or missing edges._
File diff suppressed because one or more lines are too long
+1575 -779
View File
File diff suppressed because it is too large Load Diff
+80 -25
View File
@@ -40,8 +40,8 @@
"semantic_hash": ""
},
"h5/js/app.js": {
"mtime": 1791773775.2349546,
"ast_hash": "cda77fb341a0f2573ed3a57c8e536fbb",
"mtime": 1791780010.867014,
"ast_hash": "da06fb14a0750f9aeaac3cf1cdc474df",
"semantic_hash": ""
},
"h5/js/head-pose.js": {
@@ -140,8 +140,8 @@
"semantic_hash": ""
},
"h5/index.html": {
"mtime": 1791773528.470933,
"ast_hash": "a8248357be1c886421c44845b6038203",
"mtime": 1791779513.7549675,
"ast_hash": "b0d1eaae730873cfb06dab32e997fa29",
"semantic_hash": ""
},
"h5/vendor/faceapi/README.md": {
@@ -1245,8 +1245,8 @@
"semantic_hash": ""
},
"h5/js/report-ui.js": {
"mtime": 1791735369.923658,
"ast_hash": "b05b0a2181711ce6f05bbe6c16179170",
"mtime": 1791779513.7549675,
"ast_hash": "100c3438318d0f7a8048c74ef3455252",
"semantic_hash": ""
},
"h5/lib/accounts.js": {
@@ -1440,38 +1440,38 @@
"semantic_hash": ""
},
"design/ui-20261009/verification/01-login.png": {
"mtime": 1791710678.3993611,
"ast_hash": "546575bd6c19464adfd8033f1416e41f",
"mtime": 1791780103.439073,
"ast_hash": "32f142e3c11d1c7768477e37b06e2209",
"semantic_hash": ""
},
"design/ui-20261009/verification/02-home.png": {
"mtime": 1791710681.0034657,
"ast_hash": "48cb27f9eb1d5f01f3ca00097b31d5b8",
"mtime": 1791780106.015078,
"ast_hash": "52b5c983e23e89638f6f13e4b805bc33",
"semantic_hash": ""
},
"design/ui-20261009/verification/03-history-empty.png": {
"mtime": 1791710681.5034268,
"ast_hash": "cf7cc9fdf15a8fe2c2276ffbf01e9356",
"mtime": 1791780106.5190792,
"ast_hash": "6d94cf3a87064fe79bb8a3cb7e227228",
"semantic_hash": ""
},
"design/ui-20261009/verification/04-privacy.png": {
"mtime": 1791710682.4802737,
"ast_hash": "349e9841ce4f114a2816c52fea2301c3",
"mtime": 1791780107.4590812,
"ast_hash": "7765fe3123d067a5211190f7a08e90c5",
"semantic_hash": ""
},
"design/ui-20261009/verification/05-profile.png": {
"mtime": 1791710683.0148475,
"ast_hash": "2e624c0cb91f96fe84e41878cf9852b5",
"mtime": 1791780108.0350823,
"ast_hash": "0a74071e471e65a16cf5ed22f27b7803",
"semantic_hash": ""
},
"design/ui-20261009/verification/06-report-top.png": {
"mtime": 1791710685.010846,
"ast_hash": "ad6cbf32ddbb4ef628f8b8bf6f970af9",
"mtime": 1791780109.9670866,
"ast_hash": "4f47a34364b02e9ef6189d7ac0ef6660",
"semantic_hash": ""
},
"design/ui-20261009/verification/07-report-full.png": {
"mtime": 1791710685.1146839,
"ast_hash": "8775ce75d26200ab9341f662b8aaf40d",
"mtime": 1791780110.071087,
"ast_hash": "0be8c9110241f7305598415d6c861f5c",
"semantic_hash": ""
},
"h5/assets/img/ui-20261009/bg-history-h5.jpg": {
@@ -1515,17 +1515,17 @@
"semantic_hash": ""
},
"design/ui-20261009/verification/08-camera-front-layout.png": {
"mtime": 1791710683.453275,
"mtime": 1791780108.4710832,
"ast_hash": "b9d04eed0983f1f49fed6c663070ecc1",
"semantic_hash": ""
},
"design/ui-20261009/verification/08-camera-left-layout.png": {
"mtime": 1791710683.5186543,
"mtime": 1791780108.5350835,
"ast_hash": "2cb973828c1d18bfbcb5c0cb8ad06d41",
"semantic_hash": ""
},
"design/ui-20261009/verification/08-camera-right-layout.png": {
"mtime": 1791710683.4840417,
"mtime": 1791780108.5030832,
"ast_hash": "4b299e86432feb2697dd739dc77a825d",
"semantic_hash": ""
},
@@ -1615,8 +1615,63 @@
"semantic_hash": ""
},
"docs/\u767b\u5f55\u53cd\u9988\u4e0e\u52a0\u8f7d\u4f18\u5316-2026-10-12.md": {
"mtime": 1791774062.6429472,
"ast_hash": "ffa81f4a1a2b47615d6a4b7a27de0d08",
"mtime": 1791774227.194959,
"ast_hash": "e925c1ba7a57fdcb2321229b3eb32b66",
"semantic_hash": ""
},
"h5/test/customer-feedback.js": {
"mtime": 1791779743.1310055,
"ast_hash": "a01d5f9ec4424a2f21f8239451f576f9",
"semantic_hash": ""
},
"h5/test/customer-feedback.mjs": {
"mtime": 1791780010.8710139,
"ast_hash": "a8332f17ef693582bc81bff83950fedb",
"semantic_hash": ""
},
"feedback/\u5085\u8273NEW\u4e0e\u9879\u601d\u70bc\u7684\u4f1a\u8bdd 2026\u5e7410\u670812\u65e5/\u5085\u8273NEW\u4e0e\u9879\u601d\u70bc\u7684\u4f1a\u8bdd 2026\u5e7410\u670812\u65e5.md": {
"mtime": 1791778827.0,
"ast_hash": "09b009c9cdb63cf90710b1b45fc6cee0",
"semantic_hash": ""
},
"feedback/\u5085\u8273NEW\u4e0e\u9879\u601d\u70bc\u7684\u4f1a\u8bdd 2026\u5e7410\u670812\u65e5/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_40a9e343-5d09-4760-aa80-6d13cc9ac8eg.png": {
"mtime": 1791778827.0,
"ast_hash": "7a58ab24cec7edaaac8667f37f4e8e5a",
"semantic_hash": ""
},
"feedback/\u5085\u8273NEW\u4e0e\u9879\u601d\u70bc\u7684\u4f1a\u8bdd 2026\u5e7410\u670812\u65e5/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_718c2f13-c978-4e07-bf72-21b24236bd6g.png": {
"mtime": 1791778827.0,
"ast_hash": "96c0f8befaa850e8cb6eab3c54c4ad04",
"semantic_hash": ""
},
"feedback/\u5085\u8273NEW\u4e0e\u9879\u601d\u70bc\u7684\u4f1a\u8bdd 2026\u5e7410\u670812\u65e5/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_92abffb2-da3a-42d4-bd42-6d1b6a864e3g.png": {
"mtime": 1791778827.0,
"ast_hash": "9c902ad750767d96b98b20c713c36c14",
"semantic_hash": ""
},
"feedback/\u5085\u8273NEW\u4e0e\u9879\u601d\u70bc\u7684\u4f1a\u8bdd 2026\u5e7410\u670812\u65e5/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_d5d64a81-acad-4db9-970c-2dd1398b532g.png": {
"mtime": 1791778827.0,
"ast_hash": "12c940d1d339ff8a7a5bcb4e9292f1ff",
"semantic_hash": ""
},
"feedback/\u5085\u8273NEW\u4e0e\u9879\u601d\u70bc\u7684\u4f1a\u8bdd 2026\u5e7410\u670812\u65e5/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_ee1903ba-96ef-4778-9891-027588093c7g.jpeg": {
"mtime": 1791778827.0,
"ast_hash": "12251b6acdea7aaba393c3544832572e",
"semantic_hash": ""
},
"feedback/\u5085\u8273NEW\u4e0e\u9879\u601d\u70bc\u7684\u4f1a\u8bdd 2026\u5e7410\u670812\u65e5/\u56fe\u7247\u548c\u9644\u4ef6/exported_img_v3_0216d_eec8b07b-be07-4d37-9a9f-4057c5afc56g.png": {
"mtime": 1791778827.0,
"ast_hash": "c7a7e7a16688e354c0010a278fcf9124",
"semantic_hash": ""
},
"docs/\u5ba2\u6237\u53cd\u9988\u4fee\u6539-2026-10-12.md": {
"mtime": 1791780101.1150675,
"ast_hash": "4b0cf0511f31cb1c1ea310fe72b3b808",
"semantic_hash": ""
},
"design/ui-20261009/verification/02-login-success.png": {
"mtime": 1791780103.7670736,
"ast_hash": "6e06a46e74f50f9f9a4ee94bb34bc3de",
"semantic_hash": ""
}
}
+50 -50
View File
@@ -1,6 +1,6 @@
/* ═══════════════════════════════════════════
巨子测肤 · 1'me 在线肌肤测试 — H5 样式
设计稿 1080px 基准 → 375px(÷2.88);尺寸写成 N × --u,不同宽度手机等比缩放、视觉一致
设计稿 1080px 基准 → 375px(÷2.88);布局尺寸随 --u 缩放,字号使用固定 CSS px
主色 #F5F078 · 文字 #282828 · 底 #EFEFEF
═══════════════════════════════════════════ */
:root {
@@ -56,7 +56,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
background: var(--yellow);
border-radius: calc(5 * var(--u));
color: var(--ink);
font-size: calc(14.5 * var(--u)); font-weight: 500; letter-spacing: calc(.5 * var(--u));
font-size: 14.5px; font-weight: 500; letter-spacing: calc(.5 * var(--u));
text-align: center;
transition: transform .12s ease;
}
@@ -88,7 +88,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
.splash-slogan {
position: absolute; left: 0; right: 0; top: 10.7%;
text-align: center;
color: #fff; font-size: calc(23 * var(--u)); font-weight: 500;
color: #fff; font-size: 23px; font-weight: 500;
line-height: 1.38; letter-spacing: calc(.6 * var(--u));
text-shadow: 0 1px 8px rgba(0,0,0,.15);
}
@@ -99,7 +99,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
}
.brand-row { display: flex; align-items: center; gap: calc(7 * var(--u)); }
.brand-logo { width: calc(46 * var(--u)); height: auto; filter: brightness(0) invert(1); }
.brand-text { color: #fff; font-size: calc(21.5 * var(--u)); font-weight: 500; letter-spacing: calc(1 * var(--u)); }
.brand-text { color: #fff; font-size: 21.5px; font-weight: 500; letter-spacing: calc(1 * var(--u)); }
/* ═══════════ 03 档案填写 ═══════════ */
#s-profile { background: #0e0f18; }
@@ -116,14 +116,14 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
padding: calc(34 * var(--u)) calc(64 * var(--u)) calc(40 * var(--u));
}
.page-title {
color: #fff; font-size: calc(21.5 * var(--u)); font-weight: 700;
color: #fff; font-size: 21.5px; font-weight: 700;
letter-spacing: calc(1 * var(--u)); text-align: center;
margin-bottom: calc(22 * var(--u));
}
.field { margin-bottom: calc(19 * var(--u)); }
.field-label {
display: block; text-align: center;
color: #fff; font-size: calc(14.5 * var(--u)); font-weight: 400;
color: #fff; font-size: 14.5px; font-weight: 400;
margin-bottom: calc(9 * var(--u)); letter-spacing: calc(.5 * var(--u));
}
.form-row.two { display: flex; gap: calc(23 * var(--u)); }
@@ -135,7 +135,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
width: 100%; height: calc(37 * var(--u));
padding: 0 calc(13 * var(--u)) 0 calc(12 * var(--u));
background: #fff; border-radius: calc(5 * var(--u));
font-size: calc(13.5 * var(--u)); color: var(--ink); text-align: left;
font-size: 13.5px; color: var(--ink); text-align: left;
}
.pick-value { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick .chev { flex: none; width: calc(8 * var(--u)); height: calc(8 * var(--u)); margin-top: calc(-4 * var(--u)); border-width: 0 1.5px 1.5px 0; }
@@ -152,7 +152,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
}
.axis-item {
display: inline-flex; align-items: center; gap: calc(5 * var(--u));
font-size: calc(9 * var(--u)); color: var(--ink);
font-size: 9px; color: var(--ink);
}
.chip { width: calc(8 * var(--u)); height: calc(8 * var(--u)); border-radius: 50%; display: inline-block; }
.skin-stage { position: relative; padding: 0 calc(28 * var(--u)); }
@@ -172,9 +172,9 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
display: flex; flex-direction: column; align-items: center;
}
.skin-item img { width: calc(109 * var(--u)); height: calc(109 * var(--u)); }
.skin-name { font-size: calc(12.5 * var(--u)); font-weight: 700; color: #000; margin-top: calc(16 * var(--u)); }
.skin-name { font-size: 12.5px; font-weight: 700; color: #000; margin-top: calc(16 * var(--u)); }
.skin-desc {
font-size: calc(10.3 * var(--u)); color: var(--ink);
font-size: 10.3px; color: var(--ink);
line-height: 1.55; text-align: center;
margin-top: calc(5 * var(--u)); max-width: calc(136 * var(--u));
}
@@ -222,7 +222,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
.cam-step {
position: absolute; left: 0; right: 0; top: 7.1%;
text-align: center;
color: #fff; font-size: calc(21.5 * var(--u)); font-weight: 500;
color: #fff; font-size: 21.5px; font-weight: 500;
line-height: 1.5; letter-spacing: calc(1 * var(--u));
text-shadow: 0 1px 10px rgba(0,0,0,.4);
z-index: 2;
@@ -232,7 +232,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
display: flex; justify-content: center; align-items: center; gap: calc(26 * var(--u));
z-index: 2;
}
.cam-guide span { color: #fff; font-size: calc(14.5 * var(--u)); font-weight: 400; }
.cam-guide span { color: #fff; font-size: 14.5px; font-weight: 400; }
.cam-guide i { width: 0; height: calc(15 * var(--u)); border-left: 1px solid rgba(255,255,255,.7); }
.cam-controls {
position: absolute; left: 0; right: 0; bottom: 5.2%;
@@ -281,7 +281,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
.cam-status {
position: absolute; top: 17px; left: 50%;
transform: translateX(-50%);
font-size: calc(11 * var(--u)); color: rgba(255,255,255,.85);
font-size: 11px; color: rgba(255,255,255,.85);
background: rgba(0,0,0,.35);
padding: 4px 12px; border-radius: 11px;
z-index: 2; opacity: 0; transition: opacity .25s; white-space: nowrap;
@@ -292,7 +292,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
.cam-hint {
position: absolute; left: 50%; bottom: 27%;
transform: translateX(-50%) translateY(6px);
font-size: calc(17 * var(--u)); font-weight: 500; color: #fff;
font-size: 17px; font-weight: 500; color: #fff;
text-shadow: 0 1px 8px rgba(0,0,0,.65);
z-index: 2; opacity: 0; pointer-events: none;
transition: opacity .2s ease, transform .2s ease;
@@ -305,7 +305,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
.cam-nudge {
position: absolute; left: 50%; bottom: 16.5%;
transform: translateX(-50%) translateY(6px);
font-size: calc(11.5 * var(--u)); color: #fff;
font-size: 11.5px; color: #fff;
background: rgba(0,0,0,.55);
padding: 6px 14px; border-radius: 14px;
z-index: 2; opacity: 0; pointer-events: none;
@@ -336,14 +336,14 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
border: solid #fff; border-width: 0 0 calc(3 * var(--u)) calc(3 * var(--u));
transform: translate(-50%, -50%) rotate(-45deg);
}
.cam-next b { font-size: calc(17 * var(--u)); font-weight: 500; }
.cam-next span { margin-top: calc(8 * var(--u)); font-size: calc(14 * var(--u)); color: rgba(255,255,255,.85); line-height: 1.5; }
.cam-next b { font-size: 17px; font-weight: 500; }
.cam-next span { margin-top: calc(8 * var(--u)); font-size: 14px; color: rgba(255,255,255,.85); line-height: 1.5; }
/* ═══════════ 08 报告页 ═══════════ */
#s-report { background: var(--bg); }
.report-body { padding: 0 calc(18 * var(--u)) calc(28 * var(--u)); }
.report-title {
font-size: calc(23 * var(--u)); font-weight: 700; color: var(--ink);
font-size: 23px; font-weight: 700; color: var(--ink);
text-align: center; letter-spacing: calc(.5 * var(--u));
padding: calc(32 * var(--u)) 0 calc(20 * var(--u));
}
@@ -351,7 +351,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
background: #fff; border-radius: calc(8 * var(--u));
margin-bottom: calc(12 * var(--u));
}
.stat-label { font-size: calc(12 * var(--u)); color: var(--ink); }
.stat-label { font-size: 12px; color: var(--ink); }
.stat-value { color: var(--ink); line-height: 1; white-space: nowrap; }
.stat-value b { font-weight: 500; letter-spacing: calc(.5 * var(--u)); }
.stat-value i { font-style: normal; font-weight: 500; }
@@ -371,20 +371,20 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
position: absolute; inset: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
}
.ring-center .stat-value b { font-size: calc(40 * var(--u)); }
.ring-center .stat-value i { font-size: calc(14 * var(--u)); }
.ring-center .stat-label { margin-top: calc(3 * var(--u)); font-size: calc(11.5 * var(--u)); color: var(--para); }
.ring-center .stat-value b { font-size: 40px; }
.ring-center .stat-value i { font-size: 14px; }
.ring-center .stat-label { margin-top: calc(3 * var(--u)); font-size: 11.5px; color: var(--para); }
.sum-age {
flex: 1; min-width: 0;
display: flex; flex-direction: column; align-items: flex-start;
padding-left: calc(26 * var(--u));
}
.stat-age { margin-top: calc(6 * var(--u)); }
.stat-age b { font-size: calc(58 * var(--u)); }
.stat-age i { font-size: calc(19 * var(--u)); margin-left: 2px; }
.stat-age b { font-size: 58px; }
.stat-age i { font-size: 19px; margin-left: 2px; }
.age-pill {
margin-top: calc(7 * var(--u));
font-size: calc(9.5 * var(--u)); color: var(--grey-2);
font-size: 9.5px; color: var(--grey-2);
background: #F2F2F2; border-radius: calc(9 * var(--u));
padding: calc(2 * var(--u)) calc(10 * var(--u));
white-space: nowrap;
@@ -400,9 +400,9 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
padding: 0 0 calc(4 * var(--u)) calc(14 * var(--u));
}
.dim + .dim { border-left: 1px solid #EFEFEF; }
.dim-name { font-size: calc(12 * var(--u)); color: var(--ink); }
.dim-score { margin-top: calc(4 * var(--u)); font-size: calc(30 * var(--u)); font-weight: 500; color: var(--ink); line-height: 1.1; }
.dim-score b { font-size: calc(42 * var(--u)); font-weight: 500; }
.dim-name { font-size: 12px; color: var(--ink); }
.dim-score { margin-top: calc(4 * var(--u)); font-size: 30px; font-weight: 500; color: var(--ink); line-height: 1.1; }
.dim-score b { font-size: 42px; font-weight: 500; }
.dim-score.dim-bad { color: var(--bad); }
.dim-bar {
display: block; margin-top: calc(6 * var(--u));
@@ -415,7 +415,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
.dim-flag {
visibility: hidden;
margin-top: calc(6 * var(--u));
font-style: normal; font-size: calc(9 * var(--u)); color: var(--ink);
font-style: normal; font-size: 9px; color: var(--ink);
background: #F8F4A6; border-radius: calc(8 * var(--u));
padding: calc(1 * var(--u)) calc(8 * var(--u));
}
@@ -429,15 +429,15 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
content: ""; position: absolute; left: calc(14 * var(--u)); top: calc(12 * var(--u)); bottom: calc(12 * var(--u));
width: calc(3 * var(--u)); border-radius: 2px; background: var(--yellow);
}
.dims-note-l1 { font-size: calc(13 * var(--u)); color: var(--para); }
.dims-note-l1 { font-size: 13px; color: var(--para); }
.dims-note-l1 b { font-weight: 500; color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.dims-note-l2 { margin-top: calc(4 * var(--u)); font-size: calc(12 * var(--u)); color: var(--grey); }
.dims-note-l2 { margin-top: calc(4 * var(--u)); font-size: 12px; color: var(--grey); }
/* 维度详情卡(原布局):紧塑力/活力感/稳定力三个 Tab,每个 Tab 两条「竖排标题 + 左图右文」;检测图点击放大 */
.card-detail { padding: 0 calc(19 * var(--u)) calc(19 * var(--u)); }
.detail-tabs { display: flex; border-bottom: 1px solid #F0F0F0; }
.dtab { flex: 1; padding: calc(13 * var(--u)) 0 0; font-weight: 500; color: var(--ink); }
.dtab-tx { position: relative; display: inline-block; font-size: calc(10.1 * var(--u)); padding: 0 calc(5 * var(--u)) calc(7 * var(--u)); }
.dtab-tx { position: relative; display: inline-block; font-size: 10.1px; padding: 0 calc(5 * var(--u)) calc(7 * var(--u)); }
.dtab.active { font-weight: 700; }
.dtab.active .dtab-tx::after {
content: ""; position: absolute; left: 50%; transform: translateX(-50%); bottom: -1px;
@@ -458,28 +458,28 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
.ditem-title {
writing-mode: vertical-rl;
letter-spacing: calc(4 * var(--u));
font-size: calc(11.5 * var(--u)); font-weight: 500; color: var(--ink);
font-size: 11.5px; font-weight: 500; color: var(--ink);
margin-right: calc(3.5 * var(--u)); flex-shrink: 0;
}
.ditem-para { font-size: calc(10.1 * var(--u)); font-weight: 400; color: var(--para); line-height: 1.41; overflow-wrap: anywhere; }
.ditem-para { font-size: 10.1px; font-weight: 400; color: var(--para); line-height: 1.41; overflow-wrap: anywhere; }
.vm-notes { font-size: calc(10 * var(--u)); color: var(--grey-3); line-height: 1.7; margin: calc(14 * var(--u)) calc(6 * var(--u)) 0; }
.vm-notes { font-size: 10px; color: var(--grey-3); line-height: 1.7; margin: calc(14 * var(--u)) calc(6 * var(--u)) 0; }
.report-disclaimer {
font-size: calc(10 * var(--u)); color: var(--para);
font-size: 10px; color: var(--para);
line-height: 1.7; margin: calc(18 * var(--u)) calc(6 * var(--u)) calc(14 * var(--u));
}
/* 护理建议 / 精华推荐 */
.card-care, .card-essence { padding: calc(20 * var(--u)) calc(22 * var(--u)) calc(18 * var(--u)); }
.card-heading { font-size: calc(17 * var(--u)); font-weight: 700; color: #000; margin-bottom: calc(10 * var(--u)); }
.care-item { font-size: calc(13.5 * var(--u)); color: var(--para); line-height: 1.7; margin-bottom: calc(10 * var(--u)); overflow-wrap: anywhere; }
.care-summary { font-size: calc(13.5 * var(--u)); font-weight: 500; color: #000; margin-top: calc(4 * var(--u)); }
.essence-name { font-size: calc(16 * var(--u)); font-weight: 700; color: var(--ink); }
.essence-triggers { font-size: calc(11 * var(--u)); color: var(--grey-3); margin-top: calc(6 * var(--u)); line-height: 1.5; }
.card-heading { font-size: 17px; font-weight: 700; color: #000; margin-bottom: calc(10 * var(--u)); }
.care-item { font-size: 13.5px; color: var(--para); line-height: 1.7; margin-bottom: calc(10 * var(--u)); overflow-wrap: anywhere; }
.care-summary { font-size: 13.5px; font-weight: 500; color: #000; margin-top: calc(4 * var(--u)); }
.essence-name { font-size: 16px; font-weight: 700; color: var(--ink); }
.essence-triggers { font-size: 11px; color: var(--grey-3); margin-top: calc(6 * var(--u)); line-height: 1.5; }
/* 产品推荐:大标题落在灰底;白卡左文案右图(各占一半) */
.prod-title {
font-size: calc(23 * var(--u)); font-weight: 700; color: var(--ink);
font-size: 23px; font-weight: 700; color: var(--ink);
line-height: 1.45; text-align: center; letter-spacing: calc(1 * var(--u));
margin: calc(30 * var(--u)) 0 calc(18 * var(--u));
}
@@ -491,8 +491,8 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
display: flex; flex-direction: column; align-items: flex-start;
}
.prod-badge { width: calc(44 * var(--u)); height: auto; }
.prod-name { font-size: calc(14.5 * var(--u)); font-weight: 700; color: var(--ink); line-height: 1.45; margin-top: calc(10 * var(--u)); }
.prod-sub { font-size: calc(9.5 * var(--u)); color: var(--grey-3); margin-top: auto; padding-top: calc(10 * var(--u)); }
.prod-name { font-size: 14.5px; font-weight: 700; color: var(--ink); line-height: 1.45; margin-top: calc(10 * var(--u)); }
.prod-sub { font-size: 9.5px; color: var(--grey-3); margin-top: auto; padding-top: calc(10 * var(--u)); }
.prod-scroll { flex: 0 0 51%; display: flex; overflow: hidden; }
.prod-scroll img { width: 100%; height: 100%; object-fit: cover; }
.btn-more { margin: calc(26 * var(--u)) auto 0; }
@@ -528,11 +528,11 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
}
.popup-close img { width: calc(19 * var(--u)); height: calc(19 * var(--u)); }
.popup-title {
font-size: calc(23 * var(--u)); font-weight: 500; color: var(--ink);
font-size: 23px; font-weight: 500; color: var(--ink);
line-height: 1.38; letter-spacing: calc(.5 * var(--u));
margin-bottom: calc(14 * var(--u));
}
.popup-body { font-size: calc(11.6 * var(--u)); color: var(--ink); line-height: 1.6; margin-bottom: calc(26 * var(--u)); }
.popup-body { font-size: 11.6px; color: var(--ink); line-height: 1.6; margin-bottom: calc(26 * var(--u)); }
.privacy-row { display: flex; align-items: center; gap: calc(9 * var(--u)); cursor: pointer; }
.checkbox {
width: calc(12 * var(--u)); height: calc(12 * var(--u)); border-radius: 50%;
@@ -544,7 +544,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
inset: 2px; border-radius: 50%;
background: var(--ink-2);
}
.privacy-text { font-size: calc(10.3 * var(--u)); color: var(--grey-2); }
.privacy-text { font-size: 10.3px; color: var(--grey-2); }
.privacy-link { color: var(--ink); font-weight: 500; text-decoration: underline; cursor: pointer; }
.privacy-link:active { opacity: .7; }
.popup-btn { margin: auto auto 0; }
@@ -566,10 +566,10 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
.overlay-dark { background: rgba(16,16,16,.82); }
.analyzing {
color: #fff; text-align: center;
font-size: calc(14 * var(--u)); letter-spacing: 1px;
font-size: 14px; letter-spacing: 1px;
display: flex; flex-direction: column; align-items: center; gap: 16px;
}
.analyzing-tip { font-size: calc(11 * var(--u)); color: rgba(255,255,255,.45); letter-spacing: 0; }
.analyzing-tip { font-size: 11px; color: rgba(255,255,255,.45); letter-spacing: 0; }
.spinner {
width: 34px; height: 34px;
border: 3px solid rgba(255,255,255,.25);
@@ -585,7 +585,7 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
transform: translateX(-50%) translateY(8px);
max-width: 86%;
background: rgba(20,20,20,.85); color: #fff;
font-size: calc(13 * var(--u)); line-height: 1.5; text-align: center;
font-size: 13px; line-height: 1.5; text-align: center;
padding: 9px 18px; border-radius: 18px;
opacity: 0; pointer-events: none;
transition: all .25s ease;
+33 -29
View File
@@ -9,78 +9,82 @@
#s-login { background: #142632 url(../assets/img/ui-20261009/bg-login-h5.webp) center/cover no-repeat; }
.login-brand { position: absolute; width: calc(145 * var(--u)); left: 50%; top: 14%; transform: translateX(-50%); }
.login-bottom { position: absolute; left: 0; right: 0; bottom: max(5%, env(safe-area-inset-bottom)); display: flex; flex-direction: column; align-items: center; gap: calc(12 * var(--u)); }
.login-button { width: calc(270 * var(--u)); height: calc(40 * var(--u)); display: flex; align-items: center; justify-content: center; gap: calc(14 * var(--u)); border-radius: calc(5 * var(--u)); font-size: calc(14 * var(--u)); }
.login-button { width: calc(270 * var(--u)); height: calc(40 * var(--u)); display: flex; align-items: center; justify-content: center; gap: calc(14 * var(--u)); border-radius: calc(5 * var(--u)); font-size: 14px; }
.login-button img { width: calc(23 * var(--u)); height: calc(20 * var(--u)); object-fit: contain; }
.login-wechat { background: var(--yellow); color: #000; }
.login-browser { color: #fff; border: 1px solid #ffffff80; }
.login-consent { display: flex; align-items: center; gap: calc(8 * var(--u)); max-width: 90%; color: #eee; font-size: calc(10 * var(--u)); margin-top: calc(9 * var(--u)); }
.login-consent { display: flex; align-items: center; gap: calc(8 * var(--u)); max-width: 90%; color: #eee; font-size: 10px; margin-top: calc(9 * var(--u)); }
.login-consent input { accent-color: var(--yellow); width: 13px; height: 13px; }
.login-consent button { color: var(--yellow); text-decoration: underline; font-size: inherit; }
.login-message { min-height: calc(28 * var(--u)); max-width: 84%; color: #eee; text-align: center; font-size: calc(10 * var(--u)); line-height: 1.5; }
.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: calc(23 * var(--u)); line-height: 1.16; letter-spacing: 0; font-weight: 400; }
.splash-slogan { top: 7.2%; font-size: 23px; line-height: 1.16; letter-spacing: 0; font-weight: 400; }
.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: calc(19 * var(--u)); letter-spacing: 0; }
.brand-text { font-size: 19px; letter-spacing: 0; }
.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: calc(11.5 * var(--u)); }
.skin-desc { max-width: calc(150 * var(--u)); font-size: 11.5px; }
#s-history { background: #efefef url(../assets/img/ui-20261009/bg-history-h5.webp) center/cover fixed; }
.history-content { padding: calc(28 * var(--u)) calc(17 * var(--u)) calc(36 * var(--u)); }
.history-count { text-align: center; padding: calc(16 * var(--u)) calc(12 * var(--u)) calc(21 * var(--u)); }
.history-count > span { color: #888; font-size: calc(12 * var(--u)); }
.history-count > span { color: #888; font-size: 12px; }
.history-count > span::after { content: ''; display: block; width: calc(45 * var(--u)); margin: calc(12 * var(--u)) auto; border-bottom: 1px solid #eee; }
.history-count p { font-size: calc(12 * var(--u)); }
.identity-note { color: #888; font-size: calc(10 * var(--u)); line-height: 1.6; margin: calc(12 * var(--u)) calc(4 * var(--u)) calc(20 * var(--u)); }
.history-count p { font-size: 12px; }
.identity-note { color: #888; font-size: 10px; line-height: 1.6; margin: calc(12 * var(--u)) calc(4 * var(--u)) calc(20 * var(--u)); }
.history-item { padding: calc(13 * var(--u)) calc(19 * var(--u)) calc(10 * var(--u)); overflow: hidden; margin-top: calc(6 * var(--u)); }
.history-item p { font-size: calc(12 * var(--u)); padding-bottom: calc(7 * var(--u)); border-bottom: 1px solid #eee; }
.history-item p { font-size: 12px; padding-bottom: calc(7 * var(--u)); border-bottom: 1px solid #eee; }
.history-item p span { color: #888; margin-right: calc(8 * var(--u)); }
.history-detail { display: block; margin: calc(7 * var(--u)) 0 0 auto; background: var(--yellow); border-radius: calc(5 * var(--u)); width: calc(80 * var(--u)); font-size: calc(12 * var(--u)); min-height: calc(23 * var(--u)); }
.history-detail { display: block; margin: calc(7 * var(--u)) 0 0 auto; background: var(--yellow); border-radius: calc(5 * var(--u)); width: calc(80 * var(--u)); font-size: 12px; min-height: calc(23 * var(--u)); }
.history-retry { display: block; margin: 20px auto; color: #707070; text-decoration: underline; font-size: 12px; }
#s-report { background: #efefef url(../assets/img/ui-20261009/bg-report-h5.webp) center top/100% auto; }
.report-body { padding: calc(28 * var(--u)) calc(17 * var(--u)) calc(42 * var(--u)); }
.report-title { font-size: calc(23 * var(--u)); padding: 0; margin-bottom: calc(23 * var(--u)); }
.report-title { font-size: 23px; padding: 0; margin-bottom: calc(23 * var(--u)); }
.card { border-radius: calc(7 * var(--u)); }
.card-summary { display: grid; grid-template-columns: 1fr 1fr; gap: calc(15 * var(--u)) 0; padding: calc(16 * var(--u)) calc(14 * var(--u)); }
.sum-score { width: auto; flex: none; }
.sum-age { min-width: 0; padding-left: calc(16 * var(--u)); align-items: center; }
.sum-age .stat-label { font-size: calc(12 * var(--u)); }
.stat-age b { font-size: calc(54 * var(--u)); }
.sum-age .stat-label { font-size: 12px; }
.stat-age b { font-size: 54px; }
.card-dims { grid-column: 1 / -1; padding: 0; }
.dims-row { margin: 0; }
.dim-name { font-size: calc(11 * var(--u)); }
.dim-score { font-size: calc(27 * var(--u)); }
.dim-score b { font-size: calc(36 * var(--u)); }
.dim-flag { font-size: calc(8 * var(--u)); }
.dim-name { font-size: 11px; }
.dim-score { font-size: 27px; }
.dim-score b { font-size: 36px; }
.dim-flag { font-size: 8px; }
.age-pill, .dims-note { display: none !important; }
.dimension-title { font-size: calc(23 * var(--u)); text-align: center; margin: calc(24 * var(--u)) 0 calc(12 * var(--u)); }
.dimension-title { font-size: 23px; text-align: center; margin: calc(24 * var(--u)) 0 calc(12 * var(--u)); }
.card-detail { margin-top: 0; padding: calc(4 * var(--u)) calc(39 * var(--u)) calc(19 * var(--u)); }
.detail-tabs { border: none; }
.dtab-tx { font-size: calc(12 * var(--u)); padding-bottom: calc(10 * var(--u)); }
.dtab-tx { font-size: 12px; padding-bottom: calc(10 * var(--u)); }
.dtab.active .dtab-tx::after { width: calc(76 * var(--u)); height: calc(7 * var(--u)); }
.dpanel.active { padding-top: calc(10 * var(--u)); }
.ditem { display: block; }
.viz-frame { position: relative; width: calc(215 * var(--u)); height: calc(278 * var(--u)); margin: 0 auto calc(12 * var(--u)); background: #f6f6f6; border-radius: calc(5 * var(--u)); overflow: hidden; }
.ditem-img { width: 100%; height: 100%; object-fit: cover; border-radius: 0; position: relative; z-index: 1; }
.viz-missing { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #888; font-size: calc(12 * var(--u)); }
.ditem-img { width: 100%; height: 100%; object-fit: contain; border-radius: 0; position: relative; z-index: 1; }
.viz-missing { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #888; font-size: 12px; }
.ditem-img:not([hidden]) + .viz-missing { display: none; }
.ditem-text { border-top: 1px solid #eee; padding-top: calc(10 * var(--u)); }
.ditem-para { font-size: calc(12 * var(--u)); line-height: 1.55; }
.ditem-para { font-size: 12px; line-height: 1.55; white-space: pre-line; }
.card-detail { touch-action: pan-y; }
.viz-caption { color: #707070; font-size: 11px; line-height: 1.6; margin-top: 10px; }
.card-care { padding: calc(20 * var(--u)) calc(25 * var(--u)); }
.card-heading { font-size: calc(23 * var(--u)); text-align: center; margin-bottom: calc(18 * var(--u)); }
.care-item, .care-summary { font-size: calc(12 * var(--u)); }
.card-heading { font-size: 23px; text-align: center; margin-bottom: calc(18 * var(--u)); }
.care-item, .care-summary { font-size: 12px; }
.prod-card { min-height: calc(175 * var(--u)); }
.prod-info { padding: calc(32 * var(--u)) calc(15 * var(--u)) calc(20 * var(--u)); }
#product-title { font-size: calc(14 * var(--u)); font-weight: 600; white-space: nowrap; }
.prod-name { font-size: calc(12 * var(--u)); font-weight: 400; margin-top: calc(6 * var(--u)); }
#product-title { font-size: 14px; font-weight: 600; white-space: nowrap; }
.prod-info { min-width: 0; padding-left: clamp(8px, 2.5vw, 15px); padding-right: clamp(8px, 2.5vw, 15px); }
.product-line { display: block; white-space: nowrap; }
.prod-name { font-size: 12px; font-weight: 400; margin-top: calc(6 * var(--u)); }
.prod-badge { width: calc(36 * var(--u)); margin-top: calc(20 * var(--u)); }
.prod-scroll { flex-basis: 50%; }
.prod-scroll img { object-fit: cover; aspect-ratio: 1 / 1; }
#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)); }
.guide-description { font-size: calc(12 * var(--u)); line-height: 1.6; margin-bottom: calc(35 * var(--u)); }
.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; }
.font-credit { display: block; text-align: center; color: #888; font-size: 10px; margin: 24px auto 0; }
@@ -95,4 +99,4 @@
.login-success-icon { width: 48px; height: 48px; border-radius: 50%; background: #e9f8ef; color: #17994f; font-size: 30px; line-height: 48px; }
.login-success-popup h2 { font-size: 20px; font-weight: 600; }
.login-success-popup p { font-size: 14px; line-height: 1.7; color: #666; white-space: pre-line; }
@media (max-height: 560px) { .login-brand { top: 8%; } .login-bottom { bottom: 2%; } .splash-slogan { font-size: calc(18 * var(--u)); } }
@media (max-height: 560px) { .login-brand { top: 8%; } .login-bottom { bottom: 2%; } }
+2 -1
View File
@@ -225,6 +225,7 @@
<div class="dpanel" data-panel="1">
<div class="ditem"><div class="viz-frame"><img id="viz-smooth" class="ditem-img" alt="平滑度检测图" hidden><span class="viz-missing">本次未获取平滑度检测图</span></div>
<div class="ditem-text"><p class="ditem-para" id="tx-smooth">检测完成后显示分析结果。</p></div></div>
<p class="viz-caption" id="smooth-map-note"></p>
</div>
<div class="dpanel" data-panel="2">
<div class="ditem"><div class="viz-frame"><img id="viz-water" class="ditem-img" alt="水分检测图" hidden><span class="viz-missing">本次未获取水分检测图</span></div>
@@ -306,7 +307,7 @@
<img src="assets/img/ui-20261009/icon-close.png" alt="关闭">
</button>
<h2 class="popup-title popup-title-center">请拍摄或上传<br>正面及左右侧面照片</h2>
<p class="guide-description">请在光线充足处,取下眼镜,保持面部清晰入镜。</p>
<p class="guide-description">为使测试结果更准确,请关闭美颜/滤镜、并确保光线充足均匀。</p>
<button class="btn btn-primary popup-btn" data-action="start-camera">开始</button>
</div>
</div>
+111 -23
View File
@@ -92,7 +92,12 @@
/* ─── 报告页状态重置(各维度卡回到第一个子 Tab、产品横滑归零) ─── */
function setDetailTab(card, idx) {
card.querySelectorAll('.dtab').forEach(function (t) { t.classList.toggle('active', t.dataset.tab === idx); });
card.querySelectorAll('.dtab').forEach(function (t) {
var selected = t.dataset.tab === idx;
t.classList.toggle('active', selected);
t.setAttribute('aria-selected', String(selected));
t.setAttribute('tabindex', selected ? '0' : '-1');
});
card.querySelectorAll('.dpanel').forEach(function (p) { p.classList.toggle('active', p.dataset.panel === idx); });
}
function resetReport() {
@@ -1026,11 +1031,7 @@
var VIZ_CAT = {
contour: { img: '#ditem-img1', photo: true }, // 轮廓:用户正脸实拍
wrinkles: { img: '#ditem-img2', overlay: 'texture_enhanced_lines', dim: 0.55 }, // 皱纹:正脸压暗+皱纹增强线(图层几乎全透明,直挂在白卡上不可读,故合成)
smooth: { img: '#viz-smooth', photo: true, marks: [ // 光滑:正脸+色斑/痣/痤疮标记
{ key: 'brown_spot', color: '#B26A00' }, // 色斑
{ key: 'mole', color: '#282828' }, // 痣
{ key: 'acne', color: '#D30C0C' } // 痤疮
] },
smooth: { img: '#viz-smooth', overlays: ['texture_enhanced_pores', 'texture_enhanced_blackheads'] },
water: { img: '#viz-water', blendMap: 'water_area' },
tone: { img: '#viz-tone', blendMap: 'brown_area' }, // 肤色水润:正脸×水分图正片叠底(water_area,接口返回)
sens: { img: '#viz-sens', blendMap: 'red_area' }, // 敏感:正脸×红区图正片叠底(直挂白底过淡不可读)
@@ -1043,6 +1044,25 @@
};
/* viz-pure-begin(纯函数段:与 DOM 无关,node 冒烟按锚点抽离断言,勿改锚点注释) */
// Rectangle coordinates come from the original upload; crop only the rendered
// composite, never the photo sent to the algorithm or saved in the report.
function vizFaceCrop(width, height, rectangle) {
var full = { left: 0, top: 0, width: width, height: height, focused: false };
if (!rectangle) return full;
var keys = ['left', 'top', 'width', 'height'];
if (!keys.every(function (key) { return typeof rectangle[key] === 'number' && isFinite(rectangle[key]); })) return full;
if (rectangle.width <= 0 || rectangle.height <= 0 || rectangle.left < 0 || rectangle.top < 0 ||
rectangle.left + rectangle.width > width || rectangle.top + rectangle.height > height) return full;
var ratio = 215 / 278;
var ch = Math.min(Math.max(rectangle.height * 1.55, rectangle.width * 1.18 / ratio), height, width / ratio);
var cw = ch * ratio;
if (cw < rectangle.width || ch < rectangle.height) return full;
var left = Math.max(0, Math.min(width - cw, rectangle.left + rectangle.width / 2 - cw / 2));
var top = Math.max(0, Math.min(height - ch, rectangle.top - rectangle.height * .3));
// Never omit a part of the detected face in an edge-of-frame photograph.
if (top + ch < rectangle.top + rectangle.height) top = rectangle.top + rectangle.height - ch;
return { left: left, top: top, width: cw, height: ch, focused: true };
}
// 从 result 提取某分类的标记集合:polygon 与 rectangle 都保留,缺该标记类型则跳过
function vizCollectMarks(markDefs, result) {
var out = [];
@@ -1108,26 +1128,32 @@
img.src = url;
}
// canvas 合成:照片绘制到自然尺寸 → (可选压暗)→ (可选盖透明增强图层)→ 画坐标标记 → dataURL 挂到卡片 <img>
// canvas 合成:照片和图层使用同一人脸裁切区 → (可选压暗/增强)→ 坐标标记 → 展示图
var vizEpoch = 0;
function vizCompose(photoURL, overlayURL, marks, imgEl, opts) {
var epoch = vizEpoch;
vizLoadImage(photoURL, function (photo) {
if (!photo) return; // 照片不可用:保留占位
var finish = function (overlay) {
var finish = function (overlays) {
if (epoch !== vizEpoch) return;
var w = photo.naturalWidth, h = photo.naturalHeight;
var crop = vizFaceCrop(photo.naturalWidth, photo.naturalHeight, opts && opts.faceRectangle);
var scale = Math.min(1, 960 / crop.width);
var w = Math.round(crop.width * scale), h = Math.round(crop.height * scale);
var cv = document.createElement('canvas');
cv.width = w; cv.height = h;
var ctx = cv.getContext('2d');
ctx.drawImage(photo, 0, 0, w, h);
ctx.drawImage(photo, crop.left, crop.top, crop.width, crop.height, 0, 0, w, h);
if (opts && opts.dim) { ctx.fillStyle = 'rgba(0,0,0,' + opts.dim + ')'; ctx.fillRect(0, 0, w, h); } // 压暗底色让增强线可读
if (overlay) {
overlays.forEach(function (overlay) {
if (!overlay) return;
if (opts && opts.blend) ctx.globalCompositeOperation = opts.blend; // 正片叠底:白底图层(红区/色沉)叠到脸上
ctx.drawImage(overlay, 0, 0, w, h);
var ox = overlay.naturalWidth / photo.naturalWidth, oy = overlay.naturalHeight / photo.naturalHeight;
ctx.drawImage(overlay, crop.left * ox, crop.top * oy, crop.width * ox, crop.height * oy, 0, 0, w, h);
ctx.globalCompositeOperation = 'source-over';
}
var scaled = vizScaleMarks(marks, w / photo.naturalWidth);
});
ctx.save();
ctx.translate(-crop.left * scale, -crop.top * scale);
var scaled = vizScaleMarks(marks, scale);
var lw = Math.max(3, Math.round(w / 200)); // 线宽随图走,卡片缩略后仍可见
var minBox = Math.round(w / 36); // 过小标记放大到最小框,保证缩略可见
for (var i = 0; i < scaled.length; i++) {
@@ -1154,23 +1180,42 @@
ctx.strokeRect(cx - rw / 2, cy - rh / 2, rw, rh);
}
}
try { imgEl.src = cv.toDataURL('image/jpeg', 0.9); imgEl.hidden = false; } catch (e) { /* 合成失败保留占位 */ }
ctx.restore();
try {
imgEl.src = cv.toDataURL('image/jpeg', 0.9); imgEl.hidden = false;
imgEl.dataset.crop = crop.focused ? 'face' : 'full';
} catch (e) { /* 合成失败保留占位 */ }
};
if (overlayURL) vizLoadImage(overlayURL, finish);
else finish(null);
var urls = Array.isArray(overlayURL) ? overlayURL : overlayURL ? [overlayURL] : [];
Promise.all(urls.map(function (url) { return new Promise(function (resolve) { vizLoadImage(url, resolve); }); })).then(finish);
});
}
// 报告页 6 张检测图统一入口:任一输入缺失 → 该卡保留既有占位(不报错)
// 报告页 7 张检测图统一入口:缺失输入按分类降级(不报错)
function renderViz(payload, frontPhoto) {
vizEpoch++;
document.querySelectorAll('.ditem-img').forEach(function (img) { img.hidden = true; img.removeAttribute('src'); });
var result = payload && payload.result;
var maps = (result && result.face_maps) || {};
var rectangle = payload && payload.face_rectangle;
for (var key in VIZ_CAT) {
var cfg = VIZ_CAT[key];
var imgEl = $(cfg.img);
if (!imgEl) continue;
if (cfg.overlays) {
var mapNotes = [], overlays = [];
cfg.overlays.forEach(function (name) {
var url = vizMapURL(maps[name]);
if (!url) return;
overlays.push(url);
mapNotes.push(name === 'texture_enhanced_pores' ? '蓝色为毛孔增强标记' : '红色为黑头增强标记');
});
$('#smooth-map-note').textContent = overlays.length
? mapNotes.join(',') + ';标记不代表严重程度,具体以检测计数和结果为准。'
: '本次未获取毛孔、黑头增强图;此处仅展示拍摄照片,不绘制色斑、痣或痤疮框。';
if (frontPhoto) vizCompose(frontPhoto, overlays, [], imgEl, { faceRectangle: rectangle });
continue;
}
if (cfg.map) { // 整幅图层:图层缺失 → 保留占位
var u = vizMapURL(maps[cfg.map]);
if (u) { imgEl.src = u; imgEl.hidden = false; }
@@ -1180,21 +1225,20 @@
var bu = vizMapURL(maps[cfg.blendMap]);
if (!bu) continue;
if (!frontPhoto) { imgEl.src = bu; imgEl.hidden = false; continue; }
vizCompose(frontPhoto, bu, [], imgEl, { blend: 'multiply' });
vizCompose(frontPhoto, bu, [], imgEl, { blend: 'multiply', faceRectangle: rectangle });
continue;
}
if (cfg.overlay) { // 透明增强图层(皱纹):有照片则合成(可按 cfg.dim 压暗),无照片单挂图层;图层缺失 → 保留占位
var ov = vizMapURL(maps[cfg.overlay]);
if (!ov) continue;
if (!frontPhoto) { imgEl.src = ov; imgEl.hidden = false; continue; }
vizCompose(frontPhoto, ov, [], imgEl, { dim: cfg.dim });
vizCompose(frontPhoto, ov, [], imgEl, { dim: cfg.dim, faceRectangle: rectangle });
continue;
}
// 照片类(轮廓/光滑/痘痘):无照片 → 保留占位;有坐标则 canvas 叠加,无坐标纯照片
if (!frontPhoto) continue;
var marks = cfg.marks ? vizCollectMarks(cfg.marks, result) : [];
if (!marks.length) { imgEl.src = frontPhoto; imgEl.hidden = false; continue; }
vizCompose(frontPhoto, null, marks, imgEl);
vizCompose(frontPhoto, null, marks, imgEl, { faceRectangle: rectangle });
}
}
@@ -1255,7 +1299,11 @@
$('#product-unavailable').hidden = !!product;
if (product) {
$('#product-title').textContent = product.title;
$('#product-name').textContent = product.name + '双仓精华液';
$('#product-name').replaceChildren();
[product.name, '双仓精华液'].forEach(function (line) {
var span = document.createElement('span'); span.className = 'product-line'; span.textContent = line;
$('#product-name').appendChild(span);
});
$('#product-image').src = product.image;
$('#product-image').alt = product.name + '双仓精华液';
}
@@ -1296,7 +1344,47 @@
/* ─── 报告页:每张维度卡各自的子 Tab;点检测图放大,再点返回 ─── */
document.querySelectorAll('.card-detail').forEach(function (card) {
card.querySelector('.detail-tabs').setAttribute('role', 'tablist');
card.querySelectorAll('.dtab').forEach(function (tab) { tab.setAttribute('role', 'tab'); });
setDetailTab(card, '0');
var gesture = null, suppressImageClick = false;
card.addEventListener('pointerdown', function (e) {
if (e.isPrimary === false || (e.pointerType === 'mouse' && e.button !== 0)) return;
gesture = { id: e.pointerId, x: e.clientX, y: e.clientY, horizontal: false };
suppressImageClick = false;
});
card.addEventListener('pointermove', function (e) {
if (!gesture || gesture.id !== e.pointerId) return;
var dx = e.clientX - gesture.x, dy = e.clientY - gesture.y;
if (Math.abs(dx) > 12 && Math.abs(dx) > Math.abs(dy) * 1.4) {
gesture.horizontal = true;
if (e.cancelable) e.preventDefault();
}
});
card.addEventListener('pointerup', function (e) {
if (!gesture || gesture.id !== e.pointerId) return;
var dx = e.clientX - gesture.x, dy = e.clientY - gesture.y;
if (gesture.horizontal && Math.abs(dx) >= 45 && Math.abs(dx) > Math.abs(dy) * 1.4) {
var tabs = Array.from(card.querySelectorAll('.dtab'));
var active = tabs.findIndex(function (tab) { return tab.classList.contains('active'); });
var next = Math.max(0, Math.min(tabs.length - 1, active + (dx < 0 ? 1 : -1)));
setDetailTab(card, tabs[next].dataset.tab);
suppressImageClick = true;
}
gesture = null;
});
card.addEventListener('pointercancel', function () { gesture = null; });
card.addEventListener('dragstart', function (e) { if (e.target.closest('.ditem-img')) e.preventDefault(); });
card.addEventListener('keydown', function (e) {
var tab = e.target.closest('.dtab');
if (!tab || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return;
e.preventDefault();
var tabs = Array.from(card.querySelectorAll('.dtab')), index = tabs.indexOf(tab);
index = e.key === 'Home' ? 0 : e.key === 'End' ? tabs.length - 1 : (index + (e.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length;
setDetailTab(card, tabs[index].dataset.tab); tabs[index].focus({ preventScroll: true });
});
card.addEventListener('click', function (e) {
if (suppressImageClick) { suppressImageClick = false; e.preventDefault(); return; }
var btn = e.target.closest('.dtab');
if (btn) { setDetailTab(card, btn.dataset.tab); return; }
var img = e.target.closest('.ditem-img');
+50 -3
View File
@@ -17,6 +17,47 @@
var levelMap = { '预警': '改善', '改善': '良好', '良好': '优秀' };
var dimNames = { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' };
function numeric(value) { return typeof value === 'number' && isFinite(value); }
function scalar(value) { return numeric(value) ? value : value && numeric(value.value) ? value.value : null; }
var wrinkleDefs = [
['forehead', '抬头纹(额头纹)', ['forehead_wrinkle_info'], 'forehead_count'],
['left_nasolabial', '左法令纹', ['left_nasolabial_wrinkle_info', 'left_nasolabial_fold_info'], 'left_nasolabial_count'],
['right_nasolabial', '右法令纹', ['right_nasolabial_wrinkle_info', 'right_nasolabial_fold_info'], 'right_nasolabial_count'],
['left_crowsfeet', '左鱼尾纹', ['left_crowsfeet_wrinkle_info'], 'left_crowsfeet_count'],
['right_crowsfeet', '右鱼尾纹', ['right_crowsfeet_wrinkle_info'], 'right_crowsfeet_count'],
['left_eye', '左眼部细纹', ['left_eye_wrinkle_info'], 'left_undereye_count'],
['right_eye', '右眼部细纹', ['right_eye_wrinkle_info'], 'right_undereye_count'],
['left_mouth', '左嘴角纹', ['left_mouth_wrinkle_info'], 'left_mouth_count'],
['right_mouth', '右嘴角纹', ['right_mouth_wrinkle_info'], 'right_mouth_count'],
['glabella', '眉间纹', ['glabella_wrinkle_info'], 'glabella_count'],
['left_cheek', '左脸颊纹', ['left_cheek_wrinkle_info'], 'left_cheek_count'],
['right_cheek', '右脸颊纹', ['right_cheek_wrinkle_info'], 'right_cheek_count']
];
var wrinkleMetrics = [
['wrinkle_score', '皱纹分'], ['wrinkle_severity_level', '严重等级'],
['wrinkle_deep_num', '深纹条数'], ['wrinkle_shallow_num', '浅纹条数'],
['wrinkle_norm_length', '标准化长度'], ['wrinkle_norm_depth', '标准化深度'],
['wrinkle_pixel_density', '像素密度'], ['wrinkle_area_ratio', '面积比例'], ['wrinkle_deep_ratio', '深纹比例']
];
function wrinkleDetails(result) {
result = result || {};
return wrinkleDefs.map(function (def) {
var node = null;
def[2].some(function (key) { if (result[key] && typeof result[key] === 'object') { node = result[key]; return true; } return false; });
var values = {};
wrinkleMetrics.forEach(function (metric) { values[metric[0]] = scalar(node && node[metric[0]]); });
// Region counts are a valid fallback; a missing count is never zero.
if (values.wrinkle_deep_num == null) values.wrinkle_deep_num = scalar(result.wrinkle_count && result.wrinkle_count[def[3]]);
var fine = result.fine_line || result.fine_line_count;
if (values.wrinkle_shallow_num == null) values.wrinkle_shallow_num = scalar(fine && fine[def[3]]);
return { key: def[0], name: def[1], metrics: values };
});
}
function metricText(value) { return value == null ? '未获取' : String(Math.round(value * 10000) / 10000); }
function wrinkleText(rows) {
return '\n分区明细(左右以本人为准;抬头纹与额头纹为同一分区):\n' + rows.map(function (row) {
return row.name + ':' + wrinkleMetrics.map(function (metric) { return metric[1] + ' ' + metricText(row.metrics[metric[0]]); }).join(',') + '。';
}).join('\n');
}
function fill(template, vars) {
return template.replace(/\{([a-z_]+)\}/g, function(_, key) {
var v = vars[key];
@@ -55,9 +96,15 @@
if (vars.sens_area == null) template = template.replace(/泛红面积(?:达 | )\{sens_area\}%[,;]/g, '泛红面积未获取;');
if (vars.pore_cnt == null) template = template.replace(/粗大毛孔 \{pore_cnt\} 颗/g, '毛孔计数未获取');
if (vars.blackhead_cnt == null) template = template.replace(/黑头 \{blackhead_cnt\} 颗/g, '黑头计数未获取');
if (d[0] === 'smooth' && valid && level === '优秀') {
// A high aggregate score does not mean every localized pore is absent.
if (vars.pore_cnt == null || vars.blackhead_cnt == null) template = '肌肤质感整体较好,但毛孔或黑头计数未完整获取,请结合检测图查看。';
else if (vars.pore_cnt > 0 || vars.blackhead_cnt > 0) template = '肌肤质感整体较好;本次检测到粗大毛孔 {pore_cnt} 颗、黑头 {blackhead_cnt} 颗,局部仍有可改善之处。';
}
var details = d[0] === 'wrinkles' ? wrinkleDetails(result) : null;
var advice = level ? Copy.care[d[0]][level] : null;
return { key: d[0], name: d[1], dim: d[2], source: d[3], P: cat.P, level: level || null,
text: valid ? fill(template, vars) : '本次未检出有效数据,请重新拍摄后查看。',
text: (valid ? fill(template, vars) : '本次未检出有效数据,请重新拍摄后查看。') + (details ? wrinkleText(details) : ''),
valid: valid, advice: valid ? advice : null };
});
// Preserve selection of the two weakest original categories. Tone expands into
@@ -67,7 +114,7 @@
var chosen = views.filter(function(v) { return ranked.indexOf(v.source) >= 0 && v.advice; });
var care = chosen.map(function(v) { return { name: v.name, text: v.advice.care + v.advice.lifestyle }; });
var product = products.filter(function(p) { return p.match === vm.essence.name; })[0] || null;
return { version: Copy.version, dims: dimNames, views: views, care: care, product: product };
return { version: Copy.version + '+feedback-2026-10-12', dims: dimNames, views: views, care: care, product: product };
}
return { build: build, products: products, levelMap: levelMap, dimNames: dimNames };
return { build: build, products: products, levelMap: levelMap, dimNames: dimNames, wrinkleDetails: wrinkleDetails };
});
+70
View File
@@ -0,0 +1,70 @@
'use strict';
const assert = require('assert/strict');
const fs = require('fs');
const vm = require('vm');
const path = require('path');
const SkinScore = require('../js/skin-score');
const ReportUI = require('../js/report-ui');
const result = JSON.parse(fs.readFileSync(path.join(__dirname, '../../reference/facepp-skinanalyze/examples/ref_3view_raw.json'))).result;
const details = ReportUI.wrinkleDetails(result);
assert.equal(details.length, 12);
assert.equal(new Set(details.map(row => row.key)).size, 12);
assert.equal(details[0].name, '抬头纹(额头纹)');
assert.deepEqual(details[0].metrics, {
wrinkle_score: 13, wrinkle_severity_level: 1, wrinkle_deep_num: 22, wrinkle_shallow_num: 18,
wrinkle_norm_length: result.forehead_wrinkle_info.wrinkle_norm_length,
wrinkle_norm_depth: result.forehead_wrinkle_info.wrinkle_norm_depth,
wrinkle_pixel_density: result.forehead_wrinkle_info.wrinkle_pixel_density,
wrinkle_area_ratio: result.forehead_wrinkle_info.wrinkle_area_ratio,
wrinkle_deep_ratio: result.forehead_wrinkle_info.wrinkle_deep_ratio
});
assert.equal(details.find(row => row.key === 'left_eye').metrics.wrinkle_deep_num, 58);
assert.equal(details.find(row => row.key === 'right_eye').metrics.wrinkle_shallow_num, 46);
assert.equal(details.find(row => row.key === 'left_cheek').metrics.wrinkle_score, 0);
const alternate = ReportUI.wrinkleDetails({ right_nasolabial_fold_info: { wrinkle_score: { value: 31 }, wrinkle_severity_level: { value: 2 } },
wrinkle_count: { right_nasolabial_count: 5 }, fine_line: { right_nasolabial_count: 0 } });
assert.equal(alternate.find(row => row.key === 'right_nasolabial').metrics.wrinkle_score, 31);
assert.equal(alternate.find(row => row.key === 'right_nasolabial').metrics.wrinkle_deep_num, 5);
assert.equal(alternate.find(row => row.key === 'right_nasolabial').metrics.wrinkle_shallow_num, 0);
assert(Object.values(alternate[0].metrics).every(value => value === null));
const model = SkinScore.buildViewModel(result, SkinScore.MOCK_SIDE, { realAge: 30 });
const modelBefore = JSON.stringify(model), rawBefore = JSON.stringify(result);
const presentation = ReportUI.build(model, result);
const text = presentation.views.find(view => view.key === 'wrinkles').text;
for (const row of details) assert(text.includes(row.name));
assert(text.includes('标准化长度 1.2088'));
assert.equal(JSON.stringify(model), modelBefore);
assert.equal(JSON.stringify(result), rawBefore);
const excellent = JSON.parse(JSON.stringify(model));
excellent.cats.smooth.level = '良好'; // Legacy level name maps to new "优秀".
excellent.cats.smooth.vars.pore_cnt = 7;
excellent.cats.smooth.vars.blackhead_cnt = 2;
let smooth = ReportUI.build(excellent, result).views.find(view => view.key === 'smooth');
assert(smooth.text.includes('粗大毛孔 7 颗、黑头 2 颗'));
assert(!smooth.text.includes('无明显粗大毛孔'));
assert.equal(smooth.level, '优秀');
excellent.cats.smooth.vars.pore_cnt = null;
smooth = ReportUI.build(excellent, result).views.find(view => view.key === 'smooth');
assert(smooth.text.includes('未完整获取'));
const source = fs.readFileSync(path.join(__dirname, '../js/app.js'), 'utf8');
const pure = source.match(/\/\* viz-pure-begin[\s\S]*?\*\/([\s\S]*?)\/\* viz-pure-end \*\//)[1];
const sandbox = {}; vm.runInNewContext(pure, sandbox);
const crop = sandbox.vizFaceCrop(4000, 3000, { left: 1479, top: 1133, width: 1116, height: 1116 });
assert(crop.focused);
assert(crop.width < 1600 && crop.height < 1800);
assert(crop.left <= 1479 && crop.top <= 1133);
assert(crop.left + crop.width >= 2595 && crop.top + crop.height >= 2249);
assert(Math.abs(crop.width / crop.height - 215 / 278) < 1e-8);
for (const rect of [null, { left: NaN }, { left: 0, top: 0, width: -1, height: 20 },
{ left: 3900, top: 0, width: 1000, height: 1000 }, { left: 0, top: 0, width: '100', height: 100 }]) {
const full = sandbox.vizFaceCrop(4000, 3000, rect);
assert.equal(full.focused, false); assert.equal(full.width, 4000);
}
for (const rect of [{ left: 0, top: 0, width: 300, height: 300 }, { left: 700, top: 700, width: 300, height: 300 }]) {
const edge = sandbox.vizFaceCrop(1000, 1000, rect);
assert(edge.focused); assert(edge.left >= 0 && edge.top >= 0);
assert(edge.left + edge.width <= 1000 && edge.top + edge.height <= 1000);
assert(edge.left <= rect.left && edge.top <= rect.top);
assert(edge.left + edge.width >= rect.left + rect.width && edge.top + edge.height >= rect.top + rect.height);
}
console.log('PASS: 12 unique wrinkle regions, all nine real metrics, aliases/count fallbacks, missing≠zero; positive pore counts vs excellent score; no raw/model/algorithm mutations; face crop/aspect/edge containment/invalid-box fallback');
+110
View File
@@ -0,0 +1,110 @@
// Read-only mock page. No account creation, report writes or Face++ call.
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 browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true });
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.goto(base + '?mock=1&screen=report');
await page.waitForFunction(() => document.querySelector('#stat-score b').textContent !== '--' && [...document.querySelectorAll('.ditem-img')].every(i => !i.hidden && i.naturalWidth));
const cdp = await page.context().newCDPSession(page);
async function swipe(card, dx, dy = 0) {
const frame = card.locator('.dpanel.active .viz-frame');
await frame.scrollIntoViewIfNeeded();
const box = await frame.boundingBox(), x = box.x + box.width / 2, y = Math.min(500, box.y + box.height / 2);
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x, y }] });
for (let i = 1; i <= 8; i++) {
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x: x + dx * i / 8, y: y + dy * i / 8 }] });
}
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] });
await page.waitForTimeout(300);
}
const cards = page.locator('.card-detail');
await swipe(cards.nth(0), -85);
assert.equal(await cards.nth(0).locator('.dtab.active').getAttribute('data-tab'), '1');
assert.equal(await page.locator('#p-zoom.show').count(), 0);
await swipe(cards.nth(0), 85);
assert.equal(await cards.nth(0).locator('.dtab.active').getAttribute('data-tab'), '0');
await swipe(cards.nth(0), 0, -85);
assert.equal(await cards.nth(0).locator('.dtab.active').getAttribute('data-tab'), '0', 'vertical scrolling must not change tab');
await swipe(cards.nth(1), -85);
assert.equal(await cards.nth(1).locator('.dtab.active').getAttribute('data-tab'), '1');
await swipe(cards.nth(1), -85);
assert.equal(await cards.nth(1).locator('.dtab.active').getAttribute('data-tab'), '2');
await swipe(cards.nth(1), 85);
await cards.nth(1).locator('.dtab').first().click();
assert.equal(await cards.nth(1).locator('.dtab.active').getAttribute('data-tab'), '0');
await cards.nth(1).locator('.dtab').first().focus();
await page.keyboard.press('ArrowRight');
assert.equal(await cards.nth(1).locator('.dtab.active').getAttribute('data-tab'), '1');
await swipe(cards.nth(2), -85);
assert.equal(await cards.nth(2).locator('.dtab.active').getAttribute('data-tab'), '1');
assert.equal(await cards.nth(0).locator('.dtab.active').getAttribute('data-tab'), '0');
const image = cards.nth(0).locator('.dpanel.active .ditem-img');
await image.click();
await page.waitForSelector('#p-zoom.show');
assert.equal(await page.locator('#zoom-img').getAttribute('src'), await image.getAttribute('src'));
await page.click('#p-zoom');
await page.waitForSelector('#p-zoom.show', { state: 'hidden' });
await page.waitForTimeout(300);
assert((await page.locator('#ditem-para2').textContent()).includes('抬头纹(额头纹)'));
assert((await page.locator('#ditem-para2').textContent()).includes('右法令纹'));
assert((await page.locator('#smooth-map-note').textContent()).includes('毛孔'));
const focused = await page.locator('.ditem-img').evaluateAll(images => images.map(i => ({ focused: i.dataset.crop, ratio: i.naturalWidth / i.naturalHeight })));
assert(focused.every(i => i.focused === 'face' && Math.abs(i.ratio - 215 / 278) < .005));
const fontSizes = [];
for (const width of [320, 375, 390, 414, 480]) {
await page.setViewportSize({ width, height: 844 });
await page.waitForTimeout(150);
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), 'page overflow ' + width);
fontSizes.push(await page.locator('#tx-tone').evaluate(e => getComputedStyle(e).fontSize));
assert.equal(await page.locator('#product-name .product-line').count(), 2);
for (const line of await page.locator('#product-name .product-line').all()) {
assert.equal(await line.evaluate(e => getComputedStyle(e).whiteSpace), 'nowrap');
assert(await line.evaluate(e => e.scrollWidth <= e.clientWidth), 'product line overflow ' + width);
}
}
assert(fontSizes.every(size => size === '12px'), JSON.stringify(fontSizes));
assert.equal(await page.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--yellow').trim()), '#F5F078');
if (process.env.H5_ARTIFACT_DIR) {
fs.mkdirSync(process.env.H5_ARTIFACT_DIR, { recursive: true });
await page.setViewportSize({ width: 375, height: 726 });
await image.scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, '01-face-focused.png'), animations: 'disabled' });
await cards.nth(0).locator('.dtab').nth(1).click();
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, '02-wrinkle-regions.png'), animations: 'disabled' });
await cards.nth(1).locator('.dtab').nth(1).click();
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, '03-smooth-maps.png'), animations: 'disabled' });
await page.locator('.card-product').scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, '04-product-lines.png'), animations: 'disabled' });
}
// Change only the in-browser fixture. Exercise the actual renderer and age-only
// recommendation outside boundary windows, without production report writes.
for (const [age, name, tier] of [[40, '以我自在有势挺括', 'II'], [60, '以我从容有态构塑', 'III']]) {
await page.route('**/assets/mock/report-fixture.js', async route => {
const response = await route.fetch();
await route.fulfill({ response, body: await response.text() + '\nMOCK_FIXTURE.result.skin_age = { value: ' + age + ' };\ndelete MOCK_FIXTURE.result.face_maps.texture_enhanced_blackheads;' });
});
await page.goto(base + '?mock=1&screen=report');
await page.waitForFunction(() => document.querySelector('#product-name .product-line'));
assert.equal(await page.locator('#product-title').textContent(), "1'me 主场精华 " + tier);
assert.equal(await page.locator('#product-name .product-line').first().textContent(), name);
assert(!(await page.locator('#smooth-map-note').textContent()).includes('红色'), 'missing blackhead map must not be claimed');
for (const width of [320, 375, 390, 414, 480]) {
await page.setViewportSize({ width, height: 844 });
for (const line of await page.locator('#product-name .product-line').all()) {
assert(await line.evaluate(e => e.scrollWidth <= e.clientWidth), 'tier ' + tier + ' product line overflow ' + width);
}
assert(await page.locator('#product-title').evaluate(e => e.scrollWidth <= e.clientWidth), 'tier ' + tier + ' title overflow ' + width);
}
await page.unroute('**/assets/mock/report-fixture.js');
}
assert.deepEqual(errors, []);
console.log('PASS: real mobile swipes in three independent groups, vertical scroll vs swipe, click/keyboard tabs, no zoom after swipe, zoom still works, seven face-focused maps, 12-region copy, fixed fonts/all three product lines in five widths, partial map caption, PSD yellow, no JS errors');
} finally { await browser.close(); }