diff --git a/pets-be/internal/handler/plan.go b/pets-be/internal/handler/plan.go
index cd91000..c55caaa 100644
--- a/pets-be/internal/handler/plan.go
+++ b/pets-be/internal/handler/plan.go
@@ -188,11 +188,12 @@ type planTaskReq struct {
Title string `json:"title"`
Description string `json:"description"`
SheetType string `json:"sheet_type"`
+ Daily bool `json:"daily"`
}
func (r planTaskReq) input() service.PlanTaskInput {
return service.PlanTaskInput{Date: r.Date, Title: r.Title,
- Description: r.Description, SheetType: r.SheetType}
+ Description: r.Description, SheetType: r.SheetType, Daily: r.Daily}
}
// AddPlanTask POST /api/pets/:id/plan-tasks 往 30 天计划里加一条
@@ -202,12 +203,13 @@ func (h *Handler) AddPlanTask(c *gin.Context) {
response.FailParams(c, err.Error())
return
}
- t, err := h.svc.AddPlanTask(middleware.UserID(c), idParam(c, "id"), req.input())
+ list, err := h.svc.AddPlanTask(middleware.UserID(c), idParam(c, "id"), req.input())
if err != nil {
respondErr(c, err)
return
}
- response.OK(c, t)
+ // daily 会一次建多条,统一返回数组,前端不用分两种情况处理
+ response.OK(c, list)
}
// UpdatePlanTask PUT /api/plan-tasks/:id
@@ -232,3 +234,14 @@ func (h *Handler) DeletePlanTask(c *gin.Context) {
}
response.OK(c, nil)
}
+
+// PlanMonthStatus GET /api/pets/:id/plan/month-status
+// 首页「管理计划」那个提醒角标要的:本月剩几件、下月排了没有
+func (h *Handler) PlanMonthStatus(c *gin.Context) {
+ st, err := h.svc.PlanMonthStatus(middleware.UserID(c), idParam(c, "id"))
+ if err != nil {
+ respondErr(c, err)
+ return
+ }
+ response.OK(c, st)
+}
diff --git a/pets-be/internal/router/router.go b/pets-be/internal/router/router.go
index f3dbcf5..135aad0 100644
--- a/pets-be/internal/router/router.go
+++ b/pets-be/internal/router/router.go
@@ -75,6 +75,7 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage
g.GET("/pets/:id/plan", h.GetPlan)
g.GET("/pets/:id/plan/calendar", h.PlanCalendar)
+ g.GET("/pets/:id/plan/month-status", h.PlanMonthStatus)
g.GET("/pets/:id/day-plan", h.DayPlan)
g.POST("/pets/:id/ai-plan", h.CreateAIPlan)
g.POST("/ai-plan/:id/apply", h.ApplyAIPlan)
diff --git a/pets-be/internal/service/plan.go b/pets-be/internal/service/plan.go
index 6d9da80..c99ad93 100644
--- a/pets-be/internal/service/plan.go
+++ b/pets-be/internal/service/plan.go
@@ -283,6 +283,10 @@ type PlanTaskInput struct {
Title string
Description string
SheetType string // 关联的记录类型 code,可空
+ // Daily 应用到每天:从 Date 起到窗口末尾,一天建一条。
+ // 不做「一条 + repeat 字段读时展开」是因为勾选状态是按天的——
+ // 那样得再开一张表记「哪天打过勾」,而直接建 N 条天然就有这个能力。
+ Daily bool
}
// planOf 取这只宠物当前的 30 天计划,顺带做归属校验
@@ -300,13 +304,26 @@ func (s *Service) planOf(userID, petID string) (*model.Plan, error) {
return &plan, nil
}
-// planSpanDays 计划覆盖到第几天。内置模板会生成 day=30 的节点(第 31 天),
-// 所以窗口是 [0, 30] 而不是 [0, 29]——按 29 卡的话模板自己生成的最后一个节点
-// 就落在窗口外:日历上没有那一格,用户看得到却改不动也删不掉。
-const planSpanDays = 30
+// 计划以「自然月」为单位管理。
+//
+// 不用「从计划开始日起 30 天」那种滚动窗口:计划可能是月中建的,那样窗口会
+// 跨两个月,用户看着一个 7月26日—8月25日 的日历,既不像本月也不像下月,
+// 「这个月还有什么没做」根本对不上。按自然月切,日历就是一眼能认的月历。
+//
+// 允许安排到下月末,否则「做下月计划」这件事本身做不了。
-// dayOffset 把日期换算成计划里的 Day。只接受计划开始日起 planSpanDays 天内——
-// 「只做 30 天内的计划」是产品定的边界,超出的话日历上根本没有那一格
+// monthBounds 某个月的首尾。off=0 本月,off=1 下月
+func monthBounds(off int) (time.Time, time.Time) {
+ n := time.Now()
+ first := time.Date(n.Year(), n.Month(), 1, 0, 0, 0, 0, time.Local).AddDate(0, off, 0)
+ return first, first.AddDate(0, 1, -1)
+}
+
+// dayOffset 把日期换算成 PlanTask.Day(相对计划开始日)。
+//
+// 校验按**日期**而不是 Day 上限:Day 是相对计划开始日的,月份边界是相对今天的,
+// 两者会错开。按 Day<=30 卡的话,计划建了几天之后下月末就换算成 Day=40 多,
+// 用户在日历上明明看得到那格、点进去却存不了。
func dayOffset(plan *model.Plan, dateStr string) (int, error) {
if plan.StartDate == nil {
return 0, errors.New("计划还没有开始日期")
@@ -315,18 +332,24 @@ func dayOffset(plan *model.Plan, dateStr string) (int, error) {
if err != nil {
return 0, ErrInvalidParam
}
- start := time.Date(plan.StartDate.Year(), plan.StartDate.Month(), plan.StartDate.Day(),
+ st := time.Date(plan.StartDate.Year(), plan.StartDate.Month(), plan.StartDate.Day(),
0, 0, 0, 0, time.Local)
- day := int(d.Sub(start).Hours() / 24)
- if day < 0 || day > planSpanDays {
- return 0, errors.New("只能安排计划开始后 30 天内的事,这天超出范围了")
+ // 下界放到计划开始日而不是本月 1 号:模板生成的历史节点可能在上个月,
+ // 那些要允许原地改(改标题时日期会原样传回来)
+ if d.Before(st) {
+ return 0, errors.New("这天在计划开始之前,排不了")
}
- return day, nil
+ _, nextEnd := monthBounds(1)
+ if d.After(nextEnd) {
+ return 0, errors.New("最远只能排到下个月月底(" + nextEnd.Format("1月2日") + ")")
+ }
+ return int(d.Sub(st).Hours() / 24), nil
}
-// AddPlanTask 用户自己往计划里加一条
-func (s *Service) AddPlanTask(userID, petID string, in PlanTaskInput) (*model.PlanTask, error) {
- if strings.TrimSpace(in.Title) == "" {
+// AddPlanTask 用户自己往计划里加。Daily=true 时从这天起到窗口末尾每天一条
+func (s *Service) AddPlanTask(userID, petID string, in PlanTaskInput) ([]model.PlanTask, error) {
+ title := strings.TrimSpace(in.Title)
+ if title == "" {
return nil, ErrInvalidParam
}
plan, err := s.planOf(userID, petID)
@@ -337,16 +360,43 @@ func (s *Service) AddPlanTask(userID, petID string, in PlanTaskInput) (*model.Pl
if err != nil {
return nil, err
}
- t := model.PlanTask{
- PlanID: plan.ID, Day: day, DayLabel: dayLabelFor(day),
- Title: strings.TrimSpace(in.Title), Description: strings.TrimSpace(in.Description),
- SheetType: in.SheetType,
+
+ // 要建几天。非 daily 就一天;daily 从这天排到窗口末尾
+ // 要建几天。daily 从这天排到**这一天所在那个月**的月底:
+ // 「应用到每天」说的是这个月每天,跨到下月去就变成用户没要求的事了
+ count := 1
+ if in.Daily {
+ from, e := time.ParseInLocation("2006-01-02", in.Date, time.Local)
+ if e != nil {
+ return nil, ErrInvalidParam
+ }
+ monthEnd := time.Date(from.Year(), from.Month(), 1, 0, 0, 0, 0, time.Local).
+ AddDate(0, 1, -1)
+ count = int(monthEnd.Sub(from).Hours()/24) + 1
+ if count < 1 {
+ count = 1
+ }
+ if count > 31 {
+ count = 31 // 兜一层,别让脏日期算出个巨大的数
+ }
}
- if err := s.db.Create(&t).Error; err != nil {
+
+ out := make([]model.PlanTask, 0, count)
+ for i := 0; i < count; i++ {
+ d := day + i
+ out = append(out, model.PlanTask{
+ PlanID: plan.ID, Day: d, DayLabel: dayLabelFor(d),
+ Title: title, Description: strings.TrimSpace(in.Description),
+ SheetType: in.SheetType,
+ })
+ }
+ if err := s.db.Create(&out).Error; err != nil {
return nil, err
}
- t.Date = plan.StartDate.AddDate(0, 0, day).Format("2006-01-02")
- return &t, nil
+ for i := range out {
+ out[i].Date = plan.StartDate.AddDate(0, 0, out[i].Day).Format("2006-01-02")
+ }
+ return out, nil
}
// UpdatePlanTask 改标题/描述/日期
@@ -408,3 +458,50 @@ func dayLabelFor(day int) string {
return "第 " + strconv.Itoa(day+1) + " 天"
}
}
+
+// PlanMonthStatus 首页那个提醒角标要的东西
+type PlanMonthStatus struct {
+ Month string `json:"month"` // "2026-07"
+ ThisMonth int `json:"this_month"` // 本月有几件
+ ThisUndone int `json:"this_undone"` // 本月还剩几件
+ NextMonth int `json:"next_month"` // 下月有几件
+ // NeedNext 该提醒用户排下月计划了。
+ // 只在月末最后 7 天提醒:月初就催「排下个月」太早,用户会当成噪音关掉。
+ NeedNext bool `json:"need_next"`
+ NextLabel string `json:"next_label"` // "8 月"
+}
+
+// PlanMonthStatus 统计本月/下月的计划件数
+func (s *Service) PlanMonthStatus(userID, petID string) (*PlanMonthStatus, error) {
+ plan, err := s.planOf(userID, petID)
+ if err != nil {
+ return nil, err
+ }
+ thisFirst, thisEnd := monthBounds(0)
+ nextFirst, nextEnd := monthBounds(1)
+
+ var tasks []model.PlanTask
+ s.db.Where("plan_id = ?", plan.ID).Find(&tasks)
+
+ out := &PlanMonthStatus{
+ Month: thisFirst.Format("2006-01"),
+ NextLabel: nextFirst.Format("1 月"),
+ }
+ for _, t := range tasks {
+ d := plan.StartDate.AddDate(0, 0, t.Day)
+ switch {
+ case !d.Before(thisFirst) && !d.After(thisEnd):
+ out.ThisMonth++
+ if !t.Done {
+ out.ThisUndone++
+ }
+ case !d.Before(nextFirst) && !d.After(nextEnd):
+ out.NextMonth++
+ }
+ }
+ // 距月底还剩几天。用 24h 取整会在夏令时地区差一天,这里按日期差算
+ left := int(thisEnd.Sub(time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(),
+ 0, 0, 0, 0, time.Local)).Hours()/24)
+ out.NeedNext = out.NextMonth == 0 && left <= 7
+ return out, nil
+}
diff --git a/pets-fe/app.wxss b/pets-fe/app.wxss
index 08bb31a..9935896 100644
--- a/pets-fe/app.wxss
+++ b/pets-fe/app.wxss
@@ -120,6 +120,12 @@ page{scrollbar-width:none;-ms-overflow-style:none}
}
.section-head .sh-title{font-size:var(--fs-lg);letter-spacing:-.4rpx;font-weight:var(--fw-b)}
.link{color:var(--primary-dark);font-weight:var(--fw-b);font-size:var(--fs-md)}
+/* 一组 .link 当「切换」用时:未选中压成灰,不然两个都是橙的、看不出当前在哪。
+ 单独一个 .link-tabs 而不是挂在 .head-links 上——首页的「管理 / 全部完成」
+ 也用 head-links,那是两个并列动作,不该有一个是灰的 */
+.link-tabs{display:flex;gap:var(--sp-4)}
+.link-tabs .link{color:var(--muted)}
+.link-tabs .link.on{color:var(--primary-dark)}
.head-links{display:flex;align-items:center;gap:var(--sp-4)}
/* 页面标题。
diff --git a/pets-fe/components/pet-switch/pet-switch.js b/pets-fe/components/pet-switch/pet-switch.js
index 076f6e7..8005212 100644
--- a/pets-fe/components/pet-switch/pet-switch.js
+++ b/pets-fe/components/pet-switch/pet-switch.js
@@ -2,6 +2,11 @@ const store = require('../../utils/store.js');
Component({
options: { addGlobalClass: true },
+ properties: {
+ // 「添加」这一格不是每个页面都该有:计划页/记录页只是在既有宠物之间切,
+ // 建档入口在首页和「我的」就够了,到处摆一个会让这条 chip 变得很长
+ showAdd: { type: Boolean, value: true },
+ },
data: {
pets: [],
currentId: 0,
diff --git a/pets-fe/components/pet-switch/pet-switch.wxml b/pets-fe/components/pet-switch/pet-switch.wxml
index 6889817..4907990 100644
--- a/pets-fe/components/pet-switch/pet-switch.wxml
+++ b/pets-fe/components/pet-switch/pet-switch.wxml
@@ -4,7 +4,7 @@
data-id="{{item.id}}" bindtap="onPick">
{{item.emoji}}{{item.name}}
-
+
添加
diff --git a/pets-fe/pages/home/home.js b/pets-fe/pages/home/home.js
index 413f19e..6611c54 100644
--- a/pets-fe/pages/home/home.js
+++ b/pets-fe/pages/home/home.js
@@ -33,7 +33,8 @@ Page({
sexIcon: '',
sexTone: '',
tasks: [],
- planUndone: 0,
+ planHint: '',
+ needNextPlan: false,
monthCost: 0,
timeline: [],
tlPage: 1,
@@ -93,10 +94,24 @@ Page({
loadEntries() {
const id = store.currentPetId();
if (!id) return;
+ // 副标题优先说「该排下月计划了」——那是有时效的事;
+ // 本月还剩几件是随时能看的,让位给它
api
- .getPlan(id)
- .then((plan) => this.setData({ planUndone: ((plan && plan.tasks) || []).filter((t) => !t.done).length }))
- .catch(() => this.setData({ planUndone: 0 }));
+ .planMonthStatus(id)
+ .then((st) => {
+ const s = st || {};
+ this.setData({
+ needNextPlan: !!s.need_next,
+ planHint: s.need_next
+ ? (s.next_label || '下月') + '计划还没排'
+ : s.this_undone > 0
+ ? '本月还有 ' + s.this_undone + ' 件'
+ : s.this_month > 0
+ ? '本月都做完了'
+ : '还没有安排',
+ });
+ })
+ .catch(() => this.setData({ needNextPlan: false, planHint: '' }));
api
.getBill(id, 'month')
.then((b) => this.setData({ monthCost: (b && b.total) || 0 }))
diff --git a/pets-fe/pages/home/home.wxml b/pets-fe/pages/home/home.wxml
index 3d5c868..12087b9 100644
--- a/pets-fe/pages/home/home.wxml
+++ b/pets-fe/pages/home/home.wxml
@@ -29,9 +29,14 @@
-
+
+
+
+
+
管理计划
- {{planUndone > 0 ? '还有 ' + planUndone + ' 件没做' : '30 天安排'}}
+ {{planHint}}
diff --git a/pets-fe/pages/home/home.wxss b/pets-fe/pages/home/home.wxss
index 4fbce98..6e76d1f 100644
--- a/pets-fe/pages/home/home.wxss
+++ b/pets-fe/pages/home/home.wxss
@@ -71,3 +71,10 @@
.tl-desc{font-size:var(--fs-sm);color:var(--text-2);margin-top:6rpx;line-height:1.5}
.tl-img{width:100%;height:320rpx;border-radius:var(--r-sm);margin-top:var(--sp-3);display:block}
.tl-more{text-align:center;padding:var(--sp-4);font-size:var(--fs-sm);color:var(--muted)}
+
+.entry-ic{position:relative}
+.entry-dot{
+ position:absolute;top:-2rpx;right:-2rpx;width:18rpx;height:18rpx;
+ border-radius:50%;background:var(--red);border:3rpx solid #fff;
+}
+.entry-p.warn{color:var(--red-ink);font-weight:var(--fw-b)}
diff --git a/pets-fe/pages/plan/plan.js b/pets-fe/pages/plan/plan.js
index 3ce3e4c..416c2d5 100644
--- a/pets-fe/pages/plan/plan.js
+++ b/pets-fe/pages/plan/plan.js
@@ -18,45 +18,53 @@ function parseYMD(s) {
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; // 周一为一周起点
+// 自然月网格。周一为一周起点,首尾补空格对齐星期。
+// 按月切而不是「从计划开始日起 30 天」:那样窗口跨两个月,
+// 用户看着一个 7月26日—8月25日 的日历,既不像本月也不像下月。
+function buildCells(year, month, byDate, todayStr) {
+ const first = new Date(year, month - 1, 1);
+ const lead = (first.getDay() + 6) % 7;
+ const total = new Date(year, month, 0).getDate();
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 });
+ for (let d = 1; d <= total; d++) {
+ const s = year + '-' + pad2(month) + '-' + pad2(d);
+ cells.push({ day: d, date: s, count: (byDate[s] || []).length, today: s === todayStr });
}
- // 末尾补齐整周,否则最后一行格子宽度会被 grid 拉歪
while (cells.length % 7) cells.push({ blank: true });
return cells;
}
+// off=0 本月,off=1 下月
+function monthOf(off) {
+ const n = new Date();
+ const d = new Date(n.getFullYear(), n.getMonth() + off, 1);
+ return { year: d.getFullYear(), month: d.getMonth() + 1 };
+}
+function monthEnd(off) {
+ const m = monthOf(off);
+ return m.year + '-' + pad2(m.month) + '-' + pad2(new Date(m.year, m.month, 0).getDate());
+}
+
Page({
data: {
pet: {},
plan: null,
doneCount: 0,
cells: [],
- calRange: '',
- planStart: '',
- planEnd: '',
+ monthOff: 0,
+ monthLabel: '',
+ monthEndLabel: '',
+ nextCount: 0,
+ pickStart: '',
+ pickEnd: '',
selDate: '',
selLabel: '',
dayTasks: [],
allOpen: false,
editShow: false,
editId: '',
- form: { title: '', date: '', description: '' },
+ form: { title: '', date: '', description: '', daily: false },
saving: false,
sheetShow: false,
sheetType: '',
@@ -95,22 +103,40 @@ Page({
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,
+ // 能排的区间:不早于计划开始日,不晚于下月月底。和后端 dayOffset 的校验一致
+ pickStart: (plan.start_date || '').slice(0, 10),
+ pickEnd: monthEnd(1),
});
- this.selectDay(sel, todayStr);
+ this.renderMonth(this.data.monthOff);
+ if (this._pendingSel) {
+ // 刚加/改完,跳到那一天,不然用户看不到东西去哪了
+ this.selectDay(this._pendingSel);
+ this._pendingSel = '';
+ }
+ },
+ renderMonth(off) {
+ const m = monthOf(off);
+ const todayStr = ymd(new Date());
+ const byDate = this._byDate || {};
+ const prefix = m.year + '-' + pad2(m.month);
+ this.setData({
+ monthOff: off,
+ monthLabel: m.year + ' 年 ' + m.month + ' 月',
+ monthEndLabel: fmtMD(monthEnd(off)),
+ 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),
+ });
+ // 切到本月默认选今天;切到下月默认选 1 号(今天不在那个月里)
+ const inMonth = todayStr.indexOf(prefix) === 0;
+ this.selectDay(inMonth ? todayStr : prefix + '-01', todayStr);
+ },
+ switchMonth(e) {
+ this.renderMonth(Number(e.currentTarget.dataset.off));
},
selectDay(date, todayStr) {
const t = todayStr || ymd(new Date());
@@ -124,11 +150,7 @@ Page({
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 });
},
@@ -143,7 +165,7 @@ Page({
openAdd() {
this.setData({
editShow: true, editId: '',
- form: { title: '', date: this.data.selDate || this.data.planStart, description: '' },
+ form: { title: '', date: this.data.selDate || ymd(new Date()), description: '', daily: false },
});
},
openEdit(e) {
@@ -151,7 +173,7 @@ Page({
if (!t) return;
this.setData({
editShow: true, editId: t.id,
- form: { title: t.title, date: t.date || this.data.selDate, description: t.description || '' },
+ form: { title: t.title, date: t.date || this.data.selDate, description: t.description || '', daily: false },
});
},
closeEdit() {
@@ -167,6 +189,9 @@ Page({
onFormDesc(e) {
this.setData({ 'form.description': e.detail.value });
},
+ onFormDaily(e) {
+ this.setData({ 'form.daily': e.detail.value });
+ },
onSubmit() {
if (this.data.saving) return;
const f = this.data.form;
diff --git a/pets-fe/pages/plan/plan.wxml b/pets-fe/pages/plan/plan.wxml
index 64ec331..f802031 100644
--- a/pets-fe/pages/plan/plan.wxml
+++ b/pets-fe/pages/plan/plan.wxml
@@ -1,6 +1,6 @@
-
+
@@ -21,12 +21,19 @@
-
+
- {{calRange}}
- 回今天
+ {{monthLabel}}
+
+ 本月
+ 下月
+
+
+
+ {{monthLabel}}还没有安排,点某天加几件
一二三四五六日
@@ -89,14 +96,23 @@
-
+
{{form.date}}
- 只能安排 {{planStart}} 到 {{planEnd}} 之间
+ 只能安排 {{pickStart}} 到 {{pickEnd}} 之间
+
+
+
+ 应用到每天
+ 从这天排到 {{monthEndLabel}},每天一条,可以分别打勾
+
+
+
{{saving ? '保存中…' : '保存'}}
diff --git a/pets-fe/pages/plan/plan.wxss b/pets-fe/pages/plan/plan.wxss
index ad607b1..56de678 100644
--- a/pets-fe/pages/plan/plan.wxss
+++ b/pets-fe/pages/plan/plan.wxss
@@ -43,3 +43,14 @@
box-shadow:var(--sd-3);max-height:82vh;overflow-y:auto;
}
.pe-h{font-size:var(--fs-lg);font-weight:var(--fw-b);margin-bottom:var(--sp-4);text-align:center}
+
+/* 下月还空着时的提示。放在月历里而不是弹 toast——用户切到下月本来就是来排的 */
+.plan-tip{
+ display:flex;align-items:center;gap:var(--sp-2);
+ background:var(--primary-soft);color:var(--primary-ink);
+ border-radius:var(--r-sm);padding:var(--sp-3);
+ font-size:var(--fs-sm);margin-bottom:var(--sp-3);
+}
+
+.pe-daily{display:flex;align-items:center;gap:var(--sp-4)}
+.pe-daily-main{flex:1;min-width:0}
diff --git a/pets-fe/utils/api.js b/pets-fe/utils/api.js
index 3162503..a89fe0e 100644
--- a/pets-fe/utils/api.js
+++ b/pets-fe/utils/api.js
@@ -90,6 +90,7 @@ const api = {
// createAIPlan: (id, input) => request({ url: `/api/pets/${id}/ai-plan`, method: 'POST', data: { input } }),
// applyAIPlan: (planId) => request({ url: `/api/ai-plan/${planId}/apply`, method: 'POST' }),
// 用户自己管理计划节点
+ planMonthStatus: (petId) => request({ url: `/api/pets/${petId}/plan/month-status` }),
addPlanTask: (petId, body) => request({ url: `/api/pets/${petId}/plan-tasks`, method: 'POST', data: body }),
updatePlanTask: (taskId, body) => request({ url: `/api/plan-tasks/${taskId}`, method: 'PUT', data: body }),
deletePlanTask: (taskId) => request({ url: `/api/plan-tasks/${taskId}`, method: 'DELETE' }),