vue作甘特圖,先大體介紹下核心功能: (1)橫軸、縱軸拖拽; (2)自定義監聽點擊事件(雙擊、右鍵等)(3)任務之間顯示父子層級關係;(4)左側列表信息,右側時間軸表示任務;(5)每一個任務能夠訂製樣式,而且能夠動態修改樣式;(6)自定義時間粒度顯示(小時、天、星期、月、年);(7)支持大批量數據渲染;(8) 支持同行多節點渲染;(9)支持選中,以及批量選中;(9)優秀的擴展性,支持第三方插件。等等還有其餘的一些功能。這裏先看一下效果圖:css
接下來會介紹用什麼實現的,怎麼使用,怎麼添加拖拽、點擊等各類功能,我以vue爲例進行開發。html
一、使用GSTC作甘特圖開發vue
Git項目地址:https://github.com/neuronetio/gantt-schedule-timeline-calendar#weekendhighlight-pluginreact
官方vue實例:https://github.com/neuronetio/vue-gantt-schedule-timeline-calendargit
npm指令: npm i gantt-schedule-timeline-calendargithub
官方作了 3 大主流框架的封裝,具體看Git連接,這裏我也附上了vue版本的 npm 包地址。npm
基本使用以下: ps:文章末尾我會貼一個完整的代碼,若是是vue項目能夠直接複製查看效果。下邊這個是個極度閹割的。app
<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 好多問題都關閉了,基本大部分問題均可以查到。框架
一、基礎展現,左側多列表格展現less
這個主要配置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: "描述" } } } } }
二、右側任務排列顯示(包括訂製樣式)
這個主要配置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 } } } }
三、配置右側橫軸的時間顯示
這個主要配置config中的 locale 屬性,時間的語言環境配置,這裏看文檔詳細些,下面只詳說2個屬性,
weekdays 配置 每週顯示的文案 主要是作國際化用的
months 配置月的,也是作國際化的
locale: { name: "zh", Now: "Now", weekdays:["週日","週一","週二","週三","週四","週五","週六"], months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"], }
四、監聽鼠標右擊事件
這個主要配置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: {} } } },
五、任務的橫軸、縱軸拖動
這個主要配置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 } }) ]
六、選中任務
這個主要配置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>