feat(h5): refine report layout, add finger-following tabs and optimize images
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"description": "Smaller product-card delivery images. Original PSD crops preserved.",
|
||||
"retained": "Background/scan WebPs are smaller than JPEG; transparent icons/masks remain PNG.",
|
||||
"assets": [
|
||||
{
|
||||
"source": "h5/assets/img/ui-20261012/product-25.webp",
|
||||
"source_sha256": "c18827342d23bbff2de04c3a2ecbeeae9b16eb96f3a9f7e30f2a66cdc21beddc",
|
||||
"source_bytes": 276778,
|
||||
"file": "h5/assets/img/ui-20261012/product-25.jpg",
|
||||
"sha256": "2c399cd6bd7bd783f698a91b9203a7ba42b92295d58bbeb0b1c2a884e862f00b",
|
||||
"bytes": 63959,
|
||||
"size": [
|
||||
504,
|
||||
503
|
||||
],
|
||||
"quality": 90,
|
||||
"subsampling": 0,
|
||||
"progressive": true,
|
||||
"matte": "#FFFFFF",
|
||||
"psnr_db": 40.98
|
||||
},
|
||||
{
|
||||
"source": "h5/assets/img/ui-20261012/product-35.webp",
|
||||
"source_sha256": "c1dc455c8072da17ac06cee386248b358947da65bd190b714f2ccb25360afea4",
|
||||
"source_bytes": 267094,
|
||||
"file": "h5/assets/img/ui-20261012/product-35.jpg",
|
||||
"sha256": "ab4c5623235020b1129f564b733fe843c539088e7b62b2d38ed42a204e4af113",
|
||||
"bytes": 66077,
|
||||
"size": [
|
||||
504,
|
||||
503
|
||||
],
|
||||
"quality": 90,
|
||||
"subsampling": 0,
|
||||
"progressive": true,
|
||||
"matte": "#FFFFFF",
|
||||
"psnr_db": 41.47
|
||||
},
|
||||
{
|
||||
"source": "h5/assets/img/ui-20261012/product-50.webp",
|
||||
"source_sha256": "86647c8882caf62c43eafb1efe495bad3289126cab73a88c8963b7ad64ffa084",
|
||||
"source_bytes": 331924,
|
||||
"file": "h5/assets/img/ui-20261012/product-50.jpg",
|
||||
"sha256": "78687985751afbf60c374f2605dee372ac64ffc5271aa54ddca7897443de3047",
|
||||
"bytes": 153787,
|
||||
"size": [
|
||||
504,
|
||||
503
|
||||
],
|
||||
"quality": 96,
|
||||
"subsampling": 0,
|
||||
"progressive": true,
|
||||
"matte": "#FFFFFF",
|
||||
"psnr_db": 41.94
|
||||
}
|
||||
],
|
||||
"source_bytes": 875796,
|
||||
"delivery_bytes": 283823
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
# 报告布局与跟手交互调整
|
||||
|
||||
## 本次范围
|
||||
|
||||
1. 综合评分:标题移到黄色圆环外正上方,与「生物可视年龄」同高;分数数字本身水平、垂直居中,「分」独立放在数字右侧,不把分数挤偏。
|
||||
2. 护理建议:标题左对齐,其他文案不变。
|
||||
3. 检测等待:回退实拍背景,使用原 PSD 白色人头及黄色扫描动画,不出现用户照片。保留约 1 秒一轮、成功至少播放一轮、取消/失败立即退出,以及素材失败时的文字状态和取消入口。
|
||||
4. 拍摄震动:复核现有功能。前端判断距离、居中、角度、光线等通过、进入「保持不动」时请求 `[70,40,70]` 毫秒短振;同一步抖动重试最多每两秒提醒一次,自动快门不重复震动。不支持、返回 false 或抛异常都静默跳过,不阻碍拍摄。是否实际震动仍取决于手机和浏览器,自动化不代替物理真机验证。
|
||||
5. 报告三组页签:与「日常肤质」共用轻量滑动控制器。拖动时内容随手指平移,松手按距离/速度吸附;支持短拖回弹、轻扫、首尾阻力、系统取消和旋转屏幕。保持点按/键盘切换、上下滚动、图片点按放大;拖动不误触放大。各卡片独立,容器高度跟随当前内容,非当前页签从辅助阅读/焦点导航中排除。
|
||||
6. JPG 优化:只转换在实际展示背景上可明显减小体积的三张产品图,原始 WebP、PSD 和原导出清单完整保留。背景和扫描底图仍用更小的 WebP;透明 logo、图标和蒙版保留 PNG。送检照片原本就是 JPEG,本次不二次压缩、不改变检测输入。
|
||||
|
||||
## 资源体积
|
||||
|
||||
| 产品 | 原始 WebP | 网页使用 JPG | JPG 质量 |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| 25+ | 276,778 B | 63,959 B | 90 |
|
||||
| 35+ | 267,094 B | 66,077 B | 90 |
|
||||
| 50+ | 331,924 B | 153,787 B | 96 |
|
||||
| 合计 | 875,796 B | 283,823 B | 减少 67.6% |
|
||||
|
||||
三张图保持 504×503,白色产品卡底色合成,渐进式 JPEG、4:4:4 色度采样,PSNR 均不低于 40dB。页面只加载实际推荐的一款。构建脚本 `tools/optimize_delivery_images.py` 可重复生成;原图/新图的 SHA-256、体积、尺寸与参数记录于 `design/ui-20261012/delivery-optimization-20261012.json`。
|
||||
|
||||
## 验证
|
||||
|
||||
本地 8080 服务运行后执行:
|
||||
|
||||
```bash
|
||||
node h5/test/report-touch-layout.mjs
|
||||
node h5/test/scan-placeholder.mjs
|
||||
node h5/test/profile-interaction.mjs
|
||||
node h5/test/customer-feedback.mjs
|
||||
node h5/test/customer-feedback-second.js
|
||||
node h5/test/customer-feedback-second.mjs
|
||||
node h5/test/customer-feedback-third.mjs
|
||||
node h5/test/customer-feedback-fourth.js
|
||||
node h5/test/customer-feedback-fourth.mjs
|
||||
node h5/test/capture-quality-browser.mjs
|
||||
node h5/test/report-persistence.mjs
|
||||
node h5/test/login-performance.mjs
|
||||
node h5/test/recommendation.js
|
||||
node h5/test/regression.js
|
||||
node h5/test/customer-feedback.js
|
||||
node h5/test/profile-store.js
|
||||
node h5/test/analyze-timeout.js
|
||||
```
|
||||
|
||||
以上均通过。覆盖五种报告宽度/六种扫描视口、真实移动端触摸拖动中位移、三组吸附与高度、纵向滚动、放大/键盘、原稿素材哈希、白色人头、扫描完整循环和失败降级、实际本地人脸模型自动三连拍、账号隔离/历史报告/前后台一致性,以及评分和推荐规则回归。
|
||||
|
||||
慢网性能模拟:150ms 延迟、约 2Mbps 下载、4 倍 CPU 降速,测试冷启动登录约 1.68 秒,首屏资源 271,106 B;字体、人脸识别、产品和扫描图仍不竞争登录首屏。这是本地模拟测量,不等同于所有真机的公网耗时。
|
||||
|
||||
本次未修改拍摄合格阈值、评分算法、推荐规则、报告文案、登录或保存逻辑。未追踪的 `new_feedback/` 保持原样,不纳入本次提交。
|
||||
|
||||
发布目标为 `ubuntu@1me.xajuzi.com:/home/ubuntu/juzi`,服务 `juzi-h5.service`。沿用 Git bundle 快进同步,不代用户推送 origin,不更改服务器环境配置或历史数据;上线后核对静态文件哈希、服务健康和匿名访问保护,并通过公网执行只读模拟账号浏览器测试。
|
||||
@@ -1,6 +1,8 @@
|
||||
# 报告等待页实拍背景
|
||||
|
||||
原扫描页直接展示 PSD 的白色人脸占位。改为在原 PSD 人脸蒙版内叠加本次正脸照片,扫描线置于照片上方;原素材不重绘,框、检测中文字、装饰图标与取消入口不变。
|
||||
> 此文档记录上一版实现,已按后续用户要求回退:等待页不再展示用户照片,仅保留 PSD 白色人头和扫描动画。当前验证脚本为 `h5/test/scan-placeholder.mjs`。
|
||||
|
||||
原扫描页直接展示 PSD 的白色人脸占位。上一版改为在原 PSD 人脸蒙版内叠加本次正脸照片,扫描线置于照片上方;原素材不重绘,框、检测中文字、装饰图标与取消入口不变。
|
||||
|
||||
- 首张合格正脸就预解码,三张拍完使用同一张正脸,不使用最后一张侧脸或其他报告照片。
|
||||
- 与拍摄预览一样居中、等比 `cover` 铺满,不拉伸照片。前置实时拍摄保留当时的预览镜像;后置/相册/系统相机文件不额外镜像。镜像只作用于 CSS,不改变送检 JPEG。
|
||||
@@ -9,13 +11,13 @@
|
||||
- 素材或蒙版加载失败时降级为实拍背景、检测中文字和取消入口;照片解码失败使用深色背景,不露出白色占位。
|
||||
- 不修改拍摄约束、评分、肌龄、推荐规则或报告保存逻辑。
|
||||
|
||||
验证命令(本地服务需运行于 8080):
|
||||
当前回退版验证命令(本地服务需运行于 8080):
|
||||
|
||||
```bash
|
||||
node h5/test/scan-photo.mjs
|
||||
node h5/test/scan-placeholder.mjs
|
||||
node h5/test/capture-quality-browser.mjs
|
||||
node h5/test/customer-feedback-third.mjs
|
||||
node h5/test/customer-feedback-fourth.mjs
|
||||
```
|
||||
|
||||
测试只使用模拟账号、拦截分析响应,不创建线上记录、不调用付费检测。覆盖本次正脸照片、前置镜像与文件不镜像、六种视口、扫描完整循环、成功/失败/取消清理、迟到响应及素材失败降级。
|
||||
测试只使用模拟账号、拦截分析响应,不创建线上记录、不调用付费检测。当前脚本验证白色人头、不展示用户照片、六种视口、扫描完整循环、成功/失败/取消清理、迟到响应及素材失败降级。
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"2": "app.js",
|
||||
"3": "skin-score.js",
|
||||
"4": "customer-feedback-second.js",
|
||||
"5": "f",
|
||||
"5": "n",
|
||||
"6": "ComputeAllFaceDescriptorsTask",
|
||||
"7": "test/profile-store.js",
|
||||
"8": "admin.js",
|
||||
@@ -21,11 +21,11 @@
|
||||
"19": "customer-feedback-fourth.js",
|
||||
"20": "build_fixtures.py",
|
||||
"21": "disposeUnusedWeightTensors",
|
||||
"22": "AgeGenderNet",
|
||||
"22": ".loadFromDisk",
|
||||
"23": "facepp-skinanalyze(旷视皮肤分析专业版调用技能)",
|
||||
"24": "巨子测肤 · 1'me 语音播报内容设计文档 V1.0",
|
||||
"25": "mobile-select.iife.min.js",
|
||||
"26": ".extractWeights",
|
||||
"26": "NeuralNetwork",
|
||||
"27": "gen_voice.py",
|
||||
"28": "C",
|
||||
"29": "face-api.js 本地化依赖(h5/vendor/faceapi/)",
|
||||
@@ -40,9 +40,9 @@
|
||||
"38": "FaceLandmark68Net",
|
||||
"39": "UI交互界面调整",
|
||||
"40": "第四轮反馈修改与验收",
|
||||
"41": "NeuralNetwork",
|
||||
"41": ".forwardInput",
|
||||
"42": "layer_at",
|
||||
"43": "convLayer",
|
||||
"43": "AgeGenderNet",
|
||||
"44": "start-local.sh",
|
||||
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
||||
@@ -57,27 +57,27 @@
|
||||
"55": "i",
|
||||
"56": "report-ui.js",
|
||||
"57": "regression.js",
|
||||
"58": ".dispose",
|
||||
"58": ".forwardInput",
|
||||
"59": "call_skinanalyze.sh",
|
||||
"60": "report-fixture.js",
|
||||
"61": "FaceProcessor",
|
||||
"61": "extractAllFacesAndComputeResults",
|
||||
"62": "提交前 Review(2026-10-11)",
|
||||
"63": "e2e-browser.mjs",
|
||||
"64": "account-client.js",
|
||||
"64": "start",
|
||||
"65": "微信登录接入与公网部署说明",
|
||||
"66": "FaceLandmarks",
|
||||
"67": "ui-20261009.mjs",
|
||||
"68": "analyze-timeout.js",
|
||||
"69": "misans/README.md",
|
||||
"70": "normalize",
|
||||
"71": "extractAllFacesAndComputeResults",
|
||||
"70": "scan-placeholder.mjs",
|
||||
"71": ".predictAgeAndGender",
|
||||
"72": "build_report_copy.py",
|
||||
"73": "camera-geometry.js",
|
||||
"74": "report-copy.js",
|
||||
"75": "prepare_h5_assets.py",
|
||||
"76": "doCapture",
|
||||
"76": "onPhotoCaptured",
|
||||
"77": ".runAndExtendWithFaceDetections",
|
||||
"78": "n",
|
||||
"78": ".toString",
|
||||
"79": "登录反馈与加载优化(2026-10-12)",
|
||||
"80": "线上微信登录部署记录",
|
||||
"81": "wechat-production.mjs",
|
||||
@@ -85,7 +85,7 @@
|
||||
"83": "10.09 新版 H5 实施与验收",
|
||||
"84": "微信登录联调:2026-10-12",
|
||||
"85": "test/accounts.js",
|
||||
"86": "FaceExpressionNet",
|
||||
"86": "report-touch-layout.mjs",
|
||||
"87": "产品推荐规则:肤龄基准与边界调整",
|
||||
"88": "ui-20261012/manifest.json",
|
||||
"89": "傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
@@ -98,10 +98,10 @@
|
||||
"96": "production-loop.mjs",
|
||||
"97": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
||||
"98": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
|
||||
"99": "PredictSingleFaceExpressionsTask",
|
||||
"100": "age",
|
||||
"99": "DetectSingleFaceLandmarksTask",
|
||||
"100": "ComputeSingleFaceDescriptorTask",
|
||||
"101": "drawFaceExpressions",
|
||||
"102": ".detect",
|
||||
"102": "extractFaceTensors",
|
||||
"103": "第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
"104": "export_fourth_feedback_assets.py",
|
||||
"105": "capture-quality-browser.mjs",
|
||||
@@ -109,24 +109,11 @@
|
||||
"107": "基础信息记忆与肤质拖动",
|
||||
"108": "profile-interaction.mjs",
|
||||
"109": "submitPhotos",
|
||||
"110": "startCamera",
|
||||
"111": "e",
|
||||
"110": "doCapture",
|
||||
"111": "k",
|
||||
"112": "showScreen",
|
||||
"113": "Point",
|
||||
"114": "initialize",
|
||||
"115": "FaceLandmarks68",
|
||||
"116": ".forwardInput",
|
||||
"117": "ao",
|
||||
"118": "k",
|
||||
"119": ".div",
|
||||
"120": "restoreProfile",
|
||||
"121": "TinyFaceFeatureExtractor",
|
||||
"122": "isValidNumber",
|
||||
"123": "tt",
|
||||
"113": "swipe-track.js",
|
||||
"114": "optimize_delivery_images.py",
|
||||
"124": "TinyYolov2",
|
||||
"125": "报告等待页实拍背景-2026-10-12.md",
|
||||
"126": "FaceDetection",
|
||||
"127": "ObjectDetection",
|
||||
"128": "PredictAllFaceExpressionsTask",
|
||||
"129": "PredictSingleAgeAndGenderTask"
|
||||
"125": "报告等待页实拍背景-2026-10-12.md"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"0": "f8dacdda1d052754", "1": "cb6ea50bd4e4a5ca", "2": "8fcc50e5e52e3ab3", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "0a7b6d40b794bd19", "6": "3dd68cce72af5314", "7": "03b56b13a08b5f83", "8": "5838caf1c500a4b4", "9": "4a86450ff5444c44", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "8a30574014478f1f", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "59485c317afc6c2b", "22": "f2045a9940596eb3", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "599f23689a2b8aca", "27": "eab427ce9a5219e9", "28": "5f977d8429c39f6a", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "241be5abeab1da6f", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "d96493a5f110bd4c", "42": "916170b9f5d7346b", "43": "e649ddeadda89cbb", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "b2c8c091044064e6", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "0c5797ecc09be26d", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "94ead9feda213ac6", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "bdee3c4a37df52f8", "66": "b0afc4d04f42150f", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "cc7621a6b501b8a2", "71": "5757de52275f3382", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "cdeb7b925ce5043d", "77": "2ee521497bb8506a", "78": "7e842e2bd8e95fd4", "79": "86f177bf0a7dd07c", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "2fb17901aa87e0e7", "85": "4bc370be8afd7db1", "86": "e858d75e995c92e5", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "664f8b17c5d7a9aa", "92": "fced244d93677d90", "93": "8bd61518dd267641", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "cb5ac3401283c378", "100": "b234c5e4ac7f632d", "101": "a0bc88eb14b70170", "102": "96b578f45b8bad5e", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "0567be4dc8048b9c", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91", "109": "930d097b73cefa7c", "110": "86ccefb4c68b732b", "111": "ede7733e4a295879", "112": "71cbe5098f4c1b6f", "113": "c65d8fa08757ee48", "114": "1f03fe34df693fcc", "115": "68c52c80f38f8b2c", "116": "4b58c0e8a44f5aec", "117": "8e5e97153c881130", "118": "d61ef5e4abc4bb10", "119": "39b5f1f56c2c2553", "120": "2a08cdb1d7133600", "121": "e7265770f07fa066", "122": "8f647d3ce08ecd2b", "123": "3088ae48043ee39d", "124": "29790cf710874dd0", "125": "e4f29216745ec758", "126": "287d60115d9acda8", "127": "ee7017d224ec9ad6", "128": "e800d832c255899a", "129": "47128fbd98c3ce4d"}
|
||||
{"0": "dcb526d7f5862ca1", "1": "cb6ea50bd4e4a5ca", "2": "3427a6e8a9c7cb3e", "3": "bcbfbc0b66f44357", "4": "56bf12952b0de42b", "5": "9c2200b016047a0f", "6": "3dd68cce72af5314", "7": "417f570ec3320aca", "8": "a689fcb62e7754af", "9": "78f24ff96d971d81", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "24564b251581a338", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "b53e90dd83caf582", "22": "be2f6d3dcfc9aabd", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "def3f7743d0ffa30", "27": "eab427ce9a5219e9", "28": "9fae77ec21097d72", "29": "c608f1c6492e1cb1", "30": "73c13793fca09f56", "31": "fbbcfc059751cc6a", "32": "f6bcf56c4589316b", "33": "75f750c5b7d9f63d", "34": "f024ff7942935690", "35": "241be5abeab1da6f", "36": "a7d375041779bad4", "37": "cb8f26249485324a", "38": "9a25b1230b5fe649", "39": "fdd95375b4a5ddb3", "40": "5e546676ab96de03", "41": "880a74c6d255e5cc", "42": "916170b9f5d7346b", "43": "68c816ca169c7a23", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "0148452bf4e4efe0", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "f1a11f57dc1f84a8", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "0e4124a270816f7a", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "a01bccd7f7c24c78", "65": "bdee3c4a37df52f8", "66": "04df478e971b7941", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "be0c8310abda2467", "71": "b6cde9f25b294afc", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "50a4503d0a778ed2", "77": "2ee521497bb8506a", "78": "42c9bc548e269a96", "79": "0746e33eaf6cd1f9", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "4bc370be8afd7db1", "86": "386c6fcc7be918e0", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "5a9014c4a7f87699", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "23f1d406f6c779ae", "100": "7ac475ea9bc280f8", "101": "a0bc88eb14b70170", "102": "74e90d27271154a1", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "0567be4dc8048b9c", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91", "109": "2c27e5c32bc10822", "110": "b7f77cb02f68832b", "111": "6cc4ed7ee9712ccc", "112": "71cbe5098f4c1b6f", "113": "0d99f86b9e41c4bb", "114": "b9d7c266afa84437", "124": "29790cf710874dd0", "125": "e4f29216745ec758"}
|
||||
+111
-154
@@ -1,16 +1,16 @@
|
||||
# Graph Report - juzi (2026-10-12)
|
||||
|
||||
## Corpus Check
|
||||
- 99 files · ~4,892,361 words
|
||||
- 103 files · ~4,904,011 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1431 nodes · 2879 edges · 130 communities (98 shown, 32 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 200 edges (avg confidence: 0.56)
|
||||
- 1439 nodes · 2888 edges · 117 communities (93 shown, 24 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 209 edges (avg confidence: 0.57)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `ce6dbc20`
|
||||
- Built from commit: `bcf198ca`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- app.js
|
||||
- skin-score.js
|
||||
- customer-feedback-second.js
|
||||
- f
|
||||
- n
|
||||
- ComputeAllFaceDescriptorsTask
|
||||
- test/profile-store.js
|
||||
- admin.js
|
||||
@@ -37,11 +37,11 @@
|
||||
- customer-feedback-fourth.js
|
||||
- build_fixtures.py
|
||||
- disposeUnusedWeightTensors
|
||||
- AgeGenderNet
|
||||
- .loadFromDisk
|
||||
- facepp-skinanalyze(旷视皮肤分析专业版调用技能)
|
||||
- 巨子测肤 · 1'me 语音播报内容设计文档 V1.0
|
||||
- mobile-select.iife.min.js
|
||||
- .extractWeights
|
||||
- NeuralNetwork
|
||||
- gen_voice.py
|
||||
- C
|
||||
- face-api.js 本地化依赖(h5/vendor/faceapi/)
|
||||
@@ -54,9 +54,9 @@
|
||||
- FaceLandmark68Net
|
||||
- UI交互界面调整
|
||||
- 第四轮反馈修改与验收
|
||||
- NeuralNetwork
|
||||
- .forwardInput
|
||||
- layer_at
|
||||
- convLayer
|
||||
- AgeGenderNet
|
||||
- start-local.sh
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
||||
@@ -69,23 +69,23 @@
|
||||
- i
|
||||
- report-ui.js
|
||||
- regression.js
|
||||
- .dispose
|
||||
- .forwardInput
|
||||
- call_skinanalyze.sh
|
||||
- FaceProcessor
|
||||
- extractAllFacesAndComputeResults
|
||||
- 提交前 Review(2026-10-11)
|
||||
- e2e-browser.mjs
|
||||
- account-client.js
|
||||
- start
|
||||
- 微信登录接入与公网部署说明
|
||||
- FaceLandmarks
|
||||
- ui-20261009.mjs
|
||||
- analyze-timeout.js
|
||||
- misans/README.md
|
||||
- normalize
|
||||
- extractAllFacesAndComputeResults
|
||||
- scan-placeholder.mjs
|
||||
- .predictAgeAndGender
|
||||
- build_report_copy.py
|
||||
- doCapture
|
||||
- onPhotoCaptured
|
||||
- .runAndExtendWithFaceDetections
|
||||
- n
|
||||
- .toString
|
||||
- 登录反馈与加载优化(2026-10-12)
|
||||
- 线上微信登录部署记录
|
||||
- wechat-production.mjs
|
||||
@@ -93,7 +93,7 @@
|
||||
- 10.09 新版 H5 实施与验收
|
||||
- 微信登录联调:2026-10-12
|
||||
- test/accounts.js
|
||||
- FaceExpressionNet
|
||||
- report-touch-layout.mjs
|
||||
- 产品推荐规则:肤龄基准与边界调整
|
||||
- ui-20261012/manifest.json
|
||||
- 傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
@@ -105,35 +105,20 @@
|
||||
- production-loop.mjs
|
||||
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
||||
- PredictSingleFaceExpressionsTask
|
||||
- age
|
||||
- DetectSingleFaceLandmarksTask
|
||||
- ComputeSingleFaceDescriptorTask
|
||||
- drawFaceExpressions
|
||||
- .detect
|
||||
- extractFaceTensors
|
||||
- 第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- 第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- 基础信息记忆与肤质拖动
|
||||
- profile-interaction.mjs
|
||||
- submitPhotos
|
||||
- startCamera
|
||||
- e
|
||||
- showScreen
|
||||
- Point
|
||||
- initialize
|
||||
- FaceLandmarks68
|
||||
- .forwardInput
|
||||
- ao
|
||||
- doCapture
|
||||
- k
|
||||
- .div
|
||||
- restoreProfile
|
||||
- TinyFaceFeatureExtractor
|
||||
- isValidNumber
|
||||
- tt
|
||||
- showScreen
|
||||
- TinyYolov2
|
||||
- 报告等待页实拍背景-2026-10-12.md
|
||||
- FaceDetection
|
||||
- ObjectDetection
|
||||
- PredictAllFaceExpressionsTask
|
||||
- PredictSingleAgeAndGenderTask
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `__awaiter()` - 59 edges
|
||||
@@ -150,55 +135,55 @@
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `actualLines()` --indirect_call--> `el()` [INFERRED]
|
||||
h5/test/customer-feedback-third.mjs → h5/js/admin.js
|
||||
- `browser()` --indirect_call--> `key()` [INFERRED]
|
||||
h5/js/account-client.js → h5/js/profile-store.js
|
||||
- `init()` --indirect_call--> `key()` [INFERRED]
|
||||
h5/js/account-client.js → h5/js/profile-store.js
|
||||
- `capture()` --indirect_call--> `el()` [INFERRED]
|
||||
h5/test/scan-photo.mjs → h5/js/admin.js
|
||||
- `loadRecords()` --indirect_call--> `it()` [INFERRED]
|
||||
h5/js/admin.js → h5/vendor/faceapi/face-api.js
|
||||
- `bind()` --indirect_call--> `a()` [INFERRED]
|
||||
h5/js/admin.js → h5/vendor/faceapi/face-api.js
|
||||
- `checkStill()` --indirect_call--> `d()` [INFERRED]
|
||||
h5/js/app.js → h5/vendor/faceapi/face-api.js
|
||||
- `detectFrame()` --indirect_call--> `d()` [INFERRED]
|
||||
h5/js/app.js → h5/vendor/faceapi/face-api.js
|
||||
|
||||
## Import Cycles
|
||||
- None detected.
|
||||
|
||||
## Communities (130 total, 32 thin omitted)
|
||||
## Communities (117 total, 24 thin omitted)
|
||||
|
||||
### Community 0 - "face-api.js"
|
||||
Cohesion: 0.02
|
||||
Nodes (36): cn(), computeReshapedDimensions(), decodeBoxesLayer(), dr(), en(), extractBoundingBoxes(), ff(), ge() (+28 more)
|
||||
Nodes (46): bgrToRgbTensor(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFaceDetectionNet(), createFileSystem(), createNodejsEnv(), createSsdMobilenetv1() (+38 more)
|
||||
|
||||
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
||||
Cohesion: 0.25
|
||||
Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测试(不含真实微信授权), 结论, 话术及仓库同步, 验证边界
|
||||
|
||||
### Community 2 - "app.js"
|
||||
Cohesion: 0.12
|
||||
Nodes (21): decode(), drain(), esc(), load(), openHistory(), preload(), renderHistoryList(), renderReport() (+13 more)
|
||||
Cohesion: 0.11
|
||||
Nodes (22): cancelSkinDrag(), esc(), goSkin(), openHistory(), renderHistoryList(), renderReport(), renderViz(), restartScanCycle() (+14 more)
|
||||
|
||||
### Community 3 - "skin-score.js"
|
||||
Cohesion: 0.12
|
||||
Cohesion: 0.11
|
||||
Nodes (45): acneGrade(), buildViewModel(), calcAcne(), calcContour(), calcSens(), calcSmooth(), calcTone(), calcWrinkles() (+37 more)
|
||||
|
||||
### Community 4 - "customer-feedback-second.js"
|
||||
Cohesion: 0.17
|
||||
Nodes (10): assert, context, face, fs, geometry, original, path, source (+2 more)
|
||||
|
||||
### Community 5 - "f"
|
||||
Cohesion: 0.23
|
||||
Nodes (21): a(), bf(), cf(), d(), dt(), ea(), f(), g() (+13 more)
|
||||
### Community 5 - "n"
|
||||
Cohesion: 0.12
|
||||
Nodes (48): a(), ae(), be(), bf(), ce(), cf(), d(), de() (+40 more)
|
||||
|
||||
### Community 7 - "test/profile-store.js"
|
||||
Cohesion: 0.14
|
||||
Nodes (13): assert, data, empty, now, other, Profile, quota, regions (+5 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (21): browser(), init(), read(), rememberKind(), request(), clean(), create(), key() (+13 more)
|
||||
|
||||
### Community 8 - "admin.js"
|
||||
Cohesion: 0.14
|
||||
Nodes (33): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+25 more)
|
||||
Cohesion: 0.20
|
||||
Nodes (29): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+21 more)
|
||||
|
||||
### Community 9 - "__awaiter"
|
||||
Cohesion: 0.14
|
||||
Nodes (12): __awaiter(), ComposableTask(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), getModelUris() (+4 more)
|
||||
Cohesion: 0.17
|
||||
Nodes (10): __awaiter(), extendWithFaceExpressions(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator(), loadWeightMap() (+2 more)
|
||||
|
||||
### Community 10 - "wechat-browser.mjs"
|
||||
Cohesion: 0.22
|
||||
@@ -217,8 +202,8 @@ Cohesion: 0.12
|
||||
Nodes (15): files, analyze-fail, analyzing, countdown-321, hint-far, hint-front, hint-hold, hint-near (+7 more)
|
||||
|
||||
### Community 15 - "extractFaces"
|
||||
Cohesion: 0.14
|
||||
Nodes (17): awaitMediaLoaded(), Box(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow() (+9 more)
|
||||
Cohesion: 0.21
|
||||
Nodes (17): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+9 more)
|
||||
|
||||
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
|
||||
Cohesion: 0.15
|
||||
@@ -242,7 +227,11 @@ Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), p
|
||||
|
||||
### Community 21 - "disposeUnusedWeightTensors"
|
||||
Cohesion: 0.10
|
||||
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.20
|
||||
Nodes (5): ComposableTask(), getModelUris(), Gf(), Kf(), TinyFaceDetector()
|
||||
|
||||
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
||||
Cohesion: 0.20
|
||||
@@ -256,17 +245,17 @@ Nodes (8): 1. 音频格式约定(已定), 2. 播报清单(14 条), 3. 播放策
|
||||
Cohesion: 0.20
|
||||
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
|
||||
|
||||
### Community 26 - ".extractWeights"
|
||||
Cohesion: 0.09
|
||||
Nodes (24): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+16 more)
|
||||
### Community 26 - "NeuralNetwork"
|
||||
Cohesion: 0.07
|
||||
Nodes (26): createFaceRecognitionNet(), createMtcnn(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory(), extractorsFactory(), extractorsFactory$1() (+18 more)
|
||||
|
||||
### Community 27 - "gen_voice.py"
|
||||
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.15
|
||||
Nodes (22): C(), cd(), co(), eo(), fo(), Ft(), Gc(), gn() (+14 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (29): ao(), Bo(), C(), cd(), co(), Do(), eo(), fo() (+21 more)
|
||||
|
||||
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
||||
Cohesion: 0.29
|
||||
@@ -296,13 +285,17 @@ Nodes (4): 1meAI测肤旷视UI问题反馈, UI交互界面调整, 交互, **设
|
||||
Cohesion: 0.22
|
||||
Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并实现, 生产部署及只读验收, 皱纹(紧塑力)原文, 第四轮反馈修改与验收, 验证
|
||||
|
||||
### Community 41 - ".forwardInput"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
|
||||
|
||||
### Community 42 - "layer_at"
|
||||
Cohesion: 0.18
|
||||
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
|
||||
|
||||
### Community 43 - "convLayer"
|
||||
Cohesion: 0.15
|
||||
Nodes (17): boxPredictionLayer(), convLayer(), convWithBatchNorm(), depthwiseConvLayer(), depthwiseSeparableConv$1(), fullyConnectedLayer(), getStridesForLayerIdx(), leaky() (+9 more)
|
||||
### Community 43 - "AgeGenderNet"
|
||||
Cohesion: 0.10
|
||||
Nodes (14): AgeGenderNet(), convLayer(), convWithBatchNorm(), depthwiseSeparableConv$1(), FaceLandmark68TinyNet(), FaceProcessor(), fullyConnectedLayer(), leaky() (+6 more)
|
||||
|
||||
### Community 44 - "start-local.sh"
|
||||
Cohesion: 0.50
|
||||
@@ -325,8 +318,8 @@ Cohesion: 0.11
|
||||
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
|
||||
|
||||
### Community 55 - "i"
|
||||
Cohesion: 0.14
|
||||
Nodes (15): bn(), dn(), ee(), et(), fn(), i(), je(), ln() (+7 more)
|
||||
Cohesion: 0.07
|
||||
Nodes (32): bi(), bn(), dn(), ee(), Ef(), er(), et(), fn() (+24 more)
|
||||
|
||||
### Community 56 - "report-ui.js"
|
||||
Cohesion: 0.50
|
||||
@@ -336,13 +329,13 @@ Nodes (7): build(), count(), fill(), focusOf(), numeric(), scalar(), wrinkleDeta
|
||||
Cohesion: 0.60
|
||||
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
||||
|
||||
### Community 58 - ".dispose"
|
||||
Cohesion: 0.13
|
||||
Nodes (12): bgrToRgbTensor(), DetectSingleFaceLandmarksTask(), extendWithFaceLandmarks(), FaceRecognitionNet(), getSizesForScale(), nonMaxSuppression(), normalize$1(), rescaleAndNormalize() (+4 more)
|
||||
### Community 58 - ".forwardInput"
|
||||
Cohesion: 0.14
|
||||
Nodes (12): height(), FaceLandmark68NetBase(), getSizesForScale(), nonMaxSuppression(), normalize$1(), rescaleAndNormalize(), scale(), SsdMobilenetv1() (+4 more)
|
||||
|
||||
### Community 61 - "FaceProcessor"
|
||||
Cohesion: 0.22
|
||||
Nodes (3): extractParams$1(), FaceLandmark68TinyNet(), FaceProcessor()
|
||||
### Community 61 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.31
|
||||
Nodes (4): extendWithFaceDescriptor(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), FaceRecognitionNet()
|
||||
|
||||
### Community 62 - "提交前 Review(2026-10-11)"
|
||||
Cohesion: 0.40
|
||||
@@ -352,44 +345,44 @@ Nodes (4): 仍需公网/业务确认, 已发现并修复, 提交前 Review(202
|
||||
Cohesion: 0.29
|
||||
Nodes (4): EXPECTED, fails, pageErrors, pwRoot
|
||||
|
||||
### Community 64 - "account-client.js"
|
||||
Cohesion: 0.80
|
||||
Nodes (5): browser(), init(), read(), rememberKind(), request()
|
||||
### Community 64 - "start"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): decode(), drain(), load(), preload(), start(), stopCur(), unlock()
|
||||
|
||||
### Community 65 - "微信登录接入与公网部署说明"
|
||||
Cohesion: 0.29
|
||||
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 本机开发环境限制(不代表线上状态)
|
||||
|
||||
### Community 66 - "FaceLandmarks"
|
||||
Cohesion: 0.21
|
||||
Nodes (6): FaceLandmarks(), FaceLandmarks5(), getCenterPoint(), minBbox(), pt(), vt()
|
||||
Cohesion: 0.06
|
||||
Nodes (24): conv(), conv$1(), convDown(), convLayer$1(), convNoRelu(), denseBlock3(), denseBlock4(), depthwiseSeparableConv() (+16 more)
|
||||
|
||||
### Community 68 - "analyze-timeout.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (7): assert, end, fs, path, source, start, vm
|
||||
|
||||
### Community 70 - "normalize"
|
||||
Cohesion: 0.36
|
||||
Nodes (8): conv$1(), convDown(), convLayer$1(), convNoRelu(), normalize(), residual(), residualDown(), __spreadArrays()
|
||||
### Community 70 - "scan-placeholder.mjs"
|
||||
Cohesion: 0.29
|
||||
Nodes (3): errors, samples, unexpectedWrites
|
||||
|
||||
### Community 71 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.14
|
||||
Nodes (8): ComputeSingleFaceDescriptorTask(), extendWithAge(), extendWithFaceDescriptor(), extendWithFaceExpressions(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult(), PredictAllAgeAndGenderTask()
|
||||
### Community 71 - ".predictAgeAndGender"
|
||||
Cohesion: 0.18
|
||||
Nodes (5): age(), extendWithAge(), extendWithGender(), FaceExpressionNet(), PredictSingleAgeAndGenderTask()
|
||||
|
||||
### Community 76 - "doCapture"
|
||||
Cohesion: 0.16
|
||||
Nodes (23): applyMirror(), armNudge(), busyWithFlow(), captureFromVideo(), captureHaptic(), checkStill(), doCapture(), dropHint() (+15 more)
|
||||
### Community 76 - "onPhotoCaptured"
|
||||
Cohesion: 0.18
|
||||
Nodes (15): busyWithFlow(), captureHaptic(), checkStill(), dropHint(), faceLuma(), faceOffsetX(), handleCamVisible(), hideCamStatus() (+7 more)
|
||||
|
||||
### Community 77 - ".runAndExtendWithFaceDetections"
|
||||
Cohesion: 0.18
|
||||
Nodes (8): DetectAllFacesTask(), DrawBox(), drawDetections(), DrawFaceLandmarks(), extendWithFaceDetection(), isWithFaceDetection(), isWithFaceLandmarks(), resizeResults()
|
||||
|
||||
### Community 78 - "n"
|
||||
Cohesion: 0.10
|
||||
Nodes (38): ae(), be(), bi(), ce(), de(), Ef(), er(), FaceMatch() (+30 more)
|
||||
### Community 78 - ".toString"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): FaceMatch(), la(), pr()
|
||||
|
||||
### Community 79 - "登录反馈与加载优化(2026-10-12)"
|
||||
Cohesion: 0.33
|
||||
Cohesion: 0.22
|
||||
Nodes (5): 体验变化, 公网部署后复测, 慢的原因与处理, 登录反馈与加载优化(2026-10-12), 验证
|
||||
|
||||
### Community 80 - "线上微信登录部署记录"
|
||||
@@ -409,7 +402,7 @@ Cohesion: 0.29
|
||||
Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令
|
||||
|
||||
### Community 84 - "微信登录联调:2026-10-12"
|
||||
Cohesion: 0.22
|
||||
Cohesion: 0.33
|
||||
Nodes (6): 后续真机确认与体验优化, 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
|
||||
|
||||
### Community 85 - "test/accounts.js"
|
||||
@@ -425,16 +418,16 @@ Cohesion: 0.22
|
||||
Nodes (8): assets, design_scale, document_size, native_header_px, shape_metrics, source, source_sha256, text_styles
|
||||
|
||||
### Community 91 - "wi"
|
||||
Cohesion: 0.24
|
||||
Nodes (10): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), ve() (+2 more)
|
||||
Cohesion: 0.28
|
||||
Nodes (9): Ci(), Ei(), Fi(), Ii(), ki(), pe(), ri(), wi() (+1 more)
|
||||
|
||||
### Community 92 - "客户反馈修改(2026-10-12)"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 公网部署与复测, 客户反馈修改(2026-10-12), 皱纹数据说明, 逐项处理, 验证
|
||||
|
||||
### Community 93 - ".runAndExtendWithFaceDetection"
|
||||
Cohesion: 0.23
|
||||
Nodes (6): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), DetectAllFaceLandmarksTask(), detectAllFaces(), DetectSingleFaceTask()
|
||||
Cohesion: 0.33
|
||||
Nodes (5): allFacesMtcnn(), allFacesSsdMobilenetv1(), allFacesTinyYolov2(), detectAllFaces(), DetectSingleFaceTask()
|
||||
|
||||
### Community 94 - "第三轮反馈:PSD 对齐、验收及上线记录"
|
||||
Cohesion: 0.33
|
||||
@@ -444,17 +437,13 @@ Nodes (5): 原稿与导出可追溯性, 已完成, 本地验证, 生产部署及
|
||||
Cohesion: 0.50
|
||||
Nodes (3): artifacts, errors, samples
|
||||
|
||||
### Community 100 - "age"
|
||||
Cohesion: 0.43
|
||||
Nodes (4): age(), clean(), create(), key()
|
||||
|
||||
### Community 101 - "drawFaceExpressions"
|
||||
Cohesion: 0.50
|
||||
Nodes (3): drawFaceExpressions(), FaceExpressions(), isWithFaceExpressions()
|
||||
|
||||
### Community 102 - ".detect"
|
||||
Cohesion: 0.16
|
||||
Nodes (6): drawContour(), extractFaceTensors(), SsdMobilenetv1(), TinyYolov2Base(), x(), y()
|
||||
### Community 102 - "extractFaceTensors"
|
||||
Cohesion: 0.18
|
||||
Nodes (5): Box(), DetectAllFaceLandmarksTask(), extendWithFaceLandmarks(), extractFaceTensors(), isTensor3D()
|
||||
|
||||
### Community 107 - "基础信息记忆与肤质拖动"
|
||||
Cohesion: 0.33
|
||||
@@ -465,67 +454,35 @@ Cohesion: 0.40
|
||||
Nodes (3): dragBy(), h5, touch()
|
||||
|
||||
### Community 109 - "submitPhotos"
|
||||
Cohesion: 0.16
|
||||
Nodes (17): bindBackdropClose(), clearScanPhoto(), closeLoginSuccess(), closeOverlay(), handleCamHidden(), leaveCamera(), photosByView(), popHistorySilently() (+9 more)
|
||||
|
||||
### Community 110 - "startCamera"
|
||||
Cohesion: 0.17
|
||||
Nodes (15): beginDetect(), detectFrame(), detTick(), enableFallback(), getCamStream(), handleCamVisible(), loadFaceApi(), loadModels() (+7 more)
|
||||
Nodes (16): applyMirror(), bindBackdropClose(), closeLoginSuccess(), closeOverlay(), effMirror(), getCamStream(), handleCamHidden(), leaveCamera() (+8 more)
|
||||
|
||||
### Community 111 - "e"
|
||||
### Community 110 - "doCapture"
|
||||
Cohesion: 0.17
|
||||
Nodes (15): ba(), Bo(), ca(), cl(), da(), e(), fa(), pa() (+7 more)
|
||||
Nodes (19): armNudge(), beginDetect(), captureFromVideo(), detectFrame(), detTick(), doCapture(), enableFallback(), hideNudge() (+11 more)
|
||||
|
||||
### Community 111 - "k"
|
||||
Cohesion: 0.10
|
||||
Nodes (24): b(), ba(), ca(), cl(), da(), fa(), gn(), k() (+16 more)
|
||||
|
||||
### Community 112 - "showScreen"
|
||||
Cohesion: 0.22
|
||||
Nodes (11): loadAppFont(), loginAllowed(), loginBrowser(), loginSucceeded(), loginWechat(), openOverlay(), prepareScanArtwork(), renderAccountStatus() (+3 more)
|
||||
|
||||
### Community 113 - "Point"
|
||||
Cohesion: 0.24
|
||||
Nodes (3): FaceLandmark68NetBase(), Point(), wl()
|
||||
|
||||
### Community 114 - "initialize"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): createBrowserEnv(), createFileSystem(), createNodejsEnv(), initialize(), isBrowser(), isNodejs(), monkeyPatch(), resolveInput() (+1 more)
|
||||
|
||||
### Community 116 - ".forwardInput"
|
||||
Cohesion: 0.36
|
||||
Nodes (5): conv(), depthwiseSeparableConv(), mainBlock(), reductionBlock(), TinyXception()
|
||||
|
||||
### Community 117 - "ao"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): ao(), Do(), io(), oo(), so(), To()
|
||||
|
||||
### Community 118 - "k"
|
||||
Cohesion: 0.33
|
||||
Nodes (6): b(), k(), ne(), Yt(), zn(), zr()
|
||||
|
||||
### Community 120 - "restoreProfile"
|
||||
Cohesion: 0.50
|
||||
Nodes (5): cancelSkinDrag(), goSkin(), restoreProfile(), saveProfile(), setPick()
|
||||
|
||||
### Community 122 - "isValidNumber"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): Dimensions(), isValidNumber(), isValidProbablitiy(), isWithGender()
|
||||
|
||||
### Community 123 - "tt"
|
||||
Cohesion: 0.50
|
||||
Nodes (4): kn(), tt(), wo(), z()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **309 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+304 more)
|
||||
- **311 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+306 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **32 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **24 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 `v()` connect `f` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
|
||||
_High betweenness centrality (0.060) - this node is a cross-community bridge._
|
||||
- **Why does `fill()` connect `report-ui.js` to `f`?**
|
||||
_High betweenness centrality (0.049) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `f`, `lib/accounts.js`, `n`, `e`, `C`?**
|
||||
_High betweenness centrality (0.043) - this node is a cross-community bridge._
|
||||
- **Why does `v()` connect `n` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
|
||||
_High betweenness centrality (0.047) - this node is a cross-community bridge._
|
||||
- **Why does `height()` connect `.forwardInput` to `app.js`, `customer-feedback.js`, `scan-placeholder.mjs`, `extractFaceTensors`, `extractFaces`, `showScreen`?**
|
||||
_High betweenness centrality (0.045) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `k` to `face-api.js`, `app.js`, `n`, `lib/accounts.js`, `i`?**
|
||||
_High betweenness centrality (0.040) - 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?**
|
||||
@@ -533,4 +490,4 @@ _Questions this graph is uniquely positioned to answer:_
|
||||
- **Are the 9 inferred relationships involving `i()` (e.g. with `da()` and `f()`) actually correct?**
|
||||
_`i()` has 9 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `source`, `document_size`, `pages` to the rest of the system?**
|
||||
_309 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_311 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
File diff suppressed because one or more lines are too long
+1328
-1134
File diff suppressed because it is too large
Load Diff
+51
-16
@@ -40,8 +40,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/app.js": {
|
||||
"mtime": 1791810934.438929,
|
||||
"ast_hash": "b156d7bffd1daa1e346d127052aff709",
|
||||
"mtime": 1791814713.4789917,
|
||||
"ast_hash": "daf6b9c597517206ce9477d07bcdccf7",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/head-pose.js": {
|
||||
@@ -140,8 +140,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/index.html": {
|
||||
"mtime": 1791810934.438929,
|
||||
"ast_hash": "c4d2e3bfa60d91dea4c1fdf54780046a",
|
||||
"mtime": 1791814981.3589842,
|
||||
"ast_hash": "eda8371282c16b85f04dc741b4655440",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/README.md": {
|
||||
@@ -1245,8 +1245,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/report-ui.js": {
|
||||
"mtime": 1791806985.2188702,
|
||||
"ast_hash": "5860e9a09f70fdb201b2f51bdcee88e4",
|
||||
"mtime": 1791814981.3709843,
|
||||
"ast_hash": "aac3430a5e0c0a386c10e32b7060f2f7",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/lib/accounts.js": {
|
||||
@@ -1695,8 +1695,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-third.mjs": {
|
||||
"mtime": 1791811030.7069488,
|
||||
"ast_hash": "0a74e23b1968ec821bcb01c3eb7f9a8a",
|
||||
"mtime": 1791815038.0630124,
|
||||
"ast_hash": "32560625a79f7c8a9d7b74de7337729c",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/export_third_feedback_assets.py": {
|
||||
@@ -1815,8 +1815,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/capture-quality-browser.mjs": {
|
||||
"mtime": 1791811030.7069488,
|
||||
"ast_hash": "311ed23a81012e7a35daef3a0ec40013",
|
||||
"mtime": 1791815038.0590124,
|
||||
"ast_hash": "7cc5f0db42934862b36ef947a86f55bf",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/customer-feedback-fourth.js": {
|
||||
@@ -1909,14 +1909,49 @@
|
||||
"ast_hash": "cec2f2e829751e8b5d2e5e40c348f5e4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/scan-photo.mjs": {
|
||||
"mtime": 1791811100.15898,
|
||||
"ast_hash": "04e29e75a6707367ec0171b73ce5479c",
|
||||
"docs/\u62a5\u544a\u7b49\u5f85\u9875\u5b9e\u62cd\u80cc\u666f-2026-10-12.md": {
|
||||
"mtime": 1791815038.0630124,
|
||||
"ast_hash": "518dc04cd26577c59a14994f42fa65d6",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62a5\u544a\u7b49\u5f85\u9875\u5b9e\u62cd\u80cc\u666f-2026-10-12.md": {
|
||||
"mtime": 1791811100.15898,
|
||||
"ast_hash": "0362a8e36c7e8d6b16ccb96a666b715a",
|
||||
"design/ui-20261012/delivery-optimization-20261012.json": {
|
||||
"mtime": 1791814981.6029844,
|
||||
"ast_hash": "62f52298b3e8aa5dd7adcb536e1c202b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/swipe-track.js": {
|
||||
"mtime": 1791815201.6350212,
|
||||
"ast_hash": "65957645bd9401e303978c7e0bcb1273",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/report-touch-layout.mjs": {
|
||||
"mtime": 1791815159.3550136,
|
||||
"ast_hash": "b0967f039f8c1177dec801a03651c5dc",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/scan-placeholder.mjs": {
|
||||
"mtime": 1791815038.0590124,
|
||||
"ast_hash": "bb8566ee5f3ad2a6bf95aafafb7145ba",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"tools/optimize_delivery_images.py": {
|
||||
"mtime": 1791814981.3669844,
|
||||
"ast_hash": "5d039d5e1b79e1dcd17ac2af15c81aa2",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/product-25.jpg": {
|
||||
"mtime": 1791814981.5309844,
|
||||
"ast_hash": "3bd51a1940f65bb66882b190cc74befc",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/product-35.jpg": {
|
||||
"mtime": 1791814981.5469844,
|
||||
"ast_hash": "29385e9c05f4b7bab0b28aea830f600b",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/assets/img/ui-20261012/product-50.jpg": {
|
||||
"mtime": 1791814981.5989842,
|
||||
"ast_hash": "949de309b645a513c695da5782de351c",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 150 KiB |
+26
-11
@@ -75,20 +75,12 @@
|
||||
/* Fourth feedback adds a fast one-second round trip to original PSD pixels.
|
||||
The stage/mask share one contain transform, including short/landscape screens. */
|
||||
#p-analyzing { background: #000; overflow: hidden; }
|
||||
.scan-stage { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: min(100vw, 480px, calc(100dvh * 1080 / 2091)); height: min(100dvh, calc(min(100vw, 480px) * 2091 / 1080)); overflow: hidden; }
|
||||
.scan-stage { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); width: min(100vw, 480px, calc(100dvh * 1080 / 2091)); height: min(100dvh, calc(min(100vw, 480px) * 2091 / 1080)); }
|
||||
@supports not (height: 100dvh) { .scan-stage { width: min(100vw, 480px, calc(100vh * 1080 / 2091)); height: min(100vh, calc(min(100vw, 480px) * 2091 / 1080)); } }
|
||||
.scan-artwork { width: 100%; height: 100%; object-fit: contain; }
|
||||
.scan-face { position: absolute; inset: 0; background: #111; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; overflow: hidden; }
|
||||
/* Same centered cover crop as the camera; never stretch the captured JPEG.
|
||||
The PSD mask clips the real face, covering the original white placeholder. */
|
||||
.scan-photo { position: absolute; left: 50%; top: 50%; width: min(100vw, 480px); height: 100vh; height: 100dvh; max-width: none; object-fit: cover; transform: translate(-50%, -50%); }
|
||||
.scan-photo.mirror { transform: translate(-50%, -50%) scaleX(-1); }
|
||||
.scan-photo:not([src]) { visibility: hidden; }
|
||||
.scan-face { position: absolute; inset: 0; -webkit-mask-size: 100% 100%; mask-size: 100% 100%; -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat; overflow: hidden; }
|
||||
.scan-beam { position: absolute; left: 0; right: 0; top: 20%; height: 18%; background: linear-gradient(to bottom, #F5F078 0%, #F5F078 35%, rgba(245,240,120,0) 100%); }
|
||||
#p-analyzing:not(.scan-artwork-ready) .scan-artwork { visibility: hidden; }
|
||||
#p-analyzing:not(.scan-artwork-ready) .scan-face { -webkit-mask-image: none !important; mask-image: none !important; }
|
||||
#p-analyzing:not(.scan-artwork-ready) .scan-photo { opacity: .6; }
|
||||
#p-analyzing:not(.scan-artwork-ready) .scan-beam { visibility: hidden; }
|
||||
#p-analyzing:not(.scan-artwork-ready) .scan-stage { visibility: hidden; }
|
||||
.scan-running .scan-beam { animation: report-scan var(--scan-cycle, 1000ms) ease-in-out infinite; }
|
||||
@keyframes report-scan { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(166%); } }
|
||||
.scan-running:not(.scan-artwork-ready) .scan-status { animation: report-scan-fallback var(--scan-cycle, 1000ms) ease-in-out infinite; }
|
||||
@@ -114,3 +106,26 @@
|
||||
#dims-note::before { left: 12px; top: 10px; bottom: 10px; }
|
||||
#dims-note .dims-note-l1 { font-size: 12px; line-height: 1.5; }
|
||||
#dims-note .dims-note-l2 { font-size: 11px; line-height: 1.5; margin-top: 3px; }
|
||||
|
||||
/* Labels share a top baseline outside the dial; center the score digits, not
|
||||
digits plus unit, inside the yellow ring. */
|
||||
.sum-score, .sum-age { flex-direction: column; align-items: center; justify-content: flex-start; gap: calc(6 * var(--u)); }
|
||||
.sum-age { align-self: stretch; }
|
||||
.sum-score > .stat-label, .sum-age > .stat-label { font-size: 12px; line-height: 1.5; }
|
||||
.score-dial, .age-dial { position: relative; width: calc(108 * var(--u)); height: calc(108 * var(--u)); }
|
||||
.age-dial { display: flex; align-items: center; justify-content: center; }
|
||||
.age-dial .stat-age { margin-top: 0; }
|
||||
#stat-score { position: relative; }
|
||||
#stat-score b { display: block; letter-spacing: 0; }
|
||||
#stat-score i { position: absolute; left: 100%; bottom: 3px; margin-left: 2px; }
|
||||
.card-care .card-heading { text-align: left; }
|
||||
|
||||
.detail-viewport { overflow: hidden; touch-action: pan-y; transition: height .32s ease; }
|
||||
.detail-viewport.dragging { transition: none; }
|
||||
.detail-panels { display: flex; align-items: flex-start; transition: transform .32s cubic-bezier(.22,.61,.36,1); }
|
||||
.detail-panels.dragging { transition: none; will-change: transform; }
|
||||
.detail-panels .dpanel { display: block; flex: 0 0 100%; min-width: 0; padding-top: calc(10 * var(--u)); animation: none; }
|
||||
.detail-panels .ditem-img { -webkit-user-drag: none; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.detail-panels, .detail-viewport { transition-duration: .01ms; }
|
||||
}
|
||||
|
||||
+18
-21
@@ -173,18 +173,20 @@
|
||||
<!-- 总分卡:左环形综合评分 / 右肌肤年龄 + 与实际年龄对比 -->
|
||||
<div class="card card-summary">
|
||||
<div class="sum-score">
|
||||
<svg class="ring" viewBox="0 0 120 120" aria-hidden="true">
|
||||
<circle class="ring-bg" cx="60" cy="60" r="52"/>
|
||||
<circle class="ring-fg" id="ring-fg" cx="60" cy="60" r="52"/>
|
||||
</svg>
|
||||
<div class="ring-center">
|
||||
<span class="stat-value" id="stat-score"><b>--</b><i>分</i></span>
|
||||
<span class="stat-label">综合评分</span>
|
||||
<span class="stat-label">综合评分</span>
|
||||
<div class="score-dial">
|
||||
<svg class="ring" viewBox="0 0 120 120" aria-hidden="true">
|
||||
<circle class="ring-bg" cx="60" cy="60" r="52"/>
|
||||
<circle class="ring-fg" id="ring-fg" cx="60" cy="60" r="52"/>
|
||||
</svg>
|
||||
<div class="ring-center">
|
||||
<span class="stat-value" id="stat-score"><b>--</b><i>分</i></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sum-age">
|
||||
<span class="stat-label">生物可视年龄</span>
|
||||
<span class="stat-value stat-age" id="stat-age"><b>--</b><i>岁</i></span>
|
||||
<div class="age-dial"><span class="stat-value stat-age" id="stat-age"><b>--</b><i>岁</i></span></div>
|
||||
<span class="age-pill" id="stat-age-note" hidden></span>
|
||||
</div>
|
||||
|
||||
@@ -202,7 +204,7 @@
|
||||
<h2 class="dimension-title">紧塑力</h2>
|
||||
<div class="card card-detail">
|
||||
<div class="detail-tabs"><button class="dtab active" data-tab="0"><span class="dtab-tx">轮廓</span></button><button class="dtab" data-tab="1"><span class="dtab-tx">皱纹</span></button></div>
|
||||
<div class="detail-panels">
|
||||
<div class="detail-viewport"><div class="detail-panels">
|
||||
<div class="dpanel active" data-panel="0">
|
||||
<div class="ditem"><div class="viz-frame"><img id="ditem-img1" class="ditem-img" alt="轮廓检测图" hidden><span class="viz-missing">本次未获取轮廓检测图</span></div>
|
||||
<div class="ditem-text"><p class="ditem-para" id="ditem-para1">检测完成后显示分析结果。</p></div></div>
|
||||
@@ -211,13 +213,12 @@
|
||||
<div class="ditem"><div class="viz-frame"><img id="ditem-img2" class="ditem-img" alt="皱纹检测图" hidden><span class="viz-missing">本次未获取皱纹检测图</span></div>
|
||||
<div class="ditem-text"><p class="ditem-para" id="ditem-para2">检测完成后显示分析结果。</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<h2 class="dimension-title">透亮感</h2>
|
||||
<div class="card card-detail">
|
||||
<div class="detail-tabs"><button class="dtab active" data-tab="0"><span class="dtab-tx">肤色</span></button><button class="dtab" data-tab="1"><span class="dtab-tx">平滑度</span></button><button class="dtab" data-tab="2"><span class="dtab-tx">水分</span></button></div>
|
||||
<div class="detail-panels">
|
||||
<div class="detail-viewport"><div class="detail-panels">
|
||||
<div class="dpanel active" data-panel="0">
|
||||
<div class="ditem"><div class="viz-frame"><img id="viz-tone" class="ditem-img" alt="肤色检测图" hidden><span class="viz-missing">本次未获取肤色检测图</span></div>
|
||||
<div class="ditem-text"><p class="ditem-para" id="tx-tone">检测完成后显示分析结果。</p></div></div>
|
||||
@@ -231,13 +232,12 @@
|
||||
<div class="ditem"><div class="viz-frame"><img id="viz-water" class="ditem-img" alt="水分检测图" hidden><span class="viz-missing">本次未获取水分检测图</span></div>
|
||||
<div class="ditem-text"><p class="ditem-para" id="tx-water">检测完成后显示分析结果。</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<h2 class="dimension-title">稳韧力</h2>
|
||||
<div class="card card-detail">
|
||||
<div class="detail-tabs"><button class="dtab active" data-tab="0"><span class="dtab-tx">泛红</span></button><button class="dtab" data-tab="1"><span class="dtab-tx">痘痘</span></button></div>
|
||||
<div class="detail-panels">
|
||||
<div class="detail-viewport"><div class="detail-panels">
|
||||
<div class="dpanel active" data-panel="0">
|
||||
<div class="ditem"><div class="viz-frame"><img id="viz-sens" class="ditem-img" alt="泛红检测图" hidden><span class="viz-missing">本次未获取泛红检测图</span></div>
|
||||
<div class="ditem-text"><p class="ditem-para" id="tx-sens">检测完成后显示分析结果。</p></div></div>
|
||||
@@ -246,9 +246,8 @@
|
||||
<div class="ditem"><div class="viz-frame"><img id="viz-acne" class="ditem-img" alt="痘痘检测图" hidden><span class="viz-missing">本次未获取痘痘检测图</span></div>
|
||||
<div class="ditem-text"><p class="ditem-para" id="tx-acne">检测完成后显示分析结果。</p></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div></div>
|
||||
</div>
|
||||
|
||||
<p class="vm-notes" id="vm-notes" style="display:none"></p>
|
||||
|
||||
<p class="report-disclaimer">*您的肌肤状态会随年龄、环境、作息和生活节奏动态变化。<br>*本次检测仅代表当前肌肤状态参考。</p>
|
||||
@@ -457,10 +456,7 @@
|
||||
Not camera controls: no action attributes, focus targets or file inputs. -->
|
||||
<div class="scan-stage" aria-hidden="true">
|
||||
<img id="scan-artwork" class="scan-artwork" data-src="assets/img/ui-20261012/scan-base.webp" width="1080" height="2091" alt="" draggable="false">
|
||||
<div id="scan-face" class="scan-face" data-mask="assets/img/ui-20261012/scan-face-mask.png">
|
||||
<img id="scan-photo" class="scan-photo" alt="" draggable="false">
|
||||
<i class="scan-beam"></i>
|
||||
</div>
|
||||
<div id="scan-face" class="scan-face" data-mask="assets/img/ui-20261012/scan-face-mask.png"><i class="scan-beam"></i></div>
|
||||
</div>
|
||||
<p class="scan-status" role="status">检测中…</p>
|
||||
<button class="scan-cancel" type="button" data-action="cancel-analysis">取消</button>
|
||||
@@ -491,6 +487,7 @@ if (/[?&]mock=1/.test(location.search)) document.write('<script src="assets/mock
|
||||
<script src="js/report-ui.js" defer></script>
|
||||
<script src="js/account-client.js" defer></script>
|
||||
<script src="js/profile-store.js" defer></script>
|
||||
<script src="js/swipe-track.js" defer></script>
|
||||
<script src="js/camera-geometry.js" defer></script>
|
||||
<script src="js/app.js" defer></script>
|
||||
</body>
|
||||
|
||||
+47
-130
@@ -126,39 +126,29 @@
|
||||
});
|
||||
|
||||
/* ─── 报告页状态重置(各维度卡回到第一个子 Tab、产品横滑归零) ─── */
|
||||
function setDetailTab(card, idx) {
|
||||
var detailSwipes = new WeakMap();
|
||||
function setDetailTab(card, idx, instant) {
|
||||
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); });
|
||||
card.querySelectorAll('.dpanel').forEach(function (p) {
|
||||
var selected = p.dataset.panel === idx;
|
||||
p.classList.toggle('active', selected); p.setAttribute('aria-hidden', String(!selected)); p.inert = !selected;
|
||||
});
|
||||
var detail = detailSwipes.get(card);
|
||||
if (detail) { detail.swipe.go(Number(idx), instant); detail.height(Number(idx)); }
|
||||
}
|
||||
function resetReport() {
|
||||
document.querySelectorAll('.card-detail').forEach(function (c) { setDetailTab(c, '0'); });
|
||||
document.querySelectorAll('.card-detail').forEach(function (c) { setDetailTab(c, '0', true); });
|
||||
var ps = $('#prod-scroll');
|
||||
if (ps) ps.scrollLeft = 0;
|
||||
}
|
||||
|
||||
/* ─── 弹窗管理 ─── */
|
||||
var SCAN_CYCLE_MS = 1000, scanCycle = null, scanMask = null;
|
||||
function setScanPhoto(source, mirrored) {
|
||||
var image = $('#scan-photo');
|
||||
image.classList.toggle('mirror', !!mirrored);
|
||||
if (image.getAttribute('src') === source) return;
|
||||
image.onload = function () {
|
||||
// A delayed JPEG decode must still get one complete visible scan cycle.
|
||||
if ($('#p-analyzing').classList.contains('show')) restartScanCycle();
|
||||
};
|
||||
image.onerror = function () { image.removeAttribute('src'); };
|
||||
image.src = source;
|
||||
}
|
||||
function clearScanPhoto() {
|
||||
var image = $('#scan-photo');
|
||||
image.onload = image.onerror = null;
|
||||
image.removeAttribute('src'); image.classList.remove('mirror');
|
||||
}
|
||||
function restartScanCycle() {
|
||||
if (!scanCycle || scanCycle.done) return;
|
||||
var overlay = $('#p-analyzing'), cycle = scanCycle;
|
||||
@@ -222,7 +212,7 @@
|
||||
}
|
||||
function closeOverlay(id) {
|
||||
$('#' + id).classList.remove('show');
|
||||
if (id === 'p-analyzing') { stopScanCycle(); clearScanPhoto(); }
|
||||
if (id === 'p-analyzing') stopScanCycle();
|
||||
}
|
||||
|
||||
// 点遮罩空白处关闭(与各自关闭按钮同行为:隐私→回启动页、拍照引导→留在档案页)
|
||||
@@ -278,7 +268,7 @@
|
||||
{ img: 'assets/img/ui-20261009/skin-3-oily.png', name: '油性肌肤', desc: '全脸易出油,容易泛油光、毛孔明显' },
|
||||
{ img: 'assets/img/ui-20261009/skin-4-mixedoily.png', name: '混合偏油肌肤', desc: 'T区易出油,两颊相对清爽或轻微干燥' }
|
||||
];
|
||||
var skinIndex = 0;
|
||||
var skinIndex = 0, skinSwipe;
|
||||
var track = $('#skin-track'), dotsBox = $('#skin-dots');
|
||||
|
||||
SKINS.forEach(function (s, i) {
|
||||
@@ -297,8 +287,8 @@
|
||||
|
||||
function goSkin(i, persist) {
|
||||
skinIndex = Math.max(0, Math.min(SKINS.length - 1, i));
|
||||
track.classList.remove('dragging');
|
||||
track.style.transform = 'translateX(-' + skinIndex * 100 + '%)';
|
||||
if (skinSwipe) skinSwipe.go(skinIndex);
|
||||
else track.style.transform = 'translateX(-' + skinIndex * 100 + '%)';
|
||||
dotsBox.querySelectorAll('i').forEach(function (d, k) {
|
||||
d.classList.toggle('on', k === skinIndex);
|
||||
});
|
||||
@@ -312,74 +302,12 @@
|
||||
});
|
||||
goSkin(0);
|
||||
|
||||
// Follow the finger immediately, then snap with distance/flick intent.
|
||||
// pan-y and direction locking preserve normal vertical page scrolling.
|
||||
var drag = { id: null, horizontal: false, vertical: false, suppressUntil: 0 };
|
||||
var carousel = $('#skin-carousel');
|
||||
function cancelSkinDrag() {
|
||||
var id = drag.id;
|
||||
drag.id = null; drag.horizontal = false; drag.vertical = false;
|
||||
try { if (id != null && carousel.hasPointerCapture(id)) carousel.releasePointerCapture(id); } catch (_) {}
|
||||
goSkin(skinIndex, false);
|
||||
}
|
||||
carousel.addEventListener('pointerdown', function (e) {
|
||||
if (drag.id != null || e.isPrimary === false || e.button !== 0 || !carousel.clientWidth) return;
|
||||
try { carousel.setPointerCapture(e.pointerId); } catch (err) { /* 非活动指针时忽略,退化为普通监听 */ }
|
||||
drag.id = e.pointerId;
|
||||
drag.startX = e.clientX;
|
||||
drag.startY = e.clientY;
|
||||
drag.offset = track.getBoundingClientRect().left - carousel.getBoundingClientRect().left;
|
||||
drag.width = carousel.clientWidth;
|
||||
drag.lastX = e.clientX; drag.lastAt = e.timeStamp; drag.velocity = 0;
|
||||
drag.horizontal = false; drag.vertical = false;
|
||||
});
|
||||
carousel.addEventListener('pointermove', function (e) {
|
||||
if (e.pointerId !== drag.id || drag.vertical) return;
|
||||
var dx = e.clientX - drag.startX, dy = e.clientY - drag.startY;
|
||||
if (!drag.horizontal) {
|
||||
if (Math.abs(dy) > 8 && Math.abs(dy) >= Math.abs(dx)) { drag.vertical = true; return; }
|
||||
if (Math.abs(dx) <= 8 || Math.abs(dx) <= Math.abs(dy)) return;
|
||||
drag.horizontal = true; track.classList.add('dragging');
|
||||
}
|
||||
if (e.cancelable) e.preventDefault();
|
||||
var elapsed = e.timeStamp - drag.lastAt;
|
||||
if (elapsed > 0) drag.velocity = (e.clientX - drag.lastX) / elapsed;
|
||||
drag.lastX = e.clientX; drag.lastAt = e.timeStamp;
|
||||
var offset = drag.offset + dx, min = -(SKINS.length - 1) * drag.width;
|
||||
if (offset > 0) offset *= .25;
|
||||
else if (offset < min) offset = min + (offset - min) * .25;
|
||||
track.style.transform = 'translate3d(' + offset + 'px,0,0)';
|
||||
});
|
||||
carousel.addEventListener('pointerup', function (e) {
|
||||
if (e.pointerId !== drag.id) return;
|
||||
var dx = e.clientX - drag.startX;
|
||||
if (drag.horizontal) {
|
||||
var start = Math.round(-drag.offset / drag.width), target = start;
|
||||
var speed = e.timeStamp - drag.lastAt > 100 ? 0 : drag.velocity;
|
||||
if (Math.abs(dx) >= Math.min(50, drag.width * .2) || (Math.abs(dx) > 12 && Math.abs(speed) > .4)) {
|
||||
target = Math.round(-(drag.offset + dx) / drag.width);
|
||||
if (target === start) target += (Math.abs(speed) > .4 ? speed : dx) < 0 ? 1 : -1;
|
||||
}
|
||||
goSkin(target);
|
||||
drag.suppressUntil = Date.now() + 350;
|
||||
}
|
||||
drag.id = null;
|
||||
drag.horizontal = false; drag.vertical = false;
|
||||
});
|
||||
carousel.addEventListener('pointercancel', function (e) {
|
||||
if (e.pointerId === drag.id) cancelSkinDrag();
|
||||
});
|
||||
carousel.addEventListener('lostpointercapture', function (e) {
|
||||
if (e.pointerId === drag.id) cancelSkinDrag();
|
||||
});
|
||||
carousel.addEventListener('dragstart', function (e) { e.preventDefault(); });
|
||||
carousel.addEventListener('click', function (e) {
|
||||
if (Date.now() < drag.suppressUntil) { e.preventDefault(); e.stopPropagation(); }
|
||||
}, true);
|
||||
skinSwipe = SwipeTrack.create(carousel, track, { count: SKINS.length, onSelect: goSkin });
|
||||
function cancelSkinDrag() { skinSwipe.cancel(); }
|
||||
carousel.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { e.preventDefault(); goSkin(skinIndex + (e.key === 'ArrowRight' ? 1 : -1)); }
|
||||
});
|
||||
window.addEventListener('resize', cancelSkinDrag);
|
||||
|
||||
/* ─── 档案选择控件:出生年月 / 性别 / 省份城市,统一用 mobile-select 底部滚轮(选中值写入同名 hidden input) ─── */
|
||||
var NOW_Y = new Date().getFullYear();
|
||||
@@ -461,7 +389,7 @@
|
||||
var IS_MOCK = /[?&]mock=1/.test(location.search); // mock:不加载 face-api、不跑检测,analyze 直返内置样本
|
||||
var camVideo = $('#cam-video'), camFile = $('#cam-file'), albumFile = $('#album-file');
|
||||
var camStream = null, camLive = false, camFallback = false, camSeqCam = 0; // camSeqCam:相机启动令牌,防止迟到的授权回调泄漏摄像头
|
||||
var photos = [], scanPhotoMirrored = false; // 三连拍(dataURL),拍摄中与 CAM_STEPS 同序;送检前经 photosByView 重排为 [正脸, 左脸, 右脸]
|
||||
var photos = []; // 三连拍(dataURL),拍摄中与 CAM_STEPS 同序;送检前经 photosByView 重排为 [正脸, 左脸, 右脸]
|
||||
var captureDiagnostics = [], holdState = null;
|
||||
|
||||
// 约束逐级放宽:facingMode 裸值在部分 Android 抛 OverconstrainedError,先 ideal 再退 video:true,最后才降级
|
||||
@@ -587,7 +515,7 @@
|
||||
hideCamStatus();
|
||||
shutterLock = false;
|
||||
if (err) { toast(err); voice.play(VOICE.fail, { cut: true }); return; } // 不合格:停在本步重拍
|
||||
onPhotoCaptured(url, false); // Files already have their own camera orientation.
|
||||
onPhotoCaptured(url);
|
||||
});
|
||||
};
|
||||
img.onerror = function () { toast('照片读取失败,请重新选择'); };
|
||||
@@ -1028,13 +956,9 @@
|
||||
var shutterLock = false; // 快门防连点锁:流程进行中直接忽略
|
||||
|
||||
// 照片就绪(已通过校验)后推进流程:存图→「拍摄完成」→300ms 过渡→下一步;三步严格按顺序,本步合格才进下一步
|
||||
function onPhotoCaptured(dataUrl, mirrored) {
|
||||
function onPhotoCaptured(dataUrl) {
|
||||
shutterLock = true; // 推进窗口上锁(停顿期间不检测、不接受快门),直到下一步/送检
|
||||
photos[camStep] = dataUrl;
|
||||
if (camStep === 0) {
|
||||
scanPhotoMirrored = !!mirrored;
|
||||
setScanPhoto(dataUrl, scanPhotoMirrored); // Decode the front photo before the waiting screen opens.
|
||||
}
|
||||
voice.play(VOICE.shot, { cut: true }); // 「拍摄完成」:已拍下,本步未播完的引导作废
|
||||
var seq = camSeq, hasNext = camStep < CAM_STEPS.length - 1;
|
||||
if (hasNext) {
|
||||
@@ -1072,7 +996,7 @@
|
||||
var fill = $('#cam-fill');
|
||||
fill.classList.add('on'); // .on 无过渡,瞬间点亮
|
||||
setTimeout(function () {
|
||||
var shot = captureFromVideo(), mirrored = effMirror();
|
||||
var shot = captureFromVideo();
|
||||
setTimeout(function () {
|
||||
fill.classList.remove('on'); // 移除 .on 恢复过渡,淡出
|
||||
capturing = false;
|
||||
@@ -1085,7 +1009,7 @@
|
||||
shutterLock = false; holdState = null; okSince = 0;
|
||||
setHint(error, false); armNudge(); return;
|
||||
}
|
||||
onPhotoCaptured(shot, mirrored);
|
||||
onPhotoCaptured(shot);
|
||||
});
|
||||
}, 300);
|
||||
}, 300);
|
||||
@@ -1095,7 +1019,6 @@
|
||||
function submitPhotos(seq) {
|
||||
camStep = 0;
|
||||
photos = photosByView(photos);
|
||||
setScanPhoto(photos[0], scanPhotoMirrored);
|
||||
openOverlay('p-analyzing');
|
||||
voice.play(VOICE.analyzing); // 「正在生成您的肌肤报告」
|
||||
// 档案与检测一起上报(后台记录用):年龄非法值服务端按 null 处理
|
||||
@@ -1567,38 +1490,34 @@
|
||||
});
|
||||
|
||||
/* ─── 报告页:每张维度卡各自的子 Tab;点检测图放大,再点返回 ─── */
|
||||
document.querySelectorAll('.card-detail').forEach(function (card) {
|
||||
document.querySelectorAll('.card-detail').forEach(function (card, group) {
|
||||
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;
|
||||
var tabs = Array.from(card.querySelectorAll('.dtab')), panels = Array.from(card.querySelectorAll('.dpanel'));
|
||||
var viewport = card.querySelector('.detail-viewport'), track = card.querySelector('.detail-panels'), position = 0;
|
||||
tabs.forEach(function (tab, i) {
|
||||
tab.setAttribute('role', 'tab'); tab.id = 'detail-tab-' + group + '-' + i;
|
||||
tab.setAttribute('aria-controls', 'detail-panel-' + group + '-' + i);
|
||||
panels[i].id = 'detail-panel-' + group + '-' + i;
|
||||
panels[i].setAttribute('role', 'tabpanel'); panels[i].setAttribute('aria-labelledby', tab.id);
|
||||
});
|
||||
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(); });
|
||||
function height(at) {
|
||||
position = at;
|
||||
var lower = Math.floor(at), upper = Math.min(panels.length - 1, Math.ceil(at)), ratio = at - lower;
|
||||
viewport.style.height = (panels[lower].offsetHeight * (1 - ratio) + panels[upper].offsetHeight * ratio) + 'px';
|
||||
}
|
||||
var swipe = SwipeTrack.create(viewport, track, { count: panels.length, gestureTarget: card,
|
||||
onMove: height, onCancel: height, onSelect: function (i) { setDetailTab(card, String(i)); } });
|
||||
detailSwipes.set(card, { swipe: swipe, height: height });
|
||||
setDetailTab(card, '0', true);
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
var observer = new ResizeObserver(function () { height(position); });
|
||||
panels.forEach(function (panel) { observer.observe(panel); });
|
||||
} else {
|
||||
window.addEventListener('resize', function () { height(position); });
|
||||
card.addEventListener('load', function () { height(position); }, true);
|
||||
if (document.fonts) document.fonts.ready.then(function () { height(position); });
|
||||
}
|
||||
card.querySelectorAll('.ditem-img').forEach(function (image) { image.draggable = false; });
|
||||
card.addEventListener('keydown', function (e) {
|
||||
var tab = e.target.closest('.dtab');
|
||||
if (!tab || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return;
|
||||
@@ -1608,11 +1527,10 @@
|
||||
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');
|
||||
if (img && img.src && !img.hidden) {
|
||||
if (img && img.src && !img.hidden && img.closest('.dpanel').classList.contains('active')) {
|
||||
$('#zoom-img').src = img.src;
|
||||
openOverlay('p-zoom');
|
||||
history.pushState({ screen: currentScreen, overlay: 'p-zoom' }, '');
|
||||
@@ -1653,7 +1571,6 @@
|
||||
camSeq++; // 使旧的快门/分析回调失效
|
||||
camStep = 0;
|
||||
photos = []; // 新一轮三连拍(从 startCamera 挪出:后台恢复续拍时保留已拍)
|
||||
scanPhotoMirrored = false; clearScanPhoto();
|
||||
captureDiagnostics = [];
|
||||
shutterLock = false; // 重新进入流程,保险解锁
|
||||
showScreen('s-cam', 'push');
|
||||
|
||||
+3
-3
@@ -10,9 +10,9 @@
|
||||
['sens', '泛红', 'stab', 'sens'], ['acne', '痘痘', 'stab', 'acne']
|
||||
];
|
||||
var products = [
|
||||
{ tier: 1, match: '25+束紧精华', title: "1'me 主场精华 I", name: '以我本然有光蓄活', image: 'assets/img/ui-20261012/product-25.webp' },
|
||||
{ tier: 2, match: '35+支撑精华', title: "1'me 主场精华 II", name: '以我自在有势挺括', image: 'assets/img/ui-20261012/product-35.webp' },
|
||||
{ tier: 3, match: '50+重塑精华', title: "1'me 主场精华 III", name: '以我从容有态构塑', image: 'assets/img/ui-20261012/product-50.webp' }
|
||||
{ tier: 1, match: '25+束紧精华', title: "1'me 主场精华 I", name: '以我本然有光蓄活', image: 'assets/img/ui-20261012/product-25.jpg' },
|
||||
{ tier: 2, match: '35+支撑精华', title: "1'me 主场精华 II", name: '以我自在有势挺括', image: 'assets/img/ui-20261012/product-35.jpg' },
|
||||
{ tier: 3, match: '50+重塑精华', title: "1'me 主场精华 III", name: '以我从容有态构塑', image: 'assets/img/ui-20261012/product-50.jpg' }
|
||||
];
|
||||
var levelMap = { '预警': '改善', '改善': '良好', '良好': '优秀' };
|
||||
var dimNames = { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' };
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/* Shared finger-following track for skin choices and report detail panels. */
|
||||
window.SwipeTrack = (function () {
|
||||
'use strict';
|
||||
function create(viewport, track, options) {
|
||||
var target = options.gestureTarget || viewport, index = 0;
|
||||
var drag = { id: null, horizontal: false, vertical: false }, suppressUntil = 0;
|
||||
function release() {
|
||||
var id = drag.id;
|
||||
drag.id = null; drag.horizontal = drag.vertical = false;
|
||||
viewport.classList.remove('dragging'); track.classList.remove('dragging');
|
||||
try { if (id != null && target.hasPointerCapture(id)) target.releasePointerCapture(id); } catch (_) {}
|
||||
}
|
||||
function go(next, instant) {
|
||||
index = Math.max(0, Math.min(options.count - 1, next));
|
||||
release();
|
||||
if (instant) track.style.transition = 'none';
|
||||
track.style.transform = 'translateX(-' + index * 100 + '%)';
|
||||
if (instant) { void track.offsetWidth; track.style.transition = ''; }
|
||||
}
|
||||
function cancel() { go(index, true); if (options.onCancel) options.onCancel(index); }
|
||||
target.addEventListener('pointerdown', function (e) {
|
||||
if (drag.id != null || e.isPrimary === false || e.button !== 0 || !viewport.clientWidth) return;
|
||||
suppressUntil = 0; // A fresh tap is intentional, not the preceding drag's click.
|
||||
drag.id = e.pointerId; drag.startX = e.clientX; drag.startY = e.clientY;
|
||||
drag.offset = track.getBoundingClientRect().left - viewport.getBoundingClientRect().left;
|
||||
drag.width = viewport.clientWidth; drag.lastX = e.clientX; drag.lastAt = e.timeStamp; drag.velocity = 0;
|
||||
drag.horizontal = drag.vertical = false;
|
||||
// Capture only after direction lock: an ordinary image tap must still zoom.
|
||||
});
|
||||
target.addEventListener('pointermove', function (e) {
|
||||
if (e.pointerId !== drag.id || drag.vertical) return;
|
||||
var dx = e.clientX - drag.startX, dy = e.clientY - drag.startY;
|
||||
if (!drag.horizontal) {
|
||||
if (Math.abs(dy) > 8 && Math.abs(dy) >= Math.abs(dx)) { drag.vertical = true; return; }
|
||||
if (Math.abs(dx) <= 8 || Math.abs(dx) <= Math.abs(dy)) return;
|
||||
drag.horizontal = true; viewport.classList.add('dragging'); track.classList.add('dragging');
|
||||
try { target.setPointerCapture(e.pointerId); } catch (_) {}
|
||||
}
|
||||
if (e.cancelable) e.preventDefault();
|
||||
var elapsed = e.timeStamp - drag.lastAt;
|
||||
if (elapsed > 0) drag.velocity = (e.clientX - drag.lastX) / elapsed;
|
||||
drag.lastX = e.clientX; drag.lastAt = e.timeStamp;
|
||||
var offset = drag.offset + dx, min = -(options.count - 1) * drag.width;
|
||||
if (offset > 0) offset *= .25;
|
||||
else if (offset < min) offset = min + (offset - min) * .25;
|
||||
track.style.transform = 'translate3d(' + offset + 'px,0,0)';
|
||||
if (options.onMove) options.onMove(Math.max(0, Math.min(options.count - 1, -offset / drag.width)));
|
||||
});
|
||||
window.addEventListener('pointerup', function (e) {
|
||||
if (e.pointerId !== drag.id) return;
|
||||
var horizontal = drag.horizontal, dx = e.clientX - drag.startX, next = index;
|
||||
if (horizontal) {
|
||||
var start = Math.round(-drag.offset / drag.width);
|
||||
next = start;
|
||||
var speed = e.timeStamp - drag.lastAt > 100 ? 0 : drag.velocity;
|
||||
if (Math.abs(dx) >= Math.min(50, drag.width * .2) || (Math.abs(dx) > 12 && Math.abs(speed) > .4)) {
|
||||
next = Math.round(-(drag.offset + dx) / drag.width);
|
||||
if (next === start) next += (Math.abs(speed) > .4 ? speed : dx) < 0 ? 1 : -1;
|
||||
}
|
||||
suppressUntil = Date.now() + 350;
|
||||
}
|
||||
go(next);
|
||||
if (horizontal && options.onSelect) options.onSelect(index);
|
||||
});
|
||||
window.addEventListener('pointercancel', function (e) { if (e.pointerId === drag.id) cancel(); });
|
||||
target.addEventListener('lostpointercapture', function (e) {
|
||||
// Touch starts with implicit capture on an image/button. Its bubbled loss
|
||||
// when we take horizontal capture is not a cancellation of this track.
|
||||
if (e.target === target && e.pointerId === drag.id) cancel();
|
||||
});
|
||||
target.addEventListener('dragstart', function (e) { e.preventDefault(); });
|
||||
target.addEventListener('click', function (e) {
|
||||
if (Date.now() < suppressUntil) { e.preventDefault(); e.stopPropagation(); }
|
||||
}, true);
|
||||
window.addEventListener('resize', cancel);
|
||||
go(0, true);
|
||||
return { go: go, cancel: cancel, isDragging: function () { return drag.horizontal; } };
|
||||
}
|
||||
return { create: create };
|
||||
})();
|
||||
@@ -97,10 +97,10 @@ try {
|
||||
const sent = page.waitForRequest(r => r.url().endsWith('/api/skin-analyze'));
|
||||
await page.evaluate(() => window.__cameraFixture.set('C3_-135.jpg'));
|
||||
const body = (await sent).postDataJSON();
|
||||
assert.equal(await page.locator('#scan-photo').getAttribute('src'), body.front_image, 'real live waiting uses the captured front photo');
|
||||
assert.equal(await page.locator('#scan-photo.mirror').count(), 1, 'front camera waiting keeps the preview mirror without changing the sent JPEG');
|
||||
assert.equal(await page.locator('#scan-photo').count(), 0, 'live waiting does not display the captured face');
|
||||
assert.equal(await page.locator('#scan-artwork').count(), 1, 'waiting retains original PSD head');
|
||||
await page.waitForSelector('#s-report.active');
|
||||
assert.equal(await page.locator('#scan-photo').getAttribute('src'), null, 'waiting photo reference is cleared after success');
|
||||
assert.equal(await page.locator('#p-analyzing.show').count(), 0, 'waiting is cleared after success');
|
||||
for (const key of ['front', 'left', 'right']) {
|
||||
const row = body.capture_diagnostics.views[key];
|
||||
assert.equal(row.mode, 'live'); assert.equal(row.direction_checked, true);
|
||||
|
||||
@@ -61,7 +61,7 @@ try {
|
||||
await page.goto(base);
|
||||
await page.waitForSelector('#s-splash.active');
|
||||
await page.waitForLoadState('networkidle');
|
||||
assert(!requests.some(p => /\.woff2$|faceapi|scan-(waiting|base|face)|product-\d+\.webp/.test(p)), 'heavy/scanning assets must not compete with entry');
|
||||
assert(!requests.some(p => /\.woff2$|faceapi|scan-(waiting|base|face)|product-\d+\.(webp|jpg)/.test(p)), 'heavy/scanning assets must not compete with entry');
|
||||
await page.waitForFunction(() => document.querySelector('#s-splash .splash-slogan').naturalWidth === 718 && document.querySelector('.brand-lockup').naturalWidth === 517);
|
||||
await screenshot('01-home');
|
||||
await page.click('[data-action="open-privacy"]');
|
||||
@@ -122,8 +122,7 @@ try {
|
||||
});
|
||||
assert(scanPixels[0].slice(0, 3).every(value => value > 245), 'original PSD bitmap is unchanged');
|
||||
assert(scanPixels[1].slice(0, 3).every(value => value > 245), 'original PSD bitmap is unchanged');
|
||||
assert.equal(await page.locator('#scan-photo').getAttribute('src'), pending.request().postDataJSON().front_image, 'real front photograph covers the white PSD placeholder');
|
||||
assert(await page.locator('#scan-photo').evaluate(image => image.complete && image.naturalWidth > 0));
|
||||
assert.equal(await page.locator('#scan-photo').count(), 0, 'white PSD head is not covered by user photograph');
|
||||
assert.equal(await page.locator('.scan-beam').evaluate(e => getComputedStyle(e).animationName), 'report-scan');
|
||||
assert.equal(await page.locator('#p-analyzing [data-action]').count(), 1, 'only cancel is actionable; PSD icons decorative');
|
||||
for (const [width, height] of [[320, 726], [375, 726], [390, 844], [414, 896], [480, 726], [375, 440]]) {
|
||||
@@ -173,7 +172,7 @@ try {
|
||||
});
|
||||
await page.goto(base + '?mock=1&screen=report'); await loadFonts();
|
||||
await page.waitForFunction(() => document.querySelector('#product-image').naturalWidth === 504);
|
||||
assert((await page.locator('#product-image').getAttribute('src')).endsWith('product-' + ({ 1: 25, 2: 35, 3: 50 }[tier]) + '.webp'));
|
||||
assert((await page.locator('#product-image').getAttribute('src')).endsWith('product-' + ({ 1: 25, 2: 35, 3: 50 }[tier]) + '.jpg'));
|
||||
for (const width of widths) {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
await page.locator('.card-product').scrollIntoViewIfNeeded();
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Real mobile pointer dispatch on the actual report. Auth/analysis writes blocked.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { fileURLToPath, 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 root = fileURLToPath(new URL('../../', import.meta.url));
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(root, 'design/ui-20261012/delivery-optimization-20261012.json')));
|
||||
for (const asset of manifest.assets) {
|
||||
for (const [file, expected] of [[asset.source, asset.source_sha256], [asset.file, asset.sha256]]) {
|
||||
assert.equal(crypto.createHash('sha256').update(fs.readFileSync(path.join(root, file))).digest('hex'), expected);
|
||||
}
|
||||
assert(asset.bytes < asset.source_bytes * .9 && asset.psnr_db >= 40);
|
||||
}
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true });
|
||||
const errors = [], writes = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.route('**/*', route => {
|
||||
const req = route.request();
|
||||
if (req.method() !== 'GET') { writes.push(req.url()); return route.abort(); }
|
||||
if (req.url().endsWith('/api/auth/session')) return route.fulfill({ json: { ok: true, user: { id: 'report-touch-fixture', kind: 'browser' } } });
|
||||
return route.continue();
|
||||
});
|
||||
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));
|
||||
await page.evaluate(async () => { await Promise.all([document.fonts.load('500 40px MiSans'), document.fonts.load('400 12px MiSans')]); });
|
||||
for (const width of [320, 375, 390, 414, 480]) {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
await page.waitForTimeout(400);
|
||||
for (const score of ['0', '75', '100', '--']) {
|
||||
await page.locator('#stat-score b').evaluate((el, value) => { el.textContent = value; }, score);
|
||||
const layout = await page.evaluate(() => {
|
||||
const rect = selector => {
|
||||
const r = document.querySelector(selector).getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, width: r.width, height: r.height, bottom: r.bottom };
|
||||
};
|
||||
return { dial: rect('.score-dial'), score: rect('#stat-score b'), title: rect('.sum-score > .stat-label'),
|
||||
ageTitle: rect('.sum-age > .stat-label'), age: rect('.age-dial'),
|
||||
careAlign: getComputedStyle(document.querySelector('.card-care .card-heading')).textAlign };
|
||||
});
|
||||
assert(Math.abs(layout.score.x + layout.score.width / 2 - layout.dial.x - layout.dial.width / 2) < 1, 'score horizontally centered: ' + width + '/' + score);
|
||||
assert(Math.abs(layout.score.y + layout.score.height / 2 - layout.dial.y - layout.dial.height / 2) < 1, 'score vertically centered');
|
||||
assert(layout.title.bottom < layout.dial.y && layout.ageTitle.bottom < layout.age.y, 'labels outside above their values');
|
||||
assert(Math.abs(layout.title.y - layout.ageTitle.y) < 1, 'labels share top alignment');
|
||||
assert.equal(layout.careAlign, 'left');
|
||||
}
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||
}
|
||||
await page.locator('#stat-score b').evaluate(el => { el.textContent = '75'; });
|
||||
await page.setViewportSize({ width: 375, height: 726 });
|
||||
const cdp = await page.context().newCDPSession(page), cards = page.locator('.card-detail');
|
||||
const offset = card => card.locator('.detail-panels').evaluate(el => new DOMMatrix(getComputedStyle(el).transform).m41);
|
||||
const selected = card => card.locator('.dtab.active').getAttribute('data-tab');
|
||||
const touch = (type, x, y, timestamp) => cdp.send('Input.dispatchTouchEvent', {
|
||||
type, ...(timestamp ? { timestamp } : {}), touchPoints: /touchEnd|touchCancel/.test(type) ? [] : [{ x, y, id: 1 }]
|
||||
});
|
||||
async function drag(card, dx, { dy = 0, cancel = false, pause = 0, flick = false, verify = false } = {}) {
|
||||
const frame = card.locator('.dpanel.active .viz-frame');
|
||||
await frame.scrollIntoViewIfNeeded(); await page.waitForTimeout(400);
|
||||
const box = await frame.boundingBox(), x = box.x + box.width / 2, y = Math.min(500, box.y + box.height / 2);
|
||||
const before = await offset(card), previous = await selected(card), now = flick ? Date.now() / 1000 : null;
|
||||
await touch('touchStart', x, y, now);
|
||||
await touch('touchMove', x + dx / 2, y + dy / 2, now && now + .02);
|
||||
await touch('touchMove', x + dx, y + dy, now && now + .04);
|
||||
if (verify) {
|
||||
assert(Math.abs(await offset(card) - before - dx) < 3, 'report follows finger before release');
|
||||
assert.equal(await selected(card), previous, 'selection commits only on release');
|
||||
assert.equal(await card.locator('.detail-panels.dragging').count(), 1);
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
fs.mkdirSync(process.env.H5_ARTIFACT_DIR, { recursive: true });
|
||||
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'report-drag-' + await card.locator('.dtab').first().textContent() + '.png') });
|
||||
}
|
||||
}
|
||||
if (pause) await page.waitForTimeout(pause);
|
||||
await touch(cancel ? 'touchCancel' : 'touchEnd', 0, 0, now && now + .05);
|
||||
await page.waitForTimeout(400);
|
||||
assert.equal(await card.locator('.detail-panels.dragging').count(), 0);
|
||||
assert.equal(await page.locator('#p-zoom.show').count(), 0, 'drag never opens zoom');
|
||||
}
|
||||
async function aligned(card) {
|
||||
const state = await card.evaluate(el => {
|
||||
const viewport = el.querySelector('.detail-viewport'), active = el.querySelector('.dpanel.active');
|
||||
return { viewport: viewport.getBoundingClientRect().height, panel: active.offsetHeight,
|
||||
delta: active.getBoundingClientRect().left - viewport.getBoundingClientRect().left,
|
||||
selected: el.querySelectorAll('.dtab[aria-selected="true"][tabindex="0"]').length,
|
||||
accessible: [...el.querySelectorAll('.dpanel')].filter(p => !p.inert && p.getAttribute('aria-hidden') === 'false').length };
|
||||
});
|
||||
assert(Math.abs(state.viewport - state.panel) < 2 && Math.abs(state.delta) < 2, 'selected panel fits/aligned: ' + JSON.stringify(state));
|
||||
assert.equal(state.selected, 1); assert.equal(state.accessible, 1);
|
||||
}
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const card = cards.nth(i);
|
||||
await drag(card, -100, { verify: true }); assert.equal(await selected(card), '1'); await aligned(card);
|
||||
await drag(card, 25, { pause: 160 }); assert.equal(await selected(card), '1', 'short slow drag snaps back');
|
||||
await drag(card, 100, { cancel: true }); assert.equal(await selected(card), '1', 'cancellation restores selection');
|
||||
await drag(card, 2, { dy: -90 }); assert.equal(await selected(card), '1', 'vertical scrolling preserves tab');
|
||||
await drag(card, 40, { flick: true }); assert.equal(await selected(card), '0', 'short fast flick goes back');
|
||||
await drag(card, 100); assert.equal(await selected(card), '0', 'first edge cannot overshoot');
|
||||
await card.locator('.dtab').last().click(); await page.waitForTimeout(400); await aligned(card);
|
||||
await drag(card, -100); assert.equal(await selected(card), String(i === 1 ? 2 : 1), 'last edge cannot overshoot');
|
||||
await card.locator('.dtab').last().focus(); await page.keyboard.press('Home'); await page.waitForTimeout(400); await aligned(card);
|
||||
}
|
||||
await drag(cards.nth(1), -100); await drag(cards.nth(1), -100); assert.equal(await selected(cards.nth(1)), '2');
|
||||
assert.equal(await selected(cards.nth(0)), '0'); assert.equal(await selected(cards.nth(2)), '0', 'groups are independent');
|
||||
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.waitForTimeout(400);
|
||||
for (const width of [320, 390, 480]) {
|
||||
await page.setViewportSize({ width, height: 844 }); await page.waitForTimeout(400);
|
||||
for (const card of await cards.all()) await aligned(card);
|
||||
}
|
||||
for (const asset of manifest.assets) {
|
||||
const response = await page.request.get(new URL(asset.file.replace(/^h5\//, ''), base).href);
|
||||
assert.equal(response.status(), 200); assert(response.headers()['content-type'].startsWith('image/jpeg'));
|
||||
assert.equal((await response.body()).length, asset.bytes);
|
||||
}
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
await page.setViewportSize({ width: 375, height: 726 });
|
||||
await page.locator('#s-report').evaluate(el => { el.scrollTop = 0; });
|
||||
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'report-summary.png') });
|
||||
}
|
||||
assert.deepEqual(errors, []); assert.deepEqual(writes, []);
|
||||
console.log('PASS: score centered/labels above/care left in five widths; real finger-following three report groups, flick/snap/cancel/vertical/edges/keyboard/zoom/resize/height/ARIA; smaller JPEG hashes/dimensions/MIME; no JS errors/backend writes');
|
||||
} finally { await browser.close(); }
|
||||
@@ -16,7 +16,7 @@ try {
|
||||
let pending, artworkFailure = false;
|
||||
await page.route('**/*', route => {
|
||||
const req = route.request();
|
||||
if (req.url().endsWith('/api/auth/session')) return route.fulfill({ json: { ok: true, user: { id: 'scan-photo-fixture', kind: 'browser' } } });
|
||||
if (req.url().endsWith('/api/auth/session')) return route.fulfill({ json: { ok: true, user: { id: 'scan-placeholder-fixture', kind: 'browser' } } });
|
||||
if (req.url().endsWith('/api/skin-analyze')) { pending = route; return; }
|
||||
if (req.method() !== 'GET') { unexpectedWrites.push(req.url()); return route.abort(); }
|
||||
if (/\.woff2$|\/vendor\/faceapi\//.test(req.url())) return route.abort();
|
||||
@@ -48,39 +48,43 @@ try {
|
||||
await page.setInputFiles('#cam-file', path.join(samples, name));
|
||||
}
|
||||
await page.waitForSelector('#p-analyzing.show');
|
||||
await page.waitForFunction(() => { const img = document.querySelector('#scan-photo'); return img.complete && img.naturalWidth > 0; });
|
||||
assert(pending);
|
||||
const sent = pending.request().postDataJSON();
|
||||
assert.equal(await page.locator('#scan-photo').getAttribute('src'), sent.front_image, 'waiting face is the current FRONT photo, not the last side or a placeholder');
|
||||
assert.equal(await page.locator('#scan-photo').count(), 0, 'waiting never inserts a user face');
|
||||
assert.equal(await page.locator('#p-analyzing img[src^="data:"]').count(), 0);
|
||||
assert.notEqual(sent.front_image, sent.left_image); assert.notEqual(sent.front_image, sent.right_image);
|
||||
assert.equal(await page.locator('#scan-photo.mirror').count(), 0, 'uploaded/system camera photos must not be mirrored again');
|
||||
assert.equal(await page.locator('#scan-photo').evaluate(img => getComputedStyle(img).objectFit), 'cover');
|
||||
assert.equal(await page.locator('#scan-face').evaluate(el => getComputedStyle(el).backgroundColor), 'rgb(17, 17, 17)', 'loading/error does not expose white placeholder');
|
||||
return sent;
|
||||
}
|
||||
function rejectPending(message) { const route = pending; pending = null; return route.fulfill({ json: { ok: false, message } }); }
|
||||
const cleared = async () => {
|
||||
assert.equal(await page.locator('#scan-photo').getAttribute('src'), null, 'face reference cleared on exit');
|
||||
assert.equal(await page.locator('#scan-photo').count(), 0);
|
||||
assert.equal(await page.locator('#p-analyzing.scan-running').count(), 0);
|
||||
};
|
||||
await profile();
|
||||
assert.equal(await page.locator('#scan-photo').getAttribute('src'), null, 'no face before capture');
|
||||
assert.equal(await page.locator('#scan-photo').count(), 0, 'no user photo element');
|
||||
await capture();
|
||||
await page.waitForSelector('#p-analyzing.scan-artwork-ready');
|
||||
assert((await page.locator('#scan-face').evaluate(el => getComputedStyle(el).maskImage)).includes('scan-face-mask.png'));
|
||||
assert.equal(await page.locator('.scan-beam').evaluate(el => getComputedStyle(el).animationName), 'report-scan');
|
||||
const pixels = await page.locator('#scan-artwork').evaluate(image => {
|
||||
const canvas = document.createElement('canvas'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight;
|
||||
const context = canvas.getContext('2d'); context.drawImage(image, 0, 0);
|
||||
return [[540, 720], [540, 1200]].map(([x, y]) => [...context.getImageData(x, y, 1, 1).data]);
|
||||
});
|
||||
assert(pixels.every(pixel => pixel.slice(0, 3).every(value => value > 245)), 'original white head is preserved');
|
||||
for (const [width, height] of [[320, 726], [375, 726], [390, 844], [414, 896], [480, 726], [375, 440]]) {
|
||||
await page.setViewportSize({ width, height });
|
||||
const photo = await page.locator('#scan-photo').boundingBox();
|
||||
assert(Math.abs(photo.width - Math.min(width, 480)) < 1 && Math.abs(photo.height - height) < 1, 'same cover viewport as camera');
|
||||
assert(Math.abs(photo.x - (width - photo.width) / 2) < 1 && Math.abs(photo.y) < 1, 'photo crop stays centered independently of PSD contain mask');
|
||||
const art = await page.locator('#scan-artwork').boundingBox();
|
||||
assert(art.width <= width + 1 && art.height <= height + 1);
|
||||
assert(Math.abs(art.width / art.height - 1080 / 2091) < .001, 'PSD is contained without cropping');
|
||||
assert(Math.abs(art.x - (width - art.width) / 2) < 1 && Math.abs(art.y - (height - art.height) / 2) < 1);
|
||||
assert(await page.locator('.scan-cancel').isVisible());
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||
}
|
||||
await page.setViewportSize({ width: 375, height: 726 });
|
||||
if (process.env.H5_ARTIFACT_DIR) {
|
||||
fs.mkdirSync(process.env.H5_ARTIFACT_DIR, { recursive: true });
|
||||
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'scan-photo.png') });
|
||||
await page.screenshot({ path: path.join(process.env.H5_ARTIFACT_DIR, 'scan-placeholder.png') });
|
||||
}
|
||||
await page.mouse.click(187, 350);
|
||||
assert(await page.locator('#p-analyzing').isVisible(), 'clicking the face does not cancel');
|
||||
@@ -97,14 +101,13 @@ try {
|
||||
await pending.fulfill({ json: success }); pending = null;
|
||||
await page.waitForSelector('#s-report.active'); await cleared();
|
||||
assert(await page.evaluate(() => window.__scanIterations.some(row => row.target === 'scan-beam' && row.elapsed >= 1)), 'fast success completes an actual cycle');
|
||||
// Failed artwork/mask preserves the real photograph + status + cancel.
|
||||
// Failed artwork/mask preserves a readable status and cancel, not a user photo.
|
||||
artworkFailure = true;
|
||||
await profile(); await capture();
|
||||
assert.equal(await page.locator('#p-analyzing.scan-artwork-ready').count(), 0);
|
||||
assert.equal(await page.locator('#scan-face').evaluate(el => getComputedStyle(el).maskImage), 'none');
|
||||
assert.equal(await page.locator('.scan-status').evaluate(el => getComputedStyle(el).clipPath), 'none');
|
||||
assert.equal(await page.locator('#scan-photo').evaluate(img => getComputedStyle(img).visibility), 'visible');
|
||||
assert.equal(await page.locator('#scan-artwork').evaluate(img => getComputedStyle(img).visibility), 'hidden');
|
||||
await page.click('[data-action="cancel-analysis"]'); await cleared(); await rejectPending('canceled');
|
||||
assert.deepEqual(errors, []); assert.deepEqual(unexpectedWrites, []);
|
||||
console.log('PASS: actual three-photo waiting shows current front JPEG; original orientation/cover/PSD mask/scan; six viewport sizes; photo cleanup after cancel/error/success; stale response; one real reduced-motion cycle; image/mask failure fallback; no JS errors/backend writes');
|
||||
console.log('PASS: actual three-photo waiting restores original white PSD head, no user photograph; six viewport sizes; cancel/error/success cleanup; stale response; one real reduced-motion cycle; image/mask failure fallback; no JS errors/backend writes');
|
||||
} finally { await browser.close(); }
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export smaller delivery JPEGs; never overwrite the PSD-derived originals.
|
||||
|
||||
Only opaque-on-white product cards are candidates. Other WebPs are already
|
||||
smaller than JPEG, and logos/icons/masks need their original transparency.
|
||||
"""
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image, ImageChops, ImageStat
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
DIRECTORY = ROOT / 'h5/assets/img/ui-20261012'
|
||||
|
||||
|
||||
def main():
|
||||
assets = []
|
||||
for age in (25, 35, 50):
|
||||
source = DIRECTORY / f'product-{age}.webp'
|
||||
rgba = Image.open(source).convert('RGBA')
|
||||
image = Image.new('RGB', rgba.size, 'white')
|
||||
image.paste(rgba, mask=rgba.getchannel('A'))
|
||||
for quality in (90, 92, 94, 96):
|
||||
buffer = io.BytesIO()
|
||||
image.save(buffer, 'JPEG', quality=quality, subsampling=0,
|
||||
optimize=True, progressive=True)
|
||||
encoded = buffer.getvalue()
|
||||
decoded = Image.open(io.BytesIO(encoded)).convert('RGB')
|
||||
rms = ImageStat.Stat(ImageChops.difference(image, decoded)).rms
|
||||
mse = sum(value ** 2 for value in rms) / 3
|
||||
psnr = 10 * math.log10(255 ** 2 / mse) if mse else 100
|
||||
if psnr >= 40 and len(encoded) < source.stat().st_size * .9:
|
||||
break
|
||||
else:
|
||||
raise RuntimeError(f'No suitably small high-quality JPEG for {source}')
|
||||
target = source.with_suffix('.jpg')
|
||||
target.write_bytes(encoded)
|
||||
assets.append({
|
||||
'source': str(source.relative_to(ROOT)),
|
||||
'source_sha256': hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
'source_bytes': source.stat().st_size,
|
||||
'file': str(target.relative_to(ROOT)),
|
||||
'sha256': hashlib.sha256(encoded).hexdigest(),
|
||||
'bytes': len(encoded), 'size': list(image.size),
|
||||
'quality': quality, 'subsampling': 0, 'progressive': True,
|
||||
'matte': '#FFFFFF', 'psnr_db': round(psnr, 2),
|
||||
})
|
||||
manifest = {
|
||||
'description': 'Smaller product-card delivery images. Original PSD crops preserved.',
|
||||
'retained': 'Background/scan WebPs are smaller than JPEG; transparent icons/masks remain PNG.',
|
||||
'assets': assets,
|
||||
'source_bytes': sum(row['source_bytes'] for row in assets),
|
||||
'delivery_bytes': sum(row['bytes'] for row in assets),
|
||||
}
|
||||
destination = ROOT / 'design/ui-20261012/delivery-optimization-20261012.json'
|
||||
destination.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n', encoding='utf8')
|
||||
print(json.dumps(manifest, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user