<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>
vue 3 甘特图
于 2025-03-19 16:39:16 首次发布