const store = require('../../utils/store.js'); const api = require('../../utils/api.js'); const { toastErr } = require('../../utils/ui.js'); function pad2(n) { return n < 10 ? '0' + n : '' + n; } function ymd(d) { return d.getFullYear() + '-' + pad2(d.getMonth() + 1) + '-' + pad2(d.getDate()); } // "2026-07-04" → "7月4日" function fmtMD(date) { const p = (date || '').split('-'); return p.length < 3 ? '' : +p[1] + '月' + +p[2] + '日'; } function parseYMD(s) { const p = (s || '').split('-'); return p.length < 3 ? null : new Date(+p[0], +p[1] - 1, +p[2]); } // 计划覆盖的天数。内置模板会生成 day=30 的节点(第 31 天),所以是 31 格不是 30 格; // 按 30 画的话模板自己的最后一个节点在日历上没有格子——看得到、点不到。 // 和后端 service/plan.go 的 planSpanDays 必须一致。 const SPAN = 31; // 只画计划覆盖的这段,不画整月:整月会多出一堆空白格子、跨月还要翻页。 // 按周对齐排 31 天,首尾补几个空格就够,一屏看完。 function buildCells(startStr, byDate, todayStr) { const start = parseYMD(startStr); if (!start) return []; const lead = (start.getDay() + 6) % 7; // 周一为一周起点 const cells = []; for (let i = 0; i < lead; i++) cells.push({ blank: true }); for (let i = 0; i < SPAN; i++) { const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i); const s = ymd(d); cells.push({ day: d.getDate(), date: s, count: (byDate[s] || []).length, today: s === todayStr }); } // 末尾补齐整周,否则最后一行格子宽度会被 grid 拉歪 while (cells.length % 7) cells.push({ blank: true }); return cells; } Page({ data: { pet: {}, plan: null, doneCount: 0, cells: [], calRange: '', planStart: '', planEnd: '', selDate: '', selLabel: '', dayTasks: [], allOpen: false, editShow: false, editId: '', form: { title: '', date: '', description: '' }, saving: false, sheetShow: false, sheetType: '', }, onLoad() { this._unsub = store.subscribe((pet) => { this.setData({ pet }); if (this._inited) this.load(); }); }, onUnload() { if (this._unsub) this._unsub(); }, onShow() { store .ready() .then(() => { this._inited = true; this.setData({ pet: store.getPet() }); this.load(); }) .catch((e) => toastErr(e)); }, load() { const id = store.currentPetId(); if (!id) return; api .getPlan(id) .then((plan) => this.apply(plan)) .catch(() => this.setData({ plan: null, cells: [], dayTasks: [] })); }, apply(plan) { const tasks = (plan.tasks || []).map((t) => ({ ...t, dateLabel: fmtMD(t.date) })); const byDate = {}; tasks.forEach((t) => { if (!t.date) return; (byDate[t.date] = byDate[t.date] || []).push(t); }); const start = (plan.start_date || '').slice(0, 10); const end = start ? ymd(new Date(parseYMD(start).getTime() + (SPAN - 1) * 86400000)) : ''; const todayStr = ymd(new Date()); this._byDate = byDate; // 默认选今天;今天不在计划范围内(计划早就过期了)就选开始那天 const sel = byDate[todayStr] !== undefined || (todayStr >= start && todayStr <= end) ? todayStr : start; this.setData({ plan: { ...plan, tasks }, doneCount: tasks.filter((t) => t.done).length, cells: buildCells(start, byDate, todayStr), calRange: start ? fmtMD(start) + ' — ' + fmtMD(end) : '', planStart: start, planEnd: end, }); this.selectDay(sel, todayStr); }, selectDay(date, todayStr) { const t = todayStr || ymd(new Date()); this.setData({ selDate: date, selLabel: date === t ? '今天的安排' : fmtMD(date) + '的安排', dayTasks: (this._byDate || {})[date] || [], }); }, onTapDay(e) { const d = e.currentTarget.dataset.date; if (d) this.selectDay(d); }, backToToday() { const t = ymd(new Date()); // 今天不在这 30 天里就退回开始日,否则选中一个日历上不存在的格子 this.selectDay(t >= this.data.planStart && t <= this.data.planEnd ? t : this.data.planStart); }, toggleAll() { this.setData({ allOpen: !this.data.allOpen }); }, toggleTask(e) { api .togglePlanTask(e.currentTarget.dataset.id) .then(() => this.load()) .catch((err) => toastErr(err, '操作失败')); }, // ── 加 / 改 ── openAdd() { this.setData({ editShow: true, editId: '', form: { title: '', date: this.data.selDate || this.data.planStart, description: '' }, }); }, openEdit(e) { const t = (this.data.dayTasks || []).find((x) => x.id === e.currentTarget.dataset.id); if (!t) return; this.setData({ editShow: true, editId: t.id, form: { title: t.title, date: t.date || this.data.selDate, description: t.description || '' }, }); }, closeEdit() { this.setData({ editShow: false }); }, noop() {}, onFormTitle(e) { this.setData({ 'form.title': e.detail.value }); }, onFormDate(e) { this.setData({ 'form.date': e.detail.value }); }, onFormDesc(e) { this.setData({ 'form.description': e.detail.value }); }, onSubmit() { if (this.data.saving) return; const f = this.data.form; if (!(f.title || '').trim()) return wx.showToast({ title: '写一下做什么', icon: 'none' }); const id = store.currentPetId(); if (!id) return; this.setData({ saving: true }); const req = this.data.editId ? api.updatePlanTask(this.data.editId, f) : api.addPlanTask(id, f); req .then(() => { this.setData({ saving: false, editShow: false }); // 加完/改完要跳到那一天,不然用户改了日期却看不到东西去哪了 this._pendingSel = f.date; this.load(); }) .catch((e) => { this.setData({ saving: false }); toastErr(e, '保存失败'); }); }, onDelete(e) { const { id, title } = e.currentTarget.dataset; wx.showModal({ title: '删掉这件事?', content: title || '', confirmColor: '#EE7D73', success: (r) => { if (!r.confirm) return; api .deletePlanTask(id) .then(() => this.load()) .catch((err) => toastErr(err, '删除失败')); }, }); }, openSheet(e) { const type = e.currentTarget.dataset.type; if (!type) return; this.setData({ sheetType: type, sheetShow: true }); }, closeSheet() { this.setData({ sheetShow: false }); }, onAddPet() { this.setData({ sheetType: 'addPet', sheetShow: true }); }, onShareAppMessage() { return { title: '我给毛孩子做了份养护计划', path: '/pages/plan/plan' }; }, onShareTimeline() { return { title: '我给毛孩子做了份养护计划' }; }, });