ct-gantt-core 1.0.4 → 1.0.6

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 ADDED
@@ -0,0 +1,83 @@
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)。
package/dist/index.cjs CHANGED
@@ -22,25 +22,37 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  DAY_MS: () => DAY_MS,
24
24
  GanttEngine: () => GanttEngine,
25
+ HOUR_MS: () => HOUR_MS,
26
+ RESOURCE_COLOR_PALETTE: () => RESOURCE_COLOR_PALETTE,
25
27
  addDays: () => addDays,
28
+ addHours: () => addHours,
26
29
  checkCyclicDependency: () => checkCyclicDependency,
30
+ computeHourScale: () => computeHourScale,
27
31
  computeImpact: () => computeImpact,
28
32
  computeLayout: () => computeLayout,
33
+ computeResourceLayout: () => computeResourceLayout,
29
34
  computeTimeScale: () => computeTimeScale,
30
35
  createGanttEngine: () => createGanttEngine,
31
36
  defaultConfig: () => defaultConfig,
32
37
  diffDays: () => diffDays,
38
+ diffHours: () => diffHours,
33
39
  flattenTasks: () => flattenTasks,
34
40
  formatDate: () => formatDate,
41
+ formatDateTime: () => formatDateTime,
42
+ hasTimeComponent: () => hasTimeComponent,
35
43
  inclusiveDays: () => inclusiveDays,
36
44
  isValidDate: () => isValidDate,
37
45
  mergeTaskPatch: () => mergeTaskPatch,
38
46
  normalizeLinks: () => normalizeLinks,
47
+ resolveResourceColor: () => resolveResourceColor,
48
+ resolveUsageColor: () => resolveUsageColor,
49
+ resolveUsageTimes: () => resolveUsageTimes,
39
50
  resultInvalidDate: () => resultInvalidDate,
40
51
  scheduleByDependencies: () => scheduleByDependencies,
41
52
  shiftTask: () => shiftTask,
42
53
  startOfDay: () => startOfDay,
43
- toDate: () => toDate
54
+ toDate: () => toDate,
55
+ toDateTime: () => toDateTime
44
56
  });
45
57
  module.exports = __toCommonJS(index_exports);
46
58
 
@@ -71,6 +83,8 @@ var defaultConfig = {
71
83
  plan: "#cbd5e1",
72
84
  progress: "#0f766e"
73
85
  },
86
+ timeUnit: "day",
87
+ hourWidth: 12,
74
88
  autoSchedule: true,
75
89
  editable: true
76
90
  };
@@ -211,6 +225,7 @@ function flattenTasks(tasks, collapsedIds = /* @__PURE__ */ new Set()) {
211
225
 
212
226
  // src/utils/date.ts
213
227
  var DAY_MS = 24 * 60 * 60 * 1e3;
228
+ var HOUR_MS = 60 * 60 * 1e3;
214
229
  function toDate(value) {
215
230
  if (value instanceof Date) {
216
231
  return startOfDay(value);
@@ -221,6 +236,21 @@ function toDate(value) {
221
236
  }
222
237
  return startOfDay(new Date(value));
223
238
  }
239
+ function hasTimeComponent(value) {
240
+ return typeof value === "string" && /T\d|:\d/.test(value);
241
+ }
242
+ function toDateTime(value) {
243
+ if (value instanceof Date) {
244
+ return new Date(value.getTime());
245
+ }
246
+ if (hasTimeComponent(value)) {
247
+ const date = new Date(value.replace(" ", "T"));
248
+ if (!Number.isNaN(date.getTime())) {
249
+ return date;
250
+ }
251
+ }
252
+ return toDate(value);
253
+ }
224
254
  function startOfDay(date) {
225
255
  return new Date(date.getFullYear(), date.getMonth(), date.getDate());
226
256
  }
@@ -232,6 +262,12 @@ function addDays(date, days) {
232
262
  function diffDays(start, end) {
233
263
  return Math.round((toDate(end).getTime() - toDate(start).getTime()) / DAY_MS);
234
264
  }
265
+ function addHours(date, hours) {
266
+ return new Date(toDateTime(date).getTime() + hours * HOUR_MS);
267
+ }
268
+ function diffHours(start, end) {
269
+ return (toDateTime(end).getTime() - toDateTime(start).getTime()) / HOUR_MS;
270
+ }
235
271
  function inclusiveDays(start, end) {
236
272
  return Math.max(0, diffDays(start, end) + 1);
237
273
  }
@@ -246,6 +282,12 @@ function formatDate(date) {
246
282
  const day = String(value.getDate()).padStart(2, "0");
247
283
  return `${year}-${month}-${day}`;
248
284
  }
285
+ function formatDateTime(date) {
286
+ const value = toDateTime(date);
287
+ const hours = String(value.getHours()).padStart(2, "0");
288
+ const minutes = String(value.getMinutes()).padStart(2, "0");
289
+ return `${formatDate(value)} ${hours}:${minutes}`;
290
+ }
249
291
  function resultInvalidDate(message, details) {
250
292
  return {
251
293
  ok: false,
@@ -309,6 +351,34 @@ function nextUnit(date, viewMode) {
309
351
  return addDays(date, 1);
310
352
  }
311
353
 
354
+ // src/services/computeHourScale.ts
355
+ function computeHourScale(start, end, hourWidth) {
356
+ const rangeStart = startOfDay(start);
357
+ const rangeEnd = startOfDay(end);
358
+ const totalDays = Math.max(1, inclusiveDays(rangeStart, rangeEnd));
359
+ const days = [];
360
+ const hours = [];
361
+ for (let d = 0; d < totalDays; d += 1) {
362
+ const dayStart = addDays(rangeStart, d);
363
+ days.push({
364
+ start: dayStart,
365
+ end: addHours(dayStart, 24),
366
+ left: d * 24 * hourWidth,
367
+ width: 24 * hourWidth
368
+ });
369
+ for (let h = 0; h < 24; h += 1) {
370
+ const hourStart = addHours(dayStart, h);
371
+ hours.push({
372
+ start: hourStart,
373
+ end: addHours(hourStart, 1),
374
+ left: (d * 24 + h) * hourWidth,
375
+ width: hourWidth
376
+ });
377
+ }
378
+ }
379
+ return { days, hours, start: rangeStart, end: rangeEnd };
380
+ }
381
+
312
382
  // src/engines/scheduling.ts
313
383
  function scheduleByDependencies(tasks, links) {
314
384
  const cycle = checkCyclicDependency(links);
@@ -550,6 +620,153 @@ function computeImpact(taskId, patch, tasks, links, config) {
550
620
  return { ok: true, data: { changed, conflicts } };
551
621
  }
552
622
 
623
+ // src/engines/computeResourceLayout.ts
624
+ var RESOURCE_COLOR_PALETTE = ["#d85f87", "#9b63d9", "#47b987", "#d58a5f", "#4f83d8"];
625
+ function colorHash(source) {
626
+ return [...source].reduce((sum, char) => sum + (char.codePointAt(0) ?? 0), 0);
627
+ }
628
+ function resolveResourceColor(resource, resourceColors) {
629
+ if (resource.color) {
630
+ return resource.color;
631
+ }
632
+ if (resourceColors?.[resource.id]) {
633
+ return resourceColors[resource.id];
634
+ }
635
+ const source = resource.name || resource.id;
636
+ return RESOURCE_COLOR_PALETTE[colorHash(source) % RESOURCE_COLOR_PALETTE.length];
637
+ }
638
+ function resolveUsageColor(usage, resource, resourceColors) {
639
+ return usage.color || resolveResourceColor(resource, resourceColors);
640
+ }
641
+ function resolveUsageTimes(usage, timeUnit) {
642
+ if (timeUnit === "hour") {
643
+ const start = toDateTime(usage.start);
644
+ const endRaw = toDateTime(usage.end);
645
+ return { start, end: hasTimeComponent(usage.end) ? endRaw : addHours(endRaw, 24) };
646
+ }
647
+ return { start: toDate(usage.start), end: toDate(usage.end) };
648
+ }
649
+ function computeResourceLayout(resources, usages, config, viewport) {
650
+ const mergedConfig = { ...defaultConfig, ...config };
651
+ const timeUnit = mergedConfig.timeUnit ?? "day";
652
+ const hourMode = timeUnit === "hour";
653
+ const hourW = mergedConfig.hourWidth ?? 12;
654
+ for (const usage of usages) {
655
+ const { start, end } = resolveUsageTimes(usage, timeUnit);
656
+ if (!isValidDate(start) || !isValidDate(end)) {
657
+ return {
658
+ ok: false,
659
+ error: {
660
+ code: "INVALID_DATE",
661
+ message: `Resource usage ${usage.id} has invalid dates.`
662
+ }
663
+ };
664
+ }
665
+ }
666
+ const orderedIds = [];
667
+ const seen = /* @__PURE__ */ new Set();
668
+ for (const resource of resources) {
669
+ if (!seen.has(resource.id)) {
670
+ seen.add(resource.id);
671
+ orderedIds.push(resource.id);
672
+ }
673
+ }
674
+ for (const usage of usages) {
675
+ if (!seen.has(usage.resourceId)) {
676
+ seen.add(usage.resourceId);
677
+ orderedIds.push(usage.resourceId);
678
+ }
679
+ }
680
+ const resourceById = new Map(resources.map((resource) => [resource.id, resource]));
681
+ const usagesByResource = /* @__PURE__ */ new Map();
682
+ for (const usage of usages) {
683
+ usagesByResource.set(usage.resourceId, [...usagesByResource.get(usage.resourceId) ?? [], usage]);
684
+ }
685
+ const rows = [];
686
+ const usageLayouts = [];
687
+ const laneIndexByUsage = /* @__PURE__ */ new Map();
688
+ orderedIds.forEach((resourceId, rowIndex) => {
689
+ const resource = resourceById.get(resourceId) ?? {
690
+ id: resourceId,
691
+ name: resourceId
692
+ };
693
+ const grouped = [...usagesByResource.get(resourceId) ?? []].sort((a, b) => {
694
+ const as = resolveUsageTimes(a, timeUnit);
695
+ const bs = resolveUsageTimes(b, timeUnit);
696
+ return as.start.getTime() - bs.start.getTime() || as.end.getTime() - bs.end.getTime();
697
+ });
698
+ const laneEnds = [];
699
+ for (const usage of grouped) {
700
+ const { start, end } = resolveUsageTimes(usage, timeUnit);
701
+ let laneIndex = -1;
702
+ for (let k = 0; k < laneEnds.length; k += 1) {
703
+ const laneEnd = laneEnds[k];
704
+ if (laneEnd === null || isLaneFree(laneEnd, start, hourMode)) {
705
+ laneIndex = k;
706
+ break;
707
+ }
708
+ }
709
+ if (laneIndex === -1) {
710
+ laneIndex = laneEnds.length;
711
+ laneEnds.push(null);
712
+ }
713
+ laneEnds[laneIndex] = end;
714
+ laneIndexByUsage.set(usage.id, laneIndex);
715
+ }
716
+ rows.push({
717
+ resourceId,
718
+ rowIndex,
719
+ laneCount: laneEnds.length,
720
+ color: resolveResourceColor(resource, mergedConfig.resourceColors),
721
+ usageIds: grouped.map((usage) => usage.id)
722
+ });
723
+ });
724
+ const usageById = new Map(usages.map((usage) => [usage.id, usage]));
725
+ const visibleStart = viewport ? Math.max(0, Math.floor(viewport.scrollTop / mergedConfig.rowHeight) - 8) : 0;
726
+ const visibleEnd = viewport ? Math.ceil((viewport.scrollTop + viewport.clientHeight) / mergedConfig.rowHeight) + 8 : Number.POSITIVE_INFINITY;
727
+ const firstDate = hourMode ? startOfDay(
728
+ mergedConfig.visibleRange ? toDateTime(mergedConfig.visibleRange.start) : usages.length ? minDate2(usages.map((usage) => resolveUsageTimes(usage, "hour").start)) : toDateTime(/* @__PURE__ */ new Date())
729
+ ) : mergedConfig.visibleRange ? toDate(mergedConfig.visibleRange.start) : usages.length ? minDate2(usages.map((usage) => toDate(usage.start))) : toDate(/* @__PURE__ */ new Date());
730
+ for (const row of rows) {
731
+ if (row.rowIndex < visibleStart || row.rowIndex > visibleEnd) {
732
+ continue;
733
+ }
734
+ const resource = resourceById.get(row.resourceId) ?? { id: row.resourceId, name: row.resourceId };
735
+ const laneCount = Math.max(1, row.laneCount);
736
+ const slot = mergedConfig.rowHeight / laneCount;
737
+ const barHeight = Math.max(6, Math.min(14, slot - 4));
738
+ for (const usageId of row.usageIds) {
739
+ const usage = usageById.get(usageId);
740
+ if (!usage) {
741
+ continue;
742
+ }
743
+ const { start, end } = resolveUsageTimes(usage, timeUnit);
744
+ const laneIndex = laneIndexByUsage.get(usageId) ?? 0;
745
+ const laneTop = laneIndex * slot + (slot - barHeight) / 2;
746
+ const left = hourMode ? diffHours(firstDate, start) * hourW : diffDays(firstDate, start) * mergedConfig.columnWidth;
747
+ const width = hourMode ? Math.max(hourW, diffHours(start, end) * hourW) : Math.max(mergedConfig.columnWidth, inclusiveDays(start, end) * mergedConfig.columnWidth);
748
+ usageLayouts.push({
749
+ usageId,
750
+ resourceId: row.resourceId,
751
+ rowIndex: row.rowIndex,
752
+ laneIndex,
753
+ laneCount,
754
+ left,
755
+ width,
756
+ top: mergedConfig.headerHeight + row.rowIndex * mergedConfig.rowHeight + laneTop,
757
+ height: barHeight
758
+ });
759
+ }
760
+ }
761
+ return { ok: true, data: { rows, usages: usageLayouts } };
762
+ }
763
+ function isLaneFree(laneEnd, start, hourMode) {
764
+ return hourMode ? diffHours(laneEnd, start) >= 0 : diffDays(laneEnd, start) > 0;
765
+ }
766
+ function minDate2(dates) {
767
+ return new Date(Math.min(...dates.map((date) => date.getTime())));
768
+ }
769
+
553
770
  // src/engine/GanttEngine.ts
554
771
  function mergeTaskPatch(task, patch) {
555
772
  return {
@@ -920,24 +1137,36 @@ function createGanttEngine(options = {}) {
920
1137
  0 && (module.exports = {
921
1138
  DAY_MS,
922
1139
  GanttEngine,
1140
+ HOUR_MS,
1141
+ RESOURCE_COLOR_PALETTE,
923
1142
  addDays,
1143
+ addHours,
924
1144
  checkCyclicDependency,
1145
+ computeHourScale,
925
1146
  computeImpact,
926
1147
  computeLayout,
1148
+ computeResourceLayout,
927
1149
  computeTimeScale,
928
1150
  createGanttEngine,
929
1151
  defaultConfig,
930
1152
  diffDays,
1153
+ diffHours,
931
1154
  flattenTasks,
932
1155
  formatDate,
1156
+ formatDateTime,
1157
+ hasTimeComponent,
933
1158
  inclusiveDays,
934
1159
  isValidDate,
935
1160
  mergeTaskPatch,
936
1161
  normalizeLinks,
1162
+ resolveResourceColor,
1163
+ resolveUsageColor,
1164
+ resolveUsageTimes,
937
1165
  resultInvalidDate,
938
1166
  scheduleByDependencies,
939
1167
  shiftTask,
940
1168
  startOfDay,
941
- toDate
1169
+ toDate,
1170
+ toDateTime
942
1171
  });
943
1172
  //# sourceMappingURL=index.cjs.map