admin管理员组文章数量:1794759
前端甘特图组件开发(一)
背景
- 工作中需要在网页上实现甘特图,以展示进度数据。通过网上调研相关项目,找到一款 dhtmlx-gantt 组件,在低程度上满足项目需求,但在部分定制功能(如时间轴自定义、编辑弹窗样式风格等)并不能完全满足项目需求。此外,使用此类开源项目,若遇到功能无法满足需求时,解决起来较为麻烦,基本只有在需求上进行妥协。
- 个人在工作后暂时没有开发过相对复杂且功能较为完整的组件,开发甘特图组件既可以满足工作需要、方便开发人员,也可以加深自己对前端技术的理解。 基于以上原因,开始着手开发一款甘特图组件 m-gantt,第一版首先以完成项目需求为目标,实现项目需要的功能,尽可能将配置项进行提取。后续将继续完善拓展应有功能,实现可配置化。
- 本甘特图开发的基本思路源于这两篇文章: 【参考链接1】 【参考链接2】
- 调研含 dhtmlx-gantt 在内的多款甘特图组件,了解甘特图组件所需要的基本功能以及数据的在展示方法、交互方法等。
- 本甘特图组件目前仅支持 Angular 开发
- 除 Angular 框架外,本组件无其他依赖包
- 甘特图基于svg绘画,不依赖其他工具,可塑性强,且相较于使用标签加定位的布局方式,该方法代码量较少且逻辑清晰
- 样式使用less语法
基本思路
① 主要分为左右两个部分,每个部分分上部固定区域(吸顶)和下部垂直滚动区域 ② 左右部分的下部区域需要同时滚动(共用滚动条) ③ 右部需要横向滚动
<div class="gantt-table" #table> <div class="header"></div> <div class="body"></div> </div> <div class="gantt-chart" #chart> <div class="header"></div> <div class="body"></div> </div> .gantt-container { height: 800px; display: flex; // 使用flex布局 overflow: hidden; .gantt-table, .gantt-chart { .header { position: sticky; height: @headHeight; top: 0; } .body { height: 900px; } } // 左侧表格 .gantt-table { position: relative; overflow-x: hidden; overflow-y: scroll; } // 隐藏左侧滚动条 .gantt-table::-webkit-scrollbar { width: 0; } // 右侧进度图 .gantt-chart { overflow-x: scroll; flex: 1; } } @ViewChild('table') table: any; @ViewChild('chart') chart: any; public scrollLock = { isTableScroll: false, isChartScroll: false } ngAfterViewInit(): void { // 监听左侧表格 this.table.nativeElement.addEventListener('scroll', this.scrollChart); // 监听右侧表格 this.chart.nativeElement.addEventListener('scroll', this.scrollTable); } private scrollChart = (e: any) => { // 当右侧进度图没有滚动时,使之随表格滚动 if (!this.scrollLock.isChartScroll) { this.scrollLock.isTableScroll = true; this.chart.nativeElement.scroll({ top: e.target?.scrollTop }) } this.scrollLock.isTableScroll = false; } private scrollTable = (e: any) => { // 当左侧表格没有滚动时,使之随进度图滚动 if (!this.scrollLock.isTableScroll) { this.scrollLock.isChartScroll = true; this.table.nativeElement.scroll({ top: e.target?.scrollTop }) } this.scrollLock.isChartScroll = false; } ngOnDestroy(): void { this.table.nativeElement.removeEventListener('scroll', this.scrollChart); this.chart.nativeElement.removeEventListener('scroll', this.scrollTable); } SVG本甘特图使用svg语法绘制,主要用到以下几种常用标签
- react 矩形标签
- x: 左侧距离
- y: 顶部距离
- width: 宽度
- height: 高度
- rx: x轴半径
- rx: y轴半径
- path 路径标签(eg: M 100 0 V 100)
- M: move to 传入目标点的坐标 x y
- H: horizontal lineto 平行线
- V: vertical lineto 垂直线
- line 线标签
- x1 y1: 第一个点的坐标
- x2 y2: 第二个点的坐标
- text 文本标签
- g 组合标签
- 添加到g上的变化会应用到其子元素
更加详细的SVG图知识可以参考另一篇文章【svg学习】
时间轴① 计算时间轴的长度 ② 构造时间数组 ③ 通过位置绘制时间轴
// 时间轴 public dateConfig: any = { startDate: new Date('2077-12-31'), endDate: new Date('1999-1-1'), total: 0, // 总天数 svgWidth: 0, // 整体宽度 svgHeight: 60, // 时间轴高度 dateList: [], // 日轴 monthList: [] // 月轴 } // 配置时间轴数据 private setGanttData(): void { // 遍历任务数据 获取最大/最小值 this.ganttConfig.data.forEach((task: any) => { const { startDate, endDate } = task; if (startDate && new Date(startDate) < this.dateConfig.startDate) { this.dateConfig.startDate = new Date(startDate) } if (endDate && new Date(endDate) > this.dateConfig.endDate) { this.dateConfig.endDate = new Date(endDate); } }) // 前后加N天保证显示效果 this.dateConfig.endDate = new Date(this.dateConfig.endDate.getTime() + 3 * 24 * 60 * 60 * 1000); this.dateConfig.startDate = new Date(this.dateConfig.startDate.getTime() - 3 * 24 * 60 * 60 * 1000); this.dateConfig.total = (this.dateConfig.endDate.getTime() - this.dateConfig.startDate.getTime()) / (24 * 60 * 60 * 1000); // 计算总宽度 this.dateConfig.svgWidth = this.dateConfig.total * this.squareWidth; // 时间轴 // 日 const week = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; for (let i = 0; i < this.dateConfig.total; i++) { this.dateConfig.dateList.push({ text: this.datePipe.transform(new Date(this.dateConfig.startDate.getTime() + i * 24 * 60 * 60 * 1000), 'dd'), day: week[new Date(this.dateConfig.startDate.getTime() + i * 24 * 60 * 60 * 1000).getDay()], month: this.datePipe.transform(new Date(this.dateConfig.startDate.getTime() + i * 24 * 60 * 60 * 1000), 'yyyy-MM'), }) } // 月 const monthMap = new Map(); this.dateConfig.dateList.forEach((date: any) => { const month = date.month; if (monthMap.has(month)) { monthMap.set(month, monthMap.get(month) + 1) } else { monthMap.set(month, 1) } }) let lengthBefore: number = 0; monthMap.forEach((value, key) => { this.dateConfig.monthList.push({ text: key, left: lengthBefore }) lengthBefore += value; }) } <!-- 时间轴 --> <div class="header" [style.width]="dateConfig.svgWidth + 'px'"> <!-- 月数据 --> <svg [attr.width]="dateConfig.svgWidth" [attr.height]="timeLineHeight"> <g class="date" *ngFor="let month of dateConfig.monthList; let i = index;"> <!-- 文字 --> <text [attr.x]="month.left * squareWidth + 5" [attr.y]="timeLineHeight / 2 + 4" style="font-size: 12px;">{{month.text}}</text> <!-- 时间轴边框 --> <path [attr.d]="'M ' + month.left * squareWidth + ' 0 V 30'" stroke="#d9dde0"></path> <line x1="0" y1="30" [attr.x2]="dateConfig.svgWidth" y2="30" stroke="#d9dde0" /> </g> </svg> <!-- 日数据 --> <svg [attr.width]="dateConfig.svgWidth" [attr.height]="timeLineHeight"> <g class="date" *ngFor="let date of dateConfig.dateList; let i = index;"> <text [attr.x]="i * squareWidth + 5" [attr.y]="timeLineHeight / 2 + 4" style="font-size: 12px;">{{date.text}}</text> <text [attr.x]="i * squareWidth + 20" [attr.y]="timeLineHeight / 2 + 4" style="font-size: 8px;">{{date.day}}</text> <path [attr.d]="'M ' + i * squareWidth + ' 0 V 30'" stroke="#d9dde0"></path> </g> </svg> </div> 进度图- 背景绘制 ① 用 react 绘制格子 ② 用 line 绘制横线 ③ 用 path 绘制竖线
- 进度图 bar 绘制
① 用 rect 绘制每项任务的总计划 bar ② 用 rect 绘制每项任务的已完成 bar ③ 用 text 填充文字
<g class="bar" *ngFor="let row of ganttConfig.chartData; let i = index;" (mouseenter)="showDetail(row, true)" (mouseleave)="showDetail(row)"> <!-- 全部 --> <rect [id]="'bar_' + i" [attr.x]="row.startDay * squareWidth" [attr.y]="i * lineHeight + (lineHeight - barHeight) / 2" [attr.width]="row.duration * squareWidth" [attr.height]="barHeight" [attr.rx]="barHeight / 2" [attr.ry]="barHeight / 2" [attr.fill]="row.parentId ? subBarColor : barColor"></rect> <!-- 进度 --> <rect [attr.x]="row.startDay * squareWidth" [attr.y]="i * lineHeight + (lineHeight - barHeight) / 2" [attr.width]="(row.duration * squareWidth) * row.progress" [attr.height]="barHeight" [attr.rx]="barHeight / 2" [attr.ry]="barHeight / 2" [attr.fill]="row.parentId ? subProgressBarColor : progressBarColor"> </rect> <text [attr.x]="row.startDay * squareWidth + 20" [attr.y]="(i + 0.5) * lineHeight + 5" [attr.fill]="barFontColor" style="font-size: 12px;">{{row.name}}</text> </g> 点击滚动点击任务滚动到任务开始位置
// 点击任务自动滚动 public scrollToBar(row: any): void { const targetBar = document.querySelector(`#bar_${this.ganttConfig.chartData.indexOf(row)}`); if (targetBar && this.table) { // 目标进度条左侧与client距离 const x = targetBar.getBoundingClientRect().left; // table右侧与client距离 const parentX = this.table.nativeElement.getBoundingClientRect().right; const preScroll = this.chart.nativeElement.scrollLeft || 0; const diff = x - parentX; // 滚动 this.chart.nativeElement.scrollTo({ left: preScroll + diff, behavior: 'smooth' }) } } 显示详情鼠标移动到任务上显示任务详情
① 创建一个modal标签,设置基本样式,在里面放置需要展示的详情 ② 通过监听鼠标移动事件,将鼠标的位置传递给该元素,实现跟随鼠标移动 ③ 在鼠标进入 bar 时绑定,在鼠标移出 bar 时解绑
// 弹窗显示详情 @ViewChild('msgModal') msgModal: any; public showModal: boolean = false; public modalData: any = { name: '任务1', startDate: '2022-10-1', status: '进行中', progress: '' } public showDetail(row: any, flag = false): void { if (flag) { this.showModal = true; // 绑定数据 // ... document.addEventListener('mousemove', this.moveModal) } else { this.showModal = false } } private moveModal = (e: any) => { document.querySelector('#msg-modal')?.setAttribute('style', `top: ${e.clientY}px; left: ${e.clientX - 510}px`); }结构及样式代码略
树形表格树形表格
① 表格支持点击 icon 展开与折叠 ② 进度图的对应项根据表格的折叠与否决定是否显示 ③ 为了支持父子级关系及控制显示,任务数据需添加以下字段: a: id b: parentId (仅子级数据需要,关联父子关系) c: open (仅父级数据需要,控制是否展开状态,变换icon) d: show (控制是否显示)
// 表格展开 public showSubData(id: string): void { this.ganttConfig.data.forEach((item: any) => { if (item.id === id) { item.open = !item.open; } if (item.parentId === id) { item.show = !item.show; } }) this.ganttConfig.chartData = this.ganttConfig.data.filter((row: any) => { return row.show === true }) }以上,甘特图组件基本功能开发完成,后续工作: ① 完善其他实用功能 ② 修改已有问题 ③ 将数据、功能、样式封装为可配置项
【项目GitHub地址】⭐️
原文地址 【个人博客】⭐️
相关文章 【前端甘特图组件开发(二)】
版权声明:本文标题:前端甘特图组件开发(一) 内容由林淑君副主任自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.xiehuijuan.com/baike/1686500380a74463.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论