From 9b30bfaaff58971b8baf676e167405ddf1adbcdb Mon Sep 17 00:00:00 2001 From: Blizzard Date: Thu, 30 Jul 2026 11:09:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=A1=E5=88=92=E6=8C=89=E8=87=AA?= =?UTF-8?q?=E7=84=B6=E6=9C=88=E7=AE=A1=E7=90=86=EF=BC=8C=E5=8A=A0=E3=80=8C?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=88=B0=E6=AF=8F=E5=A4=A9=E3=80=8D=E5=92=8C?= =?UTF-8?q?=E4=B8=8B=E6=9C=88=E6=9C=AA=E6=8E=92=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 为什么不是「从计划开始日起 30 天」 上一版是那样做的,截图一看就不对:计划是月中建的,日历显示 「7月26日 — 8月25日」,既不像本月也不像下月,用户想问「这个月还剩什么」 根本对不上。改成自然月,日历就是一眼能认的月历。 能翻到下月 —— 否则「做下月计划」这件事本身做不了。 ## 校验从「Day 上限」改成「日期」 PlanTask.Day 是相对计划开始日的,月份边界是相对今天的,两者会错开。 原来卡 Day<=30,计划建了几天之后下月末就换算成 Day=40 多, 用户在日历上明明看得到那格、点进去却存不了。 现在的规则:不早于计划开始日(下界放到开始日而不是本月 1 号——模板生成的 历史节点可能在上个月,那些要允许原地改),不晚于下月月底。 ## 应用到每天 一天建一条,不做「一条 + repeat 字段读时展开」:勾选状态是按天的, 展开方案还得再开一张表记「哪天打过勾」,而直接建 N 条天然就有这个能力。 范围止于**这一天所在那个月**的月底,不跨月——「应用到每天」说的是这个月每天, 跨过去就变成用户没要求的事了。只有新增能选,已有的那条改成每天等于凭空 复制 N 份(用户想要的是「以后每天」,不是「把这条撒开」)。 ## 首页那个提醒 GET /pets/:id/plan/month-status 返回本月件数/剩余、下月件数、need_next。 need_next 只在**月末最后 7 天**且下月为空时为真:月初就催「排下个月」太早, 用户会当成噪音。角标做在图标上而不是弹 toast——每次进首页弹一下很烦, 而这件事不紧急、看见就行。 副标题有优先级:「8 月计划还没排」压过「本月都做完了」,因为前者有时效。 四种状态实跑验过,包括让位关系。 ## 顺手 pet-switch 加 show-add 属性。计划页只需要在既有宠物之间切,建档入口在首页 和「我的」就够了,到处摆一个「添加」会让这条 chip 变得很长。 本月/下月的切换用新的 .link-tabs 而不是挂在 .head-links 上:首页的 「管理 / 全部完成」也用 head-links,那是两个并列动作,不该有一个是灰的。 ## 验证(预生产库实跑) 月边界 本月末通过、下月末通过、下下月拒(报出具体日期) 应用到每天 今天→本月末 2 条,没跨月 month-status 下月清空 → need_next 翻 true;补一条 → 翻回 false 四种副标题 下月未排 / 本月还有 N 件 / 本月都做完了 / 还没有安排,含优先级 测试数据全部还原 上一条 cd 打错目录,导致「日期窗口」那次改动没写进文件、只有批量那半截进去了, 所以中间出现过一次 planWindow undefined。这次一并改对。 Co-Authored-By: Claude Opus 5 --- pets-be/internal/handler/plan.go | 19 ++- pets-be/internal/router/router.go | 1 + pets-be/internal/service/plan.go | 139 +++++++++++++++--- pets-fe/app.wxss | 6 + pets-fe/components/pet-switch/pet-switch.js | 5 + pets-fe/components/pet-switch/pet-switch.wxml | 2 +- pets-fe/pages/home/home.js | 23 ++- pets-fe/pages/home/home.wxml | 9 +- pets-fe/pages/home/home.wxss | 7 + pets-fe/pages/plan/plan.js | 101 ++++++++----- pets-fe/pages/plan/plan.wxml | 30 +++- pets-fe/pages/plan/plan.wxss | 11 ++ pets-fe/utils/api.js | 1 + 13 files changed, 278 insertions(+), 76 deletions(-) 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' }),