甘特图是项目管理系统中最常见的可视化组件之一,它把任务的起止时间、工期进度和依赖关系放到一条时间轴上展示。dhtmlx-gantt 是一款功能成熟的纯 JavaScript 甘特图库,支持任务条拖拽、缩放、依赖连线等丰富交互。但它本身并不是为 Vue 设计的,直接在 Vue 3 项目里使用会遇到实例初始化时机、响应式数据同步、依赖更新触发等一系列问题。本文将从组件封装和任务依赖管理两个核心方向,详细讲解如何在 Vue 3 中稳定地使用 dhtmlx-gantt。

一、Vue 3 组件封装:把 dhtmlx-gantt 变成可复用的 SFC
dhtmlx-gantt 的标准用法是在页面加载后调用 gantt.init 方法,把图表渲染到一个 DOM 容器里。这套命令式逻辑和 Vue 的声明式渲染是冲突的:如果直接把任务数据放进 ref,Vue 的响应式代理会包裹整个数据对象,dhtmlx 内部在遍历数据时会读到 Proxy 对象,某些版本下会出现性能下降甚至渲染异常。因此封装的第一原则是:容器内部维护一份数据副本,通过普通变量或 shallowRef 持有,避免深度响应式污染。
第二个要点是生命周期。甘特图实例必须在 DOM 容器挂载完成后初始化,并且要在组件卸载时清理,否则在路由切换反复进出页面时会造成内存泄漏和事件重复绑定。下面是一个可直接使用的封装骨架,基于 setup 语法糖编写:
<template>
<div ref="ganttContainer" class="gantt-container"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
const props = defineProps({
tasks: { type: Object, required: true },
links: { type: Array, default: () => [] }
})
const emit = defineEmits(['task-updated', 'link-updated'])
const ganttContainer = ref(null)
onMounted(() => {
gantt.init(ganttContainer.value)
gantt.parse({ data: props.tasks.data, links: props.links })
bindEvents()
})
onUnmounted(() => {
gantt.clearAll()
})
function bindEvents() {
gantt.attachEvent('onAfterTaskDrag', (id, mode) => {
const task = gantt.getTask(id)
emit('task-updated', { id, start_date: task.start_date, duration: task.duration })
})
gantt.attachEvent('onAfterLinkAdd', (id, link) => {
emit('link-updated', { type: 'add', link })
})
}
watch(() => props.tasks, (val) => {
gantt.clearAll()
gantt.parse({ data: val.data, links: props.links })
}, { deep: true })
</script>
<style scoped>
.gantt-container {
width: 100%;
height: 600px;
}
</style>
这个封装有几个细节值得注意。第一,事件监听采用 attachEvent 挂载,在拖拽结束后把变更抛给父组件,由父组件决定是否落库,这样图表面板和数据层职责分明。第二,watch 监听任务数据变化后调用 clearAll 再重新 parse,这是最稳妥的刷新方式,虽然牺牲了一点性能,但可以保证状态完全一致。如果任务量超过几千条,建议改用 gantt.render 做局部刷新,或者开启智能渲染模式,把 gantt.config.smart_rendering 设为 true,只渲染可视区域内的任务行。
二、任务依赖管理的实现:links 数据结构详解
dhtmlx-gantt 用 links 数组描述任务间的依赖关系,每条 link 包含 source、target 和 type 三个核心字段。type 决定了依赖的语义:finish_to_start 表示前置任务结束后置任务才能开始,这是最常用的类型;start_to_start 表示两个任务同时开始;finish_to_finish 表示同时结束;start_to_finish 则表示前置任务开始后,后置任务必须已结束,实践中很少用到。
一条典型的依赖数据如下:前置任务 id 为 1 的任务完成后,id 为 2 的任务才能启动。此时图表上会自动画出一条从任务 1 指向任务 2 的连线,用户也可以直接在图上拖拽任务条边缘创建新的依赖:
const tasks = {
data: [
{ id: 1, text: '需求评审', start_date: '2024-03-01', duration: 3, progress: 1 },
{ id: 2, text: '接口开发', start_date: '2024-03-04', duration: 5, progress: 0.6 },
{ id: 3, text: '联调测试', start_date: '2024-03-09', duration: 4, progress: 0 }
],
links: [
{ id: 1, source: 1, target: 2, type: '0' }, // finish_to_start
{ id: 2, source: 2, target: 3, type: '0' }
]
}
除了用 links 数组,dhtmlx 还支持在任务对象上直接声明 predecessor 字段,写成类似 1FS+2 的格式,意思是任务 1 结束后再加两天启动。这种方式适合从 MS Project 之类的工具导入数据,但项目内交互通常还是以 links 为主,因为用户在图上拖出来的连线最终都会落到 links 数组里。两种方式不要混用,否则约束逻辑会重复计算,排期结果容易出现一天左右的偏差。
三、循环依赖校验:防止调度死锁
当用户可以自由在图上连线时,就可能出现 A 依赖 B、B 依赖 C、C 又依赖 A 的环。dhtmlx 内置的调度器遇到循环依赖时行为不可控,轻则日期计算错误,重则浏览器卡死。所以在 onBeforeLinkAdd 事件里做一次环检测是必须的。实现思路很直接:从新 link 的 target 出发做深度优先搜索,如果能够走到 source,说明加上这条边必然成环,直接 return false 阻止创建。
gantt.attachEvent('onBeforeLinkAdd', (id, link) => {
// 不允许任务依赖自己
if (link.source === link.target) return false
return !wouldCreateCycle(link.source, link.target, currentLinks())
})
function currentLinks() {
return gantt.getLinks().map(l => ({
source: l.source,
target: l.target
}))
}
function wouldCreateCycle(source, target, links) {
// 若从 target 出发能沿依赖走到 source,则加这条边会成环
const graph = new Map()
links.forEach(l => {
if (!graph.has(l.source)) graph.set(l.source, [])
graph.get(l.source).push(l.target)
})
const visited = new Set()
const stack = [target]
while (stack.length) {
const node = stack.pop()
if (node === source) return true
if (visited.has(node)) continue
visited.add(node)
;(graph.get(node) || []).forEach(n => stack.push(n))
}
return false
}
环检测的时间复杂度是 O(V+E),对于常规项目规模的任务数量来说完全够用。如果项目任务数上万,可以把邻接表缓存起来,只在 links 变化时重建。除了环检测,还建议在业务层补充依赖类型合法性校验,比如不允许给已完成的任务添加 start_to_start 类型的约束,这类规则 dhtmlx 不会替你做,需要自己在 onBeforeLinkAdd 里拦截。
四、自动排程与手动排程的选择
处理依赖的核心配置是 gantt.config.auto_scheduling。开启后,当用户拖动前置任务的结束日期,所有后置任务会按依赖关系自动顺延,这对计划阶段的排期非常有用。配套的 auto_scheduling_strict 可以禁止用户把任务拖到违反依赖的位置,auto_scheduling_progress 则会让已完成的任务保持位置不动,只调整未开始的任务。
gantt.config.auto_scheduling = true gantt.config.auto_scheduling_strict = true gantt.config.auto_scheduling_progress = true gantt.config.schedule_from_end = false // 从项目起点正向排程
但自动排程并不是万能的。在执行阶段的复盘场景里,任务的实际日期是既定事实,自动顺延会不断改写历史记录,这时应该关闭自动排程,把依赖冲突用标记角标的方式提示出来,让项目经理人工决策。一个折中的做法是保留自动排程开关,但在保存到后端前再做一遍完整校验,同时对每条被自动调整的任务记录一条变更日志,方便追溯排期变化的原因,这对多人协作的项目尤其重要。
总结来看,在 Vue 3 中落地 dhtmlx-gantt 的关键有三点:用非响应式副本隔离数据、用事件抛出替代双向绑定、在依赖创建前做好环检测。把这三层做扎实,剩下的缩放、列配置、皮肤定制都是配置层面的小事情。另外提醒一点,dhtmlx-gantt 的 GPL 版本在闭源商用场景下有授权限制,正式项目上线前记得评估商业授权的成本。
Vue 3dhtmlx-gantt任务依赖修改时间:2026-09-03 06:32:56