使用 Vue 3 + TypeScript 集成 DHTMLX 甘特图
前言
在现代前端开发中,Vue 3 结合 TypeScript 是一个强大的组合,能够帮助开发者更好地构建类型安全、可扩展的应用。而 DHTMLX Gantt 作为一款高效的项目管理组件,可以轻松实现任务调度、时间跟踪等功能。本文将详细介绍如何在 Vue 3 项目中结合 TypeScript 和 DHTMLX 甘特图,实现一个项目管理系统的关键功能。
官网:https://docs.dhtmlx.com/gantt/
一、使用步骤
1.1 安装dhtmlx-gantt依赖
代码如下(示例):
npm install dhtmlx-gantt -save
1.2 在页面中引入插件
import {gantt} from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import demoData from './ganttData.json' //引入数据
二.页面代码
2.1 index.vue
代码如下(示例):
<template>
<div style="height:100%;background-color: white">
<section class="my-gantt">
<div class="time-box" >
<el-form :model="search" inline style="margin-left: 15px">
<el-form-item style="margin-left: 0px">
<el-radio-group v-model="fullData.timeState" @change="changeTime">
<el-radio-button
v-for="(time, t_index) in fullData.timeList"
:key="t_index"
:label="time.code"
size="small"
border
>{{ time.name }}
</el-radio-button
>
</el-radio-group>
</el-form-item>
<el-form-item>
<div style="height: 40px;position: relative;float: left;">
<el-tooltip content="已完成" placement="top">
<div
style="background-color:#BEE4BE;width: 14px;height: 14px;position: relative;float: left;top: 13px;border: 1px solid;">
</div>
</el-tooltip>
<el-tooltip content="进行中" placement="top">
<div
style="background-color:#ffd28f;width: 14px;height: 14px;position: relative;float: left;left: 15%;top: 13px;border: 1px solid;">
</div>
</el-tooltip>
<el-tooltip content="未开始" placement="top">
<div
style="background-color: #d4d4d4;width: 14px;height: 14px;position: relative;float: left;left: 30%;top: 13px;border: 1px solid;">
</div>
</el-tooltip>
</div>
</el-form-item>
</el-form>
</div>
<div id="gantt_here" class="gantt-container"></div>
</section>
</div>
</template>
2.2 typeScript
<script setup lang="ts">
import {ref, reactive, toRefs, onBeforeMount, onMounted, watchEffect, defineExpose,nextTick} from 'vue'
import { gantt } from 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'
import {Search, Plus,Edit,Delete,ArrowDownBold } from "@element-plus/icons-vue"
import demoData from "../progressManage/demoData.json"
//此方法是通过父子节点的关联性设置并且与时间相关联,父节点 设置 render: 'split'拆分子任务过程,子任务过程关联上父节点id(用parent)。可以通过设置子进程的开始结束时间或持续时间(duration)来设置子进程的长度。
// 注意id 的唯一性
const startWidth = ref(0);
const fullData = reactive({
timeList: [
{
name: '日',
code: 'day'
},
{
name: '周',
code: 'week'
},
{
name: '月',
code: 'month'
}
],
timeState: 'month',
demoData:{data:[]}
})
const zoomConfig = {
levels: [
{
name: 'day',
scale_height: 60,
min_column_width: 18,
scales: [
{ unit: 'month', format: '%Y-%m' },
{
unit: 'day',
step: 1,
format: '%d',
css: function (date) {
if (date.getDay() == 0 || date.getDay() == 6) {
return 'day-item weekend weekend-border-bottom'
} else {
return 'day-item'
}
}
}
]
},
{
name: 'week',
height: 60,
min_column_width: 110,
scales: [
{
unit: 'quarter',
step: 1,
format: function (date) {
let yearStr = new Date(date).getFullYear() + '年'
let dateToStr = gantt.date.date_to_str('%M')
let endDate = gantt.date.add(gantt.date.add(date, 3, 'month'), -1, 'day')
return yearStr + dateToStr(date) + ' - ' + dateToStr(endDate)
}
},
{
unit: 'week',
step: 1,
format: function (date) {
let dateToStr = gantt.date.date_to_str('%m-%d')
let endDate = gantt.date.add(date, 6, 'day')
let weekNum = gantt.date.date_to_str('%W')(date)
return dateToStr(date) + ' 至 ' + dateToStr(endDate)
}
}
]
},
{
name: 'month',
scale_height: 50,
min_column_width: 150,
scales: [
{ unit: 'year', step: 1, format: '%Y年' },
{ unit: 'month', format: '%Y-%m' }
]
}
]
}
//初始化甘特图
const initGantt = () => {
let dateToStr = gantt.date.date_to_str('%Y.%m.%d')
gantt.config.grid_width = 350
gantt.config.add_column = false //添加符号
//时间轴图表中,如果不设置,只有行边框,区分上下的任务,设置之后带有列的边框,整个时间轴变成格子状。
// gantt.config.autofit = true
// gantt.config.grid_width = 300
gantt.config.row_height = 34 // 行高
gantt.config.bar_height = 28
// gantt.config.fit_tasks = true //自动延长时间刻度,以适应所有显示的任务
gantt.config.auto_types = true //将包含子任务的任务转换为项目,将没有子任务的项目转换回任务
gantt.config.xml_date = '%Y.%m.%d' //甘特图时间数据格式
gantt.config.readonly = true //是否只读
gantt.config.show_errors = false //不显示错误信息
gantt.config.show_empty_state = true //显示空状态
//激活列表展开、折叠功能
gantt.config.open_split_tasks = true
//设置时间截止线
gantt.locale.labels.deadline_enable_button = 'Set';
gantt.locale.labels.deadline_disable_button = 'Remove';
gantt.attachEvent("onTaskLoading", function (task) {
if (task.end_date)
task.end_date = gantt.date.parseDate(task.end_date, "xml_date");
return true;
});
gantt.ext.draw = true
//进度条
gantt.templates.task_text = function (start, end, task) {
let progressState = ''
let progressPerson = ''
if (task.state == '1') { //未开始
progressState = 'lag'
}else if(task.state == '3'){ //进行中
progressState = 'ing'
}else if(task.state == '2'){
progressState = 'wanch' //已完成
}else{
progressState = 'normal'
}
return ` <div class="project-progress-${progressState}">${task.text}
</div>`
}
//表格配置
gantt.config.columns = [
{
name: "end_date", label: "", width: 30, template: function (obj) {
if(obj.skip==0){
if(obj.Long||obj.gap){
return `<el-tooltip content="${obj.text}"placement="top" effect="dark"><div class="overdue-indicator" ></div></el-tooltip>`;
}
}else if(obj.skip==1){
return `<el-tooltip content="跳过该节点"placement="top" effect="dark"><div class="overdue-indicator1" ></div></el-tooltip>`;
}
}
},
{
name: 'text',
label: '项目名称',
resize:true,
tree: true,
align:'left',
width: 250,
template: function (obj) {
return ` <el-tooltip content="${obj.text}"placement="bottom"> <span class="box-item">${obj.text}</span></el-tooltip>`
}
},
{
name: 'apply_unit',
label: '项目地',
resize:true,
align:'left',
width: 150,
template: function (obj) {
return `<el-tooltip content="${obj.apply_unit}"placement="bottom"> <span class="box-item">${obj.apply_unit}</span></el-tooltip>`
}
}
]
//更改树状的图标
// gantt.templates.grid_open = (item) => {
// // console.log('item', item)
// const res = "<div class='gantt_" +
// (item.$open ? "close" : "open") + "'></div>"
// return res
// }
//更改父项图标
gantt.templates.grid_folder = (item) => {
return ""
}
//更改子项图标
gantt.templates.grid_file = (item) => {
return ""
}
gantt.i18n.setLocale('ZN') //设置语言
gantt.ext.zoom.init(zoomConfig) //配置初始化扩展
gantt.ext.zoom.setLevel('month') //切换到指定的缩放级别x
// 开启marker插件
gantt.plugins({ marker: true, tooltip: true });
// const today = new Date(dayjs(new Date()).format("YYYY-MM-DD"));
// const dateToStr2 = gantt.date.date_to_str(gantt.config.task_date);
gantt.templates.tooltip_text=function (start,end,task) {
var tooltip = "<span>项目名称:</span>"+ task.text+"<br><span>开始时间:</span>"+dateToStr(task.start_date)+
"<br><span>结束时间:</span>"+dateToStr(task.end_date)
if(task.Long){
tooltip+=`<br><font color="red">预警信息:${task.Long}</font>`
}
if(task.gap){
tooltip+=`<br><font color="red">预警信息:${task.gap}</font>`
}
if(task.skip){
tooltip+=`<br><font color="blue">预警信息:跳过该节点</font>`
}
return tooltip
}
const todayMarker = gantt.addMarker({
start_date: new Date(),
css: "today", // 今日线注意定义在初始化和填充数据前
text: "今日",
title: new Date()
});
gantt.config.autofit = true
gantt.init('gantt_here') //初始化
gantt.parse(demoData) //填充数据
scrollInit()
gantt.showDate(new Date())
scrollInit()
gantt.showDate(new Date())
gantt.attachEvent("onGridHeaderClick",function (name,e) {
console.log("name",e,name)
})
}
//拖拽滚动
const scrollInit = () => {
const nav = document.querySelectorAll('.gantt_task')[0]
const parNav = document.querySelectorAll('.gantt_hor_scroll')[0]
parNav.scrollLeft = 0
let flag
let downX
let scrollLeft
nav.addEventListener('mousedown', function (event) {
flag = true
downX = event.clientX // 获取到点击的x下标
scrollLeft = this.scrollLeft // 获取当前元素滚动条的偏移量
})
nav.addEventListener('mousemove', function (event) {
if (flag) {
let moveX = event.clientX
let scrollX = moveX - downX
parNav.scrollLeft = scrollLeft - scrollX
}
})
// 鼠标抬起停止拖动
nav.addEventListener('mouseup', function () {
flag = false
})
// 鼠标离开元素停止拖动
nav.addEventListener('mouseleave', function (event) {
flag = false
})
}
const changeTime = () => {
gantt.ext.zoom.setLevel(fullData.timeState)
gantt.showDate(new Date())
}
//点击事件
gantt.attachEvent('onTaskClick',function (id, e,parent) {
// addResizeHandlers()
// console.log("e",e.target.parentElement.dataset)
// if(e.target.className === "gantt_task_content") { // 点击进度条
if(e.target.className=='project-progress-wanch'||e.target.className=='project-progress-ing'||e.target.className=='project-progress-lag'){
console.log("Task clicked:", id,e,parent)
queryProgress.findNode({id:id}).then(res=>{
nodeInfo.value = res[0]
nodeTitle.value = "节点信息【"+nodeInfo.value.node_name +"】"
reserveProject.getFileByID({id:id}).then(res=>{
myfileList.value =res
})
console.log("myfileList.value",myfileList.value)
nodeDetail.value = true
})
}else if(e.target.className=='project-progress-normal'){
console.log("aaa",id,e)
projectDetail.value = true
queryProgress.findProject({id:id}).then(res=>{
clickInfo.value = res[0]
projectTitle.value = "项目信息【"+clickInfo.value.project_name +"】"
})
}
else if(e.target.className=='gantt_open'||e.target.className=='gantt_close'){
console.log('e', e.target.className)
// 关闭所有任务
gantt.eachTask(function(task){
gantt.close(task.id);
});
//点击展开/收起按钮
switch (e.target.className) {
case 'gantt_open':
gantt.open(id)
break;
case 'gantt_close':
gantt.close(id)
break;
default:
break;
}
}
else{ //点击左侧表格定位到右边进度条
var task = gantt.getTask(id)
gantt.showTask(task.id)
return true
}
})
//点击网状表格
gantt.attachEvent('onTaskRowClick',function (id,row) {
console.log("点击",id,row)
})
//查看
const toPdf =(id) => {
loading1.value = true
previewMmgzWj({id:id}).then(res=>{
console.log("res",res)
dialogtitle.value = res[0].file_name
pdfUrl.value = res[0].url_yl
downloadUrl.value = res[0].url
pdfShow.value = true;
loading1.value = false
})
pdfHeight.value = window.innerHeight - 200 + 'px'
};
function getColumn() {
var col = 3
return col
}
function save() {
window.open(downloadUrl.value)
}
function print() {
window.open(pdfUrl.value)
}
onMounted(() => {
initGantt()
})
watchEffect(() => {})
defineExpose({
...toRefs(fullData)
})
</script>
2.3 css样式
代码如下(示例):
<style lang="scss" scoped>
.my-gantt {
height: 800px;
width: 100vw;
::v-deep .gantt-container {
border-radius: 8px 0px 0px 8px;
overflow: hidden;
height: 100%;
}
}
.el-form--inline .el-form-item {
margin-right: 10px !important;
margin-top: 10px !important;
}
.el-form-item__label {
font-weight: 500 !important;
}
.today1{
background: red;
color: red
}
//树状图标打开
::v-deep(.gantt_open) {
width: 12px !important;
height: 100%;
background-image: url('../../assets/left.png') !important;
background-repeat: no-repeat;
background-position: center center;
background-size: 100% auto;
margin-right: 5px;
transform: rotate(-90deg);//旋转90度
}
//树状图标关闭
::v-deep(.gantt_close) {
width: 12px !important;
height: 100%;
background-image: url('../../assets/left.png') !important ;
background-repeat: no-repeat;
background-position: center center;
background-size: 100% auto;
margin-right: 10px;
}
/*表头隐藏*/
::v-deep(.box-item ){
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
display: inline-block;
}
//任务条自定义内容
::v-deep(.project-progress-ing) {
background: #ffd28f !important;
color: black;
/*border: 2px solid #ffce2a !important;*/
}
::v-deep(.project-progress-warn) {
background: #ff8f8f !important;
}
::v-deep(.project-progress-wanch ){
background: #BEE4BE !important;
/*font-weight: bolder;*/
/*border: 2px solid #8fe48c !important;*/
color: black;
/*font-size: 16px;*/
}
::v-deep(.project-progress-lag){
background: #d4d4d4 !important;
color: black;
}
::v-deep(.project-progress-normal){
background: #7ea7f5 !important;
color: black;
}
.deadline {
position: absolute;
border-radius: 12px;
border: 2px solid #585858;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 22px;
height: 22px;
margin-left: -11px;
margin-top: 6px;
z-index: 1;
background: url("../../assets/warning.png") center no-repeat;
}
::v-deep(.overdue-indicator) {
width: 20px !important;
height: 100%;
background: url("../../assets/warning.png");
background-repeat: no-repeat;
background-position: center center;
background-size: 100% auto;
}
::v-deep(.overdue-indicator1) {
width: 20px !important;
height: 100%;
background: url("../../assets/skip1.png");
background-repeat: no-repeat;
background-position: center center;
background-size: 100% auto;
}
/*::v-deep(.tooltip-line){*/
/*color:red !important;*/
/*background: red !important;*/
/*}*/
::v-deep(.gantt_task_line) {
background-color: rgba(0,0,0,0) !important;
border: 1px solid rgba(0,0,0,0) !important;
}
::v-deep(.gantt_task_line.gantt_project) {
background-color: #fff !important;
border: 1px solid #fff !important;
}
//设置任务条高度,以及圆角
::v-deep(.gantt_task_content) {
color: #fff;
top: 0px;
height: 16px;
border-radius: 50px;
}
//调整字体高度,任务条距上边框的距离
::v-deep( .gantt_data_area div, .gantt_grid div) {
-ms-touch-action: none;
-webkit-tap-highlight-color: rgba(0,0,0,0);
line-height: 18px;
top: 5px;
}
</style>
2.4 demoData.json数据
代码如下(示例):
{
"data": [
{
"apply_unit": "成都",
"end_date": "2024-12-31",
"parent": "0",
"id": "001",
"text": "项目1",
"type": "project",
"render": "split",
"open": false,
"start_date": "2024-01-01"
},
{
"apply_unit": "深圳",
"end_date": "2024-12-31",
"parent": "0",
"id": "002",
"text": "项目1",
"type": "project",
"render": "split",
"open": false,
"start_date": "2024-01-01"
},
{
"apply_unit": "广东",
"end_date": "2025-10-01",
"parent": "0",
"id": "003",
"text": "项目3",
"type": "project",
"render": "split",
"open": false,
"start_date": "2024-01-01"
},
{
"apply_unit": "北京",
"end_date": "2024-12-30",
"parent": "0",
"id": "004",
"text": "项目4",
"type": "project",
"render": "split",
"open": false,
"start_date": "2024-01-01"
},
{
"apply_unit": "天津",
"end_date": "2024-06-30",
"parent": "0",
"id": "005",
"text": "项目5",
"type": "project",
"render": "split",
"open": false,
"start_date": "2024-01-01"
},
{
"apply_unit": "山东",
"end_date": "2024-06-30",
"parent": "0",
"id": "006",
"text": "项目6",
"type": "project",
"render": "split",
"open": false,
"start_date": "2024-01-01"
},
{
"apply_unit": "广西",
"end_date": "2024-06-30",
"parent": "0",
"id": "007",
"text": "项目7",
"type": "project",
"render": "split",
"open": false,
"start_date": "2024-01-01"
},
{
"id": "0011",
"text": "项目1节点1",
"parent": "001",
"start_date": "2024-01-01",
"end_date": "2024-02-21",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 2,
"skip": 0,
"apply_unit": "成都",
"open": false
},
{
"id": "0012",
"text": "项目1节点2",
"parent": "001",
"start_date": "2024-02-23",
"end_date": "2024-03-31",
"plan_start_date": "20240331",
"plan_end_date": "20240531",
"state": 2,
"skip": 0,
"apply_unit": "成都",
"open": false,
"gap": "该节点与前一节点时间间距过长"
},
{
"id": "0013",
"text": "项目1节点3",
"parent": "001",
"start_date": "2024-04-01",
"end_date": "2024-06-30",
"plan_start_date": "20240314",
"plan_end_date": "20241231",
"state": 2,
"skip": 0,
"apply_unit": "成都",
"open": false,
"Long": "该节点耗时过长",
"gap": "该节点与前一节点时间间距过长"
},
{
"id": "0014",
"text": "项目1节点4",
"parent": "001",
"start_date": "2024-07-01",
"end_date": "2024-10-24",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 3,
"apply_unit": "成都",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0015",
"text": "项目1节点5",
"parent": "001",
"start_date": "2024-10-01",
"end_date": "2024-11-21",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"apply_unit": "成都",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0016",
"text": "项目1节点6",
"parent": "001",
"start_date": "2024-11-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"apply_unit": "成都",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0021",
"text": "项目2节点1",
"parent": "002",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"apply_unit": "广东",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0022",
"text": "项目2节点2",
"parent": "002",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"apply_unit": "广东",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0023",
"text": "项目2节点3",
"parent": "002",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"apply_unit": "广东",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0024",
"text": "项目2节点4",
"parent": "002",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"skip": 0,
"apply_unit": "广东",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0025",
"text": "项目2节点5",
"parent": "002",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"skip": 0,
"apply_unit": "广东",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0026",
"text": "项目2节点6",
"parent": "002",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"skip": 0,
"apply_unit": "广东",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0031",
"text": "项目3节点1",
"parent": "003",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"apply_unit": "深圳",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0032",
"text": "项目3节点2",
"parent": "003",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"apply_unit": "深圳",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0033",
"text": "项目3节点3",
"parent": "002",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"apply_unit": "深圳",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0034",
"text": "项目3节点4",
"parent": "003",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"skip": 0,
"apply_unit": "深圳",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0035",
"text": "项目3节点5",
"parent": "003",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"skip": 0,
"apply_unit": "深圳",
"open": false,
"Long": "该节点耗时过长"
},
{
"id": "0036",
"text": "项目3节点6",
"parent": "003",
"start_date": "2024-01-01",
"end_date": "2024-12-31",
"plan_start_date": "20240101",
"plan_end_date": "20241231",
"state": 1,
"skip": 0,
"apply_unit": "深圳",
"open": false,
"Long": "该节点耗时过长"
}
]
}