fix: resolve canvas package images and show poster generation progress
This commit is contained in:
parent
1321aa4391
commit
f82dab2fc8
@ -6,10 +6,11 @@ import {renderSharingCover, renderSharingPoster, storeSharingCode, saveSharingPo
|
||||
|
||||
export default function sharingPage(kind) {
|
||||
return {
|
||||
data:{profile:{id:0,nickname:'',photo:'',details:'',introduction:'',idealMate:''},profileLoading:true,profileReady:false,profileError:'',canvasReady:false,poster:'',cover:'',generating:false,posterError:'',posterStage:'正在准备海报',cacheNote:'',cached:false,saving:false},
|
||||
data:{profile:{id:0,nickname:'',photo:'',details:'',introduction:'',idealMate:''},profileLoading:true,profileReady:false,profileError:'',canvasReady:false,poster:'',cover:'',generating:false,posterError:'',posterStage:'正在准备海报',posterProgress:0,cacheNote:'',cached:false,saving:false},
|
||||
onReady(){this.canvasReady=true;this.ensurePoster()},
|
||||
onUnload(){this._sharingDisposed=true;if(this._sharingJob)this._sharingJob.cancelled=true},
|
||||
methods:{
|
||||
setPosterProgress(percent,stage){if(this._sharingDisposed)return;this.posterProgress=percent;this.posterStage=stage},
|
||||
async startSharing(){
|
||||
this._sharingDisposed=false
|
||||
const id=currentSharingUser()
|
||||
@ -20,14 +21,16 @@ export default function sharingPage(kind) {
|
||||
}
|
||||
if(!this._sharingDisposed)await this.loadSharingProfile()
|
||||
},
|
||||
applySharingCache(cache){this.poster=cache.poster;this.cover=cache.cover;this.cached=true;this.posterError='';this.cacheNote='已缓存,下次打开可直接使用'},
|
||||
applySharingCache(cache){this.poster=cache.poster;this.cover=cache.cover;this.cached=true;this.posterError='';this.setPosterProgress(100,'海报已准备好');this.cacheNote='已缓存,下次打开可直接使用'},
|
||||
async loadSharingProfile(force=false){
|
||||
if(this._sharingDisposed)return
|
||||
this.profileLoading=true;this.profileError=''
|
||||
this.posterError='';this.setPosterProgress(5,'正在读取个人资料')
|
||||
try{
|
||||
const data=await sharingRequest(`${service.user}/sharing/profile`)
|
||||
if(this._sharingDisposed)return
|
||||
const profile=displayProfile(data)
|
||||
this.setPosterProgress(15,kind==='personal'?'正在选择适合的生活照':'正在准备专属邀请')
|
||||
if(kind==='personal')profile.photo=await selectSharingPhoto(profile.photos)
|
||||
const cache=force?null:await readSharingCache(profile.id,kind,profile)
|
||||
if(this._sharingDisposed)return
|
||||
@ -35,29 +38,32 @@ export default function sharingPage(kind) {
|
||||
if(cache)this.applySharingCache(cache)
|
||||
else{this.poster='';this.cover='';this.cached=false;this.cacheNote=''}
|
||||
this.profileLoading=false
|
||||
if(!cache)this.setPosterProgress(25,'正在准备分享封面')
|
||||
await this.ensurePoster(force)
|
||||
}catch(error){if(!this._sharingDisposed)this.profileError=error.message||'资料加载失败,请重试'}
|
||||
finally{if(!this._sharingDisposed)this.profileLoading=false}
|
||||
},
|
||||
async ensurePoster(force=false){
|
||||
if(this._sharingDisposed||this.generating||!this.canvasReady||!this.profileReady||(!force&&this.poster))return
|
||||
this.generating=true;this.posterError='';this.cacheNote='';this.posterStage='正在准备分享图片'
|
||||
this.generating=true;this.posterError='';this.cacheNote='';this.setPosterProgress(25,'正在绘制分享封面')
|
||||
const job={cancelled:false};this._sharingJob=job
|
||||
const active=()=>{if(job.cancelled||this._sharingDisposed)throw new Error('已停止生成,请重试')}
|
||||
const profile={...this.profile}
|
||||
const generate=async()=>{
|
||||
const cover=await renderSharingCover(this,profile,kind==='personal',job)
|
||||
active();this.cover=cover;this.posterStage='正在生成专属海报'
|
||||
active();this.cover=cover;this.setPosterProgress(45,'正在获取专属小程序码')
|
||||
const data=await sharingRequest(`${service.user}/sharing/code`,'POST',{kind})
|
||||
active();profile.qrcode=await storeSharingCode(data,kind,profile.id)
|
||||
active();const poster=await renderSharingPoster(this,profile,kind==='personal',job)
|
||||
active();this.setPosterProgress(65,'正在准备小程序码');profile.qrcode=await storeSharingCode(data,kind,profile.id)
|
||||
active();this.setPosterProgress(75,'正在绘制高清海报');const poster=await renderSharingPoster(this,profile,kind==='personal',job)
|
||||
active();const assets={poster,cover,qrcode:profile.qrcode}
|
||||
this.setPosterProgress(90,'正在保存到本地')
|
||||
try{const cache=await writeSharingCache(profile,kind,assets);active();return {...cache,cached:true}}
|
||||
catch(error){active();return {...assets,cached:false}}
|
||||
}
|
||||
try{
|
||||
const result=await boundedCall((resolve,reject)=>{generate().then(resolve,reject);return()=>{job.cancelled=true}},'生成超时,请检查网络后重试',30000)
|
||||
active();this.poster=result.poster;this.cover=result.cover;this.cached=result.cached
|
||||
this.setPosterProgress(100,'海报已准备好')
|
||||
this.cacheNote=result.cached?'已缓存,下次打开可直接使用':'海报已生成,暂未能缓存到本地'
|
||||
}catch(error){if(!this._sharingDisposed)this.posterError=error.message||'海报生成失败,请重试'}
|
||||
finally{job.cancelled=true;if(!this._sharingDisposed)this.generating=false}
|
||||
|
||||
BIN
src/pages/users/assets/invitation-share-cover.png
Normal file
BIN
src/pages/users/assets/invitation-share-cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
src/pages/users/assets/personal-share-cover.png
Normal file
BIN
src/pages/users/assets/personal-share-cover.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
@ -7,13 +7,18 @@
|
||||
</view>
|
||||
<view v-show="activeTab === 'poster'" class="share-panel">
|
||||
<view class="section-top"><text class="panel-caption">我的专属邀请</text><button class="text-button" :disabled="generating || profileLoading" @tap="refreshPoster">更新海报</button></view>
|
||||
<view v-if="generating || (profileLoading && !poster)" class="generation-progress">
|
||||
<view class="generation-progress-heading"><text>{{posterStage}}</text><text>{{posterProgress}}%</text></view>
|
||||
<view class="generation-progress-track"><view class="generation-progress-fill" style="width: {{posterProgress}}%;"></view></view>
|
||||
<view class="generation-progress-note">首次生成需要下载图片,完成后会缓存到本地</view>
|
||||
</view>
|
||||
<view class="invite-stage">
|
||||
<view class="invite-poster-frame" @tap="preview">
|
||||
<image v-if="poster" class="invite-poster-image" src="{{poster}}" mode="widthFix" @error="posterImageFailed"></image>
|
||||
<view v-else class="poster-placeholder"><image src="/pages/users/assets/fulian-logo.png" mode="aspectFit"></image><text>邀请你加入福恋</text><text class="placeholder-small">上福恋认真找对象</text></view>
|
||||
<image v-else class="invite-poster-image" src="/pages/users/assets/invitation-share-cover.png" mode="widthFix"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view class="preview-caption">点击海报 · 查看大图</view>
|
||||
<view class="preview-caption">{{poster ? '点击海报 · 查看大图' : '邀请封面预览 · 海报生成后可保存'}}</view>
|
||||
<view v-if="generating || (profileLoading && !poster)" class="asset-status">{{profileLoading ? '正在准备你的邀请' : posterStage}}</view>
|
||||
<view v-else-if="posterError || profileError" class="asset-error"><text>{{posterError || profileError}}</text><button class="text-button" @tap="refreshPoster">重新生成</button></view>
|
||||
<view v-else-if="cacheNote" class="asset-status ready-status">{{cacheNote}}</view>
|
||||
@ -34,7 +39,7 @@
|
||||
<view class="orders-notice"><text>邀请订单</text><text>婚恋使者专属 · 暂未开放</text></view>
|
||||
<view class="section-heading list-heading"><text>邀请用户</text><text class="section-meta">每一次分享,都可能促成相遇</text></view>
|
||||
<view class="invite-list">
|
||||
<view v-for="item in invitees" :key="item.id" class="invite-row"><view class="invite-avatar">{{item.initial}}</view><view class="invite-name">{{item.nickname || '福恋用户'}}</view><view :class="{'verified-badge':item.verified,'pending-badge':!item.verified}">{{item.verified ? '真人认证已通过' : '待完成真人认证'}}</view></view>
|
||||
<view v-for="item in invitees" :key="item.id" class="invite-row" hover-class="invite-row-pressed" @tap="openInvitee(item.id)"><view class="invite-avatar">{{item.initial}}</view><view class="invite-name">{{item.nickname || '福恋用户'}}</view><view :class="{'verified-badge':item.verified,'pending-badge':!item.verified}">{{item.verified ? '真人认证已通过' : '待完成真人认证'}}</view><text class="invite-arrow">›</text></view>
|
||||
<view v-if="!invitees.length && !listLoading && !listError" class="empty-state"><view class="empty-title">第一份邀请,从你开始</view><view>在“专属邀请”分享海报给朋友,邀请记录会展示在这里。</view></view>
|
||||
<view v-if="listError" class="list-status">列表加载失败,请重试</view>
|
||||
<view v-if="listLoading" class="list-status">正在加载邀请用户…</view>
|
||||
@ -58,8 +63,13 @@ wepy.page({
|
||||
data:{activeTab:'poster',overviewReady:false,overviewError:false,total:0,verified:0,remaining:7,progressPercent:0,invitees:[],page:0,hasMore:true,listLoading:false,listError:false,canApply:false,checkingEligibility:false,eligibilityError:false,showApplication:false,savingContact:false,contactImage:'/pages/users/assets/ambassador-contact.jpg'},
|
||||
onLoad(){this.startSharing();this.loadOverview();this.loadMore()},
|
||||
onShow(){if(this.overviewReady)this.refreshEligibility()},
|
||||
onShareAppMessage(){return {title:'邀请你加入福恋,上福恋认真找对象',path:this.profile.id?sharingPath(this.profile.id,false):'/pages/tabBar/welcome',imageUrl:this.cover||'/pages/users/assets/fulian-logo.png'}},
|
||||
onShareAppMessage(){return {title:'邀请你加入福恋,上福恋认真找对象',path:this.profile.id?sharingPath(this.profile.id,false):'/pages/tabBar/welcome',imageUrl:this.cover||'/pages/users/assets/invitation-share-cover.png'}},
|
||||
methods:{
|
||||
openInvitee(value){
|
||||
const id=Number(value)
|
||||
if(!Number.isSafeInteger(id)||id<=0)return
|
||||
wx.navigateTo({url:`/pages/home/information?id=${id}`,fail(){wx.showToast({title:'暂时无法打开用户详情,请重试',icon:'none'})}})
|
||||
},
|
||||
showPosterTab(){this.activeTab='poster'},
|
||||
showResultsTab(){this.activeTab='results'},
|
||||
setOverview(data){this.total=Number(data.total)||0;this.verified=Number(data.verified)||0;this.remaining=Math.max(0,7-this.verified);this.progressPercent=Math.min(100,Math.max(0,this.verified/7*100));this.canApply=data.can_apply===true&&this.verified>=7;this.overviewReady=true},
|
||||
|
||||
@ -2,6 +2,11 @@
|
||||
<view class="sharing-page personal-page">
|
||||
<view class="page-intro"><view class="intro-label">属于你的认真介绍</view><view class="intro-title">让合适的人,认识真实的你</view><view class="intro-note">一张卡片,开启一次真诚的相识。</view></view>
|
||||
<view class="section-top card-tools"><text class="panel-caption">我的个人海报</text><button class="text-button" :disabled="generating || profileLoading" @tap="refreshPoster">更新海报</button></view>
|
||||
<view v-if="generating || (profileLoading && !poster)" class="generation-progress">
|
||||
<view class="generation-progress-heading"><text>{{posterStage}}</text><text>{{posterProgress}}%</text></view>
|
||||
<view class="generation-progress-track"><view class="generation-progress-fill" style="width: {{posterProgress}}%;"></view></view>
|
||||
<view class="generation-progress-note">首次生成需要下载图片,完成后会缓存到本地</view>
|
||||
</view>
|
||||
<view class="personal-stage">
|
||||
<image v-if="poster" class="personal-poster" src="{{poster}}" mode="widthFix" @tap="preview" @error="posterImageFailed"></image>
|
||||
<view v-else class="personal-fallback"><view class="fallback-brand"><image src="/pages/users/assets/fulian-logo.png" mode="aspectFit"></image><text>福恋</text></view><image v-if="profile.photo" class="fallback-photo" src="{{profile.photo}}" mode="aspectFill"></image><view v-else class="fallback-photo fallback-blank"><image src="/pages/users/assets/fulian-logo.png" mode="aspectFit"></image><text>期待认真相识</text></view><view class="fallback-name">{{profile.nickname || '我的卡片'}}</view><view class="fallback-details">{{profile.details}}</view><view class="fallback-intro">{{profile.introduction || '上福恋认真找对象'}}</view><view class="fallback-footer">{{posterError || profileError ? '海报暂未生成' : '正在准备你的个人海报'}}</view></view>
|
||||
@ -21,7 +26,7 @@ import {sharingPath} from '../../utils/sharing'
|
||||
wepy.page({
|
||||
mixins:[sharingPage('personal')],
|
||||
onLoad(){this.startSharing()},
|
||||
onShareAppMessage(){return {title:this.profile.nickname?`我是${this.profile.nickname},期待与你相识`:'我的福恋卡片,期待与你相识',path:this.profile.id?sharingPath(this.profile.id,true):'/pages/tabBar/welcome',imageUrl:this.cover||this.profile.photo||'/pages/users/assets/fulian-logo.png'}}
|
||||
onShareAppMessage(){return {title:this.profile.nickname?`我是${this.profile.nickname},期待与你相识`:'我的福恋卡片,期待与你相识',path:this.profile.id?sharingPath(this.profile.id,true):'/pages/tabBar/welcome',imageUrl:this.cover||'/pages/users/assets/personal-share-cover.png'}}
|
||||
})
|
||||
</script>
|
||||
<style lang="less" src="../../styles/sharing.less"></style>
|
||||
|
||||
@ -86,3 +86,10 @@
|
||||
.invitation-tabs { display:flex; padding:8rpx; margin-bottom:28rpx; border-radius:22rpx; background:#FFF1F4; gap:8rpx; }
|
||||
.sharing-page .invitation-tab { flex:1; padding:22rpx 12rpx; border-radius:16rpx; background:transparent; color:#97717D; font-size:28rpx; }
|
||||
.sharing-page .invitation-tab.is-active { background:#D92554; color:#FFF9F5; font-weight:600; }
|
||||
.invite-row-pressed { background:#FFF1F4; }
|
||||
.invite-arrow { color:#97717D; font-size:36rpx; flex-shrink:0; line-height:1; }
|
||||
.generation-progress { margin:18rpx 0 24rpx; padding:22rpx; background:#FFF9F5; border:1rpx solid #EED3DB; border-radius:18rpx; }
|
||||
.generation-progress-heading { display:flex; justify-content:space-between; gap:16rpx; color:#A3294D; font-size:25rpx; }
|
||||
.generation-progress-track { height:12rpx; margin:18rpx 0; border-radius:12rpx; background:#EED3DB; overflow:hidden; }
|
||||
.generation-progress-fill { height:100%; background:#D92554; border-radius:12rpx; transition:width .25s ease; }
|
||||
.generation-progress-note { font-size:22rpx; line-height:1.6; color:#97717D; }
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import {boundedCall, fileAccess} from './sharing-runtime'
|
||||
|
||||
const VERSION = 'v4'
|
||||
const VERSION = 'v5'
|
||||
const MAX_AGE = 24 * 60 * 60 * 1000
|
||||
const INDEX = 'fulian-sharing-cache-' + VERSION
|
||||
|
||||
|
||||
@ -50,13 +50,22 @@ async function canvasFor(page,id,width,height,job){
|
||||
check(job);canvas.width=width;canvas.height=height
|
||||
return canvas
|
||||
}
|
||||
async function loadImage(canvas,src,job){
|
||||
const info=await boundedCall((resolve,reject)=>wx.getImageInfo({src,success:resolve,fail:reject}),'图片加载失败,请检查网络后重试',8000)
|
||||
export function canvasImagePath(path,original) {
|
||||
// getImageInfo may remove the leading slash from a package path. Canvas
|
||||
// resolves that value relative to the current page rather than package root.
|
||||
if(typeof path!=='string'||!path)throw new Error('图片路径无效,请重试')
|
||||
if(path.startsWith('pages/')||path.startsWith('images/'))return '/'+path
|
||||
if(original===LOGO&&!/^(\/|[a-z][a-z0-9+.-]*:)/i.test(path))return original
|
||||
return path
|
||||
}
|
||||
async function loadImage(canvas,src,job,label='图片'){
|
||||
const info=await boundedCall((resolve,reject)=>wx.getImageInfo({src,success:resolve,fail:reject}),label+'加载失败,请检查网络后重试',8000)
|
||||
check(job)
|
||||
const path=canvasImagePath(info.path,src)
|
||||
return boundedCall((resolve,reject)=>{
|
||||
const image=canvas.createImage()
|
||||
image.onload=()=>resolve(image);image.onerror=()=>reject(new Error('图片读取失败,请重试'));image.src=info.path
|
||||
},'图片读取超时,请重试',5000)
|
||||
image.onload=()=>resolve(image);image.onerror=()=>reject(new Error(label+'读取失败,请重试'));image.src=path
|
||||
},label+'读取超时,请重试',5000)
|
||||
}
|
||||
async function exportCanvas(canvas,width,height,job){
|
||||
check(job)
|
||||
@ -66,6 +75,15 @@ function brand(ctx,logo,width=750){
|
||||
ctx.drawImage(logo,40,30,56,56);ctx.fillStyle='#A3294D';font(ctx,30,600);ctx.fillText('福恋',110,70)
|
||||
font(ctx,22);ctx.textAlign='right';ctx.fillText('上福恋认真找对象',width-40,66);ctx.textAlign='left'
|
||||
}
|
||||
function invitationLetter(ctx,logo,x,y,w,h){
|
||||
roundRect(ctx,x+18,y+12,w,h,22,'#EED3DB')
|
||||
roundRect(ctx,x,y,w,h,22,'#FFF9F5')
|
||||
ctx.strokeStyle='#EED3DB';ctx.lineWidth=2
|
||||
ctx.beginPath();ctx.moveTo(x+16,y+16);ctx.lineTo(x+w/2,y+h*0.64);ctx.lineTo(x+w-16,y+16);ctx.stroke()
|
||||
ctx.beginPath();ctx.moveTo(x+16,y+h-16);ctx.lineTo(x+w*.35,y+h*.52);ctx.moveTo(x+w-16,y+h-16);ctx.lineTo(x+w*.65,y+h*.52);ctx.stroke()
|
||||
ctx.fillStyle='#FFF1F4';ctx.beginPath();ctx.arc(x+w/2,y+h*.6,42,0,Math.PI*2);ctx.fill()
|
||||
ctx.drawImage(logo,x+w/2-28,y+h*.6-28,56,56)
|
||||
}
|
||||
export function drawSharingPoster(ctx,images,profile,personal){
|
||||
ctx.fillStyle='#FFF9F5';ctx.fillRect(0,0,750,1120);brand(ctx,images.logo)
|
||||
if(personal){
|
||||
@ -84,8 +102,7 @@ export function drawSharingPoster(ctx,images,profile,personal){
|
||||
ctx.fillStyle='#97717D';font(ctx,24);ctx.fillText('一份邀请 · 一次美好相遇',70,184)
|
||||
ctx.fillStyle='#A3294D';font(ctx,58,600);ctx.fillText('把认真相遇',70,282);ctx.fillText('分享给身边的人',70,362)
|
||||
ctx.fillStyle='#4E4145';font(ctx,27);ctx.fillText('邀请朋友加入福恋,开启认真交往。',70,426)
|
||||
ctx.lineWidth=20;ctx.strokeStyle='#EED3DB';ctx.beginPath();ctx.arc(310,568,74,0,Math.PI*2);ctx.stroke()
|
||||
ctx.strokeStyle='#D92554';ctx.beginPath();ctx.arc(428,568,74,0,Math.PI*2);ctx.stroke()
|
||||
invitationLetter(ctx,images.logo,192,474,348,188)
|
||||
ctx.fillStyle='#A3294D';font(ctx,26);textLines(ctx,`${profile.nickname||'一位朋友'} 邀请你加入`,70,720,610,36,1)
|
||||
roundRect(ctx,32,816,686,264,28,'#FFF1F4')
|
||||
ctx.drawImage(images.qrcode,60,852,184,184);ctx.fillStyle='#A3294D';font(ctx,32,600);ctx.fillText('扫码加入福恋',278,920)
|
||||
@ -95,15 +112,26 @@ export function drawSharingPoster(ctx,images,profile,personal){
|
||||
}
|
||||
export function drawSharingCover(ctx,images,profile,personal){
|
||||
ctx.fillStyle='#FFF9F5';ctx.fillRect(0,0,750,600);brand(ctx,images.logo)
|
||||
if(!personal){
|
||||
roundRect(ctx,28,118,694,442,28,'#FFF1F4')
|
||||
ctx.fillStyle='#97717D';font(ctx,23);ctx.fillText('送给认真寻找爱情的你',58,178)
|
||||
ctx.fillStyle='#A3294D';font(ctx,46,600);ctx.fillText('一份邀请',58,260);ctx.fillText('一次美好相遇',58,322)
|
||||
invitationLetter(ctx,images.logo,438,220,222,156)
|
||||
ctx.fillStyle='#4E4145';font(ctx,26);ctx.fillText('加入福恋,让彼此的故事开始',58,412)
|
||||
roundRect(ctx,58,458,250,58,29,'#D92554')
|
||||
ctx.fillStyle='#FFF9F5';font(ctx,25,500);ctx.fillText('点击开启认真相识',82,496)
|
||||
return
|
||||
}
|
||||
roundRect(ctx,32,120,686,438,28,'#FFF1F4')
|
||||
if(personal&&images.photo)photoFill(ctx,images.photo,424,148,264,364,20)
|
||||
else{roundRect(ctx,424,148,264,364,20,'#FFF9F5');ctx.drawImage(images.logo,482,256,148,148)}
|
||||
ctx.fillStyle='#A3294D';font(ctx,42,600);textLines(ctx,personal?(profile.nickname||'我的卡片'):'邀请你加入福恋',60,226,personal?330:620,56,2)
|
||||
ctx.fillStyle='#4E4145';font(ctx,27);textLines(ctx,personal?(profile.introduction||'期待与你认真相识'):'把福恋分享给认真寻找爱情的人',60,356,personal?330:620,42,3)
|
||||
ctx.fillStyle='#A3294D';font(ctx,25);ctx.fillText(personal?'点击认识我 →':'点击开启认真相识 →',60,512)
|
||||
}
|
||||
async function render(page,profile,personal,cover,job){
|
||||
const width=750,height=cover?600:1120,canvas=await canvasFor(page,cover?'fulianCover':'fulianPoster',width,height,job)
|
||||
const [logo,photo,qrcode]=await Promise.all([loadImage(canvas,LOGO,job),personal&&profile.photo?loadImage(canvas,profile.photo,job).catch(()=>null):null,cover?null:loadImage(canvas,profile.qrcode,job)])
|
||||
const [logo,photo,qrcode]=await Promise.all([loadImage(canvas,LOGO,job,'品牌图片'),personal&&profile.photo?loadImage(canvas,profile.photo,job,'生活照').catch(()=>null):null,cover?null:loadImage(canvas,profile.qrcode,job,'小程序码')])
|
||||
check(job)
|
||||
const ctx=canvas.getContext('2d');ctx.textBaseline='alphabetic'
|
||||
if(cover)drawSharingCover(ctx,{logo,photo},profile,personal)
|
||||
|
||||
@ -63,3 +63,15 @@ Go v1.22.4:E:/pj/love/.release-build/card-lifestyle-api 中执行 go test -mod
|
||||
## 合并授权与验收制品
|
||||
|
||||
2026-11-02 用户已审核迭代结果并明确要求将所有相关 feature 合并 main、发布新版验收。相关范围为前端 feature/invitation-card-experience 与后端 feature/card-lifestyle-photo;早先 feature/profile-sharing 已合入两仓库 main。此次按授权提交并合并本地 main,从 main 独立导出编译验收制品;不包含工作区无关修改。完整发布状态、提交 SHA、制品校验及真机待验收项以 releases/20261102-invitation-card-main 内 manifest.json 和 RELEASE.txt 为准。此前各段“未合并”描述为当时验证状态。
|
||||
|
||||
## 真机图片失败修复与阶段进度(2026-11-02)
|
||||
|
||||
用户截图两页均为“图片读取失败”,好友分享退回原始 Logo。已在开发者工具 2.02.2609240 的独立 Canvas2D 探针复现:wx.getImageInfo('/pages/users/assets/fulian-logo.png') 返回 pages/users/assets/fulian-logo.png,直接赋给 Canvas Image.src 加载失败(originalLoads=false)。补齐包根路径后,邀请与个人海报均成功导出 750×1120,封面也正常导出。探针使用合成资料、Logo 代替测试照片和二维码,禁用 wx.request/uploadFile/downloadFile/connectSocket;未打开业务应用、未调用分享码服务,不等于实机扫码/相册权限验收。输出记录位于 output/invitation-card-experience/native-canvas-result.json 与 native-canvas-check.png。
|
||||
|
||||
修改:包内图片路径规范化,保留 wxfile/http://usr/绝对文件等路径;区分品牌图片、生活照、小程序码错误;缓存升级 v5;首轮加载顶部增加阶段进度(资料5、选图15、封面25、请求二维码45、准备二维码65、绘图75、缓存90、完成100),由真实阶段完成推进而非计时假进度,错误停止。封面用原版 Logo 与福恋 VI 绘制邀请信封、标题和行动文案;两种用途各有750×600静态品牌封面兜底,不再分享放大的原始 Logo。邀请页加载占位使用完整品牌封面。
|
||||
|
||||
Node v24.19.0 六组回归均通过:check-image-path、check、check-cache、check-photo、check-contact、regression。新增覆盖返回相对包路径、原生文件 URI 保持、进度递增、错误不显示100%、二维码接口失败时封面仍可用。前端隔离编译使用 Node v16.20.2,通过;320/375/430px 合成页面预览无横向溢出,并查看进度状态和分享封面。原有16个无关文件哈希未变化,后端没有本次修改。
|
||||
|
||||
开发者工具启动过程:自动化 SDK 在 Node24 启动 bat 失败,改用官方 CLI;游客 AppID 不被当前工具接受,改用仓库已有 AppID,仍仅加载独立禁网探针。探针分包注册失败后改成普通页面入口完成验证。测试后关闭探针项目。未上传、未部署、未更新 main 验收包;改动保留当前 feature,包含此前邀请列表详情入口。
|
||||
|
||||
2026-11-02 用户确认编译本轮修复进行验收。按既有授权合并本地 main 后编译,新制品和精确提交记录见 releases/20261102-poster-fix-main。未执行上传或部署。
|
||||
|
||||
@ -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-v4');index['43-personal'].savedAt=Date.now()-2*24*3600*1000;h.storage.set('fulian-sharing-cache-v4',index)
|
||||
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 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']){
|
||||
|
||||
20
tools/sharing/check-image-path.cjs
Normal file
20
tools/sharing/check-image-path.cjs
Normal file
@ -0,0 +1,20 @@
|
||||
const assert=require('assert'),{createHarness}=require('./harness.cjs')
|
||||
;(async()=>{
|
||||
const h=createHarness(),{canvasImagePath}=h.load('utils/sharing')
|
||||
assert.equal(canvasImagePath('pages/users/assets/fulian-logo.png','/pages/users/assets/fulian-logo.png'),'/pages/users/assets/fulian-logo.png')
|
||||
for(const p of ['wxfile://tmp_test','http://usr/test.png','/synthetic-user/a.png','https://example.invalid/a.png'])assert.equal(canvasImagePath(p,p),p)
|
||||
assert.throws(()=>canvasImagePath(''))
|
||||
for(const name of ['myShare','shareCard']){
|
||||
const test=createHarness();test.flags.packageRelative=true
|
||||
const page=test.page(name),steps=[],update=page.setPosterProgress;page.setPosterProgress=function(n,s){steps.push(n);update.call(this,n,s)}
|
||||
page.hook('onReady');await page.startSharing();assert(page.poster&&page.cover);assert.equal(page.posterProgress,100)
|
||||
assert(steps.includes(45)&&steps.includes(65)&&steps.includes(75)&&steps.includes(90));assert(steps.every((n,i)=>!i||n>=steps[i-1]))
|
||||
const share=page.definition.onShareAppMessage.call({...page,cover:''});assert(share.imageUrl.endsWith('-share-cover.png'))
|
||||
}
|
||||
const fail=createHarness();fail.flags.packageRelative=true;fail.flags.failImageSrc='/pages/users/assets/fulian-logo.png'
|
||||
const page=fail.page('myShare');page.hook('onReady');await page.startSharing();assert(!page.generating);assert.equal(page.posterProgress,25);assert(page.posterError.includes('品牌图片'))
|
||||
const qr=createHarness();qr.flags.route=o=>o.url.endsWith('/code')?{statusCode:500,data:{}}:null
|
||||
const qrPage=qr.page('myShare');qrPage.hook('onReady');await qrPage.startSharing()
|
||||
assert(!qrPage.poster&&qrPage.cover&&qrPage.posterError);assert.equal(qrPage.posterProgress,45)
|
||||
console.log('PASS: package-relative path reproduction fixed for both pages; native file URI preserved; stage progress and error stop; designed share fallback.')
|
||||
})().catch(e=>{console.error(e.stack);process.exitCode=1})
|
||||
@ -5,9 +5,9 @@ function createHarness(){
|
||||
const flags={},state={profile:{id:42,nickname:'合成小林',photo:'avatar-not-for-card',photos:['synthetic-photo'],city:'合成城市',degree:'本科',industry:'设计',introduction:'喜欢阅读和认真生活。'.repeat(20),ideal_mate:'真诚沟通'},overview:{total:12,verified:7,can_apply:true},abort:0,exportID:0}
|
||||
const copy=value=>value&&typeof value==='object'?JSON.parse(JSON.stringify(value)):value
|
||||
const later=fn=>queueMicrotask(fn)
|
||||
const ctx={fillRect(){},beginPath(){},moveTo(){},arcTo(){},closePath(){},fill(){},save(){},restore(){},clip(){},arc(){},stroke(){},drawImage(image){draws.push(['image',image.src])},fillText(text){draws.push(['text',text])},measureText(text){return {width:Array.from(text).length*25}}}
|
||||
const ctx={fillRect(){},beginPath(){},moveTo(){},lineTo(){},arcTo(){},closePath(){},fill(){},save(){},restore(){},clip(){},arc(){},stroke(){},drawImage(image){draws.push(['image',image.src])},fillText(text){draws.push(['text',text])},measureText(text){return {width:Array.from(text).length*25}}}
|
||||
const canvases={}
|
||||
function canvas(id){if(!canvases[id])canvases[id]={id,width:750,height:1120,getContext(){return ctx},createImage(){const image={width:500,height:500};Object.defineProperty(image,'src',{set(value){this.value=value;if(!flags.imageHang)later(()=>(flags.imageFail||flags.failImageSrc===value)?this.onerror():this.onload())},get(){return this.value}});return image}};return canvases[id]}
|
||||
function canvas(id){if(!canvases[id])canvases[id]={id,width:750,height:1120,getContext(){return ctx},createImage(){const image={width:500,height:500};Object.defineProperty(image,'src',{set(value){this.value=value;if(!flags.imageHang)later(()=>(flags.imageFail||flags.failImageSrc===value||(flags.packageRelative&&value.startsWith('pages/')))?this.onerror():this.onload())},get(){return this.value}});return image}};return canvases[id]}
|
||||
const fileSystem={
|
||||
access({path,success,fail}){later(()=>files.has(path)?success():fail({}))},
|
||||
writeFile({filePath,data,success,fail}){later(()=>{if(flags.writeFail)return fail({});files.set(filePath,data);success()})},
|
||||
@ -28,7 +28,7 @@ function createHarness(){
|
||||
options.success({statusCode:200,data:{code:0,data:copy(data)}})
|
||||
});return task},
|
||||
createSelectorQuery(){let id;const q={in(){return q},select(value){id=value;return q},fields(){return q},exec(cb){if(!flags.canvasHang)later(()=>cb(flags.canvasMissing?[]:[{node:canvas(id)}]))}};return q},
|
||||
getImageInfo({src,success,fail}){if(!flags.imageInfoHang)later(()=>flags.imageInfoFail?fail({}):success({path:src,width:500,height:500}))},
|
||||
getImageInfo({src,success,fail}){if(!flags.imageInfoHang)later(()=>flags.imageInfoFail?fail({}):success({path:flags.packageRelative&&src.startsWith('/pages/')?src.slice(1):src,width:500,height:500}))},
|
||||
canvasToTempFilePath({canvas,success,fail}){if(!flags.exportHang)later(()=>{if(flags.exportFail)return fail({});const file='/tmp/export-'+(++state.exportID)+'.png';files.set(file,'rendered-'+canvas.id);success({tempFilePath:file})})},
|
||||
saveImageToPhotosAlbum({success,fail}){later(()=>flags.saveFail?fail({}):success({}))},showToast(value){calls.push({toast:value})},showModal(value){calls.push({modal:value})},previewImage(value){calls.push({preview:value})}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user