ct-gantt-core 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -83
- package/dist/index.cjs +134 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +436 -25
- package/dist/index.d.ts +436 -25
- package/dist/index.js +132 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,83 +1,98 @@
|
|
|
1
|
-
# ct-gantt-core
|
|
2
|
-
|
|
3
|
-
框架无关的甘特图数据、布局、依赖排程和命令式引擎。该包不包含界面,可用于 Vue、React、原生 JavaScript、服务端计算或自定义渲染器。
|
|
4
|
-
|
|
5
|
-
## 安装
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
1
|
+
# ct-gantt-core
|
|
2
|
+
|
|
3
|
+
框架无关的甘特图数据、布局、依赖排程和命令式引擎。该包不包含界面,可用于 Vue、React、原生 JavaScript、服务端计算或自定义渲染器。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
支持 npm / pnpm / yarn 三种包管理器,任选其一:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# npm
|
|
11
|
+
npm install ct-gantt-core
|
|
12
|
+
|
|
13
|
+
# pnpm
|
|
14
|
+
pnpm add ct-gantt-core
|
|
15
|
+
|
|
16
|
+
# yarn
|
|
17
|
+
yarn add ct-gantt-core
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`ct-gantt-core` 框架无关、无运行时依赖,Vue / React / 原生 JavaScript / 服务端环境均可直接使用。
|
|
21
|
+
|
|
22
|
+
## 最小示例
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { GanttEngine, type GanttTask } from "ct-gantt-core"
|
|
26
|
+
|
|
27
|
+
// 任务:type 区分 task / milestone / summary;
|
|
28
|
+
// plan 是计划日期,actual 是实际日期与进度。
|
|
29
|
+
const tasks: GanttTask[] = [
|
|
30
|
+
{
|
|
31
|
+
id: "task-1",
|
|
32
|
+
name: "需求分析",
|
|
33
|
+
type: "task",
|
|
34
|
+
plan: { start: "2026-07-01", end: "2026-07-05" },
|
|
35
|
+
actual: { start: "2026-07-01", end: "2026-07-06", progress: 60 }
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
// 创建引擎:持有任务、依赖与视口配置,负责全部数据计算。
|
|
40
|
+
const engine = new GanttEngine({
|
|
41
|
+
tasks,
|
|
42
|
+
config: { viewMode: "day", columnWidth: 30 }
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// 计算布局:ok 为 true 时 data 含每个任务条的像素位置与尺寸。
|
|
46
|
+
const layout = engine.getLayout()
|
|
47
|
+
if (layout.ok) {
|
|
48
|
+
console.log(layout.data)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
engine.setTask("task-1", { progress: 80 }) // 命令式更新任务并触发重算
|
|
52
|
+
engine.destroy() // 释放引擎内部资源
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 依赖排程
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { scheduleByDependencies, type GanttLink } from "ct-gantt-core"
|
|
59
|
+
|
|
60
|
+
// 依赖:FS 表示 design 完成后 dev 才能开始。
|
|
61
|
+
const links: GanttLink[] = [
|
|
62
|
+
{ id: "design-to-dev", sourceId: "design", targetId: "dev", type: "FS" }
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
// 按依赖推算任务日期;ok 为 false 时 error 含原因(如循环依赖)。
|
|
66
|
+
const result = scheduleByDependencies(tasks, links)
|
|
67
|
+
if (result.ok) {
|
|
68
|
+
console.log(result.data)
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
支持 `FS`、`SS`、`FF`、`SF` 四种依赖类型,以及自然日或工作日间隔。
|
|
73
|
+
|
|
74
|
+
## 主要导出
|
|
75
|
+
|
|
76
|
+
| API | 用途 |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| `GanttEngine` | 管理任务、依赖、折叠、预览、布局和视口命令 |
|
|
79
|
+
| `computeLayout` | 计算任务条位置和尺寸 |
|
|
80
|
+
| `computeTimeScale` | 计算时间刻度(day/week/month/quarter/year) |
|
|
81
|
+
| `computeHourScale` | 计算小时级时间刻度(每天 24 个小时列,顶行日期 + 底部整点小时) |
|
|
82
|
+
| `computeResourceLayout` | 资源视图布局:资源行 + 占用色条(支持 day/hour 粒度与车道堆叠) |
|
|
83
|
+
| `resolveUsageTimes` | 将资源占用解析为布局用起止时间(hour 粒度下纯日期占用含结束日整天) |
|
|
84
|
+
| `scheduleByDependencies` | 按依赖关系调整任务日期 |
|
|
85
|
+
| `computeImpact` | 分析任务变更的影响和约束冲突 |
|
|
86
|
+
| `checkCyclicDependency` | 检查循环依赖 |
|
|
87
|
+
| `normalizeLinks` | 统一任务内依赖与独立依赖数据 |
|
|
88
|
+
| `flattenTasks` | 将阶段树转换为可渲染行 |
|
|
89
|
+
| `toDate`、`toDateTime`、`addDays`、`addHours`、`diffDays`、`diffHours`、`formatDate`、`formatDateTime` | 日期工具(`toDateTime`/`diffHours` 保留时间,用于小时级计算) |
|
|
90
|
+
|
|
91
|
+
## 数据说明
|
|
92
|
+
|
|
93
|
+
- `plan` 表示计划日期。
|
|
94
|
+
- `actual` 表示实际日期和完成进度。
|
|
95
|
+
- `summary` 表示阶段,`task` 表示普通任务,`milestone` 表示任务型里程碑。
|
|
96
|
+
- Core 不负责绘制界面;需要现成 Vue 界面时请安装 `ct-gantt-vue`。
|
|
97
|
+
|
|
98
|
+
完整类型、配置和示例请查看[项目文档](https://github.com/moonlight-219/ganttu#readme)。
|
package/dist/index.cjs
CHANGED
|
@@ -35,6 +35,7 @@ __export(index_exports, {
|
|
|
35
35
|
addHours: () => addHours,
|
|
36
36
|
addPlannedDuration: () => addPlannedDuration,
|
|
37
37
|
alignPlannedDate: () => alignPlannedDate,
|
|
38
|
+
calendarIncludesWeekends: () => calendarIncludesWeekends,
|
|
38
39
|
checkCyclicDependency: () => checkCyclicDependency,
|
|
39
40
|
computeHourScale: () => computeHourScale,
|
|
40
41
|
computeImpact: () => computeImpact,
|
|
@@ -58,6 +59,7 @@ __export(index_exports, {
|
|
|
58
59
|
mergeTaskPatch: () => mergeTaskPatch,
|
|
59
60
|
normalizeLinks: () => normalizeLinks,
|
|
60
61
|
plannedDurationBetween: () => plannedDurationBetween,
|
|
62
|
+
resolveColumnWidth: () => resolveColumnWidth,
|
|
61
63
|
resolveGanttConfig: () => resolveGanttConfig,
|
|
62
64
|
resolveResourceColor: () => resolveResourceColor,
|
|
63
65
|
resolveUsageColor: () => resolveUsageColor,
|
|
@@ -109,8 +111,13 @@ var DEFAULT_WORKLOAD_COLORS = {
|
|
|
109
111
|
var RESOURCE_COLOR_PALETTE = [DEFAULT_RESOURCE_COLORS.booking];
|
|
110
112
|
var RESOURCE_EDITOR_COLOR_OPTIONS = [
|
|
111
113
|
"#409eff",
|
|
112
|
-
"#
|
|
113
|
-
"#
|
|
114
|
+
"#66b1ff",
|
|
115
|
+
"#67c23a",
|
|
116
|
+
"#e6a23c",
|
|
117
|
+
"#f56c6c",
|
|
118
|
+
"#909399",
|
|
119
|
+
"#9b87c4",
|
|
120
|
+
"#36cfc9"
|
|
114
121
|
];
|
|
115
122
|
var EDITOR_COLOR_OPTIONS = [
|
|
116
123
|
"#409eff",
|
|
@@ -137,6 +144,7 @@ var defaultConfig = {
|
|
|
137
144
|
showPlanBar: true,
|
|
138
145
|
showActualBar: true,
|
|
139
146
|
showTimelineWhenEmpty: false,
|
|
147
|
+
highlightWeekend: true,
|
|
140
148
|
builtInTaskEditor: false,
|
|
141
149
|
builtInMarkerEditor: true,
|
|
142
150
|
editablePlan: false,
|
|
@@ -147,6 +155,7 @@ var defaultConfig = {
|
|
|
147
155
|
taskColors: { ...DEFAULT_TASK_COLORS },
|
|
148
156
|
resourceUsageColors: { ...DEFAULT_RESOURCE_COLORS },
|
|
149
157
|
workloadColors: { ...DEFAULT_WORKLOAD_COLORS },
|
|
158
|
+
workloadDefaultCapacity: 8,
|
|
150
159
|
timeUnit: "day",
|
|
151
160
|
hourWidth: 12,
|
|
152
161
|
autoSchedule: true,
|
|
@@ -388,11 +397,14 @@ function alignPlannedDate(date, calendarId = "standard", direction = 1) {
|
|
|
388
397
|
}
|
|
389
398
|
return current;
|
|
390
399
|
}
|
|
400
|
+
function calendarIncludesWeekends(calendarId) {
|
|
401
|
+
return calendarId === "delivery";
|
|
402
|
+
}
|
|
391
403
|
function plannedDurationBetween(start, end, calendarId = "standard") {
|
|
392
404
|
const first = toDate(start);
|
|
393
405
|
const last = toDate(end);
|
|
394
406
|
if (last.getTime() < first.getTime()) return 0;
|
|
395
|
-
if (calendarId
|
|
407
|
+
if (calendarIncludesWeekends(calendarId)) return inclusiveDays(first, last);
|
|
396
408
|
let duration = 0;
|
|
397
409
|
for (let current = first; current <= last; current = addDays(current, 1)) {
|
|
398
410
|
const day = current.getDay();
|
|
@@ -564,6 +576,9 @@ function mergeGanttConfig(base, patch = {}) {
|
|
|
564
576
|
function resolveGanttConfig(patch = {}) {
|
|
565
577
|
return mergeGanttConfig(defaultConfig, patch);
|
|
566
578
|
}
|
|
579
|
+
function resolveColumnWidth(config, viewMode = config.viewMode ?? defaultConfig.viewMode) {
|
|
580
|
+
return config.columnWidths?.[viewMode] ?? config.columnWidth ?? defaultConfig.columnWidth;
|
|
581
|
+
}
|
|
567
582
|
|
|
568
583
|
// src/engines/scheduling.ts
|
|
569
584
|
function scheduleByDependencies(tasks, links) {
|
|
@@ -856,7 +871,7 @@ function computeResourceLayout(resources, usages, config, viewport) {
|
|
|
856
871
|
const mergedConfig = resolveGanttConfig(config);
|
|
857
872
|
const timeUnit = mergedConfig.timeUnit ?? "day";
|
|
858
873
|
const hourMode = timeUnit === "hour";
|
|
859
|
-
const hourW = mergedConfig.hourWidth ?? 12;
|
|
874
|
+
const hourW = mergedConfig.hourWidth ?? mergedConfig.columnWidth ?? 12;
|
|
860
875
|
for (const usage of usages) {
|
|
861
876
|
const { start, end } = resolveUsageTimes(usage, timeUnit);
|
|
862
877
|
if (!isValidDate(start) || !isValidDate(end)) {
|
|
@@ -976,17 +991,38 @@ function isLaneFree(laneEnd, start, hourMode) {
|
|
|
976
991
|
}
|
|
977
992
|
|
|
978
993
|
// src/engines/computeWorkload.ts
|
|
979
|
-
function computeWorkload(tasks, people, departments = [], range) {
|
|
994
|
+
function computeWorkload(tasks, people, departments = [], range, groupBy, defaultCapacityPerDay, includeIdle = false, includeWeekends, dateSource = "plan") {
|
|
980
995
|
const dated = range ?? inferRange(tasks);
|
|
981
996
|
if (!dated) return { dates: [], departments: [], ungrouped: [] };
|
|
982
997
|
const dates = datesBetween(dated.start, dated.end);
|
|
983
998
|
const dateIndex = new Map(dates.map((date, index) => [date, index]));
|
|
984
|
-
const
|
|
999
|
+
const parentIds = /* @__PURE__ */ new Set();
|
|
985
1000
|
for (const task of tasks) {
|
|
986
|
-
if (task.
|
|
1001
|
+
if (task.parentId) parentIds.add(task.parentId);
|
|
1002
|
+
}
|
|
1003
|
+
const isCountable = (task) => task.type === "task" || task.type === "summary" && !parentIds.has(task.id);
|
|
1004
|
+
const activeIds = /* @__PURE__ */ new Set();
|
|
1005
|
+
for (const task of tasks) {
|
|
1006
|
+
if (!isCountable(task)) continue;
|
|
1007
|
+
for (const id of task.resources ?? []) activeIds.add(id);
|
|
1008
|
+
}
|
|
1009
|
+
const candidates = mergePeopleWithTaskResources(people, tasks);
|
|
1010
|
+
const members = includeIdle ? candidates : candidates.filter((person) => activeIds.has(person.id));
|
|
1011
|
+
const rows = new Map(members.map((person) => [person.id, createPerson(person, dates, defaultCapacityPerDay)]));
|
|
1012
|
+
const tasksByPerson = /* @__PURE__ */ new Map();
|
|
1013
|
+
for (const task of tasks) {
|
|
1014
|
+
if (!isCountable(task)) continue;
|
|
1015
|
+
for (const id of task.resources ?? []) {
|
|
1016
|
+
const list = tasksByPerson.get(id);
|
|
1017
|
+
if (list) list.push(task);
|
|
1018
|
+
else tasksByPerson.set(id, [task]);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
for (const task of tasks) {
|
|
1022
|
+
if (!isCountable(task)) continue;
|
|
987
1023
|
const ids = task.resources ?? [];
|
|
988
1024
|
if (!ids.length) continue;
|
|
989
|
-
const values = task.dailyWorkloads ?? averageTaskWorkload(task);
|
|
1025
|
+
const values = task.dailyWorkloads ?? averageTaskWorkload(task, includeWeekends, dateSource);
|
|
990
1026
|
for (const personId of ids) {
|
|
991
1027
|
const row = rows.get(personId);
|
|
992
1028
|
if (!row) continue;
|
|
@@ -999,19 +1035,44 @@ function computeWorkload(tasks, people, departments = [], range) {
|
|
|
999
1035
|
for (const row of rows.values()) finalize(row);
|
|
1000
1036
|
const grouped = /* @__PURE__ */ new Map();
|
|
1001
1037
|
const ungrouped = [];
|
|
1038
|
+
const keyOf = groupBy ?? ((person) => person.departmentId);
|
|
1002
1039
|
for (const row of rows.values()) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1040
|
+
const key = keyOf(row.person, members, tasksByPerson.get(row.person.id) ?? []);
|
|
1041
|
+
if (key) {
|
|
1042
|
+
const list = grouped.get(key);
|
|
1043
|
+
if (list) list.push(row);
|
|
1044
|
+
else grouped.set(key, [row]);
|
|
1007
1045
|
} else {
|
|
1008
1046
|
ungrouped.push(row);
|
|
1009
1047
|
}
|
|
1010
1048
|
}
|
|
1011
1049
|
const departmentMap = new Map(departments.map((item) => [item.id, item]));
|
|
1012
|
-
const result = [...grouped].map(([id,
|
|
1050
|
+
const result = [...grouped].map(([id, list]) => aggregateDepartment(departmentMap.get(id) ?? { id, name: id }, list, dates));
|
|
1013
1051
|
return { dates, departments: result, ungrouped };
|
|
1014
1052
|
}
|
|
1053
|
+
function derivePeople(tasks) {
|
|
1054
|
+
const map = /* @__PURE__ */ new Map();
|
|
1055
|
+
for (const task of tasks) {
|
|
1056
|
+
for (const id of task.resources ?? []) {
|
|
1057
|
+
if (!map.has(id)) map.set(id, { id, name: id });
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return [...map.values()];
|
|
1061
|
+
}
|
|
1062
|
+
function mergePeopleWithTaskResources(people, tasks) {
|
|
1063
|
+
const list = people?.length ? [...people] : derivePeople(tasks);
|
|
1064
|
+
const known = new Set(list.map((person) => person.id));
|
|
1065
|
+
for (const task of tasks) {
|
|
1066
|
+
if (task.type === "summary" || task.type === "milestone") continue;
|
|
1067
|
+
for (const id of task.resources ?? []) {
|
|
1068
|
+
if (!known.has(id)) {
|
|
1069
|
+
known.add(id);
|
|
1070
|
+
list.push({ id, name: id });
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return list;
|
|
1075
|
+
}
|
|
1015
1076
|
function inferRange(tasks) {
|
|
1016
1077
|
if (!tasks.length) return void 0;
|
|
1017
1078
|
const extent = dateExtent(tasks.flatMap((task) => [toDate(task.plan.start), toDate(task.plan.end)]));
|
|
@@ -1022,18 +1083,22 @@ function datesBetween(start, end) {
|
|
|
1022
1083
|
for (let date = toDate(start); date <= toDate(end); date = addDays(date, 1)) values.push(formatDate(date));
|
|
1023
1084
|
return values;
|
|
1024
1085
|
}
|
|
1025
|
-
function averageTaskWorkload(task) {
|
|
1026
|
-
const
|
|
1027
|
-
const
|
|
1028
|
-
const
|
|
1086
|
+
function averageTaskWorkload(task, includeWeekends, dateSource = "plan") {
|
|
1087
|
+
const sourceStart = dateSource === "actual" ? task.actual.start : task.plan.start;
|
|
1088
|
+
const sourceEnd = dateSource === "actual" ? task.actual.end : task.plan.end;
|
|
1089
|
+
const allDates = datesBetween(sourceStart, sourceEnd);
|
|
1090
|
+
const workdays = allDates.filter((date) => {
|
|
1029
1091
|
const day = toDate(date).getDay();
|
|
1030
1092
|
return day !== 0 && day !== 6;
|
|
1031
1093
|
});
|
|
1094
|
+
const withWeekends = task.includeWeekends ?? includeWeekends ?? calendarIncludesWeekends(task.calendarId);
|
|
1095
|
+
const dates = withWeekends ? allDates : workdays;
|
|
1096
|
+
const total = task.workload ?? dates.length * 8;
|
|
1032
1097
|
const hours = dates.length ? total / dates.length : 0;
|
|
1033
1098
|
return Object.fromEntries(dates.map((date) => [date, hours]));
|
|
1034
1099
|
}
|
|
1035
|
-
function createPerson(person, dates) {
|
|
1036
|
-
return { person, days: dates.map((date) => ({ date, hours: 0, capacity: person.capacityPerDay ?? 8 })), workload: 0, capacity: 0, utilization: 0, progress: 0 };
|
|
1100
|
+
function createPerson(person, dates, defaultCapacityPerDay) {
|
|
1101
|
+
return { person, days: dates.map((date) => ({ date, hours: 0, capacity: person.capacityPerDay ?? defaultCapacityPerDay ?? 8 })), workload: 0, capacity: 0, utilization: 0, progress: 0 };
|
|
1037
1102
|
}
|
|
1038
1103
|
function finalize(row) {
|
|
1039
1104
|
row.workload = row.days.reduce((sum, day) => sum + day.hours, 0);
|
|
@@ -1076,15 +1141,25 @@ function mergeTaskPatch(task, patch) {
|
|
|
1076
1141
|
};
|
|
1077
1142
|
}
|
|
1078
1143
|
var GanttEngine = class {
|
|
1144
|
+
/** 任务列表(内部以浅拷贝存储,避免外部引用篡改)。 */
|
|
1079
1145
|
tasks;
|
|
1146
|
+
/** 链路列表(内部以浅拷贝存储)。 */
|
|
1080
1147
|
links;
|
|
1148
|
+
/** 合并后的完整配置。 */
|
|
1081
1149
|
config;
|
|
1150
|
+
/** 滚动容器元素;可为 null(纯计算场景)。 */
|
|
1082
1151
|
container;
|
|
1152
|
+
/** 当前折叠的任务 id 集合。 */
|
|
1083
1153
|
collapsedIds;
|
|
1154
|
+
/** 拖拽预览状态;null 表示无预览。 */
|
|
1084
1155
|
dragPreview = null;
|
|
1156
|
+
/** 里程碑标记列表。 */
|
|
1085
1157
|
markers;
|
|
1158
|
+
/** 事件名 → 监听器集合。 */
|
|
1086
1159
|
listeners;
|
|
1160
|
+
/** 是否已销毁。 */
|
|
1087
1161
|
destroyed = false;
|
|
1162
|
+
/** 构造引擎:初始化数据、合并配置,并异步发出 ready 事件。 */
|
|
1088
1163
|
constructor(options = {}) {
|
|
1089
1164
|
this.tasks = options.tasks ? options.tasks.map((task) => ({ ...task })) : [];
|
|
1090
1165
|
this.links = options.links ? options.links.map((link) => ({ ...link })) : [];
|
|
@@ -1095,40 +1170,50 @@ var GanttEngine = class {
|
|
|
1095
1170
|
this.listeners = /* @__PURE__ */ new Map();
|
|
1096
1171
|
queueMicrotask(() => this.emit("ready"));
|
|
1097
1172
|
}
|
|
1098
|
-
// ──
|
|
1173
|
+
// ── 状态查询(均返回副本,避免外部直接篡改内部状态) ──
|
|
1174
|
+
/** 获取全部任务的浅拷贝数组。 */
|
|
1099
1175
|
getTasks() {
|
|
1100
1176
|
return this.tasks.map((task) => ({ ...task }));
|
|
1101
1177
|
}
|
|
1178
|
+
/** 获取全部链路的浅拷贝数组。 */
|
|
1102
1179
|
getLinks() {
|
|
1103
1180
|
return this.links.map((link) => ({ ...link }));
|
|
1104
1181
|
}
|
|
1182
|
+
/** 获取全部里程碑标记的浅拷贝数组。 */
|
|
1105
1183
|
getMarkers() {
|
|
1106
1184
|
return this.markers.map((marker) => ({ ...marker }));
|
|
1107
1185
|
}
|
|
1186
|
+
/** 获取当前配置的浅拷贝。 */
|
|
1108
1187
|
getConfig() {
|
|
1109
1188
|
return { ...this.config };
|
|
1110
1189
|
}
|
|
1190
|
+
/** 按 id 查询单个任务,不存在时返回 undefined。 */
|
|
1111
1191
|
getTask(id) {
|
|
1112
1192
|
const task = this.tasks.find((item) => item.id === id);
|
|
1113
1193
|
return task ? { ...task } : void 0;
|
|
1114
1194
|
}
|
|
1195
|
+
/** 获取当前折叠的任务 id 列表。 */
|
|
1115
1196
|
getCollapsedIds() {
|
|
1116
1197
|
return Array.from(this.collapsedIds);
|
|
1117
1198
|
}
|
|
1199
|
+
/** 引擎是否已被销毁(destroy 之后为 true)。 */
|
|
1118
1200
|
isDestroyed() {
|
|
1119
1201
|
return this.destroyed;
|
|
1120
1202
|
}
|
|
1121
1203
|
// ── 命令式变更(均触发对应事件) ──
|
|
1204
|
+
/** 整体替换任务列表,并触发 taskschange 事件。 */
|
|
1122
1205
|
setTasks(tasks) {
|
|
1123
1206
|
this.assertNotDestroyed();
|
|
1124
1207
|
this.tasks = tasks.map((task) => ({ ...task }));
|
|
1125
1208
|
this.emit("taskschange", this.tasks);
|
|
1126
1209
|
}
|
|
1210
|
+
/** 整体替换链路列表,并触发 linkschange 事件。 */
|
|
1127
1211
|
setLinks(links) {
|
|
1128
1212
|
this.assertNotDestroyed();
|
|
1129
1213
|
this.links = links.map((link) => ({ ...link }));
|
|
1130
1214
|
this.emit("linkschange", this.links);
|
|
1131
1215
|
}
|
|
1216
|
+
/** 整体替换里程碑标记列表,并触发 markerschange 事件。 */
|
|
1132
1217
|
setMarkers(markers) {
|
|
1133
1218
|
this.assertNotDestroyed();
|
|
1134
1219
|
this.markers = markers.map((marker) => ({ ...marker }));
|
|
@@ -1140,6 +1225,7 @@ var GanttEngine = class {
|
|
|
1140
1225
|
this.config = mergeGanttConfig(this.config, patch);
|
|
1141
1226
|
this.emit("configchange", this.getConfig());
|
|
1142
1227
|
}
|
|
1228
|
+
/** 以合并方式设置配置,等价于 setConfig(mergeConfig(patch)) 的便捷封装。 */
|
|
1143
1229
|
setConfig(config) {
|
|
1144
1230
|
this.mergeConfig(config);
|
|
1145
1231
|
}
|
|
@@ -1154,11 +1240,13 @@ var GanttEngine = class {
|
|
|
1154
1240
|
this.tasks = this.tasks.map((task) => task.id === id ? updated : task);
|
|
1155
1241
|
this.emit("taskchange", id, patch, { ...updated });
|
|
1156
1242
|
}
|
|
1243
|
+
/** 追加一个新任务,并触发 taskcreate 事件。 */
|
|
1157
1244
|
addTask(task) {
|
|
1158
1245
|
this.assertNotDestroyed();
|
|
1159
1246
|
this.tasks = [...this.tasks, { ...task }];
|
|
1160
1247
|
this.emit("taskcreate", { ...task });
|
|
1161
1248
|
}
|
|
1249
|
+
/** 按 id 删除任务(不存在时静默忽略),并触发 taskdelete 事件。 */
|
|
1162
1250
|
removeTask(id) {
|
|
1163
1251
|
this.assertNotDestroyed();
|
|
1164
1252
|
const removed = this.tasks.find((task) => task.id === id);
|
|
@@ -1167,12 +1255,14 @@ var GanttEngine = class {
|
|
|
1167
1255
|
this.collapsedIds.delete(id);
|
|
1168
1256
|
this.emit("taskdelete", id);
|
|
1169
1257
|
}
|
|
1258
|
+
/** 折叠/展开单个任务,并触发 collapsechange 事件。 */
|
|
1170
1259
|
collapse(id, collapsed = true) {
|
|
1171
1260
|
this.assertNotDestroyed();
|
|
1172
1261
|
if (collapsed) this.collapsedIds.add(id);
|
|
1173
1262
|
else this.collapsedIds.delete(id);
|
|
1174
1263
|
this.emit("collapsechange", this.getCollapsedIds());
|
|
1175
1264
|
}
|
|
1265
|
+
/** 切换单个任务的折叠状态。 */
|
|
1176
1266
|
toggleCollapse(id) {
|
|
1177
1267
|
this.collapse(id, !this.collapsedIds.has(id));
|
|
1178
1268
|
}
|
|
@@ -1191,6 +1281,7 @@ var GanttEngine = class {
|
|
|
1191
1281
|
this.emit("collapsechange", this.getCollapsedIds());
|
|
1192
1282
|
}
|
|
1193
1283
|
// ── 拖拽预览(交互态源;组件写入迁移到 engine,dateRange 可据此扩展) ──
|
|
1284
|
+
/** 记录一次拖拽预览状态(含受影响的联动任务),并触发 previewchange 事件。 */
|
|
1194
1285
|
setPreview(preview) {
|
|
1195
1286
|
this.assertNotDestroyed();
|
|
1196
1287
|
this.dragPreview = {
|
|
@@ -1200,6 +1291,7 @@ var GanttEngine = class {
|
|
|
1200
1291
|
};
|
|
1201
1292
|
this.emit("previewchange", this.getPreview());
|
|
1202
1293
|
}
|
|
1294
|
+
/** 清空拖拽预览(无预览时忽略),并触发 previewchange 事件。 */
|
|
1203
1295
|
clearPreview() {
|
|
1204
1296
|
if (!this.dragPreview) {
|
|
1205
1297
|
return;
|
|
@@ -1207,6 +1299,7 @@ var GanttEngine = class {
|
|
|
1207
1299
|
this.dragPreview = null;
|
|
1208
1300
|
this.emit("previewchange", null);
|
|
1209
1301
|
}
|
|
1302
|
+
/** 获取当前拖拽预览的副本,无预览时返回 null。 */
|
|
1210
1303
|
getPreview() {
|
|
1211
1304
|
if (!this.dragPreview) {
|
|
1212
1305
|
return null;
|
|
@@ -1260,6 +1353,7 @@ var GanttEngine = class {
|
|
|
1260
1353
|
const extent = dateExtent(dates);
|
|
1261
1354
|
return extent ?? { start: dates[0], end: dates[0] };
|
|
1262
1355
|
}
|
|
1356
|
+
/** 计算时间刻度(委托给 computeTimeScale 纯函数)。 */
|
|
1263
1357
|
getTimeScale() {
|
|
1264
1358
|
const range = this.getDateRange();
|
|
1265
1359
|
return computeTimeScale(
|
|
@@ -1270,6 +1364,7 @@ var GanttEngine = class {
|
|
|
1270
1364
|
this.config.firstDayOfWeek
|
|
1271
1365
|
);
|
|
1272
1366
|
}
|
|
1367
|
+
/** 计算任务行布局(委托给 computeLayout 纯函数)。 */
|
|
1273
1368
|
getLayout() {
|
|
1274
1369
|
return computeLayout(
|
|
1275
1370
|
this.tasks,
|
|
@@ -1278,20 +1373,25 @@ var GanttEngine = class {
|
|
|
1278
1373
|
this.collapsedIds
|
|
1279
1374
|
);
|
|
1280
1375
|
}
|
|
1376
|
+
/** 计算扁平化任务列表(委托给 flattenTasks 纯函数)。 */
|
|
1281
1377
|
getFlatTasks() {
|
|
1282
1378
|
return flattenTasks(this.tasks, this.collapsedIds);
|
|
1283
1379
|
}
|
|
1380
|
+
/** timeline 总宽度(所有刻度宽度之和)。 */
|
|
1284
1381
|
getTotalWidth() {
|
|
1285
1382
|
return this.getTimeScale().reduce((sum, tick) => sum + tick.width, 0);
|
|
1286
1383
|
}
|
|
1384
|
+
/** 整体高度(表头 + 行数 × 行高)。 */
|
|
1287
1385
|
getTotalHeight() {
|
|
1288
1386
|
const rows = this.getFlatTasks().length;
|
|
1289
1387
|
return this.config.headerHeight + rows * this.config.rowHeight;
|
|
1290
1388
|
}
|
|
1291
1389
|
// ── 视口命令(需要 container) ──
|
|
1390
|
+
/** 当前横向滚动位置(无容器时为 0)。 */
|
|
1292
1391
|
getScrollLeft() {
|
|
1293
1392
|
return this.container?.scrollLeft ?? 0;
|
|
1294
1393
|
}
|
|
1394
|
+
/** 设置横向滚动位置(自动夹取到 [0, maxScrollLeft])。 */
|
|
1295
1395
|
setScrollLeft(px) {
|
|
1296
1396
|
if (!this.container) return;
|
|
1297
1397
|
const max = Math.max(0, this.container.scrollWidth - this.container.clientWidth);
|
|
@@ -1307,9 +1407,11 @@ var GanttEngine = class {
|
|
|
1307
1407
|
this.container.scrollLeft = max;
|
|
1308
1408
|
}
|
|
1309
1409
|
}
|
|
1410
|
+
/** 当前纵向滚动位置(无容器时为 0)。 */
|
|
1310
1411
|
getScrollTop() {
|
|
1311
1412
|
return this.container?.scrollTop ?? 0;
|
|
1312
1413
|
}
|
|
1414
|
+
/** 设置纵向滚动位置(自动夹取到 [0, maxScrollTop])。 */
|
|
1313
1415
|
setScrollTop(px) {
|
|
1314
1416
|
if (!this.container) return;
|
|
1315
1417
|
const max = Math.max(0, this.container.scrollHeight - this.container.clientHeight);
|
|
@@ -1324,15 +1426,18 @@ var GanttEngine = class {
|
|
|
1324
1426
|
const left = tick ? tick.left : diffDays(this.getDateRange().start, target) * this.config.columnWidth;
|
|
1325
1427
|
this.setScrollLeft(left);
|
|
1326
1428
|
}
|
|
1429
|
+
/** 滚动到某个任务的所在列(按布局 left 定位)。 */
|
|
1327
1430
|
scrollToTask(id) {
|
|
1328
1431
|
const layout = this.getLayout();
|
|
1329
1432
|
if (!layout.ok) return;
|
|
1330
1433
|
const item = layout.data.find((row) => row.taskId === id);
|
|
1331
1434
|
if (item) this.setScrollLeft(item.left);
|
|
1332
1435
|
}
|
|
1436
|
+
/** 滚动到最左侧。 */
|
|
1333
1437
|
scrollToStart() {
|
|
1334
1438
|
this.setScrollLeft(0);
|
|
1335
1439
|
}
|
|
1440
|
+
/** 滚动到最右侧(按 timeline 总宽度)。 */
|
|
1336
1441
|
scrollToEnd() {
|
|
1337
1442
|
if (!this.container) return;
|
|
1338
1443
|
this.setScrollLeft(this.getTotalWidth());
|
|
@@ -1351,10 +1456,12 @@ var GanttEngine = class {
|
|
|
1351
1456
|
const target = this.container.clientWidth / span;
|
|
1352
1457
|
this.mergeConfig({ columnWidth: this.clampColumnWidth(target) });
|
|
1353
1458
|
}
|
|
1459
|
+
/** 把列宽限制到 [4, 400] 并取整。 */
|
|
1354
1460
|
clampColumnWidth(value) {
|
|
1355
1461
|
return Math.max(4, Math.min(400, Math.round(value)));
|
|
1356
1462
|
}
|
|
1357
1463
|
// ── 事件订阅 ──
|
|
1464
|
+
/** 订阅事件,返回取消订阅函数(同一事件可多次订阅)。 */
|
|
1358
1465
|
on(event, fn) {
|
|
1359
1466
|
this.assertNotDestroyed();
|
|
1360
1467
|
let set = this.listeners.get(event);
|
|
@@ -1371,6 +1478,7 @@ var GanttEngine = class {
|
|
|
1371
1478
|
}
|
|
1372
1479
|
};
|
|
1373
1480
|
}
|
|
1481
|
+
/** 订阅单次事件:触发一次后自动取消订阅,返回取消订阅函数。 */
|
|
1374
1482
|
once(event, fn) {
|
|
1375
1483
|
const off = this.on(event, (...args) => {
|
|
1376
1484
|
off();
|
|
@@ -1378,6 +1486,7 @@ var GanttEngine = class {
|
|
|
1378
1486
|
});
|
|
1379
1487
|
return off;
|
|
1380
1488
|
}
|
|
1489
|
+
/** 取消订阅;不传 fn 时移除该事件的全部监听器。 */
|
|
1381
1490
|
off(event, fn) {
|
|
1382
1491
|
const set = this.listeners.get(event);
|
|
1383
1492
|
if (!set) return;
|
|
@@ -1388,6 +1497,7 @@ var GanttEngine = class {
|
|
|
1388
1497
|
this.listeners.delete(event);
|
|
1389
1498
|
}
|
|
1390
1499
|
}
|
|
1500
|
+
/** 触发事件,逐个调用监听器;单个监听器抛错不影响其余监听器。 */
|
|
1391
1501
|
emit(event, ...args) {
|
|
1392
1502
|
const set = this.listeners.get(event);
|
|
1393
1503
|
if (!set) return;
|
|
@@ -1402,6 +1512,7 @@ var GanttEngine = class {
|
|
|
1402
1512
|
}
|
|
1403
1513
|
}
|
|
1404
1514
|
// ── 生命周期 ──
|
|
1515
|
+
/** 销毁引擎:置位 destroyed、触发 destroy 事件并清空监听器与数据。 */
|
|
1405
1516
|
destroy() {
|
|
1406
1517
|
if (this.destroyed) return;
|
|
1407
1518
|
this.destroyed = true;
|
|
@@ -1410,6 +1521,7 @@ var GanttEngine = class {
|
|
|
1410
1521
|
this.tasks = [];
|
|
1411
1522
|
this.links = [];
|
|
1412
1523
|
}
|
|
1524
|
+
/** 断言引擎未被销毁,否则抛出异常(防止对已销毁实例操作)。 */
|
|
1413
1525
|
assertNotDestroyed() {
|
|
1414
1526
|
if (this.destroyed) {
|
|
1415
1527
|
throw new Error("GanttEngine: operation on destroyed instance");
|
|
@@ -1436,6 +1548,7 @@ function createGanttEngine(options = {}) {
|
|
|
1436
1548
|
addHours,
|
|
1437
1549
|
addPlannedDuration,
|
|
1438
1550
|
alignPlannedDate,
|
|
1551
|
+
calendarIncludesWeekends,
|
|
1439
1552
|
checkCyclicDependency,
|
|
1440
1553
|
computeHourScale,
|
|
1441
1554
|
computeImpact,
|
|
@@ -1459,6 +1572,7 @@ function createGanttEngine(options = {}) {
|
|
|
1459
1572
|
mergeTaskPatch,
|
|
1460
1573
|
normalizeLinks,
|
|
1461
1574
|
plannedDurationBetween,
|
|
1575
|
+
resolveColumnWidth,
|
|
1462
1576
|
resolveGanttConfig,
|
|
1463
1577
|
resolveResourceColor,
|
|
1464
1578
|
resolveUsageColor,
|