最近接到一个将多人任务进度做成甘特图的需求,本以为用echars很容易就实现了,没想到啊,断断续续搞的头大,大概就是使用EChars官网的实栗不足以打到需求目标,所以开始了我为期一周的修修补补,终于是完成了,写一下总结:
包含了缩放、横轴分组显示一行展示多条内容、以及数据之间的对比效果
首先效果图上个: 数据mock有点少,可自行添加,项目中数据量大,我添加了分页,可按需添加
点击效果:
点击对比效果:
右边的点击效果这次需求赶还没有添加,后续更新。。。上代码啦
加粗!! 完整到带mock数据的代码~~
index.vue
<style scoped lang="scss" src="./_.scss" />
<style lang="scss" src="./index.scss" />
<template lang="pug" src="./_.pug" />
<script src="./_.js" />
_.js
/**
* 工序多人模式甘特图
* 甘特图官网:https://echarts.apache.org/zh/option.html#title
*/
import echarts from 'echarts';
import moment from 'moment';
import * as R from 'ramda';
import {
MOCKData, employeesMOCK } from './constant';
// 注意echarts版本,高版本不支持透明度rgba,以下代码版本为3.8.5
export default {
name: 'instructGanttCopy',
data() {
return {
targetDate: moment().format('YYYY-MM-DD'),
ganttChart: null,
chartData: [],
employeeIds: {
},
employeeIdsTable: [],
};
},
computed: {
},
mounted() {
window.addEventListener(
'resize',
window._.debounce(() => {
this.resizeCharts();
}, 200),
);
this.searchResult();
// this.initChart();
},
methods: {
searchResult() {
this.chartData = (R.map(item => ({
...item,
factStartTime: moment(item.factStartTime).format('YYYY-MM-DD') === '1971-01-01' ? '--' : item.factStartTime,
factEndTime: moment(item.factEndTime).format('YYYY-MM-DD') === '1971-01-01' ? '--' : item.factEndTime,
}), MOCKData)) || [];
const setEmp = (!R.isEmpty(MOCKData) && [...new Set(MOCKData.map(x => x.employeeId))]) || [];
const emp = (setEmp && !R.isEmpty(setEmp) && setEmp.map((item, index) => {
const noPeopleEquipments = R.map(x => !R.isEmpty(x) && `${
x.equipments},`, R.filter(employee => !R.isEmpty(employee) && employee.employeeId === 'no_people', MOCKData));
return {
[item]: {
...employeesMOCK[index],
employeeOrEquipments: item === 'no_people' ? ((!R.isEmpty(noPeopleEquipments) && [...new Set(noPeopleEquipments)]) || '') : item,
},
};
})) || {
};
this.employeeIds = emp && !R.isEmpty(emp) && R.reduce((cur, per) => Object.assign(cur, per), {
}, emp);
this.employeeIdsTable = Object.values(this.employeeIds);
this.initChart();
},
// 构造y轴
getYaxis() {
const groupYaxis = R.groupWith((a, b) => a.groupId === b.groupId, this.chartData);
const Yaxis = groupYaxis.map(x => x[0].viewName);
return Yaxis;
},
// 构造图形
getSeries() {
const series = [];
const self = this;
// 分组
const groups = R.groupWith((a, b) => a.groupId === b.groupId, this.chartData);
const length = R.reduce((count, x) => x.length > count ? x.length : count, 0, groups);
const dataList = [];
for (let index = 0; index < length; index++) {
const data = groups.map(x => x[index]);
dataList.push(data);
}
const selectColor = (defaultColor, kind, params) => {
let defColor = defaultColor;
if (groups && !R.isEmpty(groups) && params && !R.isEmpty(params) && params.data && !R.isEmpty(params.data)) {
const employee = R.filter(emp => emp && emp.employeeId === params.data.name, this.chartData.filter(groupColor => groupColor && params.data.groupId === groupColor.groupId));
const employeeId = (!R.isEmpty(employee) && employee[0] && employee[0].employeeId) || '';
// 颜色设置
defColor = (employeeId && this.employeeIds[employeeId] && this.employeeIds[employeeId][kind]) || defaultColor;
}
return defColor;
};
dataList.map((group, index) => series.push({
name: '调整时间:',
type: 'bar',
stack: '调整时间',
label: {
show: true,
position: 'insideRight',
},
itemStyle: {
normal: {
color: 'rgba(0,0,0,0)',
},
},
data: group.map(item => ({
value: item && item.adjustStartTime,
name: item && item.employeeId,
groupId: item && item.groupId,
}) || ''),
showSymbol: false,
hoverAnimation: false,
},
{
name: '调整时间',
type: 'bar',
stack: '调整时间',
label: {
show: true,
position: 'insideRight',
},
barWidth: 8, // 柱宽度
itemStyle: {
normal: {
barBorderRadius: 4, // 柱圆角
color(params) {
const color = 'rgb(14,14,14)';
return selectColor(color, 'adjust', params);
},
},
},
data: group.map(item => ({
value: item && item.adjustEndTime,
name: item && item.employeeId,
groupId: item && item.groupId,
}) || ''),
showSymbol: false,
hoverAnimation: false,
},
{
name: '期望时间:',
type: 'bar',
stack: '期望时间',
barWidth: 8, // 柱宽度
itemStyle: {
normal: {
color: 'rgba(0,0,0,0)',
},
},
data: group.map(item => ({
value: item && item.expectStartTime,
name: item && item.employeeId,
groupId: item && item.groupId,
}) || ''),
showSymbol: false,
hoverAnimation: false,
},
{
name: '期望时间',
type: 'bar',
stack: '期望时间',
barWidth: 8, // 柱宽度
itemStyle: {
normal: {
barBorderRadius: 4, // 柱圆角
color(params) {
const color = 'rgba(14,14,14,0.7)';
return selectColor(color, 'expect', params);
},
},
},
data: group.map(item => ({
value: item && item.expectEndTime,
name: item && item.employeeId,
groupId: item && item.groupId,
}) || ''),
showSymbol: false,
hoverAnimation: false,
},
{
name: '实际时间:',
type: 'bar',
stack: '实际时间',
itemStyle: {
normal: {
color: 'rgba(0,0,0,0)',
},
},
data: group.map(item => ({
value: item && item.factStartTime,
name: item && item.employeeId,
groupId: item && item.groupId,
}) || ''),
showSymbol: false,
hoverAnimation: false,
},
{
name: '实际时间',
type: 'bar',
stack: '实际时间',
barWidth: 8, // 柱宽度
itemStyle: {
normal: {
barBorderRadius: 4, // 柱圆角
color(params) {
const color = 'rgba(14,14,14,0.4)';
return selectColor(color, 'fact', params);
},
},
},
data: group.map(item => ({
value: item && item.factEndTime,
name: item && item.employeeId,
groupId: item && item.groupId,
}) || ''),
showSymbol: false, // 数据卡顿
showAllSymbol: false,
},
));
return series;
},
async initChart() {
const gantt = this.$refs.chart;
if (gantt) {
this.ganttChart = echarts.init(gantt);
// const self = this;
const chartOption = {
title: {
text: '任务进度表',
left: 10,
},
grid: {
containLabel: true,
left: 10,
},
// 横轴
xAxis: {
type: 'time',
min: '2020-05-23 00:00',
max: '2020-05-23 23:59',
// min: '00:00',
// max: '23:59',
maxInterval: 60 * 1000 * 30, // 自动计算的坐标轴最大间隔大小为半小时
axisLabel: {
formatter(params) {
return (moment(params).format('HH:mm'));
},
rotate: 60,
},
},
// 纵轴
yAxis: {
data: this.getYaxis(),
inverse: true, // 设置反向坐标轴
nameLocation: 'start',
axisLabel: {
formatter(params) {
return params && params.replace(/.{9}(?!$)/g, a => `${
a}\n`);
},
// margin: 10,
lineHeight: 40,
verticalAlign: 'middle',
align: 'right',
},
axisLine: {
show: true,
},
splitLine: {
show: true,
},
},
legend: {
data: ['调整时间', '期望时间', '实际时间'],
},
series: this.getSeries(),
// 提示框组件。
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
animation: false,
},
formatter(params) {
let str = `${
params[0].name} </br>`;
for (let i = 0; i < params.length; i++) {
str += `${
i % 2 === 0 ? params[i].seriesName : ''} ${
params[i] && params[i].value && moment(params[i].value).format('HH:mm')}${
i % 2 ? '</br>' : '~'}`;
}
return str;
},
},
// 选中行置灰
toolbox: {
show: true,
feature: {
saveAsImage: {
},
},
},
// 缩放
dataZoom: [{
type: 'slider',
filterMode: 'weakFilter',
showDataShadow: false,
bottom: 50,
height: 10,
borderColor: 'transparent',
backgroundColor: '#e2e2e2',
handleIcon: 'M10.7,11.9H9.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4h1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7v-1.2h6.6z M13.3,22H6.7v-1.2h6.6z M13.3,19.6H6.7v-1.2h6.6z', // jshint ignore:line
handleSize: 20,
zoomOnMouseWheel: false,
moveOnMouseMove: false,
handleStyle: {
shadowBlur: 6,
shadowOffsetX: 1,
shadowOffsetY: 2,
shadowColor: '#aaa',
},
labelFormatter: '',
}],
};
// 第二个参数:true: 是否和之前设置的option进行合并,true为不合并,默认为false:合并
this.ganttChart.setOption(chartOption);
this.resizeCharts();
} else {
console.log('gantt', gantt);
}
},
resizeCharts() {
if (this.ganttChart) {
this.ganttChart.resize();
}
},
destroyed() {
window.removeEventListener('resize', this.resizeCharts); // 销毁事件
},
},
};
_.pug
该文件为html的另一种表现形式,具体转换参考:http://html2jade.org/
.content-wrapper
.demo
.chartTitle
p.title 员工/设备 & 色系:
el-table(
:data='employeeIdsTable'
border
size='mini'
)
el-table-column(label='员工' prop='employeeOrEquipments' align='center' width='100px')
el-table-column(label='色系' align='center' width='100px')
template(slot-scope="scope")
.opera-panel
span.employeeItem(:style='{backgroundColor:scope.row.adjust}')
span.employeeItem(:style='{backgroundColor:scope.row.expect}')
span.employeeItem(:style='{backgroundColor:scope.row.fact}')
.gantt-chart(ref="chart")
_.scss
.demo{
position: relative;
width: 100%;
overflow: scroll;
.chartTitle {
position: absolute;
right: 0;
right: -200px;
top: 5%;
// top: 0;
transform: translateX(-50%);
z-index: 999;
.employee {
display: inline-block;
display: flex;
border-radius: 5px;
vertical-align: center;
margin: 5px 0;
padding-left: 5px;
padding-right: 5px;
text-align: center;
justify-content: center;
// background-color:rgba(205,120,0,1);
}
.employeeItem{
width: 30px;
height: 14px;
}
.title{
font-size: 14px;
font-weight: 500;
padding-bottom: 5px;
}
.employeeName{
width:100px ;
text-align: end;
word-wrap:break-word ;
}
}
.gantt-chart {
margin: 1em auto;
height: 500px; // 高度必须添加,不然甘特图无法渲染
max-width: 80%;
}
}
constant.js
// mock 数据
export const MOCKData = [{
shopCode: '100023003',
shopName: '双营路天畅园1号楼店',
viewId: 2068456,
viewName: '任务一',
viewDesc: '',
employeeId: '员工-1',
groupId: 29,
equipments: '[]',
expectStartTime: '2020-05-23 09:00:00',
expectEndTime: '2020-05-23 09:30:00',
adjustStartTime: '2020-05-23 09:00:00',
adjustEndTime: '2020-05-23 09:40:00',
factStartTime: '2020-05-23 09:00:00',
factEndTime: '2020-05-23 09:30:00',
}, {
shopCode: '100023003',
shopName: '双营路天畅园1号楼店',
viewId: 2068456,
viewName: '任务一',
viewDesc: '',
employeeId: '员工-1',
groupId: 29,
equipments: '[]',
expectStartTime: '2020-05-23 13:30:00',
expectEndTime: '2020-05-23 15:30:00',
adjustStartTime: '2020-05-23 14:00:00',
adjustEndTime: '2020-05-23 15:40:00',
factStartTime: '2020-05-23 14:40:00',
factEndTime: '2020-05-23 14:00:00',
}, {
shopCode: '100023003',
shopName: '双营路天畅园1号楼店',
viewId: 2068457,
viewName: '任务二',
viewDesc: '',
employeeId: '员工-1',
groupId: 21,
equipments: '[{"276":"万能蒸烤箱_AWE- 0623DXB_750 * 630 * 665"}]',
expectStartTime: '2020-05-23 09:00:00',
expectEndTime: '2020-05-23 09:30:00',
adjustStartTime: '2020-05-23 09:00:00',
adjustEndTime: '2020-05-23 09:40:00',
factStartTime: '2020-05-23 09:00:00',
factEndTime: '2020-05-23 09:30:00',
},
{
shopCode: '100023003',
shopName: '双营路天畅园1号楼店',
viewId: 2068457,
viewName: '任务二',
viewDesc: '',
employeeId: '员工-2',
groupId: 21,
equipments: '[{"276":"万能蒸烤箱_AWE- 0623DXB_750 * 630 * 665"}]',
expectStartTime: '2020-05-23 10:30:00',
expectEndTime: '2020-05-23 14:30:00',
adjustStartTime: '2020-05-23 11:00:00',
adjustEndTime: '2020-05-23 15:40:00',
factStartTime: '2020-05-23 11:40:00',
factEndTime: '2020-05-23 16:00:00',
},
{
shopCode: '100023003',
shopName: '双营路天畅园1号楼店',
viewId: 2068458,
viewName: '任务三',
viewDesc: '',
employeeId: '员工-2',
groupId: 27,
equipments: '[]',
expectStartTime: '2020-05-23 16:13:00',
expectEndTime: '2020-05-23 16:50:00',
adjustStartTime: '2020-05-23 16:13:00',
adjustEndTime: '2020-05-23 17:13:00',
factStartTime: '1971-01-01 16:13:00',
factEndTime: '1971-01-01 17:13:00',
},
{
shopCode: '100023003',
shopName: '双营路天畅园1号楼店',
viewId: 2068458,
viewName: '任务四',
viewDesc: '',
employeeId: '员工-1',
groupId: 20,
equipments: '[]',
expectStartTime: '2020-05-23 17:13:00',
expectEndTime: '2020-05-23 17:50:00',
adjustStartTime: '2020-05-23 17:13:00',
adjustEndTime: '2020-05-23 18:13:00',
factStartTime: '1971-01-01 17:13:00',
factEndTime: '1971-01-01 18:30:00',
}];
export const employeesMOCK = [{
adjust: 'rgb(14,14,14)',
expect: 'rgba(14,14,14,0.7)',
fact: 'rgba(14,14,14,0.4)',
}, {
adjust: 'rgb(208,33,37)',
expect: 'rgba(208,33,37,0.7)',
fact: 'rgba(208,33,37,0.4)',
}, {
adjust: 'rgb(80,144,152)',
expect: 'rgba(80,144,152,0.7)',
fact: 'rgba(80,144,152,0.4)',
}, {
adjust: 'rgb(10,144,100)',
expect: 'rgba(10,144,100,0.7)',
fact: 'rgba(10,144,100,0.4)',
},
];