You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

893 lines
26 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div class="app-container">
<el-row :gutter="28">
<el-col :span="4.3" :xs="24">
<div class="head-container">
<el-input
v-model="workUnitName"
placeholder="请输入计量设备名称"
clearable
size="small"
prefix-icon="el-icon-search"
style="margin-bottom: 20px"
/>
</div>
<div class="head-container">
<el-tree
:data="monitorInfoOptions"
:props="monitorProps"
:expand-on-click-node="false"
:filter-node-method="filterNode"
ref="tree"
node-key="id"
default-expand-all
highlight-current
@node-click="handleNodeClick"
/>
</div>
</el-col>
<el-col :span="19" :xs="24">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch"
label-width="100px"
>
<el-form-item label="记录时间">
<date-picker
v-model="daterangeRecordTime"
range
type="datetime"
format="YYYY-MM-DD HH:mm:ss"
value-type="YYYY-MM-DD HH:mm:ss"
:placeholder="['开始时间', '结束时间']"
:lang="lang"
style="width: 340px"
></date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 图表联动提示 -->
<el-alert
v-if="powerOutageSummary.count > 0"
title="图表已联动,缩放任一图表将同步其它图表"
type="info"
:closable="false"
show-icon
style="margin-bottom: 10px"
/>
<!-- 停电统计信息 -->
<el-card v-if="powerOutageSummary.count > 0" class="power-outage-summary" shadow="hover">
<div slot="header">
<span><i class="el-icon-lightning" style="color: red;"></i> 停电统计</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="exportPowerOutageData">
导出停电记录
</el-button>
</div>
<div class="power-outage-info">
<p><strong>停电次数:</strong> {{ powerOutageSummary.count }} 次</p>
<p><strong>最长停电:</strong> {{ powerOutageSummary.longestDuration }} 小时</p>
<p><strong>总停电时长:</strong> {{ powerOutageSummary.totalDuration }} 小时</p>
</div>
</el-card>
<!-- 图表区域 -->
<div class="charts-container">
<Chart ref="Chart1" class="chart1"/>
<Chart ref="Chart2" class="chart2"/>
<Chart ref="Chart3" class="chart3"/>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import { getMonitorInfoTree } from '@/api/ems/base/baseMonitorInfo'
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
import { parseTime } from '@/utils/ruoyi'
import Chart from '@/components/Charts/Chart'
import * as echarts from 'echarts'
import {pointSteamInstantList} from "@/api/ems/report/reportPointSteam";
import {listRecordSteamInstant, steamInstantAvg, steamInstantList} from "@/api/ems/record/recordSteamInstant";
import DatePicker from 'vue2-datepicker';
import 'vue2-datepicker/index.css';
import 'vue2-datepicker/locale/zh-cn';
export default {
name: 'currentSteamCurve',
dicts: ['collect_type'],
components: {
Chart,
Treeselect,
DatePicker
},
data() {
return {
//下拉树List
baseMonitorInfoOptions: [],
//左侧树结构List
monitorInfoOptions: [],
workUnitName: undefined,
//左侧树结构 检索设备名称
selectMonitorName: null,
//计量设备信息
baseMonitorInfoList: [],
// 采集方式时间范围
daterangeCollectTime: [],
//记录时间范围
daterangeRecordTime: [],
//时间选择
timerangeRecordTime:[],
// 遮罩层
loading: true,
// 选中数组
ids: [],
// 非单个禁用
single: true,
// 非多个禁用
multiple: true,
// 显示搜索条件
showSearch: true,
// 总条数
total: 0,
// 电实时数据表格数据
dataList: [],
// 弹出层标题
title: '',
// 是否显示弹出层
open: false,
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
monitorCode: null,
monitorName: null,
collectTime: null,
fluxFlow: null,
steamFlow: null,
heatInstantValue: null,
heatTotalValue: null,
temperature: null,
press: null,
density: null,
differencePress: null,
recordTime: null
},
// 表单参数
form: {},
monitorProps: {
children: 'children',
label: 'label'
},
// 表单校验
rules: {
/* objId: [
{ required: true, message: '编号不能为空', trigger: 'blur' }
]*/
},
columns: [
{ key: 0, label: `自增标识`, visible: false },
{ key: 1, label: `计量设备编号`, visible: true },
{ key: 2, label: `采集时间`, visible: false },
{ key: 3, label: `瞬时流量`, visible: true },
{ key: 4, label: `累计流量`, visible: true },
{ key: 5, label: `瞬时热量`, visible: true },
{ key: 6, label: `累计热量`, visible: true },
{ key: 7, label: `温度`, visible: false },
{ key: 8, label: `压力`, visible: false },
{ key: 9, label: `密度`, visible: false },
{ key: 10, label: `压力差值`, visible: false },
{ key: 11, label: `记录时间`, visible: true },
{ key: 12, label: `计量设备名称`, visible: true }
],
lang: {
formatLocale: {
firstDayOfWeek: 1
},
monthBeforeYear: false
},
// 停电统计信息
powerOutageSummary: {
count: 0,
totalDuration: 0,
longestDuration: 0
},
// 停电详情列表
powerOutageList: [],
// 数据抽样阈值
samplingThreshold: 1000,
// 图表基础配置
baseChartOptions: {}
}
},
created() {
const nowDate = new Date();
const today = parseTime(new Date(), '{y}-{m}-{d}')
const lastDate = new Date();
lastDate.setDate(nowDate.getDate() - 1)
const yesterday = parseTime(lastDate, '{y}-{m}-{d}')
console.log(today,yesterday)
this.daterangeRecordTime[0] = yesterday+ ' 08:00:00'
this.daterangeRecordTime[1] = today + ' 08:00:00'
// 初始化图表基础配置
this.initBaseChartOptions()
this.getTreeselect()
this.getTreeMonitorInfo()
this.getList()
},
watch: {
// 根据名称筛选计量设备树
workUnitName(val) {
this.$refs.tree.filter(val)
}
},
methods: {
/** 初始化图表基础配置 */
initBaseChartOptions() {
this.baseChartOptions = {
grid: {
top: '15%',
bottom: '10%',
left: '10%',
right: '3%'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
label: {
show: true
}
}
},
dataZoom: [{
type: 'slider'
}],
legend: {
right: 0,
data: ['数据', '停电']
},
xAxis: {
axisLine: {
show: true,
lineStyle: {
color: '#000000'
}
},
axisTick: {
show: true
},
axisLabel: {
show: true,
textStyle: {
color: '#000000'
}
}
},
yAxis: {
type: 'value',
splitLine: {
show: false
},
axisTick: {
show: true
},
axisLine: {
show: true,
lineStyle: {
color: '#000000'
}
},
axisLabel: {
show: true,
textStyle: {
color: '#000000'
}
}
}
}
},
/** 转换计量设备信息数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.monitorCode,
label: node.monitorName,
children: node.children
}
},
/** 查询电实时数据列表 */
getList() {
this.loading = true
this.queryParams.params = {}
if (null != this.daterangeRecordTime && '' != this.daterangeRecordTime) {
this.queryParams.params['beginRecordTime'] = this.daterangeRecordTime[0];
this.queryParams.params['endRecordTime'] = this.daterangeRecordTime[1];
}
this.getChart()
},
// 取消按钮
cancel() {
this.open = false
this.reset()
},
// 表单重置
reset() {
this.form = {
objId: null,
monitorCode: null,
instrumentValue: null,
expend: null,
recordTime: null,
beginTime: null,
endTime: null,
updateFlag: null,
createBy: null,
createTime: null,
updateBy: null,
updateTime: null
}
this.resetForm('form')
},
/** 搜索按钮操作 */
handleQuery() {
this.getList()
},
/** 重置按钮操作 */
resetQuery() {
this.queryParams.monitorCode = null
this.resetForm('queryForm')
this.handleQuery()
},
/** 导出按钮操作 */
handleExport() {
this.download('ems/record/recordSteamInstant/export', {
...this.queryParams
}, `recordSteamInstant_${new Date().getTime()}.xlsx`)
},
/** 导出停电数据 */
exportPowerOutageData() {
if (this.powerOutageList.length === 0) {
this.$message.warning('没有停电数据可导出');
return;
}
// 准备导出数据
const columns = [
{label: '序号', prop: 'index'},
{label: '开始时间', prop: 'startTime'},
{label: '结束时间', prop: 'endTime'},
{label: '持续时间(小时)', prop: 'duration'}
];
let table = '<table border="1" cellspacing="0" cellpadding="5">';
//
table += '<tr>';
columns.forEach(col => {
table += `<th>${col.label}</th>`;
});
table += '</tr>';
// 添加数据行
this.powerOutageList.forEach(item => {
table += '<tr>';
columns.forEach(col => {
table += `<td>${item[col.prop]}</td>`;
});
table += '</tr>';
});
table += '</table>';
// 创建临时HTML文件下载
const deviceName = this.selectMonitorName || '设备';
const fileName = `${deviceName}停电记录_${new Date().getTime()}.xls`;
const blob = new Blob([table], {type: 'application/vnd.ms-excel'});
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.click();
URL.revokeObjectURL(link.href);
},
/** 查询计量设备信息下拉树结构 */
getTreeselect() {
getMonitorInfoTree({ monitorType: 4 }).then(response => {
this.monitorInfoOptions = response.data
})
},
/** 筛选节点 */
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
},
/** 节点单击事件 */
handleNodeClick(data) {
this.queryParams.monitorCode = data.code
this.selectMonitorName = data.label
this.handleQuery()
},
/**
* 数据抽样处理,当数据量大时进行抽样
* @param {Array} dataArray 原始数据数组
* @returns {Array} 抽样后的数据
*/
sampleData(dataArray) {
if (!dataArray || dataArray.length <= this.samplingThreshold) {
return dataArray;
}
// 计算抽样间隔
const interval = Math.ceil(dataArray.length / this.samplingThreshold);
const sampledData = [];
// 抽样处理,保留首尾数据
for (let i = 0; i < dataArray.length; i += interval) {
sampledData.push(dataArray[i]);
}
// 确保最后一个数据点被包含
if (sampledData[sampledData.length - 1] !== dataArray[dataArray.length - 1]) {
sampledData.push(dataArray[dataArray.length - 1]);
}
return sampledData;
},
/**
* 处理时间间隔大于1小时的数据插入标红的0值点
* @param {Array} originalData 原始数据数组
* @param {string} valueField 数值字段名
* @returns {Object} 处理后的数据和标记点
*/
processDataBreaks(originalData, valueField) {
// 先进行数据抽样处理
const sampledData = this.sampleData(originalData);
if (!sampledData || sampledData.length < 2) {
return {
processedData: sampledData || [],
timeData: sampledData ? sampledData.map(e => e.recordTime) : [],
valueData: sampledData ? sampledData.map(e => e[valueField]) : [],
markPoints: [],
markAreas: []
}
}
// 按时间排序(确保数据按时间顺序)
const sortedData = [...sampledData].sort((a, b) => {
return new Date(a.recordTime) - new Date(b.recordTime)
})
const processedData = []
const markPoints = []
const markAreas = []
const powerOutages = []
const oneHourMs = 4 * 60 * 60 * 1000 // 4小时的毫秒数
// 遍历并处理数据
for (let i = 0; i < sortedData.length; i++) {
// 添加当前数据点
processedData.push(sortedData[i])
// 检查是否有数据中断
if (i < sortedData.length - 1) {
const currentTime = new Date(sortedData[i].recordTime).getTime()
const nextTime = new Date(sortedData[i + 1].recordTime).getTime()
const timeDiff = nextTime - currentTime
// 如果时间间隔大于2小时插入断点
if (timeDiff > oneHourMs) {
// 创建断点 - 使用null值强制图表线断开
// 当前点之后的断点
const breakTime1 = new Date(currentTime + 60000)
const breakTime1Str = parseTime(breakTime1, '{y}-{m}-{d} {h}:{i}:{s}')
const nullPoint1 = {
recordTime: breakTime1Str,
[valueField]: null, // 使用null而不是0
isBreakPoint: true
}
processedData.push(nullPoint1)
// 下一个点之前的断点
const breakTime2 = new Date(nextTime - 60000)
const breakTime2Str = parseTime(breakTime2, '{y}-{m}-{d} {h}:{i}:{s}')
const nullPoint2 = {
recordTime: breakTime2Str,
[valueField]: null, // 使用null而不是0
isBreakPoint: true
}
processedData.push(nullPoint2)
const endBreakTime = new Date(nextTime - 60000)
const endBreakTimeStr = parseTime(endBreakTime, '{y}-{m}-{d} {h}:{i}:{s}')
// 添加标记点信息
markPoints.push({
value: '停电',
xAxis: breakTime1Str,
yAxis: 0,
symbol: 'path://M11.184 6.6C10.744 5.04 9.252 4 7.5 4s-3.244 1.04-3.684 2.6l-3.755 9.96A.5.5 0 0 0 .5 17h3.882a.5.5 0 0 0 .474-.65l-.333-1h6.954l-.333 1A.5.5 0 0 0 11.618 17H15.5a.5.5 0 0 0 .46-.69l-3.776-9.71zm1.372 8.15l-1.087-2.792A.5.5 0 0 0 11 11.5H4a.5.5 0 0 0-.47.342l-1.087 2.917h-1.3l3.446-9.13C5.819 4.673 6.64 4 7.5 4s1.68.673 1.908 1.63l3.446 9.13h-1.298z',
symbolSize: 30,
symbolOffset: [0, '-50%'],
itemStyle: {
color: 'red'
}
})
// 标记区域
markAreas.push([
{
xAxis: breakTime1Str,
itemStyle: { color: 'rgba(255, 0, 0, 0.1)' }
},
{
xAxis: endBreakTimeStr,
}
])
// 记录停电信息
const durationHours = (timeDiff / (1000 * 60 * 60)).toFixed(2);
powerOutages.push({
startTime: parseTime(new Date(currentTime), '{y}-{m}-{d} {h}:{i}:{s}'),
endTime: parseTime(new Date(nextTime), '{y}-{m}-{d} {h}:{i}:{s}'),
duration: durationHours
});
}
}
}
// 更新停电统计信息
this.updatePowerOutageSummary(powerOutages);
// 提取时间和数值数据
const timeData = processedData.map(e => e.recordTime)
const valueData = processedData.map(e => e[valueField])
// 计算平均值时排除中断点和无效值
const validData = processedData.filter(item => !item.isBreakPoint)
let validValues = validData.map(e => parseFloat(e[valueField]))
// 针对特定字段,如果业务上它们不应为负,则过滤掉负值 (在 NaN 过滤之前)
if (valueField === 'fluxFlow' || valueField === 'press' || valueField === 'temperature') {
validValues = validValues.filter(v => typeof v === 'number' && v >= 0);
}
// 进一步过滤掉 NaN 值
const validNumericValues = validValues.filter(v => !isNaN(v));
const average = validNumericValues.length > 0 ?
(validNumericValues.reduce((a, b) => a + b, 0) / validNumericValues.length).toFixed(2) : 0
return {
processedData,
timeData,
valueData,
markPoints,
markAreas,
average
}
},
/**
* 更新停电统计信息
* @param {Array} powerOutages 停电记录数组
*/
updatePowerOutageSummary(powerOutages) {
if (!powerOutages || powerOutages.length === 0) {
this.powerOutageSummary = {
count: 0,
totalDuration: 0,
longestDuration: 0
};
this.powerOutageList = [];
return;
}
// 计算总停电时长和最长停电时长
let totalDuration = 0;
let longestDuration = 0;
powerOutages.forEach((outage, index) => {
const duration = parseFloat(outage.duration);
totalDuration += duration;
longestDuration = Math.max(longestDuration, duration);
});
// 更新停电统计信息
this.powerOutageSummary = {
count: powerOutages.length,
totalDuration: totalDuration.toFixed(2),
longestDuration: longestDuration.toFixed(2)
};
// 更新停电详情列表
this.powerOutageList = powerOutages.map((outage, index) => ({
index: index + 1,
startTime: outage.startTime,
endTime: outage.endTime,
duration: outage.duration
}));
},
/**
* 创建图表配置
* @param {String} title 图表标题
* @param {Object} dataResult 处理后的数据结果
* @param {String} name 数据名称
* @param {String} yAxisName Y轴名称
* @param {String} color 图表颜色
* @param {Function} tooltipFormatter 提示格式化函数
* @returns {Object} 图表配置
*/
createChartOption(title, dataResult, name, yAxisName, color, tooltipFormatter) {
const option = JSON.parse(JSON.stringify(this.baseChartOptions));
// 设置标题
option.title = {
text: this.selectMonitorName + ' ' + title + ' (平均值:' + dataResult.average + ")",
x: 'center',
textStyle: {
fontSize: 15
}
};
// 设置X轴数据
option.xAxis.data = dataResult.timeData;
// 设置Y轴名称
option.yAxis.name = yAxisName;
option.yAxis.nameTextStyle = {
color: '#000000'
};
// 针对特定字段如果业务上它们不应为负则强制Y轴从0开始
if (name === '瞬时流量' || name === '压力' || name === '温度') {
option.yAxis.min = 0;
} else {
// 保留之前的逻辑或让Echarts自动计算 (如果其他图表需要不同处理)
if (dataResult.processedData.some(item => item.isBreakPoint)) {
option.yAxis.min = 0;
}
// 若希望Echarts自动计算其他图表的min可以不设置或显式提供回调
// else {
// option.yAxis.min = function(value) { return value.min; };
// }
}
// 设置提示格式化
option.tooltip.formatter = tooltipFormatter || function(params) {
const dataIndex = params[0].dataIndex;
const isBreakPoint = dataResult.processedData[dataIndex] && dataResult.processedData[dataIndex].isBreakPoint;
if (isBreakPoint) {
return '停电<br/>时间: ' + params[0].axisValue;
}
return params[0].seriesName + ': ' + params[0].value + '<br/>时间: ' + params[0].axisValue;
};
// 设置数据系列
option.series = [{
name: name,
type: 'line',
smooth: false,
showAllSymbol: true,
symbol: 'circle',
symbolSize: 4,
step: 'end',
connectNulls: false,
itemStyle: {
normal: {
color: function(params) {
return dataResult.processedData[params.dataIndex] && dataResult.processedData[params.dataIndex].isBreakPoint ? 'red' : color;
}
}
},
data: dataResult.valueData,
markPoint: {
data: dataResult.markPoints,
symbolSize: 60,
label: {
color: '#000000',
fontSize: 14,
fontWeight: 'bold',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
borderColor: '#ff0000',
borderWidth: 1,
borderRadius: 4,
padding: [4, 8]
}
},
markArea: {
data: dataResult.markAreas,
silent: true
},
markLine: {
silent: true,
symbol: 'none',
lineStyle: {
color: '#ff0000',
width: 2,
type: 'dashed'
},
label: {
show: false
},
data: dataResult.markPoints.map(point => ({
xAxis: point.xAxis
}))
}
}];
return option;
},
/** 曲线 */
async getChart() {
this.loading = true;
let query = JSON.parse(JSON.stringify(this.queryParams))
try {
const {data} = await steamInstantList(query)
// 处理瞬时流量数据
const fluxFlowResult = this.processDataBreaks(data, 'fluxFlow')
// 处理温度数据
const temperatureResult = this.processDataBreaks(data, 'temperature')
// 处理压力数据
const pressResult = this.processDataBreaks(data, 'press')
// 创建图表配置
const option1 = this.createChartOption(
'瞬时流量',
fluxFlowResult,
'瞬时流量',
'瞬时流量y',
'#5470c6'
);
const option2 = this.createChartOption(
'温度',
temperatureResult,
'温度',
'温度y',
'#91cc75'
);
const option3 = this.createChartOption(
'压力',
pressResult,
'压力',
'压力y',
'#fac858'
);
this.$refs.Chart1.setData(option1)
this.$refs.Chart2.setData(option2)
this.$refs.Chart3.setData(option3)
echarts.connect(this.$refs.Chart1.chart, this.$refs.Chart2.chart, this.$refs.Chart3.chart)
// 同步缩放
this.$refs.Chart1.chart.on('datazoom', (e) => {
option2.dataZoom[0].start = e.start
option2.dataZoom[0].end = e.end
this.$refs.Chart2.setData(option2)
option3.dataZoom[0].start = e.start
option3.dataZoom[0].end = e.end
this.$refs.Chart3.setData(option3)
})
this.$refs.Chart2.chart.on('datazoom', (e) => {
option1.dataZoom[0].start = e.start
option1.dataZoom[0].end = e.end
this.$refs.Chart1.setData(option1)
option3.dataZoom[0].start = e.start
option3.dataZoom[0].end = e.end
this.$refs.Chart3.setData(option3)
})
this.$refs.Chart3.chart.on('datazoom', (e) => {
option2.dataZoom[0].start = e.start
option2.dataZoom[0].end = e.end
this.$refs.Chart2.setData(option2)
option1.dataZoom[0].start = e.start
option1.dataZoom[0].end = e.end
this.$refs.Chart1.setData(option1)
})
} catch (error) {
console.error('获取图表数据失败:', error)
this.$message.error('获取图表数据失败')
} finally {
this.loading = false
}
},
/** 查询树形结构 */
getTreeMonitorInfo() {
getMonitorInfoTree({ monitorType: 4 }).then(response => {
this.baseMonitorInfoOptions = this.handleTree(response.data, "id", "parentId")
})
}
}
}
</script>
<style scoped >
.app-container {
padding: 10px;
}
.charts-container {
margin-top: 10px;
}
.chart1 {
width: 50%;
height: 40vh;
display: inline-block;
}
.chart2 {
width: 50%;
height: 40vh;
display: inline-block;
}
.chart3 {
width: 50%;
height: 40vh;
display: inline-block;
}
.power-outage-summary {
margin-bottom: 10px;
}
.power-outage-info {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
.power-outage-info p {
margin: 5px 10px;
}
/* 选中节点的样式 */
::v-deep .el-tree .el-tree-node.is-current > .el-tree-node__content {
background-color: #67C23A !important; /* 选中节点的背景色 */
color: #303133 !important; /* 选中节点的文字颜色 */
padding: 0; /* 清除内边距 */
margin: 0; /* 清除外边距 */
z-index: 1; /* 确保在其他元素之上 */
}
/* 鼠标悬停时的样式 */
.el-tree .el-tree-node__content:hover {
background-color: #e6f7ff; /* 鼠标悬停时的背景色 */
}
/* 闪电图标 */
.el-icon-lightning:before {
content: "\e6a8"; /* 使用现有的element-ui图标字体 */
}
</style>