Files
sundynix-pets/pets-fe/pages/home/home.js
T
Blizzard 35fc57e8de feat(fe): AI 独立成页(打字机)+ 修快速记录 6 处问题 + 导航栏去接缝
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>
2026-07-29 11:10:41 +08:00

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: '肉垫计划 · 每天照顾好毛孩子' };
},
});