diff --git a/src/pages/users/shareCard.wpy b/src/pages/users/shareCard.wpy index 7d09ebd..bb774ff 100644 --- a/src/pages/users/shareCard.wpy +++ b/src/pages/users/shareCard.wpy @@ -9,7 +9,7 @@ - 福恋期待认真相识{{profile.nickname || '我的卡片'}}{{profile.details}}{{profile.introduction || '上福恋认真找对象'}}{{posterError || profileError ? '海报暂未生成' : '正在准备你的个人海报'}} + 福恋期待认真相识{{profile.nickname || '我的卡片'}}{{profile.details}}{{profile.introduction || '上福恋认真找对象'}}{{posterError || profileError ? '海报暂未生成' : '正在准备你的个人海报'}} {{profileLoading ? '正在读取个人资料' : posterStage}} {{posterError || profileError}} diff --git a/src/utils/sharing-cache.js b/src/utils/sharing-cache.js index 597ab88..64e1081 100644 --- a/src/utils/sharing-cache.js +++ b/src/utils/sharing-cache.js @@ -1,6 +1,6 @@ import {boundedCall, fileAccess} from './sharing-runtime' -const VERSION = 'v5' +const VERSION = 'v6' const MAX_AGE = 24 * 60 * 60 * 1000 const INDEX = 'fulian-sharing-cache-' + VERSION diff --git a/src/utils/sharing.js b/src/utils/sharing.js index b63af39..e088fd4 100644 --- a/src/utils/sharing.js +++ b/src/utils/sharing.js @@ -34,10 +34,18 @@ function textLines(ctx,text,x,y,width,lineHeight,limit) { } ctx.fillText(line,x,y+row*lineHeight) } +export function photoPlacement(iw,ih,w,h){ + // Keep the whole source visible: a center cover crop can remove faces, + // especially when a portrait/group photo is placed in a landscape slot. + const scale=Math.min(w/iw,h/ih) + const width=iw*scale,height=ih*scale + return {x:(w-width)/2,y:(h-height)/2,width,height} +} function photoFill(ctx,image,x,y,w,h,r=0){ ctx.save();if(r){roundRect(ctx,x,y,w,h,r);ctx.clip()} - const scale=Math.max(w/image.width,h/image.height),sw=w/scale,sh=h/scale - ctx.drawImage(image,(image.width-sw)/2,(image.height-sh)/2,sw,sh,x,y,w,h);ctx.restore() + ctx.fillStyle='#FFF1F4';ctx.fillRect(x,y,w,h) + const fit=photoPlacement(image.width,image.height,w,h) + ctx.drawImage(image,0,0,image.width,image.height,x+fit.x,y+fit.y,fit.width,fit.height);ctx.restore() } async function canvasFor(page,id,width,height,job){ check(job) diff --git a/tools/sharing/VALIDATION-20261102.md b/tools/sharing/VALIDATION-20261102.md index 30b86de..a4a6409 100644 --- a/tools/sharing/VALIDATION-20261102.md +++ b/tools/sharing/VALIDATION-20261102.md @@ -84,3 +84,11 @@ Node v24.19.0:六组分享回归退出0。check-photo 增加随机排除当前 Node v16.20.2 在 .release-build/invitation-card-candidate 执行 tools/build/build.cjs,退出0,89模板编译完成。改动仍在功能分支,未更新 main 验收包。 2026-11-03 用户确认后端修复生效但线上换图未变化。核实上一交付前端 main f82dab2 仍固定优选,随机换图代码尚在功能分支。本轮补齐前端交付,合并后从 main 编译;多张合格照片排除当前项,缓存保留新选择。 + +## 2026-11-03 个人照片防裁头适配 + +用户反馈横向照片框居中裁剪导致头部出框。本轮采用完整照片等比例适配:保留全部源图,按海报/分享封面尺寸自动缩放居中,空余处填充福恋浅玫瑰背景;加载预览也改为 aspectFit。未引入人脸检测,不声称识别人脸;此方案直接避免照片框裁掉源图中的头部,无法恢复原图本身已缺失的内容。缓存升级v6使旧裁剪海报失效。 + +Node24几何检查覆盖竖图/横图/方图在686×470和264×364目标框内等比例完整显示,通过;check-photo/check-cache/check-image-path退出0,diff检查通过。后端无本轮修改。分支 feature/card-photo-framing,未上传发布。 + +2026-11-03 用户确认生成正式验收包。照片适配功能分支编译退出0,按授权合并本地 main 后重新编译。制品目录 releases/20261103-card-photo-framing,未上传部署。 diff --git a/tools/sharing/check-cache.cjs b/tools/sharing/check-cache.cjs index 4004a16..0761eb3 100644 --- a/tools/sharing/check-cache.cjs +++ b/tools/sharing/check-cache.cjs @@ -8,7 +8,7 @@ async function render(h,name='shareCard'){const page=h.page(name);page.hook('onR h.files.delete(changed.cover);const missing=await render(h);assert(missing.poster!==changed.poster,'missing file must regenerate') h.storage.set('token','synthetic-other-token');h.storage.set('user_id',43);h.state.profile.id=43 const other=await render(h);assert(other.poster.includes('-43-personal-'));assert(!other.poster.includes('-42-personal-')) - const index=h.storage.get('fulian-sharing-cache-v5');index['43-personal'].savedAt=Date.now()-2*24*3600*1000;h.storage.set('fulian-sharing-cache-v5',index) + const index=h.storage.get('fulian-sharing-cache-v6');index['43-personal'].savedAt=Date.now()-2*24*3600*1000;h.storage.set('fulian-sharing-cache-v6',index) const expired=await render(h);assert(expired.poster!==other.poster) h.flags.copyFail=true;const disk=await render(h,'myShare');assert(disk.poster&&!disk.cached);assert(!disk.generating) for(const flag of ['imageInfoHang','imageHang','canvasHang','exportHang','requestHang']){