import { Lunar } from 'lunar-javascript'; export interface CountdownResult { days: number; hours: number; minutes: number; seconds: number; nextDate: Date; isToday: boolean; isPast: boolean; } /** * 安全地创建日期,处理月末边界情况 * 某些月份只有28/29/30/31天 */ function safeCreateDate(year: number, month: number, day: number): Date { const date = new Date(year, month, day); // 如果日期溢出(如下个月),自动调整到月末 if (date.getMonth() !== month % 12) { date.setMonth(month + 1, 0); } return date; } /** * 安全地创建农历日期,处理月末边界情况 * 某些农历月份只有29或30天,需要检查并调整 */ function safeCreateLunarDate(year: number, month: number, day: number): { lunar: Lunar; solar: Solar } | null { try { const lunar = Lunar.fromYmd(year, month, day); return { lunar, solar: lunar.getSolar() }; } catch { // 如果日期不存在(如农历12月30日在只有29天的月份), // 尝试获取该月的最后一天 try { // 获取下个月的农历日期,然后往前推一天 const nextMonthLunar = Lunar.fromYmd(year, month + 1, 1); const lastDayOfMonth = nextMonthLunar.getLunar().getDay() - 1; if (lastDayOfMonth > 0) { const lunar = Lunar.fromYmd(year, month, lastDayOfMonth); return { lunar, solar: lunar.getSolar() }; } } catch { // 仍然失败,返回null } return null; } } /** * 计算纪念日的倒计时 * 考虑重复规则,计算下一个 occurrence 的倒计时 */ export function calculateCountdown( dateStr: string, repeatType: 'yearly' | 'monthly' | 'daily' | 'weekly' | 'none', isLunar: boolean ): CountdownResult { const now = new Date(); const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); let targetDate: Date; let isPast = false; // 解析日期字符串 // 格式假设为 ISO 格式 "YYYY-MM-DD" 或完整的 ISO datetime const dateParts = dateStr.split('T')[0].split('-').map(Number); const originalYear = dateParts[0]; const originalMonth = dateParts[1] - 1; // JavaScript月份从0开始 const originalDay = dateParts[2]; // 获取原始时间(小时和分钟) const timeParts = dateStr.includes('T') ? dateStr.split('T')[1].split(':') : ['00', '00']; const originalHours = parseInt(timeParts[0]) || 0; const originalMinutes = parseInt(timeParts[1]) || 0; // 保存农历月份和日期(用于年度重复计算) let lunarMonth: number | null = null; let lunarDay: number | null = null; if (isLunar) { // 农历日期:使用安全方法创建,处理月末边界 const result = safeCreateLunarDate(originalYear, originalMonth + 1, originalDay); if (result) { // 保存农历日期 lunarMonth = result.lunar.getMonth(); lunarDay = result.lunar.getDay(); targetDate = new Date(result.solar.getYear(), result.solar.getMonth() - 1, result.solar.getDay(), originalHours, originalMinutes); } else { // 无法解析农历日期,使用原始公历日期作为后备 targetDate = new Date(originalYear, originalMonth, originalDay, originalHours, originalMinutes); } } else { // 公历日期 targetDate = new Date(originalYear, originalMonth, originalDay, originalHours, originalMinutes); } // 先判断是否今天(日期部分相等) const isToday = targetDate.toDateString() === today.toDateString(); // 计算下一个 occurrence if (repeatType === 'yearly') { if (isLunar && lunarMonth !== null && lunarDay !== null) { // 农历年度重复:根据农历日期查找今年或明年的对应公历日期 const thisYearResult = safeCreateLunarDate(today.getFullYear(), lunarMonth, lunarDay); if (thisYearResult) { targetDate = new Date(thisYearResult.solar.getYear(), thisYearResult.solar.getMonth() - 1, thisYearResult.solar.getDay(), originalHours, originalMinutes); } // 如果今年的农历日期已过,查找明年 if (!targetDate || targetDate < today) { const nextYearResult = safeCreateLunarDate(today.getFullYear() + 1, lunarMonth, lunarDay); if (nextYearResult) { targetDate = new Date(nextYearResult.solar.getYear(), nextYearResult.solar.getMonth() - 1, nextYearResult.solar.getDay(), originalHours, originalMinutes); } } } else { // 公历年度重复:找到今年或明年的对应日期 targetDate = safeCreateDate(today.getFullYear(), targetDate.getMonth(), targetDate.getDate()); targetDate.setHours(originalHours, originalMinutes, 0, 0); if (targetDate < today) { targetDate = safeCreateDate(today.getFullYear() + 1, targetDate.getMonth(), targetDate.getDate()); targetDate.setHours(originalHours, originalMinutes, 0, 0); } } } else if (repeatType === 'monthly') { // 月度重复:找到本月或之后月份的对应日期 const originalDay = targetDate.getDate(); let currentMonth = today.getMonth(); // 从当前月份开始 targetDate = safeCreateDate(today.getFullYear(), currentMonth, originalDay); targetDate.setHours(originalHours, originalMinutes, 0, 0); // 持续递增月份直到找到未来日期 while (targetDate < today) { currentMonth++; targetDate = safeCreateDate(today.getFullYear(), currentMonth, originalDay); targetDate.setHours(originalHours, originalMinutes, 0, 0); } } else if (repeatType === 'daily') { // 每日重复:找到今天或明天的对应时间点 targetDate = new Date(today); targetDate.setHours(originalHours, originalMinutes, 0, 0); if (targetDate < now) { targetDate = new Date(today); targetDate.setDate(targetDate.getDate() + 1); targetDate.setHours(originalHours, originalMinutes, 0, 0); } } else if (repeatType === 'weekly') { // 每周重复:找到本周或下周对应星期几的时间点 const dayOfWeek = new Date(originalYear, originalMonth, originalDay).getDay(); targetDate = new Date(today); const daysUntilTarget = (dayOfWeek - today.getDay() + 7) % 7; targetDate.setDate(targetDate.getDate() + daysUntilTarget); targetDate.setHours(originalHours, originalMinutes, 0, 0); if (targetDate < now) { targetDate.setDate(targetDate.getDate() + 7); } } else { // 不重复 if (targetDate < today) { isPast = true; } } // 计算时间差 const diff = targetDate.getTime() - now.getTime(); // 如果是今天,即使是负数(已过),也显示为 0 而不是"已过" // 只要 isToday 为 true,就不算"已过",显示为"今天" if (diff < 0 && !isPast) { if (!isToday) { isPast = true; } } const absDiff = Math.abs(diff); // 如果是今天,直接返回0天 let days: number; let hours = 0; let minutes = 0; let seconds = 0; if (isToday) { days = 0; hours = 0; minutes = 0; seconds = 0; isPast = false; // 今天不算已过 } else { // 使用 ceil 来计算天数,这样用户看到的是"还有X天"的直观感受 // 例如:2/12 18:00 到 2/15 00:00 应该显示"还有3天"而不是"还有2天" days = Math.ceil(absDiff / (1000 * 60 * 60 * 24)); // 重新计算剩余的小时、分钟、秒(使用 ceil 后剩余时间需要调整) const remainingAfterDays = absDiff - (days - 1) * (1000 * 60 * 60 * 24); hours = Math.floor((remainingAfterDays % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); minutes = Math.floor((remainingAfterDays % (1000 * 60 * 60)) / (1000 * 60)); seconds = Math.floor((remainingAfterDays % (1000 * 60)) / 1000); } return { days: isPast ? -days : days, hours, minutes, seconds, nextDate: targetDate, isToday, isPast, }; } /** * 格式化倒计时显示 */ export function formatCountdown(countdown: CountdownResult): string { if (countdown.isPast) { return '已过'; } if (countdown.isToday) { return '今天'; } if (countdown.days > 0) { return `${countdown.days}天`; } if (countdown.hours > 0) { return `${countdown.hours}时${countdown.minutes}分`; } return `${countdown.minutes}分${countdown.seconds}秒`; } /** * 获取友好的日期描述 */ export function getFriendlyDateDescription( dateStr: string, repeatType: 'yearly' | 'monthly' | 'daily' | 'weekly' | 'none', isLunar: boolean ): string { const countdown = calculateCountdown(dateStr, repeatType, isLunar); const targetDate = countdown.nextDate; if (countdown.isPast) { return '已过'; } if (countdown.isToday) { return '今天'; } const now = new Date(); const tomorrow = new Date(now); tomorrow.setDate(tomorrow.getDate() + 1); if (targetDate.toDateString() === tomorrow.toDateString()) { return '明天'; } if (countdown.days < 7) { return `${countdown.days}天后`; } // 返回格式化日期 const month = targetDate.getMonth() + 1; const day = targetDate.getDate(); if (isLunar) { // 显示农历 const result = safeCreateLunarDate(targetDate.getFullYear(), month, day); if (result) { return `${result.lunar.getMonthInChinese()}月${result.lunar.getDayInChinese()}`; } return `${month}月${day}日`; } return `${month}月${day}日`; } /** * 获取农历日期的公历日期 */ export function getSolarFromLunar(lunarMonth: number, lunarDay: number, year?: number): Date { const targetYear = year || new Date().getFullYear(); const lunar = Lunar.fromYmd(targetYear, lunarMonth, lunarDay); const solar = lunar.getSolar(); return new Date(solar.getYear(), solar.getMonth() - 1, solar.getDay()); } /** * 获取公历日期的农历日期 */ export function getLunarFromSolar(solarDate: Date): { month: number; day: number; monthInChinese: string; dayInChinese: string } { const lunar = Lunar.fromYmd(solarDate.getFullYear(), solarDate.getMonth() + 1, solarDate.getDate()); return { month: lunar.getMonth(), day: lunar.getDay(), monthInChinese: lunar.getMonthInChinese(), dayInChinese: lunar.getDayInChinese(), }; }