diff --git a/.gitignore b/.gitignore index 53a8849e..9618ae05 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ speed-measure-plugin*.json # IDE - VSCode .vscode/* -!.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json diff --git a/docs/guides/basic/usage.md b/docs/guides/basic/usage.md index 1d982d62..011ea94d 100644 --- a/docs/guides/basic/usage.md +++ b/docs/guides/basic/usage.md @@ -29,6 +29,31 @@ export class AppGanttExampleComponent { } ``` +## 视图配置 + +内置视图有一套默认的配置,如默认配置不满足需求时,可传入指定的 viewOptions 来进行自定义配置 + +``` + + ... + +``` + +```javascript + class GanttViewOptions { + start?: GanttDate; // 视图开始时间 + end?: GanttDate; // 视图结束时间 + min?: GanttDate; // 视图最小时间 + max?: GanttDate; // 视图最大时间 + cellWidth?: number; // 视图最小单元宽度(小时视图,最小单元就是每小时的宽度,日视图,最新单元就是每日显示的宽度) + addAmount?: number; // 横向滚动加载时,每次加载的量 + addUnit?: GanttDateUtil; // 横向滚动加载时,每次加载的量的单位 + dateFormat?: GanttDateFormat; // 设置视图日期格式,可用于多语言 + datePrecisionUnit?: 'day' | 'hour' | 'minute'; // 日期精度单位,小时视图默认精度为分钟,其他视图默认精度为天 + dragPreviewDateFormat?: string; // 拖拽预览日期格式设置 + } +``` + ## 如何设置分组 分组模式下我们还需要传入一个 `groups` 的数组,并且保证我们传入的 `items` 数据中设置了每个数据项的 `group_id` diff --git a/docs/guides/intro/index.md b/docs/guides/intro/index.md index c55472ea..d00f677d 100644 --- a/docs/guides/intro/index.md +++ b/docs/guides/intro/index.md @@ -10,7 +10,7 @@ order: 1 # 特性 -- 5 种视图(日、周、月、季、年) +- 6 种内置视图(时、日、周、月、季、年),并支持自定义视图 - 任务分组展示 - 树形结构数据展示并支持异步加载 - 任务前后置依赖关联及展示 diff --git a/example/src/app/gantt-custom-view/custom-day-view.ts b/example/src/app/gantt-custom-view/custom-day-view.ts index b9f3d1de..567bd7b4 100644 --- a/example/src/app/gantt-custom-view/custom-day-view.ts +++ b/example/src/app/gantt-custom-view/custom-day-view.ts @@ -30,11 +30,11 @@ export class GanttViewCustom extends GanttView { super(start, end, Object.assign({}, viewOptions, options)); } - startOf(date: GanttDate) { + viewStartOf(date: GanttDate) { return date.startOfWeek({ weekStartsOn: 1 }); } - endOf(date: GanttDate) { + viewEndOf(date: GanttDate) { return date.endOfWeek({ weekStartsOn: 1 }); } diff --git a/example/src/app/gantt/gantt.component.html b/example/src/app/gantt/gantt.component.html index b55896f3..738b731d 100644 --- a/example/src/app/gantt/gantt.component.html +++ b/example/src/app/gantt/gantt.component.html @@ -53,22 +53,17 @@ (dragStarted)="onDragStarted($event)" (dragEnded)="onDragEnded($event)" > - - - {{ item.id }} - - {{ item.title }} - + - {{ item.start * 1000 | date : 'yyyy-MM-dd' }} + {{ item.start * 1000 | date : 'yyyy-MM-dd HH:mm' }} - + - {{ item.end * 1000 | date : 'yyyy-MM-dd' }} + {{ item.end * 1000 | date : 'yyyy-MM-dd HH:mm' }} diff --git a/example/src/app/gantt/gantt.component.ts b/example/src/app/gantt/gantt.component.ts index 24c4130a..7ce9c14d 100644 --- a/example/src/app/gantt/gantt.component.ts +++ b/example/src/app/gantt/gantt.component.ts @@ -1,26 +1,26 @@ -import { Component, OnInit, HostBinding, ViewChild, AfterViewInit } from '@angular/core'; +import { AfterViewInit, Component, HostBinding, OnInit, ViewChild } from '@angular/core'; import { GanttBarClickEvent, - GanttViewType, + GanttBaselineItem, GanttDragEvent, + GanttItem, GanttLineClickEvent, GanttLinkDragEvent, - GanttItem, GanttPrintService, - NgxGanttComponent, GanttSelectedEvent, - GanttBaselineItem, - GanttView, - GanttToolbarOptions, - GanttTableDragEnterPredicateContext, GanttTableDragDroppedEvent, + GanttTableDragEndedEvent, + GanttTableDragEnterPredicateContext, GanttTableDragStartedEvent, - GanttTableDragEndedEvent + GanttToolbarOptions, + GanttView, + GanttViewType, + NgxGanttComponent } from 'ngx-gantt'; +import { ThyNotifyService } from 'ngx-tethys/notify'; import { finalize, of } from 'rxjs'; import { delay } from 'rxjs/operators'; -import { ThyNotifyService } from 'ngx-tethys/notify'; -import { randomItems, random } from '../helper'; +import { random, randomItems } from '../helper'; @Component({ selector: 'app-gantt-example', @@ -30,6 +30,10 @@ import { randomItems, random } from '../helper'; }) export class AppGanttExampleComponent implements OnInit, AfterViewInit { views = [ + { + name: '小时', + value: GanttViewType.hour + }, { name: '日', value: GanttViewType.day @@ -63,7 +67,7 @@ export class AppGanttExampleComponent implements OnInit, AfterViewInit { loading = false; items: GanttItem[] = [ - { id: '000000', title: 'Task 0', start: 1627729997, end: 1628421197 }, + { id: '000000', title: 'Task 0', start: 1627729997, end: 1627769997 }, // { id: '000001', title: 'Task 1', start: 1617361997, end: 1625483597, links: ['000003', '000004', '000000'], }, { id: '000001', title: 'Task 1', start: 1617361997, end: 1625483597, links: ['000003', '000004', '0000029'] }, { id: '000002', title: 'Task 2', start: 1610536397, end: 1610622797, progress: 0.5 }, @@ -153,6 +157,8 @@ export class AppGanttExampleComponent implements OnInit, AfterViewInit { } selectedChange(event: GanttSelectedEvent) { + event.current && this.ganttComponent.scrollToDate(event.current?.start); + this.thyNotify.info( 'Event: selectedChange', `当前选中的 item 的 id 为 ${(event.selectedValue as GanttItem[]).map((item) => item.id).join('、')}` diff --git a/packages/gantt/src/class/event.ts b/packages/gantt/src/class/event.ts index 0066dbd9..208582e5 100644 --- a/packages/gantt/src/class/event.ts +++ b/packages/gantt/src/class/event.ts @@ -35,6 +35,7 @@ export class GanttBarClickEvent { export class GanttSelectedEvent { event: Event; + current?: GanttItem; selectedValue: GanttItem | GanttItem[]; } diff --git a/packages/gantt/src/class/item.ts b/packages/gantt/src/class/item.ts index 1875b81e..f86827fc 100644 --- a/packages/gantt/src/class/item.ts +++ b/packages/gantt/src/class/item.ts @@ -2,6 +2,9 @@ import { GanttDate } from '../utils/date'; import { BehaviorSubject } from 'rxjs'; import { GanttLink, GanttLinkType } from './link'; import { GanttViewType } from './view-type'; +import { GanttView } from '../views/view'; + +const DEFAULT_FILL_INCREMENT_WIDTH = 120; export interface GanttItemRefs { width: number; @@ -38,8 +41,8 @@ export interface GanttItem { export class GanttItemInternal { id: string; title: string; - start: GanttDate; - end: GanttDate; + start: GanttDate | null; + end: GanttDate | null; links: GanttLink[]; color?: string; barStyle?: Partial; @@ -53,17 +56,16 @@ export class GanttItemInternal { children: GanttItemInternal[]; type?: GanttItemType; progress?: number; - fillDays?: number; viewType?: GanttViewType; - level?: number; + level: number; get refs() { return this.refs$.getValue(); } - refs$ = new BehaviorSubject<{ width: number; x: number; y: number }>(null); + refs$ = new BehaviorSubject<{ width: number; x: number; y: number }>(null as any); - constructor(item: GanttItem, level?: number, options?: { fillDays: number }) { + constructor(item: GanttItem, level: number, private view?: GanttView) { this.origin = item; this.id = this.origin.id; this.links = (this.origin.links || []).map((link) => { @@ -86,25 +88,21 @@ export class GanttItemInternal { this.start = item.start ? new GanttDate(item.start) : null; this.end = item.end ? new GanttDate(item.end) : null; this.level = level; - // 默认填充 30 天 - this.fillDays = options?.fillDays || 30; this.children = (item.children || []).map((subItem) => { - return new GanttItemInternal(subItem, level + 1, { fillDays: this.fillDays }); + return new GanttItemInternal(subItem, level + 1, view); }); this.type = this.origin.type || GanttItemType.bar; this.progress = this.origin.progress; - // fill days when start or end is null - this.fillItemStartOrEnd(item); + this.fillDateWhenStartOrEndIsNil(item); } - fillItemStartOrEnd(item: GanttItem) { - if (this.fillDays > 0) { - const fillDays = this.fillDays - 1; + private fillDateWhenStartOrEndIsNil(item: GanttItem) { + if (this.view) { if (item.start && !item.end) { - this.end = new GanttDate(item.start).addDays(fillDays).endOfDay(); + this.end = this.view.getDateByXPoint(this.view.getXPointByDate(new GanttDate(item.start)) + DEFAULT_FILL_INCREMENT_WIDTH); } if (!item.start && item.end) { - this.start = new GanttDate(item.end).addDays(-fillDays).startOfDay(); + this.start = this.view.getDateByXPoint(this.view.getXPointByDate(new GanttDate(item.end)) - DEFAULT_FILL_INCREMENT_WIDTH); } } } @@ -114,8 +112,8 @@ export class GanttItemInternal { } updateDate(start: GanttDate, end: GanttDate) { - this.start = start.startOfDay(); - this.end = end.endOfDay(); + this.start = start; + this.end = end; this.origin.start = this.start.getUnixTime(); this.origin.end = this.end.getUnixTime(); } @@ -127,7 +125,7 @@ export class GanttItemInternal { addChildren(items: GanttItem[]) { this.origin.children = items; this.children = (items || []).map((subItem) => { - return new GanttItemInternal(subItem, this.level + 1, { fillDays: this.fillDays }); + return new GanttItemInternal(subItem, this.level + 1, this.view); }); } diff --git a/packages/gantt/src/class/test/item.spec.ts b/packages/gantt/src/class/test/item.spec.ts index e1ba8e51..3ff890c0 100644 --- a/packages/gantt/src/class/test/item.spec.ts +++ b/packages/gantt/src/class/test/item.spec.ts @@ -2,6 +2,16 @@ import { GanttLinkType } from 'ngx-gantt'; import { GanttDate } from '../../utils/date'; import { GanttItem, GanttItemInternal } from '../item'; +class FakeView { + getDateByXPoint() { + return new GanttDate(); + } + + getXPointByDate() { + return 0; + } +} + describe('GanttItemInternal', () => { let ganttItemInternal: GanttItemInternal; let ganttItem: GanttItem; @@ -24,28 +34,40 @@ describe('GanttItemInternal', () => { } ] }; - ganttItemInternal = new GanttItemInternal(ganttItem); + ganttItemInternal = new GanttItemInternal(ganttItem, 0, new FakeView() as any); }); it(`should has correct children`, () => { expect(ganttItemInternal.children.length).toBe(1); }); - it(`should has correct end`, () => { - expect(ganttItemInternal.end.getUnixTime()).toBe(new GanttDate('2020-06-19 23:59:59').getUnixTime()); + it(`should fill date when start or end date is nil`, () => { + const view = new FakeView(); + const date = new GanttDate('2020-06-01 00:00:00'); + spyOn(view, 'getDateByXPoint').and.returnValue(date); - ganttItemInternal = new GanttItemInternal(ganttItem, 0, { fillDays: 1 }); - expect(ganttItemInternal.end.getUnixTime()).toBe(new GanttDate('2020-05-21 23:59:59').getUnixTime()); - }); + let ganttItemInternal = new GanttItemInternal( + { + ...ganttItem, + start: null, + end: new GanttDate('2020-06-19 00:00:00').getUnixTime() + }, + 0, + view as any + ); - it(`should has correct start`, () => { - ganttItem.start = null; - ganttItem.end = new GanttDate('2020-05-21 12:34:35').getUnixTime(); - ganttItemInternal = new GanttItemInternal(ganttItem); - expect(ganttItemInternal.start.getUnixTime()).toBe(new GanttDate('2020-04-22 00:00:00').getUnixTime()); + expect(ganttItemInternal.start.getUnixTime()).toBe(date.getUnixTime()); - ganttItemInternal = new GanttItemInternal(ganttItem, 0, { fillDays: 1 }); - expect(ganttItemInternal.start.getUnixTime()).toBe(new GanttDate('2020-05-21 00:00:00').getUnixTime()); + ganttItemInternal = new GanttItemInternal( + { + ...ganttItem, + start: new GanttDate('2020-05-19 00:00:00').getUnixTime(), + end: null + }, + 0, + view as any + ); + expect(ganttItemInternal.end.getUnixTime()).toBe(date.getUnixTime()); }); it(`should update refs`, () => { @@ -58,8 +80,8 @@ describe('GanttItemInternal', () => { const start = new GanttDate('2020-04-21 12:34:35'); const end = new GanttDate('2020-09-21 12:34:35'); ganttItemInternal.updateDate(start, end); - expect(ganttItemInternal.start.getUnixTime()).toBe(start.startOfDay().getUnixTime()); - expect(ganttItemInternal.end.getUnixTime()).toBe(end.endOfDay().getUnixTime()); + expect(ganttItemInternal.start.getUnixTime()).toBe(start.getUnixTime()); + expect(ganttItemInternal.end.getUnixTime()).toBe(end.getUnixTime()); }); it(`should add children`, () => { diff --git a/packages/gantt/src/class/view-type.ts b/packages/gantt/src/class/view-type.ts index 93ac2ca6..13f1651a 100644 --- a/packages/gantt/src/class/view-type.ts +++ b/packages/gantt/src/class/view-type.ts @@ -3,7 +3,8 @@ export enum GanttViewType { quarter = 'quarter', month = 'month', year = 'year', - week = 'week' + week = 'week', + hour = 'hour' } export const ganttViews = [ diff --git a/packages/gantt/src/components/bar/bar-drag.ts b/packages/gantt/src/components/bar/bar-drag.ts index 503b075d..b9e25709 100644 --- a/packages/gantt/src/components/bar/bar-drag.ts +++ b/packages/gantt/src/components/bar/bar-drag.ts @@ -1,20 +1,21 @@ -import { Injectable, ElementRef, OnDestroy, NgZone } from '@angular/core'; -import { DragRef, DragDrop } from '@angular/cdk/drag-drop'; +import { DragDrop, DragRef } from '@angular/cdk/drag-drop'; +import { ElementRef, Injectable, NgZone, OnDestroy } from '@angular/core'; +import { Subject, animationFrameScheduler, fromEvent, interval } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { GanttViewType } from '../../class'; +import { GanttItemInternal } from '../../class/item'; +import { GanttLinkType } from '../../class/link'; import { GanttDomService } from '../../gantt-dom.service'; import { GanttDragContainer, InBarPosition } from '../../gantt-drag-container'; -import { GanttItemInternal } from '../../class/item'; -import { GanttDate, differenceInCalendarDays, differenceInDays } from '../../utils/date'; -import { animationFrameScheduler, interval, Subject, fromEvent } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; import { GanttUpper } from '../../gantt-upper'; -import { GanttLinkType } from '../../class/link'; -import { passiveListenerOptions } from '../../utils/passive-listeners'; +import { GanttDate } from '../../utils/date'; import { AutoScrollHorizontalDirection, + getAutoScrollSpeedRates, getHorizontalScrollDirection, - isPointerNearClientRect, - getAutoScrollSpeedRates + isPointerNearClientRect } from '../../utils/drag-scroll'; +import { passiveListenerOptions } from '../../utils/passive-listeners'; /** * Proximity, as a ratio to width/height, at which a @@ -88,7 +89,7 @@ export class GanttBarDrag implements OnDestroy { private _horizontalScrollDirection = AutoScrollHorizontalDirection.NONE; /** Record bar days when bar handle drag move. */ - private barHandleDragMoveRecordDays: number; + private barHandleDragMoveRecordDiffs: number; /** Speed ratio for auto scroll */ private autoScrollSpeedRates = 1; @@ -179,7 +180,7 @@ export class GanttBarDrag implements OnDestroy { this.dragScrollDistance = 0; this.barDragMoveDistance = 0; this.item.updateRefs({ - width: this.ganttUpper.view.getDateRangeWidth(this.item.start.startOfDay(), this.item.end.endOfDay()), + width: this.ganttUpper.view.getDateRangeWidth(this.item.start, this.item.end), x: this.ganttUpper.view.getXPointByDate(this.item.start), y: (this.ganttUpper.styles.lineHeight - this.ganttUpper.styles.barHeight) / 2 - 1 }); @@ -223,7 +224,7 @@ export class GanttBarDrag implements OnDestroy { }); dragRef.moved.subscribe((event) => { - if (this.barHandleDragMoveRecordDays && this.barHandleDragMoveRecordDays > 0) { + if (this.barHandleDragMoveRecordDiffs && this.barHandleDragMoveRecordDiffs > 0) { this.startScrollingIfNecessary(event.pointerPosition.x, event.pointerPosition.y); } this.barHandleDragMoveDistance = event.distance.x; @@ -245,7 +246,7 @@ export class GanttBarDrag implements OnDestroy { this.dragScrollDistance = 0; this.barHandleDragMoveDistance = 0; this.item.updateRefs({ - width: this.ganttUpper.view.getDateRangeWidth(this.item.start.startOfDay(), this.item.end.endOfDay()), + width: this.ganttUpper.view.getDateRangeWidth(this.item.start, this.item.end), x: this.ganttUpper.view.getXPointByDate(this.item.start), y: (this.ganttUpper.styles.lineHeight - this.ganttUpper.styles.barHeight) / 2 - 1 }); @@ -336,8 +337,8 @@ export class GanttBarDrag implements OnDestroy { dragMaskElement.style.display = 'block'; dragBackdropElement.style.display = 'block'; // This will invalidate the layout, but we won't need re-layout, because we set styles previously. - dragMaskElement.querySelector('.start').innerHTML = start.format('MM-dd'); - dragMaskElement.querySelector('.end').innerHTML = end.format('MM-dd'); + dragMaskElement.querySelector('.start').innerHTML = start.format(this.ganttUpper.view.options.dragPreviewDateFormat); + dragMaskElement.querySelector('.end').innerHTML = end.format(this.ganttUpper.view.options.dragPreviewDateFormat); } private closeDragBackdrop() { @@ -359,15 +360,19 @@ export class GanttBarDrag implements OnDestroy { const currentX = this.item.refs.x + this.barDragMoveDistance + this.dragScrollDistance; const currentDate = this.ganttUpper.view.getDateByXPoint(currentX); const currentStartX = this.ganttUpper.view.getXPointByDate(currentDate); - const dayWidth = this.ganttUpper.view.getDayOccupancyWidth(currentDate); - const diffDays = differenceInCalendarDays(this.item.end.value, this.item.start.value); - let start = currentDate; - let end = currentDate.addDays(diffDays); + const diffs = this.ganttUpper.view.differenceByPrecisionUnit(this.item.end, this.item.start); - if (currentX > currentStartX + dayWidth / 2) { - start = start.addDays(1); - end = end.addDays(1); + let start = currentDate; + let end = currentDate.add(diffs, this.ganttUpper.view?.options?.datePrecisionUnit); + + // 日视图特殊逻辑处理 + if (this.ganttUpper.view.viewType === GanttViewType.day) { + const dayWidth = this.ganttUpper.view.getDayOccupancyWidth(currentDate); + if (currentX > currentStartX + dayWidth / 2) { + start = start.addDays(1); + end = end.addDays(1); + } } if (this.dragScrolling) { @@ -384,17 +389,16 @@ export class GanttBarDrag implements OnDestroy { if (!this.isStartOrEndInsideView(start, end)) { return; } - - this.item.updateDate(start, end); + this.updateItemDate(start, end); this.dragContainer.dragMoved.emit({ item: this.item.origin }); } private barBeforeHandleDragMove() { - const { x, start, oneDayWidth } = this.startOfBarHandle(); + const { x, start, minRangeWidthWidth } = this.startOfBarHandle(); const width = this.item.refs.width + this.barHandleDragMoveAndScrollDistance * -1; - const days = differenceInDays(this.item.end.value, start.value); + const diffs = this.ganttUpper.view.differenceByPrecisionUnit(this.item.end, start); - if (width > dragMinWidth && days > 0) { + if (width > dragMinWidth && diffs > 0) { this.barElement.style.width = width + 'px'; this.barElement.style.left = x + 'px'; this.openDragBackdrop(this.barElement, start, this.item.end); @@ -403,41 +407,41 @@ export class GanttBarDrag implements OnDestroy { return; } - this.item.updateDate(start, this.item.end); + this.updateItemDate(start, this.item.end); } else { - if (this.barHandleDragMoveRecordDays > 0 && days <= 0) { - this.barElement.style.width = oneDayWidth + 'px'; + if (this.barHandleDragMoveRecordDiffs > 0 && diffs <= 0) { + this.barElement.style.width = minRangeWidthWidth + 'px'; const x = this.ganttUpper.view.getXPointByDate(this.item.end); this.barElement.style.left = x + 'px'; } - this.openDragBackdrop(this.barElement, this.item.end.startOfDay(), this.item.end); - this.item.updateDate(this.item.end.startOfDay(), this.item.end); + this.openDragBackdrop(this.barElement, this.item.end, this.item.end); + this.updateItemDate(this.item.end, this.item.end); } - this.barHandleDragMoveRecordDays = days; + this.barHandleDragMoveRecordDiffs = diffs; this.dragContainer.dragMoved.emit({ item: this.item.origin }); } private barAfterHandleDragMove() { const { width, end } = this.endOfBarHandle(); - const days = differenceInDays(end.value, this.item.start.value); + const diffs = this.ganttUpper.view.differenceByPrecisionUnit(end, this.item.start); - if (width > dragMinWidth && days > 0) { + if (width > dragMinWidth && diffs > 0) { this.barElement.style.width = width + 'px'; this.openDragBackdrop(this.barElement, this.item.start, end); if (!this.isStartOrEndInsideView(this.item.start, end)) { return; } - this.item.updateDate(this.item.start, end); + this.updateItemDate(this.item.start, end); } else { - if (this.barHandleDragMoveRecordDays > 0 && days <= 0) { - const oneDayWidth = this.ganttUpper.view.getDateRangeWidth(this.item.start, this.item.start.endOfDay()); - this.barElement.style.width = oneDayWidth + 'px'; + if (this.barHandleDragMoveRecordDiffs > 0 && diffs <= 0) { + const minRangeWidth = this.ganttUpper.view.getMinRangeWidthByPrecisionUnit(this.item.start); + this.barElement.style.width = minRangeWidth + 'px'; } - this.openDragBackdrop(this.barElement, this.item.start, this.item.start.endOfDay()); - this.item.updateDate(this.item.start, this.item.start.endOfDay()); + this.openDragBackdrop(this.barElement, this.item.start, this.item.start); + this.updateItemDate(this.item.start, this.item.start); } - this.barHandleDragMoveRecordDays = days; + this.barHandleDragMoveRecordDiffs = diffs; this.dragContainer.dragMoved.emit({ item: this.item.origin }); } @@ -518,11 +522,11 @@ export class GanttBarDrag implements OnDestroy { const xThreshold = clientWidth * DROP_PROXIMITY_THRESHOLD; if (isBefore) { - const { start, oneDayWidth } = this.startOfBarHandle(); + const { start, minRangeWidthWidth } = this.startOfBarHandle(); const xPointerByEndDate = this.ganttUpper.view.getXPointByDate(this.item.end); isStartGreaterThanEnd = start.value > this.item.end.value; - isBarAppearsInView = xPointerByEndDate + oneDayWidth + xThreshold <= scrollLeft + clientWidth; + isBarAppearsInView = xPointerByEndDate + minRangeWidthWidth + xThreshold <= scrollLeft + clientWidth; } else { const { end } = this.endOfBarHandle(); const xPointerByStartDate = this.ganttUpper.view.getXPointByDate(this.item.start); @@ -540,7 +544,7 @@ export class GanttBarDrag implements OnDestroy { return { x, start: this.ganttUpper.view.getDateByXPoint(x), - oneDayWidth: this.ganttUpper.view.getDateRangeWidth(this.item.end.startOfDay(), this.item.end) + minRangeWidthWidth: this.ganttUpper.view.getMinRangeWidthByPrecisionUnit(this.item.end) }; } @@ -570,6 +574,10 @@ export class GanttBarDrag implements OnDestroy { } } + private updateItemDate(start: GanttDate, end: GanttDate) { + this.item.updateDate(this.ganttUpper.view.startOfPrecision(start), this.ganttUpper.view.endOfPrecision(end)); + } + createDrags(elementRef: ElementRef, item: GanttItemInternal, ganttUpper: GanttUpper) { this.item = item; this.barElement = elementRef.nativeElement; diff --git a/packages/gantt/src/gantt-upper.ts b/packages/gantt/src/gantt-upper.ts index bee8665a..65831a58 100644 --- a/packages/gantt/src/gantt-upper.ts +++ b/packages/gantt/src/gantt-upper.ts @@ -211,13 +211,13 @@ export abstract class GanttUpper implements OnChanges, OnInit, OnDestroy { this.originItems.forEach((origin) => { const group = this.groupsMap[origin.group_id]; if (group) { - const item = new GanttItemInternal(origin, 0, { fillDays: this.view.options?.fillDays }); + const item = new GanttItemInternal(origin, 0, this.view); group.items.push(item); } }); } else { this.originItems.forEach((origin) => { - const item = new GanttItemInternal(origin, 0, { fillDays: this.view.options?.fillDays }); + const item = new GanttItemInternal(origin, 0, this.view); this.items.push(item); }); } @@ -377,7 +377,7 @@ export abstract class GanttUpper implements OnChanges, OnInit, OnDestroy { computeItemsRefs(...items: GanttItemInternal[] | GanttBaselineItemInternal[]) { items.forEach((item) => { item.updateRefs({ - width: item.start && item.end ? this.view.getDateRangeWidth(item.start.startOfDay(), item.end.endOfDay()) : 0, + width: item.start && item.end ? this.view.getDateRangeWidth(item.start, item.end) : 0, x: item.start ? this.view.getXPointByDate(item.start) : 0, y: (this.styles.lineHeight - this.styles.barHeight) / 2 - 1 }); diff --git a/packages/gantt/src/gantt.component.ts b/packages/gantt/src/gantt.component.ts index b5a36fd8..17ca033f 100644 --- a/packages/gantt/src/gantt.component.ts +++ b/packages/gantt/src/gantt.component.ts @@ -1,55 +1,55 @@ +import { CdkFixedSizeVirtualScroll, CdkVirtualForOf, CdkVirtualScrollViewport, ViewportRuler } from '@angular/cdk/scrolling'; +import { NgClass, NgIf, NgTemplateOutlet } from '@angular/common'; import { + AfterViewChecked, + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, - OnInit, + ContentChild, + ContentChildren, ElementRef, - ChangeDetectionStrategy, - Input, EventEmitter, - Output, - ChangeDetectorRef, + Inject, + Input, NgZone, - ContentChildren, + OnChanges, + OnInit, + Output, QueryList, - AfterViewInit, - ContentChild, + SimpleChanges, TemplateRef, - forwardRef, - Inject, ViewChild, - OnChanges, - SimpleChanges, - AfterViewChecked + forwardRef } from '@angular/core'; -import { takeUntil, take, finalize, skip } from 'rxjs/operators'; import { Observable, from } from 'rxjs'; -import { GanttUpper, GANTT_UPPER_TOKEN } from './gantt-upper'; +import { finalize, skip, take, takeUntil } from 'rxjs/operators'; import { - GanttLinkDragEvent, - GanttLineClickEvent, - GanttItemInternal, + GanttGroupInternal, GanttItem, + GanttItemInternal, + GanttLineClickEvent, + GanttLinkDragEvent, GanttSelectedEvent, - GanttGroupInternal, - GanttVirtualScrolledIndexChangeEvent, + GanttTableDragEndedEvent, GanttTableDragStartedEvent, - GanttTableDragEndedEvent + GanttVirtualScrolledIndexChangeEvent } from './class'; -import { NgxGanttTableColumnComponent } from './table/gantt-column.component'; -import { NgxGanttTableComponent } from './table/gantt-table.component'; -import { GANTT_ABSTRACT_TOKEN } from './gantt-abstract'; -import { GanttGlobalConfig, GANTT_GLOBAL_CONFIG } from './gantt.config'; -import { NgxGanttRootComponent } from './root.component'; -import { GanttDate } from './utils/date'; -import { CdkVirtualScrollViewport, ViewportRuler, CdkFixedSizeVirtualScroll, CdkVirtualForOf } from '@angular/cdk/scrolling'; -import { Dictionary, keyBy, recursiveItems, uniqBy } from './utils/helpers'; +import { GanttCalendarGridComponent } from './components/calendar/grid/calendar-grid.component'; +import { GanttCalendarHeaderComponent } from './components/calendar/header/calendar-header.component'; import { GanttDragBackdropComponent } from './components/drag-backdrop/drag-backdrop.component'; +import { GanttLoaderComponent } from './components/loader/loader.component'; import { GanttMainComponent } from './components/main/gantt-main.component'; -import { GanttCalendarGridComponent } from './components/calendar/grid/calendar-grid.component'; import { GanttTableBodyComponent } from './components/table/body/gantt-table-body.component'; -import { GanttLoaderComponent } from './components/loader/loader.component'; -import { NgIf, NgClass, NgTemplateOutlet } from '@angular/common'; -import { GanttCalendarHeaderComponent } from './components/calendar/header/calendar-header.component'; import { GanttTableHeaderComponent } from './components/table/header/gantt-table-header.component'; +import { GANTT_ABSTRACT_TOKEN } from './gantt-abstract'; +import { GANTT_UPPER_TOKEN, GanttUpper } from './gantt-upper'; +import { GANTT_GLOBAL_CONFIG, GanttGlobalConfig } from './gantt.config'; +import { NgxGanttRootComponent } from './root.component'; +import { NgxGanttTableColumnComponent } from './table/gantt-column.component'; +import { NgxGanttTableComponent } from './table/gantt-table.component'; +import { GanttDate } from './utils/date'; +import { Dictionary, keyBy, recursiveItems, uniqBy } from './utils/helpers'; @Component({ selector: 'ngx-gantt', templateUrl: './gantt.component.html', @@ -343,10 +343,10 @@ export class NgxGanttComponent extends GanttUpper implements OnInit, OnChanges, const selectedIds = this.selectionModel.selected; if (this.multiple) { const _selectedValue = this.getGanttItems(selectedIds).map((item) => item.origin); - this.selectedChange.emit({ event, selectedValue: _selectedValue }); + this.selectedChange.emit({ event, current: selectedValue as GanttItem, selectedValue: _selectedValue }); } else { const _selectedValue = this.getGanttItem(selectedIds[0])?.origin; - this.selectedChange.emit({ event, selectedValue: _selectedValue }); + this.selectedChange.emit({ event, current: selectedValue as GanttItem, selectedValue: _selectedValue }); } } diff --git a/packages/gantt/src/gantt.config.ts b/packages/gantt/src/gantt.config.ts index 9bbb502b..3231365a 100644 --- a/packages/gantt/src/gantt.config.ts +++ b/packages/gantt/src/gantt.config.ts @@ -2,6 +2,8 @@ import { GanttLinkType, GanttLinkOptions, GanttLinkLineType } from './class/link import { InjectionToken } from '@angular/core'; export interface GanttDateFormat { + hour?: string; + day?: string; week?: string; month?: string; quarter?: string; @@ -17,6 +19,8 @@ export interface GanttGlobalConfig { export const defaultConfig = { dateFormat: { + hour: 'HH:mm', + day: 'M月d日', week: '第w周', month: 'M月', quarter: 'QQQ', diff --git a/packages/gantt/src/utils/date.ts b/packages/gantt/src/utils/date.ts index a7a5b6ff..f2fdbc24 100644 --- a/packages/gantt/src/utils/date.ts +++ b/packages/gantt/src/utils/date.ts @@ -26,11 +26,10 @@ import { isWeekend, getWeek, isToday, - differenceInDays, - differenceInCalendarQuarters, - eachMonthOfInterval, - eachWeekOfInterval, - eachDayOfInterval + startOfHour, + startOfMinute, + endOfHour, + endOfMinute } from 'date-fns'; export { @@ -63,9 +62,11 @@ export { isToday, differenceInDays, differenceInCalendarQuarters, + differenceInMinutes, eachMonthOfInterval, eachWeekOfInterval, - eachDayOfInterval + eachDayOfInterval, + eachHourOfInterval } from 'date-fns'; export type GanttDateUtil = 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; @@ -209,6 +210,14 @@ export class GanttDate { return new GanttDate(addYears(this.value, amount)); } + startOfMinute(): GanttDate { + return new GanttDate(startOfMinute(this.value)); + } + + startOfHour(): GanttDate { + return new GanttDate(startOfHour(this.value)); + } + startOfDay(): GanttDate { return new GanttDate(startOfDay(this.value)); } @@ -229,6 +238,14 @@ export class GanttDate { return new GanttDate(startOfYear(this.value)); } + endOfMinute(): GanttDate { + return new GanttDate(endOfMinute(this.value)); + } + + endOfHour(): GanttDate { + return new GanttDate(endOfHour(this.value)); + } + endOfDay(): GanttDate { return new GanttDate(endOfDay(this.value)); } diff --git a/packages/gantt/src/views/day.ts b/packages/gantt/src/views/day.ts index 6e50e9fa..feac91de 100644 --- a/packages/gantt/src/views/day.ts +++ b/packages/gantt/src/views/day.ts @@ -1,15 +1,14 @@ -import { GanttView, GanttViewOptions, primaryDatePointTop, secondaryDatePointTop, GanttViewDate } from './view'; -import { GanttDate, eachWeekOfInterval, eachDayOfInterval } from '../utils/date'; -import { GanttDatePoint } from '../class/date-point'; import { GanttViewType } from '../class'; +import { GanttDatePoint } from '../class/date-point'; +import { GanttDate, eachDayOfInterval, eachWeekOfInterval } from '../utils/date'; +import { GanttView, GanttViewDate, GanttViewOptions, primaryDatePointTop, secondaryDatePointTop } from './view'; const viewOptions: GanttViewOptions = { cellWidth: 35, start: new GanttDate().startOfYear().startOfWeek({ weekStartsOn: 1 }), end: new GanttDate().endOfYear().endOfWeek({ weekStartsOn: 1 }), addAmount: 1, - addUnit: 'month', - fillDays: 1 + addUnit: 'month' }; export class GanttViewDay extends GanttView { @@ -23,11 +22,11 @@ export class GanttViewDay extends GanttView { super(start, end, Object.assign({}, viewOptions, options)); } - startOf(date: GanttDate) { + viewStartOf(date: GanttDate) { return date.startOfWeek({ weekStartsOn: 1 }); } - endOf(date: GanttDate) { + viewEndOf(date: GanttDate) { return date.endOfWeek({ weekStartsOn: 1 }); } diff --git a/packages/gantt/src/views/factory.ts b/packages/gantt/src/views/factory.ts index c11f365c..7f48a35a 100644 --- a/packages/gantt/src/views/factory.ts +++ b/packages/gantt/src/views/factory.ts @@ -5,8 +5,10 @@ import { GanttViewQuarter } from './quarter'; import { GanttViewDay } from './day'; import { GanttViewWeek } from './week'; import { GanttViewYear } from './year'; +import { GanttViewHour } from './hour'; const ganttViewsMap = { + [GanttViewType.hour]: GanttViewHour, [GanttViewType.day]: GanttViewDay, [GanttViewType.week]: GanttViewWeek, [GanttViewType.month]: GanttViewMonth, diff --git a/packages/gantt/src/views/hour.ts b/packages/gantt/src/views/hour.ts new file mode 100644 index 00000000..d05c1133 --- /dev/null +++ b/packages/gantt/src/views/hour.ts @@ -0,0 +1,102 @@ +import { GanttViewType } from '../class'; +import { GanttDatePoint } from '../class/date-point'; +import { GanttDate, differenceInMinutes, eachDayOfInterval, eachHourOfInterval } from '../utils/date'; +import { GanttView, GanttViewDate, GanttViewOptions, primaryDatePointTop, secondaryDatePointTop } from './view'; + +const viewOptions: GanttViewOptions = { + cellWidth: 80, + start: new GanttDate().startOfMonth(), + end: new GanttDate().endOfMonth(), + datePrecisionUnit: 'minute', + addAmount: 1, + addUnit: 'week', + dragPreviewDateFormat: 'HH:mm' +}; + +export class GanttViewHour extends GanttView { + override showWeekBackdrop = true; + + override showTimeline = true; + + override viewType = GanttViewType.hour; + + constructor(start: GanttViewDate, end: GanttViewDate, options?: GanttViewOptions) { + super(start, end, Object.assign({}, viewOptions, options)); + } + + viewStartOf(date: GanttDate) { + return date.startOfWeek({ weekStartsOn: 1 }); + } + + viewEndOf(date: GanttDate) { + return date.endOfWeek({ weekStartsOn: 1 }); + } + + getPrimaryWidth() { + return this.getCellWidth() * 24; + } + + getDayOccupancyWidth(): number { + return this.cellWidth * 60; + } + + private getHourOccupancyWidth() { + return this.getDayOccupancyWidth() / 60; + } + + getPrimaryDatePoints(): GanttDatePoint[] { + const days = eachDayOfInterval({ start: this.start.value, end: this.end.value }); + const points: GanttDatePoint[] = []; + for (let i = 0; i < days.length; i++) { + const start = this.start.addDays(i); + const point = new GanttDatePoint( + start, + start.format(this.options.dateFormat.day), + (this.getCellWidth() * 24) / 2 + i * (this.getCellWidth() * 24), + primaryDatePointTop + ); + points.push(point); + } + + return points; + } + + getSecondaryDatePoints(): GanttDatePoint[] { + const hours = eachHourOfInterval({ start: this.start.value, end: this.end.value }); + const points: GanttDatePoint[] = []; + for (let i = 0; i < hours.length; i++) { + const start = new GanttDate(hours[i]); + const point = new GanttDatePoint( + start, + start.format(this.options.dateFormat.hour), + i * this.getCellWidth() + this.getCellWidth() / 2, + secondaryDatePointTop, + { + isWeekend: start.isWeekend(), + isToday: start.isToday() + } + ); + points.push(point); + } + return points; + } + + override getDateIntervalWidth(start: GanttDate, end: GanttDate) { + let result = 0; + const minutes = differenceInMinutes(end.value, start.value); + for (let i = 0; i < minutes; i++) { + result += this.getHourOccupancyWidth() / 60; + } + result = minutes >= 0 ? result : -result; + return Number(result.toFixed(3)); + } + + override getDateByXPoint(x: number) { + const hourWidth = this.getHourOccupancyWidth(); + const indexOfSecondaryDate = Math.max(Math.floor(x / hourWidth), 0); + const matchDate = this.secondaryDatePoints[Math.min(this.secondaryDatePoints.length - 1, indexOfSecondaryDate)]; + const minuteWidth = hourWidth / 60; + const underOneHourMinutes = Math.floor((x % hourWidth) / minuteWidth); + return matchDate?.start.addMinutes(underOneHourMinutes); + } +} diff --git a/packages/gantt/src/views/month.ts b/packages/gantt/src/views/month.ts index 849a49b1..f856f3b3 100644 --- a/packages/gantt/src/views/month.ts +++ b/packages/gantt/src/views/month.ts @@ -8,8 +8,7 @@ const viewOptions: GanttViewOptions = { end: new GanttDate().endOfQuarter().addQuarters(2), cellWidth: 280, addAmount: 1, - addUnit: 'quarter', - fillDays: 30 + addUnit: 'quarter' }; export class GanttViewMonth extends GanttView { @@ -19,11 +18,11 @@ export class GanttViewMonth extends GanttView { super(start, end, Object.assign({}, viewOptions, options)); } - startOf(date: GanttDate) { + viewStartOf(date: GanttDate) { return date.startOfQuarter(); } - endOf(date: GanttDate) { + viewEndOf(date: GanttDate) { return date.endOfQuarter(); } diff --git a/packages/gantt/src/views/quarter.ts b/packages/gantt/src/views/quarter.ts index 596888c1..d6c00bd9 100644 --- a/packages/gantt/src/views/quarter.ts +++ b/packages/gantt/src/views/quarter.ts @@ -11,8 +11,7 @@ const viewOptions: GanttViewOptions = { max: new GanttDate().addYears(2).endOfYear(), cellWidth: 500, addAmount: 1, - addUnit: 'year', - fillDays: 30 + addUnit: 'year' }; export class GanttViewQuarter extends GanttView { @@ -22,11 +21,11 @@ export class GanttViewQuarter extends GanttView { super(start, end, Object.assign({}, viewOptions, options)); } - startOf(date: GanttDate) { + viewStartOf(date: GanttDate) { return date.startOfYear(); } - endOf(date: GanttDate) { + viewEndOf(date: GanttDate) { return date.endOfYear(); } diff --git a/packages/gantt/src/views/test/custom-view.mock.ts b/packages/gantt/src/views/test/custom-view.mock.ts index 51a1116e..8b2ddd2e 100644 --- a/packages/gantt/src/views/test/custom-view.mock.ts +++ b/packages/gantt/src/views/test/custom-view.mock.ts @@ -29,11 +29,11 @@ export class GanttViewCustom extends GanttView { super(start, end, Object.assign({}, viewOptions, options)); } - startOf(date: GanttDate) { + viewStartOf(date: GanttDate) { return date.startOfWeek({ weekStartsOn: 1 }); } - endOf(date: GanttDate) { + viewEndOf(date: GanttDate) { return date.endOfWeek({ weekStartsOn: 1 }); } diff --git a/packages/gantt/src/views/test/day.spec.ts b/packages/gantt/src/views/test/day.spec.ts index fd40fcd5..8d3430a0 100644 --- a/packages/gantt/src/views/test/day.spec.ts +++ b/packages/gantt/src/views/test/day.spec.ts @@ -14,12 +14,12 @@ describe('GanttViewDay', () => { }); it(`should has correct view start`, () => { - const startOfDay = ganttViewDay.startOf(date.start.date).getUnixTime(); + const startOfDay = ganttViewDay.viewStartOf(date.start.date).getUnixTime(); expect(startOfDay).toEqual(new GanttDate('2019-12-30 00:00:00').getUnixTime()); }); it(`should has correct view end`, () => { - const endOfDay = ganttViewDay.endOf(date.end.date).getUnixTime(); + const endOfDay = ganttViewDay.viewEndOf(date.end.date).getUnixTime(); expect(endOfDay).toEqual(new GanttDate('2021-01-03 23:59:59').getUnixTime()); }); diff --git a/packages/gantt/src/views/test/hour.spec.ts b/packages/gantt/src/views/test/hour.spec.ts new file mode 100644 index 00000000..4177955a --- /dev/null +++ b/packages/gantt/src/views/test/hour.spec.ts @@ -0,0 +1,46 @@ +import { eachDayOfInterval, eachHourOfInterval } from '../../utils/date'; +import { GanttViewHour } from '../hour'; +import { date } from './mock'; + +describe('GanttViewHour', () => { + let ganttViewHour: GanttViewHour; + + const hourWidth = 30; + + beforeEach(() => { + ganttViewHour = new GanttViewHour(date.start, date.end, { + cellWidth: hourWidth + }); + }); + + it(`should correct getPrimaryDatePoints`, () => { + const points = ganttViewHour.getPrimaryDatePoints(); + const days = eachDayOfInterval({ + start: ganttViewHour.viewStartOf(date.start.date).value, + end: ganttViewHour.viewEndOf(date.end.date).value + }); + expect(points.length).toEqual(days.length); + }); + + it(`should correct getSecondaryDatePoints`, () => { + const points = ganttViewHour.getSecondaryDatePoints(); + const hours = eachHourOfInterval({ + start: ganttViewHour.viewStartOf(date.start.date).value, + end: ganttViewHour.viewEndOf(date.end.date).value + }); + expect(points.length).toEqual(hours.length); + }); + + it(`should correct getDateIntervalWidth`, () => { + let width = ganttViewHour.getDateIntervalWidth(ganttViewHour.start, ganttViewHour.start.addDays(5)); + expect(width).toEqual(5 * (hourWidth * 24)); + + width = ganttViewHour.getDateIntervalWidth(ganttViewHour.start, ganttViewHour.start.addDays(5).addMinutes(20)); + expect(width).toEqual(5 * (hourWidth * 24) + hourWidth * (20 / 60)); + }); + + it(`should correct getDateByXPoint`, () => { + let date = ganttViewHour.getDateByXPoint(hourWidth * 40); + expect(date.getUnixTime()).toEqual(ganttViewHour.start.addHours(40).getUnixTime()); + }); +}); diff --git a/packages/gantt/src/views/test/month.spec.ts b/packages/gantt/src/views/test/month.spec.ts index 65e30122..ca5181bf 100644 --- a/packages/gantt/src/views/test/month.spec.ts +++ b/packages/gantt/src/views/test/month.spec.ts @@ -14,12 +14,12 @@ describe('GanttViewMonth', () => { }); it(`should has correct view start`, () => { - const startOfMonth = ganttViewMonth.startOf(date.start.date).getUnixTime(); + const startOfMonth = ganttViewMonth.viewStartOf(date.start.date).getUnixTime(); expect(startOfMonth).toEqual(new GanttDate('2020-01-01 00:00:00').getUnixTime()); }); it(`should has correct view end`, () => { - const endOfMonth = ganttViewMonth.endOf(date.end.date).getUnixTime(); + const endOfMonth = ganttViewMonth.viewEndOf(date.end.date).getUnixTime(); expect(endOfMonth).toEqual(new GanttDate('2020-12-31 23:59:59').getUnixTime()); }); diff --git a/packages/gantt/src/views/test/quarter.spec.ts b/packages/gantt/src/views/test/quarter.spec.ts index efadcc55..b969136f 100644 --- a/packages/gantt/src/views/test/quarter.spec.ts +++ b/packages/gantt/src/views/test/quarter.spec.ts @@ -14,12 +14,12 @@ describe('GanttViewQuarter', () => { }); it(`should has correct view start`, () => { - const startOfQuarter = ganttViewQuarter.startOf(date.start.date).getUnixTime(); + const startOfQuarter = ganttViewQuarter.viewStartOf(date.start.date).getUnixTime(); expect(startOfQuarter).toEqual(new GanttDate('2020-01-01 00:00:00').getUnixTime()); }); it(`should has correct view end`, () => { - const endOfQuarter = ganttViewQuarter.endOf(date.end.date).getUnixTime(); + const endOfQuarter = ganttViewQuarter.viewEndOf(date.end.date).getUnixTime(); expect(endOfQuarter).toEqual(new GanttDate('2020-12-31 23:59:59').getUnixTime()); }); diff --git a/packages/gantt/src/views/test/view.spec.ts b/packages/gantt/src/views/test/view.spec.ts index 11e93a00..0073062c 100644 --- a/packages/gantt/src/views/test/view.spec.ts +++ b/packages/gantt/src/views/test/view.spec.ts @@ -1,6 +1,6 @@ +import { differenceInHours, differenceInMinutes } from 'date-fns'; import { GanttDatePoint } from '../../class'; import { GanttDate } from '../../utils/date'; - import { GanttView, GanttViewDate, GanttViewOptions } from '../view'; import { date, today } from './mock'; @@ -9,11 +9,11 @@ class GanttViewMock extends GanttView { super(start, end, options); } - startOf(): GanttDate { + viewStartOf(): GanttDate { return new GanttDate('2020-01-01 00:00:00'); } - endOf(): GanttDate { + viewEndOf(): GanttDate { return new GanttDate('2020-12-31 23:59:59'); } @@ -139,6 +139,27 @@ describe('GanttView', () => { it(`should get date range width`, () => { const width = ganttView.getDateRangeWidth(new GanttDate('2020-03-01 00:00:00'), new GanttDate('2020-05-01 00:00:00')); - expect(width).toEqual(610); + expect(width).toEqual(620); + }); + + it(`should get diff precision date`, () => { + let view = new GanttViewMock(date.start, date.end, { datePrecisionUnit: 'hour' }); + const dateWithTime = new GanttDate('2022-10-10 12:50:34'); + expect(view.startOfPrecision(dateWithTime).getUnixTime()).toEqual(dateWithTime.startOfHour().getUnixTime()); + expect(view.endOfPrecision(dateWithTime).getUnixTime()).toEqual(dateWithTime.endOfHour().getUnixTime()); + + view = new GanttViewMock(date.start, date.end, { datePrecisionUnit: 'minute' }); + expect(view.startOfPrecision(dateWithTime).getUnixTime()).toEqual(dateWithTime.startOfMinute().getUnixTime()); + expect(view.endOfPrecision(dateWithTime).getUnixTime()).toEqual(dateWithTime.endOfMinute().getUnixTime()); + }); + + it(`should date difference value by precision unit`, () => { + let view = new GanttViewMock(date.start, date.end, { datePrecisionUnit: 'hour' }); + const startDate = new GanttDate('2022-10-10 12:50:34'); + const endDate = new GanttDate('2022-12-10 10:50:34'); + expect(view.differenceByPrecisionUnit(endDate, startDate)).toEqual(differenceInHours(endDate.value, startDate.value)); + + view = new GanttViewMock(date.start, date.end, { datePrecisionUnit: 'minute' }); + expect(view.differenceByPrecisionUnit(endDate, startDate)).toEqual(differenceInMinutes(endDate.value, startDate.value)); }); }); diff --git a/packages/gantt/src/views/test/week.spec.ts b/packages/gantt/src/views/test/week.spec.ts index 79f99f58..152e67e1 100644 --- a/packages/gantt/src/views/test/week.spec.ts +++ b/packages/gantt/src/views/test/week.spec.ts @@ -14,12 +14,12 @@ describe('GanttViewWeek', () => { }); it(`should has correct view start`, () => { - const startOfWeek = ganttViewWeek.startOf(date.start.date).getUnixTime(); + const startOfWeek = ganttViewWeek.viewStartOf(date.start.date).getUnixTime(); expect(startOfWeek).toEqual(new GanttDate('2019-12-30 00:00:00').getUnixTime()); }); it(`should has correct view end`, () => { - const endOfWeek = ganttViewWeek.endOf(date.end.date).getUnixTime(); + const endOfWeek = ganttViewWeek.viewEndOf(date.end.date).getUnixTime(); expect(endOfWeek).toEqual(new GanttDate('2021-01-03 23:59:59').getUnixTime()); }); diff --git a/packages/gantt/src/views/test/year.spec.ts b/packages/gantt/src/views/test/year.spec.ts index 46a09691..69a8d362 100644 --- a/packages/gantt/src/views/test/year.spec.ts +++ b/packages/gantt/src/views/test/year.spec.ts @@ -14,12 +14,12 @@ describe('GanttViewYear', () => { }); it(`should has correct view start`, () => { - const startOfDay = ganttViewYear.startOf(date.start.date).getUnixTime(); + const startOfDay = ganttViewYear.viewStartOf(date.start.date).getUnixTime(); expect(startOfDay).toEqual(new GanttDate('2020-01-01 00:00:00').getUnixTime()); }); it(`should has correct view end`, () => { - const endOfDay = ganttViewYear.endOf(date.end.date).getUnixTime(); + const endOfDay = ganttViewYear.viewEndOf(date.end.date).getUnixTime(); expect(endOfDay).toEqual(new GanttDate('2020-12-31 23:59:59').getUnixTime()); }); diff --git a/packages/gantt/src/views/view.ts b/packages/gantt/src/views/view.ts index aade6405..ef705095 100644 --- a/packages/gantt/src/views/view.ts +++ b/packages/gantt/src/views/view.ts @@ -1,8 +1,9 @@ -import { GanttDate, differenceInDays, GanttDateUtil } from '../utils/date'; -import { GanttDatePoint } from '../class/date-point'; +import { differenceInCalendarDays, differenceInHours, differenceInMinutes } from 'date-fns'; import { BehaviorSubject } from 'rxjs'; -import { defaultConfig, GanttDateFormat } from '../gantt.config'; import { GanttViewType } from '../class'; +import { GanttDatePoint } from '../class/date-point'; +import { GanttDateFormat, defaultConfig } from '../gantt.config'; +import { GanttDate, GanttDateUtil, differenceInDays } from '../utils/date'; export const primaryDatePointTop = 18; @@ -22,8 +23,8 @@ export interface GanttViewOptions { addAmount?: number; addUnit?: GanttDateUtil; dateFormat?: GanttDateFormat; - // fill days when start or end is null - fillDays?: number; + datePrecisionUnit?: 'day' | 'hour' | 'minute'; + dragPreviewDateFormat?: string; // custom key and value [key: string]: any; } @@ -31,7 +32,9 @@ export interface GanttViewOptions { const viewOptions: GanttViewOptions = { min: new GanttDate().addYears(-1).startOfYear(), max: new GanttDate().addYears(1).endOfYear(), - dateFormat: defaultConfig.dateFormat + dateFormat: defaultConfig.dateFormat, + datePrecisionUnit: 'day', + dragPreviewDateFormat: 'MM-dd' }; export abstract class GanttView { @@ -68,19 +71,35 @@ export abstract class GanttView { constructor(start: GanttViewDate, end: GanttViewDate, options: GanttViewOptions) { this.options = Object.assign({}, viewOptions, options); const startDate = start.isCustom - ? this.startOf(start.date) - : this.startOf(start.date.value < this.options.start.value ? start.date : this.options.start); + ? this.viewStartOf(start.date) + : this.viewStartOf(start.date.value < this.options.start.value ? start.date : this.options.start); const endDate = end.isCustom - ? this.endOf(end.date) - : this.endOf(end.date.value > this.options.end.value ? end.date : this.options.end); + ? this.viewEndOf(end.date) + : this.viewEndOf(end.date.value > this.options.end.value ? end.date : this.options.end); this.start$ = new BehaviorSubject(startDate); this.end$ = new BehaviorSubject(endDate); this.initialize(); } - abstract startOf(date: GanttDate): GanttDate; + abstract viewStartOf(date: GanttDate): GanttDate; - abstract endOf(date: GanttDate): GanttDate; + abstract viewEndOf(date: GanttDate): GanttDate; + + /** + * deprecated, please use viewStartOf() + * @deprecated + */ + startOf(date: GanttDate): GanttDate { + return this.viewStartOf(date); + } + + /** + * deprecated, please use viewEndOf() + * @deprecated + */ + endOf(date: GanttDate): GanttDate { + return this.viewEndOf(date); + } // 获取一级时间网格合并后的宽度 abstract getPrimaryWidth(): number; @@ -94,7 +113,40 @@ export abstract class GanttView { // 获取二级时间点(坐标,显示名称) abstract getSecondaryDatePoints(): GanttDatePoint[]; - protected getDateIntervalWidth(start: GanttDate, end: GanttDate) { + startOfPrecision(date: GanttDate) { + switch (this.options.datePrecisionUnit) { + case 'minute': + return date.startOfMinute(); + case 'hour': + return date.startOfHour(); + default: + return date.startOfDay(); + } + } + + endOfPrecision(date: GanttDate) { + switch (this.options.datePrecisionUnit) { + case 'minute': + return date.endOfMinute(); + case 'hour': + return date.endOfHour(); + default: + return date.endOfDay(); + } + } + + differenceByPrecisionUnit(dateLeft: GanttDate, dateRight: GanttDate) { + switch (this.options.datePrecisionUnit) { + case 'minute': + return differenceInMinutes(dateLeft.value, dateRight.value); + case 'hour': + return differenceInHours(dateLeft.value, dateRight.value); + default: + return differenceInCalendarDays(dateLeft.value, dateRight.value); + } + } + + getDateIntervalWidth(start: GanttDate, end: GanttDate) { let result = 0; const days = differenceInDays(end.value, start.value); for (let i = 0; i < Math.abs(days); i++) { @@ -113,7 +165,7 @@ export abstract class GanttView { } addStartDate() { - const start = this.startOf(this.start.add(this.options.addAmount * -1, this.options.addUnit)); + const start = this.viewStartOf(this.start.add(this.options.addAmount * -1, this.options.addUnit)); if (start.value >= this.options.min.value) { const origin = this.start; this.start$.next(start); @@ -124,7 +176,7 @@ export abstract class GanttView { } addEndDate() { - const end = this.endOf(this.end.add(this.options.addAmount, this.options.addUnit)); + const end = this.viewEndOf(this.end.add(this.options.addAmount, this.options.addUnit)); if (end.value <= this.options.max.value) { const origin = this.end; this.end$.next(end); @@ -135,8 +187,8 @@ export abstract class GanttView { } updateDate(start: GanttDate, end: GanttDate) { - start = this.startOf(start); - end = this.endOf(end); + start = this.viewStartOf(start); + end = this.viewEndOf(end); if (start.value < this.start.value) { this.start$.next(start); } @@ -191,6 +243,18 @@ export abstract class GanttView { // 获取指定时间范围的宽度 getDateRangeWidth(start: GanttDate, end: GanttDate) { // addSeconds(1) 是因为计算相差天会以一个整天来计算 end时间一般是59分59秒不是一个整天,所以需要加1 - return this.getDateIntervalWidth(start, end.addSeconds(1)); + return this.getDateIntervalWidth(this.startOfPrecision(start), this.endOfPrecision(end).addSeconds(1)); + } + + // 根据日期精度获取最小时间范围的宽度 + getMinRangeWidthByPrecisionUnit(date: GanttDate) { + switch (this.options.datePrecisionUnit) { + case 'minute': + return this.getDayOccupancyWidth(date) / 24 / 60; + case 'hour': + return this.getDayOccupancyWidth(date) / 24; + default: + return this.getDayOccupancyWidth(date); + } } } diff --git a/packages/gantt/src/views/week.ts b/packages/gantt/src/views/week.ts index 35ebaa31..d7e6e98b 100644 --- a/packages/gantt/src/views/week.ts +++ b/packages/gantt/src/views/week.ts @@ -8,8 +8,7 @@ const viewOptions: GanttViewOptions = { start: new GanttDate().startOfYear().startOfWeek({ weekStartsOn: 1 }), end: new GanttDate().endOfYear().endOfWeek({ weekStartsOn: 1 }), addAmount: 1, - addUnit: 'month', - fillDays: 1 + addUnit: 'month' }; export class GanttViewWeek extends GanttView { @@ -19,11 +18,11 @@ export class GanttViewWeek extends GanttView { super(start, end, Object.assign({}, viewOptions, options)); } - startOf(date: GanttDate) { + viewStartOf(date: GanttDate) { return date.startOfWeek({ weekStartsOn: 1 }); } - endOf(date: GanttDate) { + viewEndOf(date: GanttDate) { return date.endOfWeek({ weekStartsOn: 1 }); } diff --git a/packages/gantt/src/views/year.ts b/packages/gantt/src/views/year.ts index 039463bb..e4825fe4 100644 --- a/packages/gantt/src/views/year.ts +++ b/packages/gantt/src/views/year.ts @@ -9,8 +9,7 @@ const viewOptions: GanttViewOptions = { start: new GanttDate().addYears(-2).startOfYear(), end: new GanttDate().addYears(2).endOfYear(), addAmount: 1, - addUnit: 'year', - fillDays: 30 + addUnit: 'year' }; export class GanttViewYear extends GanttView { @@ -20,11 +19,11 @@ export class GanttViewYear extends GanttView { super(start, end, Object.assign({}, viewOptions, options)); } - startOf(date: GanttDate) { + viewStartOf(date: GanttDate) { return date.startOfYear(); } - endOf(date: GanttDate) { + viewEndOf(date: GanttDate) { return date.endOfYear(); }