Compare commits
2
Commits
eae22d18a1
...
7155f23213
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7155f23213 | ||
|
|
d09d29946d |
@@ -594,3 +594,67 @@ button { font-family: inherit; border: 0; background: none; cursor: pointer; }
|
||||
.his-score b { font-size: 22px; font-weight: 500; color: var(--ink); }
|
||||
.his-score span { font-size: 11px; color: var(--grey); margin-left: 2px; }
|
||||
.his-del { flex: none; font-size: 11px; color: var(--grey-2); padding: 6px 4px; }
|
||||
|
||||
/* ═══════════ 第6轮:真机反馈 13 项 ═══════════ */
|
||||
/* #2 姿态提示浮层:移到取景框下方(原 29% 与脸部轮廓/下巴重叠) */
|
||||
.cam-hint { bottom: 21%; }
|
||||
/* #10 无进展提示与快门重叠:上移 */
|
||||
.cam-nudge { bottom: 16%; }
|
||||
/* #4/#10 拍照页顶栏取消,返回键改悬浮白圆(对齐设计稿) */
|
||||
.cam-back-float {
|
||||
position: absolute; top: 12px; left: 12px; z-index: 3;
|
||||
background: rgba(255,255,255,.9); border-color: transparent;
|
||||
}
|
||||
/* #12 预览镜像改为类切换(前置镜像/后置不镜像) */
|
||||
#cam-video { transform: none; }
|
||||
#cam-video.mirror { transform: scaleX(-1); }
|
||||
|
||||
/* #1 城市自定义下拉(替代原生 select) */
|
||||
.rs { position: relative; }
|
||||
.rs-current {
|
||||
width: 100%; display: flex; align-items: center; justify-content: space-between;
|
||||
background: #fff; border: 1px solid var(--line); border-radius: 8px;
|
||||
padding: 12px 14px; font-size: 13px; color: var(--ink); text-align: left;
|
||||
}
|
||||
.rs-current .chev { transition: transform .2s; }
|
||||
.rs.open .rs-current .chev { transform: rotate(225deg); }
|
||||
.rs-value.rs-placeholder { color: #B3B3B3; }
|
||||
.rs-list {
|
||||
position: absolute; left: 0; right: 0; top: calc(100% + 6px); z-index: 30;
|
||||
max-height: 264px; overflow-y: auto; -webkit-overflow-scrolling: touch;
|
||||
background: #fff; border: 1px solid var(--line); border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,.08);
|
||||
}
|
||||
.rs-list li { padding: 12px 14px; font-size: 13px; color: var(--ink); }
|
||||
.rs-list li + li { border-top: 1px solid #F3F3F3; }
|
||||
.rs-list li:active { background: #F7F7F7; }
|
||||
|
||||
/* #5 隐私页去头部条,返回键悬浮 */
|
||||
.policy-float-back {
|
||||
position: absolute; top: 12px; left: 12px; z-index: 2;
|
||||
background: #fff; box-shadow: 0 1px 6px rgba(0,0,0,.12);
|
||||
}
|
||||
.overlay-policy .policy-page { position: relative; }
|
||||
|
||||
/* #8 维度卡关注行:灰小字 + 深色口号(对齐设计稿) */
|
||||
.dims-note { margin-top: 12px; }
|
||||
.dims-note-l1 { font-size: 10px; font-weight: 400; color: var(--grey); }
|
||||
.dims-note-l2 { font-size: 11.5px; font-weight: 500; color: var(--ink); line-height: 1.8; }
|
||||
|
||||
/* #9 Tab 黄色指示条:宽度跟随文字(原固定 78px 过宽) */
|
||||
.dtab { padding: 8px 0 0; }
|
||||
.dtab.active::after { content: none; }
|
||||
.dtab-tx { position: relative; display: inline-block; padding: 0 5px 9px; }
|
||||
.dtab.active .dtab-tx::after {
|
||||
content: ""; position: absolute; left: 0; right: 0; bottom: -1px;
|
||||
height: 7px; border-radius: 3.5px; background: var(--yellow);
|
||||
}
|
||||
|
||||
/* #13 分类卡图片对齐设计稿(约 100px 见方、圆角 10) */
|
||||
.ditem-img { width: 100px; height: 100px; border-radius: 10px; }
|
||||
|
||||
/* ═══════════ 第7轮:用户测试 agent 反馈 ═══════════ */
|
||||
/* P1-1 隐私页标题被悬浮返回键遮挡:正文上留返回键位 */
|
||||
#p-policy .policy-body { padding-top: 52px; }
|
||||
/* P1-2 姿态提示气泡压住底部引导文字:改到步骤文案下方、取景框上方 */
|
||||
.cam-hint { bottom: auto; top: 26%; }
|
||||
|
||||
+12
-25
@@ -58,12 +58,14 @@
|
||||
|
||||
<div class="field">
|
||||
<label class="field-label">您的所在地区</label>
|
||||
<div class="field-select-wrap">
|
||||
<select id="f-region" class="field-input field-select">
|
||||
<option value="" selected disabled>请选择</option>
|
||||
</select>
|
||||
<i class="chev down"></i>
|
||||
<div class="rs" id="region-rs">
|
||||
<button type="button" class="rs-current" id="rs-current">
|
||||
<span id="rs-value" class="rs-value rs-placeholder">请选择</span>
|
||||
<i class="chev down"></i>
|
||||
</button>
|
||||
<ul class="rs-list" id="rs-list" hidden></ul>
|
||||
</div>
|
||||
<input type="hidden" id="f-region">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
@@ -88,10 +90,7 @@
|
||||
<!-- ═══════════ 05-07 拍照页 ═══════════ -->
|
||||
<section class="screen" id="s-cam">
|
||||
<div class="cam-bg" id="cam-bg"></div>
|
||||
<header class="nav nav-dark">
|
||||
<button class="nav-back" data-action="cam-back" aria-label="返回"><i class="chev left"></i></button>
|
||||
<span class="nav-title">1'me在线肌肤测试</span>
|
||||
</header>
|
||||
<button class="nav-back nav-dark cam-back-float" data-action="cam-back" aria-label="返回"><i class="chev left"></i></button>
|
||||
<div class="cam-dots" id="cam-dots"><i></i><i></i><i></i></div>
|
||||
<p class="cam-step" id="cam-step">1/3<br>请单人正脸出镜</p>
|
||||
<!-- 取景蒙版:SVG mask 镂空压暗 + 轮廓线框;data-step 切换三种轮廓
|
||||
@@ -158,9 +157,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="dims-intro">我们从6个维度观察了你的肌肤,当下优先关注:紧致、光泽、纹路、耐受、细腻、水润</p>
|
||||
<p class="dims-disclaimer">*状态是动态流动的,和作息、压力、季节都有关系,仅代表此刻的状态参考。</p>
|
||||
|
||||
<div class="card card-dims">
|
||||
<div class="dims-row">
|
||||
<div class="dim"><span class="dim-name">紧塑力</span><span class="dim-score" id="dim-firm">-.-</span><span class="dim-lv" id="dim-firm-lv"></span></div>
|
||||
@@ -172,9 +168,9 @@
|
||||
|
||||
<div class="card card-detail">
|
||||
<div class="detail-tabs" id="detail-tabs">
|
||||
<button class="dtab active" data-tab="0">紧塑力</button>
|
||||
<button class="dtab" data-tab="1">活力感</button>
|
||||
<button class="dtab" data-tab="2">稳定力</button>
|
||||
<button class="dtab active" data-tab="0"><span class="dtab-tx">紧塑力</span></button>
|
||||
<button class="dtab" data-tab="1"><span class="dtab-tx">活力感</span></button>
|
||||
<button class="dtab" data-tab="2"><span class="dtab-tx">稳定力</span></button>
|
||||
</div>
|
||||
<div class="detail-panels">
|
||||
<div class="dpanel active" data-panel="0">
|
||||
@@ -182,7 +178,6 @@
|
||||
<h3 class="ditem-title">轮廓</h3>
|
||||
<img id="ditem-img1" src="assets/img/det-1.jpg" loading="lazy" class="ditem-img" alt="轮廓检测图">
|
||||
<div class="ditem-text">
|
||||
<p class="cat-head"><span class="cat-s" id="s-contour">-.-</span><i class="cat-badge" id="lv-contour"></i></p>
|
||||
<p class="ditem-para" id="ditem-para1">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -190,7 +185,6 @@
|
||||
<h3 class="ditem-title">皱纹</h3>
|
||||
<img id="ditem-img2" src="assets/img/det-2.jpg" loading="lazy" class="ditem-img" alt="皱纹检测图">
|
||||
<div class="ditem-text">
|
||||
<p class="cat-head"><span class="cat-s" id="s-wrinkles">-.-</span><i class="cat-badge" id="lv-wrinkles"></i></p>
|
||||
<p class="ditem-para" id="ditem-para2">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -200,7 +194,6 @@
|
||||
<h3 class="ditem-title">光滑</h3>
|
||||
<img id="viz-smooth" loading="lazy" class="ditem-img" alt="光滑检测标记图" hidden>
|
||||
<div class="ditem-text">
|
||||
<p class="cat-head"><span class="cat-s" id="s-smooth">-.-</span><i class="cat-badge" id="lv-smooth"></i></p>
|
||||
<p class="ditem-para" id="tx-smooth">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,7 +201,6 @@
|
||||
<h3 class="ditem-title">肤色水润</h3>
|
||||
<img id="viz-tone" loading="lazy" class="ditem-img" alt="肤色色沉图" hidden>
|
||||
<div class="ditem-text">
|
||||
<p class="cat-head"><span class="cat-s" id="s-tone">-.-</span><i class="cat-badge" id="lv-tone"></i></p>
|
||||
<p class="ditem-para" id="tx-tone">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -218,7 +210,6 @@
|
||||
<h3 class="ditem-title">敏感</h3>
|
||||
<img id="viz-sens" loading="lazy" class="ditem-img" alt="敏感红区图" hidden>
|
||||
<div class="ditem-text">
|
||||
<p class="cat-head"><span class="cat-s" id="s-sens">-.-</span><i class="cat-badge" id="lv-sens"></i></p>
|
||||
<p class="ditem-para" id="tx-sens">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -226,7 +217,6 @@
|
||||
<h3 class="ditem-title">痘痘</h3>
|
||||
<img id="viz-acne" loading="lazy" class="ditem-img" alt="痘痘检测标记图" hidden>
|
||||
<div class="ditem-text">
|
||||
<p class="cat-head"><span class="cat-s" id="s-acne">-.-</span><i class="cat-badge" id="lv-acne"></i></p>
|
||||
<p class="ditem-para" id="tx-acne">检测完成后显示分析结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -304,10 +294,7 @@
|
||||
<!-- ═══════════ 弹层:隐私政策与服务协议(全文) ═══════════ -->
|
||||
<div class="overlay overlay-policy" id="p-policy">
|
||||
<div class="policy-page">
|
||||
<header class="policy-header">
|
||||
<button class="nav-back" data-action="close-policy" aria-label="返回"><i class="chev left"></i></button>
|
||||
<span class="policy-header-title">隐私政策与服务协议</span>
|
||||
</header>
|
||||
<button class="nav-back policy-float-back" data-action="close-policy" aria-label="返回"><i class="chev left"></i></button>
|
||||
<div class="policy-body">
|
||||
<h1 class="policy-title">Face⁺⁺个人信息与隐私保护政策</h1>
|
||||
|
||||
|
||||
+101
-44
@@ -142,14 +142,27 @@
|
||||
if (drag.suppress) { e.preventDefault(); e.stopPropagation(); drag.suppress = false; }
|
||||
}, true);
|
||||
|
||||
/* ─── 地区选项 ─── */
|
||||
/* ─── 地区选项(自定义下拉:原生 select 各端样式不一,改自绘弹层) ─── */
|
||||
var REGIONS = ['北京', '上海', '广州', '深圳', '杭州', '成都', '重庆', '武汉', '西安', '南京', '天津', '苏州', '长沙', '郑州', '青岛', '其他'];
|
||||
var regionSel = $('#f-region');
|
||||
REGIONS.forEach(function (r) {
|
||||
var o = document.createElement('option');
|
||||
o.textContent = r;
|
||||
regionSel.appendChild(o);
|
||||
var regionSel = $('#f-region'); // hidden input,存选中值
|
||||
var rsBox = $('#region-rs'), rsList = $('#rs-list'), rsValue = $('#rs-value');
|
||||
rsList.innerHTML = REGIONS.map(function (r) { return '<li data-v="' + r + '">' + r + '</li>'; }).join('');
|
||||
function rsClose() { rsList.hidden = true; rsBox.classList.remove('open'); }
|
||||
$('#rs-current').addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
var willOpen = rsList.hidden;
|
||||
rsList.hidden = !willOpen;
|
||||
rsBox.classList.toggle('open', willOpen);
|
||||
});
|
||||
rsList.addEventListener('click', function (e) {
|
||||
var li = e.target.closest('li[data-v]');
|
||||
if (!li) return;
|
||||
regionSel.value = li.dataset.v;
|
||||
rsValue.textContent = li.dataset.v;
|
||||
rsValue.classList.remove('rs-placeholder');
|
||||
rsClose();
|
||||
});
|
||||
document.addEventListener('click', rsClose); // 点空白收起
|
||||
|
||||
/* ─── 实时相机(getUserMedia)+ 系统相机降级 ─── */
|
||||
var IS_MOCK = /[?&]mock=1/.test(location.search); // mock:不加载 face-api、不跑检测,analyze 直返内置样本
|
||||
@@ -158,9 +171,11 @@
|
||||
var photos = []; // 三连拍(dataURL),与 CAM_STEPS 同序:[0]正脸 [1]右脸(头左转) [2]左脸(头右转)
|
||||
|
||||
// 约束逐级放宽:facingMode 裸值在部分 Android 抛 OverconstrainedError,先 ideal 再退 video:true,最后才降级
|
||||
var camFacing = 'user'; // 当前镜头:'user' 前置 | 'environment' 后置(翻转按钮切换)
|
||||
var camMirrored = true; // 预览是否镜像(前置镜像,后置不镜像)
|
||||
function getCamStream() {
|
||||
return navigator.mediaDevices.getUserMedia({
|
||||
video: { facingMode: { ideal: 'user' }, width: { ideal: 1280 }, height: { ideal: 1280 } },
|
||||
video: { facingMode: { ideal: camFacing }, width: { ideal: 1280 }, height: { ideal: 1280 } },
|
||||
audio: false
|
||||
}).catch(function (err) {
|
||||
if (err && err.name === 'OverconstrainedError') {
|
||||
@@ -169,6 +184,18 @@
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
// 相机预热:部分设备开头 1~2s 以低分辨率出画(画面显脸巨大),等分辨率稳定两拍再放开取景与检测
|
||||
function waitVideoStable(done) {
|
||||
var lastW = 0, stableN = 0, tries = 0;
|
||||
var t = setInterval(function () {
|
||||
var w = camVideo.videoWidth;
|
||||
tries++;
|
||||
if (w && w === lastW) {
|
||||
if (++stableN >= 2) { clearInterval(t); done(); }
|
||||
} else { stableN = 0; lastW = w; }
|
||||
if (tries > 40) { clearInterval(t); done(); } // 12s 兜底,异常也不卡死
|
||||
}, 300);
|
||||
}
|
||||
function startCamera() {
|
||||
if (camStream) { stopCamera(); }
|
||||
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { enableFallback(); return; }
|
||||
@@ -178,14 +205,18 @@
|
||||
if (token !== camSeqCam) { stream.getTracks().forEach(function (t) { t.stop(); }); return; } // 期间已退出拍照页:立即释放
|
||||
camStream = stream;
|
||||
camVideo.srcObject = stream;
|
||||
camLive = true;
|
||||
camVideo.classList.toggle('mirror', camMirrored);
|
||||
// iOS 来电/切后台等系统打断会结束 track:挂 ended 监听走自动重连
|
||||
stream.getTracks().forEach(function (t) {
|
||||
t.onended = function () { onCamTrackEnded(token); };
|
||||
});
|
||||
$('#s-cam').classList.add('cam-live');
|
||||
beginDetect(); // 确认相机可用后才加载检测模型(无相机权限设备不白加载)
|
||||
maybeStartDetect(); // 模型已就绪时即刻开跑检测循环
|
||||
waitVideoStable(function () { // 预热完成前不出实时画面(静态示意底图还在),避免开头"大脸"闪变
|
||||
if (token !== camSeqCam) return;
|
||||
camLive = true;
|
||||
$('#s-cam').classList.add('cam-live');
|
||||
beginDetect(); // 确认相机可用后才加载检测模型(无相机权限设备不白加载)
|
||||
maybeStartDetect(); // 模型已就绪时即刻开跑检测循环
|
||||
});
|
||||
}).catch(function () {
|
||||
if (token !== camSeqCam) return;
|
||||
enableFallback();
|
||||
@@ -218,13 +249,18 @@
|
||||
function captureFromVideo() {
|
||||
var vw = camVideo.videoWidth, vh = camVideo.videoHeight;
|
||||
if (!vw || !vh) return null;
|
||||
var scale = Math.min(1, 1280 / Math.max(vw, vh));
|
||||
var cw = Math.round(vw * scale), ch = Math.round(vh * scale);
|
||||
// 只取取景框可见区域(object-fit:cover 的中心裁切):照片 = 用户当时看到的画面
|
||||
var box = camVideo.getBoundingClientRect();
|
||||
var scaleFit = Math.max(box.width / vw, box.height / vh);
|
||||
var visW = Math.min(vw, box.width / scaleFit), visH = Math.min(vh, box.height / scaleFit);
|
||||
var sx = (vw - visW) / 2, sy = (vh - visH) / 2;
|
||||
var scale = Math.min(1, 1280 / Math.max(visW, visH));
|
||||
var cw = Math.round(visW * scale), ch = Math.round(visH * scale);
|
||||
var canvas = document.createElement('canvas');
|
||||
canvas.width = cw; canvas.height = ch;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.translate(cw, 0); ctx.scale(-1, 1); // 镜像
|
||||
ctx.drawImage(camVideo, 0, 0, cw, ch);
|
||||
if (camMirrored) { ctx.translate(cw, 0); ctx.scale(-1, 1); } // 前置镜像,与预览一致
|
||||
ctx.drawImage(camVideo, sx, sy, visW, visH, 0, 0, cw, ch);
|
||||
return canvas.toDataURL('image/jpeg', 0.88);
|
||||
}
|
||||
// 文件 → 统一压成最长边 1280 的 JPEG → 推进流程(降级系统相机与相册选图共用)
|
||||
@@ -343,12 +379,12 @@
|
||||
m.classList.toggle('no-face', cls === 'no-face');
|
||||
m.classList.toggle('ok', cls === 'ok');
|
||||
}
|
||||
function setHint(text, ok) {
|
||||
function setHint(text, ok, silent) {
|
||||
var h = $('#cam-hint');
|
||||
if (!text) { h.classList.remove('show', 'ok'); lastHint = ''; return; }
|
||||
if (text !== lastHint) {
|
||||
h.textContent = text; // 同文案不重复触发布局
|
||||
voice.play(VOICE.hint[text]); // 语音同步(模块内 4s 去重)
|
||||
if (!silent) voice.play(VOICE.hint[text]); // 语音同步(模块内 4s 去重)
|
||||
}
|
||||
h.classList.add('show');
|
||||
h.classList.toggle('ok', !!ok);
|
||||
@@ -388,7 +424,7 @@
|
||||
c.textContent = cdN;
|
||||
c.classList.add('show');
|
||||
setMask('ok'); // 轮廓保持绿色
|
||||
setHint('保持不动', true);
|
||||
setHint('保持不动', true, /* silent= */true); // 倒计时前不再抢播「保持不动」(会被 321 立刻切断,听着像吞字)
|
||||
voice.play(VOICE.countdown); // 「三、二、一」与数字同步起播
|
||||
cdTimer = setTimeout(cdTick, 1000);
|
||||
}
|
||||
@@ -456,8 +492,8 @@
|
||||
if (ratio < FACE_NEAR) hint = '请靠近一点';
|
||||
else if (ratio > FACE_FAR) hint = '请远一点';
|
||||
else {
|
||||
// 前置预览为镜像画面 → mirrored=true;距离不合格时角度不判 ok
|
||||
var pose = HeadPose.check(d.landmarks.positions, CAM_STEPS[camStep].target, true);
|
||||
// 镜像标志跟随镜头(前置 true/后置 false);距离不合格时角度不判 ok
|
||||
var pose = HeadPose.check(d.landmarks.positions, CAM_STEPS[camStep].target, camMirrored);
|
||||
hint = pose.hint;
|
||||
ok = pose.ok;
|
||||
}
|
||||
@@ -680,19 +716,8 @@
|
||||
|
||||
function setCat(key, cat) {
|
||||
var dom = CAT_DOM[key];
|
||||
var s = $(dom.s), lv = $(dom.lv);
|
||||
if (cat.P == null) { // 缺数据分类:显示说明,不显示分数
|
||||
s.textContent = '本次未检出有效数据';
|
||||
s.className = 'cat-s cat-na';
|
||||
lv.style.display = 'none';
|
||||
} else {
|
||||
s.textContent = cat.S.toFixed(1);
|
||||
s.className = 'cat-s';
|
||||
lv.style.display = '';
|
||||
lv.textContent = cat.level;
|
||||
lv.className = 'cat-badge ' + (LV_CLASS[cat.level] || '');
|
||||
}
|
||||
$(dom.tx).textContent = cat.text;
|
||||
// 设计稿分类卡只有话术正文:不展示 S 与档位徽章;缺数据分类以说明代正文
|
||||
$(dom.tx).textContent = cat.P == null ? '本次未检出有效数据' : cat.text;
|
||||
}
|
||||
|
||||
/* ─── 报告页检测可视化(face_maps 图层 + marks 坐标叠加,数据驱动) ─── */
|
||||
@@ -706,8 +731,8 @@
|
||||
{ key: 'mole', color: '#282828' }, // 痣
|
||||
{ key: 'acne', color: '#D30C0C' } // 痤疮
|
||||
] },
|
||||
tone: { img: '#viz-tone', map: 'brown_area' }, // 肤色:色沉图
|
||||
sens: { img: '#viz-sens', map: 'red_area' }, // 敏感:红区图
|
||||
tone: { img: '#viz-tone', blendMap: 'brown_area' }, // 肤色:正脸×色沉图正片叠底(直挂是悬浮面具感,合成后色沉落在脸上)
|
||||
sens: { img: '#viz-sens', blendMap: 'red_area' }, // 敏感:正脸×红区图正片叠底(直挂白底过淡不可读)
|
||||
acne: { img: '#viz-acne', photo: true, marks: [ // 痘痘:正脸+闭口粉刺/痤疮标记
|
||||
{ key: 'closed_comedones', color: '#E8A13D' }, // 闭口粉刺
|
||||
{ key: 'acne', color: '#D30C0C' }, // 痤疮
|
||||
@@ -793,7 +818,11 @@
|
||||
var ctx = cv.getContext('2d');
|
||||
ctx.drawImage(photo, 0, 0, w, h);
|
||||
if (opts && opts.dim) { ctx.fillStyle = 'rgba(0,0,0,' + opts.dim + ')'; ctx.fillRect(0, 0, w, h); } // 压暗底色让增强线可读
|
||||
if (overlay) ctx.drawImage(overlay, 0, 0, w, h);
|
||||
if (overlay) {
|
||||
if (opts && opts.blend) ctx.globalCompositeOperation = opts.blend; // 正片叠底:白底图层(红区/色沉)叠到脸上
|
||||
ctx.drawImage(overlay, 0, 0, w, h);
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
}
|
||||
var scaled = vizScaleMarks(marks, w / photo.naturalWidth);
|
||||
var lw = Math.max(3, Math.round(w / 200)); // 线宽随图走,卡片缩略后仍可见
|
||||
var minBox = Math.round(w / 36); // 过小标记放大到最小框,保证缩略可见
|
||||
@@ -836,11 +865,18 @@
|
||||
var cfg = VIZ_CAT[key];
|
||||
var imgEl = $(cfg.img);
|
||||
if (!imgEl) continue;
|
||||
if (cfg.map) { // 整幅图层(色沉图/红区图):图层缺失 → 保留占位
|
||||
if (cfg.map) { // 整幅图层:图层缺失 → 保留占位
|
||||
var u = vizMapURL(maps[cfg.map]);
|
||||
if (u) { imgEl.src = u; imgEl.hidden = false; }
|
||||
continue;
|
||||
}
|
||||
if (cfg.blendMap) { // 图层×正脸正片叠底(色沉/红区):无照片时直挂图层;图层缺失 → 保留占位
|
||||
var bu = vizMapURL(maps[cfg.blendMap]);
|
||||
if (!bu) continue;
|
||||
if (!frontPhoto) { imgEl.src = bu; imgEl.hidden = false; continue; }
|
||||
vizCompose(frontPhoto, bu, [], imgEl, { blend: 'multiply' });
|
||||
continue;
|
||||
}
|
||||
if (cfg.overlay) { // 透明增强图层(皱纹):有照片则合成(可按 cfg.dim 压暗),无照片单挂图层;图层缺失 → 保留占位
|
||||
var ov = vizMapURL(maps[cfg.overlay]);
|
||||
if (!ov) continue;
|
||||
@@ -866,15 +902,28 @@
|
||||
}
|
||||
$('#stat-score').textContent = total != null ? total + '分' : '--分';
|
||||
$('#stat-age').textContent = vm.skinAgeDisplay != null ? vm.skinAgeDisplay + '岁' : '--岁';
|
||||
$('#stat-age-note').textContent = vm.ageClampNote || ''; // 肤龄钳制留痕(小字灰)
|
||||
// 肌龄小字:常显「与实际年龄相差X岁」,触发钳制时追加留痕(契约格式「触发(原X→Y)」不变)
|
||||
var ageNote = '';
|
||||
if (vm.skinAgeDisplay != null && vm.realAge != null) {
|
||||
var ageDiff = Math.abs(vm.skinAgeDisplay - vm.realAge);
|
||||
ageNote = ageDiff === 0 ? '与实际年龄相符' : '与实际年龄相差' + ageDiff + '岁';
|
||||
}
|
||||
if (vm.ageClampNote) ageNote += (ageNote ? ' · ' : '') + vm.ageClampNote;
|
||||
$('#stat-age-note').textContent = ageNote;
|
||||
|
||||
// 三维度:S 值(1 位小数) + 档位(良好绿/改善琥珀/预警红)
|
||||
setDim('#dim-firm', '#dim-firm-lv', vm.dims.firm);
|
||||
setDim('#dim-vita', '#dim-vita-lv', vm.dims.vita);
|
||||
setDim('#dim-stab', '#dim-stab-lv', vm.dims.stab);
|
||||
// 关注行(设计稿两行:灰小字「近期优先关注:X」+ 深色维度口号)
|
||||
var DIM_SLOGAN = {
|
||||
'紧塑力': '紧塑提拉力,轮廓上扬不松垮',
|
||||
'活力感': '活力透亮感,水润澎弹有光泽',
|
||||
'稳定力': '强韧稳态力,锁水保湿抵御刺激' // 设计稿原文
|
||||
};
|
||||
$('#dims-note').innerHTML = vm.focus.name
|
||||
? '近期优先关注:' + esc(vm.focus.name) + '<br>' + esc(vm.focus.tip)
|
||||
: '近期优先关注:<br>---';
|
||||
? '<span class="dims-note-l1">近期优先关注:' + esc(vm.focus.name) + '</span><br><span class="dims-note-l2">' + esc(DIM_SLOGAN[vm.focus.name] || '') + '</span>'
|
||||
: '<span class="dims-note-l1">近期优先关注:---</span>';
|
||||
|
||||
// 三个 Tab 面板:各所辖两个分类(S + 档位徽章 + 话术正文)
|
||||
var keys = ['contour', 'wrinkles', 'smooth', 'tone', 'sens', 'acne'];
|
||||
@@ -883,10 +932,12 @@
|
||||
// 检测可视化:轮廓=正脸实拍,其余按 VIZ_CAT 取 face_maps 图层或叠加 marks(payload 缺失时各卡保留占位)
|
||||
renderViz(payload, frontPhoto);
|
||||
|
||||
// 降级标注(下颌线未参与评分等,非空才展示)
|
||||
// 降级标注:只展示用户可感知/可执行的(下颌线、侧脸、补拍类);
|
||||
// 引擎内部技术项(上限锁未执行/字段未返回等)不面向用户展示
|
||||
var notesEl = $('#vm-notes');
|
||||
if (vm.notes && vm.notes.length) {
|
||||
notesEl.innerHTML = vm.notes.map(function (t) { return '*' + esc(t); }).join('<br>');
|
||||
var userNotes = (vm.notes || []).filter(function (t) { return /下颌线|侧脸|补拍|重拍/.test(t); });
|
||||
if (userNotes.length) {
|
||||
notesEl.innerHTML = userNotes.map(function (t) { return '*' + esc(t); }).join('<br>');
|
||||
notesEl.style.display = '';
|
||||
} else {
|
||||
notesEl.style.display = 'none';
|
||||
@@ -990,7 +1041,13 @@
|
||||
shutterLock = true; // 与降级快门同策略:前置上锁,change/cancel 解锁
|
||||
albumFile.click();
|
||||
},
|
||||
'flip': function () { toast('切换镜头功能开发中'); },
|
||||
'flip': function () { // 切换前后镜头
|
||||
if (camFallback) { toast('系统相机模式下请在系统相机内切换'); return; }
|
||||
if (!camStream) { toast('相机启动中,请稍候'); return; }
|
||||
camFacing = camFacing === 'user' ? 'environment' : 'user';
|
||||
camMirrored = camFacing === 'user';
|
||||
startCamera(); // 复用启停与预热(camSeqCam 防竞态)
|
||||
},
|
||||
'back-profile': function () { showScreen('s-profile'); },
|
||||
'history': function () { openHistory(); },
|
||||
'close-history': function () { closeOverlay('p-history'); },
|
||||
|
||||
@@ -722,6 +722,7 @@
|
||||
skinAgeRaw: skinAge,
|
||||
skinAgeDisplay: skinAgeDisplay,
|
||||
ageClampNote: ageClampNote,
|
||||
realAge: realAge,
|
||||
dims: dims,
|
||||
cats: cats,
|
||||
group: group,
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 926 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 258 KiB |
Reference in New Issue
Block a user