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

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 = true
          initGantt(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>
相关推荐
鸿蒙布道师28 分钟前
鸿蒙NEXT开发动画案例5
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
a濯6 小时前
element plus el-table多选框跨页多选保留
javascript·vue.js
橙子199110166 小时前
在 Kotlin 中什么是委托属性,简要说说其使用场景和原理
android·开发语言·kotlin
androidwork6 小时前
Kotlin Android LeakCanary内存泄漏检测实战
android·开发语言·kotlin
笨鸭先游7 小时前
Android Studio的jks文件
android·ide·android studio
gys98957 小时前
android studio开发aar插件,并用uniapp开发APP使用这个aar
android·uni-app·android studio
像风一样自由7 小时前
【001】renPy android端启动流程分析
android·gitee
CodeCraft Studio7 小时前
数据透视表控件DHTMLX Pivot v2.1发布,新增HTML 模板、增强样式等多个功能
前端·javascript·ui·甘特图
llc的足迹7 小时前
el-menu 折叠后小箭头不会消失
前端·javascript·vue.js