ct-gantt-core 1.0.13 → 1.0.15
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 +130 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +454 -28
- package/dist/index.d.ts +454 -28
- package/dist/index.js +128 -18
- 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
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
DEFAULT_HOUR_COLORS: () => DEFAULT_HOUR_COLORS,
|
|
25
25
|
DEFAULT_RESOURCE_COLORS: () => DEFAULT_RESOURCE_COLORS,
|
|
26
26
|
DEFAULT_TASK_COLORS: () => DEFAULT_TASK_COLORS,
|
|
27
|
+
DEFAULT_WORKLOAD_COLORS: () => DEFAULT_WORKLOAD_COLORS,
|
|
27
28
|
EDITOR_COLOR_OPTIONS: () => EDITOR_COLOR_OPTIONS,
|
|
28
29
|
GanttEngine: () => GanttEngine,
|
|
29
30
|
HOUR_MS: () => HOUR_MS,
|
|
@@ -57,6 +58,7 @@ __export(index_exports, {
|
|
|
57
58
|
mergeTaskPatch: () => mergeTaskPatch,
|
|
58
59
|
normalizeLinks: () => normalizeLinks,
|
|
59
60
|
plannedDurationBetween: () => plannedDurationBetween,
|
|
61
|
+
resolveColumnWidth: () => resolveColumnWidth,
|
|
60
62
|
resolveGanttConfig: () => resolveGanttConfig,
|
|
61
63
|
resolveResourceColor: () => resolveResourceColor,
|
|
62
64
|
resolveUsageColor: () => resolveUsageColor,
|
|
@@ -97,6 +99,14 @@ var DEFAULT_RESOURCE_COLORS = {
|
|
|
97
99
|
booking: "#409EFF",
|
|
98
100
|
assignment: "#909399"
|
|
99
101
|
};
|
|
102
|
+
var DEFAULT_WORKLOAD_COLORS = {
|
|
103
|
+
used: "#67C23A",
|
|
104
|
+
// 已排工时徽章 · Element Success
|
|
105
|
+
over: "#F56C6C",
|
|
106
|
+
// 超负荷徽章 / 超载面积条 · Element Danger
|
|
107
|
+
area: "#409EFF"
|
|
108
|
+
// capacity 模式容量面积条 · Element 主色
|
|
109
|
+
};
|
|
100
110
|
var RESOURCE_COLOR_PALETTE = [DEFAULT_RESOURCE_COLORS.booking];
|
|
101
111
|
var RESOURCE_EDITOR_COLOR_OPTIONS = [
|
|
102
112
|
"#409eff",
|
|
@@ -128,6 +138,7 @@ var defaultConfig = {
|
|
|
128
138
|
showPlanBar: true,
|
|
129
139
|
showActualBar: true,
|
|
130
140
|
showTimelineWhenEmpty: false,
|
|
141
|
+
highlightWeekend: true,
|
|
131
142
|
builtInTaskEditor: false,
|
|
132
143
|
builtInMarkerEditor: true,
|
|
133
144
|
editablePlan: false,
|
|
@@ -136,13 +147,17 @@ var defaultConfig = {
|
|
|
136
147
|
showLinkRejectionNotice: true,
|
|
137
148
|
virtualScroll: true,
|
|
138
149
|
taskColors: { ...DEFAULT_TASK_COLORS },
|
|
150
|
+
resourceUsageColors: { ...DEFAULT_RESOURCE_COLORS },
|
|
151
|
+
workloadColors: { ...DEFAULT_WORKLOAD_COLORS },
|
|
152
|
+
workloadDefaultCapacity: 8,
|
|
139
153
|
timeUnit: "day",
|
|
140
154
|
hourWidth: 12,
|
|
141
155
|
autoSchedule: true,
|
|
142
156
|
editable: true,
|
|
143
157
|
enableSelection: false,
|
|
144
158
|
showAddTaskColumn: true,
|
|
145
|
-
addTaskAs: "child"
|
|
159
|
+
addTaskAs: "child",
|
|
160
|
+
showAddUsageColumn: true
|
|
146
161
|
};
|
|
147
162
|
|
|
148
163
|
// src/services/checkCyclicDependency.ts
|
|
@@ -544,12 +559,17 @@ function mergeGanttConfig(base, patch = {}) {
|
|
|
544
559
|
...patch,
|
|
545
560
|
taskColors: { ...base.taskColors, ...patch.taskColors },
|
|
546
561
|
columnWidths: { ...base.columnWidths, ...patch.columnWidths },
|
|
547
|
-
resourceColors: { ...base.resourceColors, ...patch.resourceColors }
|
|
562
|
+
resourceColors: { ...base.resourceColors, ...patch.resourceColors },
|
|
563
|
+
resourceUsageColors: { ...base.resourceUsageColors, ...patch.resourceUsageColors },
|
|
564
|
+
workloadColors: { ...base.workloadColors, ...patch.workloadColors }
|
|
548
565
|
};
|
|
549
566
|
}
|
|
550
567
|
function resolveGanttConfig(patch = {}) {
|
|
551
568
|
return mergeGanttConfig(defaultConfig, patch);
|
|
552
569
|
}
|
|
570
|
+
function resolveColumnWidth(config, viewMode = config.viewMode ?? defaultConfig.viewMode) {
|
|
571
|
+
return config.columnWidths?.[viewMode] ?? config.columnWidth ?? defaultConfig.columnWidth;
|
|
572
|
+
}
|
|
553
573
|
|
|
554
574
|
// src/engines/scheduling.ts
|
|
555
575
|
function scheduleByDependencies(tasks, links) {
|
|
@@ -821,14 +841,14 @@ function resolveResourceColor(resource, resourceColors) {
|
|
|
821
841
|
const source = resource.name || resource.id;
|
|
822
842
|
return RESOURCE_COLOR_PALETTE[colorHash(source) % RESOURCE_COLOR_PALETTE.length];
|
|
823
843
|
}
|
|
824
|
-
function resolveUsageColor(usage, resource, resourceColors) {
|
|
844
|
+
function resolveUsageColor(usage, resource, resourceColors, resourceUsageColors) {
|
|
825
845
|
if (usage.color) {
|
|
826
846
|
return usage.color;
|
|
827
847
|
}
|
|
828
848
|
if (resource.color || resourceColors?.[resource.id]) {
|
|
829
849
|
return resolveResourceColor(resource, resourceColors);
|
|
830
850
|
}
|
|
831
|
-
return usage.type === "assignment" ? DEFAULT_RESOURCE_COLORS.assignment : DEFAULT_RESOURCE_COLORS.booking;
|
|
851
|
+
return usage.type === "assignment" ? resourceUsageColors?.assignment ?? DEFAULT_RESOURCE_COLORS.assignment : resourceUsageColors?.booking ?? DEFAULT_RESOURCE_COLORS.booking;
|
|
832
852
|
}
|
|
833
853
|
function resolveUsageTimes(usage, timeUnit) {
|
|
834
854
|
if (timeUnit === "hour") {
|
|
@@ -842,7 +862,7 @@ function computeResourceLayout(resources, usages, config, viewport) {
|
|
|
842
862
|
const mergedConfig = resolveGanttConfig(config);
|
|
843
863
|
const timeUnit = mergedConfig.timeUnit ?? "day";
|
|
844
864
|
const hourMode = timeUnit === "hour";
|
|
845
|
-
const hourW = mergedConfig.hourWidth ?? 12;
|
|
865
|
+
const hourW = mergedConfig.hourWidth ?? mergedConfig.columnWidth ?? 12;
|
|
846
866
|
for (const usage of usages) {
|
|
847
867
|
const { start, end } = resolveUsageTimes(usage, timeUnit);
|
|
848
868
|
if (!isValidDate(start) || !isValidDate(end)) {
|
|
@@ -962,17 +982,33 @@ function isLaneFree(laneEnd, start, hourMode) {
|
|
|
962
982
|
}
|
|
963
983
|
|
|
964
984
|
// src/engines/computeWorkload.ts
|
|
965
|
-
function computeWorkload(tasks, people, departments = [], range) {
|
|
985
|
+
function computeWorkload(tasks, people, departments = [], range, groupBy, defaultCapacityPerDay, includeIdle = false, includeWeekends) {
|
|
966
986
|
const dated = range ?? inferRange(tasks);
|
|
967
987
|
if (!dated) return { dates: [], departments: [], ungrouped: [] };
|
|
968
988
|
const dates = datesBetween(dated.start, dated.end);
|
|
969
989
|
const dateIndex = new Map(dates.map((date, index) => [date, index]));
|
|
970
|
-
const
|
|
990
|
+
const activeIds = /* @__PURE__ */ new Set();
|
|
991
|
+
for (const task of tasks) {
|
|
992
|
+
if (task.type === "summary" || task.type === "milestone") continue;
|
|
993
|
+
for (const id of task.resources ?? []) activeIds.add(id);
|
|
994
|
+
}
|
|
995
|
+
const candidates = mergePeopleWithTaskResources(people, tasks);
|
|
996
|
+
const members = includeIdle ? candidates : candidates.filter((person) => activeIds.has(person.id));
|
|
997
|
+
const rows = new Map(members.map((person) => [person.id, createPerson(person, dates, defaultCapacityPerDay)]));
|
|
998
|
+
const tasksByPerson = /* @__PURE__ */ new Map();
|
|
999
|
+
for (const task of tasks) {
|
|
1000
|
+
if (task.type === "summary" || task.type === "milestone") continue;
|
|
1001
|
+
for (const id of task.resources ?? []) {
|
|
1002
|
+
const list = tasksByPerson.get(id);
|
|
1003
|
+
if (list) list.push(task);
|
|
1004
|
+
else tasksByPerson.set(id, [task]);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
971
1007
|
for (const task of tasks) {
|
|
972
1008
|
if (task.type === "summary" || task.type === "milestone") continue;
|
|
973
1009
|
const ids = task.resources ?? [];
|
|
974
1010
|
if (!ids.length) continue;
|
|
975
|
-
const values = task.dailyWorkloads ?? averageTaskWorkload(task);
|
|
1011
|
+
const values = task.dailyWorkloads ?? averageTaskWorkload(task, includeWeekends);
|
|
976
1012
|
for (const personId of ids) {
|
|
977
1013
|
const row = rows.get(personId);
|
|
978
1014
|
if (!row) continue;
|
|
@@ -985,19 +1021,44 @@ function computeWorkload(tasks, people, departments = [], range) {
|
|
|
985
1021
|
for (const row of rows.values()) finalize(row);
|
|
986
1022
|
const grouped = /* @__PURE__ */ new Map();
|
|
987
1023
|
const ungrouped = [];
|
|
1024
|
+
const keyOf = groupBy ?? ((person) => person.departmentId);
|
|
988
1025
|
for (const row of rows.values()) {
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1026
|
+
const key = keyOf(row.person, members, tasksByPerson.get(row.person.id) ?? []);
|
|
1027
|
+
if (key) {
|
|
1028
|
+
const list = grouped.get(key);
|
|
1029
|
+
if (list) list.push(row);
|
|
1030
|
+
else grouped.set(key, [row]);
|
|
993
1031
|
} else {
|
|
994
1032
|
ungrouped.push(row);
|
|
995
1033
|
}
|
|
996
1034
|
}
|
|
997
1035
|
const departmentMap = new Map(departments.map((item) => [item.id, item]));
|
|
998
|
-
const result = [...grouped].map(([id,
|
|
1036
|
+
const result = [...grouped].map(([id, list]) => aggregateDepartment(departmentMap.get(id) ?? { id, name: id }, list, dates));
|
|
999
1037
|
return { dates, departments: result, ungrouped };
|
|
1000
1038
|
}
|
|
1039
|
+
function derivePeople(tasks) {
|
|
1040
|
+
const map = /* @__PURE__ */ new Map();
|
|
1041
|
+
for (const task of tasks) {
|
|
1042
|
+
for (const id of task.resources ?? []) {
|
|
1043
|
+
if (!map.has(id)) map.set(id, { id, name: id });
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return [...map.values()];
|
|
1047
|
+
}
|
|
1048
|
+
function mergePeopleWithTaskResources(people, tasks) {
|
|
1049
|
+
const list = people?.length ? [...people] : derivePeople(tasks);
|
|
1050
|
+
const known = new Set(list.map((person) => person.id));
|
|
1051
|
+
for (const task of tasks) {
|
|
1052
|
+
if (task.type === "summary" || task.type === "milestone") continue;
|
|
1053
|
+
for (const id of task.resources ?? []) {
|
|
1054
|
+
if (!known.has(id)) {
|
|
1055
|
+
known.add(id);
|
|
1056
|
+
list.push({ id, name: id });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return list;
|
|
1061
|
+
}
|
|
1001
1062
|
function inferRange(tasks) {
|
|
1002
1063
|
if (!tasks.length) return void 0;
|
|
1003
1064
|
const extent = dateExtent(tasks.flatMap((task) => [toDate(task.plan.start), toDate(task.plan.end)]));
|
|
@@ -1008,18 +1069,20 @@ function datesBetween(start, end) {
|
|
|
1008
1069
|
for (let date = toDate(start); date <= toDate(end); date = addDays(date, 1)) values.push(formatDate(date));
|
|
1009
1070
|
return values;
|
|
1010
1071
|
}
|
|
1011
|
-
function averageTaskWorkload(task) {
|
|
1072
|
+
function averageTaskWorkload(task, includeWeekends) {
|
|
1012
1073
|
const total = task.workload ?? (task.duration ?? inclusiveDays(task.plan.start, task.plan.end)) * 8;
|
|
1013
1074
|
const allDates = datesBetween(task.plan.start, task.plan.end);
|
|
1014
|
-
const
|
|
1075
|
+
const workdays = allDates.filter((date) => {
|
|
1015
1076
|
const day = toDate(date).getDay();
|
|
1016
1077
|
return day !== 0 && day !== 6;
|
|
1017
1078
|
});
|
|
1079
|
+
const withWeekends = includeWeekends ?? task.calendarId === "delivery";
|
|
1080
|
+
const dates = withWeekends ? allDates : workdays;
|
|
1018
1081
|
const hours = dates.length ? total / dates.length : 0;
|
|
1019
1082
|
return Object.fromEntries(dates.map((date) => [date, hours]));
|
|
1020
1083
|
}
|
|
1021
|
-
function createPerson(person, dates) {
|
|
1022
|
-
return { person, days: dates.map((date) => ({ date, hours: 0, capacity: person.capacityPerDay ?? 8 })), workload: 0, capacity: 0, utilization: 0, progress: 0 };
|
|
1084
|
+
function createPerson(person, dates, defaultCapacityPerDay) {
|
|
1085
|
+
return { person, days: dates.map((date) => ({ date, hours: 0, capacity: person.capacityPerDay ?? defaultCapacityPerDay ?? 8 })), workload: 0, capacity: 0, utilization: 0, progress: 0 };
|
|
1023
1086
|
}
|
|
1024
1087
|
function finalize(row) {
|
|
1025
1088
|
row.workload = row.days.reduce((sum, day) => sum + day.hours, 0);
|
|
@@ -1062,15 +1125,25 @@ function mergeTaskPatch(task, patch) {
|
|
|
1062
1125
|
};
|
|
1063
1126
|
}
|
|
1064
1127
|
var GanttEngine = class {
|
|
1128
|
+
/** 任务列表(内部以浅拷贝存储,避免外部引用篡改)。 */
|
|
1065
1129
|
tasks;
|
|
1130
|
+
/** 链路列表(内部以浅拷贝存储)。 */
|
|
1066
1131
|
links;
|
|
1132
|
+
/** 合并后的完整配置。 */
|
|
1067
1133
|
config;
|
|
1134
|
+
/** 滚动容器元素;可为 null(纯计算场景)。 */
|
|
1068
1135
|
container;
|
|
1136
|
+
/** 当前折叠的任务 id 集合。 */
|
|
1069
1137
|
collapsedIds;
|
|
1138
|
+
/** 拖拽预览状态;null 表示无预览。 */
|
|
1070
1139
|
dragPreview = null;
|
|
1140
|
+
/** 里程碑标记列表。 */
|
|
1071
1141
|
markers;
|
|
1142
|
+
/** 事件名 → 监听器集合。 */
|
|
1072
1143
|
listeners;
|
|
1144
|
+
/** 是否已销毁。 */
|
|
1073
1145
|
destroyed = false;
|
|
1146
|
+
/** 构造引擎:初始化数据、合并配置,并异步发出 ready 事件。 */
|
|
1074
1147
|
constructor(options = {}) {
|
|
1075
1148
|
this.tasks = options.tasks ? options.tasks.map((task) => ({ ...task })) : [];
|
|
1076
1149
|
this.links = options.links ? options.links.map((link) => ({ ...link })) : [];
|
|
@@ -1081,40 +1154,50 @@ var GanttEngine = class {
|
|
|
1081
1154
|
this.listeners = /* @__PURE__ */ new Map();
|
|
1082
1155
|
queueMicrotask(() => this.emit("ready"));
|
|
1083
1156
|
}
|
|
1084
|
-
// ──
|
|
1157
|
+
// ── 状态查询(均返回副本,避免外部直接篡改内部状态) ──
|
|
1158
|
+
/** 获取全部任务的浅拷贝数组。 */
|
|
1085
1159
|
getTasks() {
|
|
1086
1160
|
return this.tasks.map((task) => ({ ...task }));
|
|
1087
1161
|
}
|
|
1162
|
+
/** 获取全部链路的浅拷贝数组。 */
|
|
1088
1163
|
getLinks() {
|
|
1089
1164
|
return this.links.map((link) => ({ ...link }));
|
|
1090
1165
|
}
|
|
1166
|
+
/** 获取全部里程碑标记的浅拷贝数组。 */
|
|
1091
1167
|
getMarkers() {
|
|
1092
1168
|
return this.markers.map((marker) => ({ ...marker }));
|
|
1093
1169
|
}
|
|
1170
|
+
/** 获取当前配置的浅拷贝。 */
|
|
1094
1171
|
getConfig() {
|
|
1095
1172
|
return { ...this.config };
|
|
1096
1173
|
}
|
|
1174
|
+
/** 按 id 查询单个任务,不存在时返回 undefined。 */
|
|
1097
1175
|
getTask(id) {
|
|
1098
1176
|
const task = this.tasks.find((item) => item.id === id);
|
|
1099
1177
|
return task ? { ...task } : void 0;
|
|
1100
1178
|
}
|
|
1179
|
+
/** 获取当前折叠的任务 id 列表。 */
|
|
1101
1180
|
getCollapsedIds() {
|
|
1102
1181
|
return Array.from(this.collapsedIds);
|
|
1103
1182
|
}
|
|
1183
|
+
/** 引擎是否已被销毁(destroy 之后为 true)。 */
|
|
1104
1184
|
isDestroyed() {
|
|
1105
1185
|
return this.destroyed;
|
|
1106
1186
|
}
|
|
1107
1187
|
// ── 命令式变更(均触发对应事件) ──
|
|
1188
|
+
/** 整体替换任务列表,并触发 taskschange 事件。 */
|
|
1108
1189
|
setTasks(tasks) {
|
|
1109
1190
|
this.assertNotDestroyed();
|
|
1110
1191
|
this.tasks = tasks.map((task) => ({ ...task }));
|
|
1111
1192
|
this.emit("taskschange", this.tasks);
|
|
1112
1193
|
}
|
|
1194
|
+
/** 整体替换链路列表,并触发 linkschange 事件。 */
|
|
1113
1195
|
setLinks(links) {
|
|
1114
1196
|
this.assertNotDestroyed();
|
|
1115
1197
|
this.links = links.map((link) => ({ ...link }));
|
|
1116
1198
|
this.emit("linkschange", this.links);
|
|
1117
1199
|
}
|
|
1200
|
+
/** 整体替换里程碑标记列表,并触发 markerschange 事件。 */
|
|
1118
1201
|
setMarkers(markers) {
|
|
1119
1202
|
this.assertNotDestroyed();
|
|
1120
1203
|
this.markers = markers.map((marker) => ({ ...marker }));
|
|
@@ -1126,6 +1209,7 @@ var GanttEngine = class {
|
|
|
1126
1209
|
this.config = mergeGanttConfig(this.config, patch);
|
|
1127
1210
|
this.emit("configchange", this.getConfig());
|
|
1128
1211
|
}
|
|
1212
|
+
/** 以合并方式设置配置,等价于 setConfig(mergeConfig(patch)) 的便捷封装。 */
|
|
1129
1213
|
setConfig(config) {
|
|
1130
1214
|
this.mergeConfig(config);
|
|
1131
1215
|
}
|
|
@@ -1140,11 +1224,13 @@ var GanttEngine = class {
|
|
|
1140
1224
|
this.tasks = this.tasks.map((task) => task.id === id ? updated : task);
|
|
1141
1225
|
this.emit("taskchange", id, patch, { ...updated });
|
|
1142
1226
|
}
|
|
1227
|
+
/** 追加一个新任务,并触发 taskcreate 事件。 */
|
|
1143
1228
|
addTask(task) {
|
|
1144
1229
|
this.assertNotDestroyed();
|
|
1145
1230
|
this.tasks = [...this.tasks, { ...task }];
|
|
1146
1231
|
this.emit("taskcreate", { ...task });
|
|
1147
1232
|
}
|
|
1233
|
+
/** 按 id 删除任务(不存在时静默忽略),并触发 taskdelete 事件。 */
|
|
1148
1234
|
removeTask(id) {
|
|
1149
1235
|
this.assertNotDestroyed();
|
|
1150
1236
|
const removed = this.tasks.find((task) => task.id === id);
|
|
@@ -1153,12 +1239,14 @@ var GanttEngine = class {
|
|
|
1153
1239
|
this.collapsedIds.delete(id);
|
|
1154
1240
|
this.emit("taskdelete", id);
|
|
1155
1241
|
}
|
|
1242
|
+
/** 折叠/展开单个任务,并触发 collapsechange 事件。 */
|
|
1156
1243
|
collapse(id, collapsed = true) {
|
|
1157
1244
|
this.assertNotDestroyed();
|
|
1158
1245
|
if (collapsed) this.collapsedIds.add(id);
|
|
1159
1246
|
else this.collapsedIds.delete(id);
|
|
1160
1247
|
this.emit("collapsechange", this.getCollapsedIds());
|
|
1161
1248
|
}
|
|
1249
|
+
/** 切换单个任务的折叠状态。 */
|
|
1162
1250
|
toggleCollapse(id) {
|
|
1163
1251
|
this.collapse(id, !this.collapsedIds.has(id));
|
|
1164
1252
|
}
|
|
@@ -1177,6 +1265,7 @@ var GanttEngine = class {
|
|
|
1177
1265
|
this.emit("collapsechange", this.getCollapsedIds());
|
|
1178
1266
|
}
|
|
1179
1267
|
// ── 拖拽预览(交互态源;组件写入迁移到 engine,dateRange 可据此扩展) ──
|
|
1268
|
+
/** 记录一次拖拽预览状态(含受影响的联动任务),并触发 previewchange 事件。 */
|
|
1180
1269
|
setPreview(preview) {
|
|
1181
1270
|
this.assertNotDestroyed();
|
|
1182
1271
|
this.dragPreview = {
|
|
@@ -1186,6 +1275,7 @@ var GanttEngine = class {
|
|
|
1186
1275
|
};
|
|
1187
1276
|
this.emit("previewchange", this.getPreview());
|
|
1188
1277
|
}
|
|
1278
|
+
/** 清空拖拽预览(无预览时忽略),并触发 previewchange 事件。 */
|
|
1189
1279
|
clearPreview() {
|
|
1190
1280
|
if (!this.dragPreview) {
|
|
1191
1281
|
return;
|
|
@@ -1193,6 +1283,7 @@ var GanttEngine = class {
|
|
|
1193
1283
|
this.dragPreview = null;
|
|
1194
1284
|
this.emit("previewchange", null);
|
|
1195
1285
|
}
|
|
1286
|
+
/** 获取当前拖拽预览的副本,无预览时返回 null。 */
|
|
1196
1287
|
getPreview() {
|
|
1197
1288
|
if (!this.dragPreview) {
|
|
1198
1289
|
return null;
|
|
@@ -1246,6 +1337,7 @@ var GanttEngine = class {
|
|
|
1246
1337
|
const extent = dateExtent(dates);
|
|
1247
1338
|
return extent ?? { start: dates[0], end: dates[0] };
|
|
1248
1339
|
}
|
|
1340
|
+
/** 计算时间刻度(委托给 computeTimeScale 纯函数)。 */
|
|
1249
1341
|
getTimeScale() {
|
|
1250
1342
|
const range = this.getDateRange();
|
|
1251
1343
|
return computeTimeScale(
|
|
@@ -1256,6 +1348,7 @@ var GanttEngine = class {
|
|
|
1256
1348
|
this.config.firstDayOfWeek
|
|
1257
1349
|
);
|
|
1258
1350
|
}
|
|
1351
|
+
/** 计算任务行布局(委托给 computeLayout 纯函数)。 */
|
|
1259
1352
|
getLayout() {
|
|
1260
1353
|
return computeLayout(
|
|
1261
1354
|
this.tasks,
|
|
@@ -1264,20 +1357,25 @@ var GanttEngine = class {
|
|
|
1264
1357
|
this.collapsedIds
|
|
1265
1358
|
);
|
|
1266
1359
|
}
|
|
1360
|
+
/** 计算扁平化任务列表(委托给 flattenTasks 纯函数)。 */
|
|
1267
1361
|
getFlatTasks() {
|
|
1268
1362
|
return flattenTasks(this.tasks, this.collapsedIds);
|
|
1269
1363
|
}
|
|
1364
|
+
/** timeline 总宽度(所有刻度宽度之和)。 */
|
|
1270
1365
|
getTotalWidth() {
|
|
1271
1366
|
return this.getTimeScale().reduce((sum, tick) => sum + tick.width, 0);
|
|
1272
1367
|
}
|
|
1368
|
+
/** 整体高度(表头 + 行数 × 行高)。 */
|
|
1273
1369
|
getTotalHeight() {
|
|
1274
1370
|
const rows = this.getFlatTasks().length;
|
|
1275
1371
|
return this.config.headerHeight + rows * this.config.rowHeight;
|
|
1276
1372
|
}
|
|
1277
1373
|
// ── 视口命令(需要 container) ──
|
|
1374
|
+
/** 当前横向滚动位置(无容器时为 0)。 */
|
|
1278
1375
|
getScrollLeft() {
|
|
1279
1376
|
return this.container?.scrollLeft ?? 0;
|
|
1280
1377
|
}
|
|
1378
|
+
/** 设置横向滚动位置(自动夹取到 [0, maxScrollLeft])。 */
|
|
1281
1379
|
setScrollLeft(px) {
|
|
1282
1380
|
if (!this.container) return;
|
|
1283
1381
|
const max = Math.max(0, this.container.scrollWidth - this.container.clientWidth);
|
|
@@ -1293,9 +1391,11 @@ var GanttEngine = class {
|
|
|
1293
1391
|
this.container.scrollLeft = max;
|
|
1294
1392
|
}
|
|
1295
1393
|
}
|
|
1394
|
+
/** 当前纵向滚动位置(无容器时为 0)。 */
|
|
1296
1395
|
getScrollTop() {
|
|
1297
1396
|
return this.container?.scrollTop ?? 0;
|
|
1298
1397
|
}
|
|
1398
|
+
/** 设置纵向滚动位置(自动夹取到 [0, maxScrollTop])。 */
|
|
1299
1399
|
setScrollTop(px) {
|
|
1300
1400
|
if (!this.container) return;
|
|
1301
1401
|
const max = Math.max(0, this.container.scrollHeight - this.container.clientHeight);
|
|
@@ -1310,15 +1410,18 @@ var GanttEngine = class {
|
|
|
1310
1410
|
const left = tick ? tick.left : diffDays(this.getDateRange().start, target) * this.config.columnWidth;
|
|
1311
1411
|
this.setScrollLeft(left);
|
|
1312
1412
|
}
|
|
1413
|
+
/** 滚动到某个任务的所在列(按布局 left 定位)。 */
|
|
1313
1414
|
scrollToTask(id) {
|
|
1314
1415
|
const layout = this.getLayout();
|
|
1315
1416
|
if (!layout.ok) return;
|
|
1316
1417
|
const item = layout.data.find((row) => row.taskId === id);
|
|
1317
1418
|
if (item) this.setScrollLeft(item.left);
|
|
1318
1419
|
}
|
|
1420
|
+
/** 滚动到最左侧。 */
|
|
1319
1421
|
scrollToStart() {
|
|
1320
1422
|
this.setScrollLeft(0);
|
|
1321
1423
|
}
|
|
1424
|
+
/** 滚动到最右侧(按 timeline 总宽度)。 */
|
|
1322
1425
|
scrollToEnd() {
|
|
1323
1426
|
if (!this.container) return;
|
|
1324
1427
|
this.setScrollLeft(this.getTotalWidth());
|
|
@@ -1337,10 +1440,12 @@ var GanttEngine = class {
|
|
|
1337
1440
|
const target = this.container.clientWidth / span;
|
|
1338
1441
|
this.mergeConfig({ columnWidth: this.clampColumnWidth(target) });
|
|
1339
1442
|
}
|
|
1443
|
+
/** 把列宽限制到 [4, 400] 并取整。 */
|
|
1340
1444
|
clampColumnWidth(value) {
|
|
1341
1445
|
return Math.max(4, Math.min(400, Math.round(value)));
|
|
1342
1446
|
}
|
|
1343
1447
|
// ── 事件订阅 ──
|
|
1448
|
+
/** 订阅事件,返回取消订阅函数(同一事件可多次订阅)。 */
|
|
1344
1449
|
on(event, fn) {
|
|
1345
1450
|
this.assertNotDestroyed();
|
|
1346
1451
|
let set = this.listeners.get(event);
|
|
@@ -1357,6 +1462,7 @@ var GanttEngine = class {
|
|
|
1357
1462
|
}
|
|
1358
1463
|
};
|
|
1359
1464
|
}
|
|
1465
|
+
/** 订阅单次事件:触发一次后自动取消订阅,返回取消订阅函数。 */
|
|
1360
1466
|
once(event, fn) {
|
|
1361
1467
|
const off = this.on(event, (...args) => {
|
|
1362
1468
|
off();
|
|
@@ -1364,6 +1470,7 @@ var GanttEngine = class {
|
|
|
1364
1470
|
});
|
|
1365
1471
|
return off;
|
|
1366
1472
|
}
|
|
1473
|
+
/** 取消订阅;不传 fn 时移除该事件的全部监听器。 */
|
|
1367
1474
|
off(event, fn) {
|
|
1368
1475
|
const set = this.listeners.get(event);
|
|
1369
1476
|
if (!set) return;
|
|
@@ -1374,6 +1481,7 @@ var GanttEngine = class {
|
|
|
1374
1481
|
this.listeners.delete(event);
|
|
1375
1482
|
}
|
|
1376
1483
|
}
|
|
1484
|
+
/** 触发事件,逐个调用监听器;单个监听器抛错不影响其余监听器。 */
|
|
1377
1485
|
emit(event, ...args) {
|
|
1378
1486
|
const set = this.listeners.get(event);
|
|
1379
1487
|
if (!set) return;
|
|
@@ -1388,6 +1496,7 @@ var GanttEngine = class {
|
|
|
1388
1496
|
}
|
|
1389
1497
|
}
|
|
1390
1498
|
// ── 生命周期 ──
|
|
1499
|
+
/** 销毁引擎:置位 destroyed、触发 destroy 事件并清空监听器与数据。 */
|
|
1391
1500
|
destroy() {
|
|
1392
1501
|
if (this.destroyed) return;
|
|
1393
1502
|
this.destroyed = true;
|
|
@@ -1396,6 +1505,7 @@ var GanttEngine = class {
|
|
|
1396
1505
|
this.tasks = [];
|
|
1397
1506
|
this.links = [];
|
|
1398
1507
|
}
|
|
1508
|
+
/** 断言引擎未被销毁,否则抛出异常(防止对已销毁实例操作)。 */
|
|
1399
1509
|
assertNotDestroyed() {
|
|
1400
1510
|
if (this.destroyed) {
|
|
1401
1511
|
throw new Error("GanttEngine: operation on destroyed instance");
|
|
@@ -1411,6 +1521,7 @@ function createGanttEngine(options = {}) {
|
|
|
1411
1521
|
DEFAULT_HOUR_COLORS,
|
|
1412
1522
|
DEFAULT_RESOURCE_COLORS,
|
|
1413
1523
|
DEFAULT_TASK_COLORS,
|
|
1524
|
+
DEFAULT_WORKLOAD_COLORS,
|
|
1414
1525
|
EDITOR_COLOR_OPTIONS,
|
|
1415
1526
|
GanttEngine,
|
|
1416
1527
|
HOUR_MS,
|
|
@@ -1444,6 +1555,7 @@ function createGanttEngine(options = {}) {
|
|
|
1444
1555
|
mergeTaskPatch,
|
|
1445
1556
|
normalizeLinks,
|
|
1446
1557
|
plannedDurationBetween,
|
|
1558
|
+
resolveColumnWidth,
|
|
1447
1559
|
resolveGanttConfig,
|
|
1448
1560
|
resolveResourceColor,
|
|
1449
1561
|
resolveUsageColor,
|