在项目管理场景中,甘特图可能我们都不陌生,它是可视化任务进度和时间管理的利器。本文将演示如何使用Vue3结合Element Plus组件库,实现一个轻量级交互式甘特图组件,包含时间轴渲染、任务条、依赖关系展示等核心功能。
目录
一、安装组件
二、引入组件
三、使用组件实现甘特图
1.搭建框架
2.左侧固定列任务
3.右侧任务进度条
四、获取数据
五、滚动条实现分页
六、处理头部列表样式
七、全部代码
对于项目的框架具体怎么安装,这里不做说明,下面是安装element plus组件方式。
npm install element-plus
在框架中的main.js文件中引入刚安装的组件
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
在项目中可能甘特图这个使用的地方会很多,所以在这里作为一个组件来进行开发,首先是搭建一个具体的框架样式,然后给他填充相应的数据
甘特图如果大致分的话,其实分为两大部分,分别为左侧的固定列展示每个任务的具体数据信息,右侧为 每个任务的时间进度条。接下来我们先实现左侧的固定列部分,该代码是在以上代码的基础上进行展示
总体实施计划
计划
实际
由于每个人任务的甘特图分为计划时间和实际时间,即在一个任务中需要显示两个任务进度条,所以在这里我们需要做一些处理。
检修甘特图
从上述的头部列表中我们可以看到使用了很多的方法,用于获取年份、月份和天数等,先来实现获取年份和月份的
// 当前年份
const currentYear = new Date().getFullYear()
接下来是获取天数的
// 获取指定月份的天数
const getDaysInMonth = (year, month) => {
return new Date(year, month, 0).getDate()
}
然后就是要判断当前日期是否在计划时间的范围内,在写这个方法之前我们需要先来做个甘特图的配置
// 甘特图配置
const ganttConfig = {
planBeginColumn: 'planBegin',
planEndColumn: 'planEnd',
actualityBeginColumn: 'actualityBegin',
actualityEndColumn: 'actualityEnd'
}
// 判断当前日期是否在计划范围内
const isPlanDay = (row, currentDateStr) => {
if (!currentDateStr || !row) return false
const currentDay = new Date(currentDateStr)
const begin = new Date(row[ganttConfig.planBeginColumn])
const end = new Date(row[ganttConfig.planEndColumn])
return currentDay >= begin && currentDay <= end
}
实际的也一样,需要做判断当前日期是否在实际范围内这样一个处理
// 判断当前日期是否在实际范围内
const isActualDay = (row, currentDateStr) => {
if (!currentDateStr || !row || !row[ganttConfig.actualityBeginColumn] || !row[ganttConfig.actualityEndColumn])
return false
const currentDay = new Date(currentDateStr)
const begin = new Date(row[ganttConfig.actualityBeginColumn])
const end = new Date(row[ganttConfig.actualityEndColumn])
return currentDay >= begin && currentDay <= end
}
由于对于实际时间的进度条颜色有区分的需求,所以还需要写个样式的方法,该方法可根据自身项目需要进行使用
const getActualityClass = row => {
// 处理可能的null/undefined值
const actualBegin = row.actualityBegin || ''
const actualEnd = row.actualityEnd || ''
// 增加trim处理防止空格干扰
const hasActualBegin = actualBegin.trim() !== ''
const hasActualEnd = actualEnd.trim() !== ''
if (!hasActualBegin && !hasActualEnd) {
return 'actuality-green'
} else if (hasActualBegin && !hasActualEnd) {
return 'actuality-yellow'
} else {
return 'actuality-grey'
}
}
具体的样式代码
.gantt {
// margin-top: 1vw;
.plan {
display: flex;
width: calc(100% + 24px);
height: 16px;
background-color: #6dcaa6;
margin: 0 -12px;
/* border-radius: 15px; */
}
.actuality-green {
display: flex;
width: calc(100% + 24px);
height: 16px;
margin: 0 -12px;
background-color: #6dcaa6;
}
.actuality-yellow {
display: flex;
width: calc(100% + 24px);
height: 16px;
margin: 0 -12px;
background-color: #f59a23;
}
.actuality-grey {
display: flex;
width: calc(100% + 24px);
height: 16px;
margin: 0 -12px;
background-color: #aaaaaa;
}
.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;
i {
width: 32px;
height: 16px;
}
}
:deep .el-table thead {
color: #595858;
}
}
以上代码概述完之后,甘特图大致的样式就可以实现了,接下来就是如何获取后端的数据进行展示,由于本案例获取后端数据的方法是封装axios后的,在这里不做过多的阐述
import { getManiProgressTable} from '@/api/home/home-center.js'
const getMainProgressData = params => {
loading.value = true
return new Promise((resolve, reject) => {
getManiProgressTable(params)
.then(res => {
if (res.code === 200 && res.data) {
// console.log(res.data.total, '实时检修进度')
total.value = res.data.total // 更新总条数
tableData.value = res.data.data.map((item, index) => {
return {
workOrderNum: item.JHBH,
site: item.TYDW,
range: item.TDFW,
actualityBegin: item.SJGZKSSJ.substring(0, 10),
actualityEnd: item.SJGZJSSJ.substring(0, 10),
planBegin: item.PZGZKSSJ.substring(0, 10),
planEnd: item.PZGZJSSJ.substring(0, 10)
}
})
resolve(res.data)
} else {
}
})
.catch(err => {
reject(err)
})
.finally(() => {
loading.value = false
// 在数据加载完成后,恢复滚动位置
setTimeout(() => {
tableRef.value.$refs.bodyWrapper.scrollTop = currentScrollTop
}, 100)
})
})
}
异步加载数据,由于该甘特图的数据过多,需要有分页的情况,下面代码将进行阐述如何做分页,在初始化加载数据时需要传分页参数
//定义开始入参
const initParams = ref({
currentPage: 1,
pageSize: 20
})
onMounted(() => {
getMainProgressData(initParams.value)
})
因为直接在 甘特图下方做分页效果太丑,所以新的需求是通过滚动条来实现分页效果展示数据,先在头部添加滚动事件
书写滚动分页事件方法
const pageSize = ref(20)
let currentPage = 1
const total = ref(0)
// 用于记录滚动位置
let currentScrollTop = 0
// 处理滚动事件,加载更多数据
const handleScroll = e => {
if (e.scrollTop == 0) {
return
}
const scrollHeight = tableRef.value.$refs.bodyWrapper.scrollHeight
const clientHeight = tableRef.value.$refs.tableBody.clientHeight
const scrollTop = Math.round(e.scrollTop) + 1
// 是否触底判断
const isBottom = scrollHeight + scrollTop >= clientHeight
if (isBottom) {
if (currentPage * pageSize.value < total.value) {
// 记录当前滚动位置
currentScrollTop = e.scrollTop
currentPage++
initParams.value = {
currentPage,
pageSize: pageSize.value
}
getMainProgressData(initParams.value)
console.log('触底')
}
}
}
由于以上书写的头部样式只是单纯的表格样式,过于简单,所以这里再重新做一下处理,在头部添加一个方法
书写样式方法代码
const headerStyle = ({ column }) => {
// console.log(column.label, 'column')
if (column.label === '总体实施计划') {
return { 'background-color': '#02A7F0' }
}
if (column.label === '检修甘特图') {
return { 'background-color': '#FFE0B2' }
}
if (
column.label === '进度' ||
column.label === '工单编号' ||
column.label === '停电场所' ||
column.label === '停电范围'
) {
return { 'background-color': '#BBDEFB' }
}
// const monthMatch = column.label?.match(/年(\d+)月/)
// if (monthMatch) {
// return { 'background-color': parseInt(monthMatch[1]) % 2 ? '#FFCDD2' : '#C8E6C9' }
// }
const isMonthColumn = column.label?.includes('年') && column.label?.includes('月')
const isDayColumn = column.property?.includes('-') // 通过prop属性判断是否为天数列
if (isMonthColumn || isDayColumn) {
// 从label或property中提取月份
const month = isMonthColumn ? parseInt(column.label.match(/年(\d+)月/)[1]) : parseInt(column.property.split('-')[1])
return {
'background-color': month % 2 ? '#FFCDD2' : '#C8E6C9',
padding: '8px 0' // 保持原有内边距样式
}
}
return {}
}
总体实施计划
计划
实际
检修甘特图