开发甘特图组件的痛苦过程

17,412 阅读3分钟

前言

年前接到一个项目需求是关于甘特图开发,很不幸这个任务居然落到我的头上来。直觉告诉我这个需求开发难度是相当大的,然后和产品小姐姐一顿需求分析和评估,我的表情是这样的。

12e0126fe5501fb9b3e20c009e25023e.jpeg

插件选型

虽然任务很艰巨,但是我还是尝试上去github寻找看下有没有合适的插件,然后就给我发现了(dhtmlxGantt),当我满怀开心打开文档的时候,我瞬间又感觉到了无力感,因为它是纯纯的英文文档!!!! dhtmlxGantt文档地址

02f12ce373eabedd6b7c8c73ef7c3e45.jpeg

  • 经过我一段时间的软磨硬泡,我居然发现dhtmlxGantt居然能满足得了我目前需求,它可以对于重大节假日,周末,里程碑线条,任务条样式自定义,表头动态删除与增加等等都提供了方法,让我在黑暗中看到了点点光芒。

60d861b9f5d5961496ad2e8599c97e1b.jpeg

甘特图初始化

在使用dhtmlxGantt插件的时候,我使用npm下载发现会少了部分的功能,最直接体现就是左侧的表头无法拉伸了,所以我就直接从官网上下载它所需要的资源包

  • 在使用该插件的时候,一定要看它的功能例子 samples地址,对于开发者来说,代码就是最好的注释。
<div id="gantt_here" style="width: 100%; height: 100%" />
mounted() {
    gantt.init("gantt_here");
    gantt.parse({
      data: [
        {
          id: 1,
          text: "Project #2",
          start_date: "01-04-2018",
          duration: 18,
          progress: 0.4,
          open: true
        },
        {
          id: 2,
          text: "Task #1",
          start_date: "02-04-2018",
          duration: 8,
          progress: 0.6,
          parent: 1
        },
        {
          id: 3,
          text: "Task #2",
          start_date: "11-04-2018",
          duration: 8,
          progress: 0.6,
          parent: 1
        }
      ],
      links: [
        { id: 1, source: 1, target: 2, type: "1" },
        { id: 2, source: 2, target: 3, type: "0" }
      ]
    });
  }

image.png 这是一个最简单的demo例子,那么我们就会尝试对其进行封装为组件以达到项目需求,同时我也对其常见的配置项进行抽离与配置。通过观察我们也能发现数据结构中常见的字段:

  1. id 任务条的唯一id
  2. text 显示的文案
  3. start_date 任务开始时间
  4. duration 工期(结束时间会自动计算)
  5. progress 进度

自定义表头

有时候,我们需要自定义表头的需求,在Gant.config中提供了columns选项给我们来操作,结合上述的数据结构,那么我们很容易的写出组件

Gant.vue中

<template>
  <div id="gantt_here" style="width: 100%; height: 100%" />
</template><script>
const gantt = window.gantt;
import "dhtmlx-gantt/codebase/dhtmlxgantt.css";
export default {
  props: {
    data: {
      type: Array,
      default: () => []
    },
    links: {
      type: Array,
      default: () => []
    },
    gantConfig: {
      type: Object,
      default: () => {} // 甘特图配置
    },
    columnData: {
      // 表头配置
      type: Array,
      default: () => []
    }
  },
  watch: {
    gantConfig(nVal) {
      this.setGantConfig(nVal);
    }
  },
  mounted() {
    this.init();
  },
  methods: {
    setGantConfig(data) {
      const defaultGantConfig = {
        readonly: true, // 是否只读
        columns: this.columnData,
        xml_date: "%Y-%m-%d" // 日期格式
      };
      const target = { ...defaultGantConfig, ...data };
      for (const key in target) {
        gantt.config[key] = target[key];
      }
    },
    init() {
      this.setGantConfig(this.gantConfig);
      this.initGantDom();
      this.initData();
    },
    initData() {
      gantt.parse({
        data: this.data,
        links: this.links
      });
    },
    initGantDom() {
      gantt.init("gantt_here");
    }
  }
};
</script><style></style>

columns参数

名称描述默认值
name字段名称
label显示名称
tree树展开节点
width宽度
align对齐方式center
resize是否拉伸
template自定义模板可支持html结构

在业务组件bussiness.vue

<template>
  <div class="dashboard z-w-100">
    <Gant :data="data" :column-data="columnData" />
  </div>
</template><script>
import Gant from "./Gant.vue";
export default {
  name: "Dashboard",
  components: {
    Gant
  },
  data() {
    return {
      columnData: [
        {
          name: "text",
          label: "任务名称",
          tree: true,
          width: "*",
          align: "left",
          resize: true,
          template: function (obj) {
            return obj.text + "自定义";
          }
        },
        {
          name: "start_date",
          label: "开始时间",
          width: "*",
          align: "center",
          template: function (obj) {
            return obj.start_date;
          }
        }
      ],
      data: [
        {
          id: 1,
          text: "Project #2",
          start_date: "2018-01-04",
          duration: 18,
          progress: 0.4,
          open: true
        },
        {
          id: 2,
          text: "Task #1",
          start_date: "2018-02-04",
          duration: 8,
          progress: 0.6,
          parent: 1
        }
      ]
    };
  }
};
</script>

自定义任务条

任务条可以通过添加类名的方式进行改变样式

<Gant :data="data" :column-data="columnData" :task-class="taskClass" ref="gant"/>
mounted() {
    this.gantt = this.$refs.Gant.getGant();
  },
 methods: {
    taskClass(start, end, task) {
      let gantt = this.$refs.gant.getGant()
      const children = gantt.getChildren(task.id); // 区分父节点与子节点
      return children.length > 0 ? "parent_row_class" : "child_row_class";
    }
  }
  <style lang="scss">
.parent_row_class {
  .gantt_task_content {
    background-color: red;
  }
}
.child_row_class {
  .gantt_task_content {
    background-color: blue;
  }
}
</style>

image.png

自定义任务条内容

可以通过taskText方法来进行自定义任务条样式

gant.vue

props:{
    ...
    taskLabel: {
      // 定义任务条label的文案和样式
      type: Function,
      default: undefined
    }
},
methods:{
    setTaskText() {
       if (this.taskLabel) {
        gantt.templates.task_text = this.taskLabel;
      }
    },
    init(){
        ...
        this.setTaskText();
    }
},
mounted(){
    this.init();
}

bussiness.vue

methods:{
    ...
    percenToString(num) {
      return Math.floor(num * 100) + "%";
    },

    renderLabel(progress, sum) {
      var relWidth = (progress / sum) * 100;
      var cssClass = "custom_progress ";
      if (progress > 0.6) {
        cssClass += "nearly_done";
      } else if (progress > 0.3) {
        cssClass += "in_progress";
      } else {
        cssClass += "idle";
      }
      return (
        "<div class='" +
        cssClass +
        "' style='width:" +
        relWidth +
        "%'>" +
        this.percenToString(progress) +
        "</div>"
      );
    },
    taskLabel(start, end, task) {
      var summ = task.progress1 + task.progress2 + task.progress3;
      return (
        this.renderLabel(task.progress1, summ) +
        this.renderLabel(task.progress2, summ) +
        this.renderLabel(task.progress3, summ)
      );
    }
}
<style lang="scss">
.custom_progress {
  display: inline-block;
  vertical-align: top;
  text-align: center;
  height: 100%;
}

.custom_progress.nearly_done {
  background-color: #4cc259;
}

.custom_progress.in_progress {
  background-color: #88bff5;
}

.custom_progress.idle {
  background-color: #d96c49;
}
</style>

image.png

自定义时间轴跨度

需求中也包含了选择周,月,季,年的时间跨度选择

utils.js

import Dayjs from "dayjs";

export const mouthMap = {
  Jan: "1月",
  Feb: "2月",
  Mar: "3月",
  Apr: "4月",
  May: "5月",
  Jun: "6月",
  Jul: "7月",
  Aug: "8月",
  Sep: "9月",
  Oct: "10月",
  Nov: "11月",
  Dec: "12月"
};

export function getDayWeek(date) {
  var l = ["日", "一", "二", "三", "四", "五", "六"];
  var d = new Date(date).getDay();
  return "周" + l[d];
}
export function getGanttConfigByZoomValue(gantt, value) {
  const yearFormat = (date) => {
    return `${Dayjs(date).format("YYYY") + "年"}`;
  };
  if (value === "week") {
    const dayFormat = function (date) {
      const weeks = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
      return Dayjs(date).format("MM-DD") + " " + weeks[Dayjs(date).day()];
    };

    return [
      { unit: "year", step: 1, format: yearFormat },
      { unit: "day", step: 1, format: dayFormat }
    ];
  } else if (value === "month") {
    const monthFormat = (date) => {
      return Dayjs(date).month() + 1 + "月" + Dayjs(date).date() + "号";
    };
    return [
      { unit: "year", step: 1, format: yearFormat },
      { unit: "day", step: 3, format: monthFormat }
    ];
  } else if (value === "quarter") {
    const FormatQuarter = (date) => {
      console.log(Dayjs(date).month() + 1 + "月");
      return Dayjs(date).year() + "年" + (Dayjs(date).month() + 1) + "月";
    };
    const dayFormat = (date) => {
      const endDate = gantt.date.add(
        gantt.date.add(date, 1, "week"),
        -1,
        "day"
      );
      // 处理一下月份
      return Dayjs(date).date() + "号" + "-" + Dayjs(endDate).date() + "号";
    };
    return [
      { unit: "month", step: 1, format: FormatQuarter },
      { unit: "week", step: 1, format: dayFormat }
    ];
  } else if (value === "year") {
    const monthFormat = (date) => {
      return Dayjs(date).month() + 1 + "月";
    };
    return [
      { unit: "year", step: 1, format: yearFormat },
      { unit: "month", step: 1, format: monthFormat }
    ];
  }
}

bussiness.vue中

<div style="width: 100%; height: 100%">
    <el-radio-group v-model="radio" style="padding: 10px">
      <el-radio label="week"></el-radio>
      <el-radio label="month"></el-radio>
      <el-radio label="quarter"></el-radio>
      <el-radio label="year"></el-radio>
    </el-radio-group>
    <div class="dashboard" style="width: 100%; height: 400px">
      <Gant
        ref="Gant"
        :data="data"
        :column-data="columnData"
        :task-class="taskClass"
        :gant-config="gantConfig"
      />
    </div>
  </div>
  watch: {
    radio(nVal) {
      this.$refs.Gant.setDateUpdate(nVal);
    }
  },
  mounted() {
    this.$refs.Gant.setDateUpdate(this.radio);
  },

GIF.gif

添加toolTip

需要给gantt调用其中的plugin方法, gantt.vue

props:{
     pluginsConfig: {
      // 插件配置
      type: Object,
      default: () => {} // 甘特图配置
    }
},
watch:{
    ...
    pluginsConfig(nVal){
        this.setPlugin(nVal);
    }
}
methods:{
    setPlugin(pluginsConfig) {
      const defaultConfig = {
        marker: true, // 里程碑线条
        tooltip: true, // 提示语
        auto_scheduling: true, // 是否自定调度
        multiselect: true, // 是否多选
        fullscreen: true // 是否全屏
      };
      const config = { ...defaultConfig, ...pluginsConfig };
      gantt.plugins(config);
    },
    init(){
        this.setPlugin(this.pluginsConfig); // 放在initDom前面
        ...
    }
}

image.png

添加里程碑线条

里程碑线条可以突出或者标记某些重要的节日,主要调用gantt中addMarker方法即可

mounted(){
    this.gantt = this.$refs.Gant.getGant();
    this.gantt.addMarker({
      start_date: new Date(2018, 1, 4),
      css: "today",
      title: "123"
    });
}

image.png

设置周末显示隐藏或者添加样式

  • 设置周末显示隐藏
mounted(){
    this.gantt = this.$refs.Gant.getGant();
    this.gantt.ignore_time = (date) => {
      if (date.getDay() === 0 || date.getDay() === 6) {
        return true;
      }
    };
}

image.png

  • 对周末添加高亮样式
mounted(){
    this.gantt = this.$refs.Gant.getGant();
    this.gantt.templates.timeline_cell_class = (item, date) => {
        if (date.getDay() === 0 || date.getDay() === 6) { return 'weekend'; } return '';
    }
}
<style lang="scss">
    .weekend{
        background:#ddd;
    }
<style>

image.png

总结

以上是基本的甘特图需求,其实实现起来不是很难,但是需要耐心的去阅读文档即可。祝愿天下没有甘特图业务开发....

文档地址

api demo