diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.js b/pets-fe/components/bottom-sheet/bottom-sheet.js index 898eefe..6694adc 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.js +++ b/pets-fe/components/bottom-sheet/bottom-sheet.js @@ -1,5 +1,4 @@ const store = require('../../utils/store.js'); -const recordTypes = require('../../utils/recordTypes.js'); const api = require('../../utils/api.js'); const upload = require('../../utils/upload.js'); @@ -33,16 +32,6 @@ function fmtAgo(iso) { return (sameYear ? '' : d.getFullYear() + '-') + p(d.getMonth() + 1) + '-' + p(d.getDate()); } -// 任务可关联的记录弹层类型 -// 任务能关联的记录类型。原来这里也是写死的 6 种,后台加了类型这个下拉里看不到, -// 于是「自定义任务关联新类型」这条路是断的。改成读同一份类型缓存 -function taskSheets() { - const list = [{ key: '', label: '不关联' }]; - (recordTypes.groups() || []).forEach((g) => - g.items.forEach((t) => list.push({ key: t.code, label: t.label })), - ); - return list; -} // 提醒类型(与后端 model.Reminder* 常量对应) const REMINDER_TYPES = [ @@ -73,9 +62,6 @@ Component({ // 通用简易记录(洗护/清洁那 15 种共用) postContent: '', postImages: [], - manageTasks: [], - taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 }, - taskSheetLabels: [], remForm: { id: '', typeIdx: 0, title: '', date: '', freq: '' }, remTypeLabels: REMINDER_LABELS, myPets: [], @@ -122,9 +108,6 @@ Component({ patch.postContent = ''; patch.postImages = []; patch.medDose = ''; - patch.manageTasks = []; - patch.taskForm = { id: '', title: '', description: '', priority: '', sheetIdx: 0 }; - patch.taskSheetLabels = taskSheets().map((x) => x.label); patch.remForm = { id: '', typeIdx: 0, title: '', date: '', freq: '' }; patch.exportText = ''; patch.fbContent = ''; @@ -527,49 +510,6 @@ Component({ if (!id) return; api.getTasks(id).then((tasks) => this.setData({ manageTasks: tasks || [] })).catch(() => {}); }, - onTaskTitle(e) { this.setData({ 'taskForm.title': e.detail.value }); }, - onTaskDesc(e) { this.setData({ 'taskForm.description': e.detail.value }); }, - onTaskPriority() { - this.setData({ 'taskForm.priority': this.data.taskForm.priority === '重要' ? '' : '重要' }); - }, - onTaskSheet(e) { this.setData({ 'taskForm.sheetIdx': Number(e.detail.value) }); }, - onEditTaskItem(e) { - const t = this.data.manageTasks[e.currentTarget.dataset.index]; - const sheets = taskSheets(); - let sheetIdx = sheets.findIndex((s) => s.key === (t.sheet_type || '')); - if (sheetIdx < 0) sheetIdx = 0; - this.setData({ - taskForm: { id: t.id, title: t.title, description: t.description || '', priority: t.priority || '', sheetIdx }, - }); - }, - onSaveTask() { - const f = this.data.taskForm; - const title = (f.title || '').trim(); - if (!title) return wx.showToast({ title: '填个任务名', icon: 'none' }); - const sheets = taskSheets(); - const body = { title, description: f.description, priority: f.priority, sheet_type: (sheets[f.sheetIdx] || sheets[0]).key }; - const id = store.currentPetId(); - const p = f.id ? api.updateTask(f.id, body) : api.createTask(id, body); - p.then(() => { - this.setData({ taskForm: { id: '', title: '', description: '', priority: '', sheetIdx: 0 } }); - this.loadManageTasks(); - this.triggerEvent('saved'); - }).catch((e) => wx.showToast({ title: e.message || '保存失败', icon: 'none' })); - }, - onDeleteTaskItem(e) { - const t = this.data.manageTasks[e.currentTarget.dataset.index]; - wx.showModal({ - title: '删除任务', - content: '确定删除「' + t.title + '」?', - success: (r) => { - if (!r.confirm) return; - api.deleteTask(t.id).then(() => { - this.loadManageTasks(); - this.triggerEvent('saved'); - }).catch((err) => wx.showToast({ title: err.message || '删除失败', icon: 'none' })); - }, - }); - }, onPickPostImages() { const left = 9 - this.data.postImages.length; if (left <= 0) return wx.showToast({ title: '最多 9 张', icon: 'none' }); diff --git a/pets-fe/components/bottom-sheet/bottom-sheet.wxml b/pets-fe/components/bottom-sheet/bottom-sheet.wxml index 8940a1c..d380e18 100644 --- a/pets-fe/components/bottom-sheet/bottom-sheet.wxml +++ b/pets-fe/components/bottom-sheet/bottom-sheet.wxml @@ -218,37 +218,8 @@ module.exports.sel = function (map, key, index, def) { - - - 管理任务 - 增删改今日任务,改动会自动顺延到以后每天。 - - - {{item.title}}{{item.priority}} - {{item.description}} - - - - - 今天还没有任务,下面加一条吧。 - - - - - - - 重要 - - 关联:{{taskSheetLabels[taskForm.sheetIdx]}} - - - - - - - - - + 多宠物管理 点一只切换为当前宠物,或编辑它的档案。 diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js index 8b09272..52b4b40 100644 --- a/pets-fe/pages/home/home.js +++ b/pets-fe/pages/home/home.js @@ -34,13 +34,12 @@ Page({ sexTone: '', tasks: [], planHint: '', + drift: {}, needNextPlan: false, monthCost: 0, timeline: [], tlPage: 1, hasMore: false, - sheetShow: false, - sheetType: '', }, onLoad() { this._unsub = store.subscribe((pet) => { @@ -80,6 +79,7 @@ Page({ this.loadTasks(); this.loadTimeline(1); this.loadEntries(); + this.loadDrift(); }, loadTasks() { const id = store.currentPetId(); @@ -91,6 +91,16 @@ Page({ }, // 两个入口上的副标题:计划还剩几件、本月花了多少。 // 空着的话入口就是两个没有信息量的图标 + // 阶段漂移检查。放在 loadEntries 里而不是单独一次 —— 它和「计划还剩几件」 + // 都是首页那一屏要的,一起拉省一个来回 + loadDrift() { + const id = store.currentPetId(); + if (!id) return; + api + .petStageDrift(id) + .then((d) => this.setData({ drift: d || {} })) + .catch(() => this.setData({ drift: {} })); + }, loadEntries() { const id = store.currentPetId(); if (!id) return; @@ -187,6 +197,12 @@ Page({ goPlan() { wx.navigateTo({ url: '/pages/plan/plan' }); }, + // 去建档页改阶段。改完回来 onShow 会重新拉一次漂移状态,提示自然消失。 + // 不在首页直接改:改阶段之后要不要套新方案是同一件事,放在一起做才连贯 + goStageUpdate() { + const id = this.data.pet && this.data.pet.id; + if (id) wx.navigateTo({ url: '/pages/petform/petform?id=' + id }); + }, goExpense() { wx.navigateTo({ url: '/pages/expense/expense' }); }, @@ -196,16 +212,6 @@ Page({ onQuickRecord() { wx.navigateTo({ url: '/pages/record/record' }); }, - openSheet(e) { - this.setData({ sheetType: e.currentTarget.dataset.type, sheetShow: true }); - }, - closeSheet() { - this.setData({ sheetShow: false }); - }, - onSheetSave() { - this.loadTasks(); - this.loadTimeline(1); - }, onAddPet() { wx.navigateTo({ url: '/pages/petform/petform' }); }, diff --git a/pets-fe/pages/home/home.json b/pets-fe/pages/home/home.json index cfab932..d173c86 100644 --- a/pets-fe/pages/home/home.json +++ b/pets-fe/pages/home/home.json @@ -2,7 +2,6 @@ "usingComponents": { "nav-bar": "/components/nav-bar/nav-bar", "pet-switch": "/components/pet-switch/pet-switch", - "bottom-sheet": "/components/bottom-sheet/bottom-sheet", "fab": "/components/fab/fab", "pt-icon": "/components/pt-icon/index" } diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml index 12087b9..7465beb 100644 --- a/pets-fe/pages/home/home.wxml +++ b/pets-fe/pages/home/home.wxml @@ -26,6 +26,19 @@ 我们一起生活的第 {{daysTogether}} + + + + + {{pet.name}}已经 {{drift.age_label}},从{{drift.stored}}进入{{drift.computed}}了 + + {{drift.basis}} + 这个阶段的重点:{{drift.focus}} + {{drift.size}} + 去更新阶段和方案 + + @@ -49,8 +62,9 @@ 今日任务 + - 管理 + 管理 全部完成 @@ -96,4 +110,3 @@ - diff --git a/pets-fe/pages/home/home.wxss b/pets-fe/pages/home/home.wxss index 6e76d1f..c150d12 100644 --- a/pets-fe/pages/home/home.wxss +++ b/pets-fe/pages/home/home.wxss @@ -78,3 +78,12 @@ border-radius:50%;background:var(--red);border:3rpx solid #fff; } .entry-p.warn{color:var(--red-ink);font-weight:var(--fw-b)} + +/* 阶段漂移提示。用主色不用红色:这是「可以做一下」不是「出问题了」 */ +.drift-card{border:2rpx solid var(--primary-line);background:var(--primary-soft)} +.drift-top{display:flex;align-items:flex-start;gap:var(--sp-2);color:var(--primary-dark)} +.drift-b{flex:1;min-width:0;font-size:var(--fs-md);font-weight:var(--fw-b);line-height:1.5} +.drift-p{font-size:var(--fs-sm);color:var(--primary-ink);line-height:1.6;margin-top:var(--sp-2)} +.drift-focus{font-size:var(--fs-sm);color:var(--primary-ink);line-height:1.6;margin-top:6rpx} +.drift-size{font-size:var(--fs-cap);color:var(--primary-dark);opacity:.8;margin-top:6rpx} +.drift-cta{margin-top:var(--sp-4)} diff --git a/pets-fe/pages/petform/petform.js b/pets-fe/pages/petform/petform.js index 23cb382..0137fc7 100644 --- a/pets-fe/pages/petform/petform.js +++ b/pets-fe/pages/petform/petform.js @@ -41,7 +41,7 @@ Page({ editing: false, petId: '', step: 0, - steps: ['名字', '品种', '资料'], + steps: ['名字', '品种', '资料', '方案'], genders: GENDERS, stages: [], stageRules: [], @@ -54,7 +54,11 @@ Page({ species: 'cat', breed: '', gender: '不确定', birthday: '', arrived_at: '', weight: '', color: '', stage: '', + // 第四步挑的方案。'' 是「先空着自己排」,不是「没选」—— + // 所以初始值给 null,用来区分「还没走到第四步」 + template_id: null, }, + tplOptions: [], breedShow: false, breedGroups: [], breedAnchor: '', @@ -192,6 +196,25 @@ Page({ this.setData({ breedAnchor: 'bi-' + e.currentTarget.dataset.i }); }, + loadTemplates() { + const f = this.data.form; + api + .careTemplates(f.species, f.stage) + .then((opts) => { + const list = opts || []; + this.setData({ + tplOptions: list, + // 默认选第一套:多数用户是新手,给一个合理默认比让他面对空白好。 + // 但「先空着」就在下面,一眼能看到,不是藏起来的 + 'form.template_id': list.length ? list[0].id : '', + }); + }) + .catch(() => this.setData({ tplOptions: [], 'form.template_id': '' })); + }, + pickTemplate(e) { + this.setData({ 'form.template_id': e.currentTarget.dataset.id }); + }, + // ── 分步 ── prev() { if (this.data.step > 0) this.setData({ step: this.data.step - 1 }); @@ -202,6 +225,7 @@ Page({ const f = this.data.form; if (this.data.step === 0 && !f.name.trim()) return '先给它起个名字'; if (this.data.step === 1 && !f.species) return '选一下是猫还是狗'; + if (this.data.step === 2 && !f.stage) return '选一下现在是哪个阶段 —— 方案是按阶段配的'; return ''; }, next() { @@ -209,7 +233,12 @@ Page({ if (!this.data.editing) { const err = this.stepError(); if (err) return wx.showToast({ title: err, icon: 'none' }); - if (this.data.step < 2) return this.setData({ step: this.data.step + 1 }); + if (this.data.step < 3) { + const next = this.data.step + 1; + this.setData({ step: next }); + if (next === 3) this.loadTemplates(); // 方案按阶段取,得等第三步填完 + return; + } } else if (!this.data.form.name.trim()) { return wx.showToast({ title: '名字不能空', icon: 'none' }); } @@ -239,8 +268,15 @@ Page({ submit() { this.setData({ saving: true }); const b = this.body(); + const tplID = this.data.form.template_id; const req = this.data.editing ? api.updatePet(this.data.petId, b) : api.createPet(b); req + // 套方案要等档案建出来才有 pet id。套用失败不算建档失败—— + // 档案已经建好了,方案之后在计划页还能套,不该把用户退回表单 + .then((pet) => { + if (this.data.editing || !tplID || !pet || !pet.id) return; + return api.applyTemplate(pet.id, tplID, 0).catch(() => {}); + }) .then(() => store.loadPets()) .then(() => { this.setData({ saving: false }); diff --git a/pets-fe/pages/petform/petform.wxml b/pets-fe/pages/petform/petform.wxml index d3e75a2..cd65a06 100644 --- a/pets-fe/pages/petform/petform.wxml +++ b/pets-fe/pages/petform/petform.wxml @@ -86,6 +86,43 @@ + + + + + 要不要套一套养护方案? + 按 {{form.stage}} 配的,套用后会排进本月的计划。之后随时能改、能删。 + + + + {{item.name}} + + + {{item.summary}} + + 每天 {{item.daily}} 项 + 每周 {{item.weekly}} 项 + 每月 {{item.monthly}} 项 + 一次性 {{item.once}} 项 + + + + + + + 先空着,我自己排 + + + 之后在「管理计划」里随时能套用 + + + 这个阶段还没有配方案,建完档可以自己排 + 疫苗和驱虫提醒不管选哪个都会建 —— 漏针是真会出事的 + + @@ -93,7 +130,7 @@ 上一步 - {{saving ? '保存中…' : (editing ? '保存' : (step < 2 ? '下一步' : '完成建档'))}} + {{saving ? '保存中…' : (editing ? '保存' : (step < 3 ? '下一步' : '完成建档'))}} diff --git a/pets-fe/pages/petform/petform.wxss b/pets-fe/pages/petform/petform.wxss index f61c336..b486f03 100644 --- a/pets-fe/pages/petform/petform.wxss +++ b/pets-fe/pages/petform/petform.wxss @@ -91,3 +91,18 @@ border-radius:var(--r-sm);padding:var(--sp-2) var(--sp-3); font-size:var(--fs-cap);line-height:1.6; } + +/* 第四步:方案卡 */ +.pf-sub{font-size:var(--fs-sm);color:var(--muted);line-height:1.6;margin:calc(-1 * var(--sp-2)) 0 var(--sp-4)} +.pf-tpl{ + border:2rpx solid var(--line);border-radius:var(--r-md); + padding:var(--sp-4);margin-bottom:var(--sp-3);background:var(--surface-2); +} +.pf-tpl.on{border-color:var(--primary);background:var(--primary-soft)} +.pf-tpl-top{display:flex;align-items:center;gap:var(--sp-2)} +.pf-tpl-name{flex:1;font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text)} +.pf-tpl-tick{flex:none;color:var(--primary-dark);display:flex;align-items:center} +.pf-tpl-sum{font-size:var(--fs-sm);color:var(--text-2);margin-top:6rpx;line-height:1.5} +.pf-tpl-meta{display:flex;flex-wrap:wrap;gap:var(--sp-3);margin-top:var(--sp-2); + font-size:var(--fs-cap);color:var(--muted)} +.pf-tpl-note{margin-top:var(--sp-2)} diff --git a/pets-fe/pages/plan/plan.js b/pets-fe/pages/plan/plan.js index fb9a782..ba450dd 100644 --- a/pets-fe/pages/plan/plan.js +++ b/pets-fe/pages/plan/plan.js @@ -55,6 +55,9 @@ Page({ monthLabel: '', monthEndLabel: '', nextCount: 0, + monthEmpty: false, + tplOptions: [], + tplShow: false, pickStart: '', pickEnd: '', selDate: '', @@ -89,6 +92,7 @@ Page({ load() { const id = store.currentPetId(); if (!id) return; + this.loadTemplates(); api .getPlan(id) .then((plan) => this.apply(plan)) @@ -127,11 +131,50 @@ Page({ cells: buildCells(m.year, m.month, byDate, todayStr), nextCount: Object.keys(byDate).filter((d) => d.indexOf(monthOf(1).year + '-' + pad2(monthOf(1).month)) === 0) .reduce((n, d) => n + byDate[d].length, 0), + // 当前看的这个月一条都没有 —— 空月份要给套方案的入口 + monthEmpty: !Object.keys(byDate).some((d) => d.indexOf(prefix) === 0), }); // 切到本月默认选今天;切到下月默认选 1 号(今天不在那个月里) const inMonth = todayStr.indexOf(prefix) === 0; this.selectDay(inMonth ? todayStr : prefix + '-01', todayStr); }, + // 方案按宠物当前阶段取。宠物换了(pet-switch)阶段也可能不同,所以跟着 load 走 + loadTemplates() { + const pet = this.data.pet || {}; + if (!pet.stage) return; + api + .careTemplates(pet.type === '狗狗' ? 'dog' : 'cat', pet.stage) + .then((opts) => this.setData({ tplOptions: opts || [] })) + .catch(() => this.setData({ tplOptions: [] })); + }, + openTpl() { + this.setData({ tplShow: true }); + }, + closeTpl() { + this.setData({ tplShow: false }); + }, + applyTpl(e) { + const id = e.currentTarget.dataset.id; + const petID = store.currentPetId(); + if (!petID || !id) return; + wx.showLoading({ title: '排进去…' }); + api + .applyTemplate(petID, id, this.data.monthOff) + .then((res) => { + wx.hideLoading(); + this.setData({ tplShow: false }); + const r = res || {}; + wx.showToast({ + title: r.plans > 0 ? '排了 ' + r.plans + ' 件事' : '这个月已经有这些安排了', + icon: 'none', + }); + this.load(); + }) + .catch((err) => { + wx.hideLoading(); + toastErr(err, '套用失败'); + }); + }, switchMonth(e) { this.renderMonth(Number(e.currentTarget.dataset.off)); }, diff --git a/pets-fe/pages/plan/plan.wxml b/pets-fe/pages/plan/plan.wxml index ef7680d..30393e3 100644 --- a/pets-fe/pages/plan/plan.wxml +++ b/pets-fe/pages/plan/plan.wxml @@ -23,8 +23,12 @@ 下月 - - {{monthLabel}}还没有安排,点某天加几件 + + + + {{monthLabel}}还没有安排 + 套一套方案 @@ -38,7 +42,10 @@ {{selLabel}} - + 加一件 + + 套方案 + + 加一件 + @@ -88,4 +95,23 @@ + + + + 套一套方案到{{monthLabel}} + 按 {{pet.stage}} 配的。套用是「往里加」,不会覆盖你已经排好的。 + + {{item.name}} + {{item.summary}} + + 每天 {{item.daily}} 项 + 每周 {{item.weekly}} 项 + 每月 {{item.monthly}} 项 + 一次性 {{item.once}} 项 + + + 这个阶段还没有配方案 + + + diff --git a/pets-fe/pages/plan/plan.wxss b/pets-fe/pages/plan/plan.wxss index 56de678..d320448 100644 --- a/pets-fe/pages/plan/plan.wxss +++ b/pets-fe/pages/plan/plan.wxss @@ -54,3 +54,17 @@ .pe-daily{display:flex;align-items:center;gap:var(--sp-4)} .pe-daily-main{flex:1;min-width:0} + +/* 空月份的提示条:一行放不下「文案 + 按钮」,改成 flex */ +.plan-tip-b{flex:1;min-width:0} + +/* 套方案浮层 */ +.pt-sub{margin:calc(-1 * var(--sp-2)) 0 var(--sp-4);text-align:center} +.pt-item{ + border:2rpx solid var(--line);border-radius:var(--r-md); + padding:var(--sp-4);margin-bottom:var(--sp-3);background:#fff; +} +.pt-name{font-size:var(--fs-md);font-weight:var(--fw-b);color:var(--text)} +.pt-sum{font-size:var(--fs-sm);color:var(--text-2);margin-top:6rpx;line-height:1.5} +.pt-meta{display:flex;flex-wrap:wrap;gap:var(--sp-3);margin-top:var(--sp-2); + font-size:var(--fs-cap);color:var(--muted)} diff --git a/pets-fe/utils/api.js b/pets-fe/utils/api.js index 3fcf444..1b7bf97 100644 --- a/pets-fe/utils/api.js +++ b/pets-fe/utils/api.js @@ -54,6 +54,13 @@ const api = { getPets: () => request({ url: '/api/pets' }), homeSummary: (id) => request({ url: `/api/pets/${id}/home-summary` }), getPet: (id) => request({ url: `/api/pets/${id}` }), + // 养护方案:可选列表 + 套用到某个月 + careTemplates: (species, stage) => + request({ url: '/api/care-templates?species=' + species + '&stage=' + encodeURIComponent(stage) }), + applyTemplate: (petId, templateID, monthOffset) => + request({ url: `/api/pets/${petId}/apply-template`, method: 'POST', + data: { template_id: templateID, month_offset: monthOffset || 0 } }), + // 生命阶段的划分标准。判定和展示用同一份数据 —— 不另写一份说明文案, // 否则改了阈值忘了改文案,用户看到的依据和实际行为对不上 lifeStages: (species, size) =>