feat(scoring): use independent tone hydration minima and focus-first care
This commit is contained in:
@@ -134,6 +134,10 @@ node server.js # 默认 8080,可用 PORT=xxx 修改
|
||||
本机浏览器只缓存随机身份凭据,报告保存在服务器;微信需公网 HTTPS、具备网页授权权限的服务号及相应环境变量。
|
||||
详见 [新版实施与验收](../docs/10.09新版H5实施与验收.md) 和 [微信登录接入与公网部署说明](../docs/微信登录接入与公网部署说明.md)。下方旧功能说明以这两份更新文档为准。
|
||||
|
||||
2026-10-15 护理展示更新:`ReportUI.build` 从七项独立小维度分中选最低三项,再将属于当前优先关注大维度的入选项前置;同分按关注优先、文档顺序确定。肤色、水分分析及护理各自独立分档,不再共用肤色水润合成分。新版护理全文以 `reference/AI测肤报告页话术修改.md` 为准;不改变原三维度/综合得分/产品推荐,不批量改写历史presentation。详见 [七项护理选三与关注优先](../docs/七项护理选三与关注优先-2026-10-15.md)。
|
||||
|
||||
随后用户确认取消合成分参与维度计分:透亮感改为独立肤色、水分、平滑度三项P的最低有效值,不再平均肤色和水分。新分数用于总分、优先关注及原有边界推荐判断。后台新记录展示七项独立分数,历史记录不重算;旧tone字段仅为兼容。详见 [新版透亮感公式](../docs/透亮感改为三项独立最低分-2026-10-15.md)。
|
||||
|
||||
## 部署
|
||||
|
||||
需要 Node ≥ 18 运行 `server.js`(负责静态与 API 中转),**不能用纯静态托管**(密钥不能进前端)。
|
||||
|
||||
@@ -97,17 +97,13 @@
|
||||
.report-care-products .prod-card { height:auto; aspect-ratio:945 / 511; }
|
||||
.report-care-products .prod-info { flex-basis:49.100529%; }
|
||||
.report-care-products .prod-scroll { flex-basis:50.899471%; }
|
||||
.report-care-products #product-title { left:8.571429%; top:29.158513%; font-size:13.888889px; line-height:17.340278px; font-weight:700; white-space:nowrap; }
|
||||
.report-care-products .prod-name { left:8.677249%; top:41.095890%; font-size:11.532639px; line-height:14.467593px; font-weight:700; color:#989898; white-space:nowrap; }
|
||||
.report-care-products #product-title { left:8.571429%; top:29.158513%; font-size:14.467593px; line-height:18.062790px; font-weight:700; white-space:nowrap; }
|
||||
.report-care-products .prod-name { left:8.677249%; top:41.095890%; font-size:12.013166px; line-height:15.070408px; font-weight:700; color:#989898; white-space:nowrap; }
|
||||
.report-care-products .prod-badge { left:8.783069%; top:56.751468%; width:10.687831%; }
|
||||
.report-care-products + .report-disclaimer { margin:calc(12.152778 * var(--u)) calc(5.208333 * var(--u)) calc(20 * var(--u)); font-size:7.233681px; line-height:10.127431px; }
|
||||
.dimension-stability-title { font-size:22.106597px; line-height:28.738542px; }
|
||||
.stability-card .dtab-tx { font-size:11.055903px; }
|
||||
.stability-card .ditem-para { font-size:11.574074px; line-height:14.467593px; }
|
||||
@media (max-width:359px) {
|
||||
.report-care-products #product-title { font-size:12px; }
|
||||
.report-care-products .prod-name { font-size:10px; }
|
||||
}
|
||||
|
||||
/* Fourth feedback adds a fast one-second round trip to original PSD pixels.
|
||||
The stage/mask share one contain transform, including short/landscape screens. */
|
||||
|
||||
+6
-5
@@ -418,18 +418,19 @@ 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%;再与水分项各占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 ? '透亮感取独立肤色、水分、平滑度三项最低分,不再使用肤色水润平均分。' : '历史版本与水分项各占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>';
|
||||
|
||||
/* 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 = ({ contour: '轮廓', wrinkles: '皱纹', smooth: '平滑度', tone: '肤色 / 水分', sens: '泛红', acne: '痘痘' })[ck];
|
||||
var scoreOrder = r.sub_scores ? ['contour','wrinkles','color','smooth','water','sens','acne'] : CAT_ORDER;
|
||||
for (var i = 0; i < scoreOrder.length; i++) {
|
||||
var ck = scoreOrder[i], cat = (r.sub_scores || r.cats || {})[ck];
|
||||
var catName = ({ contour: '轮廓', wrinkles: '皱纹', smooth: '平滑度', color:'肤色', water:'水分', 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(r.sub_scores ? '七项独立评分' : '历史六分类评分(肤色 / 水分共用一项)', '<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 = '';
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"优秀": "肌肤质感表现优异,表面细腻平滑,角质层排列整齐,无明显粗大毛孔与黑头。"
|
||||
},
|
||||
"water": {
|
||||
"改善": "水分分 {water_score};缺水面积 {water_area}%、肌肤缺水比较严重 {water_sev},肌肤含水量偏低,水润度不足,表面偏干燥,容易出现干纹。",
|
||||
"改善": "水分分 {water_score};缺水面积 {water_area}%,{water_severity_note}肌肤水润度有待提升,建议加强补水与保湿。",
|
||||
"良好": "水分分 {water_score};缺水面积 {water_area}%、肌肤含水量处于健康水平,整体水润度良好。",
|
||||
"优秀": "水分分 {water_score};角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。"
|
||||
},
|
||||
@@ -56,7 +56,7 @@
|
||||
"wrinkles": {
|
||||
"改善": {
|
||||
"care": "重视纹路管理,日常可使用含有肽类、胶原类成分的淡纹精华,针对眼周、法令纹等动态纹高发区域配合点按手法,遏制静态纹转化,帮肌肤恢复平滑细腻的状态。",
|
||||
"lifestyle": "保证睡眠,注意睡姿,减少面部长期受压;减少夸张表情,避免揉眼、托腮等表情性挤压习惯,减少纹路加深。"
|
||||
"lifestyle": "注意睡姿,减少面部长期受压;减少夸张表情,避免揉眼、托腮等表情性挤压习惯,减少纹路加深。"
|
||||
},
|
||||
"良好": {
|
||||
"care": "加强日常淡纹与肌肤弹润护理,叠加使用含肽类、胶原类成分的淡纹精华,提升肌肤水润度与饱满感,维持肤表平滑。",
|
||||
|
||||
+34
-10
@@ -85,9 +85,30 @@
|
||||
var views = defs.map(function(d) {
|
||||
var cat = vm.cats[d[3]], vars = Object.assign({}, cat.vars || {});
|
||||
var level = levelMap[cat.level], valid = cat.P != null;
|
||||
if (d[0] === 'color' && vars.melanin == null) valid = false;
|
||||
if (d[0] === 'water' && vars.water_score == null) valid = false;
|
||||
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);
|
||||
level = valid ? (viewP < 80 ? '改善' : viewP < 90 ? '良好' : '优秀') : null;
|
||||
}
|
||||
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;
|
||||
level = valid ? (viewP < 80 ? '改善' : viewP < 90 ? '良好' : '优秀') : null;
|
||||
}
|
||||
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;
|
||||
@@ -135,18 +156,21 @@
|
||||
}
|
||||
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: cat.P, level: level || null,
|
||||
return { key: d[0], name: d[1], dim: d[2], source: d[3], P: viewP, level: level || null,
|
||||
text: valid ? fill(template, vars) : '本次未检出有效数据,请重新拍摄后查看。',
|
||||
details: details, valid: valid, advice: valid ? advice : null };
|
||||
});
|
||||
// Preserve selection of the two weakest original categories. Tone expands into
|
||||
// color+water advice, with thresholds inherited from the same original category.
|
||||
var ranked = Object.keys(vm.cats).filter(function(k) { return vm.cats[k].P != null; })
|
||||
.sort(function(a,b) { return vm.cats[a].P - vm.cats[b].P; }).slice(0,2);
|
||||
var chosen = views.filter(function(v) { return ranked.indexOf(v.source) >= 0 && v.advice; });
|
||||
var care = chosen.map(function(v) { return { name: v.name, text: v.advice.care + v.advice.lifestyle }; });
|
||||
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', dims: dimNames, views: views, care: care, product: product, focus: focusOf(vm) };
|
||||
return { version: Copy.version + '+feedback-2026-10-12+care-2026-10-15', careRule: 'seven-lowest-three-focus-first-v1', dims: dimNames, views: views, care: care, product: product, focus: focus };
|
||||
}
|
||||
return { dimensionScore: dimensionScore, build: build, products: products, levelMap: levelMap, dimNames: dimNames, wrinkleDetails: wrinkleDetails, focusOf: focusOf, customerText: customerText };
|
||||
});
|
||||
|
||||
+10
-2
@@ -752,9 +752,16 @@
|
||||
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) };
|
||||
@@ -848,6 +855,7 @@
|
||||
return {
|
||||
total: weightedTotal(dims),
|
||||
totalRule: 'dimensions-433-v1',
|
||||
vitaScoreRule: 'independent-color-water-smooth-min-v1',
|
||||
wrinkleScoreRule: 'api-overall-v1',
|
||||
acneScoreRule: 'three-view-type-max-v1',
|
||||
skinAgeRaw: skinAge,
|
||||
@@ -984,7 +992,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 === 81.5 && vm.dims.stab.P === 85, '维度木桶 min:紧塑力23 / 活力感81.5 / 稳定力85');
|
||||
ok(vm.dims.firm.P === 23 && vm.dims.vita.P === 80 && vm.dims.stab.P === 85, '维度木桶 min:紧塑力23 / 独立肤色短板80 / 稳定力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,
|
||||
@@ -1015,7 +1023,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.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.stab.P === Math.min(vm2.cats.sens.P, vm2.cats.acne.P), '三维度 P = min(旗下两分类)');
|
||||
ok(vm2.group === '25+组', '实际年龄40不影响推荐,原始肌龄20仍为25+组');
|
||||
|
||||
|
||||
@@ -281,6 +281,11 @@ function fillSuccess(rec, fpJson, persist = true) {
|
||||
rec.skin_age_display = vm.skinAgeDisplay;
|
||||
rec.recommendation = vm.recommendation;
|
||||
rec.presentation = ReportUI.build(vm, fpJson.result);
|
||||
rec.vita_score_rule = vm.vitaScoreRule;
|
||||
rec.sub_scores = Object.fromEntries(rec.presentation.views.map(view => [view.key, {
|
||||
P: view.P, S: view.P == null ? null : Math.round((Math.max(7, Math.min(10, 7 + 3 * view.P / 100)) + 1e-9) * 100) / 100,
|
||||
level: view.level === '改善' ? '预警' : view.level === '良好' ? '改善' : view.level === '优秀' ? '良好' : null
|
||||
}]));
|
||||
rec.total_score = vm.total;
|
||||
rec.total_score_rule = vm.totalRule;
|
||||
rec.wrinkle_score_rule = vm.wrinkleScoreRule;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
const assert=require('node:assert/strict'),SS=require('../js/skin-score'),UI=require('../js/report-ui'),Copy=require('../js/report-copy');
|
||||
const keys=['contour','wrinkles','color','smooth','water','sens','acne'];
|
||||
function sample(scores,focus='firm'){
|
||||
const vm=SS.buildViewModel(structuredClone(SS.MOCK_RESULT),SS.MOCK_SIDE,{});
|
||||
for(const k of ['contour','wrinkles','smooth','sens','acne']){vm.cats[k].P=scores[k];vm.cats[k].level=scores[k]==null?null:scores[k]<80?'预警':scores[k]<90?'改善':'良好';}
|
||||
Object.assign(vm.cats.tone.vars,{melanin:scores.color,color_score:scores.color,water_score:scores.water,water_sev:0});
|
||||
vm.cats.tone.vars.tone_scoring.color_score=scores.color;
|
||||
vm.cats.acne.vars.acne_counts={closed_comedones:0,acne:0,acne_pustule:0,acne_nodule:0,acne_mark:0};
|
||||
vm.cats.acne.vars.acne_cnt=0;
|
||||
for(const dim of ['firm','vita','stab'])vm.dims[dim].P=focus==null?null:dim===focus?10:99;
|
||||
const before=JSON.stringify(vm),ui=UI.build(vm,SS.MOCK_RESULT);assert.equal(JSON.stringify(vm),before);
|
||||
return{vm,ui};
|
||||
}
|
||||
function check(values,focus,expected){const scores=Object.fromEntries(keys.map((k,i)=>[k,values[i]])),x=sample(scores,focus);assert.deepEqual(x.ui.care.map(v=>v.key),expected);assert(x.ui.care.length<=3);for(const c of x.ui.care){assert.equal(c.text,Copy.care[c.key][c.level].care+Copy.care[c.key][c.level].lifestyle);assert.equal(c.priority,c.dim===focus);}return x;}
|
||||
check([20,10,30,90,95,92,99],'firm',['wrinkles','contour','color']);
|
||||
check([30,95,10,90,20,99,98],'firm',['contour','color','water']);
|
||||
check([90,30,10,20,95,96,99],'firm',['wrinkles','color','smooth']);
|
||||
check([99,98,40,30,20,97,96],'vita',['water','smooth','color']);
|
||||
check([95,96,10,20,30,90,99],'firm',['color','smooth','water']);
|
||||
check([80,80,80,80,80,80,80],'stab',['sens','acne','contour']);
|
||||
check([80,80,80,80,80,80,80],null,['contour','wrinkles','color']);
|
||||
check([79.999,79.998,79.997,99,99,99,99],'firm',['wrinkles','contour','color']);
|
||||
check([null,null,50,null,60,null,null],'firm',['color','water']);
|
||||
check([null,null,null,null,null,null,null],null,[]);
|
||||
const x=check([99,98,79.99,99,90,99,99],'vita',['color','water','wrinkles']);
|
||||
assert.equal(x.ui.views.find(v=>v.key==='color').level,'改善');assert.equal(x.ui.views.find(v=>v.key==='water').level,'优秀');
|
||||
assert.equal(x.ui.careRule,'seven-lowest-three-focus-first-v1');
|
||||
const heavy=sample(Object.fromEntries(keys.map(k=>[k,95])),'stab');heavy.vm.cats.acne.vars.acne_counts.acne_nodule=1;heavy.vm.cats.acne.vars.acne_cnt=1;
|
||||
assert.equal(UI.build(heavy.vm,{}).views.find(v=>v.key==='acne').level,'改善');
|
||||
assert(!Copy.care.wrinkles['改善'].lifestyle.startsWith('保证睡眠'));
|
||||
console.log('PASS care: global lowest3, focus-first selected only, two/one/no focus members, tie/document order, raw precision, missing, independent color/water, acne guard, exact copy and immutable scores');
|
||||
@@ -19,6 +19,7 @@ for(const [P,level] of [[79.99,'改善'],[80,'良好'],[89.99,'良好'],[90,'优
|
||||
else result.dark_circle_severity={value:severity};
|
||||
const vm=SS.buildViewModel(result,SS.MOCK_SIDE,{});
|
||||
vm.cats.tone.P=P;vm.cats.tone.level=P<80?'预警':P<90?'改善':'良好';
|
||||
vm.cats.tone.vars.color_score=P; // Test independent color thresholds.
|
||||
const before=JSON.stringify({vm,result});
|
||||
const presentation=UI.build(vm,result),view=presentation.views.find(v=>v.key==='color');
|
||||
const valid=Number.isInteger(severity)&&severity>=0&&severity<=3;
|
||||
|
||||
@@ -114,6 +114,8 @@ function main() {
|
||||
tr.cats.acne = { P: 99, S: 9.97, tier: '良好档', copy: '检出1处皮损(闭口粉刺1处)。建议维持清洁与控油节奏,注意作息与饮食。' };
|
||||
tr.avgS = 9.53;
|
||||
tr.summary = tr.summary.replace('六分类S均分9.54', '六分类S均分9.53');
|
||||
// Authorized independent minimum: water90 no longer averaged up to91.5.
|
||||
tr.dims.vita.P = 90; tr.dims.vita.S = 9.7;
|
||||
}
|
||||
var vm = SkinScore.buildViewModel(fixture.result, fixture.side, tr.profile);
|
||||
|
||||
|
||||
@@ -44,6 +44,9 @@ try{
|
||||
assert(Math.abs(geo.heading.w-595/1080*width)<.1,'Figma text group width scales with preview');
|
||||
const stabilityFont=await p.locator('.dimension-stability-title').evaluate(e=>parseFloat(getComputedStyle(e).fontSize));
|
||||
assert(Math.abs(geo.headingFont-stabilityFont)<.01,'bottom title matches stability heading font size at every width');
|
||||
const productFonts=await p.evaluate(()=>['#product-title','#product-name'].map(s=>parseFloat(getComputedStyle(document.querySelector(s)).fontSize)));
|
||||
assert(Math.abs(productFonts[0]-14.467593)<.01,'restore original product title size, including small screens');
|
||||
assert(Math.abs(productFonts[1]-12.013166)<.01,'gray subtitle enlarged proportionally');
|
||||
assert(Math.abs((geo.heading.x+geo.heading.w/2)-(geo.care.x+geo.care.w/2)-width/1080)<.1,'whole title group centered, retaining 1px design offset');
|
||||
assert(geo.product.bottom<=geo.care.bottom);assert(geo.disclaimer.y>=geo.care.bottom);
|
||||
assert(geo.title.right<=geo.info.right+1&&geo.name.right<=geo.info.right+1,'text must not overlap image');
|
||||
|
||||
@@ -70,6 +70,9 @@ try {
|
||||
assert.equal(adminRecord.total_score_rule, 'dimensions-433-v1');
|
||||
assert.deepEqual(adminRecord.acne_scoring,result.vm.cats.acne.vars.acne_scoring);
|
||||
assert.deepEqual(adminRecord.tone_scoring,result.vm.cats.tone.vars.tone_scoring);
|
||||
assert.equal(adminRecord.vita_score_rule,'independent-color-water-smooth-min-v1');
|
||||
assert.equal(Object.keys(adminRecord.sub_scores).length,7);
|
||||
assert.equal(adminRecord.dims.vita.P,Math.min(...['color','water','smooth'].map(k=>adminRecord.sub_scores[k].P).filter(v=>v!=null)));
|
||||
if(process.env.H5_ACNE_MAX_TEST==='1') {
|
||||
assert.deepEqual(result.vm.cats.acne.vars.acne_counts,{closed_comedones:5,acne:1,acne_pustule:0,acne_nodule:0,acne_mark:4});
|
||||
assert.equal(adminRecord.acne_scoring.deduction,13);assert.equal(adminRecord.cats.acne.P,87);
|
||||
|
||||
@@ -10,7 +10,7 @@ function run(dc,options={}){
|
||||
return{vm,r,v:vm.cats.tone.vars};
|
||||
}
|
||||
const a=run(50);near(a.v.color_score,75*.8+50*.2);near(a.vm.cats.tone.P,75);
|
||||
assert.equal(a.vm.cats.tone.S,9.25);assert.equal(a.vm.dims.vita.S,9.25);
|
||||
assert.equal(a.vm.cats.tone.S,9.25);assert.equal(a.vm.dims.vita.S,9.1);
|
||||
assert(!UI.build(a.vm,a.r).views.find(v=>v.key==='color').text.includes('占肤色项20%'));
|
||||
for(const dc of [0,100]){const x=run(dc);near(x.v.color_score,75*.8+dc*.2);near(x.vm.cats.tone.P,(x.v.color_score+80)/2);}
|
||||
for(const dc of [null,undefined,-1,101,NaN,Infinity]){const x=run(dc);assert.equal(x.v.dark_circle_score,null);near(x.v.color_score,75);near(x.vm.cats.tone.P,77.5);}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
const assert=require('node:assert/strict'),S=require('../js/skin-score');
|
||||
function run(melanin,water,dark){const r=structuredClone(S.MOCK_RESULT);Object.assign(r.score_info,{melanin_score:melanin,water_score:water,dark_circle_score:dark});r.melasma={value:0};r.freckle={value:0};r.water={water_severity:3};return S.buildViewModel(r,S.MOCK_SIDE,{});}
|
||||
for(const [m,w,d]of[[66,97,30],[99,20,99],[99,99,99],[null,97,null],[null,null,null]]){
|
||||
const v=run(m,w,d),vars=v.cats.tone.vars,values=[v.cats.smooth.P,vars.color_score,vars.tone_scoring&&vars.tone_scoring.water_adjusted_score].filter(x=>typeof x==='number'&&Number.isFinite(x));
|
||||
assert.equal(v.dims.vita.P,Math.min(...values));assert.equal(v.vitaScoreRule,'independent-color-water-smooth-min-v1');assert.equal(v.total,S.weightedTotal(v.dims));
|
||||
}
|
||||
assert(Math.abs(run(66,97,30).dims.vita.P-58.8)<1e-9);
|
||||
console.log('PASS independent vita min: color/water/smooth bottleneck, missing inputs, version and weighted total');
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
const assert=require('node:assert/strict'),SS=require('../js/skin-score'),UI=require('../js/report-ui'),Copy=require('../js/report-copy');
|
||||
function run(score,severity=3,pigment=66,dc=30){
|
||||
const r=structuredClone(SS.MOCK_RESULT);Object.assign(r.score_info,{water_score:score,melanin_score:pigment,dark_circle_score:dc});
|
||||
r.melasma={value:0};r.freckle={value:0};r.water={water_severity:severity,water_area:.03};
|
||||
const vm=SS.buildViewModel(r,SS.MOCK_SIDE,{}),before=JSON.stringify({r,vm});
|
||||
const presentation=UI.build(vm,r);assert.equal(JSON.stringify({r,vm}),before);
|
||||
return{r,vm,presentation,water:presentation.views.find(v=>v.key==='water')};
|
||||
}
|
||||
const example=run(97);assert.equal(example.vm.cats.tone.P,77.9);assert.equal(example.water.P,97);assert.equal(example.water.level,'优秀');
|
||||
assert(!/含水量偏低|缺水比较严重|水润度不足/.test(example.water.text));assert(example.water.text.includes('97'));
|
||||
assert.deepEqual(example.water.advice,Copy.care.water['优秀']);
|
||||
assert(!example.presentation.care.some(v=>v.name==='水分'),'healthy water97 should not displace a weaker item in global bottom three');
|
||||
for(const[p,level]of[[0,'改善'],[79.99,'改善'],[80,'良好'],[89.99,'良好'],[90,'优秀'],[100,'优秀']]){
|
||||
for(const pigment of[0,50,100]){const x=run(p,3,pigment,0);assert.equal(x.water.level,level);assert.equal(x.water.P,p);assert.deepEqual(x.water.advice,Copy.care.water[level]);}
|
||||
}
|
||||
const crossed=run(97,70);assert.equal(crossed.water.P,30);assert.equal(crossed.water.level,'改善');assert(crossed.water.text.includes('分数与缺水指标不一致'));assert.equal(crossed.vm.cats.tone.vars.tone_scoring.water_adjusted_score,30);
|
||||
assert(run(50,70).water.text.includes('缺水程度较高'));
|
||||
assert(!run(50,3).water.text.includes('程度较高'));
|
||||
assert(!run(50,null).water.text.includes('严重度'));
|
||||
for(const score of[null,undefined,-1,101,NaN,Infinity]){const x=run(score);assert.equal(x.water.valid,false);assert.equal(x.water.advice,null);assert(!x.presentation.care.some(v=>v.name==='水分'));}
|
||||
console.log('PASS water: 97 vs combined77.9, independent water copy/advice, 80/90 boundaries, true severity, existing cross-check, missing/invalid, scoring immutable');
|
||||
Reference in New Issue
Block a user