Files
sundynix-pets/pets-fe/components/bottom-sheet/bottom-sheet.js
T
Blizzard 2e3345cdfc feat: 记录删除与筛选、用户侧内容删除、AI 聊天历史
修复雪花 ID 精度丢失(真 bug)
- bottom-sheet 的 postId 声明为 Number,而 ID 是 18 位雪花字符串,
  超出 JS 安全整数范围:339346584095428608 会被转成 ...600,
  评论弹层实际查的是不存在的帖子。改为 String

记录
- 后端 DELETE /records/:id 早已就绪但前端从未调用,记错了删不掉。
  时间轴支持长按删除
- 时间轴加类型筛选(体重/便便/饮食/异常/用药/疫苗/消费/照片),
  后端 ?type= 本就支持

用户侧内容删除(UGC 合规:用户应能删除自己发布的内容)
- 新增 DELETE /posts/:id 与 DELETE /comments/:id,仅限本人,
  越权返回 40300;帖子软删除,评论删除同步递减 comment_count
- 评论列表返回 is_self;社区长按自己的帖子、长按自己的评论可删

AI 聊天历史
- ai_messages 一直在写但没有查询接口,每次打开弹层都是空白。
  新增 GET /api/ai/messages,弹层打开时接在开场白后加载

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 17:42:14 +08:00

780 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() + '日';
}
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: '2026-07-21', deworm: '2026-07-15' },
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: '',
aiInput: '',
aiMessages: [],
riskData: null,
comments: [],
reminders: [],
poster: null,
reportDetail: null,
proInfo: null,
saving: false,
},
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.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.riskData = null;
patch.saving = false;
}
if (type === 'weight') {
patch.wInput = (pet.weight || '').replace('kg', '');
patch.wNote = '';
}
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)) };
}
if (type === 'ai') {
patch.aiMessages = [
{
role: 'ai',
text: `我是 ${pet.name || '毛孩子'} 的养宠助手。我会优先基于档案、计划和历史记录回答,而不是泛泛聊天。`,
},
];
patch.aiInput = '';
}
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 === 'ai') {
this.loadAIHistory();
} 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 });
}
},
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 || [] }))
.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;
},
// 异常观察 → 生成风险评估(真调后端 AI,失败回退)
onGenRisk() {
const id = store.currentPetId();
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 || !id) {
this.close();
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());
},
onSaveWeight() {
let w = (this.data.wInput || '').toString().replace(/\s+/g, '');
if (!w) w = (this.data.pet.weight || '').replace('kg', '');
const num = parseFloat(w) || 0;
const weight = w.includes('kg') ? w : 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 || !this.data.postId) return this.close();
try {
await api.createComment(this.data.postId, text);
this.triggerEvent('commented');
} catch (e) {
wx.showToast({ title: e.message || '评论失败', icon: 'none' });
}
this.close();
},
// AI 聊天
onAiInput(e) { this.setData({ aiInput: e.detail.value }); },
// 拉取历史对话,接在开场白之后;没有历史就保持开场白
loadAIHistory() {
api
.aiMessages('sheet', 30)
.then((list) => {
if (!list || !list.length) return;
const history = list.map((m) => ({ role: m.role === 'user' ? 'user' : 'ai', text: m.text }));
this.setData({ aiMessages: this.data.aiMessages.concat(history) });
})
.catch(() => {});
},
sendAI(e) {
const preset = e.currentTarget.dataset.text;
const text = (preset || this.data.aiInput || '').trim();
if (!text) return;
const msgs = this.data.aiMessages.concat([{ role: 'user', text }]);
this.setData({ aiMessages: msgs, aiInput: '' });
api
.aiChat({ pet_id: store.currentPetId() || null, session: 'sheet', text })
.then((res) => {
this.setData({ aiMessages: this.data.aiMessages.concat([{ role: 'ai', text: res.reply }]) });
})
.catch(() => {
this.setData({
aiMessages: this.data.aiMessages.concat([{ role: 'ai', text: '网络异常,请稍后再试。' }]),
});
});
},
},
});