ct-gantt-core 1.0.14 → 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 +111 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +427 -24
- package/dist/index.d.ts +427 -24
- package/dist/index.js +110 -14
- 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
|
@@ -58,6 +58,7 @@ __export(index_exports, {
|
|
|
58
58
|
mergeTaskPatch: () => mergeTaskPatch,
|
|
59
59
|
normalizeLinks: () => normalizeLinks,
|
|
60
60
|
plannedDurationBetween: () => plannedDurationBetween,
|
|
61
|
+
resolveColumnWidth: () => resolveColumnWidth,
|
|
61
62
|
resolveGanttConfig: () => resolveGanttConfig,
|
|
62
63
|
resolveResourceColor: () => resolveResourceColor,
|
|
63
64
|
resolveUsageColor: () => resolveUsageColor,
|
|
@@ -137,6 +138,7 @@ var defaultConfig = {
|
|
|
137
138
|
showPlanBar: true,
|
|
138
139
|
showActualBar: true,
|
|
139
140
|
showTimelineWhenEmpty: false,
|
|
141
|
+
highlightWeekend: true,
|
|
140
142
|
builtInTaskEditor: false,
|
|
141
143
|
builtInMarkerEditor: true,
|
|
142
144
|
editablePlan: false,
|
|
@@ -147,6 +149,7 @@ var defaultConfig = {
|
|
|
147
149
|
taskColors: { ...DEFAULT_TASK_COLORS },
|
|
148
150
|
resourceUsageColors: { ...DEFAULT_RESOURCE_COLORS },
|
|
149
151
|
workloadColors: { ...DEFAULT_WORKLOAD_COLORS },
|
|
152
|
+
workloadDefaultCapacity: 8,
|
|
150
153
|
timeUnit: "day",
|
|
151
154
|
hourWidth: 12,
|
|
152
155
|
autoSchedule: true,
|
|
@@ -564,6 +567,9 @@ function mergeGanttConfig(base, patch = {}) {
|
|
|
564
567
|
function resolveGanttConfig(patch = {}) {
|
|
565
568
|
return mergeGanttConfig(defaultConfig, patch);
|
|
566
569
|
}
|
|
570
|
+
function resolveColumnWidth(config, viewMode = config.viewMode ?? defaultConfig.viewMode) {
|
|
571
|
+
return config.columnWidths?.[viewMode] ?? config.columnWidth ?? defaultConfig.columnWidth;
|
|
572
|
+
}
|
|
567
573
|
|
|
568
574
|
// src/engines/scheduling.ts
|
|
569
575
|
function scheduleByDependencies(tasks, links) {
|
|
@@ -856,7 +862,7 @@ function computeResourceLayout(resources, usages, config, viewport) {
|
|
|
856
862
|
const mergedConfig = resolveGanttConfig(config);
|
|
857
863
|
const timeUnit = mergedConfig.timeUnit ?? "day";
|
|
858
864
|
const hourMode = timeUnit === "hour";
|
|
859
|
-
const hourW = mergedConfig.hourWidth ?? 12;
|
|
865
|
+
const hourW = mergedConfig.hourWidth ?? mergedConfig.columnWidth ?? 12;
|
|
860
866
|
for (const usage of usages) {
|
|
861
867
|
const { start, end } = resolveUsageTimes(usage, timeUnit);
|
|
862
868
|
if (!isValidDate(start) || !isValidDate(end)) {
|
|
@@ -976,17 +982,33 @@ function isLaneFree(laneEnd, start, hourMode) {
|
|
|
976
982
|
}
|
|
977
983
|
|
|
978
984
|
// src/engines/computeWorkload.ts
|
|
979
|
-
function computeWorkload(tasks, people, departments = [], range) {
|
|
985
|
+
function computeWorkload(tasks, people, departments = [], range, groupBy, defaultCapacityPerDay, includeIdle = false, includeWeekends) {
|
|
980
986
|
const dated = range ?? inferRange(tasks);
|
|
981
987
|
if (!dated) return { dates: [], departments: [], ungrouped: [] };
|
|
982
988
|
const dates = datesBetween(dated.start, dated.end);
|
|
983
989
|
const dateIndex = new Map(dates.map((date, index) => [date, index]));
|
|
984
|
-
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
|
+
}
|
|
985
1007
|
for (const task of tasks) {
|
|
986
1008
|
if (task.type === "summary" || task.type === "milestone") continue;
|
|
987
1009
|
const ids = task.resources ?? [];
|
|
988
1010
|
if (!ids.length) continue;
|
|
989
|
-
const values = task.dailyWorkloads ?? averageTaskWorkload(task);
|
|
1011
|
+
const values = task.dailyWorkloads ?? averageTaskWorkload(task, includeWeekends);
|
|
990
1012
|
for (const personId of ids) {
|
|
991
1013
|
const row = rows.get(personId);
|
|
992
1014
|
if (!row) continue;
|
|
@@ -999,19 +1021,44 @@ function computeWorkload(tasks, people, departments = [], range) {
|
|
|
999
1021
|
for (const row of rows.values()) finalize(row);
|
|
1000
1022
|
const grouped = /* @__PURE__ */ new Map();
|
|
1001
1023
|
const ungrouped = [];
|
|
1024
|
+
const keyOf = groupBy ?? ((person) => person.departmentId);
|
|
1002
1025
|
for (const row of rows.values()) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
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]);
|
|
1007
1031
|
} else {
|
|
1008
1032
|
ungrouped.push(row);
|
|
1009
1033
|
}
|
|
1010
1034
|
}
|
|
1011
1035
|
const departmentMap = new Map(departments.map((item) => [item.id, item]));
|
|
1012
|
-
const result = [...grouped].map(([id,
|
|
1036
|
+
const result = [...grouped].map(([id, list]) => aggregateDepartment(departmentMap.get(id) ?? { id, name: id }, list, dates));
|
|
1013
1037
|
return { dates, departments: result, ungrouped };
|
|
1014
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
|
+
}
|
|
1015
1062
|
function inferRange(tasks) {
|
|
1016
1063
|
if (!tasks.length) return void 0;
|
|
1017
1064
|
const extent = dateExtent(tasks.flatMap((task) => [toDate(task.plan.start), toDate(task.plan.end)]));
|
|
@@ -1022,18 +1069,20 @@ function datesBetween(start, end) {
|
|
|
1022
1069
|
for (let date = toDate(start); date <= toDate(end); date = addDays(date, 1)) values.push(formatDate(date));
|
|
1023
1070
|
return values;
|
|
1024
1071
|
}
|
|
1025
|
-
function averageTaskWorkload(task) {
|
|
1072
|
+
function averageTaskWorkload(task, includeWeekends) {
|
|
1026
1073
|
const total = task.workload ?? (task.duration ?? inclusiveDays(task.plan.start, task.plan.end)) * 8;
|
|
1027
1074
|
const allDates = datesBetween(task.plan.start, task.plan.end);
|
|
1028
|
-
const
|
|
1075
|
+
const workdays = allDates.filter((date) => {
|
|
1029
1076
|
const day = toDate(date).getDay();
|
|
1030
1077
|
return day !== 0 && day !== 6;
|
|
1031
1078
|
});
|
|
1079
|
+
const withWeekends = includeWeekends ?? task.calendarId === "delivery";
|
|
1080
|
+
const dates = withWeekends ? allDates : workdays;
|
|
1032
1081
|
const hours = dates.length ? total / dates.length : 0;
|
|
1033
1082
|
return Object.fromEntries(dates.map((date) => [date, hours]));
|
|
1034
1083
|
}
|
|
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 };
|
|
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 };
|
|
1037
1086
|
}
|
|
1038
1087
|
function finalize(row) {
|
|
1039
1088
|
row.workload = row.days.reduce((sum, day) => sum + day.hours, 0);
|
|
@@ -1076,15 +1125,25 @@ function mergeTaskPatch(task, patch) {
|
|
|
1076
1125
|
};
|
|
1077
1126
|
}
|
|
1078
1127
|
var GanttEngine = class {
|
|
1128
|
+
/** 任务列表(内部以浅拷贝存储,避免外部引用篡改)。 */
|
|
1079
1129
|
tasks;
|
|
1130
|
+
/** 链路列表(内部以浅拷贝存储)。 */
|
|
1080
1131
|
links;
|
|
1132
|
+
/** 合并后的完整配置。 */
|
|
1081
1133
|
config;
|
|
1134
|
+
/** 滚动容器元素;可为 null(纯计算场景)。 */
|
|
1082
1135
|
container;
|
|
1136
|
+
/** 当前折叠的任务 id 集合。 */
|
|
1083
1137
|
collapsedIds;
|
|
1138
|
+
/** 拖拽预览状态;null 表示无预览。 */
|
|
1084
1139
|
dragPreview = null;
|
|
1140
|
+
/** 里程碑标记列表。 */
|
|
1085
1141
|
markers;
|
|
1142
|
+
/** 事件名 → 监听器集合。 */
|
|
1086
1143
|
listeners;
|
|
1144
|
+
/** 是否已销毁。 */
|
|
1087
1145
|
destroyed = false;
|
|
1146
|
+
/** 构造引擎:初始化数据、合并配置,并异步发出 ready 事件。 */
|
|
1088
1147
|
constructor(options = {}) {
|
|
1089
1148
|
this.tasks = options.tasks ? options.tasks.map((task) => ({ ...task })) : [];
|
|
1090
1149
|
this.links = options.links ? options.links.map((link) => ({ ...link })) : [];
|
|
@@ -1095,40 +1154,50 @@ var GanttEngine = class {
|
|
|
1095
1154
|
this.listeners = /* @__PURE__ */ new Map();
|
|
1096
1155
|
queueMicrotask(() => this.emit("ready"));
|
|
1097
1156
|
}
|
|
1098
|
-
// ──
|
|
1157
|
+
// ── 状态查询(均返回副本,避免外部直接篡改内部状态) ──
|
|
1158
|
+
/** 获取全部任务的浅拷贝数组。 */
|
|
1099
1159
|
getTasks() {
|
|
1100
1160
|
return this.tasks.map((task) => ({ ...task }));
|
|
1101
1161
|
}
|
|
1162
|
+
/** 获取全部链路的浅拷贝数组。 */
|
|
1102
1163
|
getLinks() {
|
|
1103
1164
|
return this.links.map((link) => ({ ...link }));
|
|
1104
1165
|
}
|
|
1166
|
+
/** 获取全部里程碑标记的浅拷贝数组。 */
|
|
1105
1167
|
getMarkers() {
|
|
1106
1168
|
return this.markers.map((marker) => ({ ...marker }));
|
|
1107
1169
|
}
|
|
1170
|
+
/** 获取当前配置的浅拷贝。 */
|
|
1108
1171
|
getConfig() {
|
|
1109
1172
|
return { ...this.config };
|
|
1110
1173
|
}
|
|
1174
|
+
/** 按 id 查询单个任务,不存在时返回 undefined。 */
|
|
1111
1175
|
getTask(id) {
|
|
1112
1176
|
const task = this.tasks.find((item) => item.id === id);
|
|
1113
1177
|
return task ? { ...task } : void 0;
|
|
1114
1178
|
}
|
|
1179
|
+
/** 获取当前折叠的任务 id 列表。 */
|
|
1115
1180
|
getCollapsedIds() {
|
|
1116
1181
|
return Array.from(this.collapsedIds);
|
|
1117
1182
|
}
|
|
1183
|
+
/** 引擎是否已被销毁(destroy 之后为 true)。 */
|
|
1118
1184
|
isDestroyed() {
|
|
1119
1185
|
return this.destroyed;
|
|
1120
1186
|
}
|
|
1121
1187
|
// ── 命令式变更(均触发对应事件) ──
|
|
1188
|
+
/** 整体替换任务列表,并触发 taskschange 事件。 */
|
|
1122
1189
|
setTasks(tasks) {
|
|
1123
1190
|
this.assertNotDestroyed();
|
|
1124
1191
|
this.tasks = tasks.map((task) => ({ ...task }));
|
|
1125
1192
|
this.emit("taskschange", this.tasks);
|
|
1126
1193
|
}
|
|
1194
|
+
/** 整体替换链路列表,并触发 linkschange 事件。 */
|
|
1127
1195
|
setLinks(links) {
|
|
1128
1196
|
this.assertNotDestroyed();
|
|
1129
1197
|
this.links = links.map((link) => ({ ...link }));
|
|
1130
1198
|
this.emit("linkschange", this.links);
|
|
1131
1199
|
}
|
|
1200
|
+
/** 整体替换里程碑标记列表,并触发 markerschange 事件。 */
|
|
1132
1201
|
setMarkers(markers) {
|
|
1133
1202
|
this.assertNotDestroyed();
|
|
1134
1203
|
this.markers = markers.map((marker) => ({ ...marker }));
|
|
@@ -1140,6 +1209,7 @@ var GanttEngine = class {
|
|
|
1140
1209
|
this.config = mergeGanttConfig(this.config, patch);
|
|
1141
1210
|
this.emit("configchange", this.getConfig());
|
|
1142
1211
|
}
|
|
1212
|
+
/** 以合并方式设置配置,等价于 setConfig(mergeConfig(patch)) 的便捷封装。 */
|
|
1143
1213
|
setConfig(config) {
|
|
1144
1214
|
this.mergeConfig(config);
|
|
1145
1215
|
}
|
|
@@ -1154,11 +1224,13 @@ var GanttEngine = class {
|
|
|
1154
1224
|
this.tasks = this.tasks.map((task) => task.id === id ? updated : task);
|
|
1155
1225
|
this.emit("taskchange", id, patch, { ...updated });
|
|
1156
1226
|
}
|
|
1227
|
+
/** 追加一个新任务,并触发 taskcreate 事件。 */
|
|
1157
1228
|
addTask(task) {
|
|
1158
1229
|
this.assertNotDestroyed();
|
|
1159
1230
|
this.tasks = [...this.tasks, { ...task }];
|
|
1160
1231
|
this.emit("taskcreate", { ...task });
|
|
1161
1232
|
}
|
|
1233
|
+
/** 按 id 删除任务(不存在时静默忽略),并触发 taskdelete 事件。 */
|
|
1162
1234
|
removeTask(id) {
|
|
1163
1235
|
this.assertNotDestroyed();
|
|
1164
1236
|
const removed = this.tasks.find((task) => task.id === id);
|
|
@@ -1167,12 +1239,14 @@ var GanttEngine = class {
|
|
|
1167
1239
|
this.collapsedIds.delete(id);
|
|
1168
1240
|
this.emit("taskdelete", id);
|
|
1169
1241
|
}
|
|
1242
|
+
/** 折叠/展开单个任务,并触发 collapsechange 事件。 */
|
|
1170
1243
|
collapse(id, collapsed = true) {
|
|
1171
1244
|
this.assertNotDestroyed();
|
|
1172
1245
|
if (collapsed) this.collapsedIds.add(id);
|
|
1173
1246
|
else this.collapsedIds.delete(id);
|
|
1174
1247
|
this.emit("collapsechange", this.getCollapsedIds());
|
|
1175
1248
|
}
|
|
1249
|
+
/** 切换单个任务的折叠状态。 */
|
|
1176
1250
|
toggleCollapse(id) {
|
|
1177
1251
|
this.collapse(id, !this.collapsedIds.has(id));
|
|
1178
1252
|
}
|
|
@@ -1191,6 +1265,7 @@ var GanttEngine = class {
|
|
|
1191
1265
|
this.emit("collapsechange", this.getCollapsedIds());
|
|
1192
1266
|
}
|
|
1193
1267
|
// ── 拖拽预览(交互态源;组件写入迁移到 engine,dateRange 可据此扩展) ──
|
|
1268
|
+
/** 记录一次拖拽预览状态(含受影响的联动任务),并触发 previewchange 事件。 */
|
|
1194
1269
|
setPreview(preview) {
|
|
1195
1270
|
this.assertNotDestroyed();
|
|
1196
1271
|
this.dragPreview = {
|
|
@@ -1200,6 +1275,7 @@ var GanttEngine = class {
|
|
|
1200
1275
|
};
|
|
1201
1276
|
this.emit("previewchange", this.getPreview());
|
|
1202
1277
|
}
|
|
1278
|
+
/** 清空拖拽预览(无预览时忽略),并触发 previewchange 事件。 */
|
|
1203
1279
|
clearPreview() {
|
|
1204
1280
|
if (!this.dragPreview) {
|
|
1205
1281
|
return;
|
|
@@ -1207,6 +1283,7 @@ var GanttEngine = class {
|
|
|
1207
1283
|
this.dragPreview = null;
|
|
1208
1284
|
this.emit("previewchange", null);
|
|
1209
1285
|
}
|
|
1286
|
+
/** 获取当前拖拽预览的副本,无预览时返回 null。 */
|
|
1210
1287
|
getPreview() {
|
|
1211
1288
|
if (!this.dragPreview) {
|
|
1212
1289
|
return null;
|
|
@@ -1260,6 +1337,7 @@ var GanttEngine = class {
|
|
|
1260
1337
|
const extent = dateExtent(dates);
|
|
1261
1338
|
return extent ?? { start: dates[0], end: dates[0] };
|
|
1262
1339
|
}
|
|
1340
|
+
/** 计算时间刻度(委托给 computeTimeScale 纯函数)。 */
|
|
1263
1341
|
getTimeScale() {
|
|
1264
1342
|
const range = this.getDateRange();
|
|
1265
1343
|
return computeTimeScale(
|
|
@@ -1270,6 +1348,7 @@ var GanttEngine = class {
|
|
|
1270
1348
|
this.config.firstDayOfWeek
|
|
1271
1349
|
);
|
|
1272
1350
|
}
|
|
1351
|
+
/** 计算任务行布局(委托给 computeLayout 纯函数)。 */
|
|
1273
1352
|
getLayout() {
|
|
1274
1353
|
return computeLayout(
|
|
1275
1354
|
this.tasks,
|
|
@@ -1278,20 +1357,25 @@ var GanttEngine = class {
|
|
|
1278
1357
|
this.collapsedIds
|
|
1279
1358
|
);
|
|
1280
1359
|
}
|
|
1360
|
+
/** 计算扁平化任务列表(委托给 flattenTasks 纯函数)。 */
|
|
1281
1361
|
getFlatTasks() {
|
|
1282
1362
|
return flattenTasks(this.tasks, this.collapsedIds);
|
|
1283
1363
|
}
|
|
1364
|
+
/** timeline 总宽度(所有刻度宽度之和)。 */
|
|
1284
1365
|
getTotalWidth() {
|
|
1285
1366
|
return this.getTimeScale().reduce((sum, tick) => sum + tick.width, 0);
|
|
1286
1367
|
}
|
|
1368
|
+
/** 整体高度(表头 + 行数 × 行高)。 */
|
|
1287
1369
|
getTotalHeight() {
|
|
1288
1370
|
const rows = this.getFlatTasks().length;
|
|
1289
1371
|
return this.config.headerHeight + rows * this.config.rowHeight;
|
|
1290
1372
|
}
|
|
1291
1373
|
// ── 视口命令(需要 container) ──
|
|
1374
|
+
/** 当前横向滚动位置(无容器时为 0)。 */
|
|
1292
1375
|
getScrollLeft() {
|
|
1293
1376
|
return this.container?.scrollLeft ?? 0;
|
|
1294
1377
|
}
|
|
1378
|
+
/** 设置横向滚动位置(自动夹取到 [0, maxScrollLeft])。 */
|
|
1295
1379
|
setScrollLeft(px) {
|
|
1296
1380
|
if (!this.container) return;
|
|
1297
1381
|
const max = Math.max(0, this.container.scrollWidth - this.container.clientWidth);
|
|
@@ -1307,9 +1391,11 @@ var GanttEngine = class {
|
|
|
1307
1391
|
this.container.scrollLeft = max;
|
|
1308
1392
|
}
|
|
1309
1393
|
}
|
|
1394
|
+
/** 当前纵向滚动位置(无容器时为 0)。 */
|
|
1310
1395
|
getScrollTop() {
|
|
1311
1396
|
return this.container?.scrollTop ?? 0;
|
|
1312
1397
|
}
|
|
1398
|
+
/** 设置纵向滚动位置(自动夹取到 [0, maxScrollTop])。 */
|
|
1313
1399
|
setScrollTop(px) {
|
|
1314
1400
|
if (!this.container) return;
|
|
1315
1401
|
const max = Math.max(0, this.container.scrollHeight - this.container.clientHeight);
|
|
@@ -1324,15 +1410,18 @@ var GanttEngine = class {
|
|
|
1324
1410
|
const left = tick ? tick.left : diffDays(this.getDateRange().start, target) * this.config.columnWidth;
|
|
1325
1411
|
this.setScrollLeft(left);
|
|
1326
1412
|
}
|
|
1413
|
+
/** 滚动到某个任务的所在列(按布局 left 定位)。 */
|
|
1327
1414
|
scrollToTask(id) {
|
|
1328
1415
|
const layout = this.getLayout();
|
|
1329
1416
|
if (!layout.ok) return;
|
|
1330
1417
|
const item = layout.data.find((row) => row.taskId === id);
|
|
1331
1418
|
if (item) this.setScrollLeft(item.left);
|
|
1332
1419
|
}
|
|
1420
|
+
/** 滚动到最左侧。 */
|
|
1333
1421
|
scrollToStart() {
|
|
1334
1422
|
this.setScrollLeft(0);
|
|
1335
1423
|
}
|
|
1424
|
+
/** 滚动到最右侧(按 timeline 总宽度)。 */
|
|
1336
1425
|
scrollToEnd() {
|
|
1337
1426
|
if (!this.container) return;
|
|
1338
1427
|
this.setScrollLeft(this.getTotalWidth());
|
|
@@ -1351,10 +1440,12 @@ var GanttEngine = class {
|
|
|
1351
1440
|
const target = this.container.clientWidth / span;
|
|
1352
1441
|
this.mergeConfig({ columnWidth: this.clampColumnWidth(target) });
|
|
1353
1442
|
}
|
|
1443
|
+
/** 把列宽限制到 [4, 400] 并取整。 */
|
|
1354
1444
|
clampColumnWidth(value) {
|
|
1355
1445
|
return Math.max(4, Math.min(400, Math.round(value)));
|
|
1356
1446
|
}
|
|
1357
1447
|
// ── 事件订阅 ──
|
|
1448
|
+
/** 订阅事件,返回取消订阅函数(同一事件可多次订阅)。 */
|
|
1358
1449
|
on(event, fn) {
|
|
1359
1450
|
this.assertNotDestroyed();
|
|
1360
1451
|
let set = this.listeners.get(event);
|
|
@@ -1371,6 +1462,7 @@ var GanttEngine = class {
|
|
|
1371
1462
|
}
|
|
1372
1463
|
};
|
|
1373
1464
|
}
|
|
1465
|
+
/** 订阅单次事件:触发一次后自动取消订阅,返回取消订阅函数。 */
|
|
1374
1466
|
once(event, fn) {
|
|
1375
1467
|
const off = this.on(event, (...args) => {
|
|
1376
1468
|
off();
|
|
@@ -1378,6 +1470,7 @@ var GanttEngine = class {
|
|
|
1378
1470
|
});
|
|
1379
1471
|
return off;
|
|
1380
1472
|
}
|
|
1473
|
+
/** 取消订阅;不传 fn 时移除该事件的全部监听器。 */
|
|
1381
1474
|
off(event, fn) {
|
|
1382
1475
|
const set = this.listeners.get(event);
|
|
1383
1476
|
if (!set) return;
|
|
@@ -1388,6 +1481,7 @@ var GanttEngine = class {
|
|
|
1388
1481
|
this.listeners.delete(event);
|
|
1389
1482
|
}
|
|
1390
1483
|
}
|
|
1484
|
+
/** 触发事件,逐个调用监听器;单个监听器抛错不影响其余监听器。 */
|
|
1391
1485
|
emit(event, ...args) {
|
|
1392
1486
|
const set = this.listeners.get(event);
|
|
1393
1487
|
if (!set) return;
|
|
@@ -1402,6 +1496,7 @@ var GanttEngine = class {
|
|
|
1402
1496
|
}
|
|
1403
1497
|
}
|
|
1404
1498
|
// ── 生命周期 ──
|
|
1499
|
+
/** 销毁引擎:置位 destroyed、触发 destroy 事件并清空监听器与数据。 */
|
|
1405
1500
|
destroy() {
|
|
1406
1501
|
if (this.destroyed) return;
|
|
1407
1502
|
this.destroyed = true;
|
|
@@ -1410,6 +1505,7 @@ var GanttEngine = class {
|
|
|
1410
1505
|
this.tasks = [];
|
|
1411
1506
|
this.links = [];
|
|
1412
1507
|
}
|
|
1508
|
+
/** 断言引擎未被销毁,否则抛出异常(防止对已销毁实例操作)。 */
|
|
1413
1509
|
assertNotDestroyed() {
|
|
1414
1510
|
if (this.destroyed) {
|
|
1415
1511
|
throw new Error("GanttEngine: operation on destroyed instance");
|
|
@@ -1459,6 +1555,7 @@ function createGanttEngine(options = {}) {
|
|
|
1459
1555
|
mergeTaskPatch,
|
|
1460
1556
|
normalizeLinks,
|
|
1461
1557
|
plannedDurationBetween,
|
|
1558
|
+
resolveColumnWidth,
|
|
1462
1559
|
resolveGanttConfig,
|
|
1463
1560
|
resolveResourceColor,
|
|
1464
1561
|
resolveUsageColor,
|