REAL SHOWCASE SOURCEscheduler-local-editing
2 个文件 · 698 行 · TypeScript
// ===== showcase/src/examples/scheduler/local_editing.ts ===== import { RenderButton, RenderSwitch, RenderText, RenderWrapPanel,} from 'ds-ui'import { createSchedulerDraftFactory, createSchedulerEditingSnapshot, createSchedulerStage, createShowcaseScheduler, schedulerChangeText, schedulerNowEpochMs, schedulerWeekDomain, schedulerWeekStartEpochMs,} from './shared' export function createSchedulerLocalEditingExample() { const status = new RenderText( '编辑已开启:双击空白创建;拖动 Event 移动或改派;拖左右边缘调整起止时间。', { role: 'secondary' }, ) const scheduler = createShowcaseScheduler({ data: createSchedulerEditingSnapshot(), domain: schedulerWeekDomain, queryKey: 'showcase-scheduler-local-editing', title: '资源', view: 'resourceTimelineDay', zoomPreset: 'comfortable', initialDateEpochMs: schedulerWeekStartEpochMs + 10 * 60 * 60 * 1_000, editable: true, snapDurationMs: 30 * 60 * 1_000, defaultEventDurationMs: 60 * 60 * 1_000, createEventDraft: createSchedulerDraftFactory(), onDataChange: change => { status.text = schedulerChangeText(change) }, }) scheduler.setNowEpochMs(schedulerNowEpochMs) 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') scheduler.scrollToDate( schedulerWeekStartEpochMs + 10 * 60 * 60 * 1_000, 'center', ) }, })) return createSchedulerStage({ title: 'Scheduler Canvas 本地编辑', description: '创建、编辑、移动、跨资源改派、起止拉伸和删除共用一个原子客户端变更管线。', scheduler, 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/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, }) }}