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 @@
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) =>