前言
年前接到一个项目需求是关于甘特图开发,很不幸这个任务居然落到我的头上来。直觉告诉我这个需求开发难度是相当大的,然后和产品小姐姐一顿需求分析和评估,我的表情是这样的。
插件选型
虽然任务很艰巨,但是我还是尝试上去github寻找看下有没有合适的插件,然后就给我发现了(dhtmlxGantt),当我满怀开心打开文档的时候,我瞬间又感觉到了无力感,因为它是纯纯的英文文档!!!! dhtmlxGantt文档地址
- 经过我一段时间的软磨硬泡,我居然发现dhtmlxGantt居然能满足得了我目前需求,它可以对于重大节假日,周末,里程碑线条,任务条样式自定义,表头动态删除与增加等等都提供了方法,让我在黑暗中看到了点点光芒。
甘特图初始化
在使用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" }
]
});
}
这是一个最简单的demo例子,那么我们就会尝试对其进行封装为组件以达到项目需求,同时我也对其常见的配置项进行抽离与配置。通过观察我们也能发现数据结构中常见的字段:
- id 任务条的唯一id
- text 显示的文案
- start_date 任务开始时间
- duration 工期(结束时间会自动计算)
- 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>
自定义任务条内容
可以通过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>
自定义时间轴跨度
需求中也包含了选择周,月,季,年的时间跨度选择
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);
},
添加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前面
...
}
}
添加里程碑线条
里程碑线条可以突出或者标记某些重要的节日,主要调用gantt中addMarker方法即可
mounted(){
this.gantt = this.$refs.Gant.getGant();
this.gantt.addMarker({
start_date: new Date(2018, 1, 4),
css: "today",
title: "123"
});
}
设置周末显示隐藏或者添加样式
- 设置周末显示隐藏
mounted(){
this.gantt = this.$refs.Gant.getGant();
this.gantt.ignore_time = (date) => {
if (date.getDay() === 0 || date.getDay() === 6) {
return true;
}
};
}
- 对周末添加高亮样式
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>
总结
以上是基本的甘特图需求,其实实现起来不是很难,但是需要耐心的去阅读文档即可。祝愿天下没有甘特图业务开发....