feat(report): unify display grades and add contour side photos

This commit is contained in:
xsl
2026-10-15 17:14:05 +08:00
parent 60f0965c5a
commit 2d80f6dfd0
21 changed files with 557 additions and 49 deletions
+46 -7
View File
@@ -1164,7 +1164,7 @@
popHistorySilently();
return;
}
renderReport(vm, photos[0], photos[1], payload); // img1=正脸实拍,payload 供检测可视化取 face_maps/marks
renderReport(vm, photos[0], photos[1], payload, photos); // photos 已统一为正/左/右
if (IS_MOCK) toast('演示报告,不保存至我的报告');
else if (AccountClient.user() && AccountClient.user().kind !== 'wechat') toast('游客报告仅供本次查看,不保存历史记录');
else if (!payload.report_saved) toast('报告生成成功,但保存失败,请勿关闭页面并联系管理员');
@@ -1315,6 +1315,13 @@
if (top + ch < rectangle.top + rectangle.height) top = rectangle.top + rectangle.height - ch;
return { left: left, top: top, width: cw, height: ch, focused: true };
}
function vizShiftContentDown(crop, rectangle, fraction) {
if (!crop.focused || !rectangle || !fraction) return crop;
// Move the source window upward, so its subject moves DOWN inside the
// fixed frame. Keep the full face in bounds and never create a blank edge.
var minimumTop = Math.max(0, rectangle.top + rectangle.height - crop.height);
return Object.assign({}, crop, { top: Math.max(minimumTop, crop.top - crop.height * fraction) });
}
// 从 result 提取某分类的标记集合:polygon 与 rectangle 都保留,缺该标记类型则跳过
function vizCollectMarks(markDefs, result) {
var out = [];
@@ -1389,6 +1396,8 @@
var finish = function (overlays) {
if (epoch !== vizEpoch) return;
var crop = vizFaceCrop(photo.naturalWidth, photo.naturalHeight, opts && opts.faceRectangle);
var originalTop = crop.top;
crop = vizShiftContentDown(crop, opts && opts.faceRectangle, opts && opts.contentDown);
var scale = Math.min(1, 960 / crop.width);
var w = Math.round(crop.width * scale), h = Math.round(crop.height * scale);
var cv = document.createElement('canvas');
@@ -1440,6 +1449,7 @@
try {
imgEl.src = cv.toDataURL('image/jpeg', 0.9); imgEl.hidden = false;
imgEl.dataset.crop = crop.focused ? 'face' : 'full';
imgEl.dataset.contentOffset = String((originalTop - crop.top) / crop.height);
imgEl.dataset.overlayAlpha = String(opts && typeof opts.alpha === 'number' ? opts.alpha : 1);
} catch (e) { /* 合成失败保留占位 */ }
};
@@ -1501,11 +1511,39 @@
// 照片类(轮廓/光滑/痘痘):无照片 → 保留占位;有坐标则 canvas 叠加,无坐标纯照片
if (!frontPhoto) continue;
var marks = cfg.marks ? vizCollectMarks(cfg.marks, result) : [];
vizCompose(frontPhoto, null, marks, imgEl, { faceRectangle: rectangle });
// The visible contour frame is 90% of the source height: 9% of the
// source equals a 10% downward shift relative to that visible frame.
vizCompose(frontPhoto, null, marks, imgEl, { faceRectangle: rectangle, contentDown: key === 'contour' ? .09 : 0 });
}
}
function renderReport(vm, frontPhoto, sidePhoto, payload) {
var contourPhotoEpoch = 0;
function renderContourSides(reportPhotos) {
var epoch = ++contourPhotoEpoch, wrap = $('#contour-sides');
function refreshHeight() {
var card = wrap.closest('.card-detail'), state = detailSwipes.get(card);
if (state) state.height(Number(card.querySelector('.dtab.active').dataset.tab));
}
wrap.hidden = true;
['left', 'right'].forEach(function(view, i) {
var img = $('#contour-' + view), figure = img.closest('figure');
img.onload = img.onerror = null; img.removeAttribute('src'); figure.hidden = true;
var src = reportPhotos && reportPhotos[i + 1];
if (!src) return;
img.draggable = false;
img.onload = function() { if (epoch === contourPhotoEpoch) { figure.hidden = false; wrap.hidden = false; refreshHeight(); } };
img.onerror = function() {
if (epoch !== contourPhotoEpoch) return;
figure.hidden = true; img.removeAttribute('src');
wrap.hidden = !wrap.querySelector('figure:not([hidden])');
refreshHeight();
};
img.src = src;
});
refreshHeight();
}
function renderReport(vm, frontPhoto, sidePhoto, payload, reportPhotos) {
renderContourSides(reportPhotos || [frontPhoto, sidePhoto, null]);
reportOwner = AccountClient.user() && AccountClient.user().id;
// Historical snapshots also use their saved dimensions, never the old API total.
var total = SkinScore.weightedTotal(vm.dims);
@@ -1529,7 +1567,7 @@
['firm', 'vita', 'stab'].forEach(function (k) {
setDim(k, vm.dims[k], k === worstDimKey ? 'worst' : k === bestDimKey ? 'best' : '');
});
var presentation = payload && payload.presentation || ReportUI.build(vm, payload && payload.result);
var presentation = ReportUI.forDisplay(vm, payload && payload.result, payload && payload.presentation);
var focus = ReportUI.focusOf(vm), focusEl = $('#dims-note');
focusEl.hidden = !focus;
focusEl.replaceChildren();
@@ -1654,7 +1692,8 @@
card.addEventListener('click', function (e) {
var btn = e.target.closest('.dtab');
if (btn) { setDetailTab(card, btn.dataset.tab); return; }
var img = e.target.closest('.ditem-img');
var sideButton = e.target.closest('.contour-side-photo');
var img = sideButton ? sideButton.querySelector('img') : e.target.closest('.ditem-img');
if (img && img.src && !img.hidden && img.closest('.dpanel').classList.contains('active')) {
$('#zoom-img').src = img.src;
openOverlay('p-zoom');
@@ -1779,7 +1818,7 @@
AccountClient.request('api/reports/' + encodeURIComponent(btn.dataset.reportId)).then(function (data) {
if (currentScreen !== 's-history' || seq !== historySeq || owner !== (AccountClient.user() && AccountClient.user().id)) return;
var entry = data.report;
renderReport(entry.vm, entry.photos[0], entry.photos[1], Object.assign({}, entry.payload, { presentation: entry.presentation }));
renderReport(entry.vm, entry.photos[0], entry.photos[1], Object.assign({}, entry.payload, { presentation: entry.presentation }), entry.photos);
showScreen('s-report', 'push');
}).catch(function (e) { toast(e.message); }).finally(function () { btn.disabled = false; });
});
@@ -1905,7 +1944,7 @@
renderReport(SkinScore.buildViewModel(mockRes, mockSide, {
realAge: +($('#f-age').value || 0) || 28, // 调试档案缺省 28 岁
skinType: SKINS[skinIndex].name
}), fx ? fx.photos[0] : null, null, fx ? { result: mockRes, side: mockSide, face_rectangle: fx.face_rectangle } : null);
}), fx ? fx.photos[0] : null, null, fx ? { result: mockRes, side: mockSide, face_rectangle: fx.face_rectangle } : null, fx && fx.photos);
} catch (e) { /* 调试入口异常不阻塞正式流程 */ }
}
})();