详解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 相关文章推荐
Vue3+elementui plus创建项目的方法
Dec 01 Vue.js
element-plus一个vue3.xUI框架(element-ui的3.x 版初体验)
Dec 02 Vue.js
在vue中动态修改css其中一个属性值操作
Dec 07 Vue.js
解决vue使用vant轮播组件swipe + flex时文字抖动问题
Jan 07 Vue.js
Vue 数据响应式相关总结
Jan 28 Vue.js
Vue中避免滥用this去读取data中数据
Mar 02 Vue.js
开发一个封装iframe的vue组件
Mar 29 Vue.js
vue基于Teleport实现Modal组件
May 31 Vue.js
Vue中使用import进行路由懒加载的原理分析
Apr 01 Vue.js
vue判断按钮是否可以点击
Apr 09 Vue.js
Vue OpenLayer 为地图绘制风场效果
Apr 24 Vue.js
vue里使用create, mounted调用方法
Apr 26 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 获得汉字拼音首字母的函数
2009/08/01 PHP
PHP中抽象类、接口的区别与选择分析
2016/03/29 PHP
PHP正则删除HTML代码中宽高样式的方法
2017/06/12 PHP
PHP实现微信公众号验证Token的示例代码
2019/12/16 PHP
JS是否可以跨文件同时控制多个iframe页面的应用技巧
2007/12/16 Javascript
Ext javascript建立超链接,进行事件处理的实现方法
2009/03/22 Javascript
extJs 文本框后面加上说明文字+下拉列表选中值后触发事件
2009/11/27 Javascript
整理Javascript基础入门学习笔记
2015/11/29 Javascript
jQuery 全选 全部选 反选 实现代码
2016/08/17 Javascript
在js中实现邮箱格式的验证方法(推荐)
2016/10/24 Javascript
js原生实现FastClick事件的实例
2016/11/20 Javascript
基于js实现的限制文本框只可以输入数字
2016/12/05 Javascript
Sequelize中用group by进行分组聚合查询
2016/12/12 Javascript
对比分析Django的Q查询及AngularJS的Datatables分页插件
2017/02/07 Javascript
Angular2实现自定义双向绑定属性
2017/03/22 Javascript
easyui-datagrid开发实践(总结)
2017/08/02 Javascript
DVA框架统一处理所有页面的loading状态
2017/08/25 Javascript
vue中使用echarts制作圆环图的实例代码
2018/07/27 Javascript
vue返回上一页面时回到原先滚动的位置的方法
2018/12/20 Javascript
webpack + vue 打包生成公共配置文件(域名) 方便动态修改
2019/08/29 Javascript
JS防抖和节流实例解析
2019/09/24 Javascript
js实现QQ邮箱邮件拖拽删除功能
2020/08/27 Javascript
vue-openlayers实现地图坐标弹框效果
2020/09/24 Javascript
vue-quill-editor插入图片路径太长问题解决方法
2021/01/08 Vue.js
Python的Flask框架中的Jinja2模板引擎学习教程
2016/06/30 Python
python实现猜数字游戏
2020/03/25 Python
python中如何进行连乘计算
2020/05/28 Python
什么是Python包的循环导入
2020/09/08 Python
Django-Scrapy生成后端json接口的方法示例
2020/10/06 Python
什么是数据库锁?Oracle中都有哪些类型的锁?
2015/08/21 面试题
两道JAVA笔试题
2016/09/14 面试题
应届大专毕业生个人自荐信
2013/09/22 职场文书
出国导师推荐信
2014/01/16 职场文书
《雕塑之美》教学反思
2014/04/24 职场文书
诚信考试承诺书范文
2015/04/29 职场文书
Golang 切片(Slice)实现增删改查
2022/04/22 Golang