35fc57e8de
AI 从底部弹层改成独立页面 pages/ai/ai: - 整屏聊天,输入框固定在底部,不再是「问题都打不完」的小格子 - 回复逐字吐出(30ms/2 字),点一下可跳过 - 会话 session 从 sheet 改为 chat,历史拉 50 条 - 组件里对应的 ai 分支和 aiMessages/aiInput/sendAI 一并删掉, 避免两套实现并存 快速记录九个入口逐个排查,修掉 6 处: 1. 疫苗/驱虫的默认日期写死成 2026-07-21 / 2026-07-15,早已是过去的 日期 —— 改成打开时按今天 +30 / +90 天算 2. 异常观察没选症状时,optIdx 返回 0 会被当成选了「呕吐」,等于替 用户瞎填 —— 加 optPicked 判断,没选就提示 3. 体重为空时会静默存成「上次的体重」,凭空造一条假数据 —— 改为必填校验 4. 消费金额为空会存成 ¥0 —— 必填校验 5. 用药药品名为空 —— 必填校验 6. 照片弹层里「模拟上传预览」是没删掉的假文案 顺带清理:保存按钮上的 data-text / data-icon 从来没被读过(onSave 只用 buildRecord 的返回值),是纯误导的死属性,删掉。我上一条 commit 把它们 说成「写库值」是说错了 —— 真正写库的是 buildRecord 里的 icon 字段,那部分 仍然没动。 导航栏原来是一整条半透明色块,底边和页面渐变硬碰硬,屏幕上横着一道接缝。 改成上实下虚的渐变,并用同一条 mask 把毛玻璃一起淡出(只淡背景不淡 blur 的话,接缝会从色差变成糊边)。返回键做成圆形,和右上角胶囊视觉对称。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
216 lines
6.3 KiB
JavaScript
216 lines
6.3 KiB
JavaScript
const store = require('../../utils/store.js');
|
|
const api = require('../../utils/api.js');
|
|
const { toastErr } = require('../../utils/ui.js');
|
|
|
|
function todayStr() {
|
|
const d = new Date();
|
|
const p = (n) => (n < 10 ? '0' + n : '' + n);
|
|
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate());
|
|
}
|
|
|
|
function mapTask(t) {
|
|
return {
|
|
id: t.id,
|
|
title: t.title,
|
|
sub: t.description,
|
|
priority: t.priority,
|
|
done: t.done,
|
|
sheet: t.sheet_type,
|
|
};
|
|
}
|
|
|
|
// 快速记录入口。颜色按类别分组(体重/疫苗=橙、便便/饮食=绿、异常/用药=蓝、消费/照片=紫),
|
|
// 靠颜色和图标区分,不再靠 emoji。
|
|
const QUICK_ITEMS = [
|
|
{ type: 'weight', icon: 'weight', label: '体重', tone: 'tone-1' },
|
|
{ type: 'poop', icon: 'poop', label: '便便', tone: 'tone-2' },
|
|
{ type: 'food', icon: 'food', label: '饮食', tone: 'tone-2' },
|
|
{ type: 'symptom', icon: 'symptom', label: '异常', tone: 'tone-3' },
|
|
{ type: 'cost', icon: 'cost', label: '消费', tone: 'tone-4' },
|
|
{ type: 'medicine', icon: 'medicine', label: '用药', tone: 'tone-3' },
|
|
{ type: 'photo', icon: 'photo', label: '照片', tone: 'tone-4' },
|
|
{ type: 'vaccine', icon: 'vaccine', label: '疫苗', tone: 'tone-1' },
|
|
];
|
|
|
|
// 门面上的状态 chip:健康状态 + 最多两条洞察
|
|
function heroChips(summary) {
|
|
const chips = [];
|
|
if (summary.health_status) {
|
|
chips.push({ text: summary.health_status, icon: 'check', tone: 'green' });
|
|
}
|
|
(summary.insights || []).slice(0, 2).forEach((it) => {
|
|
if (it && it.bold) chips.push({ text: it.bold, icon: 'trend', tone: '' });
|
|
});
|
|
return chips;
|
|
}
|
|
|
|
Page({
|
|
data: {
|
|
pet: {},
|
|
summary: { greeting: '', insights: [], week: [], advice: '', health_pct: 0, health_status: '正常' },
|
|
tasks: [],
|
|
quickItems: QUICK_ITEMS,
|
|
heroChips: [],
|
|
undoneText: '',
|
|
firstArticle: null,
|
|
selectedDate: '',
|
|
taskLabel: '今日任务',
|
|
todayDate: '',
|
|
sheetShow: false,
|
|
sheetType: '',
|
|
},
|
|
onLoad() {
|
|
this.setData({ todayDate: todayStr() });
|
|
this._unsub = store.subscribe((pet) => {
|
|
this.setData({ pet });
|
|
// 首次由 onShow 统一加载,这里只处理之后的「切换宠物 / 数据变更」,避免重复请求
|
|
if (!this._inited) return;
|
|
this.loadAll();
|
|
});
|
|
},
|
|
onUnload() {
|
|
if (this._unsub) this._unsub();
|
|
},
|
|
onShow() {
|
|
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
|
this.getTabBar().setData({ selected: 0 });
|
|
}
|
|
store
|
|
.ready()
|
|
.then(() => {
|
|
this._inited = true;
|
|
this.setData({ pet: store.getPet() });
|
|
this.loadAll();
|
|
})
|
|
.catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' }));
|
|
},
|
|
loadAll() {
|
|
this.loadSummary();
|
|
this.loadTasks();
|
|
if (!this.data.firstArticle) {
|
|
api
|
|
.listArticles()
|
|
.then((list) => {
|
|
if (list && list[0]) {
|
|
this.setData({ firstArticle: { id: list[0].id, icon: list[0].icon, title: list[0].title, desc: list[0].description } });
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
}
|
|
},
|
|
loadSummary() {
|
|
const id = store.currentPetId();
|
|
if (!id) return;
|
|
api
|
|
.homeSummary(id)
|
|
.then((summary) => this.setData({ summary, heroChips: heroChips(summary) }))
|
|
.catch((e) => toastErr(e));
|
|
},
|
|
loadTasks() {
|
|
const id = store.currentPetId();
|
|
if (!id) return;
|
|
const date = this.data.selectedDate || this.data.todayDate;
|
|
api
|
|
.getTasks(id, date)
|
|
.then((tasks) => this.setTasks((tasks || []).map(mapTask)))
|
|
.catch((e) => toastErr(e));
|
|
},
|
|
// 任务列表落地时顺手算「还有几件事没做完」,门面上要用
|
|
setTasks(tasks) {
|
|
const undone = tasks.filter((t) => !t.done).length;
|
|
this.setData({
|
|
tasks,
|
|
undoneText: tasks.length === 0
|
|
? '这天还没有安排'
|
|
: undone > 0 ? '还有 ' + undone + ' 件事没做完' : '今天的事都做完啦',
|
|
});
|
|
},
|
|
onTapDay(e) {
|
|
const { date, active } = e.currentTarget.dataset;
|
|
if (!date) return;
|
|
const parts = date.split('-');
|
|
this.setData({
|
|
selectedDate: date,
|
|
taskLabel: active ? '今日任务' : +parts[1] + '月' + +parts[2] + '日',
|
|
});
|
|
this.loadTasks();
|
|
},
|
|
backToToday() {
|
|
this.setData({ selectedDate: this.data.todayDate, taskLabel: '今日任务' });
|
|
this.loadTasks();
|
|
},
|
|
onTapTask(e) {
|
|
const i = e.currentTarget.dataset.index;
|
|
const t = this.data.tasks[i];
|
|
if (t.sheet) {
|
|
this.openSheetType(t.sheet);
|
|
return;
|
|
}
|
|
api
|
|
.toggleTask(t.id)
|
|
.then((res) => {
|
|
const tasks = this.data.tasks.slice();
|
|
tasks[i] = Object.assign({}, tasks[i], { done: res.done });
|
|
this.setTasks(tasks);
|
|
this.loadSummary();
|
|
})
|
|
.catch(() => {});
|
|
},
|
|
completeTasks() {
|
|
const id = store.currentPetId();
|
|
if (!id) return;
|
|
api
|
|
.completeAllTasks(id)
|
|
.then((tasks) => {
|
|
this.setTasks((tasks || []).map(mapTask));
|
|
this.loadSummary();
|
|
})
|
|
.catch(() => {});
|
|
},
|
|
openSheet(e) {
|
|
this.openSheetType(e.currentTarget.dataset.type);
|
|
},
|
|
openSheetType(type) {
|
|
this.setData({ sheetType: type, sheetShow: true });
|
|
},
|
|
closeSheet() {
|
|
// 只关闭。数据变更由 bind:saved 或 store 的宠物变更通知触发刷新,
|
|
// 不必每次关弹层(哪怕只是看了眼 AI)都全量重拉一遍
|
|
this.setData({ sheetShow: false });
|
|
},
|
|
onSheetSave() {
|
|
this.loadTasks();
|
|
this.loadSummary();
|
|
},
|
|
onAddPet() {
|
|
this.openSheetType('addPet');
|
|
},
|
|
onEditPet() {
|
|
this.openSheetType('editPet');
|
|
},
|
|
onFab() {
|
|
wx.navigateTo({ url: '/pages/ai/ai' });
|
|
},
|
|
goProfile() {
|
|
wx.navigateTo({ url: '/pages/profile/profile' });
|
|
},
|
|
goLearn() {
|
|
wx.navigateTo({ url: '/pages/learn/learn' });
|
|
},
|
|
// 首页那张卡片直接打开这篇文章正文
|
|
openFirstArticle() {
|
|
const a = this.data.firstArticle;
|
|
if (a && a.id) wx.navigateTo({ url: `/pages/article/article?id=${a.id}` });
|
|
else this.goLearn();
|
|
},
|
|
goRecord() {
|
|
wx.switchTab({ url: '/pages/record/record' });
|
|
},
|
|
onShareAppMessage() {
|
|
return { title: '肉垫计划 · 每天照顾好毛孩子', path: '/pages/home/home' };
|
|
},
|
|
onShareTimeline() {
|
|
return { title: '肉垫计划 · 每天照顾好毛孩子' };
|
|
},
|
|
});
|