feat(h5): remember profile selections and add finger-following skin swipe
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# 基础信息记忆与肤质拖动
|
||||
|
||||
## 范围
|
||||
|
||||
- 出生年月、性别、地区每次点滚轮“确定”即保存,不必拍照或提交报告。
|
||||
- 日常肤质切换后也保存。再次进入基础信息页,包括刷新、返回、重新登录同一账号时,默认使用本机上次已确认的值;滚轮重新打开也定位到原选择。
|
||||
- 保存出生年月而非固定年龄,进入页面和提交前按当前年月重新计算实际年龄。
|
||||
- 使用当前登录账号的 `kind + id` 隔离本机缓存。切换微信/游客或另一个账号时,页面值与滚轮位置一起重置/恢复,不串用其他账号信息。
|
||||
- 这是本机浏览器偏好缓存,不新增服务端档案同步接口;同一微信账号换手机仍可看服务器报告,但本次新增的基础信息默认值不跨设备。清除浏览器数据后需重新填写。
|
||||
- 老版本没有单独保存出生年月,无法准确反推出原选择;更新后需确认一次,从此开始记住。缓存被禁用或容量不足时保持当前页面可用,在该页面内记忆,不保证刷新后保留。
|
||||
- 缓存只包含出生年月、性别、省市、肤质序号,不复制身份凭据、照片、报告。
|
||||
|
||||
## 肤质交互
|
||||
|
||||
- 水平方向拖动时,图片、名称与说明一起实时跟随手指,不再等松手才切换。
|
||||
- 松手按距离/轻扫速度吸附到相应选项;短距离慢拖回原项。
|
||||
- 首尾拖动有弹性阻力,松手回到有效选项;系统取消手势时复位。
|
||||
- 横竖方向锁定,保留 `touch-action: pan-y`,不阻塞页面上下滚动;屏幕尺寸变化后保持选项对齐。
|
||||
- 原有箭头、圆点保留;补充左右方向键支持。
|
||||
- 不改变 PSD 素材、颜色、页面布局、拍摄约束、评分或推荐规则。
|
||||
|
||||
## 回归命令
|
||||
|
||||
```bash
|
||||
node h5/test/profile-store.js
|
||||
node h5/test/profile-interaction.mjs
|
||||
H5_BASE_URL=https://1me.xajuzi.com/state/ node h5/test/profile-interaction.mjs
|
||||
```
|
||||
|
||||
浏览器测试使用真实页面与 mobile-select 控件,以模拟账号和真实触摸事件检查恢复、账号隔离、跟手位移、吸附、短拖、轻扫、首尾阻力、系统取消、纵向手势以及不同屏宽。测试只跳过字体下载、拦截登录会话并暴露滚轮实例用于选择测试值;所有非 GET 请求均阻止,不会新增线上账号、报告或付费检测调用。
|
||||
|
||||
本地已通过上述两套新增测试,以及第四轮反馈浏览器闭环、登录加载与超时、报告持久化、账号/OAuth 隔离、真实本地模型自动三连拍与最终 JPEG 复验、推荐 126 用例及算法真值 86 断言。
|
||||
|
||||
部署目标为 `ubuntu@1me.xajuzi.com:/home/ubuntu/juzi`,基线 `c9352fe`。沿用 Git bundle 快进同步,不代用户推送 origin,不改服务器环境配置或历史数据。上线后核对静态文件哈希、服务健康、匿名访问保护,并执行同一套只读模拟账号浏览器测试。
|
||||
@@ -4,9 +4,9 @@
|
||||
"2": "app.js",
|
||||
"3": "skin-score.js",
|
||||
"4": "customer-feedback-second.js",
|
||||
"5": "n",
|
||||
"6": "extractFaceTensors",
|
||||
"7": ".forwardInput",
|
||||
"5": "f",
|
||||
"6": "Box",
|
||||
"7": "test/profile-store.js",
|
||||
"8": "admin.js",
|
||||
"9": "__awaiter",
|
||||
"10": "wechat-browser.mjs",
|
||||
@@ -42,7 +42,7 @@
|
||||
"40": "第四轮反馈修改与验收",
|
||||
"41": "NeuralNetwork",
|
||||
"42": "layer_at",
|
||||
"43": "normalize",
|
||||
"43": "convLayer",
|
||||
"44": "start-local.sh",
|
||||
"45": "PredictAllAgeAndGenderWithFaceAlignmentTask",
|
||||
"46": "PredictAllFaceExpressionsWithFaceAlignmentTask",
|
||||
@@ -54,7 +54,7 @@
|
||||
"52": "region-data.js",
|
||||
"53": "护理建议(全量文本,按需求摘抄)",
|
||||
"54": "UI 对齐 10.09 设计稿:调研与实施计划",
|
||||
"55": "pn",
|
||||
"55": "i",
|
||||
"56": "report-ui.js",
|
||||
"57": "regression.js",
|
||||
"58": ".dispose",
|
||||
@@ -65,19 +65,19 @@
|
||||
"63": "e2e-browser.mjs",
|
||||
"64": "account-client.js",
|
||||
"65": "微信登录接入与公网部署说明",
|
||||
"66": "FaceLandmarks68",
|
||||
"66": "FaceLandmarks",
|
||||
"67": "ui-20261009.mjs",
|
||||
"68": "analyze-timeout.js",
|
||||
"69": "misans/README.md",
|
||||
"70": "extractConvParamsFactory",
|
||||
"71": "extractAllFacesAndComputeResults",
|
||||
"70": ".forwardInput",
|
||||
"71": ".predictAgeAndGender",
|
||||
"72": "build_report_copy.py",
|
||||
"73": "camera-geometry.js",
|
||||
"74": "report-copy.js",
|
||||
"75": "prepare_h5_assets.py",
|
||||
"76": ".toString",
|
||||
"77": ".runAndExtendWithFaceDetections",
|
||||
"78": "i",
|
||||
"76": ".forwardInput",
|
||||
"77": "extractFaceTensors",
|
||||
"78": "n",
|
||||
"79": "登录反馈与加载优化(2026-10-12)",
|
||||
"80": "线上微信登录部署记录",
|
||||
"81": "wechat-production.mjs",
|
||||
@@ -98,13 +98,14 @@
|
||||
"96": "production-loop.mjs",
|
||||
"97": "PredictSingleAgeAndGenderWithFaceAlignmentTask",
|
||||
"98": "PredictSingleFaceExpressionsWithFaceAlignmentTask",
|
||||
"99": "DetectSingleFaceLandmarksTask",
|
||||
"100": "ComputeAllFaceDescriptorsTask",
|
||||
"99": "PredictSingleFaceExpressionsTask",
|
||||
"100": "age",
|
||||
"101": "drawFaceExpressions",
|
||||
"102": "TinyYolov2",
|
||||
"102": ".detect",
|
||||
"103": "第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
"104": "export_fourth_feedback_assets.py",
|
||||
"105": "capture-quality-browser.mjs",
|
||||
"106": "第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md",
|
||||
"107": "ComputeSingleFaceDescriptorTask"
|
||||
"107": "基础信息记忆与肤质拖动",
|
||||
"108": "profile-interaction.mjs"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"0": "65cda48e6d2f67a3", "1": "cb6ea50bd4e4a5ca", "2": "b159712c832c558e", "3": "bcbfbc0b66f44357", "4": "56bf12952b0de42b", "5": "8e50b0e3c19c0a11", "6": "e154e5d8b03e5206", "7": "880a74c6d255e5cc", "8": "a689fcb62e7754af", "9": "63bb420d48d7a255", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "b1a71a621511068d", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "59485c317afc6c2b", "22": "512a2535c88c4284", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "041694c55d6f8a30", "27": "eab427ce9a5219e9", "28": "d3fab0fa540bd925", "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": "7986b9d5a423a3fc", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "82703230bd5ed1e1", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "803dac08b323abc2", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "0890ad2144e37304", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "74ae017de0ffd36b", "66": "b6d6154e2dd14637", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "af5864c4ac65160d", "71": "06e9a65e05e71d35", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "74310612e8e059f4", "77": "2ee521497bb8506a", "78": "5cb44ae4ec05213b", "79": "c6cfdfe181232ecc", "80": "315325cde1352e98", "81": "37c559741825be74", "82": "200f25fbad7af8fb", "83": "1284897282eb47a3", "84": "d8bb296741d3d05c", "85": "4bc370be8afd7db1", "86": "e858d75e995c92e5", "87": "e5fa7717989eda80", "88": "8b5ab5232cef3457", "89": "9e3363b80eeceeea", "90": "dd1c246e19a2d032", "91": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "8bd61518dd267641", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "23f1d406f6c779ae", "100": "3dd68cce72af5314", "101": "a0bc88eb14b70170", "102": "29790cf710874dd0", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "0567be4dc8048b9c", "106": "8bc72c179793e1c2", "107": "7ac475ea9bc280f8"}
|
||||
{"0": "200bd332d330e79e", "1": "cb6ea50bd4e4a5ca", "2": "86eae3fd1b848c2e", "3": "1f737ca8edb978f2", "4": "56bf12952b0de42b", "5": "06906d9c95022851", "6": "7cd2b329434ded47", "7": "03b56b13a08b5f83", "8": "a689fcb62e7754af", "9": "fce137e5717d2018", "10": "2b037735e1f5b0f6", "11": "4a490abd5e503318", "12": "aeca274694e158d2", "13": "7e3b558a587a4ebe", "14": "bf75bfc99d4f9e34", "15": "2c0eaf406546159b", "16": "95add6a975c0cb72", "17": "e042d7fe7351bcb9", "18": "f93d5d85e671fcee", "19": "39abb1eef19df707", "20": "aecf2fce44ba49c5", "21": "02ac512b5e01b983", "22": "f2045a9940596eb3", "23": "90fac00055070ce2", "24": "f85b5b1634224c86", "25": "46e8db3e6e3f37ea", "26": "8367cbe6ea2844bc", "27": "eab427ce9a5219e9", "28": "4f01dfb5cd033c39", "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": "4eaa3e8b9eed3bf1", "44": "852c1bb2446edd9c", "45": "92ab3e5134ed8685", "46": "65718282b3c32109", "47": "e594ecaa8e7d1538", "48": "cf2f2424bc00436f", "49": "1f976711cda158dc", "50": "bbb2b57d72922b16", "51": "eb6a182204867275", "52": "2ec3c8c088568ba9", "53": "68c45056378341a7", "54": "c58d41e8e580fdc6", "55": "af2dbb7480405e14", "56": "9ee2ad45dc27851d", "57": "f2a40b5f1b29f669", "58": "5a45b41665ef1112", "59": "891f59cda3708718", "60": "a505b7de6257a4e8", "61": "94ead9feda213ac6", "62": "1366ae24c7e5897d", "63": "c256b43f75d7809e", "64": "ce0e68dab611f022", "65": "bdee3c4a37df52f8", "66": "7798792de7e51a25", "67": "3e126d8deaf6e200", "68": "6b91f3679541de79", "69": "e511846eeba03076", "70": "e39b60658e2c785e", "71": "a2b13160223b5a98", "72": "c84f196a7bd35861", "73": "2b887bf601ccfb9f", "74": "ae2a154311e1f53a", "75": "69fdd23278518884", "76": "880a74c6d255e5cc", "77": "2050a9da67ccfa24", "78": "9daf5044cb78b2d2", "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": "83b0fd3b14363e4d", "92": "fced244d93677d90", "93": "8bd61518dd267641", "94": "713d04996c4627df", "95": "59c31f858c7b6970", "96": "4195478d5c528371", "97": "01f68dfc34a29d32", "98": "8c471c7baeaaca54", "99": "cb5ac3401283c378", "100": "b234c5e4ac7f632d", "101": "a0bc88eb14b70170", "102": "8dc2ed447b742130", "103": "6ffbe07b521a061b", "104": "e114a7aebaaca2f1", "105": "0567be4dc8048b9c", "106": "8bc72c179793e1c2", "107": "d9c195d9ccdb137e", "108": "e0fc0a42e33bda91"}
|
||||
+104
-83
@@ -1,16 +1,16 @@
|
||||
# Graph Report - juzi (2026-10-12)
|
||||
|
||||
## Corpus Check
|
||||
- 93 files · ~4,889,148 words
|
||||
- 97 files · ~4,891,452 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
- 1387 nodes · 2814 edges · 108 communities (82 shown, 26 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 189 edges (avg confidence: 0.54)
|
||||
- 1420 nodes · 2864 edges · 109 communities (87 shown, 22 thin omitted)
|
||||
- Extraction: 93% EXTRACTED · 7% INFERRED · 0% AMBIGUOUS · INFERRED: 199 edges (avg confidence: 0.55)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Graph Freshness
|
||||
- Built from commit: `3f165bf9`
|
||||
- Built from commit: `c9352fe4`
|
||||
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
|
||||
- Run `graphify update .` after code changes (no API cost).
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
- app.js
|
||||
- skin-score.js
|
||||
- customer-feedback-second.js
|
||||
- n
|
||||
- extractFaceTensors
|
||||
- .forwardInput
|
||||
- f
|
||||
- Box
|
||||
- test/profile-store.js
|
||||
- admin.js
|
||||
- __awaiter
|
||||
- wechat-browser.mjs
|
||||
@@ -56,7 +56,7 @@
|
||||
- 第四轮反馈修改与验收
|
||||
- NeuralNetwork
|
||||
- layer_at
|
||||
- normalize
|
||||
- convLayer
|
||||
- start-local.sh
|
||||
- PredictAllAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictAllFaceExpressionsWithFaceAlignmentTask
|
||||
@@ -66,7 +66,7 @@
|
||||
- call_skinanalyze_3view.sh
|
||||
- 护理建议(全量文本,按需求摘抄)
|
||||
- UI 对齐 10.09 设计稿:调研与实施计划
|
||||
- pn
|
||||
- i
|
||||
- report-ui.js
|
||||
- regression.js
|
||||
- .dispose
|
||||
@@ -76,16 +76,16 @@
|
||||
- e2e-browser.mjs
|
||||
- account-client.js
|
||||
- 微信登录接入与公网部署说明
|
||||
- FaceLandmarks68
|
||||
- FaceLandmarks
|
||||
- ui-20261009.mjs
|
||||
- analyze-timeout.js
|
||||
- misans/README.md
|
||||
- extractConvParamsFactory
|
||||
- extractAllFacesAndComputeResults
|
||||
- .forwardInput
|
||||
- .predictAgeAndGender
|
||||
- build_report_copy.py
|
||||
- .toString
|
||||
- .runAndExtendWithFaceDetections
|
||||
- i
|
||||
- .forwardInput
|
||||
- extractFaceTensors
|
||||
- n
|
||||
- 登录反馈与加载优化(2026-10-12)
|
||||
- 线上微信登录部署记录
|
||||
- wechat-production.mjs
|
||||
@@ -105,13 +105,14 @@
|
||||
- production-loop.mjs
|
||||
- PredictSingleAgeAndGenderWithFaceAlignmentTask
|
||||
- PredictSingleFaceExpressionsWithFaceAlignmentTask
|
||||
- DetectSingleFaceLandmarksTask
|
||||
- ComputeAllFaceDescriptorsTask
|
||||
- PredictSingleFaceExpressionsTask
|
||||
- age
|
||||
- drawFaceExpressions
|
||||
- TinyYolov2
|
||||
- .detect
|
||||
- 第四轮反馈/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- 第三轮反馈20261012/傅艳NEW与项思炼的会话 2026年10月12日.md
|
||||
- ComputeSingleFaceDescriptorTask
|
||||
- 基础信息记忆与肤质拖动
|
||||
- profile-interaction.mjs
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `__awaiter()` - 59 edges
|
||||
@@ -128,23 +129,23 @@
|
||||
## 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
|
||||
- `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 (108 total, 26 thin omitted)
|
||||
## Communities (109 total, 22 thin omitted)
|
||||
|
||||
### Community 0 - "face-api.js"
|
||||
Cohesion: 0.02
|
||||
Nodes (51): bgrToRgbTensor(), cn(), computeReshapedDimensions(), createBrowserEnv(), createFaceDetectionNet(), createSsdMobilenetv1(), Dimensions(), dr() (+43 more)
|
||||
Nodes (39): cn(), computeReshapedDimensions(), createBrowserEnv(), createFileSystem(), createNodejsEnv(), Dimensions(), dr(), en() (+31 more)
|
||||
|
||||
### Community 1 - "线上闭环测试(不含真实微信授权)"
|
||||
Cohesion: 0.25
|
||||
@@ -152,35 +153,35 @@ Nodes (7): 发现并修复, 可复现命令, 已通过的检查, 线上闭环测
|
||||
|
||||
### Community 2 - "app.js"
|
||||
Cohesion: 0.05
|
||||
Nodes (85): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), captureFromVideo(), captureHaptic(), checkStill() (+77 more)
|
||||
Nodes (90): applyMirror(), armNudge(), beginDetect(), bindBackdropClose(), busyWithFlow(), cancelSkinDrag(), captureFromVideo(), captureHaptic() (+82 more)
|
||||
|
||||
### Community 3 - "skin-score.js"
|
||||
Cohesion: 0.11
|
||||
Cohesion: 0.12
|
||||
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 - "n"
|
||||
### Community 5 - "f"
|
||||
Cohesion: 0.12
|
||||
Nodes (43): a(), ae(), ba(), bf(), cf(), d(), da(), drawContour() (+35 more)
|
||||
Nodes (36): a(), b(), ba(), cf(), d(), da(), drawContour(), dt() (+28 more)
|
||||
|
||||
### Community 6 - "extractFaceTensors"
|
||||
Cohesion: 0.16
|
||||
Nodes (5): Box(), extendWithFaceLandmarks(), extractFaceTensors(), FaceLandmarks(), minBbox()
|
||||
### Community 6 - "Box"
|
||||
Cohesion: 0.11
|
||||
Nodes (5): Box(), ComputeAllFaceDescriptorsTask(), ComputeSingleFaceDescriptorTask(), extendWithFaceDescriptor(), ObjectDetection()
|
||||
|
||||
### Community 7 - ".forwardInput"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
|
||||
### Community 7 - "test/profile-store.js"
|
||||
Cohesion: 0.14
|
||||
Nodes (13): assert, data, empty, now, other, Profile, quota, regions (+5 more)
|
||||
|
||||
### Community 8 - "admin.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (29): ageText(), api(), bind(), captureQualityHtml(), closeDetail(), collectFilters(), dimsHtml(), doRetry() (+21 more)
|
||||
|
||||
### Community 9 - "__awaiter"
|
||||
Cohesion: 0.12
|
||||
Nodes (14): __awaiter(), bufferToImage(), ComposableTask(), extendWithFaceExpressions(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow() (+6 more)
|
||||
Cohesion: 0.13
|
||||
Nodes (13): __awaiter(), ComposableTask(), extractSingleFaceAndComputeResult(), fetchImage(), fetchJson(), fetchNetWeights(), fetchOrThrow(), __generator() (+5 more)
|
||||
|
||||
### Community 10 - "wechat-browser.mjs"
|
||||
Cohesion: 0.22
|
||||
@@ -199,8 +200,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.24
|
||||
Nodes (15): awaitMediaLoaded(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv(), getMediaDimensions() (+7 more)
|
||||
Cohesion: 0.19
|
||||
Nodes (18): awaitMediaLoaded(), bufferToImage(), createCanvas(), createCanvasFromMedia(), extractFaces(), extractImagePatches(), getContext2dOrThrow(), getEnv() (+10 more)
|
||||
|
||||
### Community 16 - "巨子测肤 · 1'me 在线肌肤测试 — UI 设计规范"
|
||||
Cohesion: 0.15
|
||||
@@ -223,8 +224,8 @@ Cohesion: 0.40
|
||||
Nodes (9): build_case1(), build_case2(), build_truth(), col(), dump(), main(), parse_value(), set_path() (+1 more)
|
||||
|
||||
### Community 21 - "disposeUnusedWeightTensors"
|
||||
Cohesion: 0.10
|
||||
Nodes (24): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+16 more)
|
||||
Cohesion: 0.08
|
||||
Nodes (25): disposeUnusedWeightTensors(), extractorsFactory$3(), extractorsFactory$5(), extractorsFactory$7(), extractorsFactory$9(), extractParamsFromWeigthMap(), extractParamsFromWeigthMap$1(), extractParamsFromWeigthMap$2() (+17 more)
|
||||
|
||||
### Community 23 - "facepp-skinanalyze(旷视皮肤分析专业版调用技能)"
|
||||
Cohesion: 0.20
|
||||
@@ -239,16 +240,16 @@ Cohesion: 0.20
|
||||
Nodes (16): _arrayLikeToArray(), _arrayWithHoles(), _arrayWithoutHoles(), _classCallCheck(), _createClass(), _defineProperties(), h(), _iterableToArray() (+8 more)
|
||||
|
||||
### Community 26 - ".extractWeights"
|
||||
Cohesion: 0.14
|
||||
Nodes (14): createFaceRecognitionNet(), createMtcnn(), createTinyFaceDetector(), createTinyYolov2(), extractFCParamsFactory(), extractorsFactory$2(), extractorsFactory$4(), extractorsFactory$8() (+6 more)
|
||||
Cohesion: 0.09
|
||||
Nodes (24): createFaceDetectionNet(), createFaceRecognitionNet(), createMtcnn(), createSsdMobilenetv1(), createTinyFaceDetector(), createTinyYolov2(), extractConvParamsFactory(), extractFCParamsFactory() (+16 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.09
|
||||
Nodes (36): ao(), Bo(), C(), cd(), cl(), co(), Do(), e() (+28 more)
|
||||
Cohesion: 0.08
|
||||
Nodes (35): ao(), Bo(), C(), cd(), cl(), co(), Do(), eo() (+27 more)
|
||||
|
||||
### Community 29 - "face-api.js 本地化依赖(h5/vendor/faceapi/)"
|
||||
Cohesion: 0.29
|
||||
@@ -282,9 +283,9 @@ Nodes (8): PSD 来源, 前端参数及限制, 后台校准留痕, 已确认并
|
||||
Cohesion: 0.18
|
||||
Nodes (11): list, bounds(), layer_at(), main(), save_image(), composite(), main(), sha256() (+3 more)
|
||||
|
||||
### Community 43 - "normalize"
|
||||
Cohesion: 0.07
|
||||
Nodes (28): conv(), conv$1(), convDown(), convLayer(), convLayer$1(), convNoRelu(), convWithBatchNorm(), denseBlock3() (+20 more)
|
||||
### Community 43 - "convLayer"
|
||||
Cohesion: 0.23
|
||||
Nodes (11): convLayer(), convWithBatchNorm(), depthwiseSeparableConv$1(), fullyConnectedLayer(), leaky(), ONet(), PNet(), prelu() (+3 more)
|
||||
|
||||
### Community 44 - "start-local.sh"
|
||||
Cohesion: 0.50
|
||||
@@ -306,9 +307,9 @@ Nodes (19): 5\.1 轮廓(紧塑力), 5\.2 皱纹(紧塑力), 5\.3 肤色
|
||||
Cohesion: 0.11
|
||||
Nodes (17): 1. 结论和依据, 2. 统一比较口径, 3. 逐页差异和调整内容, 4. 报告数据如何接到新 UI, 5. 登录与“我的报告”的真实能力缺口, 6. 原稿中需要整理的内容, 7. 实施顺序与交付物, 8. 验收定义 (+9 more)
|
||||
|
||||
### Community 55 - "pn"
|
||||
Cohesion: 0.25
|
||||
Nodes (8): bn(), fn(), ln(), pd(), pn(), un(), wn(), xn()
|
||||
### Community 55 - "i"
|
||||
Cohesion: 0.09
|
||||
Nodes (26): bn(), ca(), dn(), ee(), et(), fa(), fn(), i() (+18 more)
|
||||
|
||||
### Community 56 - "report-ui.js"
|
||||
Cohesion: 0.50
|
||||
@@ -319,8 +320,12 @@ Cohesion: 0.60
|
||||
Nodes (5): diffCtx(), loadJSON(), main(), okNum(), okStr()
|
||||
|
||||
### Community 58 - ".dispose"
|
||||
Cohesion: 0.15
|
||||
Nodes (9): FaceRecognitionNet(), NetInput(), nonMaxSuppression(), scale(), SsdMobilenetv1(), stage1(), stage2(), stage3() (+1 more)
|
||||
Cohesion: 0.12
|
||||
Nodes (13): bgrToRgbTensor(), DetectSingleFaceLandmarksTask(), extendWithFaceLandmarks(), extractBoundingBoxes(), FaceRecognitionNet(), getSizesForScale(), nonMaxSuppression(), normalize$1() (+5 more)
|
||||
|
||||
### Community 61 - "FaceProcessor"
|
||||
Cohesion: 0.22
|
||||
Nodes (3): extractParams$1(), FaceLandmark68TinyNet(), FaceProcessor()
|
||||
|
||||
### Community 62 - "提交前 Review(2026-10-11)"
|
||||
Cohesion: 0.40
|
||||
@@ -335,39 +340,39 @@ Cohesion: 0.80
|
||||
Nodes (5): browser(), init(), read(), rememberKind(), request()
|
||||
|
||||
### Community 65 - "微信登录接入与公网部署说明"
|
||||
Cohesion: 0.22
|
||||
Cohesion: 0.29
|
||||
Nodes (7): 公网服务器配置, 公网真机验收清单, 反向代理与后台保护, 已完成的链路, 微信登录接入与公网部署说明, 数据、迁移与备份, 本机开发环境限制(不代表线上状态)
|
||||
|
||||
### Community 66 - "FaceLandmarks68"
|
||||
Cohesion: 0.10
|
||||
Nodes (8): FaceLandmark68NetBase(), FaceLandmarks5(), FaceLandmarks68(), getCenterPoint(), Point(), pt(), vt(), wl()
|
||||
### Community 66 - "FaceLandmarks"
|
||||
Cohesion: 0.06
|
||||
Nodes (16): conv(), denseBlock3(), denseBlock4(), depthwiseSeparableConv(), FaceFeatureExtractor(), FaceLandmark68NetBase(), FaceLandmarks(), FaceLandmarks5() (+8 more)
|
||||
|
||||
### Community 68 - "analyze-timeout.js"
|
||||
Cohesion: 0.20
|
||||
Nodes (7): assert, end, fs, path, source, start, vm
|
||||
|
||||
### Community 70 - "extractConvParamsFactory"
|
||||
Cohesion: 0.20
|
||||
Nodes (9): extractConvParamsFactory(), extractorsFactory(), extractorsFactory$1(), extractorsFactory$6(), extractParams(), extractParams$2(), extractParams$6(), extractParamsTiny() (+1 more)
|
||||
### Community 70 - ".forwardInput"
|
||||
Cohesion: 0.39
|
||||
Nodes (7): conv$1(), convDown(), convLayer$1(), convNoRelu(), residual(), residualDown(), __spreadArrays()
|
||||
|
||||
### Community 71 - "extractAllFacesAndComputeResults"
|
||||
Cohesion: 0.29
|
||||
Nodes (5): extendWithAge(), extendWithFaceDescriptor(), extendWithGender(), extractAllFacesAndComputeResults(), extractSingleFaceAndComputeResult()
|
||||
### Community 71 - ".predictAgeAndGender"
|
||||
Cohesion: 0.28
|
||||
Nodes (4): extendWithAge(), extendWithGender(), PredictAllAgeAndGenderTask(), PredictSingleAgeAndGenderTask()
|
||||
|
||||
### Community 76 - ".toString"
|
||||
Cohesion: 0.33
|
||||
Nodes (5): createFileSystem(), createNodejsEnv(), FaceMatch(), la(), pr()
|
||||
### Community 76 - ".forwardInput"
|
||||
Cohesion: 0.22
|
||||
Nodes (9): boxPredictionLayer(), decodeBoxesLayer(), depthwiseConvLayer(), getCenterCoordinatesAndSizesLayer(), getStridesForLayerIdx(), mobileNetV1(), outputLayer(), pointwiseConvLayer() (+1 more)
|
||||
|
||||
### Community 77 - ".runAndExtendWithFaceDetections"
|
||||
Cohesion: 0.18
|
||||
Nodes (8): DetectAllFacesTask(), DrawBox(), drawDetections(), DrawFaceLandmarks(), extendWithFaceDetection(), isWithFaceDetection(), isWithFaceLandmarks(), resizeResults()
|
||||
### Community 77 - "extractFaceTensors"
|
||||
Cohesion: 0.12
|
||||
Nodes (13): DetectAllFacesTask(), DrawBox(), drawDetections(), DrawFaceLandmarks(), extendWithFaceDetection(), extendWithFaceExpressions(), extractAllFacesAndComputeResults(), extractFaceTensors() (+5 more)
|
||||
|
||||
### Community 78 - "i"
|
||||
Cohesion: 0.06
|
||||
Nodes (45): b(), be(), bi(), ca(), ce(), de(), dn(), ee() (+37 more)
|
||||
### Community 78 - "n"
|
||||
Cohesion: 0.10
|
||||
Nodes (39): ae(), be(), bf(), bi(), ce(), de(), e(), Ef() (+31 more)
|
||||
|
||||
### Community 79 - "登录反馈与加载优化(2026-10-12)"
|
||||
Cohesion: 0.25
|
||||
Cohesion: 0.33
|
||||
Nodes (5): 体验变化, 公网部署后复测, 慢的原因与处理, 登录反馈与加载优化(2026-10-12), 验证
|
||||
|
||||
### Community 80 - "线上微信登录部署记录"
|
||||
@@ -387,7 +392,7 @@ Cohesion: 0.29
|
||||
Nodes (5): 10.09 新版 H5 实施与验收, 按确认项实现, 文档未定义部分的实施口径, 素材再生成, 验证命令
|
||||
|
||||
### Community 84 - "微信登录联调:2026-10-12"
|
||||
Cohesion: 0.33
|
||||
Cohesion: 0.22
|
||||
Nodes (6): 后续真机确认与体验优化, 微信登录联调:2026-10-12, 本轮修正, 白名单与凭据, 真实微信验收与排查, 自动验证
|
||||
|
||||
### Community 85 - "test/accounts.js"
|
||||
@@ -422,24 +427,40 @@ 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.18
|
||||
Nodes (5): iou(), NetInput(), nonMaxSuppression$1(), TinyYolov2(), TinyYolov2Base()
|
||||
|
||||
### Community 107 - "基础信息记忆与肤质拖动"
|
||||
Cohesion: 0.33
|
||||
Nodes (4): 回归命令, 基础信息记忆与肤质拖动, 肤质交互, 范围
|
||||
|
||||
### Community 108 - "profile-interaction.mjs"
|
||||
Cohesion: 0.40
|
||||
Nodes (3): dragBy(), h5, touch()
|
||||
|
||||
## Knowledge Gaps
|
||||
- **288 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+283 more)
|
||||
- **305 isolated node(s):** `source`, `document_size`, `pages`, `assets`, `layers` (+300 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
- **26 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
|
||||
- **22 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 `n` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
|
||||
_High betweenness centrality (0.068) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `n` to `face-api.js`, `app.js`, `lib/accounts.js`, `i`, `C`?**
|
||||
_High betweenness centrality (0.062) - this node is a cross-community bridge._
|
||||
- **Why does `fill()` connect `report-ui.js` to `n`?**
|
||||
_High betweenness centrality (0.054) - this node is a cross-community bridge._
|
||||
- **Why does `v()` connect `f` to `report-ui.js`, `face-api.js`, `app.js`, `C`?**
|
||||
_High betweenness centrality (0.059) - this node is a cross-community bridge._
|
||||
- **Why does `fill()` connect `report-ui.js` to `f`?**
|
||||
_High betweenness centrality (0.047) - this node is a cross-community bridge._
|
||||
- **Why does `k()` connect `f` to `face-api.js`, `app.js`, `lib/accounts.js`, `n`, `i`, `C`?**
|
||||
_High betweenness centrality (0.042) - 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?**
|
||||
@@ -447,4 +468,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?**
|
||||
_288 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
_305 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
File diff suppressed because one or more lines are too long
+1703
-759
File diff suppressed because it is too large
Load Diff
@@ -40,8 +40,8 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/app.js": {
|
||||
"mtime": 1791801437.4952922,
|
||||
"ast_hash": "1a79141b30421384ebb80a7c7c383ba9",
|
||||
"mtime": 1791810527.8429196,
|
||||
"ast_hash": "d2cbf922e0f250d270cb3f6880fa5d73",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/head-pose.js": {
|
||||
@@ -135,13 +135,13 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/README.md": {
|
||||
"mtime": 1791809197.5909035,
|
||||
"ast_hash": "4e597d6be915f6cd0b7fbd4bf34e5a96",
|
||||
"mtime": 1791810413.4149451,
|
||||
"ast_hash": "596d308e73a48f388c67d820e8e073d8",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/index.html": {
|
||||
"mtime": 1791809137.1269257,
|
||||
"ast_hash": "505ec38f7f2120e7d4d1b61eb8328097",
|
||||
"mtime": 1791810016.13091,
|
||||
"ast_hash": "454d692d26dc6ed624d5641784c8e61d",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/vendor/faceapi/README.md": {
|
||||
@@ -1885,8 +1885,28 @@
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u62cd\u6444\u7ea6\u675f\u653e\u5bbd\u4e0e\u6807\u9898\u66f4\u65b0-2026-10-12.md": {
|
||||
"mtime": 1791809197.5949035,
|
||||
"ast_hash": "62516ef04c3cba2f54e74c522d4e0b29",
|
||||
"mtime": 1791809270.7869155,
|
||||
"ast_hash": "caf913d143a498bff9ecc2cb373f40c4",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/js/profile-store.js": {
|
||||
"mtime": 1791810317.830946,
|
||||
"ast_hash": "5723356fa753e9a66fdee448e617390e",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/profile-interaction.mjs": {
|
||||
"mtime": 1791810527.8429196,
|
||||
"ast_hash": "7a370108ec4c922412fa0f278ab59f6a",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"h5/test/profile-store.js": {
|
||||
"mtime": 1791810317.8349462,
|
||||
"ast_hash": "66a0e7d6ddc5eee9cd69c097257746d1",
|
||||
"semantic_hash": ""
|
||||
},
|
||||
"docs/\u57fa\u7840\u4fe1\u606f\u8bb0\u5fc6\u4e0e\u80a4\u8d28\u62d6\u52a8-2026-10-12.md": {
|
||||
"mtime": 1791810413.4149451,
|
||||
"ast_hash": "6159880c092b531b465257eec590092a",
|
||||
"semantic_hash": ""
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -79,7 +79,7 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
|
||||
## 档案页与报告页
|
||||
|
||||
- **档案页**:出生年月 / 性别 / 所在地区三个滚轮选择器同一风格(`vendor/mobile-select`,MIT),出生年月自动换算年龄;地区为省–市两级联动,数据为 `js/region-data.js`(由 china-division 2.7.0 `pc.json` 生成,另含港澳台与「海外」)。
|
||||
- **档案页**:出生年月 / 性别 / 所在地区三个滚轮选择器同一风格(`vendor/mobile-select`,MIT),出生年月自动换算年龄;地区为省–市两级联动,数据为 `js/region-data.js`(由 china-division 2.7.0 `pc.json` 生成,另含港澳台与「海外」)。当前账号在本机浏览器记住已确认的基础信息和日常肤质,再次进入自动回填,滚轮也定位到原值;年龄按出生年月重新计算。肤质支持跟手拖动、松手吸附、轻扫切换及边界回弹,纵向手势仍滚动页面。详见 [基础信息记忆与肤质拖动](../docs/基础信息记忆与肤质拖动-2026-10-12.md)。
|
||||
- **报告页**:无返回按钮与页头标题;总分环形图 + 肌肤年龄与「比实际年龄年轻/大 N 岁」;三维度分数 + 进度条(最低分标红并挂「优先关注」);维度详情为原布局——一张卡内紧塑力/活力感/稳定力三个 Tab,每个 Tab 两条「竖排标题 + 左图右文」;点检测图全屏放大,再点或按返回键回到报告。
|
||||
|
||||
## 检测流程与评分渲染
|
||||
|
||||
+3
-2
@@ -165,13 +165,14 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; col
|
||||
.skin-arrow.next { right: calc(5 * var(--u)); }
|
||||
.skin-arrow .chev { width: calc(9 * var(--u)); height: calc(9 * var(--u)); border-width: 0 1.5px 1.5px 0; }
|
||||
.skin-arrow[disabled] { opacity: .2; }
|
||||
.skin-carousel { overflow: hidden; touch-action: pan-y; /* 纵向滚动交给浏览器,横向留给拖拽 */ }
|
||||
.skin-carousel { overflow: hidden; touch-action: pan-y; user-select: none; -webkit-user-select: none; /* 纵向滚动交给浏览器,横向留给拖拽 */ }
|
||||
.skin-track { display: flex; transition: transform .32s cubic-bezier(.22,.61,.36,1); }
|
||||
.skin-track.dragging { transition: none; will-change: transform; }
|
||||
.skin-item {
|
||||
flex: 0 0 100%;
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
}
|
||||
.skin-item img { width: calc(109 * var(--u)); height: calc(109 * var(--u)); }
|
||||
.skin-item img { width: calc(109 * var(--u)); height: calc(109 * var(--u)); -webkit-user-drag: none; }
|
||||
.skin-name { font-size: 12.5px; font-weight: 700; color: #000; margin-top: calc(16 * var(--u)); }
|
||||
.skin-desc {
|
||||
font-size: 10.3px; color: var(--ink);
|
||||
|
||||
+2
-1
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
<div class="skin-stage">
|
||||
<button type="button" class="skin-arrow prev" data-skin-step="-1" aria-label="上一个"><i class="chev left"></i></button>
|
||||
<div class="skin-carousel" id="skin-carousel">
|
||||
<div class="skin-carousel" id="skin-carousel" role="group" tabindex="0" aria-label="日常肤质">
|
||||
<div class="skin-track" id="skin-track"></div>
|
||||
</div>
|
||||
<button type="button" class="skin-arrow next" data-skin-step="1" aria-label="下一个"><i class="chev right"></i></button>
|
||||
@@ -487,6 +487,7 @@ if (/[?&]mock=1/.test(location.search)) document.write('<script src="assets/mock
|
||||
<script src="js/report-copy.js" defer></script>
|
||||
<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/camera-geometry.js" defer></script>
|
||||
<script src="js/app.js" defer></script>
|
||||
</body>
|
||||
|
||||
+94
-26
@@ -96,6 +96,7 @@
|
||||
}
|
||||
function showScreen(id, nav) { // nav: 'push' 新增历史 | 'replace' 替换当前历史 | 缺省不动历史
|
||||
if (id !== 's-login' && !AccountClient.user() && !(/[?&]mock=1/.test(location.search))) id = 's-login';
|
||||
if (id === 's-profile' || profileOwner !== ProfileStore.key(AccountClient.user())) restoreProfile();
|
||||
if (['s-profile', 's-cam', 's-report'].includes(id)) loadAppFont();
|
||||
if (id === 's-profile' || id === 's-cam') prepareScanArtwork();
|
||||
document.querySelectorAll('.screen').forEach(function (s) {
|
||||
@@ -265,7 +266,7 @@
|
||||
var item = document.createElement('div');
|
||||
item.className = 'skin-item';
|
||||
item.innerHTML =
|
||||
'<img src="' + s.img + '"' + (i === 0 ? '' : ' loading="lazy"') + ' alt="' + s.name + '">' +
|
||||
'<img src="' + s.img + '" draggable="false"' + (i === 0 ? '' : ' loading="lazy"') + ' alt="' + s.name + '">' +
|
||||
'<span class="skin-name">' + s.name + '</span>' +
|
||||
'<p class="skin-desc">' + s.desc + '</p>';
|
||||
track.appendChild(item);
|
||||
@@ -275,67 +276,129 @@
|
||||
dotsBox.appendChild(dot);
|
||||
});
|
||||
|
||||
function goSkin(i) {
|
||||
function goSkin(i, persist) {
|
||||
skinIndex = Math.max(0, Math.min(SKINS.length - 1, i));
|
||||
track.classList.remove('dragging');
|
||||
track.style.transform = 'translateX(-' + skinIndex * 100 + '%)';
|
||||
dotsBox.querySelectorAll('i').forEach(function (d, k) {
|
||||
d.classList.toggle('on', k === skinIndex);
|
||||
});
|
||||
$('.skin-arrow.prev').disabled = skinIndex === 0;
|
||||
$('.skin-arrow.next').disabled = skinIndex === SKINS.length - 1;
|
||||
$('#skin-carousel').setAttribute('aria-label', '日常肤质:' + SKINS[skinIndex].name);
|
||||
if (persist !== false) saveProfile();
|
||||
}
|
||||
document.querySelectorAll('[data-skin-step]').forEach(function (b) {
|
||||
b.addEventListener('click', function () { goSkin(skinIndex + (+b.dataset.skinStep)); });
|
||||
});
|
||||
goSkin(0);
|
||||
|
||||
// 拖拽切换(Pointer Events 统一鼠标 + 触摸)
|
||||
var drag = { id: null, startX: 0, startY: 0, horizontal: false, suppress: false };
|
||||
// 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 (e.cancelable) e.preventDefault(); // 阻止原生图片拖拽/文字选中引发 pointercancel(click 仍会正常触发)
|
||||
if (drag.id != null || e.isPrimary === false || e.button !== 0 || !carousel.clientWidth) return;
|
||||
try { carousel.setPointerCapture(e.pointerId); } catch (err) { /* 非活动指针时忽略,退化为普通监听 */ }
|
||||
// 捕获指针:斜拖出卡片、卡外松手也能收到 pointerup
|
||||
drag.id = e.pointerId;
|
||||
drag.startX = e.clientX;
|
||||
drag.startY = e.clientY;
|
||||
drag.horizontal = false;
|
||||
drag.suppress = false;
|
||||
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) return;
|
||||
if (e.pointerId !== drag.id || drag.vertical) return;
|
||||
var dx = e.clientX - drag.startX, dy = e.clientY - drag.startY;
|
||||
// 横向位移明显大于纵向时才判定为拖拽,避免吞掉纵向滚动
|
||||
if (!drag.horizontal && Math.abs(dx) > 8 && Math.abs(dx) > Math.abs(dy)) drag.horizontal = true;
|
||||
if (drag.horizontal && e.cancelable) e.preventDefault();
|
||||
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) {
|
||||
if (Math.abs(dx) > 30) goSkin(skinIndex + (dx < 0 ? 1 : -1));
|
||||
drag.suppress = true; // 拖拽后吞掉紧随的 click,防止点击穿透
|
||||
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.horizontal = false; drag.vertical = false;
|
||||
});
|
||||
carousel.addEventListener('pointercancel', function (e) {
|
||||
if (e.pointerId !== drag.id) return; // 其它指针的 cancel 不打断当前拖拽
|
||||
drag.id = null; // 拖拽被系统取消(滚轮/新指针等):干净结束,不残留状态
|
||||
drag.horizontal = false;
|
||||
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 (drag.suppress) { e.preventDefault(); e.stopPropagation(); drag.suppress = false; }
|
||||
if (Date.now() < drag.suppressUntil) { e.preventDefault(); e.stopPropagation(); }
|
||||
}, true);
|
||||
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 = new Date(), NOW_Y = NOW.getFullYear(), NOW_M = NOW.getMonth() + 1;
|
||||
var NOW_Y = new Date().getFullYear();
|
||||
var YEARS = [], MONTHS = [];
|
||||
for (var yy = NOW_Y - 99; yy <= NOW_Y - 1; yy++) YEARS.push(yy + '年');
|
||||
for (var mm = 1; mm <= 12; mm++) MONTHS.push(mm + '月');
|
||||
var regionSel = $('#f-region');
|
||||
var regionSel = $('#f-region'), regionSelection = null;
|
||||
var profileOwner = null, profileStore = ProfileStore.create(function () { return window.localStorage; });
|
||||
function setPick(sel, text) { $(sel + ' .pick-value').textContent = text; }
|
||||
function ageFromBirth(y, m) { return NOW_Y - y - (NOW_M < m ? 1 : 0); } // 未过生日月份按少一岁
|
||||
function ageFromBirth(y, m) { return ProfileStore.age(y + '-' + (m < 10 ? '0' : '') + m); }
|
||||
function saveProfile() {
|
||||
var user = AccountClient.user();
|
||||
if (!profileStore || !profileOwner || profileOwner !== ProfileStore.key(user)) return;
|
||||
profileStore.write(user, ProfileStore.clean({ birth: $('#f-birth').value, gender: $('#f-gender').value,
|
||||
region: regionSelection, skinIndex: skinIndex }, window.REGION_DATA, SKINS.length));
|
||||
}
|
||||
function restoreProfile() {
|
||||
var user = AccountClient.user();
|
||||
profileOwner = ProfileStore.key(user);
|
||||
var saved = ProfileStore.clean(profileStore.read(user), window.REGION_DATA, SKINS.length);
|
||||
var y = Number(saved.birth.slice(0, 4)), m = Number(saved.birth.slice(5));
|
||||
if (saved.birth && YEARS.indexOf(y + '年') < 0) saved.birth = '';
|
||||
$('#f-birth').value = saved.birth;
|
||||
$('#f-age').value = saved.birth ? ProfileStore.age(saved.birth) : '';
|
||||
$('#f-gender').value = saved.gender;
|
||||
regionSelection = saved.region;
|
||||
var region = saved.region, text = region ? region.province + (region.city === region.province ? '' : ' ' + region.city) : '';
|
||||
regionSel.value = text;
|
||||
setPick('#pick-birth', saved.birth ? y + '年' + m + '月' : '');
|
||||
setPick('#pick-gender', saved.gender); setPick('#pick-region', text);
|
||||
if (birthPicker) birthPicker.setValue(saved.birth ? [y + '年', m + '月'] : [(NOW_Y - 30) + '年', '1月']);
|
||||
if (genderPicker) genderPicker.setValue([saved.gender || '女']);
|
||||
var initial = (window.REGION_DATA || [])[0];
|
||||
if (regionPicker && (region || initial)) regionPicker.setValue([
|
||||
{ value: region ? region.province : initial[0] }, { value: region ? region.city : initial[1][0] }
|
||||
]);
|
||||
cancelSkinDrag(); goSkin(saved.skinIndex, false);
|
||||
}
|
||||
function makePicker(trigger, opts) {
|
||||
if (typeof MobileSelect === 'undefined') return null;
|
||||
return new MobileSelect(Object.assign({
|
||||
@@ -343,7 +406,7 @@
|
||||
ensureBtnText: '确定', cancelBtnText: '取消'
|
||||
}, opts));
|
||||
}
|
||||
makePicker('#pick-birth', {
|
||||
var birthPicker = makePicker('#pick-birth', {
|
||||
title: '出生年月',
|
||||
wheels: [{ data: YEARS }, { data: MONTHS }],
|
||||
position: [YEARS.indexOf((NOW_Y - 30) + '年'), 0],
|
||||
@@ -352,14 +415,15 @@
|
||||
$('#f-birth').value = y + '-' + (m < 10 ? '0' : '') + m;
|
||||
$('#f-age').value = ageFromBirth(y, m);
|
||||
setPick('#pick-birth', y + '年' + m + '月');
|
||||
saveProfile();
|
||||
}
|
||||
});
|
||||
makePicker('#pick-gender', {
|
||||
var genderPicker = makePicker('#pick-gender', {
|
||||
title: '性别',
|
||||
wheels: [{ data: ['女', '男'] }],
|
||||
onChange: function (val) { $('#f-gender').value = val[0]; setPick('#pick-gender', val[0]); }
|
||||
onChange: function (val) { $('#f-gender').value = val[0]; setPick('#pick-gender', val[0]); saveProfile(); }
|
||||
});
|
||||
makePicker('#pick-region', {
|
||||
var regionPicker = makePicker('#pick-region', {
|
||||
title: '所在地区',
|
||||
wheels: [{ data: (window.REGION_DATA || []).map(function (p) {
|
||||
return { id: p[0], value: p[0], childs: p[1].map(function (c) { return { id: c, value: c }; }) };
|
||||
@@ -368,7 +432,9 @@
|
||||
var prov = val[0] && val[0].value, city = val[1] && val[1].value;
|
||||
var text = !city || city === prov ? prov : prov + ' ' + city; // 直辖市/港澳台只显示一级
|
||||
regionSel.value = text;
|
||||
regionSelection = { province: prov, city: city || prov };
|
||||
setPick('#pick-region', text);
|
||||
saveProfile();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1549,10 +1615,12 @@
|
||||
showScreen('s-profile', 'push');
|
||||
},
|
||||
'submit-profile': function () {
|
||||
$('#f-age').value = ProfileStore.age($('#f-birth').value) || '';
|
||||
var age = +$('#f-age').value;
|
||||
if (!$('#f-birth').value || !(age >= 1 && age <= 99)) { toast('请选择出生年月'); return; }
|
||||
if (!$('#f-gender').value) { toast('请选择性别'); return; }
|
||||
if (!regionSel.value) { toast('请选择所在地区'); return; }
|
||||
saveProfile();
|
||||
openOverlay('p-photoguide');
|
||||
},
|
||||
'close-photoguide': function () { closeOverlay('p-photoguide'); },
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/* Account-scoped, local-only form preferences. No credentials or report data. */
|
||||
(function(root, factory) {
|
||||
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||
else root.ProfileStore = factory();
|
||||
})(typeof window !== 'undefined' ? window : this, function() {
|
||||
'use strict';
|
||||
function key(user) {
|
||||
return user && ['browser', 'wechat'].indexOf(user.kind) >= 0 && typeof user.id === 'string' && user.id.length <= 128 && user.id
|
||||
? 'juzi.profile.v1.' + user.kind + '.' + encodeURIComponent(user.id) : null;
|
||||
}
|
||||
function age(birth, now) {
|
||||
if (typeof birth !== 'string' || !/^\d{4}-(0[1-9]|1[0-2])$/.test(birth)) return null;
|
||||
now = now || new Date();
|
||||
var y = Number(birth.slice(0, 4)), m = Number(birth.slice(5));
|
||||
var value = now.getFullYear() - y - (now.getMonth() + 1 < m ? 1 : 0);
|
||||
return value >= 1 && value <= 99 ? value : null;
|
||||
}
|
||||
function clean(input, regions, skinCount, now) {
|
||||
input = input && typeof input === 'object' ? input : {};
|
||||
var region = input.region, validRegion = null;
|
||||
if (region && typeof region === 'object') (regions || []).some(function(p) {
|
||||
if (p[0] !== region.province || p[1].indexOf(region.city) < 0) return false;
|
||||
validRegion = { province: p[0], city: region.city }; return true;
|
||||
});
|
||||
return { birth: age(input.birth, now) == null ? '' : input.birth,
|
||||
gender: ['女', '男'].indexOf(input.gender) < 0 ? '' : input.gender,
|
||||
region: validRegion, skinIndex: Number.isInteger(input.skinIndex) && input.skinIndex >= 0 && input.skinIndex < skinCount ? input.skinIndex : 0 };
|
||||
}
|
||||
function create(getStorage) {
|
||||
var memory = Object.create(null);
|
||||
return {
|
||||
read: function(user) {
|
||||
var name = key(user), raw;
|
||||
if (!name) return null;
|
||||
if (Object.prototype.hasOwnProperty.call(memory, name)) return memory[name];
|
||||
try { raw = getStorage().getItem(name); } catch (_) { /* Private/restricted storage. */ }
|
||||
if (raw == null) return null;
|
||||
try { return raw.length <= 4096 ? JSON.parse(raw) : null; } catch (_) { return null; }
|
||||
},
|
||||
write: function(user, value) {
|
||||
var name = key(user);
|
||||
if (!name) return;
|
||||
memory[name] = value;
|
||||
try { getStorage().setItem(name, JSON.stringify(value)); } catch (_) { /* Keep this page usable without persistence. */ }
|
||||
}
|
||||
};
|
||||
}
|
||||
return { key: key, age: age, clean: clean, create: create };
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
// Real app and mobile-select, synthetic accounts only. Safe against production:
|
||||
// all auth is intercepted, fonts are skipped, and any non-GET request is blocked.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawn, execSync } from 'node:child_process';
|
||||
import { once } from 'node:events';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
const { chromium } = await import(pathToFileURL(execSync('npm root -g', { encoding: 'utf8' }).trim() + '/playwright/index.mjs').href);
|
||||
const h5 = fileURLToPath(new URL('../', import.meta.url));
|
||||
let child, dataRoot, browser;
|
||||
let base = process.env.H5_BASE_URL;
|
||||
try {
|
||||
if (!base) {
|
||||
dataRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'juzi-profile-test-'));
|
||||
base = 'http://127.0.0.1:18112/';
|
||||
child = spawn(process.execPath, ['server.js'], { cwd: h5, env: { ...process.env, PORT: '18112', HOST: '127.0.0.1', JUZI_DATA_ROOT: dataRoot,
|
||||
PUBLIC_BASE_URL: '', WECHAT_APP_ID: '', WECHAT_APP_SECRET: '', SKIN_API_KEY: '', SKIN_API_SECRET: '' }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(Error('test startup timeout')), 10000);
|
||||
child.stdout.on('data', b => { if (b.toString().includes('已启动')) { clearTimeout(timeout); resolve(); } });
|
||||
child.once('exit', code => { clearTimeout(timeout); reject(Error('server exited ' + code)); });
|
||||
});
|
||||
}
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 375, height: 726 }, isMobile: true, hasTouch: true,
|
||||
userAgent: 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 Chrome/130.0.0.0 Mobile Safari/537.36' });
|
||||
const page = await context.newPage(), errors = [], blockedWrites = [];
|
||||
let user = { id: 'profile-fixture-a', kind: 'browser' };
|
||||
page.setDefaultTimeout(20000);
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
await page.route('**/*', route => {
|
||||
const req = route.request();
|
||||
if (req.method() !== 'GET') { blockedWrites.push(new URL(req.url()).pathname); return route.abort(); }
|
||||
if (/\.woff2?(\?|$)/.test(req.url())) return route.abort();
|
||||
if (req.url().endsWith('/api/auth/session')) return route.fulfill({ json: { ok: true, user } });
|
||||
return route.continue();
|
||||
});
|
||||
// Expose the actual vendor instances for choosing non-default wheel positions;
|
||||
// restoration/confirmation and carousel interactions still use the actual UI.
|
||||
await page.route('**/mobile-select.iife.min.js', async route => {
|
||||
const response = await route.fetch();
|
||||
const script = await response.text();
|
||||
await route.fulfill({ response, body: script + `
|
||||
var OriginalMobileSelect=MobileSelect; window.__pickers={};
|
||||
MobileSelect=function(config){var picker=new OriginalMobileSelect(config);window.__pickers[config.trigger]=picker;return picker;};
|
||||
` });
|
||||
});
|
||||
async function enter(reload = true) {
|
||||
if (reload) await page.goto(base, { waitUntil: 'domcontentloaded' });
|
||||
await page.click('[data-action="open-privacy"]');
|
||||
if (!(await page.locator('#privacy-check').evaluate(el => el.classList.contains('checked')))) await page.click('#privacy-check');
|
||||
await page.click('[data-action="agree-continue"]');
|
||||
await page.waitForSelector('#s-profile.active');
|
||||
await page.waitForTimeout(400);
|
||||
}
|
||||
const values = () => page.evaluate(() => ({ birth: document.querySelector('#f-birth').value, age: document.querySelector('#f-age').value,
|
||||
gender: document.querySelector('#f-gender').value, region: document.querySelector('#f-region').value,
|
||||
labels: ['birth', 'gender', 'region'].map(k => document.querySelector('#pick-' + k + ' .pick-value').textContent),
|
||||
skin: Array.from(document.querySelectorAll('#skin-dots i')).findIndex(el => el.classList.contains('on')) }));
|
||||
async function pick(trigger, positions) {
|
||||
await page.click(trigger);
|
||||
if (positions) await page.evaluate(({ trigger, positions }) => positions.forEach((index, column) => window.__pickers[trigger].locatePosition(column, index)), { trigger, positions });
|
||||
await page.click('.ms-mobile-select.ms-show .ms-ensure');
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
await enter();
|
||||
let blank = await values();
|
||||
assert.equal(blank.birth, ''); assert.equal(blank.gender, ''); assert.equal(blank.region, ''); assert.equal(blank.skin, 0);
|
||||
await pick('#pick-birth', [62, 10]); // current year minus 37, November
|
||||
await pick('#pick-gender', [1]);
|
||||
const regionPositions = await page.evaluate(() => {
|
||||
const province = REGION_DATA.findIndex(p => p[0] === '浙江省');
|
||||
return [province, REGION_DATA[province][1].indexOf('宁波市')];
|
||||
});
|
||||
assert(regionPositions.every(i => i >= 0));
|
||||
await pick('#pick-region', regionPositions);
|
||||
await page.click('.skin-arrow.next'); await page.waitForTimeout(400);
|
||||
const original = await values();
|
||||
assert.equal(original.gender, '男'); assert.equal(original.region, '浙江省 宁波市'); assert.equal(original.skin, 1);
|
||||
assert.equal(original.age, String(await page.evaluate(birth => ProfileStore.age(birth), original.birth)));
|
||||
await enter();
|
||||
assert.deepEqual(await values(), original, 'reload brings back last confirmed selections, without submitting');
|
||||
for (const trigger of ['#pick-birth', '#pick-gender', '#pick-region']) await pick(trigger);
|
||||
assert.deepEqual(await values(), original, 'picker wheels themselves reopen at the remembered values');
|
||||
user = { id: 'profile-fixture-b', kind: 'wechat' };
|
||||
await enter(); assert.deepEqual(await values(), blank, 'another account does not inherit profile/picker/skin values');
|
||||
await pick('#pick-gender'); assert.equal((await values()).gender, '女', 'new account wheel reset too');
|
||||
user = { id: 'profile-fixture-a', kind: 'browser' };
|
||||
await page.evaluate(() => AccountClient.init());
|
||||
await page.goBack(); await page.waitForSelector('#s-splash.active');
|
||||
await enter(false); assert.deepEqual(await values(), original, 'switching back without a reload restores the right account');
|
||||
// Chromium's real touch dispatcher exercises touch-action/pointer capture, not
|
||||
// just dispatched DOM events. Check the transform before the finger is lifted.
|
||||
const cdp = await context.newCDPSession(page);
|
||||
const geometry = () => page.locator('#skin-carousel').evaluate(el => {
|
||||
const r = el.getBoundingClientRect(); return { x: r.x + r.width / 2, y: r.y + 60, width: r.width };
|
||||
});
|
||||
const offset = () => page.locator('#skin-track').evaluate(el => new DOMMatrix(getComputedStyle(el).transform).m41);
|
||||
async function touch(type, x, y, timestamp) { await cdp.send('Input.dispatchTouchEvent', { type, ...(timestamp ? { timestamp } : {}), touchPoints: type === 'touchEnd' || type === 'touchCancel' ? [] : [{ x, y, id: 1 }] }); }
|
||||
async function dragBy(dx, { pause = 0, cancel = false, dy = 0, flick = false } = {}) {
|
||||
const p = await geometry();
|
||||
const at = flick ? Date.now() / 1000 : null;
|
||||
await touch('touchStart', p.x, p.y, at);
|
||||
await touch('touchMove', p.x + dx / 2, p.y + dy / 2, at && at + .02);
|
||||
await touch('touchMove', p.x + dx, p.y + dy, at && at + .04);
|
||||
const during = await offset();
|
||||
if (pause) await page.waitForTimeout(pause);
|
||||
await touch(cancel ? 'touchCancel' : 'touchEnd', 0, 0, at && at + .05);
|
||||
await page.waitForTimeout(400);
|
||||
return during;
|
||||
}
|
||||
const p = await geometry(), before = await offset();
|
||||
const during = await dragBy(-100);
|
||||
assert(Math.abs(during - (before - 100)) < 3, 'track follows the finger BEFORE release');
|
||||
assert.equal((await values()).skin, 2); assert(Math.abs(await offset() + 2 * p.width) < 2, 'snaps precisely to the adjacent item');
|
||||
await dragBy(25, { pause: 160 }); assert.equal((await values()).skin, 2, 'short slow drag snaps back');
|
||||
await dragBy(-40, { flick: true }); assert.equal((await values()).skin, 3, 'short fast flick advances');
|
||||
await dragBy(100, { cancel: true }); assert.equal((await values()).skin, 3, 'system cancellation snaps back');
|
||||
assert.equal(await page.locator('#skin-track.dragging').count(), 0);
|
||||
await dragBy(2, { dy: -90 }); assert.equal((await values()).skin, 3, 'vertical scrolling never switches skin');
|
||||
await page.locator('#skin-dots i').first().click(); await page.waitForTimeout(400);
|
||||
const edge = await dragBy(100);
|
||||
assert(edge > 0 && edge < 40, 'first item has elastic resistance');
|
||||
assert.equal((await values()).skin, 0); assert(Math.abs(await offset()) < 2);
|
||||
await page.locator('#skin-dots i').last().click(); await page.waitForTimeout(400);
|
||||
await dragBy(-100); assert.equal((await values()).skin, 4, 'last item cannot overshoot');
|
||||
await page.locator('#skin-carousel').focus(); await page.keyboard.press('ArrowLeft');
|
||||
await page.waitForTimeout(400); assert.equal((await values()).skin, 3, 'keyboard still works');
|
||||
for (const width of [320, 390, 480]) {
|
||||
await page.setViewportSize({ width, height: 726 }); await page.waitForTimeout(400);
|
||||
const size = await geometry(); assert(Math.abs(await offset() + 3 * size.width) < 2, 'resize keeps selection aligned at ' + width);
|
||||
assert(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth), 'no horizontal page overflow');
|
||||
}
|
||||
await enter(); assert.equal((await values()).skin, 3, 'skin selection also survives reload');
|
||||
await page.click('[data-action="submit-profile"]');
|
||||
await page.waitForSelector('#p-photoguide.show');
|
||||
assert.deepEqual(errors, []); assert.deepEqual(blockedWrites, [], 'no backend writes or paid requests');
|
||||
console.log('PASS: actual profile defaults/wheels/reload/account isolation; real mobile touch follows finger/snap/flick/edges/cancel/vertical scroll; arrows/dots/keyboard; three responsive widths; submission; no JS errors/backend writes');
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
if (child && child.exitCode == null) { const stopped = once(child, 'exit'); child.kill(); await stopped; }
|
||||
if (dataRoot) fs.rmSync(dataRoot, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const Profile = require('../js/profile-store');
|
||||
const regions = [['北京市', ['北京市']], ['浙江省', ['杭州市', '宁波市']]];
|
||||
const now = new Date(2026, 9, 12);
|
||||
const user = { id: 'fixture-a', kind: 'browser' }, other = { id: 'fixture-b', kind: 'browser' };
|
||||
const valid = { birth: '1991-11', gender: '男', region: { province: '浙江省', city: '宁波市' }, skinIndex: 3 };
|
||||
const empty = { birth: '', gender: '', region: null, skinIndex: 0 };
|
||||
assert.equal(Profile.age(valid.birth, now), 34);
|
||||
assert.equal(Profile.age(valid.birth, new Date(2026, 10, 1)), 35);
|
||||
assert.equal(Profile.age(valid.birth, new Date(2027, 9, 1)), 35);
|
||||
for (const birth of [null, '', '1991-1', '1991-00', '1991-13', '1991-11-01', '2026-01', '2027-01', '1926-01']) {
|
||||
assert.equal(Profile.age(birth, now), null, String(birth));
|
||||
}
|
||||
assert.equal(Profile.age('2025-10', now), 1);
|
||||
assert.equal(Profile.age('1926-11', now), 99);
|
||||
assert.deepEqual(Profile.clean(null, regions, 5, now), empty);
|
||||
assert.deepEqual(Profile.clean({ ...valid, token: 'not-persisted', report: {} }, regions, 5, now), valid);
|
||||
assert.deepEqual(Profile.clean({ birth: 'bad', gender: '<script>', region: { province: '北京市', city: '宁波市' }, skinIndex: 5 }, regions, 5, now), empty);
|
||||
for (const skinIndex of [-1, 1.5, '2', NaN]) assert.equal(Profile.clean({ skinIndex }, regions, 5, now).skinIndex, 0);
|
||||
assert.equal(Profile.clean({ gender: '女' }, regions, 5, now).gender, '女', 'partial selections can be saved');
|
||||
assert.equal(Profile.key(null), null);
|
||||
assert.equal(Profile.key({ id: '', kind: 'browser' }), null);
|
||||
assert.equal(Profile.key({ id: 'x', kind: 'unknown' }), null);
|
||||
assert.equal(Profile.key({ id: 'x'.repeat(129), kind: 'browser' }), null);
|
||||
assert.equal(Profile.key({ id: 'a/b', kind: 'wechat' }), 'juzi.profile.v1.wechat.a%2Fb');
|
||||
const data = new Map();
|
||||
const storage = { getItem: key => data.get(key) ?? null, setItem: (key, value) => data.set(key, value) };
|
||||
const store = Profile.create(() => storage);
|
||||
assert.equal(store.read(user), null);
|
||||
store.write(user, valid);
|
||||
assert.deepEqual(Profile.create(() => storage).read(user), valid, 'survives a page reload');
|
||||
assert.equal(store.read(other), null, 'different accounts are isolated');
|
||||
assert.equal(store.read({ ...user, kind: 'wechat' }), null, 'login kinds are isolated');
|
||||
store.write(other, empty); assert.deepEqual(store.read(user), valid);
|
||||
store.write(null, valid); assert.equal(data.size, 2, 'no unauthenticated writes');
|
||||
for (const raw of ['broken', 'x'.repeat(4097)]) {
|
||||
data.set(Profile.key(user), raw);
|
||||
assert.equal(Profile.create(() => storage).read(user), null, 'corrupt/oversized data is harmless');
|
||||
}
|
||||
const restricted = Profile.create(() => { throw Error('SecurityError'); });
|
||||
assert.equal(restricted.read(user), null);
|
||||
restricted.write(user, valid); assert.deepEqual(restricted.read(user), valid);
|
||||
assert.equal(restricted.read(other), null);
|
||||
const quota = Profile.create(() => ({ getItem: () => JSON.stringify(empty), setItem: () => { throw Error('QuotaExceededError'); } }));
|
||||
quota.write(user, valid); assert.deepEqual(quota.read(user), valid, 'stale disk data cannot overwrite latest in-page selections');
|
||||
console.log('PASS: profile validation, dynamic age, reload persistence, account isolation, corrupt/restricted/quota-limited storage, no credentials/report fields');
|
||||
Reference in New Issue
Block a user