GanttChart_month.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. import moment from 'moment';
  2. /**
  3. * 甘特图
  4. * @param {} options
  5. */
  6. export function GanttChartMonth(options) {
  7. // 任务列表
  8. this.tasks = options.tasks || [];
  9. // AntVG Canvas
  10. this.gCanvas = null;
  11. // 视口宽度 800,可视区域
  12. this.viewWidth = options['viewWidth'] || 800;
  13. // 物理画布宽度 800
  14. this.cWidth = options['cWidth'] || 2400;
  15. this.cHeight = options['cHeight'] || 1600;
  16. // 画布偏移位置
  17. this.startPos = 0;
  18. // 是否拖动中
  19. this.draging = false;
  20. // 开始拖动事件
  21. this.startEvent = null;
  22. // 结束拖动事件
  23. this.endEvent = null;
  24. // 拖动过程事件
  25. this.dragingEvent = null;
  26. // 拖动偏移量
  27. this.offsetDis = (options['viewWidth']/2) || 800;
  28. // 拖动定时器
  29. this.dragTimer = null;
  30. // 每天的间隔宽度
  31. this.dayStep = parseInt(this.cWidth/365);
  32. // 分组标题高度
  33. this.groupTitleHeight = 18;
  34. // 任务矩形高度
  35. this.taskRowHeight = 16;
  36. // 每行任务的纵向间距
  37. this.rowSpanDis = 22;
  38. // 总天数
  39. this.daysCount = options['daysCount'] || 365;
  40. // 任务图距离顶部高度
  41. this.graphTopDis = 60
  42. // 任务矩形最小宽度
  43. this.minTaskRectWidth = 5
  44. // 每像素代表的小时数
  45. this.timePerPix = this.cWidth/this.daysCount/24/3600
  46. // 当前视图开始时间,向前推N天
  47. this.startAt = moment().startOf('year')
  48. this.endAt = moment().endOf('year')
  49. this.graphDiv = document.getElementById(options['chartParentContainer']);
  50. this.chartContainer = options['chartContainer']
  51. // 图形容器组
  52. this.graphGroup = null;
  53. // 上一次拖动的事件
  54. this.lastDragEv = null;
  55. // 当日刻度线
  56. this.todayTimeLineEl = null;
  57. // 点击回调事件
  58. this.callback = options.callback || null;
  59. // 翻页时间回调
  60. this.pageToCallback = options.pageToCallback || null;
  61. }
  62. /**
  63. * 设置数据
  64. * @param {*} _tasks
  65. */
  66. GanttChartMonth.prototype.changeTasks = function(_tasks){
  67. this.tasks = _tasks
  68. console.error('change tasks data here....', this.tasks)
  69. this.forceRefreshGraph()
  70. }
  71. /**
  72. * 打开关闭分组
  73. */
  74. GanttChartMonth.prototype.toggle = function(index) {
  75. if (this.tasks[index].open) {
  76. this.tasks[index].open = false;
  77. } else {
  78. this.tasks[index].open = true;
  79. }
  80. this.processData();
  81. this.drawTasks();
  82. }
  83. /**
  84. * 预处理数据
  85. */
  86. GanttChartMonth.prototype.processData = function() {
  87. for (let i = 0; i < this.tasks.length; i++) {
  88. let currentTopTask = this.tasks[i];
  89. let lastTopTask = null;
  90. currentTopTask.renderOptions = {};
  91. // 设置StartY
  92. if (i != 0) {
  93. // 非0个,要补上前面的数据
  94. lastTopTask = this.tasks[i - 1];
  95. currentTopTask.renderOptions.startY = lastTopTask.renderOptions.endY + 20;
  96. } else {
  97. // 第一个
  98. currentTopTask.renderOptions.startY = this.graphTopDis;
  99. }
  100. // 设置EndY
  101. // 当有数据 且 打开时,
  102. // EndY = StartY + 标题上间距 + 标题 高度 + 任务个数 * (任务高度)
  103. if (currentTopTask.dataList.length >0 && currentTopTask.open) {
  104. currentTopTask.renderOptions.endY =
  105. currentTopTask.renderOptions.startY + this.groupTitleHeight + currentTopTask.dataList.length * 38;
  106. } else {
  107. currentTopTask.renderOptions.endY = currentTopTask.renderOptions.startY + this.groupTitleHeight;
  108. }
  109. }
  110. }
  111. /**
  112. * 强制清空图像,重绘
  113. */
  114. GanttChartMonth.prototype.forceRefreshGraph = function() {
  115. this.tasks.forEach((topTask) => {
  116. topTask.gGroup = null;
  117. });
  118. this.todayTimeLineEl = null
  119. this.gCanvas.destroy();
  120. this.initDrawingReady();
  121. }
  122. /**
  123. * 准备绘制,用于初始化和强制刷新
  124. */
  125. GanttChartMonth.prototype.initDrawingReady = function() {
  126. this.initCanvas();
  127. setTimeout(() => {
  128. this.initDragHandler();
  129. this.drawTimeZone();
  130. this.processData();
  131. this.drawTasks();
  132. this.graphGroup = null
  133. }, 200);
  134. }
  135. /**
  136. * 翻页
  137. */
  138. GanttChartMonth.prototype.pageTo = function(dir = 'next') {
  139. this.draging = false;
  140. this.endEvent = null;
  141. if (dir == 'next') {
  142. // 向后翻页`
  143. this.startAt = this.startAt.add(this.daysCount, 'days');
  144. this.offsetDis = 0
  145. } else {
  146. // 向前翻页
  147. this.startAt = this.startAt.subtract(this.daysCount, 'days');
  148. this.offsetDis = 2*this.viewWidth
  149. }
  150. this.endAt = moment(this.startAt).add(this.daysCount, 'days');
  151. this.pageToCallback({dir: dir, startAt: this.startAt.format('YYYY-MM-DD'), endAt: this.endAt.format('YYYY-MM-DD')})
  152. console.log('已翻页', this.startAt.format('YYYY-MM-DD'),this.endAt.format('YYYY-MM-DD'), this.offsetDis);
  153. // offsetDis = viewWidth;
  154. this.forceRefreshGraph();
  155. }
  156. // 上次点击时间,用于滚动时误触停止
  157. let lastClickAt = null;
  158. /**
  159. * 执行拖动
  160. * 改变graphDiv 滚动距离
  161. * 到达边界距离后,刷新页面
  162. */
  163. GanttChartMonth.prototype.doDrag = function(sEvent, eEvent) {
  164. if (sEvent == null) {
  165. sEvent = this.startEvent;
  166. }
  167. let sPos = sEvent.clientX;
  168. let cPos = eEvent.clientX;
  169. // 滚动距离
  170. let dis = cPos - sPos;
  171. let tempDis = this.offsetDis
  172. // console.log('offsetDis before:', this.offsetDis, dis)
  173. this.offsetDis = this.offsetDis - dis / 2;
  174. // console.log('draging...',tempDis, this.offsetDis, dis)
  175. if (this.offsetDis <= -20) {
  176. // 向前滑动,向前翻页
  177. console.log('此处应该向前翻页', this.startAt.format('YYYY-MM-DD'),this.endAt.format('YYYY-MM-DD'),this.offsetDis);
  178. this.offsetDis = this.viewWidth;
  179. this.pageTo('prev');
  180. }
  181. if ((this.offsetDis - 20) >= this.viewWidth*2.2 ){ //cWidth - viewWidth) {
  182. // 向后滑动,向后翻页
  183. console.log('此处应该向后翻页', this.startAt.format('YYYY-MM-DD'),this.endAt.format('YYYY-MM-DD'),this.offsetDis);
  184. this.offsetDis = this.viewWidth;
  185. this.pageTo('next');
  186. }
  187. this.graphDiv.scrollLeft = this.offsetDis;
  188. }
  189. /**
  190. * 初始化抓取拖动事件
  191. */
  192. GanttChartMonth.prototype.initDragHandler = function() {
  193. this.graphDiv.scrollLeft = this.offsetDis;
  194. let _canvas = this._canvas
  195. _canvas.addEventListener('mousedown', (ev) => {
  196. this.draging = true;
  197. this.startEvent = ev;
  198. this.dragingEvent = null;
  199. this.endEvent = null;
  200. this.lastClickAt = new Date();
  201. this.lastClickAt = ev;
  202. this.lastDragEv = ev;
  203. });
  204. _canvas.addEventListener('mouseleave', (ev) => {
  205. console.log('leave...恢复')
  206. this.draging = false;
  207. this.endEvent = ev;
  208. });
  209. _canvas.addEventListener('mouseup', (ev) => {
  210. this.draging = false;
  211. this.endEvent = ev;
  212. });
  213. _canvas.addEventListener('mousemove', (ev) => {
  214. // console.log('this over', this)
  215. if (this.draging) {
  216. if (new Date() - this.lastClickAt < 20) {
  217. return false;
  218. }
  219. this.dragingEvent = ev;
  220. this.doDrag(this.lastDragEv, ev);
  221. this.lastDragEv = ev;
  222. }
  223. });
  224. }
  225. /**
  226. * 初始化画布
  227. *
  228. */
  229. GanttChartMonth.prototype.initCanvas = function() {
  230. console.error('初始化画布...')
  231. this.gCanvas = new G.Canvas({
  232. container: this.chartContainer,
  233. width: this.cWidth,
  234. height: this.cHeight,
  235. });
  236. this._canvas = document.querySelector('#ganttContainer>canvas');
  237. }
  238. /**
  239. * 绘制时间区域
  240. */
  241. GanttChartMonth.prototype.drawTimeZone = function() {
  242. console.log('时间段', this.startAt.format('YYYY-MM-DD'),this.endAt.format('YYYY-MM-DD'));
  243. let start = moment(this.startAt);
  244. let timeGroup = this.gCanvas.addGroup();
  245. this.timeGroupEl = timeGroup
  246. timeGroup._tname = 'TimeGroup';
  247. let startSecond = moment(this.startAt);
  248. // 顶部底部边框
  249. timeGroup.addShape('rect',{
  250. attrs: {
  251. x: 0,
  252. y: 0,
  253. width: this.cWidth,
  254. height: 52,
  255. fill: '#F8F9FA',
  256. radius: [2, 4],
  257. },
  258. })
  259. // 绘制第二级
  260. let nowAtStr = moment().format('YYYY-MM-DD')
  261. let year = this.startAt.format('YYYY')
  262. this.timeZoneLineEls = []
  263. for (let i = 1; i < 13; i++) {
  264. let tempAt = moment(`${year}-${i}`)
  265. let timeText = tempAt.format('MM');
  266. if(timeText == '01'){
  267. // 第一天,顶部需要绘制年月
  268. timeGroup.addShape('text', {
  269. attrs: {
  270. x: this.dayStep * i,
  271. y: 20,
  272. fontSize: 12,
  273. text: tempAt.format('YYYY'),
  274. lineDash: [10, 10],
  275. fill: '#8D9399',
  276. },
  277. });
  278. }
  279. let isToday = nowAtStr == tempAt.format('YYYY-MM-DD')
  280. if(isToday){
  281. //是今日,需要画线
  282. console.log('绘制 当日刻度线')
  283. this.todayTimeLineOffsetPos = this.dayStep * i
  284. timeGroup.addShape('rect',{
  285. attrs: {
  286. x: this.dayStep * i-10,
  287. y: 25,
  288. width: 30,
  289. height: 16,
  290. fill: '#0091FF',
  291. radius: [2, 4],
  292. },
  293. })
  294. this.todayTimeLineEl = this.gCanvas.addShape('rect',{
  295. attrs: {
  296. x: this.todayTimeLineOffsetPos,
  297. y: 40,
  298. width: 4,
  299. height: this.cHeight,
  300. fill: '#CDE9FF',
  301. radius: [2, 4],
  302. }
  303. })
  304. // this.todayTimeLineEl.setZIndex(3)
  305. }
  306. // 当月天数
  307. let daysCount = moment(`${year}-${i}`).dayOfYear()
  308. timeGroup.addShape('text', {
  309. attrs: {
  310. x: this.dayStep * daysCount,
  311. y: 40,
  312. fontSize: 10,
  313. text: timeText,
  314. lineDash: [10, 10],
  315. fill: '#8D9399',
  316. },
  317. });
  318. }
  319. }
  320. /**
  321. * 处理点击
  322. */
  323. GanttChartMonth.prototype.handleClick = function(task, flag, ev) {
  324. // let detailDiv = document.getElementById('detailDiv')
  325. if(flag == 'enter'){
  326. // detailDiv.style.display = 'block'
  327. // detailDiv.style.left = ev.clientX+'px';
  328. // detailDiv.style.top = ev.clientY+'px';
  329. // document.getElementById('detailTaskName').textContent = task._pdata.description
  330. // document.getElementById('detailTaskStatus').textContent = task._pdata.status
  331. // document.getElementById('detailTaskStartDate').textContent = task._pdata.startDate
  332. // document.getElementById('detailTaskEndDate').textContent = task._pdata.endDate
  333. console.log('show:', task);
  334. } else if (flag === 'leave'){
  335. // detailDiv.style.display = 'none'
  336. console.log('hide:', task);
  337. } else {
  338. this.callback(task);
  339. console.log('click:', task);
  340. }
  341. }
  342. /**
  343. * 根据任务状态区分颜色
  344. *
  345. */
  346. GanttChartMonth.prototype.statusColor =function(task) {
  347. switch (task.statusType) {
  348. case 1:
  349. return ['#e7e9ea', '#c3c7cb'];
  350. case 2:
  351. return ['#dee9fe', '#5b8ff9'];
  352. case 3:
  353. return ['#fbce99', '#f58300'];
  354. case 4:
  355. return ['#fbb8b5', '#f54e45'];
  356. default:
  357. return ['#fbb8b5', '#f54e45'];
  358. }
  359. }
  360. /**
  361. * 判断任务是否在视图内
  362. *
  363. */
  364. GanttChartMonth.prototype.isInView = function(task) {
  365. let isLessThanEndAt = (task.endDate <= this.startAt.format('YYYY-MM-DD'))
  366. let isGreaterThanStartAt = task.startDate >= this.endAt.format('YYYY-MM-DD')
  367. if(task.startDate == task.endDate){
  368. // console.error('任务宽度为0',task)
  369. return true
  370. }
  371. // console.log('isInView', `${task.startDate} -- ${task.endDate}`, this.startAt.format('YYYY-MM-DD'), this.endAt.format('YYYY-MM-DD'), (!(isLessThanEndAt || isGreaterThanStartAt)))
  372. return !(isLessThanEndAt || isGreaterThanStartAt)
  373. }
  374. /**
  375. * 绘制完成之后的回调,用于某些工具缺陷的hack写法
  376. */
  377. GanttChartMonth.prototype.postDrawTasksCallBack = function(){
  378. // 画当前线条 TODO,放前面不行
  379. if(true){
  380. let todayAt = new Date()
  381. if(this.startAt < todayAt && this.endAt > todayAt){
  382. this.todayTimeLineEl = this.gCanvas.addShape('rect',{
  383. attrs: {
  384. x: this.todayTimeLineOffsetPos,
  385. y: 40,
  386. width: 4,
  387. height: this.cHeight,
  388. fill: '#CDE9FF',
  389. radius: [2, 4],
  390. }
  391. })
  392. this.todayTimeLineEl.setZIndex(3)
  393. }
  394. }
  395. if(true){
  396. let year = this.startAt.format('YYYY')
  397. for (let i = 1; i < 13; i++) {
  398. // 当月天数
  399. let daysCount = moment(`${year}-${i}`).dayOfYear()
  400. let lineEl = this.timeGroupEl.addShape('rect', {
  401. attrs: {
  402. x: this.dayStep * daysCount,
  403. y: 20,
  404. width: 1,
  405. height: this.cHeight,
  406. fill: '#deebeb',
  407. radius: [2, 4],
  408. },
  409. });
  410. lineEl.setZIndex(200)
  411. }
  412. }
  413. }
  414. /**
  415. * 分组绘制任务块
  416. *
  417. */
  418. GanttChartMonth.prototype.drawTasks = function() {
  419. this.graphGroup = this.gCanvas.addGroup();
  420. this.graphGroup._tname = 'graphGroup';
  421. // 第一层循环,用于分组,例如,维保--xxxx
  422. this.tasks.forEach((topTask, topIndex) => {
  423. try {
  424. if (topTask.open) {
  425. let taskGroup = null;
  426. taskGroup = this.graphGroup.addGroup();
  427. taskGroup._tname = 'TaskGroup_' + topTask.id;
  428. topTask.gGroup = taskGroup;
  429. if(false){
  430. // 组名背景矩形
  431. let TopGroupRectEl = taskGroup.addShape('rect', {
  432. attrs: {
  433. x: 0,
  434. y: topTask.renderOptions.startY,
  435. width: this.cWidth,
  436. height: this.taskRowHeight,
  437. fill: '#F5F6F7',
  438. radius: [2, 4],
  439. },
  440. });
  441. TopGroupRectEl.setZIndex(-1)
  442. }
  443. // 第二层循环,用于 区分具体多少任务,例如,维保-商管1/商管2...
  444. topTask.dataList.forEach((taskP, index) => {
  445. let taskPGroup = taskGroup.addGroup()
  446. taskGroup.addGroup(taskPGroup);
  447. // 任务背景矩形,主要用于Hover效果变更颜色
  448. if(true){
  449. let tempTaskContainerEl = taskPGroup.addShape('rect', {
  450. attrs: {
  451. x: 0,
  452. y: topTask.renderOptions.startY + ((index+1)* (this.taskRowHeight + this.rowSpanDis))-5,
  453. width: this.cWidth,
  454. height: this.taskRowHeight+10,
  455. fill: '#fff',
  456. // stroke: 'black',
  457. radius: [2, 4],
  458. },
  459. });
  460. tempTaskContainerEl.setZIndex(1)
  461. tempTaskContainerEl._pdata = taskP
  462. tempTaskContainerEl.on('mouseenter',(ev)=>{
  463. tempTaskContainerEl.attr({fill: '#F5F6F7'})
  464. tempTaskContainerEl._pdata.tasks.forEach(_tempTask=>{
  465. if(_tempTask._rectEl){
  466. _tempTask._rectEl.setZIndex(5)
  467. }
  468. })
  469. })
  470. tempTaskContainerEl.on('mouseleave',(ev)=>{
  471. tempTaskContainerEl.attr({fill: '#fff'})
  472. tempTaskContainerEl._pdata.tasks.forEach(_tempTask=>{
  473. if(_tempTask._rectEl){
  474. _tempTask._rectEl.setZIndex(5)
  475. }
  476. })
  477. })
  478. taskP._containerEl = tempTaskContainerEl
  479. }
  480. // 第三层循环,用户区分每个子任务的执行时间段,例如:维保-商管1-2020.05-2020.06 / 2020.08- 2020.09
  481. taskP.tasks.forEach((_taskItem, _index) => {
  482. let _isInView = this.isInView(_taskItem)
  483. // 在视图中才显示
  484. if(_isInView){
  485. let pos = this.calRectPos(_taskItem);
  486. // console.log('render rect:', _taskItem, pos, topTask.renderOptions.startY + index * taskRowHeight);
  487. let rectEl = taskPGroup.addShape('rect', {
  488. attrs: {
  489. x: pos.x,
  490. y: topTask.renderOptions.startY + ((index + 1)* (this.taskRowHeight + this.rowSpanDis)),
  491. width: pos.width,
  492. height: this.taskRowHeight,
  493. fill: this.statusColor(_taskItem)[0],
  494. stroke: this.statusColor(_taskItem)[1],
  495. radius: [2, 4],
  496. },
  497. });
  498. rectEl.setZIndex(50)
  499. rectEl._pdata = _taskItem;
  500. _taskItem._rectEl = rectEl
  501. rectEl.on('mouseover', (ev) => {
  502. this.handleClick(ev.target, 'enter', ev);
  503. });
  504. rectEl.on('mouseleave', (ev) => {
  505. this.handleClick(ev.target, 'leave', ev);
  506. });
  507. rectEl.on('click', (ev) => {
  508. this.handleClick(ev.target, 'click', ev);
  509. });
  510. }
  511. });
  512. });
  513. taskGroup.show();
  514. } else {
  515. if (topTask.gGroup) {
  516. // topTask.gGroup.hide()
  517. topTask.gGroup = null;
  518. }
  519. }
  520. } catch (error) {
  521. console.error('drawTasks error:', error)
  522. }
  523. });
  524. this.postDrawTasksCallBack()
  525. }
  526. /**
  527. * 根据 Task 计算矩形位置
  528. *
  529. */
  530. GanttChartMonth.prototype.calRectPos = function(taskItem) {
  531. let duraStartAt = new Date(taskItem.startDate) - new Date(this.startAt.format('YYYY-MM-DD'));
  532. let secondsStartAt = duraStartAt/1000
  533. let duraEndAt = new Date(taskItem.endDate) - new Date(taskItem.startDate);
  534. let secondsEndAt = duraEndAt/1000
  535. let width = secondsEndAt * this.timePerPix
  536. if(width < this.minTaskRectWidth){
  537. width = this.minTaskRectWidth
  538. }
  539. // console.error('task rect width:', width)
  540. return {
  541. x: secondsStartAt * this.timePerPix,
  542. y: 0,
  543. width: width,
  544. height: 0,
  545. };
  546. }
  547. /**
  548. * 主函数
  549. *
  550. */
  551. GanttChartMonth.prototype.main = function() {
  552. this.initDrawingReady();
  553. }