vue前端項目中開發基於甘特圖的項目計划模塊
參考鏈接
相比以前jquery的資料,vue的甘特圖插件少很多,中文資料更是少的可憐,以下兩個鏈接是在網上搜到相對還不錯的甘特圖插件
https://www.cnblogs.com/liang715200/p/12029640.html
https://github.com/neuronetio/vue-gantt-elastic
開始使用
- 安裝以下插件
npm intall gantt-elastic
npm install gantt-elastic-header
npm install dayjs
- 復制以下代碼到vue文件
<template>
<q-page class="q-pa-sm">
<gantt-elastic
:options="options"
:tasks="tasks"
@tasks-changed="tasksUpdate"
@options-changed="optionsUpdate"
@dynamic-style-changed="styleUpdate"
>
<gantt-header slot="header"></gantt-header>
</gantt-elastic>
<div class="q-mt-md" />
<q-btn @click="addTask" icon="mdi-plus" label="Add task" />
</q-page>
</template>
<style>
</style>
<script>
import GanttElastic from "gantt-elastic";
import GanttHeader from "gantt-elastic-header";
import dayjs from "dayjs";
// just helper to get current dates
function getDate(hours) {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth();
const currentDay = currentDate.getDate();
const timeStamp = new Date(
currentYear,
currentMonth,
currentDay,
0,
0,
0
).getTime();
return new Date(timeStamp + hours * 60 * 60 * 1000).getTime();
}
let tasks = [
{
id: 1,
label: "Make some noise",
user:
'<a href="https://www.google.com/search?q=John+Doe" target="_blank" style="color:#0077c0;">John Doe</a>',
start: getDate(-24 * 5),
duration: 15 * 24 * 60 * 60 * 1000,
percent: 85,
type: "project"
//collapsed: true,
},
{
id: 2,
label: "With great power comes great responsibility",
user:
'<a href="https://www.google.com/search?q=Peter+Parker" target="_blank" style="color:#0077c0;">Peter Parker</a>',
parentId: 1,
start: getDate(-24 * 4),
duration: 4 * 24 * 60 * 60 * 1000,
percent: 50,
type: "milestone",
collapsed: true,
style: {
base: {
fill: "#1EBC61",
stroke: "#0EAC51"
}
}
},
{
id: 3,
label: "Courage is being scared to death, but saddling up anyway.",
user:
'<a href="https://www.google.com/search?q=John+Wayne" target="_blank" style="color:#0077c0;">John Wayne</a>',
parentId: 2,
start: getDate(-24 * 3),
duration: 2 * 24 * 60 * 60 * 1000,
percent: 100,
type: "task"
},
{
id: 4,
label: "Put that toy AWAY!",
user:
'<a href="https://www.google.com/search?q=Clark+Kent" target="_blank" style="color:#0077c0;">Clark Kent</a>',
start: getDate(-24 * 2),
duration: 2 * 24 * 60 * 60 * 1000,
percent: 50,
type: "task",
dependentOn: [3]
},
{
id: 5,
label:
"One billion, gajillion, fafillion... shabadylu...mil...shabady......uh, Yen.",
user:
'<a href="https://www.google.com/search?q=Austin+Powers" target="_blank" style="color:#0077c0;">Austin Powers</a>',
parentId: 4,
start: getDate(0),
duration: 2 * 24 * 60 * 60 * 1000,
percent: 10,
type: "milestone",
style: {
base: {
fill: "#0287D0",
stroke: "#0077C0"
}
}
},
{
id: 6,
label: "Butch Mario and the Luigi Kid",
user:
'<a href="https://www.google.com/search?q=Mario+Bros" target="_blank" style="color:#0077c0;">Mario Bros</a>',
parentId: 5,
start: getDate(24),
duration: 1 * 24 * 60 * 60 * 1000,
percent: 50,
type: "task",
collapsed: true,
style: {
base: {
fill: "#8E44AD",
stroke: "#7E349D"
}
}
},
{
id: 7,
label: "Devon, the old man wanted me, it was his dying request",
user:
'<a href="https://www.google.com/search?q=Knight+Rider" target="_blank" style="color:#0077c0;">Knight Rider</a>',
parentId: 2,
dependentOn: [6],
start: getDate(24 * 2),
duration: 4 * 60 * 60 * 1000,
percent: 20,
type: "task",
collapsed: true
},
{
id: 8,
label: "Hey, Baby! Anybody ever tell you I have beautiful eyes?",
user:
'<a href="https://www.google.com/search?q=Johhny+Bravo" target="_blank" style="color:#0077c0;">Johhny Bravo</a>',
parentId: 7,
dependentOn: [7],
start: getDate(24 * 3),
duration: 1 * 24 * 60 * 60 * 1000,
percent: 0,
type: "task"
},
{
id: 9,
label:
"This better be important, woman. You are interrupting my very delicate calculations.",
user:
'<a href="https://www.google.com/search?q=Dexter\'s+Laboratory" target="_blank" style="color:#0077c0;">Dexter\'s Laboratory</a>',
parentId: 8,
dependentOn: [8, 7],
start: getDate(24 * 4),
duration: 4 * 60 * 60 * 1000,
percent: 20,
type: "task",
style: {
base: {
fill: "#8E44AD",
stroke: "#7E349D"
}
}
},
{
id: 10,
label: "current task",
user:
'<a href="https://www.google.com/search?q=Johnattan+Owens" target="_blank" style="color:#0077c0;">Johnattan Owens</a>',
start: getDate(24 * 5),
duration: 24 * 60 * 60 * 1000,
percent: 0,
type: "task"
},
{
id: 11,
label: "test task",
user:
'<a href="https://www.google.com/search?q=Johnattan+Owens" target="_blank" style="color:#0077c0;">Johnattan Owens</a>',
start: getDate(24 * 6),
duration: 24 * 60 * 60 * 1000,
percent: 0,
type: "task"
},
{
id: 12,
label: "test task",
user:
'<a href="https://www.google.com/search?q=Johnattan+Owens" target="_blank" style="color:#0077c0;">Johnattan Owens</a>',
start: getDate(24 * 7),
duration: 24 * 60 * 60 * 1000,
percent: 0,
type: "task",
parentId: 11
},
{
id: 13,
label: "test task",
user:
'<a href="https://www.google.com/search?q=Johnattan+Owens" target="_blank" style="color:#0077c0;">Johnattan Owens</a>',
start: getDate(24 * 8),
duration: 24 * 60 * 60 * 1000,
percent: 0,
type: "task"
},
{
id: 14,
label: "test task",
user:
'<a href="https://www.google.com/search?q=Johnattan+Owens" target="_blank" style="color:#0077c0;">Johnattan Owens</a>',
start: getDate(24 * 9),
duration: 24 * 60 * 60 * 1000,
percent: 0,
type: "task"
},
{
id: 15,
label: "test task",
user:
'<a href="https://www.google.com/search?q=Johnattan+Owens" target="_blank" style="color:#0077c0;">Johnattan Owens</a>',
start: getDate(24 * 16),
duration: 24 * 60 * 60 * 1000,
percent: 0,
type: "task"
}
];
let options = {
taskMapping: {
progress: "percent"
},
maxRows: 100,
maxHeight: 500,
title: {
label: "Your project title as html (link or whatever...)",
html: false
},
row: {
height: 24
},
calendar: {
hour: {
display: true
}
},
chart: {
progress: {
bar: false
},
expander: {
display: true
}
},
taskList: {
expander: {
straight: false
},
columns: [
{
id: 1,
label: "ID",
value: "id",
width: 40
},
{
id: 2,
label: "Description",
value: "label",
width: 200,
expander: true,
html: true,
events: {
click({ data, column }) {
alert("description clicked!\n" + data.label);
}
}
},
{
id: 3,
label: "Assigned to",
value: "user",
width: 130,
html: true結束
},
{
id: 3,
label: "Start",
value: task => dayjs(task.start).format("YYYY-MM-DD"),
width: 78
},
{
id: 4,
label: "Type",
value: "type",
width: 68
},
{
id: 5,
label: "%",
value: "progress",
width: 35,
style: {
"task-list-header-label": {
"text-align": "center",
width: "100%"
},
"task-list-item-value-container": {
"text-align": "center",
width: "100%"
}
}
}
]
},
locale: {
name: "en",
Now: "Now",
"X-Scale": "Zoom-X",
"Y-Scale": "Zoom-Y",
"Task list width": "Task list",
"Before/After": "Expand",
"Display task list": "Task list"
}
};
export default {
name: "Gantt",
components: {
GanttElastic,
GanttHeader
},
data() {
return {
tasks,
options,
dynamicStyle: {},
lastId: 16
};
},
methods: {
addTask() {
this.tasks.push({
id: this.lastId++,
label:
'<a href="https://images.pexels.com/photos/423364/pexels-photo-423364.jpeg?auto=compress&cs=tinysrgb&h=650&w=940" target="_blank" style="color:#0077c0;">Yeaahh! you have added a task bro!</a>',
user:
'<a href="https://images.pexels.com/photos/423364/pexels-photo-423364.jpeg?auto=compress&cs=tinysrgb&h=650&w=940" target="_blank" style="color:#0077c0;">Awesome!</a>',
start: getDate(24 * 3),
duration: 1 * 24 * 60 * 60 * 1000,
percent: 50,
type: "project"
});
},
tasksUpdate(tasks) {
this.tasks = tasks;
},
optionsUpdate(options) {
this.options = options;
},
styleUpdate(style) {
this.dynamicStyle = style;
}
}
};
</script>
- 至此,一個簡單的甘特圖就能顯示出來了,下面放個截圖
下面重點談下在集成插件到項目中踩過的坑
- 翻譯控件中的英文,在option下面添加如下配置,翻譯控件中的星期和月份
locale: {
weekdays:["周日","周一","周二","周三","周四","周五","周六"],
months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],
}
- 點擊事件,獲取Id,該控件的點擊事件如下,在columns中的events有點擊事件的對象,由於在events無法獲取vue頁面的this對象,因此我定義了that變量,在mount中把that=this
columns: [
{
id: 1,
label: "任務",
value: "label",
width: 200,
expander: true,
html: true,
events: {
click({ data, column }) {
that.handleEdit(data.id)
}
}
},
{
id: 2,
label: "責任人",
value: "user",
width: 78,
html: true
},
mounted() {
that=this
},
- 該插件有一個bug,當task數據位空時,會造成瀏覽器進入假死的狀態,我的處理方式是當后端返回空數據的時候,判斷如果是空的話就用默認數據填充,並且使用v-if判斷控件是否顯示
<gantt-elastic
v-if="showGantt"
:options="options"
:tasks="tasks"
@tasks-changed="tasksUpdate"
@options-changed="optionsUpdate"
@dynamic-style-changed="styleUpdate"
>
- 最后貼上完整的前端代碼
<template>
<a-card :bordered="false">
<div class="table-operator">
<a-button type="primary" icon="plus" @click="hanldleAdd()">新建</a-button>
<a-button type="primary" icon="redo" @click="showProjcetDrawer()">選擇項目</a-button>
{{ProjectName}}
</div>
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="10">
<a-col :md="4" :sm="24">
<a-form-item label="查詢類別">
<a-select allowClear v-model="queryParam.condition">
<a-select-option key="TaskDescripe">任務描述</a-select-option>
<a-select-option key="ResponseName">責任人</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="4" :sm="24">
<a-form-item>
<a-input v-model="queryParam.keyword" placeholder="關鍵字" />
</a-form-item>
</a-col>
<a-col :md="6" :sm="24">
<a-button type="primary" @click="()=>(getDataList())">查詢</a-button>
<a-button style="margin-left: 8px" @click="() => (queryParam = {})">重置</a-button>
</a-col>
<a-col :md="4" :sm="12">
<downloadexcel
class = "btn"
:data="data"
:fields = "export_fields"
:before-generate = "startDownload"
:before-finish = "finishDownload"
type = "xlsx">
<a-button type="primary" icon="file-excel" >導出</a-button>
</downloadexcel>
</a-col>
</a-row>
</a-form>
</div>
<!-- <div class="table-operator">
<a-button type="primary" icon="plus" @click="hanldleAdd()">新建</a-button>
<a-button
type="primary"
icon="minus"
@click="handleDelete(selectedRowKeys)"
:disabled="!hasSelected()"
:loading="loading"
>刪除</a-button>
<a-button type="primary" icon="redo" @click="getDataList()">刷新</a-button>
</div>
<div class="table-page-search-wrapper">
<a-form layout="inline">
<a-row :gutter="10">
<a-col :md="4" :sm="24">
<a-form-item label="查詢類別">
<a-select allowClear v-model="queryParam.condition">
<a-select-option key="TaskDescripe">任務描述</a-select-option>
<a-select-option key="ResponseBy">責任人</a-select-option>
<a-select-option key="RealFinishDate">實際完成時間</a-select-option>
<a-select-option key="ProjectId">項目Id</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :md="4" :sm="24">
<a-form-item>
<a-input v-model="queryParam.keyword" placeholder="關鍵字" />
</a-form-item>
</a-col>
<a-col :md="6" :sm="24">
<a-button type="primary" @click="()=>(pagination.current=1,getDataList())">查詢</a-button>
<a-button style="margin-left: 8px" @click="() => (queryParam = {})">重置</a-button>
</a-col>
</a-row>
</a-form>
</div> -->
<gantt-elastic
v-if="showGantt"
:options="options"
:tasks="tasks"
@tasks-changed="tasksUpdate"
@options-changed="optionsUpdate"
@dynamic-style-changed="styleUpdate"
>
<!-- <gantt-header :ProjectName="ProjectName" slot="header"></gantt-header> -->
</gantt-elastic>
<edit-form ref="editForm" :parentObj="this"></edit-form>
<a-drawer
title="選擇項目"
placement="right"
:closable="false"
@close="onClose"
:visible="ProjectDrawerVisible"
>
<p @click="SelectProject(item.Id,item.ProjectName)" v-for="item in ProjectData">{{item.ProjectName}}</p>
</a-drawer>
</a-card>
</template>
<style>
</style>
<script>
import GanttElastic from "gantt-elastic";
import GanttHeader from "gantt-elastic-header";
import dayjs from "dayjs";
import EditForm from './EditForm'
import moment from 'moment';
import JsonExcel from 'vue-json-excel'
// just helper to get current dates
function getDate(hours) {
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth();
const currentDay = currentDate.getDate();
const timeStamp = new Date(
currentYear,
currentMonth,
currentDay,
0,
0,
0
).getTime();
return new Date(timeStamp + hours * 60 * 60 * 1000).getTime();
}
let that
let tasks = [
{
id: 1,
label: "請先選擇一個項目",
user:
'123',
start: getDate(-24 * 5),
end:getDate(-24 * 5),
duration: 15 * 24 * 60 * 60 * 1000,
percent: 85,
type: "project"
}
];
let basicTask={
id: 1,
label: "請先選擇一個項目",
user:
'456',
start: getDate(-24 * 5),
end:getDate(-24 * 5),
duration: 15 * 24 * 60 * 60 * 1000,
percent: 85,
type: "project"
}
let options = {
taskMapping: {
progress: "percent"
},
maxRows: 100,
maxHeight: 500,
title: {
label: "avc",
html: false
},
row: {
height: 24
},
calendar: {
hour: {
display: false
}
},
chart: {
progress: {
bar: false
},
expander: {
display: true
}
},
taskList: {
expander: {
straight: false
},
columns: [
{
id: 1,
label: "任務",
value: "label",
width: 200,
expander: true,
html: true,
events: {
click({ data, column }) {
that.handleEdit(data.id)
}
}
},
{
id: 2,
label: "責任人",
value: "user",
width: 78,
html: true
},
{
id: 3,
label: "開始",
value: task => dayjs(task.start).format("YYYY-MM-DD"),
width: 78
},
{
id: 4,
label: "結束",
value: task => dayjs(task.end).format("YYYY-MM-DD"),
width: 78
},
{
id: 5,
label: "狀態",
value: "Status",
width: 68
},
// {
// id: 5,
// label: "%",
// value: "progress",
// width: 35,
// style: {
// "task-list-header-label": {
// "text-align": "center",
// width: "100%"
// },
// "task-list-item-value-container": {
// "text-align": "center",
// width: "100%"
// }
// }
// }
]
},
locale: {
weekdays:["周日","周一","周二","周三","周四","周五","周六"],
months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],
}
};
let doingStyle={
base: {
fill: "#BFEFFF",
stroke: "#BFEFFF"
}
}
let warnStyle={
base: {
fill: "#FF6A6A",
stroke: "#FF6A6A"
}
}
let finishStyle={
base: {
fill: "#C1FFC1",
stroke: "#C1FFC1"
}
}
let cancelStyle={
base: {
fill: "#BABABA",
stroke: "#BABABA"
}
}
export default {
name: "Gantt",
components: {
GanttElastic,
GanttHeader,
EditForm,
'downloadexcel': JsonExcel
},
mounted() {
that=this
},
data() {
return {
basicTask,
doingStyle,
finishStyle,
warnStyle,
cancelStyle,
tasks,
options,
dynamicStyle: {},
lastId: 16,
ProjectDrawerVisible:false,
ProjectId:"",
ProjectName:"",
ProjectData:[],
TaskData:[],
sorter: { field: 'StartDate', order: 'asc' },
data:[],
queryParam:{},
that,
export_fields:
{
"項目編號": "ProjectNo",
"項目名稱": "ProjectName",
"前置任務": "PreTaskName",
"父任務": "ParentTaskName",
"任務": "TaskDescripe",
"狀態": "TaskStatus",
"責任人": "ResponseName",
"計划開始時間": "StartDate",
"計划結束時間": "EndDate",
"實際完成時間": "RealFinishDate",
"資源": "Resource",
"配置人員": "ConfigMember"},
showGantt:false
};
},
methods: {
moment,
getDataList() {
this.selectedRowKeys = []
this.$http
.post('/IPMS_Manage/aspice_projectplan/GetDataList', {
SortField: this.sorter.field || 'Id',
SortType: this.sorter.order,
ProjectId:this.ProjectId,
...this.queryParam,
})
.then(resJson => {
this.data = resJson.Data
// console.log(this.data)
this.TaskData.length=0
for(var i=0;i<resJson.Data.length;i++)
{
var preList=[]
var temp={
id:resJson.Data[i].Id,
label:resJson.Data[i].TaskDescripe,
user:resJson.Data[i].ResponseName,
start:moment(resJson.Data[i].StartDate),
end:moment(resJson.Data[i].EndDate),
duration: Number(moment(resJson.Data[i].EndDate).diff(moment(resJson.Data[i].StartDate), 'days'))* 24 * 60 * 60 * 1000,
percent:100,
type:'project',
Status:resJson.Data[i].TaskStatus,
parentId:resJson.Data[i].ParentId
}
if(resJson.Data[i].PreTaskId!="")
{
preList.push(resJson.Data[i].PreTaskId)
temp.dependentOn=preList
}
if(temp.Status=="已完成")
{
temp.style=this.finishStyle
}
else
{
if(temp.Status=="未開始")
{
if(moment()>temp.start)
{
temp.style=this.warnStyle
}
else
{
temp.style=this.doingStyle
}
}
else if(temp.Status=="進行中")
{
if(moment()>temp.end)
{
temp.style=this.warnStyle
}
else
{
temp.style=this.doingStyle
}
}
else if(temp.Status=="已取消")
{
temp.style=this.cancelStyle
}
}
this.TaskData.push(temp)
}
if(this.TaskData.length>0)
{
this.showGantt=true
this.tasksUpdate(this.TaskData)
}
else
{
this.showGantt=false
this.TaskData.push(this.basicTask)
this.tasksUpdate(this.TaskData)
this.$message.warning("該項目還沒有計划,可以直接新建或者從模板導入!")
}
})
},
addTask() {
this.tasks.push({
id: this.lastId++,
label:
'<a href="https://images.pexels.com/photos/423364/pexels-photo-423364.jpeg?auto=compress&cs=tinysrgb&h=650&w=940" target="_blank" style="color:#0077c0;">Yeaahh! you have added a task bro!</a>',
user:
'<a href="https://images.pexels.com/photos/423364/pexels-photo-423364.jpeg?auto=compress&cs=tinysrgb&h=650&w=940" target="_blank" style="color:#0077c0;">Awesome!</a>',
start: getDate(24 * 3),
duration: 1 * 24 * 60 * 60 * 1000,
percent: 50,
type: "project"
});
},
tasksUpdate(tasks) {
this.tasks = tasks;
},
optionsUpdate(options) {
this.options = options;
},
styleUpdate(style) {
this.dynamicStyle = style;
},
hanldleAdd() {
if(this.ProjectId=="")
{
this.$message.warning("請先選擇一個項目!")
return false
}
this.$refs.editForm.openForm()
},
handleEdit(id) {
if(this.data.length==0)
{
this.$message.warning("該項目還沒有任務")
return false
}
this.$refs.editForm.openForm(id)
},
showProjcetDrawer()
{
this.$http.post('/IPMS_Manage/aspice_project/GetDataList').then(resJson => {
if (resJson.Success) {
this.ProjectData = resJson.Data
this.ProjectDrawerVisible=true
}
})
},
onClose()
{
this.ProjectDrawerVisible=false
},
SelectProject(id,ProjectName)
{
this.ProjectId=id
this.ProjectName=ProjectName
// this.queryParam.condition="ProjectId"
// this.queryParam.keyword=this.ProjectId
this.ProjectDrawerVisible=false
this.getDataList()
},
startDownload(){
this.loading = true
},
finishDownload(){
this.loading = false
},
}
};
</script>