自制甘特图组件

目录结构

<template>
  <div class="gantt-content">
    <ganttChart
      :timeWidth="50"
      :startStamp="startStamp"
      :endStamp="endStamp"
      :header="header"
      :body="body"
    />
  </div>
</template>

<script>
import ganttChart from "./ganttChart"
import { orderGantt } from "@api/reportCenter/orderGantt"
import { searchCalendar } from "@api/basicData/calendar"

export default {
  components:{
    ganttChart,
  },
  data(){
    return {
      startStamp: this.$moment('2020-07-06 08:00:00').valueOf(),
      endStamp: this.$moment('2020-07-11 08:00:00').valueOf(),
      header: {
        height: 100,
        table: [],
        dates: [],
        class: [],
        times: []
      },
      body: {
        height: 40,
        list: []
      },
      year: '',
      week: ''
    }
  },
  methods:{
    // 获取数据
    async getData() {
      const date = await this.searchCalendar()
      if(date.code === 200) {
        const {data: {nowDate = []}} = date
        this.year = nowDate[0].haieryear
        this.haierWeek = nowDate[0].haierweek
      }
      const params = {
        year: this.year,
        haierWeek: this.haierWeek
      }
      const res = await orderGantt(params)
      // 组装甘特图数据
      if(res.code === 200) {
        let table = [
          { label: this.$t('orderGantt.factoryNo'), value: 'factoryNo',  80, style: {} },
          { label: this.$t('orderGantt.productLine'), value: 'productLine',  120 },
          { label: `${this.haierWeek}${this.$t('orderGantt.loadCount')}`, value: 'loadCount',  120 },
        ], 
        dates = [], 
        _class = [], times = [], list = [], cla_day = this.$t('orderGantt.day'), cla_ni = this.$t('orderGantt.night');
        if(res.data.length) {
          res.data[0].dateModels.forEach(item => {
            let theD = this.$moment(item.sdate).format('YYYY/MM/DD')
            dates.push({
              label: theD, 
              value: theD, 
              height: 50
            })
          })
          let dd = res.data[0].dateModels
          this.startStamp = this.$moment(this.$moment(dd[0].sdate).format('YYYY-MM-DD') + " 08:00:00").valueOf()
          this.endStamp = this.$moment(this.$moment(dd[dd.length - 1].sdate).format('YYYY-MM-DD') + " 08:00:00").add(1,'d').valueOf()
        }
        dates.forEach((item, idx) => {
          _class = [
            ..._class,
            { label: cla_day, height: 30 },
            { label: cla_ni, height: 30 },
          ]
          times = [
            ...times,
            {label: '8:00', height: 20 }, {label: '12:00', height: 20 }, 
            {label: '16:00', height: 20 }, {label: '20:00', height: 20 }, 
            {label: '00:00', height: 20 }, {label: '4:00', height: 20 }, 
          ]
        });
        res.data.forEach(item => {
          list.push({
            factoryNo: item.factoryNo,
            productLine: item.productLine,
            loadCount: item.loadCount,
            order: item.sequentialPlanList,
          })
        });
        this.header = {
          ...this.header,
          table,
          dates,
          class: _class,
          times,
        }
        this.body = {
          ...this.body,
          list
        }
      }
    },
    // 获取海尔年、月、周、星期列表
    searchCalendar() {
      return searchCalendar({
        date: this.$moment().format("YYYY-MM-DD"),
        haiermonth: "",
        haiersweek: "",
        haierweek: "",
        haieryear: ""
      })
    },
  },
  created() {
    this.getData()
  }
}
</script>
<style lang="less">
  .gantt-content {
     100%;
    padding: 20px;
    overflow-x: scroll;
  }
</style>
<template>
  <div class="gantt-chart" :style="{ widthAll+'px'}">
    <div class="c-content">
      <div class="c-table">
        <div class="c-header">
          <div
            class="th"
            v-for="item in header.table" 
            :key="item.value" 
            :style="{...(item.style || {}),  item.width+'px', height: (item.height || header.height)+'px', lineHeight: (item.height || header.height)+'px'}"
          >
            {{item.label}}
          </div>
        </div>
        <div class="c-body">
          <div
            class="tr"
            v-for="(tr, idx) in body.list"
            :key="idx" 
          >
            <div
              class="td"
              v-for="td in header.table"
              :key="td.value" 
              :style="{...td.style,  td.width+'px', height: body.height+'px', lineHeight: body.height+'px'}"
            >
              {{tr[td.value] || ' '}}
            </div>
          </div>
        </div>
      </div>
      <div class="c-gant">
        <div class="c-header">
          <div>
            <div
              class="th"
              v-for="(item, idx) in header.dates"
              :key="idx" 
              :style="{...(item.style || {}),  dateWidth+'px', height: (item.height || header.height)+'px', lineHeight: (item.height || header.height)+'px'}"
            >
              {{item.label}}
            </div>
          </div>
          <div>
            <div
              class="th"
              v-for="(item, idx) in header.class" 
              :key="idx" 
              :style="{...(item.style || {}),  classWidth+'px', height: (item.height || header.height)+'px', lineHeight: (item.height || header.height)+'px'}"
            >
              {{item.label}}
            </div>
          </div>
          <div>
            <div
              class="th"
              v-for="(item, idx) in (theTimes.length ? theTimes : header.times)" 
              :key="idx" 
              :style="{...(item.style || {}),  timeWidth+'px', height: (item.height || header.height)+'px', lineHeight: (item.height || header.height)+'px'}"
            >
              {{item.label}}
            </div>
          </div>
        </div>
        <div class="c-body">
          <div
            class="tr"
            v-for="(tr, idx) in body.list"
            :key="'tr'+idx" 
          >
            <div
              class="td"
              v-for="(td, idx2) in (theTimes.length ? theTimes : header.times)"
              :key="'td'+idx2" 
              :style="{...td.style,  timeWidth+'px', height: body.height+'px', lineHeight: body.height+'px'}"
            >
              {{' '}}
            </div>

            <div
              class="td-g"
              v-for="(tdg, idx3) in ganttArr[idx]"
              :key="tdg.label + idx3" 
              :style="{ height: body.height-2+'px', lineHeight: body.height-2+'px', ...tdg.style }"
              @mouseover="(e) => mouseOver(e, tdg)"
              @mouseout="(e) => mouseOut(e, tdg)"
              @contextmenu="(e) => contextmenu(e, tdg)"
            >
              {{tdg.label}}
            </div>
          </div>
        </div>
      </div>
    </div>

    <div id="a-gantt-pop" :style="popStyle">
       <p>{{$t('orderGantt.orderName')}}: {{currentData.label}}</p> 
       <p>{{$t('orderGantt.num')}}: {{currentData.num}}</p> 
       <p>{{$t('orderGantt.startTime')}}: {{currentData.startTime}}</p>
       <p>{{$t('orderGantt.endTime')}}: {{currentData.endTime}}</p>
    </div>

    <a-modal 
      :visible="modal.visible" 
      :title="modal.title" 
      width="740px"
      :footer="null"
      @ok="closeModal"
      @cancel="closeModal"
    >
      <p>{{$t('orderGantt.orderName')}}: {{modal.data.label}}</p> 
      <p>{{$t('orderGantt.num')}}: {{modal.data.num}}</p> 
      <p>{{$t('orderGantt.startTime')}}: {{modal.data.startTime}}</p>
      <p>{{$t('orderGantt.endTime')}}: {{modal.data.endTime}}</p>
    </a-modal>
  </div>
</template>

<script>
  const timeDown = [
    {label: '8:00', height: 20 }, {label: '12:00', height: 20 }, 
    {label: '16:00', height: 20 }, {label: '20:00', height: 20 }, 
    {label: '24:00', height: 20 }, {label: '4:00', height: 20 }
  ]
  const timeUp = [
    {label: '8:00', height: 20 }, {label: '9:00', height: 20 }, {label: '10:00', height: 20 },
    {label: '11:00', height: 20 }, {label: '12:00', height: 20 }, {label: '13:00', height: 20 },
    {label: '14:00', height: 20 }, {label: '15:00', height: 20 }, {label: '16:00', height: 20 },
    {label: '17:00', height: 20 }, {label: '18:00', height: 20 }, {label: '19:00', height: 20 },
    {label: '20:00', height: 20 }, {label: '21:00', height: 20 }, {label: '22:00', height: 20 },
    {label: '23:00', height: 20 }, {label: '00:00', height: 20 }, {label: '1:00', height: 20 },
    {label: '2:00', height: 20 }, {label: '3:00', height: 20 }, {label: '4:00', height: 20 },
    {label: '5:00', height: 20 }, {label: '6:00', height: 20 }, {label: '7:00', height: 20 }
  ]
  function getColor(d) {
    switch (true) {
      case d<100:
        return {
          background: '#F7DEB9',
          borderRadius: '4px',
          border: '1px solid #F7DEB9'
        }
        break;
      case d===100:
        return {
          background: '#CAB9CA',
          borderRadius: '4px',
          border: '1px solid #CAB9CA'
        }
        break;
      case d>100:
        return {
          background: '#735773',
          borderRadius: '4px',
          border: '1px solid #735773'
        }
        break;
      default:
        return {
          background: '#CAB9CA',
          borderRadius: '4px',
          border: '1px solid #CAB9CA'
        }
        break;
    }
  }

  export default {
    props:{
      // 开始时间时间戳
      startStamp: {
        type: Number,
        required: true
      },
      // 结束时间时间戳
      endStamp: {
        type: Number,
        required: true
      },
      // 时间单位宽度大小,  实践单位 4小时 、 1小时
      timeWidth: {
        type: Number,
        default: 50
      },
      // 表头
      header: {
        type: Object,
        default: {
          height: 100, // 表头的默认高度
          table: [],   // 表格头
          dates: [],   // 日期
          class: [],   // 班次
          times: []    // 时间
        }
      },
      // 数据
      body: {
        type: Object,
        default: {
          height: 40,   // 表格体的默认高度
          list: []
        }
      },
    },
    data() {
      return {
        widthAll: 1200,
        dateWidth: 300,
        classWidth: 150,
        timeAll: 259200000,  // 默认显示3天, 这是3天的毫秒数
        ganttArr: [],  // 任务数组
        currentData: {},  // 当前操作任务的数据
        popStyle: {  // hover浮层的样式
          display: 'none',
          top: 0,
          left: 0
        },
        modal: {  // 弹窗信息
          visible: false,
          title: '',
          data: {}
        },
        ctrlDown: false,  // true 代表Ctrl键正被按压
        wheelHeight: 0,  // 滚轮滚动的位置
        theTimes: [] // 时间表头,需要内部维护
      };
    },
    computed: {},
    watch: {
      body() {
        this.initGantt()
      },
      timeWidth(l) {
        this.dateWidth = l * 6
        this.classWidth = l * 3
      },
    },
    created() {
      this.initGantt()
    },
    mounted() {
      this.lisenScrol()
    },
    beforeDestroy() {
      this.ctrlDown = false
      this.theTimes = []
      let gantt = document.getElementsByClassName('c-gant')
      if(gantt && gantt[0]) gantt[0].removeEventListener('mousewheel', this.ganttZoom, false)
    },
    methods: {
      // 初始化甘特图
      initGantt() {
        const {list = []} = this.body
        let gArr = [], widthAll = 0;
        this.timeAll = this.endStamp - this.startStamp
        this.header.table.forEach(item => {
          widthAll += item.width
        })
        this.widthAll = widthAll + this.dateWidth * this.header.dates.length

        list.forEach(item => {
          let arr = []
          item.order.forEach(o => {
            arr.push({
              ...o,
              label: o.orderCode,
              style: {
                ...o.style,
                ...getColor(o.num),
                ...(this.countWidth(o.startTime, o.endTime))
              }
            })
          })
          gArr.push(arr)
        })
        this.ganttArr = gArr
      },
      // 计算任务位置
      countWidth(st, et) {
        let w = this.dateWidth * this.header.dates.length, 
          s = this.$moment(st).valueOf(), 
          e = this.$moment(et).valueOf();
        return {
          left: parseInt((s - this.startStamp) * (w / this.timeAll)) + 'px',
           parseInt((e - s) * (w / this.timeAll)) + 'px',
        }
      },
      // 鼠标移入
      mouseOver(e, d) {
        if(this.popStyle && this.popStyle.display === 'none') {
          this.popStyle = {
            display: 'block',
            top: e.clientY + 12+'px',
            left: e.clientX - 100+'px'
          }
          this.currentData = {...d}
        }
      },
      // 鼠标移出
      mouseOut(e, d) {
        this.popStyle = {
          display: 'none',
          top: 0,
          left: 0
        }
        this.currentData = {}
      },
      // 右击
      contextmenu(e, d) {
        e.preventDefault();
        this.modal = {
          visible: true,
          title: d.label,
          data: {...d}
        }
      },
      //  关闭弹窗
      closeModal() {
        this.modal = {
          visible: false,
          title: '',
          data: {}
        }
      },
      // 监听 Ctrl + 滚轮,缩放甘特图
      lisenScrol() {
        let w = this
        document.onkeydown = function(e) {
          if (e.keyCode === 17) w.ctrlDown = true
        },
        document.onkeyup = function(e) {
          if (e.keyCode === 17) w.ctrlDown = false
        },
        document.getElementsByClassName('c-gant')[0].addEventListener('mousewheel', this.ganttZoom, false); 
      },
      ganttZoom(e) {
        e.preventDefault();
          if(this.ctrlDown) {
            let _newTimes = []
            if(e.wheelDeltaY > 0) {  // 放大
              this.dateWidth = this.timeWidth * 24
              this.classWidth = this.timeWidth * 12
              this.header.dates.forEach(item => {
                _newTimes = [ ..._newTimes, ...timeUp ]
              });
            } else {  // 缩小
              this.dateWidth = this.timeWidth * 6
              this.classWidth = this.timeWidth * 3
              this.header.dates.forEach(item=> {
                _newTimes = [ ..._newTimes, ...timeDown ]
              });
            }
            this.theTimes = _newTimes
            this.initGantt()
          }
      }
    },
  }
</script>

<style lang="less" scoped>
  .gantt-chart {
    position: relative;
    background-color: white;
    .c-content {
      border: 0.5px solid gray;
      white-space: nowrap;
      .c-table {
        display: inline-block;
        .c-header {
          .th {
            display: inline-block;
            text-align: center;
            border: 0.5px solid gray;
            box-sizing: border-box;
            word-break: break-all;
          }
        }
        .c-body {
          .tr{
            .td {
              display: inline-block;
              text-align: center;
              border: 0.5px solid gray;
              box-sizing: border-box;
            }
          }
        }
      }
      .c-gant {
        display: inline-block;
        .c-header {
          .th {
            display: inline-block;
            text-align: center;
            border: 0.5px solid gray;
            box-sizing: border-box;
          }
        }
        .c-body {
          margin-top: -1px;
          .tr{
            position: relative;
            .td {
              display: inline-block;
              text-align: center;
              border: 0.5px solid gray;
              box-sizing: border-box;
              user-select: none;
            }
            .td-g {
              position: absolute;
              text-align: left;
              box-sizing: border-box;
              user-select: none;
              z-index: 99;
              color: white;
              font-weight: 600;
              letter-spacing: 1px;
              cursor: pointer;
              top: 1px;
              white-space: nowrap;
              text-overflow: ellipsis;
              overflow: hidden;
              word-break: break-all;
            }
          }
        }
      }
    }
    #a-gantt-pop{
      display: none;
      position: fixed;
      z-index: 200;
       200px;
      min- 150px;
      min-height: 100px;
      box-shadow: 0 0 12px gray;
      background-color: white;
      padding: 10px;
      border-radius: 5px;
      overflow: hidden;
    }
  }
</style>
原文地址:https://www.cnblogs.com/pengfei-nie/p/13750505.html