REAL SHOWCASE SOURCEscheduler-local-editing
3 个文件 · 1106 行 · TypeScript
// ===== showcase/src/examples/scheduler/local_editing.ts ===== import { RenderButton, RenderSwitch, RenderText, RenderWrapPanel,} from 'ds-ui'import { createSchedulerDateNavigation, createSchedulerDraftFactory, createSchedulerEditingSnapshot, createSchedulerStage, createShowcaseScheduler, schedulerChangeText, schedulerNowEpochMs, schedulerShowcaseDomain, schedulerWeekStartEpochMs,} from './shared' export function createSchedulerLocalEditingExample() { const initialDateEpochMs = schedulerWeekStartEpochMs + 10 * 60 * 60 * 1_000 const status = new RenderText( '编辑已开启:双击空白创建;拖动 Event 移动或改派;拖左右边缘调整起止时间。', { role: 'secondary' }, ) const scheduler = createShowcaseScheduler({ data: createSchedulerEditingSnapshot(), domain: schedulerShowcaseDomain, queryKey: 'showcase-scheduler-local-editing', title: '资源', view: 'resourceTimelineDay', zoomPreset: 'comfortable', initialDateEpochMs, editable: true, snapDurationMs: 30 * 60 * 1_000, defaultEventDurationMs: 60 * 60 * 1_000, createEventDraft: createSchedulerDraftFactory(), onDataChange: change => { status.text = schedulerChangeText(change) }, }) scheduler.setNowEpochMs(schedulerNowEpochMs) const navigation = createSchedulerDateNavigation({ scheduler, domain: schedulerShowcaseDomain, initialDateEpochMs, initialView: 'resourceTimelineDay', }) let apiSequence = 0 const controls = new RenderWrapPanel({ spacing: 8, runSpacing: 8 }) controls.addChild(new RenderSwitch({ checked: true, label: '启用本地编辑', onChange: checked => { scheduler.editable = checked status.text = checked ? '本地编辑已开启。所有提交只修改当前组件实例。' : '本地编辑已关闭;未提交的拖动、弹层和预览已经取消。' }, })) controls.addChild(new RenderButton({ label: 'API 创建', variant: 'primary', onClick: () => { const nextSequence = apiSequence + 1 const eventId = `api-created-${nextSequence}` const startEpochMs = schedulerWeekStartEpochMs + 10 * 60 * 60 * 1_000 + (30 + (nextSequence - 1) * 30) * 60 * 1_000 const result = scheduler.createEvent({ event: { id: eventId, title: `API 创建排期 ${nextSequence}`, time: { kind: 'timed', range: { startEpochMs, endEpochMs: startEpochMs + 60 * 60 * 1_000, }, }, }, assignments: [{ id: `assignment-${eventId}`, eventId, resourceId: 'room-02', }], }) if (!result.ok) { status.text = `API 创建被拒绝:${result.reason}` return } apiSequence = nextSequence status.text = `已创建:API 创建排期 ${nextSequence} · 会议室 02 · ${formatUtcTime(startEpochMs)}` }, })) controls.addChild(new RenderButton({ label: '打开事件编辑器', onClick: () => { const opened = scheduler.openEventEditor( 'editable-customer-review', 'assignment-editable-customer-review', ) if (!opened) status.text = '当前事件尚未进入可见投影,请先定位到首日。' }, })) controls.addChild(new RenderButton({ label: '回到首日', onClick: () => { scheduler.setView('resourceTimelineDay') navigation.setView('resourceTimelineDay') navigation.reset() }, })) return createSchedulerStage({ title: 'Scheduler Canvas 本地编辑', description: '创建、编辑、移动、跨资源改派、起止拉伸和删除共用一个原子客户端变更管线。', scheduler, navigation: navigation.controls, controls, status, })} function formatUtcTime(epochMs: number): string { const value = new Date(epochMs) return `${String(value.getUTCHours()).padStart(2, '0')}:${String(value.getUTCMinutes()).padStart(2, '0')} UTC`} // ===== showcase/src/examples/scheduler/date_navigation.ts ===== import { RenderButton, RenderDatePicker, RenderScheduler, RenderText, RenderWrapPanel, type SchedulerInstantRange, type SchedulerViewportChangeEvent, type SchedulerView, type SchedulerViewport,} from 'ds-ui' const schedulerDayMs = 24 * 60 * 60 * 1_000const viewportListeners = new WeakMap< RenderScheduler, Set<(event: SchedulerViewportChangeEvent) => void>>() export interface SchedulerDateNavigation { readonly controls: RenderWrapPanel readonly datePicker: RenderDatePicker readonly dateEpochMs: number readonly view: SchedulerView navigateToDate(epochMs: number): boolean reset(): boolean setView(view: SchedulerView): void} export function createSchedulerDateNavigation(options: { scheduler: RenderScheduler domain: SchedulerInstantRange initialDateEpochMs: number initialView: SchedulerView}): SchedulerDateNavigation { const controls = new RenderWrapPanel({ spacing: 8, runSpacing: 8 }) let view = options.initialView let dateEpochMs = clampDate(options.initialDateEpochMs, options.domain) let viewport: SchedulerViewport | null = null const previousButton = new RenderButton({ label: '', onClick: () => navigateToDate(shiftDate(dateEpochMs, view, -1)), }) const datePicker = new RenderDatePicker({ value: formatIsoDate(dateEpochMs), prefixText: '日期', onChange: value => { const selected = parseIsoDate(value, dateEpochMs) if (selected === null || navigateToDate(selected)) return datePicker.value = formatIsoDate(dateEpochMs) }, }) datePicker.width = 168 const resetButton = new RenderButton({ label: '回到演示基准日', onClick: () => navigateToDate(options.initialDateEpochMs), }) const nextButton = new RenderButton({ label: '', onClick: () => navigateToDate(shiftDate(dateEpochMs, view, 1)), }) const rangeText = new RenderText('', { role: 'secondary' }) controls.addChild(previousButton) controls.addChild(datePicker) controls.addChild(resetButton) controls.addChild(nextButton) controls.addChild(rangeText) function navigateToDate(epochMs: number): boolean { const next = clampDate(epochMs, options.domain) const changed = options.scheduler.scrollToDate(next, 'center') if (!changed && next !== dateEpochMs) { if (canAdoptStationaryBoundaryDate( dateEpochMs, next, viewport, options.domain, )) { dateEpochMs = next refresh() return true } refresh() return false } dateEpochMs = next refresh() return changed } function refresh(): void { datePicker.value = formatIsoDate(dateEpochMs) const labels = navigationLabels(view) updateButtonLabel(previousButton, labels.previous) updateButtonLabel(nextButton, labels.next) previousButton.disabled = viewportTouchesDomainStart(viewport, options.domain) || clampDate(shiftDate(dateEpochMs, view, -1), options.domain) === dateEpochMs nextButton.disabled = viewportTouchesDomainEnd(viewport, options.domain) || clampDate(shiftDate(dateEpochMs, view, 1), options.domain) === dateEpochMs resetButton.disabled = dateEpochMs === clampDate(options.initialDateEpochMs, options.domain) rangeText.text = navigationRangeText(dateEpochMs, view, options.domain) } const navigation: SchedulerDateNavigation = { controls, datePicker, get dateEpochMs() { return dateEpochMs }, get view() { return view }, navigateToDate, reset: () => navigateToDate(options.initialDateEpochMs), setView: nextView => { if (view === nextView) return view = nextView refresh() }, } subscribeViewport(options.scheduler, event => { viewport = event.current view = viewport.view const range = viewport.visibleTimeRange if ( range !== null && (event.source === 'pointer' || event.source === 'keyboard') ) { dateEpochMs = clampDate( (range.startEpochMs + range.endEpochMs) / 2, options.domain, ) } refresh() }) refresh() return navigation} export function notifySchedulerDateNavigationViewport( scheduler: RenderScheduler, event: SchedulerViewportChangeEvent,): void { viewportListeners.get(scheduler)?.forEach(listener => listener(event))} function subscribeViewport( scheduler: RenderScheduler, listener: (event: SchedulerViewportChangeEvent) => void,): void { let listeners = viewportListeners.get(scheduler) if (!listeners) { listeners = new Set() viewportListeners.set(scheduler, listeners) } listeners.add(listener)} function canAdoptStationaryBoundaryDate( currentEpochMs: number, nextEpochMs: number, viewport: SchedulerViewport | null, domain: SchedulerInstantRange,): boolean { if (nextEpochMs < currentEpochMs) { return viewportTouchesDomainStart(viewport, domain) } if (nextEpochMs > currentEpochMs) { return viewportTouchesDomainEnd(viewport, domain) } return false} function viewportTouchesDomainStart( viewport: SchedulerViewport | null, domain: SchedulerInstantRange,): boolean { const range = viewport?.visibleTimeRange return range !== null && range !== undefined && range.startEpochMs <= domain.startEpochMs} function viewportTouchesDomainEnd( viewport: SchedulerViewport | null, domain: SchedulerInstantRange,): boolean { const range = viewport?.visibleTimeRange return range !== null && range !== undefined && range.endEpochMs >= domain.endEpochMs} function navigationLabels(view: SchedulerView): { readonly previous: string readonly next: string} { if (view === 'resourceTimelineDay') { return { previous: '上一天', next: '下一天' } } if (view === 'resourceTimelineMonth') { return { previous: '上一月', next: '下一月' } } return { previous: '上一周', next: '下一周' }} function shiftDate( epochMs: number, view: SchedulerView, direction: -1 | 1,): number { const value = new Date(epochMs) if (view === 'resourceTimelineMonth') { const day = value.getUTCDate() const month = new Date(Date.UTC( value.getUTCFullYear(), value.getUTCMonth() + direction, 1, value.getUTCHours(), value.getUTCMinutes(), value.getUTCSeconds(), value.getUTCMilliseconds(), )) const lastDay = new Date(Date.UTC( month.getUTCFullYear(), month.getUTCMonth() + 1, 0, )).getUTCDate() month.setUTCDate(Math.min(day, lastDay)) return month.getTime() } const days = view === 'resourceTimelineDay' ? 1 : 7 value.setUTCDate(value.getUTCDate() + direction * days) return value.getTime()} function navigationRangeText( epochMs: number, view: SchedulerView, domain: SchedulerInstantRange,): string { if (view === 'resourceTimelineDay') { return `当前日期 ${formatIsoDate(epochMs)}` } let startEpochMs = epochMs let endEpochMs = epochMs if (view === 'resourceTimelineMonth') { const value = new Date(epochMs) startEpochMs = Date.UTC(value.getUTCFullYear(), value.getUTCMonth(), 1) endEpochMs = Date.UTC(value.getUTCFullYear(), value.getUTCMonth() + 1, 0) } else { const mondayOffset = (new Date(epochMs).getUTCDay() + 6) % 7 startEpochMs = epochMs - mondayOffset * schedulerDayMs endEpochMs = startEpochMs + 6 * schedulerDayMs } startEpochMs = clampDate(startEpochMs, domain) endEpochMs = clampDate(endEpochMs, domain) const prefix = view === 'agenda' ? 'Agenda 范围' : '当前范围' return `${prefix} ${formatIsoDate(startEpochMs)} — ` + formatIsoDate(endEpochMs)} function clampDate( epochMs: number, domain: SchedulerInstantRange,): number { const maxEpochMs = Math.max(domain.startEpochMs, domain.endEpochMs - 1) if (!Number.isFinite(epochMs)) return domain.startEpochMs if (epochMs >= domain.startEpochMs && epochMs <= maxEpochMs) { return epochMs } const source = new Date(epochMs) const boundary = new Date( epochMs < domain.startEpochMs ? domain.startEpochMs : maxEpochMs, ) const sameTimeOnBoundaryDate = Date.UTC( boundary.getUTCFullYear(), boundary.getUTCMonth(), boundary.getUTCDate(), source.getUTCHours(), source.getUTCMinutes(), source.getUTCSeconds(), source.getUTCMilliseconds(), ) return Math.min( Math.max(sameTimeOnBoundaryDate, domain.startEpochMs), maxEpochMs, )} function formatIsoDate(epochMs: number): string { const value = new Date(epochMs) return [ String(value.getUTCFullYear()).padStart(4, '0'), String(value.getUTCMonth() + 1).padStart(2, '0'), String(value.getUTCDate()).padStart(2, '0'), ].join('-')} function parseIsoDate( value: string, timeSourceEpochMs: number,): number | null { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value) if (!match) return null const source = new Date(timeSourceEpochMs) const result = Date.UTC( Number(match[1]), Number(match[2]) - 1, Number(match[3]), source.getUTCHours(), source.getUTCMinutes(), source.getUTCSeconds(), source.getUTCMilliseconds(), ) const parsed = new Date(result) if ( parsed.getUTCFullYear() !== Number(match[1]) || parsed.getUTCMonth() !== Number(match[2]) - 1 || parsed.getUTCDate() !== Number(match[3]) ) { return null } return result} function updateButtonLabel(button: RenderButton, label: string): void { if (button.label === label) return button.label = label button.markNeedsLayout()} // ===== showcase/src/examples/scheduler/shared.ts ===== import { RenderPageHeader, RenderScheduler, RenderStackPanel, RenderText, type RenderBox, type RenderSchedulerOptions, type SchedulerAssignment, type SchedulerCreateEventContext, type SchedulerDataChange, type SchedulerDataSnapshot, type SchedulerEvent, type SchedulerEventDraft, type SchedulerInstantRange, type SchedulerKey, type SchedulerReadonlyItem, type SchedulerResource, type SchedulerTimeRange,} from 'ds-ui'import { notifySchedulerDateNavigationViewport,} from './date_navigation' export { createSchedulerDateNavigation, type SchedulerDateNavigation,} from './date_navigation' export const schedulerHourMs = 60 * 60 * 1_000export const schedulerDayMs = 24 * schedulerHourMsexport const schedulerWeekStartEpochMs = Date.UTC(2026, 6, 27)export const schedulerNowEpochMs = Date.UTC(2026, 6, 28, 10, 30) export const schedulerShowcaseDomain: SchedulerInstantRange = Object.freeze({ startEpochMs: Date.UTC(2026, 6, 1), endEpochMs: Date.UTC(2026, 9, 1),}) export const schedulerLargeDomain = schedulerShowcaseDomain const resourceColors = [ '#2563eb', '#0891b2', '#059669', '#65a30d', '#ca8a04', '#ea580c', '#dc2626', '#db2777', '#9333ea', '#4f46e5', '#0f766e', '#475569',] as const export interface SchedulerStageOptions { title: string description: string scheduler: RenderScheduler navigation?: RenderBox controls?: RenderBox status?: RenderText} export function createSchedulerStage(options: SchedulerStageOptions): RenderStackPanel { const panel = new RenderStackPanel({ orientation: 'vertical', spacing: 10, crossAxisAlignment: 'stretch', padding: 16, }) panel.addChild(new RenderPageHeader({ title: options.title, description: options.description, })) if (options.navigation) panel.addChild(options.navigation) if (options.controls) panel.addChild(options.controls) options.scheduler.minHeight = 330 panel.addChild(options.scheduler, 1) if (options.status) panel.addChild(options.status) return panel} export function createSchedulerOverviewSnapshot( options: { resourceCount?: number routineEventsPerResource?: number denseEventCount?: number } = {},): SchedulerDataSnapshot { const resourceCount = options.resourceCount ?? 12 const routineEventsPerResource = options.routineEventsPerResource ?? 3 const denseEventCount = options.denseEventCount ?? 8 const resources = createResources(resourceCount, 2) const events: SchedulerEvent[] = [] const assignments: SchedulerAssignment[] = [] const weekMs = 7 * schedulerDayMs const firstWeekOffset = Math.floor( (schedulerShowcaseDomain.startEpochMs - schedulerWeekStartEpochMs) / weekMs, ) const lastWeekOffset = Math.ceil( (schedulerShowcaseDomain.endEpochMs - schedulerWeekStartEpochMs) / weekMs, ) - 1 for (let resourceIndex = 0; resourceIndex < resources.length; resourceIndex += 1) { const resource = resources[resourceIndex]! for ( let weekOffset = firstWeekOffset; weekOffset <= lastWeekOffset; weekOffset += 1 ) { for ( let occurrence = 0; occurrence < routineEventsPerResource; occurrence += 1 ) { const eventId = weekOffset === 0 ? `plan-${resourceIndex + 1}-${occurrence + 1}` : `plan-${resourceIndex + 1}-${occurrence + 1}-week-${weekOffset}` const dayIndex = (resourceIndex + occurrence * 2) % 7 const startHour = 8 + (resourceIndex + occurrence) % 8 const startEpochMs = schedulerWeekStartEpochMs + weekOffset * weekMs + dayIndex * schedulerDayMs + startHour * schedulerHourMs + (occurrence % 2) * 30 * 60 * 1_000 if ( startEpochMs < schedulerShowcaseDomain.startEpochMs || startEpochMs >= schedulerShowcaseDomain.endEpochMs ) { continue } events.push({ id: eventId, title: `业务排期 ${resourceIndex + 1}-${occurrence + 1}`, time: { kind: 'timed', range: { startEpochMs, endEpochMs: startEpochMs + (60 + ((resourceIndex + occurrence) % 3) * 30) * 60 * 1_000, }, }, version: 1, }) assignments.push({ id: `assignment-${eventId}`, eventId, resourceId: resource.id, version: 1, }) } } } const readonlyEvent = createTimedEvent({ id: 'readonly-audit', title: '只读 · 审计复核', dayIndex: 0, startHour: 9, durationMinutes: 90, readOnly: true, }) events.push(readonlyEvent) assignments.push({ id: 'assignment-readonly-audit', eventId: readonlyEvent.id, resourceId: 'room-02', }) const disabledEvent = createTimedEvent({ id: 'disabled-maintenance', title: '禁用 · 设备维护', dayIndex: 2, startHour: 10, durationMinutes: 120, disabled: true, }) events.push(disabledEvent) assignments.push({ id: 'assignment-disabled-maintenance', eventId: disabledEvent.id, resourceId: 'room-03', }) events.push({ id: 'all-day-upgrade', title: '全天 · 系统升级窗口', time: { kind: 'all-day', range: { start: { year: 2026, month: 7, day: 30 }, endExclusive: { year: 2026, month: 8, day: 1 }, }, }, readOnly: true, }) assignments.push({ id: 'assignment-all-day-upgrade', eventId: 'all-day-upgrade', resourceId: 'room-04', }) appendDenseEvents( events, assignments, denseEventCount, 'room-01', schedulerWeekStartEpochMs + schedulerDayMs + 10 * schedulerHourMs, ) return { resources, events, assignments, timeRanges: createWeekTimeRanges(resources), }} export function createSchedulerEditingSnapshot(): SchedulerDataSnapshot { const snapshot = createSchedulerOverviewSnapshot({ resourceCount: 8, routineEventsPerResource: 1, denseEventCount: 0, }) return { ...snapshot, resources: snapshot.resources.map((resource, index) => ({ ...resource, ...(index < 4 ? { readOnly: false } : { readOnly: true }), title: index < 4 ? `会议室 ${String(index + 1).padStart(2, '0')}(可写)` : `会议室 ${String(index + 1).padStart(2, '0')}(只读)`, })), events: snapshot.events.map(event => event.id === 'plan-1-1' ? { ...event, id: 'editable-customer-review', title: '可编辑 · 客户评审', } : event), assignments: snapshot.assignments.map(assignment => ( assignment.eventId === 'plan-1-1' ? { ...assignment, id: 'assignment-editable-customer-review', eventId: 'editable-customer-review', } : assignment )), }} export function createSchedulerDenseSnapshot(): SchedulerDataSnapshot { const resources = createResources(6, 1) const events: SchedulerEvent[] = [] const assignments: SchedulerAssignment[] = [] const timeRanges: SchedulerTimeRange[] = [] const baselineStartEpochMs = schedulerWeekStartEpochMs + schedulerDayMs + 9 * schedulerHourMs const dayCount = Math.ceil( (schedulerShowcaseDomain.endEpochMs - schedulerShowcaseDomain.startEpochMs) / schedulerDayMs, ) for (let dayIndex = 0; dayIndex < dayCount; dayIndex += 1) { const dayStartEpochMs = schedulerShowcaseDomain.startEpochMs + dayIndex * schedulerDayMs const startEpochMs = dayStartEpochMs + 9 * schedulerHourMs const isBaseline = startEpochMs === baselineStartEpochMs const dayToken = `day-${dayIndex + 1}` appendDenseEvents( events, assignments, 28, 'room-01', startEpochMs, isBaseline ? 'dense' : `dense-${dayToken}`, ) for ( let resourceIndex = 1; resourceIndex < resources.length; resourceIndex += 1 ) { for (let index = 0; index < 4; index += 1) { const eventId = isBaseline ? `secondary-${resourceIndex}-${index}` : `secondary-${dayToken}-${resourceIndex}-${index}` const eventStart = startEpochMs + resourceIndex * 20 * 60 * 1_000 + index * 45 * 60 * 1_000 events.push({ id: eventId, title: `并发协作 ${resourceIndex}-${index + 1}`, time: { kind: 'timed', range: { startEpochMs: eventStart, endEpochMs: eventStart + 100 * 60 * 1_000, }, }, readOnly: index === 2, disabled: index === 3, }) assignments.push({ id: `assignment-${eventId}`, eventId, resourceId: resources[resourceIndex]!.id, }) } } timeRanges.push({ id: isBaseline ? 'dense-blocked-range' : `dense-blocked-range-${dayToken}`, resourceId: 'room-05', kind: 'blocked', time: { kind: 'timed', range: { startEpochMs: startEpochMs + 2 * schedulerHourMs, endEpochMs: startEpochMs + 5 * schedulerHourMs, }, }, readOnly: true, }) } return { resources, events, assignments, timeRanges, }} export function createSchedulerLargeSnapshot( resourceCount: number, eventCount: number,): SchedulerDataSnapshot { const resources = createResources(resourceCount, 0).map(resource => ({ ...resource, readOnly: true, })) const events: SchedulerEvent[] = [] const assignments: SchedulerAssignment[] = [] const timelineDays = Math.floor( (schedulerLargeDomain.endEpochMs - schedulerLargeDomain.startEpochMs) / schedulerDayMs, ) for (let index = 0; index < eventCount; index += 1) { const eventId = `large-event-${index + 1}` const resourceIndex = index % resourceCount const dayIndex = (index * 17 + Math.floor(index / resourceCount)) % timelineDays const minuteOfDay = 7 * 60 + (index * 37) % (12 * 60) const startEpochMs = schedulerLargeDomain.startEpochMs + dayIndex * schedulerDayMs + minuteOfDay * 60 * 1_000 events.push({ id: eventId, title: `计划任务 ${String(index + 1).padStart(5, '0')}`, time: { kind: 'timed', range: { startEpochMs, endEpochMs: startEpochMs + (30 + (index % 6) * 15) * 60 * 1_000, }, }, readOnly: true, }) assignments.push({ id: `large-assignment-${index + 1}`, eventId, resourceId: resources[resourceIndex]!.id, }) } return { resources, events, assignments, timeRanges: [], }} export function createSchedulerDraftFactory(): ( context: SchedulerCreateEventContext,) => SchedulerEventDraft { let sequence = 0 return context => { sequence += 1 const eventId = `created-event-${sequence}` return { event: { id: eventId, title: `新建排期 ${sequence}`, time: { kind: 'timed', range: { startEpochMs: context.startEpochMs, endEpochMs: context.endEpochMs, }, }, }, assignments: [{ id: `created-assignment-${sequence}`, eventId, resourceId: context.resourceId, }], } }} export function schedulerSelectionText(item: SchedulerReadonlyItem | null): string { if (!item) return '未选择日程' const state = item.disabled ? '禁用' : item.readOnly ? '只读' : '可写' return `已选择:${item.title} · ${item.resourceTitle} · ${state}`} export function schedulerChangeText(change: SchedulerDataChange): string { const title = change.event.after?.title ?? change.event.before?.title ?? String(change.event.after?.id ?? change.event.before?.id ?? 'Event') const gesture = change.gesture ? ` / ${change.gesture}` : '' return `本地提交 #${change.revision}:${change.action}${gesture} · ${title}`} export function createShowcaseScheduler( options: Omit<RenderSchedulerOptions, 'minHeight'> & { initialDateEpochMs?: number },): RenderScheduler { const { initialDateEpochMs, onStateChange, onViewportChange, ...schedulerOptions } = options let initialPositionApplied = false let scheduler!: RenderScheduler scheduler = new RenderScheduler({ minHeight: 330, resourceHeaderWidth: 210, timeHeaderHeight: 50, ...schedulerOptions, onViewportChange: event => { onViewportChange?.(event) notifySchedulerDateNavigationViewport(scheduler, event) }, onStateChange: state => { onStateChange?.(state) if ( initialPositionApplied || initialDateEpochMs === undefined || state.status !== 'ready' || state.bodyWidth <= 0 || state.bodyHeight <= 0 ) { return } initialPositionApplied = true queueMicrotask(() => { scheduler.scrollToDate(initialDateEpochMs, 'center') }) }, }) return scheduler} function createResources( count: number, writableCount: number,): SchedulerResource[] { return Array.from({ length: count }, (_, index) => ({ id: `room-${String(index + 1).padStart(2, '0')}`, title: index < writableCount ? `会议室 ${String(index + 1).padStart(2, '0')}(可写)` : `会议室 ${String(index + 1).padStart(2, '0')}(只读)`, color: resourceColors[index % resourceColors.length], ...(index < writableCount ? {} : { readOnly: true }), sortOrder: index, version: 1, }))} function createWeekTimeRanges( resources: readonly SchedulerResource[],): SchedulerTimeRange[] { const ranges: SchedulerTimeRange[] = Array.from( { length: 7 }, (_, dayIndex) => ({ id: `working-${dayIndex + 1}`, kind: 'working', time: { kind: 'timed', range: { startEpochMs: schedulerWeekStartEpochMs + dayIndex * schedulerDayMs + 8 * schedulerHourMs, endEpochMs: schedulerWeekStartEpochMs + dayIndex * schedulerDayMs + 18 * schedulerHourMs, }, }, readOnly: true, }), ) if (resources.length >= 3) { ranges.push({ id: 'unavailable-room-03', resourceId: resources[2]!.id, kind: 'unavailable', time: { kind: 'timed', range: { startEpochMs: schedulerWeekStartEpochMs + schedulerDayMs + 12 * schedulerHourMs, endEpochMs: schedulerWeekStartEpochMs + schedulerDayMs + 15 * schedulerHourMs, }, }, readOnly: true, }) } if (resources.length >= 6) { ranges.push({ id: 'blocked-room-06', resourceId: resources[5]!.id, kind: 'blocked', time: { kind: 'timed', range: { startEpochMs: schedulerWeekStartEpochMs + 2 * schedulerDayMs + 12 * schedulerHourMs, endEpochMs: schedulerWeekStartEpochMs + 2 * schedulerDayMs + 16 * schedulerHourMs, }, }, readOnly: true, }) } return ranges} function createTimedEvent(options: { id: SchedulerKey title: string dayIndex: number startHour: number durationMinutes: number readOnly?: boolean disabled?: boolean}): SchedulerEvent { const startEpochMs = schedulerWeekStartEpochMs + options.dayIndex * schedulerDayMs + options.startHour * schedulerHourMs return { id: options.id, title: options.title, time: { kind: 'timed', range: { startEpochMs, endEpochMs: startEpochMs + options.durationMinutes * 60 * 1_000, }, }, readOnly: options.readOnly, disabled: options.disabled, version: 1, }} function appendDenseEvents( events: SchedulerEvent[], assignments: SchedulerAssignment[], count: number, resourceId: SchedulerKey, baseStartEpochMs: number, idPrefix = 'dense',): void { for (let index = 0; index < count; index += 1) { const eventId = `${idPrefix}-${index + 1}` const startEpochMs = baseStartEpochMs + index * 5 * 60 * 1_000 events.push({ id: eventId, title: `并发任务 ${index + 1}`, time: { kind: 'timed', range: { startEpochMs, endEpochMs: startEpochMs + 100 * 60 * 1_000, }, }, readOnly: index % 7 === 5, disabled: index % 11 === 9, }) assignments.push({ id: `assignment-${eventId}`, eventId, resourceId, }) }}