feat: 真机反馈第9轮(浮层层级/侧脸镜像自校准/提示样式/Tab设计宽/去历史/档案顶栏/静态禁缓存)

1. 城市下拉浮层 z-index 999(实测本不重叠,仅 1px 相邻;用户端旧样式系缓存所致——server.js 静态 html/css/js 改为 no-cache,此后改动即时到端)
3. 侧脸步「浮层」改设计语言:去气泡底,纯白/纯绿文字+投影,置于引导 chips 上方;侧脸拍摄不通的根因(本机镜像策略差异)加「镜像自校准」:语义 yaw 连续 5 帧强反向即自动翻转镜像约定,姿态引导方向随设备自适应
4. 语音引导的侧脸照片方向与镜像约定联动(camMirrored 统一管预览/抓拍/姿态),自校准后语音方向与实际出片一致
5. 痘痘图标记:坐标随 Face++ 返回原样叠加;未检出时纯照片+话术「未检出痤疮与皮损」(详见回复)
6. Tab 条对齐设计稿:文字 11px/选中 700,黄色指示条恢复设计实测宽度 78px
7. 关注行:维度名加粗 700,两行收紧
8. 报告页「历史」入口移除(存储逻辑保留)
9. 档案页顶部白条移除,返回键改悬浮白圆
HeadPose.check 增加 yaw 返回供自校准;e2e 适配历史入口变更,全量绿
This commit is contained in:
Ubuntu
2026-09-27 11:55:20 +08:00
parent 6c634e3a66
commit 0e00f3e938
7 changed files with 58 additions and 16 deletions
+25
View File
@@ -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; }
+1 -5
View File
@@ -32,10 +32,7 @@
<!-- ═══════════ 03 档案填写 ═══════════ -->
<section class="screen" id="s-profile">
<div class="profile-bg"></div>
<header class="nav">
<button class="nav-back" data-action="back-splash" aria-label="返回"><i class="chev left"></i></button>
<span class="nav-title">1'me在线肌肤测试</span>
</header>
<button class="nav-back profile-float-back" data-action="back-splash" aria-label="返回"><i class="chev left"></i></button>
<div class="profile-body">
<h1 class="page-title">您的肌肤状态</h1>
@@ -138,7 +135,6 @@
<!-- ═══════════ 08 报告页 ═══════════ -->
<section class="screen" id="s-report">
<button class="nav-back report-float-back" data-action="back-profile" aria-label="返回"><i class="chev left"></i></button>
<button class="nav-history report-float-history" data-action="history">历史</button>
<div class="report-body">
<h1 class="report-title">您的肌肤状态表现</h1>
+17
View File
@@ -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;
}
+10 -10
View File
@@ -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 {
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

+5 -1
View File
@@ -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);
});
}