首页
/ ant-design Calendar 日历组件实战:从 cellRender 渲染管线到语义化 DOM 的完整解析

ant-design Calendar 日历组件实战:从 cellRender 渲染管线到语义化 DOM 的完整解析

2026-09-06 17:59:13作者:裘晴惠Vivianne

Calendar 是 ant-design「数据展示」分组中用于按日历形式呈现数据的容器组件,适用于日程、课表、价格日历、农历展示等场景。本文基于 ant-design 仓库中 Calendar 官方文档 的完整 API 与全部官方示例,结合 核心实现文件 generateCalendar.tsx头部实现文件 Header.tsx 的源码,讲清楚每个参数的实际行为、渲染优先级与事件触发时机,帮助你掌握从基础用法到自定义头部、农历渲染、语义化类名定制的完整能力。

何时使用

当数据是日期或按照日期划分时,例如日程、课表、价格日历等,或者需要展示农历等,适合使用 Calendar。组件目前支持年/月两种面板模式切换。官方示例覆盖了以下典型场景:

快速上手与受控机制

官方文档给出的最小用法:

// 默认语言为 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/pickerPickerPanel 封装的:generateCalendar.tsx 通过 generateConfig(默认使用 @rc-component/picker/generate/dayjs 的 dayjs 配置,见 index.tsx)生成组件,内部渲染一个 hideHeaderRCPickerPanel,再自行渲染头部与网格。值与模式都是受控/非受控混合状态:

// 简化自 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,两者的优先级完全一致:

  1. fullCellRender 优先:返回值直接替换整个单元格(包括日期数字本身);
  2. 其次是已废弃的 dateFullCellRender / monthFullCellRender(源码中有 @deprecated Please use fullCellRender instead 标注,开发模式下使用会触发 deprecation warning);
  3. 都不提供时,默认渲染一个日期数字区(-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-typescriptLunarHolidayUtil 计算农历、节气与节假日,再通过 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 });
};

由此可以得到三个实用的行为结论:

  1. onPanelChange 不是每次点击都触发,只有当新日期与当前日期「跨月(月面板)或跨年(年面板)」时才会触发,同一天重复点击也不会触发 onChange
  2. onSelect 携带来源信息source 的四种取值对应不同的点击位置:
    • 'date':月面板中点击具体日期格;
    • 'month':年面板中点击某个月份;
    • 'year' / 'month':点击默认头部里的年份 / 月份下拉选择器(见 Header.tsxYearSelectMonthSelect 分别以 'year''month' 调用 onChange);
    • 'customize':通过 headerRender 自定义头部里调用 onChange 触发的选择。

这正是官方 FAQ「如何仅获取来自面板点击的日期」的答案:

<Calendar
  onSelect={(date, { source }) => {
    if (source === 'date') {
      console.log('Panel Select:', source);
    }
  }}
/>

如果你希望「选择」只响应面板点击而忽略头部选择器的联动,过滤 source 即可。

默认头部结构与 headerRender 自定义

不传 headerRender 时,头部由 Header.tsxCalendarHeader 渲染,包含三部分(源码级细节):

  • 年份选择器 YearSelect:基于 antd Select,默认展示当前年往前 10 年、共 20 年(常量 YEAR_SELECT_OFFSET = 10YEAR_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.tsxdemo/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.tsxYearSelectMonthSelect 的实现),适合做「只能预订未来 N 个月」这类业务约束。

展示形态:fullscreen 与 showWeek

  • fullscreen(默认 true):false 时切换为「卡片模式」,根节点类名从 -full 变为 -mini,头部控件自动缩小为 size="small"。官方卡片示例(demo/card.tsx)用 theme.useToken()colorBorderSecondaryborderRadiusLG 给外层加了边框圆角:

    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),因此即使不提供 cellRenderstyles.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
dateFullCellRender 已废弃,>= 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 -

通用属性(如 classNamestylerootClassName)请参考 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 文案生效,两者都要配置。

如何仅获取来自面板点击的日期

使用 onSelectinfo.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 收窄默认选择器)、日期约束validRangedisabledDate 叠加禁用)、语义化定制(6.0.0 的 classNames / styles 覆盖 root/header/body/content/item/itemContent 六类节点)。配合 onSelectsource 来源信息与 dayjs 全局 locale 的正确初始化,即可覆盖日程、价格日历、农历等绝大多数日期展示业务。

登录后查看全文
热门项目推荐
相关项目推荐