From 5622e4b3d47baff24bb8bedb1daea61b6cf047de Mon Sep 17 00:00:00 2001 From: "jif2.zhang" Date: Mon, 2 Nov 2026 13:54:59 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E6=89=93=E5=8D=A1):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=97=A5=E5=8E=86=E5=88=87=E4=B8=8A=E6=9C=88=E7=9C=8B=E4=B8=8D?= =?UTF-8?q?=E5=88=B0=E8=AE=B0=E5=BD=95;=20=E5=BD=93=E6=97=A5=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6=E6=94=B9=E6=8C=89=E5=BD=93=E5=A4=A9=E4=BB=BD=E9=A2=9D?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - date.js: addMonths 对 "YYYY-MM" 月粒度保持月粒度, 不再生成 "YYYY-MM-01"。 旧逻辑使日历切上月时请求 month=2026-10-01, 服务端判为非法后静默回退本月, 表现为上个月一片空白。 - MergedView: 「回到今天」同时把月份切回当前月; 日标题「已完成 x/y」改为 按 items 的 dayDone 现算, 避免把同周期后续日期的打卡计入历史日期, 并让打卡/撤销的乐观更新立即反映到标题。 - server /calendar: 兼容旧的 month=YYYY-MM-DD 请求并截回月份, 不再静默回退。 - server /habits: 回看历史日期时过滤当天尚未创建的打卡项, 与 /calendar 口径一致; summary.done 改用 dayDone (当天份额) 而非 completed (周期完成度)。 验证: Vite dev + 生产数据 (admin) 2026-10 显示 10 个环 / 完成率 32%, 10-22 的标题由错误的 3/14 修正为 1/14; 「回到今天」回到 2026-11 (14%)。 --- server/routes/checkin.js | 17 ++++++++++++++--- src/pages/checkin/MergedView.jsx | 11 +++++++++-- src/pages/checkin/date.js | 15 ++++++++++++--- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/server/routes/checkin.js b/server/routes/checkin.js index 5a28016..89139d2 100644 --- a/server/routes/checkin.js +++ b/server/routes/checkin.js @@ -122,11 +122,15 @@ router.get('/habits', async (req, res, next) => { const userId = req.user.sub const today = toDateStr() const date = normDate(req.query.date) - const habits = await loadActiveHabits(userId) + const habits = (await loadActiveHabits(userId)) + // 回看历史日期时, 当天还不存在的打卡项不应出现 (与 /calendar 口径一致) + .filter((h) => toDateStr(h.createdAt) <= date) const ranges = habits.map((h) => periodRange(h.period, date)) const grouped = await loadRecords(userId, habits, ranges.length ? ranges : [{ start: date, end: date }]) const items = habits.map((h) => buildItem(h, grouped.get(h.id) || new Map(), date, today)) - const doneCount = items.filter((i) => i.completed).length + // summary 是"当天进度": 按当日份额 (countOnDate >= expectedPerDay) 统计, + // 不能用 completed (周期完成度), 否则历史日期会把同周期后续日期的打卡算进来。 + const doneCount = items.filter((i) => i.dayDone).length res.json({ today, date, items, summary: { total: items.length, done: doneCount } }) } catch (e) { next(e) } }) @@ -291,7 +295,14 @@ router.get('/calendar', async (req, res, next) => { try { const userId = req.user.sub const today = toDateStr() - const month = MONTH_RE.test(String(req.query.month || '')) ? String(req.query.month) : today.slice(0, 7) + // 兼容旧版客户端: addMonths 曾把 "YYYY-MM" 算成 "YYYY-MM-01", 请求月份带了日号, + // 旧逻辑判为非法后静默回退到本月, 导致切到上月时看不到任何记录。这里截回月份。 + const rawMonth = String(req.query.month || '') + const month = MONTH_RE.test(rawMonth) + ? rawMonth + : DATE_RE.test(rawMonth) + ? rawMonth.slice(0, 7) + : today.slice(0, 7) const start = month + '-01' const end = addDays(monthStartNext(month), -1) const habits = await prisma.habit.findMany({ where: { userId }, orderBy: [{ sortOrder: 'asc' }, { id: 'asc' }] }) diff --git a/src/pages/checkin/MergedView.jsx b/src/pages/checkin/MergedView.jsx index d15fa2e..2a82bd8 100644 --- a/src/pages/checkin/MergedView.jsx +++ b/src/pages/checkin/MergedView.jsx @@ -130,7 +130,14 @@ export default function MergedView({ date, onDateChange }) { } } - const summary = detail?.summary || { total: 0, done: 0 } + // 当日进度 = 当天已打卡项 / 当天打卡项. 用 dayDone 而非 completed: + // completed 是"周期目标已达成" (每周/每月项可能在本周期其他天完成), 放到 + // 历史日期的标题上会把当天之后、同周期内的打卡也算进来。按 items 现算还能 + // 让乐观更新 (打卡/撤销) 立即反映到标题, 不必等 /habits 重新拉取。 + const summary = useMemo( + () => ({ total: items.length, done: items.filter((i) => i.dayDone).length }), + [items], + ) const fireBurst = (id) => setBursts((m) => ({ ...m, [id]: (m[id] || 0) + 1 })) // ---------- 打卡 / 撤销 (乐观更新) ---------- @@ -256,7 +263,7 @@ export default function MergedView({ date, onDateChange }) { setMonth((m) => addMonths(m, -1))}>‹ {fmtMonth(month)} setMonth((m) => addMonths(m, 1))}>› - onDateChange(today)}>回到今天 + { setMonth(today.slice(0, 7)); onDateChange(today) }}>回到今天
diff --git a/src/pages/checkin/date.js b/src/pages/checkin/date.js index 7881d01..ac09358 100644 --- a/src/pages/checkin/date.js +++ b/src/pages/checkin/date.js @@ -25,13 +25,22 @@ export function addDays(s, n) { return fromUTC(d) } +/** + * 月份加减. 同时接受月粒度 "YYYY-MM" 与日粒度 "YYYY-MM-DD": + * - 月粒度必须原样返回 "YYYY-MM", 不能变成 "YYYY-MM-01" —— 否则 + * `/checkin/calendar?month=2026-10-01` 会被服务端 MONTH_RE 判为非法并回退到当月, + * 日历切到上个月时就会"看不到任何记录"。 + * - 日粒度保留原日号, 超出目标月天数时收敛到月末。 + */ export function addMonths(s, n) { - const [y, m] = String(s).split('-').map(Number) + const str = String(s) + const [y, m, d] = str.split('-').map(Number) + const day = d || 1 const first = new Date(Date.UTC(y, (m || 1) - 1 + n, 1)) - const day = Number(String(s).slice(8, 10)) || 1 const lastDay = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate() first.setUTCDate(Math.min(day, lastDay)) - return fromUTC(first) + const out = fromUTC(first) + return str.length === 7 ? out.slice(0, 7) : out } /** 周一为一周第一天 */