import moment from 'moment'; /** * 甘特图 * @param {} options */ export function GanttChart(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'] || 600; // 画布偏移位置 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.dayStep = 40; // 分组标题高度 this.groupTitleHeight = 38; // 任务矩形高度 this.taskRowHeight = 16; // 每行任务的纵向间距 this.rowSpanDis = 22; // 总天数 this.daysCount = options['daysCount'] || 60; // 任务图距离顶部高度 this.graphTopDis = 40 // 每像素代表的小时数 this.timePerPix = this.cWidth/this.daysCount/24/3600 // 当前视图开始时间,向前推N天 this.startAt = moment().subtract(this.daysCount / 3, 'days'); this.endAt = moment(this.startAt).add(this.daysCount, 'days'); this.graphDiv = document.getElementById(options['chartParentContainer']); this.chartContainer = options['chartContainer'] // 图形容器组 this.graphGroup = null; // 上一次拖动的事件 this.lastDragEv = null; // 当日刻度线 this.todayTimeLineEl = null; } /** * 设置数据 * @param {*} _tasks */ GanttChart.prototype.changeTasks = function(_tasks){ this.tasks = _tasks } /** * 打开关闭分组 */ GanttChart.prototype.toggle = function(index) { if (this.tasks[index].open) { this.tasks[index].open = false; } else { this.tasks[index].open = true; } this.processData(); this.drawTasks(); } /** * 预处理数据 */ GanttChart.prototype.processData = function() { for (let i = 0; i < this.tasks.length; i++) { let currentTopTask = this.tasks[i]; let lastTopTask = null; currentTopTask.renderOptions = {}; if (i != 0) { // 非0个,要补上前面的数据 lastTopTask = this.tasks[i - 1]; currentTopTask.renderOptions.startY = lastTopTask.renderOptions.endY + 20; } else { // 第一个 currentTopTask.renderOptions.startY = this.graphTopDis; } if (currentTopTask.open) { currentTopTask.renderOptions.endY = currentTopTask.renderOptions.startY + this.rowSpanDis + this.groupTitleHeight + currentTopTask.dataList.length * this.taskRowHeight; } else { currentTopTask.renderOptions.endY = currentTopTask.renderOptions.startY + this.groupTitleHeight; } } } /** * 强制清空图像,重绘 */ GanttChart.prototype.forceRefreshGraph = function() { this.tasks.forEach((topTask) => { topTask.gGroup = null; }); this.todayTimeLineEl = null this.gCanvas.destroy(); this.initDrawingReady(); } /** * 准备绘制,用于初始化和强制刷新 */ GanttChart.prototype.initDrawingReady = function() { this.initCanvas(); setTimeout(() => { this.initDragHandler(); this.drawTimeZone(); this.processData(); this.drawTasks(); this.graphGroup = null }, 200); } /** * 翻页 */ GanttChart.prototype.pageTo = function(dir = 'next') { this.draging = false; this.endEvent = null; if (dir == 'next') { // 向后翻页` this.startAt = this.startAt.add(this.daysCount, 'days'); this.offsetDis = 0 } else { // 向前翻页 this.startAt = this.startAt.subtract(this.daysCount, 'days'); this.offsetDis = 2*this.viewWidth } this.endAt = moment(this.startAt).add(this.daysCount, 'days'); console.log('已翻页', this.startAt.format('YYYY-MM-DD'),this.endAt.format('YYYY-MM-DD'), this.offsetDis); // offsetDis = viewWidth; this.forceRefreshGraph(); } // 上次点击时间,用于滚动时误触停止 let lastClickAt = null; /** * 执行拖动 * 改变graphDiv 滚动距离 * 到达边界距离后,刷新页面 */ GanttChart.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; } /** * 初始化抓取拖动事件 */ GanttChart.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; } }); } /** * 初始化画布 * */ GanttChart.prototype.initCanvas = function() { console.error('初始化画布...') this.gCanvas = new G.Canvas({ container: this.chartContainer, width: this.cWidth, height: this.cHeight, }); this._canvas = document.getElementsByTagName('canvas')[0]; } /** * 绘制时间区域 */ GanttChart.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: 40, fill: '#dbcccc', radius: [2, 4], }, }) // 绘制第二级 let nowAtStr = moment().format('YYYY-MM-DD') for (let i = 0; i < this.daysCount; i++) { let tempAt = startSecond.add(1, 'days') let timeText = tempAt.format('DD'); if(timeText == '01'){ // 第一天,顶部需要绘制年月 timeGroup.addShape('text', { attrs: { x: this.dayStep * i, y: 20, fontSize: 12, text: tempAt.format('YYYY-MM'), 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], }, }) } timeGroup.addShape('text', { attrs: { x: this.dayStep * i, y: 40, fontSize: 10, text: timeText, lineDash: [10, 10], fill: '#8D9399', }, }); timeGroup.addShape('rect', { attrs: { x: this.dayStep * i-10, y: 20, width: 1, height: this.cHeight, fill: '#deebeb', radius: [2, 4], }, }); } } /** * 处理点击 */ GanttChart.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); } } /** * 根据任务状态区分颜色 * */ GanttChart.prototype.statusColor =function(task) { switch (task.status) { case '按时完成': return 'aqua'; break; case '计划批准': return '#ff9800'; break; case '已完成': return '#19b720'; break; default: break; } } /** * 判断任务是否在视图内 * */ GanttChart.prototype.isInView = function(task) { let isLessThanEndAt = (task.endDate <= this.startAt.format('YYYY-MM-DD')) let isGreaterThanStartAt = task.startDate >= this.endAt.format('YYYY-MM-DD') return !(isLessThanEndAt || isGreaterThanStartAt) } /** * 分组绘制任务块 * */ GanttChart.prototype.drawTasks = function() { if (this.graphGroup) { this.graphGroup.clear(); } else { this.graphGroup = this.gCanvas.addGroup(); this.graphGroup._tname = 'graphGroup'; } // 第一层循环,用于分组,例如,维保--xxxx this.tasks.forEach((topTask, topIndex) => { if (topTask.open) { let taskGroup = null; taskGroup = this.graphGroup.addGroup(); taskGroup._tname = 'TaskGroup_' + topTask.id; topTask.gGroup = taskGroup; // 组名背景矩形 // let TopGroupRectEl = taskGroup.addShape('rect', { // attrs: { // x: 0, // y: topTask.renderOptions.startY, // width: this.cWidth, // height: this.taskRowHeight, // fill: '#a6ed53', // radius: [2, 4], // }, // }); // 第二层循环,用于 区分具体多少任务,例如,维保-商管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* (this.taskRowHeight + this.rowSpanDis)), width: pos.width, height: this.taskRowHeight, fill: this.statusColor(_taskItem), stroke: 'black', radius: [2, 4], }, }); rectEl.setZIndex(5) 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; } } }); // 画当前线条 TODO,放前面不行 let todayAt = new Date() if(this.startAt < todayAt && this.endAt > todayAt){ this.todayTimeLineEl = this.gCanvas.addShape('rect',{ attrs: { x: this.todayTimeLineOffsetPos, y: 40, width: 3, height: this.cHeight, fill: '#0091FF', radius: [2, 4], } }) this.todayTimeLineEl.setZIndex(50) } } /** * 根据 Task 计算矩形位置 * */ GanttChart.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 return { x: secondsStartAt * this.timePerPix, y: 0, width: secondsEndAt * this.timePerPix, height: 0, }; } /** * 主函数 * */ GanttChart.prototype.main = function() { this.initDrawingReady(); }