Add one-way task completion and exportable pet identity cards
ci-cd / verify (push) Successful in 1m31s
ci-cd / deploy (push) Skipped

This commit is contained in:
2026-10-05 11:32:32 +08:00
parent f0ee63e642
commit cb4ffa13e9
21 changed files with 999 additions and 117 deletions
+42
View File
@@ -0,0 +1,42 @@
const { profile, renderCard, saveToAlbum } = require("../../utils/pet-card");
const { toast } = require("../../utils/api");
Component({
properties: { pet: { type: Object, value: {} } },
data: { card: {}, saving: false },
observers: {
pet(value) {
this.setData({ card: profile(value || {}) });
},
},
methods: {
edit() {
this.triggerEvent("edit", { id: this.data.pet.id });
},
async generate() {
const canvas = await new Promise((resolve, reject) =>
this.createSelectorQuery()
.select("#card-canvas")
.fields({ node: true, size: true })
.exec((result) =>
result[0] && result[0].node
? resolve(result[0].node)
: reject(new Error("卡片还未准备好,请重试")),
),
);
return renderCard(canvas, this.data.pet);
},
async save() {
if (this.data.saving) return;
this.setData({ saving: true });
try {
const path = await this.generate();
if (await saveToAlbum(path))
wx.showToast({ title: "身份卡已保存到相册", icon: "success" });
} catch (error) {
toast(error);
} finally {
this.setData({ saving: false });
}
},
},
});
@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": { "app-icon": "/components/app-icon/index" }
}
+10
View File
@@ -0,0 +1,10 @@
<view class="identity">
<view class="id-top"><view class="brand"><image src="/assets/logo-corgi-v2.png" /><view><text class="brand-name">肉垫计划</text><text class="brand-en">PAWPRINT ID</text></view></view><text class="id-title">毛孩身份卡</text></view>
<view class="id-body" bindtap="edit" aria-role="button" aria-label="编辑宠物档案">
<view class="portrait"><image wx:if="{{card.avatar_url}}" src="{{card.avatar_url}}" mode="aspectFill" /><app-icon wx:else size="80" color="#d18b67" /></view>
<view class="details"><text class="pet-name">{{card.name}}</text><text class="breed">{{card.breed||'品种未填写'}} · {{card.genderLabel}}</text><view class="facts"><view><text class="label">生日</text><text class="value">{{card.birthday}}</text></view><view><text class="label">体重</text><text class="value">{{card.weightLabel}}</text></view></view><view class="coat"><text class="label">毛色</text><text class="value">{{card.coat||'未填写'}}</text></view></view>
</view>
<view class="id-bottom"><view><text class="label">陪伴编号</text><text class="number">{{card.cardNumber}}</text></view><text class="motto">把陪伴,认真记下来</text></view>
</view>
<view class="card-actions"><button bindtap="edit">编辑档案</button><button class="save" bindtap="save" loading="{{saving}}" disabled="{{saving}}">{{saving?'正在生成…':'保存身份卡'}}</button></view>
<canvas type="2d" id="card-canvas" class="export-canvas" />
+164
View File
@@ -0,0 +1,164 @@
:host {
display: block;
margin-bottom: 30rpx;
}
.identity {
position: relative;
overflow: hidden;
border-radius: 28rpx;
border: 1rpx solid #ead8ca;
padding: 25rpx;
background: linear-gradient(125deg, #fff3e5, #f0e9f8);
box-shadow: 0 14rpx 35rpx #79543a0b;
color: #604a40;
}
.id-top {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #dfcdbf;
}
.brand {
display: flex;
gap: 9rpx;
align-items: center;
}
.brand image {
width: 44rpx;
height: 44rpx;
}
.brand-name,
.brand-en {
display: block;
}
.brand-name {
font-size: 18rpx;
font-weight: 600;
color: #795a4b;
}
.brand-en {
font-size: 11rpx;
letter-spacing: 2rpx;
color: #a48d7e;
}
.id-title {
font-size: 23rpx;
letter-spacing: 3rpx;
font-weight: 600;
}
.id-body {
display: flex;
gap: 24rpx;
padding: 24rpx 0;
}
.portrait {
width: 166rpx;
height: 180rpx;
flex: none;
border: 6rpx solid #fffaf4;
border-radius: 18rpx;
overflow: hidden;
background: #fffaf4;
display: flex;
align-items: center;
justify-content: center;
}
.portrait image {
width: 100%;
height: 100%;
}
.details {
flex: 1;
min-width: 0;
}
.pet-name {
display: block;
font-size: 34rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.breed {
display: block;
font-size: 21rpx;
color: #886c5f;
margin: 2rpx 0 12rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.facts {
display: flex;
gap: 22rpx;
}
.facts > view {
flex: 1;
min-width: 0;
}
.label {
display: block;
font-size: 17rpx;
color: #a48d7e;
}
.value {
display: block;
font-size: 21rpx;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.coat {
margin-top: 9rpx;
}
.id-bottom {
border-top: 1rpx solid #dfcdbf;
padding-top: 15rpx;
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 10rpx;
}
.number {
display: block;
font-size: 18rpx;
letter-spacing: 1rpx;
color: #795a4b;
}
.motto {
font-size: 16rpx;
color: #886c5f;
max-width: 200rpx;
text-align: right;
}
.card-actions {
display: flex;
justify-content: flex-end;
gap: 16rpx;
margin-top: 12rpx;
}
.card-actions button {
margin: 0;
padding: 9rpx 20rpx;
border: 1rpx solid #ead8ca;
border-radius: 99rpx;
background: #fffaf4;
color: #886c5f;
font-size: 22rpx;
line-height: 1.5;
}
.card-actions .save {
background: #fff0e2;
color: #b95738;
border-color: #f1d6c4;
}
.export-canvas {
position: fixed;
left: -2000px;
top: 0;
width: 600px;
height: 380px;
pointer-events: none;
}