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;
}
相关推荐
沿着路走到底15 分钟前
JS事件循环
java·前端·javascript
子春一233 分钟前
Flutter 2025 可访问性(Accessibility)工程体系:从合规达标到包容设计,打造人人可用的数字产品
前端·javascript·flutter
白兰地空瓶39 分钟前
别再只会调 API 了!LangChain.js 才是前端 AI 工程化的真正起点
前端·langchain
jlspcsdn2 小时前
20251222项目练习
前端·javascript·html
行走的陀螺仪2 小时前
Sass 详细指南
前端·css·rust·sass
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ2 小时前
React 怎么区分导入的是组件还是函数,或者是对象
前端·react.js·前端框架
LYFlied2 小时前
【每日算法】LeetCode 136. 只出现一次的数字
前端·算法·leetcode·面试·职场和发展
子春一22 小时前
Flutter 2025 国际化与本地化工程体系:从多语言支持到文化适配,打造真正全球化的应用
前端·flutter
QT 小鲜肉3 小时前
【Linux命令大全】001.文件管理之file命令(实操篇)
linux·运维·前端·网络·chrome·笔记
羽沢313 小时前
ECharts 学习
前端·学习·echarts