feat: 补齐五块缺失功能(记录丢数据/文章正文/提醒管理/我的页入口/分享/关注)

P0 修数据问题
- 记录弹层有 5 个输入框从未绑定,用户填的便便备注、饮食内容、药品名称、
  剂量、消费备注全部丢弃;用药记录因此只存下「用药记录」四个字。已全部绑定并入库
- 异常观察保存时写死「风险中」,AI 实际判定的等级被丢掉。改为使用 riskData
  的真实 risk_level,并把成因与建议存进备注

文章正文
- 早期 seed 的 4 篇文章 Content 全空,点开只是打开关联记录弹层,内容运营等于白做
- 补写 4 篇真实正文(疫苗驱虫/软便判断/换粮方案/养宠花销),遵循不诊断、
  异常必提就医的护栏;对已存在的空正文做幂等回填
- 新增文章详情页,学习页与首页卡片改为打开正文

提醒管理
- 后端 CRUD 早已具备但小程序只读,用户改不了疫苗驱虫日期。提醒弹层支持增删改
- 修复后端 UpdateReminder 漏掉 type 字段导致改类型不生效

「我的」页 7 个死入口接活
- 提醒设置、健康记录、多宠物管理、会员权益分别落到对应功能
- 数据导出:汇总档案与记录为文本,可复制发给医生
- 意见反馈:新增 feedback 表与提交接口,后台加「意见反馈」管理页
- 关于我们:应用说明与免责声明

分享
- 各页面补 onShareAppMessage/onShareTimeline
- 帖子分享按钮改用微信原生 open-type="share",删除原来点了没反应的空壳弹层

社区关注
- 新增 follows 表与关注/取关接口,「关注」tab 此前等同「推荐」,
  现只显示已关注用户的帖子;帖子头部可直接关注,标记 followed / is_self

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-25 18:11:08 +08:00
parent 11c85da65d
commit 3fde90e97b
42 changed files with 1028 additions and 117 deletions
+216 -18
View File
@@ -32,6 +32,15 @@ const TASK_SHEETS = [
];
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 = ['食品', '医疗', '用品'];
@@ -53,12 +62,24 @@ Component({
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: '',
@@ -100,8 +121,18 @@ Component({
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 = '';
@@ -146,20 +177,14 @@ Component({
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) {
api.listComments(this.data.postId).then((page) => this.setData({ comments: page.list || [] })).catch(() => {});
} else if (type === 'reminders' && id) {
api
.getReminders(id)
.then((list) =>
this.setData({
reminders: (list || []).map((r) => ({
title: r.title,
when: r.next_due_date ? fmtDate(r.next_due_date) : r.frequency || '',
})),
}),
)
.catch(() => {});
this.loadReminders();
} else if (type === 'poster' && id) {
api.getPoster(id).then((p) => this.setData({ poster: p })).catch(() => {});
} else if (type === 'reportDetail' && id) {
@@ -212,8 +237,157 @@ Component({
},
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 }); },
// ---- 多宠物管理 ----
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();
@@ -328,25 +502,49 @@ Component({
switch (t) {
case 'poop': {
const s = POOP[this.segIdx('poopState')];
return { type: 'poop', icon: '💩', title: '排便记录:' + s, category: s };
return {
type: 'poop', icon: '💩', title: '排便记录:' + s, category: s,
description: (this.data.poopNote || '').trim(),
};
}
case 'food': {
const a = FOOD[this.segIdx('food')];
return { type: 'food', icon: '🍽️', title: '饮食记录:食欲' + a };
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 'medicine':
return { type: 'medicine', icon: '💊', title: '用药记录' };
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 };
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':
return { type: 'symptom', icon: '🤒', title: '异常观察:风险中', category: '中' };
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: