详解gantt甘特图可拖拽、编辑(vue、react都可用 highcharts)


Posted in Vue.js onNovember 27, 2021

前言

 Excel功能强大,应用广泛。随着web应用的兴起和完善,用户的要求也越来越高。很多Excel的功能都搬到了sass里面。恨不得给他们做个Excel出来。。。程序员太难了。。。

  去年我遇到了一个甘特图的需求,做了很多工作,也写了两篇博客。一篇是用 GSTC 这个包做的甘特图,另一篇是自己手写了一个简易的甘特图。两个的效果都不理想,特别是GSTC,问题很多,好多道友看了博客遇到了问题,惭愧,没能帮大家解决这个问题。之前太忙了,这个甘特图就再没搞,直到今天发现了新的包,几乎是完全符合我们的需求。

  首先,我们用的是 highcharts;其次,大团队的产品,后期维护有保障,文档也齐全。

  我用 Vue3 写的,但是highcharts不区分他,是js包,所以无论 vue react 还是原生多页面都没问题。

  接下来先看一下我们的需求,也是最基本的,需要实现的功能,然后会有效果图的gif,最后就是源代码,我放在了Git上,觉得好用,麻烦给个star。

需求

1、横轴左侧是表格数据,可以展示基本信息
2、横轴右侧是时间轴,可以切换不同精度的时间展示
3、横向数据块有多个,最好可以叠加
4、数据块可以拖拽、点击等,修改任务的时间和其他信息

效果图

详解gantt甘特图可拖拽、编辑(vue、react都可用 highcharts)

  这个highcharts,不仅实现了左边表格,右边图标,而且数据是联动的;右边横轴是时间轴,可以自定义格式;数据允许叠加,不冲突;数据有点击等各种事件,可以选中编辑单个数据块;数据可以拖拽,比如上下换列拖拽、水平拖拽,还可以单边拖拽,而且事件都有回调函数。这些功能基本可以满足我们的需求。比如对时间段、时间长度、数据信息的修改和展示。

源码地址、代码解析

  先贴一下代码的Git地址,点击GitHub源代码 下载源代码。建议直接下源码,跑项目,另外,这个项目是vue3的,不过对于这种包,写法差别不大,主要是参数。

  我贴一下代码,对功能实现做一个讲解,当然注释写的也是很详细的。

  首先,highcharts-gantt.js 是专门用来实现甘特图的文件,draggable-points.js 是实现点事件绑定的文件。因为vue直接引入有找不到变量的报错,我将draggable-points的两个module直接添加到了highcharts-gantt里面,然后重新压缩,没有混淆,所以最终的包只有160K+,小了很多,大家可以直接用。压缩之后的源代码放心使用就行,我只是合并了2个文件的功能函数,但是也要格外提醒,不是官方的源文件了,感兴趣的同学可以去看官方源代码, .src 的文件是未压缩混淆的,注释也很详细。这是我合并文件时的版本Highcharts Gantt JS v9.3.1 (2021-11-05),这个也是当前的稳定版本。

  功能有点简单,好像代码没什么好说的。关键的地方我都做了注释,还有不明白的可以留言或者评论。

  最后,还存在一个问题,我没仔细研究源码,这个示例还存在一个问题,就是拖拽事件没有中断,而且直接修改了图表的数据展示。比如,纵向拖动换行时,左侧表格的数据会变化。暂时我还没有找到满意解决的方法。目前,我在拖拽结束的回调 drop 函数中,对数据做了处理,然后将我们希望的数据回写,更新图表,同样你也可以做 不能拖拽或者时间冲突等各种校验,达到上面我所说的需求。但是还有一点瑕疵,就是拖拽过程中的数据变化,左侧表格的数据拖拽过程中我也不希望他变化,暂时没能解决掉。如果您有好的案例、好的使用、好的建议,都希望可以提出来,共同进步。

<div class="hightChart-gantt">
    <div id="container"></div>
    <button @click="getData">打印当前数据</button>
  </div>
</template>

<script>
import { defineComponent, onMounted, ref } from 'vue';
import * as Highcharts from '@jsModule/highcharts/highcharts-gantt.src.js'
import dayjs from 'dayjs'
import{ WEEKS } from './constants'

// api文档:  https://api.Highcharts.com.cn/gantt/index.html
// 社区地址: https://forum.jianshukeji.com/tags/c/Highcharts/35/Highcharts-gantt
// 官方示例: https://www.highcharts.com.cn/demo/gantt/interactive-gantt

// 待解决问题
// 1、拖拽中断: 用户操作应该需要校验,但是现在对中断用户操作这块还没搞明白。
//    解决方案: 目前的做法是,在 drop 里面做判断,根据业务逻辑,做出提示,重新渲染数据。能实现,不够友好。


export default defineComponent({
  name: 'hightCharts-gantt',
  components: {},
  setup () {
    const gantt = ref({});
    // 官方建议用UTC的时间,鉴于业务需要,我们需要和数据库时间保持统一,得看数据库的存储格式
    const data = [
      {start: '2021-6-1 0',end: '2021-6-1 18',factory: '华为',material: 'P50', uid: 1, y: 0, completed: 0.35}, 
      {start: '2021-6-2 8',end: '2021-6-2 16',factory: '华为',material: 'P50', uid: 2, y: 0}, 
      {start: '2021-6-3 8',end: '2021-6-4 24',factory: '华为',material: 'P50', uid: 3, y: 0}, 
      {start: '2021-6-4 12',end: '2021-6-5 15',factory: '华为',material: 'P50', uid: 4, y: 0}, 

      {start: '2021-6-1 8',end: '2021-6-1 12',factory: '小米',material: '红米3', uid: 5, y: 1}, 
      {start: '2021-6-3 3',end: '2021-6-3 9',factory: '小米',material: '红米3', uid: 6, y: 1}, 

      {start: '2021-6-1 6',end: '2021-6-1 16',factory: '苹果',material: 'iPhone13', uid: 7, y: 2}, 
      {start: '2021-6-2 3',end: '2021-6-2 19',factory: '苹果',material: 'iPhone13', uid: 8, y: 2}, 
      {start: '2021-6-3 8',end: '2021-6-3 17',factory: '苹果',material: 'iPhone13', uid: 9, y: 2}, 

      {start: '2021-6-1 12',end: '2021-6-1 24',factory: 'OPPO',material: 'Reno7', uid: 10, y: 3},
      {start: '2021-6-2 5',end: '2021-6-2 18',factory: 'OPPO',material: 'Reno7', uid: 11, y: 3},
      {start: '2021-6-3 1',end: '2021-6-5 12',factory: 'OPPO',material: 'Reno7', uid: 12, y: 3},
    ];
    let newData = data.map(item => {
      item.start = dayjs(item.start).valueOf();
      item.end = dayjs(item.end).valueOf();
      return item
    });
    
    // 全局配置,需要在图标初始化之前配置
    Highcharts.setOptions({
      global: {
        useUTC: false  // 不使用utc时间
      },    // 默认都是英文的,这里做了部分中文翻译
      lang: {
        noData: '暂无数据',
        weekdays: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
        months: ['一月', '儿月', '三月', '四月', '五月', '六月', '七月', '八月', '九月', '十月', '十一月', '十二月']
      },
    });
    const dragStart = (e) => {
    }
    const drag = (e) => {
    }
    const drop = (e) => {
      const { newPoint = {}, target = {} } = e;
      if(newPoint.y || newPoint.y === 0) {
        let list = [], tar = newData.find(item => item.y === newPoint.y && item.uid !== target.uid);
        list = newData.map(item => {
          // 当前拖拽数据
          if(item.uid === target.uid) {
            return {
              ...item,
              factory: tar.factory,
              material: tar.material, 
              ...newPoint
            }
          } else {
            return item
          }
        })
        gantt.value.update({
          series: [{
            data: list
          }]
        })
      }
    }
    // 选中,可以弹窗,编辑一些业务数据
    const handleSelect = (e) => {
      console.log('选中')
    }
    // 获取最终数据
    const getData = () => {
      let data = gantt.value.series[0].data.map(item => {
        return {
          uid: item.uid,
          factory: item.factory,
          material: item.material,
          start: item.start,
          end: item.end
        }
      })
      console.log(data)
    }


    onMounted(() => {
      try {
        gantt.value = Highcharts.ganttChart('container', {
          title: {
            text: 'hightCharts甘特图示例'
          },
          xAxis: [{
            currentDateIndicator: true,
            tickPixelInterval: 70,
            grid: {
              borderWidth: 1, // 右侧表头边框宽度
              cellHeight: 35, // 右侧日期表头高度
            },
            labels: {
              align: 'center',
              formatter: function() {
                return `${dayjs(this.value).format('M月D')}  ${WEEKS[dayjs(this.value).day()]}`;
              }
            },
          }, {
            labels: {
              align: 'center',
              formatter: function() {
                return `${dayjs(this.value).format('YYYY年M月')}`;
              }
            },
          }],
          yAxis: {
            type: 'category',
            grid: {
              enabled: true,
              borderColor: 'rgba(0,0,0,0.3)',
              borderWidth: 1,
              columns: [
                { title: { text: '工厂' }, labels: { format: '{point.factory}' } }, 
                { title: { text: '型号' }, labels: { format: '{point.material}' } }, 
              ]
            }
          },
          tooltip: {
            formatter: function () {
              return `<div>
               工厂: ${this.point.factory}<br/>
              开始时间: ${dayjs(this.point.start).format('YYYY-MM-DD HH:mm:ss')}<br/>
              结束时间: ${dayjs(this.point.end).format('YYYY-MM-DD HH:mm:ss')}<br/>
              </div>`
            }
          },
          series: [{ data: newData }],
          plotOptions: {
            series: {
              animation: false,     // Do not animate dependency connectors
              dragDrop: {
                draggableX: true,   // 横向拖拽
                draggableY: true,   // 纵向拖拽
                dragMinY: 0,        // 纵向拖拽下限
                dragMaxY: 3,        // 纵向拖拽上限
                dragPrecisionX: 3600000   // 横向拖拽精度,单位毫秒
              },
              dataLabels: {
                enabled: true,
                format: '{point.factory}-{point.uid}',
                style: {
                  cursor: 'default',
                  pointerEvents: 'none'
                }
              },
              allowPointSelect: true,
              point: {
                events: {
                  dragStart: dragStart,
                  drag: drag,
                  drop: drop,
                  select: handleSelect
                }
              }
            }
          },
          exporting: {
            sourceWidth: 1000
          },
          credits: {    // 去掉右下角版权信息
            enabled: false
          },
        });
      } catch (error) {
        console.log(error)
      }
    })

    return {
      gantt,
      getData
    }
  },
})
</script>

<style scoped>
.hightChart-gantt {
  overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}
#container {
    max-width: 1200px;
    min-width: 800px;
    height: 400px;
    margin: 1em auto;
}
</style>

到此这篇关于详解gantt甘特图可拖拽、编辑(vue、react都可用 highcharts)的文章就介绍到这了,更多相关vue 甘特图gantt内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Vue.js 相关文章推荐
vue中利用three.js实现全景图的完整示例
Dec 07 Vue.js
Vue中强制组件重新渲染的正确方法
Jan 03 Vue.js
vue监听键盘事件的相关总结
Jan 29 Vue.js
Vue中使用wangeditor富文本编辑的问题
Feb 07 Vue.js
Vue+Bootstrap实现简易学生管理系统
Feb 09 Vue.js
Vue包大小优化的实现(从1.72M到94K)
Feb 18 Vue.js
vue3.0中使用element的完整步骤
Mar 04 Vue.js
一篇文章学会Vue中间件管道
Jun 20 Vue.js
详解gantt甘特图可拖拽、编辑(vue、react都可用 highcharts)
Nov 27 Vue.js
vue+iview实现手机号分段输入框
Mar 25 Vue.js
vue实现可以快进后退的跑马灯组件
Apr 08 Vue.js
vue选项卡切换的实现案例
Apr 11 Vue.js
Vue实现跑马灯样式文字横向滚动
Nov 23 #Vue.js
详解Vue的列表渲染
Nov 20 #Vue.js
详解Vue slot插槽
Nov 20 #Vue.js
详解Vue router路由
Nov 20 #Vue.js
vue中 this.$set的使用详解
如何用vue实现网页截图你知道吗
Vue3中的Refs和Ref详情
Nov 11 #Vue.js
You might like
用PHP+java实现自动新闻滚动窗口
2006/10/09 PHP
详解php设置session(过期、失效、有效期)
2015/11/12 PHP
php获取服务器操作系统相关信息的方法
2016/10/08 PHP
Laravel构建即时应用的一种实现方法详解
2017/08/31 PHP
Mootools 1.2教程(21)——类(二)
2009/09/15 Javascript
javascript 面向对象全新理练之原型继承
2009/12/03 Javascript
JavaScript在多浏览器下for循环的使用方法
2012/11/07 Javascript
解析js原生方法创建表格效率测试
2013/07/08 Javascript
JavaScript中判断整字类型最简洁的实现方法
2014/11/08 Javascript
Javascript 拖拽雏形(逐行分析代码,让你轻松了拖拽的原理)
2015/01/23 Javascript
jQuery的图片轮播插件PgwSlideshow使用详解
2016/08/11 Javascript
浅谈JS之tagNaem和nodeName
2016/09/13 Javascript
Bootstrap学习笔记 轮播(Carousel)插件
2017/03/21 Javascript
通过vue-router懒加载解决首次加载时资源过多导致的速度缓慢问题
2018/04/08 Javascript
Vue+iview+webpack ie浏览器兼容简单处理
2019/09/20 Javascript
Vue实现浏览器打印功能的代码
2020/04/17 Javascript
[35:26]DOTA2上海特级锦标赛B组小组赛#2 VG VS Fnatic第三局
2016/02/26 DOTA
zbar解码二维码和条形码示例
2014/02/07 Python
Python连接SQLServer2000的方法详解
2017/04/19 Python
Python入门_学会创建并调用函数的方法
2017/05/16 Python
Python基于递归算法实现的走迷宫问题
2017/08/04 Python
利用标准库fractions模块让Python支持分数类型的方法详解
2017/08/11 Python
Python中函数参数匹配模型详解
2019/06/09 Python
Python实现K折交叉验证法的方法步骤
2019/07/11 Python
CSS3教程(1):什么是CSS3
2009/04/02 HTML / CSS
基于CSS3实现的黑色个性导航菜单效果
2015/09/14 HTML / CSS
Mankind西班牙男士护肤品网站:购买皮肤护理、护发和剃须
2017/04/27 全球购物
华为慧通笔试题
2016/04/22 面试题
敏捷开发的主要原则都有哪些
2015/04/26 面试题
如何理解transaction事务的概念
2015/05/27 面试题
元旦晚会主持词
2014/03/24 职场文书
2014党委书记四风对照检查材料思想汇报
2014/09/21 职场文书
2014年社区矫正工作总结
2014/11/18 职场文书
导游词之太原天龙山
2020/01/02 职场文书
关于Python OS模块常用文件/目录函数详解
2021/07/01 Python
Ruby处理CSV数据方法详解
2022/04/18 Ruby