自制甘特图组件
很抱歉,这是我自己写的甘特图,太简陋了。关于甘特图,我有了更简单、功能更强大、效果更完美的解决方案,不要再看这个。
用hightcharts实现,对vue react 原生js等等都支持。支持左侧表格,右侧时间轴,支持点击选中,拖拽,换列等功能
新的甘特图以及源代码下载请点击这里查看 hightcharts版甘特图
目录结构

<template>
<div class="gantt-content">
<ganttChart
:timeWidth="50"
:startStamp="startStamp"
:endStamp="endStamp"
:header="header"
:body="body"
/>
</div>
</template>
<script>
import ganttChart from "./ganttChart"
import { orderGantt } from "@api/reportCenter/orderGantt"
import { searchCalendar } from "@api/basicData/calendar"
export default {
components:{
ganttChart,
},
data(){
return {
startStamp: this.$moment('2020-07-06 08:00:00').valueOf(),
endStamp: this.$moment('2020-07-11 08:00:00').valueOf(),
header: {
height: 100,
table: [],
dates: [],
class: [],
times: []
},
body: {
height: 40,
list: []
},
year: '',
week: ''
}
},
methods:{
// 获取数据
async getData() {
const date = await this.searchCalendar()
if(date.code === 200) {
const {data: {nowDate = []}} = date
this.year = nowDate[0].haieryear
this.haierWeek = nowDate[0].haierweek
}
const params = {
year: this.year,
haierWeek: this.haierWeek
}
const res = await orderGantt(params)
// 组装甘特图数据
if(res.code === 200) {
let table = [
{ label: this.$t('orderGantt.factoryNo'), value: 'factoryNo', width: 80, style: {} },
{ label: this.$t('orderGantt.productLine'), value: 'productLine', width: 120 },
{ label: `${this.haierWeek}${this.$t('orderGantt.loadCount')}`, value: 'loadCount', width: 120 },
],
dates = [],
_class = [], times = [], list = [], cla_day = this.$t('orderGantt.day'), cla_ni = this.$t('orderGantt.night');
if(res.data.length) {
res.data[0].dateModels.forEach(item => {
let theD = this.$moment(item.sdate).format('YYYY/MM/DD')
dates.push({
label: theD,
value: theD,
height: 50
})
})
let dd = res.data[0].dateModels
this.startStamp = this.$moment(this.$moment(dd[0].sdate).format('YYYY-MM-DD') + " 08:00:00").valueOf()
this.endStamp = this.$moment(this.$moment(dd[dd.length - 1].sdate).format('YYYY-MM-DD') + " 08:00:00").add(1,'d').valueOf()
}
dates.forEach((item, idx) => {
_class = [
..._class,
{ label: cla_day, height: 30 },
{ label: cla_ni, height: 30 },
]
times = [
...times,
{label: '8:00', height: 20 }, {label: '12:00', height: 20 },
{label: '16:00', height: 20 }, {label: '20:00', height: 20 },
{label: '00:00', height: 20 }, {label: '4:00', height: 20 },
]
});
res.data.forEach(item => {
list.push({
factoryNo: item.factoryNo,
productLine: item.productLine,
loadCount: item.loadCount,
order: item.sequentialPlanList,
})
});
this.header = {
...this.header,
table,
dates,
class: _class,
times,
}
this.body = {
...this.body,
list
}
}
},
// 获取海尔年、月、周、星期列表
searchCalendar() {
return searchCalendar({
date: this.$moment().format("YYYY-MM-DD"),
haiermonth: "",
haiersweek: "",
haierweek: "",
haieryear: ""
})
},
},
created() {
this.getData()
}
}
</script>
<style lang="less">
.gantt-content {
width: 100%;
padding: 20px;
overflow-x: scroll;
}
</style>
<template>
<div class="gantt-chart" :style="{width: widthAll+'px'}">
<div class="c-content">
<div class="c-table">
<div class="c-header">
<div
class="th"
v-for="item in header.table"
:key="item.value"
:style="{...(item.style || {}), width: item.width+'px', height: (item.height || header.height)+'px', lineHeight: (item.height || header.height)+'px'}"
>
{{item.label}}
</div>
</div>
<div class="c-body">
<div
class="tr"
v-for="(tr, idx) in body.list"
:key="idx"
>
<div
class="td"
v-for="td in header.table"
:key="td.value"
:style="{...td.style, width: td.width+'px', height: body.height+'px', lineHeight: body.height+'px'}"
>
{{tr[td.value] || ' '}}
</div>
</div>
</div>
</div>
<div class="c-gant">
<div class="c-header">
<div>
<div
class="th"
v-for="(item, idx) in header.dates"
:key="idx"
:style="{...(item.style || {}), width: dateWidth+'px', height: (item.height || header.height)+'px', lineHeight: (item.height || header.height)+'px'}"
>
{{item.label}}
</div>
</div>
<div>
<div
class="th"
v-for="(item, idx) in header.class"
:key="idx"
:style="{...(item.style || {}), width: classWidth+'px', height: (item.height || header.height)+'px', lineHeight: (item.height || header.height)+'px'}"
>
{{item.label}}
</div>
</div>
<div>
<div
class="th"
v-for="(item, idx) in (theTimes.length ? theTimes : header.times)"
:key="idx"
:style="{...(item.style || {}), width: timeWidth+'px', height: (item.height || header.height)+'px', lineHeight: (item.height || header.height)+'px'}"
>
{{item.label}}
</div>
</div>
</div>
<div class="c-body">
<div
class="tr"
v-for="(tr, idx) in body.list"
:key="'tr'+idx"
>
<div
class="td"
v-for="(td, idx2) in (theTimes.length ? theTimes : header.times)"
:key="'td'+idx2"
:style="{...td.style, width: timeWidth+'px', height: body.height+'px', lineHeight: body.height+'px'}"
>
{{' '}}
</div>
<div
class="td-g"
v-for="(tdg, idx3) in ganttArr[idx]"
:key="tdg.label + idx3"
:style="{ height: body.height-2+'px', lineHeight: body.height-2+'px', ...tdg.style }"
@mouseover="(e) => mouseOver(e, tdg)"
@mouseout="(e) => mouseOut(e, tdg)"
@contextmenu="(e) => contextmenu(e, tdg)"
>
{{tdg.label}}
</div>
</div>
</div>
</div>
</div>
<div id="a-gantt-pop" :style="popStyle">
<p>{{$t('orderGantt.orderName')}}: {{currentData.label}}</p>
<p>{{$t('orderGantt.num')}}: {{currentData.num}}</p>
<p>{{$t('orderGantt.startTime')}}: {{currentData.startTime}}</p>
<p>{{$t('orderGantt.endTime')}}: {{currentData.endTime}}</p>
</div>
<a-modal
:visible="modal.visible"
:title="modal.title"
width="740px"
:footer="null"
@ok="closeModal"
@cancel="closeModal"
>
<p>{{$t('orderGantt.orderName')}}: {{modal.data.label}}</p>
<p>{{$t('orderGantt.num')}}: {{modal.data.num}}</p>
<p>{{$t('orderGantt.startTime')}}: {{modal.data.startTime}}</p>
<p>{{$t('orderGantt.endTime')}}: {{modal.data.endTime}}</p>
</a-modal>
</div>
</template>
<script>
const timeDown = [
{label: '8:00', height: 20 }, {label: '12:00', height: 20 },
{label: '16:00', height: 20 }, {label: '20:00', height: 20 },
{label: '24:00', height: 20 }, {label: '4:00', height: 20 }
]
const timeUp = [
{label: '8:00', height: 20 }, {label: '9:00', height: 20 }, {label: '10:00', height: 20 },
{label: '11:00', height: 20 }, {label: '12:00', height: 20 }, {label: '13:00', height: 20 },
{label: '14:00', height: 20 }, {label: '15:00', height: 20 }, {label: '16:00', height: 20 },
{label: '17:00', height: 20 }, {label: '18:00', height: 20 }, {label: '19:00', height: 20 },
{label: '20:00', height: 20 }, {label: '21:00', height: 20 }, {label: '22:00', height: 20 },
{label: '23:00', height: 20 }, {label: '00:00', height: 20 }, {label: '1:00', height: 20 },
{label: '2:00', height: 20 }, {label: '3:00', height: 20 }, {label: '4:00', height: 20 },
{label: '5:00', height: 20 }, {label: '6:00', height: 20 }, {label: '7:00', height: 20 }
]
function getColor(d) {
switch (true) {
case d<100:
return {
background: '#F7DEB9',
borderRadius: '4px',
border: '1px solid #F7DEB9'
}
break;
case d===100:
return {
background: '#CAB9CA',
borderRadius: '4px',
border: '1px solid #CAB9CA'
}
break;
case d>100:
return {
background: '#735773',
borderRadius: '4px',
border: '1px solid #735773'
}
break;
default:
return {
background: '#CAB9CA',
borderRadius: '4px',
border: '1px solid #CAB9CA'
}
break;
}
}
export default {
props:{
// 开始时间时间戳
startStamp: {
type: Number,
required: true
},
// 结束时间时间戳
endStamp: {
type: Number,
required: true
},
// 时间单位宽度大小, 实践单位 4小时 、 1小时
timeWidth: {
type: Number,
default: 50
},
// 表头
header: {
type: Object,
default: {
height: 100, // 表头的默认高度
table: [], // 表格头
dates: [], // 日期
class: [], // 班次
times: [] // 时间
}
},
// 数据
body: {
type: Object,
default: {
height: 40, // 表格体的默认高度
list: []
}
},
},
data() {
return {
widthAll: 1200,
dateWidth: 300,
classWidth: 150,
timeAll: 259200000, // 默认显示3天, 这是3天的毫秒数
ganttArr: [], // 任务数组
currentData: {}, // 当前操作任务的数据
popStyle: { // hover浮层的样式
display: 'none',
top: 0,
left: 0
},
modal: { // 弹窗信息
visible: false,
title: '',
data: {}
},
ctrlDown: false, // true 代表Ctrl键正被按压
wheelHeight: 0, // 滚轮滚动的位置
theTimes: [] // 时间表头,需要内部维护
};
},
computed: {},
watch: {
body() {
this.initGantt()
},
timeWidth(l) {
this.dateWidth = l * 6
this.classWidth = l * 3
},
},
created() {
this.initGantt()
},
mounted() {
this.lisenScrol()
},
beforeDestroy() {
this.ctrlDown = false
this.theTimes = []
let gantt = document.getElementsByClassName('c-gant')
if(gantt && gantt[0]) gantt[0].removeEventListener('mousewheel', this.ganttZoom, false)
},
methods: {
// 初始化甘特图
initGantt() {
const {list = []} = this.body
let gArr = [], widthAll = 0;
this.timeAll = this.endStamp - this.startStamp
this.header.table.forEach(item => {
widthAll += item.width
})
this.widthAll = widthAll + this.dateWidth * this.header.dates.length
list.forEach(item => {
let arr = []
item.order.forEach(o => {
arr.push({
...o,
label: o.orderCode,
style: {
...o.style,
...getColor(o.num),
...(this.countWidth(o.startTime, o.endTime))
}
})
})
gArr.push(arr)
})
this.ganttArr = gArr
},
// 计算任务位置
countWidth(st, et) {
let w = this.dateWidth * this.header.dates.length,
s = this.$moment(st).valueOf(),
e = this.$moment(et).valueOf();
return {
left: parseInt((s - this.startStamp) * (w / this.timeAll)) + 'px',
width: parseInt((e - s) * (w / this.timeAll)) + 'px',
}
},
// 鼠标移入
mouseOver(e, d) {
if(this.popStyle && this.popStyle.display === 'none') {
this.popStyle = {
display: 'block',
top: e.clientY + 12+'px',
left: e.clientX - 100+'px'
}
this.currentData = {...d}
}
},
// 鼠标移出
mouseOut(e, d) {
this.popStyle = {
display: 'none',
top: 0,
left: 0
}
this.currentData = {}
},
// 右击
contextmenu(e, d) {
e.preventDefault();
this.modal = {
visible: true,
title: d.label,
data: {...d}
}
},
// 关闭弹窗
closeModal() {
this.modal = {
visible: false,
title: '',
data: {}
}
},
// 监听 Ctrl + 滚轮,缩放甘特图
lisenScrol() {
let w = this
document.onkeydown = function(e) {
if (e.keyCode === 17) w.ctrlDown = true
},
document.onkeyup = function(e) {
if (e.keyCode === 17) w.ctrlDown = false
},
document.getElementsByClassName('c-gant')[0].addEventListener('mousewheel', this.ganttZoom, false);
},
ganttZoom(e) {
e.preventDefault();
if(this.ctrlDown) {
let _newTimes = []
if(e.wheelDeltaY > 0) { // 放大
this.dateWidth = this.timeWidth * 24
this.classWidth = this.timeWidth * 12
this.header.dates.forEach(item => {
_newTimes = [ ..._newTimes, ...timeUp ]
});
} else { // 缩小
this.dateWidth = this.timeWidth * 6
this.classWidth = this.timeWidth * 3
this.header.dates.forEach(item=> {
_newTimes = [ ..._newTimes, ...timeDown ]
});
}
this.theTimes = _newTimes
this.initGantt()
}
}
},
}
</script>
<style lang="less" scoped>
.gantt-chart {
position: relative;
background-color: white;
.c-content {
border: 0.5px solid gray;
white-space: nowrap;
.c-table {
display: inline-block;
.c-header {
.th {
display: inline-block;
text-align: center;
border: 0.5px solid gray;
box-sizing: border-box;
word-break: break-all;
}
}
.c-body {
.tr{
.td {
display: inline-block;
text-align: center;
border: 0.5px solid gray;
box-sizing: border-box;
}
}
}
}
.c-gant {
display: inline-block;
.c-header {
.th {
display: inline-block;
text-align: center;
border: 0.5px solid gray;
box-sizing: border-box;
}
}
.c-body {
margin-top: -1px;
.tr{
position: relative;
.td {
display: inline-block;
text-align: center;
border: 0.5px solid gray;
box-sizing: border-box;
user-select: none;
}
.td-g {
position: absolute;
text-align: left;
box-sizing: border-box;
user-select: none;
z-index: 99;
color: white;
font-weight: 600;
letter-spacing: 1px;
cursor: pointer;
top: 1px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
}
}
}
}
#a-gantt-pop{
display: none;
position: fixed;
z-index: 200;
width: 200px;
min-width: 150px;
min-height: 100px;
box-shadow: 0 0 12px gray;
background-color: white;
padding: 10px;
border-radius: 5px;
overflow: hidden;
}
}
</style>

浙公网安备 33010602011771号