29d2cd3faf
## tabBar 首页 / 报告 / 社区 / 我的。记录不再是 tab。 因为高亮是 tabBar 组件按当前路由自己算的(Phase A 那次改的),删一个 tab 只需要动 LIST 一处,四个页面一行都没改。 ## 快速记录独立成页 —— 但没有新建页面 弹层里塞 24 项确实挤(用户原话「很丑」)。撤掉上一版加的 quickRecord 分支。 **pages/record/record 本身就是那个页面**:它顶部已经是 24 项 4 组宫格, 下面接着健康洞察、体重趋势、健康时间轴。再单独建一个只放宫格的页面,等于同一个 宫格维护两份,而且那页除了宫格什么都没有,进去点一下就得退出来。 所以把它从 tab 降级成二级页:nav 标题从「快速记录」改成「记录」+ 返回键, 移除 tabBar 同步。首页右下角悬浮键直接跳它,落地就是宫格。 pet-switch 保留 —— 时间轴/趋势/洞察都是按宠物看的,而且计划页作为二级页 也是这么处理的,一致。 ## 入口 首页悬浮键 → 记录页(全部 24 项) 首页「快速记录」卡 → 8 个高频项直接开表单,「更多」→ 记录页 文章页 / 设置页 → 记录页 三处 switchTab 改成 navigateTo(记录页已经不是 tab,switchTab 会静默失败)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1090 lines
41 KiB
JavaScript
1090 lines
41 KiB
JavaScript
const store = require('../../utils/store.js');
|
||
const recordTypes = require('../../utils/recordTypes.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"
|
||
//
|
||
// 后端用 time.Parse(time.RFC3339) 解 occurred_at,只给日期它解不出来会静默
|
||
// 回退成 time.Now()——用户选了「昨天洗澡」会存成今天,而且不报错。
|
||
// 时区必须带真实偏移:转成 UTC 的 Z 形式,落库再转回本地时会把边界日期挪一天
|
||
// (首页任务那个 bug 就是这么来的)。取正午同样是为了远离日界。
|
||
function dayToISO(day) {
|
||
if (!day) return '';
|
||
const off = -new Date().getTimezoneOffset(); // 东八区是 +480
|
||
const sign = off >= 0 ? '+' : '-';
|
||
const a = Math.abs(off);
|
||
const p = (x) => (x < 10 ? '0' + x : '' + x);
|
||
return day + 'T12:00:00' + sign + p(Math.floor(a / 60)) + ':' + p(a % 60);
|
||
}
|
||
|
||
// 相对时间。社区里关心的是「多久以前发的」,精确到秒没意义
|
||
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);
|
||
const now = new Date();
|
||
let months = (now.getFullYear() - b.getFullYear()) * 12 + (now.getMonth() - b.getMonth());
|
||
if (months < 0) months = 0;
|
||
return months < 24 ? months + '个月' : Math.floor(months / 12) + '岁';
|
||
}
|
||
|
||
const GENDERS = ['男孩', '女孩', '不确定'];
|
||
const STAGES = ['刚到家 0-30 天', '幼年期', '成年期', '老年期'];
|
||
|
||
// 任务可关联的记录弹层类型
|
||
// 任务能关联的记录类型。原来这里也是写死的 6 种,后台加了类型这个下拉里看不到,
|
||
// 于是「自定义任务关联新类型」这条路是断的。改成读同一份类型缓存
|
||
function taskSheets() {
|
||
const list = [{ key: '', label: '不关联' }];
|
||
(recordTypes.groups() || []).forEach((g) =>
|
||
g.items.forEach((t) => list.push({ key: t.code, label: t.label })),
|
||
);
|
||
return list;
|
||
}
|
||
|
||
// 提醒类型(与后端 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 SIMPLE_HINT = {
|
||
water: '大概喝了多少、换水了没有',
|
||
bath: '用了什么沐浴露、有没有吹干',
|
||
nail: '剪了几只爪、有没有出血',
|
||
ear: '耳道干不干净、有没有异味',
|
||
tooth: '用了什么牙膏、配合度怎么样',
|
||
brush: '掉毛多不多、有没有打结',
|
||
groom: '在哪家做的、剪了什么造型、花了多少',
|
||
litter: '换了多少、用的什么砂',
|
||
litterbox: '洗了几个、有没有消毒',
|
||
bowl: '有没有用洗碗液、有没有滑腻感',
|
||
waterbowl: '滤芯还好吗、有没有水垢',
|
||
clean: '消了哪些地方、用的什么消毒液',
|
||
checkup: '在哪家做的、结果怎么样、下次什么时候',
|
||
vet: '什么症状、医生怎么说、开了什么药',
|
||
supplement: '吃的什么、多大剂量、吃多久',
|
||
};
|
||
|
||
// 异常观察的四组选项。原来散在 AI 风险评估那个方法里当局部变量,
|
||
// 现在 buildRecord 也要用,提到模块级
|
||
const SYMPTOMS = ['呕吐', '拉稀', '精神差', '皮肤红'];
|
||
const DURATIONS = ['刚刚', '半天', '1天以上'];
|
||
const SPIRITS = ['正常', '一般', '明显变差'];
|
||
const SEVERITY = ['低', '中', '高'];
|
||
|
||
const POOP = ['正常', '软便', '拉稀'];
|
||
const FOOD = ['正常', '偏少', '不吃'];
|
||
const COST = ['食品', '医疗', '用品'];
|
||
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: {},
|
||
dateVals: { vaccine: '', deworm: '' },
|
||
// 通用简易记录(洗护/清洁那 15 种共用)
|
||
isSimple: false,
|
||
simpleLabel: '',
|
||
simpleDate: '',
|
||
simpleNote: '',
|
||
simpleImages: [],
|
||
simplePlaceholder: '',
|
||
wInput: '',
|
||
wNote: '',
|
||
poopNote: '',
|
||
foodText: '',
|
||
medName: '',
|
||
medDose: '',
|
||
costNote: '',
|
||
costAmount: '',
|
||
postContent: '',
|
||
postImages: [],
|
||
manageTasks: [],
|
||
taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 },
|
||
taskSheetLabels: [],
|
||
remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' },
|
||
remTypeLabels: REMINDER_LABELS,
|
||
myPets: [],
|
||
exportText: '',
|
||
fbContent: '',
|
||
fbContact: '',
|
||
fbDone: false,
|
||
addName: '',
|
||
addBirthday: '',
|
||
addWeight: '',
|
||
addColor: '',
|
||
addBreed: '',
|
||
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.costAmount = '';
|
||
patch.postContent = '';
|
||
patch.postImages = [];
|
||
patch.poopNote = '';
|
||
patch.foodText = '';
|
||
patch.medName = '';
|
||
patch.medDose = '';
|
||
patch.costNote = '';
|
||
patch.manageTasks = [];
|
||
patch.taskForm = { id: '', title: '', description: '', priority: '', sheetIdx: 0 };
|
||
patch.taskSheetLabels = taskSheets().map((x) => x.label);
|
||
patch.remForm = { id: '', typeIdx: 0, title: '', date: '', freq: '' };
|
||
patch.exportText = '';
|
||
patch.fbContent = '';
|
||
patch.fbContact = '';
|
||
patch.fbDone = false;
|
||
patch.addName = '';
|
||
patch.addBirthday = '';
|
||
patch.addWeight = '';
|
||
patch.addColor = '';
|
||
patch.addBreed = '';
|
||
patch.commentText = '';
|
||
patch.commentImages = [];
|
||
patch.replyTo = {};
|
||
patch.cmFocus = false;
|
||
patch.kbHeight = 0;
|
||
patch.saving = false;
|
||
patch.simpleNote = '';
|
||
patch.simpleImages = [];
|
||
}
|
||
// 是否走通用简易表单。缓存热的时候是同步的;万一没热就按「不是」处理,
|
||
// 那 9 种的分支写死在 wxml 里不依赖这份缓存,不会因此打不开
|
||
const simple = recordTypes.isSimple(type);
|
||
patch.isSimple = simple;
|
||
if (simple) {
|
||
const label = recordTypes.label(type);
|
||
patch.simpleLabel = label;
|
||
patch.simpleDate = daysLater(0);
|
||
patch.simplePlaceholder = SIMPLE_HINT[type] || ('这次' + label + '的情况,几个字就行');
|
||
}
|
||
if (type === 'weight') {
|
||
patch.wInput = (pet.weight || '').replace('kg', '');
|
||
patch.wNote = '';
|
||
}
|
||
// 提醒日期给个合理的默认值。原本写死成固定日期,时间一过就成了「提醒昨天」
|
||
if (type === 'vaccine') patch.dateVals = { ...this.data.dateVals, vaccine: daysLater(30) };
|
||
if (type === 'deworm') patch.dateVals = { ...this.data.dateVals, deworm: daysLater(90) };
|
||
if (type === 'editPet') {
|
||
// 预填当前宠物档案
|
||
patch.addName = pet.name || '';
|
||
patch.addColor = pet.color || '';
|
||
patch.addBreed = pet.breed || '';
|
||
patch.addWeight = (pet.weight || '').replace('kg', '');
|
||
patch.addBirthday = pet.birthday ? String(pet.birthday).slice(0, 10) : '';
|
||
patch.segSel = {
|
||
addType: pet.type === '狗狗' ? 1 : 0,
|
||
addGender: Math.max(0, GENDERS.indexOf(pet.gender)),
|
||
};
|
||
patch.optSel = { addStage: Math.max(0, STAGES.indexOf(pet.stage)) };
|
||
}
|
||
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(() => {});
|
||
}
|
||
},
|
||
|
||
onActivatePro() {
|
||
api
|
||
.activatePro()
|
||
.then((p) => {
|
||
this.setData({ proInfo: p });
|
||
wx.showToast({ title: '已开通 Pro', icon: 'success' });
|
||
setTimeout(() => this.close(), 800);
|
||
})
|
||
.catch((e) => wx.showToast({ title: e.message || '开通失败', icon: 'none' }));
|
||
},
|
||
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 });
|
||
},
|
||
onSimpleDate(e) {
|
||
this.setData({ simpleDate: e.detail.value });
|
||
},
|
||
onSimpleNote(e) {
|
||
this.setData({ simpleNote: e.detail.value });
|
||
},
|
||
onPickSimpleImage() {
|
||
upload
|
||
.chooseAndUploadImage()
|
||
.then((f) => this.setData({ simpleImages: [f] }))
|
||
.catch((e) => {
|
||
if (e && e.canceled) return;
|
||
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
|
||
});
|
||
},
|
||
onRemoveSimpleImage() {
|
||
this.setData({ simpleImages: [] });
|
||
},
|
||
onDate(e) {
|
||
this.setData({ [`dateVals.${e.currentTarget.dataset.key}`]: e.detail.value });
|
||
},
|
||
onWInput(e) { this.setData({ wInput: e.detail.value }); },
|
||
onWNote(e) { this.setData({ wNote: e.detail.value }); },
|
||
onPoopNote(e) { this.setData({ poopNote: e.detail.value }); },
|
||
onFoodText(e) { this.setData({ foodText: e.detail.value }); },
|
||
onMedName(e) { this.setData({ medName: e.detail.value }); },
|
||
onMedDose(e) { this.setData({ medDose: e.detail.value }); },
|
||
onCostNote(e) { this.setData({ costNote: e.detail.value }); },
|
||
onCostInput(e) { this.setData({ costAmount: e.detail.value }); },
|
||
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);
|
||
},
|
||
onEditPetFromList(e) {
|
||
store.switchPet(e.currentTarget.dataset.id);
|
||
this.setType('editPet', true);
|
||
},
|
||
|
||
// ---- 导出健康档案 ----
|
||
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(() => {});
|
||
},
|
||
onTaskTitle(e) { this.setData({ 'taskForm.title': e.detail.value }); },
|
||
onTaskDesc(e) { this.setData({ 'taskForm.description': e.detail.value }); },
|
||
onTaskPriority() {
|
||
this.setData({ 'taskForm.priority': this.data.taskForm.priority === '重要' ? '' : '重要' });
|
||
},
|
||
onTaskSheet(e) { this.setData({ 'taskForm.sheetIdx': Number(e.detail.value) }); },
|
||
onEditTaskItem(e) {
|
||
const t = this.data.manageTasks[e.currentTarget.dataset.index];
|
||
const sheets = taskSheets();
|
||
let sheetIdx = sheets.findIndex((s) => s.key === (t.sheet_type || ''));
|
||
if (sheetIdx < 0) sheetIdx = 0;
|
||
this.setData({
|
||
taskForm: { id: t.id, title: t.title, description: t.description || '', priority: t.priority || '', sheetIdx },
|
||
});
|
||
},
|
||
onSaveTask() {
|
||
const f = this.data.taskForm;
|
||
const title = (f.title || '').trim();
|
||
if (!title) return wx.showToast({ title: '填个任务名', icon: 'none' });
|
||
const sheets = taskSheets();
|
||
const body = { title, description: f.description, priority: f.priority, sheet_type: (sheets[f.sheetIdx] || sheets[0]).key };
|
||
const id = store.currentPetId();
|
||
const p = f.id ? api.updateTask(f.id, body) : api.createTask(id, body);
|
||
p.then(() => {
|
||
this.setData({ taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 } });
|
||
this.loadManageTasks();
|
||
this.triggerEvent('saved');
|
||
}).catch((e) => wx.showToast({ title: e.message || '保存失败', icon: 'none' }));
|
||
},
|
||
onDeleteTaskItem(e) {
|
||
const t = this.data.manageTasks[e.currentTarget.dataset.index];
|
||
wx.showModal({
|
||
title: '删除任务',
|
||
content: '确定删除「' + t.title + '」?',
|
||
success: (r) => {
|
||
if (!r.confirm) return;
|
||
api.deleteTask(t.id).then(() => {
|
||
this.loadManageTasks();
|
||
this.triggerEvent('saved');
|
||
}).catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' }));
|
||
},
|
||
});
|
||
},
|
||
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 });
|
||
},
|
||
onAddName(e) { this.setData({ addName: e.detail.value }); },
|
||
onAddBirthday(e) { this.setData({ addBirthday: e.detail.value }); },
|
||
onAddWeight(e) { this.setData({ addWeight: e.detail.value }); },
|
||
onAddColor(e) { this.setData({ addColor: e.detail.value }); },
|
||
onAddBreed(e) { this.setData({ addBreed: e.detail.value }); },
|
||
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;
|
||
},
|
||
// 与 optIdx 的区别:没选就是没选,不当成选了第 0 项
|
||
optPicked(group) {
|
||
return this.data.optSel[group] !== undefined;
|
||
},
|
||
|
||
// 异常观察 → 生成风险评估(真调后端 AI,失败回退)
|
||
// 依据当前弹层类型组装一条健康记录
|
||
buildRecord() {
|
||
const t = this.data.innerType;
|
||
// 简易类型统一在这里组装。icon 故意留空:那 9 种老类型的 icon 里存的是
|
||
// emoji(写库值,不能动),新类型要是存 pt-icon 名,这一列就变成两套语义了。
|
||
// 渲染本来就按 type 取图标(type code 和图标名是同一个词),不需要这一列
|
||
if (this.data.isSimple) {
|
||
const img = (this.data.simpleImages || [])[0];
|
||
return {
|
||
type: t,
|
||
title: this.data.simpleLabel || t,
|
||
description: (this.data.simpleNote || '').trim(),
|
||
occurred_at: dayToISO(this.data.simpleDate),
|
||
image_file_id: img ? img.id : '',
|
||
image_url: img ? img.url : '',
|
||
};
|
||
}
|
||
switch (t) {
|
||
case 'poop': {
|
||
const s = POOP[this.segIdx('poopState')];
|
||
return {
|
||
type: 'poop', icon: '💩', title: '排便记录:' + s, category: s,
|
||
description: (this.data.poopNote || '').trim(),
|
||
};
|
||
}
|
||
case 'food': {
|
||
const a = FOOD[this.segIdx('food')];
|
||
const what = (this.data.foodText || '').trim();
|
||
return {
|
||
type: 'food', icon: '🍽️',
|
||
title: what ? '饮食记录:' + what : '饮食记录:食欲' + a,
|
||
category: a,
|
||
description: what ? '食欲' + a : '',
|
||
};
|
||
}
|
||
case 'medicine': {
|
||
const name = (this.data.medName || '').trim();
|
||
const dose = (this.data.medDose || '').trim();
|
||
return {
|
||
type: 'medicine', icon: '💊',
|
||
title: name ? '用药:' + name : '用药记录',
|
||
description: dose,
|
||
};
|
||
}
|
||
case 'cost': {
|
||
const c = COST[this.segIdx('cost')];
|
||
const amt = parseFloat(this.data.costAmount) || 0;
|
||
return {
|
||
type: 'cost', icon: '💰', title: '消费记录:¥' + amt, num_value: amt, category: c,
|
||
description: (this.data.costNote || '').trim(),
|
||
};
|
||
}
|
||
case 'vaccine':
|
||
return { type: 'vaccine', icon: '💉', title: '疫苗提醒:' + this.data.dateVals.vaccine };
|
||
case 'deworm':
|
||
return { type: 'deworm', icon: '🛡️', title: '驱虫提醒:' + this.data.dateVals.deworm };
|
||
case 'symptom': {
|
||
// 原来这一支不落库:symptom 只是收集表单,真正建记录是在 AI 风险评估
|
||
// 之后的 risk 那一步。AI 下掉后必须自己存,否则「记了异常但没存下来」。
|
||
//
|
||
// category 存严重程度(高/中/低)——这是周报 high_risk_count 的依据
|
||
// (report.go:46 按 category='高' 过滤)。原来这个值是 AI 给的,
|
||
// 现在改成用户自己选:谁看着它谁最清楚,比规则化猜一个准。
|
||
const sy = SYMPTOMS[this.optIdx('symp')];
|
||
const lv = SEVERITY[this.segIdx('sev')] || '中';
|
||
const desc = ['持续' + DURATIONS[this.segIdx('dur')], '精神' + SPIRITS[this.segIdx('spirit')]].join(',');
|
||
return { type: 'symptom', icon: '🤒', title: '异常观察:' + sy, category: lv, description: desc };
|
||
}
|
||
case 'vetSummary':
|
||
return { type: 'note', icon: '📄', title: '生成就医前摘要' };
|
||
default:
|
||
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() {
|
||
const t = this.data.innerType;
|
||
if (t === 'cost' && !(parseFloat(this.data.costAmount) > 0)) {
|
||
wx.showToast({ title: '先填个金额', icon: 'none' });
|
||
return;
|
||
}
|
||
if (t === 'medicine' && !(this.data.medName || '').trim()) {
|
||
wx.showToast({ title: '先填药品名称', icon: 'none' });
|
||
return;
|
||
}
|
||
this.createAndClose(this.buildRecord());
|
||
},
|
||
onSaveWeight() {
|
||
const w = (this.data.wInput || '').toString().replace(/\s+/g, '').replace('kg', '');
|
||
const num = parseFloat(w);
|
||
// 原来空值会静默存成「上次的体重」,等于凭空造一条假数据
|
||
if (!w || !(num > 0)) {
|
||
wx.showToast({ title: '填一个有效体重', icon: 'none' });
|
||
return;
|
||
}
|
||
const weight = w + 'kg';
|
||
this.createAndClose({ type: 'weight', icon: '⚖️', title: '体重 ' + weight, num_value: num, description: (this.data.wNote || '').trim() });
|
||
},
|
||
onSavePhoto() {
|
||
const id = store.currentPetId();
|
||
if (!id) return this.close();
|
||
upload
|
||
.chooseAndUploadImage()
|
||
.then((f) =>
|
||
api.createRecord(id, { type: 'photo', icon: '📷', title: '成长照片', image_file_id: f.id, image_url: f.url }),
|
||
)
|
||
.then((saved) => {
|
||
wx.showToast({ title: '照片已保存', icon: 'success' });
|
||
this.triggerEvent('saved', saved);
|
||
this.close();
|
||
})
|
||
.catch((e) => {
|
||
if (e && e.canceled) return;
|
||
wx.showToast({ title: (e && e.message) || '上传失败', icon: 'none' });
|
||
});
|
||
},
|
||
|
||
// 组装宠物档案表单
|
||
petFormBody() {
|
||
const isDog = this.segIdx('addType') === 1;
|
||
return {
|
||
name: (this.data.addName || '').trim(),
|
||
type: isDog ? '狗狗' : '猫猫',
|
||
emoji: isDog ? '🐶' : '🐱',
|
||
gender: GENDERS[this.segIdx('addGender')],
|
||
birthday: this.data.addBirthday,
|
||
weight: this.data.addWeight,
|
||
color: (this.data.addColor || '').trim(),
|
||
breed: (this.data.addBreed || '').trim(),
|
||
stage: STAGES[this.optIdx('addStage')],
|
||
age: ageFromBirthday(this.data.addBirthday),
|
||
};
|
||
},
|
||
|
||
// 添加/编辑 共用提交入口
|
||
onPetSubmit() {
|
||
if (this.data.innerType === 'editPet') return this.onEditPetSubmit();
|
||
return this.onAddPetSubmit();
|
||
},
|
||
|
||
async onAddPetSubmit() {
|
||
const body = this.petFormBody();
|
||
if (!body.name) return wx.showToast({ title: '请输入名字', icon: 'none' });
|
||
if (this.data.saving) return;
|
||
this.setData({ saving: true });
|
||
try {
|
||
await api.createPet(body);
|
||
await store.loadPets();
|
||
this.triggerEvent('petadded');
|
||
this.close();
|
||
} catch (e) {
|
||
wx.showToast({ title: e.message || '添加失败', icon: 'none' });
|
||
this.setData({ saving: false });
|
||
}
|
||
},
|
||
|
||
async onEditPetSubmit() {
|
||
const body = this.petFormBody();
|
||
if (!body.name) return wx.showToast({ title: '请输入名字', icon: 'none' });
|
||
const id = store.currentPetId();
|
||
if (!id) return this.close();
|
||
if (this.data.saving) return;
|
||
this.setData({ saving: true });
|
||
try {
|
||
await api.updatePet(id, body);
|
||
await store.loadPets();
|
||
this.triggerEvent('petadded');
|
||
this.close();
|
||
} catch (e) {
|
||
wx.showToast({ title: e.message || '保存失败', icon: 'none' });
|
||
this.setData({ saving: false });
|
||
}
|
||
},
|
||
|
||
onDeletePet() {
|
||
const id = store.currentPetId();
|
||
const name = this.data.pet.name || '这只宠物';
|
||
if (!id) return;
|
||
wx.showModal({
|
||
title: '删除宠物',
|
||
content: `删除「${name}」后,它的记录、计划、提醒都会一并移除,且不可恢复。确定删除吗?`,
|
||
confirmText: '删除',
|
||
confirmColor: '#EE7D73',
|
||
success: (res) => {
|
||
if (!res.confirm) return;
|
||
api
|
||
.deletePet(id)
|
||
.then(async () => {
|
||
await store.loadPets();
|
||
this.triggerEvent('petadded');
|
||
this.close();
|
||
if (store.getPets().length === 0) {
|
||
wx.reLaunch({ url: '/pages/onboarding/onboarding' });
|
||
}
|
||
})
|
||
.catch((e) => wx.showToast({ title: e.message || '删除失败', icon: 'none' }));
|
||
},
|
||
});
|
||
},
|
||
|
||
// 发帖
|
||
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 });
|
||
},
|
||
|
||
},
|
||
});
|