REAL SHOWCASE SOURCEscheduler-large-virtualization

2 个文件 · 677 行 · TypeScript

// ===== showcase/src/examples/scheduler/large_virtualization.ts ===== import {  RenderButton,  RenderText,  RenderWrapPanel,} from 'ds-ui'import {  createSchedulerLargeSnapshot,  createSchedulerStage,  createShowcaseScheduler,  schedulerLargeDomain,  schedulerNowEpochMs,} from './shared' const defaultResourceCount = 50const defaultEventCount = 2_000const commercialResourceCount = 1_000const commercialEventCount = 50_000 export function createSchedulerLargeVirtualizationExample() {  let commercialScale = false  const status = new RenderText(    `${defaultResourceCount} 个资源 · ${defaultEventCount.toLocaleString()} 个 Event · 仅物化当前可见窗口。`,    { role: 'secondary' },  )  const scheduler = createShowcaseScheduler({    data: createSchedulerLargeSnapshot(      defaultResourceCount,      defaultEventCount,    ),    domain: schedulerLargeDomain,    queryKey: 'showcase-scheduler-large-default',    title: '资源',    view: 'resourceTimelineWeek',    zoomPreset: 'compact',    initialDateEpochMs: schedulerNowEpochMs,    onStateChange: state => {      if (state.status !== 'ready') return      status.text =        `${commercialScale ? commercialResourceCount : defaultResourceCount} 个资源` +        ` · ${commercialScale ? commercialEventCount.toLocaleString() : defaultEventCount.toLocaleString()} 个 Event` +        ` · 当前绘制 ${state.visibleRows} 行 / ${state.visibleEvents} 个 Event`    },  })   let scaleButton!: RenderButton  scaleButton = new RenderButton({    label: '加载商业规模 1,000 / 50,000',    variant: 'primary',    onClick: () => {      commercialScale = !commercialScale      const resourceCount =        commercialScale ? commercialResourceCount : defaultResourceCount      const eventCount =        commercialScale ? commercialEventCount : defaultEventCount      status.text =        `正在原子替换为 ${resourceCount} 个资源 / ${eventCount.toLocaleString()} 个 Event…`      scheduler.setData(        createSchedulerLargeSnapshot(resourceCount, eventCount),        {          queryKey: commercialScale            ? 'showcase-scheduler-large-commercial'            : 'showcase-scheduler-large-default',          domain: schedulerLargeDomain,        },      )      scaleButton.label = commercialScale        ? '恢复默认规模 50 / 2,000'        : '加载商业规模 1,000 / 50,000'      scaleButton.markNeedsLayout()    },  })   const controls = new RenderWrapPanel({ spacing: 8, runSpacing: 8 })  controls.addChild(scaleButton)  controls.addChild(new RenderButton({    label: '跳转中部',    onClick: () => {      scheduler.scrollToDate(Date.UTC(2026, 7, 15, 12), 'center')      scheduler.scrollToResource(        commercialScale ? 'room-500' : 'room-25',        'center',      )    },  }))  controls.addChild(new RenderButton({    label: '跳转末尾',    onClick: () => {      scheduler.scrollToDate(Date.UTC(2026, 8, 29, 12), 'end')      scheduler.scrollToResource(        commercialScale ? 'room-1000' : 'room-50',        'end',      )    },  }))   return createSchedulerStage({    title: 'Scheduler 双轴虚拟化',    description:      '资源行与时间窗口都按视口有界物化;可主动切换到 1,000 个资源、50,000 个 Event 的商业规模。',    scheduler,    controls,    status,  })} // ===== 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,    })  }}