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.followed ? '已关注' : '+ 关注'}}
{{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}
/* 关注按钮 */