Files
sundynix-pets/pets-fe/components/bottom-sheet/bottom-sheet.js
T
Blizzard 6fd4977f46 feat(fe): 建档挑方案、计划页套方案、阶段漂移提示
后端上一轮做完了但小程序上看不到,这次接上。

## 建档第四步:挑方案
方案按阶段取,所以必须排在「资料」之后(阶段是第三步定的)。卡片上按重复方式
分开报数(每天 3 项 / 每周 2 项 / 一次性 2 项),不报一个总数——4 条「每天」
套一整月就是 120 多条,只给总数用户会以为出 bug。

「先空着,我自己排」是个平等的选项,和方案卡长一样、就在下面一眼能看到,
不是藏起来的退路:已经养熟的宠物主人有自己的节奏,硬塞一套只会被删掉。

默认选第一套——多数用户是新手,给一个合理默认比让他面对空白好。

编辑档案时不出现第四步:改档案不该顺手改计划。

套方案失败不算建档失败:档案已经建好了,方案之后在计划页还能套,
不该把用户退回表单。

## 计划页:空月份和平时都能套
空月份的提示条带「套一套方案」按钮 —— 空着让用户从零排,多数人会直接退出去。
但「+ 加一件」始终在,不能只给套用。

浮层里写明「套用是往里加,不会覆盖你已经排好的」,因为服务端就是按
「日期+标题」去重的,这句话是真的。套完 toast 报排了几件;一件都没排
(全都已存在)时说「这个月已经有这些安排了」,不说「成功」。

## 阶段漂移提示
首页门面卡下面一张卡:「钞票已经 17个月,从幼年期进入成年期了」,
带上新阶段的划分依据、养护重点和体型说明,点进去到建档页改。

用主色不用红色 —— 这是「可以做一下」不是「出问题了」。
改完回首页 onShow 重拉一次,卡片自然消失。

不在首页直接改阶段:改完要不要套新方案是同一件事,放在建档页一起做才连贯。

## 顺手把合并的尾巴清了
manageTasks 那一支删了:它管的是 DailyTask,而今日任务已经并进计划节点,
增删改都在计划页。留着会出现「这里加的任务」和「计划页加的节点」进不同的表。

连带清掉 7 个死方法 + taskSheets + 一个死 require(简易表单那套早随记录表单
搬到 pages/addrecord 了)。首页现在一个弹层都不开,bottom-sheet 组件也去掉了。

弹层累计:js 1110 → 689 行,wxml 485 → 278 行。

## 验证(预生产库实跑)
  建档四步    6 个月柯基 → 幼年期 → 拉到「幼犬标准照护」→ 套用本月 7 条
              → 首页今日任务 4 条
  先空着      今日任务 0 条,疫苗驱虫提醒仍然建
  计划页套用  0 → 7 条;重复套用 0 条(去重生效)
  漂移检测    17 个月柯基存着幼年期 → drifted=true,给出依据/重点/体型
              改成成年期 → drifted=false
              没生日 → 不提示(算不出就不猜)
              「刚到家」→ 不提示(叠加层不是年龄段)

第一版验证脚本把阶段改成了和算出来一样的值,drifted=False 是对的,
但我的 print 无条件打了提示文案,看着像 bug。重测才验到真的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:59:27 +08:00

679 lines
24 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 IDENTITY = ['petName', 'anonymous', 'official'];
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: '',
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,
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.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 || {};
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) });
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 }); },
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 = 9 - this.data.postImages.length;
if (left <= 0) return wx.showToast({ title: '最多 9 张', 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 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 {
await api.createPost({
pet_id: identity === 'petName' ? store.currentPetId() : null,
identity,
content,
tags: [tag],
image_file_ids: this.data.postImages.map((i) => i.id),
});
this.triggerEvent('posted');
this.close();
} 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: {} });
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 }, cmFocus: true });
},
onCommentFocus(e) {
// adjust-position=false,键盘高度自己接管:弹层是 fixed 定位,
// 交给系统顶会把整块推出屏幕
this.setData({ kbHeight: (e.detail && e.detail.height) || 0 });
},
onCommentBlur() {
this.setData({ kbHeight: 0, cmFocus: false });
},
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 });
},
},
});