甘特图
需求
展示一天时间中的所有会议,甘特图每条高度固定默认初始化展示4条,超出4条则出现滚动条滑动(包含鼠标上移出现基本信息/左键出现扩展菜单)以及其他UI样式
效果图
html主要代码
<div>
<!-- 甘特图选择器 -->
<div
id="myChart"
ref="chart"
class="meeting-time-echarts"
/>
<!--右键弹出菜单-->
<div
id="RightMenu"
class="right-menu-echarts"
>
<template v-if="mousedownState === true">
<div
:class="clickState === true ? 'right-menu-item' : 'right-menu-item-not'"
@click="clickState === true ? enterMeeting(meetingsTypes.customMeeting) : ''"
>
<span>进入会议</span>
</div>
</template>
<div
class="right-menu-item"
@click="toViewDetail"
>
<span>会议详情</span>
</div>
<!-- 允许请假、以及有请假的ID、会议状态为1->待开始的情况下可以请假 -->
<div
v-if="leaveMeetingStatus.leave === true && leaveMeetingStatus.leaveId === null && leaveMeetingStatus.meetingStatus === 1"
class="right-menu-item"
@click="toViewLeave"
>
<span>请假</span>
</div>
<!-- 有请假的ID情况下展示 -->
<div
v-if="leaveMeetingStatus.leaveId !== null"
class="right-menu-item"
@click="toViewLeaveDateil"
>
<span>请假详情</span>
</div>
<div
v-if="testState === true"
class="right-menu-item"
@click="enterMeeting(meetingsTypes.testMeeting)"
>
<span>会议室测试</span>
</div>
</div>
主要js
//组件传值
props: {
timeData: {
type: Array,
default: () => [],
},
},
data() {
return {
that: this,
chosenTime: "",
timelineListPop: [],
timelineListPush: [],
monthData: [],
monthTime: "",
nowday: "",
daySchedule: [],
value: new Date(),
editData: {},
meetingId: null,
calendarData: [],
mousedownState: true, // 进入会议按钮是否显示
clickState: true, // 是否是可点击状态,true:可以点击
testState: false, // 是否有【会议测试】按钮,true:可以测试
meetingRoomPath: process.env.VUE_APP_REQUEST_DOMAIN_PREFIX_MEETING_ROOM, // 进入会议室项目
meetingsTypes: meetingsTypes
};
},
activated() {
let nowDate = new Date();
this.nowday = nowDate.getDate(); //获取当日
if (this.timeData.length > 0) {
this.selectTimeline(this.timeData);
}else{
this.drawLine([],[]) //没有指定的日期数据时默认绘制空的甘特图
}
},
mounted() {
//点击其他地方关闭点击菜单
document.addEventListener("click", (e) => {
let menu = document.getElementById("myChart");
if (menu && !menu.contains(e.target)) {
//这句是说如果我们点击到了id为menu以外的区域
document.getElementById("RightMenu").style.display = "none";
}
});
//监听页签变化时刷新甘特图数据--会议室结束会议返回甘特图时会刷新数据
let _this = this;
document.addEventListener('visibilitychange',function(){
if(document.visibilityState){
_this.onEChart(_this.chosenTime); //获取甘特图数据
}
});
},
methods:{
//根据选择的日期来更新数据
selectTimeline(val) {
this.init();
this.chosenTime = val[0];
this.onEChart(this.chosenTime); //获取甘特图数据
this.monthTime = val[0].getMonth() + 1;
},
//组装甘特图所需要的数据类型
onEChart(data) {
let that = this;
let arr = [];
let ii = 0;
let selectTime = formatTime(data, "{y}-{m}-{d} {h}:{i}:{s}");
meetingApi.selectDate({ selectDate: selectTime }).then((response) => {
const res = response.data;
if (res.code === 200) {
for (let item of res.data.meetingList) {
if (item.meetingStatus === 1) {
//待开始
ii++; //让数据有多少length就展示多少行
let aa = {
name: ii,
data: item, //为了在鼠标经过显示数据时可以获取到完整的数据
//这里的ii是value值,就是为了展示多行不重叠,planStartTime/planEndTime预计开始/结束时间,最后时间realStartedTime(会加深颜色显示)
value: [
ii,
formatTime(item.planStartTime, "{y}-{m}-{d} {h}:{i}:{s}"),
formatTime(item.planEndTime, "{y}-{m}-{d} {h}:{i}:{s}"),
formatTime(item.realStartedTime, "{y}-{m}-{d} {h}:{i}:{s}"),
],
};
arr.push(aa);
} else if (item.meetingStatus === 2) {
//已开始
ii++;
let aa = {
name: ii,
data: item,
value: [
ii,
formatTime(item.planStartTime, "{y}-{m}-{d} {h}:{i}:{s}"),
formatTime(item.planEndTime, "{y}-{m}-{d} {h}:{i}:{s}"),
formatTime(item.realStartedTime, "{y}-{m}-{d} {h}:{i}:{s}"),
],
};
arr.push(aa);
} else if (item.meetingStatus === 3) {
//已结束
ii++;
let aa = {
name: ii,
data: item,
value: [
ii,
formatTime(item.planStartTime, "{y}-{m}-{d} {h}:{i}:{s}"),
formatTime(item.planEndTime, "{y}-{m}-{d} {h}:{i}:{s}"),
formatTime(item.realStartedTime, "{y}-{m}-{d} {h}:{i}:{s}"),
],
};
arr.push(aa);
} else if (item.meetingStatus === 5) {
//已逾期
ii++;
let aa = {
name: ii,
data: item,
value: [
ii,
formatTime(item.planStartTime, "{y}-{m}-{d} {h}:{i}:{s}"),
formatTime(item.planEndTime, "{y}-{m}-{d} {h}:{i}:{s}"),
formatTime(item.realStartedTime, "{y}-{m}-{d} {h}:{i}:{s}"),
],
};
arr.push(aa);
}
}
that.daySchedule = arr;
let markAreaTimeList = that.markAreaTimeData(arr); //计算时间轴重叠的区域
that.drawLine(arr,markAreaTimeList); //得到数据进行绘制
}
});
},
//计算时间轴重叠的区域
markAreaTimeData(drawData) {
/*
* 二维数组,里面的item代表一段标域,item[0]代表标域的起始位置,item[1]代表标域的结束位置,x表示横坐标,如果标域要求竖向,则使用y
* [[{xAxis: "",},{xAxis: "",}],[]]或者[[{x: "",},{x: "",}],[]]
* xAxis为根据x轴时间区域 ,x、y为百分比位置
* */
let timeData = [];
drawData.forEach((i) =>{
const n = i.data;
drawData.forEach((time) => {
const t = time.data;
let data = [
{
xAxis: "",
}, {
xAxis: "",
}
]
if(n.id !== t.id) { //不需要比对同一会议
//取开始时间与结束时间,如果开始时间满足重合->再计算结束时间
if (formatTime(n.planStartTime, "{h}:{i}") < formatTime(t.planStartTime, "{h}:{i}") && formatTime(t.planStartTime, "{h}:{i}") < formatTime(n.planEndTime, "{h}:{i}")) {
//如果前一条甘特图开始时间比后一条时间小并且后一条开始时间小于第一条结束时间,那么重合开始时间取后一条的开始时间
data[0].xAxis = formatTime(t.planStartTime, "{y}-{m}-{d} {h}:{i}:{s}");
} else if (formatTime(n.planStartTime, "{h}:{i}") > formatTime(t.planStartTime, "{h}:{i}") && formatTime(t.planStartTime, "{h}:{i}") > formatTime(n.planEndTime, "{h}:{i}")) {
//如果前一条甘特图开始时间比后一条时间大并且后一条开始时间大于前一条结束时间,那么重合开始时间取前一条的开始时间
data[0].xAxis = formatTime(i.planStartTime, "{y}-{m}-{d} {h}:{i}:{s}");
} else if (formatTime(n.planStartTime, "{h}:{i}") === formatTime(t.planStartTime, "{h}:{i}")) {
//如果开始时间一致,那么取任意一条时间,因为必会重合
data[0].xAxis = formatTime(n.planStartTime, "{y}-{m}-{d} {h}:{i}:{s}");
// data[1] = formatTime(i.planEndTime,"{h}:{i}") > formatTime(time.planEndTime,"{h}:{i}") ? formatTime(time.planEndTime, "{y}-{m}-{d} 00:00:00") : formatTime(i.planEndTime, "{y}-{m}-{d} 00:00:00");
}
if (data[0].xAxis !== "") { //如果开始时间满足重合->再计算结束时间
//如果前一条结束时间大于后一条结束时间,那么取后一条结束时间,否则取前一条结束时间
data[1].xAxis = formatTime(n.planEndTime, "{h}:{i}") > formatTime(t.planEndTime, "{h}:{i}") ?
formatTime(t.planEndTime, "{y}-{m}-{d} {h}:{i}:{s}") :
formatTime(n.planEndTime, "{y}-{m}-{d} {h}:{i}:{s}")
timeData.push(data)
}
}
})
})
return timeData;
},
//甘特图绘制以及配置
drawLine(data,markAreaTimeList) {
const that = this;
let chart = document.getElementById("myChart");
if(chart) {
//先销毁实例
that.$echarts.init(document.getElementById("myChart")).dispose();
}
// 基于准备好的dom,初始化echarts实例
let barDv = that.$refs.chart; //或者document.getElementById('myChart')
let drawData = data;
let nowDateStr = that.chosenTime ? that.chosenTime : that.nowday; //如果传参日期为空则默认当天时间初始化
let planStartDate; let planEndDate;let pillarsWidth;
//在渲染时,data中的每个数据项都会调用这个方法
function renderItem(params, api) {
//params为data中的数据项的信息对象 api中是一些开发者可调用的方法集合,可以对data中的数据项进行操作
let categoryIndex = api.value(0); //取出data中数据项的第一个维度的值
//===============会议进度条
//计划开始日期(在屏幕上的像素值)
planStartDate = api.coord([api.value(1), categoryIndex]); //将数据项中的数值对应的坐标系上的点,转换为屏幕上的像素值
//坐标系上的点:是数据项映射到坐标系的x轴和y轴后,对应的位置
//屏幕上的像素值:是坐标系上的点,在屏幕上的位置
//计划结束日期(在屏幕上的像素值)
planEndDate = api.coord([api.value(2), categoryIndex]);
//由于data.value中维度1和维度2的数据会被映射到x轴,而x轴的type为time,即时间轴,
//所以api.value(1)和api.value(2)获取到的值是将日期转换后的毫秒值
//设置图形的高度 TODO 限制固定高度,目前弃用
// let height = api.size([0, 1])[1] * 0.4; //获得Y轴上数值范围为1的一段所对应的像素长度;这是官方文档的注释,对于api.size()方法,目前我还不是很理解;先做个标记??? 以后再说
pillarsWidth = planEndDate[0] - planStartDate[0];
//使用graphic图形元素组件,绘制矩形
//clipRectByRect方法,在绘制矩形时,如果矩形大小超出了当前坐标系的包围盒,则裁剪这个矩形
let rectShape1 = echarts.graphic.clipRectByRect(
{
//矩形的位置
x: planStartDate[0],
y: planStartDate[1]-14,
//矩形的宽高
width: planEndDate[0] - planStartDate[0],
// height: height,
height: 30,
},
{
//当前坐标系的包围盒
x: params.coordSys.x,
y: params.coordSys.y,
width: params.coordSys.width,
height: params.coordSys.height,
// height: 219.12,
}
);
//设置绘制的矩形的元素定义
return (
rectShape1 && {
type: "group",
children: [
{
//类型为矩形
type: "rect",
//具体形状
shape: rectShape1,
//样式
style: api.style({
fill: new echarts.graphic.LinearGradient(1, 0, 0, 0, [{
offset: 0,
color: 'rgba(30,117,241,0.89)'
}, {
offset: 1,
color: '#406BF9'
}]),
shadowBlur: 12, // 阴影的大小
shadowOffsetX: 0, // 阴影水平方向上的偏移
shadowOffsetY: 12, // 阴影垂直方向上的偏移
shadowColor: 'rgba(16, 103, 235, 0.24)', // 阴影颜色
barBorderRadius: 7, //无效
}),
}
],
}
);
}
if (barDv) {
let myChart = this.$echarts.init(barDv);
// 初始化echarts实例
// var mytime24 = new Array();
// 绘制图表
var option = {
tooltip: {
trigger: "axis",
axisPointer: {
// 坐标轴指示器,坐标轴触发有效--鼠标上移蒙层
// type: "shadow", // 默认为直线,可选为:'line' | 'shadow'
},
position: function (point, params, dom, rect, size) {
//固定浮窗位置在鼠标位置左侧
//其中point为当前鼠标的位置,size中有两个属性:viewSize和contentSize,分别为外层div和tooltip提示框的大小
let x = point[0];
let y = point[1];
let boxWidth = size.contentSize[0];
let boxHeight = size.contentSize[1];
let posX, posY; //y坐标位置
if (x < boxWidth) {
//左边放不开
posX = 5;
} else {
//左边放的下
posX = x - boxWidth;
}
if (y < boxHeight) {
//上边放不开
posY = 5;
} else {
//上边放得下
posY = y - boxHeight;
}
return [posX, posY];
},
//鼠标经过显示数据
formatter: function (params) {
let pas = params[0].data.data;
let val = "";
if (pas.meetingTitle.length > 6) {
val = pas.meetingTitle.substr(0, 6) + '...';
pas.meetingTitle = val;
}
return (
`会议名称:${pas.meetingTitle}` + "<br/>" +
`创建人:${pas.createdName}` + "<br/>" +
`参会角色:${pas.attendanceRule}` +
"<br/>" +
`会议时间: ${formatTime(
pas.planStartTime,
"{h}:{i}"
)} - ${formatTime(pas.planEndTime, "{h}:{i}")}` +
"<br/>" +
`会议类别: ${pas.createType === 1 ? "内联会议" : "外联会议"} ` +
"<br/>" +
`会议规模: ${pas.attendance}` +
"<br/>" +
`会议状态: ${
pas.meetingStatus === 1
? "待开始"
: pas.meetingStatus === 2
? "已开始"
: pas.meetingStatus === 3
? "已结束"
: pas.meetingStatus === 5
? "已逾期"
: ""
}`
);
},
},
grid: {
//grid 为直角坐标系内绘图网格
left: "1.2%",
right: "1.2%",
bottom: "1%",
height: "98%",
containLabel: true,
show: true, //是否显示
},
yAxis: {
//竖轴数据显示
type: "category", //控制显示Y轴的类型
splitLine:{
show:true, //显示网格线
},
axisLabel: {
//坐标轴刻度标签的相关设置
show: false, //是否显示
// interval: 0, //坐标轴刻度标签的显示间隔
},
axisTick: {
alignWithLabel: true, //保证刻度线和标签对齐,当boundaryGap为true的时候有效,不过boundaryGap默认就是true
}
},
xAxis: {
//横轴数据显示
type: "time",
position: "top",
min: formatTime(nowDateStr, "{y}-{m}-{d} 00:00:00"),
max: formatTime(nowDateStr, "{y}-{m}-{d} 24:00:00"),
spiltLine: {
show: true, //想要不显示网格线,改为false
},
axisLabel: {
//坐标轴刻度标签的相关设置
interval: 0, //坐标轴刻度标签的显示间隔
formatter: function (val) {
let date = new Date(val);
let texts = [date.getHours(), date.getMinutes()];
return texts.join(":");
}
},
},
dataZoom: [ //滑动条
{
yAxisIndex: 0, //这里是从X轴的0刻度开始
show: drawData.length > 4 ? true : false, //是否显示滑动条,不影响使用
startValue: drawData.length - 1, // 从头开始为0,如果是从上方开始的话就数据长度,注意这里后端数据需要与你相反的排序
endValue: drawData.length - 4, // 初始化展示4个,超过4个滚动查找
zoomLock:true, //取消鼠标拉动缩放
width:12, //设置宽度
textStyle:false, //取消滚动条上面的提示数字
handleStyle:{ //滚动条样式
borderColor:'#406BF9',
}
},
//使鼠标滚动轮可以使滚动条滚动
{
type:'inside',
yAxisIndex:0,
zoomOnMouseWheel:false, //滚轮是否触发缩放
moveOnMouseMove:true, //鼠标滚轮触发滚动
moveOnMouseWheel:true
}
],
series: [
{
type: "custom",
// name: "",
//使用自定义的图形元素
renderItem: renderItem,
encode: {
//将维度1和维度2的数据映射到x轴
x: [1, 2],
//将维度0的数据映射到y轴
y: 0,
},
label: {
show: true,
position: "inside",
//在甘特图上显示会议名称
color:'#FFFFFF',
fontSize:'14',
formatter: function (drawData) {
let meetingName = "";
let textWidth = drawData.data.data.meetingTitle.length * 14; //按字体大小的px来计算文本宽度
let val = "";
if (pillarsWidth < textWidth) { //如果文本宽度超出甘特图柱状图宽度则省略显示,如果柱状图宽度不超过20的话就不显示任何
val = pillarsWidth > 30 ? drawData.data.data.meetingTitle.substr(0, (pillarsWidth / 14) - 1) + '...' : '';
meetingName = val;
}else{ //显示完整的名称
meetingName = drawData.data.data.meetingTitle;
}
return meetingName;
}
},
markArea:{ //重叠时间区域背景设置
itemStyle: { // 这个是所有标域的样式,也可以写在data的item[0]里面,表示该段标域的样式
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{
offset: 1, //100%处样式
color: 'rgba(255,66,0,0.3)'
}, {
offset: 0, //开始处样式
color: 'rgba(255,122,41,0.3)'
}
]) // 也可以使用渐变色
},
//时间重叠数据
data: markAreaTimeList
},
data: drawData, //绘制需要的数据
},
],
};
myChart.setOption(option); //重点,应用数据等数据配置
//页面自适应
setTimeout(function () {
window.onresize = function () {
myChart.resize();
};
}, 200);
// 处理甘特图点击事件
myChart.on("click", function(params){
that.leaveMeetingStatus = params.data.data;
that.meetingId = that.leaveMeetingStatus.id;
// 若该场会议为分别3-已结束、4-已取消、5-已逾期三种状态的时候没有【进入会议】该按钮
that.mousedownState = ![3, 4, 5].includes(that.leaveMeetingStatus.meetingStatus);
// 若会议为1:待开始状态,且用户在该场会议拥有操作员角色,会议预计开始时间前20分钟按钮启用
if(that.leaveMeetingStatus.meetingStatus === 1){
meetingApi.getVerifyEntryStatus(that.meetingId).then(res =>{
if(res.data.code === 200){
// 仅会议为“待开始”状态且拥有会议测试权限的用户拥有该按钮
res.data.data.isTest === true ? that.testState = true : that.testState = false;
if(res.data.data.isJoin === false){
that.clickState =false;
// 判断是否是操作员
if(res.data.data.isController === false){
return that.msgWarning(that.$t('meetingAbout.beforeStartOperator'))
}else{
return that.msgWarning(that.$t('meetingAbout.toStartOperator'))
}
}else{
that.clickState = true;
// 当会议室的进入状态首次变为“允许进入”后,该会议的会议测试功能禁用(会议管理模块/首页的会议测试按钮不显示)
that.testState = false;
}
}else{
that.msgError(res.data.msg)
}
}).catch(err =>{
that.msgError(err)
});
}
that.$nextTick(()=>{
let menu = document.getElementById("RightMenu");
let menuHeight = menu.children.length * 40; //扩展菜单高度
let clientHeight = document.documentElement.clientHeight; //窗口高度
let pageX = params.event.event.pageX; //鼠标点击X轴坐标
let pageY = params.event.event.pageY; //鼠标点击Y轴坐标
//右键菜单边界位置处理
if (pageY + menuHeight > clientHeight) {
pageY = pageY - menuHeight - 180;
}else{
pageY = pageY - 226;
}
menu.style.left = pageX - 276 + "px";
menu.style.top = pageY + "px";
menu.style.display = "block";
})
});
}
},
}