楼主最近在重构一个项目管理模块,为项目下的任务生成甘特图。尝试了github上的几款开源JavaScript工具,踩过一些坑,最终还是选择TWProject的jQueryGantt来做。
jQueryGantt的Demo地址在:https://github.com/robicch/jQueryGantt
因为我们的项目和任务数据都是从后台读取,并且页面也有地方做CRUD,所以甘特图只用来将从后台查到数据展示出来,并不需要原demo中的保存、加载和动态编辑等功能。楼主调试源码大概花了一天时间,页面上调整了按钮、表格列数据,去除了多余的事件之后,就长这样。
大概说一下主要的定制地方:
(1)html中被注释掉的模板标签内容,请不要删除,它们在JS中有用到。
(2)整个gantt图的绘制,需要给它一个div,为其取个id,这在作者的demo代码中也能看到。
//创建GanttMaster对象
var ge= new GanttMaster();
//初始化它,init方法中的参数用的就是这个div的id
ge.init($("#workSpace"));
在构造函数里,为ge对象默认添加了很多属性,我们可以去构造函数里覆盖这些属性来做一些定制,比如
// 设置行高
ge.rowHeight = 30;
// 设置显示多少行
ge.minRowsInEditor = 15;
其他属性大家可以自己去尝试。
(3)真正开始绘制和渲染gantt图的步骤,都在这个ge.init()方法里面,我们定制要修改的地方,主要也是从个方法里入手。
我大概贴一下自己的注释,帮助看的人节省点调试时间。
GanttMaster.prototype.init = function (workSpace) {
// 动态创建一个div,为其添加id和设置样式,然后append到workspace下一级作为子元素
var place = $("<div>").prop("id","TWGanttArea").css({padding:0, "overflow-y":"auto", "overflow-x":"hidden","border":"1px solid #e5e5e5",position:"relative"});
workSpace.append(place).addClass("TWGanttWorkSpace");
this.workSpace = workSpace;
this.element = place;
this.numOfVisibleRows=Math.ceil(this.element.height()/this.rowHeight);
// by default task are coloured by status
// 颜色代表的状态
this.element.addClass('colorByStatus')
var self = this;
// 加载前面html中提到过的模板
$("#gantEditorTemplates").loadTemplates().remove();
// 创建GridEditor,左侧表格
this.editor = new GridEditor(this);
//editor.gridified就是左侧的table了
place.append(this.editor.gridified);
// 创建gantt图,起止时间为当前时间前2天到后5天
this.gantt = new Ganttalendar(new Date().getTime() - 3600000 * 24 * 2, new Date().getTime() + 3600000 * 24 * 5, this, place.width() * .6);
// splitter是甘特图和table的分隔符,table是first,甘特图是second
self.splitter = $.splittify.init(place, this.editor.gridified, this.gantt.element, 40);
self.splitter.firstBoxMinWidth = 5;
self.splitter.secondBoxMinWidth = 20;
// 添加toolbar buttons
var ganttButtons = $.JST.createFromTemplate({}, "GANTBUTTONS");
// buttons放在place之前,并且check一下permission,这个permission是可以在new GanttMaster()后自己设置的
place.before(ganttButtons);
this.checkButtonPermissions();
//开始为workspace绑定事件,如果不需要的事件,可以在这里删除绑定(最好同时去把对应的事件定义也删掉,减少代码size)
workSpace.bind("collapseAll.gantt", function () {
self.collapseAll();
}).bind("expandAll.gantt", function () {
self.expandAll();
}).bind("zoomPlus.gantt", function () {
self.gantt.zoomGantt(true);
}).bind("zoomMinus.gantt", function () {
self.gantt.zoomGantt(false);
}).bind("resize.gantt", function () {
self.resize();
});
// 绑定大分隔条的拖动事件
self.splitter.firstBox.scroll(function () {
//notify scroll to editor and gantt
self.gantt.element.stopTime("test").oneTime(10, "test", function () {
var oldFirstRow = self.firstScreenLine;
var newFirstRow = Math.floor(self.splitter.firstBox.scrollTop() / self.rowHeight);
if (Math.abs(oldFirstRow - newFirstRow) >= self.rowBufferSize) {
self.firstScreenLine = newFirstRow;
self.scrolled(oldFirstRow);
}
});
});
// 窗口调整的resize事件
$(window).resize(function () {
place.css({width: "100%", height: $(window).height() - place.position().top});
place.trigger("resize.gantt");
}).oneTime(2, "resize", function () {$(window).trigger("resize")});
};
//msg的内容会影响右侧甘特图表头的显示
GanttMaster.messages = {
"CANNOT_WRITE": "CANNOT_WRITE",
"CHANGE_OUT_OF_SCOPE": "NO_RIGHTS_FOR_UPDATE_PARENTS_OUT_OF_EDITOR_SCOPE",
"START_IS_MILESTONE": "START_IS_MILESTONE",
"END_IS_MILESTONE": "END_IS_MILESTONE",
"TASK_HAS_CONSTRAINTS": "TASK_HAS_CONSTRAINTS",
"GANTT_ERROR_DEPENDS_ON_OPEN_TASK": "GANTT_ERROR_DEPENDS_ON_OPEN_TASK",
"GANTT_ERROR_DESCENDANT_OF_CLOSED_TASK": "GANTT_ERROR_DESCENDANT_OF_CLOSED_TASK",
"TASK_HAS_EXTERNAL_DEPS": "TASK_HAS_EXTERNAL_DEPS",
"GANTT_ERROR_LOADING_DATA_TASK_REMOVED": "GANTT_ERROR_LOADING_DATA_TASK_REMOVED",
"CIRCULAR_REFERENCE": "CIRCULAR_REFERENCE",
"CANNOT_MOVE_TASK": "CANNOT_MOVE_TASK",
"CANNOT_DEPENDS_ON_ANCESTORS": "CANNOT_DEPENDS_ON_ANCESTORS",
"CANNOT_DEPENDS_ON_DESCENDANTS": "CANNOT_DEPENDS_ON_DESCENDANTS",
"INVALID_DATE_FORMAT": "INVALID_DATE_FORMAT",
"GANTT_SEMESTER_SHORT": "GANTT_SEMESTER_SHORT",
"GANTT_SEMESTER": "GANTT_SEMESTER",
"GANTT_QUARTER_SHORT": "季度",
"GANTT_QUARTER": "季度",
"GANTT_WEEK": "星期",
"GANTT_WEEK_SHORT": "周",
"CANNOT_CLOSE_TASK_IF_OPEN_ISSUE": "CANNOT_CLOSE_TASK_IF_OPEN_ISSUE",
"PLEASE_SAVE_PROJECT": "PLEASE_SAVE_PROJECT",
"CANNOT_CREATE_SAME_LINK": "CANNOT_CREATE_SAME_LINK"
};
// 下面是GanttMaster的一众方法
// 创建任务,参数为每个任务的列属性
GanttMaster.prototype.createTask = function (id, name, code, level, start, duration) {
var factory = new TaskFactory();
return factory.build(id, name, code, level, start, duration);
};
//...............
(4)如何自定义左侧表格的列?
你可能要在上面的代码里打个断点debug看一下加载模板生成表格的过程,然后去下面这两个模板里将表头和内容列都对应清除掉。如果删除时数据错位了,内容就会显示不出来,并且不会在console报错,因此可能需要反复尝试。
最后我上面清完的效果对应的代码是:
<div class="__template__" type="GANTBUTTONS">
<div class="ganttButtonBar noprint">
<div class="buttons">
<button onclick="$('#workSpace').trigger('expandAll.gantt');return false;" class="button textual icon " title="EXPAND_ALL"><span class="teamworkIcon">6</span></button>
<button onclick="$('#workSpace').trigger('collapseAll.gantt'); return false;" class="button textual icon " title="COLLAPSE_ALL"><span class="teamworkIcon">5</span></button>
<span class="ganttButtonSeparator"></span>
<button onclick="$('#workSpace').trigger('zoomMinus.gantt'); return false;" class="button textual icon " title="zoom out"><span class="teamworkIcon">)</span></button>
<button onclick="$('#workSpace').trigger('zoomPlus.gantt');return false;" class="button textual icon " title="zoom in"><span class="teamworkIcon">(</span></button>
<span class="ganttButtonSeparator"></span>
<button onclick="ge.gantt.showCriticalPath=!ge.gantt.showCriticalPath; ge.redraw();return false;" class="button textual icon requireCanSeeCriticalPath" title="CRITICAL_PATH"><span class="teamworkIcon">£</span></button>
<span class="ganttButtonSeparator requireCanSeeCriticalPath"></span>
<button onclick="ge.splitter.resize(.1);return false;" class="button textual icon" ><span class="teamworkIcon">F</span></button>
<button onclick="ge.splitter.resize(50);return false;" class="button textual icon" ><span class="teamworkIcon">O</span></button>
<button onclick="ge.splitter.resize(100);return false;" class="button textual icon"><span class="teamworkIcon">R</span></button>
</div>
</div>
</div>
<!-- 表头 -->
<div class="__template__" type="TASKSEDITHEAD">
<table class="gdfTable" cellspacing="0" cellpadding="0">
<thead>
<tr style="height:40px">
<th class="gdfColHeader" style="width:35px; border-right: none"></th>
<th class="gdfColHeader" style="width:25px;"></th>
<th class="gdfColHeader gdfResizable" style="width:300px;">任务名</th>
<th class="gdfColHeader gdfResizable" style="width:80px;">开始日期</th>
<th class="gdfColHeader gdfResizable" style="width:80px;">结束日期</th>
<th class="gdfColHeader gdfResizable" style="width:60px;">跨度(天)</th>
<th class="gdfColHeader gdfResizable" style="width:300px; text-align: left; padding-left: 10px;">责任人</th>
</tr>
</thead>
</table>
</div>
<!-- 内容行 -->
<div class="__template__" type="TASKROW">
<!--
<tr id="tid_(#=obj.id#)" taskId="(#=obj.id#)" class="taskEditRow (#=obj.isParent()?'isParent':''#) (#=obj.collapsed?'collapsed':''#)" level="(#=level#)">
<th class="gdfCell edit" align="right" style="cursor:pointer;"><span class="taskRowIndex">(#=obj.getRow()+1#)</span> <span class="teamworkIcon" style="font-size:12px;" >e</span></th>
<td class="gdfCell noClip" align="center"><div class="taskStatus cvcColorSquare" status="(#=obj.status#)"></div></td>
<td class="gdfCell indentCell" style="padding-left:(#=obj.level*10+18#)px;">
<div class="exp-controller" align="center"></div>
<input type="text" name="name" value="(#=obj.name#)" placeholder="name">
</td>
<td class="gdfCell"><input type="text" name="start" value="" class="date"></td>
<td class="gdfCell"><input type="text" name="end" value="" class="date"></td>
<td class="gdfCell"><input type="text" name="duration" autocomplete="off" value="(#=obj.duration#)"></td>
<td class="gdfCell taskAssigs">(#=obj.getAssigsString()#)</td>
</tr>
-->
</div>
(5)如何从服务器读取数据?
之前看别人说直接用$.ajax()读取后台,在success里赋值,会导致数据灌不进去,我试了一下,果然报了一个什么jquery错误。
jquery-3.4.1.min.js:2 Uncaught TypeError: Cannot read property 'rowElement' of undefined
本来考虑要不要用Promise的then来解决,但后来尝试了一下ajax同步取数据,发现也可以,就懒得改了。
$.ajax({
url: "你自己的请求url",
type: "GET",
async: false, //改为同步
data: {id: $('#projectId').val()},
success: function (res) {
if (res.success) {
//按照格式从服务器取出部分数据,组装出渲染需要的字段即可
res.data.forEach(item => {
item.level = 0;
item.status = 'STATUS_ACTIVE';
item.start = new Date(item.beginDate).getTime();
item.end = new Date(item.endDate).getTime();
item.collapsed = false;
item.hasChild = false;
item.duration = (item.end - item.start)/(3600000*24);
});
project.tasks = res.data;
//console.log(project.tasks);
} else {
toastr.error(res.message);
}
},
error: function (data) {
toastr.error("请求失败");
}
});
gantt表格渲染需要的数据格式如下,如果自己的后台返回的JSON不是这种格式,至少要拼装出你前面定制的table列需要的字段。注意start和end的值都是毫秒,如果写字符串似乎出不来。
{
"id": -1,
"name": "做一顿年夜饭",
"progress": 0,
"progressByWorklog": false,
"relevance": 0,
"type": "",
"typeId": "",
"description": "",
"code": "",
"level": 0,
"status": "STATUS_ACTIVE",
"depends": "",
"canWrite": true,
"start": 1396994400000,
"duration": 20,
"end": 1399586399999,
"startIsMilestone": false,
"endIsMilestone": false,
"collapsed": false,
"assigs": [],
"hasChild": true
}
(6)至于去除不需要的btn等元素,自己斟酌着删除就好了。需要注意的是,渲染gantt图的div你要手动给它宽高,或者它能从父元素继承到宽高,大家可以自己体会一下。
其他的样式不满意的,可以去gantt.css这个文件中找,然后在<style>标签里覆盖就行了。