欢迎光临
我们一直在努力

从零构建Angular甘特图组件:SVG渲染与交互设计实战

1. 为什么我们要从零开始造一个甘特图轮子?

如果你正在为一个复杂的项目管理后台寻找一个完美的甘特图组件,那你很可能和我有过同样的经历:在开源世界里逛了一圈,发现要么是功能太臃肿,引入了一堆用不上的依赖;要么是定制性太差,想改个时间轴样式或者加个自定义交互,简直比登天还难。我之前在项目里用过 dhtmlx-gantt,它确实能跑起来,但一到要深度定制,比如把时间轴从“年-月-日”改成“季度-周”,或者想把任务条的拖拽逻辑完全重写,就发现处处受制,文档也语焉不详,最后只能向需求妥协。

这种“受制于人”的感觉,让我下决心自己动手。从零构建一个 Angular 甘特图组件,听起来是个大工程,但实际拆解下来,核心就是两件事:用 SVG 把图画出来,以及设计一套流畅的交互逻辑。自己掌控源码的好处是显而易见的:性能优化可以做到极致,交互体验可以完全按产品经理的“奇葩”想法来,而且没有第三方依赖,打包体积小得感人。更重要的是,这个过程能让你对前端图形渲染和复杂状态管理有脱胎换骨的理解。这篇文章,我就把我踩过的坑和总结的经验,手把手分享给你。

2. 项目骨架搭建与核心布局设计

2.1 创建 Angular 组件与基础结构

万事开头难,我们先从创建一个干净的 Angular 组件开始。我习惯用 Angular CLI 快速生成,但这里我们更关注核心逻辑,所以直接看代码结构。我们的甘特图组件核心是一个容器,里面分成左右两大块:左边是任务表格,右边是时间轴和进度条区域。

// gantt.component.ts
import { Component, OnInit, AfterViewInit, ElementRef, ViewChild, HostListener } from \’@angular/core\’;

@Component({
selector: \’app-gantt\’,
templateUrl: \’./gantt.component.html\’,
styleUrls: [\’./gantt.component.less\’] // 使用 Less 方便写嵌套样式
})
export class GanttComponent implements OnInit, AfterViewInit {
// 获取左右两个可滚动区域的 DOM 引用
@ViewChild(\’tableContainer\’) tableContainer!: ElementRef<HTMLDivElement>;
@ViewChild(\’chartContainer\’) chartContainer!: ElementRef<HTMLDivElement>;

// 组件配置项,后续会扩展成完整的配置对象
public config = {
data: [], // 任务数据
startDate: new Date(),
endDate: new Date(),
// … 其他配置
};

constructor() { }

ngOnInit(): void {
// 初始化数据
this.initializeData();
}

ngAfterViewInit(): void {
// DOM 渲染完成后,绑定滚动同步事件
this.bindScrollSync();
}

private initializeData(): void {
// 这里初始化任务数据和时间轴数据
}

private bindScrollSync(): void {
// 实现左右区域滚动同步的核心逻辑
}
}

对应的模板文件 gantt.component.html 是布局的关键。我采用 Flex 布局来实现左右分栏,并且确保右侧区域能够横向滚动,而左右两侧的内容区域可以同步垂直滚动。

<!– gantt.component.html –>
<div class=\”gantt-container\”>
<!– 左侧任务表格区域 –>
<div class=\”gantt-table-area\” #tableContainer>
<div class=\”table-header\”>
<!– 表格标题行,固定不动 –>
<div class=\”header-row\” *ngFor=\”let col of tableColumns\”>
{

{ col.header }}
</div>
</div>
<div class=\”table-body\”>
<!– 表格数据行,可垂直滚动 –>
<div class=\”data-row\” *ngFor=\”let task of config.data\”>
<div class=\”cell\” *ngFor=\”let col of tableColumns\”>
{

{ task[col.field] }}
</div>
</div>
</div>
</div>

<!– 右侧图表区域 –>
<div class=\”gantt-chart-area\” #chartContainer>
<div class=\”timeline-header\”>
<!– 时间轴头部,固定不动 –>
</div>
<div class=\”chart-body\”>
<!– SVG 画布,用于绘制时间轴和任务条 –>
<svg class=\”gantt-svg\” [attr.width]=\”timelineWidth\” [attr.height]=\”chartHeight\”>
<!– 这里会动态生成时间轴网格和任务条 –>
</svg>
</div>
</div>
</div>

2.2 实现双向滚动同步的“锁”机制

甘特图一个核心体验是,当你在左侧表格上下滚动查看不同任务时,右侧对应的任务条区域必须同步滚动,反之亦然。但这里有个陷阱:直接互相监听滚动事件会形成死循环,导致页面抖动甚至卡死。我采用的解决方案是引入一个“滚动锁”标志位。

原理很简单:当左侧表格触发滚动时,我们立刻将一个标志位 isTableScrolling 设为 true,然后去设置右侧区域的 scrollTop。在设置之前,检查右侧是否正在由用户触发滚动(即 isChartScrolling 是否为 true),如果不是,则安全地设置其滚动位置。设置完成后,立刻将 isTableScrolling 复位。右侧滚动监听左侧的逻辑完全对称。这样就避免了事件的无限循环。

// 在组件类中
private scrollLock = {
isTableScrolling: false,
isChartScrolling: false
};

private bindScrollSync(): void {
const tableEl = this.tableContainer.nativeElement;
const chartEl = this.chartContainer.nativeElement;

// 监听左侧表格滚动
tableEl.addEventListener(\’scroll\’, (event: Event) => {
this.onTableScroll(event);
});

// 监听右侧图表滚动
chartEl.addEventListener(\’scroll\’, (event: Event) => {
this.onChartScroll(event);
});
}

private onTableScroll(event: Event): void {
// 如果右侧正在滚动,则忽略此次事件,防止循环
if (this.scrollLock.isChartScrolling) {
return;
}
// 标记左侧正在触发滚动
this.scrollLock.isTableScrolling = true;
const target = event.target as HTMLDivElement;
// 同步右侧滚动条位置
this.chartContainer.nativeElement.scrollTop = target.scrollTop;
// 操作完成后立即释放锁
setTimeout(() => {
this.scrollLock.isTableScrolling = false;
}, 0);
}

private onChartScroll(event: Event): void {
// 如果左侧正在滚动,则忽略此次事件
if (this.scrollLock.isTableScrolling) {
return;
}
this.scrollLock.isChartScrolling = true;
const target = event.target as HTMLDivElement;
this.tableContainer.nativeElement.scrollTop = target.scrollTop;
setTimeout(() => {
this.scrollLock.isChartScrolling = false;
}, 0);
}

这里用 setTimeout 将锁的释放放到下一个事件循环,是一个很实用的小技巧,能确保即使在快速连续滚动时,锁机制也能正确工作。对应的样式也需要精心调整,特别是要隐藏左侧表格的滚动条,让用户感觉只有一个统一的滚动条在控制。

// gantt.component.less
.gantt-container {
display: flex;
height: 600px; // 固定高度,内部滚动
border: 1px solid #e8e8e8;
overflow: hidden; // 隐藏容器自身的滚动条

.gantt-table-area {
width: 300px; // 左侧表格固定宽度
border-right: 1px solid #e8e8e8;
display: flex;
flex-direction: column;
.table-header {
height: 40px;
background: #fafafa;
border-bottom: 1px solid #e8e8e8;
flex-shrink: 0; // 防止被压缩
}
.table-body {
flex: 1;
overflow-y: auto; // 只有这里产生垂直滚动条
&::-webkit-scrollbar {
display: none; // 隐藏滚动条,视觉上统一用右侧的
}
}
}

.gantt-chart-area {
flex: 1;
display:

赞(0)
未经允许不得转载:171主机测评 » 从零构建Angular甘特图组件:SVG渲染与交互设计实战
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址