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.dayStep = parseInt(this.cWidth/365*2/3); // 分组标题高度 this.groupTitleHeight = 18; // 任务矩形高度 this.taskRowHeight = 16; // 每行任务的纵向间距 this.rowSpanDis = 22; // 总天数 this.daysCount = options['daysCount'] || 365; // 任务图距离顶部高度 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') 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(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'); 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 < 13; i++) { let tempAt = moment(`${year}-${i}`) let timeText = tempAt.format('MM'); if(timeText == '01'){ // 第一天,顶部需要绘制年月 timeGroup.addShape('text', { attrs: { x: this.dayStep * i, 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) } // 当月天数 let daysCount = moment(`${year}-${i}`).dayOfYear() timeGroup.addShape('text', { attrs: { x: this.dayStep * daysCount + 180, 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 < 13; i++) { // 当月天数 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(); }