【重要修复】重写农历双向转换算法(原来全错): - solarToLunar/lunarToSolar 改用标准算法(基准1900-01-31正月初一,UTC避免时区误差) - 验证:2026春节2-17、中秋9-25、端午6-19、2023闰二月全部正确 【首页大日历】 - 日期数字加大到240rpx,超醒目 - 节假日红色喜庆卡片+红数字(春节/国庆/中秋等法定假日) - 工作日绿色卡片+绿数字(一眼知道要上班) - 周末红色数字+米黄卡片 - 节气显示具体时刻(如 立秋 20:29) - 初一/十五金色标记(可在设置关闭) - 佛历提醒:距下一佛诞还有几天(可在设置开启) 【全屏放大弹窗(老年友好)】 - 大日历卡片/宜忌/详情/节日速查 点击均可全屏放大 - 宜忌放大后字号44rpx,详情放大后40rpx 【月历视图】 - swiper 左右滑动换月,预计算5个月居中显示 - 滑到边缘自动补月,可无限滚动 - 点选日期切回日视图 【节日速查】 - 首页显示近期5个节日(中秋/重阳等)+公历日期+倒计时 - 点击直接跳到该日老黄历 【设置页】 - 新增'显示佛历提醒'开关(默认关) - 新增'初一十五高亮'开关(默认开)
This commit is contained in:
@@ -101,6 +101,54 @@ const BUDDHIST_FESTIVALS = {
|
||||
'11-17': '阿弥陀佛圣诞', '12-8': '释迦牟尼佛成道日(腊八)', '12-29': '华严菩萨圣诞'
|
||||
};
|
||||
|
||||
/** 法定节假日(公历) */
|
||||
const LEGAL_HOLIDAYS_SOLAR = {
|
||||
'1-1': '元旦', '5-1': '劳动节', '10-1': '国庆节'
|
||||
};
|
||||
|
||||
/** 法定节假日(农历) */
|
||||
const LEGAL_HOLIDAYS_LUNAR = {
|
||||
'1-1': '春节', '5-5': '端午节', '8-15': '中秋节', '12-30': '除夕'
|
||||
};
|
||||
|
||||
/** 二十四节气大致时刻(用于显示,小时:分钟) */
|
||||
const SOLAR_TERM_TIMES = {
|
||||
'立春': '04:01', '雨水': '23:51', '惊蛰': '22:07', '春分': '23:06',
|
||||
'清明': '03:20', '谷雨': '10:24', '立夏': '20:26', '小满': '09:23',
|
||||
'芒种': '00:26', '夏至': '17:14', '小暑': '10:38', '大暑': '04:07',
|
||||
'立秋': '20:29', '处暑': '11:16', '白露': '23:32', '秋分': '09:04',
|
||||
'寒露': '15:22', '霜降': '18:36', '立冬': '18:45', '小雪': '16:20',
|
||||
'大雪': '11:46', '冬至': '05:48', '小寒': '23:05', '大寒': '16:30'
|
||||
};
|
||||
|
||||
/** 常用节日速查(名称 → 类型+月日) */
|
||||
const FESTIVAL_QUICK_SEARCH = [
|
||||
{ name: '春节', type: 'lunar', date: '1-1' },
|
||||
{ name: '元宵节', type: 'lunar', date: '1-15' },
|
||||
{ name: '龙抬头', type: 'lunar', date: '2-2' },
|
||||
{ name: '端午节', type: 'lunar', date: '5-5' },
|
||||
{ name: '七夕', type: 'lunar', date: '7-7' },
|
||||
{ name: '中元节', type: 'lunar', date: '7-15' },
|
||||
{ name: '中秋节', type: 'lunar', date: '8-15' },
|
||||
{ name: '重阳节', type: 'lunar', date: '9-9' },
|
||||
{ name: '腊八节', type: 'lunar', date: '12-8' },
|
||||
{ name: '小年', type: 'lunar', date: '12-23' },
|
||||
{ name: '除夕', type: 'lunar', date: '12-30' },
|
||||
{ name: '元旦', type: 'solar', date: '1-1' },
|
||||
{ name: '情人节', type: 'solar', date: '2-14' },
|
||||
{ name: '妇女节', type: 'solar', date: '3-8' },
|
||||
{ name: '植树节', type: 'solar', date: '3-12' },
|
||||
{ name: '劳动节', type: 'solar', date: '5-1' },
|
||||
{ name: '青年节', type: 'solar', date: '5-4' },
|
||||
{ name: '儿童节', type: 'solar', date: '6-1' },
|
||||
{ name: '建党节', type: 'solar', date: '7-1' },
|
||||
{ name: '建军节', type: 'solar', date: '8-1' },
|
||||
{ name: '教师节', type: 'solar', date: '9-10' },
|
||||
{ name: '国庆节', type: 'solar', date: '10-1' },
|
||||
{ name: '平安夜', type: 'solar', date: '12-24' },
|
||||
{ name: '圣诞节', type: 'solar', date: '12-25' }
|
||||
];
|
||||
|
||||
/** 纳音表 */
|
||||
const NAYIN_TABLE = [
|
||||
'海中金', '炉中火', '大林木', '路旁土', '剑锋金', '山头火',
|
||||
@@ -370,6 +418,10 @@ module.exports = {
|
||||
SOLAR_FESTIVALS,
|
||||
LUNAR_FESTIVALS,
|
||||
BUDDHIST_FESTIVALS,
|
||||
LEGAL_HOLIDAYS_SOLAR,
|
||||
LEGAL_HOLIDAYS_LUNAR,
|
||||
SOLAR_TERM_TIMES,
|
||||
FESTIVAL_QUICK_SEARCH,
|
||||
NAYIN_TABLE,
|
||||
HOUR_NAMES,
|
||||
HOUR_RANGES,
|
||||
|
||||
+195
-83
@@ -13,6 +13,10 @@ const {
|
||||
SOLAR_FESTIVALS,
|
||||
LUNAR_FESTIVALS,
|
||||
BUDDHIST_FESTIVALS,
|
||||
LEGAL_HOLIDAYS_SOLAR,
|
||||
LEGAL_HOLIDAYS_LUNAR,
|
||||
SOLAR_TERM_TIMES,
|
||||
FESTIVAL_QUICK_SEARCH,
|
||||
NAYIN_TABLE,
|
||||
HOUR_NAMES,
|
||||
HOUR_RANGES,
|
||||
@@ -84,124 +88,107 @@ function getLunarMonthDays(year, month) {
|
||||
return (LUNAR_INFO[year - 1900] & (0x10000 >> month)) ? 30 : 29;
|
||||
}
|
||||
|
||||
/** 公历转农历 */
|
||||
/** 公历转农历(标准算法,基准 1900-01-31 = 农历1900年正月初一) */
|
||||
function solarToLunar(year, month, day) {
|
||||
if (year < 1900 || year > 2100) return null;
|
||||
|
||||
let offset = 0;
|
||||
for (let y = 1900; y < year; y++) {
|
||||
offset += getLunarYearDays(y);
|
||||
// 与基准日 1900-01-31(农历1900年正月初一)的天数差(用 UTC 避免时区误差)
|
||||
const baseDate = Date.UTC(1900, 0, 31);
|
||||
const objDate = Date.UTC(year, month - 1, day);
|
||||
let offset = Math.floor((objDate - baseDate) / 86400000);
|
||||
|
||||
let lunarYear = 1900;
|
||||
let daysOfYear = 0;
|
||||
|
||||
// 确定农历年
|
||||
for (; lunarYear < 2101 && offset > 0; lunarYear++) {
|
||||
daysOfYear = getLunarYearDays(lunarYear);
|
||||
if (offset < daysOfYear) break;
|
||||
offset -= daysOfYear;
|
||||
}
|
||||
|
||||
let leapMonth = getLeapMonth(year);
|
||||
const leapMonth = getLeapMonth(lunarYear);
|
||||
let isLeap = false;
|
||||
let lunarMonth = 1;
|
||||
let lunarDay = 1;
|
||||
let daysInMonth = 0;
|
||||
let daysOfMonth = 0;
|
||||
|
||||
// 计算从正月初一到目标日期的天数
|
||||
let targetOffset = offset;
|
||||
for (let m = 1; m < month; m++) {
|
||||
targetOffset += getSolarMonthDays(year, m);
|
||||
}
|
||||
targetOffset += day - 1;
|
||||
|
||||
// 农历年剩余天数
|
||||
let daysInLunarYear = getLunarYearDays(year);
|
||||
let remaining = targetOffset - offset;
|
||||
|
||||
if (remaining < 0 || remaining >= daysInLunarYear) {
|
||||
// 需要调整年份
|
||||
if (remaining < 0) {
|
||||
year--;
|
||||
remaining += getLunarYearDays(year);
|
||||
} else {
|
||||
remaining -= daysInLunarYear;
|
||||
year++;
|
||||
}
|
||||
leapMonth = getLeapMonth(year);
|
||||
}
|
||||
|
||||
// 逐月计算
|
||||
for (let m = 1; m <= 12; m++) {
|
||||
daysInMonth = getLunarMonthDays(year, m);
|
||||
if (remaining < daysInMonth) {
|
||||
lunarMonth = m;
|
||||
lunarDay = remaining + 1;
|
||||
break;
|
||||
}
|
||||
remaining -= daysInMonth;
|
||||
|
||||
// 处理闰月
|
||||
if (m === leapMonth && !isLeap) {
|
||||
m--;
|
||||
// 确定农历月日
|
||||
for (; lunarMonth < 13 && offset > 0; lunarMonth++) {
|
||||
// 闰月
|
||||
if (leapMonth > 0 && lunarMonth === leapMonth + 1 && !isLeap) {
|
||||
lunarMonth--;
|
||||
isLeap = true;
|
||||
daysInMonth = getLeapMonthDays(year);
|
||||
if (remaining < daysInMonth) {
|
||||
lunarMonth = m + 1;
|
||||
lunarDay = remaining + 1;
|
||||
isLeap = true;
|
||||
break;
|
||||
}
|
||||
remaining -= daysInMonth;
|
||||
daysOfMonth = getLeapMonthDays(lunarYear);
|
||||
} else {
|
||||
daysOfMonth = getLunarMonthDays(lunarYear, lunarMonth);
|
||||
}
|
||||
|
||||
if (offset < daysOfMonth) break;
|
||||
offset -= daysOfMonth;
|
||||
|
||||
// 解除闰月标记
|
||||
if (isLeap && lunarMonth === leapMonth + 1) isLeap = false;
|
||||
}
|
||||
|
||||
// offset 为 0 且恰好是闰月边界
|
||||
if (offset === 0 && leapMonth > 0 && lunarMonth === leapMonth + 1) {
|
||||
if (isLeap) {
|
||||
isLeap = false;
|
||||
} else {
|
||||
isLeap = true;
|
||||
lunarMonth--;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset < 0) {
|
||||
offset += daysOfMonth;
|
||||
lunarMonth--;
|
||||
}
|
||||
|
||||
return {
|
||||
year,
|
||||
year: lunarYear,
|
||||
month: lunarMonth,
|
||||
day: lunarDay,
|
||||
isLeap: isLeap && lunarMonth === leapMonth
|
||||
day: offset + 1,
|
||||
isLeap
|
||||
};
|
||||
}
|
||||
|
||||
/** 农历转公历 */
|
||||
/** 农历转公历(标准算法,基准 1900-01-31 = 农历1900年正月初一) */
|
||||
function lunarToSolar(year, month, day, isLeap = false) {
|
||||
if (year < 1900 || year > 2100) return null;
|
||||
|
||||
// 累计从 1900 年正月初一到目标农历日期的天数
|
||||
let offset = 0;
|
||||
for (let y = 1900; y < year; y++) {
|
||||
offset += getLunarYearDays(y);
|
||||
}
|
||||
|
||||
const leapMonth = getLeapMonth(year);
|
||||
let days = 0;
|
||||
|
||||
// 累计当年 1 月到目标月前一月的 days
|
||||
for (let m = 1; m < month; m++) {
|
||||
days += getLunarMonthDays(year, m);
|
||||
if (m === leapMonth && !isLeap) {
|
||||
days += getLeapMonthDays(year);
|
||||
offset += getLunarMonthDays(year, m);
|
||||
// 经过闰月(在 m 月之后)
|
||||
if (m === leapMonth) {
|
||||
offset += getLeapMonthDays(year);
|
||||
}
|
||||
}
|
||||
|
||||
// 目标月本身是闰月:需先加上该月正常月天数
|
||||
if (isLeap && month === leapMonth) {
|
||||
days += getLunarMonthDays(year, month);
|
||||
offset += getLunarMonthDays(year, month);
|
||||
}
|
||||
|
||||
days += day - 1;
|
||||
offset += days;
|
||||
offset += day - 1;
|
||||
|
||||
// 从1900年1月31日(农历正月初一)开始计算
|
||||
let solarYear = 1900;
|
||||
let solarMonth = 1;
|
||||
let solarDay = 31;
|
||||
|
||||
let remaining = offset;
|
||||
while (remaining > 0) {
|
||||
const daysInMonth = getSolarMonthDays(solarYear, solarMonth);
|
||||
if (remaining < daysInMonth) {
|
||||
solarDay = remaining + 1;
|
||||
break;
|
||||
}
|
||||
remaining -= daysInMonth;
|
||||
solarMonth++;
|
||||
if (solarMonth > 12) {
|
||||
solarMonth = 1;
|
||||
solarYear++;
|
||||
}
|
||||
}
|
||||
|
||||
return { year: solarYear, month: solarMonth, day: solarDay };
|
||||
// 基准日 1900-01-31 + offset
|
||||
const baseMs = Date.UTC(1900, 0, 31);
|
||||
const target = new Date(baseMs + offset * 86400000);
|
||||
return {
|
||||
year: target.getUTCFullYear(),
|
||||
month: target.getUTCMonth() + 1,
|
||||
day: target.getUTCDate()
|
||||
};
|
||||
}
|
||||
|
||||
/** 获取日期的干支 */
|
||||
@@ -1109,6 +1096,124 @@ function getBuddhistFestival(lunarMonth, lunarDay) {
|
||||
return BUDDHIST_FESTIVALS[`${lunarMonth}-${lunarDay}`] || null;
|
||||
}
|
||||
|
||||
/** 判断是否为法定节假日 */
|
||||
function getLegalHoliday(year, month, day) {
|
||||
const solarKey = `${month}-${day}`;
|
||||
if (LEGAL_HOLIDAYS_SOLAR[solarKey]) return LEGAL_HOLIDAYS_SOLAR[solarKey];
|
||||
const lunar = solarToLunar(year, month, day);
|
||||
if (lunar) {
|
||||
const lunarKey = `${lunar.month}-${lunar.day}`;
|
||||
if (LEGAL_HOLIDAYS_LUNAR[lunarKey]) return LEGAL_HOLIDAYS_LUNAR[lunarKey];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 计算某天是星期几(0=周日) */
|
||||
function getWeekDay(year, month, day) {
|
||||
return new Date(year, month - 1, day).getDay();
|
||||
}
|
||||
|
||||
/** 判断是否为工作日(周一到周五且非法定节假日) */
|
||||
function isWorkday(year, month, day) {
|
||||
if (getLegalHoliday(year, month, day)) return false;
|
||||
const wd = getWeekDay(year, month, day);
|
||||
return wd >= 1 && wd <= 5;
|
||||
}
|
||||
|
||||
/** 农历转公历(某年) */
|
||||
function lunarFestivalToSolar(year, lunarMonth, lunarDay) {
|
||||
// 除夕特殊:腊月最后一天
|
||||
if (lunarMonth === 12 && lunarDay === 30) {
|
||||
const daysInMonth = getLunarMonthDays(year, 12);
|
||||
const leap = getLeapMonth(year);
|
||||
if (leap === 12) {
|
||||
return lunarToSolar(year, 12, daysInMonth, true);
|
||||
}
|
||||
return lunarToSolar(year, 12, daysInMonth, false);
|
||||
}
|
||||
return lunarToSolar(year, lunarMonth, lunarDay, false);
|
||||
}
|
||||
|
||||
/** 计算某农历节日在公历 year 年的日期 */
|
||||
function getFestivalSolarDate(year, fest) {
|
||||
if (fest.type === 'solar') {
|
||||
const [m, d] = fest.date.split('-').map(Number);
|
||||
return { year, month: m, day: d };
|
||||
}
|
||||
const [lm, ld] = fest.date.split('-').map(Number);
|
||||
// 农历节日可能落在上一公历年(如春节在1-2月)
|
||||
// 先按当前公历年算
|
||||
let solar = lunarFestivalToSolar(year, lm, ld);
|
||||
if (!solar) return null;
|
||||
return solar;
|
||||
}
|
||||
|
||||
/** 计算两个日期相差天数(target - from) */
|
||||
function daysBetween(fromY, fromM, fromD, toY, toM, toD) {
|
||||
const a = new Date(fromY, fromM - 1, fromD);
|
||||
const b = new Date(toY, toM - 1, toD);
|
||||
return Math.round((b - a) / (24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
/** 获取即将到来的 N 个节日(含今天之后的) */
|
||||
function getUpcomingFestivals(count = 6) {
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth() + 1;
|
||||
const d = now.getDate();
|
||||
const list = [];
|
||||
|
||||
for (const fest of FESTIVAL_QUICK_SEARCH) {
|
||||
// 尝试今年和明年
|
||||
for (const tryYear of [y, y + 1]) {
|
||||
const solar = getFestivalSolarDate(tryYear, fest);
|
||||
if (!solar) continue;
|
||||
const diff = daysBetween(y, m, d, solar.year, solar.month, solar.day);
|
||||
if (diff >= 0) {
|
||||
list.push({
|
||||
name: fest.name,
|
||||
solarDate: `${solar.year}-${String(solar.month).padStart(2, '0')}-${String(solar.day).padStart(2, '0')}`,
|
||||
solarYear: solar.year,
|
||||
solarMonth: solar.month,
|
||||
solarDay: solar.day,
|
||||
daysLeft: diff
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.sort((a, b) => a.daysLeft - b.daysLeft);
|
||||
return list.slice(0, count);
|
||||
}
|
||||
|
||||
/** 获取下一个佛教节日及倒计时 */
|
||||
function getNextBuddhistFestival() {
|
||||
const now = new Date();
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth() + 1;
|
||||
const d = now.getDate();
|
||||
|
||||
const entries = Object.entries(BUDDHIST_FESTIVALS);
|
||||
const list = [];
|
||||
|
||||
for (const [key, name] of entries) {
|
||||
const [lm, ld] = key.split('-').map(Number);
|
||||
for (const tryYear of [y, y + 1]) {
|
||||
const solar = lunarFestivalToSolar(tryYear, lm, ld);
|
||||
if (!solar) continue;
|
||||
const diff = daysBetween(y, m, d, solar.year, solar.month, solar.day);
|
||||
if (diff >= 0) {
|
||||
list.push({ name, solarDate: `${solar.year}-${String(solar.month).padStart(2, '0')}-${String(solar.day).padStart(2, '0')}`, daysLeft: diff });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list.sort((a, b) => a.daysLeft - b.daysLeft);
|
||||
return list[0] || null;
|
||||
}
|
||||
|
||||
/** 神煞分析 */
|
||||
function analyzeShensha(bazi) {
|
||||
const result = [];
|
||||
@@ -1245,6 +1350,13 @@ module.exports = {
|
||||
calculatePlumBlossom,
|
||||
calculateBoneWeight,
|
||||
getBuddhistFestival,
|
||||
getLegalHoliday,
|
||||
isWorkday,
|
||||
getWeekDay,
|
||||
getFestivalSolarDate,
|
||||
getUpcomingFestivals,
|
||||
getNextBuddhistFestival,
|
||||
daysBetween,
|
||||
analyzeShensha,
|
||||
analyzeFortuneGanzhi,
|
||||
getYearMonths,
|
||||
|
||||
Reference in New Issue
Block a user