204 lines
6.7 KiB
TypeScript
204 lines
6.7 KiB
TypeScript
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;
|
|
}
|