很抱歉这篇博客不够好,关于甘特图,我有了更简单、功能更强大、效果更完美的解决方案,不要再看这个。

  用hightcharts实现,对vue react 原生js等等都支持。

  新的甘特图以及源代码下载请点击这里查看        hightcharts版甘特图

 

 

  vue做甘特图,先大致介绍下核心功能: (1)横轴、纵轴拖拽; (2)自定义监听点击事件(双击、右键等)(3)任务之间显示父子层级关系;(4)左侧列表信息,右侧时间轴表示任务;(5)每个任务可以订制样式,并且可以动态修改样式;(6)自定义时间粒度显示(小时、天、星期、月、年);(7)支持大批量数据渲染;(8) 支持同行多节点渲染;(9)支持选中,以及批量选中;(9)优秀的扩展性,支持第三方插件。等等还有其他的一些功能。这里先看一下效果图:

 

 

 

 

  接下来会介绍用什么实现的,怎么使用,怎么添加拖拽、点击等各种功能,我以vue为例进行开发。

1、使用GSTC做甘特图开发

  Git项目地址:https://github.com/neuronetio/gantt-schedule-timeline-calendar#weekendhighlight-plugin

  官方vue实例:https://github.com/neuronetio/vue-gantt-schedule-timeline-calendar

  npm指令: npm i gantt-schedule-timeline-calendar

  官方做了 3 大主流框架的封装,具体看Git链接,这里我也附上了vue版本的 npm 包地址。

  基本使用如下:  ps:文章末尾我会贴一个完整的代码,如果是vue项目可以直接复制查看效果。下边这个是个极度阉割的。

  1. <template>
  2. <GSTC :config="config" />
  3. </template>
  4. <script>
  5. import GSTC from "vue-gantt-schedule-timeline-calendar";
  6. export default {
  7. data(){
  8. return {
  9. config: {
  10. height: 500,
  11. list: {
  12. rows: {
  13. "1": { id: "1", order: \'订单1\', },
  14. },
  15. columns: {
  16. data: {
  17. id: { id: "id", data: "id", width: 50, header: { content: "序号" } },
  18. }
  19. }
  20. },
  21. chart: {
  22. items: {
  23. "1": { id: "1", rowId: "1", time: { start: new Date().getTime() + 1 * 24 * 60 * 60 * 1000, end: new Date().getTime() + 2 * 24 * 60 * 60 * 1000 } }
  24. }
  25. },
  26. },
  27. subs: []
  28. }
  29. },
  30. beforeDestroy() { this.subs.forEach(unsub => unsub()); }
  31. }
  32. </script>
  33. <style lang="less" scoped>
  34. .wrapper .gantt-elastic__grid-line-time {
  35. display: none;
  36. }
  37. </style>

 

基础使用已经贴代码了,不做赘述,不清楚的查看官方示例,接下来主要说核心功能如何配置,这方面官方描述的不是很清楚,但是Git的 issues 好多问题都关闭了,基本大部分问题都可以查到。

1、基础展示,左侧多列表格展示

        

 

 

 这个主要配置config中的 list 属性,

  rows 代表左侧表格的行属性,key值是每行的id,多个key就有多行,通常都以数字做key值, 内部 具体属性是列信息。比如 order label line 等都是列信息,这个会一一对应到指定列。

    parentID 是父节点配置,一般配置了父节点,就会在 甘特图 中展示出父子层级来。

    expanded 是展开属性,默认false,父子层级是合上的,折叠隐藏子节点。如果想默认展示需要每个节点都加上这个属性。

  columns 代表左侧表格的列属性,key唯一就是列关键字。

    data 属性,是列,可以有多个属性,每个代表一列

      id 当前列的id

      data 列标识,和rows中每行的数据的字段唯一对应,比如  order、line 等

      isHTML 是否要展示HTML,默认false。  这个直接关系到content、html字段用哪个

      width 当前列宽度

      expander 是否显示层级,默认false不展示,设置为true,会展示出父子层级来,一般我们仅设置一列,当然设置多列也行。

      header 配置表头内容的

        content 表头想显示的内容

        html 写HTML,用来订制表头样式的,内容就是HTML,行内css

    percent 是左侧表格总宽度占甘特图的百分比,0就直接隐藏表格

    minWidth:是左侧表格的最小宽度

  1. list: {
  2. rows: {
  3. "1": {
  4. id: "1",
  5. order: \'订单1\',
  6. label: "压缩机",
  7. line: \'线体1\',
  8. expanded: true
  9. },
  10. "3": {
  11. id: "3",
  12. order: \'订单3\',
  13. label: "箱体",
  14. line: \'线体3\',
  15. parentId: \'2\',
  16. }
  17. },
  18. columns: {
  19. data: {
  20. id: {
  21. id: "id",
  22. data: "id",
  23. width: 50,
  24. expander: true,
  25. header: { content: "序号" }
  26. },
  27. order: {
  28. id: "order",
  29. data: "order",
  30. header: { content: "生产订单" }
  31. },
  32. label: {
  33. id: "label",
  34. data: "label",
  35. header: { content: "描述" }
  36. }
  37. }
  38. }
  39. }

 

2、右侧任务排列显示(包括订制样式)

      

 

这个主要配置config中的 chart 属性,

   time 配置时间轴

    from 左侧开始时间,填写毫秒数

    to 右侧结束时间,填写毫秒数

    zoom 显示层级,10-22,越大,时间粒度展示的越大,越小,显示越精细,最小到5分钟

  items 任务快配置,注意这个可以同行若干任务展示

    id 当前任务的id

    rowId 左侧表格 rows 的id,通过这个关联,渲染到某一行

    label 当前任务的名称,会默认展示在任务中

    time 任务的开始、结束时间

      start 开始时间,填写毫秒数

      end 结束时间, 填写毫秒数

    style 订制样式,是个对象,写过jsx写法,写过react 、vue jsx 的应该都不默认,这里举个简单的例子,订制任务div的背景色 圆角等样式  { background: \’red\’, borderRadius: \’3px\’ }

  1. chart: {
  2. time: {
  3. from: new Date().getTime() - 2 * 24 * 60 * 60 * 1000,
  4. to: new Date().getTime() + 8 * 24 * 60 * 60 * 1000,
  5. zoom: 22,
  6. },
  7. items: {
  8. "1": {
  9. id: "1",
  10. rowId: "1",
  11. label: "Item 1",
  12. time: {
  13. start: new Date().getTime() + 1 * 24 * 60 * 60 * 1000,
  14. end: new Date().getTime() + 2 * 24 * 60 * 60 * 1000
  15. },
  16. style: { // 每个块的样式
  17. background: \'blue\'
  18. }
  19. },
  20. "21": {
  21. id: "21",
  22. rowId: "2",
  23. label: "Item 2-1",
  24. time: {
  25. start: new Date().getTime() + 2 * 24 * 60 * 60 * 1000,
  26. end: new Date().getTime() + 3 * 24 * 60 * 60 * 1000
  27. }
  28. }
  29. }
  30. }

 

3、配置右侧横轴的时间显示

     

 

 这个主要配置config中的 locale 属性,时间的语言环境配置,这里看文档详细些,下面只详说2个属性,

  weekdays 配置 每周显示的文案   主要是做国际化用的

  months 配置月的,也是做国际化的

  1. locale: {
  2. name: "zh",
  3. Now: "Now",
  4. weekdays:["周日","周一","周二","周三","周四","周五","周六"],
  5. months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],
  6. }

 

4、监听鼠标右击事件

      

 

 这个主要配置config中的 actions 属性,他是对象,以下是他所有能监听dom,很多,这篇博客就只介绍人物块的事件监听,其他的不做一一赘述了

  • main
  • list
  • list-column
  • list-column-header
  • list-column-header-resizer
  • list-column-header-resizer-dots
  • list-column-row
  • list-column-row-expander
  • list-column-row-expander-toggle
  • list-toggle
  • chart
  • chart-calendar
  • chart-calendar-date
  • chart-timeline
  • chart-timeline-grid
  • chart-timeline-grid-row
  • chart-timeline-grid-row-block
  • chart-timeline-items
  • chart-timeline-items-row
  • chart-timeline-items-row-item

  这个监听函数会接收2个参数,element  和 data ,一个是dom,另一个是 任务节点的数据。根据官方要求,监听函数必须返回一个对象,此对象必须包含 update  destroy 2个方法,分别是位置更新和销毁时需要执行的方法。具体写法请见如下代码:

  1. actions: {
  2. \'chart-timeline-items-row-item\': [this.addListenClick] // 监听右击事件
  3. }
  4. methods:{
  5. addListenClick(element, data) {
  6. const onClick = (e) => {
  7. e.preventDefault()
  8. // console.log(data)
  9. this.modal = {
  10. visible: true,
  11. title: data.item.label,
  12. data
  13. }
  14. return false
  15. }
  16. element.addEventListener(\'contextmenu\', onClick);
  17. return {
  18. update(element, newData) {
  19. data = newData;
  20. },
  21. destroy(element, data) {
  22. element.removeEventListener(\'click\', onClick);
  23. }
  24. };
  25. },
  26. closeModal() {
  27. this.modal = {
  28. visible: false,
  29. title: \'\',
  30. data: {}
  31. }
  32. }
  33. },

 

5、任务的横轴、纵轴拖动

       

 

 

这个主要配置config中的 plugins 属性,

  ItemMovement 插件,这个是官方开发的用来拖拽任务的插件。这个包的插件系统做的很好,官方提供了几种不错的插件,同时还支持其他的第三方插件,有兴趣的可以自己试试,这里先介绍拖拽插件,

     moveable 拖拽的方向, x 支持横轴拖拽;   y 支持纵轴拖拽;  true 横轴、纵轴都可以拖拽; false 禁止拖拽

    resizeable 是否可以拖拽,true开启拖拽

    resizerContent 拖拽的图标,直接写HTML,可以自己定制拖拽图标的样式

    collisionDetection: 拖拽过程中是否允许元素重叠, true 不允许重叠

    ghostNode  false 不展示重影节点

    snapStart 拖拽开始时间点回调,这个比较机制特殊,拖拽位置的时候触发这个方法,参数接收开始时间  时间变化 当前节点数据,默认是毫秒级的刷新,会卡,我们做if判断1小时拖拽

    snapEnd 拖拽结束时间点回调,这个是拖动任务块大小时触发,接收结束时间 时间段。用法同上。具体请看如下代码:

  1. plugins: [
  2. // 拖动 x 横向, y 纵向
  3. ItemMovement({
  4. moveable: \'x\',
  5. resizerContent: \'<div class="resizer">-></div>\',
  6. ghostNode: false,
  7. snapStart(time, diff, item) {
  8. if(Math.abs(diff) > 14400000) {
  9. return time + diff
  10. }
  11. return time
  12. },
  13. snapEnd(time, diff, item) {
  14. if(Math.abs(diff) > 14400000) {
  15. return time + diff
  16. }
  17. return time
  18. }
  19. })
  20. ]

 

6、选中任务

      

 

 

这个主要配置config中的 plugins 属性,

  Selection插件,单个选中、批量选中插件。

    grid 能否选中单元格

    items  能否选中任务

    rows  能否选中行

    rectStyle 矩形样式

    selected 选中的回调

    deselected  取消选中的回调

    canSelected  可选中的的回调,用来过滤哪些可以选中

    canDeselected 可取消选中的回调,用来过滤哪些可以取消选中

  1. plugins: [
  2. Selection({
  3. items: false,
  4. rows: false,
  5. grid: true,
  6. rectStyle: { opacity: \'0.0\' },
  7. canSelect(type, currentlySelecting) {
  8. if (type === \'chart-timeline-grid-row-block\') {
  9. return currentlySelecting.filter(selected => {
  10. if (!selected.row.canSelect) return false;
  11. for (const item of selected.row._internal.items) {
  12. if (
  13. (item.time.start >= selected.time.leftGlobal && item.time.start <= selected.time.rightGlobal) ||
  14. (item.time.end >= selected.time.leftGlobal && item.time.end <= selected.time.rightGlobal) ||
  15. (item.time.start <= selected.time.leftGlobal && item.time.end >= selected.time.rightGlobal)
  16. ) {
  17. return false;
  18. }
  19. }
  20. return true;
  21. });
  22. }
  23. return currentlySelecting;
  24. },
  25. canDeselect(type, currently, all) {
  26. if (type === \'chart-timeline-grid-row-blocks\') {
  27. return all.selecting[\'chart-timeline-grid-row-blocks\'].length ? [] : currently;
  28. }
  29. return [];
  30. }
  31. })
  32. ]

 

小结:

  以上就是整个甘特图的使用了,这是我用过最符合项目需求的甘特图,他的开发团队也在持续的维护这个项目,很赞。

  最后贴一段完整的 vue 示例代码:

  1. <template>
  2. <div class="wrapper">
  3. <GSTC :config="config" />
  4. <infor-modal
  5. :visible="modal.visible"
  6. :title="modal.title"
  7. :dataSource="modal.data"
  8. @handleModal="closeModal"
  9. />
  10. </div>
  11. </template>
  12.  
  13. <script>
  14. import GSTC from "vue-gantt-schedule-timeline-calendar";
  15. import ItemMovement from "gantt-schedule-timeline-calendar/dist/ItemMovement.plugin.js"
  16. import Selection from "gantt-schedule-timeline-calendar/dist/Selection.plugin.js"
  17. import inforModal from "./inforModal"
  18. export default {
  19. components:{
  20. GSTC,
  21. inforModal
  22. },
  23. props:{},
  24. data(){
  25. return {
  26. config: {
  27. height: 500,
  28. list: {
  29. // 行属性
  30. rows: {
  31. "1": {
  32. id: "1",
  33. order: \'订单1\',
  34. label: "压缩机",
  35. line: \'线体1\',
  36. expanded: true
  37. },
  38. "3": {
  39. id: "3",
  40. order: \'订单3\',
  41. label: "箱体",
  42. line: \'线体3\',
  43. parentId: \'2\',
  44. },
  45. "4": {
  46. id: "4",
  47. order: \'订单4\',
  48. label: "空调总装",
  49. line: \'线体4\',
  50. },
  51. "2": {
  52. id: "2",
  53. order: \'订单2\',
  54. label: "门体",
  55. parentId: \'1\',
  56. line: \'线体2\',
  57. expanded: true
  58. },
  59. "5": {
  60. id: "5",
  61. order: \'订单5\',
  62. label: "冰箱总装",
  63. line: \'线体5\',
  64. },
  65. "6": {
  66. id: "6",
  67. order: \'订单6\',
  68. label: "洗衣机总装",
  69. line: \'线体6\',
  70. },
  71. },
  72. // 列定义
  73. columns: {
  74. data: {
  75. id: {
  76. id: "id",
  77. data: "id",
  78. width: 50,
  79. header: {
  80. content: "序号"
  81. }
  82. },
  83. order: {
  84. id: "order",
  85. data: "order",
  86. width: 120,
  87. header: {
  88. content: "生产订单"
  89. }
  90. },
  91. label: {
  92. id: "label",
  93. data: "label",
  94. width: 120,
  95. expander: true,
  96. header: {
  97. content: "描述"
  98. }
  99. },
  100. line: {
  101. id: "line",
  102. data: "line",
  103. width: 120,
  104. header: {
  105. content: "线体"
  106. }
  107. },
  108. }
  109. }
  110. },
  111. chart: {
  112. time: { // 时间轴开始截至,
  113. from: new Date().getTime() - 2 * 24 * 60 * 60 * 1000,
  114. to: new Date().getTime() + 8 * 24 * 60 * 60 * 1000,
  115. zoom: 22, // 10-22 缩放,默认 Shift + 滚轮, 默认缩放展示时间粒度, 一共有 小时、天、周、月、年
  116. },
  117. items: {
  118. "1": {
  119. id: "1",
  120. rowId: "1",
  121. label: "Item 1",
  122. time: {
  123. start: new Date().getTime() + 1 * 24 * 60 * 60 * 1000,
  124. end: new Date().getTime() + 2 * 24 * 60 * 60 * 1000
  125. },
  126. style: { // 每个块的样式
  127. background: \'blue\'
  128. }
  129. },
  130. "21": {
  131. id: "21",
  132. rowId: "2",
  133. label: "Item 2-1",
  134. time: {
  135. start: new Date().getTime() + 2 * 24 * 60 * 60 * 1000,
  136. end: new Date().getTime() + 3 * 24 * 60 * 60 * 1000
  137. }
  138. },
  139. "22": {
  140. id: "22",
  141. rowId: "2",
  142. label: "Item 2-2",
  143. time: {
  144. start: new Date().getTime() + 3 * 24 * 60 * 60 * 1000,
  145. end: new Date().getTime() + 4 * 24 * 60 * 60 * 1000
  146. }
  147. },
  148. "3": {
  149. id: "3",
  150. rowId: "3",
  151. label: "Item 3",
  152. time: {
  153. start: new Date().getTime() + 3 * 24 * 60 * 60 * 1000,
  154. end: new Date().getTime() + 5 * 24 * 60 * 60 * 1000
  155. }
  156. },
  157. "4": {
  158. id: "4",
  159. rowId: "4",
  160. label: "Item 4",
  161. time: {
  162. start: new Date().getTime() + 2 * 24 * 60 * 60 * 1000,
  163. end: new Date().getTime() + 5 * 24 * 60 * 60 * 1000
  164. }
  165. },
  166. "5": {
  167. id: "5",
  168. rowId: "5",
  169. label: "Item 5",
  170. time: {
  171. start: new Date().getTime() + 3 * 24 * 60 * 60 * 1000,
  172. end: new Date().getTime() + 5 * 24 * 60 * 60 * 1000
  173. }
  174. },
  175. "6": {
  176. id: "6",
  177. rowId: "6",
  178. label: "Item 6",
  179. time: {
  180. start: new Date().getTime() + 5 * 24 * 60 * 60 * 1000,
  181. end: new Date().getTime() + 6 * 24 * 60 * 60 * 1000
  182. }
  183. },
  184. }
  185. },
  186. locale: {
  187. name: "zh",
  188. Now: "Now",
  189. weekdays:["周日","周一","周二","周三","周四","周五","周六"],
  190. months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],
  191. },
  192. actions: {
  193. \'chart-timeline-items-row-item\': [this.addListenClick] // 监听右击事件
  194. },
  195. plugins: [
  196. // 拖动 x 横向, y 纵向
  197. ItemMovement({
  198. moveable: \'x\',
  199. resizerContent: \'<div class="resizer">-></div>\',
  200. ghostNode: false,
  201. collisionDetection: false,
  202. snapStart(time, diff, item) {
  203. if(Math.abs(diff) > 14400000) {
  204. return time + diff
  205. }
  206. return time
  207. },
  208. snapEnd(time, diff, item) {
  209. if(Math.abs(diff) > 14400000) {
  210. return time + diff
  211. }
  212. return time
  213. }
  214. }),
  215. Selection({
  216. items: false,
  217. rows: false,
  218. grid: true,
  219. rectStyle: { opacity: \'0.0\' },
  220. canSelect(type, currentlySelecting) {
  221. if (type === \'chart-timeline-grid-row-block\') {
  222. return currentlySelecting.filter(selected => {
  223. if (!selected.row.canSelect) return false;
  224. for (const item of selected.row._internal.items) {
  225. if (
  226. (item.time.start >= selected.time.leftGlobal && item.time.start <= selected.time.rightGlobal) ||
  227. (item.time.end >= selected.time.leftGlobal && item.time.end <= selected.time.rightGlobal) ||
  228. (item.time.start <= selected.time.leftGlobal && item.time.end >= selected.time.rightGlobal)
  229. ) {
  230. return false;
  231. }
  232. }
  233. return true;
  234. });
  235. }
  236. return currentlySelecting;
  237. },
  238. canDeselect(type, currently, all) {
  239. if (type === \'chart-timeline-grid-row-blocks\') {
  240. return all.selecting[\'chart-timeline-grid-row-blocks\'].length ? [] : currently;
  241. }
  242. return [];
  243. }
  244. })
  245. ]
  246. },
  247. modal: {
  248. visible: false,
  249. title: \'\',
  250. data: {}
  251. },
  252. subs: []
  253. }
  254. },
  255. watch:{},
  256. computed:{},
  257. methods:{
  258. addListenClick(element, data) {
  259. const onClick = (e) => {
  260. e.preventDefault()
  261. // console.log(data)
  262. this.modal = {
  263. visible: true,
  264. title: data.item.label,
  265. data
  266. }
  267. return false
  268. }
  269. element.addEventListener(\'contextmenu\', onClick);
  270. return {
  271. update(element, newData) {
  272. data = newData;
  273. },
  274. destroy(element, data) {
  275. element.removeEventListener(\'click\', onClick);
  276. }
  277. };
  278. },
  279. closeModal() {
  280. this.modal = {
  281. visible: false,
  282. title: \'\',
  283. data: {}
  284. }
  285. }
  286. },
  287. created(){},
  288. mounted(){
  289. },
  290. beforeDestroy() {
  291. this.subs.forEach(unsub => unsub());
  292. }
  293. }
  294. </script>
  295. <style lang="less" scoped>
  296. .wrapper .gantt-elastic__grid-line-time {
  297. display: none;
  298. }
  299. </style>

 

版权声明:本文为pengfei-nie原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/pengfei-nie/p/13092159.html