diff --git a/docs/assets/api/en/GanttAPI.md b/docs/assets/api/en/GanttAPI.md index 3afda553c7..1b7ab7b384 100644 --- a/docs/assets/api/en/GanttAPI.md +++ b/docs/assets/api/en/GanttAPI.md @@ -89,6 +89,28 @@ Update a specific data record updateTaskRecord(record: any, task_index: number, sub_task_index: number): void; ``` +### getBaselineInfoByTaskListIndex(Function) + +Get baseline information (baseline start/end date and baseline days) for the task at the specified list index. If the task has no baseline or it is outside the current date range, the returned dates are `null` and days is `0`. + +``` +getBaselineInfoByTaskListIndex( + taskShowIndex: number, + sub_task_index?: number | number[] +): { + baselineStartDate: Date | null; + baselineEndDate: Date | null; + baselineDays: number; +} +``` + +Example: + +``` +const info = ganttInstance.getBaselineInfoByTaskListIndex(0); +// info.baselineStartDate / info.baselineEndDate / info.baselineDays +``` + ### release(Function) Release the Gantt instance diff --git a/docs/assets/api/zh/GanttAPI.md b/docs/assets/api/zh/GanttAPI.md index 010150a69b..be2c0b889f 100644 --- a/docs/assets/api/zh/GanttAPI.md +++ b/docs/assets/api/zh/GanttAPI.md @@ -90,6 +90,28 @@ ITimelineScale的类型参考[配置文档]:https://visactor.com/vtable/option/G updateTaskRecord(record: any, task_index: number, sub_task_index: number): void; ``` +### getBaselineInfoByTaskListIndex(Function) + +获取指定任务在列表中的基线信息(基线开始/结束日期与基线天数)。当任务未配置基线或超出当前日期范围时,返回的日期为 `null`,天数为 `0`。 + +``` +getBaselineInfoByTaskListIndex( + taskShowIndex: number, + sub_task_index?: number | number[] +): { + baselineStartDate: Date | null; + baselineEndDate: Date | null; + baselineDays: number; +} +``` + +示例: + +``` +const info = ganttInstance.getBaselineInfoByTaskListIndex(0); +// info.baselineStartDate / info.baselineEndDate / info.baselineDays +``` + ### release(Function) 释放 Gantt 实例 diff --git a/docs/assets/demo/en/gantt/gantt-baseline.md b/docs/assets/demo/en/gantt/gantt-baseline.md new file mode 100644 index 0000000000..7b1a1b2c55 --- /dev/null +++ b/docs/assets/demo/en/gantt/gantt-baseline.md @@ -0,0 +1,190 @@ +--- +category: examples +group: gantt +title: Gantt Baseline Bar +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/gantt/gantt-baseline-preview.png +link: gantt/gantt_baseline +option: Gantt#taskBar.baselineStartDateField +--- + +# Gantt Baseline Bar + +This example shows how to configure baselines for tasks and control drawing via `baselinePosition`, and `baselineStyle`. See guide: [Gantt Baseline](../../guide/en/gantt/gantt_baseline). + +## Key Options + +- `taskBar.baselineStartDateField` / `taskBar.baselineEndDateField` +- `taskBar.baselineStyle` + +## Demo + +```javascript livedemo template=vtable +// import * as VTableGantt from '@visactor/vtable-gantt'; +let ganttInstance; + const records = [ + { + id: 1, + title: '项目规划', + developer: '张三', + startDate: '2024-07-05', + endDate: '2024-07-14', + baselineStartDate: '2024-07-01', + baselineEndDate: '2024-07-10', + progress: 80 + }, + { + id: 2, + title: '需求分析', + developer: '李四', + startDate: '2024-07-08', + endDate: '2024-07-12', + baselineStartDate: '2024-07-03', + baselineEndDate: '2024-07-08', + progress: 100 + }, + { + id: 3, + title: '设计阶段', + developer: '王五', + startDate: '2024-07-15', + endDate: '2024-07-25', + baselineStartDate: '2024-07-11', + baselineEndDate: '2024-07-20', + progress: 40 + }, + { + id: 4, + title: '开发阶段', + developer: '赵六', + startDate: '2024-07-20', + endDate: '2024-08-10', + baselineStartDate: '2024-07-18', + baselineEndDate: '2024-08-05', + progress: 30 + }, + { + id: 5, + title: '测试阶段', + developer: '钱七', + startDate: '2024-08-05', + endDate: '2024-08-20', + baselineStartDate: '2024-08-01', + baselineEndDate: '2024-08-15', + progress: 0 + }, + { + id: 6, + title: '部署上线', + developer: '孙八', + startDate: '2024-08-18', + endDate: '2024-08-25', + baselineStartDate: '2024-08-15', + baselineEndDate: '2024-08-22', + progress: 0 + } + ]; + + const columns = [ + { + field: 'title', + title: '任务名称', + width: 180 + }, + { + field: 'developer', + title: '负责人', + width: 120 + }, + { + field: 'progress', + title: '进度', + width: 80, + format: (val) => `${val}%` + } + ]; + + const option = { + records, + taskListTable: { + columns: columns, + tableWidth: 'auto', + minTableWidth: 300, + maxTableWidth: 500 + }, + headerRowHeight: 50, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate', + // baselinePosition: 'top', + labelText: '{title}', + labelTextStyle: { + fontFamily: 'Arial', + fontSize: 14, + color: '#ffffff' + }, + barStyle: { + // paddingTop: 50, + width: 25, + barColor: '#3498db', + completedBarColor: '#27ae60', + cornerRadius: 5 + }, + baselineStyle: { + // paddingTop: 0, + width: 15, + barColor: 'gray', + cornerRadius: 5 + } + }, + timelineHeader: { + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + backgroundColor: '#EEF1F5', + colWidth: 50, + scales: [ + { + unit: 'month', + step: 1 + }, + { + unit: 'week', + step: 1, + startOfWeek: 'monday', + format(date) { + return `W${date.dateIndex}`; + } + }, + { + unit: 'day', + step: 1, + format(date) { + return date.dateIndex.toString(); + } + } + ] + }, + minDate: '2024-06-25', + maxDate: '2024-09-01', + grid: { + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + }, + overscrollBehavior: 'none' + }; +ganttInstance = new VTableGantt.Gantt(document.getElementById(CONTAINER_ID), option); +window['ganttInstance'] = ganttInstance; + +``` + diff --git a/docs/assets/demo/menu.json b/docs/assets/demo/menu.json index 3ced3e050d..467d6ecf8c 100644 --- a/docs/assets/demo/menu.json +++ b/docs/assets/demo/menu.json @@ -440,6 +440,14 @@ "en": "Gantt DataZoomAxis Scrollbar" } } + , + { + "path": "gantt-baseline", + "title": { + "zh": "甘特图基线任务条", + "en": "Gantt Baseline Bar" + } + } ] }, { @@ -1837,4 +1845,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/docs/assets/demo/zh/gantt/gantt-baseline.md b/docs/assets/demo/zh/gantt/gantt-baseline.md new file mode 100644 index 0000000000..c7ebf219a8 --- /dev/null +++ b/docs/assets/demo/zh/gantt/gantt-baseline.md @@ -0,0 +1,191 @@ +--- +category: examples +group: gantt +title: 甘特图基线任务条 +cover: https://lf9-dp-fe-cms-tos.byteorg.com/obj/bit-cloud/VTable/gantt/gantt-baseline-preview.png +link: gantt/gantt_baseline +option: Gantt#taskBar.baselineStartDateField +--- + +# 甘特图基线任务条 + +该示例展示了如何为任务配置基线,并通过 `baselinePosition` 与 `baselineStyle` 控制基线与主任务条的绘制关系。教程详见:[基线任务条](../../guide/zh/gantt/gantt_baseline). + +## 关键配置 + +- `taskBar.baselineStartDateField` / `taskBar.baselineEndDateField` +- `taskBar.baselineStyle` +- `taskBar.baselinePosition` + +## 代码演示 + +```javascript livedemo template=vtable +// import * as VTableGantt from '@visactor/vtable-gantt'; +let ganttInstance; + const records = [ + { + id: 1, + title: '项目规划', + developer: '张三', + startDate: '2024-07-05', + endDate: '2024-07-14', + baselineStartDate: '2024-07-01', + baselineEndDate: '2024-07-10', + progress: 80 + }, + { + id: 2, + title: '需求分析', + developer: '李四', + startDate: '2024-07-08', + endDate: '2024-07-12', + baselineStartDate: '2024-07-03', + baselineEndDate: '2024-07-08', + progress: 100 + }, + { + id: 3, + title: '设计阶段', + developer: '王五', + startDate: '2024-07-15', + endDate: '2024-07-25', + baselineStartDate: '2024-07-11', + baselineEndDate: '2024-07-20', + progress: 40 + }, + { + id: 4, + title: '开发阶段', + developer: '赵六', + startDate: '2024-07-20', + endDate: '2024-08-10', + baselineStartDate: '2024-07-18', + baselineEndDate: '2024-08-05', + progress: 30 + }, + { + id: 5, + title: '测试阶段', + developer: '钱七', + startDate: '2024-08-05', + endDate: '2024-08-20', + baselineStartDate: '2024-08-01', + baselineEndDate: '2024-08-15', + progress: 0 + }, + { + id: 6, + title: '部署上线', + developer: '孙八', + startDate: '2024-08-18', + endDate: '2024-08-25', + baselineStartDate: '2024-08-15', + baselineEndDate: '2024-08-22', + progress: 0 + } + ]; + + const columns = [ + { + field: 'title', + title: '任务名称', + width: 180 + }, + { + field: 'developer', + title: '负责人', + width: 120 + }, + { + field: 'progress', + title: '进度', + width: 80, + format: (val) => `${val}%` + } + ]; + + const option = { + records, + taskListTable: { + columns: columns, + tableWidth: 'auto', + minTableWidth: 300, + maxTableWidth: 500 + }, + headerRowHeight: 50, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate', + // baselinePosition: 'top', + labelText: '{title}', + labelTextStyle: { + fontFamily: 'Arial', + fontSize: 14, + color: '#ffffff' + }, + barStyle: { + // paddingTop: 50, + width: 25, + barColor: '#3498db', + completedBarColor: '#27ae60', + cornerRadius: 5 + }, + baselineStyle: { + // paddingTop: 0, + width: 15, + barColor: 'gray', + cornerRadius: 5 + } + }, + timelineHeader: { + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + backgroundColor: '#EEF1F5', + colWidth: 50, + scales: [ + { + unit: 'month', + step: 1 + }, + { + unit: 'week', + step: 1, + startOfWeek: 'monday', + format(date) { + return `W${date.dateIndex}`; + } + }, + { + unit: 'day', + step: 1, + format(date) { + return date.dateIndex.toString(); + } + } + ] + }, + minDate: '2024-06-25', + maxDate: '2024-09-01', + grid: { + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + }, + overscrollBehavior: 'none' + }; +ganttInstance = new VTableGantt.Gantt(document.getElementById(CONTAINER_ID), option); +window['ganttInstance'] = ganttInstance; + +``` + diff --git a/docs/assets/guide/en/gantt/gantt_baseline.md b/docs/assets/guide/en/gantt/gantt_baseline.md new file mode 100644 index 0000000000..63a686c6a5 --- /dev/null +++ b/docs/assets/guide/en/gantt/gantt_baseline.md @@ -0,0 +1,77 @@ +# Gantt Baseline Bar + +The baseline bar visualizes the "planned baseline" against the actual task schedule. It can be drawn above, below, or overlapped with the task bar to highlight deviations. + +## Options + +- `taskBar.baselineStartDateField`: baseline start date field in records, e.g. `baselineStartDate`. +- `taskBar.baselineEndDateField`: baseline end date field in records, e.g. `baselineEndDate`. +- `taskBar.baselineStyle`: baseline bar style, same fields as `barStyle`, or a function returning per-task style. +- `taskBar.baselinePosition`: `'top' | 'bottom' | 'overlap'`, default `'bottom'`. + +Default baseline style: +``` +{ barColor: '#d3d3d3', completedBarColor: '#a9a9a9', width: 20, cornerRadius: 3, borderWidth: 0 } +``` + +## Positioning Suggestions + +- If you want the task bar and baseline bar to be layered, set `baselinePosition` to `'top'` or `'bottom'`; if you want them to overlap, set `baselinePosition: 'overlap'`, and the task bar and baseline bar will be centered vertically. +- If you want to customize the position of the task bar, you can adjust `paddingTop` in `baselineStyle` and `barStyle` to achieve it. + +## API: Read baseline info + +Use `getBaselineInfoByTaskListIndex(index, subIndex?)` to get baseline start/end dates and days: +``` +const info = ganttInstance.getBaselineInfoByTaskListIndex(0); +// { baselineStartDate, baselineEndDate, baselineDays } +``` + +## Example + +```javascript +import { Gantt } from '@visactor/vtable-gantt'; + +const records = [ + { + id: 1, + title: 'Project Planning', + startDate: '2024-07-05', + endDate: '2024-07-14', + baselineStartDate: '2024-07-01', + baselineEndDate: '2024-07-10', + progress: 80 + } +]; + +const option = { + records, + taskListTable: { + columns: [{ field: 'title', title: 'Task', width: 180 }], + tableWidth: 300 + }, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate', + baselinePosition: 'bottom', + baselineStyle: { + width: 15, + barColor: 'gray', + cornerRadius: 5 + } + }, + timelineHeader: { + colWidth: 50, + scales: [{ unit: 'day', step: 1 }] + }, + minDate: '2024-06-25', + maxDate: '2024-09-01' +}; + +const ganttInstance = new Gantt(document.getElementById('vTable'), option); +``` + diff --git a/docs/assets/guide/menu.json b/docs/assets/guide/menu.json index 8e846137f6..87d90d6d4f 100644 --- a/docs/assets/guide/menu.json +++ b/docs/assets/guide/menu.json @@ -275,6 +275,14 @@ "en": "Gantt DataZoomAxis Scrollbar" } } + , + { + "path": "gantt_baseline", + "title": { + "zh": "基线任务条", + "en": "Gantt Baseline" + } + } ] }, { @@ -1096,4 +1104,4 @@ } } ] -} \ No newline at end of file +} diff --git a/docs/assets/guide/zh/gantt/gantt_baseline.md b/docs/assets/guide/zh/gantt/gantt_baseline.md new file mode 100644 index 0000000000..40e7bb2d1a --- /dev/null +++ b/docs/assets/guide/zh/gantt/gantt_baseline.md @@ -0,0 +1,77 @@ +# 甘特图基线任务条 + +基线任务条用于展示“计划基线”与“实际任务”的差异。通过在任务条上方、下方或与其重叠位置绘制一条基线条,帮助识别偏差。 + +## 配置项 + +- `taskBar.baselineStartDateField`:基线开始日期字段,例如 `baselineStartDate`。 +- `taskBar.baselineEndDateField`:基线结束日期字段,例如 `baselineEndDate`。 +- `taskBar.baselineStyle`:基线条样式,支持与 `barStyle` 相同字段(如 `width`、`barColor`、`cornerRadius`、`borderLineWidth` 等),也支持传函数按任务返回样式。 +- `taskBar.baselinePosition`:`'top' | 'bottom' | 'overlap'`,默认 `'bottom'`。 + +默认基线样式为: +``` +{ barColor: '#d3d3d3', completedBarColor: '#a9a9a9', width: 20, cornerRadius: 3, borderWidth: 0 } +``` +## 定位建议 + +- 若希望主任务条与基线条分层展示,设置 `baselinePosition` 为 `top` 或 `bottom` ,重叠请设置 `baselinePosition: 'overlap'`,任务条和基线条会垂直居中进行展示; +- 如果要自定义任务条位置,可以通过调整`baselineStyle`中的`paddingTop`,及`barStyle`中的`paddingTop`来实现。 + +## API:读取基线信息 + +使用 `getBaselineInfoByTaskListIndex(index, subIndex?)` 可获取指定任务的基线开始/结束日期与天数: +``` +const info = ganttInstance.getBaselineInfoByTaskListIndex(0); +// { baselineStartDate, baselineEndDate, baselineDays } +``` + +## 代码示例 + +```javascript +import { Gantt } from '@visactor/vtable-gantt'; + +const records = [ + { + id: 1, + title: '项目规划', + startDate: '2024-07-05', + endDate: '2024-07-14', + baselineStartDate: '2024-07-01', + baselineEndDate: '2024-07-10', + progress: 80 + } +]; + +const option = { + records, + taskListTable: { + columns: [{ field: 'title', title: '任务名称', width: 180 }], + tableWidth: 300 + }, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate', + baselinePosition: 'bottom', + baselineStyle: { + width: 15, + barColor: 'gray', + cornerRadius: 5 + } + }, + timelineHeader: { + colWidth: 50, + scales: [{ unit: 'day', step: 1 }] + }, + minDate: '2024-06-25', + maxDate: '2024-09-01' +}; + +const ganttInstance = new Gantt(document.getElementById('vTable'), option); +``` + + diff --git a/docs/assets/option/en/common/gantt/task-bar-style.md b/docs/assets/option/en/common/gantt/task-bar-style.md index d6620a7c06..d642f59f6c 100644 --- a/docs/assets/option/en/common/gantt/task-bar-style.md +++ b/docs/assets/option/en/common/gantt/task-bar-style.md @@ -16,7 +16,9 @@ export interface ITaskBarStyle { borderLineWidth?: number; /** The border color of the task bar */ borderColor?: string; - / ** The minimum size of the task bar, when the width of the task bar is calculated to be too small, it can ensure that the task bar can be displayed normally */ + /** The minimum size of the task bar, when the width of the task bar is calculated to be too small, it can ensure that the task bar can be displayed normally */ minSize?: number; + /** The distance from the task bar to the top of the row (useful for customizing vertical position) */ + paddingTop?: number; } ``` diff --git a/docs/assets/option/en/common/gantt/task-bar.md b/docs/assets/option/en/common/gantt/task-bar.md index 1649bcefca..447f271528 100644 --- a/docs/assets/option/en/common/gantt/task-bar.md +++ b/docs/assets/option/en/common/gantt/task-bar.md @@ -236,3 +236,35 @@ ${prefix} clip(boolean) Whether to crop out the part that overflows the taskBar, the default is true Optional + +${prefix} baselineStartDateField(string) + +Data field name for the baseline start date. Used to draw the starting position of the "baseline bar". Not set by default; when set, it should match a field in records such as `baselineStartDate`. + +Optional + +${prefix} baselineEndDateField(string) + +Data field name for the baseline end date. Used to draw the ending position of the "baseline bar". Not set by default; when set, it should match a field in records such as `baselineEndDate`. + +Optional + +${prefix} baselineStyle(ITaskBarStyle | Function) + +Baseline bar style, supports the same style fields as `barStyle`. You can also configure a function to return styles per task. The default is `{ barColor: '#d3d3d3', completedBarColor: '#a9a9a9', width: 20, cornerRadius: 3, borderWidth: 0 }`. + +``` +baselineStyle?: ITaskBarStyle | ((args: TaskBarInteractionArgumentType) => ITaskBarStyle); +``` + +Optional + +${prefix} baselinePosition('top' | 'bottom' | 'overlap') = 'bottom' + +Vertical position of the baseline bar relative to the main task bar: +- `top`: baseline above the task bar; +- `bottom`: baseline below the task bar; +- `overlap`: baseline overlaps and is centered with the task bar. + +Optional + diff --git a/docs/assets/option/zh/common/gantt/task-bar-style.md b/docs/assets/option/zh/common/gantt/task-bar-style.md index 56240ee394..fa25cda45b 100644 --- a/docs/assets/option/zh/common/gantt/task-bar-style.md +++ b/docs/assets/option/zh/common/gantt/task-bar-style.md @@ -12,11 +12,13 @@ export interface ITaskBarStyle { width?: number; /** 任务条的圆角 */ cornerRadius?: number; - /** 任务条的边框 */ + /** 任务条的边框宽度 */ borderLineWidth?: number; /** 边框颜色 */ borderColor?: string; /** 任务条最小尺寸,当任务条计算宽度过小时,可以保证任务条可以正常展示 */ minSize?: number; + /** 任务条距离行顶部的距离(当需要自定义垂直位置时使用) */ + paddingTop?: number; } ``` diff --git a/docs/assets/option/zh/common/gantt/task-bar.md b/docs/assets/option/zh/common/gantt/task-bar.md index 33ad8b8032..d27d462d7f 100644 --- a/docs/assets/option/zh/common/gantt/task-bar.md +++ b/docs/assets/option/zh/common/gantt/task-bar.md @@ -242,3 +242,35 @@ ${prefix} clip(boolean) 是否裁剪掉溢出 taskBar 的部分,默认为 true 非必填 + +${prefix} baselineStartDateField(string) + +任务基线开始日期对应的数据字段名。用于绘制“基线任务条”的起始位置。默认不设置;设置后需与数据中的字段匹配,如 `baselineStartDate`。 + +非必填 + +${prefix} baselineEndDateField(string) + +任务基线结束日期对应的数据字段名。用于绘制“基线任务条”的结束位置。默认不设置;设置后需与数据中的字段匹配,如 `baselineEndDate`。 + +非必填 + +${prefix} baselineStyle(ITaskBarStyle | Function) + +基线任务条样式,支持与 `barStyle` 相同的样式字段,并可配置函数按任务返回不同样式。默认样式为:`{ barColor: '#d3d3d3', completedBarColor: '#a9a9a9', width: 20, cornerRadius: 3, borderWidth: 0 }`。 + +``` +baselineStyle?: ITaskBarStyle | ((args: TaskBarInteractionArgumentType) => ITaskBarStyle); +``` + +非必填 + +${prefix} baselinePosition('top' | 'bottom' | 'overlap') = 'bottom' + +基线任务条相对于主任务条的垂直位置: +- `top`:基线在主任务条上方; +- `bottom`:基线在主任务条下方; +- `overlap`:基线与主任务条重叠居中。 + +非必填 + diff --git a/packages/vtable-gantt/__tests__/gantt-baseline.test.ts b/packages/vtable-gantt/__tests__/gantt-baseline.test.ts new file mode 100644 index 0000000000..d4e1298e43 --- /dev/null +++ b/packages/vtable-gantt/__tests__/gantt-baseline.test.ts @@ -0,0 +1,218 @@ +// @ts-nocheck + +global.__VERSION__ = 'none'; + +import { defaultBaselineStyle } from '../src/gantt-helper'; +import { createDiv } from './dom'; +import { Gantt } from '../src/index'; + +describe('gantt baseline test', () => { + test('defaultBaselineStyle should have correct default values', () => { + expect(defaultBaselineStyle.barColor).toBe('#d3d3d3'); + expect(defaultBaselineStyle.completedBarColor).toBe('#a9a9a9'); + expect(defaultBaselineStyle.width).toBe(20); + expect(defaultBaselineStyle.cornerRadius).toBe(3); + expect(defaultBaselineStyle.borderWidth).toBe(0); + }); + + test('baseline configuration options should be correctly defined', () => { + const baselinePositions = ['top', 'bottom', 'overlap']; + expect(baselinePositions).toContain('top'); + expect(baselinePositions).toContain('bottom'); + expect(baselinePositions).toContain('overlap'); + }); + + test('Gantt should initialize with baseline configuration', () => { + const container = createDiv(); + const records = [ + { + id: 1, + title: '项目规划', + startDate: '2024-07-05', + endDate: '2024-07-14', + baselineStartDate: '2024-07-01', + baselineEndDate: '2024-07-10', + progress: 80 + } + ]; + + const option = { + records, + taskListTable: { + columns: [{ field: 'title', title: '任务名称', width: 180 }], + tableWidth: 300 + }, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate', + baselinePosition: 'bottom', + baselineStyle: { + width: 15, + barColor: 'gray', + cornerRadius: 5 + } + }, + timelineHeader: { + colWidth: 50, + scales: [{ unit: 'day', step: 1 }] + }, + minDate: '2024-06-25', + maxDate: '2024-09-01' + }; + + const ganttInstance = new Gantt(container, option); + + expect(ganttInstance.parsedOptions.baselineStartDateField).toBe('baselineStartDate'); + expect(ganttInstance.parsedOptions.baselineEndDateField).toBe('baselineEndDate'); + expect(ganttInstance.parsedOptions.baselinePosition).toBe('bottom'); + expect(ganttInstance.parsedOptions.baselineStyle).toBeDefined(); + expect(ganttInstance.parsedOptions.baselineStyle.width).toBe(15); + expect(ganttInstance.parsedOptions.baselineStyle.barColor).toBe('gray'); + expect(ganttInstance.parsedOptions.baselineStyle.cornerRadius).toBe(5); + + container.remove(); + }); + + test('getBaselineInfoByTaskListIndex should return correct baseline information', () => { + const container = createDiv(); + const records = [ + { + id: 1, + title: '项目规划', + startDate: '2024-07-05', + endDate: '2024-07-14', + baselineStartDate: '2024-07-01', + baselineEndDate: '2024-07-10', + progress: 80 + } + ]; + + const option = { + records, + taskListTable: { + columns: [{ field: 'title', title: '任务名称', width: 180 }], + tableWidth: 300 + }, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate' + }, + timelineHeader: { + colWidth: 50, + scales: [{ unit: 'day', step: 1 }] + }, + minDate: '2024-06-25', + maxDate: '2024-09-01' + }; + + const ganttInstance = new Gantt(container, option); + const baselineInfo = ganttInstance.getBaselineInfoByTaskListIndex(0); + + expect(baselineInfo).toBeDefined(); + expect(baselineInfo.baselineStartDate).not.toBeNull(); + expect(baselineInfo.baselineEndDate).not.toBeNull(); + expect(baselineInfo.baselineDays).toBeGreaterThan(0); + + container.remove(); + }); + + test('getBaselineInfoByTaskListIndex should return null for tasks without baseline dates', () => { + const container = createDiv(); + const records = [ + { + id: 1, + title: '项目规划', + startDate: '2024-07-05', + endDate: '2024-07-14', + progress: 80 + } + ]; + + const option = { + records, + taskListTable: { + columns: [{ field: 'title', title: '任务名称', width: 180 }], + tableWidth: 300 + }, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate' + }, + timelineHeader: { + colWidth: 50, + scales: [{ unit: 'day', step: 1 }] + }, + minDate: '2024-06-25', + maxDate: '2024-09-01' + }; + + const ganttInstance = new Gantt(container, option); + const baselineInfo = ganttInstance.getBaselineInfoByTaskListIndex(0); + + expect(baselineInfo).toBeDefined(); + expect(baselineInfo.baselineStartDate).toBeNull(); + expect(baselineInfo.baselineEndDate).toBeNull(); + expect(baselineInfo.baselineDays).toBe(0); + + container.remove(); + }); + + test('Gantt should initialize with different baseline positions', () => { + const positions = ['top', 'bottom', 'overlap']; + + positions.forEach(position => { + const container = createDiv(); + const records = [ + { + id: 1, + title: '项目规划', + startDate: '2024-07-05', + endDate: '2024-07-14', + baselineStartDate: '2024-07-01', + baselineEndDate: '2024-07-10', + progress: 80 + } + ]; + + const option = { + records, + taskListTable: { + columns: [{ field: 'title', title: '任务名称', width: 180 }], + tableWidth: 300 + }, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate', + baselinePosition: position as any + }, + timelineHeader: { + colWidth: 50, + scales: [{ unit: 'day', step: 1 }] + }, + minDate: '2024-06-25', + maxDate: '2024-09-01' + }; + + const ganttInstance = new Gantt(container, option); + expect(ganttInstance.parsedOptions.baselinePosition).toBe(position); + + container.remove(); + }); + }); +}); diff --git a/packages/vtable-gantt/examples/gantt/gantt-baseline.ts b/packages/vtable-gantt/examples/gantt/gantt-baseline.ts new file mode 100644 index 0000000000..a8137fb7e3 --- /dev/null +++ b/packages/vtable-gantt/examples/gantt/gantt-baseline.ts @@ -0,0 +1,179 @@ +import type { ColumnsDefine } from '@visactor/vtable'; +import type { GanttConstructorOptions } from '../../src/index'; +import { Gantt } from '../../src/index'; +import { bindDebugTool } from '../../../vtable/src/scenegraph/debug-tool'; + +const CONTAINER_ID = 'vTable'; + +export function createTable() { + const records = [ + { + id: 1, + title: '项目规划', + developer: '张三', + startDate: '2024-07-05', + endDate: '2024-07-14', + baselineStartDate: '2024-07-01', + baselineEndDate: '2024-07-10', + progress: 80 + }, + { + id: 2, + title: '需求分析', + developer: '李四', + startDate: '2024-07-08', + endDate: '2024-07-12', + baselineStartDate: '2024-07-03', + baselineEndDate: '2024-07-08', + progress: 100 + }, + { + id: 3, + title: '设计阶段', + developer: '王五', + startDate: '2024-07-15', + endDate: '2024-07-25', + baselineStartDate: '2024-07-11', + baselineEndDate: '2024-07-20', + progress: 40 + }, + { + id: 4, + title: '开发阶段', + developer: '赵六', + startDate: '2024-07-20', + endDate: '2024-08-10', + baselineStartDate: '2024-07-18', + baselineEndDate: '2024-08-05', + progress: 30 + }, + { + id: 5, + title: '测试阶段', + developer: '钱七', + startDate: '2024-08-05', + endDate: '2024-08-20', + baselineStartDate: '2024-08-01', + baselineEndDate: '2024-08-15', + progress: 0 + }, + { + id: 6, + title: '部署上线', + developer: '孙八', + startDate: '2024-08-18', + endDate: '2024-08-25', + baselineStartDate: '2024-08-15', + baselineEndDate: '2024-08-22', + progress: 0 + } + ]; + + const columns: ColumnsDefine = [ + { + field: 'title', + title: '任务名称', + width: 180 + }, + { + field: 'developer', + title: '负责人', + width: 120 + }, + { + field: 'progress', + title: '进度', + width: 80, + format: (val: number) => `${val}%` + } + ]; + + const option: GanttConstructorOptions = { + records: [], + taskListTable: { + columns: columns, + tableWidth: 'auto', + minTableWidth: 300, + maxTableWidth: 500 + }, + headerRowHeight: 50, + rowHeight: 90, + taskBar: { + startDateField: 'startDate', + endDateField: 'endDate', + progressField: 'progress', + baselineStartDateField: 'baselineStartDate', + baselineEndDateField: 'baselineEndDate', + // baselinePosition: 'top', + labelText: '{title}', + labelTextStyle: { + fontFamily: 'Arial', + fontSize: 14, + color: '#ffffff' + }, + barStyle: { + // paddingTop: 50, + width: 25, + barColor: '#3498db', + completedBarColor: '#27ae60', + cornerRadius: 5 + }, + baselineStyle: { + // paddingTop: 0, + width: 15, + barColor: 'gray', + cornerRadius: 5 + } + }, + timelineHeader: { + verticalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + }, + backgroundColor: '#EEF1F5', + colWidth: 50, + scales: [ + { + unit: 'month', + step: 1 + }, + { + unit: 'week', + step: 1, + startOfWeek: 'monday', + format(date: any) { + return `W${date.dateIndex}`; + } + }, + { + unit: 'day', + step: 1, + format(date: any) { + return date.dateIndex.toString(); + } + } + ] + }, + minDate: '2024-06-25', + maxDate: '2024-09-01', + grid: { + horizontalLine: { + lineWidth: 1, + lineColor: '#e1e4e8' + } + }, + overscrollBehavior: 'none' + }; + + const ganttInstance = new Gantt(document.getElementById(CONTAINER_ID)!, option); + window.ganttInstance = ganttInstance; + ganttInstance.setRecords(records); + + bindDebugTool(ganttInstance.scenegraph.stage as any, { + customGrapicKeys: ['role', '_updateTag'] + }); +} diff --git a/packages/vtable-gantt/examples/menu.ts b/packages/vtable-gantt/examples/menu.ts index 458be80155..ce9d8a3b6a 100644 --- a/packages/vtable-gantt/examples/menu.ts +++ b/packages/vtable-gantt/examples/menu.ts @@ -6,6 +6,10 @@ export const menus = [ path: 'gantt', name: 'gantt' }, + { + path: 'gantt', + name: 'gantt-baseline' + }, { path: 'gantt', name: 'gantt-zoom' diff --git a/packages/vtable-gantt/jest.config.js b/packages/vtable-gantt/jest.config.js index 4adcc29c0c..80750b3c8f 100644 --- a/packages/vtable-gantt/jest.config.js +++ b/packages/vtable-gantt/jest.config.js @@ -13,9 +13,13 @@ module.exports = { diagnostics: { exclude: ['**'] }, - tsconfig: { + tsconfig: { resolveJsonModule: true, - esModuleInterop: true + esModuleInterop: true, + paths: { + '@src/vrender': ['../vtable/src/vrender.ts'], + '@src/*': ['../vtable/src/*'] + } } }, __DEV__: true @@ -47,10 +51,12 @@ module.exports = { 'd3-dsv': path.resolve(__dirname, './node_modules/d3-dsv/dist/d3-dsv.min.js'), 'd3-hexbin': path.resolve(__dirname, './node_modules/d3-hexbin/build/d3-hexbin.min.js'), 'd3-hierarchy': path.resolve(__dirname, './node_modules/d3-hierarchy/dist/d3-hierarchy.min.js'), - '@visactor/vtable-editors': path.resolve(__dirname, '../vtable-editors/src/index.ts'), - '@visactor/vtable': path.resolve(__dirname, '../vtable/src/index.ts'), - '@visactor/vtable/es/vrender': path.resolve(__dirname, '../vtable/src/vrender.ts'), - '@vutils-extension': path.resolve(__dirname, './src/vutil-extension-temp/index.ts') + '^@visactor/vtable-editors$': path.resolve(__dirname, '../vtable-editors/src/index.ts'), + '^@visactor/vtable/es/themes$': path.resolve(__dirname, '../vtable/src/themes.ts'), + '^@visactor/vtable/es/vrender$': path.resolve(__dirname, '../vtable/src/vrender.ts'), + '^@visactor/vtable$': path.resolve(__dirname, '../vtable/src/index.ts'), + '^@src/vrender$': path.resolve(__dirname, '../vtable/src/vrender.ts'), + '^@vutils-extension$': path.resolve(__dirname, './src/vutil-extension-temp/index.ts') }, setupFiles: ['./setup-mock.js'] }; diff --git a/packages/vtable-gantt/src/Gantt.ts b/packages/vtable-gantt/src/Gantt.ts index 8831278b35..183bd67674 100644 --- a/packages/vtable-gantt/src/Gantt.ts +++ b/packages/vtable-gantt/src/Gantt.ts @@ -136,6 +136,7 @@ export class Gantt extends EventTarget { taskBarStyle: ITaskBarStyle | ((interactionArgs: TaskBarInteractionArgumentType) => ITaskBarStyle); taskBarMilestoneStyle: IMilestoneStyle; projectBarStyle: ITaskBarStyle | ((interactionArgs: TaskBarInteractionArgumentType) => ITaskBarStyle); + baselineStyle: ITaskBarStyle | ((interactionArgs: TaskBarInteractionArgumentType) => ITaskBarStyle); /** 里程碑是旋转后的矩形,所以需要计算里程碑的对角线长度 */ taskBarMilestoneHypotenuse: number; taskBarHoverStyle: ITaskBarHoverStyle; @@ -169,6 +170,9 @@ export class Gantt extends EventTarget { startDateField: string; endDateField: string; progressField: string; + baselineStartDateField: string; + baselineEndDateField: string; + baselinePosition: 'top' | 'bottom' | 'overlap'; minDate: Date; maxDate: Date; _minDateTime: number; @@ -632,6 +636,7 @@ export class Gantt extends EventTarget { if (record) { return (record.children?.length || 1) * this.parsedOptions.rowHeight; } + return undefined; }; listTable_options.defaultRowHeight = 'auto'; listTable_options.customConfig = { forceComputeAllRowHeight: true }; @@ -642,6 +647,7 @@ export class Gantt extends EventTarget { if (record) { return computeRowsCountByRecordDateForCompact(this, record) * this.parsedOptions.rowHeight; } + return undefined; }; listTable_options.defaultRowHeight = 'auto'; listTable_options.customConfig = { forceComputeAllRowHeight: true }; @@ -652,6 +658,7 @@ export class Gantt extends EventTarget { if (record) { return computeRowsCountByRecordDate(this, record) * this.parsedOptions.rowHeight; } + return undefined; }; listTable_options.defaultRowHeight = 'auto'; listTable_options.customConfig = { forceComputeAllRowHeight: true }; @@ -962,6 +969,81 @@ export class Gantt extends EventTarget { }; } + getBaselineInfoByTaskListIndex( + taskShowIndex: number, + sub_task_index?: number | number[] + ): { + baselineStartDate: Date | null; + baselineEndDate: Date | null; + baselineDays: number; + } { + const taskRecord = this.getRecordByIndex(taskShowIndex, sub_task_index); + const baselineStartDateField = this.parsedOptions.baselineStartDateField; + const baselineEndDateField = this.parsedOptions.baselineEndDateField; + + if ( + !baselineStartDateField || + !baselineEndDateField || + !taskRecord?.[baselineStartDateField] || + !taskRecord?.[baselineEndDateField] + ) { + return { + baselineStartDate: null, + baselineEndDate: null, + baselineDays: 0 + }; + } + + const rawBaselineStartDateTime = createDateAtMidnight(taskRecord?.[baselineStartDateField]).getTime(); + const rawBaselineEndDateTime = createDateAtMidnight(taskRecord?.[baselineEndDateField]).getTime(); + + if ( + rawBaselineEndDateTime < this.parsedOptions._minDateTime || + rawBaselineStartDateTime > this.parsedOptions._maxDateTime + ) { + return { + baselineStartDate: null, + baselineEndDate: null, + baselineDays: 0 + }; + } + + let baselineStartDate; + let baselineEndDate; + if (this.parsedOptions.timeScaleIncludeHour) { + baselineStartDate = createDateAtMidnight( + Math.min(Math.max(this.parsedOptions._minDateTime, rawBaselineStartDateTime), this.parsedOptions._maxDateTime) + ); + const rawEnd = taskRecord?.[baselineEndDateField]; + let hasMillisecondProvided = false; + if (typeof rawEnd === 'string') { + hasMillisecondProvided = /:\d{2}\.\d+/.test(rawEnd); + } + const shouldForceMillisecond = !hasMillisecondProvided; + baselineEndDate = createDateAtLastMillisecond( + Math.max(Math.min(this.parsedOptions._maxDateTime, rawBaselineEndDateTime), this.parsedOptions._minDateTime), + shouldForceMillisecond + ); + } else { + baselineStartDate = createDateAtMidnight( + Math.min(Math.max(this.parsedOptions._minDateTime, rawBaselineStartDateTime), this.parsedOptions._maxDateTime), + true + ); + baselineEndDate = createDateAtLastHour( + Math.max(Math.min(this.parsedOptions._maxDateTime, rawBaselineEndDateTime), this.parsedOptions._minDateTime), + true + ); + } + + const baselineDays = (baselineEndDate.getTime() - baselineStartDate.getTime() + 1) / (1000 * 60 * 60 * 24); + + return { + baselineStartDate, + baselineEndDate, + baselineDays + }; + } + /** * 更新任务的开始日期 * @param startDate 新的开始日期 @@ -1449,6 +1531,22 @@ export class Gantt extends EventTarget { return style; } + getBaselineStyle(task_index: number, sub_task_index?: number | number[]) { + const { startDate, endDate, taskRecord } = this.getTaskInfoByTaskListIndex(task_index, sub_task_index); + const style = this.parsedOptions.baselineStyle; + if (typeof style === 'function') { + const args = { + index: task_index, + startDate, + endDate, + taskRecord, + ganttInstance: this + }; + return style(args); + } + return style; + } + /** * 格式化日期 * @param date 日期对象或字符串 diff --git a/packages/vtable-gantt/src/gantt-helper.ts b/packages/vtable-gantt/src/gantt-helper.ts index 72ca80fea5..1d35cc8b60 100644 --- a/packages/vtable-gantt/src/gantt-helper.ts +++ b/packages/vtable-gantt/src/gantt-helper.ts @@ -35,6 +35,14 @@ export const defaultTaskBarStyle = { fontFamily: 'Arial', fontSize: 14 }; + +export const defaultBaselineStyle = { + barColor: '#d3d3d3', + completedBarColor: '#a9a9a9', + width: 20, + cornerRadius: 3, + borderWidth: 0 +}; function setWidthToDefaultTaskBarStyle(width: number) { defaultTaskBarStyle.width = width; } @@ -127,6 +135,9 @@ export function initOptions(gantt: Gantt) { gantt.parsedOptions.startDateField = options.taskBar?.startDateField ?? 'startDate'; gantt.parsedOptions.endDateField = options.taskBar?.endDateField ?? 'endDate'; gantt.parsedOptions.progressField = options.taskBar?.progressField ?? 'progress'; + gantt.parsedOptions.baselineStartDateField = options.taskBar?.baselineStartDateField; + gantt.parsedOptions.baselineEndDateField = options.taskBar?.baselineEndDateField; + gantt.parsedOptions.baselinePosition = options.taskBar?.baselinePosition ?? 'bottom'; gantt.parsedOptions.taskBarClip = options?.taskBar?.clip ?? true; gantt.parsedOptions.projectSubTasksExpandable = options?.projectSubTasksExpandable ?? true; // gantt.parsedOptions.minDate = options?.minDate @@ -219,6 +230,10 @@ export function initOptions(gantt: Gantt) { : options?.taskBar?.projectStyle ? Object.assign({}, defaultTaskBarStyle, options?.taskBar?.projectStyle) : gantt.parsedOptions.taskBarStyle; + gantt.parsedOptions.baselineStyle = + options?.taskBar?.baselineStyle && typeof options?.taskBar?.baselineStyle === 'function' + ? options.taskBar.baselineStyle + : Object.assign({}, defaultBaselineStyle, options?.taskBar?.baselineStyle); const defaultMilestoneStyle = { labelTextStyle: { fontSize: 16, diff --git a/packages/vtable-gantt/src/scenegraph/task-bar.ts b/packages/vtable-gantt/src/scenegraph/task-bar.ts index a28bd2a7eb..abf1b44f3f 100644 --- a/packages/vtable-gantt/src/scenegraph/task-bar.ts +++ b/packages/vtable-gantt/src/scenegraph/task-bar.ts @@ -142,15 +142,21 @@ export class TaskBar { const record = this._scene._gantt.getRecordByIndex(i); if (record.children?.length > 0) { for (let j = 0; j < record.children?.length; j++) { - const barGroup = this.initBar(i, j, record.children.length); - if (barGroup) { - this.barContainer.appendChild(barGroup); + const { barGroupBox, baselineBar } = this.initBar(i, j, record.children.length); + if (baselineBar) { + this.barContainer.appendChild(baselineBar); + } + if (barGroupBox) { + this.barContainer.appendChild(barGroupBox); } } } else { - const barGroup = this.initBar(i); - if (barGroup) { - this.barContainer.appendChild(barGroup); + const { barGroupBox, baselineBar } = this.initBar(i); + if (baselineBar) { + this.barContainer.appendChild(baselineBar); + } + if (barGroupBox) { + this.barContainer.appendChild(barGroupBox); } } continue; @@ -167,9 +173,12 @@ export class TaskBar { for (let j = 0; j < record.children?.length; j++) { const child_record = record.children[j]; if (child_record.type !== TaskType.PROJECT) { - const barGroup = this.initBar(i, [...sub_task_indexs, j], record.children.length); - if (barGroup) { - this.barContainer.appendChild(barGroup); + const { barGroupBox, baselineBar } = this.initBar(i, [...sub_task_indexs, j], record.children.length); + if (baselineBar) { + this.barContainer.appendChild(baselineBar); + } + if (barGroupBox) { + this.barContainer.appendChild(barGroupBox); } } else { //如果是project类型的子任务,需要递归调用 只将类型不是project的子任务添加到barContainer中 @@ -181,16 +190,22 @@ export class TaskBar { callInitBar(record, sub_task_indexs); } else { // For non-project tasks, use the default Tasks_Separate mode - const barGroup = this.initBar(i); - if (barGroup) { - this.barContainer.appendChild(barGroup); + const { barGroupBox, baselineBar } = this.initBar(i); + if (baselineBar) { + this.barContainer.appendChild(baselineBar); + } + if (barGroupBox) { + this.barContainer.appendChild(barGroupBox); } } continue; } else { - const barGroup = this.initBar(i); - if (barGroup) { - this.barContainer.appendChild(barGroup); + const { barGroupBox, baselineBar } = this.initBar(i); + if (baselineBar) { + this.barContainer.appendChild(baselineBar); + } + if (barGroupBox) { + this.barContainer.appendChild(barGroupBox); } } } @@ -212,7 +227,7 @@ export class TaskBar { (isMilestone && !startDate) || (!isMilestone && (taskDays <= 0 || !startDate || !endDate || startDate.getTime() > endDate.getTime())) ) { - return null; + return { barGroupBox: null, baselineBar: null }; } const { unit, step } = this._scene._gantt.parsedOptions.reverseSortedTimelineScales[0]; let taskBarSize = @@ -223,36 +238,115 @@ export class TaskBar { if (isValid(taskBarStyle.minSize)) { taskBarSize = Math.max(taskBarSize, taskBarStyle.minSize); } - // const minDate = createDateAtMidnight(this._scene._gantt.parsedOptions.minDate); - - // const subTaskShowRowCount = - // this._scene._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Separate - // ? childrenLength || 1 - // : this._scene._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Arrange - // ? computeRowsCountByRecordDate(this._scene._gantt, this._scene._gantt.records[index]) - // : this._scene._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Compact - // ? computeRowsCountByRecordDateForCompact(this._scene._gantt, this._scene._gantt.records[index]) - // : 1; - const oneTaskHeigth = this._scene._gantt.parsedOptions.rowHeight; // this._scene._gantt.getRowHeightByIndex(index) / subTaskShowRowCount; + + const oneTaskHeigth = this._scene._gantt.parsedOptions.rowHeight; const milestoneTaskBarHeight = this._scene._gantt.parsedOptions.taskBarMilestoneStyle.width; const x = computeCountToTimeScale(startDate, this._scene._gantt.parsedOptions.minDate, unit, step) * this._scene._gantt.parsedOptions.timelineColWidth - (isMilestone ? milestoneTaskBarHeight / 2 : 0); - const y = + let y = this._scene._gantt.getRowsHeightByIndex(0, index - 1) + (this._scene._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Separate ? ((childIndex as number) ?? 0) * oneTaskHeigth : this._scene._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Arrange || this._scene._gantt.parsedOptions.tasksShowMode === TasksShowMode.Sub_Tasks_Compact ? taskRecord.vtable_gantt_showIndex * oneTaskHeigth - : 0) + - (oneTaskHeigth - (isMilestone ? milestoneTaskBarHeight : taskbarHeight)) / 2; + : 0); + + const baselineInfo = this._scene._gantt.getBaselineInfoByTaskListIndex(index, childIndex); + const hasBaseline = baselineInfo.baselineStartDate && baselineInfo.baselineEndDate && baselineInfo.baselineDays > 0; + const baselinePosition = this._scene._gantt.parsedOptions.baselinePosition; + + let baselineBar: any = null; + let taskBarYOffset = 0; + + if (hasBaseline && !isMilestone) { + const baselineStyle = this._scene._gantt.getBaselineStyle(index, childIndex); + const baselineX = + computeCountToTimeScale(baselineInfo.baselineStartDate, this._scene._gantt.parsedOptions.minDate, unit, step) * + this._scene._gantt.parsedOptions.timelineColWidth; + const baselineWidth = + computeCountToTimeScale(baselineInfo.baselineEndDate, baselineInfo.baselineStartDate, unit, step, 1) * + this._scene._gantt.parsedOptions.timelineColWidth; + + let baselineY: number; + const taskBarPaddingTop = taskBarStyle.paddingTop ?? undefined; + const baselinePaddingTop = baselineStyle.paddingTop ?? undefined; + + if (baselinePosition === 'overlap') { + if (taskBarPaddingTop !== undefined) { + baselineY = y + taskBarPaddingTop; + } else { + baselineY = y + (oneTaskHeigth - baselineStyle.width) / 2; + } + } else if (baselinePosition === 'top') { + const gap = 4; + if (baselinePaddingTop !== undefined && taskBarPaddingTop !== undefined) { + baselineY = y + baselinePaddingTop; + taskBarYOffset = taskBarPaddingTop; + } else if (baselinePaddingTop !== undefined) { + baselineY = y + baselinePaddingTop; + taskBarYOffset = baselinePaddingTop + baselineStyle.width + gap; + } else if (taskBarPaddingTop !== undefined) { + const totalHeight = baselineStyle.width + gap + taskbarHeight; + const startY = (oneTaskHeigth - totalHeight) / 2; + baselineY = y + startY; + taskBarYOffset = taskBarPaddingTop; + } else { + const totalHeight = baselineStyle.width + gap + taskbarHeight; + const startY = (oneTaskHeigth - totalHeight) / 2; + baselineY = y + startY; + taskBarYOffset = startY + baselineStyle.width + gap; + } + } else { + const gap = 4; + if (taskBarPaddingTop !== undefined && baselinePaddingTop !== undefined) { + taskBarYOffset = taskBarPaddingTop; + baselineY = y + baselinePaddingTop; + } else if (taskBarPaddingTop !== undefined) { + taskBarYOffset = taskBarPaddingTop; + baselineY = y + taskBarPaddingTop + taskbarHeight + gap; + } else if (baselinePaddingTop !== undefined) { + const totalHeight = taskbarHeight + gap + baselineStyle.width; + const startY = (oneTaskHeigth - totalHeight) / 2; + taskBarYOffset = startY; + baselineY = y + baselinePaddingTop; + } else { + const totalHeight = taskbarHeight + gap + baselineStyle.width; + const startY = (oneTaskHeigth - totalHeight) / 2; + taskBarYOffset = startY; + baselineY = y + startY + taskbarHeight + gap; + } + } + + baselineBar = createRect({ + x: baselineX, + y: baselineY, + width: Math.max(baselineWidth, baselineStyle.minSize || 0), + height: baselineStyle.width, + fill: baselineStyle.barColor, + cornerRadius: baselineStyle.cornerRadius, + lineWidth: (baselineStyle.borderLineWidth ?? baselineStyle.borderWidth) * 2, + stroke: baselineStyle.borderColor, + pickable: false + }); + baselineBar.name = 'baseline-bar'; + } + + const taskBarPaddingTop = taskBarStyle.paddingTop ?? undefined; + if (hasBaseline && !isMilestone && baselinePosition !== 'overlap') { + y = y + taskBarYOffset; + } else if (taskBarPaddingTop !== undefined) { + y = y + taskBarPaddingTop; + } else { + y += (oneTaskHeigth - (isMilestone ? milestoneTaskBarHeight : taskbarHeight)) / 2 + taskBarYOffset; + } + const barGroupBox = new GanttTaskBarNode({ x, y, width: isMilestone ? milestoneTaskBarHeight : taskBarSize, - // height: this._scene._gantt.parsedOptions.rowHeight, height: isMilestone ? milestoneTaskBarHeight : taskbarHeight, cornerRadius: isMilestone ? this._scene._gantt.parsedOptions.taskBarMilestoneStyle.cornerRadius @@ -265,12 +359,9 @@ export class TaskBar { : taskBarStyle.borderColor, angle: isMilestone ? (45 / 180) * Math.PI : 0, anchor: isMilestone ? [x + milestoneTaskBarHeight / 2, y + milestoneTaskBarHeight / 2] : undefined - // clip: true }); barGroupBox.name = 'task-bar'; - //如果TaskShowMode是tasks_separate模式 这里的task_index其实是table中的bodyIndex;如果TaskShowMode是sub_tasks_***模式 task_index也是对应父节点任务条在table中的bodyIndex(但不会渲染父节点,只是渲染子节点) barGroupBox.task_index = index; - //如果TaskShowMode是tasks_separate模式,不会赋值sub_task_index;如果TaskShowMode是sub_tasks_***模式 这里的sub_task_index是父节点下子元素的index barGroupBox.sub_task_index = childIndex as any; barGroupBox.record = taskRecord; @@ -310,9 +401,6 @@ export class TaskBar { customLayoutObj = taskBarCustomLayout; } if (customLayoutObj) { - // if (customLayoutObj.rootContainer) { - // customLayoutObj.rootContainer = decodeReactDom(customLayoutObj.rootContainer); - // } rootContainer = customLayoutObj.rootContainer; renderDefaultBar = customLayoutObj.renderDefaultBar ?? false; renderDefaultText = customLayoutObj.renderDefaultText ?? false; @@ -321,10 +409,9 @@ export class TaskBar { } if (renderDefaultBar) { - // 创建整个任务条rect const rect = createRect({ x: 0, - y: 0, //this._scene._gantt.parsedOptions.rowHeight - taskbarHeight) / 2, + y: 0, width: barGroupBox.attribute.width, height: barGroupBox.attribute.height, fill: isMilestone ? this._scene._gantt.parsedOptions.taskBarMilestoneStyle.fillColor : taskBarStyle.barColor, @@ -334,10 +421,9 @@ export class TaskBar { barGroup.appendChild(rect); barGroupBox.barRect = rect; if (taskRecord.type !== TaskType.MILESTONE) { - // 创建已完成部分任务条rect const progress_rect = createRect({ x: 0, - y: 0, //(this._scene._gantt.parsedOptions.rowHeight - taskbarHeight) / 2, + y: 0, width: (taskBarSize * progress) / 100, height: taskbarHeight, fill: taskBarStyle.completedBarColor, @@ -430,17 +516,20 @@ export class TaskBar { barGroupBox.milestoneTextLabel = milestoneLabel; barGroupBox.milestoneTextContainer = textContainer; } - return barGroupBox; + return { barGroupBox, baselineBar }; } updateTaskBarNode(index: number, sub_task_index?: number) { const taskbarGroup = this.getTaskBarNodeByIndex(index, sub_task_index); if (taskbarGroup) { this.barContainer.removeChild(taskbarGroup); } - const barGroup = this.initBar(index, sub_task_index); - if (barGroup) { - this.barContainer.insertInto(barGroup, index); //TODO - barGroup.updateTextPosition(); + const { barGroupBox, baselineBar } = this.initBar(index, sub_task_index); + if (barGroupBox) { + this.barContainer.insertInto(barGroupBox, index); //TODO + barGroupBox.updateTextPosition(); + } + if (baselineBar) { + this.barContainer.insertBefore(baselineBar, barGroupBox); } } initHoverBarIcons() { diff --git a/packages/vtable-gantt/src/ts-types/gantt-engine.ts b/packages/vtable-gantt/src/ts-types/gantt-engine.ts index 9292a40f24..9cfc0b82d2 100644 --- a/packages/vtable-gantt/src/ts-types/gantt-engine.ts +++ b/packages/vtable-gantt/src/ts-types/gantt-engine.ts @@ -93,6 +93,14 @@ export interface GanttConstructorOptions { endDateField?: string; /** 任务进度对应的数据字段名 */ progressField?: string; + /** 基线开始日期对应的数据字段名 默认按'baselineStartDate' */ + baselineStartDateField?: string; + /** 基线结束日期对应的数据字段名 默认按'baselineEndDate' */ + baselineEndDateField?: string; + /** 基线样式 */ + baselineStyle?: ITaskBarStyle | ((args: TaskBarInteractionArgumentType) => ITaskBarStyle); + /** 基线相对于任务条的位置:'top'|'bottom'|'overlap',默认'bottom' */ + baselinePosition?: 'top' | 'bottom' | 'overlap'; /** 任务条展示文字。可以配置固定文本 或者 字符串模版`${fieldName}` */ labelText?: ITaskBarLabelText; /** 任务条文字样式 */ @@ -288,6 +296,8 @@ export interface ITaskBarStyle { /** 任务条的最小尺寸 */ minSize?: number; + /** 任务条距离行顶部的距离 */ + paddingTop?: number; } export interface IMilestoneStyle { /** 里程碑边框颜色 */