feat(fe+be): 身份卡加公章、修性别符号、电话显示全、照护卡标题两行

- 修正性别 ♂/♀:用名字字号量宽度再定位,不再怼到名字上(之前用小字号量偏了)
- 正反两张卡右上角各盖一枚「肉垫计划 · 官方认证」红色公章(双圈+五角星,微旋转半透明)
- 照护卡标题改成两行「X星地球移民 / 居民身份卡」(按猫狗取 喵/汪/宠)
- 正面「物ID」改为「ID」
- 后端 IDCard.Phone 返回完整手机号,不再脱敏

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-31 18:32:09 +08:00
parent 4e3859227e
commit 59412010dc
2 changed files with 67 additions and 11 deletions
+1 -1
View File
@@ -159,7 +159,7 @@ func (s *Service) GetIDCard(userID, petID string) (*IDCard, error) {
Birthday: bd, Age: pet.Age, Days: days, Zodiac: zodiac, Weight: pet.Weight, Intro: intro, Birthday: bd, Age: pet.Age, Days: days, Zodiac: zodiac, Weight: pet.Weight, Intro: intro,
Color: pet.Color, Personality: pet.Personality, Color: pet.Color, Personality: pet.Personality,
AvatarURL: pet.AvatarURL, IDNo: petIDNo(pet), Validity: validity, AvatarURL: pet.AvatarURL, IDNo: petIDNo(pet), Validity: validity,
Owner: owner, Phone: maskPhone(user.Phone), Allergy: allergy, Notes: pet.Notes, Owner: owner, Phone: user.Phone, Allergy: allergy, Notes: pet.Notes,
VaccineState: vaccineState, DewormState: dewormState, Org: "肉垫计划", VaccineState: vaccineState, DewormState: dewormState, Org: "肉垫计划",
} }
+66 -10
View File
@@ -135,16 +135,20 @@ Page({
ctx.lineWidth = 8; ctx.lineWidth = 8;
ctx.stroke(); ctx.stroke();
// 名字 + 性别(头像右侧) // 名字 + 性别(头像右侧)。性别符号必须用「名字的字号」量宽度再定位,
// 否则换成小字号后 measureText 量出的宽度偏小,符号会怼到名字上
const nx = avX + avD + 28; const nx = avX + avD + 28;
const nm = c.name || '毛孩子';
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillStyle = '#2D2925'; ctx.fillStyle = '#2D2925';
ctx.font = '700 52px sans-serif'; ctx.font = '700 52px sans-serif';
ctx.fillText(c.name || '毛孩子', nx, avY + 44); ctx.fillText(nm, nx, avY + 46);
const nmW = ctx.measureText(nm).width;
if (c.gender) { if (c.gender) {
ctx.fillStyle = c.gender === '女孩' ? '#E86A9B' : '#5B9BE8'; const isF = c.gender === '女孩';
ctx.font = '30px sans-serif'; ctx.fillStyle = isF ? '#E86A9B' : '#5B9BE8';
ctx.fillText(c.gender === '女孩' ? '♀' : '♂', nx + ctx.measureText(c.name || '毛孩子').width + 12, avY + 44); ctx.font = '600 34px sans-serif';
ctx.fillText(isF ? '♀' : '♂', nx + nmW + 16, avY + 44);
} }
// 头像右的资料行 // 头像右的资料行
const kvF = (label, value, yy) => { const kvF = (label, value, yy) => {
@@ -201,19 +205,23 @@ Page({
ctx.textAlign = 'left'; ctx.textAlign = 'left';
ctx.fillStyle = '#9C7A3E'; ctx.fillStyle = '#9C7A3E';
ctx.font = '22px sans-serif'; ctx.font = '22px sans-serif';
ctx.fillText('ID ' + (c.id_no || ''), fx + 34, fY + fH - 56); ctx.fillText('ID ' + (c.id_no || ''), fx + 34, fY + fH - 56);
if (c.days > 0) { if (c.days > 0) {
ctx.fillText('和主人一起生活的第 ' + c.days + ' 天', fx + 34, fY + fH - 22); ctx.fillText('和主人一起生活的第 ' + c.days + ' 天', fx + 34, fY + fH - 22);
} }
// 正面印章(右上角)
this.drawStamp(ctx, fx + cardW2 - 78, fY + 82);
// ══════════ 照护卡(白 + 蓝)══════════ // ══════════ 照护卡(白 + 蓝)══════════
const bY = fY + fH + 40, bH = 566; const bY = fY + fH + 40, bH = 566;
this.roundRect(ctx, fx, bY, cardW2, bH, 30); this.roundRect(ctx, fx, bY, cardW2, bH, 30);
ctx.fillStyle = '#FFFFFF'; ctx.fillStyle = '#FFFFFF';
ctx.fill(); ctx.fill();
// 蓝色标题条 // 蓝色标题条(两行:「X星地球移民 / 居民身份卡」)
const hb = 64; const star = c.species === '狗狗' ? '汪' : c.species === '猫猫' ? '喵' : '宠';
const hb = 92;
const gb = ctx.createLinearGradient(fx, bY, fx + cardW2, bY); const gb = ctx.createLinearGradient(fx, bY, fx + cardW2, bY);
gb.addColorStop(0, '#5AA0E8'); gb.addColorStop(0, '#5AA0E8');
gb.addColorStop(1, '#7FB6F0'); gb.addColorStop(1, '#7FB6F0');
@@ -222,8 +230,10 @@ Page({
ctx.fill(); ctx.fill();
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillStyle = '#fff'; ctx.fillStyle = '#fff';
ctx.font = '700 28px sans-serif'; ctx.font = '700 30px sans-serif';
ctx.fillText('照护信息', W / 2, bY + 24 + 42); ctx.fillText(star + '星地球移民', W / 2, bY + 24 + 40);
ctx.font = '700 26px sans-serif';
ctx.fillText('居民身份卡', W / 2, bY + 24 + 76);
// 照护行(标签 + 值,不放图标) // 照护行(标签 + 值,不放图标)
const rowX = fx + 44; const rowX = fx + 44;
@@ -267,6 +277,9 @@ Page({
ctx.fillText('待发布', qrX + qrD / 2, qrY + qrD / 2 + 24); ctx.fillText('待发布', qrX + qrD / 2, qrY + qrD / 2 + 24);
} }
// 背面印章(右上角,压在蓝条右侧)
this.drawStamp(ctx, fx + cardW2 - 78, bY + 70);
// 只导出到实际内容底部,避免卡片下面拖一条空白 // 只导出到实际内容底部,避免卡片下面拖一条空白
const dpr = this._dpr || 2; const dpr = this._dpr || 2;
const contentH = bY + bH + pad; const contentH = bY + bH + pad;
@@ -297,6 +310,49 @@ Page({
ctx.closePath(); ctx.closePath();
}, },
// 五角星路径(调用方负责 fill/stroke
star(ctx, cx, cy, rO, rI, n) {
ctx.beginPath();
for (let i = 0; i < n * 2; i++) {
const r = i % 2 === 0 ? rO : rI;
const a = -Math.PI / 2 + (i * Math.PI) / n;
const x = cx + r * Math.cos(a), y = cy + r * Math.sin(a);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
},
// 红色公章:双圈 + 顶部五角星 + 「肉垫计划 / 官方认证」,略微旋转、半透明
drawStamp(ctx, cx, cy) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(-0.16);
ctx.globalAlpha = 0.68;
ctx.strokeStyle = '#CE4534';
ctx.fillStyle = '#CE4534';
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(0, 0, 52, 0, Math.PI * 2);
ctx.stroke();
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(0, 0, 43, 0, Math.PI * 2);
ctx.stroke();
this.star(ctx, 0, -23, 11, 4.6, 5);
ctx.fill();
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '700 25px sans-serif';
ctx.fillText('肉垫计划', 0, 10);
ctx.font = '13px sans-serif';
ctx.fillText('官方认证', 0, 32);
ctx.restore();
ctx.globalAlpha = 1;
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
},
// 微信取手机号:新版 getPhoneNumber 返回动态 code,后端换成真实手机号存库, // 微信取手机号:新版 getPhoneNumber 返回动态 code,后端换成真实手机号存库,
// 拿回后重新取卡(此时电话就有了)并重画 // 拿回后重新取卡(此时电话就有了)并重画
onGetPhone(e) { onGetPhone(e) {