feat(fe): 宠物照片替代默认头像,身份卡移到首页门面卡

- 首页/我的/切换条:上传过照片就用照片,没有才退回 emoji
- 报告页分享海报:先异步加载头像再开画,画成圆形头像
- 一起生活的天数改成从到家日期算,没填就显示「-」并可点去补
- 身份卡入口从「我的」页移到首页门面卡右上角

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-30 17:08:32 +08:00
parent cbfcaada55
commit 11c4d490e6
10 changed files with 79 additions and 22 deletions
@@ -207,6 +207,18 @@ Component({
const pet = this.data.pet || {};
const p = this.data.poster || {};
// 头像是远程图,异步加载。加载完(或没有头像)再往下画,
// 否则头像会画不上。加载失败当没有头像处理
const loadAvatar = () =>
new Promise((res) => {
if (!pet.avatar_url) return res(null);
const img = node.createImage();
img.onload = () => res(img);
img.onerror = () => res(null);
img.src = pet.avatar_url;
});
loadAvatar().then((avatar) => {
const bg = ctx.createLinearGradient(0, 0, W, H);
bg.addColorStop(0, '#FFF7E8');
bg.addColorStop(1, '#FFFFFF');
@@ -220,9 +232,22 @@ Component({
ctx.font = '600 40px sans-serif';
ctx.fillText((pet.name || '毛孩子') + ' 的成长报告', W / 2, 96);
ctx.font = '96px sans-serif';
ctx.fillText(pet.emoji || '🐾', W / 2, 216);
// 上传过照片就画圆形头像,没有才退回 emoji(默认头像太丑)
const avD = 132, avX = (W - avD) / 2, avY = 132;
if (avatar) {
ctx.save();
ctx.beginPath();
ctx.arc(W / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
ctx.drawImage(avatar, avX, avY, avD, avD);
ctx.restore();
} else {
ctx.font = '96px sans-serif';
ctx.fillText(pet.emoji || '🐾', W / 2, 216);
}
ctx.textAlign = 'center';
ctx.fillStyle = '#8D8277';
ctx.font = '24px sans-serif';
ctx.fillText([pet.age, pet.weight, pet.stage].filter(Boolean).join(' | '), W / 2, 268);
@@ -255,7 +280,7 @@ Component({
ctx.font = '22px sans-serif';
ctx.fillText('生成自 · 肉垫计划', W / 2, 780);
// canvas 2d 是同步绘制,画完直接导出
// 头像加载完(或没有)才走到这里,此刻画布已完整
wx.canvasToTempFilePath(
{
canvas: node,
@@ -265,6 +290,7 @@ Component({
},
this,
);
}); // loadAvatar().then
});
});
},
@@ -2,7 +2,10 @@
<view wx:for="{{pets}}" wx:key="id"
class="pet-chip {{item.id === currentId ? 'active' : ''}}"
data-id="{{item.id}}" bindtap="onPick">
<view class="mini-avatar">{{item.emoji}}</view>{{item.name}}
<view class="mini-avatar">
<image wx:if="{{item.avatar_url}}" class="mini-avatar-img" src="{{item.avatar_url}}" mode="aspectFill"></image>
<block wx:else>{{item.emoji}}</block>
</view>{{item.name}}
</view>
<view wx:if="{{showAdd}}" class="pet-chip" bindtap="onAdd">
<view class="mini-avatar"><pt-icon name="plus" size="{{30}}"></pt-icon></view>添加
@@ -3,3 +3,5 @@
}
.pet-switch .pet-chip{margin-right:20rpx}
.pet-switch .pet-chip:last-child{margin-right:0}
.mini-avatar{overflow:hidden}
.mini-avatar-img{width:100%;height:100%;display:block}
+10
View File
@@ -199,6 +199,16 @@ Page({
},
// 去建档页改阶段。改完回来 onShow 会重新拉一次漂移状态,提示自然消失。
// 不在首页直接改:改阶段之后要不要套新方案是同一件事,放在一起做才连贯
// 点「一起生活的天数」空态,去建档页补到家日期
onDaysTap() {
const id = this.data.pet && this.data.pet.id;
if (!this.data.daysTogether && id) wx.navigateTo({ url: '/pages/petform/petform?id=' + id });
},
// 身份卡:门面卡右上角进,用当前宠物
goIDCard() {
const id = this.data.pet && this.data.pet.id;
if (id) wx.navigateTo({ url: '/pages/idcard/idcard?id=' + id });
},
goStageUpdate() {
const id = this.data.pet && this.data.pet.id;
if (id) wx.navigateTo({ url: '/pages/petform/petform?id=' + id });
+8 -2
View File
@@ -21,9 +21,15 @@
<text wx:if="{{zodiac}}">{{zodiac}}</text>
</view>
</view>
<view class="hero-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view>
<view wx:if="{{pet.id}}" class="hero-idcard" catchtap="goIDCard">
<pt-icon name="user" size="{{30}}"></pt-icon>身份卡
</view>
</view>
<!-- 一起生活的天数从到家日算。没填到家日就显示「-」,点一下去补 -->
<view wx:if="{{pet.id}}" class="hero-days {{daysTogether ? '' : 'hero-days-empty'}}" catchtap="onDaysTap">
<block wx:if="{{daysTogether}}">我们一起生活的第 <text class="hd-n">{{daysTogether}}</text> 天</block>
<block wx:else>还没填到家日期,一起生活的天数显示 <text class="hd-n">-</text></block>
</view>
<view wx:if="{{daysTogether}}" class="hero-days">我们一起生活的第 <text class="hd-n">{{daysTogether}}</text> 天</view>
</view>
<!-- 阶段漂移。系统只负责发现,换不换由用户点 —— 偷偷换会让他莫名发现
+13
View File
@@ -87,3 +87,16 @@
.drift-focus{font-size:var(--fs-sm);color:var(--primary-ink);line-height:1.6;margin-top:6rpx}
.drift-size{font-size:var(--fs-cap);color:var(--primary-dark);opacity:.8;margin-top:6rpx}
.drift-cta{margin-top:var(--sp-4)}
/* 没填到家日期时的天数空态:压成灰、可点去补 */
.hero-days-empty{background:var(--surface-2);color:var(--muted)}
.hero-days-empty .hd-n{color:var(--muted)}
/* 门面卡右上角的身份卡入口。原来那里是个 next 箭头(点整卡进编辑),
身份卡做成小胶囊,和整卡点击分开 */
.hero-idcard{
flex:none;align-self:flex-start;display:flex;align-items:center;gap:4rpx;
padding:8rpx var(--sp-3);border-radius:var(--r-full);
background:var(--primary-soft);color:var(--primary-dark);
font-size:var(--fs-cap);font-weight:var(--fw-b);
}
-6
View File
@@ -56,12 +56,6 @@ Page({
goDecorate() {
wx.navigateTo({ url: '/pages/decorate/decorate' });
},
// 身份卡:当前这只的
goIDCard() {
const id = store.currentPetId();
if (!id) return wx.showToast({ title: '先选一只毛孩子', icon: 'none' });
wx.navigateTo({ url: '/pages/idcard/idcard?id=' + id });
},
goPlan() {
wx.navigateTo({ url: '/pages/plan/plan' });
},
+4 -5
View File
@@ -17,7 +17,10 @@
<view class="link" bindtap="onAddPet"> 添加</view>
</view>
<view wx:for="{{pets}}" wx:key="id" class="pet-row" data-id="{{item.id}}" bindtap="onPickPet">
<view class="pet-emoji">{{item.emoji}}</view>
<view class="pet-emoji">
<image wx:if="{{item.avatar_url}}" class="pet-emoji-img" src="{{item.avatar_url}}" mode="aspectFill"></image>
<block wx:else>{{item.emoji}}</block>
</view>
<view class="pet-info">
<view class="tt-b">{{item.name}}</view>
<view class="tt-p">{{item.age}}{{item.weight}}{{item.stage}}</view>
@@ -33,10 +36,6 @@
<view class="pr-ic"><pt-icon name="user" size="{{34}}"></pt-icon></view>
<text class="pr-label">宠物档案</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view>
</view>
<view class="profile-row" bindtap="goIDCard">
<view class="pr-ic"><pt-icon name="user" size="{{34}}"></pt-icon></view>
<text class="pr-label">身份卡</text><text class="pr-val">可保存、分享</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view>
</view>
<view class="profile-row" bindtap="goPlan">
<view class="pr-ic"><pt-icon name="plan" size="{{34}}"></pt-icon></view>
<text class="pr-label">养护计划</text><view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view>
+2 -1
View File
@@ -1,7 +1,7 @@
.pet-row{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-3) 0;border-bottom:1rpx solid var(--line)}
.pet-row:last-child{border-bottom:0}
.pet-emoji{
.pet-emoji{overflow:hidden;
width:76rpx;height:76rpx;border-radius:var(--r-sm);flex:none;
display:flex;align-items:center;justify-content:center;
background:var(--primary-soft);font-size:40rpx;
@@ -9,3 +9,4 @@
.pet-info{flex:1;min-width:0}
.me-preview{margin-bottom:var(--sp-5)}
.pet-emoji-img{width:100%;height:100%;display:block}
+7 -4
View File
@@ -23,13 +23,16 @@ function zodiac(birthday) {
// 一起生活了多少天。优先用到家日期——「我们一起生活的第 N 天」说的是
// 它到你家之后的日子,不是它出生之后的日子。没填到家日就退回建档日。
// 一起生活了多少天,只从「到家日期」算 —— 没填到家日就返回 null(首页显示「-」)。
// 原来会退回建档日兜底,但那是「在这个 App 上建档的日子」,不是「它到你家的日子」,
// 领养多年后才装 App 的会算出个很小的数,反而误导
function daysTogether(pet) {
const from = (pet && (pet.arrived_at || pet.created_at)) || '';
if (!from) return 0;
const from = (pet && pet.arrived_at) || '';
if (!from) return null;
const d = new Date(String(from).slice(0, 10) + 'T00:00:00');
if (isNaN(d.getTime())) return 0;
if (isNaN(d.getTime())) return null;
const days = Math.floor((Date.now() - d.getTime()) / 86400000) + 1;
return days > 0 ? days : 0;
return days > 0 ? days : null;
}
// 性别徽章。后端存的是「男孩 / 女孩 / 不确定」