$("#gantttable").on('mouseover mousemove','.progress',function(e,b,c){
console.log(e);
let parentleft=e.target.style.transform.replace(/\D/g,'');
let left=Number(parentleft)+e.offsetX+40;
let top=e.target.offsetTop+e.offsetY+40;
$(e.target).prev().css({
"display":"block",
"position":"absolute",
"background":"red",
"left":left+'px',
"top":top+'px',
"z-index":'999',
})
})
$("#gantttable").on('mouseout','.progress',function(e,b,c){
$(e.target).prev().css({
"display":"none",
})
})
<template>
<div class="box" ref="box">
<div class="placeholder" v-show="!ganttTitleDate.length">暂无数据</div>
<div class="top" v-show="ganttTitleDate.length">
<div v-for="item in ganttTitleDate" :key="item.id">{{ item.name }}</div>
</div>
<div class="main" v-show="ganttTitleDate.length">
<div class="row" v-for="(item, index) in calcList" :key="item.id">
<div class="posx" :style="{ width: width + 'px' }">
<!-- 蓝色 -->
<div v-show="false">{{ `${item.planTimeScope}` }}</div>
<div
class="progress"
slot="reference"
:style="handlecalc(item, ['planStartTime', 'planEndTime'])"
@mouseenter="handlerMouseenter($event, '1popover' + index)"
></div>
<div v-show="false">
<div class="vc-toolbox" v-if="item.status === '2'">
<p style="text-align: center">{{ item.actTimeScope || "" }}</p>
<div v-if="item.weekbackTime">
<p style="text-align: center">最新周反馈:</p>
<p>周时间段: {{ item.weekbackTime }}</p>
<p>周反馈内容:</p>
<p>{{ item.weekbackContent }}</p>
</div>
<p style="text-align: center" v-else>最新周反馈:无</p>
</div>
<div v-else>{{ `${item.actTimeScope}` }}</div>
</div>
<div
class="progress"
slot="reference"
:class="getStyleclass(item.status, index, 'class')"
:style="handlecalc(item, ['actStartTime', 'actEndTime'])"
@mouseenter="handlerMouseenter($event, '2popover' + index)"
></div>
</div>
<!-- <div class='fixed1'>111</div>
<div class='fixed2'>22</div>-->
<div v-for="item in ganttTitleDate" :key="item.id" class="cell"></div>
</div>
</div>
<div class="point_line" v-if="showpointline" :style="{ left: showpointlineleft + 'px' }"></div>
</div>
</template>
<script>
import dayjs from "dayjs"; // 导入日期js
const uuidv4 = () =>
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
export default {
props: {
type: {
default: "月",
type: String,
},
isfullscreen: {
default: false,
type: Boolean,
},
listshow: {
default: () => [],
type: Array,
},
},
data() {
return {
startTime: "2023-01-01",
endTime: "2026-01-01",
width: "",
ganttTitleDate: [],
list: [],
showpointline: false,
showpointlineleft: 0,
};
},
computed: {
//显示的数据 listshow
calcList() {
if (Array.isArray(this.listshow) && Array.isArray(this.list)) {
return this.list.filter((item, index) => this.listshow.includes(index));
}
return [];
},
// 单元格宽度
cellWidth() {
if (this.width && this.ganttTitleDate.length) {
return parseInt(this.width / this.ganttTitleDate.length);
}
return 0;
},
},
directives: {
tooltip(el, binding) {
const config = binding.value || {};
const classname = `${config.type || ""}_tootip`;
// 鼠标移入时,将浮沉元素插入到body中
el.onmouseenter = function (e) {
// 创建浮层元素并设置样式
const vcTooltipDom = document.createElement("div");
vcTooltipDom.style.cssText = `
position:absolute;
font-size:14px;
z-index:1000
`;
// 设置id方便寻找
vcTooltipDom.setAttribute("id", "vc-tooltip");
vcTooltipDom.setAttribute("class", classname);
// 将浮层插入到body中
document.body.appendChild(vcTooltipDom);
// 浮层中的文字 通过属性值传递动态的显示文案
document.getElementById("vc-tooltip").innerHTML = config.text;
};
// 鼠标移动时,动态修改浮沉的位置属性
el.onmousemove = function (e) {
const vcTooltipDom = document.getElementById("vc-tooltip");
vcTooltipDom.style.top = `${e.clientY + 15}px`;
vcTooltipDom.style.left = `${e.clientX + 15}px`;
};
// 鼠标移出时将浮层元素销毁
el.onmouseleave = function () {
// 找到浮层元素并移出
const vcTooltipDom = document.getElementById("vc-tooltip");
vcTooltipDom && document.body.removeChild(vcTooltipDom);
};
},
},
watch: {
type(val) {
this.typechange();
},
},
mounted() {
this.init(0);
// 监听 window.onresize
// window.addEventListener("resize", this.resize, false);
},
beforeDestroy() {
window.removeEventListener("resize", this.resize);
},
methods: {
typechange() {
this.ganttTitleDate = this.generateHeader();
this.handlelist();
this.resize();
this.showpointline = false;
document.getElementById("gantttable").scrollLeft = 0;
},
resize() {
this.width = 0;
this.$nextTick(() => {
this.width = this.$refs.box.scrollWidth;
});
},
ganttfix(e) {
const index = e.number;
const fixed = this.list[index].planStartTimepos;
const width = fixed * this.cellWidth;
this.$refs.box.scrollLeft = width;
},
getStyleclass(status, index, type) {
let result = "info";
switch (status) {
case "0":
result = "info";
break;
case "1":
result = "primary";
break;
case "2":
result = "danger";
break;
case "3":
result = "info";
break;
case "4":
result = "success";
break;
case "5":
result = "warning";
break;
default:
result = "emptyState";
break;
}
if (index === 0 || index === this.list.length - 1) {
return "primary";
}
if (type === "class") {
return {
[result]: true,
};
}
return result;
},
handlerMouseenter(event, ref) {
// 鼠标进入触发元素
const popover = this.$refs[ref][0];
const timer = setTimeout(() => {
const { clientX } = event;
const bodyWidth = document.body.clientWidth;
// 找到气泡元素
const { popperElm } = popover;
/**
* 鼠标位置+气泡弹框宽度是否小于body的宽度
* 1. 是,设置鼠标位置为气泡弹框横向位移
* 2. 否,设置body宽度-气泡弹框宽度为气泡弹框横向位移
*/
const disX =
clientX + popperElm.offsetWidth < bodyWidth
? clientX
: bodyWidth - popperElm.offsetWidth;
popover.popperElm.style.left = `${disX}px`;
clearTimeout(timer);
}, 5);
},
init(top) {
this.width = 0;
this.ganttTitleDate = this.generateHeader();
this.$nextTick(() => {
this.width = this.$refs.box.scrollWidth;
});
//处理列表 得到所有单元格列+百分比
this.handlelist();
// this.$nextTick(() => {
// this.$refs.box.scrollTop = top;
// this.$refs.box.scrollLeft = 0;
// });
},
// 生成表头
generateHeader() {
if (this.startTime === "" && this.endTime === "") {
return [];
}
// 分解开始和结束日期
const start_date_spilt = dayjs(this.startTime)
.format("YYYY-M-D")
.split("-");
const end_date_spilt = dayjs(this.endTime).format("YYYY-M-D").split("-");
const start_year = Number(start_date_spilt[0]);
const start_mouth = Number(start_date_spilt[1]);
const end_year = Number(end_date_spilt[0]);
const end_mouth = Number(end_date_spilt[1]);
let database = [];
if (this.type === "月") {
database = this.yearAndMouthTitleDate(
start_year,
start_mouth,
end_year,
end_mouth
);
database = this.databasechange(database);
} else if (this.type === "年") {
database = this.yearTitleDate(start_year, end_year);
} else if (this.type === "季度") {
database = this.jdTitleDate(
start_year,
start_mouth,
end_year,
end_mouth
);
}
console.log(database);
this.$nextTick(() => {
this.width = this.$refs.box.scrollWidth; //总宽度
});
return database;
},
//跳转到今天 等
toTimePos(toTime) {
//位置+ 容器一半宽度
if (this.ganttTitleDate.length) {
if (
new Date(toTime).valueOf() < new Date(this.startTime).valueOf() ||
new Date(toTime).valueOf() > new Date(this.endTime).valueOf()
) {
return this.$message.warning("今天不在当前范围");
}
let left = document.getElementById("gantttable").offsetWidth / 2;
const downindex = this.ganttTitleDate.findIndex((item) => {
return (
new Date(item.endTime).valueOf() >= new Date(toTime).valueOf() &&
new Date(item.startTime).valueOf() <= new Date(toTime).valueOf()
);
});
let all =
new Date(this.ganttTitleDate[downindex].endTime).valueOf() -
new Date(this.ganttTitleDate[downindex].startTime).valueOf();
let thisitem =
new Date(toTime).valueOf() -
new Date(this.ganttTitleDate[downindex].startTime).valueOf();
const downratio = (thisitem / all).toFixed(4);
let width = this.cellWidth * (downindex + Number(downratio));
this.showpointline = true;
this.showpointlineleft = width;
document.getElementById("gantttable").scrollLeft = width - left;
}
},
// 处理list 获取区间width 和偏移量
handlelist() {
this.list = this.list.map((item) => this.handlelistitemcalc(item));
},
handlelistitemcalcnext(item = {}, fieldmap = ["", ""]) {
let uppos = []; // 值1为 单元格索引 值2为在单元格的百分比
let downpos = [];
const startTime = item[fieldmap[0]];
const endTime = item[fieldmap[1]];
const celllenindex = this.ganttTitleDate.length - 1;
// 开始时间不在区间
if (new Date(startTime).valueOf() < new Date(this.startTime).valueOf()) {
uppos = [0, 0];
} else if (
new Date(startTime).valueOf() > new Date(this.endTime).valueOf()
) {
uppos = [celllenindex, 100];
} else {
if (this.type === "月") {
const upindex = this.ganttTitleDate.findIndex(
(item) => item.full_date === dayjs(startTime).format("YYYY-M")
);
const upgetMonthDay = this.getMonthDay(
dayjs(startTime).format("YYYY-M")
);
const upday = dayjs(startTime).format("D");
let upratio = "";
if (upday == 1) {
upratio = 0;
} else if (upday == upgetMonthDay) {
upratio = 1;
} else {
upratio = parseFloat(upday / upgetMonthDay).toFixed(3);
}
uppos = [upindex, upratio];
} else if (this.type === "年") {
const upindex = this.ganttTitleDate.findIndex(
(item) => item.full_date === dayjs(startTime).format("YYYY")
);
let all =
new Date(this.ganttTitleDate[upindex].endTime).valueOf() -
new Date(this.ganttTitleDate[upindex].startTime).valueOf();
let thisitem =
new Date(startTime).valueOf() -
new Date(this.ganttTitleDate[upindex].startTime).valueOf();
const upratio = (thisitem / all).toFixed(5);
uppos = [upindex, upratio];
} else if (this.type === "季度") {
const upindex = this.ganttTitleDate.findIndex((item) => {
return (
new Date(item.endTime).valueOf() >=
new Date(startTime).valueOf() &&
new Date(item.startTime).valueOf() <=
new Date(startTime).valueOf()
);
});
let all =
new Date(this.ganttTitleDate[upindex].endTime).valueOf() -
new Date(this.ganttTitleDate[upindex].startTime).valueOf();
let thisitem =
new Date(startTime).valueOf() -
new Date(this.ganttTitleDate[upindex].startTime).valueOf();
const upratio = (thisitem / all).toFixed(4);
uppos = [upindex, upratio];
}
}
// 结束时间不在区间
if (new Date(endTime).valueOf() < new Date(this.startTime).valueOf()) {
downpos = [0, 0];
} else if (
new Date(endTime).valueOf() > new Date(this.endTime).valueOf()
) {
downpos = [celllenindex, 100];
} else {
if (this.type === "月") {
const downindex = this.ganttTitleDate.findIndex(
(item) => item.full_date === dayjs(endTime).format("YYYY-M")
);
const downgetMonthDay = this.getMonthDay(
dayjs(endTime).format("YYYY-M")
);
const downday = dayjs(endTime).format("D");
let downratio = "";
if (downday == 1) {
downratio = 0;
} else if (downday == downgetMonthDay) {
downratio = 1;
} else {
downratio = parseFloat(downday / downgetMonthDay).toFixed(3);
}
downpos = [downindex, downratio];
} else if (this.type === "年") {
const downindex = this.ganttTitleDate.findIndex(
(item) => item.full_date === dayjs(endTime).format("YYYY")
);
let all =
new Date(this.ganttTitleDate[downindex].endTime).valueOf() -
new Date(this.ganttTitleDate[downindex].startTime).valueOf();
let thisitem =
new Date(endTime).valueOf() -
new Date(this.ganttTitleDate[downindex].startTime).valueOf();
const downratio = (thisitem / all).toFixed(5);
downpos = [downindex, downratio];
} else if (this.type === "季度") {
const downindex = this.ganttTitleDate.findIndex((item) => {
return (
new Date(item.endTime).valueOf() >= new Date(endTime).valueOf() &&
new Date(item.startTime).valueOf() <= new Date(endTime).valueOf()
);
});
let all =
new Date(this.ganttTitleDate[downindex].endTime).valueOf() -
new Date(this.ganttTitleDate[downindex].startTime).valueOf();
let thisitem =
new Date(endTime).valueOf() -
new Date(this.ganttTitleDate[downindex].startTime).valueOf();
const downratio = (thisitem / all).toFixed(4);
//--
downpos = [downindex, downratio];
}
}
return {
[`${fieldmap[0]}pos`]: uppos[0] + Number(uppos[1]),
[`${fieldmap[1]}pos`]: downpos[0] + Number(downpos[1]),
};
},
handlelistitemcalc(item) {
const { actStartTimepos, actEndTimepos } = this.handlelistitemcalcnext(
item,
["actStartTime", "actEndTime"]
);
const { planStartTimepos, planEndTimepos } = this.handlelistitemcalcnext(
item,
["planStartTime", "planEndTime"]
);
return {
...item,
// 计划
planStartTimepos,
planEndTimepos,
// 实际
actStartTimepos,
actEndTimepos,
};
},
// 计算 开头 结尾所在的单元格 //计算 单元格内的具体百分比 //已经是否超出当前区间
handlecalc(item, fieldmap = ["", ""]) {
if (
item[fieldmap[0]] &&
item[fieldmap[1]] &&
new Date(item[fieldmap[0]]).valueOf() <=
new Date(item[fieldmap[1]]).valueOf()
) {
const start = item[`${fieldmap[0]}pos`];
const end = item[`${fieldmap[1]}pos`];
const startpx = start * this.cellWidth;
let progresswidth = end * this.cellWidth - startpx;
if (
new Date(item[fieldmap[0]]).valueOf() ===
new Date(item[fieldmap[1]]).valueOf() &&
new Date(item[fieldmap[0]]).valueOf() >=
new Date(this.startTime).valueOf() &&
new Date(item[fieldmap[0]]).valueOf() <=
new Date(this.endTime).valueOf()
) {
progresswidth = "3";
}
return {
width: `${progresswidth}px`,
transform: `translateX(${startpx}px)`,
};
}
return {
width: 0,
display: "none",
};
},
// 获取一个月有多少天
getMonthDay(time) {
const date = new Date(time);
date.setMonth(date.getMonth() + 1); // 先设置为下个月
date.setDate(0); // 再置0,变成当前月最后一天
return date.getDate();
},
databasechange(data) {
const array = [];
if (Array.isArray(data) && data.length) {
data.forEach((item) => {
if (Array.isArray(item.children) && data.length) {
item.children.forEach((item2) => {
let monthdays = new Date(
item2.full_date.split("-")[0],
item2.full_date.split("-")[1],
0
).getDate();
array.push({
...item2,
startTime: item2.full_date + "-1",
endTime: item2.full_date + "-" + monthdays,
name: item.name + item2.name,
});
});
}
});
}
return array;
},
/**
* 是否闰年函数
* year: Number 当前年份
*/
isLeap(year) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
},
/**
* 生成月份函数
* year: Number 当前年份
* start_num: Number 开始月分
* end_num:Number 结束月份
* isLeap: Boolean 是否闰年
* insert_days: Boolean 是否需要插入 日
* week: 是否以周的间隔
*/
generationMonths(
year,
start_num = 1,
end_num = 13,
isLeap = false,
insert_days = true,
week = false
) {
const months = [];
if (insert_days) {
// 无需 日 的模式
for (let i = start_num; i < end_num; i++) {
// 需要 日 的模式
const days = this.generationDays(year, i, isLeap, week);
months.push({
name: `${i}月`,
date: i,
full_date: `${year}-${i}`,
children: days,
id: uuidv4(),
});
}
return months;
}
for (let i = start_num; i < end_num; i++) {
// 需要 日 的模式
months.push({
name: `${i}月`,
date: i,
full_date: `${year}-${i}`,
id: uuidv4(),
});
}
return months;
},
yearTitleDate(start_year, end_year) {
if (end_year && start_year && end_year >= start_year) {
let res = [];
while (start_year <= end_year) {
res.push({
name: start_year + "年",
startTime: start_year + "-1-1", //区间开始时间
endTime: start_year + "-12-31", //结束开始时间
full_date: `${start_year}`,
id: uuidv4(),
});
start_year++;
}
return res;
}
return [];
},
jdTitleDate(start_year, start_mouth, end_year, end_mouth) {
if (end_year && start_year && end_year >= start_year) {
let timespace = [
{ name: "一", start: "01-01", end: "03-31" },
{ name: "二", start: "04-01", end: "06-30" },
{ name: "三", start: "07-01", end: "09-30" },
{ name: "四", start: "10-01", end: "12-31" },
];
function getQuarterByMonth(month) {
if (month >= 1 && month <= 3) {
return 0;
} else if (month >= 4 && month <= 6) {
return 1;
} else if (month >= 7 && month <= 9) {
return 2;
} else {
return 3;
}
}
let jd1 = getQuarterByMonth(start_mouth);
let jd2 = getQuarterByMonth(end_mouth);
console.log(jd1, jd2);
let res = [];
while (Number(start_year + "." + jd1) <= Number(end_year + "." + jd2)) {
res.push({
name: start_year + "年第" + timespace[jd1].name + "季度",
startTime: start_year + "-" + timespace[jd1].start, //区间开始时间
endTime: start_year + "-" + timespace[jd1].end, //结束开始时间
id: uuidv4(),
});
jd1++;
if (jd1 > 3) {
jd1 = 0;
start_year++;
}
console.log(111);
}
return res;
} else {
return [];
}
},
yearAndMouthTitleDate(start_year, start_mouth, end_year, end_mouth) {
// 日期数据盒子
const dates = [
{
name: `${start_year}年`,
date: start_year,
id: uuidv4(),
children: [],
},
];
// 处理年份
const year_diff = end_year - start_year;
// 年间隔小于一年
if (year_diff === 0) {
const isLeap = this.isLeap(start_year); // 是否闰年
const mouths = this.generationMonths(
start_year,
start_mouth,
end_mouth + 1,
isLeap,
false
); // 处理月份
dates[0].children = mouths;
return dates;
}
// 处理开始月份
const startIsLeap = this.isLeap(start_year);
const start_mouths = this.generationMonths(
start_year,
start_mouth,
13,
startIsLeap,
false
);
// 处理结束月份
const endIsLeap = this.isLeap(end_year);
const end_mouths = this.generationMonths(
end_year,
1,
end_mouth + 1,
endIsLeap,
false
);
// 年间隔等于一年
if (year_diff === 1) {
dates[0].children = start_mouths;
dates.push({
name: `${end_year}年`,
date: end_year,
children: end_mouths,
id: uuidv4(),
});
return dates;
}
// 年间隔大于1年
if (year_diff > 1) {
dates[0].children = start_mouths;
for (let i = 1; i < year_diff; i++) {
const item_year = start_year + i;
const isLeap = this.isLeap(item_year);
const month_and_day = this.generationMonths(
item_year,
1,
13,
isLeap,
false
);
dates.push({
name: `${item_year}年`,
date: item_year,
id: uuidv4(),
children: month_and_day,
});
}
dates.push({
name: `${end_year}年`,
date: end_year,
children: end_mouths,
id: uuidv4(),
});
return dates;
}
},
},
};
</script>
<style scoped lang="scss">
.box {
width: max-content;
position: relative;
min-width: 100%;
}
.placeholder {
display: flex;
height: 100%;
width: 100%;
align-items: center;
justify-content: center;
border-right: 1px solid #ccc;
}
.top {
display: flex;
position: sticky;
top: 0;
z-index: 99;
> div + div {
border-left-width: 0;
}
}
.top > div {
min-width: 150px;
border: 1px solid #ccc;
border-top-width: 0;
flex: 1;
text-align: center;
font-size: 14px;
font-weight: 700;
// padding: 10px 0;
background: #fff;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.row {
display: flex;
position: relative;
> div.cell + div.cell {
border-left-width: 0;
}
}
.row > div.cell {
min-width: 150px;
border: 1px solid #ccc;
flex: 1;
height: 60px;
border-top-width: 0;
}
.posx {
position: absolute;
top: 0;
left: 0;
z-index: 10;
height: 100%;
width: 100%;
// overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
}
.progress {
height: 16px;
background: #409eff;
}
.progress.success {
background: #cff1c0;
margin-top: 5px;
}
.progress.danger {
background: #fcd9d9;
margin-top: 5px;
}
.progress.warning {
background: #f9ecd9;
margin-top: 5px;
}
.progress.primary {
background: #c1e0ff;
margin-top: 5px;
}
</style>
<style lang="scss">
.vc-toolbox {
max-width: 600px;
p {
word-break: break-all;
}
}
.el-popover.el-popper.success {
color: #67c23a;
background: #f0f9eb;
border-color: #c2e7b0;
}
.el-popover.el-popper.primary {
color: #409eff;
background: #ecf5ff;
border-color: #b3d8ff;
}
.el-popover.el-popper.danger {
color: #f56c6c;
background: #fef0f0;
border-color: #fbc4c4;
}
.el-popover.el-popper.emptyState {
color: #fff;
background: #7d7d7d;
border-color: #7d7d7d;
}
.point_line {
background: #4881fd;
width: 2px;
height: 100%;
position: absolute;
top: 0;
left: 0px;
z-index: 20;
}
.fixed1 {
width: 50px;
height: 20px;
background: #409eff;
position: absolute;
left: 10px;
}
.fixed2 {
width: 50px;
height: 20px;
background: #409eff;
position: absolute;
left: 10px;
top: 20px;
}
</style>