- 修复农历年度重复的计算bug:正确使用农历月/日查找对应公历日期 - 增强 FixedCalendar:显示农历日期(初一、十五等特殊日期) - 增强 AnniversaryCard:显示详细农历信息(如"正月十五") Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
303 lines
10 KiB
TypeScript
303 lines
10 KiB
TypeScript
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(),
|
||
};
|
||
}
|