Files
sundynix-pets/pets-fe/pages/mine/mine.wxml
T
Blizzard cbfcaada55 feat: 宠物身份卡(canvas 绘制 + 小程序码 + 存相册/分享)
竞品那张「居民身份卡」,完整做了。

## 后端
service/wxacode.go —— 小程序码。两步:先拿 access_token(2h 有效,进程内缓存、
提前 5 分钟过期),再调 getwxacodeunlimit 换一张 PNG 存到 MinIO 返回 URL。
同一只宠物的码 object 名按 pet id 定死,存在就直接返回不重新生成 ——
微信侧有日调用配额,不能每次点都烧一次。

⚠️ getwxacodeunlimit 要求 page 在**已发布**的小程序版本里。开发/未发布时返回
errcode 41030,实测确认。这时身份卡照常出、只是没码(qr_ready=false),
发布后自动就有,不用改代码。best-effort,取不到不算错。

service/idcard.go —— 组装卡片内容。身份 ID 用「宠物雪花 id 的数字 + 生日」
拼成 18 位、每 4 位空一下,纯装饰不入库不校验,同一只每次都一样。
介绍按物种给默认(汪星人/喵星人来地球啦~)。有效期用生日起算。

GET /api/pets/:id/id-card
storage 加了 Exists(StatObject),生成前先查。

## 前端 pages/idcard
离屏 canvas 2d 画卡,导出成图展示 + 存相册 + 分享(open-type=share,
imageUrl 用画好的卡当封面)。

两张远程图(头像 + 小程序码)都用 node.createImage() 异步加载,
**Promise.all 等两张都就绪再开画** —— 否则小程序码会画不上或画一半
(规划里专门点过名的坑)。任一张加载失败 resolve(null),缺图也照画:
头像缺→画物种 emoji,码缺→画「小程序码 待发布」占位框。

存相册的权限被拒过一次后 wx.authorize 不再弹,走 openSetting 引导(同海报那套)。

绘制复用了 bottom-sheet 里 poster 的 canvas 模式(dpr 放大、roundRect、
canvasToTempFilePath),但 poster 只画文字不画远程图,这里的双图加载是新写的。

入口:「我的」→ 身份卡,用当前选中的宠物。

## 验证(预生产库实跑)
  id-card 接口返回:姓名/性别/品种/生日/介绍/身份ID/有效期 全对
  身份 ID = 3410 4333 1420 2503 17(宠物id数字+生日,稳定)
  qr_ready = false,日志确认 41030 invalid page(小程序未发布,预期)
  canvas 版式用 HTML 复刻确认过:未发布=占位、发布后=真码

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 16:48:16 +08:00

58 lines
3.0 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<nav-bar title="我的"></nav-bar>
<scroll-view class="page-scroll" scroll-y="{{true}}" enhanced="{{true}}" show-scrollbar="{{false}}">
<view class="page-body tab-nofab">
<!-- 和「宠友主页」同一个组件。自己看到的就是别人看到的,装扮才有意义 -->
<profile-head card="{{card}}" bind:relations="goRelations"
bind:decorate="goDecorate" bind:pet="onPickPet"></profile-head>
<view class="btn btn-ghost btn-block me-preview" bindtap="previewPublic">
<pt-icon name="user" size="{{30}}"></pt-icon>看看别人眼里的我
</view>
<!-- 我的毛孩子 -->
<view class="card">
<view class="section-head">
<view class="sh-title">我的毛孩子</view>
<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-info">
<view class="tt-b">{{item.name}}</view>
<view class="tt-p">{{item.age}}{{item.weight}}{{item.stage}}</view>
</view>
<view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view>
</view>
<view wx:if="{{!pets.length}}" class="empty">还没有毛孩子,先建一份档案</view>
</view>
<!-- 常用入口。设置类的都收进设置页了,这里只留高频的 -->
<view class="profile-list">
<view class="profile-row" bindtap="goPetForm">
<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>
</view>
<view class="profile-row" data-type="managePets" bindtap="openSheet">
<view class="pr-ic"><pt-icon name="community" size="{{34}}"></pt-icon></view>
<text class="pr-label">多宠物管理</text><text class="pr-val">{{card.stats.pet_count || 0}} 只</text>
<view class="pr-arrow"><pt-icon name="next" size="{{28}}"></pt-icon></view>
</view>
<view class="profile-row" bindtap="goSettings">
<view class="pr-ic"><pt-icon name="settings" 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>
</view>
</scroll-view>
<bottom-sheet show="{{sheetShow}}" type="{{sheetType}}" bind:close="closeSheet"></bottom-sheet>