您现在的位置是:首页 > 技术教程 正文

vue2实现可拖拽甘特图(结合element-ui的gantt图)

admin 阅读: 2024-03-29
后台-插件-广告管理-内容页头部广告(手机)

一、前言

  接到公司需求,要做一个可拖拽的甘特图来实现排期需求,官方的插件要付费还没有中文的官方文档可以看,就去找了各种开源的demo来看,功能上都不是很齐全,于是总结了很多demo,合在一起组成了一版较为完整的满足需求的甘特图。

二、主要功能

1.拖拽  拖拽功能是甘特图的主要功能,该demo实现了甘特图时间块上、下、左、右拖拽功能。

2.排序 拖拽后时间块进行排序,计算重叠区域大小确定插入位置。

3.时间选择 结合element-ui的日期时间选择器来确定时间轴。

4.搜索  搜索已存在的时间块,并定位到相应位置。

5.新建排期任务  使用element-ui的弹框以及表单 新建成功的排期列表添加到排期任务中。

6.右键菜单 右击时间块,可以进行查看、删除、修改等操作。

7.撤回 每删除或移动时间块后,增加一条操作记录,点击撤回可撤回当前操作。

8.批量操作 在批量操作后点击保存,才向后端存储数据。

三、功能实现

1.默认时间轴线今天的前三后四

  1. // 设置默认时间 当前日期前三后四
  2. defaultDate() {
  3. const beg = new Date(new Date().getTime() - 3600 * 1000 * 24 * 3)
  4. .toISOString()
  5. .replace('T', ' ')
  6. .split('.')[0] //默认开始时间3天前
  7. const end = new Date(new Date().getTime() + 3600 * 1000 * 24 * 4)
  8. .toISOString()
  9. .replace('T', ' ')
  10. .split('.')[0]//默认结束时间4天后
  11. this.choiceTime = [beg, end] //将值设置给插件绑定的数据
  12. // console.log(this.value1);
  13. },

 2.拖拽事件实现

  1. onMouseDown(e, blockId, rowIndex) {
  2. // 删除模式下不处理拖动事件
  3. if (this.isDeleteMode) {
  4. return;
  5. }
  6. this.moveX = 0;
  7. this.moveY = 0;
  8. // 用box 移动,不采用 Doucment
  9. const box = this.$refs.box;
  10. const dom = e.target;
  11. // 算出鼠标相对元素的位置
  12. const disX = e.clientX - dom.offsetLeft;
  13. const disY = e.clientY - dom.offsetTop;
  14. console.log('鼠标正在拖动',e.clientX,dom.offsetLeft);
  15. // 当点击下来的时候 nowSuck 其实等于的就是当前index
  16. this.nowSuck = rowIndex;
  17. // 让本来拥有手掌样式的class取消
  18. dom.classList.remove('gantt-grab');
  19. // 让整个box 鼠标都是抓住
  20. box.classList.add('gantt-grabbing');
  21. // 如果事件绑定在 dom上,那么一旦鼠标移动过快就会脱离掌控
  22. box.onmousemove = ee => {
  23. // 获得当前受到拖拽的div 用于计算suck 所谓拖引的行数
  24. const top = parseInt(dom.style.top);
  25. // 四舍五入 获得磁性吸附激活的值 (索引值) 60是block的height 10是时间块距离block的top suck 可以当作row的索引
  26. let suck = Math.round((top - 10) / 60) + rowIndex;
  27. // suck的边界值设置
  28. if (suck < 0) {
  29. suck = 0;
  30. } else if (suck > this.ganttData.length - 1) {
  31. suck = this.ganttData.length - 1;
  32. }
  33. // suck 行样式变化
  34. this.nowSuck = suck;
  35. // 移动事件
  36. this.onMouseMove(ee, disX, disY, dom);
  37. // dom.style.left=this.moveX;
  38. };
  39. // 不管在哪里,鼠标松开的时候,清空事件 所以对于这个 “不管在哪里,使用了window”
  40. window.onmouseup = () => {
  41. // 鼠标松开了,让时间块恢复手掌样式
  42. dom.classList.add('gantt-grab');
  43. // 整个box 不在抓住了,变成箭头鼠标
  44. box.classList.remove('gantt-grabbing');
  45. // 当移动距离小于5时,不做移动处理
  46. //console.log('移动距离:', this.moveX, this.moveY);
  47. if (this.moveX < 1 && this.moveY < 1 && this.moveX > -1 && this.moveY > -1) {
  48. console.log('无效移动');
  49. box.onmousemove = null;
  50. window.onmouseup = null;
  51. this.nowSuck = -1;
  52. return;
  53. }
  54. // 保存操作日志
  55. this._addHisList(this.ganttData);
  56. const index = this.ganttData[rowIndex][this.listKey].findIndex(item => {
  57. return item.id === blockId;
  58. });
  59. const oldTimeBlock = this.ganttData[rowIndex][this.listKey][index];
  60. // let timeId = oldTimeBlock.id;
  61. // startTime 与 endTime 用于数据重新替换 上面css已经经过计算 15px为1小时
  62. const startTime = new Date(Date.parse(this.choiceTime[0]) + (parseInt(dom.style.left) * 3600000) / 15);
  63. const endTime = new Date(this._getTime(startTime) + (parseInt(dom.style.width) * 3600000) / 15);
  64. // console.log(startTime, endTime, dom.style.width, parseInt(dom.style.left) * 60 * 1000);
  65. const suck = this.nowSuck;
  66. // 加入新数据
  67. const data = oldTimeBlock;
  68. // 更新开始时间和结束时间
  69. this.$set(data, 'startTime', startTime);
  70. this.$set(data, 'endTime', endTime);
  71. // 修改时间块的equipmentId
  72. this.$set(data, 'equipmentId', this.ganttData[suck].id);
  73. /**
  74. * 本来dom元素磁性吸附是打算用document.appendChild() 方式来做的,但是对于vue来说 for出来的子元素就算变了位置,其index也不属于新的row
  75. */
  76. // 老数据 删除
  77. this.ganttData[rowIndex][this.listKey].splice(index, 1);
  78. // 新数据加入
  79. this.ganttData[suck][this.listKey].push(data);
  80. // 坐标定位 磁性吸附 永远的10px 不知道为啥动态绑定的元素也会受到以前元素的影响,可能是 for 中 index带来的影响
  81. dom.style.top = this.defaultTop + 'px';
  82. // 松开鼠标的时候 清空
  83. box.onmousemove = null;
  84. window.onmouseup = null;
  85. // 需要重新计算吸附位置,以及整行重新排列
  86. this.$nextTick(() => {
  87. this._recalcRowTimes(data, this.ganttData[suck][this.listKey]);
  88. });
  89. // 将当前row 清空
  90. this.nowSuck = -1;
  91. // 改变位置后回调事件
  92. this.afterChangePosition(data, this.ganttData[rowIndex].id, this.ganttData[suck].id);
  93. };
  94. },
  95. /**
  96. * 鼠标移动的时候,前置条件鼠标按下
  97. * @param e 时间块的 event 事件
  98. * @param disX x轴
  99. * @param disY y轴
  100. * @param targetDom 时间块的dom 其实可以直接 e.target 获取
  101. */
  102. onMouseMove(e, disX, disY, targetDom) {
  103. // 用鼠标的位置减去鼠标相对元素的位置,得到元素的位置
  104. let left = e.clientX - disX;
  105. const top = e.clientY - disY;
  106. console.log('x轴移动距离',left);
  107. this.moveX = left;
  108. this.moveY = top;
  109. // 移动位置不能小于零(开始时间)
  110. if (left < 0) {
  111. left = 0;
  112. }
  113. //拖动时间块关闭右键菜单
  114. this.menuVisible = false;
  115. targetDom.style.left = left + 'px';
  116. targetDom.style.top = top + 'px';
  117. },
  118. /**
  119. * 时间块位置变化后回调事件
  120. * @param data 时间块的值 包括时间块id startTime endTime
  121. * @param rowOId 时间块原来所在的那个飞机id (一条横线)
  122. * @param rowNId 时间块新的所在的那个飞机id
  123. */
  124. afterChangePosition(data, rowOId, rowNId) {
  125. console.log('时间块位置变化后回调事件', rowOId, rowNId);
  126. },
  127. save() {
  128. console.log(JSON.stringify(this.ganttData));
  129. },

3. 右击设置自定义右键菜单

  1. onRightClick(MouseEvent, row, block) {
  2. //编辑需要把时间长度先计算好
  3. MouseEvent.preventDefault(); //关闭浏览器右键默认事件
  4. block.timeDiff = (block.endTime - block.startTime) / 3600000;
  5. this.editRow = row;
  6. this.editBlock = block;
  7. // this.menuVisible = false; // 先把模态框关死,目的是 第二次或者第n次右键鼠标的时候 它默认的是true
  8. this.menuVisible = true; // 显示模态窗口,跳出自定义菜单栏
  9. console.log('唤醒点击事件', this.menuVisible, this.editBlock, MouseEvent.clientX);
  10. this.CurrentRow = row;
  11. var menu = document.querySelector('.menu');
  12. if (MouseEvent.clientX > 1800) {
  13. menu.style.left = MouseEvent.clientX - 100 + 'px';
  14. } else {
  15. menu.style.left = MouseEvent.clientX + 1 + 'px';
  16. }
  17. document.addEventListener('click', this.cancelMouse); // 给整个document新增监听鼠标事件,点击任何位置执行foo方法
  18. if (MouseEvent.clientY > 700) {
  19. menu.style.top = MouseEvent.clientY - 30 + 'px';
  20. } else {
  21. menu.style.top = MouseEvent.clientY - 10 + 'px';
  22. }
  23. console.log('位置問題', MouseEvent.clientY - 30 + 'px', MouseEvent.clientY - 10 + 'px');
  24. // this.styleMenu(menu);
  25. },
  26. cancelMouse() {
  27. // document.oncontextmenu=false;
  28. // 取消鼠标监听事件 菜单栏
  29. this.menuVisible = false;
  30. document.removeEventListener('click', this.foo); // 关掉监听,
  31. },

4.计算时间选择器相差天数以渲染时间轴

  1. choiceTimeArr() {
  2. const timeArr = [];
  3. // 时间戳毫秒为单位
  4. // 尾时间-首时间 算出头尾的时间戳差 再换算成天单位 毫秒->分->时->天
  5. // const diffDays = (this.choiceTime[1].getTime() - this.choiceTime[0].getTime()) / 1000 / 60 / 60 / 24;
  6. const diffDays = Math.abs(Date.parse(this.choiceTime[1])- Date.parse(this.choiceTime[0])) / 1000 / 60 / 60 / 24
  7. console.log('我是时间差啊', diffDays);
  8. // 一天的时间戳)
  9. const oneDayMs = 24 * 60 * 60 * 1000;
  10. // 差了多少天就便利多少天 首时间+当前便利的天数的毫秒数
  11. for (let i = 0; i < diffDays + 1; i++) {
  12. // 时间戳来一个相加,得到的就是时间数组
  13. timeArr.push(new Date(Date.parse(this.choiceTime[0]) + i * oneDayMs));
  14. }
  15. // console.log(diffDays, oneDayMs, timeArr);
  16. return timeArr;
  17. },

 5.搜索功能(使用element-ui的示例)

  1. // 搜索数据数组
  2. loadAll() {
  3. return [
  4. { "value": "三全鲜食(北新泾店)", "address": "长宁区新渔路144号" },
  5. { "value": "Hot honey 首尔炸鸡(仙霞路)", "address": "上海市长宁区淞虹路661号" },
  6. { "value": "新旺角茶餐厅", "address": "上海市普陀区真北路988号创邑金沙谷6号楼113" },
  7. { "value": "泷千家(天山西路店)", "address": "天山西路438号" },
  8. { "value": "胖仙女纸杯蛋糕(上海凌空店)", "address": "上海市长宁区金钟路968号1幢18号楼一层商铺18-101" },
  9. { "value": "贡茶", "address": "上海市长宁区金钟路633号" },
  10. { "value": "豪大大香鸡排超级奶爸", "address": "上海市嘉定区曹安公路曹安路1685号" },
  11. { "value": "茶芝兰(奶茶,手抓饼)", "address": "上海市普陀区同普路1435号" },
  12. { "value": "十二泷町", "address": "上海市北翟路1444弄81号B幢-107" },
  13. { "value": "星移浓缩咖啡", "address": "上海市嘉定区新郁路817号" },
  14. { "value": "阿姨奶茶/豪大大", "address": "嘉定区曹安路1611号" },
  15. { "value": "新麦甜四季甜品炸鸡", "address": "嘉定区曹安公路2383弄55号" }
  16. ];
  17. },
  18. querySearchAsync(queryString, cb) {
  19. var restaurants = this.restaurants;
  20. var results = queryString ? restaurants.filter(this.createStateFilter(queryString)) : restaurants;
  21. clearTimeout(this.timeout);
  22. this.timeout = setTimeout(() => {
  23. cb(results);
  24. }, 3000 * Math.random());
  25. },
  26. createStateFilter(queryString) {
  27. return (state) => {
  28. return (state.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
  29. };
  30. },
  31. handleSelect(item) {
  32. console.log(item);
  33. },

 四、实现效果 

      由于需求是以弹框形式实现,没有做全屏显示,具体效果如下:

甘特图实现

 

         

标签:
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

在线投稿:投稿 站长QQ:1888636

后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索
排行榜