feat(h5): align October UI and add WeChat auth with saved reports

This commit is contained in:
xsl
2026-10-11 17:28:11 +08:00
parent ee744b6b5f
commit ce8d0ff1f7
126 changed files with 18711 additions and 1898 deletions
+31
View File
@@ -0,0 +1,31 @@
/* Browser identity is a random credential, never a fingerprint or a report. */
window.AccountClient = (function () {
'use strict';
var key = 'juzi.identity.v1', user = null;
function read() { try { return localStorage.getItem(key); } catch (_) { return null; } }
function request(url, data) {
return fetch(url, { credentials: 'same-origin', method: data ? 'POST' : 'GET',
headers: data ? { 'Content-Type': 'application/json' } : {},
body: data ? JSON.stringify(data) : undefined }).then(function (r) {
return r.json().then(function (v) { if (!r.ok || !v.ok) throw new Error(v.message || '请求失败'); return v; });
});
}
function browser(consent) {
return request('api/auth/browser', { consent: consent, token: read() }).then(function (v) {
try { localStorage.setItem(key, v.token); } catch (_) { /* session cookie still works */ }
user = v.user; return v;
});
}
function init() {
return request('api/auth/session').then(function (v) {
user = v.user;
if (!user && read()) return browser(true).catch(function (e) {
// Do not replace an existing credential on a transient server/network error.
if (/身份已失效/.test(e.message)) { try { localStorage.removeItem(key); } catch (_) {} }
throw e;
});
return v;
});
}
return { request: request, init: init, browser: browser, user: function () { return user; } };
})();
+135 -126
View File
@@ -4,6 +4,22 @@
var $ = function (sel, root) { return (root || document).querySelector(sel); };
// The PSD geometry uses the same 375 × 812 coordinate space as the capture mask.
(window.CameraGeometry || []).forEach(function (geometry, index) {
document.querySelectorAll('#cam-mask path[data-shape="' + index + '"]').forEach(function (path) {
path.setAttribute('d', geometry.outline);
});
var group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
group.setAttribute('data-shape', index);
group.setAttribute('class', 'cam-feature-lines');
geometry.guides.forEach(function (d) {
var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', d);
group.appendChild(path);
});
$('#cam-mask').appendChild(group);
});
/* ─── Toast ─── */
var toastEl = $('#toast'), toastTimer = null;
function toast(msg) {
@@ -15,12 +31,15 @@
}
/* ─── 屏幕管理:页面内不放返回按钮,前进时 pushState,微信/浏览器返回键经 popstate 回到上一屏 ─── */
var currentScreen = 's-splash';
var currentScreen = 's-login';
function showScreen(id, nav) { // nav: 'push' 新增历史 | 'replace' 替换当前历史 | 缺省不动历史
if (id !== 's-login' && !AccountClient.user() && !(/[?&]mock=1/.test(location.search))) id = 's-login';
document.querySelectorAll('.screen').forEach(function (s) {
s.classList.toggle('active', s.id === id);
});
var sc = document.getElementById(id);
window.scrollTo(0, 0);
document.body.scrollTop = 0;
sc.scrollTop = 0;
currentScreen = id;
if (id === 's-report') resetReport(); // 每次进入报告页都还原初始状态
@@ -33,7 +52,7 @@
window.addEventListener('popstate', function (e) {
if (Date.now() < popSilentUntil) { popSilentUntil = 0; return; }
var to = (e.state && e.state.screen) || 's-splash';
['p-privacy', 'p-photoguide', 'p-policy', 'p-zoom', 'p-history'].forEach(closeOverlay);
['p-privacy', 'p-photoguide', 'p-policy', 'p-zoom'].forEach(closeOverlay);
if (currentScreen === 's-cam') leaveCamera();
if (to === 's-cam') { to = 's-profile'; history.replaceState({ screen: to }, ''); } // 「前进」回拍照页不自动开相机
if (to !== currentScreen) showScreen(to);
@@ -92,11 +111,11 @@
/* ─── 肤质轮播 ─── */
var SKINS = [
{ img: 'assets/img/skin-0-dry.png', name: '干性肌肤', desc: '全脸干燥,容易紧绷、缺乏光泽' },
{ img: 'assets/img/skin-1-mixeddry.png', name: '混合偏干肌肤', desc: '两颊偏干,T区油脂较少或偶尔出油' },
{ img: 'assets/img/skin-2-neutral.png', name: '中性肌肤', desc: '水油平衡,肤感舒适,无明显干燥出油' },
{ img: 'assets/img/skin-3-oily.png', name: '油性肌肤', desc: '全脸易出油,容易泛油光、毛孔明显' },
{ img: 'assets/img/skin-4-mixedoily.png', name: '混合偏油肌肤', desc: 'T区易出油,两颊相对清爽或轻微干燥' }
{ img: 'assets/img/ui-20261009/skin-0-dry.png', name: '干性肌肤', desc: '全脸干燥,容易紧绷、<br>缺乏光泽' },
{ img: 'assets/img/ui-20261009/skin-1-mixeddry.png', name: '混合偏干肌肤', desc: '两颊偏干,T区油脂较少或偶尔出油' },
{ img: 'assets/img/ui-20261009/skin-2-neutral.png', name: '中性肌肤', desc: '水油平衡,肤感舒适,无明显干燥出油' },
{ img: 'assets/img/ui-20261009/skin-3-oily.png', name: '油性肌肤', desc: '全脸易出油,容易泛油光、毛孔明显' },
{ img: 'assets/img/ui-20261009/skin-4-mixedoily.png', name: '混合偏油肌肤', desc: 'T区易出油,两颊相对清爽或轻微干燥' }
];
var skinIndex = 0;
var track = $('#skin-track'), dotsBox = $('#skin-dots');
@@ -711,7 +730,7 @@
// 左脸出镜=头向自己的右侧转(target 'left',产物作 left_image)。
// mask 为 index.html 取景轮廓编号(1=左脸出镜 耳在画面左,2=右脸出镜 耳在画面右,均按镜像预览绘制)
var CAM_STEPS = [
{ step: '1/3<br>请正对屏幕', guides: ['正脸居中', '取下眼镜', '光线充足'], target: 'front', mask: 0,
{ step: '1/3<br>请正脸出镜', guides: ['正脸居中', '取下眼镜', '光线充足'], target: 'front', mask: 0,
done: '正脸拍摄完成' },
{ step: '2/3<br>请右脸出镜', guides: ['右脸居中', '取下眼镜', '光线充足'], target: 'right', mask: 2,
done: '右脸拍摄完成', preview: '下一张:右脸出镜<br>请向左转头' },
@@ -826,7 +845,7 @@
}
var vm = null;
try {
vm = SkinScore.buildViewModel(payload.result, payload.side, {
vm = payload.vm || SkinScore.buildViewModel(payload.result, payload.side, {
realAge: profile.age,
skinType: profile.skinType
});
@@ -838,7 +857,8 @@
return;
}
renderReport(vm, photos[0], photos[1], payload); // img1=正脸实拍,payload 供检测可视化取 face_maps/marks
storeReport(payload, photos, profile); // 以检测为单位存三张照片+分析结果(本地历史)
if (IS_MOCK) toast('演示报告,不保存至我的报告');
else if (!payload.report_saved) toast('报告生成成功,但保存失败,请勿关闭页面并联系管理员');
photos = []; // 报告已渲染(图片 src 已挂):释放三连拍 dataURL 内存
showScreen('s-report', 'replace'); // 报告顶替拍照页:返回键回档案页,不再回到相机
}, wait);
@@ -879,7 +899,7 @@
}
var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
var timer = setTimeout(function () { if (ctrl) ctrl.abort(); }, ANALYZE_TIMEOUT_MS);
fetch('/api/skin-analyze', {
fetch('api/skin-analyze', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -897,7 +917,7 @@
clearTimeout(timer);
if (seq !== camSeq) return; // 流程已被重置,丢弃过期响应
if (!data.ok) done(data.message || '检测失败,请重试');
else done(null, { result: data.result, side: data.side });
else done(null, data);
})
.catch(function (err) {
clearTimeout(timer);
@@ -952,7 +972,8 @@
{ key: 'mole', color: '#282828' }, // 痣
{ key: 'acne', color: '#D30C0C' } // 痤疮
] },
tone: { img: '#viz-tone', blendMap: 'water_area' }, // 肤色水润:正脸×水分图正片叠底(water_area,接口返回)
water: { img: '#viz-water', blendMap: 'water_area' },
tone: { img: '#viz-tone', blendMap: 'brown_area' }, // 肤色水润:正脸×水分图正片叠底(water_area,接口返回)
sens: { img: '#viz-sens', blendMap: 'red_area' }, // 敏感:正脸×红区图正片叠底(直挂白底过淡不可读)
acne: { img: '#viz-acne', photo: true, marks: [ // 痘痘:正脸+闭口粉刺/痤疮标记
{ key: 'closed_comedones', color: '#E8A13D' }, // 闭口粉刺
@@ -1029,10 +1050,13 @@
}
// canvas 合成:照片绘制到自然尺寸 → (可选压暗)→ (可选盖透明增强图层)→ 画坐标标记 → dataURL 挂到卡片 <img>
var vizEpoch = 0;
function vizCompose(photoURL, overlayURL, marks, imgEl, opts) {
var epoch = vizEpoch;
vizLoadImage(photoURL, function (photo) {
if (!photo) return; // 照片不可用:保留占位
var finish = function (overlay) {
if (epoch !== vizEpoch) return;
var w = photo.naturalWidth, h = photo.naturalHeight;
var cv = document.createElement('canvas');
cv.width = w; cv.height = h;
@@ -1080,6 +1104,8 @@
// 报告页 6 张检测图统一入口:任一输入缺失 → 该卡保留既有占位(不报错)
function renderViz(payload, frontPhoto) {
vizEpoch++;
document.querySelectorAll('.ditem-img').forEach(function (img) { img.hidden = true; img.removeAttribute('src'); });
var result = payload && payload.result;
var maps = (result && result.face_maps) || {};
for (var key in VIZ_CAT) {
@@ -1140,19 +1166,10 @@
['firm', 'vita', 'stab'].forEach(function (k) {
setDim(k, vm.dims[k], k === worstDimKey ? 'worst' : k === bestDimKey ? 'best' : '');
});
// 关注行(设计稿两行:「近期优先关注:」灰 + 维度名深色下划线,下行为灰色口号)
var DIM_SLOGAN = {
'紧塑力': '紧塑提拉力,轮廓上扬不松垮。',
'活力感': '活力透亮感,水润澎弹有光泽。',
'稳定力': '强韧稳态力,锁水保湿抵御刺激。' // 设计稿原文
};
$('#dims-note').innerHTML = vm.focus.name
? '<span class="dims-note-l1">近期优先关注:<b>' + esc(vm.focus.name) + '</b></span><br><span class="dims-note-l2">' + esc(DIM_SLOGAN[vm.focus.name] || '') + '</span>'
: '<span class="dims-note-l1">近期优先关注:<b>---</b></span>';
// 三个 Tab 面板:各所辖两个分类(S + 档位徽章 + 话术正文)
var keys = ['contour', 'wrinkles', 'smooth', 'tone', 'sens', 'acne'];
for (var i = 0; i < keys.length; i++) setCat(keys[i], vm.cats[keys[i]]);
var presentation = payload && payload.presentation || ReportUI.build(vm, payload && payload.result);
$('#dims-note').textContent = '';
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 = v.text; });
// 检测可视化:轮廓=正脸实拍,其余按 VIZ_CAT 取 face_maps 图层或叠加 marks(payload 缺失时各卡保留占位)
renderViz(payload, frontPhoto);
@@ -1170,27 +1187,18 @@
notesEl.style.display = 'none';
}
// 护理建议(V2.3 §8 五段:肤质定制/最差2分类/精华/生活方式);综合分析与生活方式通栏已按需求移除
// 展示对齐 Figma:纯「序号、正文」,不显示段名【tag】、不加粗
var careBox = $('#care-list');
if (careBox) {
careBox.innerHTML = (vm.careAdvice || []).map(function (s, i) {
return '<p class="care-item">' + (i + 1) + '、' + esc(s.text) + '</p>';
}).join('');
$('#care-list').innerHTML = presentation.care.map(function (s, i) {
return '<p class="care-item">' + (i + 1) + '、' + esc(s.text) + '</p>';
}).join('') || '<p class="care-item">本次有效数据不足,暂无个性化护理建议。</p>';
var product = presentation.product;
$('.card-product').hidden = !product;
$('.prod-title').hidden = !product;
if (product) {
$('#product-title').textContent = product.title;
$('#product-name').textContent = product.name + '双仓精华液';
$('#product-image').src = product.image;
$('#product-image').alt = product.name + '双仓精华液';
}
// 精华推荐卡
// 精华推荐卡只放产品名 + 命中判据:推荐正文已是护理建议第 4 段(V2.3 §8.5),卡内不再重复同一句
$('#essence-name').textContent = vm.essence.name;
// 命中判据:引擎内部名 → 用户可读措辞(引擎值被真值回归逐字锁定,仅展示层映射)
var TRIGGER_LABEL = {
'均分档': vm.avgS != null ? '六项平均分 ' + vm.avgS : '六项平均分',
'最差项约束': vm.worst ? vm.worst.name + '得分偏低' : '单项得分偏低',
'年龄组档': vm.group ? '年龄段 ' + vm.group.replace('组', '') : '年龄段'
};
$('#essence-triggers').textContent = vm.essence.triggers.length
? '推荐依据:' + vm.essence.triggers.map(function (t) { return TRIGGER_LABEL[t] || t; }).join('、')
: '';
}
/* ─── 相机可用后加载检测模型(加载完成前快门即手动模式) ─── */
@@ -1252,7 +1260,10 @@
/* ─── 动作分发 ─── */
var actions = {
'open-privacy': function () { openOverlay('p-privacy'); },
'open-policy': function () { openOverlay('p-policy'); },
'login-browser': function () { loginBrowser(); },
'login-wechat': function () { loginWechat(); },
'open-privacy': function () { if (!AccountClient.user()) { showScreen('s-login', 'replace'); return; } openOverlay('p-privacy'); },
'close-privacy': function () { closeOverlay('p-privacy'); },
'close-policy': function () { closeOverlay('p-policy'); },
'agree-continue': function () {
@@ -1291,13 +1302,7 @@
startCamera(); // 复用启停与预热(camSeqCam 防竞态)
},
'history': function () { openHistory(); },
'close-history': function () { closeOverlay('p-history'); },
'clear-history': function () {
saveReports([]);
renderHistoryList();
toast('历史报告已清空');
},
'more': function () { toast('了解更多功能开发中'); }
'history-retry': function () { renderHistoryList(); }
};
document.addEventListener('click', function (e) {
@@ -1307,84 +1312,78 @@
if (fn) fn();
});
/* ─── 报告历史(localStorage):以检测为单位,存三张照片+分析结果(数据/坐标/文案),
不含 face_maps 整幅图层(体积原因);上限 5 条,超配额逐条淘汰 ─── */
var REPORTS_KEY = 'juzi.reports.v1', REPORTS_MAX = 5;
function loadReports() {
try { var v = JSON.parse(localStorage.getItem(REPORTS_KEY)); return Array.isArray(v) ? v : []; }
catch (e) { return []; }
}
function saveReports(list) {
try { localStorage.setItem(REPORTS_KEY, JSON.stringify(list)); return true; }
catch (e) { return false; } // QuotaExceeded:交给调用方淘汰重试
}
function storeReport(payload, ph, profile) {
if (!payload || !payload.result) return;
var result = Object.assign({}, payload.result);
delete result.face_maps; // 历史不存图层;可视化中图层类(皱纹/肤色/敏感)回看时降级为占位,坐标叠加类(光滑/痘痘)仍可用
var entry = {
id: 'r' + Date.now(), ts: Date.now(), profile: profile,
total: payload.result.score_info && payload.result.score_info.total_score,
skinAge: payload.result.skin_age && payload.result.skin_age.value,
photos: [ph[0] || null, ph[1] || null, ph[2] || null],
payload: { result: result, side: payload.side || null, face_rectangle: payload.face_rectangle || null }
};
var list = loadReports();
list.unshift(entry);
while (list.length > REPORTS_MAX) list.pop();
while (!saveReports(list) && list.length > 1) list.pop(); // 淘汰至可写
saveReports(list);
}
function fmtTime(ts) {
var d = new Date(ts);
function p(n) { return (n < 10 ? '0' : '') + n; }
return d.getFullYear() + '/' + (d.getMonth() + 1) + '/' + d.getDate() + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
}
/* Reports and complete immutable snapshots live on the authenticated backend. */
var historySeq = 0;
function renderHistoryList() {
var box = $('#history-list'), list = loadReports();
if (!list.length) {
box.innerHTML = '<p class="history-empty">暂无历史报告<br>完成一次肌肤检测后会自动保存在这里</p>';
return;
}
box.innerHTML = list.map(function (e) {
var sub = [e.profile && e.profile.age ? e.profile.age + '岁' : '', e.profile && e.profile.skinType || '']
.filter(Boolean).join(' · ');
var score = e.total != null ? e.total : '--';
var img = e.photos && e.photos[0] ? '<img class="his-thumb" src="' + e.photos[0] + '" alt="">' : '<i class="his-thumb"></i>';
return '<div class="his-item" data-id="' + e.id + '">' + img +
'<div class="his-main"><b>' + fmtTime(e.ts) + '</b><span>' + sub + ' · 肌龄' + (e.skinAge != null ? e.skinAge : '--') + '岁</span></div>' +
'<div class="his-score"><b>' + score + '</b><span>分</span></div>' +
'<button class="his-del" data-del="' + e.id + '">删除</button></div>';
}).join('');
var seq = ++historySeq, box = $('#history-list');
box.textContent = '正在读取报告…';
AccountClient.request('api/reports').then(function (data) {
if (seq !== historySeq) return;
$('#history-count').textContent = '您最近已进行过' + data.total + '次面部问题检测';
$('#identity-note').textContent = AccountClient.user().kind === 'wechat'
? '保留最近 5 份报告,同一微信账号可跨手机查看。'
: '保留最近 5 份报告。本机身份仅限当前浏览器;更换手机或清除浏览器数据后无法找回。';
if (!data.items.length) { box.innerHTML = '<p class="history-empty">暂无报告<br>完成一次检测后,报告会自动保存在这里。</p>'; return; }
box.innerHTML = data.items.map(function (item) {
var date = new Date(item.created_at).toLocaleDateString('sv-SE');
return '<article class="card history-item"><p><span>测试时间</span> ' + esc(date) +
'</p><button class="history-detail" data-report-id="' + esc(item.id) + '">查看详情</button></article>';
}).join('');
}).catch(function (e) {
if (seq !== historySeq) return;
$('#history-count').textContent = '暂时无法读取';
box.textContent = e.message + ',请刷新列表重试。';
});
}
function openHistory() {
showScreen('s-history', 'push');
renderHistoryList();
openOverlay('p-history');
}
$('#history-list').addEventListener('click', function (e) {
var del = e.target.closest('[data-del]');
if (del) { // 删除单条(不打开报告)
e.stopPropagation();
saveReports(loadReports().filter(function (it) { return it.id !== del.dataset.del; }));
renderHistoryList();
return;
}
var item = e.target.closest('.his-item');
if (!item) return;
var entry = loadReports().filter(function (it) { return it.id === item.dataset.id; })[0];
if (!entry) return;
var vm = null;
try {
vm = SkinScore.buildViewModel(entry.payload.result, entry.payload.side, {
realAge: entry.profile && entry.profile.age, skinType: entry.profile && entry.profile.skinType
});
} catch (err) { vm = null; }
if (!vm) { toast('该记录已损坏'); return; }
renderReport(vm, entry.photos && entry.photos[0], null, entry.payload);
closeOverlay('p-history');
showScreen('s-report', currentScreen === 's-report' ? undefined : 'push');
$('#history-list').addEventListener('click', function (event) {
var btn = event.target.closest('[data-report-id]');
if (!btn || btn.disabled) return;
btn.disabled = true;
AccountClient.request('api/reports/' + encodeURIComponent(btn.dataset.reportId)).then(function (data) {
if (currentScreen !== 's-history') return;
var entry = data.report;
renderReport(entry.vm, entry.photos[0], entry.photos[1], Object.assign({}, entry.payload, { presentation: entry.presentation }));
showScreen('s-report', 'push');
}).catch(function (e) { toast(e.message); }).finally(function () { btn.disabled = false; });
});
var loginBusy = false;
function loginAllowed() {
if (loginBusy) return false;
if (!$('#login-consent').checked) { toast('请先阅读并同意用户协议和隐私政策'); return false; }
return true;
}
function loginBrowser() {
if (!loginAllowed()) return;
loginBusy = true;
$('#login-message').textContent = '正在登录…';
AccountClient.browser(true).then(function () {
showScreen('s-splash', 'replace');
}).catch(function (e) { $('#login-message').textContent = e.message; })
.finally(function () { loginBusy = false; });
}
function loginWechat() {
if (!loginAllowed()) return;
loginBusy = true;
AccountClient.request('api/auth/config').then(function (data) {
if (!data.wechat) {
$('#login-message').textContent = data.reason + '。报告仍会保存至服务端。';
return;
}
if (!/MicroMessenger/i.test(navigator.userAgent)) {
$('#login-message').textContent = '请在微信内打开本页面,或使用本机浏览器登录。';
return;
}
return AccountClient.request('api/auth/wechat', { consent: true }).then(function (data) {
location.assign(data.url);
});
}).catch(function (e) { $('#login-message').textContent = e.message; })
.finally(function () { loginBusy = false; });
}
/* ─── 页面隐藏/来电生命周期:隐藏即停相机与全部定时器,回前台从当前步恢复(保留 photos[] 与 camStep) ─── */
function handleCamHidden() {
if (currentScreen !== 's-cam') return;
@@ -1423,8 +1422,18 @@
})();
/* ─── 初始化 ─── */
history.replaceState({ screen: 's-splash' }, ''); // 刷新后从启动页开始,历史栈的屏标记与之对齐
history.replaceState({ screen: 's-login' }, ''); // 刷新后从启动页开始,历史栈的屏标记与之对齐
renderCamStep();
if (!(/[?&]mock=1/.test(location.search) && /[?&]screen=report/.test(location.search))) {
loginBusy = true;
AccountClient.init().then(function () {
if (AccountClient.user()) showScreen('s-splash', 'replace');
else $('#login-message').textContent = /[?&]login_error=/.test(location.search)
? '微信登录未完成,可重试或使用本机浏览器登录。'
: '本机登录仅限当前浏览器,不支持换手机找回。';
}).catch(function (e) { $('#login-message').textContent = e.message; })
.finally(function () { loginBusy = false; });
}
// 仅调试用:?mock=1&screen=report 直渲 mock 报告页(供无头浏览器截图验证布局,非正式入口)
// 有 MOCK_FIXTURE 时走 fixture(检测可视化同步出图),否则回退 SkinScore 内置样本
if (IS_MOCK && /[?&]screen=report/.test(location.search) && typeof SkinScore !== 'undefined') {
File diff suppressed because one or more lines are too long
+141
View File
@@ -0,0 +1,141 @@
// Generated from reference/AI测肤报告页话术修改.md; run tools/build_report_copy.py.
(function(root,factory){if(typeof module==="object"&&module.exports)module.exports=factory();else root.ReportCopy=factory();})(typeof window!=="undefined"?window:this,function(){return {
"version": "2026-10-09",
"conclusion": {
"contour": {
"改善": "面部轮廓紧致度有待提升,下颌线清晰度不足,左右下颌线角度分别为左 {la}°/ 右 {ra}°;眼袋明显、黑眼圈轻/中/重度(根据分数给出判断);中面部容积略欠饱满,肌肤支撑力偏弱。",
"良好": "面部轮廓清晰,下颌线较为清晰,左右下颌线角度左 {la}°/ 右 {ra}°;眼周眼袋初显、黑眼圈轻/中/重度(根据分数给出判断);中面部容积无明显流失;肌肤处于健康状态,支撑力表现良好。",
"优秀": "面部轮廓紧致立体,下颌线清晰;眼周无明显眼袋问题、黑眼圈轻/中/重度(根据分数给出判断);中面部容积饱满,肌肤支撑力充足。"
},
"wrinkles": {
"改善": "全脸共检测到皱纹 {wr_cnt} 条、细纹 {fl_cnt} 条,面中、口周、眼周等区域已出现静态纹倾向,纹路已呈现固化趋势,并非临时性干纹。",
"良好": "全脸共检测到皱纹 {wr_cnt} 条、细纹 {fl_cnt} 条,面部整体平整,仅眼周或口周有细微的干纹,无明显静态深纹,纹路面积占比相对较低。",
"优秀": "面中、口周、眼周等各区域均无明显皱纹沟壑,无静态深纹与粗纹,各类纹路面积均处于极低水平,面部整体平整细腻。"
},
"color": {
"改善": "检测到当前肌肤色素分 {melanin},存在色素沉着较显著,整体透亮感不足。",
"良好": "检测到当前肌肤色素分 {melanin},局部有色素沉积;整体状态稳定,局部透亮感稍弱。",
"优秀": "检测到当前肌肤色素分 {melanin},无明显色素沉积;肤色均匀透亮。"
},
"smooth": {
"改善": "当前肌肤平滑度还有提升空间,检测到粗大毛孔 {pore_cnt} 颗、黑头 {blackhead_cnt} 颗,肤感整体细腻度有提升空间,纹理不均。",
"良好": "肌肤整体较为平滑,局部区域细腻度不足;检测到粗大毛孔 {pore_cnt} 颗、黑头 {blackhead_cnt} 颗,有轻微角质粗糙感。",
"优秀": "肌肤质感表现优异,表面细腻平滑,角质层排列整齐,无明显粗大毛孔与黑头。"
},
"water": {
"改善": "缺水面积 {water_area}%、肌肤缺水比较严重 {water_sev},肌肤含水量偏低,水润度不足,表面偏干燥,容易出现干纹。",
"良好": "水分分 {water_score};缺水面积 {water_area}%、肌肤含水量处于健康水平,整体水润度良好。",
"优秀": "水分分 {water_score};角质层含水量充足,肌肤屏障锁水能力强,表面饱满水润,无干燥紧绷表现。"
},
"sens": {
"改善": "泛红面积达 {sens_area}%,肌肤泛红明显,敏感区域可见泛红;对外界刺激耐受度不足,容易受外界因素影响产生状态波动。",
"良好": "泛红面积 {sens_area}%,肌肤存在局部轻微泛红;对外界环境有良好的耐受度,仅在接触强刺激物时偶有轻微反应。",
"优秀": "泛红面积 {sens_area}%,无明显泛红、敏感迹象;对外界刺激耐受度佳,肌肤处于稳定的健康状态。"
},
"acne": {
"改善": "检测到 {acne_cnt} 处皮损({acne_detail}){heavy_note},肌肤状态不稳定,毛孔易堵塞,皮脂分泌波动明显。",
"良好": "检测到 {acne_cnt} 处皮损({acne_detail}),以闭口粉刺、丘疹为主,无明显红肿痘痘;皮脂分泌基本正常,毛孔偶有轻微堵塞。",
"优秀": "未检出明显痘痘、粉刺或闭口,无明显红肿、脓包迹象。"
}
},
"care": {
"contour": {
"改善": {
"care": "重点进行轮廓紧致管理,关注面中、苹果肌与下颌线区域。日常增加紧致精华步骤,配合由下向上的轻柔提升按摩,帮助肌肤维持弹润和支撑感,让轮廓看起来更紧实。",
"lifestyle": "保证充足、规律的睡眠,让肌肤在夜间充分修护;坚持防晒,减少紫外线带来的老化影响;减少熬夜。"
},
"良好": {
"care": "关注面中、苹果肌、下颌线等容易显疲态区域的支撑感。日常可叠加紧致精华步骤,并配合向上提升按摩手法,预防疲态引起的松弛与垮塌累积,稳住面部支撑线。",
"lifestyle": "日常注意防晒,保持规律作息;坚持适度运动,帮助肌肤维持活力。"
},
"优秀": {
"care": "保持现有紧致抗衰护理节奏,日常叠加使用提亮精华,坚持防晒,加强光损伤防护与状态管理,持续关注面中疲态与轮廓支撑状态,让紧致状态更稳定、更持久。",
"lifestyle": "坚持防晒与规律作息,均衡饮食,保证优质蛋白、新鲜蔬果和健康脂肪摄入,适度运动。"
}
},
"wrinkles": {
"改善": {
"care": "重视纹路管理,日常可使用含有肽类、胶原类成分的淡纹精华,针对眼周、法令纹等动态纹高发区域配合点按手法,遏制静态纹转化,帮肌肤恢复平滑细腻的状态。",
"lifestyle": "保证睡眠,注意睡姿,减少面部长期受压;减少夸张表情,避免揉眼、托腮等表情性挤压习惯,减少纹路加深。"
},
"良好": {
"care": "加强日常淡纹与肌肤弹润护理,叠加使用含肽类、胶原类成分的淡纹精华,提升肌肤水润度与饱满感,维持肤表平滑。",
"lifestyle": "减少夸张表情反复牵拉,睡前加强保湿修护。"
},
"优秀": {
"care": "以预防初纹为核心,搭配抗初老型精华,提前建立抗氧化/紧致护理习惯,延缓第一道静态纹出现。",
"lifestyle": "保持良好作息与运动习惯,坚持防晒,减轻光老化与糖化负担,维持肌肤稳定状态。"
}
},
"color": {
"改善": {
"care": "重点关照暗沉与色素易沉积区域,强化透亮与肤色均匀,严格防晒,配合温和焕亮或抗氧化精华帮助均匀肤色与透亮感。",
"lifestyle": "日常加强防晒与抗氧化护理,减少熬夜与压力引发的暗沉,适当增加富含维C的蔬果摄入。"
},
"良好": {
"care": "加强日常光泽管理与局部暗沉护理,做好防晒与抗氧化打底;叠加使用温和焕亮精华,稳住当前肤色状态并提升整体通透感。",
"lifestyle": "持续做好防晒,保持作息规律,避免波动性色沉,守护均匀透亮感。"
},
"优秀": {
"care": "维持现有防晒与抗氧化护肤习惯,叠加使用温和焕亮精华,巩固肤色均匀度与透亮感。",
"lifestyle": "坚持日常防晒,保持均衡饮食支持皮肤稳态。"
}
},
"smooth": {
"改善": {
"care": "关注毛孔可见度与肤表粗糙感改善,加强角质管理,提升肌肤平滑细腻度。",
"lifestyle": "减少高糖高脂饮食,降低皮脂分泌刺激,定期温和调理角质,避免用手挤压黑头。"
},
"良好": {
"care": "加强基础清洁与角质管理,关注鼻翼、下颌等易堵塞部位,提升毛孔通畅度与肌肤细腻度。",
"lifestyle": "温水洁面避免烫洗,保持床品清洁减少局部刺激。"
},
"优秀": {
"care": "维持当前温和护理节奏,维护角质层健康与肌肤屏障稳定状态。",
"lifestyle": "保持规律清洁与保湿习惯,避免频繁更换功效类产品,减少肌肤适应负担。"
}
},
"water": {
"改善": {
"care": "强化屏障锁水修复,注意水油平衡,每周2-3次深度补水,深度改善肌肤缺水状态。",
"lifestyle": "每日足量饮水,补充身体水分,改善干燥环境湿度,避免长时间空调直吹。"
},
"良好": {
"care": "日常需注重水油平衡调理,避免过度清洁损伤皮肤屏障,注意为肌肤持续补水。",
"lifestyle": "每日足量饮水,环境干燥时及时增加肌肤水分补充。"
},
"优秀": {
"care": "持续为皮肤补水与保湿,避免过度清洁与频繁去角质,巩固肌肤水润度与水油平衡。",
"lifestyle": "坚持多喝水与规律作息,季节交替时提前调整保湿方案。"
}
},
"sens": {
"改善": {
"care": "关注屏障修复与舒缓,精简护肤流程,选择温和洁面方式,修复受损角质层。若状态持续无明显好转,可寻求专业护肤建议。",
"lifestyle": "减少外界刺激,如酒精、香精、高浓度酸类精华等成分,避免冷热骤变刺激。"
},
"良好": {
"care": "保持温和护理节奏,注意防晒与环境防护,逐步提升皮肤耐受度。",
"lifestyle": "避免冷热骤变刺激肌肤屏障,运动后及时清洁降温。"
},
"优秀": {
"care": "持续规律护理并按季节阶段性调整,巩固皮肤耐受稳态。",
"lifestyle": "保持健康作息与均衡饮食,维持皮肤屏障稳态。"
}
},
"acne": {
"改善": {
"care": "优先针对性调理痘区肌肤状态,精简护肤流程,避免频繁更换护肤方案与叠加功效成分;严重时及时就医。",
"lifestyle": "减少高糖高脂饮食与熬夜,避免手挤痘痘;保持床品、毛巾清洁,运动后及时清洁,减少汗液与皮脂堆积。"
},
"良好": {
"care": "保持温和清洁与肌肤炎症调理,关注T区、下颌等易出油区域,提升毛孔通畅度,同时注意防晒与保湿。",
"lifestyle": "保持规律作息与清淡饮食,运动后及时清洁,减少汗液与皮脂堆积。"
},
"优秀": {
"care": "坚持定期清洁,维持水油平衡与毛孔通畅,巩固皮肤耐受稳态。",
"lifestyle": "保持健康作息与均衡饮食,维持皮肤屏障健康稳定。"
}
}
}
};});
+72
View File
@@ -0,0 +1,72 @@
/* 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-20261009/product-25.png' },
{ tier: 2, match: '35+支撑精华', title: "1'me 主场精华 II", name: '以我自在有势挺括', image: 'assets/img/ui-20261009/product-35.png' },
{ tier: 3, match: '50+重塑精华', title: "1'me 主场精华 III", name: '以我从容有态构塑', image: 'assets/img/ui-20261009/product-50.png' }
];
var levelMap = { '预警': '改善', '改善': '良好', '良好': '优秀' };
function numeric(value) { return typeof value === 'number' && isFinite(value); }
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 = 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 template = valid && level ? Copy.conclusion[d[0]][level] : '';
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] === 'acne') {
var pustules = count(result, 'acne_pustule'), nodules = count(result, 'acne_nodule');
var heavy = [pustules > 0 ? '脓包' : '', nodules > 0 ? '结节' : ''].filter(Boolean);
vars.heavy_note = heavy.length ? ',含' + heavy.join('、') + '等中重度皮损,炎症反应明显' : '';
if (heavy.length) { level = '改善'; template = Copy.conclusion.acne[level]; }
else if (vars.acne_cnt > 0 && level === '优秀') { level = '良好'; template = Copy.conclusion.acne[level]; }
if (vars.acne_cnt == null) template = '本次未获取完整皮损计数,请结合检测图片查看。';
else if (vars.acne_cnt === 0) template = '本次未检出明显皮损,请结合检测图片查看。';
}
// 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, '黑头计数未获取');
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,
text: valid ? fill(template, vars) : '本次未检出有效数据,请重新拍摄后查看。',
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 product = products.filter(function(p) { return p.match === vm.essence.name; })[0] || null;
return { version: Copy.version, dims: { firm: '紧塑力', vita: '透亮感', stab: '稳韧力' }, views: views, care: care, product: product };
}
return { build: build, products: products, levelMap: levelMap };
});
+1
View File
@@ -668,6 +668,7 @@
res = def.key === 'contour' ? def.calc(r, sideResults, notes) : def.calc(r, notes);
cats[def.key] = {
name: def.name, dim: def.dim,
vars: res.vars, // 原始模板变量供新版展示层使用,不参与评分规则变更
P: res.P, S: sShow(res.P), level: levelOf(res.P),
text: res.P == null
? (res.vars.missing ? def.text(null, res.vars) : '本次检测' + def.name + '分类数据缺失,暂不参与评分。')