vue 3 甘特图

<template>
  <div ref="ganttChart" class="ganttChart" @mousewheel="mousewheel">
    <div class="ganttChart-left">
      <div class="ganttChart-left-title">
        {{ title }}
      </div>
      <div class="ganttChart-left-body">
        <div
          ref="listDom"
          class="ganttChart-task-list"
          :style="{transform:`translateY(-${scrollTop}px)`}"
        >
          <div
            class="ganttChart-task-item"
            @mouseout="hoverIndex=null"
            @mouseover="hoverIndex=i"
            :class="{'ganttChart-hover': hoverIndex===i}"
            v-for="(item,i) in list"
            :key="item[nameCode]">
            <span>{{item[nameCode]}}</span>
          </div>
        </div>
      </div>
    </div>
    <div class="ganttChart-right">
      <div class="ganttChart-right-title">
        <div
          ref="dateDom"
          class="ganttChart-date-list"
          :style="{transform:`translateX(-${scrollLeft}px)`}"
        >
          <div class="ganttChart-date-item"
            :style="{width: item.days.length*40 +'px'}"
            v-for="item in dateList" :key="item.formater">
            <div class="ganttChart-date-month">{{item.formater}}</div>
            <div class="ganttChart-date-day">
              <span
                class="ganttChart-cell"
                :class="{weekend: item.week===6 || item.week===0,nowDay: item.formater == nowDate}"
                v-for="item in item.days" :key="item.day">
                {{item.day}}
              </span>
            </div>
          </div>
        </div>
      </div>
      <div class="ganttChart-right-body">
        <div
          ref="bodyDom"
          class="ganttChart-taskLine-list"
          :style="{transform:`translate(-${scrollLeft}px,-${scrollTop}px)`}"
        >
          <div
            class="ganttChart-taskLine-item"
            @mouseout="hoverIndex=null"
            @mouseover="hoverIndex=i"
            :class="{'ganttChart-hover':hoverIndex===i}"
            :style="{width:allWhidth +'px'}"
            v-for="(item,i) in realList" :key="item.startPos+'-'+i">
            <!-- 预计进度start -->
            <div
              class="ganttChart-task-line"
              :endDate="'节点:'+item.target[endCode]"
              :class="item.status"
              :style="{
                left: item.startPos*40+'px',
                width: item.width*40+'px'
              }"
            >
              <div
                class="ganttChart-task-line-active"
                :class="item.status"
                :style="{width: item.activeWidth*40+'px'}"
              ></div>
              <div
                v-if="item.target[tooltipCode]"
                class="ganttChart-task-line-tooltip"
                v-html="item.target[tooltipCode]"
              ></div>
            </div>
            <!-- 预计进度end -->
            <template v-for="item in dateList">
              <span class="ganttChart-cell" v-for="cue in item.days" :key="cue.formater"></span>
            </template>
          </div>
        </div>
      </div>
    </div>
    <div @scroll="srollVertical" ref="srollVertical" class="ganttChart-verticalSrcoll ganttChart-scrollbar">
      <div :style="{height: allHeight+80+'px'}"></div>
    </div>
    <div @scroll="scrollHorizontal" ref="scrollHorizontal" class="ganttChart-horizontalSrcoll ganttChart-scrollbar">
      <div :style="{width: allWhidth+300+'px'}"></div>
    </div>
  </div>
</template>

<script setup name="ganttChart">
import { parseTime} from '@/utils/ruoyi'

const { proxy } = getCurrentInstance();

const props = defineProps({
  title: {
    default() {
      return '任务甘特图';
    }
  },
  list: {
    default: [
      {name: '任务1',start:'2023-10-06',end: '2023-10-13',finish:'2023-11-11'},
      {name: '任务2',start:'2023-10-16',end: '2023-10-27',finish:''},
      {name: '任务3',start:'2023-11-01',end: '2023-11-05',finish:''},
      {name: '任务4',start:'2023-11-06',end: '2023-11-13',finish:'2023-11-11'},
      {name: '任务5',start:'2023-11-16',end: '2023-11-27',finish:''},
      {name: '任务6',start:'2023-12-01',end: '2023-11-24',finish:''},
    ]
  },
  tooltipCode: {
    default: 'tooltip'
  },
  nameCode: {
    default: 'name'
  },
  startCode: {
    default: 'start'
  },
  endCode: {
    default: 'end'
  },
  finishCode: {
    default: 'finish'
  }
})

const nowDate = ref(parseTime(new Date(), `{y}-{m}-{d}`));
const hoverIndex = ref(null);
const scrollTop = ref(0);
const scrollLeft = ref(0);
const allWhidth = ref(0);
const allHeight = ref(0);
const dateList = ref([]);
const realList = ref([]);

watchEffect(() => props.list, () => {
  init();
})

onMounted(() =>{
  init();
})


function getMonthDay(str){
  const date = new Date(formatDate(str || new Date(),`{y}-{m}-{d}`));
  const year = date.getFullYear(),
      month = date.getMonth()+1,
      day = date.getDate();
  const maxDay = new Date(year, month, 0).getDate();
  let start = year+'-'+(month>9?month:'0'+month)+'-01'
  let end = year+'-'+(month>9?month:'0'+month)+'-'+maxDay;
  return [start,end]
}
function formatDate(time, pattern) {
  if (arguments.length === 0 || !time) {
    return null
  }
  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}'
  let date
  if (typeof time === 'object') {
    date = time
  } else {
    if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) {
      time = parseInt(time)
    } else if (typeof time === 'string') {
      time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm), '');
    }
    if ((typeof time === 'number') && (time.toString().length === 10)) {
      time = time * 1000
    }
    date = new Date(time)
  }
  const formatObj = {
    y: date.getFullYear(),
    m: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    i: date.getMinutes(),
    s: date.getSeconds(),
    a: date.getDay()
  }
  return format.replace(/{([ymdhisa])+}/g, (result, key) => {
    let value = formatObj[key]
    if (key === 'a') {
      return ['日', '一', '二', '三', '四', '五', '六'][value]
    }
    if (result.length > 0 && value < 10) {
      value = '0' + value
    }
    return value || 0
  })
}

function date2number(str){
  return new Date(str).getTime();
}
function getDestanceDay(start,end){
  let startTime = new Date(start).getTime();
  let endTime = new Date(end).getTime();
  return +((endTime - startTime)/(1000*60*60*24)).toFixed();
}
function getMinAndMaxDate(list){
  let { startCode, endCode, finishCode} = proxy;
  let [start,end] = getMonthDay();
  if (!list || !list.length) return {start,end};
  list.forEach(item=>{
    let itemStart = item[startCode];
    let itemEnd = item[endCode];
    let itemFinish = item[finishCode];
    if(!itemStart || !itemEnd)return;
    start = start || itemStart;
    end = end || itemStart;
    let dateArr = [itemStart,itemEnd]
    if(itemFinish && itemFinish !== '0000-00-00'){
      dateArr.push(itemFinish)
    }
    dateArr.forEach(val=>{
      let nowNumber = date2number(val);
      let startNumber = date2number(start),
          endNumber = date2number(end);
      start = startNumber > nowNumber?val:start;
      end = endNumber > nowNumber?end:val;
    })
  })
  end = getMonthDay(end)[1];
  return {start,end}
}
function getdateList(start,end){
  let echo = [];
  let startDate = new Date(start),
      endDate = new Date(end);
  let startYear = startDate.getFullYear(),
      startMonth = startDate.getMonth(),
      startDay = startDate.getDate();
  let endYear = endDate.getFullYear(),
      endMonth = endDate.getMonth(),
      endDay = endDate.getDate();
  for(let year = startYear; year<= endYear; year++){
    let _month = year === startYear?startMonth:0;
    let _maxMonth = year === endYear?endMonth:11;
    for(let month = _month; month <= _maxMonth; month++){
      const maxDate = new Date(year, month+1, 0).getDate();
      let _day = (year+'-'+month) === (startYear+'-'+startMonth)?startDay:1;
      let _maxDay = (year+'-'+month) === (endYear+'-'+endMonth)?endDay:maxDate;
      let item = {
        formater: year+'-'+((month+1)>9?(month+1):('0'+(month+1))),
        year,month,
        days:[]
      };
      for(let day = _day; day <= _maxDay; day++){
        let formater = year+'-'+((month+1)>9?(month+1):('0'+(month+1)))+'-'+(day>9?day:('0'+day));
        item.days.push({
          year,
          month,
          day,
          week: new Date(formater).getDay(),
          formater
        })
      }
      echo.push(item)
    }
  }
  return echo;
}

async function init(){
  let { start,end } = getMinAndMaxDate(props.list);
  scrollTop.value = 0;
  scrollLeft.value = 0;
  allWhidth.value = 0;
  allHeight.value = 0;
  dateList.value = [];
  realList.value = [];
  //二次赋值
  dateList.value = getdateList(start,end);
  allWhidth.value = (getDestanceDay(start,end)+1)*40;
  allHeight.value = props.list.length*40;
  props.list.forEach(item=>{
    let startDate = item[props.startCode],
        endDate = item[props.endCode],
        finishDate = item[props.finishCode];
    let status = 'none';
    if(finishDate){
      status = new Date(finishDate).getTime() - new Date(endDate).getTime() > 0?'delay':'normal'
    }
    realList.value.push({
      status,
      startPos: getDestanceDay(start,startDate),
      endPos: getDestanceDay(endDate,end),
      width: getDestanceDay(startDate,endDate)+1,
      activeWidth: finishDate?(getDestanceDay(startDate,finishDate)+1):0,
      target: item
    })
  })
  await proxy.$nextTick()
  scrollLeft.value = Math.max((getDestanceDay(start,nowDate)+1)*40 + 300 - proxy.$refs.ganttChart.clientWidth,0);
  proxy.$refs.scrollHorizontal.scrollLeft = scrollLeft;
}

function srollVertical(event){
  scrollTop.value = event.target.scrollTop;
}

function scrollHorizontal(event){
  scrollLeft.value = event.target.scrollLeft;
}

function mousewheel(event){
  let { wheelDeltaY } = event;
  scrollTop.value -= wheelDeltaY;
  if(scrollTop <= 0){
    scrollTop.value = 0;
  }else if(scrollTop + proxy.$refs.srollVertical.clientHeight >= allHeight+80){
    scrollTop.value = allHeight+80 - proxy.$refs.srollVertical.clientHeight;
  }
  proxy.$refs.srollVertical.scrollTop = scrollTop;
}
</script>

<style lang="scss">
$borderColor: #ebeef5;
$leftWidth: 300px;
.ganttChart{
  position: relative;
  display: flex;
  margin: 10px;
  height: 300px;
  border: 1px solid $borderColor;
  border-radius: 4px;
  font-size: 14px;
}
.ganttChart-left{
  width: $leftWidth;
  height: 100%;
  border-right: 1px solid $borderColor;
}
.ganttChart-left-title{
  display: flex;
  justify-content: center;
  align-items: center;
  height: 80px;
  width: 100%;
  border-bottom: 1px solid $borderColor;
  font-size: 16px;
  font-weight: 600;
}
.ganttChart-left-body{
  width: 100%;
  height: calc(100% - 80px);
  overflow: hidden;
}
.ganttChart-task-list{
  width: 100%;
}
.ganttChart-task-item{
  display: flex;
  height: 40px;
  padding: 0 15px;
  align-items: center;
  justify-content: center;
  border-bottom: 1px solid $borderColor;
  span{
    display: inline-block;
    line-height: 20px;
    height: 20px;
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
  }
}

.ganttChart-right{
  width: calc(100% - #{$leftWidth});
  height: 100%;
}
.ganttChart-right-title{
  width: 100%;
  height: 80px;
  overflow: hidden;
}
.ganttChart-date-list{
  width: max-content;
  overflow: hidden;
}
.ganttChart-date-item{
  float: left;
}
.ganttChart-date-month{
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 40px;
  border-right: 1px solid $borderColor;
  border-bottom: 1px solid $borderColor;
}
.ganttChart-right-body{
  width: 100%;
  height: calc(100% - 80px);
  overflow: hidden;
}
.ganttChart-taskLine-item,
.ganttChart-date-day{
  position: relative;
  display: flex;
  height: 40px;
  width: fit-content;
}
.ganttChart-cell{
  display: flex;
  justify-content: center;
  align-items: center;
  flex: none;
  width: 40px;
  height: 40px;
  border-right: 1px solid $borderColor;
  border-bottom: 1px solid $borderColor;
  &.weekend{
    background: #e2e2e2;
  }
  &.nowDay{
    background: #1376ce;
  }
}
.ganttChart-taskLine-item .ganttChart-cell{
  border-bottom:none;
}
.ganttChart-taskLine-item:last-child .ganttChart-cell{
  border-bottom: 1px solid $borderColor;
}
.ganttChart-taskLine-item
.ganttChart-task-line{
  position: absolute;
  top: 50%;
  left: 0;
  transform: translateY(-50%);
  height: 18px;
  border-radius: 10px;
  background: #c3c3c3;
  &.delay::before{
    position: absolute;
    top: -4px;
    bottom: -4px;
    right: 0;
    width: 4px;
    content: '';
    background: #1376ce;
    z-index: 1;
  }
  &.delay::after{
    position: absolute;
    bottom: -15px;
    right: 5px;
    font-size: 12px;
    content: attr(endDate);
    color: #1376ce;
    width: 200px;
    text-align: right;
    z-index: 1;
  }
}
.ganttChart-task-line-active{
  position: absolute;
  top: 0;
  left: 0;
  height: 18px;
  border-radius: 10px;
  background: #65eb65;
  &.delay{
    background: red;
  }
}
.ganttChart-task-line-tooltip{
  position: absolute;
  top: 0;
  right: 5px;
  bottom: 0;
  display: flex;
  justify-content: flex-end;
  align-items: center;
}
.ganttChart-verticalSrcoll{
  position: absolute;
  top: 0;
  right: -10px;
  bottom: 0;
  width: 10px;
  overflow: hidden;
  overflow-y: auto;
  div{
    width: 0px;
  }
}
.ganttChart-horizontalSrcoll{
  position: absolute;
  bottom: -10px;
  right: 0;
  left: 0;
  height: 10px;
  overflow: hidden;
  overflow-x: auto;
  div{
    height: 0px;
  }
}
.ganttChart-hover{
  background: rgba(0,0,0,0.05);
}
.ganttChart-scrollbar{
	&::-webkit-scrollbar {
		display: block;
		width: 8px; /*高宽分别对应横竖滚动条的尺寸*/
		height: 8px;
	}
	&::-webkit-scrollbar-thumb {
		/*滚动条里面小方块*/
		border-radius: 10px;
		background-color: #617ce9bd;
		background-image: -webkit-linear-gradient(
		45deg,
		rgba(255, 255, 255, 0.2) 25%,
		transparent 25%,
		transparent 50%,
		rgba(255, 255, 255, 0.2) 50%,
		rgba(255, 255, 255, 0.2) 75%,
		transparent 75%,
		transparent
		);
	}
	&::-webkit-scrollbar-track {
		/*滚动条里面轨道*/
		box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
		background: #ededed;
		border-radius: 10px;
	}
}
</style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值