vue 实现项目进度甘特图

项目需求:

实现以1天、7天、30天为周期(周期根据筛选条件选择),展示每个项目不同里程碑任务进度。

项目在Vue-Gantt-chart: 使用Vue做数据控制的Gantt图表基础上进行了改造。

有需要的小伙伴也可以直接引入插件,自己修改。

我是直接把甘特图封装成了组件,结构如下图:

首先,安装插件

npm install v-gantt-chart

引入插件(我是全局引入的)

javascript 复制代码
import vGanttChart from 'v-gantt-chart';

Vue.use(vGanttChart);

代码如下:

index.js

javascript 复制代码
<template>
  <div class="container">
    <v-gantt-chart
      :startTime="times[0]"
      :endTime="times[1]"
      :cellWidth="cellWidth"
      :cellHeight="cellHeight"
      :timeLines="timeLines"
      :titleHeight="titleHeight"
      :scale="Number(1440 * scale)"
      :titleWidth="titleWidth"
      showCurrentTime
      :hideHeader="hideHeader"
      :dataKey="dataKey"
      :arrayKeys="arrayKeys"
      :scrollToTime="scrollToTime"
      :scrollToPostion="positionA"
      @scrollLeft="scrollLeftA"
      customGenerateBlocks
      :datas="ganttData"
    >
      <template
        v-slot:block="{
          data,
          getPositonOffset,
          getWidthAbout2Times,
          isInRenderingTimeRange,
          startTimeOfRenderArea,
          endTimeOfRenderArea,
          isAcrossRenderingTimeRange
        }"
      >
        <div
          class="gantt-block-item"
          v-for="(item, index) in data.gtArray"
          v-if="
            isInRenderingTimeRange(item.start) ||
            isInRenderingTimeRange(item.end) ||
            isAcrossRenderingTimeRange(item.start, item.end)
          "
          :key="item.id"
          :style="{
            left: getPositonOffset(item.start) + 'px',
            width: getWidthAbout2Times(item.start, item.end) + 'px',
            height: judgeTime(data.gtArray) ? '50%' : '100%',
            top: !judgeTime(data.gtArray)
              ? ''
              : index % 2 !== 1
              ? '0px'
              : '22px'
          }"
        >
          <Test
            :data="data"
            :updateTimeLines="updateTimeLines"
            :cellHeight="cellHeight"
            :currentTime="currentTime"
            :item="item"
            @nodeEvent="nodeEvent"
          ></Test>
        </div>
      </template>
      <template v-slot:left="{ data }">
        <TestLeft :data="data" @panelDb="panelDb"></TestLeft>
      </template>
      <!-- <template v-slot:timeline="{ day , getTimeScales }">
          <TestTimeline :day="day" :getTimeScales="getTimeScales"></TestTimeline>
        </template> -->
      <template v-slot:title>
        <div class="title">名称</div>
      </template>
    </v-gantt-chart>
  </div>
</template>

<script>
import moment from 'moment';
import Test from './components/test.vue';
import TestLeft from './components/test-left.vue';
import TestTimeline from './components/test-timeline.vue';
import TestMarkline from './components/test-markline.vue';

import dayjs from 'dayjs';

export default {
  name: '',
  components: { Test, TestLeft, TestTimeline, TestMarkline },
  props: {
    ganttData: {
      type: Array,
      default: () => []
    },
    scaleData: {
      type: Number,
      default: 10080
    },
    scrollToTime: {
      type: String,
      default: moment().subtract(4, 'days').format('YYYY-MM-DD')
    }
  },

  data() {
    return {
      timeLines: [],
      currentTime: dayjs(),
      cellWidth: 100,
      cellHeight: 50,
      titleHeight: 50,
      titleWidth: 250,
      // scale: 1440 * 30,
      startDate: moment().startOf('year'),
      endDate: moment().endOf('year'),
      times: [
        moment().subtract(1, 'year').format('YYYY-MM-DD hh:mm:ss'),
        moment().add(6, 'months').format('YYYY-MM-DD hh:mm:ss')
      ],
      rowNum: 100,
      colNum: 10,
      datasB: [],
      dataKey: 'projectId',
      // scrollToTime: moment().subtract(14, 'days').format('YYYY-MM-DD'),
      // scrollToTime: moment().subtract(4, 'days').format('YYYY-MM-DD'),
      scrollToPostion: { x: 10000, y: 10000 },
      hideHeader: false,
      hideSecondGantt: false,
      arrayKeys: ['gtArray'],
      scrollToY: 0,
      positionB: {},
      positionA: {}
    };
  },
  watch: {
    scrollToY(val) {
      this.positionA = { x: val };
    },
    ganttData(newVal, oldVal) {
      console.log('newVal===', newVal);
      console.log('oldVal===', oldVal);
    }
  },
  computed: {
    scale() {
      console.log(this.scaleData);
      return this.scaleData / 1440;
    }
  },
  methods: {
    judgeTime(arr) {
      let startTimeArr = [];
      let endTimeArr = [];
      arr.map(function (item) {
        startTimeArr.push(
          item.startDate ? new Date(item.startDate).getTime() : ''
        );
        endTimeArr.push(
          item.delayDate
            ? new Date(item.delayDate).getTime()
            : item.endDate
            ? new Date(item.endDate).getTime()
            : ''
        );
      });
      let allStartTime = startTimeArr.sort(); // 排序
      let allEndTime = endTimeArr.sort();
      let result = 0; // 判断时间是否有重复区间
      for (let k = 0; k < allStartTime.length; k++) {
        if (k > 0) {
          if (allStartTime[k] <= allEndTime[k - 1]) {
            result += 1;
          }
        }
      }
      return result > 0;
    },
    nodeEvent(item) {
      this.$emit('nodeEventClick', item);
    },
    panelDb(item) {
      this.$emit('panelDbClick', item);
    },
    updateTimeLines(timeA, timeB) {
      this.timeLines = [
        {
          time: timeA,
          text: '自定义'
        },
        {
          time: timeB,
          text: '测试',
          color: '#747e80'
        }
      ];
    },
    scrollLeftA(val) {
      this.positionB = { x: val };
    }
  }
};
</script>

<style lang="scss" scoped>
.container {
  height: 82vh;
  background-color: #f5faff;
}
.title {
  width: 100%;
  height: 100%;
  color: #96aaca;
  background: #f5faff;
}
:deep(.gantt-leftbar-wrapper) {
  border-right: 1px solid #c6d8ee !important;
}
</style>

test-left.vue

javascript 复制代码
<template>
  <div class="name">
    <div class="carId" @dblclick="onDblclick" >{{ data.projectName }}</div>
  </div>
</template>

<script>
export default {
  name: "TestLeft",
  props: {
    data: Object,
  },
  methods: {
    onDblclick() {
      // this.updateTimeLines(this.item.start, this.item.end);
      this.$emit('panelDb', this.data);
    }
  }
};
</script>

<style scoped>
.name {
  color: #000000;
  display: flex;
  box-sizing: border-box;
  overflow: hidden;
  height: 100%;
  width: 100%;
  padding: 10px 0;
  align-items: center;
  text-align: center;
  background: #f5faff;
  box-shadow: 2px 0px 4px 0px rgba(0, 0, 0, 0.1);
}

.carId {
  flex: 1;
}

.type {
  padding: 0 5px 0 0;
  font-size: 1.2rem;
}
</style>

test-markline.vue

javascript 复制代码
<template>
  <div
    class="markline"
    :style="{  left: getPosition() + 'px' }"
  >
    <div class="markline-label">
      {{timeConfig.text}}{{ dayjs(timeConfig.time).format("HH:mm:ss") }}
    </div>
  </div>
</template>

<script>
import dayjs from "dayjs"
export default {
  name: "TestMarkLine",
	props:['getPosition','timeConfig'],
	data(){
		return {
			dayjs
		}
	}
}
</script>

<style lang="scss" scoped>

.markline {
    position: absolute;
    z-index: 100;
    width: 2px;
    height: 100vh;
    background: #747e80;

    &-label {
      padding: 3px;
      width: 6rem;
      margin-left: -3rem;
      margin-top: 5rem;
      color: #fff;
      background: #747e80;
      text-align: center;
      border-radius: 5px;
      font-size: 0.7rem;
    }
}
</style>

test-timeline.vue

javascript 复制代码
<template>
 <div class="test">
  <span v-for="i in getTimeScales(day)"> {{i.format('HH:mm')}}</span>
 </div> 
</template>

<script>
export default {
  name: "TestLeft",
  props: {
    day: Object,
    getTimeScales:Function,
  }
};
</script>

<style lang="scss" scoped>
.test{
  display: flex;

  span{
    flex:1
  }
}
</style>

test.vue

javascript 复制代码
<template>
  <el-popover placement="bottom" trigger="hover">
    <div
      slot="reference"
      class="plan"
      :style="{
        'background-color': statusColor,
        'margin-top': 0.1 * cellHeight + 'px'
      }"
      @click="onClick"
    >
      <div class="middle">{{ item.summary }}</div>
    </div>

    <div class="detail">{{ item.summary }}</div>
  </el-popover>
</template>

<script>
import dayjs from 'dayjs';
export default {
  name: 'Test',
  props: {
    data: Object,
    item: Object,
    currentTime: dayjs,
    updateTimeLines: Function,
    cellHeight: Number,
    startTimeOfRenderArea: Number
  },
  data() {
    return {
      dayjs: dayjs,
      stateObj: {
        DelayStart: '#F56C6C',
        Normal: '#C2F1E2',
        NoStart: '#D9E3ED',
        Delay: '#F56C6C',
        Stop: '#D9E3ED',
        DelayRisk: '#FFD4C7',
        NoControl: '#F56C6C',
        Close: '#D9E3ED'
      }
    };
  },
  computed: {
    statusColor() {
      console.log('data=======', this.data);
      let { item } = this;

      return this.stateObj[item.state] || '#D9E3ED';
    },
    startToString() {
      return dayjs(this.item.start).format('HH:mm');
    },
    endToString() {
      return dayjs(this.item.end).format('HH:mm');
    }
  },
  methods: {
    onClick() {
      // this.updateTimeLines(this.item.start, this.item.end);
      this.$emit('nodeEvent', this.item);
    }
  }
};
</script>

<style lang="scss" scoped>
.middle {
  flex: 1;
  text-align: center;
  padding-left: 5px;
  text-overflow: ellipsis;  /* ellipsis:显示省略符号来代表被修剪的文本  string:使用给定的字符串来代表被修剪的文本*/ 
  white-space: nowrap;   /* nowrap:规定段落中的文本不进行换行   */ 
  overflow: hidden; /*超出部分隐藏*/
}
.runTime {
  display: flex;
  flex-direction: column;
}
.plan {
  display: flex;
  align-items: center;
  box-sizing: border-box;
  height: 80%;
  border: 1px solid #f0f0f0;
  border-radius: 5px;
  color: #333333;
  padding-left: 5px;
  font-size: 0.8rem;
  // opacity: 0.8;
}

.detail {
  .header {
    text-align: center;
    font-size: 1rem;
  }
}

.detail ul {
  list-style: none;
  padding: 0px;
  li {
    span {
      display: inline-block;
      width: 80px;
      color: #777;
      font-size: 0.8rem;
    }
    span:first-child {
      text-align: right;
    }

    span:last-child {
    }
  }
}
</style>

页面中使用

javascript 复制代码
<div>
    <ganttChart
      :ganttData="ganttArr"
      :scaleData="scaleData"
      :scrollToTime="scrollToTime"
      @nodeEventClick="nodeEventClick"
      @panelDbClick="panelDbClick"
     ></ganttChart>
</div>
javascript 复制代码
import moment from 'moment';
import ganttChart from './components/ganttChart/index.vue';

export default {
    components: { ganttChart },
    data(){
        return{
            ganttArr: [],
            scaleData: 10080,
            scrollToTime: moment().subtract(4, 'days').format('YYYY-MM-DD'),
        }
    },
    methods: {
        // 点击甘特图node节点
        nodeEventClick(item) {
            // 执行自己的逻辑
        },
        // 双击甘特图左侧标题
        panelDbClick(item) {
            //执行自己的逻辑
        }
    }
    
}

以上就是实现甘特图的全部过程,欢迎大佬们指教。

相关推荐
Ganttable10 天前
甘特图代做服务
甘特图
进度猫10 天前
项目管理软件:5款甘特图工具测评
甘特图
RumbleWx21 天前
什么是甘特图?
甘特图
小菜花2921 天前
table标签实现甘特图效果
甘特图
Ganttable21 天前
甘特图基线-用起来了吗~
甘特图
Ganttable1 个月前
免费的高质量、美观的甘特图模板
甘特图
和风微凉2 个月前
Highcharts甘特图基本用法(highcharts-gantt.js)
前端·javascript·echarts·甘特图
Ganttable2 个月前
5分钟快速制作高质量、美观的Excel甘特图
excel·甘特图
白鹭凡2 个月前
react 甘特图之旅
前端·react.js·甘特图
百炼成神 LV@菜哥2 个月前
软件设计画图,流程图、甘特图、时间轴图、系统架构图、网络拓扑图、E-R图、思维导图
流程图·甘特图·思维导图