qia-client/src/utils/countdown.ts
ddshi eb7aeb586b feat: 完善纪念日农历日期功能
- 修复农历年度重复的计算bug:正确使用农历月/日查找对应公历日期
- 增强 FixedCalendar:显示农历日期(初一、十五等特殊日期)
- 增强 AnniversaryCard:显示详细农历信息(如"正月十五")

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 10:37:30 +08:00

303 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(),
};
}