export interface Recurrence { type: 'NONE' | 'DAILY' | 'WEEKLY'; interval: number; by: number[]; } export interface EventForReminder { id: string; title: string; start: string; end?: string | null; remindAt?: string | null; recurrence?: string | null; completed?: boolean; deleted?: boolean; } export function parseRecurrenceStr(str: string | null): Recurrence { const r = (str || '').trim(); if (!r) return { type: 'NONE', interval: 0, by: [] }; const type = r.startsWith('DAILY') ? 'DAILY' : (r.startsWith('WEEKLY') ? 'WEEKLY' : 'NONE'); const mInt = /interval=(\d+)/.exec(r); const interval = mInt ? Math.max(1, parseInt(mInt[1], 10)) : 1; const mBy = /by=([0-9,]+)/.exec(r); const by = mBy ? mBy[1].split(',').map(s => parseInt(s, 10)).filter(n => n >= 1 && n <= 7) : []; return { type, interval, by }; } export function setTimeFromIso(dateObj: Date, timeSourceIso: string): string { const d = new Date(dateObj.getTime()); const t = timeSourceIso ? safeParseDate(timeSourceIso) : null; const hh = t ? t.getHours() : 0; const mm = t ? t.getMinutes() : 0; d.setHours(hh, mm, 0, 0); return d.toISOString(); } export function extractDateStr(dateStr: string): string { // 支持两种格式:2026-06-28T21:30:00 和 2026-06-28 21:30 if (dateStr.includes('T')) { return dateStr.split('T')[0]; } if (dateStr.includes(' ')) { return dateStr.split(' ')[0]; } return dateStr; } export function safeParseDate(dateStr: string): Date { // 尝试安全地解析日期,支持多种格式 try { // 先尝试直接解析 let date = new Date(dateStr); if (!isNaN(date.getTime())) { return date; } } catch (e) { // 忽略 } // 如果是 2026-06-28 21:30 格式,尝试替换空格为 T if (dateStr.includes(' ')) { try { let date = new Date(dateStr.replace(' ', 'T')); if (!isNaN(date.getTime())) { return date; } } catch (e) { // 忽略 } } // 最后尝试用 Date.UTC 解析 const match = dateStr.match(/(\d{4})-(\d{2})-(\d{2})/); if (match) { const year = parseInt(match[1], 10); const month = parseInt(match[2], 10) - 1; const day = parseInt(match[3], 10); return new Date(year, month, day); } return new Date(); } export function shouldShowEventOnDate( event: { start: string; recurrence: string | null; end?: string | null }, targetDate: Date ): boolean { const eventDate = extractDateStr(event.start); const targetDateStr = targetDate.toISOString().split('T')[0]; // 1. 普通事件 - 开始日期就是当天 if (eventDate === targetDateStr) return true; const evStart = safeParseDate(event.start); const targetDayStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()); const eventStartDay = new Date(evStart.getFullYear(), evStart.getMonth(), evStart.getDate()); // 2. 持续多天的事件 - 目标日期在 [start, end] 之间 if (event.end) { const evEnd = safeParseDate(event.end); const eventEndDay = new Date(evEnd.getFullYear(), evEnd.getMonth(), evEnd.getDate()); // 如果目标日期在事件日期范围内,就显示 if (targetDayStart >= eventStartDay && targetDayStart <= eventEndDay) { return true; } } // 3. 循环事件 const rec = parseRecurrenceStr(event.recurrence); if (rec.type === 'NONE') return false; // 如果目标日期在开始日期之前,不显示 if (targetDayStart < eventStartDay) return false; // 如果有结束日期,并且目标日期在结束日期之后,不显示 if (event.end) { const evEnd = safeParseDate(event.end); const eventEndDay = new Date(evEnd.getFullYear(), evEnd.getMonth(), evEnd.getDate()); if (targetDayStart > eventEndDay) return false; } // 没有结束日期的话,就永远显示 if (rec.type === 'DAILY') { const diffDays = Math.floor((targetDayStart.getTime() - eventStartDay.getTime()) / (24 * 3600 * 1000)); return diffDays % rec.interval === 0; } if (rec.type === 'WEEKLY') { const weeksSinceStart = Math.floor((targetDayStart.getTime() - eventStartDay.getTime()) / (7 * 24 * 3600 * 1000)); const weekday = targetDayStart.getDay() === 0 ? 7 : targetDayStart.getDay(); const by = rec.by.length ? rec.by : [(safeParseDate(event.start).getDay() || 7)]; return by.includes(weekday) && (weeksSinceStart % rec.interval === 0); } return false; } export function computeUpcomingReminders( e: EventForReminder, sinceMs: number, untilMs: number ): string[] { const rec = parseRecurrenceStr(e.recurrence || null); const evStart = safeParseDate(e.start); const reminders: string[] = []; if (rec.type === 'NONE') { const iso = e.remindAt || e.start; if (!iso) return reminders; const t = safeParseDate(iso).getTime(); if (t >= sinceMs && t <= untilMs) reminders.push(iso); return reminders; } const startDayMs = new Date(evStart.getFullYear(), evStart.getMonth(), evStart.getDate()).getTime(); const windowStartDayMs = new Date( new Date(sinceMs).getFullYear(), new Date(sinceMs).getMonth(), new Date(sinceMs).getDate() ).getTime(); const windowEndDayMs = new Date( new Date(untilMs).getFullYear(), new Date(untilMs).getMonth(), new Date(untilMs).getDate() ).getTime(); let endDayMs: number; if (e.end) { const evEnd = safeParseDate(e.end); endDayMs = new Date(evEnd.getFullYear(), evEnd.getMonth(), evEnd.getDate()).getTime(); } else { // 无结束日期时,使用 untilMs 作为结束边界,避免提前终止 endDayMs = windowEndDayMs; } const fromDay = Math.max(startDayMs, windowStartDayMs); const toDay = Math.min(endDayMs, windowEndDayMs); const timeSourceIso = e.remindAt || e.start; for (let d = fromDay; d <= toDay; d += 24 * 3600 * 1000) { const cursor = new Date(d); if (rec.type === 'DAILY') { const diffDays = Math.floor((d - startDayMs) / (24 * 3600 * 1000)); if (diffDays % rec.interval === 0) { const iso = setTimeFromIso(cursor, timeSourceIso); const t = safeParseDate(iso).getTime(); if (t >= sinceMs && t <= untilMs) reminders.push(iso); } } else if (rec.type === 'WEEKLY') { const weekday = cursor.getDay() === 0 ? 7 : cursor.getDay(); const weeksSinceStart = Math.floor((d - startDayMs) / (7 * 24 * 3600 * 1000)); const by = rec.by.length ? rec.by : [(safeParseDate(e.start).getDay() || 7)]; if (by.includes(weekday) && (weeksSinceStart % rec.interval === 0)) { const iso = setTimeFromIso(cursor, timeSourceIso); const t = safeParseDate(iso).getTime(); if (t >= sinceMs && t <= untilMs) reminders.push(iso); } } } return reminders; }