import moment from 'moment' /** * 甘特图 * @param {} options */ export function GanttChartMonth(options) { // 任务列表 this.tasks = options.tasks || [] // AntVG Canvas this.gCanvas = null // 视口宽度 800,可视区域 this.viewWidth = options['viewWidth'] || 800 // 物理画布宽度 800 this.cWidth = options['cWidth'] || 2400 this.cHeight = options['cHeight'] || 1600 // 画布偏移位置 this.startPos = 0 // 是否拖动中 this.draging = false // 开始拖动事件 this.startEvent = null // 结束拖动事件 this.endEvent = null // 拖动过程事件 this.dragingEvent = null // 拖动偏移量 this.offsetDis = options['viewWidth'] / 2 || 800 // 拖动定时器 this.dragTimer = null // 分组标题高度 this.groupTitleHeight = 18 // 任务矩形高度 this.taskRowHeight = 16 // 每行任务的纵向间距 this.rowSpanDis = 22 // 总天数 this.daysCount = options['daysCount'] || 365 * 1.5 // 每天的间隔宽度 this.dayStep = parseInt(this.cWidth / this.daysCount) // 任务图距离顶部高度 this.graphTopDis = 60 // 任务矩形最小宽度 this.minTaskRectWidth = 5 // 每像素代表的小时数 this.timePerPix = this.cWidth / this.daysCount / 24 / 3600 // 当前视图开始时间,向前推N天 this.startAt = moment().startOf('year') this.endAt = moment() .endOf('year') .add(6, 'months') this.graphDiv = document.getElementById(options['chartParentContainer']) this.chartContainer = options['chartContainer'] // 图形容器组 this.graphGroup = null // 上一次拖动的事件 this.lastDragEv = null // 当日刻度线 this.todayTimeLineEl = null // 点击回调事件 this.callback = options.callback || null // 翻页时间回调 this.pageToCallback = options.pageToCallback || null } /** * 设置数据 * @param {*} _tasks */ GanttChartMonth.prototype.changeTasks = function(_tasks) { this.tasks = _tasks console.error('change tasks data here....', this.tasks) this.forceRefreshGraph() } /** * 打开关闭分组 */ GanttChartMonth.prototype.toggle = function(index) { if (this.tasks[index].open) { this.tasks[index].open = false } else { this.tasks[index].open = true } this.processData() this.drawTasks() } /** * 预处理数据 */ GanttChartMonth.prototype.processData = function() { for (let i = 0; i < this.tasks.length; i++) { let currentTopTask = this.tasks[i] let lastTopTask = null currentTopTask.renderOptions = {} // 设置StartY if (i != 0) { // 非0个,要补上前面的数据 lastTopTask = this.tasks[i - 1] currentTopTask.renderOptions.startY = lastTopTask.renderOptions.endY + 20 } else { // 第一个 currentTopTask.renderOptions.startY = this.graphTopDis } // 设置EndY // 当有数据 且 打开时, // EndY = StartY + 标题上间距 + 标题 高度 + 任务个数 * (任务高度) if (currentTopTask.dataList.length > 0 && currentTopTask.open) { currentTopTask.renderOptions.endY = currentTopTask.renderOptions.startY + this.groupTitleHeight + currentTopTask.dataList.length * 38 } else { currentTopTask.renderOptions.endY = currentTopTask.renderOptions.startY + this.groupTitleHeight } } } /** * 强制清空图像,重绘 */ GanttChartMonth.prototype.forceRefreshGraph = function() { this.tasks.forEach((topTask) => { topTask.gGroup = null }) this.todayTimeLineEl = null this.gCanvas.destroy() this.initDrawingReady() } /** * 准备绘制,用于初始化和强制刷新 */ GanttChartMonth.prototype.initDrawingReady = function() { this.initCanvas() setTimeout(() => { this.initDragHandler() this.drawTimeZone() this.processData() this.drawTasks() this.graphGroup = null }, 200) } /** * 翻页 */ GanttChartMonth.prototype.pageTo = function(dir = 'next') { this.draging = false this.endEvent = null if (dir == 'next') { // 向后翻页` this.startAt = this.startAt.add(366, 'days') this.offsetDis = 0 } else { // 向前翻页 this.startAt = this.startAt.subtract(365, 'days') this.offsetDis = 2380 //2*this.viewWidth } this.endAt = moment(this.startAt).add(this.daysCount, 'days') this.pageToCallback({ dir: dir, startAt: this.startAt.format('YYYY-MM-DD'), endAt: this.endAt.format('YYYY-MM-DD') }) console.log('已翻页', this.startAt.format('YYYY-MM-DD'), this.endAt.format('YYYY-MM-DD'), this.offsetDis) // offsetDis = viewWidth; // this.forceRefreshGraph(); } // 上次点击时间,用于滚动时误触停止 let lastClickAt = null /** * 执行拖动 * 改变graphDiv 滚动距离 * 到达边界距离后,刷新页面 */ GanttChartMonth.prototype.doDrag = function(sEvent, eEvent) { if (sEvent == null) { sEvent = this.startEvent } let sPos = sEvent.clientX let cPos = eEvent.clientX // 滚动距离 let dis = cPos - sPos let tempDis = this.offsetDis // console.log('offsetDis before:', this.offsetDis, dis) this.offsetDis = this.offsetDis - dis / 2 // console.log('draging...',tempDis, this.offsetDis, dis) if (this.offsetDis <= -20) { // 向前滑动,向前翻页 console.log('此处应该向前翻页', this.startAt.format('YYYY-MM-DD'), this.endAt.format('YYYY-MM-DD'), this.offsetDis) this.offsetDis = this.viewWidth this.pageTo('prev') } if (this.offsetDis - 20 >= this.viewWidth * 2.2) { //cWidth - viewWidth) { // 向后滑动,向后翻页 console.log('此处应该向后翻页', this.startAt.format('YYYY-MM-DD'), this.endAt.format('YYYY-MM-DD'), this.offsetDis) this.offsetDis = this.viewWidth this.pageTo('next') } this.graphDiv.scrollLeft = this.offsetDis } /** * 初始化抓取拖动事件 */ GanttChartMonth.prototype.initDragHandler = function() { this.graphDiv.scrollLeft = this.offsetDis let _canvas = this._canvas _canvas.addEventListener('mousedown', (ev) => { this.draging = true this.startEvent = ev this.dragingEvent = null this.endEvent = null this.lastClickAt = new Date() this.lastClickAt = ev this.lastDragEv = ev }) _canvas.addEventListener('mouseleave', (ev) => { console.log('leave...恢复') this.draging = false this.endEvent = ev }) _canvas.addEventListener('mouseup', (ev) => { this.draging = false this.endEvent = ev }) _canvas.addEventListener('mousemove', (ev) => { // console.log('this over', this) if (this.draging) { if (new Date() - this.lastClickAt < 20) { return false } this.dragingEvent = ev this.doDrag(this.lastDragEv, ev) this.lastDragEv = ev } }) } /** * 初始化画布 * */ GanttChartMonth.prototype.initCanvas = function() { console.error('初始化画布...') this.gCanvas = new G.Canvas({ container: this.chartContainer, width: this.cWidth, height: this.cHeight, }) this._canvas = document.querySelector('#ganttContainer>canvas') } /** * 绘制时间区域 */ GanttChartMonth.prototype.drawTimeZone = function() { console.log('时间段', this.startAt.format('YYYY-MM-DD'), this.endAt.format('YYYY-MM-DD')) let start = moment(this.startAt) let timeGroup = this.gCanvas.addGroup() this.timeGroupEl = timeGroup timeGroup._tname = 'TimeGroup' let startSecond = moment(this.startAt) // 顶部底部边框 timeGroup.addShape('rect', { attrs: { x: 0, y: 0, width: this.cWidth, height: 52, fill: '#F8F9FA', radius: [2, 4], }, }) // 绘制第二级 let nowAtStr = moment().format('YYYY-MM-DD') let year = this.startAt.format('YYYY') this.timeZoneLineEls = [] for (let i = 1; i < 19; i++) { let tempAt = moment(this.startAt) tempAt.add(i - 1, 'months') // console.log('tempAt:', tempAt.format('YYYY-MM-DD')) // 当月天数 let daysCount = tempAt.dayOfYear() if (tempAt.format('YYYY') != year) { daysCount = parseInt(daysCount) + 365 } let timeText = tempAt.format('MM') if (timeText == '01') { // 第一天,顶部需要绘制年月 timeGroup.addShape('text', { attrs: { x: this.dayStep * daysCount + 100, y: 20, fontSize: 12, text: tempAt.format('YYYY'), lineDash: [10, 10], fill: '#8D9399', }, }) } let isToday = nowAtStr == tempAt.format('YYYY-MM-DD') if (isToday) { //是今日,需要画线 console.log('绘制 当日刻度线') this.todayTimeLineOffsetPos = this.dayStep * i timeGroup.addShape('rect', { attrs: { x: this.dayStep * i - 10, y: 25, width: 30, height: 16, fill: '#0091FF', radius: [2, 4], }, }) this.todayTimeLineEl = this.gCanvas.addShape('rect', { attrs: { x: this.todayTimeLineOffsetPos, y: 40, width: 4, height: this.cHeight, fill: '#CDE9FF', radius: [2, 4], }, }) // this.todayTimeLineEl.setZIndex(3) } // console.error('time:',tempAt.format('YYYY-MM'),', dayscount:', daysCount) timeGroup.addShape('text', { attrs: { x: this.dayStep * daysCount + 180 - 70, y: 40, fontSize: 10, text: timeText, lineDash: [10, 10], fill: '#8D9399', }, }) } } /** * 处理点击 */ GanttChartMonth.prototype.handleClick = function(task, flag, ev) { // let detailDiv = document.getElementById('detailDiv') if (flag == 'enter') { // detailDiv.style.display = 'block' // detailDiv.style.left = ev.clientX+'px'; // detailDiv.style.top = ev.clientY+'px'; // document.getElementById('detailTaskName').textContent = task._pdata.description // document.getElementById('detailTaskStatus').textContent = task._pdata.status // document.getElementById('detailTaskStartDate').textContent = task._pdata.startDate // document.getElementById('detailTaskEndDate').textContent = task._pdata.endDate console.log('show:', task) } else if (flag === 'leave') { // detailDiv.style.display = 'none' console.log('hide:', task) } else { this.callback(task) console.log('click:', task) } } /** * 根据任务状态区分颜色 * */ GanttChartMonth.prototype.statusColor = function(task) { switch (task.statusType) { case 1: return ['#e7e9ea', '#c3c7cb'] case 2: return ['#dee9fe', '#5b8ff9'] case 3: return ['#fbce99', '#f58300'] case 4: return ['#fbb8b5', '#f54e45'] default: return ['#fbb8b5', '#f54e45'] } } /** * 判断任务是否在视图内 * */ GanttChartMonth.prototype.isInView = function(task) { let isLessThanEndAt = task.endDate <= this.startAt.format('YYYY-MM-DD') let isGreaterThanStartAt = task.startDate >= this.endAt.format('YYYY-MM-DD') if (task.startDate == task.endDate) { // console.error('任务宽度为0',task) return true } // console.log('isInView', `${task.startDate} -- ${task.endDate}`, this.startAt.format('YYYY-MM-DD'), this.endAt.format('YYYY-MM-DD'), (!(isLessThanEndAt || isGreaterThanStartAt))) return !(isLessThanEndAt || isGreaterThanStartAt) } /** * 绘制完成之后的回调,用于某些工具缺陷的hack写法 */ GanttChartMonth.prototype.postDrawTasksCallBack = function() { // 画当前线条 TODO,放前面不行 if (true) { let todayAt = new Date() if (this.startAt < todayAt && this.endAt > todayAt) { let duraStartAt = new Date() - new Date(this.startAt.format('YYYY-MM-DD')) let secondsStartAt = duraStartAt / 1000 this.todayTimeLineOffsetPos = secondsStartAt * this.timePerPix this.todayTimeLineEl = this.gCanvas.addShape('rect', { attrs: { x: this.todayTimeLineOffsetPos, y: 50, width: 4, height: this.cHeight, fill: '#CDE9FF', radius: [2, 4], }, }) this.todayTimeLineEl.setZIndex(3) } } if (true) { let year = this.startAt.format('YYYY') for (let i = 1; i < 19; i++) { let tempAt = moment(this.startAt) tempAt.add(i - 1, 'months') // console.log('tempAt:', tempAt.format('YYYY-MM-DD')) // 当月天数 let daysCount = tempAt.dayOfYear() if (tempAt.format('YYYY') != year) { daysCount = parseInt(daysCount) + 365 } // 当月天数 // let daysCount = moment(`${year}-${i}`).dayOfYear() let lineEl = this.timeGroupEl.addShape('rect', { attrs: { x: this.dayStep * daysCount, y: 20, width: 1, height: this.cHeight, fill: '#deebeb', radius: [2, 4], }, }) lineEl.setZIndex(200) } } } /** * 分组绘制任务块 * */ GanttChartMonth.prototype.drawTasks = function() { this.graphGroup = this.gCanvas.addGroup() this.graphGroup._tname = 'graphGroup' // 第一层循环,用于分组,例如,维保--xxxx this.tasks.forEach((topTask, topIndex) => { try { if (topTask.open) { let taskGroup = null taskGroup = this.graphGroup.addGroup() taskGroup._tname = 'TaskGroup_' + topTask.id topTask.gGroup = taskGroup if (false) { // 组名背景矩形 let TopGroupRectEl = taskGroup.addShape('rect', { attrs: { x: 0, y: topTask.renderOptions.startY, width: this.cWidth, height: this.taskRowHeight, fill: '#F5F6F7', radius: [2, 4], }, }) TopGroupRectEl.setZIndex(-1) } // 第二层循环,用于 区分具体多少任务,例如,维保-商管1/商管2... topTask.dataList.forEach((taskP, index) => { let taskPGroup = taskGroup.addGroup() taskGroup.addGroup(taskPGroup) // 任务背景矩形,主要用于Hover效果变更颜色 if (true) { let tempTaskContainerEl = taskPGroup.addShape('rect', { attrs: { x: 0, y: topTask.renderOptions.startY + (index + 1) * (this.taskRowHeight + this.rowSpanDis) - 5, width: this.cWidth, height: this.taskRowHeight + 10, fill: '#fff', // stroke: 'black', radius: [2, 4], }, }) tempTaskContainerEl.setZIndex(1) tempTaskContainerEl._pdata = taskP tempTaskContainerEl.on('mouseenter', (ev) => { tempTaskContainerEl.attr({ fill: '#F5F6F7' }) tempTaskContainerEl._pdata.tasks.forEach((_tempTask) => { if (_tempTask._rectEl) { _tempTask._rectEl.setZIndex(5) } }) }) tempTaskContainerEl.on('mouseleave', (ev) => { tempTaskContainerEl.attr({ fill: '#fff' }) tempTaskContainerEl._pdata.tasks.forEach((_tempTask) => { if (_tempTask._rectEl) { _tempTask._rectEl.setZIndex(5) } }) }) taskP._containerEl = tempTaskContainerEl } // 第三层循环,用户区分每个子任务的执行时间段,例如:维保-商管1-2020.05-2020.06 / 2020.08- 2020.09 taskP.tasks.forEach((_taskItem, _index) => { let _isInView = this.isInView(_taskItem) // 在视图中才显示 if (_isInView) { let pos = this.calRectPos(_taskItem) // console.log('render rect:', _taskItem, pos, topTask.renderOptions.startY + index * taskRowHeight); let rectEl = taskPGroup.addShape('rect', { attrs: { x: pos.x, y: topTask.renderOptions.startY + (index + 1) * (this.taskRowHeight + this.rowSpanDis), width: pos.width, height: this.taskRowHeight, fill: this.statusColor(_taskItem)[0], stroke: this.statusColor(_taskItem)[1], radius: [2, 4], }, }) rectEl.setZIndex(50) rectEl._pdata = _taskItem _taskItem._rectEl = rectEl rectEl.on('mouseover', (ev) => { this.handleClick(ev.target, 'enter', ev) }) rectEl.on('mouseleave', (ev) => { this.handleClick(ev.target, 'leave', ev) }) rectEl.on('click', (ev) => { this.handleClick(ev.target, 'click', ev) }) } }) }) taskGroup.show() } else { if (topTask.gGroup) { // topTask.gGroup.hide() topTask.gGroup = null } } } catch (error) { console.error('drawTasks error:', error) } }) this.postDrawTasksCallBack() } /** * 根据 Task 计算矩形位置 * */ GanttChartMonth.prototype.calRectPos = function(taskItem) { let duraStartAt = new Date(taskItem.startDate) - new Date(this.startAt.format('YYYY-MM-DD')) let secondsStartAt = duraStartAt / 1000 let duraEndAt = new Date(taskItem.endDate) - new Date(taskItem.startDate) let secondsEndAt = duraEndAt / 1000 let width = secondsEndAt * this.timePerPix if (width < this.minTaskRectWidth) { width = this.minTaskRectWidth } // console.error('task rect width:', width) return { x: secondsStartAt * this.timePerPix, y: 0, width: width, height: 0, } } /** * 主函数 * */ GanttChartMonth.prototype.main = function() { this.initDrawingReady() }