vue3 + element plus实现甘特图
vue3 + element plus实现甘特图
效果展示
实现思路
甘特图,个人理解就是表格的一种展现形式。左侧填充数据,右侧用图例填充表示时间、工期、进度等信息。
技术选型
常用的ui框架有很多,以前用vue2的时候多搭配element ui,最近在看vue3的内容,所以选择了element plus。所以本文示例使用vue3+element plus实现甘特图。看过原理之后改用其他技术方案也很简单。
代码实现
新建项目 vue3 + element plus
自己搜索吧,有很多,这里不是重点。
封装组件
这里说下为什么要封装成组件,因为项目里可能多处用到类似的功能,总不能每次都拷贝,然后修改。一次封装,多次引用。
上代码:我存放的路径 /src/components/gantt/index.vue
<template>
<div class="gantt">
<div class="legend">
<!-- 渲染图例 -->
<i class="plan"></i>
<label>计划</label>
<i class="actuality"></i>
<label>实际</label>
</div>
<el-table :data="data">
<!-- 渲染表格 -->
<el-table-column
v-for="(column, index) in columnsConfig"
:key="index"
v-bind="column"
></el-table-column>
<el-table-column
v-for="monthItem in monthData"
:key="monthItem.month"
align="center"
min-width="80"
:prop="monthItem.month"
:label="monthItem.month"
>
<template #header>
<span>{{ monthItem.month.substring(5) + '月' }}</span>
</template>
<el-table-column
v-for="day in monthItem.dayArray"
:key="day"
align="center"
:width="50"
:prop="day"
>
<template #header>
<span>{{ day.substring(8) }}</span>
</template>
<template #default="scope">
<i class="plan" v-if="showPlan(scope)"></i>
<i class="empty" v-else></i>
<i class="actuality" v-if="showActuality(scope)"></i>
<i class="empty" v-else></i>
</template>
</el-table-column>
</el-table-column>
</el-table>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
data: {
type: Array,
default: []
},
columnsConfig: {
type: Array,
default: []
},
ganttConfig: {
type: Object,
default: {
planBeginColumn: 'planBegin',
planEndColumn: 'planEnd',
actualityBeginColumn: 'actualityBegin',
actualityEndColumn: 'actualityEnd'
}
}
})
const monthData = ref({})
const init = () => {
let minDate = undefined
let maxDate = undefined
props.data.forEach((row, index) => {
let current = new Date(row[props.ganttConfig.planBeginColumn])
if (minDate) {
minDate = minDate.getTime() < current.getTime() ? minDate : current
} else {
minDate = current
}
current = new Date(row[props.ganttConfig.planEndColumn])
if (maxDate) {
maxDate = maxDate.getTime() > current.getTime() ? maxDate : current
} else {
maxDate = current
}
current = props.ganttConfig.actualityBeginColumn || row[props.ganttConfig.actualityBeginColumn] ? new Date(row[props.ganttConfig.actualityBeginColumn]) : undefined
if (current) {
minDate = minDate.getTime() < current.getTime() ? minDate : current
}
current = props.ganttConfig.actualityEndColumn || row[props.ganttConfig.actualityEndColumn] ? new Date(row[props.ganttConfig.actualityEndColumn]) : undefined
if (current) {
maxDate = maxDate.getTime() > current.getTime() ? maxDate : current
}
})
// 甘特图前后各放宽2天
minDate = new Date(minDate.getTime() - 2 * 24 * 60 * 60 * 1000)
maxDate = new Date(maxDate.getTime() + 2 * 24 * 60 * 60 * 1000)
let current = new Date(format(minDate))
while(!isAfter(current, maxDate)) {
const month = formatYearMonth(current)
const day = format(current)
if (monthData.value[month]) {
monthData.value[month].dayArray.push(day)
} else {
monthData.value[month] = {
month: month,
dayArray: [day]
}
}
// 加一天
current = after(current)
}
}
/**
* 格式化 YYYY-MM-DD
*/
const format = (date) => {
const day = String(date.getDate()).padStart(2, '0')
return formatYearMonth(date) + '-' + day
}
/**
* 格式化 YYYY-MM
*/
const formatYearMonth = (date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
return year + '-' + month
}
/**
* 加一天
*/
const after = (date) => {
return new Date(date.getTime() + 24 * 60 * 60 * 1000)
}
/**
* date1是否大于等于date2
*/
const isAfter = (date1, date2) => {
return date1.getTime() >= date2.getTime()
}
/**
* 显示计划进度
*/
const showPlan = ({row, column}) => {
const currentDay = new Date(column.property)
const begin = new Date(row[props.ganttConfig.planBeginColumn])
const end = new Date(row[props.ganttConfig.planEndColumn])
return currentDay.getTime() >= begin.getTime() && currentDay.getTime() <= end.getTime()
}
/**
* 显示实际进度
*/
const showActuality = ({row, column}) => {
const currentDay = new Date(column.property)
const begin = props.ganttConfig.actualityBeginColumn || row[props.ganttConfig.actualityBeginColumn] ? new Date(row[props.ganttConfig.actualityBeginColumn]) : undefined
const end = props.ganttConfig.actualityEndColumn || row[props.ganttConfig.actualityEndColumn] ? new Date(row[props.ganttConfig.actualityEndColumn]) : undefined
return begin && end && currentDay.getTime() >= begin.getTime() && currentDay.getTime() <= end.getTime()
}
init()
</script>
<style scoped>
.plan {
display: flex;
width: calc(100% + 24px);
height: 16px;
background-color: limegreen;
margin: 0 -12px;
}
.actuality {
display: flex;
width: calc(100% + 24px);
height: 16px;
background-color: yellow;
margin: 0 -12px;
}
.empty {
display: flex;
width: calc(100% + 24px);
height: 16px;
margin: 0 -12px;
}
.legend {
display: flex;
line-height: 40px;
flex-direction: row;
justify-content: right;
align-items: center;
padding: 0 20px;
* {
margin: 0 5px;
}
i {
width: 32px;
height: 16px;
}
}
</style>
引用组件
app.vue中引用上面的组件
<script setup>
import Gantt from '@/components/gantt/index.vue'
const data = ref([
{
title: '第一阶段',
planBegin: '2022-01-01',
planEnd: '2022-01-09',
actualityBegin: '2022-01-02',
actualityEnd: '2022-01-10'
},
{
title: '第二阶段',
planBegin: '2022-01-09',
planEnd: '2022-01-15',
actualityBegin: '2022-01-09',
actualityEnd: '2022-01-18'
}
])
const columnsConfig = ref([
{
label: '事项',
prop: 'title',
fixed: 'left',
align: 'center',
'min-width': 120
},
{
label: '开始',
prop: 'planBegin',
fixed: 'left',
align: 'center',
'min-width': 120
},
{
label: '结束',
prop: 'planEnd',
fixed: 'left',
align: 'center',
'min-width': 120
}
])
</script>
<template>
<div>
<Gantt
:data="data"
:columnsConfig="columnsConfig"
:ganttConfig ="{
planBeginColumn: 'planBegin',
planEndColumn: 'planEnd',
actualityBeginColumn: 'actualityBegin',
actualityEndColumn: 'actualityEnd'
}"
/>
</div>
</template>
<style scoped>
</style>