REAL SHOWCASE SOURCEscheduler-multi-view
2 个文件 · 703 行 · TypeScript
// ===== showcase/src/examples/scheduler/multi_view.ts ===== import { RenderButton, RenderSegmentedControl, RenderText, RenderWrapPanel, type SchedulerView, type SchedulerZoomPreset,} from 'ds-ui'import { createSchedulerOverviewSnapshot, createSchedulerStage, createShowcaseScheduler, schedulerMonthDomain, schedulerNowEpochMs,} from './shared' const views: readonly SchedulerView[] = [ 'resourceTimelineDay', 'resourceTimelineWeek', 'resourceTimelineMonth', 'agenda',] const zoomPresets: readonly SchedulerZoomPreset[] = [ 'compact', 'standard', 'comfortable',] export function createSchedulerMultiViewExample() { const status = new RenderText( '当前:Week · 标准密度。切换视图不会修改底层 Event 时间。', { role: 'secondary' }, ) const scheduler = createShowcaseScheduler({ data: createSchedulerOverviewSnapshot({ resourceCount: 12, routineEventsPerResource: 4, denseEventCount: 6, }), domain: schedulerMonthDomain, queryKey: 'showcase-scheduler-multi-view', title: '资源', view: 'resourceTimelineWeek', zoomPreset: 'standard', initialDateEpochMs: schedulerNowEpochMs, onViewChange: event => { status.text = `视图已切换:${viewLabel(event.current)} · Event 时间保持不变。` }, onZoomChange: event => { status.text = `显示密度:${zoomLabel(event.current)} · 围绕当前视口锚点缩放。` }, onViewportChange: event => { const range = event.current.visibleTimeRange if (!range) return status.text = `${viewLabel(event.current.view)} · 可见资源 ${event.current.visibleResourceIds.length}` + ` · 可见时间 ${formatUtc(range.startEpochMs)}—${formatUtc(range.endEpochMs)}` }, }) scheduler.setNowEpochMs(schedulerNowEpochMs) const controls = new RenderWrapPanel({ spacing: 8, runSpacing: 8 }) controls.addChild(new RenderText('视图', { role: 'secondary' })) controls.addChild(new RenderSegmentedControl({ value: 'resourceTimelineWeek', options: [ { value: 'resourceTimelineDay', label: 'Day' }, { value: 'resourceTimelineWeek', label: 'Week' }, { value: 'resourceTimelineMonth', label: 'Month' }, { value: 'agenda', label: 'Agenda' }, ], onChange: value => { if (views.includes(value as SchedulerView)) { scheduler.setView(value as SchedulerView) } }, })) controls.addChild(new RenderText('密度', { role: 'secondary' })) controls.addChild(new RenderSegmentedControl({ value: 'standard', options: [ { value: 'compact', label: '紧凑' }, { value: 'standard', label: '标准' }, { value: 'comfortable', label: '舒适' }, ], onChange: value => { if (zoomPresets.includes(value as SchedulerZoomPreset)) { scheduler.setZoomPreset(value as SchedulerZoomPreset) } }, })) controls.addChild(new RenderButton({ label: '回到当前时间', onClick: () => scheduler.scrollToDate(schedulerNowEpochMs, 'center'), })) controls.addChild(new RenderButton({ label: '定位末尾资源', onClick: () => scheduler.scrollToResource('room-12', 'end'), })) return createSchedulerStage({ title: 'Scheduler 多视图与语义导航', description: '在 Day、Week、Month 和 Agenda 之间切换,并通过密度、日期与资源 API 控制同一份排期快照。', scheduler, controls, status, })} function viewLabel(view: SchedulerView): string { if (view === 'resourceTimelineDay') return 'Day' if (view === 'resourceTimelineWeek') return 'Week' if (view === 'resourceTimelineMonth') return 'Month' return 'Agenda'} function zoomLabel(zoom: SchedulerZoomPreset): string { if (zoom === 'compact') return '紧凑' if (zoom === 'comfortable') return '舒适' return '标准'} function formatUtc(epochMs: number): string { const value = new Date(epochMs) return `${value.getUTCMonth() + 1}/${value.getUTCDate()} ${String(value.getUTCHours()).padStart(2, '0')}:${String(value.getUTCMinutes()).padStart(2, '0')}`} // ===== 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' 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 schedulerWeekDomain: SchedulerInstantRange = Object.freeze({ startEpochMs: schedulerWeekStartEpochMs, endEpochMs: schedulerWeekStartEpochMs + 7 * schedulerDayMs,}) export const schedulerMonthDomain: SchedulerInstantRange = Object.freeze({ startEpochMs: Date.UTC(2026, 6, 1), endEpochMs: Date.UTC(2026, 7, 1),}) export const schedulerLargeDomain: SchedulerInstantRange = Object.freeze({ startEpochMs: Date.UTC(2026, 6, 1), endEpochMs: Date.UTC(2026, 9, 1),}) 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 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.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[] = [] for (let resourceIndex = 0; resourceIndex < resources.length; resourceIndex += 1) { const resource = resources[resourceIndex]! for (let occurrence = 0; occurrence < routineEventsPerResource; occurrence += 1) { const eventId = `plan-${resourceIndex + 1}-${occurrence + 1}` const dayIndex = (resourceIndex + occurrence * 2) % 7 const startHour = 8 + (resourceIndex + occurrence) % 8 const startEpochMs = schedulerWeekStartEpochMs + dayIndex * schedulerDayMs + startHour * schedulerHourMs + (occurrence % 2) * 30 * 60 * 1_000 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 startEpochMs = schedulerWeekStartEpochMs + schedulerDayMs + 9 * schedulerHourMs appendDenseEvents(events, assignments, 28, 'room-01', startEpochMs) for (let resourceIndex = 1; resourceIndex < resources.length; resourceIndex += 1) { for (let index = 0; index < 4; index += 1) { const eventId = `secondary-${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, }) } } return { resources, events, assignments, timeRanges: [ { id: 'dense-blocked-range', resourceId: 'room-05', kind: 'blocked', time: { kind: 'timed', range: { startEpochMs: startEpochMs + 2 * schedulerHourMs, endEpochMs: startEpochMs + 5 * schedulerHourMs, }, }, readOnly: true, }, ], }} 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, ...schedulerOptions } = options let initialPositionApplied = false let scheduler!: RenderScheduler scheduler = new RenderScheduler({ minHeight: 330, resourceHeaderWidth: 210, timeHeaderHeight: 50, ...schedulerOptions, 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,): void { for (let index = 0; index < count; index += 1) { const eventId = `dense-${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, }) }}