当前位置: 首页 > news >正文

vue3+dhtmlx-gantt实现甘特图展示

最终效果

在这里插入图片描述

数据源demo

{"data": [{"actual_end_date": "2025-04-23","actual_start_date": "2025-04-15","duration": 10,"end_date": "2025-05-01","id": "2|jvUiek","parent": "0","progress": 0,"start_date": "2025-04-21","text": "2","time": -8,"responsible_person": "1|管理员","milestone_node": null},{"actual_end_date": "2025-04-24","actual_start_date": "2025-04-21","duration": 1,"end_date": "2025-04-22","id": "1|rMyrYE","parent": "0","progress": 0,"start_date": "2025-04-21","text": "1","time": 2,"responsible_person": "1|管理员","milestone_node": null},{"actual_end_date": null,"actual_start_date": "2025-05-06","duration": 7,"end_date": "2025-05-02","id": "1","parent": "1|rMyrYE","progress": 0.11,"start_date": "2025-04-25","text": "节点2","time": 6,"responsible_person": "1|管理员","milestone_node": "0"},{"actual_end_date": null,"actual_start_date": null,"duration": 2,"end_date": "2025-04-29","id": "3","parent": "1","progress": 0,"start_date": "2025-04-27","text": "节点1","time": 9,"responsible_person": "1|管理员","milestone_node": "1"},{"actual_end_date": null,"actual_start_date": null,"duration": 33,"end_date": "2025-05-30","id": "4","parent": "1","progress": 0,"start_date": "2025-04-27","text": "节点2","time": -22,"responsible_person": "1|管理员","milestone_node": "0"},{"actual_end_date": null,"actual_start_date": null,"duration": 1,"end_date": "2025-05-02","id": "2","parent": "1|rMyrYE","progress": 0,"start_date": "2025-05-01","text": "节点1","time": 6,"responsible_person": "1|管理员","milestone_node": "1"}]
}
{"data": [{"actual_end_date": "2025-04-23","actual_start_date": "2025-04-15","duration": 10,"end_date": "2025-05-01","id": "2|jvUiek","parent": "0","progress": 0,"start_date": "2025-04-21","text": "2","time": -8,"responsible_person": "1|管理员","milestone_node": null},{"actual_end_date": "2025-04-24","actual_start_date": "2025-04-21","duration": 1,"end_date": "2025-04-22","id": "1|rMyrYE","parent": "0","progress": 0,"start_date": "2025-04-21","text": "1","time": 2,"responsible_person": "1|管理员","milestone_node": null},{"actual_end_date": null,"actual_start_date": "2025-05-06","duration": 7,"end_date": "2025-05-02","id": "1","parent": "1|rMyrYE","progress": 0.11,"start_date": "2025-04-25","text": "节点2","time": 6,"responsible_person": "1|管理员","milestone_node": "0"},{"actual_end_date": null,"actual_start_date": null,"duration": 2,"end_date": "2025-04-29","id": "3","parent": "1","progress": 0,"start_date": "2025-04-27","text": "节点1","time": 9,"responsible_person": "1|管理员","milestone_node": "1"},{"actual_end_date": null,"actual_start_date": null,"duration": 33,"end_date": "2025-05-30","id": "4","parent": "1","progress": 0,"start_date": "2025-04-27","text": "节点2","time": -22,"responsible_person": "1|管理员","milestone_node": "0"},{"actual_end_date": null,"actual_start_date": null,"duration": 1,"end_date": "2025-05-02","id": "2","parent": "1|rMyrYE","progress": 0,"start_date": "2025-05-01","text": "节点1","time": 6,"responsible_person": "1|管理员","milestone_node": "1"}]
}

index.vue

<template><div><div v-loading="loading" ref="ganttRef"></div></div>
</template><script setup lang='ts'>
import {gantt} from 'dhtmlx-gantt';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
import {onBeforeUnmount} from 'vue';
import demoData from './ganttData.json'
import {ElLoading} from "element-plus";const props = defineProps({taskData: {type: Array,default: () => []}
});const loading = ref(false);
const ganttRef = ref(null);// 通过taskData计算links数组
const generateLinks = (data: any) => {const links: any = [];data.forEach((task: any) => {if (task.parent) {const linkId = `${task.parent}-${task.id}`;links.push({id: linkId,source: task.parent,target: task.id,type: '0'});}});return links;
};const initGantt = (task: any) => {let loading = ElLoading.service({target: '.dialogLoading',lock: true,background: 'rgba(0, 0, 0, 0.3)',text: '甘特图渲染中...',});gantt.clearAll();gantt.i18n.setLocale('cn');gantt.config.columns = [{name: 'text',label: '项目名称',resize: true,tree: true,width: '150',// 自定义左侧的数据,比如有的任务是里程碑节点,在这里加个小旗子的图标template: (item) => {const milestoneIcon = item.milestone_node == '0' ?`<svg xmlns="http://www.w3.org/2000/svg"width="14"height="14"viewBox="0 0 24 24"style="vertical-align: middle; margin-right: 6px;"><path fill="#ff4d4f" d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/></svg>`: '';return `<div class="text_cell" style="display: flex; align-items: center;">${milestoneIcon}<span class="text-cell-text">${item.text}</span></div>`;},},{name: 'start_date',label: '计划开始时间',align: 'center',resize: true,tree: false,width: 110,},{name: 'end_date',label: '计划结束时间',width: 110,align: 'center',resize: true,},{name: 'actual_start_date',label: '实际开始时间',align: 'center',resize: true,tree: false,width: 110,},{name: 'actual_end_date',label: '实际结束时间',width: 110,align: 'center',resize: true,},{name: 'time',label: '时间差值',width: 60,align: 'center',resize: true,template: (item) => {const time = Number(item.time);const statusClass = time >= 0 ? 'negative' : 'positive';return `<div class="status-cell"><span class="status-text ${statusClass}">${time}</span></div>`;},},{name: 'progress',label: '项目进度',align: 'center',tree: false,resize: true,width: '100',template: (item) => {return `<div class="status-cell"><span class="status-text">${(item.progress * 100).toFixed(2)}%</span></div>`;},},];gantt.plugins({tooltip: true});gantt.config.smart_rendering = true;gantt.config.auto_scheduling = true;gantt.config.auto_scheduling_strict = true;gantt.config.show_grid = true;gantt.config.xml_date = '%Y-%m-%d';gantt.config.autosize = true;gantt.config.show_links = true;gantt.config.readonly = true;gantt.config.sort = true;gantt.config.drag_project = true;gantt.config.scale_height = 50;gantt.config.open_split_tasks = true;gantt.config.show_tasks_outside_timescale = true;// 鼠标悬浮提示框gantt.templates.tooltip_text = function (start, end, task) {// console.log('start----------->', start)// console.log('end----------->', end)// console.log('task----------->', task)return ('<b>标题:</b> ' +task.text +'<br/><span>开始:</span> ' +gantt.templates.tooltip_date_format(start) +'<br/><span>结束:</span> ' +gantt.templates.tooltip_date_format(end) +'<br/><span>进度:</span> ' +Math.round(task.progress * 100) +'%');};gantt.config.tooltip_offset_x = 10;gantt.config.tooltip_offset_y = 30;gantt.init(ganttRef.value);gantt.parse(task);loading.close();// 设置鼠标悬浮与之关联的线产生高亮效果let lastLinkIds: string[] = [];gantt.event(gantt.$task_data, 'mousemove', (e: MouseEvent) => {const taskId = gantt.locate(e);if (taskId && gantt.isTaskExists(taskId)) {// 找到所有与当前任务关联的链接const related = gantt.getLinks().filter(l => l.source==taskId || l.target==taskId);const newLinkIds = related.map(l => l.id);// 1) 移除上次高亮中,本次不再关联的lastLinkIds.filter(id => !newLinkIds.includes(id)).forEach(id => {const el = gantt.getLinkNode(id);el && el.style.removeProperty('--dhx-gantt-link-background');});// 2) 给本次所有关联加高亮newLinkIds.filter(id => !lastLinkIds.includes(id)).forEach(id => {const el = gantt.getLinkNode(id);el && el.style.setProperty('--dhx-gantt-link-background', '#ffb84d');});// 更新记录lastLinkIds = newLinkIds;return;}// 鼠标不在任务上时,清除所有高亮lastLinkIds.forEach(id => {const el = gantt.getLinkNode(id);el && el.style.removeProperty('--dhx-gantt-link-background');});lastLinkIds = [];});// 在配置项中添加任务颜色模板(在 initGantt 函数中添加)gantt.templates.task_class = function (start, end, task) {// return task.time < 0 ? "positive-task" : "negative-task";if (task.actual_end_date) {if (task.time <= 0) {return "tqTask";  // 提前} else if (task.time > 0) {return "cqTask"; // 超期} else {return "style0";}} else {  // 如果没有实际结束时间,则根据当前时间判断任务状态const currentTime = new Date();if (currentTime > end) {return "cqTask"; // 超期} else {return "style0";}}};// 设置任务条上展示的内容,参数task会返回当前任务的数据gantt.templates.task_text = function (start, end, task) {// console.log('task--111--------->', task)return task.text + "-" + (task.responsible_person ? task.responsible_person.split('|')[1] : '');};// 遍历所有任务并展开父任务gantt.eachTask(function (task) {if (gantt.hasChild(task.id)) {gantt.open(task.id);  // 展开任务}});gantt.attachEvent('onBeforeTaskChanged', (id, mode, task) => {console.log('Before task changed:', task);return true;});gantt.attachEvent('onAfterTaskDrag', (id, mode, e) => {const task = gantt.getTask(id);gantt.resetLayout();});gantt.attachEvent('onAfterTaskUpdate', (id, item) => {console.log('After task update:', item)let links = gantt.getLinks();let end_date = item.end_date.getTime();const endDateCache = {};const updatedTasks = [];links.forEach(link => {const sourceId = link.source + '';if (!endDateCache[sourceId]) {endDateCache[sourceId] = gantt.getTask(sourceId).end_date.getTime();}});let findlinks = links.filter((link) => link.source + '' === id + '');findlinks.forEach((link) => {let linktask = gantt.getTask(link.target);let sourceList = links.filter((linkInfo) => linkInfo.target + '' === linktask.id + '');let linkDate = end_date;sourceList.forEach((s) => {let sourceTask = gantt.getTask(s.source);let time = sourceTask.end_date.getTime();if (linkDate < time) {linkDate = time;}});let taskEndDate = new Date().getTime();let days = linktask.duration * 3600000 * 24;taskEndDate = linkDate + days;let taskInfo = {duration: linktask.duration,id: linktask.id,open: linktask.open,parent: linktask.parent,progress: linktask.progress,text: linktask.text,type: linktask.type,color: linktask.color,start_date: new Date(linkDate),end_date: new Date(taskEndDate),};updatedTasks.push(taskInfo);});updatedTasks.forEach(taskInfo => {gantt.updateTask(taskInfo.id, taskInfo);});return true;});
};// 监听数据的变化,可以在这里将demoData换成真实的数据,但是格式得和demoData一样
watch(() => props.taskData,(newValue) => {if (newValue) {const taskLinks = generateLinks(demoData.data);const task = {data: newValue,demoData.data, // 测试links: taskLinks};nextTick(() => {loading.value = trueinitGantt(task);loading.value = false});}}
);// 销毁甘特图实例
onBeforeUnmount(() => {gantt.clearAll()
})
</script><style scoped lang="scss">
.gantt-container {width: 100%;height: 100%;
}.status-text {font-weight: bold;
}::v-deep(.positive) {color: #67C23A !important;
}::v-deep(.negative) {color: #F56C6C !important;
}:deep(.gantt_task_line) {&.style0 {border: 1px solid #409EFF;background: #409EFF;}&.tqTask {border: 1px solid #67C23A;background: #67C23A;}&.cqTask {border: 1px solid #F56C6C;background: #F56C6C;}
}::v-deep .text_cell {display: flex;align-items: center;
}::v-deep .text-cell-text {overflow: hidden;text-overflow: ellipsis;
}/* 自定义图标颜色 */
::v-deep .text_cell img {filter: hue-rotate(120deg); /* 调整颜色 */
}.gantt_task_line {&.highlighted {border: 2px solid #ff4d4f;background-color: rgba(255, 77, 79, 0.2);}
}.gantt_task_link.highlighted-link .gantt_line_wrapper div {background-color: #ff4d4f;box-shadow: 0 0 5px 0 #ff4d4f;
}
.gantt_task_link.highlighted-link .gantt_link_arrow_left,
.gantt_task_link.highlighted-link .gantt_link_arrow_right {border-left-color: #ff4d4f !important;border-right-color: #ff4d4f !important;
}::v-deep(.gantt_task_link.highlight-link) {--dhx-gantt-link-background: #ff4d4f !important;
}
</style>

相关文章:

  • 前端项目2-01:个人简介页面
  • 使用 DMM 测试 TDR
  • openpi π₀ 项目部署运行逻辑(一)——综述
  • WebGIS开发新突破:揭秘未来地理信息系统的神秘面纱
  • OpenHarmony 开源鸿蒙南向开发——linux下使用make交叉编译第三方库——gnutls
  • Linux512 ssh免密登录 ssh配置回顾
  • 容器化-Docker-私有仓库Harbor
  • 因子分析基础指南:原理、步骤与地球化学数据分析应用解析
  • fetch post请求SSE「eventsource-parser/stream」
  • 解决 CJSON 浮点数精度问题:从 `cJSON_AddNumberToObject` 到 `cJSON_AddRawToObject`
  • 大项目k8s集群有多大规模,多少节点,有多少pod
  • 基于 Cursor + 浏览器MCP服务 实现 Web端自动化测试
  • 【Dv3Admin】工具数据验证配置文件解析
  • Python-Flask-Dive
  • mapbox进阶,使用mapbox-plugins插件加载饼状图
  • 【Python】Python常用数据类型详解
  • 一周学完计算机网络之三:1、数据链路层概述
  • 安装Hadoop并运行WordCount程序
  • ACL访问控制列表:access-list 10 permit 192.168.10.1
  • MySQL-逻辑架构
  • 5月12日-14日,上海小升初民办初中进行网上报名
  • 左娅︱悼陈昊
  • 中国工程院院士、国医大师石学敏逝世
  • 种罂粟喂鸡防病?四川广元一村民非法种植毒品原植物被罚​
  • 一生要出片的年轻人,买爆相机
  • 海南省三亚市委原常委、秘书长黄兴武被“双开”