前端之甘特图-dhtmlx-gantt

12,571 阅读3分钟

背景

最近遇到一个业务需求,是在开发管理后台时候,需要有用到日历以及甘特图去展示管理任务相关视图,因此进行相关甘特图的前端生成的调研-说是调研,不如说是找了一个半开源的库(但不得不说,这个库,很强)。 具体场景如图:

image.png

功能分析

  1. 基础元素:左侧任务树 & 右侧图例任务 Progress
  2. 新增任务
  3. 删除任务
  4. 编辑任务
  5. ...

Gantt 的 NPM地址 docs.dhtmlx.com

官网 Gannt

开发 Demo

1. 安装

npm i dhtmlx-gantt

2. 组件导入

    <script lang="ts">
    import { defineComponent, onMounted, ref } from 'vue';
    import { gantt } from 'dhtmlx-gantt'; // 核心模块
    import 'dhtmlx-gantt/codebase/dhtmlxgantt.css'; // 样式模块
    ....
    </script>

3. 准备引入DOM

    <template>
        <div class="gantt-no" ref="ganttRef"></div>
    </template>
    <script>
        setup() {
            const ganttRef = ref<HTMLElement | null>(null);
            ...
            return {
                ganttRef
            }
        }
    </script>

4. 准备 MOCK 数据

    const tasks = {
        data: [
            { id: 1, text: '任务 1', start_date: '2021-10-17', duration: 3, progress: 0.6 },
            { id: 2, text: '任务 2', start_date: '2021-10-20', duration: 10, progress: 0.4 }
        ],
        links: []
    }

参数简析:

  • 整体数据是以对象的形式存放,其中的data是一个 Task[],links是任务连线,其结构是 Link[]
  • 单个 Task 可能包含以下的字段
    • id: 任务唯一标识
    • text: 任务名称
    • start_date: 任务开始时间
    • duration: 任务时长
    • progress: 任务进度
    • parent: 父级的ID(树结构关系)
    • ... 其他参数要查阅其官方文档
  • 单个 Link 可能包含:
    • id: 连线的唯一标识
    • source: 源节点
    • target: 目标节点
    • type: 连线类型(0|1|2)标识是否有箭头

5. 初始化以及传入 tasks

onMounted((0 => {
    if (ganttRef.value) {
        gantt.init(ganttRef.value); // 初始化 DOM
        gantt.parse(tasks); // 传入 tasks
    }
}) 

6. 配置图例参数

  • 禁用连线 (本需求是不需要连线功能的)
    gantt.config.show_links = false;
  • 禁用工作进度拖拽 (必须通过界面弹窗的方式进行修改信息)
    gantt.config.drag_progress = false;
  • 设置任务分段参数以及单位
    gantt.config.duration_unit = 'day';
    gantt.config.duration_step = 1;
  • 配置左侧表格栏目
    gantt.config.columns = [
      {
        name: 'text',
        label: '任务名称',
        tree: true,
        width: '*',
        align: 'left',
        template: function (obj: any) {
          return obj.text;
        }
      },
      {
        name: 'start_date',
        label: '时间',
        width: '*',
        align: 'center',
        template: function (obj: any) {
          return obj.start_date;
        }
      },
      {
        name: 'progress',
        label: '进度',
        width: '*',
        align: 'center',
        template: function (obj: any) {
          return `${obj.progress * 100}%`;
        }
      }
    ];
    参数简析: **ColumsItem[]**
    1. name: 'text' [String] , 索取的 tasks 里 **Task[]** 的 Task 的属性
    2. label: 'xxx' [String], 当前栏显示的文本
    3. tree: true [Boolean],当前的任务是否为树结构这样
    4. align: [String: left|right|center],label文本位置属性
    5. template: [Function],函数类型,入参是 obj,即为当前的 Task 对象
    6. ... 其他参数要查阅文档
  • 配置右侧表头日期栏
    gantt.config.xml_date = '%Y-%m-%d'; // 日期格式化的匹配格式
    gantt.config.scale_height = 90; // 日期栏的高度 
    const weekScaleTemplate = function (date: any) {
        const mouthMap: { [key: string]: string } = {
            Jan: '一月',
            Feb: '二月',
            Mar: '三月',
            Apr: '四月',
            May: '五月',
            Jun: '六月',
            Jul: '七月',
            Aug: '八月',
            Sept: '九月',
            Oct: '十月',
            Nov: '十一月',
            Dec: '十二月'
        };
        // 可以时使用dayjs 处理返回
        const dateToStr = gantt.date.date_to_str('%d');
        const mToStr = gantt.date.date_to_str('%M');
        const endDate = gantt.date.add(gantt.date.add(date, 1, 'week'), -1, 'day');
          // 处理一下月份
         return `${dateToStr(date)} 号 - ${dateToStr(endDate)} 号 (${
            mouthMap[mToStr(date) as string]
        })`;
    };
    const dayFormat = function (date: any) {
        const weeks = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
        return weeks[Dayjs(date).day()];
    };
    gantt.config.scales = [
        { unit: 'year', step: 1, format: '%Y' },
        { unit: 'week', step: 1, format: weekScaleTemplate },
        { unit: 'day', step: 1, format: dayFormat }
    ];

  • 添加今日的 Marker Line
    gantt.plugins({
        marker: true
    });
    gantt.addMarker({
        start_date: new Date(),
        text: '今日'
    });

image.png

任务菜单以及事件

  • 右键菜单功能
// menu.vue
<template>
  <div class="menu" :style="{ left: x + 'px', top: y + 'px' }">
    <el-menu
      @select="handleSelect"
      background-color="#545c64"
      text-color="#fff"
      active-text-color="#fff"
    >
      <el-menu-item index="add">新增任务</el-menu-item>
      <el-menu-item index="edit">编辑任务</el-menu-item>
      <el-menu-item index="del">删除任务</el-menu-item>
    </el-menu>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
  props: {
    x: {
      type: Number,
      default: 0
    },
    y: {
      type: Number,
      default: 0
    }
  },
  emits: ['menu-item'],
  setup(props, ctx) {
    const handleSelect = (action: string) => {
      ctx.emit('menu-item', action);
    };
    return {
      handleSelect
    };
  }
});
</script>

<style lang="less" scoped>
.menu {
  position: fixed;
  transition: all 1s ease;
  ::v-deep(.el-menu-item) {
    height: 40px;
    line-height: 40px;
    width: 200px;
  }
}
</style>
// Gantt.vue
<template>
    <transition name="el-fade-in-linear">
      <Menu :x="menuX" :y="menuY" v-show="menuVisible" @menu-item="handleItemClick" />
    </transition>
</template>
<script lang="ts">
const menuVisible = ref<boolean>(false); // 控制菜单显示
const menuX = ref<number>(0); // left
const menuY = ref<number>(0); // top
const handleItemClick = (item: any) => {
  menuVisible.value = false; // 隐藏菜单
  dialogVisible.value = true; // 显示编辑弹窗
};
gantt.attachEvent(
  'onContextMenu',
  function (taskId, linkId, event) {
    var x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft,
      y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
    // 判断要是在树上的右键菜单才有效果
    if (taskId && event.target.className === 'gantt_tree_content') {
      console.log('task ContentMenu', taskId, linkId, event);
      menuX.value = x;
      menuY.value = y;
      menuVisible.value = true;
    }

    if (taskId || linkId) {
      return false;
    }

    return true;
  },
  {}
);
// 取消菜单显示
gantt.attachEvent(
  'onEmptyClick',
  function (e) {
    //any custom logic here
    menuVisible.value = false;
  },
  {}
);
</script>

效果 image.png

  • 其他事件 (禁用原来自带的弹窗)
 gantt.attachEvent(
  'onBeforeLightbox',
  function (id) {
    console.log(1);
    return false; // 返回 false
  },
  {}
);
  • 任务双击进入编辑事件
gantt.attachEvent(
  'onTaskDblClick',
  function (id, e) {
    console.log('id', id, e);
    dialogVisible.value = true;
    return false;
  },
  {}
);

image.png

体验和心得

  1. 总体而言视乎满足了需求要的样子
  2. 具体代码嘛,只处于一个 Dome 级别
  3. 至于npm源码方面,开源出来的功能从其官网看还是基本满足日常需求的
  4. 库的稳定和功能升级方面,每周下载还是处于活跃的状态
  5. 个人体会:官网Base是英文的,然后 Samples 样库例提供了很多功能的案例,需要开发着耐心去发掘。

image.png

最后

To be someone who needs it. 是自己成为别人需要的人 此文仅是本着目前需求出发,可能考虑的点和开源的方案并不是一个最佳的,所以如果有幸能被阅读到或者有更加成熟的方案,请留言交流,万分感谢。