基于原生js手撸一个不一样的gantt图

1,522 阅读2分钟

简介

本文基于客户需求及参考老东家plusgantt实现不同角度的gantt图,额外引入了moment.js对日期快速处理

view.png

1.为了达到客户这种效果首先要确认数据源,项目作为一条任务,作业作为横道。达到多计划管理的需求
  {
        "id": "2103211629501365",
        "name": "测试项目",
        "projCode": "458",
        "taskNum": "2",
        "latest_date": "2021/3/21",
        "tasks": [
            {
                "id": "2103211630124214",
                "start_date": "2021/3/21",
                "end_date": "2021/3/23",
                "name": "TF-L233-002-001",
                "state": "add",
                "resources": [{
                    "id": "a2fa0534-ba64-4b16-8fb8-0638868a09d1",
                    "name": "沈松涛"
                }]
            },
            {
                "id": "2103211636069805",
                "start_date": "2021/3/21",
                "end_date": "2021/3/23",
                "name": "TF-458-002-002",
            }
        ]
    },
2.为了实现此效果gantt图,要先设计gantt的基础配置。总结了以下几个基础属性
  this.config = {
        width: "100%",                   
        height: "100%",                 
        columnDefaultWidth: "100px",     //表格列默认宽度
        scaleDefautWidth: 28,            //gantt列默认宽度
        scaleDefaultHeight: 48,          //gantt 默认单行行高
        ganttTopDateMode: "YYYY年MM月",  //日历格式
        type: "project",                 //项目维度, project:项目    resources:人员
        initScrollTop: 0,                //记录初始化滚动条top
        initScrollLeft: 0,               //记录初始化滚动条left
        scrollTicking: false             //滚动定时器
    }

代码入口及使用方法

<div id="ganttChart" class="row row-grid ganttview" style="margin: 5px;height: 100%;"></div>

 var powerGantt = new powerGantt(document.getElementById("ganttChart"));
 powerGantt.setColumn([]);
 powerGantt.setData([])
var powerGantt = function (element, config) {
    this.config = {
        width: "100%",                   
        height: "100%",                 
        columnDefaultWidth: "100px",     //表格列默认宽度
        scaleDefautWidth: 28,            //gantt列默认宽度
        scaleDefaultHeight: 48,          //gantt 默认单行行高
        ganttTopDateMode: "YYYY年MM月",  //日历格式
        type: "project",                 //项目维度, project:项目    user:人员
        initScrollTop: 0,                //记录初始化滚动条top
        initScrollLeft: 0,               //记录初始化滚动条left
        scrollTicking: false             //滚动定时器
    }
    this.data = [];
    this.columns = [];

    this.el = element;
    this.config = {
        ...this.config,
        ...config
    };
    this.init();
}

powerGantt.prototype.init = function () {
    let self = this;
    //初始化gantt结构和配置
    this.el.classList.add("power-gantt");
    // this.el.style.width = this.config.width;
    // this.el.style.height = this.config.height;
    this.el.style.position = "relative";
    
    //创建grid区域
    this.ganttGrid = document.createElement("div");
    this.ganttGrid.innerHTML = "";
    this.ganttGrid.className = "power-ganttgrid"

    //创建gantt区域
    this.ganttView = document.createElement("div");
    this.ganttView.innerHTML = "";
    this.ganttView.className = "power-ganttview "
    this.el.appendChild(this.ganttGrid);
    this.el.appendChild(this.ganttView);
    
    //开始绘制
    this.render();
   
}

预览生成效果、是不是啥也看不到,看不到就对了,因为还没开始

init.png

3.开始绘制gantt,含预处理验证数据、计算gantt视图区域的开始结束、根据任务的startDate、endDate对数据进行level处理
powerGantt.prototype.render = function () {
    //数据level处理
    this.startDate = moment().add(-7, 'day').format('L');
    this.endDate = moment().add(7, 'day').format('L');
    if (this.data.length) {
        //找出gantt的开始结束日期
        for (var i = 0; i < this.data.length; i++) {
            let projectTask = this.data[i].tasks;
            var project = this.data[i];
            //对每个项目的任务进行level分组
            this.toLevelData(project);
            
            //循环过程中更新startDate、endDate
             this.startDate=....
             this.endDate=....
        }
    }
    //绘制grid,表格绘制,不在额外描述
    this.renderGrid();

    //绘制ganttview区域
    this.renderGantt();
}
4.绘制ganttview(绘制grid不在额外介绍)
powerGantt.prototype.renderGantt = function () {
    let self = this;
    self.ganttView.innerHTML = '';
    self.ganttViewHeader = document.createElement("div");
    self.ganttViewHeader.className = "power-ganttview-header"
    self.ganttViewBody = document.createElement("div");
    self.ganttViewBody.className = "power-ganttview-body";
    self.ganttView.appendChild(self.ganttViewHeader);
    self.ganttView.appendChild(self.ganttViewBody);

    self.ganttViewBody.onscroll = function (event) {
        //上下滚动,同步表格和gantt的滚动高度
    }
    self.renderGanttHeader();
    self.renderGanttBody();
}
//ganttview时间刻度
powerGantt.prototype.renderGanttHeader = function () {
    //month-day
    this.scalcColumnLength = 0;
    let scaleWidth = this.config.scaleDefautWidth;
    let topMode = this.config.ganttTopDateMode;
    let topTimeScale = `<div class="power-ganttview-toptimescale">`;
    let bottomTimeScale = `<div class="power-ganttview-bottomtimescale">`;
    let viewData = this.startDate;
    let left = 0;
    //头部区域编辑无事件,可用字符串拼接
    ...
    topTimeScale += `</div>`
    bottomTimeScale += `</div>`
    let gutter = `<div class='gutter' style='left:${left}px;width:17px'></div>`
    this.ganttView.children[0].innerHTML = topTimeScale + bottomTimeScale + gutter;

}

setHeader.png

//ganttview内容区域
powerGantt.prototype.renderGanttBody = function () {
    let ganttGrid = this.renderGanttGrid();
    this.ganttView.children[1].appendChild(ganttGrid)
    let ganttTask = this.renderGanttTask();
    this.ganttView.children[1].appendChild(ganttTask)

}
//gantt表格绘制
powerGantt.prototype.renderGanttGrid = function () {
    let ganttGrid = document.createElement("div");
    ganttGrid.className = "power-ganttview-grid";
    //计算行、列数量
    let scaleWidth = this.config.scaleDefautWidth;
    let scaleHeight = this.config.scaleDefaultHeight;
    let rowLength = this.data.length;
    let columnLength = this.scalcColumnLength;
    let rowHeight = 0;

    for (let i = 0; i < rowLength; i++) {
        let level = this.data[i].level;
        let gridRow = document.createElement("div");
        gridRow.className = "power-ganttview-row";
        gridRow.style.top = rowHeight + "px";
        gridRow.style.width = columnLength * scaleWidth + "px";
        gridRow.style.height = scaleHeight * level + "px";
        rowHeight += scaleHeight * level;
        ganttGrid.appendChild(gridRow)

    }
    for (let l = 0; l < columnLength; l++) {
        let weekdd = moment(this.startDate).add(l, 'day').days();;
        let gridColumn = document.createElement("div");
        gridColumn.className = "power-ganttview-column"
        if (weekdd == "6" || weekdd == "0") {
            gridColumn.classList.add("weeked");
        }
        gridColumn.style.left = l * scaleWidth + "px";
        gridColumn.style.width = scaleWidth + "px";
        gridColumn.style.height = rowHeight + "px";
        ganttGrid.appendChild(gridColumn)
    }

    return ganttGrid;
}

setbody.png

//gantt任务绘制
powerGantt.prototype.renderGanttTask = function () {
    let self = this;
    let scaleHeight = this.config.scaleDefaultHeight;
    let scaleWidth = this.config.scaleDefautWidth;
    let taskDomContainer = document.createElement("div");
    taskDomContainer.className = "power-ganttview-taskview"
    let taskTop = 0;
    let levelTop = 0;
    for (var i = 0; i < this.data.length; i++) {
        var row = this.data[i];
        let rowLevelList = row.levelList;
        let projectRow = document.createElement("div");
        projectRow.className = "power-ganttview-project-row"


        for (let key in rowLevelList) {
            for (let j = 0; j < rowLevelList[key].length; j++) {
                let task = rowLevelList[key][j]
                let days = moment(task.end_date).diff(moment(task.start_date), 'days') + 1;
                let width = scaleWidth * days;
                let left = moment(task.start_date).diff(moment(this.startDate), 'days') * scaleWidth;
                let taskDom = document.createElement("div");
                taskDom.className = "power-ganttview-task";
                taskDom.setAttribute("title", task.name);
                taskDom.style.left = left + "px";
                taskDom.style.top = levelTop + "px";
                taskDom.style.width = width + "px";
                taskDom.style.position = "absolute";
                taskDom.ondblclick = this.onTaskDblClick.bind(this, taskDom, task);
                projectRow.appendChild(taskDom);

                let userContent = document.createElement("div");

                userContent.className = "ganttview-block-user";
                userContent.id = task.id;
                userContent.setAttribute("project-id", row.id);

                taskDom.appendChild(userContent);
                let reszieLeft = document.createElement("div");
                reszieLeft.className = "ui-resizable-handle ui-resizable-w";
                reszieLeft.style.zIndex = 90;

                let reszieRight = document.createElement("div");
                reszieRight.className = "ui-resizable-handle ui-resizable-e";
                reszieRight.style.zIndex = 90;
                taskDom.appendChild(reszieLeft);
                taskDom.appendChild(reszieRight);

                let taksName = document.createElement("div");
                taksName.className = "ganttview-block-text";
             
                taksName.innerText = task.name;
                taskDom.appendChild(taksName);
              
                let startX = 0;
                
                //任务拖动,以下省略业务代码
                taskDom.onmousedown = function (event) {
                    event.stopPropagation();
                    document.onmousemove = function (event) {
                        event.stopPropagation();
                        console.log("onmousemove")
                    }
                    document.onmouseup = function (event) {
                        console.log("onmouseup")
                        event.stopPropagation();
                        document.onmousemove = null;
                        document.onmouseup = null;
                    }
                }
                //拖动修改任务结束日期
                reszieRight.onmousedown = function (event) {
                    event.stopPropagation();
                    document.onmousemove = function (event) {
                        event.stopPropagation();
                    }
                    document.onmouseup = function (event) {
                        event.stopPropagation();
                        document.onmousemove = null;
                        document.onmouseup = null;

                    }
                }
                //拖动修改开始日期
                reszieLeft.onmousedown = function (event) {
                    event.stopPropagation();
                    document.onmousemove = function (event) {
                        event.stopPropagation();
                    }
                    document.onmouseup = function (event) {
                        event.stopPropagation();
                        document.onmousemove = null;
                        document.onmouseup = null;
                    }
                }
            }
            levelTop += scaleHeight;
        }
        //添加最晚时间线
        if (row.latest_date) {
            let left = (moment(row.latest_date).diff(moment(this.startDate), 'days') + 1) * scaleWidth;
            let lastLine = document.createElement("div");
            lastLine.className = "power-ganttview-laseLine";
            lastLine.style.background = "red";
            lastLine.style.left = left + "px";
            lastLine.style.top = taskTop + "px";
            lastLine.style.width = "2px";
            lastLine.style.marginLeft = "-1.5px";
            lastLine.style.position = "absolute";
            lastLine.style.height = (row.level * scaleHeight) + "px";
            projectRow.appendChild(lastLine);
        }
        taskTop += row.level * scaleHeight;
        levelTop = taskTop;
        taskDomContainer.appendChild(projectRow)
    }
    return taskDomContainer;
}

task.png

5.绘制完成。方法使用、绑定事件及事件使用方法

使用方式

<div id="ganttChart" class="row row-grid ganttview" style="margin: 5px;height: 100%;"></div>

 var powerGantt = new powerGantt(document.getElementById("ganttChart"));
 
 powerGantt.setData([]);
 
 powerGantt.on("onTaskDblClick", function (event, task) {
      console.log("onTaskDblClick",task)
 })
powerGantt.prototype.setColumn = function (columns) {
    this.columns = columns;
    this.renderGrid();
}
powerGantt.prototype.on = function (event, callback) {
    let eventName = "on" + event.replace(event[0], event[0].toUpperCase());
    this[eventName] = callback.bind(this);
}
powerGantt.prototype.onTaskDblClick = function (event, task) {
    console.log(task)
    return false
}

6.预览效果及源代码

源码地址

cywyg-hqtna.gif