GanttChart_day.js 20 KB

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