From c85281736320fe880c6c1bcdcea5934adac73789 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 08:32:22 +0800 Subject: [PATCH 01/33] =?UTF-8?q?fix(fe):=20=E4=BF=AE=E7=A4=BE=E5=8C=BA?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E6=92=91=E7=A0=B4=E7=BD=91=E6=A0=BC=EF=BC=9B?= =?UTF-8?q?=E5=8F=91=E5=B8=96=E9=99=90=203=20=E5=BC=A0=EF=BC=9B=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=81=B6=E5=8F=91=E5=A4=B1=E8=B4=A5=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E9=87=8D=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .photo-tile 补 width:100%+min-width:0:微信 image 默认 320x240, 不给宽高会冲破 photo-grid(社区帖子图片"爆炸") - 发帖图片上限从 9 张改成 3 张 - uploadFile 三重兜底:冷启动没 token 先续期、40100 续期重试、 网络/网关瞬时错误自动重试一次(后端 MD5 去重,重传安全) Co-Authored-By: Claude Opus 5 --- pets-fe/app.wxss | 5 +- .../components/bottom-sheet/bottom-sheet.js | 4 +- pets-fe/utils/request.js | 56 ++++++++++++------- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss index b418ae5..3fb35a8 100644 --- a/pets-fe/app.wxss +++ b/pets-fe/app.wxss @@ -391,7 +391,10 @@ page{scrollbar-width:none;-ms-overflow-style:none} .photo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-1);margin-bottom:var(--sp-3)} .photo-tile{ - aspect-ratio:1;border-radius:var(--r-sm);background:var(--primary-soft); + /* 微信 image 默认 320x240,不显式给宽高会撑破网格。width:100% + min-width:0 + 让它缩进 1fr 格子,aspect-ratio 保持正方形,overflow 裁掉溢出 */ + width:100%;min-width:0;aspect-ratio:1;overflow:hidden; + border-radius:var(--r-sm);background:var(--primary-soft); display:flex;align-items:center;justify-content:center;font-size:56rpx; } diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js index e059234..9d9c383 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.js +++ b/pets-fe/components/bottom-sheet/bottom-sheet.js @@ -539,8 +539,8 @@ Component({ api.getTasks(id).then((tasks) => this.setData({ manageTasks: tasks || [] })).catch(() => {}); }, onPickPostImages() { - const left = 9 - this.data.postImages.length; - if (left <= 0) return wx.showToast({ title: '最多 9 张', icon: 'none' }); + const left = 3 - this.data.postImages.length; + if (left <= 0) return wx.showToast({ title: '最多 3 张', icon: 'none' }); upload .chooseAndUploadImages(left) .then((list) => this.setData({ postImages: this.data.postImages.concat(list) })) diff --git a/pets-fe/utils/request.js b/pets-fe/utils/request.js index bbfe6a5..4a62872 100644 --- a/pets-fe/utils/request.js +++ b/pets-fe/utils/request.js @@ -106,19 +106,34 @@ function request(options, _retried) { }); } -// 文件上传(multipart);token 过期同样先续期再重试一次 +// 文件上传(multipart)。三重兜底,应对「第一次报错、重试就好」的偶发: +// 1) 上传前若还没 token(冷启动竞态),先续期拿到再传; +// 2) token 过期(40100)→ 续期后重试; +// 3) 网络 fail / 网关 5xx(响应非 JSON)等瞬时错误 → 自动重试一次。 +// 后端按 MD5 去重,重复上传同一张图返回同一条记录,重试是安全的。 function uploadFile(filePath, _retried) { return new Promise((resolve, reject) => { - const token = getToken(); - wx.uploadFile({ - url: BASE_URL + '/api/upload', - filePath, - name: 'file', - timeout: 60000, - header: token ? { Authorization: 'Bearer ' + token } : {}, - success(res) { - try { - const body = JSON.parse(res.data); + // 瞬时错误:还没重试过就延后再传一次,否则直接失败 + const softRetry = (err) => { + if (_retried) return reject(err); + setTimeout(() => uploadFile(filePath, true).then(resolve, reject), 500); + }; + const fire = () => { + const token = getToken(); + wx.uploadFile({ + url: BASE_URL + '/api/upload', + filePath, + name: 'file', + timeout: 60000, + header: token ? { Authorization: 'Bearer ' + token } : {}, + success(res) { + let body; + try { + body = JSON.parse(res.data); + } catch (e) { + // 非 JSON 多为网关 5xx / 服务重启窗口,按瞬时错误重试 + return softRetry(new Error('上传失败,请重试')); + } if (body.code === 0) return resolve(body.data); if (body.code === 40100 && !_retried) { return recoverAuth() @@ -126,14 +141,17 @@ function uploadFile(filePath, _retried) { .catch(() => reject(new Error('登录已过期,请重进小程序'))); } reject(new Error(body.message || '上传失败')); - } catch (e) { - reject(new Error('上传响应解析失败')); - } - }, - fail(err) { - reject(new Error((err && err.errMsg) || '上传失败')); - }, - }); + }, + fail(err) { + softRetry(new Error((err && err.errMsg) || '上传失败')); + }, + }); + }; + // 冷启动竞态:点得太快、登录还没完成就上传,先把 token 拿到再传 + if (!getToken() && !_retried) { + return recoverAuth().then(fire, fire); + } + fire(); }); } -- 2.52.0 From f0ea05696cbe6c7eff06e995bb5e98460ebaeb69 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 08:33:55 +0800 Subject: [PATCH 02/33] =?UTF-8?q?feat(fe):=20=E7=A4=BE=E5=8C=BA=E5=B8=96?= =?UTF-8?q?=E5=AD=90=E5=9B=BE=E7=89=87=E7=82=B9=E5=87=BB=E6=94=BE=E5=A4=A7?= =?UTF-8?q?=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 缩略图保持网格展示,点击走 wx.previewImage 全屏预览, 可左右切换同帖多图(emoji 占位不参与)。 Co-Authored-By: Claude Opus 5 --- pets-fe/pages/community/community.js | 7 +++++++ pets-fe/pages/community/community.wxml | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pets-fe/pages/community/community.js b/pets-fe/pages/community/community.js index 001371b..c487a37 100644 --- a/pets-fe/pages/community/community.js +++ b/pets-fe/pages/community/community.js @@ -132,6 +132,13 @@ Page({ onCreatePost() { this.setData({ sheetType: 'createPost', sheetShow: true }); }, + // 点缩略图放大预览。只取 http 图(机器人帖的 emoji 占位不参与) + onPreviewImage(e) { + const { urls, cur } = e.currentTarget.dataset; + const list = (urls || []).filter((u) => typeof u === 'string' && u.indexOf('http') === 0); + if (!list.length) return; + wx.previewImage({ current: cur, urls: list }); + }, // 长按自己的帖子可删除 onLongPressPost(e) { const p = this.data.posts[e.currentTarget.dataset.index]; diff --git a/pets-fe/pages/community/community.wxml b/pets-fe/pages/community/community.wxml index e68084c..aaf8166 100644 --- a/pets-fe/pages/community/community.wxml +++ b/pets-fe/pages/community/community.wxml @@ -25,7 +25,8 @@ module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; } {{item.content}} - + {{img}} -- 2.52.0 From b01effaf5e4bd8c435f2ee5f8a3ef20fa5bea7c0 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 08:40:06 +0800 Subject: [PATCH 03/33] =?UTF-8?q?style(fe):=20=E7=A4=BE=E5=8C=BA=E5=B8=96?= =?UTF-8?q?=E5=AD=90=E5=9B=BE=E7=89=87=E6=94=B9=E5=9B=BA=E5=AE=9A=E5=B0=8F?= =?UTF-8?q?=E7=BC=A9=E7=95=A5=E5=9B=BE=EF=BC=88210rpx=20=E6=AD=A3=E6=96=B9?= =?UTF-8?q?=E5=BD=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aspect-ratio 在微信渲染不生效导致竖图被撑长。改成 flex-wrap + 显式 210rpx 见方的缩略图,小而统一,点击 previewImage 放大。 Co-Authored-By: Claude Opus 5 --- pets-fe/app.wxss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss index 3fb35a8..1f7307a 100644 --- a/pets-fe/app.wxss +++ b/pets-fe/app.wxss @@ -389,11 +389,11 @@ page{scrollbar-width:none;-ms-overflow-style:none} .pu-s{font-size:var(--fs-sm);color:var(--muted)} .post-content{font-size:var(--fs-md);line-height:1.62;color:var(--text-2);margin-bottom:var(--sp-3)} -.photo-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sp-1);margin-bottom:var(--sp-3)} +.photo-grid{display:flex;flex-wrap:wrap;gap:var(--sp-1);margin-bottom:var(--sp-3)} .photo-tile{ - /* 微信 image 默认 320x240,不显式给宽高会撑破网格。width:100% + min-width:0 - 让它缩进 1fr 格子,aspect-ratio 保持正方形,overflow 裁掉溢出 */ - width:100%;min-width:0;aspect-ratio:1;overflow:hidden; + /* 固定尺寸小缩略图,正方形。不用 aspect-ratio(微信渲染不生效,竖图会被撑长), + 显式 width/height 最稳;mode=aspectFill 裁成正方,点击再走 previewImage 放大 */ + width:210rpx;height:210rpx;flex:none;overflow:hidden; border-radius:var(--r-sm);background:var(--primary-soft); display:flex;align-items:center;justify-content:center;font-size:56rpx; } -- 2.52.0 From 603bd5773e6fdb9c550e3157c18469177ac6403b Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 08:49:20 +0800 Subject: [PATCH 04/33] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=E8=87=AA=E5=B7=B1=E5=B8=96=E5=AD=90=E7=9A=84=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 发帖后若未直接过审,弹窗明确「审核中,通过后才出现在社区」, 不再让用户以为发失败/是 bug - 后端 ListUserPosts:看自己主页时返回待审核/未通过的帖子(带状态), 看别人仍只返回已发布 - 个人主页帖子加「审核中/未通过」状态标 + 说明 - 我的页加「我的帖子」入口,进自己主页看审核状态 Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/user_profile.go | 9 ++++++++- pets-fe/components/bottom-sheet/bottom-sheet.js | 13 ++++++++++++- pets-fe/pages/mine/mine.js | 6 ++++++ pets-fe/pages/mine/mine.wxml | 7 ++++++- pets-fe/pages/user/user.js | 4 ++++ pets-fe/pages/user/user.wxml | 3 +++ pets-fe/pages/user/user.wxss | 13 +++++++++++++ 7 files changed, 52 insertions(+), 3 deletions(-) diff --git a/pets-be/internal/service/user_profile.go b/pets-be/internal/service/user_profile.go index e2233b3..3d4ae2d 100644 --- a/pets-be/internal/service/user_profile.go +++ b/pets-be/internal/service/user_profile.go @@ -188,7 +188,14 @@ func (s *Service) UpdateDecoration(userID string, bio, bgFileID, theme *string, // ListUserPosts 某人发布的帖子 func (s *Service) ListUserPosts(viewerID, userID string, offset, limit int) ([]model.Post, int64, error) { - q := s.db.Model(&model.Post{}).Where("user_id = ? AND status = ?", userID, "published") + q := s.db.Model(&model.Post{}).Where("user_id = ?", userID) + if viewerID == userID { + // 看自己:待审核/未通过的也要能看到(带状态展示),只藏软删除的 + q = q.Where("status <> ?", model.PostDeleted) + } else { + // 看别人:只放已过审的 + q = q.Where("status = ?", model.PostPublished) + } var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js index 9d9c383..238fb0b 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.js +++ b/pets-fe/components/bottom-sheet/bottom-sheet.js @@ -609,7 +609,7 @@ Component({ if (this.data.saving) return; this.setData({ saving: true }); try { - await api.createPost({ + const post = await api.createPost({ pet_id: identity === 'petName' ? store.currentPetId() : null, identity, content, @@ -618,6 +618,17 @@ Component({ }); this.triggerEvent('posted'); this.close(); + // 内容安全审核:没直接过审的,明确告诉用户在审核,别以为发失败/是 bug + if (post && post.status && post.status !== 'published') { + wx.showModal({ + title: '发布成功,审核中', + content: '内容需要通过安全审核后才会出现在社区。可在「我的 → 我的帖子」查看审核状态。', + showCancel: false, + confirmText: '知道了', + }); + } else { + wx.showToast({ title: '已发布', icon: 'success' }); + } } catch (e) { wx.showToast({ title: e.message || '发布失败', icon: 'none' }); this.setData({ saving: false }); diff --git a/pets-fe/pages/mine/mine.js b/pets-fe/pages/mine/mine.js index 1bab4e1..9b41413 100644 --- a/pets-fe/pages/mine/mine.js +++ b/pets-fe/pages/mine/mine.js @@ -39,6 +39,12 @@ Page({ const uid = this.data.user && this.data.user.id; if (uid) wx.navigateTo({ url: `/pages/user/user?id=${uid}` }); }, + // 我的帖子:进自己的主页,能看到待审核/未通过的帖子及状态 + goMyPosts() { + const uid = this.data.user && this.data.user.id; + if (!uid) return wx.showToast({ title: '请先登录', icon: 'none' }); + wx.navigateTo({ url: '/pages/user/user?id=' + uid }); + }, goSettings() { wx.navigateTo({ url: '/pages/settings/settings' }); }, diff --git a/pets-fe/pages/mine/mine.wxml b/pets-fe/pages/mine/mine.wxml index ea1d2ad..85a0d33 100644 --- a/pets-fe/pages/mine/mine.wxml +++ b/pets-fe/pages/mine/mine.wxml @@ -29,8 +29,13 @@ 还没有毛孩子,先建一份档案 - + + + + 我的帖子看审核状态 + + 设置 diff --git a/pets-fe/pages/user/user.js b/pets-fe/pages/user/user.js index 60c66be..a4da6c5 100644 --- a/pets-fe/pages/user/user.js +++ b/pets-fe/pages/user/user.js @@ -3,6 +3,8 @@ const store = require('../../utils/store.js'); const { toastErr } = require('../../utils/ui.js'); const TAG_CLASS = { 求助: 'warn', 经验: 'blue', 精选: 'purple', 避坑: 'red', 晒宠: '' }; +// 审核状态 → 展示文案(只在看自己主页时出现) +const STATUS_TEXT = { pending: '审核中', rejected: '未通过' }; function fmtAgo(iso) { if (!iso) return ''; @@ -47,6 +49,8 @@ Page({ author_emoji: p.author_emoji || '🐾', tag, tagClass: TAG_CLASS[tag] || '', + // 只有看自己主页才会拿到非「已发布」的帖子,给个审核状态标 + statusText: STATUS_TEXT[p.status] || '', }; }); const merged = page === 1 ? list : this.data.posts.concat(list); diff --git a/pets-fe/pages/user/user.wxml b/pets-fe/pages/user/user.wxml index 39b4d14..fee2bff 100644 --- a/pets-fe/pages/user/user.wxml +++ b/pets-fe/pages/user/user.wxml @@ -20,8 +20,11 @@ module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; } {{item.author_name}} {{item.timeText}} + {{item.statusText}} {{item.tag}} + 内容审核中,通过后其他人才能看到 + 未通过内容安全审核,仅你可见 {{item.content}} diff --git a/pets-fe/pages/user/user.wxss b/pets-fe/pages/user/user.wxss index dba8ff6..3e9ff8c 100644 --- a/pets-fe/pages/user/user.wxss +++ b/pets-fe/pages/user/user.wxss @@ -1,2 +1,15 @@ /* 头部整块(头像/昵称/签名/计数/养宠数据/宠物墙)已抽成 components/profile-head, 帖子卡走 app.wxss 的 .post-card。这里没有页面独有样式了。 */ + +/* 审核状态标(只在看自己主页出现) */ +.post-status{ + padding:2rpx var(--sp-2);border-radius:var(--r-full); + font-size:var(--fs-cap);font-weight:var(--fw-b);margin-right:var(--sp-2); +} +.post-status.st-pending{background:#FDECC8;color:#B26A00} +.post-status.st-rejected{background:var(--red-soft);color:var(--red-ink)} +.status-note{ + margin:0 0 var(--sp-2);padding:var(--sp-2) var(--sp-3);border-radius:var(--r-sm); + background:var(--surface-2);color:var(--muted);font-size:var(--fs-cap);line-height:1.5; +} +.status-note.st-note-red{background:var(--red-soft);color:var(--red-ink)} -- 2.52.0 From 96c92f2cfbd227500a5be4d7331a0414224a676c Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 09:03:27 +0800 Subject: [PATCH 05/33] =?UTF-8?q?feat(fe):=20=E4=BB=8A=E6=97=A5=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=AE=8C=E6=88=90=E8=B5=B0=E8=AE=B0=E5=BD=95=E8=A1=A8?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E5=8E=BB=E6=8E=89=E3=80=8C=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 去掉首页「全部完成」按钮,避免一键刷完不留记录 - 勾选带记录类型的任务 → 跳记录表单,保存成功后才回填任务完成, 返回首页 onShow 自动刷新任务勾选 + 最近记录(带 taskId 传参) - 只对未完成任务传 taskId,避免重复完成把已完成任务 toggle 回去 - 无记录类型的任务(如遛狗)仍直接勾选完成 Co-Authored-By: Claude Opus 5 --- pets-fe/pages/addrecord/addrecord.js | 8 +++++++- pets-fe/pages/home/home.js | 16 ++++++---------- pets-fe/pages/home/home.wxml | 1 - 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/pets-fe/pages/addrecord/addrecord.js b/pets-fe/pages/addrecord/addrecord.js index 5b6843e..bd7d4f6 100644 --- a/pets-fe/pages/addrecord/addrecord.js +++ b/pets-fe/pages/addrecord/addrecord.js @@ -41,6 +41,8 @@ Page({ }, onLoad(q) { const code = (q && q.type) || ''; + // 从首页「今日任务」勾选进来会带 taskId:这一笔记录保存后要把那条任务标完成 + this.taskId = (q && q.taskId) || ''; const d = today(); this.setData({ code, date: d, dateLabel: fmtDate(d) }); store @@ -151,9 +153,13 @@ Page({ this.setData({ saving: true }); api .createRecord(id, this.buildBody()) + .then(() => { + // 从任务勾选进来的:记录存好后把那条任务标完成(失败不影响记录已保存) + if (this.taskId) return api.toggleTask(this.taskId).catch(() => {}); + }) .then(() => { this.setData({ saving: false }); - wx.showToast({ title: '已记录', icon: 'success' }); + wx.showToast({ title: this.taskId ? '已完成' : '已记录', icon: 'success' }); setTimeout(() => wx.navigateBack(), 600); }) .catch((e) => { diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js index ee6953c..36b649a 100644 --- a/pets-fe/pages/home/home.js +++ b/pets-fe/pages/home/home.js @@ -172,11 +172,15 @@ Page({ onTapTask(e) { const i = e.currentTarget.dataset.index; const t = this.data.tasks[i]; - // 任务关联了记录类型就直接去记那一笔 + // 任务关联了记录类型:去记那一笔。未完成的带上 taskId, + // 在记录页保存成功后才回填这条任务完成(onShow 回来自动刷新任务+最近记录) if (t.sheet) { - wx.navigateTo({ url: '/pages/addrecord/addrecord?type=' + t.sheet }); + let url = '/pages/addrecord/addrecord?type=' + t.sheet; + if (!t.done) url += '&taskId=' + t.id; + wx.navigateTo({ url }); return; } + // 没有关联记录类型的任务(如遛狗),直接切换完成 api .toggleTask(t.id) .then((res) => { @@ -186,14 +190,6 @@ Page({ }) .catch((err) => toastErr(err, '操作失败')); }, - completeTasks() { - const id = store.currentPetId(); - if (!id) return; - api - .completeAllTasks(id) - .then((tasks) => this.setData({ tasks: (tasks || []).map(mapTask) })) - .catch((e) => toastErr(e, '操作失败')); - }, goPlan() { wx.navigateTo({ url: '/pages/plan/plan' }); }, diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml index dc86625..078bad3 100644 --- a/pets-fe/pages/home/home.wxml +++ b/pets-fe/pages/home/home.wxml @@ -71,7 +71,6 @@ 管理 - 全部完成 -- 2.52.0 From bb7db7876321a56544964a81d7aae8a11f33c115 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 11:50:04 +0800 Subject: [PATCH 06/33] =?UTF-8?q?feat(fe+be):=20=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E5=8D=A1=E5=A4=B4=E5=83=8F=E4=B8=8D=E5=86=8D=E5=8E=8B=E6=89=81?= =?UTF-8?q?=20+=20=E5=86=85=E5=AE=B9=E4=B8=B0=E5=AF=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 头像用九参数 drawImage 居中裁剪(cover),竖图/横图都不变形,加白边圈 - 后端 IDCard 增 zodiac(按生日算星座) + weight,卡面加「星座/体重」行 - 加淡淡的肉垫水印 + 旋转的「肉垫计划·官方认证」印章 - 画布加高容纳新增内容 Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/idcard.go | 31 ++++++++- pets-fe/pages/idcard/idcard.js | 103 +++++++++++++++++++++++------ 2 files changed, 110 insertions(+), 24 deletions(-) diff --git a/pets-be/internal/service/idcard.go b/pets-be/internal/service/idcard.go index 81c191d..a11dd47 100644 --- a/pets-be/internal/service/idcard.go +++ b/pets-be/internal/service/idcard.go @@ -14,6 +14,8 @@ type IDCard struct { Gender string `json:"gender"` Breed string `json:"breed"` Birthday string `json:"birthday"` // "2023 年 12 月 15 日" + Zodiac string `json:"zodiac"` // 星座,按生日算 + Weight string `json:"weight"` // 体重,如 "13.5kg" Intro string `json:"intro"` AvatarURL string `json:"avatar_url"` IDNo string `json:"id_no"` // 玩具身份号,不入库 @@ -51,6 +53,26 @@ func petIDNo(pet *model.Pet) string { return b.String() } +// zodiacOf 按月日算西方星座。按各星座起始日(含)顺序判定, +// 落在 12/22–1/19 的默认摩羯座 +func zodiacOf(month, day int) string { + starts := []struct { + m, d int + name string + }{ + {1, 20, "水瓶座"}, {2, 19, "双鱼座"}, {3, 21, "白羊座"}, {4, 20, "金牛座"}, + {5, 21, "双子座"}, {6, 22, "巨蟹座"}, {7, 23, "狮子座"}, {8, 23, "处女座"}, + {9, 23, "天秤座"}, {10, 24, "天蝎座"}, {11, 22, "射手座"}, {12, 22, "摩羯座"}, + } + name := "摩羯座" + for _, s := range starts { + if month > s.m || (month == s.m && day >= s.d) { + name = s.name + } + } + return name +} + // GetIDCard 组装身份卡。小程序码是 best-effort:拿不到(未发布/配额)也照出卡 func (s *Service) GetIDCard(userID, petID string) (*IDCard, error) { pet, err := s.GetPet(userID, petID) @@ -72,10 +94,15 @@ func (s *Service) GetIDCard(userID, petID string) (*IDCard, error) { validity = pet.Birthday.Format("2006.01.02") + " - 永远" } + zodiac := "" + if pet.Birthday != nil && !pet.Birthday.IsZero() { + zodiac = zodiacOf(int(pet.Birthday.Month()), pet.Birthday.Day()) + } + card := &IDCard{ Name: pet.Name, Species: pet.Type, Gender: pet.Gender, Breed: pet.Breed, - Birthday: bd, Intro: intro, AvatarURL: pet.AvatarURL, - IDNo: petIDNo(pet), Validity: validity, + Birthday: bd, Zodiac: zodiac, Weight: pet.Weight, Intro: intro, + AvatarURL: pet.AvatarURL, IDNo: petIDNo(pet), Validity: validity, } // 小程序码取不到不算错——未发布/配额用尽时卡照样出,只是没码 diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index b0ebe3f..7958780 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -4,7 +4,7 @@ const { toastErr } = require('../../utils/ui.js'); // 卡片画布尺寸(逻辑像素)。真实导出会按 dpr 放大 const W = 620; -const H = 760; +const H = 812; // 在 canvas 2d 上加载一张图(远程或本地),resolve 出可直接 drawImage 的 image 对象。 // 加载失败 resolve(null)——头像或小程序码缺一张不该让整张卡画不出来 @@ -84,7 +84,7 @@ Page({ const cardW = W - pad * 2; // ── 上半张:资料区(蓝紫渐变)── - const topH = 380; + const topH = 430; const g1 = ctx.createLinearGradient(pad, pad, W - pad, topH); g1.addColorStop(0, '#FFF0D6'); g1.addColorStop(0.5, '#FFE0EC'); @@ -93,15 +93,36 @@ Page({ ctx.fillStyle = g1; ctx.fill(); - // 头像圆 - const avX = pad + 34, avY = pad + 34, avD = 108; + // 肉垫水印:卡内几枚淡淡的爪印,裁剪在圆角卡里 + ctx.save(); + this.roundRect(ctx, pad, pad, cardW, topH - pad, 28); + ctx.clip(); + ctx.globalAlpha = 0.06; + ctx.font = '120px sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('🐾', W - pad - 70, pad + 90); + ctx.font = '80px sans-serif'; + ctx.fillText('🐾', W - pad - 150, topH - 70); + ctx.restore(); + ctx.globalAlpha = 1; + ctx.textBaseline = 'alphabetic'; + + // 头像圆(描一圈白边更像证件照) + const avX = pad + 34, avY = pad + 34, avD = 120; ctx.save(); ctx.beginPath(); ctx.arc(avX + avD / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); if (avatar) { - ctx.drawImage(avatar, avX, avY, avD, avD); + // 居中裁剪成正方形再画:竖图/横图都不会被压变形(cover 效果) + const iw = avatar.width || avD; + const ih = avatar.height || avD; + const side = Math.min(iw, ih); + const sx = (iw - side) / 2; + const sy = (ih - side) / 2; + ctx.drawImage(avatar, sx, sy, side, side, avX, avY, avD, avD); } else { ctx.fillStyle = '#FFD9A3'; ctx.fillRect(avX, avY, avD, avD); @@ -113,6 +134,12 @@ Page({ } ctx.restore(); ctx.textBaseline = 'alphabetic'; + // 头像白边圈 + ctx.beginPath(); + ctx.arc(avX + avD / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2); + ctx.strokeStyle = 'rgba(255,255,255,.9)'; + ctx.lineWidth = 5; + ctx.stroke(); // 资料行 const infoX = avX + avD + 28; @@ -127,30 +154,35 @@ Page({ ctx.fillText(value || '—', infoX + ctx.measureText(label).width + 14, y); y += 46; }; - line('姓名', c.name); - // 性别 + 品种放一行 - ctx.textAlign = 'left'; - ctx.font = '24px sans-serif'; - ctx.fillStyle = '#9A8F82'; - ctx.fillText('性别', infoX, y); - ctx.fillStyle = '#2D2925'; - ctx.font = '600 26px sans-serif'; - const gw = ctx.measureText(c.gender || '—').width; - ctx.fillText(c.gender || '—', infoX + 62, y); - if (c.breed) { - ctx.fillStyle = '#9A8F82'; + // 一行放两组「标签 值」,X 起点给定 + const pair = (l1, v1, l2, v2) => { + ctx.textAlign = 'left'; ctx.font = '24px sans-serif'; - ctx.fillText('品种', infoX + 62 + gw + 24, y); + ctx.fillStyle = '#9A8F82'; + ctx.fillText(l1, infoX, y); ctx.fillStyle = '#2D2925'; ctx.font = '600 26px sans-serif'; - ctx.fillText(c.breed, infoX + 62 + gw + 24 + 62, y); - } - y += 46; + const w1 = ctx.measureText(v1 || '—').width; + ctx.fillText(v1 || '—', infoX + 62, y); + if (l2 && v2) { + const x2 = infoX + 62 + w1 + 26; + ctx.fillStyle = '#9A8F82'; + ctx.font = '24px sans-serif'; + ctx.fillText(l2, x2, y); + ctx.fillStyle = '#2D2925'; + ctx.font = '600 26px sans-serif'; + ctx.fillText(v2, x2 + 62, y); + } + y += 46; + }; + line('姓名', c.name); + pair('性别', c.gender, '品种', c.breed); line('出生', c.birthday); + pair('星座', c.zodiac, '体重', c.weight); line('介绍', c.intro); // 身份 ID:虚线 + 号 - const idY = pad + 300; + const idY = pad + 344; ctx.strokeStyle = 'rgba(122,74,8,.22)'; ctx.lineWidth = 1; ctx.setLineDash([6, 6]); @@ -190,6 +222,33 @@ Page({ ctx.fillText('签发机构 肉垫计划', pad + 40, botY + 180); ctx.fillText('有效期限 ' + (c.validity || '永久有效'), pad + 40, botY + 214); + // 官方认证印章(装饰):淡棕红、旋一点角度,像盖上去的 + ctx.save(); + ctx.translate(W - pad - 120, botY + 100); + ctx.rotate((-14 * Math.PI) / 180); + ctx.globalAlpha = 0.5; + ctx.strokeStyle = '#B4623F'; + ctx.fillStyle = '#B4623F'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.lineWidth = 4; + ctx.beginPath(); + ctx.arc(0, 0, 60, 0, Math.PI * 2); + ctx.stroke(); + ctx.lineWidth = 1.5; + ctx.beginPath(); + ctx.arc(0, 0, 50, 0, Math.PI * 2); + ctx.stroke(); + ctx.font = '32px sans-serif'; + ctx.fillText('★', 0, -16); + ctx.font = '700 22px sans-serif'; + ctx.fillText('肉垫计划', 0, 16); + ctx.font = '12px sans-serif'; + ctx.fillText('官方认证', 0, 38); + ctx.restore(); + ctx.globalAlpha = 1; + ctx.textBaseline = 'alphabetic'; + // 小程序码(未发布时 qr 为 null,画个占位框) const qrD = 128, qrX = W - pad - 40 - qrD, qrY = botY + botH - 40 - qrD; ctx.fillStyle = '#fff'; -- 2.52.0 From bbc34c5bd603c9da445af520a9e64111e09bf4e3 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 13:42:11 +0800 Subject: [PATCH 07/33] =?UTF-8?q?feat(fe):=20Step4=20=E9=97=A8=E9=9D=A2?= =?UTF-8?q?=E5=93=81=E7=89=8C=E8=89=B2=E5=B8=A6=EF=BC=88=E6=96=B9=E6=A1=88?= =?UTF-8?q?A=EF=BC=89+=20nav-bar=20tone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - nav-bar 加 tone="brand":透明底 + 深棕字,压在色带上;默认分支不动 - 首页/记录页顶部加 252rpx 品牌橘色带(下沿大圆角,z-index:-1 垫背后), 盖住 nav + 切换条,门面卡在色带下方 - 切换条压在色带上:chip 半透明白 + 深棕字,选中实白 - page 加 position:relative 供色带绝对定位锚定 - 新增 --band-a/--band-b/--band-ink token(取自 preview-v3 定稿) Co-Authored-By: Claude Opus 5 --- pets-fe/app.wxss | 20 ++++++++++++++++++++ pets-fe/components/nav-bar/nav-bar.js | 5 ++++- pets-fe/components/nav-bar/nav-bar.wxml | 2 +- pets-fe/components/nav-bar/nav-bar.wxss | 10 ++++++++++ pets-fe/pages/home/home.wxml | 6 ++++-- pets-fe/pages/record/record.wxml | 4 +++- 6 files changed, 42 insertions(+), 5 deletions(-) diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss index 1f7307a..53dd91d 100644 --- a/pets-fe/app.wxss +++ b/pets-fe/app.wxss @@ -21,6 +21,10 @@ page{ --primary-soft:#FFF4E4; /* 唯一的浅橙底(收编了 9 种近似值) */ --primary-line:#FFD6A1; /* 浅橙块的描边 */ --primary-ink:#A96500; /* 浅橙底上的文字 */ + /* 门面品牌色带(首页/记录页顶部,方案 A)*/ + --band-a:#FFC768; + --band-b:#F5A947; + --band-ink:#7A4A08; /* 色带上的深棕文字 */ /* ── 语义色:soft 底 / base 主体 / ink 文字,三件套配齐 ── */ --green:#73BE9D; --green-soft:#EAF7F1; --green-ink:#2E7D5B; @@ -92,6 +96,7 @@ page{ overflow:hidden; display:flex; flex-direction:column; + position:relative; /* 门面色带 .brand-band 绝对定位锚在页面上 */ } view,text{box-sizing:border-box} @@ -373,6 +378,21 @@ page{scrollbar-width:none;-ms-overflow-style:none} .top-bar .pet-switch{padding-bottom:var(--sp-2)} .top-bar .st-pill{padding-bottom:var(--sp-2)} +/* 门面品牌色带(方案 A):绝对定位在页面最上层背后,盖住 nav + 切换条, + 下沿大圆角。放在 .page-scroll 外面,所以不跟着滚。z-index 低于 nav(100) */ +.brand-band{ + /* z-index:-1 垫在所有内容背后、页面背景之上。用 0 的话定位元素会盖住切换条 */ + position:absolute;left:0;right:0;top:0;height:252rpx;z-index:-1; + background:linear-gradient(160deg,var(--band-a),var(--band-b)); + border-radius:0 0 40rpx 40rpx; +} +/* 切换条压在色带上:chip 改成半透明白 + 深棕字,选中的实白 */ +.top-bar.on-band .pet-chip{ + background:rgba(255,255,255,.42);border-color:transparent;color:var(--band-ink); +} +.top-bar.on-band .pet-chip.active{background:#fff;color:var(--text);border-color:transparent} +.top-bar.on-band .pet-chip-add{color:rgba(122,74,8,.75)} + /* 帖子卡。社区和用户主页都要用,页面级 wxss 不跨页生效,所以放全局 */ .post-card{background:#fff;border-radius:var(--r-lg);box-shadow:var(--sd-1);padding:var(--sp-4);margin-bottom:var(--sp-4)} .post-head{display:flex;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-3)} diff --git a/pets-fe/components/nav-bar/nav-bar.js b/pets-fe/components/nav-bar/nav-bar.js index 54a8f6c..c950f34 100644 --- a/pets-fe/components/nav-bar/nav-bar.js +++ b/pets-fe/components/nav-bar/nav-bar.js @@ -4,7 +4,10 @@ Component({ options: { addGlobalClass: true }, properties: { title: { type: String, value: '肉垫计划' }, - showBack: { type: Boolean, value: false } + showBack: { type: Boolean, value: false }, + // tone="brand":透明底 + 深棕字,压在品牌色带上(首页/记录页)。 + // 默认空 = 原来的奶白毛玻璃,另外 7 个页面都靠它,别动默认分支 + tone: { type: String, value: '' } }, data: { statusBarHeight: 20, diff --git a/pets-fe/components/nav-bar/nav-bar.wxml b/pets-fe/components/nav-bar/nav-bar.wxml index 230b1ca..97805cb 100644 --- a/pets-fe/components/nav-bar/nav-bar.wxml +++ b/pets-fe/components/nav-bar/nav-bar.wxml @@ -1,4 +1,4 @@ - + {{title}} diff --git a/pets-fe/components/nav-bar/nav-bar.wxss b/pets-fe/components/nav-bar/nav-bar.wxss index b6fc5ec..b376d59 100644 --- a/pets-fe/components/nav-bar/nav-bar.wxss +++ b/pets-fe/components/nav-bar/nav-bar.wxss @@ -31,3 +31,13 @@ box-shadow:0 4rpx 12rpx rgba(70,48,25,.06); } .nav-back:active{background:rgba(255,255,255,.95)} + +/* tone="brand":色带自己是实色,nav 不再铺奶白/毛玻璃/mask,文字换深棕。 + 只作用在带 tone-brand 的实例,默认分支纹丝不动 */ +.nav-wrap.tone-brand{ + background:none; + backdrop-filter:none;-webkit-backdrop-filter:none; + -webkit-mask-image:none;mask-image:none; +} +.nav-wrap.tone-brand .nav-title{color:var(--band-ink)} +.nav-wrap.tone-brand .nav-back{color:var(--band-ink);background:rgba(255,255,255,.5)} diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml index 078bad3..0636b68 100644 --- a/pets-fe/pages/home/home.wxml +++ b/pets-fe/pages/home/home.wxml @@ -1,6 +1,8 @@ - + + + - + diff --git a/pets-fe/pages/record/record.wxml b/pets-fe/pages/record/record.wxml index 3510b50..e830071 100644 --- a/pets-fe/pages/record/record.wxml +++ b/pets-fe/pages/record/record.wxml @@ -1,4 +1,6 @@ - + + + -- 2.52.0 From b52729dec8fccf289bf52b93cb48393e81130201 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 13:59:05 +0800 Subject: [PATCH 08/33] =?UTF-8?q?feat(fe):=20=E6=95=B4=E7=AB=99=E6=94=B9?= =?UTF-8?q?=E5=93=81=E7=89=8C=E6=9A=96=E6=A9=98=E7=AB=96=E5=90=91=E6=B7=A1?= =?UTF-8?q?=E5=87=BA=E6=B8=90=E5=8F=98=EF=BC=8C=E5=8F=96=E4=BB=A3=E7=8B=AC?= =?UTF-8?q?=E7=AB=8B=E8=89=B2=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - page 背景换成顶部品牌橘 → 约半屏褪到米白的竖向淡出,铺全部页面, 没有色块接缝问题,整屏更一体 - nav-bar 默认改透明 + 深棕字(顶部本身就暖,不再铺奶白毛玻璃); 去掉 Step4 的 tone 属性和独立 .brand-band - 带切换条的页面(首页/报告/历史/计划)统一 on-band:chip 半透明白、 选中实白,在暖底上更清晰 Co-Authored-By: Claude Opus 5 --- pets-fe/app.wxss | 22 +++++++----------- pets-fe/components/nav-bar/nav-bar.js | 5 +--- pets-fe/components/nav-bar/nav-bar.wxml | 2 +- pets-fe/components/nav-bar/nav-bar.wxss | 31 ++++++------------------- pets-fe/pages/history/history.wxml | 2 +- pets-fe/pages/home/home.wxml | 4 +--- pets-fe/pages/plan/plan.wxml | 2 +- pets-fe/pages/record/record.wxml | 4 +--- pets-fe/pages/report/report.wxml | 2 +- 9 files changed, 23 insertions(+), 51 deletions(-) diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss index 53dd91d..0415502 100644 --- a/pets-fe/app.wxss +++ b/pets-fe/app.wxss @@ -84,11 +84,15 @@ page{ color:var(--text); font-weight:var(--fw); font-family:-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang SC","Microsoft YaHei",sans-serif; + /* 整页暖橘竖向淡出:顶部品牌橘暖调 → 约半屏处褪到米白。 + 渐变铺在 page 上(不滚),内容在 .page-scroll 里滚过它,顶部常暖。 */ background: - radial-gradient(circle at 10% 8%, rgba(245,169,71,.26), transparent 30%), - radial-gradient(circle at 88% 10%, rgba(115,190,157,.20), transparent 26%), - radial-gradient(circle at 72% 88%, rgba(120,166,232,.14), transparent 28%), - linear-gradient(135deg,#FBF5EA,#EEF6F1 55%,#F8F1E7); + linear-gradient(180deg, + #FFC66E 0%, + #FFD489 10%, + #FCE6C4 28%, + var(--bg) 48%, + var(--bg) 100%); /* 页面自身不滚,滚动交给 .page-scroll,这样 nav-bar 和 tabBar 才能固定住。 这 4 行原本在 9 个页面的 wxss 里逐字重复。 */ @@ -378,15 +382,7 @@ page{scrollbar-width:none;-ms-overflow-style:none} .top-bar .pet-switch{padding-bottom:var(--sp-2)} .top-bar .st-pill{padding-bottom:var(--sp-2)} -/* 门面品牌色带(方案 A):绝对定位在页面最上层背后,盖住 nav + 切换条, - 下沿大圆角。放在 .page-scroll 外面,所以不跟着滚。z-index 低于 nav(100) */ -.brand-band{ - /* z-index:-1 垫在所有内容背后、页面背景之上。用 0 的话定位元素会盖住切换条 */ - position:absolute;left:0;right:0;top:0;height:252rpx;z-index:-1; - background:linear-gradient(160deg,var(--band-a),var(--band-b)); - border-radius:0 0 40rpx 40rpx; -} -/* 切换条压在色带上:chip 改成半透明白 + 深棕字,选中的实白 */ +/* 切换条压在暖色顶部:chip 改成半透明白 + 深棕字,选中的实白 */ .top-bar.on-band .pet-chip{ background:rgba(255,255,255,.42);border-color:transparent;color:var(--band-ink); } diff --git a/pets-fe/components/nav-bar/nav-bar.js b/pets-fe/components/nav-bar/nav-bar.js index c950f34..54a8f6c 100644 --- a/pets-fe/components/nav-bar/nav-bar.js +++ b/pets-fe/components/nav-bar/nav-bar.js @@ -4,10 +4,7 @@ Component({ options: { addGlobalClass: true }, properties: { title: { type: String, value: '肉垫计划' }, - showBack: { type: Boolean, value: false }, - // tone="brand":透明底 + 深棕字,压在品牌色带上(首页/记录页)。 - // 默认空 = 原来的奶白毛玻璃,另外 7 个页面都靠它,别动默认分支 - tone: { type: String, value: '' } + showBack: { type: Boolean, value: false } }, data: { statusBarHeight: 20, diff --git a/pets-fe/components/nav-bar/nav-bar.wxml b/pets-fe/components/nav-bar/nav-bar.wxml index 97805cb..230b1ca 100644 --- a/pets-fe/components/nav-bar/nav-bar.wxml +++ b/pets-fe/components/nav-bar/nav-bar.wxml @@ -1,4 +1,4 @@ - + {{title}} diff --git a/pets-fe/components/nav-bar/nav-bar.wxss b/pets-fe/components/nav-bar/nav-bar.wxss index b376d59..ca96ab0 100644 --- a/pets-fe/components/nav-bar/nav-bar.wxss +++ b/pets-fe/components/nav-bar/nav-bar.wxss @@ -1,24 +1,17 @@ :host{display:block} -/* 原来是一整条半透明色块,底边和页面渐变硬碰硬,屏幕上横着一道接缝。 - 改成上实下虚的渐变,并且用同一条 mask 把毛玻璃也一起淡出—— - 只淡化背景色而不淡化 blur 的话,接缝会从「色差」变成「糊边」,一样难看。 */ +/* 整页暖橘渐变后,顶部本身就是暖色,nav 不再铺奶白/毛玻璃,透明即可, + 文字用深棕压在暖底上。内容在 .page-scroll 里滚,不会跑到 nav 背后, + 所以不需要毛玻璃遮挡。 */ .nav-wrap{ position:fixed;top:0;left:0;right:0;z-index:100; - background:linear-gradient(180deg, - rgba(250,247,242,.94) 0%, - rgba(250,247,242,.88) 58%, - rgba(250,247,242,0) 100%); - backdrop-filter:blur(20rpx); - -webkit-backdrop-filter:blur(20rpx); - -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 62%,transparent 100%); - mask-image:linear-gradient(180deg,#000 0%,#000 62%,transparent 100%); + background:none; } .nav-inner{ position:relative;display:flex;align-items:center;justify-content:center; } .nav-title{ - font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text); + font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--band-ink); letter-spacing:.5rpx; max-width:56%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; } @@ -27,17 +20,7 @@ position:absolute;left:var(--sp-3);top:50%;transform:translateY(-50%); width:60rpx;height:60rpx;border-radius:50%; display:flex;align-items:center;justify-content:center; - color:var(--text);background:rgba(255,255,255,.72); + color:var(--band-ink);background:rgba(255,255,255,.6); box-shadow:0 4rpx 12rpx rgba(70,48,25,.06); } -.nav-back:active{background:rgba(255,255,255,.95)} - -/* tone="brand":色带自己是实色,nav 不再铺奶白/毛玻璃/mask,文字换深棕。 - 只作用在带 tone-brand 的实例,默认分支纹丝不动 */ -.nav-wrap.tone-brand{ - background:none; - backdrop-filter:none;-webkit-backdrop-filter:none; - -webkit-mask-image:none;mask-image:none; -} -.nav-wrap.tone-brand .nav-title{color:var(--band-ink)} -.nav-wrap.tone-brand .nav-back{color:var(--band-ink);background:rgba(255,255,255,.5)} +.nav-back:active{background:rgba(255,255,255,.92)} diff --git a/pets-fe/pages/history/history.wxml b/pets-fe/pages/history/history.wxml index d0db908..473f1bf 100644 --- a/pets-fe/pages/history/history.wxml +++ b/pets-fe/pages/history/history.wxml @@ -1,6 +1,6 @@ - + diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml index 0636b68..87e37a2 100644 --- a/pets-fe/pages/home/home.wxml +++ b/pets-fe/pages/home/home.wxml @@ -1,6 +1,4 @@ - - - + diff --git a/pets-fe/pages/plan/plan.wxml b/pets-fe/pages/plan/plan.wxml index 30393e3..530b6c8 100644 --- a/pets-fe/pages/plan/plan.wxml +++ b/pets-fe/pages/plan/plan.wxml @@ -1,6 +1,6 @@ - + diff --git a/pets-fe/pages/record/record.wxml b/pets-fe/pages/record/record.wxml index e830071..3510b50 100644 --- a/pets-fe/pages/record/record.wxml +++ b/pets-fe/pages/record/record.wxml @@ -1,6 +1,4 @@ - - - + diff --git a/pets-fe/pages/report/report.wxml b/pets-fe/pages/report/report.wxml index f897697..2e5239c 100644 --- a/pets-fe/pages/report/report.wxml +++ b/pets-fe/pages/report/report.wxml @@ -1,6 +1,6 @@ - + -- 2.52.0 From a91f5e39ca1e24efc3f05ff77672f11b1421b706 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 14:12:52 +0800 Subject: [PATCH 09/33] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=96=E5=8E=BB?= =?UTF-8?q?=E8=BA=AB=E4=BB=BD=E7=94=A8=E6=98=B5=E7=A7=B0=E3=80=81=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E5=AF=B9=E9=BD=90=E7=AD=9B=E9=80=89=E3=80=81=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E9=99=902=E3=80=81=E6=97=A0=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E4=B9=9F=E7=95=99=E7=97=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 发帖去掉「发布身份」选择,统一用发布者昵称(后端按 user 取昵称) - 发帖标签改成 晒宠/新手求助/经验,和社区顶部筛选完全一致; tabTag 新手求助→新手求助(原来映射成「求助」对不上) - 内容输入框:点整块 field 都聚焦(原生 textarea 在 scroll-view 里 命中区域会偏,只有 placeholder 处能点) - 发帖图片上限 3→2,文案标注「最多 2 张」 - 无记录类型的自定义任务完成后,补一条 note 记录,最近记录里留痕 Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/community.go | 25 ++++++------------- .../components/bottom-sheet/bottom-sheet.js | 17 +++++++------ .../components/bottom-sheet/bottom-sheet.wxml | 19 ++++++-------- pets-fe/pages/home/home.js | 10 +++++++- 4 files changed, 34 insertions(+), 37 deletions(-) diff --git a/pets-be/internal/service/community.go b/pets-be/internal/service/community.go index 03aeca2..38d750e 100644 --- a/pets-be/internal/service/community.go +++ b/pets-be/internal/service/community.go @@ -48,7 +48,7 @@ func (s *Service) attachPostImages(posts []model.Post) { func tabTag(tab string) string { switch tab { case "新手求助": - return "求助" + return "新手求助" case "晒宠": return "晒宠" case "经验": @@ -111,23 +111,14 @@ type PostInput struct { // CreatePost 发帖 func (s *Service) CreatePost(userID string, in PostInput) (*model.Post, error) { - authorName := "匿名宠友" - authorEmoji := "🐾" - switch in.Identity { - case "official": - authorName = "肉垫计划官方" - case "petName": - if in.PetID != nil { - var pet model.Pet - if err := s.db.First(&pet, *in.PetID).Error; err == nil { - authorName = pet.Name + "的铲屎官" - authorEmoji = pet.Emoji - } - } - } - // 取作者 openid(内容安全接口 v2 要求带上),并把图片 file id 解析成可访问 URL + // 取作者信息:统一用发布者昵称(去掉了发布身份选择),openid 给内容安全接口用 var user model.User - s.db.Select("id, open_id").First(&user, userID) + s.db.Select("id, open_id, nickname").First(&user, userID) + authorName := user.Nickname + if authorName == "" { + authorName = "宠友" + } + authorEmoji := "🐾" imgURLs := s.resolveImageURLs(in.ImageFileIDs) // 发布前过内容安全:文本同步判、图片提交异步查。检测不了或有图 → 待人工/待回调 diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js index 238fb0b..444d667 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.js +++ b/pets-fe/components/bottom-sheet/bottom-sheet.js @@ -43,8 +43,8 @@ const REMINDER_TYPES = [ const REMINDER_LABELS = REMINDER_TYPES.map((t) => t.label); -const IDENTITY = ['petName', 'anonymous', 'official']; -const PTAG = ['晒宠', '求助', '经验', '避坑']; +// 标签和社区顶部筛选保持一致(晒宠 / 新手求助 / 经验) +const PTAG = ['晒宠', '新手求助', '经验']; Component({ options: { addGlobalClass: true }, @@ -61,6 +61,7 @@ Component({ optSel: {}, // 通用简易记录(洗护/清洁那 15 种共用) postContent: '', + pfFocus: false, // 发帖内容框聚焦标记(点整块 field 聚焦用) postImages: [], remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' }, remTypeLabels: REMINDER_LABELS, @@ -345,6 +346,9 @@ Component({ this.setData({ optSel }); }, onPostInput(e) { this.setData({ postContent: e.detail.value }); }, + // 点内容 field 任意处都聚焦(原生 textarea 在 scroll-view 里命中区域会偏) + onPostFieldTap() { this.setData({ pfFocus: true }); }, + onPostBlur() { this.setData({ pfFocus: false }); }, loadComments() { if (!this.data.postId) return; api.listComments(this.data.postId) @@ -539,8 +543,8 @@ Component({ api.getTasks(id).then((tasks) => this.setData({ manageTasks: tasks || [] })).catch(() => {}); }, onPickPostImages() { - const left = 3 - this.data.postImages.length; - if (left <= 0) return wx.showToast({ title: '最多 3 张', icon: 'none' }); + const left = 2 - this.data.postImages.length; + if (left <= 0) return wx.showToast({ title: '最多 2 张', icon: 'none' }); upload .chooseAndUploadImages(left) .then((list) => this.setData({ postImages: this.data.postImages.concat(list) })) @@ -604,14 +608,13 @@ Component({ async onCreatePost() { const content = (this.data.postContent || '').trim(); if (!content) return wx.showToast({ title: '写点什么吧', icon: 'none' }); - const identity = IDENTITY[this.segIdx('ident')]; const tag = PTAG[this.data.optSel.ptag === undefined ? 0 : this.data.optSel.ptag]; if (this.data.saving) return; this.setData({ saving: true }); try { + // 去掉发布身份,统一用发布者昵称(后端按 user 取昵称) const post = await api.createPost({ - pet_id: identity === 'petName' ? store.currentPetId() : null, - identity, + identity: 'user', content, tags: [tag], image_file_ids: this.data.postImages.map((i) => i.id), diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxml b/pets-fe/components/bottom-sheet/bottom-sheet.wxml index 0d7c241..7850ebd 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxml +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxml @@ -201,27 +201,22 @@ module.exports.sel = function (map, key, index, def) { → 其他资料),品种从后端 breeds 表来、按首字母索引。弹层塞不下这些 --> 发布图文 - 选择发布身份,分享你的养宠日常。 - - {{pet.name}} - 匿名宠友 - 官方笔记 - - - + 用你的昵称分享养宠日常。 + + + 晒宠 - 求助 + 新手求助 经验 - 避坑 - + - + diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js index 36b649a..1ddcae6 100644 --- a/pets-fe/pages/home/home.js +++ b/pets-fe/pages/home/home.js @@ -180,13 +180,21 @@ Page({ wx.navigateTo({ url }); return; } - // 没有关联记录类型的任务(如遛狗),直接切换完成 + // 没有关联记录类型的任务(自定义/无 sheet_type):直接切换完成, + // 并且切到「完成」时补一条 note 记录,这样最近记录里也留痕 api .toggleTask(t.id) .then((res) => { const tasks = this.data.tasks.slice(); tasks[i] = Object.assign({}, tasks[i], { done: res.done }); this.setData({ tasks }); + if (!res.done) return; // 取消完成就不建记录 + const pid = store.currentPetId(); + if (!pid) return; + return api + .createRecord(pid, { type: 'note', title: '完成:' + t.title }) + .then(() => this.loadTimeline(1)) + .catch(() => {}); // 建记录失败不影响任务已完成 }) .catch((err) => toastErr(err, '操作失败')); }, -- 2.52.0 From 0f3efbc89545beaba6fbaa7b2c4bc5aea29e4629 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 14:16:53 +0800 Subject: [PATCH 10/33] =?UTF-8?q?chore(be):=20=E4=B8=8A=E4=BC=A0=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E6=8C=89=E6=9C=88=E4=BB=BD=E5=88=86=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E5=A4=B9=20uploads/YYYY-MM/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MD5 去重本来就有(相同内容直接返回已有记录不重传);对象名从 files/ 改成 uploads/2026-07/,跨月的同内容仍走去重不重复占用。 Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/file.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pets-be/internal/service/file.go b/pets-be/internal/service/file.go index e108749..4c187cd 100644 --- a/pets-be/internal/service/file.go +++ b/pets-be/internal/service/file.go @@ -8,6 +8,7 @@ import ( "io" "path" "strings" + "time" "github.com/sundynix/pets-be/internal/model" ) @@ -41,7 +42,9 @@ func (s *Service) UploadFile(reader io.Reader, filename, contentType string) (*m if contentType == "" { contentType = "application/octet-stream" } - objectName := "files/" + md5hex + ext + // 按上传月份分文件夹:uploads/2026-07/.jpg。文件名仍用 md5 保证唯一。 + // 同内容命中上面的去重直接复用,不会因为跨月再传一份 + objectName := "uploads/" + time.Now().Format("2006-01") + "/" + md5hex + ext url, err := s.storage.Upload(objectName, bytes.NewReader(data), int64(len(data)), contentType) if err != nil { return nil, err -- 2.52.0 From 2f2757267e1fcb3087844b139aeee1b1f0bed5c0 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 14:29:04 +0800 Subject: [PATCH 11/33] =?UTF-8?q?fix(be):=20=E4=BB=BB=E5=8A=A1=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E7=BB=9F=E8=AE=A1=E4=B8=8E=E7=95=99=E7=97=95=E6=94=B9?= =?UTF-8?q?=E7=94=A8=20PlanTask=EF=BC=88DailyTask=20=E5=B7=B2=E7=A9=BA?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TogglePlanTask:完成无 sheet_type 的任务时补一条 note 记录, 首页/计划页两条路径都统一留痕(ToggleTask 委托它);去掉前端重复建记录 - 报告页「完成任务」原来统计空的 DailyTask 恒为 0,改成经 plan 关联 统计 PlanTask 本周完成数;用户汇总的 tasksDone 同改 - 首页今日完成度 todayCompletionPct 同样从 DailyTask 改成今日 PlanTask Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/home.go | 16 ++++++++++++---- pets-be/internal/service/plan.go | 11 ++++++++++- pets-be/internal/service/report.go | 9 ++++++--- pets-fe/pages/home/home.js | 12 +++--------- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/pets-be/internal/service/home.go b/pets-be/internal/service/home.go index e9d6a6f..304c330 100644 --- a/pets-be/internal/service/home.go +++ b/pets-be/internal/service/home.go @@ -180,14 +180,22 @@ func (s *Service) datesWithActivity(petID string, start, end time.Time) map[stri } func (s *Service) todayCompletionPct(petID string, now time.Time) int { - start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - end := start.AddDate(0, 0, 1) + // 任务是 PlanTask(DailyTask 已空)。取宠物的 active 30 天计划,算今天那天的完成占比 + var plan model.Plan + if s.db.Where("pet_id = ? AND kind = ? AND status = ?", petID, model.PlanThirtyDay, "active"). + Order("id desc").First(&plan).Error != nil { + return 0 + } + day := dayIndexOf(&plan, now) + if day < 0 { + return 0 + } var total, done int64 - s.db.Model(&model.DailyTask{}).Where("pet_id = ? AND task_date >= ? AND task_date < ?", petID, start, end).Count(&total) + s.db.Model(&model.PlanTask{}).Where("plan_id = ? AND day = ?", plan.ID, day).Count(&total) if total == 0 { return 0 } - s.db.Model(&model.DailyTask{}).Where("pet_id = ? AND task_date >= ? AND task_date < ? AND done = ?", petID, start, end, true).Count(&done) + s.db.Model(&model.PlanTask{}).Where("plan_id = ? AND day = ? AND done = ?", plan.ID, day, true).Count(&done) return int(math.Round(float64(done) / float64(total) * 100)) } diff --git a/pets-be/internal/service/plan.go b/pets-be/internal/service/plan.go index 129a46e..3776e40 100644 --- a/pets-be/internal/service/plan.go +++ b/pets-be/internal/service/plan.go @@ -23,7 +23,16 @@ func (s *Service) TogglePlanTask(userID, taskID string) error { if err := s.db.Where("id = ? AND user_id = ?", pt.PlanID, userID).First(&plan).Error; err != nil { return ErrNotFound } - return s.db.Model(&model.PlanTask{}).Where("id = ?", taskID).Update("done", !pt.Done).Error + newDone := !pt.Done + if err := s.db.Model(&model.PlanTask{}).Where("id = ?", taskID).Update("done", newDone).Error; err != nil { + return err + } + // 勾选完成、且没关联记录类型的任务(自定义/无 sheet_type),补一条 note 记录, + // 让最近记录里留痕。有 sheet_type 的任务是走记录表单单独落记录的,这里不重复。 + if newDone && pt.SheetType == "" { + _, _ = s.CreateRecord(userID, plan.PetID, RecordInput{Type: "note", Title: "完成:" + pt.Title}) + } + return nil } // newStagePlan 按宠物当前阶段的养护模板生成一份 30 天计划(未保存) diff --git a/pets-be/internal/service/report.go b/pets-be/internal/service/report.go index 88db0cb..dd87d82 100644 --- a/pets-be/internal/service/report.go +++ b/pets-be/internal/service/report.go @@ -26,9 +26,10 @@ func (s *Service) GetWeeklyReport(userID, petID string) (*WeeklyReport, error) { } weekAgo := time.Now().AddDate(0, 0, -7) + // 任务已并成 PlanTask(DailyTask 表已空),要经 plan 关联到宠物来统计本周完成数 var tasksCompleted int64 - s.db.Model(&model.DailyTask{}). - Where("pet_id = ? AND done = ? AND updated_at >= ?", petID, true, weekAgo). + s.db.Model(&model.PlanTask{}). + Where("plan_id IN (SELECT id FROM sundynix_plans WHERE pet_id = ?) AND done = ? AND updated_at >= ?", petID, true, weekAgo). Count(&tasksCompleted) // 体重增长:最近 7 天最新 - 最早 @@ -215,7 +216,9 @@ func (s *Service) GetPoster(userID, petID string) (*Poster, error) { return nil, err } var tasksDone, weightRecs, vaccineRecs int64 - s.db.Model(&model.DailyTask{}).Where("pet_id = ? AND done = ?", petID, true).Count(&tasksDone) + s.db.Model(&model.PlanTask{}). + Where("plan_id IN (SELECT id FROM sundynix_plans WHERE pet_id = ?) AND done = ?", petID, true). + Count(&tasksDone) s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordWeight).Count(&weightRecs) s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordVaccine).Count(&vaccineRecs) diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js index 1ddcae6..cb9c9f0 100644 --- a/pets-fe/pages/home/home.js +++ b/pets-fe/pages/home/home.js @@ -180,21 +180,15 @@ Page({ wx.navigateTo({ url }); return; } - // 没有关联记录类型的任务(自定义/无 sheet_type):直接切换完成, - // 并且切到「完成」时补一条 note 记录,这样最近记录里也留痕 + // 没有关联记录类型的任务(自定义/无 sheet_type):直接切换完成。 + // 后端 toggle 会在完成时补一条 note 记录,这里完成后刷新最近记录即可 api .toggleTask(t.id) .then((res) => { const tasks = this.data.tasks.slice(); tasks[i] = Object.assign({}, tasks[i], { done: res.done }); this.setData({ tasks }); - if (!res.done) return; // 取消完成就不建记录 - const pid = store.currentPetId(); - if (!pid) return; - return api - .createRecord(pid, { type: 'note', title: '完成:' + t.title }) - .then(() => this.loadTimeline(1)) - .catch(() => {}); // 建记录失败不影响任务已完成 + if (res.done) this.loadTimeline(1); }) .catch((err) => toastErr(err, '操作失败')); }, -- 2.52.0 From 282306b9196ee99484d08f55dbd034fcc8b228c2 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 14:45:29 +0800 Subject: [PATCH 12/33] =?UTF-8?q?feat(fe):=20=E9=A6=96=E9=A1=B5=E6=9C=80?= =?UTF-8?q?=E8=BF=91=E8=AE=B0=E5=BD=95=E5=8F=AA=E7=95=99=E6=9C=80=E6=96=B0?= =?UTF-8?q?6=E6=9D=A1=20+=20=E6=9F=A5=E7=9C=8B=E5=85=A8=E9=83=A8=E5=85=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E5=8E=BB=E6=8E=89=E6=97=A0=E9=99=90=E6=BB=9A?= =?UTF-8?q?=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 首页是速览,记录多了不该无限加载。固定拉最新 6 条,有更多就露出 「查看全部记录 ›」跳到能分页筛选的全部记录页;移除滚动分页/加载更多。 Co-Authored-By: Claude Opus 5 --- pets-fe/pages/home/home.js | 16 ++++++++-------- pets-fe/pages/home/home.wxml | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js index cb9c9f0..c7491b6 100644 --- a/pets-fe/pages/home/home.js +++ b/pets-fe/pages/home/home.js @@ -23,7 +23,8 @@ function fmtWhen(iso) { return d.getMonth() + 1 + '月' + d.getDate() + '日'; } -const TL_PAGE = 10; +// 首页最近记录只是速览,固定最新几条,翻全量去「全部记录」页,避免首页无限滚 +const TL_LIMIT = 6; Page({ data: { @@ -38,7 +39,6 @@ Page({ needNextPlan: false, monthCost: 0, timeline: [], - tlPage: 1, hasMore: false, }, onLoad() { @@ -127,11 +127,11 @@ Page({ .then((b) => this.setData({ monthCost: (b && b.total) || 0 })) .catch(() => this.setData({ monthCost: 0 })); }, - loadTimeline(page) { + loadTimeline() { const id = store.currentPetId(); if (!id) return; api - .getRecords(id, { page, pageSize: TL_PAGE }) + .getRecords(id, { page: 1, pageSize: TL_LIMIT }) .then((res) => { const list = (res.list || []).map((r) => ({ id: r.id, @@ -142,13 +142,13 @@ Page({ image: r.image_url || '', timeText: fmtWhen(r.occurred_at), })); - const merged = page === 1 ? list : this.data.timeline.concat(list); - this.setData({ timeline: merged, tlPage: page, hasMore: merged.length < (res.total || 0) }); + // 有更多就露出「查看全部」入口,跳到能分页筛选的全部记录页 + this.setData({ timeline: list, hasMore: (res.total || 0) > list.length }); }) .catch(() => {}); }, - loadMore() { - if (this.data.hasMore) this.loadTimeline(this.data.tlPage + 1); + goHistory() { + wx.navigateTo({ url: '/pages/history/history' }); }, previewImage(e) { const url = e.currentTarget.dataset.url; diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml index 87e37a2..7ac843e 100644 --- a/pets-fe/pages/home/home.wxml +++ b/pets-fe/pages/home/home.wxml @@ -2,8 +2,7 @@ - + @@ -110,7 +109,7 @@ 还没有记录。点上面的「记一笔」开始, 体重趋势和健康洞察都会从这些记录里长出来。 - 加载更多 + 查看全部记录 › -- 2.52.0 From d5dfde14f4395955599b73fcf5fc6dd0d9e56753 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 14:50:49 +0800 Subject: [PATCH 13/33] =?UTF-8?q?fix(fe):=20textarea=20=E5=8D=A0=E4=BD=8D/?= =?UTF-8?q?=E6=96=87=E5=AD=97=E4=BB=8E=E7=AC=AC=E4=B8=80=E8=A1=8C=E5=BC=80?= =?UTF-8?q?=E5=A7=8B=EF=BC=88=E6=94=B9=E5=9B=9E=E5=9D=97=E7=BA=A7=E9=A1=B6?= =?UTF-8?q?=E9=83=A8=E5=AF=B9=E9=BD=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 共用输入样式 .input,input,textarea,.picker-box 里的 display:flex+ align-items:center 是给单行 input 竖向居中的,但选择器带了 textarea, 导致所有多行输入的占位/光标被竖向居中、不从第一行开始。加一条 textarea{display:block} 恢复顶部对齐,一处修计划备注/记录描述/体重描述/评论。 Co-Authored-By: Claude Opus 5 --- pets-fe/app.wxss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss index 0415502..419d4a7 100644 --- a/pets-fe/app.wxss +++ b/pets-fe/app.wxss @@ -227,6 +227,10 @@ page{scrollbar-width:none;-ms-overflow-style:none} width:100%;border:1rpx solid var(--line);background:var(--surface-2);height:184rpx;border-radius:var(--r-md); padding:var(--sp-4);color:var(--text);font-size:var(--fs-lg);box-sizing:border-box; } +/* 上面那条共用规则的 flex+align-items:center 是给单行 input 竖向居中用的, + 但选择器带了 textarea,导致多行输入的占位/文字被竖向居中、不从第一行开始。 + 这里把 textarea 改回块级顶部对齐,各自的高度仍由具体 class 决定 */ +textarea{display:block;padding:var(--sp-4);line-height:1.6} .placeholder{color:var(--muted2)} /* 分段选择 */ -- 2.52.0 From d45696140bb932fdb4d43dc1275120cf1b64186d Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 14:53:43 +0800 Subject: [PATCH 14/33] =?UTF-8?q?style(fe):=20=E9=A6=96=E9=A1=B5=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E8=AE=A1=E5=88=92/=E8=AE=B0=E8=8A=B1=E9=94=80=20banne?= =?UTF-8?q?r=20=E5=8E=8B=E4=BD=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上下 padding sp-5→sp-4、图标块 96→76rpx、间距收紧,整卡矮一截。 Co-Authored-By: Claude Opus 5 --- pets-fe/pages/home/home.wxss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pets-fe/pages/home/home.wxss b/pets-fe/pages/home/home.wxss index 543326f..b0b0914 100644 --- a/pets-fe/pages/home/home.wxss +++ b/pets-fe/pages/home/home.wxss @@ -44,11 +44,11 @@ .hd-n{font-size:var(--fs-lg);margin:0 4rpx} /* ===== 两个入口 ===== */ -.entry-card{display:flex;padding:var(--sp-5) var(--sp-3)} -.entry{flex:1;display:flex;flex-direction:column;align-items:center;gap:6rpx} +.entry-card{display:flex;padding:var(--sp-4) var(--sp-3)} +.entry{flex:1;display:flex;flex-direction:column;align-items:center;gap:4rpx} .entry-ic{ - width:96rpx;height:96rpx;border-radius:var(--r-md); - display:flex;align-items:center;justify-content:center;margin-bottom:4rpx; + width:76rpx;height:76rpx;border-radius:var(--r-md); + display:flex;align-items:center;justify-content:center;margin-bottom:2rpx; } .entry-b{font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text)} .entry-p{font-size:var(--fs-cap);color:var(--muted)} -- 2.52.0 From 4a1b5e66d3f1508661098fdd30efd37b89340e3a Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 15:00:47 +0800 Subject: [PATCH 15/33] =?UTF-8?q?fix:=20=E6=88=90=E9=95=BF=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=B5=B7=E6=8A=A5=E7=94=A8=E5=AE=A0=E7=89=A9=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E5=A4=B4=E5=83=8F=EF=BC=88=E6=B2=A1=E4=BC=A0=E6=89=8D?= =?UTF-8?q?=E7=94=A8=20emoji=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 Poster 增 pet_avatar_url(取 pet.AvatarURL,和身份卡同一可靠路径) - 海报 canvas 优先用它、store 兜底;有照片画圆形头像、没有才 emoji - 头像九参数居中裁剪,竖图/横图都不压扁 Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/report.go | 4 +++- pets-fe/components/bottom-sheet/bottom-sheet.js | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/pets-be/internal/service/report.go b/pets-be/internal/service/report.go index dd87d82..a038ebd 100644 --- a/pets-be/internal/service/report.go +++ b/pets-be/internal/service/report.go @@ -199,6 +199,7 @@ func (s *Service) GetHealthSummary(userID, petID string) (*HealthSummary, error) type Poster struct { PetName string `json:"pet_name"` PetEmoji string `json:"pet_emoji"` + PetAvatarURL string `json:"pet_avatar_url"` // 上传过照片就有,海报优先用它,没有才用 emoji Age string `json:"age"` Weight string `json:"weight"` Stage string `json:"stage"` @@ -223,7 +224,8 @@ func (s *Service) GetPoster(userID, petID string) (*Poster, error) { s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordVaccine).Count(&vaccineRecs) return &Poster{ - PetName: pet.Name, PetEmoji: pet.Emoji, Age: pet.Age, Weight: pet.Weight, Stage: pet.Stage, + PetName: pet.Name, PetEmoji: pet.Emoji, PetAvatarURL: pet.AvatarURL, + Age: pet.Age, Weight: pet.Weight, Stage: pet.Stage, TasksCompleted: tasksDone, WeightRecords: weightRecs, VaccineRecords: vaccineRecs, HighRiskCount: 0, Headline: "稳定成长", }, nil diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js index 444d667..e9d1366 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.js +++ b/pets-fe/components/bottom-sheet/bottom-sheet.js @@ -209,16 +209,18 @@ Component({ const pet = this.data.pet || {}; const p = this.data.poster || {}; + // 优先用后端海报数据里的头像(和身份卡同一路径,可靠),store 兜底 + const avatarUrl = p.pet_avatar_url || pet.avatar_url || ''; // 头像是远程图,异步加载。加载完(或没有头像)再往下画, // 否则头像会画不上。加载失败当没有头像处理 const loadAvatar = () => new Promise((res) => { - if (!pet.avatar_url) return res(null); + if (!avatarUrl) return res(null); const img = node.createImage(); img.onload = () => res(img); img.onerror = () => res(null); - img.src = pet.avatar_url; + img.src = avatarUrl; }); loadAvatar().then((avatar) => { @@ -243,7 +245,13 @@ Component({ ctx.arc(W / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2); ctx.closePath(); ctx.clip(); - ctx.drawImage(avatar, avX, avY, avD, avD); + // 居中裁剪成正方形再画,竖图/横图都不压扁 + const iw = avatar.width || avD; + const ih = avatar.height || avD; + const side = Math.min(iw, ih); + const sx = (iw - side) / 2; + const sy = (ih - side) / 2; + ctx.drawImage(avatar, sx, sy, side, side, avX, avY, avD, avD); ctx.restore(); } else { ctx.font = '96px sans-serif'; -- 2.52.0 From 4c9fbc4a8856fa6318e3f0f0c3e55e39a4623d8e Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 15:11:59 +0800 Subject: [PATCH 16/33] =?UTF-8?q?fix(fe+be):=20=E6=88=91=E7=9A=84=E9=A1=B5?= =?UTF-8?q?=E5=8E=BB=E9=87=8D=E5=AE=A0=E7=89=A9=E5=A2=99=20+=20=E5=85=BB?= =?UTF-8?q?=E5=AE=A0=E5=A4=A9=E6=95=B0=E6=8C=89=E5=88=B0=E5=AE=B6=E6=97=A5?= =?UTF-8?q?=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 养宠天数原来按建档日算,重新建档会被重置成 1(和首页「第869天」矛盾); 改成按最早那只的到家日算(无到家日退建档日),和首页口径一致 - 「我的」页 profile-head 传 hideWall,藏掉社交宠物墙(下面「我的毛孩子」 卡更详细,重复了);宠物墙只在别人访问/预览时出现,受「展示我的毛孩子」 开关控制,语义更对(那开关本来就是给别人看的) Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/user_profile.go | 16 +++++++++++++--- pets-fe/components/profile-head/index.js | 1 + pets-fe/components/profile-head/index.wxml | 6 ++++-- pets-fe/pages/mine/mine.wxml | 2 +- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/pets-be/internal/service/user_profile.go b/pets-be/internal/service/user_profile.go index 3d4ae2d..e330f2f 100644 --- a/pets-be/internal/service/user_profile.go +++ b/pets-be/internal/service/user_profile.go @@ -123,9 +123,19 @@ func (s *Service) keepStats(userID string, withPets bool) (*KeepStats, []PetCard cards = nil } - // 养宠天数按最早那只的建档日算。用建档日而不是生日:这里要表达的是 - // 「你在这个 App 上照顾它多久了」,不是宠物活了多久 - st.Days = int(time.Since(pets[0].CreatedAt).Hours()/24) + 1 + // 养宠天数按最早那只的到家日算(没填到家日就退回建档日),和首页 + // 「一起生活的第 N 天」口径一致。用建档日会因为重新建档而被重置成 1,很怪 + earliest := time.Now() + for _, p := range pets { + start := p.CreatedAt + if p.ArrivedAt != nil && !p.ArrivedAt.IsZero() { + start = *p.ArrivedAt + } + if start.Before(earliest) { + earliest = start + } + } + st.Days = int(time.Since(earliest).Hours()/24) + 1 var total int64 s.db.Model(&model.HealthRecord{}).Where("pet_id IN ?", ids).Count(&total) diff --git a/pets-fe/components/profile-head/index.js b/pets-fe/components/profile-head/index.js index e02950b..9b32445 100644 --- a/pets-fe/components/profile-head/index.js +++ b/pets-fe/components/profile-head/index.js @@ -7,6 +7,7 @@ Component({ card: { type: Object, value: null }, // 装扮页的实时预览:只看效果,不要底部那排按钮 preview: { type: Boolean, value: false }, + hideWall: { type: Boolean, value: false }, // 「我的」页藏掉社交宠物墙,避免和「我的毛孩子」卡重复 }, data: { initial: '', theme: 'warm' }, observers: { diff --git a/pets-fe/components/profile-head/index.wxml b/pets-fe/components/profile-head/index.wxml index 4507357..bc4210d 100644 --- a/pets-fe/components/profile-head/index.wxml +++ b/pets-fe/components/profile-head/index.wxml @@ -29,8 +29,10 @@ {{card.stats.streak || 0}}连续打卡 - - + diff --git a/pets-fe/pages/mine/mine.wxml b/pets-fe/pages/mine/mine.wxml index 85a0d33..f3b9ec1 100644 --- a/pets-fe/pages/mine/mine.wxml +++ b/pets-fe/pages/mine/mine.wxml @@ -3,7 +3,7 @@ - -- 2.52.0 From 1610fd68149337944a60ad788688a7d1fdd96c64 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 15:22:22 +0800 Subject: [PATCH 17/33] =?UTF-8?q?style(fe):=20=E8=A3=85=E6=89=AE/=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E4=B8=A4=E4=B8=AA=E6=8C=89=E9=92=AE=E6=94=B9=E4=B8=80?= =?UTF-8?q?=E8=A1=8C=E4=B8=A4=E5=88=97=E3=80=81=E7=BC=A9=E5=B0=8F=E6=96=87?= =?UTF-8?q?=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 装扮按钮从 profile-head 挪到「我的」页,和「看看别人眼里的我」并成 一行两列(装扮主页 / 别人眼里的我),字号缩小、图标小一号; profile-head 只保留别人主页的关注键。 Co-Authored-By: Claude Opus 5 --- pets-fe/components/profile-head/index.wxml | 12 ++++-------- pets-fe/pages/mine/mine.wxml | 10 ++++++++-- pets-fe/pages/mine/mine.wxss | 4 +++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pets-fe/components/profile-head/index.wxml b/pets-fe/components/profile-head/index.wxml index bc4210d..0932efe 100644 --- a/pets-fe/components/profile-head/index.wxml +++ b/pets-fe/components/profile-head/index.wxml @@ -44,13 +44,9 @@ - - - 装扮我的主页 - - - {{card.followed ? '已关注' : '关注 TA'}} - - + + + {{card.followed ? '已关注' : '关注 TA'}} + diff --git a/pets-fe/pages/mine/mine.wxml b/pets-fe/pages/mine/mine.wxml index f3b9ec1..29857bb 100644 --- a/pets-fe/pages/mine/mine.wxml +++ b/pets-fe/pages/mine/mine.wxml @@ -6,8 +6,14 @@ - - 看看别人眼里的我 + + + + 装扮主页 + + + 别人眼里的我 + diff --git a/pets-fe/pages/mine/mine.wxss b/pets-fe/pages/mine/mine.wxss index f6c2de0..51f4c4f 100644 --- a/pets-fe/pages/mine/mine.wxss +++ b/pets-fe/pages/mine/mine.wxss @@ -8,5 +8,7 @@ } .pet-info{flex:1;min-width:0} -.me-preview{margin-bottom:var(--sp-5)} +/* 装扮 + 预览:一行两列,文字缩小、图标也小一号 */ +.me-actions{display:flex;gap:var(--sp-3);margin-bottom:var(--sp-5)} +.me-act{flex:1;height:var(--h-md);padding:0 var(--sp-2);font-size:var(--fs-sm);gap:var(--sp-1)} .pet-emoji-img{width:100%;height:100%;display:block} -- 2.52.0 From 95471d05498ac1ee8049ac73943d6899140d4615 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 15:28:23 +0800 Subject: [PATCH 18/33] =?UTF-8?q?style(fe):=20=E6=88=91=E7=9A=84=E9=A1=B6?= =?UTF-8?q?=E9=83=A8=E5=8D=A1=E7=89=87=E9=87=8D=E6=8E=92=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=B7=A6=E5=AF=B9=E9=BD=90=E7=B4=A7=E5=87=91=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 没设头图时不再留一块空橙色 banner(和整页暖橘背景撞色的怪感消除) - 身份行改左对齐:头像 + 昵称/签名并排;社交数(帖子/关注/粉丝)缩成 一小行内联;养宠数据保留高亮带。头图设了才显示 - 主题色/头图/签名/展示开关都照常生效 Co-Authored-By: Claude Opus 5 --- pets-fe/components/profile-head/index.wxml | 42 +++++++++++----------- pets-fe/components/profile-head/index.wxss | 38 +++++++++++--------- 2 files changed, 41 insertions(+), 39 deletions(-) diff --git a/pets-fe/components/profile-head/index.wxml b/pets-fe/components/profile-head/index.wxml index 0932efe..8028bc3 100644 --- a/pets-fe/components/profile-head/index.wxml +++ b/pets-fe/components/profile-head/index.wxml @@ -1,37 +1,35 @@ - - - - + + - - - {{initial}} + + + + + {{initial}} + + + {{card.nickname || '毛孩子家长'}}AI + {{card.bio || '这个人很懒,还没写签名'}} + - {{card.nickname || '毛孩子家长'}}AI - {{card.bio || '这个人很懒,还没写签名'}} - + + {{card.post_count || 0}}帖子 - - {{card.following_count || 0}}关注 - - - {{card.follower_count || 0}}粉丝 - + {{card.following_count || 0}}关注 + {{card.follower_count || 0}}粉丝 - + {{card.stats.days || 0}}养宠天数 {{card.stats.records || 0}}累计记录 {{card.stats.streak || 0}}连续打卡 - + @@ -44,8 +42,8 @@ - - + + {{card.followed ? '已关注' : '关注 TA'}} diff --git a/pets-fe/components/profile-head/index.wxss b/pets-fe/components/profile-head/index.wxss index 8c268d6..33e24ff 100644 --- a/pets-fe/components/profile-head/index.wxss +++ b/pets-fe/components/profile-head/index.wxss @@ -11,41 +11,43 @@ background:#fff;border-radius:var(--r-lg);overflow:hidden; box-shadow:var(--sd-1);margin-bottom:var(--sp-5); } -.pf-bg{height:180rpx;background:linear-gradient(135deg,var(--pf-a),var(--pf-b))} -.pf-bg-img{width:100%;height:100%;display:block} +/* 头图:设了才显示 */ +.pf-bg-img{width:100%;height:200rpx;display:block} -.pf-body{padding:0 var(--sp-5) var(--sp-5);text-align:center} -/* 头像压在头图上,负 margin 提上去一半 */ +.pf-body{padding:var(--sp-5)} + +/* 身份行:头像 + 昵称/签名 左对齐 */ +.pf-top{display:flex;align-items:center;gap:var(--sp-4)} .pf-av{ - /* 必须定位:头像要压在头图上面,靠 DOM 顺序不够——只要头图是定位元素就会反盖过来 */ - position:relative;z-index:1; - width:144rpx;height:144rpx;margin:-72rpx auto var(--sp-3); - border-radius:50%;background:var(--pf-b);color:var(--pf-ink); + flex:none;width:120rpx;height:120rpx;border-radius:50%; + background:var(--pf-b);color:var(--pf-ink);overflow:hidden; display:flex;align-items:center;justify-content:center; font-size:var(--fs-xl);font-weight:var(--fw-b); - border:6rpx solid #fff;overflow:hidden;box-shadow:var(--sd-1); } .pf-av-img{width:100%;height:100%;display:block} -.pf-name{font-size:var(--fs-lg);font-weight:var(--fw-b);color:var(--text)} +.pf-id{flex:1;min-width:0} +.pf-name{font-size:var(--fs-xl);font-weight:var(--fw-b);color:var(--text)} .pf-bio{ - font-size:var(--fs-sm);color:var(--muted);margin-top:6rpx; + font-size:var(--fs-sm);color:var(--muted);margin-top:8rpx; line-height:1.5;word-break:break-all; } -.pf-stats{display:flex;margin:var(--sp-4) 0} -.pf-stat{flex:1;display:flex;flex-direction:column;align-items:center;gap:4rpx} -.pf-n{font-size:var(--fs-lg);font-weight:var(--fw-b);color:var(--text)} +/* 社交数:小一行,数字+标签内联,左对齐 */ +.pf-social{display:flex;gap:var(--sp-6);margin-top:var(--sp-4)} +.pf-stat{display:flex;align-items:baseline;gap:6rpx} +.pf-n{font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text)} .pf-l{font-size:var(--fs-cap);color:var(--muted)} +/* 养宠数据高亮带 */ .pf-keep{ display:flex;background:var(--pf-b);border-radius:var(--r-md); - padding:var(--sp-3) 0;margin-bottom:var(--sp-4); + padding:var(--sp-4) 0;margin-top:var(--sp-4); } .pf-keep-i{flex:1;display:flex;flex-direction:column;align-items:center;gap:2rpx} -.pf-kn{font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--pf-ink)} +.pf-kn{font-size:var(--fs-lg);font-weight:var(--fw-b);color:var(--pf-ink)} .pf-kl{font-size:var(--fs-cap);color:var(--pf-ink);opacity:.75} -.pf-wall{white-space:nowrap;margin-bottom:var(--sp-4)} +.pf-wall{white-space:nowrap;margin-top:var(--sp-4)} .pf-pet{ display:inline-block;width:150rpx;vertical-align:top; margin-right:var(--sp-3);white-space:normal; @@ -59,3 +61,5 @@ .pf-pet-img{width:100%;height:100%;display:block} .pf-pet-n{font-size:var(--fs-cap);font-weight:var(--fw-b);color:var(--text);text-align:center} .pf-pet-s{font-size:var(--fs-cap);color:var(--muted);text-align:center} + +.pf-follow{margin-top:var(--sp-4)} -- 2.52.0 From c8907324106aaabfce6a18c22ccd903944f4300e Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 15:33:12 +0800 Subject: [PATCH 19/33] =?UTF-8?q?feat:=20=E6=88=90=E9=95=BF=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=B5=B7=E6=8A=A5=E6=94=B9=E5=91=A8=E6=8A=A5=E5=8F=A3?= =?UTF-8?q?=E5=BE=84=20+=20=E6=98=BE=E7=A4=BA=E7=BB=9F=E8=AE=A1=E5=91=A8?= =?UTF-8?q?=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 海报四项统计原来是全部累计,和「周报告」不符;改成近 7 天口径 (和报告页本周概览一致),高风险异常也按本周真实统计(原来写死 0) - Poster 加 period 字段,海报上画出「近 7 天 · 07.25 - 07.31」 Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/report.go | 17 ++++++++++++----- pets-fe/components/bottom-sheet/bottom-sheet.js | 11 +++++++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/pets-be/internal/service/report.go b/pets-be/internal/service/report.go index a038ebd..5a8287d 100644 --- a/pets-be/internal/service/report.go +++ b/pets-be/internal/service/report.go @@ -203,6 +203,7 @@ type Poster struct { Age string `json:"age"` Weight string `json:"weight"` Stage string `json:"stage"` + Period string `json:"period"` // 报告周期,如 "07.25 - 07.31" TasksCompleted int64 `json:"tasks_completed"` WeightRecords int64 `json:"weight_records"` VaccineRecords int64 `json:"vaccine_records"` @@ -216,17 +217,23 @@ func (s *Service) GetPoster(userID, petID string) (*Poster, error) { if err != nil { return nil, err } - var tasksDone, weightRecs, vaccineRecs int64 + // 这是周报告,所有统计按近 7 天算(和报告页「本周概览」口径一致) + now := time.Now() + weekAgo := now.AddDate(0, 0, -7) + + var tasksDone, weightRecs, vaccineRecs, highRisk int64 s.db.Model(&model.PlanTask{}). - Where("plan_id IN (SELECT id FROM sundynix_plans WHERE pet_id = ?) AND done = ?", petID, true). + Where("plan_id IN (SELECT id FROM sundynix_plans WHERE pet_id = ?) AND done = ? AND updated_at >= ?", petID, true, weekAgo). Count(&tasksDone) - s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordWeight).Count(&weightRecs) - s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordVaccine).Count(&vaccineRecs) + s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordWeight, weekAgo).Count(&weightRecs) + s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordVaccine, weekAgo).Count(&vaccineRecs) + s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ? AND category = ? AND occurred_at >= ?", petID, model.RecordSymptom, "高", weekAgo).Count(&highRisk) return &Poster{ PetName: pet.Name, PetEmoji: pet.Emoji, PetAvatarURL: pet.AvatarURL, Age: pet.Age, Weight: pet.Weight, Stage: pet.Stage, + Period: weekAgo.Format("01.02") + " - " + now.Format("01.02"), TasksCompleted: tasksDone, WeightRecords: weightRecs, VaccineRecords: vaccineRecs, - HighRiskCount: 0, Headline: "稳定成长", + HighRiskCount: highRisk, Headline: "稳定成长", }, nil } diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js index e9d1366..9fe6f36 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.js +++ b/pets-fe/components/bottom-sheet/bottom-sheet.js @@ -261,10 +261,17 @@ Component({ 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); + ctx.fillText([pet.age, pet.weight, pet.stage].filter(Boolean).join(' | '), W / 2, 262); + + // 周报告:显示统计周期,让人知道这四个数是近 7 天的 + if (p.period) { + ctx.fillStyle = '#C7A36A'; + ctx.font = '22px sans-serif'; + ctx.fillText('近 7 天 · ' + p.period, W / 2, 298); + } ctx.fillStyle = 'rgba(255,255,255,.8)'; - this.roundRect(ctx, 60, 310, W - 120, 232, 24); + this.roundRect(ctx, 60, 316, W - 120, 232, 24); ctx.fill(); const lines = [ '完成任务 ' + (p.tasks_completed || 0) + ' 项', -- 2.52.0 From 72c53082148a7b4af689c5e81a4ecd58aeb467cb Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 16:08:23 +0800 Subject: [PATCH 20/33] =?UTF-8?q?fix(be):=20=E6=B5=B7=E6=8A=A5=20pet=5Fava?= =?UTF-8?q?tar=5Furl=20=E4=B8=80=E7=9B=B4=E4=B8=BA=E7=A9=BA=E2=80=94?= =?UTF-8?q?=E2=80=94ownedPet=20=E4=B8=8D=E8=A7=A3=E6=9E=90=E5=A4=B4?= =?UTF-8?q?=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GetPoster 用 ownedPet 只填了 Age,头像 URL 没解析,导致 pet_avatar_url 恒为空、海报总是退回 emoji。改成显式 s.fileURL(pet.AvatarFileID)。 Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/report.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pets-be/internal/service/report.go b/pets-be/internal/service/report.go index 5a8287d..4f53507 100644 --- a/pets-be/internal/service/report.go +++ b/pets-be/internal/service/report.go @@ -229,8 +229,9 @@ func (s *Service) GetPoster(userID, petID string) (*Poster, error) { s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ? AND occurred_at >= ?", petID, model.RecordVaccine, weekAgo).Count(&vaccineRecs) s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ? AND category = ? AND occurred_at >= ?", petID, model.RecordSymptom, "高", weekAgo).Count(&highRisk) + // ownedPet 只填了 Age,头像 URL 得在这里从 file id 解析(否则是空的) return &Poster{ - PetName: pet.Name, PetEmoji: pet.Emoji, PetAvatarURL: pet.AvatarURL, + PetName: pet.Name, PetEmoji: pet.Emoji, PetAvatarURL: s.fileURL(pet.AvatarFileID), Age: pet.Age, Weight: pet.Weight, Stage: pet.Stage, Period: weekAgo.Format("01.02") + " - " + now.Format("01.02"), TasksCompleted: tasksDone, WeightRecords: weightRecs, VaccineRecords: vaccineRecs, -- 2.52.0 From 931fb69ef2e061ff1e57fca36af2a7ab5fd2eb5e Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 16:10:42 +0800 Subject: [PATCH 21/33] =?UTF-8?q?fix(fe):=20=E6=B5=B7=E6=8A=A5=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E5=8D=A1(wxml)=E4=B9=9F=E6=98=BE=E7=A4=BA=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E5=A4=B4=E5=83=8F=20+=20=E7=BB=9F=E8=AE=A1=E5=91=A8?= =?UTF-8?q?=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前只改了导出用的 canvas,但弹层里看到的预览卡是 wxml 渲染的, 头像写死 emoji、也没周期。现在预览卡:有照片用照片(poster.pet_avatar_url 兜底 pet.avatar_url)、加「近 7 天 · 日期」周期行,和导出图一致。 Co-Authored-By: Claude Opus 5 --- pets-fe/app.wxss | 4 +++- pets-fe/components/bottom-sheet/bottom-sheet.wxml | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss index 419d4a7..a628551 100644 --- a/pets-fe/app.wxss +++ b/pets-fe/app.wxss @@ -342,8 +342,10 @@ textarea{display:block;padding:var(--sp-4);line-height:1.6} .poster-avatar{ width:152rpx;height:152rpx;margin:var(--sp-3) auto var(--sp-3);border-radius:var(--r-lg); background:linear-gradient(135deg,#FFE5B7,#FFC06B);display:flex;align-items:center;justify-content:center; - font-size:80rpx;box-shadow:var(--sd-1); + font-size:80rpx;box-shadow:var(--sd-1);overflow:hidden; } +.poster-avatar-img{width:100%;height:100%;display:block} +.poster-period{margin-top:8rpx;font-size:var(--fs-cap);color:var(--primary-ink);font-weight:var(--fw-b)} .poster-list{ background:rgba(255,255,255,.78);border-radius:var(--r-md);padding:var(--sp-4);margin:var(--sp-4) 0;text-align:left;line-height:1.8;font-size:var(--fs-md); } diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxml b/pets-fe/components/bottom-sheet/bottom-sheet.wxml index 7850ebd..adc2d57 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxml +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxml @@ -155,8 +155,12 @@ module.exports.sel = function (map, key, index, def) { {{pet.name}} 的成长报告 - {{pet.emoji}} + + + {{pet.emoji}} + {{pet.age}}|{{pet.weight}}|{{pet.stage}} + 近 7 天 · {{poster.period}} ✓ 完成任务 {{poster ? poster.tasks_completed : 0}} 项 ✓ 体重记录 {{poster ? poster.weight_records : 0}} 次 ✓ 疫苗记录 {{poster ? poster.vaccine_records : 0}} 次 -- 2.52.0 From 93fc9bc5dad58f6c4b522513ed138921402c0823 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 16:16:38 +0800 Subject: [PATCH 22/33] =?UTF-8?q?style(fe):=20=E6=88=90=E9=95=BF=E6=8A=A5?= =?UTF-8?q?=E5=91=8A=E6=B5=B7=E6=8A=A5=E9=A2=84=E8=A7=88=E5=8D=A1=E6=8E=92?= =?UTF-8?q?=E7=89=88=E6=89=93=E7=A3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 头像改圆形 + 白边(和保存的图一致) - 周期做成小胶囊;数据清单每行「绿勾 + 项目 + 数值右对齐」,更整齐 - 健康状态做成绿色徽章;生成自弱化 Co-Authored-By: Claude Opus 5 --- pets-fe/app.wxss | 37 ++++++++++++++++--- .../components/bottom-sheet/bottom-sheet.wxml | 16 ++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss index a628551..5a04c51 100644 --- a/pets-fe/app.wxss +++ b/pets-fe/app.wxss @@ -339,16 +339,43 @@ textarea{display:block;padding:var(--sp-4);line-height:1.6} linear-gradient(160deg,#FFF7E8,#FFFFFF); border:1rpx solid #F2DFC3;border-radius:var(--r-lg);padding:var(--sp-5);text-align:center;overflow:hidden;position:relative; } +/* 圆头像 + 白边,和保存的图一致 */ .poster-avatar{ - width:152rpx;height:152rpx;margin:var(--sp-3) auto var(--sp-3);border-radius:var(--r-lg); + width:160rpx;height:160rpx;margin:var(--sp-4) auto var(--sp-3);border-radius:50%; background:linear-gradient(135deg,#FFE5B7,#FFC06B);display:flex;align-items:center;justify-content:center; - font-size:80rpx;box-shadow:var(--sd-1);overflow:hidden; + font-size:80rpx;box-shadow:var(--sd-1);overflow:hidden;border:6rpx solid #fff; } .poster-avatar-img{width:100%;height:100%;display:block} -.poster-period{margin-top:8rpx;font-size:var(--fs-cap);color:var(--primary-ink);font-weight:var(--fw-b)} -.poster-list{ - background:rgba(255,255,255,.78);border-radius:var(--r-md);padding:var(--sp-4);margin:var(--sp-4) 0;text-align:left;line-height:1.8;font-size:var(--fs-md); +.poster-meta{font-size:var(--fs-md);color:var(--muted)} +/* 周期做成小胶囊 */ +.poster-period{ + display:inline-block;margin-top:12rpx;padding:4rpx var(--sp-3);border-radius:var(--r-full); + background:var(--primary-soft);color:var(--primary-ink); + font-size:var(--fs-cap);font-weight:var(--fw-b); } +/* 数据清单:每行 打勾 + 项目 + 数值 右对齐 */ +.poster-list{ + background:rgba(255,255,255,.78);border-radius:var(--r-md); + padding:var(--sp-3) var(--sp-4);margin:var(--sp-5) 0 var(--sp-4); +} +.poster-row{display:flex;align-items:center;gap:var(--sp-3);padding:var(--sp-2) 0} +.poster-tick{ + width:36rpx;height:36rpx;flex:none;border-radius:50%; + background:var(--green-soft);color:var(--green-ink); + display:flex;align-items:center;justify-content:center; +} +.poster-rt{flex:1;text-align:left;font-size:var(--fs-md);color:var(--text-2)} +.poster-rn{font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text)} +/* 健康状态徽章 */ +.poster-status{ + display:flex;align-items:center;justify-content:center;gap:var(--sp-2); + font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text);margin-top:var(--sp-2); +} +.poster-badge{ + padding:4rpx var(--sp-3);border-radius:var(--r-full); + background:var(--green-soft);color:var(--green-ink);font-size:var(--fs-sm); +} +.poster-from{margin-top:var(--sp-4);font-size:var(--fs-cap);color:var(--muted2)} /* 风险盒 */ .risk-box{ diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxml b/pets-fe/components/bottom-sheet/bottom-sheet.wxml index adc2d57..96e5314 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxml +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxml @@ -159,14 +159,16 @@ module.exports.sel = function (map, key, index, def) { {{pet.emoji}} - {{pet.age}}|{{pet.weight}}|{{pet.stage}} + {{pet.age}}|{{pet.weight}}|{{pet.stage}} 近 7 天 · {{poster.period}} - ✓ 完成任务 {{poster ? poster.tasks_completed : 0}} 项 -✓ 体重记录 {{poster ? poster.weight_records : 0}} 次 -✓ 疫苗记录 {{poster ? poster.vaccine_records : 0}} 次 -✓ 高风险异常 {{poster ? poster.high_risk_count : 0}} 次 - 健康状态:{{poster ? poster.headline : '稳定成长'}} - 生成自:肉垫计划 + + 完成任务{{poster ? poster.tasks_completed : 0}} 项 + 体重记录{{poster ? poster.weight_records : 0}} 次 + 疫苗记录{{poster ? poster.vaccine_records : 0}} 次 + 高风险异常{{poster ? poster.high_risk_count : 0}} 次 + + 健康状态{{poster ? poster.headline : '稳定成长'}} + 生成自 · 肉垫计划 -- 2.52.0 From ef172f4e4aab6cb56470ddf301d5cb5cfa1870b8 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 17:12:56 +0800 Subject: [PATCH 23/33] =?UTF-8?q?feat:=20=E5=AE=A0=E7=89=A9=E5=8A=A0?= =?UTF-8?q?=E6=80=A7=E6=A0=BC/=E8=BF=87=E6=95=8F/=E5=A4=87=E6=B3=A8?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=20+=20=E8=BA=AB=E4=BB=BD=E5=8D=A1=E7=85=A7?= =?UTF-8?q?=E6=8A=A4=E4=BF=A1=E6=81=AF=20+=20=E5=BE=AE=E4=BF=A1=E5=8F=96?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Pet 增 personality/allergy/notes;建档表单加「性格标签(多选)/过敏/备注」 - IDCard 增 color/personality/owner/phone(脱敏)/allergy/notes/疫苗驱虫状态/发证机构 - 微信取手机号:POST /api/user/phone 用 getPhoneNumber 的 code 换手机号存库 (BindPhoneByCode 走 getuserphonenumber,复用 access_token) Co-Authored-By: Claude Opus 5 --- pets-be/internal/handler/auth.go | 17 +++++++++ pets-be/internal/handler/pet.go | 15 ++++++++ pets-be/internal/model/pet.go | 5 +++ pets-be/internal/router/router.go | 1 + pets-be/internal/service/idcard.go | 53 +++++++++++++++++++++++++++-- pets-be/internal/service/pet.go | 6 ++++ pets-be/internal/service/wxacode.go | 37 ++++++++++++++++++++ pets-fe/pages/petform/petform.js | 27 +++++++++++++++ pets-fe/pages/petform/petform.wxml | 10 ++++++ 9 files changed, 169 insertions(+), 2 deletions(-) diff --git a/pets-be/internal/handler/auth.go b/pets-be/internal/handler/auth.go index 96696e9..d730872 100644 --- a/pets-be/internal/handler/auth.go +++ b/pets-be/internal/handler/auth.go @@ -210,3 +210,20 @@ func (h *Handler) UpdateProfile(c *gin.Context) { } response.OK(c, user) } + +// BindPhone POST /api/user/phone 用小程序 getPhoneNumber 的 code 绑定手机号 +func (h *Handler) BindPhone(c *gin.Context) { + var req struct { + Code string `json:"code"` + } + if err := c.ShouldBindJSON(&req); err != nil || req.Code == "" { + response.FailParams(c, "缺少 code") + return + } + phone, err := h.svc.BindPhoneByCode(middleware.UserID(c), req.Code) + if err != nil { + response.FailErr(c, err) + return + } + response.OK(c, gin.H{"phone": phone}) +} diff --git a/pets-be/internal/handler/pet.go b/pets-be/internal/handler/pet.go index 6aa8546..b49c187 100644 --- a/pets-be/internal/handler/pet.go +++ b/pets-be/internal/handler/pet.go @@ -26,6 +26,9 @@ type petReq struct { Breed string `json:"breed"` AvatarFileID string `json:"avatar_file_id"` Goals []string `json:"goals"` + Personality string `json:"personality"` + Allergy string `json:"allergy"` + Notes string `json:"notes"` } func (r petReq) toInput() service.PetInput { @@ -33,6 +36,7 @@ func (r petReq) toInput() service.PetInput { Name: r.Name, Emoji: r.Emoji, Type: r.Type, Gender: r.Gender, Weight: r.Weight, Stage: r.Stage, Age: r.Age, Color: r.Color, Breed: r.Breed, AvatarFileID: r.AvatarFileID, + Personality: r.Personality, Allergy: r.Allergy, Notes: r.Notes, } if r.Birthday != "" { if t, err := time.ParseInLocation("2006-01-02", r.Birthday, time.Local); err == nil { @@ -123,6 +127,17 @@ func (h *Handler) UpdatePet(c *gin.Context) { if req.Color != "" { fields["color"] = req.Color } + // 性格/过敏/备注:空串也允许写(用户可能想清空),用指针语义太重, + // 这里约定前端总是回传当前值,非空才更新;清空场景暂用「无」占位 + if req.Personality != "" { + fields["personality"] = req.Personality + } + if req.Allergy != "" { + fields["allergy"] = req.Allergy + } + if req.Notes != "" { + fields["notes"] = req.Notes + } if req.Breed != "" { fields["breed"] = req.Breed } diff --git a/pets-be/internal/model/pet.go b/pets-be/internal/model/pet.go index d8e6566..9d85e0a 100644 --- a/pets-be/internal/model/pet.go +++ b/pets-be/internal/model/pet.go @@ -27,4 +27,9 @@ type Pet struct { Breed string `gorm:"size:64" json:"breed"` // 品种 HealthStatus string `gorm:"size:16;default:正常" json:"health_status"` Goals datatypes.JSON `json:"goals"` // onboarding 目标多选 + + // —— 身份卡用的补充信息 —— + Personality string `gorm:"size:64" json:"personality"` // 性格标签,逗号分隔,如 "活泼,亲人,贪吃" + Allergy string `gorm:"size:128" json:"allergy"` // 过敏情况,没有就填「无」 + Notes string `gorm:"size:255" json:"notes"` // 备注信息(照护提醒) } diff --git a/pets-be/internal/router/router.go b/pets-be/internal/router/router.go index b7394f0..058ed77 100644 --- a/pets-be/internal/router/router.go +++ b/pets-be/internal/router/router.go @@ -49,6 +49,7 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage g.GET("/user/profile", h.Profile) g.PUT("/user/profile", h.UpdateProfile) + g.POST("/user/phone", h.BindPhone) g.GET("/user/summary", h.UserSummary) g.PUT("/user/decoration", h.UpdateDecoration) diff --git a/pets-be/internal/service/idcard.go b/pets-be/internal/service/idcard.go index a11dd47..26b0a2f 100644 --- a/pets-be/internal/service/idcard.go +++ b/pets-be/internal/service/idcard.go @@ -17,11 +17,23 @@ type IDCard struct { Zodiac string `json:"zodiac"` // 星座,按生日算 Weight string `json:"weight"` // 体重,如 "13.5kg" Intro string `json:"intro"` + Color string `json:"color"` // 毛色 + Personality string `json:"personality"` // 性格标签,逗号分隔 AvatarURL string `json:"avatar_url"` IDNo string `json:"id_no"` // 玩具身份号,不入库 Validity string `json:"validity"` // "2023.12.15 - 永远" - QRURL string `json:"qr_url"` // 小程序码,未发布时为空 - QRReady bool `json:"qr_ready"` // 码有没有生成出来 + + // —— 背面:照护信息 —— + Owner string `json:"owner"` // 主人昵称 + Phone string `json:"phone"` // 联系电话(脱敏) + Allergy string `json:"allergy"` // 过敏情况 + Notes string `json:"notes"` // 备注 + VaccineState string `json:"vaccine_state"` // 疫苗情况 + DewormState string `json:"deworm_state"` // 驱虫情况 + Org string `json:"org"` // 发证机构 + + QRURL string `json:"qr_url"` // 小程序码,未发布时为空 + QRReady bool `json:"qr_ready"` // 码有没有生成出来 } // petIDNo 造一个稳定的「身份号」。纯装饰,不入库、不做校验。 @@ -73,6 +85,14 @@ func zodiacOf(month, day int) string { return name } +// maskPhone 脱敏手机号:138****8888。空或非 11 位原样返回 +func maskPhone(p string) string { + if len(p) != 11 { + return p + } + return p[:3] + "****" + p[7:] +} + // GetIDCard 组装身份卡。小程序码是 best-effort:拿不到(未发布/配额)也照出卡 func (s *Service) GetIDCard(userID, petID string) (*IDCard, error) { pet, err := s.GetPet(userID, petID) @@ -99,10 +119,39 @@ func (s *Service) GetIDCard(userID, petID string) (*IDCard, error) { zodiac = zodiacOf(int(pet.Birthday.Month()), pet.Birthday.Day()) } + // 主人昵称 + 脱敏电话 + var user model.User + s.db.First(&user, userID) + owner := user.Nickname + if owner == "" { + owner = "铲屎官" + } + + // 疫苗:有记录算已接种;驱虫:取下一次到期提醒的日期 + var vaccineCnt int64 + s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordVaccine).Count(&vaccineCnt) + vaccineState := "待接种" + if vaccineCnt > 0 { + vaccineState = "已接种" + } + dewormState := "—" + var dw model.Reminder + if err := s.db.Where("pet_id = ? AND type = ?", petID, model.ReminderDeworm).Order("next_due_date asc").First(&dw).Error; err == nil && dw.NextDueDate != nil { + dewormState = dw.NextDueDate.Format("2006.01.02") + } + + allergy := pet.Allergy + if allergy == "" { + allergy = "无" + } + card := &IDCard{ Name: pet.Name, Species: pet.Type, Gender: pet.Gender, Breed: pet.Breed, Birthday: bd, Zodiac: zodiac, Weight: pet.Weight, Intro: intro, + Color: pet.Color, Personality: pet.Personality, AvatarURL: pet.AvatarURL, IDNo: petIDNo(pet), Validity: validity, + Owner: owner, Phone: maskPhone(user.Phone), Allergy: allergy, Notes: pet.Notes, + VaccineState: vaccineState, DewormState: dewormState, Org: "肉垫计划", } // 小程序码取不到不算错——未发布/配额用尽时卡照样出,只是没码 diff --git a/pets-be/internal/service/pet.go b/pets-be/internal/service/pet.go index 5cf8d22..e70c3d2 100644 --- a/pets-be/internal/service/pet.go +++ b/pets-be/internal/service/pet.go @@ -27,6 +27,9 @@ type PetInput struct { Breed string AvatarFileID string Goals datatypes.JSON + Personality string + Allergy string + Notes string } func normalizeWeight(w string) string { @@ -86,6 +89,9 @@ func (s *Service) CreatePet(userID string, in PetInput) (*model.Pet, error) { Color: in.Color, Breed: in.Breed, AvatarFileID: in.AvatarFileID, + Personality: in.Personality, + Allergy: in.Allergy, + Notes: in.Notes, HealthStatus: "正常", Goals: in.Goals, } diff --git a/pets-be/internal/service/wxacode.go b/pets-be/internal/service/wxacode.go index 769c368..12b3e6d 100644 --- a/pets-be/internal/service/wxacode.go +++ b/pets-be/internal/service/wxacode.go @@ -11,6 +11,8 @@ import ( "net/url" "sync" "time" + + "github.com/sundynix/pets-be/internal/model" ) // 小程序码(wxacode)。身份卡右下角那个码,扫了能进小程序。 @@ -123,3 +125,38 @@ func (s *Service) PetQRCode(petID string) (string, error) { // 41030 = page 不在已发布版本里。开发期常见,往上层透传让它降级 return "", fmt.Errorf("微信生成小程序码失败(%d): %s", e.ErrCode, e.ErrMsg) } + +// BindPhoneByCode 用小程序 getPhoneNumber 拿到的 code 换手机号,存到用户上。 +// 返回脱敏后的手机号。开发期/未认证时微信会报错,往上层透传。 +func (s *Service) BindPhoneByCode(userID, code string) (string, error) { + token, err := s.wechatAccessToken() + if err != nil { + return "", err + } + payload, _ := json.Marshal(map[string]string{"code": code}) + var out struct { + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + PhoneInfo struct { + PhoneNumber string `json:"phoneNumber"` + PurePhoneNumber string `json:"purePhoneNumber"` + } `json:"phone_info"` + } + if err := postJSON("https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token="+token, payload, &out); err != nil { + return "", err + } + if out.ErrCode != 0 { + return "", fmt.Errorf("获取手机号失败(%d): %s", out.ErrCode, out.ErrMsg) + } + phone := out.PhoneInfo.PurePhoneNumber + if phone == "" { + phone = out.PhoneInfo.PhoneNumber + } + if phone == "" { + return "", fmt.Errorf("微信没返回手机号") + } + if err := s.db.Model(&model.User{}).Where("id = ?", userID).Update("phone", phone).Error; err != nil { + return "", err + } + return maskPhone(phone), nil +} diff --git a/pets-fe/pages/petform/petform.js b/pets-fe/pages/petform/petform.js index 0137fc7..0c6c725 100644 --- a/pets-fe/pages/petform/petform.js +++ b/pets-fe/pages/petform/petform.js @@ -4,6 +4,8 @@ const upload = require('../../utils/upload.js'); const { toastErr } = require('../../utils/ui.js'); const GENDERS = ['男孩', '女孩', '不确定']; +// 性格标签预设,多选,存成逗号分隔串 +const PERSONA_TAGS = ['活泼', '亲人', '粘人', '高冷', '贪吃', '胆小', '好奇', '温顺']; // 阶段列表和划分标准都从后端来。原来这里写死 4 个,后端细分成 5 段 // (而且狗还按体型走不同阈值)之后就对不上了 —— 而且不会报错,只是少两个选项。 // 「刚到家 0-30 天」在后端列表的第一个,它和年龄正交,是叠加层 @@ -54,10 +56,13 @@ Page({ species: 'cat', breed: '', gender: '不确定', birthday: '', arrived_at: '', weight: '', color: '', stage: '', + personality: '', allergy: '', notes: '', // 第四步挑的方案。'' 是「先空着自己排」,不是「没选」—— // 所以初始值给 null,用来区分「还没走到第四步」 template_id: null, }, + personaTags: PERSONA_TAGS, + personaSel: {}, // 性格标签选中态 map tplOptions: [], breedShow: false, breedGroups: [], @@ -88,8 +93,12 @@ Page({ arrived_at: p.arrived_at ? String(p.arrived_at).slice(0, 10) : '', weight: (p.weight || '').replace('kg', ''), color: p.color || '', + personality: p.personality || '', + allergy: p.allergy || '', + notes: p.notes || '', stage: p.stage || '', }, + personaSel: (p.personality || '').split(',').filter(Boolean).reduce((m, t) => ((m[t] = true), m), {}), }); }) .catch((e) => toastErr(e)); @@ -170,6 +179,21 @@ Page({ onColor(e) { this.setData({ 'form.color': e.detail.value }); }, + onAllergy(e) { + this.setData({ 'form.allergy': e.detail.value }); + }, + onNotes(e) { + this.setData({ 'form.notes': e.detail.value }); + }, + // 性格标签多选:切换选中态,再拼回逗号串存进 form.personality + togglePersona(e) { + const tag = e.currentTarget.dataset.v; + const sel = Object.assign({}, this.data.personaSel); + if (sel[tag]) delete sel[tag]; + else sel[tag] = true; + const personality = this.data.personaTags.filter((t) => sel[t]).join(','); + this.setData({ personaSel: sel, 'form.personality': personality }); + }, // ── 品种选择器 ── openBreed() { @@ -258,6 +282,9 @@ Page({ weight: f.weight, color: f.color.trim(), breed: f.breed, + personality: f.personality, + allergy: f.allergy.trim(), + notes: f.notes.trim(), stage: f.stage || stageFromBirthday(f.birthday) || '成年期', age: ageLabel(f.birthday), }; diff --git a/pets-fe/pages/petform/petform.wxml b/pets-fe/pages/petform/petform.wxml index cd65a06..77cfc5a 100644 --- a/pets-fe/pages/petform/petform.wxml +++ b/pets-fe/pages/petform/petform.wxml @@ -74,6 +74,16 @@ + + {{item}} + + + + + {{item}} -- 2.52.0 From 15adfb76aca9e3ff760313860d37da574128995b Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 17:28:26 +0800 Subject: [PATCH 24/33] =?UTF-8?q?feat(fe):=20=E8=BA=AB=E4=BB=BD=E5=8D=A1?= =?UTF-8?q?=E9=87=8D=E7=BB=98=E6=88=90=E6=AD=A3=E9=9D=A2+=E7=85=A7?= =?UTF-8?q?=E6=8A=A4=E4=BF=A1=E6=81=AF+=E8=AF=81=E4=BB=B6=E4=B8=89?= =?UTF-8?q?=E6=AE=B5=E5=BC=8F=20+=20=E6=89=8B=E6=9C=BA=E5=8F=B7=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 正面:头像/名字/物种性别 + 品种·毛色 / 出生·星座 / 体重 + 性格标签chips - 照护信息:主人 / 联系电话 / 疫苗情况 / 备注(去掉驱虫、过敏) - 证件区:居民身份卡 + 身份ID / 发证机构 / 有效期 + 小程序码 - 未验证手机号时页面出现「用微信验证手机号」(getPhoneNumber), 拿到 code 换手机号后重取卡重画;表单去掉过敏输入 Co-Authored-By: Claude Opus 5 --- pets-fe/pages/idcard/idcard.js | 219 +++++++++++++++-------------- pets-fe/pages/idcard/idcard.wxml | 8 ++ pets-fe/pages/idcard/idcard.wxss | 4 + pets-fe/pages/petform/petform.wxml | 3 - pets-fe/utils/api.js | 1 + 5 files changed, 124 insertions(+), 111 deletions(-) diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index 7958780..151af77 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -4,7 +4,7 @@ const { toastErr } = require('../../utils/ui.js'); // 卡片画布尺寸(逻辑像素)。真实导出会按 dpr 放大 const W = 620; -const H = 812; +const H = 1000; // 在 canvas 2d 上加载一张图(远程或本地),resolve 出可直接 drawImage 的 image 对象。 // 加载失败 resolve(null)——头像或小程序码缺一张不该让整张卡画不出来 @@ -83,46 +83,37 @@ Page({ const pad = 30; const cardW = W - pad * 2; - // ── 上半张:资料区(蓝紫渐变)── - const topH = 430; - const g1 = ctx.createLinearGradient(pad, pad, W - pad, topH); + // ═══ 正面:资料区(暖粉渐变)═══ + const s1Y = pad, s1H = 378; + const g1 = ctx.createLinearGradient(pad, s1Y, W - pad, s1Y + s1H); g1.addColorStop(0, '#FFF0D6'); g1.addColorStop(0.5, '#FFE0EC'); g1.addColorStop(1, '#E6EEFF'); - this.roundRect(ctx, pad, pad, cardW, topH - pad, 28); + this.roundRect(ctx, pad, s1Y, cardW, s1H, 28); ctx.fillStyle = g1; ctx.fill(); - - // 肉垫水印:卡内几枚淡淡的爪印,裁剪在圆角卡里 + // 爪印水印 ctx.save(); - this.roundRect(ctx, pad, pad, cardW, topH - pad, 28); + this.roundRect(ctx, pad, s1Y, cardW, s1H, 28); ctx.clip(); ctx.globalAlpha = 0.06; - ctx.font = '120px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillText('🐾', W - pad - 70, pad + 90); - ctx.font = '80px sans-serif'; - ctx.fillText('🐾', W - pad - 150, topH - 70); + ctx.font = '130px sans-serif'; + ctx.fillText('🐾', W - pad - 80, s1Y + 100); ctx.restore(); ctx.globalAlpha = 1; ctx.textBaseline = 'alphabetic'; - // 头像圆(描一圈白边更像证件照) - const avX = pad + 34, avY = pad + 34, avD = 120; + // 头像圆 + 白边 + const avX = pad + 34, avY = s1Y + 34, avD = 118; ctx.save(); ctx.beginPath(); ctx.arc(avX + avD / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2); - ctx.closePath(); ctx.clip(); if (avatar) { - // 居中裁剪成正方形再画:竖图/横图都不会被压变形(cover 效果) - const iw = avatar.width || avD; - const ih = avatar.height || avD; - const side = Math.min(iw, ih); - const sx = (iw - side) / 2; - const sy = (ih - side) / 2; - ctx.drawImage(avatar, sx, sy, side, side, avX, avY, avD, avD); + const iw = avatar.width || avD, ih = avatar.height || avD, side = Math.min(iw, ih); + ctx.drawImage(avatar, (iw - side) / 2, (ih - side) / 2, side, side, avX, avY, avD, avD); } else { ctx.fillStyle = '#FFD9A3'; ctx.fillRect(avX, avY, avD, avD); @@ -134,27 +125,25 @@ Page({ } ctx.restore(); ctx.textBaseline = 'alphabetic'; - // 头像白边圈 ctx.beginPath(); ctx.arc(avX + avD / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2); ctx.strokeStyle = 'rgba(255,255,255,.9)'; ctx.lineWidth = 5; ctx.stroke(); - // 资料行 - const infoX = avX + avD + 28; - let y = pad + 52; - const line = (label, value) => { - ctx.textAlign = 'left'; - ctx.font = '24px sans-serif'; - ctx.fillStyle = '#9A8F82'; - ctx.fillText(label, infoX, y); - ctx.fillStyle = '#2D2925'; - ctx.font = '600 26px sans-serif'; - ctx.fillText(value || '—', infoX + ctx.measureText(label).width + 14, y); - y += 46; - }; - // 一行放两组「标签 值」,X 起点给定 + // 名字大字 + 物种·性别(头像右侧) + const nameX = avX + avD + 26; + ctx.textAlign = 'left'; + ctx.fillStyle = '#2D2925'; + ctx.font = '700 46px sans-serif'; + ctx.fillText(c.name || '毛孩子', nameX, avY + 54); + ctx.fillStyle = '#9A8F82'; + ctx.font = '24px sans-serif'; + ctx.fillText(c.species + (c.gender ? ' · ' + c.gender : ''), nameX, avY + 96); + + // 资料 pair 行(头像下方,全宽两列) + const infoX = pad + 40; + let y = s1Y + avD + 60; const pair = (l1, v1, l2, v2) => { ctx.textAlign = 'left'; ctx.font = '24px sans-serif'; @@ -162,95 +151,86 @@ Page({ ctx.fillText(l1, infoX, y); ctx.fillStyle = '#2D2925'; ctx.font = '600 26px sans-serif'; - const w1 = ctx.measureText(v1 || '—').width; - ctx.fillText(v1 || '—', infoX + 62, y); - if (l2 && v2) { - const x2 = infoX + 62 + w1 + 26; + ctx.fillText(v1 || '—', infoX + 66, y); + if (l2) { + const x2 = infoX + 300; ctx.fillStyle = '#9A8F82'; ctx.font = '24px sans-serif'; ctx.fillText(l2, x2, y); ctx.fillStyle = '#2D2925'; ctx.font = '600 26px sans-serif'; - ctx.fillText(v2, x2 + 62, y); + ctx.fillText(v2 || '—', x2 + 66, y); } - y += 46; + y += 44; }; - line('姓名', c.name); - pair('性别', c.gender, '品种', c.breed); - line('出生', c.birthday); - pair('星座', c.zodiac, '体重', c.weight); - line('介绍', c.intro); + pair('品种', c.breed, '毛色', c.color); + pair('出生', c.birthday, '星座', c.zodiac); + pair('体重', c.weight, '', ''); - // 身份 ID:虚线 + 号 - const idY = pad + 344; - ctx.strokeStyle = 'rgba(122,74,8,.22)'; - ctx.lineWidth = 1; - ctx.setLineDash([6, 6]); - ctx.beginPath(); - ctx.moveTo(pad + 34, idY); - ctx.lineTo(W - pad - 34, idY); - ctx.stroke(); - ctx.setLineDash([]); - ctx.textAlign = 'left'; - ctx.font = '22px sans-serif'; - ctx.fillStyle = '#9A8F82'; - ctx.fillText('身份 ID', pad + 34, idY + 34); - ctx.font = '600 26px sans-serif'; - ctx.fillStyle = '#2D2925'; - ctx.fillText(c.id_no || '', pad + 34 + 90, idY + 34); + // 性格标签 chips + const tags = (c.personality || '').split(',').filter(Boolean); + if (tags.length) { + let tx = infoX; + const ty = y + 8; + tags.forEach((t) => { + ctx.font = '22px sans-serif'; + const tw = ctx.measureText(t).width + 28; + ctx.fillStyle = 'rgba(245,169,71,.2)'; + this.roundRect(ctx, tx, ty - 26, tw, 36, 18); + ctx.fill(); + ctx.fillStyle = '#A96500'; + ctx.textAlign = 'left'; + ctx.fillText(t, tx + 14, ty); + tx += tw + 12; + }); + } - // ── 下半张:证件标题 + 小程序码 ── - const botY = topH + 12; - const botH = H - botY - pad; - const g2 = ctx.createLinearGradient(pad, botY, W - pad, botY + botH); - g2.addColorStop(0, '#E8F0FF'); - g2.addColorStop(1, '#F0E8FF'); - this.roundRect(ctx, pad, botY, cardW, botH, 28); - ctx.fillStyle = g2; + // ═══ 照护信息 ═══ + const s2Y = s1Y + s1H + 14, s2H = 290; + this.roundRect(ctx, pad, s2Y, cardW, s2H, 28); + ctx.fillStyle = '#FFFFFF'; ctx.fill(); + ctx.textAlign = 'left'; + ctx.fillStyle = '#B4623F'; + ctx.font = '700 30px sans-serif'; + ctx.fillText('照护信息', pad + 40, s2Y + 56); + const kv = (label, value, yy) => { + ctx.textAlign = 'left'; + ctx.font = '24px sans-serif'; + ctx.fillStyle = '#9A8F82'; + ctx.fillText(label, pad + 40, yy); + ctx.font = '600 26px sans-serif'; + ctx.fillStyle = '#2D2925'; + ctx.fillText(value || '—', pad + 190, yy); + }; + kv('主人', c.owner, s2Y + 108); + kv('联系电话', c.phone || '未验证', s2Y + 152); + kv('疫苗情况', c.vaccine_state, s2Y + 196); + kv('备注', c.notes || '无', s2Y + 240); + // ═══ 证件区(蓝紫渐变)+ 小程序码 ═══ + const s3Y = s2Y + s2H + 14, s3H = H - s3Y - pad; + const g3 = ctx.createLinearGradient(pad, s3Y, W - pad, s3Y + s3H); + g3.addColorStop(0, '#E8F0FF'); + g3.addColorStop(1, '#F0E8FF'); + this.roundRect(ctx, pad, s3Y, cardW, s3H, 28); + ctx.fillStyle = g3; + ctx.fill(); ctx.textAlign = 'left'; ctx.fillStyle = '#7D89A8'; ctx.font = '22px sans-serif'; - ctx.fillText('宠 星 人 地 球 移 民', pad + 40, botY + 60); + ctx.fillText('宠 星 人 地 球 移 民', pad + 40, s3Y + 50); ctx.fillStyle = '#4A5878'; - ctx.font = '700 52px sans-serif'; - ctx.fillText('居民身份卡', pad + 40, botY + 128); - + ctx.font = '700 48px sans-serif'; + ctx.fillText('居民身份卡', pad + 40, s3Y + 112); ctx.fillStyle = '#8A93A8'; ctx.font = '22px sans-serif'; - ctx.fillText('签发机构 肉垫计划', pad + 40, botY + 180); - ctx.fillText('有效期限 ' + (c.validity || '永久有效'), pad + 40, botY + 214); + ctx.fillText('身份 ID ' + (c.id_no || ''), pad + 40, s3Y + 156); + ctx.fillText('签发机构 ' + (c.org || '肉垫计划'), pad + 40, s3Y + 190); + ctx.fillText('有效期限 ' + (c.validity || '永久有效'), pad + 40, s3Y + 224); - // 官方认证印章(装饰):淡棕红、旋一点角度,像盖上去的 - ctx.save(); - ctx.translate(W - pad - 120, botY + 100); - ctx.rotate((-14 * Math.PI) / 180); - ctx.globalAlpha = 0.5; - ctx.strokeStyle = '#B4623F'; - ctx.fillStyle = '#B4623F'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.lineWidth = 4; - ctx.beginPath(); - ctx.arc(0, 0, 60, 0, Math.PI * 2); - ctx.stroke(); - ctx.lineWidth = 1.5; - ctx.beginPath(); - ctx.arc(0, 0, 50, 0, Math.PI * 2); - ctx.stroke(); - ctx.font = '32px sans-serif'; - ctx.fillText('★', 0, -16); - ctx.font = '700 22px sans-serif'; - ctx.fillText('肉垫计划', 0, 16); - ctx.font = '12px sans-serif'; - ctx.fillText('官方认证', 0, 38); - ctx.restore(); - ctx.globalAlpha = 1; - ctx.textBaseline = 'alphabetic'; - - // 小程序码(未发布时 qr 为 null,画个占位框) - const qrD = 128, qrX = W - pad - 40 - qrD, qrY = botY + botH - 40 - qrD; + // 小程序码 + const qrD = 118, qrX = W - pad - 36 - qrD, qrY = s3Y + s3H - 36 - qrD; ctx.fillStyle = '#fff'; this.roundRect(ctx, qrX, qrY, qrD, qrD, 12); ctx.fill(); @@ -285,6 +265,29 @@ Page({ ctx.closePath(); }, + // 微信取手机号:新版 getPhoneNumber 返回动态 code,后端换成真实手机号存库, + // 拿回后重新取卡(此时电话就有了)并重画 + onGetPhone(e) { + const d = e.detail || {}; + if (!d.code) return; // 用户拒绝授权 + wx.showLoading({ title: '验证中…', mask: true }); + api + .bindPhone(d.code) + .then(() => api.petIDCard(this.data.petId)) + .then((card) => { + wx.hideLoading(); + if (card) { + this.setData({ card, qrReady: !!card.qr_ready }); + this.draw(); + } + wx.showToast({ title: '手机号已验证', icon: 'success' }); + }) + .catch((err) => { + wx.hideLoading(); + toastErr(err, '验证失败'); + }); + }, + onSave() { if (!this.data.cardImg) return wx.showToast({ title: '还在生成,稍等一下', icon: 'none' }); if (this.data.saving) return; diff --git a/pets-fe/pages/idcard/idcard.wxml b/pets-fe/pages/idcard/idcard.wxml index 8b85f45..7cab28d 100644 --- a/pets-fe/pages/idcard/idcard.wxml +++ b/pets-fe/pages/idcard/idcard.wxml @@ -8,6 +8,14 @@ 正在生成身份卡… + + + 背面「联系电话」还没验证,走失时联系不到你 + + + 小程序码要在小程序发布后才会出现,现在先放一个占位。 diff --git a/pets-fe/pages/idcard/idcard.wxss b/pets-fe/pages/idcard/idcard.wxss index be0963d..d93322c 100644 --- a/pets-fe/pages/idcard/idcard.wxss +++ b/pets-fe/pages/idcard/idcard.wxss @@ -19,3 +19,7 @@ button.ic-btn::after{border:none} /* 离屏 canvas:挪出可视区,只用来生成图 */ .ic-canvas{position:fixed;left:-9999rpx;top:0;width:620px;height:760px} + +/* 手机号验证提示 */ +.ic-phone-tip{display:flex;flex-direction:column;align-items:center;gap:var(--sp-3);color:var(--red-ink)} +.ic-phone-btn{width:auto;padding:0 var(--sp-6)} diff --git a/pets-fe/pages/petform/petform.wxml b/pets-fe/pages/petform/petform.wxml index 77cfc5a..8b3d69c 100644 --- a/pets-fe/pages/petform/petform.wxml +++ b/pets-fe/pages/petform/petform.wxml @@ -78,9 +78,6 @@ {{item}} - - diff --git a/pets-fe/utils/api.js b/pets-fe/utils/api.js index 20461f5..4a90407 100644 --- a/pets-fe/utils/api.js +++ b/pets-fe/utils/api.js @@ -42,6 +42,7 @@ const api = { // 用户 getProfile: () => request({ url: '/api/user/profile' }), + bindPhone: (code) => request({ url: '/api/user/phone', method: 'POST', data: { code } }), updateProfile: (body) => request({ url: '/api/user/profile', method: 'PUT', data: body }), // 用户 -- 2.52.0 From 586875f8c50260aad9c58a68aee88ff1638089c0 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 17:48:39 +0800 Subject: [PATCH 25/33] =?UTF-8?q?feat:=20=E8=BA=AB=E4=BB=BD=E5=8D=A1?= =?UTF-8?q?=E6=94=B9=E4=B8=8A=E4=B8=8B=E4=B8=A4=E5=BC=A0=E5=8D=A1(?= =?UTF-8?q?=E6=AD=A3=E9=9D=A2/=E8=83=8C=E9=9D=A2)=20+=20=E6=89=8B=E6=9C=BA?= =?UTF-8?q?=E5=8F=B7=E7=A1=AC=E9=97=A8=E6=A7=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - canvas 重画成上下两张:正面(暖黄,头像/名字性别/性别品种毛色年龄/性格 彩色标签/介绍/物ID+一起生活天数)、背面(蓝色照护信息条 + 主人/电话/疫苗/ 备注/发证机构/有效期 + 走失联系框&小程序码) - 生成前查手机号:没有则唤起 getPhoneNumber 组件,验证成功才生成; 拒绝则提示「无法生成」不出卡 - IDCard 增 age/days;ic-canvas 高度改 1480 否则导出被裁 Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/idcard.go | 11 +- pets-fe/pages/idcard/idcard.js | 279 +++++++++++++++++------------ pets-fe/pages/idcard/idcard.wxml | 51 +++--- pets-fe/pages/idcard/idcard.wxss | 19 +- 4 files changed, 221 insertions(+), 139 deletions(-) diff --git a/pets-be/internal/service/idcard.go b/pets-be/internal/service/idcard.go index 26b0a2f..b026f01 100644 --- a/pets-be/internal/service/idcard.go +++ b/pets-be/internal/service/idcard.go @@ -3,6 +3,7 @@ package service import ( "log" "strings" + "time" "github.com/sundynix/pets-be/internal/model" ) @@ -14,6 +15,8 @@ type IDCard struct { Gender string `json:"gender"` Breed string `json:"breed"` Birthday string `json:"birthday"` // "2023 年 12 月 15 日" + Age string `json:"age"` // 年龄,如 "2岁7个月" + Days int `json:"days"` // 一起生活的天数(按到家日) Zodiac string `json:"zodiac"` // 星座,按生日算 Weight string `json:"weight"` // 体重,如 "13.5kg" Intro string `json:"intro"` @@ -145,9 +148,15 @@ func (s *Service) GetIDCard(userID, petID string) (*IDCard, error) { allergy = "无" } + // 一起生活的天数:按到家日算,没填就 0 + days := 0 + if pet.ArrivedAt != nil && !pet.ArrivedAt.IsZero() { + days = int(time.Since(*pet.ArrivedAt).Hours()/24) + 1 + } + card := &IDCard{ Name: pet.Name, Species: pet.Type, Gender: pet.Gender, Breed: pet.Breed, - Birthday: bd, 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, AvatarURL: pet.AvatarURL, IDNo: petIDNo(pet), Validity: validity, Owner: owner, Phone: maskPhone(user.Phone), Allergy: allergy, Notes: pet.Notes, diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index 151af77..6eff9ac 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -4,7 +4,7 @@ const { toastErr } = require('../../utils/ui.js'); // 卡片画布尺寸(逻辑像素)。真实导出会按 dpr 放大 const W = 620; -const H = 1000; +const H = 1480; // 在 canvas 2d 上加载一张图(远程或本地),resolve 出可直接 drawImage 的 image 对象。 // 加载失败 resolve(null)——头像或小程序码缺一张不该让整张卡画不出来 @@ -25,6 +25,8 @@ Page({ cardImg: '', qrReady: false, saving: false, + needPhone: false, // 没手机号:门槛,先验证才生成 + phoneRefused: false, // 用户拒绝了授权 }, onLoad(q) { store @@ -41,6 +43,11 @@ Page({ .then((card) => { if (!card) return; this.setData({ card, qrReady: !!card.qr_ready }); + // 门槛:没手机号不生成,先引导用微信验证 + if (!card.phone) { + this.setData({ needPhone: true }); + return; + } // 布局要等页面渲染完 canvas 节点才拿得到,放 nextTick wx.nextTick(() => this.draw()); }) @@ -83,30 +90,50 @@ Page({ const pad = 30; const cardW = W - pad * 2; - // ═══ 正面:资料区(暖粉渐变)═══ - const s1Y = pad, s1H = 378; - const g1 = ctx.createLinearGradient(pad, s1Y, W - pad, s1Y + s1H); - g1.addColorStop(0, '#FFF0D6'); - g1.addColorStop(0.5, '#FFE0EC'); - g1.addColorStop(1, '#E6EEFF'); - this.roundRect(ctx, pad, s1Y, cardW, s1H, 28); - ctx.fillStyle = g1; + const fx = pad, cardW2 = cardW; + + // 顶部标题 + ctx.textAlign = 'center'; + ctx.fillStyle = '#8A5A18'; + ctx.font = '700 40px sans-serif'; + ctx.fillText('🐾 宠物身份卡 🐾', W / 2, 66); + + // ══════════ 正面卡(暖黄)══════════ + ctx.fillStyle = '#C7A36A'; + ctx.font = '22px sans-serif'; + ctx.fillText('· 正面 ·', W / 2, 116); + const fY = 140, fH = 700; + const gf = ctx.createLinearGradient(fx, fY, fx, fY + fH); + gf.addColorStop(0, '#FFF7E0'); + gf.addColorStop(1, '#FFE7AE'); + this.roundRect(ctx, fx, fY, cardW2, fH, 30); + ctx.fillStyle = gf; ctx.fill(); // 爪印水印 ctx.save(); - this.roundRect(ctx, pad, s1Y, cardW, s1H, 28); + this.roundRect(ctx, fx, fY, cardW2, fH, 30); ctx.clip(); - ctx.globalAlpha = 0.06; + ctx.globalAlpha = 0.07; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.font = '130px sans-serif'; - ctx.fillText('🐾', W - pad - 80, s1Y + 100); + ctx.font = '150px sans-serif'; + ctx.fillText('🐾', fx + cardW2 - 90, fY + fH - 80); ctx.restore(); ctx.globalAlpha = 1; ctx.textBaseline = 'alphabetic'; + // 卡头:左 logo,右 徽章 + ctx.textAlign = 'left'; + ctx.fillStyle = '#8A5A18'; + ctx.font = '600 26px sans-serif'; + ctx.fillText('🐱 宠星人', fx + 32, fY + 52); + ctx.textAlign = 'right'; + ctx.fillStyle = '#B07A1E'; + ctx.font = '600 22px sans-serif'; + ctx.fillText('🛡 宠物身份卡', fx + cardW2 - 32, fY + 50); + // 头像圆 + 白边 - const avX = pad + 34, avY = s1Y + 34, avD = 118; + const avD = 150, avX = fx + 36, avY = fY + 90; ctx.save(); ctx.beginPath(); ctx.arc(avX + avD / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2); @@ -118,7 +145,7 @@ Page({ ctx.fillStyle = '#FFD9A3'; ctx.fillRect(avX, avY, avD, avD); ctx.fillStyle = '#8A5A18'; - ctx.font = '52px sans-serif'; + ctx.font = '64px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(c.species === '狗狗' ? '🐶' : '🐱', avX + avD / 2, avY + avD / 2); @@ -127,121 +154,150 @@ Page({ ctx.textBaseline = 'alphabetic'; ctx.beginPath(); ctx.arc(avX + avD / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2); - ctx.strokeStyle = 'rgba(255,255,255,.9)'; - ctx.lineWidth = 5; + ctx.strokeStyle = '#fff'; + ctx.lineWidth = 8; ctx.stroke(); - // 名字大字 + 物种·性别(头像右侧) - const nameX = avX + avD + 26; + // 名字 + 性别(头像右侧) + const nx = avX + avD + 28; ctx.textAlign = 'left'; ctx.fillStyle = '#2D2925'; - ctx.font = '700 46px sans-serif'; - ctx.fillText(c.name || '毛孩子', nameX, avY + 54); - ctx.fillStyle = '#9A8F82'; - ctx.font = '24px sans-serif'; - ctx.fillText(c.species + (c.gender ? ' · ' + c.gender : ''), nameX, avY + 96); - - // 资料 pair 行(头像下方,全宽两列) - const infoX = pad + 40; - let y = s1Y + avD + 60; - const pair = (l1, v1, l2, v2) => { + ctx.font = '700 52px sans-serif'; + ctx.fillText(c.name || '毛孩子', nx, avY + 44); + if (c.gender) { + ctx.fillStyle = c.gender === '女孩' ? '#E86A9B' : '#5B9BE8'; + ctx.font = '30px sans-serif'; + ctx.fillText(c.gender === '女孩' ? '♀' : '♂', nx + ctx.measureText(c.name || '毛孩子').width + 12, avY + 44); + } + // 头像右的资料行 + const kvF = (label, value, yy) => { ctx.textAlign = 'left'; ctx.font = '24px sans-serif'; - ctx.fillStyle = '#9A8F82'; - ctx.fillText(l1, infoX, y); - ctx.fillStyle = '#2D2925'; + ctx.fillStyle = '#A98C5A'; + ctx.fillText(label, nx, yy); + ctx.fillStyle = '#3D3427'; ctx.font = '600 26px sans-serif'; - ctx.fillText(v1 || '—', infoX + 66, y); - if (l2) { - const x2 = infoX + 300; - ctx.fillStyle = '#9A8F82'; - ctx.font = '24px sans-serif'; - ctx.fillText(l2, x2, y); - ctx.fillStyle = '#2D2925'; - ctx.font = '600 26px sans-serif'; - ctx.fillText(v2 || '—', x2 + 66, y); - } - y += 44; + ctx.fillText(value || '—', nx + 78, yy); }; - pair('品种', c.breed, '毛色', c.color); - pair('出生', c.birthday, '星座', c.zodiac); - pair('体重', c.weight, '', ''); + kvF('性别', c.gender, avY + 92); + kvF('品种', c.breed, avY + 130); + kvF('毛色', c.color, avY + 168); + kvF('年龄', c.age, avY + 206); - // 性格标签 chips + // 性格标签 + let ty = avY + avD + 34; + ctx.textAlign = 'left'; + ctx.fillStyle = '#8A5A18'; + ctx.font = '600 24px sans-serif'; + ctx.fillText('🐾 性格标签', fx + 34, ty); const tags = (c.personality || '').split(',').filter(Boolean); - if (tags.length) { - let tx = infoX; - const ty = y + 8; - tags.forEach((t) => { - ctx.font = '22px sans-serif'; - const tw = ctx.measureText(t).width + 28; - ctx.fillStyle = 'rgba(245,169,71,.2)'; - this.roundRect(ctx, tx, ty - 26, tw, 36, 18); - ctx.fill(); - ctx.fillStyle = '#A96500'; - ctx.textAlign = 'left'; - ctx.fillText(t, tx + 14, ty); - tx += tw + 12; - }); + const tagColors = [['#FFE7A8', '#8A5A18'], ['#D6E9FF', '#2E6DA8'], ['#FFD9DE', '#C24A63']]; + let tx = fx + 34; + ty += 42; + tags.forEach((t, i) => { + ctx.font = '23px sans-serif'; + const tw = ctx.measureText(t).width + 32; + const col = tagColors[i % tagColors.length]; + ctx.fillStyle = col[0]; + this.roundRect(ctx, tx, ty - 28, tw, 40, 20); + ctx.fill(); + ctx.fillStyle = col[1]; + ctx.textAlign = 'left'; + ctx.fillText(t, tx + 16, ty); + tx += tw + 14; + }); + + // 介绍 quote + if (c.intro) { + const qy = ty + 30; + ctx.fillStyle = 'rgba(255,255,255,.6)'; + this.roundRect(ctx, fx + 34, qy, cardW2 - 68, 60, 16); + ctx.fill(); + ctx.fillStyle = '#7A5A2E'; + ctx.font = '25px sans-serif'; + ctx.textAlign = 'left'; + ctx.fillText('“ ' + c.intro + ' ”', fx + 54, qy + 40); } - // ═══ 照护信息 ═══ - const s2Y = s1Y + s1H + 14, s2H = 290; - this.roundRect(ctx, pad, s2Y, cardW, s2H, 28); + // 底部:身份ID + 一起生活天数 + ctx.textAlign = 'left'; + ctx.fillStyle = '#9C7A3E'; + ctx.font = '22px sans-serif'; + ctx.fillText('物ID ' + (c.id_no || ''), fx + 34, fY + fH - 60); + if (c.days > 0) { + ctx.fillText('和主人一起生活的第 ' + c.days + ' 天', fx + 34, fY + fH - 26); + } + + // ══════════ 背面卡(白 + 蓝)══════════ + const bY = fY + fH + 46, bH = 620; + ctx.textAlign = 'center'; + ctx.fillStyle = '#C7A36A'; + ctx.font = '22px sans-serif'; + ctx.fillText('· 背面 ·', W / 2, bY - 16); + this.roundRect(ctx, fx, bY, cardW2, bH, 30); ctx.fillStyle = '#FFFFFF'; ctx.fill(); - ctx.textAlign = 'left'; - ctx.fillStyle = '#B4623F'; - ctx.font = '700 30px sans-serif'; - ctx.fillText('照护信息', pad + 40, s2Y + 56); - const kv = (label, value, yy) => { - ctx.textAlign = 'left'; - ctx.font = '24px sans-serif'; - ctx.fillStyle = '#9A8F82'; - ctx.fillText(label, pad + 40, yy); - ctx.font = '600 26px sans-serif'; - ctx.fillStyle = '#2D2925'; - ctx.fillText(value || '—', pad + 190, yy); - }; - kv('主人', c.owner, s2Y + 108); - kv('联系电话', c.phone || '未验证', s2Y + 152); - kv('疫苗情况', c.vaccine_state, s2Y + 196); - kv('备注', c.notes || '无', s2Y + 240); - // ═══ 证件区(蓝紫渐变)+ 小程序码 ═══ - const s3Y = s2Y + s2H + 14, s3H = H - s3Y - pad; - const g3 = ctx.createLinearGradient(pad, s3Y, W - pad, s3Y + s3H); - g3.addColorStop(0, '#E8F0FF'); - g3.addColorStop(1, '#F0E8FF'); - this.roundRect(ctx, pad, s3Y, cardW, s3H, 28); - ctx.fillStyle = g3; + // 蓝色标题条 + const hb = 64; + 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 28px sans-serif'; + ctx.fillText('🌿 照护信息', W / 2, bY + 24 + 42); + + // 照护行(emoji 图标 + 标签 + 值) + const rowX = fx + 40; + const kvB = (icon, label, value, yy) => { + ctx.textAlign = 'left'; + ctx.font = '26px sans-serif'; + ctx.fillText(icon, rowX, yy); + ctx.font = '24px sans-serif'; + ctx.fillStyle = '#8D8277'; + ctx.fillText(label, rowX + 44, yy); + ctx.font = '600 25px sans-serif'; + ctx.fillStyle = '#2D2925'; + ctx.fillText(value || '—', rowX + 170, yy); + }; + let ry = bY + 140; + kvB('👤', '主人', c.owner, ry); ry += 46; + kvB('📞', '联系电话', c.phone || '未验证', ry); ry += 46; + kvB('💉', '疫苗情况', c.vaccine_state, ry); ry += 46; + kvB('📝', '备注', c.notes || '无', ry); ry += 46; + kvB('🏛', '发证机构', c.org || '肉垫计划', ry); ry += 46; + kvB('📅', '有效期', c.validity || '永久有效', ry); ry += 40; + + // 走失联系框 + 小程序码 + 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 = '#7D89A8'; - ctx.font = '22px sans-serif'; - ctx.fillText('宠 星 人 地 球 移 民', pad + 40, s3Y + 50); - ctx.fillStyle = '#4A5878'; - ctx.font = '700 48px sans-serif'; - ctx.fillText('居民身份卡', pad + 40, s3Y + 112); - ctx.fillStyle = '#8A93A8'; - ctx.font = '22px sans-serif'; - ctx.fillText('身份 ID ' + (c.id_no || ''), pad + 40, s3Y + 156); - ctx.fillText('签发机构 ' + (c.org || '肉垫计划'), pad + 40, s3Y + 190); - ctx.fillText('有效期限 ' + (c.validity || '永久有效'), pad + 40, s3Y + 224); - + ctx.fillStyle = '#2E6DA8'; + ctx.font = '600 25px sans-serif'; + ctx.fillText('🔔 走失时请联系主人', fx + 44, lostY + 44); + ctx.fillStyle = '#5B7A9C'; + ctx.font = '21px sans-serif'; + ctx.fillText('微信扫一扫,查看' + (c.name || '') + '的身份信息', fx + 44, lostY + 84); + ctx.fillText('或联系主人', fx + 44, lostY + 114); // 小程序码 - const qrD = 118, qrX = W - pad - 36 - qrD, qrY = s3Y + s3H - 36 - qrD; + const qrD = 118, qrX = fx + cardW2 - 24 - 20 - qrD, qrY = lostY + (lostH - qrD) / 2; ctx.fillStyle = '#fff'; this.roundRect(ctx, qrX, qrY, qrD, qrD, 12); ctx.fill(); if (qr) { ctx.drawImage(qr, qrX + 8, qrY + 8, qrD - 16, qrD - 16); } else { - ctx.fillStyle = '#B5A99D'; - ctx.font = '20px sans-serif'; + ctx.fillStyle = '#9CB4CC'; + ctx.font = '18px sans-serif'; ctx.textAlign = 'center'; - ctx.fillText('小程序码', qrX + qrD / 2, qrY + qrD / 2 - 4); - ctx.fillText('待发布', qrX + qrD / 2, qrY + qrD / 2 + 26); + ctx.fillText('小程序码', qrX + qrD / 2, qrY + qrD / 2 - 2); + ctx.fillText('待发布', qrX + qrD / 2, qrY + qrD / 2 + 24); } wx.canvasToTempFilePath( @@ -269,17 +325,20 @@ Page({ // 拿回后重新取卡(此时电话就有了)并重画 onGetPhone(e) { const d = e.detail || {}; - if (!d.code) return; // 用户拒绝授权 + if (!d.code) { + // 用户拒绝授权:不给生成,明确提示 + this.setData({ phoneRefused: true }); + return wx.showToast({ title: '没有手机号,无法生成身份卡', icon: 'none' }); + } wx.showLoading({ title: '验证中…', mask: true }); api .bindPhone(d.code) .then(() => api.petIDCard(this.data.petId)) .then((card) => { wx.hideLoading(); - if (card) { - this.setData({ card, qrReady: !!card.qr_ready }); - this.draw(); - } + if (!card) return; + this.setData({ card, qrReady: !!card.qr_ready, needPhone: false, phoneRefused: false }); + wx.nextTick(() => this.draw()); wx.showToast({ title: '手机号已验证', icon: 'success' }); }) .catch((err) => { diff --git a/pets-fe/pages/idcard/idcard.wxml b/pets-fe/pages/idcard/idcard.wxml index 7cab28d..384f591 100644 --- a/pets-fe/pages/idcard/idcard.wxml +++ b/pets-fe/pages/idcard/idcard.wxml @@ -2,32 +2,35 @@ + + + + 生成身份卡需要手机号 + 身份卡背面的「联系电话」用于走失时联系你,验证后才能生成 + + 没有手机号,无法生成身份卡 + + - - - 正在生成身份卡… - - - - - 背面「联系电话」还没验证,走失时联系不到你 - - - - - 小程序码要在小程序发布后才会出现,现在先放一个占位。 - - - - - 下载到相册 + + + + 正在生成身份卡… - - + + + 小程序码要在小程序发布后才会出现,现在先放一个占位。 + + + + + 下载到相册 + + + + diff --git a/pets-fe/pages/idcard/idcard.wxss b/pets-fe/pages/idcard/idcard.wxss index d93322c..93976ed 100644 --- a/pets-fe/pages/idcard/idcard.wxss +++ b/pets-fe/pages/idcard/idcard.wxss @@ -18,8 +18,19 @@ button.ic-btn{padding:0;line-height:normal} button.ic-btn::after{border:none} /* 离屏 canvas:挪出可视区,只用来生成图 */ -.ic-canvas{position:fixed;left:-9999rpx;top:0;width:620px;height:760px} +.ic-canvas{position:fixed;left:-9999rpx;top:0;width:620px;height:1480px} -/* 手机号验证提示 */ -.ic-phone-tip{display:flex;flex-direction:column;align-items:center;gap:var(--sp-3);color:var(--red-ink)} -.ic-phone-btn{width:auto;padding:0 var(--sp-6)} +/* 手机号门槛卡 */ +.ic-gate{ + background:#fff;border-radius:var(--r-lg);box-shadow:var(--sd-1); + padding:var(--sp-6) var(--sp-5);margin-top:var(--sp-5);text-align:center; +} +.ic-gate-ic{ + width:140rpx;height:140rpx;margin:0 auto var(--sp-4);border-radius:50%; + background:var(--primary-soft);color:var(--primary); + display:flex;align-items:center;justify-content:center; +} +.ic-gate-t{font-size:var(--fs-lg);font-weight:var(--fw-b);color:var(--text)} +.ic-gate-p{font-size:var(--fs-sm);color:var(--muted);line-height:1.6;margin:var(--sp-2) 0 var(--sp-5)} +.ic-gate-refuse{color:var(--red-ink);font-weight:var(--fw-b);font-size:var(--fs-md)} +.ic-gate-btn{margin-top:var(--sp-2)} -- 2.52.0 From 747e7c9d3a5955759e0a6a737fb97ed3c1cd6e63 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 17:52:31 +0800 Subject: [PATCH 26/33] =?UTF-8?q?feat(fe):=20=E8=BA=AB=E4=BB=BD=E5=8D=A1?= =?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7=E9=97=A8=E6=A7=9B=E5=8A=A0=E3=80=8C?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E5=A1=AB=E5=86=99=E3=80=8D=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getPhoneNumber 需企业认证 + 手机号验证权限,个人/未认证小程序唤不起。 门槛卡加「手动填写手机号」入口:填 11 位号 → updateProfile 存库 → 重取卡生成。 Co-Authored-By: Claude Opus 5 --- pets-fe/pages/idcard/idcard.js | 28 ++++++++++++++++++++++++++++ pets-fe/pages/idcard/idcard.wxml | 13 +++++++++++-- pets-fe/pages/idcard/idcard.wxss | 2 ++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index 6eff9ac..43444bf 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -27,6 +27,8 @@ Page({ saving: false, needPhone: false, // 没手机号:门槛,先验证才生成 phoneRefused: false, // 用户拒绝了授权 + manualMode: false, // 手动填手机号(getPhoneNumber 唤不起时的兜底) + manualPhone: '', }, onLoad(q) { store @@ -347,6 +349,32 @@ Page({ }); }, + // 手动填手机号兜底(getPhoneNumber 需企业认证,唤不起时用这个) + showManual() { + this.setData({ manualMode: true }); + }, + onManualPhone(e) { + this.setData({ manualPhone: e.detail.value }); + }, + submitManual() { + const p = (this.data.manualPhone || '').trim(); + if (!/^1\d{10}$/.test(p)) return wx.showToast({ title: '手机号格式不对', icon: 'none' }); + wx.showLoading({ title: '保存中…', mask: true }); + api + .updateProfile({ phone: p }) + .then(() => api.petIDCard(this.data.petId)) + .then((card) => { + wx.hideLoading(); + if (!card) return; + this.setData({ card, qrReady: !!card.qr_ready, needPhone: false, manualMode: false }); + wx.nextTick(() => this.draw()); + }) + .catch((err) => { + wx.hideLoading(); + toastErr(err, '保存失败'); + }); + }, + onSave() { if (!this.data.cardImg) return wx.showToast({ title: '还在生成,稍等一下', icon: 'none' }); if (this.data.saving) return; diff --git a/pets-fe/pages/idcard/idcard.wxml b/pets-fe/pages/idcard/idcard.wxml index 384f591..7e545cb 100644 --- a/pets-fe/pages/idcard/idcard.wxml +++ b/pets-fe/pages/idcard/idcard.wxml @@ -7,8 +7,17 @@ 生成身份卡需要手机号 身份卡背面的「联系电话」用于走失时联系你,验证后才能生成 - - 没有手机号,无法生成身份卡 + + + + 没有手机号,无法生成身份卡 + 唤不起来?手动填写手机号 + + + + + + diff --git a/pets-fe/pages/idcard/idcard.wxss b/pets-fe/pages/idcard/idcard.wxss index 93976ed..6cd7fe7 100644 --- a/pets-fe/pages/idcard/idcard.wxss +++ b/pets-fe/pages/idcard/idcard.wxss @@ -34,3 +34,5 @@ button.ic-btn::after{border:none} .ic-gate-p{font-size:var(--fs-sm);color:var(--muted);line-height:1.6;margin:var(--sp-2) 0 var(--sp-5)} .ic-gate-refuse{color:var(--red-ink);font-weight:var(--fw-b);font-size:var(--fs-md)} .ic-gate-btn{margin-top:var(--sp-2)} +.ic-gate-manual{margin-top:var(--sp-4);color:var(--muted);font-size:var(--fs-sm);text-decoration:underline} +.ic-gate-input{margin-bottom:var(--sp-3);text-align:center} -- 2.52.0 From d9a80d7f8a15ff20df376f4fce8588bae41057de Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 18:15:45 +0800 Subject: [PATCH 27/33] =?UTF-8?q?style(fe):=20=E8=BA=AB=E4=BB=BD=E5=8D=A1?= =?UTF-8?q?=E6=8C=89=E5=8F=82=E8=80=83=E5=9B=BE=E6=94=B6=E6=8B=BE=20?= =?UTF-8?q?=E2=80=94=20=E5=8E=BB=E6=A0=87=E9=A2=98/=E6=AD=A3=E5=8F=8D?= =?UTF-8?q?=E9=9D=A2=E6=A0=87=E7=AD=BE/=E7=96=AB=E8=8B=97=EF=BC=8C?= =?UTF-8?q?=E5=8D=A1=E9=AB=98=E6=8C=89=E5=86=85=E5=AE=B9=E8=87=AA=E9=80=82?= =?UTF-8?q?=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 去掉顶部「宠物身份卡」大标题和「· 正面 ·/· 背面 ·」灰字,只留两张卡 - 正面卡高度按性格/介绍有无动态算,消掉中间大块空白 - 性格标签为空时整段不画 - 照护卡去掉「疫苗情况」行 - 导出只裁到实际内容底部,卡片下面不再拖空白 Co-Authored-By: Claude Opus 5 --- pets-fe/pages/idcard/idcard.js | 100 +++++++++++++++++---------------- 1 file changed, 51 insertions(+), 49 deletions(-) diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index 43444bf..08a7a25 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -69,6 +69,7 @@ Page({ } catch (e) { dpr = 2; } + this._dpr = dpr; node.width = W * dpr; node.height = H * dpr; const ctx = node.getContext('2d'); @@ -93,18 +94,14 @@ Page({ const cardW = W - pad * 2; const fx = pad, cardW2 = cardW; - - // 顶部标题 - ctx.textAlign = 'center'; - ctx.fillStyle = '#8A5A18'; - ctx.font = '700 40px sans-serif'; - ctx.fillText('🐾 宠物身份卡 🐾', W / 2, 66); + const tags = (c.personality || '').split(',').filter(Boolean); // ══════════ 正面卡(暖黄)══════════ - ctx.fillStyle = '#C7A36A'; - ctx.font = '22px sans-serif'; - ctx.fillText('· 正面 ·', W / 2, 116); - const fY = 140, fH = 700; + // 高度按内容算,避免留一大块空白 + let fH = 420; + if (tags.length) fH += 90; + if (c.intro) fH += 90; + const fY = pad; const gf = ctx.createLinearGradient(fx, fY, fx, fY + fH); gf.addColorStop(0, '#FFF7E0'); gf.addColorStop(1, '#FFE7AE'); @@ -186,56 +183,53 @@ Page({ kvF('毛色', c.color, avY + 168); kvF('年龄', c.age, avY + 206); - // 性格标签 - let ty = avY + avD + 34; - ctx.textAlign = 'left'; - ctx.fillStyle = '#8A5A18'; - ctx.font = '600 24px sans-serif'; - ctx.fillText('🐾 性格标签', fx + 34, ty); - const tags = (c.personality || '').split(',').filter(Boolean); - const tagColors = [['#FFE7A8', '#8A5A18'], ['#D6E9FF', '#2E6DA8'], ['#FFD9DE', '#C24A63']]; - let tx = fx + 34; - ty += 42; - tags.forEach((t, i) => { - ctx.font = '23px sans-serif'; - const tw = ctx.measureText(t).width + 32; - const col = tagColors[i % tagColors.length]; - ctx.fillStyle = col[0]; - this.roundRect(ctx, tx, ty - 28, tw, 40, 20); - ctx.fill(); - ctx.fillStyle = col[1]; + // 性格标签(没有就不画,避免留空行) + let ty = fY + 320; + if (tags.length) { ctx.textAlign = 'left'; - ctx.fillText(t, tx + 16, ty); - tx += tw + 14; - }); + ctx.fillStyle = '#8A5A18'; + ctx.font = '600 24px sans-serif'; + ctx.fillText('🐾 性格标签', fx + 34, ty); + ty += 44; + const tagColors = [['#FFE7A8', '#8A5A18'], ['#D6E9FF', '#2E6DA8'], ['#FFD9DE', '#C24A63']]; + let tx = fx + 34; + tags.forEach((t, i) => { + ctx.font = '23px sans-serif'; + const tw = ctx.measureText(t).width + 32; + const col = tagColors[i % tagColors.length]; + ctx.fillStyle = col[0]; + this.roundRect(ctx, tx, ty - 28, tw, 40, 20); + ctx.fill(); + ctx.fillStyle = col[1]; + ctx.textAlign = 'left'; + ctx.fillText(t, tx + 16, ty); + tx += tw + 14; + }); + ty += 46; + } // 介绍 quote if (c.intro) { - const qy = ty + 30; ctx.fillStyle = 'rgba(255,255,255,.6)'; - this.roundRect(ctx, fx + 34, qy, cardW2 - 68, 60, 16); + this.roundRect(ctx, fx + 34, ty, cardW2 - 68, 60, 16); ctx.fill(); ctx.fillStyle = '#7A5A2E'; ctx.font = '25px sans-serif'; ctx.textAlign = 'left'; - ctx.fillText('“ ' + c.intro + ' ”', fx + 54, qy + 40); + ctx.fillText('“ ' + c.intro + ' ”', fx + 54, ty + 40); } - // 底部:身份ID + 一起生活天数 + // 底部:身份ID + 一起生活天数(贴卡片底边) ctx.textAlign = 'left'; ctx.fillStyle = '#9C7A3E'; ctx.font = '22px sans-serif'; - ctx.fillText('物ID ' + (c.id_no || ''), fx + 34, fY + fH - 60); + ctx.fillText('物ID ' + (c.id_no || ''), fx + 34, fY + fH - 56); if (c.days > 0) { - ctx.fillText('和主人一起生活的第 ' + c.days + ' 天', fx + 34, fY + fH - 26); + ctx.fillText('和主人一起生活的第 ' + c.days + ' 天', fx + 34, fY + fH - 22); } - // ══════════ 背面卡(白 + 蓝)══════════ - const bY = fY + fH + 46, bH = 620; - ctx.textAlign = 'center'; - ctx.fillStyle = '#C7A36A'; - ctx.font = '22px sans-serif'; - ctx.fillText('· 背面 ·', W / 2, bY - 16); + // ══════════ 照护卡(白 + 蓝)══════════ + const bY = fY + fH + 40, bH = 566; this.roundRect(ctx, fx, bY, cardW2, bH, 30); ctx.fillStyle = '#FFFFFF'; ctx.fill(); @@ -267,12 +261,11 @@ Page({ ctx.fillText(value || '—', rowX + 170, yy); }; let ry = bY + 140; - kvB('👤', '主人', c.owner, ry); ry += 46; - kvB('📞', '联系电话', c.phone || '未验证', ry); ry += 46; - kvB('💉', '疫苗情况', c.vaccine_state, ry); ry += 46; - kvB('📝', '备注', c.notes || '无', ry); ry += 46; - kvB('🏛', '发证机构', c.org || '肉垫计划', ry); ry += 46; - kvB('📅', '有效期', c.validity || '永久有效', ry); ry += 40; + 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; // 走失联系框 + 小程序码 const lostY = ry + 16, lostH = bY + bH - lostY - 30; @@ -302,9 +295,18 @@ Page({ ctx.fillText('待发布', qrX + qrD / 2, qrY + qrD / 2 + 24); } + // 只导出到实际内容底部,避免卡片下面拖一条空白 + const dpr = this._dpr || 2; + const contentH = bY + bH + pad; wx.canvasToTempFilePath( { canvas: node, + x: 0, + y: 0, + width: W, + height: contentH, + destWidth: W * dpr, + destHeight: contentH * dpr, fileType: 'png', success: (r) => this.setData({ cardImg: r.tempFilePath }), fail: () => wx.showToast({ title: '生成失败,稍后再试', icon: 'none' }), -- 2.52.0 From 4e3859227e3beb2359808a18ee05850fa079c2cc Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 18:23:42 +0800 Subject: [PATCH 28/33] =?UTF-8?q?style(fe):=20=E8=BA=AB=E4=BB=BD=E5=8D=A1?= =?UTF-8?q?=E5=86=8D=E7=B2=BE=E7=AE=80=20=E2=80=94=20=E5=8E=BB=E6=8E=89=20?= =?UTF-8?q?logo/=E5=BE=BD=E7=AB=A0/=E5=9B=BE=E6=A0=87=20emoji=20=E5=92=8C?= =?UTF-8?q?=E5=A4=9A=E4=BD=99=E6=8B=9B=E5=91=BC=E8=AF=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 正面去掉「宠星人」logo、「宠物身份卡」盾牌徽章、爪印水印,头像上移 - 照护卡标题去掉 🌿,各行去掉 👤📞📝🏛📅 图标只留文字 - 走失框去掉「🔔 走失时请联系主人」标题和「或联系主人」,只留扫码说明 - 去掉页面底部「小程序码发布后出现」占位提示 Co-Authored-By: Claude Opus 5 --- pets-fe/pages/idcard/idcard.js | 60 +++++++++----------------------- pets-fe/pages/idcard/idcard.wxml | 4 --- 2 files changed, 16 insertions(+), 48 deletions(-) diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index 08a7a25..4bb2392 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -98,7 +98,7 @@ Page({ // ══════════ 正面卡(暖黄)══════════ // 高度按内容算,避免留一大块空白 - let fH = 420; + let fH = 372; if (tags.length) fH += 90; if (c.intro) fH += 90; const fY = pad; @@ -108,31 +108,9 @@ Page({ this.roundRect(ctx, fx, fY, cardW2, fH, 30); ctx.fillStyle = gf; ctx.fill(); - // 爪印水印 - ctx.save(); - this.roundRect(ctx, fx, fY, cardW2, fH, 30); - ctx.clip(); - ctx.globalAlpha = 0.07; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.font = '150px sans-serif'; - ctx.fillText('🐾', fx + cardW2 - 90, fY + fH - 80); - ctx.restore(); - ctx.globalAlpha = 1; - ctx.textBaseline = 'alphabetic'; - - // 卡头:左 logo,右 徽章 - ctx.textAlign = 'left'; - ctx.fillStyle = '#8A5A18'; - ctx.font = '600 26px sans-serif'; - ctx.fillText('🐱 宠星人', fx + 32, fY + 52); - ctx.textAlign = 'right'; - ctx.fillStyle = '#B07A1E'; - ctx.font = '600 22px sans-serif'; - ctx.fillText('🛡 宠物身份卡', fx + cardW2 - 32, fY + 50); // 头像圆 + 白边 - const avD = 150, avX = fx + 36, avY = fY + 90; + const avD = 150, avX = fx + 36, avY = fY + 52; ctx.save(); ctx.beginPath(); ctx.arc(avX + avD / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2); @@ -184,7 +162,7 @@ Page({ kvF('年龄', c.age, avY + 206); // 性格标签(没有就不画,避免留空行) - let ty = fY + 320; + let ty = fY + 298; if (tags.length) { ctx.textAlign = 'left'; ctx.fillStyle = '#8A5A18'; @@ -245,27 +223,25 @@ Page({ ctx.textAlign = 'center'; ctx.fillStyle = '#fff'; ctx.font = '700 28px sans-serif'; - ctx.fillText('🌿 照护信息', W / 2, bY + 24 + 42); + ctx.fillText('照护信息', W / 2, bY + 24 + 42); - // 照护行(emoji 图标 + 标签 + 值) - const rowX = fx + 40; - const kvB = (icon, label, value, yy) => { + // 照护行(标签 + 值,不放图标) + const rowX = fx + 44; + const kvB = (label, value, yy) => { ctx.textAlign = 'left'; - ctx.font = '26px sans-serif'; - ctx.fillText(icon, rowX, yy); ctx.font = '24px sans-serif'; ctx.fillStyle = '#8D8277'; - ctx.fillText(label, rowX + 44, yy); + ctx.fillText(label, rowX, yy); ctx.font = '600 25px sans-serif'; ctx.fillStyle = '#2D2925'; - ctx.fillText(value || '—', rowX + 170, yy); + ctx.fillText(value || '—', rowX + 150, 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; + 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; // 走失联系框 + 小程序码 const lostY = ry + 16, lostH = bY + bH - lostY - 30; @@ -273,13 +249,9 @@ Page({ ctx.fillStyle = '#EAF3FE'; ctx.fill(); ctx.textAlign = 'left'; - ctx.fillStyle = '#2E6DA8'; - ctx.font = '600 25px sans-serif'; - ctx.fillText('🔔 走失时请联系主人', fx + 44, lostY + 44); ctx.fillStyle = '#5B7A9C'; - ctx.font = '21px sans-serif'; - ctx.fillText('微信扫一扫,查看' + (c.name || '') + '的身份信息', fx + 44, lostY + 84); - ctx.fillText('或联系主人', fx + 44, lostY + 114); + 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; ctx.fillStyle = '#fff'; diff --git a/pets-fe/pages/idcard/idcard.wxml b/pets-fe/pages/idcard/idcard.wxml index 7e545cb..972a709 100644 --- a/pets-fe/pages/idcard/idcard.wxml +++ b/pets-fe/pages/idcard/idcard.wxml @@ -27,10 +27,6 @@ 正在生成身份卡… - - 小程序码要在小程序发布后才会出现,现在先放一个占位。 - - 下载到相册 -- 2.52.0 From 59412010dc11c55c04e547267f3e9d7ac9a1f84e Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 18:32:09 +0800 Subject: [PATCH 29/33] =?UTF-8?q?feat(fe+be):=20=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E5=8D=A1=E5=8A=A0=E5=85=AC=E7=AB=A0=E3=80=81=E4=BF=AE=E6=80=A7?= =?UTF-8?q?=E5=88=AB=E7=AC=A6=E5=8F=B7=E3=80=81=E7=94=B5=E8=AF=9D=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=85=A8=E3=80=81=E7=85=A7=E6=8A=A4=E5=8D=A1=E6=A0=87?= =?UTF-8?q?=E9=A2=98=E4=B8=A4=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修正性别 ♂/♀:用名字字号量宽度再定位,不再怼到名字上(之前用小字号量偏了) - 正反两张卡右上角各盖一枚「肉垫计划 · 官方认证」红色公章(双圈+五角星,微旋转半透明) - 照护卡标题改成两行「X星地球移民 / 居民身份卡」(按猫狗取 喵/汪/宠) - 正面「物ID」改为「ID」 - 后端 IDCard.Phone 返回完整手机号,不再脱敏 Co-Authored-By: Claude Opus 5 --- pets-be/internal/service/idcard.go | 2 +- pets-fe/pages/idcard/idcard.js | 76 ++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/pets-be/internal/service/idcard.go b/pets-be/internal/service/idcard.go index b026f01..493057a 100644 --- a/pets-be/internal/service/idcard.go +++ b/pets-be/internal/service/idcard.go @@ -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, Color: pet.Color, Personality: pet.Personality, 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: "肉垫计划", } diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index 4bb2392..a39876c 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -135,16 +135,20 @@ Page({ ctx.lineWidth = 8; ctx.stroke(); - // 名字 + 性别(头像右侧) + // 名字 + 性别(头像右侧)。性别符号必须用「名字的字号」量宽度再定位, + // 否则换成小字号后 measureText 量出的宽度偏小,符号会怼到名字上 const nx = avX + avD + 28; + const nm = c.name || '毛孩子'; ctx.textAlign = 'left'; ctx.fillStyle = '#2D2925'; 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) { - ctx.fillStyle = c.gender === '女孩' ? '#E86A9B' : '#5B9BE8'; - ctx.font = '30px sans-serif'; - ctx.fillText(c.gender === '女孩' ? '♀' : '♂', nx + ctx.measureText(c.name || '毛孩子').width + 12, avY + 44); + const isF = c.gender === '女孩'; + ctx.fillStyle = isF ? '#E86A9B' : '#5B9BE8'; + ctx.font = '600 34px sans-serif'; + ctx.fillText(isF ? '♀' : '♂', nx + nmW + 16, avY + 44); } // 头像右的资料行 const kvF = (label, value, yy) => { @@ -201,19 +205,23 @@ Page({ ctx.textAlign = 'left'; ctx.fillStyle = '#9C7A3E'; 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) { ctx.fillText('和主人一起生活的第 ' + c.days + ' 天', fx + 34, fY + fH - 22); } + // 正面印章(右上角) + this.drawStamp(ctx, fx + cardW2 - 78, fY + 82); + // ══════════ 照护卡(白 + 蓝)══════════ const bY = fY + fH + 40, bH = 566; this.roundRect(ctx, fx, bY, cardW2, bH, 30); ctx.fillStyle = '#FFFFFF'; ctx.fill(); - // 蓝色标题条 - const hb = 64; + // 蓝色标题条(两行:「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'); @@ -222,8 +230,10 @@ Page({ ctx.fill(); ctx.textAlign = 'center'; ctx.fillStyle = '#fff'; - ctx.font = '700 28px sans-serif'; - ctx.fillText('照护信息', W / 2, bY + 24 + 42); + 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); // 照护行(标签 + 值,不放图标) const rowX = fx + 44; @@ -267,6 +277,9 @@ Page({ ctx.fillText('待发布', qrX + qrD / 2, qrY + qrD / 2 + 24); } + // 背面印章(右上角,压在蓝条右侧) + this.drawStamp(ctx, fx + cardW2 - 78, bY + 70); + // 只导出到实际内容底部,避免卡片下面拖一条空白 const dpr = this._dpr || 2; const contentH = bY + bH + pad; @@ -297,6 +310,49 @@ Page({ 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,后端换成真实手机号存库, // 拿回后重新取卡(此时电话就有了)并重画 onGetPhone(e) { -- 2.52.0 From 473c9f9200472e05b9408cd423307e58ddbd5ccb Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 18:35:11 +0800 Subject: [PATCH 30/33] =?UTF-8?q?style(fe):=20=E7=85=A7=E6=8A=A4=E5=8D=A1?= =?UTF-8?q?=E6=94=B9=E8=AF=81=E4=BB=B6=E7=89=88=E5=BC=8F=20=E2=80=94=20?= =?UTF-8?q?=E5=A4=A7=E5=AD=97=E3=80=8CX=E6=98=9F=E4=BA=BA=E5=9C=B0?= =?UTF-8?q?=E7=90=83=E7=A7=BB=E6=B0=91/=E5=B1=85=E6=B0=91=E8=BA=AB?= =?UTF-8?q?=E4=BB=BD=E5=8D=A1=E3=80=8D=E5=B7=A6=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 去掉蓝色标题条,照护卡整体换淡紫底 - 标题两行左对齐:小字带字距「汪星人/喵星人/宠星人地球移民」+ 大字「居民身份卡」 - 按物种动态取字:狗→汪星人、猫→喵星人、其它→宠星人 - 发证机构→签发机构、有效期→有效期限;小程序码落到右下角淡紫底上 - 新增 spacedText 逐字字距工具 Co-Authored-By: Claude Opus 5 --- pets-fe/pages/idcard/idcard.js | 94 ++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index a39876c..6549d67 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -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(); -- 2.52.0 From c07ed0353938eaf3dace8882d06ff9d8d414f5f0 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 18:37:17 +0800 Subject: [PATCH 31/33] =?UTF-8?q?style(fe):=20=E8=BA=AB=E4=BB=BD=E5=8D=A1?= =?UTF-8?q?=E6=AD=A3=E5=8F=8D=E4=B8=A4=E5=BC=A0=E7=AD=89=E9=AB=98=EF=BC=88?= =?UTF-8?q?=E9=83=BD=E7=94=A8=20cardH=3D630=EF=BC=89=EF=BC=8C=E6=AD=A3?= =?UTF-8?q?=E9=9D=A2=E4=B8=AD=E9=97=B4=E5=86=85=E5=AE=B9=E5=9E=82=E7=9B=B4?= =?UTF-8?q?=E5=B1=85=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- pets-fe/pages/idcard/idcard.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index 6549d67..b9f1a77 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -97,10 +97,9 @@ Page({ const tags = (c.personality || '').split(',').filter(Boolean); // ══════════ 正面卡(暖黄)══════════ - // 高度按内容算,避免留一大块空白 - let fH = 372; - if (tags.length) fH += 90; - if (c.intro) fH += 90; + // 正反两张卡等高(都用 cardH),中间内容垂直居中,避免正面留空 + const cardH = 630; + const fH = cardH; const fY = pad; const gf = ctx.createLinearGradient(fx, fY, fx, fY + fH); gf.addColorStop(0, '#FFF7E0'); @@ -165,8 +164,10 @@ Page({ kvF('毛色', c.color, avY + 168); kvF('年龄', c.age, avY + 206); - // 性格标签(没有就不画,避免留空行) - let ty = fY + 298; + // 性格标签(没有就不画,避免留空行);性格+介绍整体在 info 和底栏之间垂直居中 + const midTop = fY + 300, midBot = fY + fH - 92; + const blockH = (tags.length ? 90 : 0) + (c.intro ? 70 : 0); + let ty = midTop + Math.max(0, Math.floor((midBot - midTop - blockH) / 2)); if (tags.length) { ctx.textAlign = 'left'; ctx.fillStyle = '#8A5A18'; @@ -214,7 +215,7 @@ Page({ this.drawStamp(ctx, fx + cardW2 - 78, fY + 82); // ══════════ 照护卡(淡紫,仿证件版式)══════════ - const bY = fY + fH + 40, bH = 630; + const bY = fY + fH + 40, bH = cardH; const gbc = ctx.createLinearGradient(fx, bY, fx, bY + bH); gbc.addColorStop(0, '#E6E7FB'); gbc.addColorStop(1, '#F2F0FA'); -- 2.52.0 From 1544151e5c77b53db065a38e9112797261379d24 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 18:39:09 +0800 Subject: [PATCH 32/33] =?UTF-8?q?style(fe):=20=E8=BA=AB=E4=BB=BD=E5=8D=A1?= =?UTF-8?q?=E9=99=8D=E5=88=B0=20cardH=3D540=EF=BC=8C=E5=8F=8D=E9=9D=A2?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E5=A4=87=E6=B3=A8=E8=A1=8C=E5=B9=B6=E5=8E=8B?= =?UTF-8?q?=E7=B4=A7=E7=89=88=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- pets-fe/pages/idcard/idcard.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index b9f1a77..6f43e6b 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -98,7 +98,7 @@ Page({ // ══════════ 正面卡(暖黄)══════════ // 正反两张卡等高(都用 cardH),中间内容垂直居中,避免正面留空 - const cardH = 630; + const cardH = 540; const fH = cardH; const fY = pad; const gf = ctx.createLinearGradient(fx, fY, fx, fY + fH); @@ -229,10 +229,10 @@ Page({ ctx.textAlign = 'left'; ctx.fillStyle = '#8A90AE'; ctx.font = '600 27px sans-serif'; - this.spacedText(ctx, star + '星人地球移民', fx + 44, bY + 82, 8); + this.spacedText(ctx, star + '星人地球移民', fx + 44, bY + 70, 8); ctx.fillStyle = '#3E4A63'; - ctx.font = '800 66px sans-serif'; - ctx.fillText('居民身份卡', fx + 44, bY + 168); + ctx.font = '800 60px sans-serif'; + ctx.fillText('居民身份卡', fx + 44, bY + 150); // 资料行(标签 + 值) const rowX = fx + 44; @@ -245,15 +245,14 @@ Page({ ctx.fillStyle = '#3E4A63'; ctx.fillText(value || '—', rowX + 160, yy); }; - 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; + let ry = bY + 210; + kvB('主人', c.owner, ry); ry += 48; + kvB('联系电话', c.phone || '未验证', ry); ry += 48; + kvB('签发机构', c.org || '肉垫计划', ry); ry += 48; kvB('有效期限', c.validity || '永久有效', ry); // 小程序码(右下角,直接落在淡紫底上) - const qrD = 128, qrX = fx + cardW2 - 44 - qrD, qrY = bY + bH - 40 - qrD; + const qrD = 120, qrX = fx + cardW2 - 44 - qrD, qrY = bY + bH - 34 - qrD; ctx.fillStyle = '#fff'; this.roundRect(ctx, qrX, qrY, qrD, qrD, 14); ctx.fill(); -- 2.52.0 From 51ead8f411d92da17e68814771cbabf15d3ccd95 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 31 Jul 2026 18:41:38 +0800 Subject: [PATCH 33/33] =?UTF-8?q?style(fe):=20=E8=BA=AB=E4=BB=BD=E5=8D=A1?= =?UTF-8?q?=E5=8F=AA=E7=95=99=E6=88=AA=E5=9B=BE=E5=AD=97=E6=AE=B5=20?= =?UTF-8?q?=E2=80=94=20=E5=8E=BB=E6=8E=89=E6=80=A7=E6=A0=BC=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=EF=BC=8C=E9=AB=98=E5=BA=A6=E5=8E=8B=E5=88=B0=20cardH?= =?UTF-8?q?=3D510?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 --- pets-fe/pages/idcard/idcard.js | 49 ++++++++-------------------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/pets-fe/pages/idcard/idcard.js b/pets-fe/pages/idcard/idcard.js index 6f43e6b..d3bae6c 100644 --- a/pets-fe/pages/idcard/idcard.js +++ b/pets-fe/pages/idcard/idcard.js @@ -94,11 +94,10 @@ Page({ const cardW = W - pad * 2; const fx = pad, cardW2 = cardW; - const tags = (c.personality || '').split(',').filter(Boolean); // ══════════ 正面卡(暖黄)══════════ // 正反两张卡等高(都用 cardH),中间内容垂直居中,避免正面留空 - const cardH = 540; + const cardH = 510; const fH = cardH; const fY = pad; const gf = ctx.createLinearGradient(fx, fY, fx, fY + fH); @@ -164,35 +163,9 @@ Page({ kvF('毛色', c.color, avY + 168); kvF('年龄', c.age, avY + 206); - // 性格标签(没有就不画,避免留空行);性格+介绍整体在 info 和底栏之间垂直居中 - const midTop = fY + 300, midBot = fY + fH - 92; - const blockH = (tags.length ? 90 : 0) + (c.intro ? 70 : 0); - let ty = midTop + Math.max(0, Math.floor((midBot - midTop - blockH) / 2)); - if (tags.length) { - ctx.textAlign = 'left'; - ctx.fillStyle = '#8A5A18'; - ctx.font = '600 24px sans-serif'; - ctx.fillText('🐾 性格标签', fx + 34, ty); - ty += 44; - const tagColors = [['#FFE7A8', '#8A5A18'], ['#D6E9FF', '#2E6DA8'], ['#FFD9DE', '#C24A63']]; - let tx = fx + 34; - tags.forEach((t, i) => { - ctx.font = '23px sans-serif'; - const tw = ctx.measureText(t).width + 32; - const col = tagColors[i % tagColors.length]; - ctx.fillStyle = col[0]; - this.roundRect(ctx, tx, ty - 28, tw, 40, 20); - ctx.fill(); - ctx.fillStyle = col[1]; - ctx.textAlign = 'left'; - ctx.fillText(t, tx + 16, ty); - tx += tw + 14; - }); - ty += 46; - } - - // 介绍 quote + // 介绍 quote(在 info 和底栏之间垂直居中) if (c.intro) { + const ty = fY + 300 + Math.max(0, Math.floor(((fY + fH - 92) - (fY + 300) - 60) / 2)); ctx.fillStyle = 'rgba(255,255,255,.6)'; this.roundRect(ctx, fx + 34, ty, cardW2 - 68, 60, 16); ctx.fill(); @@ -229,10 +202,10 @@ Page({ ctx.textAlign = 'left'; ctx.fillStyle = '#8A90AE'; ctx.font = '600 27px sans-serif'; - this.spacedText(ctx, star + '星人地球移民', fx + 44, bY + 70, 8); + this.spacedText(ctx, star + '星人地球移民', fx + 44, bY + 64, 8); ctx.fillStyle = '#3E4A63'; - ctx.font = '800 60px sans-serif'; - ctx.fillText('居民身份卡', fx + 44, bY + 150); + ctx.font = '800 58px sans-serif'; + ctx.fillText('居民身份卡', fx + 44, bY + 140); // 资料行(标签 + 值) const rowX = fx + 44; @@ -245,14 +218,14 @@ Page({ ctx.fillStyle = '#3E4A63'; ctx.fillText(value || '—', rowX + 160, yy); }; - let ry = bY + 210; - kvB('主人', c.owner, ry); ry += 48; - kvB('联系电话', c.phone || '未验证', ry); ry += 48; - kvB('签发机构', c.org || '肉垫计划', ry); ry += 48; + let ry = bY + 198; + kvB('主人', c.owner, ry); ry += 46; + kvB('联系电话', c.phone || '未验证', ry); ry += 46; + kvB('签发机构', c.org || '肉垫计划', ry); ry += 46; kvB('有效期限', c.validity || '永久有效', ry); // 小程序码(右下角,直接落在淡紫底上) - const qrD = 120, qrX = fx + cardW2 - 44 - qrD, qrY = bY + bH - 34 - qrD; + const qrD = 114, qrX = fx + cardW2 - 44 - qrD, qrY = bY + bH - 30 - qrD; ctx.fillStyle = '#fff'; this.roundRect(ctx, qrX, qrY, qrD, qrD, 14); ctx.fill(); -- 2.52.0