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 CHANGED
@@ -1,83 +1,98 @@
1
- # ct-gantt-core
2
-
3
- 框架无关的甘特图数据、布局、依赖排程和命令式引擎。该包不包含界面,可用于 Vue、React、原生 JavaScript、服务端计算或自定义渲染器。
4
-
5
- ## 安装
6
-
7
- ```bash
8
- pnpm add ct-gantt-core
9
- ```
10
-
11
- 也可以使用 `npm install ct-gantt-core` 或 `yarn add ct-gantt-core`。
12
-
13
- ## 最小示例
14
-
15
- ```ts
16
- import { GanttEngine, type GanttTask } from "ct-gantt-core"
17
-
18
- const tasks: GanttTask[] = [
19
- {
20
- id: "task-1",
21
- name: "需求分析",
22
- type: "task",
23
- plan: { start: "2026-07-01", end: "2026-07-05" },
24
- actual: { start: "2026-07-01", end: "2026-07-06", progress: 60 }
25
- }
26
- ]
27
-
28
- const engine = new GanttEngine({
29
- tasks,
30
- config: { viewMode: "day", columnWidth: 30 }
31
- })
32
-
33
- const layout = engine.getLayout()
34
- if (layout.ok) {
35
- console.log(layout.data)
36
- }
37
-
38
- engine.setTask("task-1", { progress: 80 })
39
- engine.destroy()
40
- ```
41
-
42
- ## 依赖排程
43
-
44
- ```ts
45
- import { scheduleByDependencies, type GanttLink } from "ct-gantt-core"
46
-
47
- const links: GanttLink[] = [
48
- { id: "design-to-dev", sourceId: "design", targetId: "dev", type: "FS" }
49
- ]
50
-
51
- const result = scheduleByDependencies(tasks, links)
52
- if (result.ok) {
53
- console.log(result.data)
54
- }
55
- ```
56
-
57
- 支持 `FS`、`SS`、`FF`、`SF` 四种依赖类型,以及自然日或工作日间隔。
58
-
59
- ## 主要导出
60
-
61
- | API | 用途 |
62
- | --- | --- |
63
- | `GanttEngine` | 管理任务、依赖、折叠、预览、布局和视口命令 |
64
- | `computeLayout` | 计算任务条位置和尺寸 |
65
- | `computeTimeScale` | 计算时间刻度(day/week/month/quarter/year) |
66
- | `computeHourScale` | 计算小时级时间刻度(每天 24 个小时列,顶行日期 + 底部整点小时) |
67
- | `computeResourceLayout` | 资源视图布局:资源行 + 占用色条(支持 day/hour 粒度与车道堆叠) |
68
- | `resolveUsageTimes` | 将资源占用解析为布局用起止时间(hour 粒度下纯日期占用含结束日整天) |
69
- | `scheduleByDependencies` | 按依赖关系调整任务日期 |
70
- | `computeImpact` | 分析任务变更的影响和约束冲突 |
71
- | `checkCyclicDependency` | 检查循环依赖 |
72
- | `normalizeLinks` | 统一任务内依赖与独立依赖数据 |
73
- | `flattenTasks` | 将阶段树转换为可渲染行 |
74
- | `toDate`、`toDateTime`、`addDays`、`addHours`、`diffDays`、`diffHours`、`formatDate`、`formatDateTime` | 日期工具(`toDateTime`/`diffHours` 保留时间,用于小时级计算) |
75
-
76
- ## 数据说明
77
-
78
- - `plan` 表示计划日期。
79
- - `actual` 表示实际日期和完成进度。
80
- - `summary` 表示阶段,`task` 表示普通任务,`milestone` 表示任务型里程碑。
81
- - Core 不负责绘制界面;需要现成 Vue 界面时请安装 `ct-gantt-vue`。
82
-
83
- 完整类型、配置和示例请查看[项目文档](https://github.com/moonlight-219/ganttu#readme)。
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
- "#79bbff",
113
- "#909399"
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 === "delivery") return inclusiveDays(first, last);
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 rows = new Map(people.map((person) => [person.id, createPerson(person, dates)]));
999
+ const parentIds = /* @__PURE__ */ new Set();
985
1000
  for (const task of tasks) {
986
- if (task.type === "summary" || task.type === "milestone") continue;
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
- if (row.person.departmentId) {
1004
- const members = grouped.get(row.person.departmentId);
1005
- if (members) members.push(row);
1006
- else grouped.set(row.person.departmentId, [row]);
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, members]) => aggregateDepartment(departmentMap.get(id) ?? { id, name: id }, members, dates));
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 total = task.workload ?? (task.duration ?? inclusiveDays(task.plan.start, task.plan.end)) * 8;
1027
- const allDates = datesBetween(task.plan.start, task.plan.end);
1028
- const dates = task.calendarId === "delivery" ? allDates : allDates.filter((date) => {
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,