)
Ant Design Calendar 实战用 cellRender 绘制跨天事件范围Event Range【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-designAnt Design 的 Calendar 组件通过cellRender属性开放了自定义日期格内容的扩展点。本文围绕官方示例 event-range 演示 展开讲解如何根据每个日期判定事件的开始、中间、结束、单日四种范围状态并用带负边距的连续色条把跨天事件如发布窗口、维护窗口绘制成视觉上一条连贯的横幅。读完本文你将掌握cellRender的调用机制、事件范围定位的纯函数写法以及色条圆角/负边距的 CSS 细节可以直接迁移到排期看板、发布日历等场景。一、事件数据模型一个 CalendarEvent 就是一个范围示例源码位于 event-range.tsx。整个方案建立在一条简单的心智模型上事件不是挂在某一天的而是挂在日期区间上的日历的每一格只负责回答我今天和这条事件是什么关系。export interface CalendarEvent { key: string; title: string; start: Dayjs; end: Dayjs; color: string; } const getEvents (token: ReturnTypetypeof theme.useToken[token]): CalendarEvent[] [ { key: release, title: Release window, start: dayjs(2026-01-08), end: dayjs(2026-01-10), color: token.colorPrimary, }, { key: design-review, title: Design review, start: dayjs(2026-01-14), end: dayjs(2026-01-14), color: token.colorSuccess, }, { key: maintenance, title: Maintenance, start: dayjs(2026-01-21), end: dayjs(2026-01-24), color: token.colorWarning, }, { key: bug-fix, title: Bug fix, start: dayjs(2026-01-30), end: dayjs(2026-01-31), color: token.colorError, }, ];几个值得注意的设计start与end都是闭区间端点isBefore/isAfter判定均含边界单日事件的写法就是start end不需要单独的类型字段颜色来自 Design Tokentoken.colorPrimary/colorSuccess/colorWarning/colorError事件颜色自动跟随主题与暗色模式而不是写死十六进制色值事件列表通过React.useMemo(() getEvents(token), [token])缓存token变化如切换主题时重新计算。由于事件都落在 2026 年 1 月组件必须把日历定位到该月否则打开示例一片空白Calendar classNames{{ itemContent: styles.itemContent }} defaultValue{dayjs(2026-01-01)} cellRender{cellRender} /二、核心逻辑两个纯函数判定范围状态cellRender会对面板中的每一个日期格各调用一次因此判定逻辑必须是无状态的纯函数。示例把判定拆成两步const isInRange (current: Dayjs, event: CalendarEvent) { return !current.isBefore(event.start, day) !current.isAfter(event.end, day); }; const getRangePosition (current: Dayjs, event: CalendarEvent) { const starts current.isSame(event.start, day); const ends current.isSame(event.end, day); if (starts ends) { return single; } if (starts) { return start; } if (ends) { return end; } return middle; };isInRange以day为粒度做闭区间比较先把与我无关的日期过滤掉getRangePosition对落在区间内的日期再细分出start/middle/end/single四种位置。注意starts ends的分支必须放在最前面——单日事件同时满足 starts 和 ends若先判starts会把它误标为开端。这两步构成start左圆角、显示标题→middle贯通两端、不显示标题→end右圆角的完整状态机。跨月场景下如果面板跨月middle/end状态自然会延续到下一个月逻辑无需任何额外处理。三、视觉表达负边距 圆角拼出连续色条一条横幅横跨多个日期格的关键技巧在于每个格子只渲染自己那一小段色条再用负边距把相邻格子的色条在视觉上接成一条。示例使用antd-style的createStyles从 CSS 变量读取 Design Tokenconst useStyle createStyles(({ cssVar, css }) { const barRadius 999; const { controlHeight, marginXXS, controlHeightSM, colorTextLightSolid, fontSizeSM, paddingXS, marginXS, paddingXXS, } cssVar; return { itemContent: css overflow: visible; , cell: css min-height: ${controlHeight}; , list: css display: flex; flex-direction: column; gap: ${marginXXS}; margin-top: ${marginXXS}; , bar: css display: block; height: calc(${controlHeightSM} - ${marginXXS}); overflow: hidden; color: ${colorTextLightSolid}; font-size: ${fontSizeSM}; white-space: nowrap; text-overflow: ellipsis; , barStart: css margin-inline-end: calc(-1 * (${paddingXS} ${marginXS} / 2)); padding-inline-start: calc(${paddingXXS} ${paddingXXS}); border-start-start-radius: ${barRadius}px; border-end-start-radius: ${barRadius}px; , barMiddle: css margin-inline: calc(-1 * (${paddingXS} ${marginXS} / 2)); , barEnd: css margin-inline-start: calc(-1 * (${paddingXS} ${marginXS} / 2)); border-start-end-radius: ${barRadius}px; border-end-end-radius: ${barRadius}px; , barSingle: css padding-inline-start: calc(${paddingXXS} ${paddingXXS}); border-radius: ${barRadius}px; , }; });逐项拆解这套样式与日历内部布局的配合关系负边距的数值从哪来。日历格内容-date-content本身带有内边距色条左右各用calc(-1 * (paddingXS marginXS / 2))向两侧伸出恰好抵消单元格内边距与相邻单元格间的间距日历格的水平间距是marginXS / 2见 style/index.ts 中${calendarCls}-date的margin定义。这样start段延伸到本格右边界、middle段同时伸出左右两边、end段补齐左边界三段在像素上首尾相接观感上就是一条完整横幅。圆角只在两端出现。barStart只给左侧两个圆角barEnd只给右侧两个圆角barMiddle无圆角barSingle四角全圆——999的超大半径保证色条两端呈半圆胶囊形。overflow: visible是前提。Calendar 的日期内容区-date-content默认有固定高度与滚动裁剪overflowY: auto见 style/index.ts如果不把itemContent的 overflow 放开向右伸出的负边距部分会被格子裁掉色条就接不上了。这也是示例中classNames{{ itemContent: styles.itemContent }}这一行的真正作用——它通过 6.0 的语义化 DOM 结构Semantic DOM精确命中了内容层。多事件用纵向 flex 堆叠。list是flex-direction: column加gap: marginXXS同一天命中多条事件时色条自上而下排列互不干扰。文案防溢出。bar上用white-space: nowraptext-overflow: ellipsisoverflow: hidden长标题自动省略号标题只在start或single位置渲染避免同一事件文案沿范围重复出现。四、源码视角cellRender 在 Calendar 内部如何被调用cellRender是 Calendar 在 5.4.0 引入的统一单元格渲染扩展点。查看组件实现 generateCalendar.tsx 可以确认其调用链// generateCalendar.tsx 内部节选 const dateRender React.useCallback( (date: DateType, info: CellRenderInfoDateType): React.ReactNode { if (isFunction(fullCellRender)) { return fullCellRender(date, info); } // ... return ( div className{clsx(${prefixCls}-cell-inner, ${calendarPrefixCls}-date, { /* ... */ })} div className{${calendarPrefixCls}-date-value} {String(generateConfig.getDate(date)).padStart(2, 0)} /div div className{clsx(${calendarPrefixCls}-date-content, mergedItemContentClassName)} style{mergedItemContentStyle} {isFunction(cellRender) ? cellRender(date, info) : dateCellRender?.(date)} /div /div ); }, [/* ... */], );由此可以得到几个实现层面的结论cellRender的返回值只填充日期格的内容区-date-content日期数字那一行-date-value仍然由组件渲染如果想要整格接管连日期数字一起覆盖应改用fullCellRender。第二个参数info是CellRenderInfo来自rc-component/picker的CellRenderInfo类型见 generateCalendar.tsx 的导入包含prefixCls、originNode、today、type、locale等字段。示例中的info.type ! date守卫正是用它区分日期格与月份格——cellRender在 year 模式下会被用于月份格monthRender分支见 generateCalendar.tsx若不做类型过滤事件条会出现在不该出现的位置。classNames.itemContent/styles.itemContent会被组件透传。mergedItemContentClassName与mergedItemContentStyle直接挂在内容层的div上上方代码中可见这正是示例能覆盖 overflow 的机制来源完整的语义结构定义见CalendarSemanticTypegenerateCalendar.tsxroot/header/body/content/item/itemContent。旧 API 已弃用。dateCellRender、dateFullCellRender、monthCellRender、monthFullCellRender在开发模式下会触发deprecated警告generateCalendar.tsx官方文档index.en-US.md也明确建议统一使用cellRender/fullCellRender。同目录下的 notice-calendar 示例 展示了cellRender的另一典型用法——按info.type分发dateCellRender/monthCellRender未匹配的分支返回info.originNode以保留默认渲染对比两者可以更清楚地理解info参数的用途。五、完整渲染流程与 cellRender 实现把范围判定与样式组合起来示例的cellRender全貌如下const cellRender React.useCallbackNonNullableCalendarPropsDayjs[cellRender]( (current, info) { if (info.type ! date) { return null; } const currentEvents events.filter((event) isInRange(current, event)); return ( div className{styles.cell} div className{styles.list} {currentEvents.map((event) { const position getRangePosition(current, event); const rangeClassName { start: styles.barStart, middle: styles.barMiddle, end: styles.barEnd, single: styles.barSingle, }[position]; return ( span key{event.key} className{clsx(styles.bar, rangeClassName)} style{{ backgroundColor: event.color }} {position start || position single ? event.title : null} /span ); })} /div /div ); }, [events, styles], );整体数据流可以概括为RCPickerPanel遍历当月或当年的每个日期格逐格调用cellRender(date, info)info.type date过滤出日期格否则返回nullevents.filter(isInRange)得到当天命中的全部事件——注意这里是多条因此渲染的是列表而非单一条目对每条事件用getRangePosition取位置映射到对应的圆角/负边距样式类背景色由内联style{{ backgroundColor: event.color }}提供事件与事件之间互不影响。React.useCallback把cellRender稳定为只依赖events与styles的引用避免面板每次重渲染都拿到新函数引用NonNullableCalendarPropsDayjs[cellRender]则让函数体内省去空值判断类型上锁定为必填版本。六、落地时的注意点结合源码与样式实现把该方案迁移到自己的项目时有几点值得留意负边距数值必须与单元格实际内边距/间距匹配。示例的paddingXS marginXS / 2对应的是当前版本日历格-date节点的padding与margin见 style/index.ts。如果通过 ConfigProvider 或语义化样式修改了日历的间距 Token这套负边距需要同步调整否则色条会出现重叠或留缝。classNames/styles语义化 API 从 6.0 起可用见 index.en-US.md 的属性表低版本可通过全局样式覆盖ant-picker-calendar-date-content的 overflow 达到同样效果。单日事件务必单独分支。若缺少starts ends的优先判断单日事件会被渲染成只有左圆角的start段右端悬空。性能上isInRange是 O(事件数) 的线性过滤每月最多渲染 42 格左右事件量在数百条以内时没有压力若事件量很大可以先按月份建立索引或把当月事件在面板切换时onPanelChange预筛一次。组件文档提示Calendar 的部分 locale 信息会读取value请在全局入口正确设置 dayjs 的 locale见 index.en-US.md 的 Note 与 FAQ。小结这个事件范围示例的核心价值不在于画了几个色条而是示范了一套日历扩展点的标准范式用纯函数把日期与区间的数据关系isInRange/getRangePosition和视觉表达四种圆角/负边距样式类彻底解耦再借cellRender的info.type守卫保证只在正确的格子类型上生效。配合classNames.itemContent放开 overflow就能让每格独立渲染的片段拼接成连贯的跨天横幅——这套模式同样适用于甘特式排期、值班表、发布窗口等任何日期 × 事件的展示场景。更多 Calendar 用法周数显示、迷你模式、语义化 DOM 结构等可参考组件文档 index.en-US.md 与 index.zh-CN.md。【免费下载链接】ant-designAn enterprise-class UI design language and React UI library项目地址: https://gitcode.com/GitHub_Trending/an/ant-design创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考