diff --git a/h5/css/style.css b/h5/css/style.css index 2cbf9f1..7dbde51 100644 --- a/h5/css/style.css +++ b/h5/css/style.css @@ -686,3 +686,28 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; } #care-list .care-item b { font-weight: 600; color: var(--ink); } /* 护理建议/精华推荐卡标题对齐设计稿(≈16px) */ .card-care .card-heading, .card-essence .card-heading { font-size: 16px; } + +/* ═══════════ 第9轮:真机反馈 ═══════════ */ +/* #1 城市下拉浮层再提级,并加实体阴影边界(部分端 stacking 表现不同) */ +.rs, .rs-list { z-index: 999; } +/* #3 姿态提示浮层改设计语言:无气泡底,纯白文字+投影,置于引导 chips 上方 */ +.cam-hint { + top: auto; bottom: 26%; + background: none; padding: 0; border-radius: 0; + font-size: 15px; font-weight: 500; text-shadow: 0 1px 8px rgba(0,0,0,.65); +} +.cam-hint.ok { background: none; color: #34C759; } +/* #6 Tab 条对齐设计稿:文字 11px、选中 700,黄色指示条恢复设计宽度 78px */ +.dtab-tx { font-size: 11px; padding: 0 5px 10px; } +.dtab.active .dtab-tx::after { left: 50%; right: auto; transform: translateX(-50%); width: 78px; } +/* #7 关注行:维度名加粗 700,两行收紧 */ +.dims-note-l1 b { font-weight: 700; } +.dims-note-l2 { line-height: 1.6; } +/* #9 档案页顶栏移除,返回键悬浮(白圆带影,压暗背景上可读) */ +.profile-float-back { + position: fixed; top: 12px; left: 12px; z-index: 21; + background: rgba(255,255,255,.92); border-color: transparent; + box-shadow: 0 1px 6px rgba(0,0,0,.18); +} +/* 下拉浮层与肤质圆点之间留明确间隙 */ +.rs-list { max-height: 236px; } diff --git a/h5/index.html b/h5/index.html index 65c7944..86ba445 100644 --- a/h5/index.html +++ b/h5/index.html @@ -32,10 +32,7 @@
- +

您的肌肤状态

@@ -138,7 +135,6 @@
-

您的肌肤状态表现

diff --git a/h5/js/app.js b/h5/js/app.js index 2418a17..cb0a97c 100644 --- a/h5/js/app.js +++ b/h5/js/app.js @@ -173,6 +173,8 @@ // 约束逐级放宽:facingMode 裸值在部分 Android 抛 OverconstrainedError,先 ideal 再退 video:true,最后才降级 var camFacing = 'user'; // 当前镜头:'user' 前置 | 'environment' 后置(翻转按钮切换) var camMirrored = true; // 预览是否镜像(前置镜像,后置不镜像) + var mirrorCalibrated = false; // 镜像自校准:本轮是否已翻转过一次(防振荡,一次校准管整个会话) + var oppFrames = 0; // 侧脸步语义 yaw 连续反向帧计数 function getCamStream() { return navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: camFacing }, width: { ideal: 1280 }, height: { ideal: 1280 } }, @@ -396,6 +398,7 @@ function resetDetectState() { okSince = 0; missFrames = 0; + oppFrames = 0; lastHint = ''; cancelCountdown(); clearTimeout(nudgeTimer); @@ -494,6 +497,20 @@ else { // 镜像标志跟随镜头(前置 true/后置 false);距离不合格时角度不判 ok var pose = HeadPose.check(d.landmarks.positions, CAM_STEPS[camStep].target, camMirrored); + // 镜像自校准:侧脸步用户实际转向与语义 yaw 持续相反(本机镜像策略与 camMirrored 假设不符), + // 连续 5 帧强反向即翻转镜像约定一次(翻转后语义回正,通常立即姿态合格) + var tgt = CAM_STEPS[camStep].target; + if (tgt !== 'front' && pose.yaw != null && !mirrorCalibrated) { + var wantSign = tgt === 'right' ? 1 : -1; + if (pose.yaw * wantSign < -12) { + if (++oppFrames >= 5) { + camMirrored = !camMirrored; + mirrorCalibrated = true; + oppFrames = 0; + camVideo.classList.toggle('mirror', camMirrored); + } + } else oppFrames = 0; + } hint = pose.hint; ok = pose.ok; } diff --git a/h5/js/head-pose.js b/h5/js/head-pose.js index 6c798ab..18da3cb 100644 --- a/h5/js/head-pose.js +++ b/h5/js/head-pose.js @@ -166,9 +166,9 @@ try { e = estimate(points, mirrored); } catch (err) { - return { ok: false, hint: HINT_FRONT }; + return { ok: false, hint: HINT_FRONT, yaw: null }; } - if (!e) return { ok: false, hint: HINT_FRONT }; // 退化特征点(重合/脸高过小 → NaN/Infinity) + if (!e) return { ok: false, hint: HINT_FRONT, yaw: null }; // 退化特征点(重合/脸高过小 → NaN/Infinity) // 语义 yaw:>0 = 被摄者向自己的左侧转头。estimate(mirrored) 内部已按镜像翻转过符号, // 这里直接用 e.yaw(此前再翻一次是双重镜像 bug:侧脸引导方向完全相反) @@ -177,13 +177,13 @@ if (target === 'front') { if (Math.abs(sYaw) <= FRONT_YAW_MAX && Math.abs(pitch) <= FRONT_PITCH_MAX) { - return { ok: true, hint: HINT_KEEP }; + return { ok: true, hint: HINT_KEEP, yaw: sYaw }; } if (Math.abs(sYaw) > FRONT_YAW_MAX) { // 已向左转过头 → 提示向右回正,反之亦然 - return { ok: false, hint: sYaw > 0 ? HINT_TURN_RIGHT : HINT_TURN_LEFT }; + return { ok: false, hint: sYaw > 0 ? HINT_TURN_RIGHT : HINT_TURN_LEFT, yaw: sYaw }; } - return { ok: false, hint: pitch > 0 ? HINT_LOWER : HINT_RAISE }; + return { ok: false, hint: pitch > 0 ? HINT_LOWER : HINT_RAISE, yaw: sYaw }; } if (target === 'left' || target === 'right') { @@ -191,20 +191,20 @@ var want = target === 'left' ? -1 : 1; var sy = sYaw * want; // 统一折算到正区间判断 if (sy >= SIDE_YAW_MIN && sy <= SIDE_YAW_MAX && Math.abs(pitch) <= SIDE_PITCH_MAX) { - return { ok: true, hint: HINT_KEEP }; + return { ok: true, hint: HINT_KEEP, yaw: sYaw }; } if (sy < SIDE_YAW_MIN) { // 转得不够(含转错方向):提示朝目标方向转 - return { ok: false, hint: target === 'left' ? HINT_TURN_RIGHT : HINT_TURN_LEFT }; + return { ok: false, hint: target === 'left' ? HINT_TURN_RIGHT : HINT_TURN_LEFT, yaw: sYaw }; } if (sy > SIDE_YAW_MAX) { // 转过了:提示往回 - return { ok: false, hint: target === 'left' ? HINT_TURN_LEFT : HINT_TURN_RIGHT }; + return { ok: false, hint: target === 'left' ? HINT_TURN_LEFT : HINT_TURN_RIGHT, yaw: sYaw }; } - return { ok: false, hint: pitch > 0 ? HINT_LOWER : HINT_RAISE }; + return { ok: false, hint: pitch > 0 ? HINT_LOWER : HINT_RAISE, yaw: sYaw }; } - return { ok: false, hint: HINT_FRONT }; + return { ok: false, hint: HINT_FRONT, yaw: null }; } return { diff --git a/h5/preview/e2e-20260926/r9-hint.png b/h5/preview/e2e-20260926/r9-hint.png new file mode 100644 index 0000000..2096a46 Binary files /dev/null and b/h5/preview/e2e-20260926/r9-hint.png differ diff --git a/h5/preview/e2e-20260926/r9-tab.png b/h5/preview/e2e-20260926/r9-tab.png new file mode 100644 index 0000000..7e5ed46 Binary files /dev/null and b/h5/preview/e2e-20260926/r9-tab.png differ diff --git a/h5/server.js b/h5/server.js index 3511bed..2d81d4d 100644 --- a/h5/server.js +++ b/h5/server.js @@ -61,7 +61,11 @@ function serveStatic(req, res, pathname) { if (file !== ROOT && !file.startsWith(ROOT + path.sep)) { res.writeHead(403); return res.end('Forbidden'); } fs.readFile(file, (err, data) => { if (err) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); return res.end('Not Found'); } - res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'application/octet-stream' }); + const ext = path.extname(file); + // html/css/js 不缓存(迭代期样式/脚本改动必须即时到端;图片/字体/音频走默认) + const headers = { 'Content-Type': MIME[ext] || 'application/octet-stream' }; + if (['.html', '.css', '.js'].includes(ext)) headers['Cache-Control'] = 'no-cache'; + res.writeHead(200, headers); res.end(data); }); }