dhtmlx-gantt 用法
- 安装
npm i dhtmlx-gantt
;- 页面创建一个用来展示甘特图的盒子,给盒子设置 宽高 和
id
;- 调用
gantt.init("ganttId")
初始化甘特图,传递容器id 或 容器dom;- 调用
gantt.parse(tasks)
渲染图形,(传递的 tasks 是一个对象,内部的data
属性,就是用来渲染甘特图的数据)
简单案例演示
注:甘特图的任务条数据对象中只要有 开始时间 和 结束时间/持续时间 ( start_date
和 end_date
/ duration
),就可以显示出来。
下面的模拟数据中传递了两条
- 第一个对象使用
start_date
end_date
,传递时间是 包含年-月-日 时:分:秒的日期格式对象,甘特图不配置日期格式,默认是日期对象格式;- 第二个对象使用
start_date
duration
,传递时间中由于只有年月日,new Date() 后,时刻是所传递日期的 08:00:00(所以在上图中展示的任务条,开始不是从25号列左边的边框起始的),duration 字段代表这个任务的持续时间,不做配置的话默认是天。
<template>
<div id="ganttContainer" style="width: 1000px; height: 200px;"></div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, onDeactivated, h, onActivated, nextTick, watch } from 'vue';
import { gantt } from 'dhtmlx-gantt';
import "dhtmlx-gantt/codebase/dhtmlxgantt.css";
const initGannt = () => {
// ...这里写甘特图配置
gantt.init("ganttContainer");
}
const getRequestData = () => {
// ...获取接口数据
// 模拟数据
const ganttData = [
{
start_date: new Date("2024-12-23 00:00:00"),
end_date: new Date("2024-12-24 23:59:59"),
},
{
start_date: new Date("2024-12-25"),
// end_date: new Date("2024-12-26"),
duration: 1
},
]
// 渲染甘特图
gantt.parse({
data: ganttData
})
}
onMounted(() => {
initGannt()
getRequestData()
})
</script>
简单配置项设置
上图是对 gantt 的部分属性值以及数据做了修改后的显示效果,代码如下
const initGannt = () => {
// ...这里写甘特图配置
// 语言设置-中文
gantt.i18n.setLocale("cn");
// 设置甘特图表格列的最小列宽
gantt.config.mcolumn_width = 20;
// 设置表格列,name为数据字段,label为表头显示的名称,width为列宽,align为对齐方式
gantt.config.columns = [
{ name: "cCode", label: "学号", width: 80, align: "left" },
{ name: "cName", label: "名称", width: 50, align: "center" },
{ name: "cHeight", label: "身高", width: 30, align: "center" },
];
// 设置右侧时间刻度相关属性,上方显示年月日,下方显示小时,每个格子代表12小时
gantt.config.scales = [
{ unit: "day", format: "%Y-%m-%d" },
{ unit: "hour", step: 12, format: "%H" }
];
// 设置任务条上展示的内容,参数task会返回当前任务的数据
gantt.templates.task_text = function (start, end, task) {
return task.cCode + "-" + task.cName + "-" + task.cHeight;
};
// 禁用任务条链接拖动功能
gantt.config.drag_links = false;
// 禁用任务条左右边缘拉动功能
gantt.config.drag_resize = false;
// 禁用任务条长按移动功能
gantt.config.drag_move = false;
// 禁用任务条拖动进度条功能
gantt.config.drag_progress = false;
// 设置甘特图滚动条的大小为25px
gantt.config.scroll_size = 25;
// 开启表格排序功能
gantt.config.sort = true;
// 渲染发生错误时不显示默认的错误弹框
gantt.config.show_errors = false;
gantt.init("ganttContainer");
}
const getRequestData = () => {
// ...获取接口数据
// 模拟数据
const ganttData = [
{
start_date: new Date("2024-12-23 00:00:00"),
end_date: new Date("2024-12-24 23:59:59"),
cCode: "001",
cName: "李四",
cHeight: "180",
progress: 0.6 // 当前任务进度
},
{
start_date: new Date("2024-12-25"),
// end_date: new Date("2024-12-26"),
duration: 1,
cCode: "002",
cName: "张三",
cHeight: "170",
progress: 0.9
},
]
// 更新甘特图数据
gantt.parse({
data: ganttData
})
}
其它配置项
1. 改变颜色
给甘特图渲染数据上添加自定义字段 cStatus,通过
gantt.templates.task_class
返回的字符串作为类名,添加到甘特图任务条的dom上,然后写对应类名的CSS样式来改变颜色。
const initsetGannt = () => {
// ...省略上面所做的配置项
// 给任务条根据数据设置类名,实现颜色变化 (start:开始日期, end:开始日期, task:数据对象)
gantt.templates.task_class = function (start, end, task) {
if (task.cStatus == '已完成') {
return "ywcTask";
} else if (task.cStatus == '在执行') {
return "zzxTask";
} else {
return "style0";
}
};
}
const getRequestData = () => {
// ...获取接口数据
// 模拟数据
const ganttData = [
{
// ... 省略上面数据
cStatus: '已完成'
},
{
// ... 省略上面数据
cStatus: '在执行'
},
]
}
// 任务条添加类名改变样式
<style lang="less" scoped>
:deep(.gantt_task_line) {
&.style0 {
border: 1px solid #d9d9d9;
background: #d9d9d9;
}
&.ywcTask {
border: 1px solid #606060;
background: #606060;
}
&.zzxTask {
border: 1px solid #029f08;
background: #029f08;
}
}
</style>
2. lightBox
双击任务条时,可以打开弹窗lightBox,下面给任务条渲染数据添加 machine 字段来显示到 lightBox 的机台内容中
const initsetGannt = () => {
// ...省略上面所做的配置项
// lightbox 弹窗相关设置
// map_to 用于将表单部分的输入值与任务对象的属性进行绑定。当用户在 Lightbox 中编辑表单部分时,输入的值会自动映射到指定的任务属性上。
// name 显示为表单部分的标题,字段和 gantt.locale.labels.section_XXX 相对应,设置显示标题
// height 设置表单部分的高度
// type 设置表单部分的类型,可以是 template、time、time_range、textarea、select、checkbox、radio、color_picker、duration、time_picker、time_range、recurring、custom_table、customer_time_picker
gantt.config.lightbox.sections = [
{ name: "orderNumber", height: 15, map_to: "orderNumber", type: "template" },
{ name: "machine", height: 15, map_to: "machine", type: "template" },
{ name: "matnrPlan", height: 30, map_to: "auto", type: "custom_table" },
{ name: "nPlanWgt", height: 15, map_to: "nPlanWgt", type: "template" },
{ name: "nCompWgt", height: 15, map_to: "nCompWgt", type: "template" },
{ name: "plan_date", height: 15, map_to: "auto", type: "customer_time_picker" }
];
// 设置标题文字
gantt.locale.labels.section_orderNumber = "销售订单号";
gantt.locale.labels.section_machine = "机台";
gantt.locale.labels.section_matnrPlan = "物料详情";
gantt.locale.labels.section_nPlanWgt = "计划量";
gantt.locale.labels.section_nCompWgt = "完成量";
gantt.locale.labels.section_plan_date = "计划时间";
// 底部按钮
gantt.config.buttons_left = ["gantt_cancel_btn"];
gantt.config.buttons_right = ["gantt_save_btn"];
// gantt.form_blocks[XXX] 自定义表单部分组件
gantt.form_blocks["customer_time_picker"] = {
render: function (sns) {
return "<div class='dhx_cal_ltext' style='height:30px; margin-left:10px;'>" +
"<input id='start_time' class='time_start' type='datetime-local' disabled></input>" +
"-" +
"<input id='end_time' class='time_end' type='datetime-local' disabled></input>" +
"</div>"
},
set_value: function (node, value, task) {
function convertLocalTime(date: Date) {
let yyyy = date.getFullYear();
let MM = (date.getMonth() + 1) < 10 ? ("0" + (date.getMonth() + 1)) : (date.getMonth() + 1);
let dd = date.getDate() < 10 ? ("0" + date.getDate()) : date.getDate();
let HH = date.getHours() < 10 ? ("0" + date.getHours()) : date.getHours();
let mm = date.getMinutes() < 10 ? ("0" + date.getMinutes()) : date.getMinutes();
let curDay = yyyy + '-' + MM + '-' + dd + 'T' + HH + ':' + mm;
return curDay;
}
node.querySelector(".time_start").value = convertLocalTime(task.start_date);
node.querySelector(".time_end").value = convertLocalTime(task.end_date);
},
get_value: function (node, task) {
task.start_date = new Date(node.querySelector(".time_start").value);
task.end_date = new Date(node.querySelector(".time_end").value);
return task;
},
focus: function (node) {
}
};
gantt.form_blocks["custom_template"] = {
// render 用于渲染自定义表单块的 HTML 内容
render: function (sns, task, section) {
return "<div class='dhx_cal_ltext' style='height:24px; margin-left:10px;'>" +
"<input type='text' class='cpltAmt' style='width:100%; border:none; background-color:transparent;' readonly />" +
"</div>";
},
// set_value 用于设置表单块的值
set_value: function (node, value, task) {
var input = node.querySelector(".cpltAmt");
if (input) {
// 如果值为 undefined 或 null,则将其设置为0
var displayValue = value == undefined || value == null ? "0" : value;
input.value = displayValue;
} else {
task.cpltAmt = value;
}
},
// get_value 用于获取表单块的值
get_value: function (node, task) {
// 无需实现此方法,因为输入框是只读的
return task;
},
// 用于在 Lightbox 打开时将焦点设置到特定的表单块上
focus: function (node) {
// 无需实现此方法,因为输入框是只读的
}
};
gantt.form_blocks["custom_select"] = {
render: function (sns) {
return "<div class='dhx_cal_ltext' style='height:20px; margin-left:10px;'>" +
"<select class='plnumStatus'></select>" +
"</div>";
},
set_value: function (node, value, task) {
var select = node.querySelector(".plnumStatus");
// 如果下拉框存在,则设置下拉框选项
if (select) {
// 获取当前任务的状态值
var status = task.plnumStatus;
// 如果状态为"完成"或"执行中",将下拉框选项设置为对应的选项
if (status === "完成" || status === "执行中") {
select.innerHTML = "<option value='执行中'>执行中</option><option value='完成'>完成</option>";
// 设置下拉框的选中值
select.value = status;
// 设置下拉框的样式为可见
select.style.display = "block";
// 移除节点中的状态值
var span = node.querySelector("span");
if (span) {
span.remove();
}
} else {
// 如果状态为其他值,将下拉框选项置为空
select.innerHTML = "";
// 设置下拉框的选中值为当前任务的状态值
select.value = status;
// 设置下拉框的样式为不可见
select.style.display = "none";
// 在节点中添加当前任务的状态值
var span = node.querySelector("span");
if (span) {
span.innerHTML = status;
} else {
node.innerHTML += "<span>" + status + "</span>";
}
}
} else {
// 如果下拉框不存在,将任务状态设置为节点的内容
task.plnumStatus = node.innerHTML;
}
},
get_value: function (node, task) {
var select = node.querySelector(".plnumStatus");
// 如果下拉框存在,获取下拉框的选中值
if (select) {
var value = select.value;
// 如果选中值为"完成"或"执行中",将任务状态设置为对应的值
if (value === "完成" || value === "执行中") {
task.plnumStatus = value;
}
} else {
// 如果下拉框不存在,将任务状态设置为节点的内容
var span = node.querySelector("span");
if (span) {
task.plnumStatus = span.innerHTML;
}
}
return task;
},
focus: function (node) {
var select = node.querySelector(".plnumStatus");
if (select) {
select.focus();
}
}
};
gantt.form_blocks["custom_table"] = {
render: function (sns) {
// 注:这里不能使用反引号,否则会报错
return "<div class='dhx_cal_ltext' style='height:100px; margin-left:5px;'>" +
"<table class='wuliaoTable' style='border: 1px solid black; border-collapse: collapse; width:100%;'>" +
"<thead>" +
"<tr>" +
"<th style='padding-left: 5px; border-right: 1px solid black;'>编号</th>" +
"<th style='padding: 8px; padding-left: 8px; border-right: 1px solid black;'>名称</th>" +
"<th style='padding: 8px; padding-left: 8px; border-right: 1px solid black;'>计划量</th>" +
"<th style='padding: 8px; padding-left: 8px; border-right: 1px solid black;'>库存量</th>" +
"<th style='padding: 8px; padding-left: 8px;'>缺量</th>" +
"</tr>" +
"</thead>" +
"<tbody>"+
"</tbody>" +
"</table>" +
"</div>";
},
set_value: async function (node, value, task) {
console.log('获取自定义表格node', node)
var table = node.querySelector(".wuliaoTable");
if (table) {
table.querySelector("tbody").innerHTML = "";
const WuliaoList = [1, 2, 3]
WuliaoList.forEach(item => {
var rowElement = document.createElement('tr');
var rowData = "<td style='padding-left: 5px; border-right: 1px solid black;'>" + '' + "</td>" +
"<td style='padding-left: 10px; border-right: 1px solid black;'>" + '' + "</td>" +
"<td style='padding-left: 10px; border-right: 1px solid black;'>" + '' + "</td>" +
"<td style='padding-left: 10px; border-right: 1px solid black;'>" + '' + "</td>" +
"<td style='padding-left: 10px; border-right: 1px solid black;'>" + '' + "</td>";
rowElement.innerHTML = rowData;
table.querySelector("tbody").appendChild(rowElement);
});
}
},
get_value: function (node, task) {
var table = node.querySelector(".wuliaoTable");
if (table) {
var rows = table.querySelectorAll("tbody tr");
var data = [];
rows.forEach(row => {
var rowData = {
c_MTRL_NO: row.cells[0].textContent,
c_MATNRTEXT: row.cells[1].textContent,
c_PLAN_WGT: parseFloat(row.cells[2].textContent),
c_ERPLOCK_WGT: row.cells[3].textContent,
n_PICKING_WGT: row.cells[4].textContent,
};
data.push(rowData);
});
return data;
}
return task;
},
focus: function (node) {
}
};
}
const getRequestData = () => {
// ...获取接口数据
// 模拟数据
const ganttData = [
{
// ... 省略上面数据
machine: '机台1',
},
{
// ... 省略上面数据
machine: '机台2',
},
]
}
3.事件监听
const initsetGannt = () => {
// ...省略上面所做的配置项
// 任务条双击事件
gantt.attachEvent("onTaskDblClick", (id, e) => {
console.log('双击')
return false; // 阻止默认行为
})
// 任务条单击事件
gantt.attachEvent("onTaskClick", (id, e) => {
console.log('单击')
return false
})
// 任务条移动事件
gantt.attachEvent("onMouseMove", function(id, e: MouseEvent) {
// 移动到任务条上,id存在
if (id) {
}
// 移动到甘特图其它区域,id不存在
else {
}
});
// 配置tooltip插件功能
gantt.plugins({
tooltip: true,
});
// 设置tooltip显示内容
gantt.templates.tooltip_text = function(start, end, task) {
return `
<div>
<strong>完成量:</strong> ${task.nCompWgt}<br>
<strong>计划量:</strong> ${task.nPlanWgt}
</div>
`;
};
// 设置甘特图行的类名,可以改变样式
gantt.templates.grid_row_class = gantt.templates.task_row_class = function (start, end, task) {
if (task.$index % 2 == 0) {
return "custom_row";
}
};
}
4. 在一行显示多个任务条
要在一行显示多个任务条,需要在甘特图的数据中建立父子关系
每一个,任务条数据添加3个字段
id
parent
render
;
- id 属性不设置默认甘特图插件会自动生成,每条数据的id唯一;
- parent 用来指定当前数据的父级id,parent等于某个id时,就代表这条数据是这个对应parent的子级;
- render 设置为 split,表示任务条可以分段显示,内部容纳的子级任务条,它们平铺显示,不会挤到一块,此值设置给父级;
数据如下,设置了两个父级。第一个父级id为 f1,有两个子级;第二个父级id为 f2,有一个子级。并且父级没有设置时间,所以不会显示出来;
父级的数据会作为左侧表格的数据,子级的数据作为任务条的数据。这里在模拟数据中将子级的 cName 字段值添加了 '-子级' 区分,如图子级的该值展示到图形的任务条中,父级该值展示到左侧表格中
// 模拟数据
const getRequestData = () => {
// ...获取接口数据
const ganttData = [
// 第一行
{
cCode: "001",
cName: "李四",
cHeight: "180",
cStatus: '已完成',
id: 'f1',
parent: null,
render: 'split'
},
{
cCode: "001",
cName: "李四-子级",
cHeight: "180",
start_date: new Date("2024-12-25"),
duration: 1,
cStatus: '在执行',
id: 'c1',
parent: 'f1',
render: null
},
{
cCode: "001",
cName: "李四-子级",
cHeight: "180",
start_date: new Date("2024-12-26"),
duration: 1,
cStatus: '已完成',
id: 'c2',
parent: 'f1',
render: null
},
// 第二行
{
cCode: "002",
cName: "张三",
cHeight: "170",
cStatus: '已完成',
id: 'f2',
parent: null,
render: 'split'
},
{
cCode: "002",
cName: "张三-子级",
cHeight: "170",
start_date: new Date("2024-12-28"),
duration: 1,
cStatus: '在执行',
id: 'cc1',
parent: 'f2',
render: null
},
]
}
补充
时间轴的日期和小时对不齐的问题
展示效果及代码如下,设置每个格子为4小时,一天被分隔成6个格子。但是发现,当时间轴的日期结束时,下面的小时却没有结束,天和小时对不齐。
const initsetGannt = () => {
gantt.config.date_format = "%Y-%m-%d %H:%I:%S";
gantt.config.scales = [
{ unit: "day", format: "%Y-%m-%d" },
{ unit: "hour", step: 4, format: "%H" }
];
const tasks = {
data: [
{
start_date: "2025-03-15 11:30:40",
end_date: '2025-03-16 06:50:00'
},
],
}
gantt.init("gantt_here");
gantt.parse(tasks);
}
解决方法
- 这是因为时间轴的计算,会根据任务数据的开始和结束时间自动计算一个范围,这会导致一天的时间,可能无法和设置的一格代表多少小时整除,出现时间轴偏移的情况,天数列和小时列不对齐;
- 这时需要固定时间轴的起始、结束时间范围,设置为 0 点保证能整除。
- 定义如下函数,返回时间轴的起始、结束时间。然后赋值给
gantt.config.start_date
和gantt.config.end_date
,来设置时间范围即可
/**
* 计算甘特图时间轴的开始时间和结束时间,用来将日期和小时列对齐,时间从00点开始
* @param taskList 甘特图任务数据
* @returns 开始时间、结束时间日期对象
* 注:要添加到如下配置
* gantt.config.start_date = startDate
* gantt.config.end_date = endDate
*/
export const getGanttDateRange = (taskList) => {
// 获取当前任务数据的,最小时间
const minDate = taskList.flatMap(a => a.start_date ? a.start_date : []).sort((a, b) => new Date(a).getTime() - new Date(b).getTime())[0]
// 获取当前任务数据的,最大时间
const maxDate = taskList.flatMap(a => a.end_date ? a.end_date : []).sort((a, b) => new Date(b).getTime() - new Date(a).getTime())[0]
const startDate = getToDay(minDate)
const endDate = getNextDayMidnight(maxDate)
// 获取当天的0点日期对象
function getToDay(dateVal) {
const now = new Date(dateVal)
now.setHours(0, 0, 0, 0)
return now
}
// 获取第二天的0点日期对象
function getNextDayMidnight(dateVal) {
const now = new Date(dateVal).getTime();
const nextDayTimestamp = now + 24 * 60 * 60 * 1000;
const nextDayDate = new Date(nextDayTimestamp);
nextDayDate.setHours(0, 0, 0, 0);
return nextDayDate;
}
return {
startDate,
endDate
}
}
注
- 如果甘特图展示的数据是变化的,那么要在每次数据改变后先执行
gantt.clearAll();
清除原来的gantt数据和图形,再执行gantt.parse();
重新渲染;- 页面关闭后执行
gantt.detachAllEvents();
来移除 gantt 所有的事件监听器。因为dhtmlx-gantt插件的事件监听,在页面卸载后任然存在,当多个页面有这个监听的时候,当前页面触发事件,其他页面也会触发他们监听事件的处理逻辑;- 使用免费版本的 dhtmlx-gantt,一个页面不能同时展示多个甘特图,因为每次生成图形用的gantt一直都是同一个实例对象。并且多个页面有甘特图,如果使用
keep-alive
缓存了,它们互相切换时会互相影响的,就是由于一直同的一个gantt实例对象;- 使用免费版的原来要在一行显示多个任务条时,通过
id parent render
这些字段建立数据的父子关系,子级个数是没有限制的。但是发现在 dhtmlx-gantt 更新到 9.0 版本后,这个子级数量超过 12 个浏览器控制台就会报错了。
参考链接