REAL SHOWCASE SOURCEscheduler-interaction-events
2 个文件 · 732 行 · TypeScript
// ===== showcase/src/examples/scheduler/interaction_events.ts ===== import { RenderButton, RenderText, RenderWrapPanel, type SchedulerView,} from 'ds-ui'import { createSchedulerEditingSnapshot, createSchedulerStage, createShowcaseScheduler, schedulerChangeText, schedulerSelectionText, schedulerWeekDomain, schedulerWeekStartEpochMs,} from './shared' export function createSchedulerInteractionEventsExample() { let currentView: SchedulerView = 'resourceTimelineWeek' let blockNextViewChange = false let lastViewChangeIntercepted = false let viewButton!: RenderButton let blockNextDelete = true const status = new RenderText( '事件日志:等待选择、激活、视图切换、导航或本地数据提交。', { role: 'secondary' }, ) const scheduler = createShowcaseScheduler({ data: createSchedulerEditingSnapshot(), domain: schedulerWeekDomain, queryKey: 'showcase-scheduler-interaction-events', title: '资源', view: 'resourceTimelineWeek', zoomPreset: 'standard', initialDateEpochMs: schedulerWeekStartEpochMs + 10 * 60 * 60 * 1_000, editable: true, onBeforeSelectionChange: event => { if (event.current?.disabled) { event.preventDefault({ code: 'disabled-demo-item', message: '禁用 Event 不参与业务选择。', }) } }, onSelectionChange: event => { status.text = `after selection (${event.source}/${event.reason}):` + schedulerSelectionText(event.current) }, onBeforeEventActivate: event => { if (event.item.title.includes('审计')) { event.preventDefault({ code: 'audit-review-only', message: '审计排期只允许选择查看,不执行默认激活动作。', }) status.text = 'before activation 已拦截:审计排期只允许选择查看。' } }, onEventActivate: event => { status.text = `after activation:${event.item.title} · ${event.source}/${event.surface}` }, onBeforeViewChange: event => { if (!blockNextViewChange) return blockNextViewChange = false lastViewChangeIntercepted = true event.preventDefault({ code: 'demo-view-policy', message: '宿主同步规则拦截了这一次视图切换。', }) status.text = `before view 已拦截:${event.previous} → ${event.current}` }, onViewChange: event => { currentView = event.current viewButton.label = viewToggleLabel(currentView) viewButton.markNeedsLayout() status.text = `after view:${event.previous} → ${event.current}` }, onBeforeNavigate: event => { status.text = `before navigate:${event.request.kind} · ${event.source}` }, onNavigate: event => { status.text = `after navigate:${event.request.kind} · 可见资源 ${event.viewport.visibleResourceIds.length}` }, onBeforeDataChange: event => { if (event.change.action !== 'remove' || !blockNextDelete) return blockNextDelete = false event.preventDefault({ code: 'demo-delete-policy', message: '演示规则拦截第一次删除,第二次允许提交。', }) status.text = 'before data change 已拦截:第一次删除保持零提交。' }, onDataChange: change => { status.text = `after data change:${schedulerChangeText(change)}` }, }) const controls = new RenderWrapPanel({ spacing: 8, runSpacing: 8 }) controls.addChild(new RenderButton({ label: '拦截下一次视图切换', variant: 'primary', onClick: () => { blockNextViewChange = true status.text = `已设置一次性 before view 拦截,请点击“${viewToggleLabel(currentView)}”。` }, })) viewButton = new RenderButton({ label: viewToggleLabel(currentView), onClick: () => { lastViewChangeIntercepted = false const targetView = currentView === 'agenda' ? 'resourceTimelineWeek' : 'agenda' const changed = scheduler.setView(targetView) if (!changed && !lastViewChangeIntercepted) { blockNextViewChange = false status.text = '视图切换未发生,当前状态保持不变。' } }, }) controls.addChild(viewButton) controls.addChild(new RenderButton({ label: '定位会议室 08', onClick: () => scheduler.scrollToResource('room-08', 'center'), })) controls.addChild(new RenderButton({ label: 'API 删除客户评审', variant: 'danger', onClick: () => { const result = scheduler.removeEvent('editable-customer-review') if (!result.ok) { status.text = `删除未提交:${result.reason}` + (result.rejection?.message ? ` · ${result.rejection.message}` : '') } }, })) return createSchedulerStage({ title: 'Scheduler 业务事件与同步拦截', description: 'before/after 事件让宿主观察或取消选择、激活、视图、导航和本地数据提交,不暴露内部布局对象。', scheduler, controls, status, })} function viewToggleLabel(view: SchedulerView): string { return view === 'agenda' ? '切换到 Week' : '切换到 Agenda'} // ===== 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, }) }}