DirectSurface UIDirectSurface UI
开始使用
文档/组件手册

Scheduler 资源日程

发布状态:Client-side Editable Public Preview。 默认仍为 editable=false 的只读本地完整快照;宿主显式开启编辑后,组件提供客户端本地 CRUD、默认 Canvas Event Editor、Day/Week 空白时间槽创建,以及 Day/Week timed Event 的指针/受限键盘移动、资源重分配和起止时间拉伸。Public Preview 不代表 GA 或完整商用承诺;本地提交不代表服务端保存,也不表示 all-day/Month/Agenda 空间编辑、权限、远程数据、recurrence engine、生产级时区适配、跨浏览器或屏幕阅读能力已经可用。

通用布局能力RenderScheduler 支持 widthheight、min/max、margin 和槽位对齐。时间轴、资源表头和事件区域在组件给定的有限尺寸内完成布局;详见组件通用布局属性

RenderScheduler 是面向后台管理系统的 Canvas 资源日程组件。它使用本地完整数据 快照,在 Canvas 中绘制资源时间线或 Agenda 列表,并提供滚动、定位、选择、 激活、overflow、状态反馈和可见窗口虚拟化。默认 editable=false;开启后可通过 同步公共方法、默认事件编辑器或 Timeline 指针/键盘交互修改组件当前持有的本地快照, 但不会写入服务端。

LIVE CANVAS可运行组件示例
ON-DEMAND RUNTIME交互式示例将在进入视区时启动避免文档首屏同时初始化多个 Canvas Runtime

适合排班查看、预约总览、床位与设备占用、人员计划等场景。需要表格编辑和 任意列操作时使用 GridView;需要聚合趋势时使用 Charts;需要自由业务轨道时评估 ClinicalFlowSheet。需要远程分页、权限、 服务端持久化或规则引擎时,当前 Scheduler 不适用。

API 总览

所有 Scheduler API 都从 ds-ui 包入口导入。

运行时类

API 类型 用途
RenderScheduler class 构造和绘制资源日程,并提供可选本地 CRUD。
RenderSchedulerConfigurationError class 报告缺少数据、无效数据、无效 domain、formatter 或 editing 配置。

组件契约类型

API 用途
RenderSchedulerOptions 构造参数。
RenderSchedulerDebugState 稳定的标量诊断摘要。
SchedulerTimelineView Day / Week / Month 资源时间线视图。
SchedulerView Day / Week / Month / Agenda 视图联合类型。
SchedulerZoomPreset compactstandardcomfortable 密度。
SchedulerScrollAlignment 定位时使用的 startcenterendnearest 对齐。
SchedulerStatus 当前加载、刷新、就绪、空、错误或释放状态。
SchedulerHeaderFormatter Month、Day 和 time header 文本格式化协议。
SchedulerReadonlyItem 选择和激活回调使用的稳定只读数据。
SchedulerInteractionSource 交互来源:API、editor、pointer、keyboard 或 reconcile。
SchedulerInteractionRejection before 事件取消时可选的稳定 code 与用户可见 message。
SchedulerCancelableInteractionEvent 统一的同步 preventDefault() 交互事件基础契约。
SchedulerSelectionChangeReason selectclearactivationeditormutationdata-replacementprojection-loss 选择变更原因。
SchedulerSelectionChangeDescriptor 选择变更的来源、原因、前值、候选值与 forced 标记。
SchedulerBeforeSelectionChangeEvent 可同步取消的选择变更事件。
SchedulerSelectionChangeEvent 已提交或强制协调后的选择变更通知。
SchedulerEventActivationSurface Event 激活所在的 canvas 或 overflow 表面。
SchedulerEventActivateDescriptor Event 激活的来源、表面与只读 Event 投影。
SchedulerBeforeEventActivateEvent 可同步取消的 Event 激活事件。
SchedulerEventActivateEvent Event 已激活通知。
SchedulerViewChangeDescriptor 视图切换的来源、前视图与候选视图。
SchedulerBeforeViewChangeEvent 可同步取消的视图切换事件。
SchedulerViewChangeEvent 已提交的视图切换通知。
SchedulerZoomChangeDescriptor 密度切换的来源、前密度与候选密度。
SchedulerBeforeZoomChangeEvent 可同步取消的密度切换事件。
SchedulerZoomChangeEvent 已提交的密度切换通知。
SchedulerNavigationRequest date、resource 或 page 语义导航请求。
SchedulerNavigateDescriptor 导航来源、请求与导航前 viewport。
SchedulerBeforeNavigateEvent 可同步取消的语义导航事件。
SchedulerNavigateEvent 导航提交后的请求与 viewport 通知。
SchedulerViewport 当前滚动偏移和可见时间、资源窗口。
SchedulerViewportChangeEvent 布局周期内合并后的 viewport 变更通知。
SchedulerSetDataOptions 完整快照替换时的 query key 和 domain。
SchedulerEventDraft 创建 Event 及其 Assignments 的原子草稿。
SchedulerCreateEventContext 后续交互创建入口使用的时间与 Resource 上下文。
SchedulerUpdateEventCommand 更新标题、时间和单个 Assignment Resource 的命令。
SchedulerChangedEvent 数据变更回调中的最小 Event 记录。
SchedulerChangedAssignment 数据变更回调中的最小 Assignment 记录。
SchedulerDataAction createupdateremove 数据动作。
SchedulerDataChangeKind 一次提交中可组合的变更字段类别。
SchedulerDataGesture moveresizereassign 空间编辑手势。
SchedulerDataChangeSource apieditorpointerkeyboard 来源。
SchedulerDataChangeDescriptor proposed 与 committed change 共用的 action、changeKinds、gesture、source 和前后记录。
SchedulerProposedDataChange 提交前深度冻结的候选变更,包含 baseRevision
SchedulerBeforeDataChangeEvent 通过 preventDefault() 同步取消候选提交。
SchedulerDataChangeRejection 可选的稳定取消 code 与用户可见 message。
SchedulerDataChange 已提交本地变更的深度冻结描述。
SchedulerMutationResult 同步成功结果或稳定拒绝原因。

本地快照记录

API 用途
SchedulerKey Event、Resource 和 Assignment 的 string | number 身份。
SchedulerVersion 可选业务版本值。
SchedulerInstantRange epoch milliseconds 半开时间区间。
SchedulerAllDayRange 使用日历日期表达的全天半开区间。
SchedulerEventTime timed 或 all-day Event 时间。
SchedulerWallDateTime recurrence 记录中的墙钟日期时间。
SchedulerTimeDisambiguation 重复或缺失墙钟时间的消歧策略记录。
SchedulerOccurrenceIdentity 已展开 occurrence 的原始身份。
SchedulerRecurrenceDefinition 调用方 recurrence 定义记录;组件不负责展开。
SchedulerEvent 日程事件。
SchedulerResource 资源记录。
SchedulerAssignment Event 与 Resource 的关联。
SchedulerTimeRangeKind working、unavailable、background、blocked 区间类型。
SchedulerTimeRange 全局或资源级背景时间区间。
SchedulerDataSnapshot Events、Resources、Assignments 和 TimeRanges 的完整本地快照。

开发者数据字段

SchedulerDataSnapshot 是传给 RenderScheduler 的完整客户端数据边界:

字段 类型 说明
events readonly SchedulerEvent[] 已经展开到可直接绘制的 Event 记录。
resources readonly SchedulerResource[] 资源及其可选父子关系。
assignments readonly SchedulerAssignment[] Event 与 Resource 的关联;同一 Event 可以有多个 Assignment。
timeRanges readonly SchedulerTimeRange[] 全局或资源级工作、不可用、背景和阻塞区间。

SchedulerEvent

字段 类型 必填 说明
id SchedulerKey Event 身份。
title string Canvas 和 Editor 中显示的标题。
time SchedulerEventTime timed 或 all-day 时间。
recurrence SchedulerRecurrenceDefinition recurrence master 定义;RenderScheduler 的本地快照采用 expanded-occurrences 模式,不接受 master,详见时间、recurrence 和时区边界
recurrenceParentId SchedulerKey 已展开 occurrence 的外部 series 身份,必须与 originalOccurrenceStart 同时提供。
originalOccurrenceStart SchedulerOccurrenceIdentity 已展开 occurrence 的稳定原始起点身份。
readOnly boolean 可选择和查看,但不可本地写入或空间编辑。
disabled boolean 不参与选择、激活或编辑。
version SchedulerVersion 调用方业务版本。
metadata unknown 调用方业务数据;组件保留但不放入通用 change DTO。

SchedulerResource

字段 类型 必填 说明
id SchedulerKey Resource 身份。
title string Resource header 或 Agenda 中显示的标题。
parentId SchedulerKey 父 Resource 身份。
color string Resource 语义颜色。
readOnly boolean 该 Resource 上的 Event 不允许本地写入。
expanded boolean 层级资源的初始展开状态。
sortOrder number 同级 Resource 排序值。
version SchedulerVersion 调用方业务版本。
metadata unknown 调用方业务数据。

SchedulerAssignment

字段 类型 必填 说明
id SchedulerKey Assignment 身份。
eventId SchedulerKey 必须引用当前 snapshot 中存在的 Event。
resourceId SchedulerKey 必须引用当前 snapshot 中存在的 Resource。
units number 调用方容量或占用单位。
version SchedulerVersion 调用方业务版本。
metadata unknown 调用方业务数据。

SchedulerTimeRange

字段 类型 必填 说明
id SchedulerKey 时间区间身份。
resourceId SchedulerKey 省略时作用于全部 Resource。
kind working | unavailable | background | blocked 绘制和本地编辑校验使用的区间语义。
time SchedulerEventTime timed 或 all-day 时间。
readOnly boolean 调用方只读标记。
metadata unknown 调用方业务数据。

时间字段均使用半开区间:

类型 字段 说明
SchedulerInstantRange startEpochMsendEpochMs timed Event 的 epoch milliseconds 起点和排他终点。
timed SchedulerEventTime kind: 'timed'range、可选 timeZone timeZone 只作为业务记录保留,不触发时区换算。
SchedulerAllDayRange startendExclusive 两端均为 { year, month, day },结束日期不包含在 Event 中。
all-day SchedulerEventTime kind: 'all-day'range 不隐式转换成浏览器本地时区 instant。

最小示例

下面的时间值都使用 epoch milliseconds。domain 和 Event range 都采用 [startEpochMs, endEpochMs) 半开区间。

import {  RenderScheduler,  type SchedulerDataSnapshot,  type SchedulerInstantRange,} from 'ds-ui' const startEpochMs = Date.UTC(2026, 6, 28, 0, 0, 0)const endEpochMs = Date.UTC(2026, 7, 4, 0, 0, 0) const domain: SchedulerInstantRange = {  startEpochMs,  endEpochMs,} const schedulerData: SchedulerDataSnapshot = {  resources: [    { id: 'room-a', title: '会议室 A', color: '#2563eb' },  ],  events: [    {      id: 'event-1',      title: '项目评审',      time: {        kind: 'timed',        range: {          startEpochMs: Date.UTC(2026, 6, 28, 9, 0, 0),          endEpochMs: Date.UTC(2026, 6, 28, 10, 30, 0),        },      },      readOnly: true,    },  ],  assignments: [    {      id: 'assignment-1',      eventId: 'event-1',      resourceId: 'room-a',    },  ],  timeRanges: [],} const scheduler = new RenderScheduler({  data: schedulerData,  domain,  height: 520,  title: '资源排期',  view: 'resourceTimelineWeek',  zoomPreset: 'standard',  onEventActivate: event => {    console.log(      event.item.eventId,      event.item.resourceId,      event.surface,    )  },})

全天 Event 使用日历日期而不是浏览器本地时间:

const allDayData: SchedulerDataSnapshot = {  resources: [{ id: 'team-a', title: '交付团队 A' }],  events: [{    id: 'release-window',    title: '版本发布窗口',    time: {      kind: 'all-day',      range: {        start: { year: 2026, month: 7, day: 31 },        endExclusive: { year: 2026, month: 8, day: 2 },      },    },  }],  assignments: [{    id: 'release-window-team-a',    eventId: 'release-window',    resourceId: 'team-a',  }],  timeRanges: [],}

该 Event 覆盖 7 月 31 日和 8 月 1 日,不包含 8 月 2 日。

组件必须由父布局提供有限宽高。构造完成后,把 scheduler 作为普通 render object 放入页面布局树即可。

构造参数

参数 类型 默认值 说明
data SchedulerDataSnapshot 必填 完整本地数据快照。
domain SchedulerInstantRange 必填 有限显示时间范围。
height number 420 Scheduler 未由父布局强制高度时使用的组件默认高度;其他通用布局属性见上方链接。
view SchedulerView resourceTimelineDay 初始 Day、Week、Month 或 Agenda 视图。
zoomPreset SchedulerZoomPreset standard 时间轴密度。
slotDurationMs number 1_800_000(30 分钟) 单个时间槽持续毫秒数。
slotWidth number 72 standard 密度下的时间槽宽度。
monthDayWidth number 64 Month standard 密度下每个 UTC 日历日的显示宽度。
resourceRowHeight number 36 资源行高度。
resourceHeaderWidth number 180 冻结资源表头宽度。
timeHeaderHeight number 48 冻结时间表头高度。
title string 'Resources' 资源表头标题。
queryKey string 'render-scheduler' 当前数据查询身份。
headerFormatter SchedulerHeaderFormatter 内置 formatter Month、Month day、Day 和 time header 文本格式化。formatMonthformatMonthDay 可选。
editable boolean false 是否允许公共本地 mutation API 和默认事件编辑器写入当前快照。
snapDurationMs number 当前时间槽时长 指针创建、移动和受限键盘空间编辑使用的正整数毫秒步长。
defaultEventDurationMs number max(3_600_000, snapDurationMs) 空白时间槽创建使用的默认时长,不能小于 snap。
createEventDraft (context) => draft | null undefined 双击空白 Timeline 时间槽时调用一次的同步草稿工厂;返回 null 表示取消创建。
onStateChange (state) => void undefined 加载状态、视图、滚动位置、视口/内容尺寸、可见投影、编辑开关或 Event Editor 状态变化后接收最新 RenderSchedulerDebugState
onBeforeDataChange (event) => void undefined 本地 transaction 校验成功、Store 提交前同步触发;调用 event.preventDefault(rejection?) 可取消提交。
onDataChange (change) => void undefined 每次成功本地提交后接收一次深度冻结的变更。
onBeforeSelectionChange (event) => void undefined 选择提交前同步触发;调用 preventDefault() 可取消非强制选择变化。
onSelectionChange (event) => void undefined 选择提交后接收 previouscurrentsourcereasonforced
onBeforeEventActivate (event) => void undefined Event 激活前同步触发;取消后不执行默认激活行为。
onEventActivate (event) => void undefined 普通 Canvas 与 overflow Event 的统一激活通知;使用 surface 区分来源表面。
onBeforeViewChange / onViewChange (event) => void undefined View 切换前后的业务事件。
onBeforeZoomChange / onZoomChange (event) => void undefined Zoom 切换前后的业务事件。
onBeforeNavigate / onNavigate (event) => void undefined 日期、资源和键盘分页语义导航前后的业务事件。
onViewportChange (event) => void undefined 实际可视区域变化后的合并通知,不对滚轮的每个像素提供同步拦截。

缺少 data、数据关系无效、domain 无效或 formatter 协议无效时, RenderSchedulerConfigurationError.reason 分别为:

  • missing-local-data
  • invalid-local-data
  • invalid-domain
  • invalid-header-formatter
  • invalid-editing-options
  • invalid-interaction-options

实例属性

以下是业务开发者可能直接读取或修改的 Scheduler 自有属性;通用布局属性和框架 焦点路由仍遵循 RenderBox / render tree 契约。

属性 类型 可写 说明
resourceHeaderWidth number 构造时解析后的 Resource header 宽度。
timeHeaderHeight number 构造时解析后的 time header 高度。
title string Resource header 标题。
headerFormatter SchedulerHeaderFormatter 构造时验证后的 header formatter。
editable boolean 运行时可动态开启或关闭本地编辑;关闭时会取消未提交交互和编辑器。
queryKey string 当前查询身份;使用 setQueryKey() 更新。
isFocused boolean 当前是否持有框架焦点;通常由 runtime 输入路由维护。

Header formatter

formatDay()formatTime() 必须提供;Month 使用的 formatMonth()formatMonthDay() 可选,省略时使用内置 UTC 文本。

方法 输入 返回
formatMonth(input) { monthIndex, startEpochMs } 月份表头文本。
formatMonthDay(input) { dayIndex, startEpochMs } Month 日期槽文本。
formatDay(input) { dayIndex, startEpochMs } Day / Week 日期表头文本。
formatTime(input) { slotIndex, startEpochMs } Timeline 时间槽文本。

monthIndexdayIndexslotIndex 是当前 domain 内从 0 开始的投影索引; startEpochMs 是对应槽位的 UTC instant。

const scheduler = new RenderScheduler({  data: schedulerData,  domain,  headerFormatter: {    formatMonth: ({ startEpochMs }) => {      const date = new Date(startEpochMs)      return `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}`    },    formatMonthDay: ({ startEpochMs }) =>      String(new Date(startEpochMs).getUTCDate()),    formatDay: ({ dayIndex }) => `Day ${dayIndex + 1}`,    formatTime: ({ startEpochMs }) => {      const date = new Date(startEpochMs)      return `${String(date.getUTCHours()).padStart(2, '0')}:` +        String(date.getUTCMinutes()).padStart(2, '0')    },  },})

数据模型和身份

SchedulerDataSnapshot 是一次完整替换的数据边界:

  • resources 描述资源及可选层级、颜色和顺序;
  • events 描述 timed 或 all-day 事件;
  • assignments 使用 ID 把 Event 分配到 Resource;
  • timeRanges 描述 working、unavailable、background 或 blocked 背景区间。

SchedulerKey 保留调用方传入的 string | number 类型和值,因此字符串 '1' 和数字 1 是不同身份。Event、Resource 和 Assignment ID 应在各自集合 内稳定且唯一,Assignment 引用的 Event 和 Resource 必须存在。

timed Event 使用 epoch milliseconds;endEpochMs 不包含在事件区间中。all-day Event 使用 SchedulerAllDayRange.startendExclusive 的日历日期,不应把 日期字符串隐式转换成浏览器本地时区的 instant。

视图、密度和导航

SchedulerView 支持:

  • resourceTimelineDay:单日资源时间线;
  • resourceTimelineWeek:多日资源时间线;
  • resourceTimelineMonth:按 UTC 日历日压缩显示的资源月时间线,使用 Month / Day 双层表头;timed Event 按覆盖的 UTC 日历日视觉槽排列,all-day Event 按原始 SchedulerAllDayRange 排列。两者都支持选择、激活和 Event Editor,且不会因为 Month 的视觉压缩而修改底层 Event 时间;
  • agenda:按资源和时间排序的列表视图。

Month 不会把 domain 自动扩展为自然月;调用方传入的半开 domain 仍是唯一显示 边界。若要显示完整自然月,应传入该月第一天零点到下月第一天零点。Month 的 compactstandardcomfortable 分别调整日宽,不改变原始事件时间。 Month 的 UTC 月份与日期计算覆盖 Scheduler 接受的安全整数 epoch,不依赖 JavaScript Date 的 0–99 年特殊规则或 TimeClip 范围。

LIVE CANVAS可运行组件示例
ON-DEMAND RUNTIME交互式示例将在进入视区时启动避免文档首屏同时初始化多个 Canvas Runtime

SchedulerZoomPreset 支持 compactstandardcomfortable。以下方法返回 true 表示状态实际发生变化;无效参数、无匹配项、无变化或组件已释放时通常 返回 false

方法 用途
setView(view) 切换 Day、Week、Month 或 Agenda。
setZoomPreset(preset, anchorViewportRatio?) 围绕视口锚点切换密度。
setScroll(scrollX, scrollY) 设置并裁剪双轴滚动位置。
scrollToDate(epochMs, alignment?) 定位日期或时间。
scrollToResource(resourceId, alignment?) 定位资源。
setNowEpochMs(epochMs | null) 设置或清除当前时间指示线。
clearSelection() 清除当前选择并回调 null

定位对齐可以使用 startcenterendnearest。指针和框架键盘交互都 使用当前可见的只读目标;这属于 Canvas 组件交互,不表示存在语义 DOM。

交互可发现性和光标

Scheduler 在 Canvas 内直接绘制 hover、pressed、selection、active 和不可操作状态。 鼠标进入目标后,可先根据填充、边框、状态标记和光标判断交互,不需要猜测整个 网格都是按钮。单击静态区域只会让组件获得框架焦点,不会自动给第一个 Event 绘制 active 边框;active 边框来自实际选择或键盘导航。

区域或状态 可见反馈与光标 单击 双击或拖动
可写 timed Event,editable=true 且当前 projection 已就绪 hover/pressed 填充;主体为 grab,拖动后为 grabbing;左右边缘为 ew-resize 选择并激活 Event 双击打开编辑器;拖主体移动/改派,拖边缘拉伸
Month timed / all-day Event hover/pressed 填充;可用行为 pointer,disabled 行为 not-allowed 选择并激活可用 Event 编辑开启时双击打开 edit 或 view-only editor;不支持空间拖动、改派或拉伸
Month 空白日格 default,不绘制创建预览 清除当前 Event 选择 不调用 createEventDraft,不创建 Event
普通 Event,editable=false hover/pressed 填充;pointer 选择并激活 Event 不进入本地编辑
readonly Event 或 readonly Resource 上的 Event 锁定标记;hover/pressed 填充;pointer 选择并激活 Event 编辑开启时双击以 view-only 模式查看;不能移动、改派或拉伸
disabled Event 禁用标记;not-allowed 不选择、不激活 不打开编辑器,也不开始拖动
可创建的 Timeline 空白时间格 按默认 Event 时长绘制预览框;crosshair 清除当前 Event 选择 双击调用 createEventDraft 并在草稿有效时打开 create editor
readonly Resource 或 blocked 区间中的空白时间格 readonly/blocked 预览框;not-allowed 仅清除当前 Event 选择 不创建 Event
未开启编辑或未提供 createEventDraft 的空白时间格 不绘制创建预览;default 清除当前 Event 选择 不创建 Event
time header、resource header 和 Resource 标题 default,无业务 hover 无选择或激活动作 无业务动作
overflow 指示器 hover/pressed 填充;pointer 打开隐藏 Event 列表 不适用
overflow 可用行 / disabled 行 可用行为 pointer;disabled 行为 not-allowed 可用行触发 overflow activation;编辑开启时继续打开 Event Editor disabled 行不响应
scrollbar thumb hover 为 grab,按住为 grabbing 开始捕获并拖动 拖动滚动
scrollbar track pointer;按下显示 pressed 向目标方向翻页一次 不适用
Agenda Event hover/pressed 填充;可用行为 pointer,disabled 行为 not-allowed 选择并激活可用 Event 编辑开启时双击打开 edit 或 view-only editor;不支持空间拖动

Event 的 selection/active 描边与 hover/pressed 填充可以同时出现,因此已选 Event 仍能表达当前鼠标位置。指针离开、取消、失焦、视图或数据切换、隐藏、卸载和释放 都会清理临时 hover、pressed、tooltip 与 cursor;不会留下“看起来还能操作”的旧状态。 刷新期间如果保留了上一份 projection,旧内容仍可查看和选择,但不会显示 grab、resize 或空白创建光标,也不能开始新的本地编辑;当前 projection 再次 进入 ready 后才恢复这些编辑反馈。

LIVE CANVAS可运行组件示例
ON-DEMAND RUNTIME交互式示例将在进入视区时启动避免文档首屏同时初始化多个 Canvas Runtime

事件和回调

选择、激活、View、Zoom 和语义导航使用统一的同步 before/after 管线。before 事件可以调用 preventDefault({ code, message? }) 取消操作;before 回调抛错时 采用 fail-closed,状态不提交。after 回调在状态提交后触发,回调抛错不会回滚 Scheduler。事件对象和其中的 SchedulerReadonlyItem、viewport 均为冻结对象。

import {  RenderScheduler,  type SchedulerDataSnapshot,  type SchedulerInstantRange,} from 'ds-ui' const interactionData: SchedulerDataSnapshot = {  resources: [{ id: 'room-a', title: '会议室 A' }],  events: [],  assignments: [],  timeRanges: [],}const interactionDomain: SchedulerInstantRange = {  startEpochMs: Date.UTC(2026, 6, 28),  endEpochMs: Date.UTC(2026, 6, 29),}const interactionScheduler = new RenderScheduler({  data: interactionData,  domain: interactionDomain,  onBeforeSelectionChange: event => {    if (event.current?.readOnly) {      event.preventDefault({        code: 'readonly-selection',        message: '当前业务不允许选择只读日程。',      })    }  },  onSelectionChange: event => {    console.log(event.previous, event.current, event.source)  },  onBeforeEventActivate: event => {    if (event.item.disabled) event.preventDefault()  },  onEventActivate: event => {    console.log(event.item.eventId, event.surface)  },})

回调数据保留原始 ID 类型,并包含标题、Resource 标题、当前视图、半开时间区间、 readOnlydisabled。它不返回业务 metadata 或内部布局对象。disabled Event 不触发 activation;清除选择时 onSelectionChange.currentnull。 普通 Event 与 overflow Event 共用 onEventActivate,通过 surfacecanvas | overflow 区分,不再提供独立的 overflow activation 回调。

onSelectionChange.reason 使用以下稳定值:

reason 含义
select pointer、keyboard 或 API 选择了 Event。
clear 空白区交互或 clearSelection() 清除了选择。
activation Event 激活流程同步了选择目标。
editor 打开 Event Editor 时同步了选择目标。
mutation 本地创建、更新、改派或删除后协调选择。
data-replacement setData() 后按新 snapshot 协调原选择身份。
projection-loss 原选择目标不再存在于当前可见 projection。

reconcileSchedulerInteractionSource,不是 selection reason;它表示组件为 维持公开选择状态一致性而执行的强制协调。

setView()setZoomPreset()scrollToDate()scrollToResource() 和键盘 分页分别通过 View、Zoom 和 Navigate 事件暴露。滚轮、滚动条和实际布局引起的 可视区域变化使用合并后的 onViewportChange;它是提交后通知,不会在每个像素 变化前同步阻塞绘制。

SchedulerViewport 同时包含像素窗口和业务可用的语义窗口:

  • view 表示快照对应的当前视图;
  • scrollXscrollYbodyWidthbodyHeightcontentWidthcontentHeight 表示 Canvas 内部滚动与尺寸;
  • visibleTimeRange 是当前可见的半开时间区间;过渡状态无法可靠投影时为 null
  • visibleResourceIds 保留原始 Resource ID 类型,并按当前可见顺序返回。

onBeforeNavigatepreviousViewport 是导航提交前快照;onNavigate 同时 保留该快照,并通过 viewport 提供提交后的最终快照。onViewportChange 在 浏览器绘制帧内合并多次滚动、布局或 projection 更新:previous 保留该帧最早 快照,current 使用最后快照;若最终语义与像素窗口均未变化,则不发送事件。 setData() 引起的 domain、内容尺寸或可见资源变化也遵守同一通知规则。

在 before 回调中同步执行 setData()、本地 CRUD、View、Zoom、滚动或其他已 提交交互时,外层旧提案会按 revision 失效,不会把旧 Store 的选择或激活状态 提交到新快照。已选 Event 的标题、时间、Resource、状态或当前 View 变化时, 后续回调读取到的 SchedulerReadonlyItem 会同步刷新;Event 被删除时,强制 选择清理发生在对应 onDataChange 之前。

选择目标因数据替换或 projection 丢失而消失时,Scheduler 必须清除无效身份。 这种协调只发送 forced: trueonSelectionChange,不调用可取消的 before 事件。其他 pointer、keyboard、editor 和 API 选择变化均可同步取消。

Month all-day Event 的 startEpochMs / endEpochMs 是其日历日期范围对应的 UTC 零点半开投影,仅用于统一的可见区间回调;Event Editor 和本地 CRUD 始终读取并 写回原始 SchedulerAllDayRange

LIVE CANVAS可运行组件示例
ON-DEMAND RUNTIME交互式示例将在进入视区时启动避免文档首屏同时初始化多个 Canvas Runtime

可选客户端本地 CRUD

设置 editable: true 后,可以通过同步 API 原子修改 Scheduler 当前持有的客户端 快照:

方法 用途
createEvent(draft) 一次创建一个 Event 和至少一个 Assignment。
updateEvent(command) 更新标题、时间,以及可选的单个 Assignment Resource。
removeEvent(eventId) 一次删除 Event 及其全部 Assignments。
openEventEditor(eventId, assignmentId?) 为当前可见 projection 中的 Event 打开默认编辑器;成功返回 true
cancelEdit() 取消当前编辑器;实际关闭时返回 true,没有 editor 时返回 false

createEvent() 接收 { event, assignments },并要求至少一个 Assignment; createEventDraft(context) 使用相同草稿结构。空白 Timeline 创建时的 context 包含 viewresourceIdstartEpochMsendEpochMs

SchedulerUpdateEventCommand 只修改显式提供的字段:

字段 类型 必填 说明
eventId SchedulerKey 要更新的 Event。
title string 新标题。
time SchedulerEventTime 新 timed 或 all-day 时间。
assignment { assignmentId, resourceId } 把指定 Assignment 改派到目标 Resource。
import {  RenderScheduler,  type SchedulerDataChange,  type SchedulerDataSnapshot,  type SchedulerInstantRange,} from 'ds-ui' const domain: SchedulerInstantRange = {  startEpochMs: Date.UTC(2026, 6, 28),  endEpochMs: Date.UTC(2026, 6, 29),}const schedulerData: SchedulerDataSnapshot = {  resources: [    { id: 'room-a', title: '会议室 A' },    { id: 'room-b', title: '会议室 B' },  ],  events: [],  assignments: [],  timeRanges: [],}const changes: SchedulerDataChange[] = []const scheduler = new RenderScheduler({  data: schedulerData,  domain,  editable: true,  onBeforeDataChange: event => {    if (event.change.action === 'remove') {      event.preventDefault({        code: 'demo-delete-policy',        message: '当前客户端规则不允许删除该日程。',      })    }  },  onDataChange: change => {    changes.push(change)  },}) const created = scheduler.createEvent({  event: {    id: 'event-2',    title: '设计评审',    time: {      kind: 'timed',      range: {        startEpochMs: Date.UTC(2026, 6, 28, 11, 0, 0),        endEpochMs: Date.UTC(2026, 6, 28, 12, 0, 0),      },    },  },  assignments: [{    id: 'assignment-2',    eventId: 'event-2',    resourceId: 'room-a',  }],}) if (created.ok) {  scheduler.updateEvent({    eventId: 'event-2',    title: '设计评审(已确认)',    time: {      kind: 'timed',      range: {        startEpochMs: Date.UTC(2026, 6, 28, 13, 0, 0),        endEpochMs: Date.UTC(2026, 6, 28, 14, 30, 0),      },    },    assignment: {      assignmentId: 'assignment-2',      resourceId: 'room-b',    },  })  scheduler.openEventEditor('event-2', 'assignment-2')}

成功时,SchedulerMutationResult 包含 revision 和同一个 SchedulerDataChange;失败时包含稳定 reason,且不会增加 revision 或触发 onDataChange。同时修改 Event 时间与 Assignment Resource 仍只产生一次提交和 一次回调。完整顺序为“transaction 校验 → onBeforeDataChange → revision 再确认 → 本地提交 → 选择状态协调 → onDataChange”。before 回调取消会返回 reason: 'cancelled',其 code / message 会随 rejection 返回;before 回调 抛错返回 callback-error,回调期间数据身份或 revision 失效返回 stale。三种 情况都保持零提交。回调内重入 mutation 返回 busy

业务代码应按以下类别处理失败结果:

类别 reason 说明
生命周期或状态 disposedediting-disablednot-readybusy 当前组件不能接受新的本地提交。
身份或可写性 readonlydisablednot-foundduplicate 目标不可写、找不到或身份重复。
数据校验 invalid-eventinvalid-rangeinvalid-relationoutside-domainblocked-rangeunsupported-recurrenceno-change 候选数据不满足 Scheduler 本地约束。
宿主回调或并发 cancelledcallback-errorstaleinternal-error before 回调取消/抛错、候选 revision 失效或内部提交失败。

失败结果不会增加 revision 或调用 onDataChangecancelled 可以带 rejection: { code, message? };其他 reason 不应依赖异常文本做业务分支。

SchedulerDataChangeDescriptor 不再用一个 operation 混合描述数据动作和交互 手势:

  • actioncreateupdateremove
  • changeKinds 可同时包含 titletimerecurrencestateversionmetadataassignment
  • gesture 仅在空间编辑时出现,为 moveresizereassign
  • source 表示 apieditorpointerkeyboard

例如编辑器同时修改标题与 Resource 时,change 为 action: 'update'changeKinds: ['title', 'assignment'],不会再被错误归类为一次 reassign

change DTO 会深度冻结,并保留 string | number ID;它只包含变更需要的 Event 与 Assignment 字段,不包含业务 metadata、内部 token、布局或绘制对象。组件内部 记录仍保留原始 metadata。此 API 只修改当前 Web 客户端组件实例,不提供权限、 服务端保存、optimistic rollback 或冲突解决。默认 editable=false 时,调用 create/update/delete 会返回 editing-disabled

LIVE CANVAS可运行组件示例
ON-DEMAND RUNTIME交互式示例将在进入视区时启动避免文档首屏同时初始化多个 Canvas Runtime

本地提交不代表服务端保存。业务宿主可以在 onDataChange 中自行持久化,但 Scheduler 不等待网络结果,也不提供 pending、rollback 或 conflict 状态。

Timeline 指针创建、移动、重分配和拉伸

设置 editable: true 后,Day 和 Week Timeline 提供以下 Canvas 指针交互:

  • 双击空白时间槽:按 snapDurationMs 对齐时间,并以 defaultEventDurationMs 生成 SchedulerCreateEventContext;组件同步调用一次 createEventDraft(context),返回草稿时打开 create editor,返回 null 或抛错 时不创建记录;
  • 在 timed Event 上按下并移动至少 4px:进入拖动预览;水平移动按 snap 调整 start/end,并保持原 duration;
  • 拖到另一个可写 Resource 行:在同一次候选中移动时间并重分配当前 Assignment;
  • 悬停在 timed Event 已物化的左/右边缘:显示 Canvas resize handle 和 ew-resize 游标;按下后立即捕获指针,拖动左边缘只修改 start,拖动右边缘只 修改 end;
  • 拉伸使用与移动相同的 snapDurationMs,Event 最短可缩到一个 snap;原本短于 snap 的合法 Event 保留其原时长作为最小值。交叉、零时长、domain 外和 blocked 候选会被拒绝,不做静默裁剪;
  • 指针靠近 Timeline 边缘时:由一个内部计时器同时处理水平和垂直自动滚动, 预览按最新 scroll offset 重新投影。拉伸只启用水平自动滚动,不改变 Resource 轴位置。

拖动期间只绘制预览,不修改本地快照。松开指针时进行最后一次校验;合法候选最多 提交一个 revision 并触发一次 onDataChange,未变化、被拒绝、Escape、失焦、 取消 capture、切换 view/data 或释放组件都不会提交。

移动/重分配预览会弱化当前 Assignment 对应的源 Event,显示目标 Resource、snap guide 和 ghost;拉伸预览会同步弱化当前可见窗口内同一 Event 的全部 Assignment 投影,并在各自原 Resource 行绘制同一 start/end ghost。合法候选使用 selection/accent 语义色;readonly、disabled、recurrence、blocked range、domain 外或其他无效候选使用 danger 语义色并保持零写入。

指针空间编辑只适用于 Timeline 中的 timed Event。all-day、Agenda 和 copy/paste 不属于指针 candidate;这些记录仍可在适用时通过公共 CRUD 或默认 Event Editor 处理。

Timeline 键盘本地编辑

设置 editable: true、让 Scheduler 获得框架焦点并把 active 目标移动到可写的 Timeline timed Event 后,可以使用以下固定键位:

键位 本地操作
Enter 打开当前 Event 的默认编辑器;Agenda 也支持。
Delete 打开已有的二次删除确认;第一次按键不会删除。
Ctrl+← / Ctrl+→ 按一个 snapDurationMs 向前/向后移动,保持 duration。
Ctrl+↑ / Ctrl+↓ 把当前 Assignment 改派到扁平资源顺序中的上一个/下一个可写 Resource;跳过 readonly Resource。
Ctrl+Shift+← / Ctrl+Shift+→ 按一个 snap 调整 start。
Alt+← / Alt+→ 按一个 snap 调整 end。

组合键使用精确修饰键匹配;额外修饰键、输入法 composition 和未列出的键不会触发 本地修改。移动、改派和拉伸直接复用指针编辑的吸附候选与本地事务校验,不建立 pointer capture、拖动预览或 auto-scroll timer。成功命令恰好增加一次 revision, 触发一次 onDataChange,其 sourcekeyboard;移动、改派和拉伸的 action 均为 updategesture 分别为 movereassignresize,删除的 actionremove。readonly、disabled、recurrence、 blocked range、domain 外、最小时长和资源边界候选均保持零写入,不做静默裁剪。

直接空间键位只适用于 Day/Week Timeline 的 timed Event。all-day Event 与 Agenda 不执行 move/reassign/resize,但仍支持 Enter 打开编辑器和 Delete 二次确认。 这些键位是 Canvas 后台排班组件的效率交互,不表示存在语义 DOM、ARIA item projection 或屏幕阅读能力。

默认事件编辑器

设置 editable: true 后,Timeline、Agenda 和 dense overflow 共用同一个 Canvas Event Editor。可以通过以下方式打开:

  • 在 Timeline Event 上双击;
  • 在 Month Event 上双击;
  • 在 Agenda Event 上双击;
  • 在 overflow popup 中选择 Event;
  • 让 Scheduler 获得焦点后,对当前 active Event 按 Enter
  • 调用 openEventEditor(eventId, assignmentId?)

openEventEditor() 只为当前 ready、可见 projection 中能够解析到 Event、 Assignment 和 Resource 的目标打开 editor。组件已释放、编辑未开启、目标不存在、 目标不可见或 Event 为 disabled 时返回 false。再次打开另一个 Event 会先取消 前一个未提交会话。

编辑器提供:

  • Title;
  • start/end 日期;
  • timed Event 的 start/end 时间;
  • all-day 开关;
  • 当前 Assignment 的 Resource;
  • Save、Cancel 和经过二次确认的 Delete。

Save 使用与 public CRUD 相同的同步本地 command pipeline。标题、时间和 Resource 同时变化时仍只产生一个 revision 和一次 onDataChange。无变化的 Save 直接 关闭且不提交;无效字段保持 editor 打开并显示错误;Cancel、Escape 和 outside dismiss 都不会产生 callback。Delete 第一次只进入确认状态,第二次确认才删除 Event 及其全部 Assignments,并协调当前 selection。

readonly Event、带 recurrence identity 的 Event,以及当前 Assignment 指向 readonly Resource 时,editor 以 view-only 模式打开。view-only 允许查看和关闭, 不显示写入动作。disabled Event 不打开 editor。

UTC、all-day 和 Resource 语义

默认 editor 把 timed Event 的 epoch milliseconds 按 UTC 拆成日期和时间字段, 保存时再按 UTC 组合;它不会使用浏览器本地时区猜测业务时间。原 Event 上已有的 timeZone 记录会保留,但当前仍不提供生产级 IANA timezone / DST adapter。

all-day 表单中的结束日期是用户可见的包含式日期;写回 SchedulerAllDayRange.endExclusive 时转换成下一日的排他边界。例如,UI 中 “2026-07-31 至 2026-07-31”会保存为:

const savedAllDayTime = {  kind: 'all-day',  range: {    start: { year: 2026, month: 7, day: 31 },    endExclusive: { year: 2026, month: 8, day: 1 },  },} as const

Resource 选择保留原始 string | number ID,因此数字 1 与字符串 '1' 不会 碰撞。readonly Resource 可以显示,但不能成为可写提交目标。

Event 和 Assignment 的业务 metadata 会在组件内部原记录中保留,但不会进入 SchedulerDataChange。change DTO 只返回已经公开的最小字段,避免把业务 metadata 或内部布局对象意外暴露给通用组件层。

取消与宿主切换

Scheduler 把尚未提交的 pointer candidate、Event Editor 会话和被动 popup 分成 独立 owner。取消只清理临时 UI 状态,不产生 Store revision 或 onDataChange

原因 处理结果
pointercancel、touch cancel、window blur 释放 pointer capture,停止 auto-scroll,清除 preview、resize cursor、pressed/hover 和 tooltip;不提交候选。
Scheduler focusOut() 取消仍由 Scheduler 持有的 pointer sequence,但不关闭 Event Editor;焦点从 Scheduler 转入 popup 表单属于正常流程。
Escape 最内层 Editor popup / Delete confirmation 优先,其次是 Event Editor、overflow、active pointer 和 tooltip;一次按键只关闭一层,且不提交。
editable=false 取消 pointer,关闭 Editor、Delete confirmation、overflow 和 tooltip;保留仍有效的 selection 身份。
setView()setZoomPreset() 先取消所有未提交编辑,再切换 view 或重新投影密度;zoom 保留有效 selection,view 重新建立 active navigation。
成功的 setData() 先完整验证候选快照,验证成功后才清理旧 owner 并原子替换;旧 pointerup、Save 或 Delete confirmation 随后均失效。
setQueryKey()refresh() 取消未提交交互并等待新 projection;不产生本地 mutation。
visible=falsedetach()dispose() 清理 capture、timer、preview、popup、tooltip 和框架焦点注册,不尝试把焦点恢复到已隐藏或已卸载的 Scheduler。

无效 setData() 候选在清理之前失败,因此当前数据、Editor、pointer owner、view、 zoom、scroll 和 selection 都保持原样。普通滚动不属于全局取消:captured drag 可继续自动滚动,Editor anchor 可见时继续跟随;目标离开 projection 后才关闭 Editor。

键盘 move、reassign 和 resize 是当前调用栈内完成的同步命令,不留下可取消的 capture、timer 或 preview。Agenda 与 all-day 仍是 editor-only,不执行空间 pointer 或键盘 mutation。

完整快照替换

使用 setData() 原子替换整个本地快照:

const accepted = scheduler.setData(nextData, {
  queryKey: 'week-2026-08-03',
  domain: nextDomain,
})
  • 返回 true 表示新快照已通过同步校验并完成替换;
  • invalid data 或 domain 会同步抛出 configuration error;
  • 校验失败时旧 data、domain、query key、view、zoom、scroll 和 selection 保持 不变;
  • view 和 zoom 会保留,时间与资源锚点会裁剪到新数据边界;
  • 原身份仍存在时保留选择,否则在新快照就绪后清除;
  • hover、pressed、tooltip、overflow 和 pointer capture 不跨数据代际;
  • 组件已释放时返回 false

setData() 是完整替换,不是增量 patch。调用方应先在业务层构造一致的新数组, 再一次提交。

状态、刷新和错误

API 用途
setQueryKey(queryKey) 改变当前查询身份并请求重新布局。
refresh() 重新处理当前本地快照的可见范围。
retry() 当前为可重试的 errorrefreshing-error 时重试。
whenSettled() 等待当前可见更新全部收敛。
debugState() 读取稳定的只读标量摘要。

SchedulerStatus 包含 idleloadingrefreshingrefreshing-errorreadyemptyerrordisposed。刷新时可以保留上一份可见内容;错误面 是否提供 retry 由 debugState().retryable 表示。保留内容不等同于当前数据已 ready:刷新或刷新失败期间不会接受新的指针、键盘或 Editor 本地编辑。

当前数据源仍是本地 snapshot。以上状态 API 不代表存在远程 Provider、server paging 或网络请求契约。

Theme、高对比和绘制

Scheduler 使用框架统一主题,支持 light、dark 和 high-contrast palette。业务层 通过应用主题切换外观,不应覆盖内部 Event、header、selection、拖动 ghost、 target highlight、snap guide 或 scrollbar 绘制状态。High Contrast 的合法与 拒绝预览同时依靠语义颜色和明确边框区分。

组件按可见资源行和时间窗口执行 layout、paint 和 hit-test。长时间范围或大量 Event 不会要求每帧物化全部记录;setData() 本身仍允许进行 O(input size) 的 关系校验和索引重建。

RenderSchedulerDebugState 只暴露:

  • status、retryable、disposed;
  • view、zoom、scroll、content/body/max-scroll 尺寸;
  • visible rows、events、overflow;
  • pending updates、active requests;
  • layout、paint、hit-test 次数;
  • transient surface、focus、pointer capture 和 overflow popup 状态。
  • editing enabled、editor open/mode、mutation commit/reject 和 change callback 计数。
  • pointer interaction、edit gesture phase、preview mode/validity、preview/ validation/commit/cancel 计数、draft factory 计数,以及 auto-scroll tick/ active timer 计数。

这些字段用于稳定诊断,不提供内部 rect、token、owner、完整布局快照或 painter 对象。

LIVE CANVAS可运行组件示例
ON-DEMAND RUNTIME交互式示例将在进入视区时启动避免文档首屏同时初始化多个 Canvas Runtime

时间、recurrence 和时区边界

  • domain 必须是固定、有限且有效的 SchedulerInstantRange
  • timed Event 和时间定位统一使用 epoch milliseconds;
  • instant 和 all-day 区间都使用结束端 exclusive 的半开语义;
  • SchedulerRecurrenceDefinition 是共享的 recurrence master 记录,但 RenderScheduler 的本地数据入口采用 expanded-occurrences 模式,不接受 master;
  • Scheduler 不执行 recurrence expansion;调用方必须先生成需要显示的 occurrences,并为每条记录同时提供 recurrenceParentIdoriginalOccurrenceStart
  • 当前不提供生产级 IANA timezone / DST adapter;
  • 调用方必须在构造 snapshot 前确定 display zone 和消歧规则,不能依赖浏览器 本地时区猜测业务时间。

例如,调用方把外部 series 展开后,可以把单次 timed occurrence 写成:

import type { SchedulerEvent } from 'ds-ui' const expandedOccurrence: SchedulerEvent = {  id: 'series-42@2026-07-28T09:00Z',  title: '每日交接班',  time: {    kind: 'timed',    range: {      startEpochMs: Date.UTC(2026, 6, 28, 9),      endEpochMs: Date.UTC(2026, 6, 28, 10),    },    timeZone: 'UTC',  },  recurrenceParentId: 'series-42',  originalOccurrenceStart: {    kind: 'timed',    originalWallStart: {      date: { year: 2026, month: 7, day: 28 },      hour: 9,      minute: 0,      second: 0,      millisecond: 0,    },    timeZone: 'UTC',    originalOffsetMinutes: 0,    fold: 'unambiguous',  },}

recurrenceParentId 可以引用 snapshot 外部的 series 身份;不要同时把带 recurrence 的 master 混入同一个 RenderScheduler 本地快照。带 recurrence identity 的 occurrence 当前只读,不进入 Editor 或空间 mutation。

生命周期

Scheduler 跟随 render tree 的 attach、detach 和 dispose 生命周期。宿主页面 销毁时应释放持有它的 render tree。dispose() 会清理当前更新、popup、tooltip、 pointer capture、拖动预览、auto-scroll timer 和内部 owned resources;释放后 业务方法不会重新创建资源。

在替换页面或执行确定性截图前,可以先等待 whenSettled(),再读取 debugState() 或进行下一步操作。

当前产品限制

  • 仅支持 local SchedulerDataSnapshot
  • 默认仍是只读浏览;opt-in 客户端编辑提供 create/update/delete 和默认 Canvas Event Editor,并提供 Timeline blank-slot create、timed Event move 与 Resource reassign、start/end resize,以及上述受限键盘空间编辑,但不提供 all-day/Agenda 空间编辑或 copy/paste;
  • 不提供 remote provider、server paging 或 optimistic mutation;
  • 不提供 recurrence engine/editor,recurrence 必须由调用方预先展开;
  • 不提供生产级 IANA timezone / DST adapter;
  • Canvas 绘制和交互当前只支持 Google Chrome / Chromium;
  • 浏览器兼容范围与屏幕阅读能力是两个独立边界;
  • Scheduler 在任何浏览器上都不提供屏幕阅读能力,也不生成供屏幕阅读器消费的 Event/Resource 语义或 ARIA item projection。

相关文档