feat: 完善中国节假日和农历数据系统

- 修复 isHoliday 函数逻辑错误(农历日期比较方向错误)
- 扩展节假日数据:添加更多中国传统节日(龙抬头、七夕、中元节、寒衣节、下元节、小年等)
- 添加现代节日(情人节、妇女节、青年节、儿童节、建军节、教师节、圣诞节)
- 新增 utils/lunar.ts 工具模块,提供:
  - 生肖和天干地支获取
  - 二十四节气查询
  - 传统节日查询
  - 农历/公历节日判断
- 扩展 lunar-javascript 类型声明

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ddshi 2026-02-28 10:14:32 +08:00
parent 8725108195
commit d73a87709c
3 changed files with 412 additions and 36 deletions

View File

@ -18,6 +18,7 @@ export interface Holiday {
} }
export const HOLIDAYS: Holiday[] = [ export const HOLIDAYS: Holiday[] = [
// ========== 公历节日 ==========
// 元旦 // 元旦
{ {
id: 'new-year', id: 'new-year',
@ -28,24 +29,24 @@ export const HOLIDAYS: Holiday[] = [
isStatutory: true, isStatutory: true,
repeatYearly: true, repeatYearly: true,
}, },
// // 情人
{ {
id: 'spring-festival', id: 'valentines-day',
name: '节', name: '情人节',
isLunar: true, month: 2,
lunarMonth: 1, day: 14,
lunarDay: 1, isLunar: false,
isStatutory: true, isStatutory: false,
repeatYearly: true, repeatYearly: true,
}, },
// 元宵 // 妇女
{ {
id: 'lantern', id: 'womens-day',
name: '元宵节', name: '妇女节',
isLunar: true, month: 3,
lunarMonth: 1, day: 8,
lunarDay: 15, isLunar: false,
isStatutory: false, isStatutory: true,
repeatYearly: true, repeatYearly: true,
}, },
// 清明节 // 清明节
@ -68,6 +69,108 @@ export const HOLIDAYS: Holiday[] = [
isStatutory: true, isStatutory: true,
repeatYearly: true, repeatYearly: true,
}, },
// 青年节
{
id: 'youth-day',
name: '青年节',
month: 5,
day: 4,
isLunar: false,
isStatutory: true,
repeatYearly: true,
},
// 儿童节
{
id: 'childrens-day',
name: '儿童节',
month: 6,
day: 1,
isLunar: false,
isStatutory: true,
repeatYearly: true,
},
// 建军节
{
id: 'army-day',
name: '建军节',
month: 8,
day: 1,
isLunar: false,
isStatutory: true,
repeatYearly: true,
},
// 教师节
{
id: 'teachers-day',
name: '教师节',
month: 9,
day: 10,
isLunar: false,
isStatutory: false,
repeatYearly: true,
},
// 国庆节
{
id: 'national-day',
name: '国庆节',
month: 10,
day: 1,
isLunar: false,
isStatutory: true,
repeatYearly: true,
},
// 圣诞节
{
id: 'christmas',
name: '圣诞节',
month: 12,
day: 25,
isLunar: false,
isStatutory: false,
repeatYearly: true,
},
// 冬至(使用固定公历日期)
{
id: 'winter-solstice',
name: '冬至',
month: 12,
day: 21,
isLunar: false,
isStatutory: false,
repeatYearly: true,
},
// ========== 农历节日 ==========
// 春节
{
id: 'spring-festival',
name: '春节',
isLunar: true,
lunarMonth: 1,
lunarDay: 1,
isStatutory: true,
repeatYearly: true,
},
// 元宵节
{
id: 'lantern',
name: '元宵节',
isLunar: true,
lunarMonth: 1,
lunarDay: 15,
isStatutory: false,
repeatYearly: true,
},
// 龙抬头(春耕节)
{
id: 'dragon-head',
name: '龙抬头',
isLunar: true,
lunarMonth: 2,
lunarDay: 2,
isStatutory: false,
repeatYearly: true,
},
// 端午节 // 端午节
{ {
id: 'dragon-boat', id: 'dragon-boat',
@ -78,6 +181,26 @@ export const HOLIDAYS: Holiday[] = [
isStatutory: true, isStatutory: true,
repeatYearly: true, repeatYearly: true,
}, },
// 七夕节
{
id: 'Qixi',
name: '七夕节',
isLunar: true,
lunarMonth: 7,
lunarDay: 7,
isStatutory: false,
repeatYearly: true,
},
// 中元节
{
id: 'ghost',
name: '中元节',
isLunar: true,
lunarMonth: 7,
lunarDay: 15,
isStatutory: false,
repeatYearly: true,
},
// 中秋节 // 中秋节
{ {
id: 'mid-autumn', id: 'mid-autumn',
@ -88,16 +211,6 @@ export const HOLIDAYS: Holiday[] = [
isStatutory: true, isStatutory: true,
repeatYearly: true, repeatYearly: true,
}, },
// 国庆节
{
id: 'national-day',
name: '国庆节',
month: 10,
day: 1,
isLunar: false,
isStatutory: true,
repeatYearly: true,
},
// 重阳节 // 重阳节
{ {
id: 'double-ninth', id: 'double-ninth',
@ -108,13 +221,23 @@ export const HOLIDAYS: Holiday[] = [
isStatutory: false, isStatutory: false,
repeatYearly: true, repeatYearly: true,
}, },
// 冬至 // 寒衣节
{ {
id: 'winter-solstice', id: 'han-yi',
name: '冬至', name: '寒衣节',
month: 12, isLunar: true,
day: 21, lunarMonth: 10,
isLunar: false, lunarDay: 1,
isStatutory: false,
repeatYearly: true,
},
// 下元节
{
id: 'xia-yuan',
name: '下元节',
isLunar: true,
lunarMonth: 10,
lunarDay: 15,
isStatutory: false, isStatutory: false,
repeatYearly: true, repeatYearly: true,
}, },
@ -128,6 +251,16 @@ export const HOLIDAYS: Holiday[] = [
isStatutory: false, isStatutory: false,
repeatYearly: true, repeatYearly: true,
}, },
// 小年
{
id: 'little-year',
name: '小年',
isLunar: true,
lunarMonth: 12,
lunarDay: 23,
isStatutory: false,
repeatYearly: true,
},
// 除夕 // 除夕
{ {
id: 'chinese-new-years-eve', id: 'chinese-new-years-eve',
@ -156,6 +289,7 @@ export function getHolidayById(id: string): Holiday | undefined {
/** /**
* *
*
*/ */
export function isHoliday( export function isHoliday(
date: Date, date: Date,
@ -166,13 +300,14 @@ export function isHoliday(
for (const holiday of HOLIDAYS) { for (const holiday of HOLIDAYS) {
if (holiday.isLunar && holiday.lunarMonth && holiday.lunarDay) { if (holiday.isLunar && holiday.lunarMonth && holiday.lunarDay) {
// 农历日期需要转换 // 农历日期:把农历节日转换成公历日期再比较
try { try {
const lunar = Lunar.fromYmd(year, month, day); const lunar = Lunar.fromYmd(year, holiday.lunarMonth, holiday.lunarDay);
const solar = lunar.getSolar(); const solar = lunar.getSolar();
if ( if (
solar.getMonth() === holiday.lunarMonth && solar.getMonth() === month &&
solar.getDay() === holiday.lunarDay solar.getDay() === day &&
solar.getYear() === year
) { ) {
return holiday; return holiday;
} }
@ -199,8 +334,8 @@ export function getHolidaysForYear(year: number): Array<Holiday & { date: Date }
for (const holiday of HOLIDAYS) { for (const holiday of HOLIDAYS) {
if (holiday.isLunar && holiday.lunarMonth && holiday.lunarDay) { if (holiday.isLunar && holiday.lunarMonth && holiday.lunarDay) {
try { try {
// 使用 Lunar 构造函数创建农历对象,再获取对应的公历日期 // 使用 Lunar.fromYmd 创建农历对象,再获取对应的公历日期
const lunar = new Lunar(year, holiday.lunarMonth, holiday.lunarDay); const lunar = Lunar.fromYmd(year, holiday.lunarMonth, holiday.lunarDay);
const solar = lunar.getSolar(); const solar = lunar.getSolar();
result.push({ result.push({
...holiday, ...holiday,

View File

@ -4,13 +4,36 @@ declare module 'lunar-javascript' {
getSolar(): Solar; getSolar(): Solar;
getMonth(): number; getMonth(): number;
getDay(): number; getDay(): number;
getYear(): number;
getMonthInChinese(): string; getMonthInChinese(): string;
getDayInChinese(): string; getDayInChinese(): string;
isLeapMonth(): boolean;
getYearInChinese(): string;
getZodiac(): string;
getYearInGanZhi(): string;
getMonthInGanZhi(): string;
getDayInGanZhi(): string;
getFestivals(): string[];
getPrevJieQi(): JieQi | null;
getLunar(): Lunar;
} }
export class Solar { export class Solar {
static fromYmd(year: number, month: number, day: number): Solar;
getYear(): number; getYear(): number;
getMonth(): number; getMonth(): number;
getDay(): number; getDay(): number;
getFestivals(): string[];
}
export class JieQi {
getName(): string;
getSolar(): Solar;
}
export class Holiday {
static fromYear(year: number): Holiday[];
getName(): string;
getSolar(): Solar;
} }
} }

218
src/utils/lunar.ts Normal file
View File

@ -0,0 +1,218 @@
/**
*
*
*/
import { Lunar, Solar, Holiday as LunarHoliday } from 'lunar-javascript';
/**
*
*/
export interface LunarInfo {
year: number; // 农历年份
month: number; // 农历月份
day: number; // 农历日期
monthInChinese: string; // 农历月份中文
dayInChinese: string; // 农历日期中文
isLeapMonth: boolean; // 是否闰月
yearInChinese: string; // 农历年(甲子年等)
zodiac: string; // 生肖
}
/**
*
*/
export interface JieQiInfo {
name: string; // 节气名称
nameInChinese: string; // 节气中文名
date: Date; // 节气日期
}
/**
*
*/
export function getLunarInfo(date: Date): LunarInfo {
const lunar = Lunar.fromYmd(
date.getFullYear(),
date.getMonth() + 1,
date.getDate()
);
return {
year: lunar.getYear(),
month: lunar.getMonth(),
day: lunar.getDay(),
monthInChinese: lunar.getMonthInChinese(),
dayInChinese: lunar.getDayInChinese(),
isLeapMonth: lunar.isLeapMonth(),
yearInChinese: lunar.getYearInChinese(),
zodiac: lunar.getZodiac(),
};
}
/**
*
*/
export function getJieQiForYear(year: number): JieQiInfo[] {
const result: JieQiInfo[] = [];
try {
// 遍历获取每个节气
const holidays = LunarHoliday.fromYear(year);
for (const holiday of holidays) {
if (holiday.getName()) {
const solarDate = holiday.getSolar();
result.push({
name: holiday.getName(),
nameInChinese: holiday.getName(),
date: new Date(solarDate.getYear(), solarDate.getMonth() - 1, solarDate.getDay()),
});
}
}
} catch {
// 忽略错误
}
// 按日期排序
result.sort((a, b) => a.date.getTime() - b.date.getTime());
return result;
}
/**
*
*/
export function getJieQiForDate(date: Date): string | null {
try {
const lunar = Lunar.fromYmd(
date.getFullYear(),
date.getMonth() + 1,
date.getDate()
);
const jieQi = lunar.getPrevJieQi();
if (jieQi) {
return jieQi.getName();
}
} catch {
// 忽略错误
}
return null;
}
/**
*
*/
export function getYearGanZhi(year: number): string {
try {
const lunar = Lunar.fromYmd(year, 1, 1);
return lunar.getYearInGanZhi();
} catch {
return '';
}
}
/**
*
*/
export function getMonthGanZhi(year: number, month: number): string {
try {
const lunar = Lunar.fromYmd(year, month, 1);
return lunar.getMonthInGanZhi();
} catch {
return '';
}
}
/**
*
*/
export function getDayGanZhi(date: Date): string {
try {
const lunar = Lunar.fromYmd(
date.getFullYear(),
date.getMonth() + 1,
date.getDate()
);
return lunar.getDayInGanZhi();
} catch {
return '';
}
}
/**
*
*/
export function isLunarFestival(date: Date): string | null {
try {
const lunar = Lunar.fromYmd(
date.getFullYear(),
date.getMonth() + 1,
date.getDate()
);
const festival = lunar.getFestivals();
if (festival && festival.length > 0) {
return festival[0];
}
} catch {
// 忽略错误
}
return null;
}
/**
*
*/
export function isSolarFestival(date: Date): string | null {
try {
const solar = Solar.fromYmd(
date.getFullYear(),
date.getMonth() + 1,
date.getDate()
);
const festival = solar.getFestivals();
if (festival && festival.length > 0) {
return festival[0];
}
} catch {
// 忽略错误
}
return null;
}
/**
*
*
*/
export interface TraditionalFestival {
id: string;
name: string;
date: Date; // 该年的公历日期
isLunar: boolean; // 是否为农历节日
description?: string; // 节日描述
}
/**
*
*/
export function getTraditionalFestivals(year: number): TraditionalFestival[] {
const festivals: TraditionalFestival[] = [];
// 使用 lunar-javascript 的节假日功能
try {
const holidays = LunarHoliday.fromYear(year);
for (const holiday of holidays) {
const solar = holiday.getSolar();
festivals.push({
id: holiday.getName(),
name: holiday.getName(),
date: new Date(solar.getYear(), solar.getMonth() - 1, solar.getDay()),
isLunar: true,
description: holiday.getName(),
});
}
} catch {
// 忽略错误
}
// 按日期排序
festivals.sort((a, b) => a.date.getTime() - b.date.getTime());
return festivals;
}