如何在 Angular 中实现带有可拖动项的多行时间轴

How to implement a multi-row timeline with dragable items in Angular

我想创建一个类似于此的时间轴网格:https://demo.mobiscroll.com/angular/timeline/month-view#drag-drop=true&themeVariant=light

目标是在 x 轴上有一个时间轴,在 y 轴上有一个可扩展的行数(基本上是一个常规的日历周视图,但天数不固定)。我希望能够沿 x 轴(精确到分钟)在细粒度级别上拖放项目,以及将它们拖放到新行。如何在 Angular 中实现?

我尝试使用 Angular Material's Drag and Drop feature 提出一个解决方案,但无法真正弄清楚我将如何表示细粒度的 x 轴,而不创建一个包含代表每一分钟的项目的长列表当天。这是一种可行的技术,还是有更简单的方法?

我一直在创建 similar component lately, but I haven't implemented the timeline mode yet. A demo can be found here

我做的是

将资源组保持在 BehaviorSubject

resources$ = new BehaviorSubject<(Resource | ResourceGroup)[]>([]);

接口:

export interface ResourceGroup {
    description: string;
    children: (ResourceGroup | Resource)[];
}

export interface Resource {
    description: string;
    events: SchedulerEvent[];
}

export interface SchedulerEvent {
    start: Date;
    end: Date;
    color: string;
    description: string;
}

事件是资源->事件组的映射:

this.events$ = this.resources$
  .pipe(map((resourcesOrGroups) => resourcesOrGroups.map(resOrGroup => this.getResourcesForGroup(resOrGroup))))
  .pipe(map(jaggedResources => jaggedResources.reduce((flat, toFlatten) => flat.concat(toFlatten), [])))
  .pipe(map(resources => resources.map(res => res.events)))
  .pipe(map(jaggedEvents => jaggedEvents.reduce((flat, toFlatten) => flat.concat(toFlatten), [])));

然后你需要将这些事件分成几部分(对于日历模式,时间线模式将按 week/month 每天拆分):

this.eventParts$ = this.events$.pipe(
  map((events) =>  events.map((ev) => this.timelineService.splitInParts(ev)))
);

这是每天将事件分成几个部分的代码:

export class BsTimelineService {

  public splitInParts(event: SchedulerEvent | PreviewEvent) {
    let startTime = event.start;
    const result: SchedulerEventPart[] = [];
    const eventOrNull = 'color' in event ? event : null;
    while (!this.dateEquals(startTime, event.end)) {
      const end = new Date(startTime.getFullYear(), startTime.getMonth(), startTime.getDate() + 1, 0, 0, 0);
      result.push({ start: startTime, end: end, event: eventOrNull });
      startTime = end;
    }
    if (startTime != event.end) {
      result.push({ start: startTime, end: event.end, event: eventOrNull });
    }

    return <SchedulerEventWithParts>{ event: event, parts: result };
  }
  
  private dateEquals(date1: Date, date2: Date) {
    return (
      date1.getFullYear() === date2.getFullYear() &&
      date1.getMonth() === date2.getMonth() &&
      date1.getDate() === date2.getDate()
    );
  }

}

接口:

export interface SchedulerEventWithParts {
    event: SchedulerEvent;
    parts: SchedulerEventPart[];
}

最后您可以只筛选出本周的活动部分:

this.eventPartsForThisWeek$ = combineLatest([
  this.daysOfWeekWithTimestamps$,
  this.eventParts$
    .pipe(map(eventParts => eventParts.map(evp => evp.parts)))
    .pipe(map(jaggedParts =>  jaggedParts.reduce((flat, toFlatten) => flat.concat(toFlatten), [])))
  ])
  .pipe(map(([startAndEnd, eventParts]) => {
    return eventParts.filter(eventPart => {
      return !((eventPart.end.getTime() <= startAndEnd.start) || (eventPart.start.getTime() >= startAndEnd.end));
    });
  }));

您可以使用 async 管道在您的视图中呈现:

<div *ngFor="let eventPart of (eventPartsForThisWeek$ | async)"></div>

在您的情况下,您对此没有问题,但是当您需要日历模式(就像我开始使用的那个)时,您仍然需要在多列中显示这些事件。因此我创建了另一个可观察对象:

this.timelinedEventPartsForThisWeek$ = this.eventPartsForThisWeek$
  .pipe(map(eventParts => {
    // We'll only use the events for this week
    const events = eventParts.map(ep => ep.event)
      .filter((e, i, list) => list.indexOf(e) === i)
      .filter((e) => !!e)
      .map((e) => <SchedulerEvent>e);
    const timeline = this.timelineService.getTimeline(events);

    const result = timeline.map(track => track.events.map(ev => ({ event: ev, index: track.index })))
      .reduce((flat, toFlatten) => flat.concat(toFlatten), [])
      .map((evi) => eventParts.filter(p => p.event === evi.event).map(p => ({ part: p, index: evi.index })))
      .reduce((flat, toFlatten) => flat.concat(toFlatten), []);

    return {
      total: timeline.length,
      parts: result
    };
  }));

以下方法正在为给定事件创建一个时间轴:

public getTimeline(events: SchedulerEvent[]) {
    const timestamps = this.getTimestamps(events);
    const tracks: TimelineTrack[] = [];

    timestamps.forEach((timestamp, tIndex) => {
        const starting = events.filter((e) => e.start === timestamp);
        // const ending = events.filter((e) => e.end === timestamp);

        starting.forEach((startedEvent, eIndex) => {
            const freeTracks = tracks.filter(t => this.trackIsFreeAt(t, startedEvent));
            if (freeTracks.length === 0) {
                tracks.push({ index: tracks.length, events: [startedEvent] });
            } else {
                freeTracks[0].events.push(startedEvent);
            }
        });
    });

    return tracks;
}

private getTimestamps(events: SchedulerEvent[]) {
    const allTimestamps = events.map(e => [e.start, e.end])
        .reduce((flat, toFlatten) => flat.concat(toFlatten), []);

    return allTimestamps
        .filter((t, i) => allTimestamps.indexOf(t) === i)
        .sort((t1, t2) => <any>t1 - <any>t2);
}

private trackIsFreeAt(track: TimelineTrack, event: SchedulerEvent) {
    if (track.events.every((ev) => (ev.end <= event.start) || (event.end <= ev.start))) {
        return true;
    } else {
        return false;
    }
}

请注意,对于由 dragging-dropping 创建的事件或移动的事件,我必须省略 TimelineService。这会导致通过 TimelineService 进行过多的计算,并且网络浏览器无法处理。