Mantine Schedule 组件库实战指南:用 @mantine/schedule 构建企业级日历与排期界面
【免费下载链接】mantineA fully featured React components library项目地址: https://gitcode.com/GitHub_Trending/ma/mantine
@mantine/schedule是 Mantine 生态中面向 React 的完整日历/排期组件包,提供日、周、月、年四种视图以及资源视图(Resource Views)与议程视图(Agenda View),内置重复事件(RFC 5545 RRule)、拖拽移动、边缘缩放、时间槽框选、外部拖入等交互能力。本文以该包在 packages/@mantine/schedule 中的真实源码实现为依据,从安装配置、事件数据结构、各视图 API、交互事件到 i18n 与样式定制,系统讲解如何在一个 Mantine 应用中落地一套生产可用的排期界面。
安装与依赖关系
在 Mantine 项目中安装@mantine/schedule需要同时安装它的几个核心依赖包。根据包内 package.json 中的peerDependencies声明,它依赖:
@mantine/core@mantine/dates@mantine/hooksreact(^19.2.0)与react-dom(^19.2.0)
官方 README 提供了两种包管理器安装方式:
# With yarn yarn add @mantine/schedule @mantine/core @mantine/hooks @mantine/dates # With npm npm install @mantine/schedule @mantine/core @mantine/hooks @mantine/dates其中@mantine/dates是必须的,因为 Schedule 系列组件通过useDatesContext()读取DatesProvider提供的 locale、firstDayOfWeek、weekendDays等全局日期配置(见 DayView.tsx 与 WeekView.tsx),组件间通过上下文共享同一套日期语义。
运行时依赖方面,@mantine/schedule还直接依赖rrule(^2.8.1)用于解析与展开 RFC 5545 重复规则。安装后需要确保引入包导出的样式文件(如@mantine/schedule/styles.css,见 package.json 的exports字段),并按 Mantine 文档约定在全局配置中启用 MantineProvider。
包结构与组件全景
从 index.ts 的导出清单可以完整看到包的组成:
- 主入口组件
Schedule:一个可切换四种视图的“一站式”调度器; - 独立视图组件:
DayView、WeekView、MonthView、YearView; - 资源视图:
ResourcesDayView、ResourcesWeekView、ResourcesMonthView、ResourcesSchedule; - 辅助视图:
AgendaView、MobileMonthView; - 底层构件:
ScheduleEvent、ScheduleBackgroundEvent、ScheduleHeader、CurrentTimeIndicator、MoreEvents、DragContext; - 类型、工具函数与标签(i18n)定义。
仓库结构上,components/下每个视图目录都自带同名.module.css、.story.tsx与.test.tsx,并配有独立的布局计算工具目录(如WeekView/get-week-view-events/),便于读者按模块深入阅读实现细节。
事件数据模型:ScheduleEventData
所有视图消费统一的事件数据结构ScheduleEventData,它由三种变体联合而成(定义在 types.ts):
- 单次事件
ScheduleSingleEventData:不含recurrence字段; - 重复系列
ScheduleRecurringSeriesEventData:带recurrence(rrule 规则); - 单次覆盖
ScheduleRecurringOverrideEventData:通过recurringEventId+recurrenceId覆盖系列中的某一次发生。
公共字段(ScheduleEventBase,见 types.ts):
| 字段 | 类型 | 说明 |
|---|---|---|
id | string \| number | 事件唯一标识,用作 key 与识别 |
title | string | 事件标题 |
start/end | Date \| DateTimeStringValue | 开始/结束时间,字符串格式为YYYY-MM-DD HH:mm:ss |
color | MantineColor | theme.colors键名或任意合法 CSS 颜色 |
variant | 'filled' \| 'light' | 事件样式变体,默认light |
display | 'default' \| 'background' | 背景事件以整块色块渲染在普通事件之后,默认非交互 |
payload | Record<PropertyKey, any> | 业务自定义数据,库内部不消费 |
resourceId | string \| number | 资源视图中使用的事件归属 |
时间统一约定:日期字符串为YYYY-MM-DD(DateStringValue),日期时间字符串为YYYY-MM-DD HH:mm:ss(DateTimeStringValue),这是包内所有回调、工具函数与内部计算的通用格式。
一个典型的事件数组示例(取自 Schedule.story.tsx 的真实用法):
import { ScheduleEventData } from '@mantine/schedule'; const events: ScheduleEventData[] = [ { id: 1, title: 'Team Meeting', start: new Date(2024, 0, 15, 10, 0), end: new Date(2024, 0, 15, 11, 0), color: 'blue', payload: {}, }, { id: 2, title: 'Conference Day 1', start: new Date(2024, 0, 15, 0, 0, 0), end: new Date(2024, 0, 15, 23, 59, 59), color: 'cyan', payload: {}, }, ];重复事件(Recurrence)
重复系列通过 RFC 5545 规则描述:
const recurringEvent: ScheduleEventData = { id: 'weekly-standup', title: 'Weekly Standup', start: '2024-01-15 09:00:00', end: '2024-01-15 09:30:00', color: 'blue', recurrence: { rrule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR', exdate: ['2024-01-17 09:00:00'], // 排除特定日期 dtstart: '2024-01-15 09:00:00', // 显式系列起点(可选) }, };运行时会由expandRecurringEvents(见 expand-recurring-events.ts)在渲染前把系列展开为可见范围内的具体发生实例:
- 使用
RRule.parseString解析 rrule 字符串,兼容带RRULE:前缀与多行 iCalendar 文本的情况(getRRuleString); - 通过
rule.between()查询与当前视图范围重叠的发生,并按expansionLimit(默认 2000,见DEFAULT_EXPANSION_LIMIT)截断,防止无限循环; - 生成实例的
id形如{seriesId}::{recurrenceId},并附带recurringInstance元数据(isRecurringInstance、recurringEventId、recurrenceId、originalStart/originalEnd),拖拽/缩放后可用于回写原系列; exdate命中的发生被跳过,若存在对应的 override 事件则替换为该 override。
每个视图组件都在渲染前调用expandRecurringEvents(例如 DayView.tsx),因此无需手工展开。
一站式入口:Schedule 组件
Schedule是面向“快速搭建”的聚合组件(源码见 Schedule.tsx),内部用useUncontrolled同时管理date与view两个状态:既可以传入date/onDateChange、view/onViewChange做受控使用,也可以只传defaultDate(默认今天)与defaultView(默认week)做非受控使用。
import { Schedule } from '@mantine/schedule'; function CalendarPage() { return ( <Schedule defaultDate="2024-01-15" defaultView="week" events={events} withAgenda layout="responsive" /> ); }核心 props 一览(详见 Schedule.tsx):
| Prop | 默认值 | 说明 |
|---|---|---|
view/defaultView | 'week' | 视图级别:day、week、month、year |
date/defaultDate | 今天 | 当前展示日期(受控/非受控) |
events | — | 跨所有视图展示的事件数组 |
layout | 'default' | 'responsive'时小屏自动切换到YearView/MobileMonthView |
mode | 'default' | 'static'时禁用全部事件交互(拖拽、缩放、点击) |
withEventsDragAndDrop | false | 启用事件拖拽 |
withEventResize | false | 启用事件边缘缩放 |
withDragSlotSelect | false | 启用拖拽框选时间段 |
withInteractiveBackgroundEvents | false | 背景事件可点击 |
recurrenceExpansionLimit | 2000 | 每条重复系列最多展开的实例数 |
withAgenda | false | 在 Day/Week/Month 视图头部显示 Agenda 按钮 |
locale、radius、labels | — | 语言、圆角、文案覆盖 |
视图专属配置通过dayViewProps、weekViewProps、monthViewProps、yearViewProps、mobileMonthViewProps透传。Schedule在mode === 'static'时会把withEventsDragAndDrop、withEventResize、withInteractiveBackgroundEvents强制置为false(见 Schedule.tsx),保证只读展示场景下不会有任何交互残留。
视图组件逐个击破
DayView:单日时间轴
DayView是“日网格”视图(DayView.tsx),其时间槽布局由getDayTimeIntervals生成。关键 props 与默认值:
startTime/endTime(HH:mm:ss):时间轴范围,默认00:00:00至23:59:59;intervalMinutes(默认15):每个时间槽的分钟数,必须能整除一小时(如 15、30)或为整小时数(如 120、240);withSubHourGridLines(默认true):小于 1 小时的间隔是否显示细分网格线;withAllDaySlot(默认true):显示顶部全天槽;slotLabelFormat(默认HH:mm)与headerFormat(默认MMMM D, YYYY):时间标签与头部日期格式,可传 dayjs 格式字符串或回调函数;slotHeight(默认64px)与allDaySlotHeight(默认44px):行高,通过 CSS 变量--day-view-slot-height、--day-view-all-day-slot-height注入;highlightBusinessHours(默认false)与businessHours(默认['09:00:00', '17:00:00']):高亮工作时间;withCurrentTimeIndicator(默认仅当天显示)与withCurrentTimeBubble(默认true):当前时间指示线及其气泡;getCurrentTime:自定义“当前时间”获取函数,可用于时区修正;startScrollTime:初始渲染时滚动到指定时刻;getTimeSlotProps:为每个时间槽注入额外 props,返回的事件处理器会与内部处理器组合而非覆盖。
交互回调:onTimeSlotClick(携带slotStart/slotEnd与原生事件)、onAllDaySlotClick、onEventClick、onEventDragStart/onEventDrop/onEventDragEnd、onEventResize、onSlotDragEnd、onExternalEventDrop。
事件定位算法位于 get-day-positioned-events.ts,对重叠事件进行分组布局,为每个事件计算百分比top、height、width、offset(见DayEventPositionData),从而在时间轴上实现多列并排而不互相遮挡。全天事件最多展示 2 条,超出部分通过MoreEvents弹出“+N more”列表(由getVisibleEvents计算)。
WeekView:周网格
WeekView在 DayView 能力基础上增加周维度(WeekView.tsx)。差异点:
intervalMinutes默认60(周视图单格更大);firstDayOfWeek(默认1,周一)与weekendDays(默认由DatesProvider决定):周起始日与周末标记,也可从DatesProvider继承;withWeekendDays(默认true):隐藏周末列;withWeekNumber(默认true):左上角显示周数;weekdayFormat(默认ddd)与weekLabelFormat(默认MMM DD);highlightToday(默认false):高亮今天所在列;forceCurrentTimeIndicator(默认false):跨周浏览时也显示当前时间线;businessHours支持按天配置:传入以星期为键的记录(0为周日),将某天设为null表示该天完全不在工作时间内;renderWeekLabel:完全自定义头部周标签的渲染。
布局计算位于 get-week-view-events 目录:assign-event-rows负责全天事件的纵向堆叠,calculate-regular-event-overlaps负责定时事件的重叠分列,calculate-all-day-event-width/calculate-all-day-event-offset处理跨天全天事件的宽度与偏移,calculate-event-days与get-hanging-status计算跨周事件的“悬挂”状态(hanging:start/end/both/none)。点击星期头部会通过onViewChange('day')联动切换到日视图。
MonthView:月历网格
MonthView把事件按“周行 + 天列”布局(目录见 MonthView)。布局管线由多个带独立单测的小工具组成:
get-weeks-in-range:计算当月覆盖的周;calculate-event-position-in-week与find-available-row:为事件分配行号(row)与起始偏移(startOffset);get-renderable-month-event-segments:处理跨周事件的分段渲染;get-visible-columns:计算可展示的列数。
关键 props 包括firstDayOfWeek、weekendDays、withWeekendDays、weekdayFormat、getDayProps(细粒度定制每天单元格)等,同时支持onDayClick与事件点击回调。月视图顶部同样可通过withAgenda展开议程列表。
YearView:年视图
YearView将一年拆分为 12 个月的小网格(YearView.tsx),支持onMonthClick点击月份后切换到月视图;在Schedule中该跳转由handleMonthClick统一处理(先更新日期再切换视图,见 Schedule.tsx)。
资源视图:按资源分列的排期
资源视图适合会议室、医生排班、工位等“资源 × 时间”场景,由ResourcesDayView、ResourcesWeekView、ResourcesMonthView与ResourcesSchedule(资源版一站式入口)提供。资源数据模型见 types.ts:
interface ScheduleResourceData { id: string | number; // 资源唯一标识 label: React.ReactNode; // 资源显示名 color?: MantineColor; // 资源颜色 payload?: Record<PropertyKey, any>; // 自定义数据 } interface ScheduleResourceGroup { label: React.ReactNode; resourceIds: (string | number)[]; // 组内资源 }事件通过resourceId关联到具体资源列。重叠事件的分组计算在 get-overlap-clusters 与get-resources-week-view-events中实现;分组以类似 rowspan 的方式跨列合并显示。ResourcesSchedule在顶部提供资源切换能力,适合“同一天内快速切换资源”的运营型界面。
AgendaView 与 MobileMonthView
AgendaView:把一段时间内的事件渲染为扁平列表(get-agenda-view-events负责收集与排序),可作为 Day/Week/Month 视图的补充信息流,通过withAgenda开启;MobileMonthView:为触屏优化的精简月视图,layout="responsive"时在窄屏替代 Day/Week/Month 视图(见 Schedule.tsx)。
交互能力:拖拽、缩放与框选
Schedule系列的交互由hooks/目录下的专用 hooks 驱动(hooks/index.ts):
use-drag-drop-handlers:事件拖拽主逻辑,计算落点(calculateDropTarget)、维护DragContext(被拖事件、预览、落点高亮),并处理拖拽中自动滚动(配合use-auto-scroll-on-drag);use-event-resize:事件边缘缩放,按intervalMinutes或eventResizeInterval对齐步长;use-slot-drag-select:按住拖拽框选连续时间段,结束时通过onSlotDragEnd(rangeStart, rangeEnd)回调;use-drag-state:拖拽状态机(use-drag-state.test.ts覆盖了状态迁移)。
启用拖拽移动
<Schedule events={events} withEventsDragAndDrop canDragEvent={(event) => event.payload?.editable !== false} onEventDrop={({ eventId, newStart, newEnd, event }) => { // 更新业务数据源,例如调用 API 保存新时间 console.log(eventId, newStart, newEnd, event); }} />拖放时间由calculateDropTime(utils/calculate-drop-time)计算:按时间槽或eventDragInterval对齐,并保留鼠标在事件内的偏移量,保证拖拽手感。拖拽期间会渲染dragPreview预览块(DayView 与 WeekView 分别有dayViewDragPreview、weekViewDragPreview样式名)。
启用边缘缩放
<Schedule events={events} withEventResize onEventResize={({ eventId, newStart, newEnd, event }) => { // 保存新的开始/结束时间 }} />缩放同样按时间步长吸附,DayView/WeekView 中受eventResizeInterval控制(默认取intervalMinutes)。resize 过程中事件本体即时更新(getResizePosition),松手后触发onEventResize。
外部元素拖入
通过onExternalEventDrop(dataTransfer, dropDateTime)可以把日程应用之外的元素(如任务列表)直接拖入时间轴,dataTransfer中可携带自定义数据;与内部事件拖拽共用onDragOver高亮与自动滚动逻辑。
框选时间段
<Schedule withDragSlotSelect onSlotDragEnd={(rangeStart, rangeEnd) => { // 例如弹出“新建事件”表单并预填时间 setNewEvent({ start: rangeStart, end: rangeEnd }); }} />框选在 DayView 与 WeekView 中可用,选中的槽位带drag-selectedmod 高亮。
mode="static" 只读模式
将mode设为'static'会同时关闭事件拖拽、缩放、点击、时间槽点击与键盘导航(各槽位tabIndex变为 -1),适合“只读预览/分享页”场景;背景事件交互也会被强制关闭。
背景事件与自定义渲染
背景事件
设置display: 'background'的事件会作为整块半透明色块渲染在普通事件之后,常用来表示工作时间、假期、维护窗口等。默认不可交互;Day/Week/Month 视图可通过withInteractiveBackgroundEvents开启点击并触发onEventClick(注意YearView与响应式布局下的MobileMonthView不渲染背景事件,见 Schedule.tsx 的注释说明)。
自定义事件渲染
两个层级:
// 1. 只改事件内容 const renderEventBody = (event: ScheduleEventData) => ( <div> <strong>{event.title}</strong> <span>{event.payload?.location}</span> </div> ); // 2. 完全接管事件根元素渲染 const renderEvent: RenderEvent = (event, props) => ( <button {...props}><Schedule events={events} labels={{ today: '今天', week: '周', month: '月', allDay: '全天', moreLabel: (n) => `还有 ${n} 个`, noEvents: '暂无事件', }} />日期与星期名的本地化则走@mantine/dates的DatesProvider(locale字段),各视图的localeprop 可单独覆盖;Schedule的locale也会透传给内部视图。
无障碍与键盘导航
源码在多处体现了无障碍设计:
- 时间槽、星期头、全天槽均为可聚焦的
UnstyledButton,带aria-label(如Time slot 09:00:00 - 10:00:00),并提供previousControlProps/nextControlProps/todayControlProps/viewSelectProps透传 ARIA 属性; - DayView 时间槽支持
ArrowUp/ArrowDown在槽位间移动焦点(DayView.tsx); - WeekView 提供完整的焦点网格导航(
handleWeekViewKeyDown),可在星期列、全天槽、时间槽之间用方向键穿梭(WeekView.tsx); mode="static"时所有交互控件移出 Tab 顺序。
样式定制:Styles API 与 CSS 变量
所有视图组件都遵循 Mantine 的 Styles API 约定:factory+StylesApiProps+classNames/styles/vars/unstyled。每个视图导出StylesNames联合类型,例如WeekViewStylesNames覆盖weekView、weekViewHeader、weekViewDay、weekViewAllDaySlots、weekViewSlotLabel、weekViewBackgroundEvent、weekViewDragPreview等全部命名节点(WeekView.tsx)。
尺寸与圆角通过 CSS 变量注入(varsResolver):如 DayView 的--day-view-radius、--day-view-slot-height、--day-view-all-day-slot-height,WeekView 的--week-view-radius、--week-view-slot-height、--week-view-all-day-slots-height,ScheduleEvent 的--event-bg/--event-color/--event-radius。示例:
<WeekView date="2024-01-15" events={events} slotHeight={72} allDaySlotHeight={56} radius="md" classNames={{ weekViewDay: classes.myDay }} />包内每个组件都带.module.css与 Storybook story(.story.tsx),可作为样式覆盖的参照实现。
测试与可靠性
该包对布局算法与交互状态做了较充分的测试覆盖,可作为实现正确性的参照:
- 布局算法单测:
get-week-positioned-events.test.ts、get-day-positioned-events.test.ts、get-month-view-events下的calculate-event-position-in-week.test.ts、find-available-row.test.ts、get-weeks-in-range.test.ts,以及get-overlap-clusters.test.ts(资源视图重叠分组)等; - 交互 hook 测试:
use-drag-drop-handlers.test.ts、use-drag-state.test.ts、use-event-resize.test.ts、use-horizontal-event-resize.test.ts; - 组件测试:每个视图组件均有
*.test.tsx(如 Schedule.test.tsx)。
在自行扩展布局算法或交互逻辑时,这些测试文件是很好的行为契约参考。
结语
@mantine/schedule以“单一事件模型 + 多视图聚合”的方式,把日/周/月/年视图、资源排期、议程列表、重复事件、拖拽/缩放/框选等排期系统的高频能力收敛进一个包中。无论是直接用Schedule快速搭建,还是用DayView/WeekView等独立组件定制专属界面,其 源码目录 都提供了清晰的分层:视图组件负责组合与交互,utils/负责纯布局计算(可独立单测),hooks/负责拖拽等交互状态。结合 Mantine 的 Styles API 与DatesProvider国际化体系,它足以支撑从内部工具到对外产品的各类日历场景。
【免费下载链接】mantineA fully featured React components library项目地址: https://gitcode.com/GitHub_Trending/ma/mantine
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考