拖拽甘特图

1,186 阅读2分钟

先上代码,后续更新文档

ezgif.com-gif-maker (1).gif

import moment from 'moment';
// 显示时间段
export const timeList = [
    '00:00\n-\n02:00',
    '02:00\n-\n04:00',
    '04:00\n-\n06:00',
    '06:00\n-\n08:00',
    '08:00\n-\n10:00',
    '10:00\n-\n12:00',
    '12:00\n-\n14:00',
    '14:00\n-\n16:00',
    '16:00\n-\n18:00',
    '18:00\n-\n20:00',
    '20:00\n-\n22:00',
    '22:00\n-\n24:00',
];

const timeArr = [];
for (let i = 0; i < 24; i++) {
    timeArr.push((i < 10 ? '0' + i : i) + ':15:00');
    timeArr.push((i < 10 ? '0' + i : i) + ':30:00');
    timeArr.push((i < 10 ? '0' + i : i) + ':45:00');
    if (i < 23) {
        timeArr.push(((i + 1) < 10 ? '0' + (i + 1) : (i + 1))  + ':00:00');
    } else {
        timeArr.push('00:00:00');
    }
}

// 单元格时间段
export const colTimeList = timeArr;

const dataArr = [];

for (let i = 0; i < 96; i++) {
    dataArr.push(0);
}
// 单元格占位标识符
export const setDateArr = dataArr;


/**
 * 右侧table数据
 * @param showEl {boolean} 是否显示隐藏的占位单元
 * @param title {string} 显示星期
 * @param date {string} 显示日期
 * @param dataList {Array<Object>} 已添加数据
 * @param setDateArr {0 | 1} 单元格占位数据 0 无数据 1 有数据
 */
export function weekDataList(date) {
    const dayList = [];
    const times = [-1, 0, 1, 2, 3, 4, 5, 6, 7];
    const weeks = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日', '星期一'];
    for (let i = 0; i < times.length; i++) {
        const day = {
                title: weeks[i],
                date: moment().week(moment(new Date(date)).week()).weekday(times[i]).format('YYYY-MM-DD'),
                showEl: false,
                dataList: [],
            };
        dayList.push(day);
    }
    return dayList;
}
<template>
  <div class="simulator-schedule-box">
    <!-- 查询区域 -->
    <div class="table-page-search-wrapper">
      <a-form layout="inline" class="table-page-search-wrapper-form">

        <div class="table-page-search-wrapper-form-left">
          <a-form-item label="排课信息">
            <a-select style="width: 120px">
              <a-select-option value="jack">Jack</a-select-option>
              <a-select-option value="lucy">Lucy</a-select-option>
            </a-select>
          </a-form-item>

          <a-form-item label="搜索">
            <a-input placeholder="输入关键字进行过滤" />
          </a-form-item>

          <a-form-item>
            <a-tooltip>
              <template #title>隐藏/显示工号</template>
              <a-switch/>
            </a-tooltip>
          </a-form-item>

          <a-form-item>
            <a-tooltip>
              <template #title>显示全部/未排课程</template>
              <a-switch/>
            </a-tooltip>
          </a-form-item>

          <a-form-item>
            <a-tooltip>
              <template #title>折叠/展开课程</template>
              <a-switch/>
            </a-tooltip>
          </a-form-item>
        </div>

        <div class="table-page-search-wrapper-form-right">
          <a-button>上一周</a-button>
          <a-button>下一周</a-button>

          <a-form-item>
            <a-week-picker v-model="week" @change="changeWeek" placeholder="请选择日期"/>
          </a-form-item>
        </div>

      </a-form>
    </div>
    <!--    &lt;!&ndash; 操作按钮区域 &ndash;&gt;-->
    <!--    <div class="table-operator" style="border-top: 5px">-->
    <!--      <a-button type="primary" icon="plus">{{ $t('common.add') }}</a-button>-->
    <!--    </div>-->

    <div class="table-box">
      <div class="left">
        <ul>
          <li
            draggable="true"
            @dragstart="dragstartList($event, {length: item, type: 'new'})"
            @dragend="drag($event)"
            v-for="item in 10"
            :key="item">{{ item }}</li>
        </ul>

      </div>
      <div class="right">
        <div class="right-header">

        </div>

        <div class="gantt-box">
          <div class="gantt-list">
            <template v-if="tableList.length > 0">
              <ul class="gantt-item" v-for="(gantt, g) in tableList" :key="g">
                <!--甘特图头部 头部两小时划分显示的单元格-->
                <li>
                  <ul class="gantt-header">
                    <li class="gantt-header-label">
                      日期
                    </li>
                    <li
                      class="gantt-header-item"
                      :style="{width: (colWidth * 8) + 'px'}"
                      v-for="item in timeList"
                      :key="item">
                      <pre>{{ item }}</pre>
                    </li>
                  </ul>
                </li>

                <!--甘特图内容-->
                <li v-for="(item, i) of gantt.list" :key="i">
                  <ul class="gantt-row-list">

                    <!--日期title 左侧星期几与日期-->
                    <li class="gantt-row-list-label">
                    <span>
                      {{ item.date | dateFormat('MM-DD') }}
                      <br>
                      {{ item.title }}
                    </span>
                    </li>

                    <!--甘特图具体显示数据逻辑-->
                    <li class="gantt-row-content">

                      <!--以每两小时划分单元格-->
                      <ul class="gantt-row-time">
                        <li
                            :style="{width: (colWidth * 8) + 'px'}"
                            class="gantt-row-time-item"
                            v-for="date in timeList"
                            :key="date"></li>
                      </ul>

                      <!--以每十五分钟划分单元格-->
                      <ul class="gantt-row-col">
                        <li
                            @drop="drop($event, gantt.list, g, item, i, k)"
                            @dragover="dragoverList($event, gantt.list, item, i, k)"
                            class="gantt-row-col-item"
                            :class="{'gantt-row-col-item-border' : isDrag}"
                            :style="{width: colWidth + 'px'}"
                            v-for="(data, k) in colTimeList"
                            :key="k"></li>
                      </ul>

                      <!--甘特图排课数据-->
                      <ul class="gantt-row-data-list">
                        <li
                            v-for="(data, d) in item.dataList"
                            :key="d"
                            @dragstart="dragstartList($event, {length: data.width, type: 'old', index: d, item, data})"
                            @dragend="drag($event, {length: data.width, type: 'old', index: d, item, data})"
                            draggable="true"
                            @click="selectData(data)"
                            :style="{
                          left: (data.col * colWidth) + 'px',
                          width: (data.width * colWidth) + 'px',
                        }"
                            :class="{'gantt-row-data-item-border' : (courseData.indexKey === data.indexKey)}"
                            class="gantt-row-data-item">
                          <!--当前拖拽课程内部单元格-->
                          <ul class="gantt-row-data-item-col-list" v-show="data.event">
                            <li
                                @drop="drop($event, gantt.list, g, item, i, (data.col + c))"
                                @dragover="dragoverList($event, gantt.list, item, i, (data.col + c))"
                                class="gantt-row-data-item-col-item"
                                v-for="(col, c) in data.width"
                                :key="c"
                                :style="{
                              width: colWidth + 'px',
                            }"></li>
                          </ul>
                        </li>
                      </ul>

                      <!--元素拖拽显示预占位蒙层-->
                      <p
                          class="table-row-item-show"
                          :style="{left: showElLeft + 'px', width: showElWidth + 'px'}"
                          v-show="item.showEl"></p>
                    </li>
                  </ul>
                </li>


              </ul>
            </template>
            <div class="empty">
              <a-empty description="暂未选择模拟器" v-if="tableList.length === 0"/>
            </div>
          </div>
        </div>

        <div class="right-footer">

          <div class="simulator-select">
            <a-checkbox-group @change="onChange" v-model="simulatorValue" >
              <a-checkbox v-for="(item, i) in simulatorDataList" :key="i" :value="item.id">{{item.title}}</a-checkbox>
            </a-checkbox-group>
          </div>

          <div class="simulator-load">
            <a-button type="link" @click="colWidth--">W-</a-button>
            <a-button type="link" @click="colWidth++">W+</a-button>
            <a-button @click="loadSimulator">加载模拟机</a-button>
          </div>

        </div>

      </div>
    </div>
  </div>
</template>

<script>
import moment from 'moment';

import {timeList, colTimeList, weekDataList} from '@views/inspect/services/SimulatorScheduleService'

export default {
  name: "SimulatorSchedule",
  data() {
    return {
      // 单元格宽度
      colWidth: 15,
      // 两小时单元格
      timeList,
      // 十五分钟单元格
      colTimeList,
      /**
       * 右侧甘特图数据
       * @param showEl {boolean} 是否显示隐藏的占位单元
       * @param title {string} 显示星期
       * @param date {string} 显示日期
       * @param list {number} 单元格数量
       * @param dataList {Array<Object>} 已添加数据,已排课课程
       * @param setDateArr {0 | 1 | 2} 单元格占位数据 0 无数据 1 有数据 2 当前元素被拖拽
       */
      tableList: [],
      // 元素距左侧距离
      showElLeft: 0,
      // 元素单元宽度
      showElWidth: this.colWidth,
      // 拖拽元素是否是课程
      isDrag: false,
      // 当前拖拽单元时间跨度的长度
      dataLength: 0,
      // 已生成课程数据
      oldData: {},
      // 课程索引
      courseData: '',
      // 模拟机列表
      simulatorDataList: [
        {title: 'FFS-1', id: 'FFS-1'},
        {title: 'FFS-2', id: 'FFS-2'},
        {title: 'FFS-3', id: 'FFS-3'},
        {title: 'FFS-4', id: 'FFS-4'},
        {title: 'FFS-5', id: 'FFS-5'},
        {title: 'MAX', id: 'MAX'},
        {title: '外训', id: '外训'},
      ],
      // 模拟机选中
      simulatorValue: [],
      // 当前周
      week: moment(new Date())
    }
  },
  filters: {
    dateFormat: function (date, format) {
      return moment(new Date(date)).format(format)
    }
  },
  created() {
  },
  methods: {
    // 加载模拟机
    loadSimulator() {
      this.tableList = [];
      this.simulatorValue.forEach(item => {
        this.tableList.push({
          id: item,
          list: weekDataList(this.week)
        });
      });
    },
    // 周选择
    changeWeek() {

    },
    // 选择模拟器
    onChange(event) {
      console.log(event);
    },
    // 选中课程数据
    selectData(data) {
      this.courseData = data;
    },
    // 表格生成下载图片
    save() {
      // html2canvas(this.$refs['tableEl']).then(res => {
      //   const url = res.toDataURL('image/png');
      //   const a = document.createElement('a');
      //   a.href = url;
      //   a.download = '课程表';
      //   a.click();
      // });
    },
    /**
     * 拖拽元素释放将所有行的预占位块隐藏掉并且将课程拖拽状态置为false
     * @param event {HTMLElement} 拖拽元素对象
     * @param data {any?} 传入数据
     */
    drag(event, data) {
      if (data && data.type === 'old') {
        data.data.event = false;
      }
      this.tableList.forEach(item => {
        item.list.forEach(data => {
          data.showEl = false
        });
      });
      this.isDrag = false;
    },
    /**
     * 当课程拖拽生成元素在时间段节点上释放以后
     * 判断当前预置课程时间段是否与已经生成时间段存在交集,如果存在交集,则不允许生成课程
     * @param event
     * @param gantt {Array<Object>} 单独一项甘特图数据
     * @param item {Object} 甘特图行数据
     * @param g {number} 当前甘特图索引
     * @param i {number} 当前所在行
     * @param k {number} 所在单元格
     * @returns {boolean}
     */
    drop(event, gantt, g, item, i, k) {
      event.stopPropagation();
      event.preventDefault();
      const num = this.dataLength + k;
      let isDrop = false;
      // 预设时间范围
      const arr1 = [];
      for (let j =  k + 1; j <= num; j++) {
        arr1.push(j);
      }
      item.dataList.forEach(data => {
        // 课程时间范围
        const arr2 = [];
        if (this.courseData?.indexKey !== data.indexKey) {
          for (let j = data.col + 1; j < (data.col + data.width + 1); j++) {
            arr2.push(j);
          }
          // 计算范围交集
          const arr3 = arr1.filter(function(num) {
            return arr2.indexOf(num) !== -1;
          });
          if (arr3.length > 0) {
            isDrop = true;
          }
        }
      });
      // 如果剩余时间小于课程时间,那么不允许添加课程
      if (isDrop || num > this.colTimeList.length) {
        this.$message.warning('当前剩余时间不足');
        return false
      }
      if (this.courseData !== '') {
        this.tableList.forEach(tableList => {
          tableList.list.forEach(data => {
            const list = data.dataList.filter(item => item.indexKey !== this.courseData.indexKey);
            data.dataList = [...list];
          });
        });
      }
      // 如果剩余空闲时间大于等于课程时间那么将生成时间段课程数据
      item.dataList.push({
        col: k,
        width: Number(JSON.stringify(JSON.parse(this.dataLength))),
        event: false,
        indexKey: (g + '' + i + k),
        data: {
          startTime: this.colTimeList[k],
          endTime: this.colTimeList[num]
        }
      });
      this.courseData = '';
      return false;
    },
    /**
     * 当前为模拟数据,当替换为真实数据时候需要将数据挂起
     * @param event {Object} 数据(当前不存在数据)
     * @param data {Object} 冗余传参数据
     */
    dragstartList(event, data) {
      if (data.type === 'old') {
        data.data.event = true;
        this.courseData = data.data;
      }
      this.showElWidth = (data.length * this.colWidth);
      this.dataLength = data.length;
      this.isDrag = true;
    },
    /**
     * 当拖拽元素进入单元格时
     * 进行判断是否是需要生成课程条目
     *
     * @param event
     * @param gantt {Array<Object>} 甘特图数据
     * @param item 当前行数据域
     * @param i 当前在第几行
     * @param k {number} 当前所在第几个单元格
     * @returns {boolean}
     */
    dragoverList(event, gantt, item, i, k) {
      if (!this.isDrag) return false;
      gantt.forEach(data => data.showEl = false);
      item.showEl = true;
      this.showElLeft = (k * this.colWidth);
      event.stopPropagation();
      event.preventDefault();
      return false;
    },
  },
  watch: {
  }
}
</script>
<style lang="less" scoped>

.simulator-schedule-box {

  .table-page-search-wrapper-form{
    display: flex;
    justify-content: space-between;
    flex-wrap: wrap;
    grid-column-gap: 30px;

    .table-page-search-wrapper-form-left{
      display: flex;
      flex-wrap: wrap;
      grid-column-gap: 10px;
    }

    .table-page-search-wrapper-form-right{
      display: flex;
      flex-wrap: wrap;
      grid-column-gap: 10px;
    }

  }

  .table-box {
    width: 100%;
    height: auto;
    border: 1px solid #000;
    display: flex;

    .left {
      width: 25%;
      height: auto;
      box-sizing: border-box;
      border-right: 1px solid #000;

      ul{
        list-style: none;
        padding: 0;
        li{
          width: 30px;
          text-align: center;
          background: blue;
          margin: 10px;
        }
      }

    }

    .right {
      width: 75%;
      height: auto;
      display: flex;
      flex-direction: column;
      justify-content: space-between;

      .right-header {
        width: 100%;
        height: 65px;
        background: #d3dce6;
      }

      .gantt-box {
        width: 100%;
        overflow: auto;

        .gantt-list {
          width: auto;
          height: auto;
          display: flex;
          grid-column-gap: 30px;

          ul {
            list-style: none;
            padding: 0;
            margin: 0;
          }

          .gantt-item {
            width: auto;
            height: auto;
            border: 1px solid #000;
            margin: 10px;

            .gantt-header {
              width: auto;
              height: 40px;
              display: flex;

              .gantt-header-label {
                width: 60px;
                line-height: 40px;
                text-align: center;
              }

              .gantt-header-item {
                text-align: center;
                line-height: 10px;
                border-left: 1px solid #000;
                box-sizing: border-box;

                pre {
                  margin: 5px;
                  overflow: hidden;
                }

              }

            }

            .gantt-row-list {
              width: auto;
              height: 40px;
              display: flex;
              border-top: 1px solid #000;

              .gantt-row-list-label {
                width: 60px;
                text-align: center;
              }


              .gantt-row-content {
                width: auto;
                height: 40px;
                display: flex;
                position: relative;
                overflow: hidden;

                .gantt-row-time {
                  position: absolute;
                  width: auto;
                  height: 40px;
                  top: 0;
                  left: 0;
                  display: flex;
                  pointer-events: none;

                  .gantt-row-time-item {
                    border-left: 1px solid #000;
                    height: 40px;
                    pointer-events: none;
                    box-sizing: border-box;
                  }
                }

                .gantt-row-col{
                  width: auto;
                  height: 40px;
                  display: flex;

                  .gantt-row-col-item{
                  }

                  .gantt-row-col-item-border{
                    border-left: 1px solid #ddd;
                  }

                }

                .gantt-row-data-list{
                  width: 100%;
                  height: 40px;
                  pointer-events: none;
                  position: absolute;
                  box-sizing: border-box;

                  .gantt-row-data-item{
                    width: 0;
                    height: 40px;
                    position: absolute;
                    background: blue;
                    pointer-events: auto;
                    box-sizing: border-box;
                    z-index: 999;
                    top: -1px;

                    &.gantt-row-data-item-border{
                      border: 1px solid #000;
                    }

                    .gantt-row-data-item-col-list{
                      width: 100%;
                      height: 40px;
                      position: absolute;
                      top: 0;
                      left: 0;
                      display: flex;

                      .gantt-row-data-item-col-item{
                        height: 40px;
                        box-sizing: border-box;
                      }

                    }

                  }

                }

                .table-row-item-show{
                  width: 10px;
                  height: 40px;
                  position: absolute;
                  top: 0;
                  background: rgba(0,0,0,.3);
                  pointer-events: none;
                  z-index: 9999;
                }

              }

            }

          }

          .empty{
            width: 100%;
            height: 300px;
            display: flex;
            align-items: center;
            justify-content: center;
          }

        }

      }

      .right-footer{
        width: 100%;
        height: 40px;
        background: #d3dce6;
        display: flex;
        justify-content: space-between;
        padding: 0 10px;

        .simulator-select{
          width: auto;
          height: 100%;
          display: flex;
          align-items: center;
        }

        .simulator-load{
          width: auto;
          display: flex;
          justify-content: flex-end;
          align-items: center;
        }

      }



    }

  }

}


</style>