From ce6dbc20d4bdc406f26f0d50e265d324add702c8 Mon Sep 17 00:00:00 2001 From: xsl Date: Mon, 12 Oct 2026 21:10:00 +0800 Subject: [PATCH] feat(h5): remember profile selections and add finger-following skin swipe --- docs/基础信息记忆与肤质拖动-2026-10-12.md | 34 + graphify-out/.graphify_labels.json | 31 +- graphify-out/.graphify_labels.json.sig | 2 +- graphify-out/GRAPH_REPORT.md | 187 +- graphify-out/graph.html | 8 +- graphify-out/graph.json | 2462 ++++++++++++++------- graphify-out/manifest.json | 36 +- h5/README.md | 2 +- h5/css/style.css | 5 +- h5/index.html | 3 +- h5/js/app.js | 120 +- h5/js/profile-store.js | 49 + h5/test/profile-interaction.mjs | 145 ++ h5/test/profile-store.js | 46 + 14 files changed, 2230 insertions(+), 900 deletions(-) create mode 100644 docs/基础信息记忆与肤质拖动-2026-10-12.md create mode 100644 h5/js/profile-store.js create mode 100644 h5/test/profile-interaction.mjs create mode 100644 h5/test/profile-store.js diff --git a/docs/基础信息记忆与肤质拖动-2026-10-12.md b/docs/基础信息记忆与肤质拖动-2026-10-12.md new file mode 100644 index 0000000..8e92810 --- /dev/null +++ b/docs/基础信息记忆与肤质拖动-2026-10-12.md @@ -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,不改服务器环境配置或历史数据。上线后核对静态文件哈希、服务健康、匿名访问保护,并执行同一套只读模拟账号浏览器测试。 diff --git a/graphify-out/.graphify_labels.json b/graphify-out/.graphify_labels.json index e869d3e..2270168 100644 --- a/graphify-out/.graphify_labels.json +++ b/graphify-out/.graphify_labels.json @@ -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" } diff --git a/graphify-out/.graphify_labels.json.sig b/graphify-out/.graphify_labels.json.sig index 385553f..1a04873 100644 --- a/graphify-out/.graphify_labels.json.sig +++ b/graphify-out/.graphify_labels.json.sig @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index 31feb7e..bb5724c 100644 --- a/graphify-out/GRAPH_REPORT.md +++ b/graphify-out/GRAPH_REPORT.md @@ -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._ \ No newline at end of file + _305 weakly-connected nodes found - possible documentation gaps or missing edges._ \ No newline at end of file diff --git a/graphify-out/graph.html b/graphify-out/graph.html index bedbc05..2b97348 100644 --- a/graphify-out/graph.html +++ b/graphify-out/graph.html @@ -63,12 +63,12 @@
-
1387 nodes · 2814 edges · 108 communities
+
1420 nodes · 2864 edges · 109 communities
+ diff --git a/h5/js/app.js b/h5/js/app.js index aebfd4d..48171d6 100644 --- a/h5/js/app.js +++ b/h5/js/app.js @@ -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 = - '' + s.name + '' + + '' + s.name + '' + '' + s.name + '' + '

' + s.desc + '

'; 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'); }, diff --git a/h5/js/profile-store.js b/h5/js/profile-store.js new file mode 100644 index 0000000..bab53cb --- /dev/null +++ b/h5/js/profile-store.js @@ -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 }; +}); diff --git a/h5/test/profile-interaction.mjs b/h5/test/profile-interaction.mjs new file mode 100644 index 0000000..f2c51c7 --- /dev/null +++ b/h5/test/profile-interaction.mjs @@ -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 }); +} diff --git a/h5/test/profile-store.js b/h5/test/profile-store.js new file mode 100644 index 0000000..0cea25b --- /dev/null +++ b/h5/test/profile-store.js @@ -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: '