更新
This commit is contained in:
320
src/components/TimeseriesGraph/chart.js
Normal file
320
src/components/TimeseriesGraph/chart.js
Normal file
@@ -0,0 +1,320 @@
|
||||
import * as echarts from "echarts";
|
||||
|
||||
function getStartTime(timestamp) {
|
||||
return new Date(new Date(timestamp).toLocaleDateString()).getTime();
|
||||
}
|
||||
|
||||
function renderItem(params, api) {
|
||||
var categoryIndex = api.value(0);
|
||||
var start = api.coord([api.value(1), categoryIndex]);
|
||||
var end = api.coord([api.value(2), categoryIndex]);
|
||||
|
||||
var height = api.size([0, 1])[1] * 2;
|
||||
var rectShape = echarts.graphic.clipRectByRect(
|
||||
{
|
||||
x: start[0],
|
||||
y: start[1] - height / 2,
|
||||
width: end[0] - start[0],
|
||||
height: height,
|
||||
},
|
||||
{
|
||||
x: params.coordSys.x,
|
||||
y: params.coordSys.y - 16,
|
||||
width: params.coordSys.width,
|
||||
height: params.coordSys.height,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
rectShape && {
|
||||
type: "rect",
|
||||
transition: ["shape"],
|
||||
shape: rectShape,
|
||||
style: api.style(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function getTodayStart(today) {
|
||||
const [y, m, d] = [today.getFullYear(), today.getMonth(), today.getDate()];
|
||||
// debugger;
|
||||
return new Date(y, m, d).getTime();
|
||||
}
|
||||
|
||||
/** 颜色配置 */
|
||||
const types = [
|
||||
{ name: "运行", color: "#288AFF" },
|
||||
{ name: "故障", color: "#FC9C91" },
|
||||
{ name: "计划停机", color: "#FFDC94" },
|
||||
{ name: "空白", color: "#F2F4F9" },
|
||||
];
|
||||
|
||||
export default class GanttGraph {
|
||||
// tooltip - 基本是固定的
|
||||
tooltip = {
|
||||
trigger: "item",
|
||||
axisPointer: {
|
||||
type: "none",
|
||||
},
|
||||
formatter: (params) => {
|
||||
// debugger;
|
||||
return `
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span>${new Date(
|
||||
params.value[1]
|
||||
).toLocaleTimeString()} ~ ${new Date(
|
||||
params.value[2]
|
||||
).toLocaleTimeString()}</span>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span class="icon" style="width: 8px; height: 8px; border-radius: 2px; background: ${
|
||||
params.color
|
||||
}"></span>
|
||||
<span class="eq-name" style="margin-left: 4px;">${
|
||||
params.seriesName
|
||||
}</span>
|
||||
</div>
|
||||
<span class="run-status" style="margin-left: 8px; opacity: 0.6">${
|
||||
params.name
|
||||
}</span>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
grid = [];
|
||||
xAxis = [];
|
||||
yAxis = [];
|
||||
series = [];
|
||||
|
||||
constructor(el, startTime) {
|
||||
this.el = el;
|
||||
this.gridIndex = -1;
|
||||
this.currentGraphIndex = -2;
|
||||
this.startTime = new Date(startTime);
|
||||
// this.startTime = new Date(new Date('2023/10/8').toLocaleDateString());
|
||||
// console.log('<> Gantt Created', this.startTime);
|
||||
}
|
||||
|
||||
// 构造一个新的 grid
|
||||
makeGrid() {
|
||||
this.gridIndex++;
|
||||
return {
|
||||
id: "GRID_" + this.gridIndex,
|
||||
// top: 12 + 128 * this.gridIndex,
|
||||
top: 12 + 104 * this.gridIndex,
|
||||
right: 48,
|
||||
left: 183,
|
||||
height: 56,
|
||||
};
|
||||
}
|
||||
|
||||
// 构造一个 xAxis
|
||||
makeXaxis() {
|
||||
const [id1, id2] = ["" + Math.random(), "" + Math.random()];
|
||||
return [
|
||||
{
|
||||
id: id1,
|
||||
gridIndex: this.gridIndex,
|
||||
axisTick: {
|
||||
alignWithLabel: true,
|
||||
inside: true,
|
||||
},
|
||||
type: "time",
|
||||
min: getTodayStart(this.startTime),
|
||||
max: getStartTime(this.startTime.getTime() + 3600 * 24 * 1000),
|
||||
splitNumber: 10,
|
||||
axisLabel: {
|
||||
margin: 12,
|
||||
formatter: function (val) {
|
||||
return new Date(val)
|
||||
.toLocaleTimeString()
|
||||
.split(":")
|
||||
.slice(0, 2)
|
||||
.join(":");
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: "#0005",
|
||||
},
|
||||
},
|
||||
boundaryGap: false,
|
||||
// data: getXaxisRange(getTodayStart(new Date())),
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
gridIndex: this.gridIndex,
|
||||
axisLabel: { show: false },
|
||||
axisLine: { show: false },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 构造一个 yAxis
|
||||
makeYaxis(equipmentName) {
|
||||
const [id1, id2] = ["" + Math.random(), "" + Math.random()];
|
||||
return [
|
||||
// 主y轴
|
||||
{
|
||||
id: id1,
|
||||
gridIndex: this.gridIndex,
|
||||
type: "value",
|
||||
splitLine: { show: false },
|
||||
name: equipmentName,
|
||||
nameLocation: "center",
|
||||
nameGap: 14,
|
||||
nameRotate: 0,
|
||||
nameTextStyle: {
|
||||
fontSize: 16,
|
||||
color: "#000A",
|
||||
},
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#0005",
|
||||
},
|
||||
},
|
||||
axisLabel: { show: false },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
// 辅y轴
|
||||
{
|
||||
id: id2,
|
||||
gridIndex: this.gridIndex,
|
||||
type: "value",
|
||||
splitLine: { show: false },
|
||||
axisLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
color: "#0005",
|
||||
},
|
||||
},
|
||||
axisLabel: { show: false },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 构造一个 series
|
||||
makeSeries({ equipmentName, arr }) {
|
||||
this.currentGraphIndex += 2;
|
||||
const bgStartTime = this.startTime.getTime();
|
||||
const bgEndTime = bgStartTime + 3600 * 24 * 1000;
|
||||
return [
|
||||
// 沉默的背景
|
||||
{
|
||||
xAxisIndex: this.currentGraphIndex,
|
||||
yAxisIndex: this.currentGraphIndex,
|
||||
type: "custom",
|
||||
renderItem: renderItem,
|
||||
silent: true,
|
||||
itemStyle: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
encode: {
|
||||
x: [1, 2],
|
||||
y: 0,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
name: "无数据",
|
||||
value: [0, bgStartTime, bgEndTime, 0],
|
||||
tooltip: { show: false },
|
||||
itemStyle: {
|
||||
color: "#F2F4F9",
|
||||
opacity: 0.3,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: equipmentName,
|
||||
xAxisIndex: this.currentGraphIndex,
|
||||
yAxisIndex: this.currentGraphIndex,
|
||||
type: "custom",
|
||||
renderItem: renderItem,
|
||||
itemStyle: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
encode: {
|
||||
x: [1, 2],
|
||||
y: 0,
|
||||
},
|
||||
data: arr.map((item) => ({
|
||||
name: ["运行", "故障", "计划停机"][item.status],
|
||||
value: [
|
||||
0,
|
||||
item.startTime,
|
||||
item.startTime + item.duration * 60 * 1000,
|
||||
0,
|
||||
],
|
||||
itemStyle: {
|
||||
color: types[item.status].color,
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
init(data) {
|
||||
if (!this.el) throw new Error("没有可供echarts初始化的容器");
|
||||
if (typeof this.el == "string") {
|
||||
this.el = document.querySelector(this.el);
|
||||
}
|
||||
this.chart = echarts.init(this.el);
|
||||
this.handleProps(data);
|
||||
|
||||
setTimeout(() => {
|
||||
this.chart.setOption(this.option);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
update(data) {
|
||||
this.clear();
|
||||
this.init(data);
|
||||
}
|
||||
|
||||
resize() {
|
||||
this.chart.resize();
|
||||
}
|
||||
|
||||
get option() {
|
||||
return {
|
||||
tooltip: this.tooltip,
|
||||
grid: this.grid,
|
||||
xAxis: this.xAxis,
|
||||
yAxis: this.yAxis,
|
||||
series: this.series,
|
||||
};
|
||||
}
|
||||
|
||||
// 每次 graphList 刷新都会重新渲染整个所有图表
|
||||
// 可以改进的地方:添加一个 handleAdd() 方法,一次添加一个新的
|
||||
handleProps(props) {
|
||||
// props 是父组件的 graphList
|
||||
console.log("props: ", props);
|
||||
props.forEach((eqArr) => {
|
||||
this.grid.push(this.makeGrid());
|
||||
this.xAxis.push(...this.makeXaxis());
|
||||
this.yAxis.push(...this.makeYaxis(eqArr.key));
|
||||
this.series.push(
|
||||
...this.makeSeries({ equipmentName: eqArr.key, arr: eqArr })
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.grid = [];
|
||||
this.xAxis = [];
|
||||
this.yAxis = [];
|
||||
this.series = [];
|
||||
this.currentGraphIndex = -2;
|
||||
this.gridIndex = -1;
|
||||
this.chart.dispose();
|
||||
}
|
||||
|
||||
// print option
|
||||
print() {
|
||||
console.log(JSON.stringify(this.option, null, 2));
|
||||
}
|
||||
}
|
||||
316
src/components/TimeseriesGraph/index.vue
Normal file
316
src/components/TimeseriesGraph/index.vue
Normal file
@@ -0,0 +1,316 @@
|
||||
<!--
|
||||
filename: index.vue
|
||||
author: liubin
|
||||
date: 2023-09-04 09:34:52
|
||||
description: 设备状态时序图
|
||||
-->
|
||||
|
||||
<template>
|
||||
<el-row
|
||||
style="
|
||||
flex: 1;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
padding: 16px 16px 32px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="blue-title">设备状态时序图</div>
|
||||
</el-col>
|
||||
<el-col :span="18" class="legend-row">
|
||||
<div class="legend">
|
||||
<div class="icon running"></div>
|
||||
<div>运行中</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="icon fault"></div>
|
||||
<div>故障</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<div class="icon stop"></div>
|
||||
<div>计划停机</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div
|
||||
class="main-area"
|
||||
style="flex: 1; display: flex; flex-direction: column; position: relative"
|
||||
>
|
||||
<div
|
||||
class="graphs"
|
||||
v-show="graphList.length"
|
||||
id="status-chart"
|
||||
style="height: 1px; flex: 1"
|
||||
></div>
|
||||
<h2 v-if="!graphList || graphList.length == 0" class="no-data-bg"></h2>
|
||||
</div>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Gantt from "./chart";
|
||||
|
||||
export default {
|
||||
name: "TimeseriesGraph",
|
||||
props: {
|
||||
graphList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
validator: (val) => Array.isArray(val),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
chart: null,
|
||||
existingEquipments: [],
|
||||
eqList: [],
|
||||
startTime: 1711987200000,
|
||||
gantt: null,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
graphList: {
|
||||
handler(val) {
|
||||
// TODO: delete this line
|
||||
this.startTime = new Date(val[0][0].startTime).setHours(0,0,0,0);
|
||||
// end TODO
|
||||
if (val && val.length) {
|
||||
this.$nextTick(() => {
|
||||
if (!this.gantt) {
|
||||
this.gantt = new Gantt("#status-chart", this.startTime);
|
||||
this.gantt.init(
|
||||
val.map((item) => {
|
||||
item.key = item[0].equipmentName;
|
||||
return item;
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.gantt.update(
|
||||
val.map((item) => {
|
||||
item.key = item[0].equipmentName;
|
||||
return item;
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
return;
|
||||
},
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
findMin() {
|
||||
let min = 0;
|
||||
this.graphList.forEach((arr) => {
|
||||
arr.forEach((item) => {
|
||||
if (min < item.startTime) min = item.startTime;
|
||||
});
|
||||
});
|
||||
return min;
|
||||
},
|
||||
|
||||
/** 对象到数组的转换 */
|
||||
objectToArray(obj) {
|
||||
return Object.keys(obj).map((key) => {
|
||||
obj[key].sort((a, b) => a.startTime - b.startTime);
|
||||
obj[key].key = key;
|
||||
return obj[key];
|
||||
});
|
||||
},
|
||||
|
||||
async getList() {
|
||||
const { code, data } = await this.$axios({
|
||||
url: "/monitoring/equipment-monitor/status-series",
|
||||
method: "get",
|
||||
params: this.queryParams,
|
||||
});
|
||||
if (code == 0) {
|
||||
this.existingEquipments = Object.values(data).map(
|
||||
(eq) => eq[0].equipmentId
|
||||
);
|
||||
// this.graphList = this.objectToArray(data);
|
||||
this.graphList = this.demo.map((item) => {
|
||||
item.key = item[0].equipmentName;
|
||||
return item;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/** 准备设备数据 */
|
||||
async initEquipment() {
|
||||
const { code, data } = await this.$axios({
|
||||
url: "/base/core-equipment/listAll",
|
||||
method: "get",
|
||||
});
|
||||
if (code == 0) {
|
||||
this.eqList = data.map((item) => {
|
||||
return {
|
||||
name: item.name,
|
||||
id: item.id,
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.graph {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.graph-title {
|
||||
padding: 0 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.graph-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 22px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-bottom-width: 2px;
|
||||
border-top: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.graph-content::after,
|
||||
.graph-content::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
height: 80%;
|
||||
background: #fff;
|
||||
right: -1px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.graph-content::before {
|
||||
right: unset;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.graph-item,
|
||||
.graph-item-fixed {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.graph-item-fixed {
|
||||
flex: unset;
|
||||
}
|
||||
|
||||
.graph-item::before,
|
||||
.graph-item-fixed::before {
|
||||
position: absolute;
|
||||
bottom: -16px;
|
||||
left: 0;
|
||||
content: attr(data-time);
|
||||
color: #777;
|
||||
transform-origin: left top;
|
||||
transform: rotate(12deg);
|
||||
}
|
||||
|
||||
.graph-item-fixed::after,
|
||||
.graph-item::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: -3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.graph-item.tick::after,
|
||||
.graph-item-fixed.tick::after {
|
||||
width: 1px;
|
||||
height: 6px;
|
||||
border-left: 1px solid #777;
|
||||
}
|
||||
|
||||
.running {
|
||||
background-color: #288aff;
|
||||
}
|
||||
|
||||
.waiting {
|
||||
background-color: #5ad8a6;
|
||||
}
|
||||
|
||||
.fault {
|
||||
background-color: #fc9c91;
|
||||
}
|
||||
|
||||
.full {
|
||||
background-color: #598fff;
|
||||
}
|
||||
|
||||
.lack {
|
||||
background-color: #7585a2;
|
||||
}
|
||||
|
||||
.stop {
|
||||
background-color: #ffdc94;
|
||||
}
|
||||
|
||||
.legend-row {
|
||||
margin: 6px 0;
|
||||
padding-right: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
> .legend:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
margin-right: 4px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.blue-title {
|
||||
position: relative;
|
||||
padding: 4px 0;
|
||||
padding-left: 12px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
font-weight: 700;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 6px;
|
||||
height: 16px;
|
||||
width: 4px;
|
||||
border-radius: 1px;
|
||||
background: #0b58ff;
|
||||
}
|
||||
}
|
||||
|
||||
.echarts__status-chart {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
.echarts__status-chart > div {
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user