diff --git a/docs/侧脸皱纹浮层异步加载-2026-10-16.md b/docs/侧脸皱纹浮层异步加载-2026-10-16.md index bba4ecc..5ed627b 100644 --- a/docs/侧脸皱纹浮层异步加载-2026-10-16.md +++ b/docs/侧脸皱纹浮层异步加载-2026-10-16.md @@ -2,7 +2,7 @@ 主测肤请求只等待主接口、评分及报告保存,不再同步等待左右侧脸的皱纹浮层。正面浮层随主接口返回。前端在报告切屏并经过两帧绘制后,才调用 `POST api/wrinkle-views`,获取该报告所属任务的左右侧脸标注。 -加载期间保留两张侧脸原图并显示「侧脸皱纹标注加载中…」。完成后只更新侧脸图片,不重算分数、不改结论或护理建议。失败/超时仅退回原图提示,报告继续使用。扫描等待页 30 秒兜底只针对主报告,不受浮层任务影响;可选浮层请求前端预算 65 秒,服务端两次顺序请求仍各最多 20 秒、间隔 4 秒。 +加载期间保留两张侧脸原图,各在图片上覆盖半透明「皱纹标注 / 加载中…」提示,不在图片下方重复显示加载文字。完成或失败后自动移除提示浮层,图片尺寸、裁切和点击查看大图规则不变。完成后只更新侧脸图片,不重算分数、不改结论或护理建议。失败/超时仅退回原图提示,报告继续使用。扫描等待页 30 秒兜底只针对主报告,不受浮层任务影响;可选浮层请求前端预算 65 秒,服务端两次顺序请求仍各最多 20 秒、间隔 4 秒。 使用随机任务令牌并绑定服务器会话所属账号;不同账号无法获取他人的标注。重复/并发请求共用同一任务,成功或失败结果都缓存,不自动重新收费调用。微信任务元数据及完成结果持久化,补写原报告 payload、后台标注状态和原始结果,不修改 vm/presentation。服务重启后可继续尚未启动的任务;已标记运行但被重启打断的任务不自动重跑,避免重复调用。老报告无任务令牌时不自动补检。 diff --git a/h5/css/ui-20261012.css b/h5/css/ui-20261012.css index d474923..c3e5464 100644 --- a/h5/css/ui-20261012.css +++ b/h5/css/ui-20261012.css @@ -215,6 +215,8 @@ .contour-side-photo img, .wrinkle-side-photo img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; transform: translateY(-25%) scale(2.8); transform-origin: center; -webkit-user-drag: none; } .contour-side-photo:focus-visible, .wrinkle-side-photo:focus-visible { outline: 2px solid #333; outline-offset: 3px; } .wrinkle-layer-note { width:calc(215 * var(--u)); margin:0 auto calc(12 * var(--u)); color:#888; font-size:10px; line-height:1.4; } +.wrinkle-loading { position:absolute; inset:0; z-index:1; display:flex; align-items:center; justify-content:center; background:rgba(0,0,0,.4); color:#fff; text-align:center; font-size:calc(10 * var(--u)); line-height:1.6; pointer-events:none; } +.wrinkle-loading[hidden] { display:none; } @media (prefers-reduced-motion: reduce) { .detail-panels, .detail-viewport { transition-duration: .01ms; } } diff --git a/h5/index.html b/h5/index.html index 82d82cf..e3ee91f 100644 --- a/h5/index.html +++ b/h5/index.html @@ -221,8 +221,8 @@
本次未获取皱纹检测图

检测完成后显示分析结果。

diff --git a/h5/js/app.js b/h5/js/app.js index 9f54da9..ed4861b 100644 --- a/h5/js/app.js +++ b/h5/js/app.js @@ -1635,17 +1635,21 @@ function renderWrinkleSides(reportPhotos, payload) { var epoch = ++wrinklePhotoEpoch, wrap = $('#wrinkle-sides'), note = $('#wrinkle-layer-note'); function refresh() { - note.textContent=payload && payload.wrinkle_job && payload.wrinkle_job.status==='pending' ? - '侧脸皱纹标注加载中…' : '部分侧脸未获取皱纹标注,未标注的侧脸仅展示原图。'; + var pending=!!(payload && payload.wrinkle_job && payload.wrinkle_job.status==='pending'); + note.textContent='部分侧脸未获取皱纹标注,未标注的侧脸仅展示原图。'; + wrap.querySelectorAll('figure').forEach(function(figure){ + figure.querySelector('.wrinkle-loading').hidden=!pending || figure.hidden || figure.querySelector('img').dataset.hasAnnotation==='true'; + }); var visible = Array.from(wrap.querySelectorAll('figure:not([hidden])')); wrap.hidden = !visible.length; - note.hidden = !visible.some(function(f) { return f.querySelector('img').dataset.hasAnnotation !== 'true'; }); + note.hidden = pending || !visible.some(function(f) { return f.querySelector('img').dataset.hasAnnotation !== 'true'; }); var card = wrap.closest('.card-detail'), state = detailSwipes.get(card); if (state) state.height(Number(card.querySelector('.dtab.active').dataset.tab)); } wrap.hidden = note.hidden = true; ['left', 'right'].forEach(function(view, i) { var img = $('#wrinkle-' + view), figure = img.closest('figure'); + figure.querySelector('.wrinkle-loading').hidden=true; img.onload = img.onerror = null; img.removeAttribute('src'); img.dataset.hasAnnotation = 'false'; img.draggable = false; figure.hidden = true; var src = reportPhotos && reportPhotos[i + 1]; diff --git a/h5/test/wrinkle-async-browser.mjs b/h5/test/wrinkle-async-browser.mjs index bb874c8..d985ec7 100644 --- a/h5/test/wrinkle-async-browser.mjs +++ b/h5/test/wrinkle-async-browser.mjs @@ -29,7 +29,12 @@ try { const result=await(await primary).json();assert.equal(result.ok,true);assert.equal(result.wrinkle_views.left.status,'pending'); assert.equal(fs.readFileSync(path.join(dir,'provider-calls.jsonl'),'utf8').trim().split('\n').length,1,'main response cannot wait for side paid calls'); await page.waitForSelector('#s-report.active');const conclusion=await page.locator('#ditem-para2').textContent(); - await page.waitForFunction(()=>document.querySelector('#wrinkle-layer-note').textContent.includes('加载中')); + await page.waitForFunction(()=>document.querySelectorAll('#wrinkle-sides .wrinkle-loading:not([hidden])').length===2); + assert(await page.locator('#wrinkle-layer-note').evaluate(e=>e.hidden),'pending text is on images, not duplicated below'); + for(const view of ['left','right']) { + const sizes=await page.locator('#wrinkle-'+view).evaluate(img=>{const a=img.closest('button').getBoundingClientRect(),b=img.parentNode.querySelector('.wrinkle-loading').getBoundingClientRect();return [a.width,a.height,b.width,b.height];}); + assert.equal(sizes[0],sizes[2]);assert.equal(sizes[1],sizes[3]); + } await page.waitForFunction(()=>window.__annotationStarts.length===1); await page.evaluate(()=>window.__asyncScreen('s-splash')); const api=page.context().request; @@ -39,6 +44,7 @@ try { assert.equal(await page.locator('#wrinkle-left').getAttribute('data-has-annotation'),'false','hidden report is not patched by stale response'); await page.evaluate(()=>window.__asyncScreen('s-report')); await page.waitForFunction(()=>['left','right'].every(v=>document.querySelector('#wrinkle-'+v).dataset.hasAnnotation==='true')); + assert.equal(await page.locator('#wrinkle-sides .wrinkle-loading:not([hidden])').count(),0,'loading mask disappears when layers are ready'); assert.equal(await page.locator('#ditem-para2').textContent(),conclusion); assert.deepEqual(await page.evaluate(()=>window.__annotationStarts),[{report:true,scan:false}]); const stored=await(await api.get(base+'api/reports/'+result.record_id)).json();assert.equal(stored.report.payload.wrinkle_job.status,'complete');assert.deepEqual(stored.report.vm,result.vm);assert.deepEqual(stored.report.presentation,result.presentation); @@ -49,6 +55,7 @@ try { await page.evaluate(()=>window.__startAsyncReport());await page.waitForSelector('#s-report.active'); await page.waitForFunction(()=>document.querySelector('#wrinkle-layer-note').textContent.includes('部分侧脸未获取')); assert.equal(await page.locator('#s-report.active').count(),1,'optional annotation failure keeps report accessible'); + assert.equal(await page.locator('#wrinkle-sides .wrinkle-loading:not([hidden])').count(),0,'failed load never leaves a permanent mask'); assert.equal(await page.locator('#ditem-para2').textContent(),conclusion); assert.equal(fs.readFileSync(path.join(dir,'provider-calls.jsonl'),'utf8').trim().split('\n').length,4,'failed optional endpoint adds no provider retry'); assert.deepEqual(errors,[]);console.log('PASS: fast main report, side requests only after active report, pending/ready UI, unchanged conclusions/scores, owner isolation, snapshot cache, exactly 3 fixture calls');