feat: 初始提交(仅核心代码,已排除大文件)
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user