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 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
@@ -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 rows = new Map(people.map((person) => [person.id, createPerson(person, dates)]));
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
- 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]);
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, members]) => aggregateDepartment(departmentMap.get(id) ?? { id, name: id }, members, dates));
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 dates = task.calendarId === "delivery" ? allDates : allDates.filter((date) => {
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,