diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js index d88579d..829d59d 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.js +++ b/pets-fe/components/bottom-sheet/bottom-sheet.js @@ -15,6 +15,21 @@ function daysLater(n) { return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()); } +// 相对时间。社区里关心的是「多久以前发的」,精确到秒没意义 +function fmtAgo(iso) { + if (!iso) return ''; + const d = new Date(iso); + if (isNaN(d.getTime())) return ''; + const min = Math.floor((Date.now() - d.getTime()) / 60000); + if (min < 1) return '刚刚'; + if (min < 60) return min + ' 分钟前'; + if (min < 60 * 24) return Math.floor(min / 60) + ' 小时前'; + if (min < 60 * 24 * 7) return Math.floor(min / 1440) + ' 天前'; + const p = (n) => (n < 10 ? '0' + n : '' + n); + const sameYear = d.getFullYear() === new Date().getFullYear(); + return (sameYear ? '' : d.getFullYear() + '-') + p(d.getMonth() + 1) + '-' + p(d.getDate()); +} + function ageFromBirthday(bday) { if (!bday) return ''; const b = new Date(bday); @@ -223,6 +238,148 @@ Component({ page.getTabBar().setData({ hidden }); } }, + // ---- 成长海报:画进 canvas 才能存进相册 ---- + + onSavePoster() { + if (this.data.posterSaving) return; + this.setData({ posterSaving: true }); + this.drawPoster() + .then((tempPath) => this.saveToAlbum(tempPath)) + .then(() => wx.showToast({ title: '已保存到相册', icon: 'success' })) + .catch((e) => { + if (e && e.canceled) return; + wx.showToast({ title: (e && e.message) || '保存失败', icon: 'none' }); + }) + .then(() => this.setData({ posterSaving: false })); + }, + + // 把海报画到离屏 canvas 上,返回临时图片路径 + drawPoster() { + const W = 600; + const H = 840; + return new Promise((resolve, reject) => { + wx.createSelectorQuery() + .in(this) + .select('#posterCanvas') + .fields({ node: true, size: true }) + .exec((res) => { + const node = res && res[0] && res[0].node; + if (!node) return reject(new Error('画布还没准备好,稍后再试')); + + // 按设备像素比放大,否则在高分屏上导出的图是糊的 + let dpr = 2; + try { + dpr = (wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync()).pixelRatio || 2; + } catch (err) { + dpr = 2; + } + node.width = W * dpr; + node.height = H * dpr; + const ctx = node.getContext('2d'); + ctx.scale(dpr, dpr); + + const pet = this.data.pet || {}; + const p = this.data.poster || {}; + + const bg = ctx.createLinearGradient(0, 0, W, H); + bg.addColorStop(0, '#FFF7E8'); + bg.addColorStop(1, '#FFFFFF'); + ctx.fillStyle = bg; + ctx.fillRect(0, 0, W, H); + ctx.fillStyle = '#F2DFC3'; + ctx.fillRect(0, 0, W, 8); + + ctx.textAlign = 'center'; + ctx.fillStyle = '#2D2925'; + ctx.font = '600 40px sans-serif'; + ctx.fillText((pet.name || '毛孩子') + ' 的成长报告', W / 2, 96); + + ctx.font = '96px sans-serif'; + ctx.fillText(pet.emoji || '🐾', W / 2, 216); + + ctx.fillStyle = '#8D8277'; + ctx.font = '24px sans-serif'; + ctx.fillText([pet.age, pet.weight, pet.stage].filter(Boolean).join(' | '), W / 2, 268); + + ctx.fillStyle = 'rgba(255,255,255,.8)'; + this.roundRect(ctx, 60, 310, W - 120, 232, 24); + ctx.fill(); + const lines = [ + '完成任务 ' + (p.tasks_completed || 0) + ' 项', + '体重记录 ' + (p.weight_records || 0) + ' 次', + '疫苗记录 ' + (p.vaccine_records || 0) + ' 次', + '高风险异常 ' + (p.high_risk_count || 0) + ' 次', + ]; + ctx.textAlign = 'left'; + ctx.font = '28px sans-serif'; + lines.forEach((t, i) => { + const y = 360 + i * 52; + ctx.fillStyle = '#73BE9D'; + ctx.fillText('✓', 92, y); + ctx.fillStyle = '#443D37'; + ctx.fillText(t, 132, y); + }); + + ctx.textAlign = 'center'; + ctx.fillStyle = '#2D2925'; + ctx.font = '600 30px sans-serif'; + ctx.fillText('健康状态:' + (p.headline || '稳定成长'), W / 2, 620); + + ctx.fillStyle = '#B5A99D'; + ctx.font = '22px sans-serif'; + ctx.fillText('生成自 · 肉垫计划', W / 2, 780); + + // canvas 2d 是同步绘制,画完直接导出 + wx.canvasToTempFilePath( + { + canvas: node, + fileType: 'png', + success: (r) => resolve(r.tempFilePath), + fail: () => reject(new Error('生成图片失败')), + }, + this, + ); + }); + }); + }, + + roundRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); + }, + + // 存相册要相册写权限。用户拒过一次之后 wx.authorize 不会再弹, + // 必须引导他去设置页打开,否则这里会永远静默失败。 + saveToAlbum(filePath) { + return new Promise((resolve, reject) => { + wx.saveImageToPhotosAlbum({ + filePath, + success: resolve, + fail: (err) => { + const msg = (err && err.errMsg) || ''; + if (msg.indexOf('cancel') >= 0) return reject({ canceled: true }); + if (msg.indexOf('auth deny') >= 0 || msg.indexOf('authorize') >= 0) { + wx.showModal({ + title: '需要相册权限', + content: '保存卡片需要允许访问相册,去设置里打开一下?', + confirmText: '去设置', + success: (r) => { + if (r.confirm) wx.openSetting({}); + }, + }); + return reject({ canceled: true }); + } + reject(new Error('保存失败')); + }, + }); + }); + }, + onSeg(e) { const { group, index } = e.currentTarget.dataset; const segSel = Object.assign({}, this.data.segSel, { [group]: Number(index) }); @@ -248,7 +405,16 @@ Component({ loadComments() { if (!this.data.postId) return; api.listComments(this.data.postId) - .then((page) => this.setData({ comments: page.list || [] })) + .then((page) => + this.setData({ + comments: (page.list || []).map((c) => ({ + ...c, + timeText: fmtAgo(c.created_at), + // 没有头像字段,用昵称首字做个色块,比一律显示同一个 emoji 强 + initial: (c.author_name || '?').slice(0, 1), + })), + }), + ) .catch(() => {}); }, // 长按删除自己的评论 diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxml b/pets-fe/components/bottom-sheet/bottom-sheet.wxml index b8f9567..ec92a03 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxml +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxml @@ -319,16 +319,27 @@ module.exports.sel = function (map, key, index, def) { - 评论 - - {{item.author_name}} - {{item.content}} + 评论 {{comments.length ? comments.length : ''}} + + + {{item.initial}} + + + {{item.author_name}} + + {{item.timeText}} + + {{item.content}} + - 还没有评论,来抢沙发~ - 长按自己的评论可删除 - - - + + 还没有评论,来抢个沙发 + 长按自己的评论可删除 + + + + 发送 diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxss b/pets-fe/components/bottom-sheet/bottom-sheet.wxss index 4857492..550e8ef 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxss +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxss @@ -82,3 +82,29 @@ /* 离屏画布:挪到可视区外而不是 display:none,否则 selectorQuery 取不到节点 */ .poster-canvas{position:fixed;left:-9999rpx;top:0;width:600rpx;height:840rpx} + +/* 评论。原来拿 .record-row(左右两端对齐)当评论用,昵称在左内容在右, + 内容一长就被挤扁。改成头像 + 昵称时间 + 内容的正常结构。 */ +.cm{display:flex;gap:var(--sp-3);padding:var(--sp-3) 0;border-bottom:1rpx solid var(--line)} +.cm:last-of-type{border-bottom:0} +.cm-av{ + width:64rpx;height:64rpx;border-radius:var(--r-sm);flex:none; + display:flex;align-items:center;justify-content:center; + background:var(--primary-soft);color:var(--primary-ink); + font-size:var(--fs-md);font-weight:var(--fw-b); +} +.cm-body{flex:1;min-width:0} +.cm-head{display:flex;align-items:center;gap:var(--sp-1)} +.cm-name{font-size:var(--fs-sm);font-weight:var(--fw-b);color:var(--text-2)} +.cm-time{margin-left:auto;font-size:var(--fs-cap);color:var(--muted2);flex:none} +.cm-text{margin-top:6rpx;font-size:var(--fs-md);line-height:1.55;word-break:break-word} +.cm-tip{margin-top:var(--sp-3);text-align:center;color:var(--muted2);font-size:var(--fs-cap)} + +.cm-input{display:flex;align-items:center;gap:var(--sp-3);margin-top:var(--sp-4)} +.cm-input .input{flex:1} +.cm-send{ + flex:none;height:88rpx;padding:0 var(--sp-5);border-radius:var(--r-md); + display:flex;align-items:center;justify-content:center; + background:var(--line);color:#fff;font-size:var(--fs-md);font-weight:var(--fw-b);transition:.16s ease; +} +.cm-send.on{background:var(--cta)} diff --git a/pets-fe/pages/community/community.js b/pets-fe/pages/community/community.js index 9de9097..b3fa87f 100644 --- a/pets-fe/pages/community/community.js +++ b/pets-fe/pages/community/community.js @@ -2,6 +2,21 @@ const store = require('../../utils/store.js'); const api = require('../../utils/api.js'); const { toastErr } = require('../../utils/ui.js'); +// 相对时间。社区里关心的是「多久以前发的」,精确到秒没意义 +function fmtAgo(iso) { + if (!iso) return ''; + const d = new Date(iso); + if (isNaN(d.getTime())) return ''; + const min = Math.floor((Date.now() - d.getTime()) / 60000); + if (min < 1) return '刚刚'; + if (min < 60) return min + ' 分钟前'; + if (min < 60 * 24) return Math.floor(min / 60) + ' 小时前'; + if (min < 60 * 24 * 7) return Math.floor(min / 1440) + ' 天前'; + const p = (n) => (n < 10 ? '0' + n : '' + n); + const sameYear = d.getFullYear() === new Date().getFullYear(); + return (sameYear ? '' : d.getFullYear() + '-') + p(d.getMonth() + 1) + '-' + p(d.getDate()); +} + const TAG_CLASS = { 求助: 'warn', 经验: 'blue', 精选: 'purple', 避坑: 'red', 晒宠: '' }; function mapPost(p) { @@ -22,6 +37,7 @@ function mapPost(p) { user_id: p.user_id, followed: !!p.followed, is_self: !!p.is_self, + timeText: fmtAgo(p.created_at), }; } diff --git a/pets-fe/pages/community/community.wxml b/pets-fe/pages/community/community.wxml index 33d219a..b0246af 100644 --- a/pets-fe/pages/community/community.wxml +++ b/pets-fe/pages/community/community.wxml @@ -14,7 +14,10 @@ module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; } {{item.author_emoji}} - {{item.author_name}}AI宠友 + + {{item.author_name}}AI + {{item.timeText}} + {{item.tag}} @@ -27,9 +30,18 @@ module.exports.isUrl = function (s) { return s && s.indexOf('http') === 0; } - {{item.like_count}} - {{item.comment_count}} - + + + {{item.like_count || '点赞'}} + + + + {{item.comment_count || '评论'}} + + diff --git a/pets-fe/pages/community/community.wxss b/pets-fe/pages/community/community.wxss index 69d0834..d4b5e37 100644 --- a/pets-fe/pages/community/community.wxss +++ b/pets-fe/pages/community/community.wxss @@ -21,14 +21,18 @@ display:flex;align-items:center;justify-content:center;font-size:56rpx; } -.post-actions{display:flex;justify-content:space-around;border-top:1rpx solid var(--line);padding-top:var(--sp-3)} +/* 三等分。原来是 space-around 排三个宽度不一的项,点赞数一变宽度就跳 */ +.post-actions{display:flex;border-top:1rpx solid var(--line);padding-top:var(--sp-2)} .pa-btn{ - display:flex;align-items:center;gap:var(--sp-1); - color:var(--muted);font-weight:var(--fw-b);font-size:var(--fs-sm); + flex:1;height:72rpx; + display:flex;align-items:center;justify-content:center;gap:var(--sp-1); + color:var(--muted);font-weight:var(--fw);font-size:var(--fs-sm); } +.pa-n{line-height:1} .pa-btn.liked{color:var(--red-ink)} +.pa-btn:active{background:var(--surface-2);border-radius:var(--r-sm)} /* 分享用微信原生 open-type=share,得抹掉 button 默认外观 */ -.pa-share{background:transparent;border:none;padding:0;margin:0;line-height:inherit;height:auto} +.pa-share{background:transparent;border:none;padding:0;margin:0;line-height:inherit} .pa-share::after{border:none} /* 关注按钮 */