ant-design Calendar 日历组件实战:从 cellRender 渲染管线到语义化 DOM 的完整解析
Calendar 是 ant-design「数据展示」分组中用于按日历形式呈现数据的容器组件,适用于日程、课表、价格日历、农历展示等场景。本文基于 ant-design 仓库中 Calendar 官方文档 的完整 API 与全部官方示例,结合 核心实现文件 generateCalendar.tsx 和 头部实现文件 Header.tsx 的源码,讲清楚每个参数的实际行为、渲染优先级与事件触发时机,帮助你掌握从基础用法到自定义头部、农历渲染、语义化类名定制的完整能力。
何时使用
当数据是日期或按照日期划分时,例如日程、课表、价格日历等,或者需要展示农历等,适合使用 Calendar。组件目前支持年/月两种面板模式切换。官方示例覆盖了以下典型场景:
- 基本:默认日历(demo/basic.tsx)
- 通知事项日历:在日期单元格内展示事件列表(demo/notice-calendar.tsx)
- 跨日期事件:事件横跨多个日期(demo/event-range.tsx)
- 卡片模式:非全屏的紧凑日历(demo/card.tsx)
- 选择功能:响应日期选择(demo/select.tsx)
- 农历日历:叠加农历、节气、节假日(demo/lunar.tsx)
- 周数(5.23.0 起):显示周数列(demo/week.tsx)
- 自定义头部(demo/customize-header.tsx)
- 自定义语义结构的样式和类(6.0.0 起,demo/style-class.tsx)
- 组件 Token(demo/component-token.tsx)
快速上手与受控机制
官方文档给出的最小用法:
// 默认语言为 en-US,所以如果需要使用其他语言,推荐在入口文件全局设置 locale
// import dayjs from 'dayjs';
// import 'dayjs/locale/zh-cn';
// dayjs.locale('zh-cn');
<Calendar cellRender={cellRender} onPanelChange={onPanelChange} onSelect={onSelect} />
注意官方特别强调:Calendar 部分 locale 是从 value 中读取的,所以必须先正确设置 dayjs 的 locale,否则头部显示的年月文案可能与 locale 配置不一致。
从源码结构看,Calendar 是基于 @rc-component/picker 的 PickerPanel 封装的:generateCalendar.tsx 通过 generateConfig(默认使用 @rc-component/picker/generate/dayjs 的 dayjs 配置,见 index.tsx)生成组件,内部渲染一个 hideHeader 的 RCPickerPanel,再自行渲染头部与网格。值与模式都是受控/非受控混合状态:
// 简化自 components/calendar/generateCalendar.tsx
const [mergedValue, setMergedValue] = useControlledState(
() => defaultValue || generateConfig.getNow(), // 初始值:defaultValue 或当前时间
value,
);
const [mergedMode, setMergedMode] = useControlledState<CalendarMode>('month', mode);
也就是说 value 存在时完全受控,不传 value 时组件内部用 defaultValue(或今天)自管理状态。日历根节点会根据 fullscreen 添加 -full / -mini 类名,根据 direction === 'rtl' 添加 -rtl 类名。
mode(初始模式,month | year,默认 month)与内部面板模式并非一一对应,源码中做了映射:
const panelMode = React.useMemo<'month' | 'date'>(
() => (mergedMode === 'year' ? 'month' : 'date'),
[mergedMode],
);
即「年」模式实际渲染 12 个月份的网格(rc-picker 的 month 面板),「月」模式渲染日期网格。
单元格渲染:cellRender 与 fullCellRender 的优先级
这是 Calendar 最核心的定制能力。两者回调签名相同:
function(
current: Dayjs,
info: {
prefixCls: string;
originNode: React.ReactElement;
today: Dayjs;
range?: 'start' | 'end';
type: PanelMode; // 'date' | 'month' 等
locale?: Locale;
subType?: 'hour' | 'minute' | 'second' | 'meridiem';
}
): React.ReactNode
区别在于渲染粒度。从 generateCalendar.tsx 的实现看,日期单元格走 dateRender、月份单元格走 monthRender,两者的优先级完全一致:
fullCellRender优先:返回值直接替换整个单元格(包括日期数字本身);- 其次是已废弃的
dateFullCellRender/monthFullCellRender(源码中有@deprecated Please use fullCellRender instead标注,开发模式下使用会触发 deprecation warning); - 都不提供时,默认渲染一个日期数字区(
-date-value)+ 内容区(-date-content),此时cellRender只替换内容区,默认还会根据是否与今天相同来添加-date-today高亮类名。
开发模式下,源码会对 4 个旧 API 统一发出废弃警告:
[
['dateFullCellRender', 'fullCellRender'],
['dateCellRender', 'cellRender'],
['monthFullCellRender', 'fullCellRender'],
['monthCellRender', 'cellRender'],
].forEach(([deprecatedName, newName]) => {
warning.deprecated(!(deprecatedName in props), deprecatedName, newName);
});
所以 5.4.0 之后的项目应统一迁移到 cellRender / fullCellRender。
通知事项日历示例
官方「通知事项日历」示例展示了 cellRender 的标准写法——按 info.type 区分日期格与月份格,未覆盖的分支返回 info.originNode 以保持默认样式(完整代码见 demo/notice-calendar.tsx):
const cellRender: CalendarProps<Dayjs>['cellRender'] = (current, info) => {
if (info.type === 'date') {
return dateCellRender(current); // 渲染 Badge 事件列表
}
if (info.type === 'month') {
return monthCellRender(current); // 渲染该月统计数字
}
return info.originNode;
};
return <Calendar cellRender={cellRender} />;
农历日历:fullCellRender + cloneElement 的进阶用法
「农历日历」示例(demo/lunar.tsx)是 fullCellRender 的典型应用:它借助第三方库 lunar-typescript 的 Lunar、HolidayUtil 计算农历、节气与节假日,再通过 React.cloneElement(info.originNode, { ... }) 在保留默认单元格 DOM 结构的前提下替换类名与子节点,从而同时拿到默认布局和自定义内容:
const cellRender: CalendarProps<Dayjs>['fullCellRender'] = (date, info) => {
const d = Lunar.fromDate(date.toDate());
const lunar = d.getDayInChinese();
const solarTerm = d.getJieQi();
if (info.type === 'date') {
return React.cloneElement(info.originNode, {
className: clsx(styles.dateCell, {
[styles.current]: selectDate.isSame(date, 'date'),
[styles.today]: date.isSame(dayjs(), 'date'),
}),
children: (
<div className={styles.text}>
<span>{date.get('date')}</span>
<div className={styles.lunar}>{displayHoliday || solarTerm || lunar}</div>
</div>
),
});
}
// info.type === 'month' 时渲染「1月(正月)」这样的农历月份标签
};
该示例还搭配了 fullscreen={false} 卡片模式和 headerRender 自定义头部(年/月选择器展示农历干支与生肖),是三者联动的完整参考。
选择功能与事件体系:onSelect 的 source 来源
Calendar 有三个事件:onChange(date)、onPanelChange(date, mode)、onSelect(date, info)。它们各自的触发时机在 generateCalendar.tsx 中定义得非常清晰:
const triggerChange = (date: DateType) => {
setMergedValue(date);
if (!isSameDate(date, mergedValue, generateConfig)) {
// 月面板切换月份、或年面板切换年份时才会触发 onPanelChange
if (
(panelMode === 'date' && !isSameMonth(date, mergedValue, generateConfig)) ||
(panelMode === 'month' && !isSameYear(date, mergedValue, generateConfig))
) {
triggerPanelChange(date, mergedMode);
}
onChange?.(date);
}
};
const onInternalSelect = (date: DateType, source: SelectInfo['source']) => {
triggerChange(date);
onSelect?.(date, { source });
};
由此可以得到三个实用的行为结论:
onPanelChange不是每次点击都触发,只有当新日期与当前日期「跨月(月面板)或跨年(年面板)」时才会触发,同一天重复点击也不会触发onChange;onSelect携带来源信息,source的四种取值对应不同的点击位置:'date':月面板中点击具体日期格;'month':年面板中点击某个月份;'year'/'month':点击默认头部里的年份 / 月份下拉选择器(见 Header.tsx 中YearSelect、MonthSelect分别以'year'、'month'调用onChange);'customize':通过headerRender自定义头部里调用onChange触发的选择。
这正是官方 FAQ「如何仅获取来自面板点击的日期」的答案:
<Calendar
onSelect={(date, { source }) => {
if (source === 'date') {
console.log('Panel Select:', source);
}
}}
/>
如果你希望「选择」只响应面板点击而忽略头部选择器的联动,过滤 source 即可。
默认头部结构与 headerRender 自定义
不传 headerRender 时,头部由 Header.tsx 的 CalendarHeader 渲染,包含三部分(源码级细节):
- 年份选择器
YearSelect:基于 antdSelect,默认展示当前年往前 10 年、共 20 年(常量YEAR_SELECT_OFFSET = 10、YEAR_SELECT_TOTAL = 20);若设置了validRange,选项会被限制在范围的起止年份内,且跨年切换时会把月份钳制到允许范围内(避免选中范围外的月份); - 月份选择器
MonthSelect:仅当mode === 'month'时渲染,12 个月份(受validRange同年范围约束); - 模式切换器
ModeSwitch:基于Radio.Group的「月 / 年」切换,文案取自locale.month/locale.year。
传入 headerRender 则整体替换头部,回调参数为:
function(
object: {
value: Dayjs; // 当前受控/内部日期
type: 'year' | 'month'; // 当前模式
onChange: (date: Dayjs) => void; // 触发选择,source 为 'customize'
onTypeChange: (type: 'year' | 'month') => void; // 切换模式
}
) => React.ReactNode
完整示例见 demo/customize-header.tsx;demo/lunar.tsx 中则展示了如何用 Row/Col + Select + Radio.Group 重排头部布局。
日期约束:disabledDate 与 validRange
两个约束属性在源码中合并为同一个判定函数(generateCalendar.tsx):
const mergedDisabledDate = React.useCallback(
(date: DateType) => {
const notInRange = validRange
? generateConfig.isAfter(validRange[0], date) ||
generateConfig.isAfter(date, validRange[1])
: false;
return notInRange || !!disabledDate?.(date);
},
[disabledDate, validRange],
);
即:validRange(可显示日期区间,[Dayjs, Dayjs])超出即禁用,且与用户自定义的 disabledDate(currentDate) => boolean 是「或」关系。文档同时提醒:使用 disabledDate 时不要直接修改传入的 currentDate 对象。
如前所述,validRange 的影响不止于禁用单元格:默认头部的年份下拉选项、月份下拉选项,以及跨年/跨月时的日期钳制逻辑,都会根据 validRange 收窄(见 Header.tsx 中 YearSelect 与 MonthSelect 的实现),适合做「只能预订未来 N 个月」这类业务约束。
展示形态:fullscreen 与 showWeek
-
fullscreen(默认true):false时切换为「卡片模式」,根节点类名从-full变为-mini,头部控件自动缩小为size="small"。官方卡片示例(demo/card.tsx)用theme.useToken()的colorBorderSecondary、borderRadiusLG给外层加了边框圆角:const { token } = theme.useToken(); const wrapperStyle: React.CSSProperties = { width: 300, border: `${token.lineWidth}px ${token.lineType} ${token.colorBorderSecondary}`, borderRadius: token.borderRadiusLG, }; return ( <div style={wrapperStyle}> <Calendar fullscreen={false} onPanelChange={onPanelChange} /> </div> ); -
showWeek(5.23.0 起,默认false):显示周数列。源码中该属性直接透传给RCPickerPanel。示例 demo/week.tsx 同时演示了全屏与非全屏两种形态:<Calendar fullscreen showWeek /> <Calendar fullscreen={false} showWeek />
value / defaultValue 与 locale
value:展示日期(Dayjs),受控;defaultValue:非受控初始日期,默认今天;locale:国际化配置对象,默认值来自 components/calendar/locale/en_US.ts,并会与全局 ConfigProvider 的Calendar.locale合并(源码:merge(contextLocale, props.locale || {}))。日期类组件的完整国际化配置方式可参考 DatePicker 文档 中的「国际化配置」章节。
再次强调文档中的注意事项:因为部分 locale 文案直接从 value(dayjs 实例)读取,使用非英文 locale 时务必先 dayjs.locale('zh-cn')(入口文件全局设置),这是 FAQ「为什么时间类组件的国际化 locale 设置不生效」问题的根源之一。
语义化 DOM:classNames 与 styles(6.0.0)
6.0.0 起,Calendar 支持通过 classNames / styles 属性(支持对象或 (info: { props }) => Record<...> 函数)定制内部各语义化结构的类名和行内样式,类型定义见 generateCalendar.tsx 中的 CalendarSemanticType,语义结构及说明(参考 demo/_semantic.tsx)如下:
| 语义节点 | 说明 |
|---|---|
root |
根元素:背景色、边框、圆角等基础样式和整体布局结构 |
header |
头部元素:年份选择器、月份选择器、模式切换器的布局和样式控制 |
body |
主体元素:日历表格的内边距、布局控制,用于容纳日历网格 |
content |
内容元素:日历表格的宽度、高度等尺寸控制和表格样式 |
item |
条目元素:日历单元格的背景色、边框、悬停态、选中态等交互样式 |
itemContent |
条目内容元素:单元格内自定义内容区域的高度、溢出等样式控制 |
从源码实现看(generateCalendar.tsx),root / header 会被拆出应用到外层容器与头部节点,其余语义类合并进 RCPickerPanel;其中 itemContent 还额外注入到默认单元格的内容区(-date-content),因此即使不提供 cellRender,styles.itemContent 也能影响默认单元格内容区的样式。这些语义配置会先经过 useMergeSemantic 与 ConfigProvider 的 calendar 全局配置(contextClassNames / contextStyles)合并,再叠加组件自身的 className / style,优先级为:ConfigProvider 全局 → 组件属性。
完整 API 一览
| 参数 | 说明 | 类型 | 默认值 | 版本 |
|---|---|---|---|---|
| cellRender | 自定义单元格的内容(内容区) | function(current: Dayjs, info) => ReactNode |
- | 5.4.0 |
| classNames | 自定义各语义化结构的 class,支持对象或函数 | Record<SemanticDOM, string> | (info) => Record<SemanticDOM, string> |
- | 6.0.0 |
已废弃,>= 5.4.0 请用 fullCellRender |
function(date: Dayjs): ReactNode |
- | < 5.4.0 | |
| fullCellRender | 自定义整个单元格的内容 | function(current: Dayjs, info) => ReactNode |
- | 5.4.0 |
| defaultValue | 默认展示的日期 | Dayjs |
- | - |
| disabledDate | 不可选择的日期,注意不要直接修改参数 | (currentDate: Dayjs) => boolean |
- | - |
| fullscreen | 是否全屏显示 | boolean |
true |
- |
| showWeek | 是否显示周数列 | boolean |
false |
5.23.0 |
| styles | 自定义各语义化结构的行内 style,支持对象或函数 | Record<SemanticDOM, CSSProperties> | (info) => Record<SemanticDOM, CSSProperties> |
- | 6.0.0 |
| headerRender | 自定义头部内容 | function(object: { value: Dayjs, type: 'year' | 'month', onChange: f(), onTypeChange: f() }) |
- | - |
| locale | 国际化配置 | object |
en_US 默认配置 | - |
| mode | 初始模式 | month | year |
month |
- |
| validRange | 设置可以显示的日期 | [Dayjs, Dayjs] |
- | - |
| value | 展示日期(受控) | Dayjs |
- | - |
| onChange | 日期变化回调 | function(date: Dayjs) |
- | - |
| onPanelChange | 日期面板变化回调 | function(date: Dayjs, mode: string) |
- | - |
| onSelect | 选择日期回调,包含来源信息 | function(date: Dayjs, info: { source: 'year' | 'month' | 'date' | 'customize' }) |
info 自 5.6.0 |
- |
通用属性(如 className、style、rootClassName)请参考 ant-design 的 通用属性文档。
FAQ
如何在 Calendar 中使用自定义日期库
参考 ant-design 的 使用自定义日期库 文档中 Calendar 相关章节:源码上 index.tsx 暴露了 Calendar.generateCalendar(generateConfig),可以传入非 dayjs 的 GenerateConfig 生成组件。
如何给日期类组件配置国际化
参考 DatePicker 文档 的「国际化配置」章节:在 ConfigProvider 中设置 locale,同时确保 dayjs.locale() 与之一致。
为什么时间类组件的国际化 locale 设置不生效
常见原因是未同步设置 dayjs 的全局 locale。Calendar 的部分文案(如 value 相关的格式化结果)直接从 dayjs 实例读取,因此 ConfigProvider.locale 生效不等于 dayjs 文案生效,两者都要配置。
如何仅获取来自面板点击的日期
使用 onSelect 的 info.source 过滤,见上文「选择功能与事件体系」一节的示例。
主题变量(Design Token)
Calendar 支持在 ConfigProvider 中通过 theme.components.Calendar 配置组件级 Token(组件 Token),具体 Token 列表可在官方文档页面「主题变量(Design Token)」表格中查看,示例见 demo/component-token.tsx;组件样式实现位于 components/calendar/style/index.ts。
小结
Calendar 的定制能力可以归纳为四条主线:单元格渲染(cellRender 改内容区、fullCellRender 换整个格子、旧 API 一律迁移)、头部定制(headerRender 或依赖 validRange 收窄默认选择器)、日期约束(validRange 与 disabledDate 叠加禁用)、语义化定制(6.0.0 的 classNames / styles 覆盖 root/header/body/content/item/itemContent 六类节点)。配合 onSelect 的 source 来源信息与 dayjs 全局 locale 的正确初始化,即可覆盖日程、价格日历、农历等绝大多数日期展示业务。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00