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
+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 {