vue甘特圖gantt


  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項目可以直接復制查看效果。下邊這個是個極度閹割的。

<template>
    <GSTC :config="config" />
</template>
<script>
import GSTC from "vue-gantt-schedule-timeline-calendar";
export default {
  data(){
    return {
      config: {
        height: 500,
        list: {
          rows: {
            "1": { id: "1", order: '訂單1', },
          },
          columns: {
            data: {
              id: { id: "id", data: "id", width: 50, header: { content: "序號" } },
            }
          }
        },
        chart: {
          items: {
            "1": { id: "1", rowId: "1", time: { start: new Date().getTime() + 1 * 24 * 60 * 60 * 1000, end: new Date().getTime() + 2 * 24 * 60 * 60 * 1000 } }
          }
        },
      },
      subs: []
    }
  },
  beforeDestroy() { this.subs.forEach(unsub => unsub()); }
}
</script>
<style lang="less" scoped>
  .wrapper .gantt-elastic__grid-line-time {
    display: none;
  }
</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:是左側表格的最小寬度

      list: {
          rows: {
            "1": {
              id: "1",
              order: '訂單1',
              label: "壓縮機",
              line: '線體1',
              expanded: true
            },
            "3": {
              id: "3",
              order: '訂單3',
              label: "箱體",
              line: '線體3',
              parentId: '2',
            }
          },
          columns: {
            data: {
              id: {
                id: "id",
                data: "id",
                width: 50,
                expander: true,
                header: { content: "序號" }
              },
              order: {
                id: "order",
                data: "order",
                header: { content: "生產訂單" }
              },
              label: {
                id: "label",
                data: "label",
                header: { content: "描述" }
              }
            }
          }
        }

 

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' }

     chart: {
          time: {
            from: new Date().getTime() - 2 * 24 * 60 * 60 * 1000,
            to: new Date().getTime() + 8 * 24 * 60 * 60 * 1000,
            zoom: 22,    
          },
          items: {
            "1": {
              id: "1",
              rowId: "1",
              label: "Item 1",
              time: {
                start: new Date().getTime() + 1 * 24 * 60 * 60 * 1000,
                end: new Date().getTime() + 2 * 24 * 60 * 60 * 1000
              },
              style: {  // 每個塊的樣式
                background: 'blue'
              }
            },
            "21": {
              id: "21",
              rowId: "2",
              label: "Item 2-1",
              time: {
                start: new Date().getTime() + 2 * 24 * 60 * 60 * 1000,
                end: new Date().getTime() + 3 * 24 * 60 * 60 * 1000
              }
            }
          }
        }

 

3、配置右側橫軸的時間顯示

     

 

 這個主要配置config中的 locale 屬性,時間的語言環境配置,這里看文檔詳細些,下面只詳說2個屬性,

  weekdays 配置 每周顯示的文案   主要是做國際化用的

  months 配置月的,也是做國際化的

        locale: {
          name: "zh",
          Now: "Now",
          weekdays:["周日","周一","周二","周三","周四","周五","周六"],
          months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],
        }

 

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個方法,分別是位置更新和銷毀時需要執行的方法。具體寫法請見如下代碼:

actions: {
      'chart-timeline-items-row-item': [this.addListenClick] // 監聽右擊事件
}

methods:{
    addListenClick(element, data) {
      const onClick = (e) => {
        e.preventDefault()
        // console.log(data)
        this.modal = {
          visible: true,
          title: data.item.label,
          data
        }
        return false
      }
      element.addEventListener('contextmenu', onClick);
      return {
        update(element, newData) {
          data = newData;
        },
        destroy(element, data) {
          element.removeEventListener('click', onClick);
        }
      };
    },
    closeModal() {
      this.modal = {
        visible: false,
        title: '',
        data: {}
      }
    }
  },

 

5、任務的橫軸、縱軸拖動

       

 

 

這個主要配置config中的 plugins 屬性,

  ItemMovement 插件,這個是官方開發的用來拖拽任務的插件。這個包的插件系統做的很好,官方提供了幾種不錯的插件,同時還支持其他的第三方插件,有興趣的可以自己試試,這里先介紹拖拽插件,

     moveable 拖拽的方向, x 支持橫軸拖拽;   y 支持縱軸拖拽;  true 橫軸、縱軸都可以拖拽; false 禁止拖拽

    resizeable 是否可以拖拽,true開啟拖拽

    resizerContent 拖拽的圖標,直接寫HTML,可以自己定制拖拽圖標的樣式

    collisionDetection: 拖拽過程中是否允許元素重疊, true 不允許重疊

    ghostNode  false 不展示重影節點

    snapStart 拖拽開始時間點回調,這個比較機制特殊,拖拽位置的時候觸發這個方法,參數接收開始時間  時間變化 當前節點數據,默認是毫秒級的刷新,會卡,我們做if判斷1小時拖拽

    snapEnd 拖拽結束時間點回調,這個是拖動任務塊大小時觸發,接收結束時間 時間段。用法同上。具體請看如下代碼:

      plugins: [
          // 拖動 x 橫向, y 縱向
          ItemMovement({
            moveable: 'x',
            resizerContent: '<div class="resizer">-></div>',
            ghostNode: false,
            snapStart(time, diff, item) {
              if(Math.abs(diff) > 14400000) {
                return time + diff
              }
              return time
            },
            snapEnd(time, diff, item) {
              if(Math.abs(diff) > 14400000) {
                return time + diff
              }
              return time
            }
          })
        ]

 

6、選中任務

      

 

 

這個主要配置config中的 plugins 屬性,

  Selection插件,單個選中、批量選中插件。

    grid 能否選中單元格

    items  能否選中任務

    rows  能否選中行

    rectStyle 矩形樣式

    selected 選中的回調

    deselected  取消選中的回調

    canSelected  可選中的的回調,用來過濾哪些可以選中

    canDeselected 可取消選中的回調,用來過濾哪些可以取消選中

      plugins: [
          Selection({
            items: false,
            rows: false,
            grid: true,
            rectStyle: { opacity: '0.0' }, 
            canSelect(type, currentlySelecting) {
              if (type === 'chart-timeline-grid-row-block') {
                return currentlySelecting.filter(selected => {
                  if (!selected.row.canSelect) return false;
                  for (const item of selected.row._internal.items) {
                    if (
                      (item.time.start >= selected.time.leftGlobal && item.time.start <= selected.time.rightGlobal) ||
                      (item.time.end >= selected.time.leftGlobal && item.time.end <= selected.time.rightGlobal) ||
                      (item.time.start <= selected.time.leftGlobal && item.time.end >= selected.time.rightGlobal)
                    ) {
                      return false;
                    }
                  }
                  return true;
                });
              }
              return currentlySelecting;
            },
            canDeselect(type, currently, all) {
              if (type === 'chart-timeline-grid-row-blocks') {
                return all.selecting['chart-timeline-grid-row-blocks'].length ? [] : currently;
              }
              return [];
            }
          })
        ]

 

小結:

  以上就是整個甘特圖的使用了,這是我用過最符合項目需求的甘特圖,他的開發團隊也在持續的維護這個項目,很贊。

  最后貼一段完整的 vue 示例代碼:

<template>
  <div class="wrapper">
    <GSTC :config="config" />
    <infor-modal
      :visible="modal.visible"
      :title="modal.title"
      :dataSource="modal.data"
      @handleModal="closeModal"
    />
  </div>
</template>

<script>
import GSTC from "vue-gantt-schedule-timeline-calendar";
import ItemMovement from "gantt-schedule-timeline-calendar/dist/ItemMovement.plugin.js"
import Selection from "gantt-schedule-timeline-calendar/dist/Selection.plugin.js"
import inforModal from "./inforModal"

export default {
  components:{
    GSTC,
    inforModal
  },
  props:{},
  data(){
    return {
      config: {
        height: 500,
        list: {
          // 行屬性
          rows: {
            "1": {
              id: "1",
              order: '訂單1',
              label: "壓縮機",
              line: '線體1',
              expanded: true
            },
            "3": {
              id: "3",
              order: '訂單3',
              label: "箱體",
              line: '線體3',
              parentId: '2',
            },
            "4": {
              id: "4",
              order: '訂單4',
              label: "空調總裝",
              line: '線體4',
            },
            "2": {
              id: "2",
              order: '訂單2',
              label: "門體",
              parentId: '1',
              line: '線體2',
              expanded: true
            },
            "5": {
              id: "5",
              order: '訂單5',
              label: "冰箱總裝",
              line: '線體5',
            },
            "6": {
              id: "6",
              order: '訂單6',
              label: "洗衣機總裝",
              line: '線體6',
            },
          },
          // 列定義
          columns: {
            data: {
              id: {
                id: "id",
                data: "id",
                width: 50,
                header: {
                  content: "序號"
                }
              },
              order: {
                id: "order",
                data: "order",
                width: 120,
                header: {
                  content: "生產訂單"
                }
              },
              label: {
                id: "label",
                data: "label",
                width: 120,
                expander: true,
                header: {
                  content: "描述"
                }
              },
              line: {
                id: "line",
                data: "line",
                width: 120,
                header: {
                  content: "線體"
                }
              },
            }
          }
        },
        chart: {
          time: {  // 時間軸開始截至,
            from: new Date().getTime() - 2 * 24 * 60 * 60 * 1000,
            to: new Date().getTime() + 8 * 24 * 60 * 60 * 1000,
            zoom: 22,    // 10-22 縮放,默認 Shift + 滾輪, 默認縮放展示時間粒度, 一共有 小時、天、周、月、年
          },
          items: {
            "1": {
              id: "1",
              rowId: "1",
              label: "Item 1",
              time: {
                start: new Date().getTime() + 1 * 24 * 60 * 60 * 1000,
                end: new Date().getTime() + 2 * 24 * 60 * 60 * 1000
              },
              style: {  // 每個塊的樣式
                background: 'blue'
              }
            },
            "21": {
              id: "21",
              rowId: "2",
              label: "Item 2-1",
              time: {
                start: new Date().getTime() + 2 * 24 * 60 * 60 * 1000,
                end: new Date().getTime() + 3 * 24 * 60 * 60 * 1000
              }
            },
            "22": {
              id: "22",
              rowId: "2",
              label: "Item 2-2",
              time: {
                start: new Date().getTime() + 3 * 24 * 60 * 60 * 1000,
                end: new Date().getTime() + 4 * 24 * 60 * 60 * 1000
              }
            },
            "3": {
              id: "3",
              rowId: "3",
              label: "Item 3",
              time: {
                start: new Date().getTime() + 3 * 24 * 60 * 60 * 1000,
                end: new Date().getTime() + 5 * 24 * 60 * 60 * 1000
              }
            },
            "4": {
              id: "4",
              rowId: "4",
              label: "Item 4",
              time: {
                start: new Date().getTime() + 2 * 24 * 60 * 60 * 1000,
                end: new Date().getTime() + 5 * 24 * 60 * 60 * 1000
              }
            },
            "5": {
              id: "5",
              rowId: "5",
              label: "Item 5",
              time: {
                start: new Date().getTime() + 3 * 24 * 60 * 60 * 1000,
                end: new Date().getTime() + 5 * 24 * 60 * 60 * 1000
              }
            },
            "6": {
              id: "6",
              rowId: "6",
              label: "Item 6",
              time: {
                start: new Date().getTime() + 5 * 24 * 60 * 60 * 1000,
                end: new Date().getTime() + 6 * 24 * 60 * 60 * 1000
              }
            },
          }
        },
        locale: {
          name: "zh",
          Now: "Now",
          weekdays:["周日","周一","周二","周三","周四","周五","周六"],
          months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],
        },
        actions: {
          'chart-timeline-items-row-item': [this.addListenClick] // 監聽右擊事件
        },
        plugins: [
          // 拖動 x 橫向, y 縱向
          ItemMovement({
            moveable: 'x',
            resizerContent: '<div class="resizer">-></div>',
            ghostNode: false,
            collisionDetection: false,
            snapStart(time, diff, item) {
              if(Math.abs(diff) > 14400000) {
                return time + diff
              }
              return time
            },
            snapEnd(time, diff, item) {
              if(Math.abs(diff) > 14400000) {
                return time + diff
              }
              return time
            }
          }),
          Selection({
            items: false,
            rows: false,
            grid: true,
            rectStyle: { opacity: '0.0' }, 
            canSelect(type, currentlySelecting) {
              if (type === 'chart-timeline-grid-row-block') {
                return currentlySelecting.filter(selected => {
                  if (!selected.row.canSelect) return false;
                  for (const item of selected.row._internal.items) {
                    if (
                      (item.time.start >= selected.time.leftGlobal && item.time.start <= selected.time.rightGlobal) ||
                      (item.time.end >= selected.time.leftGlobal && item.time.end <= selected.time.rightGlobal) ||
                      (item.time.start <= selected.time.leftGlobal && item.time.end >= selected.time.rightGlobal)
                    ) {
                      return false;
                    }
                  }
                  return true;
                });
              }
              return currentlySelecting;
            },
            canDeselect(type, currently, all) {
              if (type === 'chart-timeline-grid-row-blocks') {
                return all.selecting['chart-timeline-grid-row-blocks'].length ? [] : currently;
              }
              return [];
            }
          })
        ]
      },
      modal: {
        visible: false,
        title: '',
        data: {}
      },
      subs: []
    }
  },
  watch:{},
  computed:{},
  methods:{
    addListenClick(element, data) {
      const onClick = (e) => {
        e.preventDefault()
        // console.log(data)
        this.modal = {
          visible: true,
          title: data.item.label,
          data
        }
        return false
      }
      element.addEventListener('contextmenu', onClick);
      return {
        update(element, newData) {
          data = newData;
        },
        destroy(element, data) {
          element.removeEventListener('click', onClick);
        }
      };
    },
    closeModal() {
      this.modal = {
        visible: false,
        title: '',
        data: {}
      }
    }
  },
  created(){},
  mounted(){
  },
  beforeDestroy() {
    this.subs.forEach(unsub => unsub());
  }
}
</script>
<style lang="less" scoped>
  .wrapper .gantt-elastic__grid-line-time {
    display: none;
  }
</style>

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM