蛇形时间轴线图绘制
本人在之前的项目中(vue项目),由于业务需求根据设计图自行利用css样式绘制了一个简单的时间轴线图,可自适应收缩并且左右来回循环,在某些方面可满足大部分的需求,并且添加了图例,特此分享。如有需要,大家可自行复制代码应用到自己的项目之中,并且根据自己的业务需求加以调整。
1. 效果图展示
2. 完整源码地址(包含演示地址)
https://ext.dcloud.net.cn/plugin?id=12929
3. 代码呈现(代码全部粘贴应用之后即可呈现除小汽车图片之外的上图效果)
- HTML
<template>
<div class="only-content">
<!--图例显示-->
<div class="legend-block">
<div class="every-legend" v-for="item in cloneLegend" @click="clickLegend(item)" :style="item.statusName === '无计划' ? 'cursor: default;' : ''">
<span class="color-block" :style="'background:' + item.background"></span>
<span class="name-block">{{item.statusName}}</span>
</div>
<div class="clear"></div>
</div>
<!--时间轴线显示-->
<div class="time-line">
<div class="every-block" v-for="(item,index) in cloneTimeDatas"
:style="item.type === 0 ? 'float: left;' + 'width:' + item.width + '%;' : item.type === 1 ? 'float: right;' + 'width:' + item.width : item.type === 2 ? 'float: right;' + 'width:' + item.width + '%;' : 'float: left;' + 'width:' + item.width + '%;'" style="height: 100px;">
<div class="identical to-right" v-if="item.type === 0">
<!--分类名称-->
<div class="class-name" :style="item.status === 2 ? 'color: #6078AB;' : 'color: #000;'">
<span :title="item.className">{{item.className}}</span>
</div>
<!--进行中小车图标-->
<span class="small-car" v-if="item.status === 2">
<img src="/static/images/carPlan1.png" alt="">
</span>
<!--时间线-->
<div class="class-line" :style="item.className !== null && item.className !== '' ? 'background:' + item.background : 'background:' + item.background + ';cursor: default;'"></div>
<!--开始时间-->
<span class="start-time" v-if="index === 0">{{item.startTime}}</span>
<!--结束时间-->
<span class="end-time" v-if="!(cloneTimeDatas[index+1] && cloneTimeDatas[index+1].type === 1)">
<span v-if="cloneTimeDatas.length == (index + 1)">{{item.endTime}}</span>
<span v-else>{{item.nextTime}}</span>
</span>
<!--结束箭头-->
<div class="end-arrow" v-if="cloneTimeDatas.length === (index + 1)" :style="'border-color: transparent transparent transparent' + item.background"></div>
<!--方向箭头-->
<div class="small-arrow-left" v-if="cloneTimeDatas.length !== (index + 1)"></div>
<div class="small-arrow-right" v-if="cloneTimeDatas.length !== (index + 1)"></div>
</div>
<!--右回转空心半圆环-->
<div class="right-box" v-if="item.type === 1">
<div class="big-circular" :style="item.className !== null && item.className !== '' ? 'background:' + item.background : 'background:' + item.background + ';cursor: default;'">
<div class="small-circular"></div>
<!--结束时间-->
<span class="end-time" v-if="cloneTimeDatas.length == (index + 1)">{{item.endTime}}</span>
<span class="end-time" v-else>{{item.nextTime}}</span>
</div>
<!--结束箭头-->
<div class="end-arrow" v-if="cloneTimeDatas.length === (index + 1)" :style="'border-color: transparent' + ' ' + item.background + ' ' + 'transparent transparent'"></div>
</div>
<div class="identical to-left" v-if="item.type === 2">
<!--分类名称-->
<div class="class-name" :style="item.status === 2 ? 'color: #6078AB;' : 'color: #000;'">
<span :title="item.className">{{item.className}}</span>
</div>
<!--进行中小车图标-->
<span class="small-car" v-if="item.status === 2">
<img src="/static/images/carPlan.png" alt="">
</span>
<!--时间线-->
<div class="class-line" :style="item.className !== null && item.className !== '' ? 'background:' + item.background : 'background:' + item.background + ';cursor: default;'"></div>
<!--开始时间-->
<span class="start-time" v-if="index === 0">{{item.startTime}}</span>
<!--结束时间-->
<span class="end-time" v-if="!(cloneTimeDatas[index+1] && cloneTimeDatas[index+1].type === 3)">
<span v-if="cloneTimeDatas.length == (index + 1)">{{item.endTime}}</span>
<span v-else>{{item.nextTime}}</span>
</span>
<!--结束箭头-->
<div class="end-arrow" v-if="cloneTimeDatas.length === (index + 1)" :style="'border-color: transparent' + ' ' + item.background + ' ' + 'transparent transparent'"></div>
<!--反向箭头-->
<div class="small-arrow-left" v-if="cloneTimeDatas.length !== (index + 1)"></div>
<div class="small-arrow-right" v-if="cloneTimeDatas.length !== (index + 1)"></div>
</div>
<!--左回转空心半圆环-->
<div class="left-box" v-if="item.type === 3">
<div class="big-circular" :style="item.className !== null && item.className !== '' ? 'background:' + item.background : 'background:' + item.background + ';cursor: default;'">
<div class="small-circular"></div>
<!--结束时间-->
<span class="end-time" v-if="cloneTimeDatas.length == (index + 1)">{{item.endTime}}</span>
<span class="end-time" v-else>{{item.nextTime}}</span>
</div>
<!--结束箭头-->
<div class="end-arrow" v-if="cloneTimeDatas.length === (index + 1)" :style="'border-color: transparent transparent transparent' + item.background"></div>
</div>
</div>
<div class="clear"></div>
</div>
</div>
</template>
- 定义参数
data() {
return {
// 所有分类节点数据
allTimeListDatas: [],
// 深克隆后的数据
cloneTimeDatas: [],
// 所有计划目前状态集合
planLegend: [
{"statusName": "已完成","background": "#4ECB74","status": 3,"bool": true,},
{"statusName": "进行中","background": "#F6AF80","status": 2,"bool": true,},
{"statusName": "未开始","background": "#A8C7FC","status": 1,"bool": true,},
{"statusName": "无计划","background": "#E4E4E4","status": 0,"bool": false,},
],
// 深克隆后的图例数据
cloneLegend: [],
}
},
- JS
注:克隆数据是为了操作数据后不影响原数据
methods: {
// 点击图例显示隐藏
clickLegend(item){
item.bool = !item.bool;
if (item.statusName !== '无计划') {
this.cloneTimeDatas.forEach( value => {
if (value.status === item.status) {
if (item.bool) {
this.planLegend.forEach( cloneItem => {
if (cloneItem.status === item.status) {
value.background = cloneItem.background;
item.background = cloneItem.background;
}
});
} else {
value.background = '#E4E4E4';
item.background = '#E4E4E4';
}
}
})
}
},
// 获取计划分类数据
getPlanClassData(){
//调取接口————————可根据自己的业务需求在此调接口获取展示数据
let data = [
{className: "单车计划",status: 1,day: 60,startTime: "2019-01-01",endTime: "2019-01-31",nextTime: "2019-02-01",},
{className: "整车计划",status: 2,day: 120,startTime: "2019-02-01",endTime: "2019-03-31",nextTime: "2019-04-01",},
{className: "修复计划",status: 3,day: 120,startTime: "2019-04-01",endTime: "2019-05-31",nextTime: "2019-06-01",},
{className: "空闲",status: 0,day: 60,startTime: "2019-06-01",endTime: "2019-06-30",nextTime: "2019-07-01",},
{className: "拆解计划",status: 2,day: 150,startTime: "2019-07-01",endTime: "2019-09-30",nextTime: "2019-10-01",},
{className: "组装计划",status: 3,day: 180,startTime: "2019-10-01",endTime: "2019-12-31",nextTime: "2020-01-01",},
{className: "检测计划",status: 2,day: 100,startTime: "2020-01-01",endTime: "2020-06-30",nextTime: "2020-07-01",},
{className: "保养计划",status: 3,day: 100,startTime: "2020-07-01",endTime: "2020-12-31",nextTime: "2020-12-31",},
];
let allPlanClass = $.extend(true,[],data); // 所有的计划分类数据
let typeIndex = 0; //类型标记
let addLength = 0; // 计划分类赋值前叠加的总长度
let otherLength = 0; // 计划分类赋值后叠加的总长度
let moreThanPlan = []; // 每行结尾多于出的计划分类集合
allPlanClass.forEach( (value,index) => {
// 根据计划分类状态值显示不同的颜色条(1—未开始;2—进行中;3—已完成;0—无计划)
if (value.status === 1) {
value.background = '#A8C7FC';
} else if (value.status === 2) {
value.background = '#F6AF80';
} else if (value.status === 3) {
value.background = '#4ECB74';
} else if (value.status === 0) {
value.background = '#E4E4E4';
}
let moreThan = {}; // 每行结尾多于出的计划分类
// 根据计划周期(天数)计算显示长度(最短为10%)
// 计算每一段计划分类的长度
let thisWidth = 0;
if (value.day < 30) { // width的数字代表百分比
thisWidth = 10;
} else if (value.day%30 < 15) {
thisWidth = parseInt(value.day/30) * 10
} else if (value.day%30 >= 15) {
thisWidth = (parseInt(value.day/30) + 1) * 10
}
// 叠加每个计划分类的长度
addLength = addLength + thisWidth;
if (value.day == 375) {
console.log(addLength,otherLength,typeIndex,thisWidth,"时间呢?????");
}
// 根据总的长度来判断是否大于100%
if (addLength < 100) {
value.width = thisWidth; // 每段计划分类的长度
value.type = typeIndex; // 每段计划分类的类型(1—从左往右;2—右半圆弧;3—从右往左;4—左半圆弧)
otherLength = otherLength + value.width;
} else {
value.width = 100 - otherLength;
value.type = typeIndex;
typeIndex++;
// 处理每行结尾多于出长度的计划分类
moreThan = $.extend(true,{},value);
moreThan.width = 0;
moreThan.type = typeIndex;
moreThanPlan.push(moreThan);
typeIndex++;
if (typeIndex === 4) {
typeIndex = 0;
}
addLength = 0;
otherLength = 0;
}
});
// 处理后的计划分类数据集合
this.allTimeListDatas = this.sorts(allPlanClass,moreThanPlan);
// 深克隆计划分类数据
this.cloneTimeDatas = $.extend(true,[],this.allTimeListDatas);
// 深克隆图例(计划状态)数据
this.cloneLegend = $.extend(true,[],this.planLegend);
if (this.cloneTimeDatas.length > 0 && (this.cloneTimeDatas[this.cloneTimeDatas.length - 1].type === 1 || this.cloneTimeDatas[this.cloneTimeDatas.length - 1].type === 3)) {
$('.only-content').attr('style',"padding-bottom: 50px;")
} else {
$('.only-content').removeAttr('style',"padding-bottom")
}
},
// 循环排序(将每行多于长度的计划分类插入原数组)
sorts(initArr,insertArr){
let arr = initArr.concat(); //拷贝数组
initArr.forEach((value,index) => {
insertArr.forEach((val,ind) => {
if(value.startTime == val.startTime){ //判断值相同 插入
let idx =[];
arr.forEach((data,tt) => {
if(data.startTime == val.startTime){
idx.push(tt); // 获取所有相同值得下标数组
}
});
arr.splice(idx[idx.length-1] + 1,0,val); //取最后相同值插入数据
}
})
});
return arr
},
},
created () {
this.getPlanClassData();
},
- CSS
<style scoped lang="less">
.only-content{
width: 100%;
height: 100%;
font-size: 12px;
background-color: #fff;
.clear{
clear: both;
}
/*图例样式*/
.legend-block{
padding: 15px 20px 0 20px;
.every-legend{
float: left;
margin-right: 30px;
cursor: pointer;
user-select: none;
span{
display: inline-block;
}
.color-block{
width: 23px;
height: 11px;
}
}
}
/*计划分类蛇形道样式*/
.time-line{
padding: 20px 100px;
.every-block{
position: relative;
cursor: default;
.identical{
position: relative;
}
// 从左向右的方向直线样式
.to-right{
position: relative;
/*分类名称样式*/
.class-name{
width: 100%;
height: 20px;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
span{
cursor: pointer;
}
}
// 小车样式
.small-car{
position: absolute;
top: -18px;
left: calc(50% - 20px);
z-index: 200;
}
/*分类颜色线样式*/
.class-line{
width: 100%;
height: 18px;
border-right: 1px solid #ccc;
cursor: pointer;
}
/*开始时间*/
.start-time{
z-index: 200;
position: absolute;
bottom: -24px;
left: -32px;
color: #000;
}
/*结束时间*/
.end-time{
z-index: 200;
position: absolute;
bottom: -24px;
right: -30px;
color: #000;
}
/*结束箭头样式*/
.end-arrow{
position: absolute;
bottom: -9px;
right: -36px;
width: 0;
height: 0;
border: 18px solid;
}
/*方向箭头样式*/
.small-arrow-left{
z-index: 20;
position: absolute;
bottom: 7px;
right: 0;
float: left;
width: 8px;
height: 4px;
background-color: #fff;
}
.small-arrow-right{
z-index: 20;
position: absolute;
bottom: 3px;
right: -12px;
float: left;
width: 0;
height: 0;
border: 6px solid;
border-color: transparent transparent transparent #fff;
}
}
// 从右向左的方向直线样式
.to-left{
position: relative;
/*分类名称样式*/
.class-name{
width: 100%;
height: 20px;
text-align: center;
}
// 小车样式
.small-car{
position: absolute;
top: -18px;
left: calc(50% - 20px);
z-index: 200;
}
/*分类颜色线样式*/
.class-line{
height: 18px;
border-right: 1px solid #ccc;
position: relative;
cursor: pointer;
}
/*开始时间*/
.start-time{
z-index: 200;
position: absolute;
bottom: -24px;
right: -32px;
color: #000;
}
/*结束时间*/
.end-time{
z-index: 200;
position: absolute;
bottom: -24px;
left: -30px;
color: #000;
}
/*结束箭头样式*/
.end-arrow{
position: absolute;
bottom: -9px;
left: -36px;
width: 0;
height: 0;
border: 18px solid;
}
/*方向箭头样式*/
.small-arrow-left{
z-index: 20;
position: absolute;
bottom: 7px;
left: 0;
float: left;
width: 8px;
height: 4px;
background-color: #fff;
}
.small-arrow-right{
z-index: 20;
position: absolute;
bottom: 3px;
left: -12px;
float: left;
width: 0;
height: 0;
border: 6px solid;
border-color: transparent #fff transparent transparent ;
}
}
// 左空心半圆样式
.left-box{
position: relative;
top: 20px;
left: -60px;
.big-circular {
float: left;
width: 60px;
height: 118px;
cursor: pointer;
-webkit-border-radius: 118px 0 0 118px;
position: relative;
.small-circular {
width: 42px;
height: 82px;
background-color: #fff;
-webkit-border-radius: 82px 0 0 82px;
position: absolute;
left: 18px;
top: 18px;
}
/*结束时间*/
.end-time{
z-index: 200;
position: absolute;
bottom: -24px;
left: 30px;
white-space: nowrap;
color: #000;
}
}
/*结束箭头样式*/
.end-arrow{
position: absolute;
bottom: -128px;
right: -96px;
width: 0;
height: 0;
border: 18px solid;
}
}
// 右空心半圆样式
.right-box{
position: relative;
top: 20px;
right: -60px;
.big-circular {
float: right;
width: 60px;
height: 118px;
cursor: pointer;
-webkit-border-radius: 0 118px 118px 0;
position: relative;
.small-circular {
width: 42px;
height: 82px;
background-color: #fff;
-webkit-border-radius: 0 82px 82px 0;
position: absolute;
left: 0;
top: 18px;
}
/*结束时间*/
.end-time{
z-index: 200;
position: absolute;
bottom: -24px;
left: -30px;
white-space: nowrap;
color: #000;
}
}
/*结束箭头样式*/
.end-arrow{
position: absolute;
bottom: -128px;
left: -96px;
width: 0;
height: 0;
border: 18px solid;
}
}
}
}
}
</style>