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()); } // 相对时间。社区里关心的是「多久以前发的」,精确到秒没意义 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 天', '幼年期', '成年期', '老年期']; // 任务可关联的记录弹层类型 const TASK_SHEETS = [ { key: '', label: '不关联' }, { key: 'weight', label: '体重' }, { key: 'poop', label: '便便' }, { key: 'food', label: '饮食' }, { key: 'symptom', label: '异常' }, { key: 'vaccine', label: '疫苗' }, { key: 'medicine', label: '用药' }, ]; const TASK_SHEET_LABELS = TASK_SHEETS.map((s) => s.label); // 提醒类型(与后端 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 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: '' }, wInput: '', wNote: '', poopNote: '', foodText: '', medName: '', medDose: '', costNote: '', costAmount: '', postContent: '', postImages: [], manageTasks: [], taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 }, taskSheetLabels: TASK_SHEET_LABELS, remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' }, remTypeLabels: REMINDER_LABELS, myPets: [], exportText: '', fbContent: '', fbContact: '', fbDone: false, addName: '', addBirthday: '', addWeight: '', addColor: '', addBreed: '', commentText: '', riskData: null, comments: [], reminders: [], poster: null, reportDetail: null, proInfo: null, saving: false, posterSaving: false, centered: false, commentImages: [], }, 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, centered: type === 'comments' }; 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.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.riskData = null; patch.saving = false; } 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 }); }, 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({ comments: (page.list || []).map((c) => ({ ...c, timeText: fmtAgo(c.created_at), // 没有头像字段,用昵称首字做个色块,比一律显示同一个 emoji 强 initial: (c.author_name || '?').slice(0, 1), })), }), ) .catch(() => {}); }, // 长按删除自己的评论 onDeleteComment(e) { const c = this.data.comments[e.currentTarget.dataset.index]; if (!c || !c.is_self) return; 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]; let sheetIdx = TASK_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 body = { title, description: f.description, priority: f.priority, sheet_type: TASK_SHEETS[f.sheetIdx].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,失败回退) onGenRisk() { const id = store.currentPetId(); if (!this.optPicked('symp')) { wx.showToast({ title: '先选一下发生了什么', icon: 'none' }); return; } const symptoms = ['呕吐', '拉稀', '精神差', '皮肤红']; const durations = ['刚刚', '半天', '1天以上']; const spirits = ['正常', '一般', '明显变差']; const body = { symptoms: [symptoms[this.optIdx('symp')]], duration: durations[this.segIdx('dur')], spirit: spirits[this.segIdx('spirit')], }; if (!id) { this.setData({ innerType: 'risk', riskData: null }); return; } wx.showLoading({ title: '分析中...' }); api .assessSymptom(id, body) .then((res) => { wx.hideLoading(); this.setData({ innerType: 'risk', riskData: res }); }) .catch(() => { wx.hideLoading(); this.setData({ innerType: 'risk', riskData: null }); }); }, // 依据当前弹层类型组装一条健康记录 buildRecord() { const t = this.data.innerType; 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 'risk': { // 用 AI 返回的真实风险等级,而不是写死「中」 const r = this.data.riskData || {}; const lv = r.risk_level || '中'; const desc = [r.causes, r.suggestion].filter(Boolean).join(';'); return { type: 'symptom', icon: '🤒', title: '异常观察:风险' + lv, 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.setData({ commentText: '', commentImages: [] }); 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' }); }); }, onRemoveCommentImage(e) { const list = this.data.commentImages.slice(); list.splice(e.currentTarget.dataset.index, 1); this.setData({ commentImages: list }); }, }, });