自制甘特图组件
很抱歉,这是我自己写的甘特图,太简陋了。关于甘特图,我有了更简单、功能更强大、效果更完美的解决方案,不要再看这个。
用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>