204 lines
14 KiB
JavaScript
204 lines
14 KiB
JavaScript
/* 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.round((value + 1e-9) * 10) / 10).toFixed(1) : '-';
|
||
}
|
||
var gradeRule = 'display-score-8-9-v1';
|
||
var copyRule = 'wrinkles-excellent-acne-detected-v1';
|
||
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) {
|
||
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('本次未检出有效数据,请重新拍摄后查看。', '本次未获取轮廓有效数据。');
|
||
}
|
||
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] === '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;
|
||
template = template.replace(/黑眼圈轻\/中\/重度(根据分数给出判断)/g, description || '黑眼圈数据未获取');
|
||
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 };
|
||
});
|