小肥貓公寓 發表於 2021-11-26 15:58:00

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

<p><span style="font-size: 14pt"><strong>前言</strong></span></p>
<p><span style="font-size: 14pt"><strong> &nbsp;&nbsp;</strong></span><span style="font-size: 16px">Excel功能强大,应用广泛。随着web应用的兴起和完善,用户的要求也越来越高。很多Excel的功能都搬到了sass里面。恨不得给他们做个Excel出来。。。程序员太难了。。。</span></p>
<p><span style="font-size: 16px">  去年我遇到了一个甘特图的需求,做了很多工作,也写了两篇博客。一篇是用 GSTC 这个包做的甘特图,另一篇是自己手写了一个简易的甘特图。两个的效果都不理想,特别是GSTC,问题很多,好多道友看了博客遇到了问题,惭愧,没能帮大家解决这个问题。之前太忙了,这个甘特图就再没搞,直到今天发现了新的包,几乎是完全符合我们的需求。</span></p>
<p><span style="font-size: 16px">  首先,我们用的是 highcharts;其次,大团队的产品,后期维护有保障,文档也齐全。</span></p>
<p><span style="font-size: 16px">  我用 Vue3 写的,但是highcharts不区分他,是js包,所以无论 vue&nbsp; react 还是原生多页面都没问题。</span></p>
<p><span style="font-size: 16px">  接下来先看一下我们的需求,也是最基本的,需要实现的功能,然后会有效果图的gif,最后就是源代码,我放在了Git上,觉得好用,麻烦给个star。</span></p>
<p>&nbsp;</p>
<p><span style="font-size: 14pt"><strong>需求</strong></span></p>
<p><span style="font-size: 16px">1、横轴左侧是表格数据,可以展示基本信息</span><br><span style="font-size: 16px">2、横轴右侧是时间轴,可以切换不同精度的时间展示</span><br><span style="font-size: 16px">3、横向数据块有多个,最好可以叠加</span><br><span style="font-size: 16px">4、数据块可以拖拽、点击等,修改任务的时间和其他信息</span></p>
<p>&nbsp;</p>
<p><span style="font-size: 14pt"><strong>效果图</strong></span></p>
<p>&nbsp;</p>
<p><img src="https://img2020.cnblogs.com/blog/1113254/202111/1113254-20211126145949276-1820618751.gif"></p>
<p id="1637909989913">&nbsp;</p>
<p>&nbsp;</p>
<p><span style="font-size: 16px">  这个highcharts,不仅实现了左边表格,右边图标,而且数据是联动的;右边横轴是时间轴,可以自定义格式;数据允许叠加,不冲突;数据有点击等各种事件,可以选中编辑单个数据块;数据可以拖拽,比如上下换列拖拽、水平拖拽,还可以单边拖拽,而且事件都有回调函数。这些功能基本可以满足我们的需求。比如对时间段、时间长度、数据信息的修改和展示。</span></p>
<p>&nbsp;</p>
<p><strong><span style="font-size: 14pt">源码地址、代码解析</span></strong></p>
<p><span style="font-size: 16px">  </span><span style="font-size: 16px">先贴一下代码的Git地址,点击&nbsp;GitHub源代码&nbsp; 下载源代码。建议直接下源码,跑项目,另外,这个项目是vue3的,不过对于这种包,写法差别不大,主要是参数。</span></p>
<p><span style="font-size: 16px">  我贴一下代码,对功能实现做一个讲解,当然注释写的也是很详细的。</span></p>
<p>  <span style="font-size: 16px">首先,highcharts-gantt.js 是专门用来实现甘特图的文件,draggable-points.js 是实现点事件绑定的文件。因为vue直接引入有找不到变量的报错,</span><span style="font-size: 16px">我将draggable-points的两个module直接添加到了highcharts-gantt里面,然后重新压缩,没有混淆,</span><span style="font-size: 16px">所以最终的包只有160K+,小了很多,大家可以直接用。压缩之后的源代码放心使用就行,我只是合并了2个文件的功能函数,但是也要格外提醒,不是官方的源文件了,感兴趣的同学可以去看官方源代码,&nbsp; .src 的文件是未压缩混淆的,注释也很详细。这是我合并文件时的版本&nbsp;&nbsp;</span><span style="font-size: 16px">Highcharts Gantt JS v9.3.1 (2021-11-05),这个也是当前的稳定版本。</span></p>
<p>&nbsp;</p>
<p><span style="font-size: 16px">  功能有点简单,好像代码没什么好说的。关键的地方我都做了注释,还有不明白的可以留言或者评论。</span></p>
<p><span style="font-size: 16px">  最后,还存在一个问题,我没仔细研究源码,这个示例还存在一个问题,就是拖拽事件没有中断,而且直接修改了图表的数据展示。比如,纵向拖动换行时,左侧表格的数据会变化。暂时我还没有找到满意解决的方法。目前,我在拖拽结束的回调 drop 函数中,对数据做了处理,然后将我们希望的数据回写,更新图表,同样你也可以做 不能拖拽或者时间冲突等各种校验,达到上面我所说的需求。但是还有一点瑕疵,就是拖拽过程中的数据变化,左侧表格的数据拖拽过程中我也不希望他变化,暂时没能解决掉。如果您有好的案例、好的使用、好的建议,都希望可以提出来,共同进步。</span></p>
<div class="cnblogs_code">
<pre><em id="__mceDel">&lt;div class="hightChart-gantt"&gt;
    &lt;div id="container"&gt;&lt;/div&gt;
    &lt;button @click="getData"&gt;打印当前数据&lt;/button&gt;
&lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;<span style="color: rgba(0, 0, 0, 1)">
import { defineComponent, onMounted, ref } from </span>'vue'<span style="color: rgba(0, 0, 0, 1)">;
import </span>* as Highcharts from '@jsModule/highcharts/highcharts-gantt.src.js'<span style="color: rgba(0, 0, 0, 1)">
import dayjs from </span>'dayjs'<span style="color: rgba(0, 0, 0, 1)">
import{ WEEKS } from </span>'./constants'

<span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> api文档:https://api.Highcharts.com.cn/gantt/index.html</span><span style="color: rgba(0, 128, 0, 1)">
//</span><span style="color: rgba(0, 128, 0, 1)"> 社区地址: https://forum.jianshukeji.com/tags/c/Highcharts/35/Highcharts-gantt</span><span style="color: rgba(0, 128, 0, 1)">
//</span><span style="color: rgba(0, 128, 0, 1)"> 官方示例: https://www.highcharts.com.cn/demo/gantt/interactive-gantt</span>

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

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

      {start: </span>'2021-6-1 8',end: '2021-6-1 12',factory: '小米',material: '红米3', uid: 5, y: 1<span style="color: rgba(0, 0, 0, 1)">},
      {start: </span>'2021-6-3 3',end: '2021-6-3 9',factory: '小米',material: '红米3', uid: 6, y: 1<span style="color: rgba(0, 0, 0, 1)">},

      {start: </span>'2021-6-1 6',end: '2021-6-1 16',factory: '苹果',material: 'iPhone13', uid: 7, y: 2<span style="color: rgba(0, 0, 0, 1)">},
      {start: </span>'2021-6-2 3',end: '2021-6-2 19',factory: '苹果',material: 'iPhone13', uid: 8, y: 2<span style="color: rgba(0, 0, 0, 1)">},
      {start: </span>'2021-6-3 8',end: '2021-6-3 17',factory: '苹果',material: 'iPhone13', uid: 9, y: 2<span style="color: rgba(0, 0, 0, 1)">},

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


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

    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> {
      gantt,
      getData
    }
},
})
</span>&lt;/script&gt;

&lt;style scoped&gt;<span style="color: rgba(0, 0, 0, 1)">
.hightChart</span>-<span style="color: rgba(0, 0, 0, 1)">gantt {
overflow</span>-<span style="color: rgba(0, 0, 0, 1)">x: auto;
    </span>-webkit-overflow-<span style="color: rgba(0, 0, 0, 1)">scrolling: touch;
}
#container {
    max</span>-<span style="color: rgba(0, 0, 0, 1)">width: 1200px;
    min</span>-<span style="color: rgba(0, 0, 0, 1)">width: 800px;
    height: 400px;
    margin: 1em auto;
}
</span>&lt;/style&gt;</em></pre>
</div>
<p>&nbsp;</p>
<p>  </p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/pengfei-nie/p/15608101.html
頁: [1]
查看完整版本: gantt甘特图可拖拽、编辑(vue、react都可用 highcharts)