feat: 完善纪念日农历日期功能

- 修复农历年度重复的计算bug:正确使用农历月/日查找对应公历日期
- 增强 FixedCalendar:显示农历日期(初一、十五等特殊日期)
- 增强 AnniversaryCard:显示详细农历信息(如"正月十五")

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ddshi 2026-02-28 10:37:30 +08:00
parent e7b6864b42
commit eb7aeb586b
3 changed files with 88 additions and 8 deletions

View File

@ -1,8 +1,9 @@
import { Paper, Text, Group, Stack, Badge } from '@mantine/core';
import { IconRepeat, IconCalendar, IconFlag } from '@tabler/icons-react';
import { IconRepeat } from '@tabler/icons-react';
import type { Event } from '../../types';
import { calculateCountdown } from '../../utils/countdown';
import { getHolidayById } from '../../constants/holidays';
import { getLunarInfo } from '../../utils/lunar';
interface AnniversaryCardProps {
event: Event;
@ -19,7 +20,17 @@ export function AnniversaryCard({ event, onClick }: AnniversaryCardProps) {
const getNextDateText = () => {
if (countdown.isPast) return '已过';
if (countdown.isToday) return '今天';
return `${countdown.nextDate.getMonth() + 1}${countdown.nextDate.getDate()}${isLunar ? ' (农历)' : ''}`;
const month = countdown.nextDate.getMonth() + 1;
const day = countdown.nextDate.getDate();
if (isLunar) {
// 显示农历信息
const lunarInfo = getLunarInfo(countdown.nextDate);
return `${lunarInfo.monthInChinese}${lunarInfo.dayInChinese}`;
}
return `${month}${day}`;
};
// 获取循环icon颜色

View File

@ -9,6 +9,7 @@ import {
IconChevronLeft,
IconChevronRight,
} from '@tabler/icons-react';
import { Lunar } from 'lunar-javascript';
interface FixedCalendarProps {
value: Date | null;
@ -45,6 +46,32 @@ function isDateInRange(date: Date, min?: Date, max?: Date): boolean {
const WEEKDAYS = ['日', '一', '二', '三', '四', '五', '六'];
const MONTH_NAMES = ['一月', '二月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月'];
// 获取农历日期简写(用于日历显示)
function getLunarDayText(date: Date): string {
try {
const lunar = Lunar.fromYmd(date.getFullYear(), date.getMonth() + 1, date.getDate());
const day = lunar.getDay();
// 初一显示为"初一",其他日期只显示日期数字
if (day === 1) {
return lunar.getMonthInChinese();
}
// 初一、十五、廿五等特殊日子
const specialDays: Record<number, string> = {
1: '初一',
15: '十五',
20: '廿',
25: '廿五',
30: '卅',
};
if (specialDays[day]) {
return specialDays[day];
}
return lunar.getDayInChinese();
} catch {
return '';
}
}
export function FixedCalendar({ value, onChange, minDate, maxDate }: FixedCalendarProps) {
const [currentMonth, setCurrentMonth] = useState(value || new Date());
@ -62,30 +89,35 @@ export function FixedCalendar({ value, onChange, minDate, maxDate }: FixedCalend
isToday: boolean;
isSelected: boolean;
isDisabled: boolean;
lunarText?: string; // 农历日期简写
}> = [];
// 上月剩余的天数
const prevMonthDays = getDaysInMonth(new Date(year, month - 1, 1));
for (let i = firstDay - 1; i >= 0; i--) {
const date = new Date(year, month - 1, prevMonthDays - i);
const lunarText = getLunarDayText(date);
days.push({
date,
isCurrentMonth: false,
isToday: isSameDay(date, new Date()),
isSelected: isSameDay(date, value),
isDisabled: !isDateInRange(date, minDate, maxDate),
lunarText,
});
}
// 当月的天数
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(year, month, i);
const lunarText = getLunarDayText(date);
days.push({
date,
isCurrentMonth: true,
isToday: isSameDay(date, new Date()),
isSelected: isSameDay(date, value),
isDisabled: !isDateInRange(date, minDate, maxDate),
lunarText,
});
}
@ -93,12 +125,14 @@ export function FixedCalendar({ value, onChange, minDate, maxDate }: FixedCalend
const remainingDays = 42 - days.length;
for (let i = 1; i <= remainingDays; i++) {
const date = new Date(year, month + 1, i);
const lunarText = getLunarDayText(date);
days.push({
date,
isCurrentMonth: false,
isToday: isSameDay(date, new Date()),
isSelected: isSameDay(date, value),
isDisabled: !isDateInRange(date, minDate, maxDate),
lunarText,
});
}
@ -200,6 +234,7 @@ export function FixedCalendar({ value, onChange, minDate, maxDate }: FixedCalend
flex: 1,
aspectRatio: '1',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
fontSize: 13,
@ -233,7 +268,19 @@ export function FixedCalendar({ value, onChange, minDate, maxDate }: FixedCalend
}
}}
>
{day.date.getDate()}
<span>{day.date.getDate()}</span>
{day.lunarText && day.isCurrentMonth && !day.isDisabled && (
<span
style={{
fontSize: 9,
color: day.isSelected ? 'rgba(255,255,255,0.8)' : '#FF9500',
fontWeight: 400,
lineHeight: 1.2,
}}
>
{day.lunarText}
</span>
)}
</Box>
);
})}

View File

@ -75,10 +75,17 @@ export function calculateCountdown(
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 {
// 无法解析农历日期,使用原始公历日期作为后备
@ -94,12 +101,27 @@ export function calculateCountdown(
// 计算下一个 occurrence
if (repeatType === 'yearly') {
// 年度重复:找到今年或明年的对应日期
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());
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') {
// 月度重复:找到本月或之后月份的对应日期