feat(report): add annotated wrinkle views and restore capture countdown
This commit is contained in:
+5
-1
@@ -419,7 +419,7 @@ function renderDetail(data) {
|
||||
if (r.wrinkle_score_rule === 'api-overall-v1') h += '<p class="sub">皱纹评分直接使用接口整体皱纹分数,不做100减分,也不再按分区严重度限分。</p>';
|
||||
if (r.tone_scoring && r.tone_scoring.version === 'color-dark-circle-20-v1') {
|
||||
var tone = r.tone_scoring;
|
||||
h += '<p class="sub">肤色项=色素扣斑分×80%+黑眼圈评分×20%;' + (r.vita_score_rule ? '透亮感取独立肤色、水分、平滑度三项最低分,不再使用肤色水润平均分。' : '历史版本与水分项各占50%。') + '色素扣斑分:' + esc(fmtToneScore(tone.pigment_adjusted_score)) + ',黑眼圈评分:' + esc(fmtToneScore(tone.dark_circle_score)) + ',独立肤色项:' + esc(fmtToneScore(tone.color_score)) + ',水分项:' + esc(fmtToneScore(tone.water_adjusted_score)) + '。肤色缺项按有效权重归一。</p>';
|
||||
h += '<p class="sub">肤色项=色素扣斑分×80%+黑眼圈评分×20%;' + (r.vita_score_rule === 'independent-color-water-smooth-min-v1' ? '该历史版本透亮感取独立肤色、水分、平滑度三项最低分。' : '肤色项与水分项各占50%合成肤色水润分,透亮感取肤色水润与平滑度的最低分。') + '色素扣斑分:' + esc(fmtToneScore(tone.pigment_adjusted_score)) + ',黑眼圈评分:' + esc(fmtToneScore(tone.dark_circle_score)) + ',独立肤色项:' + esc(fmtToneScore(tone.color_score)) + ',水分项:' + esc(fmtToneScore(tone.water_adjusted_score)) + '。肤色缺项按有效权重归一。</p>';
|
||||
}
|
||||
h += '<p class="sub">前端展示肌龄限制在实际年龄±5岁;推荐仍使用原始肌龄。历史报告保留当时的推荐。</p>';
|
||||
|
||||
@@ -449,6 +449,10 @@ function renderDetail(data) {
|
||||
h += dSec('痘痘三视图计分明细', '<p class="sub">每种皮损分别取三视图最大数量,不相加,也不是全脸去重总数;“-”表示未获取。整体评分仍用正脸:' + esc(fmtScore(audit.front_score)) + ',映射分:' + esc(fmtScore(audit.mapped_score)) + '。计数扣分合计:' + esc(fmtScore(audit.raw_deduction)) + ',封顶50后:' + esc(fmtScore(audit.deduction)) + '。</p><table class="d-table"><thead><tr><th>类型</th><th>正脸</th><th>左脸</th><th>右脸</th><th>计分数量</th><th>单处扣分</th><th>该项扣分</th></tr></thead><tbody>' + acneRows + '</tbody></table>');
|
||||
}
|
||||
|
||||
if (r.wrinkle_view_status) {
|
||||
h += dSec('侧脸皱纹标注获取状态', '<p class="sub">左右侧脸各自独立调用,仅用于图层展示,不参与计分;查看报告不重新调用。</p>' +
|
||||
['left','right'].map(function(view) { var s=r.wrinkle_view_status[view]||{}; return '<p>'+ (view==='left'?'左脸':'右脸') +':'+esc(s.status==='ready'?'已获取':'未获取')+(s.error_code?'('+esc(s.error_code)+')':'')+'</p>'; }).join(''));
|
||||
}
|
||||
/* 文案全文 */
|
||||
var copyHtml = '';
|
||||
var presentation = r.presentation;
|
||||
|
||||
+103
-18
@@ -516,6 +516,7 @@
|
||||
startCamera();
|
||||
}
|
||||
function stopCamera(keepFrozen) {
|
||||
clearCaptureCountdown();
|
||||
camSeqCam++; // 使进行中的 getUserMedia 回调失效
|
||||
clearInterval(camReadyTimer); clearTimeout(camOpenTimer);
|
||||
camReadyTimer = camOpenTimer = null;
|
||||
@@ -701,14 +702,14 @@
|
||||
cur = null;
|
||||
if (c.src) { c.src.onended = null; try { c.src.stop(); } catch (e) {} }
|
||||
}
|
||||
function start(id, kind) {
|
||||
function start(id, kind, onStart) {
|
||||
stopCur();
|
||||
lastAt[id] = Date.now();
|
||||
var token = {};
|
||||
cur = { id: id, kind: kind, token: token, src: null, end: 0 };
|
||||
load(id).then(function (buf) {
|
||||
if (!cur || cur.token !== token) return; // 加载期间已被打断
|
||||
if (!buf || !ctx) { cur = null; drain(); return; }
|
||||
if (!buf || !ctx) { if (onStart) onStart(3000); cur = null; drain(); return; }
|
||||
var src = ctx.createBufferSource();
|
||||
src.buffer = buf;
|
||||
src.connect(ctx.destination);
|
||||
@@ -716,11 +717,12 @@
|
||||
cur.end = Date.now() + buf.duration * 1000;
|
||||
src.onended = function () { if (cur && cur.token === token) { cur = null; drain(); } };
|
||||
src.start(0);
|
||||
if (onStart) onStart(buf.duration * 1000);
|
||||
});
|
||||
}
|
||||
function drain() {
|
||||
if (cur) return;
|
||||
if (queue.length) { start(queue.shift(), 'flow'); return; }
|
||||
if (queue.length) { var next = queue.shift(); start(next.id, 'flow', next.onStart); return; }
|
||||
if (pendingHint) { var h = pendingHint; pendingHint = null; start(h, 'hint'); return; }
|
||||
var w = waiters;
|
||||
waiters = [];
|
||||
@@ -731,16 +733,16 @@
|
||||
// 流程音是离散事件,每次都播(连拍的「拍摄完成」、取消后重来的倒计时)
|
||||
function play(id, opts) {
|
||||
opts = opts || {};
|
||||
if (!ctx || !id || ctx.state !== 'running' || !files || !files[id]) return;
|
||||
if (!ctx || !id || ctx.state !== 'running' || !files || !files[id]) { if (opts.onStart) opts.onStart(3000); return; }
|
||||
if (opts.hint) {
|
||||
if (lastAt[id] && Date.now() - lastAt[id] < 4000) return;
|
||||
if (busyWithFlow()) { pendingHint = id; return; }
|
||||
start(id, 'hint');
|
||||
return;
|
||||
}
|
||||
if (opts.cut) { queue = []; pendingHint = null; start(id, 'flow'); return; }
|
||||
if (busyWithFlow()) { if (queue.indexOf(id) < 0) queue.push(id); return; }
|
||||
start(id, 'flow'); // 在播的姿态提示让位给流程音
|
||||
if (opts.cut) { queue = []; pendingHint = null; start(id, 'flow', opts.onStart); return; }
|
||||
if (busyWithFlow()) { if (!queue.some(function(item) { return item.id === id; })) queue.push({ id:id, onStart:opts.onStart }); return; }
|
||||
start(id, 'flow', opts.onStart); // 在播的姿态提示让位给流程音
|
||||
}
|
||||
function dropHint() { pendingHint = null; } // 提示已清空/换步:挂起的旧提示作废
|
||||
// 流程切走:作废排队/挂起/等待;hard 连在播一并掐断(退出拍照、页面隐藏),否则让在播自然收尾(出报告)
|
||||
@@ -1082,7 +1084,29 @@
|
||||
}, hasNext ? NEXT_PAUSE_MS : 300);
|
||||
}
|
||||
|
||||
// 白屏补光抓拍:全屏纯白瞬间点亮→约300ms 后抓全分辨率帧→保持300ms 再淡出(总 600–900ms)
|
||||
var captureCountdownTimers = [];
|
||||
function clearCaptureCountdown() {
|
||||
captureCountdownTimers.forEach(clearTimeout);
|
||||
captureCountdownTimers = [];
|
||||
var el = $('#cam-countdown');
|
||||
el.hidden = true; el.textContent = '';
|
||||
}
|
||||
function startCaptureCountdown(seq, duration, done) {
|
||||
clearCaptureCountdown();
|
||||
function tick(number) {
|
||||
if (seq !== camSeq || !camLive || currentScreen !== 's-cam') { clearCaptureCountdown(); return; }
|
||||
var el = $('#cam-countdown'), digit = document.createElement('span');
|
||||
digit.className = 'cam-countdown-tick'; digit.textContent = String(number);
|
||||
el.replaceChildren(digit); el.hidden = false;
|
||||
}
|
||||
tick(3);
|
||||
[2,1].forEach(function(number,i) { captureCountdownTimers.push(setTimeout(function() { tick(number); }, duration * (i+1) / 3)); });
|
||||
captureCountdownTimers.push(setTimeout(function() {
|
||||
clearCaptureCountdown();
|
||||
if (seq === camSeq && camLive && currentScreen === 's-cam') done();
|
||||
}, duration));
|
||||
}
|
||||
// 白屏补光抓拍:数字倒计时和语音都结束后才点亮补光、抓帧。
|
||||
function doCapture() {
|
||||
if (shutterLock) return;
|
||||
if (!camLive) return;
|
||||
@@ -1093,8 +1117,19 @@
|
||||
hideNudge();
|
||||
var seq = camSeq;
|
||||
voice.dropHint();
|
||||
voice.play(VOICE.countdown);
|
||||
var audioDone = false, visualDone = false, started = false;
|
||||
function finishCountdown() { if (audioDone && visualDone) capture(); }
|
||||
voice.play(VOICE.countdown, { onStart:function(duration) {
|
||||
if (seq !== camSeq || !camLive || currentScreen !== 's-cam') return;
|
||||
started = true;
|
||||
startCaptureCountdown(seq, duration, function() { visualDone = true; finishCountdown(); });
|
||||
} });
|
||||
voice.afterCurrent(function () {
|
||||
audioDone = true;
|
||||
if (!started) { started = true; startCaptureCountdown(seq, 3000, function() { visualDone = true; finishCountdown(); }); }
|
||||
finishCountdown();
|
||||
}, true);
|
||||
function capture() {
|
||||
if (seq !== camSeq || !camLive || currentScreen !== 's-cam') return;
|
||||
var fill = $('#cam-fill');
|
||||
fill.classList.add('on'); // .on 无过渡,瞬间点亮
|
||||
@@ -1117,7 +1152,7 @@
|
||||
});
|
||||
}, 300);
|
||||
}, 300);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
// 三张拍完:按视角重排后送检(正脸=photos[0],左脸=photos[1],右脸=photos[2])
|
||||
@@ -1200,7 +1235,7 @@
|
||||
if (fx) {
|
||||
// marks 坐标空间=fixture 照片空间:三连拍替换为 fixture 照片,保证可视化叠加对齐
|
||||
ph[0] = fx.photos[0]; ph[1] = fx.photos[1]; ph[2] = fx.photos[2];
|
||||
done(null, { result: fx.result, side: fx.side, face_rectangle: fx.face_rectangle });
|
||||
done(null, { result: fx.result, side: fx.side, wrinkle_views: fx.wrinkle_views, face_rectangle: fx.face_rectangle });
|
||||
} else {
|
||||
done(null, { result: SkinScore.MOCK_RESULT, side: SkinScore.MOCK_SIDE });
|
||||
}
|
||||
@@ -1270,8 +1305,8 @@
|
||||
|
||||
function setCat(key, cat) {
|
||||
var dom = CAT_DOM[key];
|
||||
// 设计稿分类卡只有话术正文:不展示 S 与档位徽章;缺数据分类以说明代正文
|
||||
$(dom.tx).textContent = cat.P == null ? '本次未检出有效数据' : cat.text;
|
||||
// 分类卡只展示有效话术正文;缺数据不在报告中提示重拍。
|
||||
$(dom.tx).textContent = cat.P == null ? '' : cat.text;
|
||||
}
|
||||
|
||||
/* ─── 报告页检测可视化(face_maps 图层 + marks 坐标叠加,数据驱动) ─── */
|
||||
@@ -1350,6 +1385,17 @@
|
||||
}
|
||||
return out;
|
||||
}
|
||||
function vizWrinkleMarks(result) {
|
||||
var outlines = result && result.wrinkle_mark;
|
||||
if (!outlines || typeof outlines !== 'object') return [];
|
||||
var normalized = {}, defs = [];
|
||||
Object.keys(outlines).forEach(function(key) {
|
||||
if (!/_wrinkle_outline$/.test(key) || !Array.isArray(outlines[key])) return;
|
||||
normalized[key] = { polygon: [outlines[key]] };
|
||||
defs.push({ key: key, color: '#6E9900' });
|
||||
});
|
||||
return vizCollectMarks(defs, normalized);
|
||||
}
|
||||
|
||||
// marks 坐标空间 = 上传图片像素空间:按「画布自然宽 / 坐标空间宽」换算(画布=照片自然尺寸时比值为 1)
|
||||
function vizScaleMarks(marks, scale) {
|
||||
@@ -1395,6 +1441,7 @@
|
||||
if (!photo) return; // 照片不可用:保留占位
|
||||
var finish = function (overlays) {
|
||||
if (epoch !== vizEpoch) return;
|
||||
if (!overlays.some(Boolean) && opts && opts.fallbackMarks) marks = opts.fallbackMarks;
|
||||
var crop = vizFaceCrop(photo.naturalWidth, photo.naturalHeight, opts && opts.faceRectangle);
|
||||
var originalTop = crop.top;
|
||||
crop = vizShiftContentDown(crop, opts && opts.faceRectangle, opts && opts.contentDown);
|
||||
@@ -1450,6 +1497,7 @@
|
||||
imgEl.src = cv.toDataURL('image/jpeg', 0.9); imgEl.hidden = false;
|
||||
imgEl.dataset.crop = crop.focused ? 'face' : 'full';
|
||||
imgEl.dataset.contentOffset = String((originalTop - crop.top) / crop.height);
|
||||
imgEl.dataset.hasAnnotation = String(overlays.some(Boolean) || marks.length > 0);
|
||||
imgEl.dataset.overlayAlpha = String(opts && typeof opts.alpha === 'number' ? opts.alpha : 1);
|
||||
} catch (e) { /* 合成失败保留占位 */ }
|
||||
};
|
||||
@@ -1503,9 +1551,10 @@
|
||||
}
|
||||
if (cfg.overlay) { // 透明增强图层(皱纹):有照片则合成(可按 cfg.dim 压暗),无照片单挂图层;图层缺失 → 保留占位
|
||||
var ov = vizMapURL(maps[cfg.overlay]);
|
||||
if (!ov) continue;
|
||||
var wrinkleMarks = key === 'wrinkles' && !ov ? vizWrinkleMarks(result) : [];
|
||||
if (!ov && !frontPhoto) continue;
|
||||
if (!frontPhoto) { imgEl.src = ov; imgEl.hidden = false; continue; }
|
||||
vizCompose(frontPhoto, ov, [], imgEl, { dim: cfg.dim, faceRectangle: rectangle });
|
||||
vizCompose(frontPhoto, ov, wrinkleMarks, imgEl, { dim: ov || wrinkleMarks.length ? cfg.dim : 0, faceRectangle: rectangle, contentDown: key === 'wrinkles' ? .09 : 0, fallbackMarks: key === 'wrinkles' ? vizWrinkleMarks(result) : null });
|
||||
continue;
|
||||
}
|
||||
// 照片类(轮廓/光滑/痘痘):无照片 → 保留占位;有坐标则 canvas 叠加,无坐标纯照片
|
||||
@@ -1542,6 +1591,37 @@
|
||||
});
|
||||
refreshHeight();
|
||||
}
|
||||
var wrinklePhotoEpoch = 0;
|
||||
function renderWrinkleSides(reportPhotos, payload) {
|
||||
var epoch = ++wrinklePhotoEpoch, wrap = $('#wrinkle-sides'), note = $('#wrinkle-layer-note');
|
||||
function refresh() {
|
||||
var visible = Array.from(wrap.querySelectorAll('figure:not([hidden])'));
|
||||
wrap.hidden = !visible.length;
|
||||
note.hidden = !visible.some(function(f) { return f.querySelector('img').dataset.hasAnnotation !== 'true'; });
|
||||
var card = wrap.closest('.card-detail'), state = detailSwipes.get(card);
|
||||
if (state) state.height(Number(card.querySelector('.dtab.active').dataset.tab));
|
||||
}
|
||||
wrap.hidden = note.hidden = true;
|
||||
['left', 'right'].forEach(function(view, i) {
|
||||
var img = $('#wrinkle-' + view), figure = img.closest('figure');
|
||||
img.onload = img.onerror = null; img.removeAttribute('src');
|
||||
img.dataset.hasAnnotation = 'false'; img.draggable = false; figure.hidden = true;
|
||||
var src = reportPhotos && reportPhotos[i + 1];
|
||||
if (!src) return;
|
||||
// Only this photograph's own API data may be used. The front-face map
|
||||
// is NEVER a fallback for a side photo.
|
||||
var side = payload && payload.wrinkle_views && payload.wrinkle_views[view];
|
||||
if (!side || !side.result) side = payload && payload.side && payload.side[view];
|
||||
var result = side && (side.result || side);
|
||||
var overlay = vizMapURL(result && result.face_maps && result.face_maps.texture_enhanced_lines);
|
||||
var marks = overlay ? [] : vizWrinkleMarks(result);
|
||||
img.onload = function() { if (epoch === wrinklePhotoEpoch) { figure.hidden = false; refresh(); } };
|
||||
img.onerror = function() { if (epoch === wrinklePhotoEpoch) { figure.hidden = true; img.removeAttribute('src'); refresh(); } };
|
||||
if (overlay || marks.length) vizCompose(src, overlay, marks, img, { dim: .55, fallbackMarks: vizWrinkleMarks(result) });
|
||||
else img.src = src;
|
||||
});
|
||||
refresh();
|
||||
}
|
||||
function renderReport(vm, frontPhoto, sidePhoto, payload, reportPhotos) {
|
||||
renderContourSides(reportPhotos || [frontPhoto, sidePhoto, null]);
|
||||
reportOwner = AccountClient.user() && AccountClient.user().id;
|
||||
@@ -1578,10 +1658,15 @@
|
||||
focusEl.append(first, second);
|
||||
}
|
||||
var textIds = { contour: '#ditem-para1', wrinkles: '#ditem-para2', color: '#tx-tone', smooth: '#tx-smooth', water: '#tx-water', sens: '#tx-sens', acne: '#tx-acne' };
|
||||
presentation.views.forEach(function (v) { $(textIds[v.key]).textContent = ReportUI.customerText(v); });
|
||||
presentation.views.forEach(function (v) {
|
||||
var text = ReportUI.customerText(v), el = $(textIds[v.key]);
|
||||
el.textContent = text;
|
||||
el.hidden = !text;
|
||||
});
|
||||
|
||||
// 检测可视化:轮廓=正脸实拍,其余按 VIZ_CAT 取 face_maps 图层或叠加 marks(payload 缺失时各卡保留占位)
|
||||
renderViz(payload, frontPhoto);
|
||||
renderWrinkleSides(reportPhotos || [frontPhoto, sidePhoto, null], payload);
|
||||
|
||||
// Fourth feedback: capture quality belongs in the protected admin record,
|
||||
// not in the customer report. Preserve vm.notes/snapshots unchanged.
|
||||
@@ -1692,7 +1777,7 @@
|
||||
card.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.dtab');
|
||||
if (btn) { setDetailTab(card, btn.dataset.tab); return; }
|
||||
var sideButton = e.target.closest('.contour-side-photo');
|
||||
var sideButton = e.target.closest('.contour-side-photo, .wrinkle-side-photo');
|
||||
var img = sideButton ? sideButton.querySelector('img') : e.target.closest('.ditem-img');
|
||||
if (img && img.src && !img.hidden && img.closest('.dpanel').classList.contains('active')) {
|
||||
$('#zoom-img').src = img.src;
|
||||
@@ -1944,7 +2029,7 @@
|
||||
renderReport(SkinScore.buildViewModel(mockRes, mockSide, {
|
||||
realAge: +($('#f-age').value || 0) || 28, // 调试档案缺省 28 岁
|
||||
skinType: SKINS[skinIndex].name
|
||||
}), fx ? fx.photos[0] : null, null, fx ? { result: mockRes, side: mockSide, face_rectangle: fx.face_rectangle } : null, fx && fx.photos);
|
||||
}), fx ? fx.photos[0] : null, null, fx ? { result: mockRes, side: mockSide, wrinkle_views: fx.wrinkle_views, face_rectangle: fx.face_rectangle } : null, fx && fx.photos);
|
||||
} catch (e) { /* 调试入口异常不阻塞正式流程 */ }
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
"优秀": "肌肤质感表现优异,表面细腻平滑,角质层排列整齐,无明显粗大毛孔与黑头。"
|
||||
},
|
||||
"water": {
|
||||
"改善": "水分分 {water_score};缺水面积 {water_area}%,{water_severity_note}肌肤水润度有待提升,建议加强补水与保湿。",
|
||||
"良好": "水分分 {water_score};缺水面积 {water_area}%、肌肤含水量处于健康水平,整体水润度良好。",
|
||||
"优秀": "水分分 {water_score};角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。"
|
||||
"改善": "角质层含水量较低,缺水面积 {water_area}%、肌肤缺水比较严重 {water_sev},肌肤含水量偏低,水润度不足,表面偏干燥,容易出现干纹。",
|
||||
"良好": "角质层含水量适中,缺水面积 {water_area}%、肌肤含水量处于健康水平,整体水润度良好。",
|
||||
"优秀": "角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。"
|
||||
},
|
||||
"sens": {
|
||||
"改善": "泛红面积达 {sens_area}%,肌肤泛红明显,敏感区域可见泛红;对外界刺激耐受度不足,容易受外界因素影响产生状态波动。",
|
||||
|
||||
+5
-10
@@ -22,7 +22,7 @@
|
||||
return numeric(value) ? (Math.round((value + 1e-9) * 10) / 10).toFixed(1) : '-';
|
||||
}
|
||||
var gradeRule = 'display-score-8-9-v1';
|
||||
var copyRule = 'wrinkles-excellent-acne-detected-v1';
|
||||
var copyRule = 'wrinkles-excellent-acne-detected-water-v2';
|
||||
function scoreOfP(P) {
|
||||
return numeric(P) ? Math.round((Math.max(7, Math.min(10, 7 + 3 * P / 100)) + 1e-9) * 100) / 100 : null;
|
||||
}
|
||||
@@ -45,11 +45,13 @@
|
||||
return selected;
|
||||
}
|
||||
function customerText(view) {
|
||||
// Keep missing-data diagnostics in the saved/admin record, not on the
|
||||
// customer report. Also suppress equivalent text in older snapshots.
|
||||
if (view.valid === false || /本次未检出有效数据|本次未获取轮廓有效数据/.test(view.text || '')) return '';
|
||||
if (view.key === 'wrinkles') return view.text.split('\n分区明细')[0];
|
||||
if (view.key !== 'contour') return view.text;
|
||||
// Also cover immutable old presentations, without modifying stored copy.
|
||||
return view.text.replace(/侧脸角度数据未完整获取[;;]?/g, '')
|
||||
.replace('本次未检出有效数据,请重新拍摄后查看。', '本次未获取轮廓有效数据。');
|
||||
return view.text.replace(/侧脸角度数据未完整获取[;;]?/g, '');
|
||||
}
|
||||
function scalar(value) { return numeric(value) ? value : value && numeric(value.value) ? value.value : null; }
|
||||
var wrinkleDefs = [
|
||||
@@ -115,13 +117,6 @@
|
||||
var viewS = valid ? scoreOfP(viewP) : null;
|
||||
level = levelForScore(viewS);
|
||||
var template = valid && level ? Copy.conclusion[d[0]][level] : '';
|
||||
if (d[0] === 'water' && valid) {
|
||||
vars.water_severity_note = numeric(vars.water_sev) && vars.water_sev >= 0 && vars.water_sev <= 100
|
||||
? (vars.water_sev >= 60 ? '缺水程度较高(严重度 {water_sev}),' : '缺水严重度 {water_sev},') : '';
|
||||
if (viewP !== vars.water_score) template = '水分分 {water_score};缺水面积 {water_area}%,缺水严重度 {water_sev}。水分分数与缺水指标不一致,本次按缺水指标评估,建议加强补水与保湿。';
|
||||
// Resolve the optional phrase before normal scalar substitution.
|
||||
template = template.replace(/\{water_severity_note\}/g, vars.water_severity_note);
|
||||
}
|
||||
if (d[0] === 'contour') {
|
||||
var severity = vars.dark_circle_sev;
|
||||
var description = numeric(severity) ? ['无明显黑眼圈', '黑眼圈轻度', '黑眼圈中度', '黑眼圈重度'][severity] : null;
|
||||
|
||||
+3
-10
@@ -752,16 +752,9 @@
|
||||
var d = DIM_DEFS[i], ps = [], j;
|
||||
for (j = 0; j < CAT_DEFS.length; j++) {
|
||||
if (CAT_DEFS[j].dim !== d.key) continue;
|
||||
if (d.key === 'vita' && CAT_DEFS[j].key === 'tone') continue; // retired combined score
|
||||
var P = cats[CAT_DEFS[j].key].P;
|
||||
if (P != null) ps.push(P);
|
||||
}
|
||||
if (d.key === 'vita') {
|
||||
var toneVars = cats.tone.vars;
|
||||
[toneVars.color_score, toneVars.tone_scoring && toneVars.tone_scoring.water_adjusted_score].forEach(function(score) {
|
||||
if (isNum(score)) ps.push(score);
|
||||
});
|
||||
}
|
||||
var dP = ps.length ? Math.min.apply(null, ps) : null;
|
||||
if (!ps.length) notes.push(d.name + ':旗下分类数据全部缺失,维度不参与评分');
|
||||
dims[d.key] = { name: d.name, P: dP, S: sShow(dP), level: levelOf(dP) };
|
||||
@@ -855,7 +848,7 @@
|
||||
return {
|
||||
total: weightedTotal(dims),
|
||||
totalRule: 'dimensions-433-v1',
|
||||
vitaScoreRule: 'independent-color-water-smooth-min-v1',
|
||||
vitaScoreRule: 'combined-tone-smooth-min-v2',
|
||||
wrinkleScoreRule: 'api-overall-v1',
|
||||
acneScoreRule: 'three-view-type-max-v1',
|
||||
skinAgeRaw: skinAge,
|
||||
@@ -992,7 +985,7 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
ok(vm.cats.sens.P === 98, '敏感P = min(98, 100−0.1×1.5=99.85) = 98(面积 0.001 小数口径×100)');
|
||||
ok(vm.cats.acne.P === 85, '痘痘P = min(分级映射85, 100−14.5=85.5) = 85');
|
||||
ok(vm.cats.wrinkles.S === 7.69 && vm.cats.smooth.S === 9.45 && vm.cats.tone.S === 9.45, 'S=7+3×P/100 两位小数(含 9.445→9.45 浮点修正)');
|
||||
ok(vm.dims.firm.P === 23 && vm.dims.vita.P === 80 && vm.dims.stab.P === 85, '维度木桶 min:紧塑力23 / 独立肤色短板80 / 稳定力85');
|
||||
ok(vm.dims.firm.P === 23 && vm.dims.vita.P === Math.min(vm.cats.smooth.P,vm.cats.tone.P) && vm.dims.stab.P === 85, '维度木桶 min:紧塑力23 / 平滑度与肤色水润最低分 / 稳定力85');
|
||||
ok(vm.skinAgeRaw === 20 && vm.skinAgeDisplay === 20 && vm.ageClampNote === '', '肌肤年龄直接展示原值20,不按实际年龄钳制');
|
||||
ok(vm.group === '25+组', '原始肌龄20 → 25+组');
|
||||
ok(vm.essence.name === '25+束紧精华' && vm.recommendation.tier === 1,
|
||||
@@ -1023,7 +1016,7 @@ if (typeof require === 'function' && typeof module !== 'undefined' && require.ma
|
||||
console.log('== T2 木桶 min(显式构造) ==');
|
||||
var vm2 = SS.buildViewModel(clone(SS.MOCK_RESULT), SS.MOCK_SIDE, { realAge: 40 });
|
||||
ok(vm2.dims.firm.P === Math.min(vm2.cats.contour.P, vm2.cats.wrinkles.P)
|
||||
&& vm2.dims.vita.P === Math.min(vm2.cats.smooth.P, vm2.cats.tone.vars.color_score, vm2.cats.tone.vars.tone_scoring.water_adjusted_score)
|
||||
&& vm2.dims.vita.P === Math.min(vm2.cats.smooth.P, vm2.cats.tone.P)
|
||||
&& vm2.dims.stab.P === Math.min(vm2.cats.sens.P, vm2.cats.acne.P), '三维度 P = min(旗下两分类)');
|
||||
ok(vm2.group === '25+组', '实际年龄40不影响推荐,原始肌龄20仍为25+组');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user