style(fe): 照护卡改证件版式 — 大字「X星人地球移民/居民身份卡」左对齐

- 去掉蓝色标题条,照护卡整体换淡紫底
- 标题两行左对齐:小字带字距「汪星人/喵星人/宠星人地球移民」+ 大字「居民身份卡」
- 按物种动态取字:狗→汪星人、猫→喵星人、其它→宠星人
- 发证机构→签发机构、有效期→有效期限;小程序码落到右下角淡紫底上
- 新增 spacedText 逐字字距工具

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-31 18:35:11 +08:00
parent 59412010dc
commit 473c9f9200
+49 -45
View File
@@ -213,72 +213,67 @@ Page({
// 正面印章(右上角)
this.drawStamp(ctx, fx + cardW2 - 78, fY + 82);
// ══════════ 照护卡(白 + 蓝)══════════
const bY = fY + fH + 40, bH = 566;
// ══════════ 照护卡(淡紫,仿证件版式)══════════
const bY = fY + fH + 40, bH = 630;
const gbc = ctx.createLinearGradient(fx, bY, fx, bY + bH);
gbc.addColorStop(0, '#E6E7FB');
gbc.addColorStop(1, '#F2F0FA');
this.roundRect(ctx, fx, bY, cardW2, bH, 30);
ctx.fillStyle = '#FFFFFF';
ctx.fillStyle = gbc;
ctx.fill();
// 蓝色标题(两行「X星地球移民 / 居民身份卡」
// 标题(两行左对齐):小字「X星地球移民」+ 大字「居民身份卡」
// 汪星人=狗、喵星人=猫,其它兜底宠星人
const star = c.species === '狗狗' ? '汪' : c.species === '猫猫' ? '喵' : '宠';
const hb = 92;
const gb = ctx.createLinearGradient(fx, bY, fx + cardW2, bY);
gb.addColorStop(0, '#5AA0E8');
gb.addColorStop(1, '#7FB6F0');
this.roundRect(ctx, fx + 24, bY + 24, cardW2 - 48, hb, 18);
ctx.fillStyle = gb;
ctx.fill();
ctx.textAlign = 'center';
ctx.fillStyle = '#fff';
ctx.font = '700 30px sans-serif';
ctx.fillText(star + '星地球移民', W / 2, bY + 24 + 40);
ctx.font = '700 26px sans-serif';
ctx.fillText('居民身份卡', W / 2, bY + 24 + 76);
ctx.textAlign = 'left';
ctx.fillStyle = '#8A90AE';
ctx.font = '600 27px sans-serif';
this.spacedText(ctx, star + '星人地球移民', fx + 44, bY + 82, 8);
ctx.fillStyle = '#3E4A63';
ctx.font = '800 66px sans-serif';
ctx.fillText('居民身份卡', fx + 44, bY + 168);
// 照护行(标签 + 值,不放图标
// 资料行(标签 + 值)
const rowX = fx + 44;
const kvB = (label, value, yy) => {
ctx.textAlign = 'left';
ctx.font = '24px sans-serif';
ctx.fillStyle = '#8D8277';
ctx.font = '25px sans-serif';
ctx.fillStyle = '#9A9FB5';
ctx.fillText(label, rowX, yy);
ctx.font = '600 25px sans-serif';
ctx.fillStyle = '#2D2925';
ctx.fillText(value || '—', rowX + 150, yy);
ctx.font = '600 26px sans-serif';
ctx.fillStyle = '#3E4A63';
ctx.fillText(value || '—', rowX + 160, yy);
};
let ry = bY + 140;
kvB('主人', c.owner, ry); ry += 48;
kvB('联系电话', c.phone || '未验证', ry); ry += 48;
kvB('备注', c.notes || '无', ry); ry += 48;
kvB('发机构', c.org || '肉垫计划', ry); ry += 48;
kvB('有效期', c.validity || '永久有效', ry); ry += 42;
let ry = bY + 244;
kvB('主人', c.owner, ry); ry += 52;
kvB('联系电话', c.phone || '未验证', ry); ry += 52;
kvB('备注', c.notes || '无', ry); ry += 52;
kvB('发机构', c.org || '肉垫计划', ry); ry += 52;
kvB('有效期', c.validity || '永久有效', ry);
// 走失联系框 + 小程序码
const lostY = ry + 16, lostH = bY + bH - lostY - 30;
this.roundRect(ctx, fx + 24, lostY, cardW2 - 48, lostH, 18);
ctx.fillStyle = '#EAF3FE';
ctx.fill();
ctx.textAlign = 'left';
ctx.fillStyle = '#5B7A9C';
ctx.font = '22px sans-serif';
ctx.fillText('微信扫一扫,查看' + (c.name || '') + '的身份信息', fx + 44, lostY + lostH / 2 + 8);
// 小程序码
const qrD = 118, qrX = fx + cardW2 - 24 - 20 - qrD, qrY = lostY + (lostH - qrD) / 2;
// 小程序码(右下角,直接落在淡紫底上)
const qrD = 128, qrX = fx + cardW2 - 44 - qrD, qrY = bY + bH - 40 - qrD;
ctx.fillStyle = '#fff';
this.roundRect(ctx, qrX, qrY, qrD, qrD, 12);
this.roundRect(ctx, qrX, qrY, qrD, qrD, 14);
ctx.fill();
if (qr) {
ctx.drawImage(qr, qrX + 8, qrY + 8, qrD - 16, qrD - 16);
ctx.drawImage(qr, qrX + 9, qrY + 9, qrD - 18, qrD - 18);
} else {
ctx.fillStyle = '#9CB4CC';
ctx.fillStyle = '#A7ABC4';
ctx.font = '18px sans-serif';
ctx.textAlign = 'center';
ctx.fillText('小程序码', qrX + qrD / 2, qrY + qrD / 2 - 2);
ctx.fillText('待发布', qrX + qrD / 2, qrY + qrD / 2 + 24);
}
// 扫码说明(左侧,和小程序码同高)
ctx.textAlign = 'left';
ctx.fillStyle = '#8A90AE';
ctx.font = '22px sans-serif';
ctx.fillText('微信扫一扫', fx + 44, qrY + qrD / 2 - 6);
ctx.fillText('查看' + (c.name || '') + '的身份信息', fx + 44, qrY + qrD / 2 + 28);
// 背面印章(右上角,压在蓝条右侧
this.drawStamp(ctx, fx + cardW2 - 78, bY + 70);
// 背面印章(右上角)
this.drawStamp(ctx, fx + cardW2 - 84, bY + 92);
// 只导出到实际内容底部,避免卡片下面拖一条空白
const dpr = this._dpr || 2;
@@ -310,6 +305,15 @@ Page({
ctx.closePath();
},
// 逐字加字距地画一行(canvas 的 letterSpacing 各端支持不一,手动排最稳)
spacedText(ctx, text, x, y, gap) {
let cx = x;
for (const ch of text) {
ctx.fillText(ch, cx, y);
cx += ctx.measureText(ch).width + gap;
}
},
// 五角星路径(调用方负责 fill/stroke
star(ctx, cx, cy, rO, rI, n) {
ctx.beginPath();