Files
admin-sjzx/src/views/pqs/runManage/cleaning/components/abnormal.vue
2025-07-17 13:57:46 +08:00

675 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div>
<TableHeader date-picker area ref="TableHeaderRef">
<template v-slot:select>
<el-form-item label="对象类型">
<el-select
v-model="tableStore.table.params.objType"
clearable
style="width: 100%"
placeholder="请选择对象类型"
>
<el-option
v-for="item in objTypeList"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="终端厂家:">
<el-select
v-model="tableStore.table.params.manufacturer"
clearable
placeholder="请选择终端厂家"
>
<el-option
v-for="item in terminaloption"
:key="item.id"
:label="item.name"
:value="item.id"
></el-option>
</el-select>
</el-form-item>
</template>
<template v-slot:operation></template>
</TableHeader>
<div class="card-list pt10" v-loading="loading">
<div class="monitoringPoints">
<el-card style="height: 200px">
<template #header>
<div class="card-header">
<span>监测点统计</span>
</div>
</template>
<div>
<div class="statistics">
<div class="divBox">
<span class="iconfont icon-qiyezongshu" style="color: #57bc6e"></span>
<span class="divBox_title">监测点总数</span>
<span class="divBox_num" style="color: #57bc6e">{{ monitoringPoints.runNum }}</span>
</div>
<div class="divBox" style="width: 200px">
<span class="iconfont icon-igw-f-warning-data" style="color: #ff6600"></span>
<span class="divBox_title">异常测点数</span>
<span class="divBox_num" style="color: #ff6600">
{{ monitoringPoints.abnormalNum }}
</span>
</div>
</div>
<div class="echartTitle">
<div>异常占比</div>
<div>
{{
isNaN((monitoringPoints.abnormalNum / monitoringPoints.runNum) * 100)
? 0
: ((monitoringPoints.abnormalNum / monitoringPoints.runNum) * 100).toFixed(2)
}}%
</div>
</div>
<div style="height: 30px">
<MyEchart :options="percentage"></MyEchart>
</div>
</div>
</el-card>
<el-card class="mt10">
<template #header>
<div class="card-header">
<span>异常指标统计</span>
</div>
</template>
<div class="mb5" style="height: 40px">
<el-segmented style="height: 100%" v-model="segmented" :options="segmentedList" block>
<template #default="scope">
<div>
<div
class="segmentedIcon"
:style="{
backgroundColor:
scope.item.num > 0
? '#FF9100'
: scope.item.num > 99
? '#ff0000'
: '#007D7B'
}"
>
{{ scope.item.num > 99 ? '99+' : scope.item.num }}
</div>
<div>{{ scope.item.label }}</div>
</div>
</template>
</el-segmented>
</div>
<div class="header">
<span style="width: 170px; text-align: left">指标名称</span>
<span style="flex: 1">合理范围</span>
<span style="width: 90px">异常测点数</span>
</div>
<div :style="indicatorHeight" style="overflow-y: auto">
<div v-for="o in abnormal.filter(item => item.remark == segmented)" class="abnormal mb10">
<span style="width: 170px; height: 24px" class="iconDiv">
<div :style="{ backgroundColor: o.ids.length > 0 ? '#FF9100' : '' }"></div>
{{ o.targetName }}
</span>
<span style="flex: 1; text-align: center">
<!-- 合理范围 -->
<span style="color: #388e3c" class="text">{{ o.rangeDesc }}</span>
</span>
<span style="width: 90px; text-align: center">
<span
style="color: #388e3c"
:class="` ${o.ids.length > 0 ? 'text-red' : ''}`"
class="text"
@click="quantityClick(o, 0)"
>
{{ o.ids.length }}
</span>
</span>
</div>
</div>
</el-card>
</div>
<el-card class="detail ml10" :style="pageHeight">
<template #header>
<div class="card-header">
<span>异常详情统计</span>
</div>
</template>
<div style="height: 350px">
<MyEchart :options="options"></MyEchart>
</div>
<el-form :inline="true" class="form">
<el-form-item label="异常持续天数"></el-form-item>
<el-form-item label="异常阀值(天)">
<el-input-number
v-model="tableStore.table.params.alarmDayLimit"
:min="0"
:step="1"
step-strictly
/>
</el-form-item>
<el-form-item label="预警阀值(天)">
<el-input-number
v-model="tableStore.table.params.warnDayLimit"
:min="0"
:step="1"
step-strictly
/>
</el-form-item>
<el-form-item class="form_but">
<el-button type="primary" @click="MonitorVerify" icon="el-icon-Refresh">更新</el-button>
<el-button type="primary" @click="onExport" icon="el-icon-Download">导出</el-button>
</el-form-item>
</el-form>
<!--表格-->
<div :style="{ height: tableStore.table.height }" v-loading="tableStore.table.loading">
<vxe-table height="auto" :data="tableStore.table.data" v-bind="defaultAttribute" ref="tableRef">
<vxe-column type="seq" title="序号" width="80px">
<template #default="{ rowIndex }">
<span>
{{
(tableStore.table.params.pageNum - 1) * tableStore.table.params.pageSize +
rowIndex +
1
}}
</span>
</template>
</vxe-column>
<vxe-column field="monitorName" title="监测点名称"></vxe-column>
<vxe-column field="manufacturer" title="终端厂家"></vxe-column>
<vxe-column field="devName" title="所属终端名称"></vxe-column>
<vxe-column field="ip" title="IP">
<template #default="{ row }">
{{ decryptFromBase64(row.ip) }}
</template>
</vxe-column>
<vxe-column field="stationName" title="所属电站"></vxe-column>
<vxe-column field="objType" title="监测对象类型" :formatter="formatter"></vxe-column>
<vxe-column field="objName" title="监测对象名称" :formatter="formatter"></vxe-column>
<vxe-column field="voltageLevel" title="电压等级"></vxe-column>
<vxe-column field="abnormalDay" title="异常天数">
<template #default="{ row }">
<span class="table_name" @click="quantityClick(row, 1)">
{{ row.abnormalDay }}
</span>
</template>
</vxe-column>
<vxe-column field="abnormalDay" title="严重度">
<template #default="{ row }">
<el-tag type="warning" v-if="row.severity == 0">预警</el-tag>
<el-tag type="danger" v-if="row.severity == 1">告警</el-tag>
</template>
</vxe-column>
</vxe-table>
</div>
</el-card>
</div>
<anomalyDetails ref="anomalyDetailsRef" />
</div>
</template>
<script setup lang="ts">
import TableStore from '@/utils/tableStore'
import Table from '@/components/table/index.vue'
import TableHeader from '@/components/table/header/index.vue'
import { onMounted, provide, ref } from 'vue'
import { defaultAttribute } from '@/components/table/defaultAttribute'
import { useDictData } from '@/stores/dictData'
import { mainHeight } from '@/utils/layout'
import MyEchart from '@/components/echarts/MyEchart.vue'
import anomalyDetails from './anomalyDetails.vue'
import { getMonitorVerifyDay } from '@/api/device-boot/dataVerify'
import { queryFirstNode } from '@/api/auth'
import { decryptFromBase64 } from '@/utils/index'
const anomalyDetailsRef = ref()
const dictData = useDictData()
//字典获取监督对象类型
const objTypeList: any = ref([])
const pageHeight = mainHeight(97)
const indicatorHeight = mainHeight(447)
const monitoringPoints = ref({
runNum: 0,
abnormalNum: 0
})
const tableRef = ref()
const percentage = ref({})
const loading = ref(false)
const TableHeaderRef = ref()
const abnormal: any = ref([])
const mapList: any = ref([])
const segmented = ref('base')
const time = ref(['', ''])
const terminaloption = dictData.getBasicData('Dev_Manufacturers')
const segmentedList = ref([
{
label: '基础指标',
value: 'base',
num: 0
},
{
label: '稳态指标',
value: 'harmonic',
num: 0
},
{
label: '暂态指标',
value: 'event',
num: 0
}
])
const tableStore = new TableStore({
url: '/device-boot/dataVerify/getMonitorVerifyData',
method: 'POST',
showPage: false,
filename: '异常数据统计',
publicHeight: 470,
column: [],
beforeSearchFun: () => {
loading.value = true
tableStore.table.params.deptId = tableStore.table.params.deptIndex
time.value = [tableStore.table.params.startTime, tableStore.table.params.endTime]
},
loadCallback: () => {
segmentedList.value[0].num = 0
segmentedList.value[1].num = 0
segmentedList.value[2].num = 0
monitoringPoints.value.runNum = tableStore.table.data.runNum //总数
monitoringPoints.value.abnormalNum = tableStore.table.data.abnormalNum //异常测点数
abnormal.value = tableStore.table.data.targetList
mapList.value = tableStore.table.data.mapList
tableStore.table.allData = tableStore.table.data.monitorAlarmInfo
tableStore.table.data = tableStore.table.data.monitorAlarmInfo
abnormal.value.forEach(item => {
const { remark, ids } = item
if (remark === 'base') segmentedList.value[0].num += ids.length
else if (remark === 'harmonic') segmentedList.value[1].num += ids.length
else if (remark === 'event') segmentedList.value[2].num += ids.length
})
echart()
loading.value = false
}
})
const options = ref({})
const echart = () => {
percentage.value = {
color: ['#FF9100'],
options: {
dataZoom: null,
toolbox: {
show: false
},
grid: {
top: '0%',
left: '0%',
right: '0%',
bottom: '0%'
},
tooltip: {
show: false
},
legend: {
show: false
},
yAxis: {
show: false,
data: ['']
},
xAxis: [
{
show: false,
type: 'value'
}
],
series: [
{
name: '异常总数',
type: 'bar',
barWidth: 12,
data: [100],
z: 0,
zlevel: 0,
itemStyle: {
normal: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{
offset: 1,
color: '#57bc6e' // 100% 处的颜色
}
],
global: false // 缺省为 false
}
}
}
},
{
name: '异常占比',
type: 'bar',
barWidth: 13,
data: [
(monitoringPoints.value.abnormalNum / monitoringPoints.value.runNum) * 100 == 0
? ''
: ((monitoringPoints.value.abnormalNum / monitoringPoints.value.runNum) * 100).toFixed(2)
],
z: 0,
zlevel: 0,
itemStyle: {
normal: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 1,
y2: 0,
colorStops: [
{
offset: 0,
color: '#FF9100' // 0% 处的颜色
},
{
offset: 1,
color: '#FF9100' // 100% 处的颜色
}
],
global: false // 缺省为 false
}
}
}
},
{
type: 'pictorialBar',
itemStyle: {
normal: {
color: '#fff'
}
},
symbolRepeat: 50,
// symbolMargin: 300,
symbol: 'rect',
symbolClip: true,
symbolSize: [2, 20],
symbolPosition: 'start',
symbolOffset: [0, 0],
data: [100],
z: 1,
zlevel: 0
},
{
name: '',
type: 'bar',
barGap: '-110%',
data: [100],
barWidth: 18,
itemStyle: {
normal: {
color: 'transparent',
barBorderColor: 'rgb(148,217,249)',
barBorderWidth: 1
}
},
z: 2
}
]
}
}
options.value = {
title: {
text: `监测点异常统计(${mapList.value[0].time} ~ ${mapList.value[mapList.value.length - 1].time})`
},
xAxis: {
name: '时间',
axisLine: {
show: true
},
data: mapList.value.map(item => item.time),
axisLabel: {
formatter: function (value) {
let time = ''
if (value.slice(-2) == '01') {
time = value
} else {
time = value.slice(5, 10)
}
return time
}
}
},
yAxis: {
name: '数量' // 给X轴加单位
},
grid: {},
color: ['#FF9100'],
options: {
series: [
{
name: '异常监测点数量',
type: 'bar',
data: mapList.value.map(item => item.val)
// mapList.value.map(item => ({
// value: [item.time, item.val]
// }))
}
]
}
}
}
tableStore.table.params.objType = ''
tableStore.table.params.alarmDayLimit = 5
tableStore.table.params.warnDayLimit = 1
tableStore.table.params.manufacturer = ''
const quantityClick = (e: any, num: number) => {
if (num == 0 && e.ids?.length == 0) return
anomalyDetailsRef.value.open(e, time.value, num)
}
// 更新
const MonitorVerify = () => {
tableStore.table.loading = true
getMonitorVerifyDay(tableStore.table.params).then(res => {
tableStore.table.data = res.data
tableStore.table.allData = res.data
tableStore.table.loading = false
})
}
const formatter = (row: any) => {
return row.cellValue || '/'
}
// 导出
const onExport = () => {
tableRef.value?.exportData({
filename: tableStore.table.filename, // 文件名字
sheetName: 'Sheet1',
type: 'xlsx', //导出文件类型 xlsx 和 csv
useStyle: true,
data: tableStore.table.data, // 数据源 // 过滤那个字段导出
columnFilterMethod: function (column: any) {
return !(
column.column.title === undefined ||
column.column.title === '序号' ||
column.column.title === '操作'
)
}
})
}
onMounted(() => {
// TableHeaderRef.value.setDatePicker([
// { label: '年份', value: 1 },
// { label: '季度', value: 2 },
// { label: '月份', value: 3 }
// ])
queryFirstNode({ type: 0 }).then(res => {
objTypeList.value = res.data
})
// 加载数据
tableStore.index()
})
tableStore.table.params.name = ''
provide('tableStore', tableStore)
</script>
<style lang="scss" scoped>
.card-list {
display: flex;
.monitoringPoints {
width: 440px;
position: relative;
.statistics {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
margin-bottom: 10px;
.divBox {
width: 200px;
height: 70px;
padding: 10px;
display: flex;
.iconfont {
font-size: 40px;
margin-right: 5px;
}
.divBox_title {
font-weight: 550;
}
.divBox_num {
font-size: 20px;
font-weight: 550;
margin-left: auto;
font-family: AlimamaDongFangDaKai;
}
align-items: center;
// text-align: center;
border-radius: 5px;
&:nth-child(1) {
background-color: #eef8f0;
}
&:nth-child(2) {
background-color: #fff6ed;
}
&:nth-child(3) {
background-color: #e5f8f6;
}
}
}
}
.detail {
flex: 1;
}
.abnormal {
width: 100%;
background-color: #f3f6f9;
border-radius: 5px;
display: flex;
// justify-content: space-between;
align-items: center;
padding: 5px 0px 5px 10px;
.iconDiv {
display: flex;
align-items: center;
div {
width: 4px;
height: 18px;
margin-right: 5px;
background-color: var(--el-color-primary);
}
}
.text {
font-weight: 700;
font-size: 16px;
font-family: 'Source Code Pro', monospace;
// font-feature-settings: 'tnum';
}
}
}
.header {
display: flex;
text-align: center;
font-weight: 700;
padding: 5px;
}
:deep(.el-card__header) {
padding: 10px;
span {
font-weight: 600;
}
}
:deep(.el-card__body) {
padding: 10px;
}
.iconFont {
font-size: 18px;
display: inline-block;
vertical-align: middle;
}
.form {
position: relative;
.form_but {
position: absolute;
right: -22px;
}
}
.card-header {
font-size: 16px;
}
:deep(.table_name) {
color: var(--el-color-primary);
cursor: pointer;
text-underline-offset: 4px;
}
.echartTitle {
display: flex;
justify-content: space-between;
font-size: 14px;
font-weight: 600;
div:nth-child(2) {
font-size: 16px;
color: #ff6600;
}
}
:deep(.el-segmented__item-selected, ) {
clip-path: polygon(10% 0, 100% 0, 90% 100%, 0 100%);
}
:deep(.el-segmented__item, ) {
clip-path: polygon(10% 0, 100% 0, 90% 100%, 0 100%);
position: relative;
}
:deep(.el-segmented) {
clip-path: polygon(4% 0, 100% 0, 96% 100%, 0 100%);
}
.text-red {
color: #ff9100 !important;
cursor: pointer;
text-decoration: underline;
}
.segmentedIcon {
position: absolute;
top: 1px;
left: 100px;
height: 18px !important;
line-height: 19px;
padding: 0 4px;
font-size: 12px;
background: #ff9100;
color: #fff;
border-radius: 8px;
}
</style>