vue3+element plus实现甘特图效果

在项目管理场景中,甘特图可能我们都不陌生,它是可视化任务进度和时间管理的利器。本文将演示如何使用Vue3结合Element Plus组件库,实现一个轻量级交互式甘特图组件,包含时间轴渲染、任务条、依赖关系展示等核心功能。

目录

一、安装组件

二、引入组件

三、使用组件实现甘特图

1.搭建框架

2.左侧固定列任务

3.右侧任务进度条

四、获取数据

五、滚动条实现分页

六、处理头部列表样式

七、全部代码


vue3+element plus实现甘特图效果_第1张图片

一、安装组件

对于项目的框架具体怎么安装,这里不做说明,下面是安装element plus组件方式。

npm install element-plus

二、引入组件

在框架中的main.js文件中引入刚安装的组件

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)

三、使用组件实现甘特图

1.搭建框架

在项目中可能甘特图这个使用的地方会很多,所以在这里作为一个组件来进行开发,首先是搭建一个具体的框架样式,然后给他填充相应的数据






2.左侧固定列任务

甘特图如果大致分的话,其实分为两大部分,分别为左侧的固定列展示每个任务的具体数据信息,右侧为 每个任务的时间进度条。接下来我们先实现左侧的固定列部分,该代码是在以上代码的基础上进行展示

 
        
          
          
          
          
          
            
          
        

3.右侧任务进度条

由于每个人任务的甘特图分为计划时间和实际时间,即在一个任务中需要显示两个任务进度条,所以在这里我们需要做一些处理。


        
          
          
          
            
            
              
            
          
        

从上述的头部列表中我们可以看到使用了很多的方法,用于获取年份、月份和天数等,先来实现获取年份和月份的

// 当前年份
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 {}
}

七、全部代码





你可能感兴趣的:(案例分享,甘特图,vue.js,前端,elementui,javascript)