jQuery+Layui+Bootstrap整合dhtmlxgantt实现甘特图
2026/9/16 9:59:41 网站建设 项目流程

简介:一套融合jQuery、LayUI、Bootstrap与dhtmlxGantt的任务进度甘特图开发示例,适合具备一定前端基础、希望快速上手项目管理可视化场景的Web开发者。资源共499个文件,以139个js脚本、128个html页面、107个gif演示图为主,另含css、map、json、字体等资源,总量仅2.9MB,典型目录中可见bootstrap.css、layui.css及dhtmlxgantt_skyblue/material等多样主题样式文件。示例通过jQuery处理页面动态交互与数据请求,LayUI和Bootstrap搭建响应式后台界面,dhtmlxGantt承担核心甘特图展示与编辑能力,可完成任务的增删改、依赖关系设置和时间线缩放等操作。配套的html与gif便于对照页面效果,js与css便于二次开发,整体结构清晰,适合学习多种技术整合及甘特图定制方案。已有477人学习,对想要提升前端综合运用能力的开发者有直接参考价值。

1. 一个 gantt 示例压缩包里,真正难复制的不是甘特图

这种 rar 示例包,打开之后最值钱的往往不是那张渲染好的 gantt 大图,而是 jquery、layui、bootstrap、dhtmlxgantt 这四个库在一个存量后台里怎么不打架地各干各的。项目本身不新潮,但需求很常见:后台任务表要加一个可视化进度面板,能拖、能看、能编辑,还要求不推翻原有 JSP 或 jQuery 页面。

常见做法是把 dhtmlxgantt 塞进 layui 弹窗和 bootstrap 布局之间,用任务 JSON 把所有模块串起来。这条路径不是最优架构,却是改造老系统时成本最低、风险最小的方案,对维护传统管理后台、想加甘特图又暂时不换 Vue/React 的团队尤其合适。下面顺着这套技术栈,把数据模型、初始化参数、弹窗联动和进度刷新完整拆开讲,重点标出那些参数不统一就会整图变白的地方。

2. jquery、layui、bootstrap、dhtmlxgantt 各管一段,靠数据模型衔接

拿到示例包先别急着开页面,先看四个依赖的分工。标题里这串技术栈不是替身关系,不是非此即彼的选型,而是同一个页面上四个独立模块:jquery 负责底层 DOM 操作和 ajax 数据装载,bootstrap 提供栅格、按钮、进度条这类静态基础组件,layui 提供弹窗、日期、表单和表格,dhtmlxgantt 负责把任务进度画成可拖拽的调度图。它们职责不重叠,真正的衔接点是任务 JSON。

2.1 依赖职责划分:避免 bootstrap 和 layui 互相抢活

先把职责分清楚,后面写代码才不会纠结某个按钮到底该用谁的样式。

技术主营业务在本示例中承担不负责
jqueryDOM 操作、$.ajax任务数据装载、滚动到底部加载更多、按钮事件不碰数据渲染
bootstrap栅格布局、navbar、progress 进度条页面骨架、顶部导航、任务条上的进度条不做弹窗,避免和 layer 抢层级
layuilayer 弹窗、laydate 日期、form 表单、table 表格新增/编辑任务弹窗、左侧任务列表不画甘特图
dhtmlxgantt任务条、时间轴、拖动、缩放、依赖连线整个甘特图视图及增删改后的刷新不主动请求后端

一个容易翻车的点是 bootstrap 的 modal 和 layui 的 layer 同时出现在一个页面。两者都会往 body 里塞遮罩层,z-index 一旦被业务代码手动改过,弹窗就可能被遮罩盖住。我一般只保留 layer,把 bootstrap 的>{ "data": [ { "id": 1, "text": "需求评审", "start_date": "2025-03-01 09:00", "duration": 2, "progress": 1, "parent": 0 }, { "id": 2, "text": "接口联调", "start_date": "2025-03-03 09:00", "duration": 3, "progress": 0.4, "parent": 0 } ], "links": [ { "id": 1, "source": 1, "target": 2, "type": "finish_to_start" } ] }

任务字段的语义很直接,但有两个地方容易写错。progress 是 0 到 1 的小数,不是 0 到 100 的整数,写错后进度条会超出任务条。parent 表示父子关系,根任务的 parent 填 0,子任务填父任务的 id。

字段含义易错点
id任务唯一标识必须全局唯一,不能跨任务重复
text任务名称插入模板前要做 HTML 转义
start_date开始时间字符串格式必须匹配 xml_date
duration工期与时间刻度单位一致
progress完成度取值 0~1,不是百分比
parent父任务 id根任务填 0

links 里的 type 只有四个枚举值,控制任务之间的依赖关系:

type 值含义
finish_to_start前一个完成后,后一个才能开始
start_to_start两个任务必须同时开始
finish_to_finish两个任务必须同时结束
start_to_finish前一个开始后,后一个才结束

最常见的依赖是 finish_to_start,也就是图上的 A 结束指向 B 开始。写 links 时要保证 source 和 target 都在任务列表里存在,否则 gantt 只会在控制台告警,不会告诉你具体哪个边断了。

2.3 xml_date:时间字段与后端格式的接缝

这是示例里最容易翻车的一个配置。dhtmlxgantt 接收 Date 对象或配置了格式的字符串,默认格式是 "%d-%m-%Y",跟大多数后台返回的 "yyyy-MM-dd HH:mm:ss" 差得很远。常见做法是让后端直接返回 "2025-03-01 09:00" 这样的字符串,前端初始化时声明解析格式:

gantt.config.xml_date = "%Y-%m-%d %H:%i";

注意 %i 是分钟,%H 是小时,这是 dhtmlxgantt 自己的格式化语法,跟 Java 的 SimpleDateFormat 不是一回事。如果后端给的是 "2025-03-01 09:00:00" 带秒的格式,而 xml_date 没配秒的对应项,时间会被解析成 Invalid Date,任务条直接画不出来。

我的习惯是后端在序列化阶段就统一好字符串格式,不依赖前端做二次转换。如果项目里后端返回的是时间戳,可以用 gantt.date.str_to_date 写一个自定义解析函数挂到数据读取链路上,但尽量别在 gantt.parse 之后再逐条修补时间字段,那样 latency 高还容易漏。

3. 初始化 dhtmlxgantt:最小页面骨架、依赖顺序与首次数据装载

标题里这串技术栈能不能跑起来,七成取决于静态资源引入顺序,三成取决于甘特图容器的高度。传统后台页面不是 SPA,不存在打包器帮你处理依赖树,所有库都靠 script 标签堆叠。顺序错了最常见的现象是 bootstrap 的下拉没反应,或者 dhtmlxgantt 的样式被 layui 的 reset 洗掉。

3.1 静态资源引入顺序与版本选择

页面里同时出现 bootstrap、layui、dhtmlxgantt 三套 CSS 时,引入顺序直接决定最终样式。推荐的顺序如下:

顺序资源原因
1bootstrap.min.css先定栅格和基础组件样式
2layui.css覆盖按钮、表格的局部变量
3dhtmlxgantt.css放最后,防止被前面的 reset 洗掉
4jquery.min.jsbootstrap 依赖它
5bootstrap.min.jsjquery 之后才能绑定><nav class="navbar navbar-default"> <div class="container-fluid"> <div class="navbar-header"> <span class="navbar-brand">任务进度看板</span> </div> </div> </nav> <div class="container-fluid"> <div class="row"> <div class="col-md-4"> <table id="taskTable" lay-filter="taskTable"></table> </div> <div class="col-md-8"> <div id="gantt"></div> </div> </div> </div>

左侧的 layui table 用来展示任务明细和做搜索过滤,右侧的高宽必须显式声明。很多人第一次跑示例包,甘特图容器高度写死 600px,窗口一缩小就出现滚动条乱跳。推荐用 calc 计算高度,撑满视口剩余空间:

#gantt { width: 100%; height: calc(100vh - 50px); }

如果甘特图不是放在页面顶层,而是藏在 bootstrap tab 或 layui tab 里,切换 tab 后容器宽度可能从 0 变成正常宽度,此时必须调用 gantt.setSizes() 重算尺寸,否则任务条全部挤在 1px 宽的容器里。

3.3 首次数据装载:gantt.load 与 $.getJSON 的取舍

初始化代码很短,但顺序不能乱。先声明 xml_date,再 init,最后装载数据。装载方式有两种:gantt.load(url) 适合十行代码起步验证,直接把 URL 交给 gantt 内部 ajax;真实项目里更多用 $.getJSON,因为要带上统一鉴权头、错误提示和权限拦截。

gantt.config.xml_date = "%Y-%m-%d %H:%i"; gantt.init("gantt"); $.getJSON("/api/task/list", { projectId: 1001 }) .done(function (res) { if (res.code !== 0) { layer.msg(res.msg, { icon: 2 }); return; } gantt.clearAll(); gantt.parse(res.data); gantt.render(); }) .fail(function (xhr) { console.error("任务数据装载失败", xhr.status); });

parse 能自动识别 { data, links } 结构和 { tasks, links } 结构,不用手工拆分。clearAll 的作用是防止第二次查询后新旧数据叠加,导致同一任务出现两条重复的条。render 放在 parse 之后,数据变更完成后统一绘一次;频繁调用 render 会明显卡顿,批量更新场景下最后只调一次。

4. gantt 视图调参:columns、scales、tooltip 三组必改配置

数据能装载只是第一步。示例包一旦要在真实项目里用,改动最多的一定是这三组配置:左侧任务列表列、顶部时间刻度轴、悬浮提示。这三块互相影响,列决定用户能看到哪些字段,刻度决定一个格子代表多长时间,tooltip 决定悬停时还能补多少信息。

4.1 columns:任务表格里每一列显示什么

常见的列配置像下面这样,name 对应任务对象里的字段名:

gantt.config.columns = [ { name: "text", label: "任务名称", tree: true, width: "*" }, { name: "start_date", label: "开始时间", align: "center", width: 120 }, { name: "duration", label: "工期", align: "center", width: 60 }, { name: "add", label: "", width: 40 } ];

tree:true 是必须保留的配置项,只有它为 true 时左侧才会渲染层级缩进和展开收起箭头,子任务才能折叠。add 列是 dhtmlxgantt 内置的快捷入口,会在表格底部自动出现一个加号按钮,点击后调用 gantt.createTask 并把新任务挂在当前行层级下,比自己在外面加一个“新建任务”按钮更顺手。不想要这个入口时,把该列对象从数组里移除即可。

列宽配置有一个注意点:各列固定宽度之和不要超过容器宽度,否则最后一列会被压缩。给 text 列用 width:"*" 让它吃掉剩余空间,其他列按内容给固定值。

4.2 scales:时间刻度和单位怎么定

时间刻度决定横向滚动时看到的单位,通常配两级,上面显示大单位,下面显示小单位:

gantt.config.scales = [ { unit: "month", step: 1, format: "%Y-%m" }, { unit: "day", step: 1, format: "%m-%d" } ]; gantt.config.min_column_width = 32; gantt.config.fit_tasks = true;

这套配置下顶部刻度是年-月,底部刻度是月-日,每个最小列宽 32px。如果任务粒度是小时级,就把 unit 改成 "hour",format 改成 "%H:%i",duration 也按小时数给。

参数作用常见取值
fit_tasks时间轴自动适配任务范围true / false
min_column_width最小列宽,控制缩放下限24 ~ 60
drag_move是否允许拖动任务条true / false
drag_resize是否允许拖拽任务条两端调整工期true / false

fit_tasks 设置为 true 后,横向时间轴会以第一条任务和最后一条任务的日期为边界,避免任务在 1 月、项目在 6 月时左侧空出一大片。如果希望右侧留出未来几天的空余,手动给 gantt.config.end_date 加一个值覆盖自动范围。

4.3 tooltip 和进度文本模板

进度数字默认显示在任务条内部,格式是浮点数,直接显示 0.6 很难看。用模板统一改成百分比:

gantt.templates.progress_text = function (start, end, task) { return Math.round((task.progress || 0) * 100) + "%"; }; gantt.templates.tooltip_text = function (start, end, task) { var ratio = Math.round((task.progress || 0) * 100); return "<b>" + task.text + "</b><br/>" + "开始: " + gantt.templates.tooltip_date_format(start) + "<br/>进度: " + ratio + "%"; };

tooltip_text 返回的是一个 HTML 字符串,会直接插进页面。task.text 如果来自用户输入,插入模板前必须做 HTML 转义,否则用户把<img src=x onerror=alert(1)>写进任务名,控制台就会跳出业务代码之外的报错。tooltip_date_format 是内置的时间格式化方法,按当前 scale 的单位输出,不用自己再拼年月日时分。

4.4 任务树排序和跨父节点拖拽

排序和拖拽是一对容易混淆的配置。order_branch 控制的是同级任务之间的拖拽排序,sort_tree 控制加载时按某个字段自动排序。

gantt.config.order_branch = true; // 允许同级拖拽排序 gantt.config.order_branch_free = true; // 允许跨父节点拖拽 gantt.config.sort_tree = true; gantt.config.drag_mode = "both"; // both / move / resize / none

order_branch_free 开启后,子任务可以拖到另一个父任务下面,gantt 会自动更新 task.parent 字段。拖拽结果要持久化时,后端保存的不只是任务的时间变化,还有 parent 和排序字段,这两个字段在图上不直观,最容易漏掉。drag_mode 设为 none 可以关闭所有拖拽,适合只有展示权限的只读视图。

5. layui 弹窗 + bootstrap 进度条:gantt 任务增删改查与进度刷新

如果第一步数据能看到图,第二步就该让图上能改任务。dhtmlxgantt 自带 lightbox 弹窗,但长得很库味儿,跟 layui 的表单风格不一致。常见的做法是关闭自带弹窗,把编辑界面换成 layui layer + form,再用 bootstrap 的 progress 组件直观展示任务完成率。

5.1 关掉 lightbox,把双击事件接到 layui layer

自带弹窗由 details_on_create 和 details_on_dblclick 两个开关控制,先关掉,再把双击事件挂到自己的函数上:

gantt.config.details_on_create = false; gantt.config.details_on_dblclick = false; gantt.attachEvent("onTaskDblClick", function (id) { var task = gantt.getTask(id); setTimeout(function () { openTaskForm(task); }, 0); });

setTimeout 延迟 0 毫秒是为了避开 dhtmlxgantt 内部对本次双击事件的捕获。不加这一层,layer 弹窗经常刚出现就被后续事件抢走焦点,表现为闪一下直接关闭,排查起来很费时间。

5.2 弹窗表单与 laydate 日期边界

弹窗内容放在一个隐藏的 script 模板里,用 layer.open 读取这段 HTML:

<script type="text/html" id="taskFormTpl"> <form class="layui-form" lay-filter="taskForm" style="padding:20px;"> <div class="layui-form-item"> <label class="layui-form-label">任务名</label> <div class="layui-input-block"> <input type="text" name="text" lay-verify="required" class="layui-input"/> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">开始时间</label> <div class="layui-input-block"> <input type="text" name="start_date" id="startDate" class="layui-input"/> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">工期</label> <div class="layui-input-block"> <input type="number" name="duration" value="1" min="1" class="layui-input"/> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">完成度</label> <div class="layui-input-block"> <input type="number" name="progress" min="0" max="100" value="0" class="layui-input"/> </div> </div> <div class="layui-form-item" style="text-align:center;"> <button lay-submit lay-filter="taskFormSubmit" class="layui-btn">保存</button> </div> </form> </script>

打开弹窗时做两件事:回填表单、再渲染一次 layui 组件。很多示例包挂在回填后少了 form.render(),结果 select 下拉框没有皮肤、laydate 点击不弹日历。

var editingTaskId = null; function openTaskForm(task) { editingTaskId = task ? task.id : null; var form = layui.form; var laydate = layui.laydate; layer.open({ type: 1, title: task ? "编辑任务" : "新建任务", content: $("#taskFormTpl").html(), success: function (layero) { form.val("taskForm", { text: task ? task.text : "", duration: task ? task.duration : 1, progress: task ? Math.round(task.progress * 100) : 0 }); form.render(); laydate.render({ elem: layero.find("#startDate")[0], type: "datetime", max: laydate.now(0, "yyyy-MM-dd HH:mm:ss") }); } }); }

laydate 的 max 参数就是热搜里经常出现的“最大日期当前日期”的写法,laydate.now(0, ...) 返回当前时间字符串,这样开始时间就不能选未来日期。如果业务允许排期在未来,把 max 去掉即可。

注意:layer.open 的 success 回调里,弹窗内容刚刚挂到 DOM 上。layui 的 select 和 laydate 都必须等这一步完成后再 render,且 render 目标要从 layero 里查找,不能用全局 id,否则可能出现页面里同时存在两个相同 id 的元素。

弹窗里如果还需要从接口拉取负责人列表,用 form.val 先给 select 赋值,再调用 form.render("select"),这就是 layui select 动态赋值 的标准套路。

5.3 保存时写回 task 并刷新进度模板

保存按钮走 layui 的 submit 事件。字段读取、时间转换、进度换算都在这一个函数里完成:

form.on("submit(taskFormSubmit)", function (data) { var f = data.field; if (editingTaskId == null) { var task = gantt.createTask({ text: f.text, start_date: gantt.date.parseDate(f.start_date, "xml_date"), duration: parseInt(f.duration, 10), progress: parseFloat(f.progress) / 100, parent: 0 }); editingTaskId = task.id; } else { var task = gantt.getTask(editingTaskId); task.text = f.text; task.start_date = gantt.date.parseDate(f.start_date, "xml_date"); task.duration = parseInt(f.duration, 10); task.progress = parseFloat(f.progress) / 100; gantt.updateTask(task.id); } gantt.refreshData(); layer.closeAll(); return false; });

parseDate 的第二个参数填 "xml_date" 是 dhtmlxgantt 的快捷方式,等于用 gantt.config.xml_date 作为解析模板。不要直接把字符串赋给 task.start_date,gantt 后续所有时间计算都要求 Date 对象,字符串会直接报错。progress 从表单的 0~100 换算成 0~1,这一步漏了,进度条宽度就会超出任务条十倍。

5.4 bootstrap 进度条接进任务条

bootstrap 基础组件里的 progress 进度条可以直接塞进 dhtmlxgantt 的任务条模板,比默认的纯色进度块好看,也跟页面风格统一:

gantt.templates.task_text = function (start, end, task) { var percent = Math.round((task.progress || 0) * 100); return '<div style="line-height:18px; overflow:hidden;">' + '<span style="font-size:12px; display:block; padding:0 4px;">' + task.text + '</span>' + '<div class="progress" style="height:6px; margin:0 4px;">' + '<div class="progress-bar progress-bar-striped active" style="width:' + percent + '%;"></div>' + '</div>' + '</div>'; };

task_text 是任务条内容模板,默认只显示任务名,覆盖后返回什么就显示什么。progress 组件要压矮到 6px 左右才不会遮挡拖拽手柄,因为任务条默认高度有限,塞太多 DOM 会导致 resize 的手柄点不中。这里的模板在拖动任务条时也会跟随绘制,模板里不要写宽尺寸的绝对定位元素,否则拖拽时手柄经常被一层透明的 DOM 盖住。

左侧 layui table 如果想显示同样的进度条,可以在 table 的 templet 里复用这段 HTML,数据字段从行数据里读,两边的视觉就统一了。

表单字段读值方式写回 task 的转换
textdata.field.text直接赋值
start_datelaydate 回填字符串gantt.date.parseDate(value, "xml_date")
durationparseInt正整数天数
progressparseFloat / 1000~1 的小数

6. gantt 交付前自检:一段断言脚本和五档参数清单

甘特图在浏览器里能出图,和能交付给同事用,中间隔着一层数据完整性校验。我一般会在首次 render 后跑一段断言代码,把任务 ID 重复、依赖指向不存在的节点、进度越界、日期解析失败这四个最常见的坑一次暴露出来。

function checkGanttData() { var ids = {}; gantt.eachTask(function (task) { console.assert(task.id != null, "存在空 ID 任务"); if (ids[task.id]) { console.warn("ID 重复:", task.id); } ids[task.id] = true; console.assert( task.progress >= 0 && task.progress <= 1, "progress 越界:", task.text ); console.assert( !isNaN(task.start_date.getTime()), "start_date 非法:", task.text ); }); gantt.eachLink(function (link) { console.assert(ids[link.source], "link.source 缺失:", link.source); console.assert(ids[link.target], "link.target 缺失:", link.target); }); } gantt.attachEvent("onDataUpdated", function () { if (location.hostname === "localhost") { checkGanttData(); } });

onDataUpdated 在每次数据变更后触发,开发环境直接输出结果。控制台出现 assertion failed 时,优先查后端映射出来的字段名,而不是去 gantt 配置里找原因。生产环境把 console.assert 换成错误提示接口即可。

最后是一组容易散落在配置里的参数清单,示例包交付前逐条核对一遍,比临时翻文档快得多:

参数期望值
gantt.config.xml_date与 start_date 字符串完全匹配
task.progress0~1 浮点数
task.duration正整数,单位与 scales 一致
link.typefinish_to_start / start_to_start / finish_to_finish / start_to_finish
容器高度calc(100vh - 导航高度),保证非 0

最后一个交付技巧:如果甘特图被放在 bootstrap tab 或 layui tab 里,tab 切换后容器宽度从 0 变回正常宽度时,gantt 不会自动感知。在 tab 切换回调里执行 gantt.setSizes(),并在首次渲染后手动调用一次,任务条就不会全部挤在最左侧。把这条写进示例包的 README,能省下后续联调的大段排错时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询