feat(h5): apply October 12 customer report feedback
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 315 KiB |
|
Before Width: | Height: | Size: 405 KiB After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 235 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 428 KiB |
@@ -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 或后台访问保护。部署实测结果在完成后追加。
|
||||
@@ -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 @@
|
||||
{"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"}
|
||||
@@ -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._
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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%; } }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 };
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
@@ -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(); }
|
||||