fix(report): recommend by measured skin age and align admin results

This commit is contained in:
xsl
2026-10-12 00:23:57 +08:00
parent 7518dc2307
commit 0ec5e5dc32
21 changed files with 3010 additions and 1875 deletions
+56 -19
View File
@@ -24,7 +24,7 @@ function fmtMs(ms) { return ms == null ? '-' : ms + 'ms'; }
function fmtScore(v) { return v == null ? '-' : v; }
var LV_CLASS = { '预警': 'lv-bad', '改善': 'lv-warn', '良好': 'lv-good' };
var DIM_NAMES = { firm: '紧塑力', vita: '活力感', stab: '稳定力' };
var DIM_NAMES = ReportUI.dimNames;
var CAT_ORDER = ['contour', 'wrinkles', 'smooth', 'tone', 'sens', 'acne'];
var ST_CLASS = { '成功': 'st-ok', '失败': 'st-fail', '检测中': 'st-ing', '待检测': 'st-wait' };
@@ -60,15 +60,16 @@ function switchTab(name) {
/* ─── 档位标签 ─── */
function lvHtml(level) {
if (!level) return '<span class="sub">-</span>';
return '<span class="lv ' + (LV_CLASS[level] || '') + '">' + esc(level) + '</span>';
return '<span class="lv ' + (LV_CLASS[level] || '') + '">' + esc(ReportUI.levelMap[level] || level) + '</span>';
}
function dimsHtml(dims) {
if (!dims) return '<span class="sub">-</span>';
var h = '';
for (var k in DIM_NAMES) {
var d = dims[k];
h += '<span class="lv ' + (d && LV_CLASS[d.level] || '') + '" title="' + DIM_NAMES[k] + '">' +
DIM_NAMES[k].slice(0, 1) + (d ? esc(d.level) : '-') + '</span>';
h += '<div><span class="lv ' + (d && LV_CLASS[d.level] || '') + '" title="' + DIM_NAMES[k] + '">' +
DIM_NAMES[k] + ' ' + (d && typeof d.S === 'number' ? d.S.toFixed(1) : '-') + ' ' +
(d ? esc(ReportUI.levelMap[d.level] || '-') : '-') + '</span></div>';
}
return h;
}
@@ -80,6 +81,19 @@ function profileHtml(it) {
parts.push(it.daily_skin_type || '-');
return esc(parts.join(' / '));
}
function ageText(age) { return age == null ? '-' : esc(age) + '岁'; }
function skinAgeHtml(r) {
return '<b>原始 ' + ageText(r.skin_age) + '</b><div class="sub">展示 ' + ageText(r.skin_age_display) + '</div>';
}
function productName(name) {
for (var i = 0; i < ReportUI.products.length; i++) {
var p = ReportUI.products[i];
if (p.match === name) return p.title + '(' + ['25+', '35+', '50+'][i] + ')';
}
return name || '-';
}
function ruleLabel(r) { return r.recommendation ? r.recommendation.version : '旧版规则(历史记录)'; }
function tierText(tier) { return tier ? ['25+ / I', '35+ / II', '50+ / III'][tier - 1] : '无'; }
/* ═══════════ 记录列表 ═══════════ */
var pageState = { page: 1, page_size: 20, total: 0 };
@@ -108,7 +122,7 @@ function loadRecords() {
var tbody = $('#tbl-records tbody');
tbody.innerHTML = '';
if (!data.items.length) {
tbody.innerHTML = '<tr><td colspan="11" style="text-align:center;color:#8a94a6;padding:32px">暂无记录</td></tr>';
tbody.innerHTML = '<tr><td colspan="12" style="text-align:center;color:#8a94a6;padding:32px">暂无记录</td></tr>';
}
for (var i = 0; i < data.items.length; i++) {
(function (it) {
@@ -126,8 +140,9 @@ function loadRecords() {
'<td><span class="st ' + (ST_CLASS[it.detect_status] || '') + '">' + esc(it.detect_status) + '</span>' +
(it.fail_code ? '<div class="sub">' + esc(it.fail_code) + '</div>' : '') + '</td>' +
'<td><b>' + fmtScore(it.total_score) + '</b></td>' +
'<td>' + skinAgeHtml(it) + '</td>' +
'<td>' + dimsHtml(it.dims) + '</td>' +
'<td>' + esc(it.essence || '-') + '</td>' +
'<td>' + esc(productName(it.essence)) + '<div class="sub">' + esc(ruleLabel(it)) + '</div></td>' +
'<td class="mono">' + fmtMs(it.cost_ms) + '</td>' +
'<td class="mono">' + fmtTime(it.created_at) + '</td>';
tr.addEventListener('click', function (e) {
@@ -230,7 +245,7 @@ function loadStats() {
for (var l = 0; l < levels.length; l++) {
var cnt = dist[levels[l][0]] || 0;
bars += '<div style="display:flex;align-items:center;gap:8px;margin:3px 0">' +
'<span style="width:34px;color:#5a6578;font-size:12px">' + levels[l][0] + '</span>' +
'<span style="width:34px;color:#5a6578;font-size:12px">' + ReportUI.levelMap[levels[l][0]] + '</span>' +
'<span style="flex:1;height:14px;background:#f0f2f5;border-radius:3px;overflow:hidden">' +
'<i style="display:block;height:100%;width:' + (cnt * 100 / max) + '%" class="' + levels[l][1] + '"></i></span>' +
'<span style="width:24px;text-align:right;font-size:12px;color:#4a5568">' + cnt + '</span></div>';
@@ -342,7 +357,7 @@ function renderDetail(data) {
/* 档案 */
h += dSec('档案信息', '<div class="d-kv">' +
'<span>年龄 <b>' + (r.real_age != null ? r.real_age + '岁' : '-') + '</b></span>' +
'<span>实际年龄 <b>' + ageText(r.real_age) + '</b></span>' +
'<span>性别 <b>' + esc(r.gender || '-') + '</b></span>' +
'<span>地区 <b>' + esc(r.region || '-') + '</b></span>' +
'<span>日常肤质 <b>' + esc(r.daily_skin_type || '-') + '</b></span>' +
@@ -374,20 +389,22 @@ function renderDetail(data) {
if (r.detect_status === '成功') {
h += dSec('综合', '<div class="d-kv">' +
'<span>总分 <b>' + fmtScore(r.total_score) + '</b></span>' +
'<span>肌肤年龄(接口原始) <b>' + fmtScore(r.skin_age) + '岁</b></span>' +
'<span>原始肌龄(新推荐依据) <b>' + ageText(r.skin_age) + '</b></span>' +
'<span>前端展示肌龄 <b>' + ageText(r.skin_age_display) + '</b></span>' +
'<span>六分类均分S <b>' + fmtScore(r.avg_s) + '</b></span>' +
'<span>左下颌线角 <b>' + (r.jawline_angle_l != null ? r.jawline_angle_l + '°' : '-') + '</b></span>' +
'<span>右下颌线角 <b>' + (r.jawline_angle_r != null ? r.jawline_angle_r + '°' : '-') + '</b></span>' +
'</div>');
h += '<p class="sub">前端展示肌龄沿用实际年龄±3岁的修正;新推荐只使用原始肌龄。历史报告保留当时的推荐。</p>';
/* 6 分类表格 */
var catRows = '';
for (var i = 0; i < CAT_ORDER.length; i++) {
var ck = CAT_ORDER[i], cat = r.cats && r.cats[ck];
var catName = (cat && cat.name) || ({ contour: '轮廓', wrinkles: '皱纹', smooth: '光滑', tone: '肤色水润', sens: '敏感', acne: '痘痘' })[ck];
var catName = ({ contour: '轮廓', wrinkles: '皱纹', smooth: '平滑度', tone: '肤色 / 水分', sens: '泛红', acne: '痘痘' })[ck];
catRows += '<tr><td>' + esc(catName) + '</td><td>' + fmtScore(cat && cat.P) + '</td><td>' + fmtScore(cat && cat.S) + '</td><td>' + lvHtml(cat && cat.level) + '</td></tr>';
}
h += dSec('六分类评分', '<table class="d-table"><thead><tr><th>分类</th><th>P</th><th>S</th><th>档位</th></tr></thead><tbody>' + catRows + '</tbody></table>');
h += dSec('原始六分类评分(肤色 / 水分共用一项)', '<table class="d-table"><thead><tr><th>分类</th><th>P</th><th>S</th><th>档位</th></tr></thead><tbody>' + catRows + '</tbody></table>');
/* 3 维度表格 */
var dimRows = '';
@@ -399,7 +416,13 @@ function renderDetail(data) {
/* 文案全文 */
var copyHtml = '';
if (r.copy_6) {
var presentation = r.presentation;
if (presentation && presentation.views) {
for (var v = 0; v < presentation.views.length; v++) {
var view = presentation.views[v];
copyHtml += '<div class="d-copy"><b>' + esc(view.name) + ' / ' + esc(view.level || '未获取') + ':</b>' + esc(view.text) + '</div>';
}
} else if (r.copy_6) {
for (var c2 = 0; c2 < CAT_ORDER.length; c2++) {
var txt = r.copy_6[CAT_ORDER[c2]];
if (txt) copyHtml += '<div class="d-copy"><b>' + esc((r.cats[CAT_ORDER[c2]] || {}).name || '') + ':</b>' + esc(txt) + '</div>';
@@ -407,19 +430,33 @@ function renderDetail(data) {
}
if (r.copy_summary) copyHtml += '<div class="d-copy"><b>综合文案:</b>' + esc(r.copy_summary) + '</div>';
if (r.lifestyle) copyHtml += '<div class="d-copy"><b>生活方式:</b>' + esc(r.lifestyle) + '</div>';
if (copyHtml) h += dSec('生成文案', copyHtml);
if (copyHtml) h += dSec(presentation ? '前端报告结论(保存快照)与计算摘要' : '旧版生成文案(无前端快照)', copyHtml);
if (r.careAdvice && r.careAdvice.length) {
var care = presentation ? presentation.care : r.careAdvice;
if (care && care.length) {
var advice = '';
for (var a = 0; a < r.careAdvice.length; a++) {
advice += '<div class="d-copy"><b>[' + esc(r.careAdvice[a].tag) + ']</b>' + esc(r.careAdvice[a].text) + '</div>';
for (var a = 0; a < care.length; a++) {
advice += '<div class="d-copy"><b>[' + esc(care[a].name || care[a].tag) + ']</b>' + esc(care[a].text) + '</div>';
}
h += dSec('护理建议', advice);
}
h += dSec('推荐精华', '<div class="d-copy"><b>' + esc((r.essence && r.essence.name) || '-') + '</b><br>' +
esc((r.essence && r.essence.text) || '') + '</div>' +
'<div class="d-kv"><span>触发判据 <b>' + esc(r.trigger || '-') + '</b></span></div>');
var product = presentation && presentation.product;
h += dSec('推荐精华', '<div class="d-copy"><b>' + esc(productName(r.essence && r.essence.name)) + '</b><br>' +
esc(product ? product.name + '双仓精华液' : (r.essence && r.essence.text) || '') + '</div>' +
'<div class="d-kv"><span>规则版本 <b>' + esc(ruleLabel(r)) + '</b></span>' +
'<span>触发判据 <b>' + esc(r.trigger || '-') + '</b></span></div>');
if (r.recommendation) {
var rec = r.recommendation, scoreParts = [];
for (var sk in DIM_NAMES) {
var score = rec.scores[sk];
scoreParts.push(DIM_NAMES[sk] + ':' + (score == null ? '未获取' : String(score)));
}
h += dSec('推荐依据', '<div class="d-kv"><span>使用原始肌龄 <b>' + ageText(rec.skinAge) + '</b></span>' +
'<span>基础档 <b>' + esc(tierText(rec.baseTier)) + '</b></span>' +
'<span>最终档 <b>' + esc(tierText(rec.tier)) + '</b></span></div>' +
'<div class="d-copy">' + esc(rec.reason) + '</div><div class="sub">判定分(未舍入S,不使用前端一位小数):' + esc(scoreParts.join(';')) + '</div>');
}
if (r.notes && r.notes.length) {
var notes = '<ul class="d-notes">';