Files
sundynix-pets/pets-fe/components/bottom-sheet/bottom-sheet.js
T
Blizzard c890732410 feat: 成长报告海报改周报口径 + 显示统计周期
- 海报四项统计原来是全部累计,和「周报告」不符;改成近 7 天口径
  (和报告页本周概览一致),高风险异常也按本周真实统计(原来写死 0)
- Poster 加 period 字段,海报上画出「近 7 天 · 07.25 - 07.31」

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 15:33:12 +08:00

748 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const store = require('../../utils/store.js');
const api = require('../../utils/api.js');
const upload = require('../../utils/upload.js');
function fmtDate(iso) {
if (!iso) return '';
const d = new Date(iso);
return d.getMonth() + 1 + '月' + d.getDate() + '日';
}
// n 天后的 YYYY-MM-DD,给提醒类弹层做默认日期
function daysLater(n) {
const d = new Date(Date.now() + n * 86400000);
const p = (x) => (x < 10 ? '0' + x : '' + x);
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
}
// "2026-07-30" → "2026-07-30T12:00:00+08:00"
//
// 相对时间。社区里关心的是「多久以前发的」,精确到秒没意义
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());
}
// 提醒类型(与后端 model.Reminder* 常量对应)
const REMINDER_TYPES = [
{ key: 'vaccine', label: '疫苗' },
{ key: 'deworm', label: '驱虫' },
{ key: 'weight', label: '体重' },
{ key: 'monthlyReport', label: '月度报告' },
];
const REMINDER_LABELS = REMINDER_TYPES.map((t) => t.label);
// 标签和社区顶部筛选保持一致(晒宠 / 新手求助 / 经验)
const PTAG = ['晒宠', '新手求助', '经验'];
Component({
options: { addGlobalClass: true },
properties: {
show: { type: Boolean, value: false },
type: { type: String, value: '' },
// 雪花 ID 是 18 位字符串,声明成 Number 会超出 JS 安全整数范围而丢精度
postId: { type: String, value: '' },
},
data: {
innerType: '',
pet: {},
segSel: {},
optSel: {},
// 通用简易记录(洗护/清洁那 15 种共用)
postContent: '',
pfFocus: false, // 发帖内容框聚焦标记(点整块 field 聚焦用)
postImages: [],
remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' },
remTypeLabels: REMINDER_LABELS,
myPets: [],
exportText: '',
fbContent: '',
fbContact: '',
fbDone: false,
commentText: '',
comments: [],
reminders: [],
poster: null,
reportDetail: null,
proInfo: null,
saving: false,
posterSaving: false,
commentImages: [],
replyTo: {},
cmFocus: false,
composing: false, // 评论输入面板是否展开(收起时只是一条细条)
kbHeight: 0,
total: 0,
},
observers: {
show: function (show) {
// 自定义 tabBar 会盖住弹层底部按钮;打开弹层时把它藏起来,关闭时恢复
// 用 getTabBar().hidden 开关,不用 wx.showTabBar(真机上会额外冒出原生 tabBar)
this.toggleTabBar(!!show);
},
'show, type': function (show, type) {
if (show && type) this.setType(type, true);
},
},
lifetimes: {
detached() {
this.toggleTabBar(false);
},
},
methods: {
setType(type, fresh) {
const pet = store.getPet();
const patch = { innerType: type, pet };
if (fresh) {
patch.segSel = {};
patch.optSel = {};
patch.postContent = '';
patch.postImages = [];
patch.medDose = '';
patch.remForm = { id: '', typeIdx: 0, title: '', date: '', freq: '' };
patch.exportText = '';
patch.fbContent = '';
patch.fbContact = '';
patch.fbDone = false;
patch.commentText = '';
patch.commentImages = [];
patch.replyTo = {};
patch.cmFocus = false;
patch.composing = false;
patch.kbHeight = 0;
patch.saving = false;
}
this.setData(patch);
this.loadSheetData(type);
},
// 按弹层类型拉取真实数据
loadSheetData(type) {
const id = store.currentPetId();
if (type === 'manageTasks') {
this.loadManageTasks();
} else if (type === 'managePets') {
this.loadMyPets();
} else if (type === 'exportData') {
this.buildExport();
} else if (type === 'comments' && this.data.postId) {
this.loadComments();
} else if (type === 'reminders' && id) {
this.loadReminders();
} else if (type === 'poster' && id) {
api.getPoster(id).then((p) => this.setData({ poster: p })).catch(() => {});
} else if (type === 'reportDetail' && id) {
api.weeklyReport(id).then((r) => this.setData({ reportDetail: r })).catch(() => {});
} else if (type === 'pro') {
api.getPro().then((p) => this.setData({ proInfo: p })).catch(() => {});
}
},
goSheet(e) {
this.setType(e.currentTarget.dataset.type, true);
},
close() {
this.triggerEvent('close');
},
onMaskTap() {
this.close();
},
noop() {},
// 藏/显当前 tab 页的自定义 tabBar(非 tab 页无 getTabBar,安全跳过)
toggleTabBar(hidden) {
const pages = getCurrentPages();
const page = pages[pages.length - 1];
if (page && typeof page.getTabBar === 'function' && page.getTabBar()) {
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 || {};
// 优先用后端海报数据里的头像(和身份卡同一路径,可靠),store 兜底
const avatarUrl = p.pet_avatar_url || pet.avatar_url || '';
// 头像是远程图,异步加载。加载完(或没有头像)再往下画,
// 否则头像会画不上。加载失败当没有头像处理
const loadAvatar = () =>
new Promise((res) => {
if (!avatarUrl) return res(null);
const img = node.createImage();
img.onload = () => res(img);
img.onerror = () => res(null);
img.src = avatarUrl;
});
loadAvatar().then((avatar) => {
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);
// 上传过照片就画圆形头像,没有才退回 emoji(默认头像太丑)
const avD = 132, avX = (W - avD) / 2, avY = 132;
if (avatar) {
ctx.save();
ctx.beginPath();
ctx.arc(W / 2, avY + avD / 2, avD / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
// 居中裁剪成正方形再画,竖图/横图都不压扁
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';
ctx.fillText(pet.emoji || '🐾', W / 2, 216);
}
ctx.textAlign = 'center';
ctx.fillStyle = '#8D8277';
ctx.font = '24px sans-serif';
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, 316, 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);
// 头像加载完(或没有)才走到这里,此刻画布已完整
wx.canvasToTempFilePath(
{
canvas: node,
fileType: 'png',
success: (r) => resolve(r.tempFilePath),
fail: () => reject(new Error('生成图片失败')),
},
this,
);
}); // loadAvatar().then
});
});
},
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) });
this.setData({ segSel });
},
onOpt(e) {
const { group, index } = e.currentTarget.dataset;
const optSel = Object.assign({}, this.data.optSel, { [group]: Number(index) });
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)
.then((page) =>
this.setData({
total: page.total || 0,
comments: (page.list || []).map((c) => ({
...c,
timeText: fmtAgo(c.created_at),
// 没有头像字段,用昵称首字做个色块,比一律显示同一个 emoji 强
initial: (c.author_name || '?').slice(0, 1),
imgs: Array.isArray(c.images) ? c.images : [],
replies: (c.replies || []).map((r) => ({
...r,
timeText: fmtAgo(r.created_at),
initial: (r.author_name || '?').slice(0, 1),
})),
})),
}),
)
.catch(() => {});
},
// 长按删除自己的评论
onDeleteComment(e) {
const { id, self } = e.currentTarget.dataset;
if (!id || !self) return;
const c = { id };
wx.showModal({
title: '删除评论',
content: '确定删除这条评论?',
confirmColor: '#D9534F',
success: (res) => {
if (!res.confirm) return;
api.deleteComment(c.id)
.then(() => { this.loadComments(); this.triggerEvent('commented'); })
.catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' }));
},
});
},
// ---- 多宠物管理 ----
loadMyPets() {
const cur = store.currentPetId();
this.setData({
myPets: (store.getPets() || []).map((p) => ({
id: p.id, name: p.name, emoji: p.emoji,
meta: [p.type, p.stage, p.weight].filter(Boolean).join(' · '),
current: p.id === cur,
})),
});
},
onSwitchPet(e) {
const id = e.currentTarget.dataset.id;
store.switchPet(id);
this.loadMyPets();
wx.showToast({ title: '已切换', icon: 'success' });
setTimeout(() => this.close(), 500);
},
// 多宠管理里点某只去编辑。editPet 那支已经搬到 pages/petform
// 这里必须关掉弹层再跳页 —— 留着 setType('editPet') 会开一个空白弹层
onEditPetFromList(e) {
const id = e.currentTarget.dataset.id;
store.switchPet(id);
this.close();
wx.navigateTo({ url: '/pages/petform/petform?id=' + id });
},
// ---- 导出健康档案 ----
buildExport() {
const id = store.currentPetId();
const pet = store.getPet() || {};
if (!id) {
this.setData({ exportText: '还没有建档,先添加一只毛孩子吧。' });
return;
}
Promise.all([
api.getRecords(id, { page: 1, pageSize: 50 }).catch(() => ({ list: [], total: 0 })),
api.getReminders(id).catch(() => []),
]).then(([recPage, rems]) => {
const lines = [];
lines.push('【肉垫计划 · 健康档案】');
lines.push(`宠物:${pet.name || ''}${pet.type || ''}${pet.gender || ''}`);
lines.push(`阶段:${pet.stage || '-'} 年龄:${pet.age || '-'} 体重:${pet.weight || '-'}`);
if (pet.breed || pet.color) lines.push(`品种/毛色:${pet.breed || '-'} / ${pet.color || '-'}`);
lines.push('');
lines.push(`— 健康记录(共 ${recPage.total || 0} 条,导出最近 ${(recPage.list || []).length} 条)—`);
(recPage.list || []).forEach((r) => {
lines.push(`· ${fmtDate(r.occurred_at)} ${r.title}${r.description ? '' + r.description : ''}`);
});
lines.push('');
lines.push('— 提醒 —');
(rems || []).forEach((r) => {
lines.push(`· ${r.title}${r.next_due_date ? fmtDate(r.next_due_date) : r.frequency || '未设置'}`);
});
lines.push('');
lines.push('(本档案仅供日常照护与就医参考,不构成诊断意见)');
this.setData({ exportText: lines.join('\n') });
});
},
onCopyExport() {
const t = this.data.exportText;
if (!t) return;
wx.setClipboardData({
data: t,
success: () => wx.showToast({ title: '已复制到剪贴板', icon: 'success' }),
});
},
// ---- 意见反馈 ----
onFbContent(e) { this.setData({ fbContent: e.detail.value }); },
onFbContact(e) { this.setData({ fbContact: e.detail.value }); },
onSubmitFeedback() {
const content = (this.data.fbContent || '').trim();
if (!content) return wx.showToast({ title: '写点什么再提交', icon: 'none' });
if (this.data.saving) return;
this.setData({ saving: true });
api
.submitFeedback({ content, contact: (this.data.fbContact || '').trim() })
.then(() => this.setData({ fbDone: true, saving: false }))
.catch((e) => {
wx.showToast({ title: e.message || '提交失败', icon: 'none' });
this.setData({ saving: false });
});
},
// ---- 提醒增删改 ----
loadReminders() {
const id = store.currentPetId();
if (!id) return;
api
.getReminders(id)
.then((list) =>
this.setData({
reminders: (list || []).map((r) => ({
id: r.id,
type: r.type,
title: r.title,
date: r.next_due_date ? String(r.next_due_date).slice(0, 10) : '',
freq: r.frequency || '',
when: r.next_due_date ? fmtDate(r.next_due_date) : r.frequency || '未设置',
})),
}),
)
.catch(() => {});
},
onRemTitle(e) { this.setData({ 'remForm.title': e.detail.value }); },
onRemFreq(e) { this.setData({ 'remForm.freq': e.detail.value }); },
onRemType(e) { this.setData({ 'remForm.typeIdx': Number(e.detail.value) }); },
onRemDate(e) { this.setData({ 'remForm.date': e.detail.value }); },
onEditReminder(e) {
const r = this.data.reminders[e.currentTarget.dataset.index];
let idx = REMINDER_TYPES.findIndex((t) => t.key === r.type);
if (idx < 0) idx = 0;
this.setData({ remForm: { id: r.id, typeIdx: idx, title: r.title, date: r.date, freq: r.freq } });
},
onSaveReminder() {
const f = this.data.remForm;
const title = (f.title || '').trim();
if (!title) return wx.showToast({ title: '填个提醒名', icon: 'none' });
const body = {
type: REMINDER_TYPES[f.typeIdx].key,
title,
next_due_date: f.date || '',
frequency: (f.freq || '').trim(),
};
const petId = store.currentPetId();
const p = f.id ? api.updateReminder(f.id, body) : api.createReminder(petId, body);
p.then(() => {
this.setData({ remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' } });
this.loadReminders();
this.triggerEvent('saved');
}).catch((e) => wx.showToast({ title: e.message || '保存失败', icon: 'none' }));
},
onDeleteReminder(e) {
const r = this.data.reminders[e.currentTarget.dataset.index];
wx.showModal({
title: '删除提醒',
content: '确定删除「' + r.title + '」?',
success: (res) => {
if (!res.confirm) return;
api.deleteReminder(r.id).then(() => {
this.loadReminders();
this.triggerEvent('saved');
}).catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' }));
},
});
},
// ---- 管理任务 ----
loadManageTasks() {
const id = store.currentPetId();
if (!id) return;
api.getTasks(id).then((tasks) => this.setData({ manageTasks: tasks || [] })).catch(() => {});
},
onPickPostImages() {
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) }))
.catch((e) => {
if (e && e.canceled) return;
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
});
},
onRemovePostImage(e) {
const i = e.currentTarget.dataset.index;
const arr = this.data.postImages.slice();
arr.splice(i, 1);
this.setData({ postImages: arr });
},
onCommentInput(e) { this.setData({ commentText: e.detail.value }); },
segIdx(group) {
const v = this.data.segSel[group];
return v === undefined ? 0 : v;
},
optIdx(group) {
const v = this.data.optSel[group];
return v === undefined ? 0 : v;
},
// 记录表单整体搬去 pages/addrecord 之后,这里只剩「生成就医前摘要」
// 会往 health_records 写一条 note —— 它是个动作留痕,不是用户填的表单
buildRecord() {
if (this.data.innerType === 'vetSummary') {
return { type: 'note', icon: '📄', title: '生成就医前摘要' };
}
return null;
},
async createAndClose(rec) {
const id = store.currentPetId();
if (!rec) {
this.close();
return;
}
// 没建档时点「保存」原来是静默关掉弹层,用户以为存上了,其实什么都没发生
if (!id) {
wx.showToast({ title: '先建一份宠物档案再记录', icon: 'none' });
return;
}
if (this.data.saving) return;
this.setData({ saving: true });
try {
const saved = await api.createRecord(id, rec);
if (rec.type === 'weight') store.patchCurrent({ weight: rec.title.replace('体重 ', '') });
this.triggerEvent('saved', saved);
this.close();
} catch (e) {
wx.showToast({ title: e.message || '保存失败', icon: 'none' });
this.setData({ saving: false });
}
},
onSave() {
this.createAndClose(this.buildRecord());
},
async onCreatePost() {
const content = (this.data.postContent || '').trim();
if (!content) return wx.showToast({ title: '写点什么吧', icon: 'none' });
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({
identity: 'user',
content,
tags: [tag],
image_file_ids: this.data.postImages.map((i) => i.id),
});
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 });
}
},
// 评论
async onSendComment() {
const text = (this.data.commentText || '').trim();
// 原来空评论会直接把弹层关掉,看起来像发成功了。改成明确提示
if (!text) return wx.showToast({ title: '写点什么再发', icon: 'none' });
if (!this.data.postId) return this.close();
try {
await api.createComment(
this.data.postId,
text,
this.data.commentImages.map((i) => i.url),
this.data.replyTo.id || '',
);
this.setData({ commentText: '', commentImages: [], replyTo: {}, composing: false, cmFocus: false });
this.loadComments();
this.triggerEvent('commented');
} catch (e) {
wx.showToast({ title: e.message || '评论失败', icon: 'none' });
}
},
// 评论配图,最多 3 张
onPickCommentImages() {
const left = 3 - this.data.commentImages.length;
if (left <= 0) return wx.showToast({ title: '最多 3 张', icon: 'none' });
upload
.chooseAndUploadImages(left)
.then((files) => this.setData({ commentImages: this.data.commentImages.concat(files) }))
.catch((e) => {
if (e && e.canceled) return;
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
});
},
// 回复某条评论:输入区顶部显示「回复 xxx」,发送时带上 parent_id
// 小红书那套:点任意一条评论/回复,直接把输入框激活并填上「回复 @xxx」,
// 不用再去底部找输入框、也不用单独点一个「回复」按钮
onReplyTo(e) {
const { id, name } = e.currentTarget.dataset;
if (!id) return;
// 点任意评论直接展开面板并预填回复对象
this.setData({ replyTo: { id, name }, composing: true, cmFocus: true });
},
// 点细条:展开大输入面板并聚焦(拉起键盘)
onStartCompose() {
this.setData({ composing: true, cmFocus: true });
},
// 收起面板,草稿(文字/图片)留着,下次点开接着写
onCloseCompose() {
this.setData({ composing: false, cmFocus: false, kbHeight: 0 });
},
onCommentFocus(e) {
// adjust-position=false,键盘高度自己接管:弹层是 fixed 定位,
// 交给系统顶会把整块推出屏幕
this.setData({ kbHeight: (e.detail && e.detail.height) || 0 });
},
onCommentBlur() {
// 失焦先放下键盘。有草稿就保持展开(否则点「发送」会因面板收起而丢掉这一下点击);
// 空内容才收回细条
const empty = !(this.data.commentText || '').trim() && !this.data.commentImages.length;
this.setData({ kbHeight: 0, cmFocus: false, composing: empty ? false : this.data.composing });
},
onCancelReply() {
this.setData({ replyTo: {} });
},
// 一级评论默认只带 3 条回复,点开拉全量
onExpandReplies(e) {
const { id, index } = e.currentTarget.dataset;
api
.listReplies(id)
.then((list) =>
this.setData({
[`comments[${index}].replies`]: (list || []).map((r) => ({
...r,
timeText: fmtAgo(r.created_at),
initial: (r.author_name || '?').slice(0, 1),
})),
}),
)
.catch((err) => wx.showToast({ title: (err && err.message) || '加载失败', icon: 'none' }));
},
onPreviewCommentImage(e) {
const { urls, cur } = e.currentTarget.dataset;
if (urls && urls.length) wx.previewImage({ urls, current: cur });
},
onRemoveCommentImage(e) {
const list = this.data.commentImages.slice();
list.splice(e.currentTarget.dataset.index, 1);
this.setData({ commentImages: list });
},
},
});