React 日历组件完全指南:从网格生成到农历转换
本文详细介绍如何从零实现一个功能完整的 React 日历组件,包括日历网格生成、农历显示和月份切换功能。
前言
在开发排班管理系统时,我们需要实现一个功能完整的日历组件。这个组件不仅要显示标准的月历网格,还要支持农历显示和流畅的月份切换。经过实践,我总结了一套完整的实现方案,适用于任何 React 项目。
一、日历网格生成
1.1 核心需求
一个标准的月历网格需要满足以下要求:
- 显示当前月份的所有日期
- 补齐上月末尾的日期(填充第一周)
- 补齐下月开头的日期(填充最后一周)
- 总是显示完整的 6 周(42 天)
- 周日为每周的第一天
1.2 实现思路
我们使用 date-fns 库来处理日期计算,整个算法分为三个步骤:
// DateService.ts
getMonthCalendarGrid(date: Date): Date[] {
// Step 1: 获取月份的起止日期
const monthStart = startOfMonth(date);
const monthEnd = endOfMonth(date);
// Step 2: 扩展到完整的周
const calendarStart = startOfWeek(monthStart, { weekStartsOn: 0 });
const calendarEnd = endOfWeek(monthEnd, { weekStartsOn: 0 });
// Step 3: 生成日期数组
return eachDayOfInterval({
start: calendarStart,
end: calendarEnd
});
}
关键点解析:
-
获取月份边界:使用
startOfMonth和endOfMonth获取当月的第一天和最后一天 -
扩展到完整周:使用
startOfWeek和endOfWeek确保日历从周日开始,到周六结束 -
生成连续日期:使用
eachDayOfInterval生成两个日期之间的所有日期
1.3 实际案例
以 2024 年 11 月为例:
输入:new Date(2024, 10, 15) // 2024-11-15
Step 1: 月份边界
monthStart = 2024-11-01 (周五)
monthEnd = 2024-11-30 (周六)
Step 2: 扩展到周
calendarStart = 2024-10-27 (周日)
calendarEnd = 2024-11-30 (周六)
Step 3: 生成日期
共 35 天 (5周)
渲染结果:
日 一 二 三 四 五 六
27 28 29 30 31 1 2 ← 10月27-31 + 11月1-2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
1.4 为什么是 6 周?
大多数月份只需要 5 周(35 天)就能显示完整,但某些特殊情况需要 6 周(42 天)。
需要 6 周的条件:
- 月份有 31 天
- 月初是周六(需要补充前面 6 天)
为了保持布局一致性,我们统一使用 6 周布局,这样月份切换时高度不变,动画过渡更流畅。
二、农历(阴历)显示
2.1 实现原理
农历转换使用预定义数据表 + 算法计算的方式,无需外部依赖,支持 1900-2100 年。
2.2 数据结构
农历信息表
private static lunarInfo = [
0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, // 1900-1904
// ... 共 201 个元素(1900-2100年)
];
每个十六进制数编码了一年的农历信息:
0x04bd8 的二进制表示:
0000 0100 1011 1101 1000
解析:
├─ 后 4 位 (1000 = 8):闰月位置(8月)
├─ 第 5 位 (1):闰月天数(1=30天,0=29天)
└─ 第 6-17 位:每月天数(1=30天,0=29天)
农历日期文本
private static lunarDays = [
'初一', '初二', '初三', ..., '廿九', '三十'
];
2.3 转换算法
公历转农历分为四个步骤:
Step 1: 计算与基准日期的天数差
基准:1900-01-31(农历1900年正月初一)
Step 2: 从1900年开始,逐年累减天数,确定农历年份
Step 3: 逐月累减天数,确定农历月份(处理闰月)
Step 4: 剩余天数 + 1 = 农历日期
2.4 实际案例
以 2024-11-24 为例:
Step 1: 天数差
(2024-11-24 - 1900-01-31) = 45590 天
Step 2: 确定农历年
1900年:354天,剩余 45236天
1901年:354天,剩余 44882天
...
2023年:384天,剩余 324天
→ 农历2024年
Step 3: 确定农历月
正月:30天,剩余 294天
二月:29天,剩余 265天
...
十月:30天,剩余 29天
→ 农历十月
Step 4: 确定农历日
29 + 1 = 30
→ 三十
结果:2024-11-24 = 农历2024年十月三十
2.5 使用方法
// 获取农历日期文本
const lunarText = LunarUtil.getLunarDateText(new Date(2024, 10, 24));
console.log(lunarText); // 输出:三十
// 在日历中应用
<div className="day-cell">
<div className="day-text">{date.getDate()}</div>
<div className="lunar-text">
{LunarUtil.getLunarDateText(date)}
</div>
</div>
渲染效果:
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┐
│ 24 │ 25 │ 26 │ 27 │ 28 │ 29 │ 30 │
│廿四 │廿五 │廿六 │廿七 │廿八 │廿九 │三十 │
└─────┴─────┴─────┴─────┴─────┴─────┴─────┘
三、月份切换功能
3.1 核心思路
月份切换的本质是改变当前显示的月份,然后重新生成日历网格。
核心要素:
- 维护一个
currentDate状态 - 提供切换方法(上一月/下一月)
- 根据
currentDate重新生成日历网格
3.2 状态管理
const [currentDate, setCurrentDate] = useState(new Date());
currentDate 的作用:
- 决定显示哪个月份
- 作为生成日历网格的输入
3.3 切换方法
使用 date-fns 实现月份切换:
import { addMonths, subMonths } from 'date-fns';
// 下一个月
const goToNextMonth = () => {
setCurrentDate(prevDate => addMonths(prevDate, 1));
};
// 上一个月
const goToPrevMonth = () => {
setCurrentDate(prevDate => subMonths(prevDate, 1));
};
// 通用方法
const handleMonthChange = (direction: 'next' | 'prev') => {
setCurrentDate(prevDate => {
return direction === 'next'
? addMonths(prevDate, 1)
: subMonths(prevDate, 1);
});
};
3.4 自动处理边界
JavaScript Date 构造函数会自动处理月份溢出:
// 12月 → 1月(跨年)
new Date(2024, 12, 1) // 自动变为 2025-01-01
// 1月 → 12月(跨年)
new Date(2024, -1, 1) // 自动变为 2023-12-01
3.5 响应式更新
使用 useMemo 实现响应式更新:
const MonthView: React.FC<MonthViewProps> = ({ currentDate }) => {
const currentMonthDates = useMemo(() => {
return DateService.getMonthCalendarGrid(currentDate);
}, [currentDate]); // 依赖 currentDate
// currentDate 变化 → useMemo 重新计算 → 生成新的日历网格
};
3.6 完整数据流
用户点击"下一月"
↓
setCurrentDate(新月份)
↓
useMemo 重新计算
↓
生成新的日历网格
↓
渲染新月份
四、完整实现
4.1 日历组件
import React, { useState, useMemo } from 'react';
import { addMonths, subMonths, format } from 'date-fns';
function Calendar() {
const [currentDate, setCurrentDate] = useState(new Date());
// 切换月份
const handleMonthChange = (direction: 'next' | 'prev') => {
setCurrentDate(prev => {
return direction === 'next'
? addMonths(prev, 1)
: subMonths(prev, 1);
});
};
// 生成日历网格
const dates = useMemo(() => {
return generateCalendarGrid(currentDate);
}, [currentDate]);
return (
<div className="calendar">
{/* 标题 */}
<h2>{format(currentDate, 'yyyy年MM月')}</h2>
{/* 切换按钮 */}
<button onClick={() => handleMonthChange('prev')}>上一月</button>
<button onClick={() => handleMonthChange('next')}>下一月</button>
{/* 日历网格 */}
<CalendarGrid dates={dates} />
</div>
);
}
4.2 渲染网格
function CalendarGrid({ dates }) {
// 分组为周
const weeks = [];
for (let i = 0; i < dates.length; i += 7) {
weeks.push(dates.slice(i, i + 7));
}
return (
<div className="calendar-grid">
{/* 星期头部 */}
<div className="week-header">
{['日', '一', '二', '三', '四', '五', '六'].map(day => (
<div key={day} className="week-day">{day}</div>
))}
</div>
{/* 日历网格 */}
{weeks.map((week, weekIndex) => (
<div key={weekIndex} className="week-row">
{week.map((date, dayIndex) => (
<div key={dayIndex} className="day-cell">
{/* 公历日期 */}
<div className="day-text">{date.getDate()}</div>
{/* 农历日期 */}
<div className="lunar-text">
{LunarUtil.getLunarDateText(date)}
</div>
</div>
))}
</div>
))}
</div>
);
}
五、性能优化
5.1 使用 useMemo 缓存计算
// 缓存日历网格
const dates = useMemo(() => {
return generateCalendarGrid(currentDate);
}, [currentDate]);
5.2 使用 useCallback 缓存回调
const handleDatePress = useCallback((date: Date) => {
onDatePress(date);
}, [onDatePress]);
5.3 使用 React.memo 避免无效渲染
export default React.memo(MonthView);
六、关键要点总结
6.1 日历网格生成
核心 API:
-
startOfMonth/endOfMonth- 获取月份边界 -
startOfWeek/endOfWeek- 扩展到完整周 -
eachDayOfInterval- 生成连续日期
数据结构:
Date[] (35-42个元素)
↓ 分组
Date[][] (5-6个数组,每个7个元素)
↓ 渲染
6行 × 7列的网格
6.2 农历转换
核心算法:
- 计算天数差 - 与基准日期(1900-01-31)的差值
- 确定农历年 - 逐年累减天数
- 确定农历月 - 逐月累减天数,处理闰月
- 确定农历日 - 剩余天数 + 1
数据结构:
- 预定义表 - 201个十六进制数(1900-2100年)
- 位运算 - 高效解析农历信息
- 文本数组 - 30个农历日期名称
6.3 月份切换
核心流程:
状态变化 → 网格重新生成 → 数据重新加载 → 组件重新渲染
关键技术:
-
useState- 状态管理 -
useMemo- 缓存计算结果 -
useEffect- 监听变化,自动加载数据 - JavaScript Date - 自动处理月份边界
七、总结
通过本文,我们实现了一个功能完整的 React 日历组件,包括:
✅ 标准的月历网格生成(支持 5-6 周布局) ✅ 农历显示(支持 1900-2100 年) ✅ 流畅的月份切换(自动处理跨年) ✅ 响应式数据更新(状态驱动) ✅ 性能优化(useMemo、useCallback、React.memo)
核心思想是利用 date-fns 处理日期计算,使用 React Hooks 实现响应式更新,通过预定义数据表实现农历转换。整个实现简洁高效,易于维护和扩展。
这套方案不仅适用于 Web 应用,也可以轻松移植到 React Native 等其他 React 生态项目中。
希望这篇文章能帮助你理解日历组件的实现原理,并应用到自己的项目中。
相关资源: