先看效果
左侧为任务列表,是树形结构。右侧是渲染的甘特图,可以用鼠标滚轮滚动来缩放时间线宽度。
实现思路
树形列表维护一个AST,类似以下结构:
declare interface TaskNode {
name: string, //任务名称
start_time: string, //假定日期都是以xxxx-xx-xxTxx:xx:xx格式
end_time: string, //假定日期都是以xxxx-xx-xxTxx:xx:xx格式
list_order: number,
par_id?: string,
children?: TaskNode[], //子任务列表
color?: string, //颜色
expanded?: boolean, //是否展开
}
然后就可以通过解析该AST渲染出甘特图,每当列表数据发生修改、删除、增加操作时立即再次渲染保证左右同步。
主要难点
1. 需要知道甘特图的显示算法,摸清它的规律。
2. 如何实现鼠标滚轮滚动时,甘特图相应缩放?
3. 如何渲染任务块?
甘特图的显示规律:从上往下,从左往右,越早的任务在越左上的位置,越晚的任务在越偏右下的位置,子任务呈阶梯状在父任务下方显示。来看project的甘特图
project 甘特图
当然本文没有实现类似的箭头效果
如何实现鼠标滚轮滚动时,甘特图相应缩放?
需要对dom的了解比较好,目前浏览器支持对DOM的鼠标滚轮事件
handleMouseWheel(e : any){
e.preventDefault();
// console.log(e);
if(e.deltaY < 0){
// 向上滚动
}else{
// 向下滚动
}
},
我实现的甘特图主要是有两个部分,一个是时间线,二是任务块。
如果保证时间线和任务块缩放同步,使得甘特图收放自如,需要一个统一的"指挥"。
scaleX: 1, //当前缩放倍率
dayColDomWidth: 1200, // "天"列宽
hourColDomWidth: 50, //"时"列宽
taskBlockHeight: 40, //"任务块"默认高度
鼠标滚轮事件改变的只是scaleX,就这么简单。通过获取dom动态修改宽度即可,任务块的修改稍有不同
handleScale(scaleX){
// 时间线
var doms = document.getElementsByClassName('day-col');
// console.log(doms);
for(var i=0;i<doms.length;i++){
const ele : any = doms.item(i);
ele.style.width = scaleX * this.dayColDomWidth + 'px';
var hourDoms = ele.getElementsByClassName('hour-col');
for(var j=0;j<hourDoms.length;j++){
const hourEle : any = hourDoms.item(j);
hourEle.style.width = scaleX * this.hourColDomWidth + 'px';
}
}
// 任务块
var doms = document.getElementsByClassName('task-block');
for(var i=0;i<doms.length;i++){
const ele : any = doms.item(i);
// 任务块宽度的修改 稍有不同
ele.style.left = parseFloat(ele.dataset.left) * scaleX + 'px';
ele.style.width = parseFloat(ele.dataset.width) * scaleX + 'px';
}
},
由于JS存在浮点数计算误差的问题,缩放过程会让甘特图产生一定的偏差,当然,这非常小。
任务块不像"天"列和"小时"列有在scaleX为1时的固定宽度,它本身的宽度是不固定的,所以它的缩放处理稍微麻烦。那么,我们只需要给它找一个"固定宽度"即可:
任务块的宽度与它的时长有关,距离左边的偏移量与他的起始时间和“最小时间”有关,那么不管它处于什么scaleX时渲染,我们都可以计算出它的"固定宽度",即 实际宽度 / scaleX。左侧偏移量同理。
var width = (this.hourColDomWidth * this.scaleX * totalHour) + (this.hourColDomWidth * this.scaleX * restSecond / 3600);
taskBlockDom.setAttribute('data-width',`${width / this.scaleX}`);
对于时间线的渲染,无非就是递归下AST,找到最小日期和最大日期,得出天数,剩下的就是DOM的一些遍历操作,这里不再赘述。
任务块的渲染
主要是递归AST,伪代码如下
/**
* 渲染任务块
* @param {TaskNode} taskNode 任务节点
* @param {*} params 参数
* @param {Date} minDate 最小日期
*/
renderTaskBlock(treeNode : TaskNode, params : any, minDate : Date){
if(该节点有开始时间和结束世间){
// 得到当前任务的开始时间和结束时间,注意时区处理
// 得到当前任务的开始时间和结束时间的之间总的秒数差值
// 得到整除小时后的余秒数
// 得到小时数(取整)
// 得到左侧偏移小时数
// 创建 dom 任务块, 设置类名和大小和位置
// 任务块的Y坐标系数
params.index = params.index + 1;
}
// 如果有子任务,递归渲染子任务
},