Files
juzi/h5/js/report-ui.js
T

202 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Presentation only: original scores and recommendation rules remain in SkinScore. */
(function(root, factory) {
if (typeof module === 'object' && module.exports) module.exports = factory(require('./report-copy.js'));
else root.ReportUI = factory(root.ReportCopy);
})(typeof window !== 'undefined' ? window : this, function(Copy) {
'use strict';
var defs = [
['contour', '轮廓', 'firm', 'contour'], ['wrinkles', '皱纹', 'firm', 'wrinkles'],
['color', '肤色', 'vita', 'tone'], ['smooth', '平滑度', 'vita', 'smooth'], ['water', '水分', 'vita', 'tone'],
['sens', '泛红', 'stab', 'sens'], ['acne', '痘痘', 'stab', 'acne']
];
var products = [
{ tier: 1, match: '25+束紧精华', title: "1'me 主场精华 I", name: '焕活光泽 透亮紧致', image: 'assets/img/ui-20261015/product-25.png' },
{ tier: 2, match: '35+支撑精华', title: "1'me 主场精华 II", name: '恢复弹性 紧实利落', image: 'assets/img/ui-20261015/product-35.png' },
{ tier: 3, match: '50+重塑精华', title: "1'me 主场精华 III", name: '强韧屏障 轮廓向上', image: 'assets/img/ui-20261015/product-50.png' }
];
var levelMap = { '预警': '改善', '改善': '良好', '良好': '优秀' };
var dimNames = { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' };
var focusTips = { firm: '强化紧塑力,提拉轮廓抚平细纹。', vita: '提升透亮感,补水提亮加强防晒。', stab: '增强稳韧力,舒缓泛红强韧屏障。' };
function numeric(value) { return typeof value === 'number' && isFinite(value); }
function dimensionScore(value) {
return numeric(value) ? Math.min(9.9, Math.round((value + 1e-9) * 10) / 10).toFixed(1) : '-';
}
var gradeRule = 'display-score-8-9-v1';
var copyRule = 'wrinkles-excellent-acne-detected-water-contour-v3';
function scoreOfP(P) {
return numeric(P) ? Math.round((Math.max(7, Math.min(10, 7 + 3 * P / 100)) + 1e-9) * 100) / 100 : null;
}
function levelForScore(S) {
if (!numeric(S)) return null;
var displayed = Number(dimensionScore(S));
return displayed < 8 ? '改善' : displayed < 9 ? '良好' : '优秀';
}
function internalLevel(S) {
return { '改善': '预警', '良好': '改善', '优秀': '良好' }[levelForScore(S)] || null;
}
// Read saved raw dimension values, never re-score historical reports or use
// the legacy focus.name (which contains the old dimension terminology).
function focusOf(vm) {
var selected = null;
['firm', 'vita', 'stab'].forEach(function(key) {
var dim = vm.dims && vm.dims[key];
if (dim && numeric(dim.P) && (!selected || dim.P < selected.score)) selected = { key: key, score: dim.P, name: dimNames[key], tip: focusTips[key] };
});
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, '');
}
function scalar(value) { return numeric(value) ? value : value && numeric(value.value) ? value.value : null; }
var wrinkleDefs = [
['forehead', '抬头纹(额头纹)', ['forehead_wrinkle_info'], 'forehead_count'],
['left_nasolabial', '左法令纹', ['left_nasolabial_wrinkle_info', 'left_nasolabial_fold_info'], 'left_nasolabial_count'],
['right_nasolabial', '右法令纹', ['right_nasolabial_wrinkle_info', 'right_nasolabial_fold_info'], 'right_nasolabial_count'],
['left_crowsfeet', '左鱼尾纹', ['left_crowsfeet_wrinkle_info'], 'left_crowsfeet_count'],
['right_crowsfeet', '右鱼尾纹', ['right_crowsfeet_wrinkle_info'], 'right_crowsfeet_count'],
['left_eye', '左眼部细纹', ['left_eye_wrinkle_info'], 'left_undereye_count'],
['right_eye', '右眼部细纹', ['right_eye_wrinkle_info'], 'right_undereye_count'],
['left_mouth', '左嘴角纹', ['left_mouth_wrinkle_info'], 'left_mouth_count'],
['right_mouth', '右嘴角纹', ['right_mouth_wrinkle_info'], 'right_mouth_count'],
['glabella', '眉间纹', ['glabella_wrinkle_info'], 'glabella_count'],
['left_cheek', '左脸颊纹', ['left_cheek_wrinkle_info'], 'left_cheek_count'],
['right_cheek', '右脸颊纹', ['right_cheek_wrinkle_info'], 'right_cheek_count']
];
var wrinkleMetrics = [
['wrinkle_score', '皱纹分'], ['wrinkle_severity_level', '严重等级'],
['wrinkle_deep_num', '深纹条数'], ['wrinkle_shallow_num', '浅纹条数'],
['wrinkle_norm_length', '标准化长度'], ['wrinkle_norm_depth', '标准化深度'],
['wrinkle_pixel_density', '像素密度'], ['wrinkle_area_ratio', '面积比例'], ['wrinkle_deep_ratio', '深纹比例']
];
function wrinkleDetails(result) {
result = result || {};
return wrinkleDefs.map(function (def) {
var node = null;
def[2].some(function (key) { if (result[key] && typeof result[key] === 'object') { node = result[key]; return true; } return false; });
var values = {};
wrinkleMetrics.forEach(function (metric) { values[metric[0]] = scalar(node && node[metric[0]]); });
// Region counts are a valid fallback; a missing count is never zero.
if (values.wrinkle_deep_num == null) values.wrinkle_deep_num = scalar(result.wrinkle_count && result.wrinkle_count[def[3]]);
var fine = result.fine_line || result.fine_line_count;
if (values.wrinkle_shallow_num == null) values.wrinkle_shallow_num = scalar(fine && fine[def[3]]);
return { key: def[0], name: def[1], metrics: values };
});
}
function fill(template, vars) {
return template.replace(/\{([a-z_]+)\}/g, function(_, key) {
var v = vars[key];
return v == null ? '未获取' : numeric(v) ? String(Math.round(v * 100) / 100) : String(v);
}).replace(/\s+/g, ' ').trim();
}
function count(result, key) { var v = result[key]; return v && numeric(v.count) ? v.count : null; }
function build(vm, result) {
result = result || {};
var views = defs.map(function(d) {
var cat = vm.cats[d[3]], vars = Object.assign({}, cat.vars || {});
var level, valid = numeric(cat.P);
var viewP = cat.P;
if (d[0] === 'color') {
viewP = numeric(vars.color_score) ? vars.color_score : vars.tone_scoring && vars.tone_scoring.color_score;
valid = vars.melanin != null && numeric(viewP);
}
if (d[0] === 'water') {
// Water copy/advice is independent of pigment/dark circles. Reuse
// the existing water cross-check; never change dimension scoring.
var waterScore = vars.water_score;
var waterValid = numeric(waterScore) && waterScore >= 0 && waterScore <= 100;
viewP = waterValid ? waterScore : null;
if (waterValid && numeric(vars.water_sev) && vars.water_sev >= 60 && vars.water_sev <= 100 && waterScore >= 80) viewP = 100 - vars.water_sev;
valid = waterValid;
}
var viewS = valid ? scoreOfP(viewP) : null;
level = levelForScore(viewS);
var template = valid && level ? Copy.conclusion[d[0]][level] : '';
if (d[0] === 'contour') {
// Mention only positively detected eye problems, independent of the
// contour grade. Missing data must not imply absence or presence.
var eyeNotes = [], pouch = vars.eye_pouch, severity = vars.dark_circle_sev;
if (numeric(pouch) && pouch % 1 === 0 && pouch >= 1 && pouch <= 3) eyeNotes.push(pouch === 1 ? '眼周眼袋初显' : '眼袋明显');
if (numeric(severity) && severity % 1 === 0 && severity >= 1 && severity <= 3) eyeNotes.push(['','黑眼圈轻度','黑眼圈中度','黑眼圈重度'][severity]);
template = template.replace(/(?:眼袋明显|眼周眼袋初显|眼周无明显眼袋问题)、黑眼圈轻\/中\/重度(根据分数给出判断);/g, eyeNotes.length ? eyeNotes.join('、') + ';' : '');
if (vars.la == null || vars.ra == null) template = template.replace(/左右下颌线角度(?:分别为左|左) \{la\}°\/ 右 \{ra\}°[;;]/g, '');
}
if (d[0] === 'color') {
// Severity is an independent detection, not inferred from tone level
// or dark_circle_score. Missing/invalid severity must not imply mild.
var colorSeverity = scalar(result.dark_circle_severity);
var colorDescriptions = ['无明显黑眼圈', '仅存在轻度黑眼圈', '存在中度黑眼圈', '可见明显黑眼圈'];
if (numeric(colorSeverity) && colorSeverity % 1 === 0 && colorSeverity >= 0 && colorSeverity <= 3) {
vars.dark_circle_note = colorDescriptions[colorSeverity];
} else {
template = template.replace(/\{dark_circle_note\}[,;]/g, '');
}
}
if (d[0] === 'acne') {
var pustules = vars.acne_counts ? vars.acne_counts.acne_pustule : count(result, 'acne_pustule');
var nodules = vars.acne_counts ? vars.acne_counts.acne_nodule : count(result, 'acne_nodule');
var heavy = [pustules > 0 ? '脓包' : '', nodules > 0 ? '结节' : ''].filter(Boolean);
vars.heavy_note = heavy.length ? ',含' + heavy.join('、') + '等中重度皮损,炎症反应明显' : '';
// Never downgrade the grade independently of the displayed score,
// but never claim absence when the API detected lesions either.
if (vars.acne_cnt > 0) template = '检测到 {acne_cnt} 处皮损({acne_detail}){heavy_note},局部皮损仍需关注。';
if (vars.acne_cnt == null) template = '本次未获取完整皮损计数,请结合检测图片查看。';
else if (vars.acne_cnt === 0) template = '本次未检出明显皮损,请结合检测图片查看。';
if (vars.acne_cnt != null && vars.acne_counts_complete === false) {
template = vars.acne_cnt > 0
? '本次已获取数据中检测到 {acne_cnt} 处皮损({acne_detail}){heavy_note},部分类型计数未获取,请结合检测图片查看。'
: '本次已获取的皮损计数为0,部分类型数据缺失,请结合检测图片查看。';
}
}
// Missing counts are unknown, not zero: remove only the unavailable metric.
if (vars.fl_cnt == null) template = template.replace(/、细纹 \{fl_cnt\} 条/g, '(细纹计数未获取)');
if (vars.wr_cnt == null) template = template.replace(/皱纹 \{wr_cnt\} 条/g, '皱纹计数未获取');
if (vars.water_area == null) template = template.replace(/缺水面积 \{water_area\}%[、,]/g, '缺水面积未获取;');
if (vars.water_sev == null) template = template.replace(/肌肤缺水比较严重 \{water_sev\},/g, '');
if (vars.sens_area == null) template = template.replace(/泛红面积(?:达 | )\{sens_area\}%[,;]/g, '泛红面积未获取;');
if (vars.pore_cnt == null) template = template.replace(/粗大毛孔 \{pore_cnt\} 颗/g, '毛孔计数未获取');
if (vars.blackhead_cnt == null) template = template.replace(/黑头 \{blackhead_cnt\} 颗/g, '黑头计数未获取');
if (d[0] === 'smooth' && valid && level === '优秀') {
// A high aggregate score does not mean every localized pore is absent.
if (vars.pore_cnt == null || vars.blackhead_cnt == null) template = '肌肤质感整体较好,但毛孔或黑头计数未完整获取,请结合检测图查看。';
else if (vars.pore_cnt > 0 || vars.blackhead_cnt > 0) template = '肌肤质感整体较好;本次检测到粗大毛孔 {pore_cnt} 颗、黑头 {blackhead_cnt} 颗,局部仍有可改善之处。';
}
var details = d[0] === 'wrinkles' ? wrinkleDetails(result) : null;
var advice = level ? Copy.care[d[0]][level] : null;
return { key: d[0], name: d[1], dim: d[2], source: d[3], P: viewP, S: viewS, level: level || null,
text: valid ? fill(template, vars) : '本次未检出有效数据,请重新拍摄后查看。',
details: details, valid: valid, advice: valid ? advice : null };
});
var focus = focusOf(vm);
function priority(view) { return focus && view.dim === focus.key ? 0 : 1; }
function order(a, b) { return a.P - b.P || priority(a) - priority(b) || views.indexOf(a) - views.indexOf(b); }
// Select globally weakest three FIRST, then move selected focus members
// to the front. Never insert an unselected stronger focus-category view.
var chosen = views.filter(function(v) { return v.valid && numeric(v.P) && v.advice; }).sort(order).slice(0,3);
chosen.sort(function(a,b) { return priority(a) - priority(b) || order(a,b); });
var care = chosen.map(function(v) { return { key: v.key, name: v.name, dim: v.dim, P: v.P, level: v.level,
priority: priority(v) === 0, text: v.advice.care + v.advice.lifestyle }; });
var product = products.filter(function(p) { return p.match === vm.essence.name; })[0] || null;
return { version: Copy.version + '+feedback-2026-10-12+care-2026-10-15+grade-8-9+two-copy-fixes', gradeRule: gradeRule, copyRule: copyRule, careRule: 'seven-lowest-three-focus-first-v1', dims: dimNames, views: views, care: care, product: product, focus: focus };
}
// Refresh only presentation for old snapshots: scores, selected care order,
// product and original stored objects are never rewritten/recomputed.
function forDisplay(vm, result, saved) {
if (!saved || (saved.gradeRule === gradeRule && saved.copyRule === copyRule)) return saved || build(vm, result);
var current = build(vm, result);
var care = (saved.care || []).map(function(item) {
// Pre-seven-item snapshots contain name/text only, without a stable key.
// Match exact known names; never infer from array position or insert items.
var view = current.views.filter(function(v) { return item.key ? v.key === item.key : v.name === item.name; })[0];
return view && view.advice ? Object.assign({}, item, { key: view.key, level: view.level, text: view.advice.care + view.advice.lifestyle }) : item;
});
return Object.assign({}, saved, { gradeRule: gradeRule, copyRule: copyRule, views: current.views, care: care });
}
return { dimensionScore: dimensionScore, scoreOfP: scoreOfP, levelForScore: levelForScore, internalLevel: internalLevel, gradeRule: gradeRule, copyRule: copyRule, build: build, forDisplay: forDisplay, products: products, levelMap: levelMap, dimNames: dimNames, wrinkleDetails: wrinkleDetails, focusOf: focusOf, customerText: customerText };
});