feat(h5): apply fourth feedback and tighten local capture validation
This commit is contained in:
+158
-89
@@ -76,7 +76,7 @@
|
||||
function renderAccountStatus() {
|
||||
var user = AccountClient.user();
|
||||
document.querySelectorAll('[data-auth-status]').forEach(function (label) {
|
||||
label.textContent = user ? (user.kind === 'wechat' ? '微信已登录' : '本机浏览器已登录') : '尚未登录';
|
||||
label.textContent = user ? (user.kind === 'wechat' ? '微信已登录' : '游客已登录') : '尚未登录';
|
||||
label.parentElement.dataset.kind = user ? user.kind : '';
|
||||
});
|
||||
}
|
||||
@@ -84,7 +84,7 @@
|
||||
showScreen('s-splash', 'replace');
|
||||
$('#login-success-message').textContent = AccountClient.user().kind === 'wechat'
|
||||
? '当前使用:微信登录\n同一微信账号可跨手机查看报告。'
|
||||
: '当前使用:本机浏览器登录\n报告仅能通过当前浏览器身份查看。';
|
||||
: '当前使用:游客登录\n游客报告仅可在当前浏览器查看,不能跨手机同步。';
|
||||
openOverlay('p-login-success');
|
||||
document.querySelectorAll('.screen').forEach(function (screen) { screen.inert = true; });
|
||||
$('[data-action="login-confirm"]').focus();
|
||||
@@ -141,21 +141,69 @@
|
||||
}
|
||||
|
||||
/* ─── 弹窗管理 ─── */
|
||||
var SCAN_CYCLE_MS = 1000, scanCycle = null, scanMask = null;
|
||||
function restartScanCycle() {
|
||||
if (!scanCycle || scanCycle.done) return;
|
||||
var overlay = $('#p-analyzing'), cycle = scanCycle;
|
||||
overlay.classList.remove('scan-running');
|
||||
clearTimeout(cycle.timer);
|
||||
// Restart from the first painted frame, including a late-loaded PSD image.
|
||||
requestAnimationFrame(function () { requestAnimationFrame(function () {
|
||||
if (scanCycle !== cycle || cycle.done) return;
|
||||
overlay.style.setProperty('--scan-cycle', SCAN_CYCLE_MS + 'ms');
|
||||
overlay.classList.add('scan-running');
|
||||
// Legacy browsers without CSS animations still get a bounded visible wait.
|
||||
if (getComputedStyle($('.scan-beam')).animationName === 'none') cycle.timer = setTimeout(cycle.resolve, SCAN_CYCLE_MS);
|
||||
}); });
|
||||
}
|
||||
function startScanCycle() {
|
||||
stopScanCycle();
|
||||
var cycle = { done: false, timer: null };
|
||||
cycle.promise = new Promise(function (resolve) {
|
||||
cycle.resolve = function () { if (!cycle.done) { cycle.done = true; clearTimeout(cycle.timer); resolve(); } };
|
||||
});
|
||||
scanCycle = cycle;
|
||||
restartScanCycle();
|
||||
return cycle.promise;
|
||||
}
|
||||
function stopScanCycle() {
|
||||
if (scanCycle) scanCycle.resolve();
|
||||
scanCycle = null;
|
||||
$('#p-analyzing').classList.remove('scan-running');
|
||||
}
|
||||
$('#p-analyzing').addEventListener('animationiteration', function (event) {
|
||||
if (!scanCycle || scanCycle.done) return;
|
||||
var ready = this.classList.contains('scan-artwork-ready');
|
||||
if ((ready && event.target.classList.contains('scan-beam')) || (!ready && event.target.classList.contains('scan-status'))) scanCycle.resolve();
|
||||
});
|
||||
function prepareScanArtwork() {
|
||||
var image = $('#scan-artwork');
|
||||
function ready() {
|
||||
var wasReady = $('#p-analyzing').classList.contains('scan-artwork-ready');
|
||||
var loaded = image.complete && image.naturalWidth && scanMask && scanMask.complete && scanMask.naturalWidth;
|
||||
$('#p-analyzing').classList.toggle('scan-artwork-ready', !!loaded);
|
||||
if (loaded && !wasReady) restartScanCycle();
|
||||
}
|
||||
if (!scanMask || (scanMask.complete && !scanMask.naturalWidth)) {
|
||||
scanMask = new Image(); scanMask.onload = ready; scanMask.onerror = ready;
|
||||
scanMask.src = $('#scan-face').dataset.mask;
|
||||
$('#scan-face').style.webkitMaskImage = 'url("' + scanMask.src + '")';
|
||||
$('#scan-face').style.maskImage = 'url("' + scanMask.src + '")';
|
||||
}
|
||||
if (!image.getAttribute('src') || (image.complete && !image.naturalWidth)) {
|
||||
image.onload = function () { $('#p-analyzing').classList.add('scan-artwork-ready'); };
|
||||
image.onerror = function () { $('#p-analyzing').classList.remove('scan-artwork-ready'); };
|
||||
image.onload = ready;
|
||||
image.onerror = ready;
|
||||
image.src = image.dataset.src;
|
||||
}
|
||||
ready();
|
||||
}
|
||||
function openOverlay(id) {
|
||||
if (id === 'p-privacy' || id === 'p-photoguide') loadAppFont();
|
||||
if (id === 'p-analyzing') prepareScanArtwork();
|
||||
$('#' + id).classList.add('show');
|
||||
if (id === 'p-analyzing') $('.scan-cancel').focus({ preventScroll: true });
|
||||
if (id === 'p-analyzing') { startScanCycle(); $('.scan-cancel').focus({ preventScroll: true }); }
|
||||
}
|
||||
function closeOverlay(id) { $('#' + id).classList.remove('show'); }
|
||||
function closeOverlay(id) { $('#' + id).classList.remove('show'); if (id === 'p-analyzing') stopScanCycle(); }
|
||||
|
||||
// 点遮罩空白处关闭(与各自关闭按钮同行为:隐私→回启动页、拍照引导→留在档案页)
|
||||
// 只响应遮罩本身(拍照引导另含底图层),不误伤弹窗卡片内交互
|
||||
@@ -329,6 +377,7 @@
|
||||
var camVideo = $('#cam-video'), camFile = $('#cam-file'), albumFile = $('#album-file');
|
||||
var camStream = null, camLive = false, camFallback = false, camSeqCam = 0; // camSeqCam:相机启动令牌,防止迟到的授权回调泄漏摄像头
|
||||
var photos = []; // 三连拍(dataURL),拍摄中与 CAM_STEPS 同序;送检前经 photosByView 重排为 [正脸, 左脸, 右脸]
|
||||
var captureDiagnostics = [], holdState = null;
|
||||
|
||||
// 约束逐级放宽:facingMode 裸值在部分 Android 抛 OverconstrainedError,先 ideal 再退 video:true,最后才降级
|
||||
var camFacing = 'user'; // 当前镜头:'user' 前置 | 'environment' 后置(翻转按钮切换)
|
||||
@@ -440,6 +489,7 @@
|
||||
reader.onload = function () {
|
||||
var img = new Image();
|
||||
img.onload = function () {
|
||||
if (seq !== camSeq || step !== camStep) return;
|
||||
var scale = Math.min(1, 1280 / Math.max(img.width, img.height));
|
||||
var c = document.createElement('canvas');
|
||||
c.width = Math.round(img.width * scale); c.height = Math.round(img.height * scale);
|
||||
@@ -447,9 +497,9 @@
|
||||
var url = c.toDataURL('image/jpeg', 0.88);
|
||||
shutterLock = true; // 校验期间不接受新的拍摄/选图
|
||||
showCamStatus('照片检查中…');
|
||||
checkStill(url, CAM_STEPS[step].target).then(function (err) {
|
||||
hideCamStatus();
|
||||
checkStill(url, CAM_STEPS[step].target, { mode: 'file', ignoreDirection: true, step: step, seq: seq }).then(function (err) {
|
||||
if (seq !== camSeq || step !== camStep) return; // 期间已退出/重置
|
||||
hideCamStatus();
|
||||
shutterLock = false;
|
||||
if (err) { toast(err); voice.play(VOICE.fail, { cut: true }); return; } // 不合格:停在本步重拍
|
||||
onPhotoCaptured(url);
|
||||
@@ -460,27 +510,42 @@
|
||||
};
|
||||
reader.readAsDataURL(f);
|
||||
}
|
||||
// 静态照片校验:有人脸、朝向与当前步一致(检测模型不可用时放行,由服务端兜底)
|
||||
// 系统相机的前置照片是否镜像因机型而异,侧脸只校验转头幅度、不校验左右
|
||||
var STILL_FRONT_YAW_MAX = 20, STILL_SIDE_YAW_MIN = 20;
|
||||
function checkStill(dataUrl, target) {
|
||||
if (IS_MOCK || !modelsOk || typeof faceapi === 'undefined') return Promise.resolve('');
|
||||
// Live final frames and file uploads share all local quality gates. Files may
|
||||
// be mirrored by the system camera; their side direction remains unverified.
|
||||
function checkStill(dataUrl, target, options) {
|
||||
options = options || { mode: 'file', ignoreDirection: true, step: camStep, seq: camSeq };
|
||||
function remember(sample) { if (options.seq === camSeq) captureDiagnostics[options.step] = sample; }
|
||||
if (IS_MOCK) { remember({ mode: 'mock' }); return Promise.resolve(''); }
|
||||
if (!modelsOk || typeof faceapi === 'undefined') {
|
||||
if (modelsPromise && !options.waited) {
|
||||
options.waited = true;
|
||||
return new Promise(function (resolve) {
|
||||
var timer = setTimeout(resolve, 8000);
|
||||
modelsPromise.then(function () { clearTimeout(timer); resolve(); }, function () { clearTimeout(timer); resolve(); });
|
||||
}).then(function () { return checkStill(dataUrl, target, options); });
|
||||
}
|
||||
// Keep existing unsupported-device access, explicitly marked for admin.
|
||||
// Never silently treat an unavailable local engine as a passed check.
|
||||
remember({ mode: 'unchecked', reason: 'model-unavailable' });
|
||||
return Promise.resolve('');
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
var img = new Image();
|
||||
img.onload = function () {
|
||||
faceapi
|
||||
.detectSingleFace(img, new faceapi.TinyFaceDetectorOptions({ inputSize: DETECT_INPUT_SIZE, scoreThreshold: DETECT_SCORE }))
|
||||
.detectAllFaces(img, new faceapi.TinyFaceDetectorOptions({ inputSize: DETECT_INPUT_SIZE, scoreThreshold: DETECT_SCORE }))
|
||||
.withFaceLandmarks()
|
||||
.then(function (d) {
|
||||
if (!d) { resolve('照片中未识别到人脸,请重新拍摄'); return; }
|
||||
var e = null;
|
||||
try { e = HeadPose.estimate(d.landmarks.positions, false); } catch (err) { e = null; }
|
||||
if (!e) { resolve(''); return; }
|
||||
var yaw = Math.abs(e.yaw);
|
||||
if (target === 'front' && yaw > STILL_FRONT_YAW_MAX) { resolve('请拍摄正脸照片'); return; }
|
||||
if (target !== 'front' && yaw < STILL_SIDE_YAW_MIN) { resolve('请拍摄' + (target === 'right' ? '右' : '左') + '侧脸照片'); return; }
|
||||
.then(function (faces) {
|
||||
var d = faces[0], box = d && d.detection.box;
|
||||
var verdict = CaptureQuality.evaluate(d, { count: faces.length, target: target,
|
||||
width: img.naturalWidth, height: img.naturalHeight,
|
||||
offsetX: box ? (box.x + box.width / 2) / img.naturalWidth - .5 : 0,
|
||||
luma: box ? faceLuma(box, img) : null,
|
||||
mirrored: options.mode === 'live' && sideYawFlip, ignoreDirection: options.ignoreDirection });
|
||||
if (!verdict.ok) { resolve(verdict.hint + ',请重新拍摄'); return; }
|
||||
remember(Object.assign({ mode: options.mode, direction_checked: !options.ignoreDirection }, verdict.observation));
|
||||
resolve('');
|
||||
}, function () { resolve(''); });
|
||||
}, function () { resolve('照片检查失败,请重新拍摄'); });
|
||||
};
|
||||
img.onerror = function () { resolve('照片读取失败,请重新选择'); };
|
||||
img.src = dataUrl;
|
||||
@@ -628,15 +693,10 @@
|
||||
var MODEL_URL = 'vendor/faceapi/models';
|
||||
var DETECT_INTERVAL = 150; // 检测帧间隔(ms)
|
||||
var DETECT_INPUT_SIZE = 416; // tinyFaceDetector 输入尺寸:320 下正脸置信度仅 0.67 且框偏松,416 置信度 ~0.99 框更准(CPU 端略慢,移动端 GPU 无感)
|
||||
var DETECT_SCORE = 0.3; // 置信度阈值:0.4 在噪声帧易漏检,0.3 配合距离/角度闸门足够抗误检
|
||||
var DETECT_SCORE = CaptureQuality.config.score;
|
||||
var MISS_FRAMES_MAX = 3; // 连续漏检 ≥3 帧(~450ms)才报「未检测到人脸」,容忍单帧抖动
|
||||
var OK_HOLD_MS = 450; // 条件连续合格时长:滤掉一闪而过的合格帧(防运动模糊),达标立即抓拍
|
||||
var NO_PROGRESS_MS = 12000; // 单步超过该时长仍未抓拍→浮出调整提示
|
||||
var PREVIEW_SCALE = 1; // 与 CSS #cam-video 铺满预览一致,镜像只改变方向
|
||||
// 人脸框高/原始视频高;保留现有距离判定阈值。
|
||||
var FACE_NEAR = 0.30, FACE_FAR = 0.68;
|
||||
var CENTER_TOL = 0.18; // 人脸中心偏离取景中线超过屏宽 18% → 提示移入取景框
|
||||
var LUMA_DARK = 65, LUMA_BRIGHT = 235; // 脸部平均亮度(0–255):过暗/过曝提示调整光线
|
||||
|
||||
var modelsPromise = null, modelsOk = false, detectDisabled = false;
|
||||
var faceApiPromise = null;
|
||||
@@ -716,6 +776,7 @@
|
||||
// 每步进入/回退时的检测状态机重置(蒙版/提示/无进展计时全部归位)
|
||||
function resetDetectState() {
|
||||
okSince = 0;
|
||||
holdState = null;
|
||||
readyHapticAt = 0;
|
||||
missFrames = 0;
|
||||
oppFrames = 0;
|
||||
@@ -762,13 +823,13 @@
|
||||
if (shutterLock || capturing || !camLive) return Promise.resolve(); // 抓帧/流程推进中不判姿态
|
||||
var step = camStep, seq = camSeq; // 快照:检测是异步的,帧落地时步骤/流程可能已变
|
||||
return faceapi
|
||||
.detectSingleFace(camVideo, new faceapi.TinyFaceDetectorOptions({ inputSize: DETECT_INPUT_SIZE, scoreThreshold: DETECT_SCORE }))
|
||||
.detectAllFaces(camVideo, new faceapi.TinyFaceDetectorOptions({ inputSize: DETECT_INPUT_SIZE, scoreThreshold: DETECT_SCORE }))
|
||||
.withFaceLandmarks()
|
||||
.then(function (d) {
|
||||
if (step !== camStep || seq !== camSeq) return; // 跨步骤陈帧/流程已重置:丢弃
|
||||
if (detRunning && !shutterLock && !capturing) onDetect(d);
|
||||
})
|
||||
.catch(function () { /* 单帧异常忽略,下一帧继续 */ });
|
||||
.catch(function () { if (step === camStep && seq === camSeq) { holdState = null; okSince = 0; } }); // Unchecked frames break continuous readiness.
|
||||
}
|
||||
// 人脸框中心相对取景中线的水平偏移(占屏宽比例);按 object-fit:cover + 预览缩放换算到屏幕,镜像只影响符号
|
||||
function faceOffsetX(box) {
|
||||
@@ -779,11 +840,11 @@
|
||||
}
|
||||
// 脸部平均亮度:人脸框缩到 16×16 取均值
|
||||
var lumaCanvas = null;
|
||||
function faceLuma(box) {
|
||||
function faceLuma(box, source) {
|
||||
try {
|
||||
if (!lumaCanvas) { lumaCanvas = document.createElement('canvas'); lumaCanvas.width = lumaCanvas.height = 16; }
|
||||
var c = lumaCanvas.getContext('2d', { willReadFrequently: true });
|
||||
c.drawImage(camVideo, box.x, box.y, box.width, box.height, 0, 0, 16, 16);
|
||||
c.drawImage(source || camVideo, box.x, box.y, box.width, box.height, 0, 0, 16, 16);
|
||||
var px = c.getImageData(0, 0, 16, 16).data, sum = 0;
|
||||
for (var i = 0; i < px.length; i += 4) sum += 0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2];
|
||||
return sum / (px.length / 4);
|
||||
@@ -798,57 +859,51 @@
|
||||
}
|
||||
/* camera-haptic-pure-end */
|
||||
function onDetect(d) {
|
||||
var count = Array.isArray(d) ? d.length : d ? 1 : 0;
|
||||
if (Array.isArray(d)) d = d[0];
|
||||
var hint = '', ok = false;
|
||||
missFrames = d ? 0 : missFrames + 1;
|
||||
var hasFace = !!d, noFace = !hasFace && missFrames >= MISS_FRAMES_MAX;
|
||||
if (noFace) {
|
||||
hint = '未检测到人脸';
|
||||
} else if (!hasFace) {
|
||||
return; // 偶发漏检帧:保持上一条提示与状态,不扰动 ok 累积
|
||||
holdState = null; okSince = 0;
|
||||
return; // Keep the hint quiet, but never count a missing frame as stable.
|
||||
} else {
|
||||
// 提示优先级:无人脸 > 距离 > 居中 > 角度 > 光线
|
||||
var box = d.detection.box;
|
||||
var ratio = box.height / (camVideo.videoHeight || 1);
|
||||
if (ratio < FACE_NEAR) hint = '请靠近一点';
|
||||
else if (ratio > FACE_FAR) hint = '请远一点';
|
||||
else if (Math.abs(faceOffsetX(box)) > CENTER_TOL) hint = '请将脸部移入取景框';
|
||||
else {
|
||||
// 特征点取自原始帧(预览的 CSS 镜像不影响),三步的语义方向都按原始视角判定:
|
||||
// 标准机型 sideYawFlip 恒为 false,个别前置帧本身已镜像的机型由侧脸自校准翻转
|
||||
var tgt = CAM_STEPS[camStep].target;
|
||||
var pose = HeadPose.check(d.landmarks.positions, tgt, sideYawFlip);
|
||||
// 侧脸自校准:个别机型前置帧本身已镜像(语义 yaw 与实际转向持续相反)时自救——
|
||||
// 需连续 30 帧(~10s)强反向才翻转一次:阈值要高,否则用户转错方向 1~2s 就会被误判为镜像机,
|
||||
// 反而把错误的侧脸当合格抓走(左右图归属错乱)。翻转只动判定方向,不动预览画面
|
||||
if (tgt !== 'front' && pose.yaw != null && !mirrorCalibrated) {
|
||||
var wantSign = tgt === 'right' ? 1 : -1;
|
||||
if (pose.yaw * wantSign < -12) {
|
||||
if (++oppFrames >= 30) {
|
||||
sideYawFlip = !sideYawFlip;
|
||||
mirrorCalibrated = true;
|
||||
oppFrames = 0;
|
||||
}
|
||||
} else oppFrames = 0;
|
||||
}
|
||||
if (!pose.ok) hint = pose.hint;
|
||||
else {
|
||||
var luma = faceLuma(box);
|
||||
if (luma != null && luma < LUMA_DARK) hint = '请到光线充足处';
|
||||
else if (luma != null && luma > LUMA_BRIGHT) hint = '光线过强,请避开强光';
|
||||
else ok = true;
|
||||
}
|
||||
// 原始帧判断语义方向;只有已确认镜像的机型才翻转。
|
||||
var tgt = CAM_STEPS[camStep].target;
|
||||
var pose = HeadPose.check(d.landmarks.positions, tgt, sideYawFlip);
|
||||
// 侧脸自校准需连续 30 帧强反向才翻转一次,避免短暂转错方向误判为镜像。
|
||||
if (tgt !== 'front' && pose.yaw != null && !mirrorCalibrated) {
|
||||
var wantSign = tgt === 'right' ? 1 : -1;
|
||||
if (pose.yaw * wantSign < -12) {
|
||||
if (++oppFrames >= 30) {
|
||||
sideYawFlip = !sideYawFlip;
|
||||
mirrorCalibrated = true;
|
||||
oppFrames = 0;
|
||||
}
|
||||
} else oppFrames = 0;
|
||||
}
|
||||
var verdict = CaptureQuality.evaluate(d, { count: count, target: tgt, mirrored: sideYawFlip,
|
||||
width: camVideo.videoWidth, height: camVideo.videoHeight,
|
||||
offsetX: faceOffsetX(box), luma: faceLuma(box) });
|
||||
hint = verdict.hint; ok = verdict.ok;
|
||||
window.__dbgRatio = ratio; // 自动化校准/测试用(检测框高/视频高)
|
||||
}
|
||||
setMask(ok ? 'ok' : (noFace ? 'no-face' : ''));
|
||||
// 合格时「保持不动」只显示不播报:随即抓拍,「拍摄完成」会截断它
|
||||
setHint(ok ? '保持不动' : hint, ok, ok);
|
||||
if (!ok) { okSince = 0; return; }
|
||||
if (!okSince) {
|
||||
okSince = Date.now();
|
||||
if (!ok) { okSince = 0; holdState = null; return; }
|
||||
var held = CaptureQuality.hold(holdState, verdict.observation, Date.now());
|
||||
holdState = held.state;
|
||||
if (holdState.frames === 1) {
|
||||
okSince = holdState.since;
|
||||
if (!readyHapticAt || okSince - readyHapticAt >= 2000) { readyHapticAt = okSince; captureHaptic(navigator); }
|
||||
}
|
||||
else if (Date.now() - okSince >= OK_HOLD_MS) { okSince = 0; doCapture(); }
|
||||
if (held.ready) { okSince = 0; holdState = null; doCapture(); }
|
||||
}
|
||||
|
||||
/* ─── 拍照流程(三步,顺序对齐 Figma:正脸 → 右脸出镜 → 左脸出镜) ─── */
|
||||
@@ -934,7 +989,15 @@
|
||||
capturing = false;
|
||||
if (seq !== camSeq) return; // 期间已退出/重置:不再推进(遮罩仍正常淡出)
|
||||
if (!shot) { shutterLock = false; okSince = 0; toast('相机尚未就绪,请稍候'); return; } // 重置 okSince:避免陈旧累积立即再抓
|
||||
onPhotoCaptured(shot);
|
||||
var step = camStep;
|
||||
checkStill(shot, CAM_STEPS[step].target, { mode: 'live', ignoreDirection: false, step: step, seq: seq }).then(function (error) {
|
||||
if (seq !== camSeq || step !== camStep) return;
|
||||
if (error) {
|
||||
shutterLock = false; holdState = null; okSince = 0;
|
||||
setHint(error, false); armNudge(); return;
|
||||
}
|
||||
onPhotoCaptured(shot);
|
||||
});
|
||||
}, 300);
|
||||
}, 300);
|
||||
}
|
||||
@@ -952,11 +1015,12 @@
|
||||
region: regionSel.value || '',
|
||||
skinType: SKINS[skinIndex].name
|
||||
};
|
||||
var t0 = Date.now();
|
||||
var animationComplete = scanCycle.promise;
|
||||
requestAnalyze(photos, seq, profile, function (errMsg, payload) {
|
||||
if (seq !== camSeq) return; // 流程已被重置:丢弃
|
||||
var wait = Math.max(0, 1200 - (Date.now() - t0)); // 分析中至少展示 1.2s,避免闪烁
|
||||
setTimeout(function () {
|
||||
// A fast successful response waits for one actual visible animation cycle;
|
||||
// cancellation/errors never get held hostage by the presentation animation.
|
||||
(errMsg ? Promise.resolve() : animationComplete).then(function () {
|
||||
if (seq !== camSeq) return;
|
||||
closeOverlay('p-analyzing');
|
||||
stopCamera();
|
||||
@@ -987,7 +1051,7 @@
|
||||
else if (!payload.report_saved) toast('报告生成成功,但保存失败,请勿关闭页面并联系管理员');
|
||||
photos = []; // 报告已渲染(图片 src 已挂):释放三连拍 dataURL 内存
|
||||
showScreen('s-report', 'replace'); // 报告顶替拍照页:返回键回档案页,不再回到相机
|
||||
}, wait);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1031,6 +1095,9 @@
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
front_image: ph[0], left_image: ph[1], right_image: ph[2],
|
||||
capture_diagnostics: { version: CaptureQuality.config.version, views: {
|
||||
front: captureDiagnostics[0] || null, right: captureDiagnostics[1] || null, left: captureDiagnostics[2] || null
|
||||
} },
|
||||
profile: { // 档案随检测上报,后台记录用(服务端缺省 null)
|
||||
real_age: profile && profile.age,
|
||||
gender: profile && profile.gender,
|
||||
@@ -1333,25 +1400,26 @@
|
||||
setDim(k, vm.dims[k], k === worstDimKey ? 'worst' : k === bestDimKey ? 'best' : '');
|
||||
});
|
||||
var presentation = payload && payload.presentation || ReportUI.build(vm, payload && payload.result);
|
||||
$('#dims-note').textContent = '';
|
||||
var focus = ReportUI.focusOf(vm), focusEl = $('#dims-note');
|
||||
focusEl.hidden = !focus;
|
||||
focusEl.replaceChildren();
|
||||
if (focus) {
|
||||
var first = document.createElement('p'), name = document.createElement('b'), second = document.createElement('p');
|
||||
first.className = 'dims-note-l1'; first.textContent = '近期优先关注:'; name.textContent = focus.name; first.appendChild(name);
|
||||
second.className = 'dims-note-l2'; second.textContent = focus.tip;
|
||||
focusEl.append(first, second);
|
||||
}
|
||||
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; });
|
||||
presentation.views.forEach(function (v) { $(textIds[v.key]).textContent = ReportUI.customerText(v); });
|
||||
|
||||
// 检测可视化:轮廓=正脸实拍,其余按 VIZ_CAT 取 face_maps 图层或叠加 marks(payload 缺失时各卡保留占位)
|
||||
renderViz(payload, frontPhoto);
|
||||
|
||||
// 降级标注:只展示用户可感知/可执行的(下颌线、侧脸、补拍类);
|
||||
// 引擎内部技术项(上限锁未执行/字段未返回等)不面向用户展示
|
||||
// Fourth feedback: capture quality belongs in the protected admin record,
|
||||
// not in the customer report. Preserve vm.notes/snapshots unchanged.
|
||||
var notesEl = $('#vm-notes');
|
||||
var userNotes = (vm.notes || [])
|
||||
.filter(function (t) { return /下颌线|侧脸|补拍|重拍/.test(t); })
|
||||
.map(function (t) { return t.replace(/\(quality=[^)]*\)/g, ''); }); // 接口质量码仅留在引擎留痕里
|
||||
if (userNotes.length) {
|
||||
notesEl.innerHTML = userNotes.map(function (t) { return '*' + esc(t); }).join('<br>');
|
||||
notesEl.style.display = '';
|
||||
} else {
|
||||
notesEl.style.display = 'none';
|
||||
}
|
||||
notesEl.textContent = '';
|
||||
notesEl.style.display = 'none';
|
||||
|
||||
$('#care-list').innerHTML = presentation.care.map(function (s, i) {
|
||||
return '<p class="care-item">' + (i + 1) + '、' + esc(s.text) + '</p>';
|
||||
@@ -1493,6 +1561,7 @@
|
||||
camSeq++; // 使旧的快门/分析回调失效
|
||||
camStep = 0;
|
||||
photos = []; // 新一轮三连拍(从 startCamera 挪出:后台恢复续拍时保留已拍)
|
||||
captureDiagnostics = [];
|
||||
shutterLock = false; // 重新进入流程,保险解锁
|
||||
showScreen('s-cam', 'push');
|
||||
startCamera(); // beginDetect 在其成功回调里:确认相机可用才加载模型
|
||||
@@ -1515,7 +1584,7 @@
|
||||
'switch-login': function () {
|
||||
historySeq++;
|
||||
$('#login-consent').checked = false;
|
||||
$('#login-message').textContent = '微信与本机浏览器为独立账号,报告不会自动合并。请选择登录方式;返回可保留当前登录。';
|
||||
$('#login-message').textContent = '微信与游客为独立账号,报告不会自动合并。请选择登录方式;返回可保留当前登录。';
|
||||
showScreen('s-login', 'push');
|
||||
}
|
||||
};
|
||||
@@ -1537,7 +1606,7 @@
|
||||
$('#history-count').textContent = '您最近已进行过' + data.total + '次面部问题检测';
|
||||
$('#identity-note').textContent = AccountClient.user().kind === 'wechat'
|
||||
? '保留最近 5 份报告,同一微信账号可跨手机查看。'
|
||||
: '保留最近 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');
|
||||
@@ -1584,7 +1653,7 @@
|
||||
function loginWechat() {
|
||||
if (!loginAllowed()) return;
|
||||
if (!/MicroMessenger/i.test(navigator.userAgent)) {
|
||||
$('#login-message').textContent = '请在微信内打开本页面,或使用本机浏览器登录。';
|
||||
$('#login-message').textContent = '请在微信内打开本页面,或使用游客登录。';
|
||||
return;
|
||||
}
|
||||
loginBusy = true;
|
||||
@@ -1645,14 +1714,14 @@
|
||||
$('#login-message').textContent = code === 'state'
|
||||
? '登录链接已过期或会话丢失,请在同一微信窗口重新点击微信登录。'
|
||||
: code === 'network' ? '暂时无法连接微信服务,请稍后重新登录。'
|
||||
: '微信授权未完成,请重试;如仍失败,请将错误页面截图发给管理员。也可选择本机浏览器登录。';
|
||||
: '微信授权未完成,请重试;如仍失败,请将错误页面截图发给管理员。也可选择游客登录。';
|
||||
} else if (loginSuccess && (!AccountClient.user() || AccountClient.user().kind !== 'wechat')) {
|
||||
$('#login-message').textContent = '微信返回后登录会话未能保存,请在同一微信窗口重新登录。';
|
||||
} else if (AccountClient.user()) {
|
||||
if (loginSuccess) loginSucceeded();
|
||||
else showScreen('s-splash', 'replace');
|
||||
}
|
||||
else $('#login-message').textContent = '微信登录可跨手机查看报告;本机登录仅限当前浏览器。';
|
||||
else $('#login-message').textContent = '微信登录可跨手机查看报告;游客登录仅限当前浏览器。';
|
||||
}).catch(function (e) { $('#login-message').textContent = e.message; })
|
||||
.finally(function () {
|
||||
loginBusy = false;
|
||||
|
||||
Reference in New Issue
Block a user