GanttChart_month.js 20 KB

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