免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

jQuery+Layui+Bootstrap整合dhtmlxgantt实现甘特图

jQuery+Layui+Bootstrap整合dhtmlxgantt实现甘特图 简介一套融合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_dateduration工期与时间刻度单位一致progress完成度取值 0~1不是百分比parent父任务 id根任务填 0links 里的 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 classnavbar navbar-default div classcontainer-fluid div classnavbar-header span classnavbar-brand任务进度看板/span /div /div /nav div classcontainer-fluid div classrow div classcol-md-4 table idtaskTable lay-filtertaskTable/table /div div classcol-md-8 div idgantt/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 改成 hourformat 改成 %H:%iduration 也按小时数给。参数作用常见取值fit_tasks时间轴自动适配任务范围true / falsemin_column_width最小列宽控制缩放下限24 ~ 60drag_move是否允许拖动任务条true / falsedrag_resize是否允许拖拽任务条两端调整工期true / falsefit_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 /bbr/ 开始: gantt.templates.tooltip_date_format(start) br/进度: ratio %; };tooltip_text 返回的是一个 HTML 字符串会直接插进页面。task.text 如果来自用户输入插入模板前必须做 HTML 转义否则用户把img srcx onerroralert(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 / noneorder_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 读取这段 HTMLscript typetext/html idtaskFormTpl form classlayui-form lay-filtertaskForm stylepadding:20px; div classlayui-form-item label classlayui-form-label任务名/label div classlayui-input-block input typetext nametext lay-verifyrequired classlayui-input/ /div /div div classlayui-form-item label classlayui-form-label开始时间/label div classlayui-input-block input typetext namestart_date idstartDate classlayui-input/ /div /div div classlayui-form-item label classlayui-form-label工期/label div classlayui-input-block input typenumber nameduration value1 min1 classlayui-input/ /div /div div classlayui-form-item label classlayui-form-label完成度/label div classlayui-input-block input typenumber nameprogress min0 max100 value0 classlayui-input/ /div /div div classlayui-form-item styletext-align:center; button lay-submit lay-filtertaskFormSubmit classlayui-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_dategantt 后续所有时间计算都要求 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 styleline-height:18px; overflow:hidden; span stylefont-size:12px; display:block; padding:0 4px; task.text /span div classprogress styleheight:6px; margin:0 4px; div classprogress-bar progress-bar-striped active stylewidth: 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能省下后续联调的大段排错时间。本文还有配套的精品资源点击获取
返回列表