vue实现甘特图效果

从零开始:用Vue.js打造一个功能完整的甘特图组件

甘特图作为项目管理中最重要的可视化工具之一,能够直观地展示任务的时间安排、进度和依赖关系。本文将详细介绍如何使用Vue.js从零开始构建一个功能完整、性能优秀的甘特图组件。

🏗️ 架构设计

在开始编码之前,我们需要先设计好整体架构。一个好的甘特图组件应该包含以下几个核心部分:

1. 数据层设计

首先定义清晰的数据结构,这是整个组件的基础:

javascript 复制代码
// 一级任务数据结构(父任务)
parentTask: {
  taskId: String,           // 任务ID
  taskName: String,         // 任务名称
  projectId: String,        // 所属项目ID
  projectName: String,      // 所属项目名称
  createTimeMills: Number,  // 创建时间戳
  deadlineMills: Number,    // 截止时间戳
  status: Number,           // 任务状态 (1:进行中, 2:已完成, 3:已取消)
  progress: Number,         // 整体进度百分比
  isParent: true,           // 标识为父任务
  children: Array<Task>,    // 子任务列表
  expanded: Boolean         // 是否展开显示子任务
}

// 二级任务数据结构(子任务)
childTask: {
  taskId: String,           // 任务ID
  taskName: String,         // 任务名称
  parentTaskId: String,     // 父任务ID
  documentName: String,     // 文档名称
  createTimeMills: Number,  // 创建时间戳
  deadlineMills: Number,    // 截止时间戳
  status: Number,           // 任务状态 (1:进行中, 2:已完成, 3:已取消)
  stageName: String,        // 阶段名称
  progress: Number,         // 进度百分比
  assignee: String,         // 负责人
  isParent: false,          // 标识为子任务
  level: 2                  // 任务层级
}

// 层级任务树结构
taskTree: {
  projectId: String,
  projectName: String,
  tasks: Array<ParentTask>  // 一级任务列表(包含子任务)
}

2. 时间轴算法

时间轴是甘特图的核心,我们需要实现灵活的时间范围计算:

🕐 多时间周期支持

甘特图支持三种时间周期,满足不同的查看需求(也可自行修改为时间范围):

  • 本周视图: 从周一到周日,适合查看短期任务
  • 本月视图: 从月初到月末,适合月度规划
  • 本季视图: 从季度初到季度末,适合长期项目管理
javascript 复制代码
// 时间范围计算逻辑
switch (this.timePeriod) {
  case 'week':
    startDate = today.startOf('week').add(1, 'day')
    endDate = today.endOf('week').subtract(1, 'day')
    break
  case 'month':
    startDate = today.startOf('month')
    endDate = today.endOf('month')
    break
  case 'quarter':
    startDate = today.startOf('quarter')
    endDate = today.endOf('quarter')
    break
}
📅 日期生成
javascript 复制代码
// 生成时间线日期数组
const dates = []
let current = startDate

while (current.isBefore(endDate) || current.isSame(endDate, 'day')) {
  dates.push(current.format('YYYY-MM-DD'))
  current = current.add(1, 'day')
}

3. 核心:任务条定位

这是甘特图最核心的算法,决定了每个任务条在时间轴上的精确位置:

javascript 复制代码
getTaskBarStyle(task) {
  // 标准化任务开始和结束日期
  const taskStartDate = dayjs(task.createTimeMills).startOf('day')
  const taskEndDate = dayjs(task.deadlineMills).startOf('day')
  
  // 获取时间线范围
  const timelineStartDate = dayjs(this.localFilters.timeRange[0]).startOf('day')
  const timelineEndDate = dayjs(this.localFilters.timeRange[1]).startOf('day')
  
  // 计算有效显示范围(任务与时间线的交集)
  const effectiveStartDate = taskStartDate.isBefore(timelineStartDate) 
    ? timelineStartDate : taskStartDate
  const effectiveEndDate = taskEndDate.isAfter(timelineEndDate) 
    ? timelineEndDate : taskEndDate
  
  // 计算位置百分比
  const totalTimelineDays = timelineEndDate.diff(timelineStartDate, 'day') + 1
  const taskStartOffset = effectiveStartDate.diff(timelineStartDate, 'day')
  const taskDisplayDays = effectiveEndDate.diff(effectiveStartDate, 'day') + 1
  
  const leftPercent = (taskStartOffset / totalTimelineDays) * 100
  const widthPercent = (taskDisplayDays / totalTimelineDays) * 100
  
  return {
    left: `${leftPercent}%`,
    width: `${widthPercent}%`
  }
}

🎨 HTML部分

1. 组件结构设计

html 复制代码
<div class="task-gantt-chart">
  <!-- 筛选器区域 -->
  <div class="gantt-header">
    <div class="gantt-filters">...</div>
  </div>
  
  <!-- 甘特图主体 -->
  <div class="gantt-container">
    <div class="gantt-timeline">
      <!-- 时间线头部 -->
      <div class="timeline-header">...</div>
      
      <!-- 甘特图主体内容 -->
      <div class="gantt-body">...</div>
    </div>
  </div>
  
  <!-- 分页组件 -->
  <div class="table-pagination">...</div>
</div>

2. 时间轴头部设计

html 复制代码
<div class="timeline-header">
  <div class="timeline-left-title">任务名称</div>
  <div class="timeline-dates">
    <div v-for="date in timelineDates" :key="date" class="date-cell">
      {{ formatDate(date) }}
    </div>
  </div>
</div>

3. 层级任务行布局

支持父子任务的层级显示和展开/折叠功能:

html 复制代码
<!-- 递归渲染任务树 -->
<template v-for="task in taskTree" :key="task.taskId">
  <!-- 父任务行 -->
  <div class="task-row parent-task" :class="{ 'has-children': task.children.length }">
    <!-- 左侧任务信息 -->
    <div class="task-info">
      <div class="task-name">
        <!-- 展开/折叠按钮 -->
        <span 
          v-if="task.children.length" 
          class="expand-btn"
          :class="{ 'expanded': task.expanded }"
          @click="toggleTaskExpand(task)">
          <i class="el-icon-arrow-right"></i>
        </span>
        <span class="task-title">{{ task.taskName }}</span>
        <span class="task-count" v-if="task.children.length">
          ({{ task.children.length }}个子任务)
        </span>
      </div>
      <div class="task-meta">
        <span class="task-progress">{{ task.progress }}%</span>
      </div>
    </div>
    
    <!-- 右侧时间线区域 -->
    <div class="task-timeline">
      <div class="task-bar parent-bar" :style="getTaskBarStyle(task)">
        <span class="task-bar-text">{{ getTaskDuration(task) }}天</span>
      </div>
    </div>
  </div>
  
  <!-- 子任务行(可折叠) -->
  <template v-if="task.expanded && task.children.length">
    <div 
      v-for="childTask in task.children" 
      :key="childTask.taskId"
      class="task-row child-task">
      <!-- 左侧任务信息(带缩进) -->
      <div class="task-info indented">
        <div class="task-name">
          <span class="indent-line"></span>
          <span class="task-title">{{ childTask.documentName }}</span>
        </div>
        <div class="task-meta">
          <span class="task-stage">{{ childTask.stageName }}</span>
          <span class="task-assignee">{{ childTask.assignee }}</span>
        </div>
      </div>
      
      <!-- 右侧时间线区域 -->
      <div class="task-timeline">
        <div class="task-bar child-bar" :style="getTaskBarStyle(childTask)">
          <span class="task-bar-text">{{ getTaskDuration(childTask) }}天</span>
        </div>
      </div>
    </div>
  </template>
</template>

⚡ 交互体验优化

良好的交互体验是优秀组件的重要标志。

1. 智能日期提示

当用户在时间轴上移动鼠标时,实时显示对应的日期:

javascript 复制代码
handleMouseMove(event) {
  const timelineElement = event.currentTarget
  const rect = timelineElement.getBoundingClientRect()
  const mouseX = event.clientX - rect.left
  const timelineWidth = rect.width
  
  // 计算鼠标位置对应的日期
  const positionPercent = (mouseX / timelineWidth) * 100
  const dayIndex = Math.floor((positionPercent / 100) * this.timelineDates.length)
  
  if (dayIndex >= 0 && dayIndex < this.timelineDates.length) {
    this.hoverLine.visible = true
    this.hoverLine.position = positionPercent
    this.hoverLine.date = this.timelineDates[dayIndex]
  }
}

2. 层级任务交互功能

实现展开/折叠和层级状态管理:

javascript 复制代码
// 展开/折叠任务
toggleTaskExpand(task) {
  task.expanded = !task.expanded
  
  // 可选:保存展开状态到本地存储
  this.saveExpandState(task.taskId, task.expanded)
},

// 批量展开/折叠所有任务
toggleAllTasks(expanded = true) {
  const toggleTask = (task) => {
    if (task.children && task.children.length) {
      task.expanded = expanded
      task.children.forEach(child => {
        if (child.children) {
          toggleTask(child)
        }
      })
    }
  }
  
  this.taskTree.forEach(toggleTask)
},

// 任务状态样式计算
getTaskBarClass(task) {
  const classes = ['task-bar']
  
  // 添加层级类型
  if (task.isParent) {
    classes.push('parent-bar')
  } else {
    classes.push('child-bar')
  }
  
  // 添加状态类
  if (task.status === 1) {
    classes.push('status-ongoing')
    if (task.isExpired) {
      classes.push('status-expired')
    }
  } else if (task.status === 2) {
    classes.push('status-completed')
  } else if (task.status === 3) {
    classes.push('status-canceled')
  }
  
  return classes.join(' ')
},

// 计算任务的可见性(考虑父任务折叠状态)
isTaskVisible(task, parentTask = null) {
  if (!parentTask) return true
  return parentTask.expanded && this.isTaskVisible(parentTask, parentTask.parent)
}

🎨 样式系统设计

现代化的样式设计让甘特图既美观又实用。

1. 响应式网格系统

使用CSS Grid创建灵活的时间轴布局:

css 复制代码
.timeline-dates {
  display: grid;
  grid-template-columns: repeat(var(--timeline-days), 1fr);
  border-left: 1px solid #e0e0e0;
}

.date-cell {
  padding: 8px 4px;
  text-align: center;
  border-right: 1px solid #e0e0e0;
  font-size: 12px;
}

2. 层级任务样式设计

为父子任务设计不同的视觉样式,增强层级感:

css 复制代码
/* 基础任务条样式 */
.task-bar {
  height: 20px;
  border-radius: 4px;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  color: white;
  transition: all 0.3s ease;
}

/* 父任务条样式 - 更粗更突出 */
.parent-bar {
  height: 24px;
  font-weight: bold;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* 子任务条样式 - 相对较细 */
.child-bar {
  height: 18px;
  opacity: 0.9;
}

/* 任务状态颜色 */
.status-ongoing {
  background: linear-gradient(90deg, #409EFF 0%, #66B3FF 100%);
}

.status-completed {
  background: linear-gradient(90deg, #67C23A 0%, #85CE61 100%);
}

.status-expired {
  background: linear-gradient(90deg, #F56C6C 0%, #F78989 100%);
}

/* 层级缩进样式 */
.task-info.indented {
  padding-left: 30px;
  position: relative;
}

.indent-line {
  position: absolute;
  left: 15px;
  top: 50%;
  width: 12px;
  height: 1px;
  background: #ddd;
  transform: translateY(-50%);
}

.indent-line::before {
  content: '';
  position: absolute;
  left: -15px;
  top: -10px;
  width: 1px;
  height: 20px;
  background: #ddd;
}

/* 展开/折叠按钮样式 */
.expand-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  margin-right: 8px;
  cursor: pointer;
  border-radius: 2px;
  transition: all 0.2s ease;
  color: #666;
}

.expand-btn:hover {
  background: #f0f0f0;
  color: #409EFF;
}

.expand-btn.expanded {
  transform: rotate(90deg);
}

/* 父任务行样式 */
.parent-task {
  background: #fafafa;
  border-left: 3px solid #409EFF;
}

.parent-task .task-name {
  font-weight: 600;
  color: #303133;
}

/* 子任务行样式 */
.child-task {
  background: #fff;
  border-left: 3px solid transparent;
}

.child-task:hover {
  background: #f8f9fa;
}

/* 任务计数标签 */
.task-count {
  font-size: 12px;
  color: #909399;
  margin-left: 8px;
}

/* 任务进度显示 */
.task-progress {
  font-size: 12px;
  color: #67C23A;
  font-weight: 500;
}
相关推荐
yes or ok5 分钟前
前端工程师面试题-vue
前端·javascript·vue.js
我要成为前端高手20 分钟前
给不支持摇树的三方库(phaser) tree-shake?
前端·javascript
Noxi_lumors35 分钟前
VITE BALABALA require balabla not supported
前端·vite
周胜237 分钟前
node-sass
前端
aloha_43 分钟前
Windows 系统中,杀死占用某个端口(如 8080)的进程
前端
牧野星辰44 分钟前
让el-table长个小脑袋,记住我的滚动位置
前端·javascript·element
code_YuJun1 小时前
React 常见问题
前端
_Congratulate1 小时前
vue3高德地图api整合封装(自定义撒点、轨迹等)
前端·javascript·vue.js
用户904706683571 小时前
TL如何进行有效的CR
前端
富婆苗子1 小时前
关于wangeditor的自定义组件和元素
前端·javascript