实现效果
需求甘特图
任务甘特图
组件部分代码
<template>
<div class="gantt-box" style="height: 100%">
<div class="ganttComponent" :id="refName" :ref="refName" v-if="this.tableData.data.length>0"></div>
<div class="full-screen-box el-icon-full-screen" v-if="this.tableData.data.length>0 && allowFullScreen" @click="fullScreen"></div>
<div class="no-data" v-else>
<p style="font-size: 100px;margin-bottom: 12px"><nav-icon icon="public-empty-small"></nav-icon></p>
<p style="font-size: 14px">{{ $t('public.noData') }}</p>
</div>
</div>
</template>
<script>
import { gantt } from 'dhtmlx-gantt';
import 'dhtmlx-gantt/codebase/dhtmlxgantt.css';
import ganttFormat from '@/components/rainbowGantt/ganttFormat';
export default {
name: 'ganttComponent',
data() {
return {
// tableData: {
// data: [
// {
// id: 1,
// name: '需求1的名字很长长到离谱哈哈啊哈哈',
// text: '研发中',
// priority: 'P1',
// status: 0,
// start_date: '01-04-2013',
// end_date: '01-05-2013',
// schedule_test_time: '04-04-2013', // 计划提测时间
// schedule_check_time: '10-04-2013', // 计划验收时间
// color: '#73A5BD',
// assignedTo: '🐒'
// },
// ],
// },
};
},
props: {
tableData: {
type: Object,
default: () => ({
data: [],
})
},
allowFullScreen: {
type: Boolean,
default: false
},
refName: {
type: String,
default: 'ganttComponent'
},
isTaskGantt: {
type: Boolean,
default: false
}
},
methods: {
fullScreen() {
if (!gantt.getState().fullscreen) {
// expanding the gantt to full screen
const ganttComponent = document.getElementById(this.refName);
ganttComponent.style.background = '#FFF';
gantt.expand();
} else {
// collapsing the gantt to the normal mode
gantt.collapse();
}
},
initGantt() {
const { taskFormat, demandFormat } = ganttFormat;
gantt.config.readonly = true; // 只读
gantt.config.columns = this.isTaskGantt ? taskFormat.columns : demandFormat.columns;
// 设置表头的高度
gantt.config.scale_height = 60;
// 设置行高
gantt.config.row_height = 48;
gantt.attachEvent('onCollapse', () => {
// any custom logic here
const ganttComponent = document.getElementById(this.refName);
if (!ganttComponent) return;
ganttComponent.style.background = 'transparent';
});
// 重置条状图样式
gantt.config.type_renderers[gantt.config.types.task] = (task) => {
const mainEl = document.createElement('div');
mainEl.setAttribute(gantt.config.task_attribute, task.id);
const size = gantt.getTaskPosition(task);
mainEl.innerText = task.text;
mainEl.className = 'custom-project';
mainEl.style.left = `${size.left }px`;
mainEl.style.top = `${size.top + 7}px`;
mainEl.style.width = `${size.width }px`;
mainEl.style.background = task.color;
return mainEl;
};
// 配置插件
gantt.plugins({ tooltip: true, fullscreen: true, marker: true, });
// 配置marker
const today = new Date();
gantt.addMarker({
start_date: today,
css: 'today',
text: 'Today',
});
// 自定义tooltip内
const toolTipFunction = this.isTaskGantt ? taskFormat.tooltip : demandFormat.tooltip;
gantt.templates.tooltip_text = (start, end, task) => toolTipFunction(start, end, task);
gantt.config.layout = {
css: 'gantt_container',
cols: [
{
width: this.isTaskGantt ? taskFormat.tableWidth : demandFormat.tableWidth,
rows: [
{
view: 'grid', scrollX: 'gridScroll', scrollable: true, scrollY: 'scrollVer'
},
{ view: 'scrollbar', id: 'gridScroll' }
]
},
{ resizer: true, width: 1 },
{
rows: [
{ view: 'timeline', scrollX: 'scrollHor', scrollY: 'scrollVer' },
{ view: 'scrollbar', id: 'scrollHor' }
]
},
{ view: 'scrollbar', id: 'scrollVer' }
]
};
const zoomConfig = {
// 设置右侧时间轴格式及宽高
levels: [
{
name: 'month',
scale_height: 60,
min_column_width: 60,
scales: [
{ unit: 'month', format: '%Y/%m' },
{ unit: 'day', step: 1, format: '%d' }
]
},
]
};
// 给年月展示框加类名
gantt.templates.scale_cell_class = () => 'month-box';
gantt.ext.zoom.init(zoomConfig);
const ganttComponent = document.getElementById(this.refName);
if (!ganttComponent) return;
gantt.init(ganttComponent);
gantt.parse(this.tableData);
},
nameClick(e) {
if (!e) return;
const target = e.target;
const classNameList = target.classList;
if (![].includes.call(classNameList, 'gantt-name-click')) return;
const storyName = target.innerText;
this.$emit('getTasksBelongDemand', storyName);
},
},
watch: {
'tableData.data': {
immediate: true,
deep: true,
handler(val) {
if (val.length === 0 && gantt.getState().fullscreen) {
gantt.collapse();
}
this.$nextTick(() => {
// 清空后再添加
gantt.clearAll();
this.initGantt();
});
}
}
},
mounted() {
this.initGantt();
window.addEventListener('click', this.nameClick);
},
beforeDestroy() {
window.removeEventListener('click', this.nameClick);
},
};
</script>
<style scoped lang="less">
@baseBackground:transparent;
@deepBackground:#EEF1F4;
@selectedBackground:#E2E5EA;
@fontBoldColor:#323438;
@baseFontColor:#62666D;
@hoverBackground:#E2E5EA;
@borderColor:#D4D7E5;
.gantt-box{
position: relative;
.full-screen-box{
cursor: pointer;
position: absolute;
right: 20px;
bottom: 20px;
font-size: 40px;
}
}
.ganttComponent{
position: relative;
min-height: 560px;
height:calc(100% - 10px);
padding-top: 10px;
/deep/.gantt_layout_cell{
background: @baseBackground;
}
/deep/.gantt_layout_cell_border_bottom{
border-bottom: none;
}
/deep/.gantt_layout_cell_border_top{
border-top: none;
}
/deep/.gantt_layout_cell_border_right{
border-right: none;
}
/deep/.gantt_layout_cell_border_left{
border-left: none;
}
/deep/.gantt_row{
border-bottom: none;
background: @baseBackground;
&:hover{
background-color: transparent;
}
}
/deep/.gantt_grid_data .gantt_cell{
color: @baseFontColor;
font-size: 14px;
}
/deep/.custom-project{
position: absolute;
height: 32px;
line-height: 32px;
color: #fff;
text-align: center;
border: none !important;
border-radius: 4px;
font-weight: 500;
font-size: 14px;
cursor: pointer;
}
/deep/.gantt_grid_scale .gantt_grid_head_cell{
color: @fontBoldColor;
font-size: 14px;
font-weight: 600;
&:first-of-type{
text-align: left;
padding-left: 25px;
}
}
/deep/.gantt_grid_scale{
background: @baseBackground;
border: none;
}
/deep/.gantt_task_scale{
background: @baseBackground;
border: none;
}
/deep/.gantt_scale_line{
border: none;
}
/deep/.gantt_scale_cell{
//border: none;
border-bottom: 1px solid @borderColor;
border-right: 1px solid @borderColor;
border-top: 1px solid @borderColor;
color: #62666D;
font-size: 14px;
&:first-of-type{
border-left: 1px solid @borderColor;
}
&.month-box{
text-align: left;
border:none
}
}
/deep/.gantt_task_row{
border: none;
background: @baseBackground;
}
/deep/.gantt_task_cell{
border: none;
}
/deep/.gantt_task_row.gantt_selected{
background: @selectedBackground !important;
}
/deep/.gantt_row.gantt_selected{
background: @selectedBackground !important;
}
/deep/.gantt_task_content{
font-size: 14px;
font-weight: bold;
}
/deep/.gantt_task_inline_color.gantt_selected{
box-shadow: none !important;
}
}
</style>
<style lang="less">
.gantt_tooltip{
padding: 20px;
padding-bottom: 13px;
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.15);
border-radius: 4px;
}
.gantt_tooltip_task_name{
font-size: 16px;
font-weight: 500;
margin-bottom: 7px;
color: #373737;
}
.gantt_tooltip_task_content{
font-size: 14px;
height: 28px;
line-height: 28px;
color: #62666D;
}
.gantt-name-box{
box-sizing: border-box;
display: block;
width: 100%;
text-align: left;
padding-left: 19px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.gantt-name-click{
cursor: pointer;
&:hover{
color: @baseBlue;
}
}
#priority-box{
margin-top: 12px;
display: block;
font-size: 14px;
height: 24px;
line-height: 24px;
padding: 0 16px;
border-radius: 12px;
}
.priority-P1{
color: #FF3900;
background: #FFF1ED;
}
.priority-P2{
color: #FF8400;
background: #FFF2E5;
}
.priority-P3{
color: #285DE2;
background: #E6EDFF;
}
.priority-P4{
color: #4FA401;
background: #F3FFE8;
}
.gantt-error{
display: none;
}
</style>
数据格式化代码
const priorityStyle = (story) => {
const priority = story.priority;
if (!priority) {
return '';
}
const className = `priority-P${story.priority}`;
return `<span class=${className} id="priority-box">P${story.priority}</span>`;
};
const demandFormat = {
tooltip: (start, end, task) => `<p class="gantt_tooltip_task_name">${task.name}</p>
<p class="gantt_tooltip_task_content">持续时间:${task.duration}天</p>
<p class="gantt_tooltip_task_content">当前进度:${task.progress * 100}%</p>
<p class="gantt_tooltip_task_content">计划提测时间:${task.planTestTime || ''}</p>
<p class="gantt_tooltip_task_content">实际提测时间:${task.actualTestTime}</p>`,
columns: [
{
name: 'name',
label: '需求名称',
align: 'left',
width: 120,
template: obj => `<span class="gantt-name-box gantt-name-click">${obj.name}</span>`
},
{
name: 'priority',
label: '优先级',
align: 'center',
width: 60,
template: obj => priorityStyle(obj)
},
{
name: 'start_date',
label: '开始时间',
align: 'center',
width: 100,
template: obj => `<span>${obj.beginTime}</span>`
},
{
name: 'end_date',
label: '结束时间',
align: 'center',
width: 100,
template: obj => (obj.isOvertime ? `<span style="color: red">${obj.endTime}</span>` : `<span>${obj.endTime}</span>`)
},
{
name: 'planTestTime', label: '计划提测时间', align: 'center', width: 100
},
{
name: 'planAcceptanceTime', label: '计划验收时间', align: 'center', width: 100
},
{
name: 'assignedTo', label: '处理人', align: 'center', width: 100
},
],
tableWidth: 680
};
const taskFormat = {
tooltip: (start, end, task) => `<p class="gantt_tooltip_task_name">${task.name}</p>
<p class="gantt_tooltip_task_content">实际开始时间:${task.beginTime}</p>
<p class="gantt_tooltip_task_content">实际结束时间:${task.endTime}</p>`,
columns: [
{
name: 'name',
label: '任务名称',
align: 'left',
width: 120,
template: obj => `<span class="gantt-name-box">${obj.name}</span>`
},
{
name: 'priority',
label: '优先级',
align: 'center',
width: 60,
template: obj => priorityStyle(obj)
},
{
name: 'start_date',
label: '开始时间',
align: 'center',
width: 100,
template: obj => `<span>${obj.beginTime}</span>`
},
{
name: 'end_date',
label: '结束时间',
align: 'center',
width: 100,
template: obj => (obj.isOvertime ? `<span style="color: red">${obj.endTime}</span>` : `<span>${obj.endTime}</span>`)
},
{
name: 'assignedTo', label: '处理人', align: 'center', width: 100
},
],
tableWidth: 480
};
export default { demandFormat, taskFormat };
遇到的问题
本来要做的效果是点击需求名称弹窗展示任务的甘特图,但是dhtmlGantt貌似不支持一个页面同时渲染,两个甘特图,实现出来的效果是需求的甘特图变为空,只渲染任务的甘特图。后来无奈改变成点击需求名称刷新页面的方式实现。如果有懂得大佬希望能指点一下,感激不尽!!!