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:
@@ -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
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user