const store = require('../../utils/store.js'); const api = require('../../utils/api.js'); const { toastErr } = require('../../utils/ui.js'); const { syncTabBar } = require('../../utils/tabbar.js'); const recordTypes = require('../../utils/recordTypes.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 pad2(n) { return n < 10 ? '0' + n : '' + n; } // 整月网格。周一为一周起点,前面补空格子对齐星期。 // 与计划页原来的实现同源——计划页的日历 tab 已并到这里,那边删掉了 function buildCells(year, month, taskedDays, today) { const first = new Date(year, month - 1, 1); const lead = (first.getDay() + 6) % 7; const daysInMonth = new Date(year, month, 0).getDate(); const set = {}; (taskedDays || []).forEach((d) => (set[d] = true)); const cells = []; for (let i = 0; i < lead; i++) cells.push({ day: 0 }); for (let d = 1; d <= daysInMonth; d++) { cells.push({ day: d, date: year + '-' + pad2(month) + '-' + pad2(d), tasked: !!set[d], today: d === today }); } return cells; } // 「接下来」用的相对日期,比 7月31日 更好读 function whenLabel(date, today) { if (!date) return ''; const diff = Math.round((new Date(date + 'T00:00:00') - new Date(today + 'T00:00:00')) / 86400000); if (diff <= 0) return '今天'; if (diff === 1) return '明天'; if (diff === 2) return '后天'; if (diff < 7) return diff + ' 天后'; const p = date.split('-'); return +p[1] + '月' + +p[2] + '日'; } function mapTask(t) { return { id: t.id, title: t.title, sub: t.description, priority: t.priority, done: t.done, sheet: t.sheet_type, }; } // 快速记录入口取后端类型表的前 8 个(按分组顺序再按 sort)。 // 原来这里是 8 项写死的,后台加一种类型首页看不到;现在调 sort 就能换首屏露出哪几个。 // 首页只放 8 个是版式限制(4 列 2 行),全部 24 种在记录页 function quickFrom(groups) { const flat = []; (groups || []).forEach((g) => g.items.forEach((t) => flat.push(t))); return flat.slice(0, 8); } // 门面上的状态 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: [], heroChips: [], undoneText: '', firstArticle: null, selectedDate: '', taskLabel: '今日任务', todayDate: '', calOpen: false, calCells: [], calLabel: '', dayNodes: [], dayReminders: [], upcoming: [], planPct: -1, 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() { syncTabBar(this); this.loadTypes(); store .ready() .then(() => { this._inited = true; this.setData({ pet: store.getPet() }); this.loadAll(); }) .catch((e) => wx.showToast({ title: e.message || '加载失败', icon: 'none' })); }, loadTypes() { if (this.data.quickItems.length) return; recordTypes .load() .then((groups) => this.setData({ quickItems: quickFrom(groups) })) .catch(() => {}); }, loadAll() { this.loadSummary(); this.loadTasks(); this.loadDayExtra(); this.loadUpcoming(); // 日历只在展开过之后才跟着刷新,没展开就别白拉一次 if (this.data.calOpen) this.loadCalendar(); 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 + ' 件事没做完' : '今天的事都做完啦', }); }, // 这天挂着的计划节点和到期提醒。任务列表仍走 /tasks(那边会补生成当天任务), // day-plan 只取 tasks 之外的两类,不拿它的 tasks 覆盖列表 loadDayExtra() { const id = store.currentPetId(); if (!id) return; const date = this.data.selectedDate || this.data.todayDate; api .dayPlan(id, date) .then((res) => { this.setData({ dayNodes: (res.plan_nodes || []).map((n) => ({ title: n.title, done: n.done })), dayReminders: (res.reminders || []).map((r) => ({ title: r.title })), }); }) .catch(() => this.setData({ dayNodes: [], dayReminders: [] })); }, // 30 天计划里最近 3 个还没做的节点 loadUpcoming() { const id = store.currentPetId(); if (!id) return; const today = this.data.todayDate; api .getPlan(id) .then((plan) => { const list = ((plan && plan.tasks) || []) .filter((t) => !t.done && t.date && t.date >= today) .sort((a, b) => (a.date < b.date ? -1 : 1)) .slice(0, 3) .map((t) => ({ id: t.id, title: t.title, description: t.description, whenLabel: whenLabel(t.date, today) })); this.setData({ upcoming: list, planPct: plan ? plan.completion_pct || 0 : -1 }); }) .catch(() => this.setData({ upcoming: [], planPct: -1 })); }, loadCalendar() { const id = store.currentPetId(); if (!id) return; api .planCalendar(id) .then((res) => { this.setData({ calCells: buildCells(res.year, res.month, res.tasked_days, res.today), calLabel: `${res.year} 年 ${res.month} 月`, }); }) .catch(() => {}); }, toggleCal() { const open = !this.data.calOpen; this.setData({ calOpen: open }); if (open && !this.data.calCells.length) this.loadCalendar(); }, 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(); this.loadDayExtra(); }, backToToday() { this.setData({ selectedDate: this.data.todayDate, taskLabel: '今日任务' }); this.loadTasks(); this.loadDayExtra(); }, goPlan() { wx.navigateTo({ url: '/pages/plan/plan' }); }, 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((e) => toastErr(e, '操作失败')); }, completeTasks() { const id = store.currentPetId(); if (!id) return; api .completeAllTasks(id) .then((tasks) => { this.setTasks((tasks || []).map(mapTask)); this.loadSummary(); }) .catch((e) => toastErr(e, '操作失败')); }, 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(); this.loadDayExtra(); if (this.data.calOpen) this.loadCalendar(); // 加了任务,格子上的圆点要跟着变 }, onAddPet() { this.openSheetType('addPet'); }, // 没建档时点门面卡应该去「添加」,而不是打开一个空的编辑表单 onHeroTap() { if (this.data.pet && this.data.pet.id) this.openSheetType('editPet'); else this.openSheetType('addPet'); }, onEditPet() { this.openSheetType('editPet'); }, onFab() { wx.navigateTo({ url: '/pages/ai/ai' }); }, goSettings() { wx.navigateTo({ url: '/pages/settings/settings' }); }, 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: '肉垫计划 · 每天照顾好毛孩子' }; }, });