// 任务模块: 自然日边界工具 (Asia/Shanghai) // 与前端 src/pages/task/date.js 保持同一口径, 改动需同步 // // 保留选项口径 (T-FL-02): // never 永不显示 // today 当天 -> 今天 00:00 // 3d 3 天 -> 今天-2 天 00:00 (含今天共 3 个自然日) // week 本周 -> 本周一 00:00 // month 本月 -> 本月 1 日 00:00 // forever 永久显示 -> 无下界 export const TZ = 'Asia/Shanghai' export const RETENTION = ['never', 'today', '3d', 'week', 'month', 'forever'] export const RETENTION_LABEL = { never: '永不显示', today: '当天', '3d': '3 天', week: '本周', month: '本月', forever: '永久显示', } export const STATUSES = ['todo', 'doing', 'done'] export const STATUS_LABEL = { todo: '未开始', doing: '执行中', done: '完成' } // 当前时刻在 Asia/Shanghai 的 "YYYY-MM-DD" 与星期几 const FMT = new Intl.DateTimeFormat('en-CA', { timeZone: TZ, year: 'numeric', month: '2-digit', day: '2-digit', }) /** 任意时刻 -> 本地日历日 "YYYY-MM-DD" */ export function toDateStr(d = new Date()) { return FMT.format(d instanceof Date ? d : new Date(d)) } /** "YYYY-MM-DD" -> UTC 零点 Date (仅做日期运算) */ function toUTC(s) { const [y, m, d] = String(s).split('-').map(Number) return new Date(Date.UTC(y, (m || 1) - 1, d || 1)) } function fromUTC(dt) { return dt.toISOString().slice(0, 10) } export function addDays(s, n) { const d = toUTC(s) d.setUTCDate(d.getUTCDate() + n) return fromUTC(d) } /** 周一为一周第一天 */ export function startOfWeek(s) { const dow = (toUTC(s).getUTCDay() + 6) % 7 return addDays(s, -dow) } export function startOfMonth(s) { return String(s).slice(0, 8) + '01' } /** * 保留选项 -> 起始时间(不含时区偏移的 Date, 以 UTC 表示当日零点) * 返回 null 表示"全可见"(forever); 返回 'never' 表示"全隐藏" */ export function retentionStart(option, now = new Date()) { const today = toDateStr(now) switch (option) { case 'never': return { mode: 'never' } case 'forever': return { mode: 'all' } case 'today': return { mode: 'since', date: today } case '3d': return { mode: 'since', date: addDays(today, -2) } case 'week': return { mode: 'since', date: startOfWeek(today) } case 'month': return { mode: 'since', date: startOfMonth(today) } default: return { mode: 'all' } } } /** * 判断一条已完成任务在给定保留选项下是否可见。 * 依据任务的完成时刻 doneAt (T-FL-10); 未完成任务恒可见 (T-FL-12)。 */ export function isVisibleByRetention(task, option, now = new Date()) { if (task.status !== 'done') return true const win = retentionStart(option, now) if (win.mode === 'never') return false if (win.mode === 'all') return true if (!task.doneAt) return false // doneAt 是绝对时刻, 转成本地日历日再与窗口起点比较 const doneDay = toDateStr(task.doneAt) return doneDay >= win.date }