|
|
@@ -4,6 +4,7 @@ import { cancelAllRequests, IotStatApi } from '@/api/pms/stat'
|
|
|
import { useTableComponents } from '@/components/ZmTable/useTableComponents'
|
|
|
import { useAlarmSound } from '@/utils/useAlarmSound'
|
|
|
import { useMqtt } from '@/utils/useMqtt'
|
|
|
+import { getWindowThresholdAlarm } from '@/utils/thresholdAlarmWindow'
|
|
|
import {
|
|
|
Dimensions,
|
|
|
formatChartValue,
|
|
|
@@ -91,6 +92,9 @@ interface ThresholdAlarm {
|
|
|
name: string
|
|
|
value: number
|
|
|
threshold: number
|
|
|
+ baseThreshold: number
|
|
|
+ duration: number
|
|
|
+ range: number
|
|
|
direction: 'high' | 'low'
|
|
|
suffix?: string
|
|
|
}
|
|
|
@@ -102,7 +106,14 @@ function getThresholdAlarmText(alarm: ThresholdAlarm) {
|
|
|
const directionText = alarm.direction === 'high' ? '高于上限' : '低于下限'
|
|
|
const suffix = alarm.suffix || ''
|
|
|
|
|
|
- return `${alarm.name}:当前 ${formatChartValue(alarm.value)}${suffix},${directionText} ${formatChartValue(alarm.threshold)}${suffix}`
|
|
|
+ const rangeText =
|
|
|
+ alarm.range > 0
|
|
|
+ ? `(阈值 ${formatChartValue(alarm.baseThreshold)}${suffix},超限范围 ${formatChartValue(alarm.range)}${suffix})`
|
|
|
+ : ''
|
|
|
+ const durationText = alarm.duration > 0 ? `(最近 ${formatChartValue(alarm.duration)} 秒内)` : ''
|
|
|
+ const valueText = alarm.duration > 0 ? '出现' : '当前'
|
|
|
+
|
|
|
+ return `${alarm.name}:${durationText}${valueText} ${formatChartValue(alarm.value)}${suffix},${directionText} ${formatChartValue(alarm.threshold)}${suffix}${rangeText}`
|
|
|
}
|
|
|
|
|
|
function clearThresholdAlarms() {
|
|
|
@@ -145,6 +156,7 @@ function getPointTimestamp(value: unknown) {
|
|
|
const handleMessageUpdate = (_topic: string, data: any) => {
|
|
|
const list = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : []
|
|
|
const valueMap = new Map<string, number>()
|
|
|
+ const pointMap = new Map<string, ChartPoint>()
|
|
|
const updatedNames = new Set<string>()
|
|
|
|
|
|
for (const item of list) {
|
|
|
@@ -154,23 +166,23 @@ const handleMessageUpdate = (_topic: string, data: any) => {
|
|
|
if (!identity || !Number.isFinite(value)) continue
|
|
|
|
|
|
valueMap.set(identity, value)
|
|
|
+ const point = {
|
|
|
+ ts: getPointTimestamp(item.remark || item.readTime || item.ts || item.time),
|
|
|
+ value
|
|
|
+ }
|
|
|
+ pointMap.set(identity, point)
|
|
|
|
|
|
const modelName = dimensions.value.find((item) => item.identifier === identity)?.name
|
|
|
|
|
|
if (modelName && chartData.value[modelName]) {
|
|
|
- const ts = getPointTimestamp(item.remark || item.readTime || item.ts || item.time)
|
|
|
-
|
|
|
- appendChartPoint(modelName, {
|
|
|
- ts,
|
|
|
- value
|
|
|
- })
|
|
|
+ appendChartPoint(modelName, point)
|
|
|
|
|
|
updatedNames.add(modelName)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateDimensionValues(valueMap)
|
|
|
- updateThresholdAlarm(valueMap)
|
|
|
+ updateThresholdAlarm(pointMap)
|
|
|
|
|
|
if (!updatedNames.size) return
|
|
|
|
|
|
@@ -459,36 +471,34 @@ function updateDimensionValues(valueMap: Map<string, number>) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-function updateThresholdAlarm(valueMap: Map<string, number>) {
|
|
|
+function updateThresholdAlarm(pointMap: Map<string, ChartPoint>) {
|
|
|
const nextAlarms = { ...thresholdAlarms.value }
|
|
|
|
|
|
dimensions.value.forEach((item) => {
|
|
|
- const value = valueMap.get(item.identifier)
|
|
|
- if (value === undefined) return
|
|
|
+ const point = pointMap.get(item.identifier)
|
|
|
+ if (!point) return
|
|
|
|
|
|
const min = normalizeRangeValue(item.minValue)
|
|
|
const max = normalizeRangeValue(item.maxValue)
|
|
|
+ const duration = Math.max(0, normalizeRangeValue(item.periodSecond) ?? 0)
|
|
|
+ const range = Math.max(0, normalizeRangeValue(item.changeValue) ?? 0)
|
|
|
const alarmKey = `${item.identifier}|${item.name}`
|
|
|
- if (max !== undefined && value > max) {
|
|
|
- nextAlarms[alarmKey] = {
|
|
|
- key: alarmKey,
|
|
|
- name: item.name,
|
|
|
- value,
|
|
|
- threshold: max,
|
|
|
- direction: 'high',
|
|
|
- suffix: item.suffix
|
|
|
- }
|
|
|
- } else if (min !== undefined && value < min) {
|
|
|
- nextAlarms[alarmKey] = {
|
|
|
- key: alarmKey,
|
|
|
- name: item.name,
|
|
|
- value,
|
|
|
- threshold: min,
|
|
|
- direction: 'low',
|
|
|
- suffix: item.suffix
|
|
|
- }
|
|
|
- } else {
|
|
|
+ const series = chartData.value[item.name] || [point]
|
|
|
+ const anchorTs = series[series.length - 1]?.ts ?? point.ts
|
|
|
+ const alarm = getWindowThresholdAlarm(series, anchorTs, duration, min, max, range)
|
|
|
+
|
|
|
+ if (!alarm) {
|
|
|
delete nextAlarms[alarmKey]
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ nextAlarms[alarmKey] = {
|
|
|
+ key: alarmKey,
|
|
|
+ name: item.name,
|
|
|
+ ...alarm,
|
|
|
+ duration,
|
|
|
+ range,
|
|
|
+ suffix: item.suffix
|
|
|
}
|
|
|
})
|
|
|
|
|
|
@@ -904,6 +914,8 @@ function buildRangeSettingDrafts() {
|
|
|
color: item.color,
|
|
|
minValue: normalizeRangeValue(item.minValue),
|
|
|
maxValue: normalizeRangeValue(item.maxValue),
|
|
|
+ periodSecond: normalizeRangeValue(item.periodSecond),
|
|
|
+ changeValue: normalizeRangeValue(item.changeValue),
|
|
|
axisMin: normalizeRangeValue(item.axisMin),
|
|
|
axisMax: normalizeRangeValue(item.axisMax)
|
|
|
}))
|
|
|
@@ -942,21 +954,34 @@ async function saveRangeSetting(
|
|
|
item: Dimensions,
|
|
|
minValue?: number,
|
|
|
maxValue?: number,
|
|
|
+ periodSecondValue?: number,
|
|
|
+ changeValueValue?: number,
|
|
|
axisMinValue?: number,
|
|
|
axisMaxValue?: number
|
|
|
) {
|
|
|
const min = normalizeRangeValue(minValue)
|
|
|
const max = normalizeRangeValue(maxValue)
|
|
|
+ const periodSecond = normalizeRangeValue(periodSecondValue)
|
|
|
+ const changeValue = normalizeRangeValue(changeValueValue)
|
|
|
const axisMin = normalizeRangeValue(axisMinValue)
|
|
|
const axisMax = normalizeRangeValue(axisMaxValue)
|
|
|
|
|
|
- if (min === undefined && max === undefined && axisMin === undefined && axisMax === undefined) {
|
|
|
+ if (
|
|
|
+ min === undefined &&
|
|
|
+ max === undefined &&
|
|
|
+ periodSecond === undefined &&
|
|
|
+ changeValue === undefined &&
|
|
|
+ axisMin === undefined &&
|
|
|
+ axisMax === undefined
|
|
|
+ ) {
|
|
|
if (item.id) {
|
|
|
await IotDeviceApi.deleteMaxMin({ id: item.id })
|
|
|
}
|
|
|
|
|
|
item.minValue = undefined
|
|
|
item.maxValue = undefined
|
|
|
+ item.periodSecond = undefined
|
|
|
+ item.changeValue = undefined
|
|
|
item.axisMin = undefined
|
|
|
item.axisMax = undefined
|
|
|
item.id = undefined
|
|
|
@@ -975,6 +1000,22 @@ async function saveRangeSetting(
|
|
|
throw new Error(`${item.name} 的告警上限和告警下限不能相等`)
|
|
|
}
|
|
|
|
|
|
+ if ((periodSecond === undefined) !== (changeValue === undefined)) {
|
|
|
+ throw new Error(`${item.name} 的检查时间和超限范围需要同时填写`)
|
|
|
+ }
|
|
|
+
|
|
|
+ if ((periodSecond !== undefined || changeValue !== undefined) && min === undefined) {
|
|
|
+ throw new Error(`${item.name} 需要先设置告警上限和告警下限`)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (periodSecond !== undefined && (!Number.isInteger(periodSecond) || periodSecond <= 0)) {
|
|
|
+ throw new Error(`${item.name} 的检查时间必须为大于 0 的整数秒`)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (changeValue !== undefined && changeValue < 0) {
|
|
|
+ throw new Error(`${item.name} 的超限范围不能小于 0`)
|
|
|
+ }
|
|
|
+
|
|
|
if ((axisMin === undefined) !== (axisMax === undefined)) {
|
|
|
throw new Error(`${item.name} 的 Y 轴上限和 Y 轴下限需要同时填写`)
|
|
|
}
|
|
|
@@ -986,6 +1027,8 @@ async function saveRangeSetting(
|
|
|
const body = {
|
|
|
minValue: min ?? null,
|
|
|
maxValue: max ?? null,
|
|
|
+ periodSecond: periodSecond ?? null,
|
|
|
+ changeValue: changeValue ?? null,
|
|
|
axisMin: axisMin ?? null,
|
|
|
axisMax: axisMax ?? null,
|
|
|
deviceId: props.id,
|
|
|
@@ -1000,6 +1043,8 @@ async function saveRangeSetting(
|
|
|
if (res.id) item.id = res.id
|
|
|
item.minValue = min
|
|
|
item.maxValue = max
|
|
|
+ item.periodSecond = periodSecond
|
|
|
+ item.changeValue = changeValue
|
|
|
item.axisMin = axisMin
|
|
|
item.axisMax = axisMax
|
|
|
}
|
|
|
@@ -1013,9 +1058,18 @@ async function handleSettingsSave() {
|
|
|
const item = dimensions.value.find((dimension) => dimension.identifier === draft.identifier)
|
|
|
if (!item) continue
|
|
|
|
|
|
- await saveRangeSetting(item, draft.minValue, draft.maxValue, draft.axisMin, draft.axisMax)
|
|
|
+ await saveRangeSetting(
|
|
|
+ item,
|
|
|
+ draft.minValue,
|
|
|
+ draft.maxValue,
|
|
|
+ draft.periodSecond,
|
|
|
+ draft.changeValue,
|
|
|
+ draft.axisMin,
|
|
|
+ draft.axisMax
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
+ clearThresholdAlarms()
|
|
|
updateSeriesByNames(dimensions.value.map((item) => item.name))
|
|
|
settingsDialogVisible.value = false
|
|
|
message.success('设置成功')
|
|
|
@@ -1031,6 +1085,8 @@ function handleRangeDialogReset() {
|
|
|
...item,
|
|
|
minValue: undefined,
|
|
|
maxValue: undefined,
|
|
|
+ periodSecond: undefined,
|
|
|
+ changeValue: undefined,
|
|
|
axisMin: undefined,
|
|
|
axisMax: undefined
|
|
|
}))
|
|
|
@@ -1039,6 +1095,8 @@ function handleRangeDialogReset() {
|
|
|
function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
row.minValue = undefined
|
|
|
row.maxValue = undefined
|
|
|
+ row.periodSecond = undefined
|
|
|
+ row.changeValue = undefined
|
|
|
row.axisMin = undefined
|
|
|
row.axisMax = undefined
|
|
|
}
|
|
|
@@ -1109,7 +1167,7 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
<el-dialog
|
|
|
v-model="settingsDialogVisible"
|
|
|
title="设备设置"
|
|
|
- width="1100px"
|
|
|
+ width="min(1140px, calc(100vw - 32px))"
|
|
|
class="board-settings-dialog"
|
|
|
modal-class="board-settings-overlay"
|
|
|
append-to-body
|
|
|
@@ -1125,19 +1183,24 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
</el-form>
|
|
|
|
|
|
<div class="board-settings-section">
|
|
|
- <div class="board-settings-section__title">告警阈值与 Y 轴范围</div>
|
|
|
- <ZmTable :data="rangeSettingDrafts" :loading="false" :max-height="420" :show-border="true">
|
|
|
- <ZmTableColumn label="曲线" min-width="180">
|
|
|
+ <div class="board-settings-section__title">告警阈值、检查时间、超限范围与 Y 轴范围</div>
|
|
|
+ <ZmTable
|
|
|
+ :data="rangeSettingDrafts"
|
|
|
+ :loading="false"
|
|
|
+ :max-height="420"
|
|
|
+ :show-border="true"
|
|
|
+ :fit="false">
|
|
|
+ <ZmTableColumn label="曲线" width="210" fixed="left" align="left">
|
|
|
<template #default="{ row }">
|
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
|
<span
|
|
|
class="inline-block size-2.5 rounded-full shrink-0"
|
|
|
:style="{ backgroundColor: row.color }"></span>
|
|
|
- <span class="board-settings-line-name">{{ row.name }}</span>
|
|
|
+ <span class="board-settings-line-name" :title="row.name">{{ row.name }}</span>
|
|
|
</div>
|
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="告警下限" width="140">
|
|
|
+ <ZmTableColumn label="告警下限" width="115">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
|
v-model="row.minValue"
|
|
|
@@ -1146,7 +1209,7 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
placeholder="未设置" />
|
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="告警上限" width="140">
|
|
|
+ <ZmTableColumn label="告警上限" width="115">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
|
v-model="row.maxValue"
|
|
|
@@ -1155,7 +1218,28 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
placeholder="未设置" />
|
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="Y 轴下限" width="140">
|
|
|
+ <ZmTableColumn label="检查时间(秒)" width="125">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input-number
|
|
|
+ v-model="row.periodSecond"
|
|
|
+ class="!w-full"
|
|
|
+ :controls="false"
|
|
|
+ :min="1"
|
|
|
+ :precision="0"
|
|
|
+ placeholder="仅当前值" />
|
|
|
+ </template>
|
|
|
+ </ZmTableColumn>
|
|
|
+ <ZmTableColumn label="超限范围" width="115">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-input-number
|
|
|
+ v-model="row.changeValue"
|
|
|
+ class="!w-full"
|
|
|
+ :controls="false"
|
|
|
+ :min="0"
|
|
|
+ placeholder="0" />
|
|
|
+ </template>
|
|
|
+ </ZmTableColumn>
|
|
|
+ <ZmTableColumn label="Y 轴下限" width="110">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
|
v-model="row.axisMin"
|
|
|
@@ -1164,7 +1248,7 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
:placeholder="formatChartValue(getSeriesRange(row.name).labelMin)" />
|
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="Y 轴上限" width="140">
|
|
|
+ <ZmTableColumn label="Y 轴上限" width="110">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
|
v-model="row.axisMax"
|
|
|
@@ -1173,7 +1257,7 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
:placeholder="formatChartValue(getSeriesRange(row.name).labelMax)" />
|
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="当前 Y 轴范围" width="150">
|
|
|
+ <ZmTableColumn label="当前 Y 轴范围" width="120">
|
|
|
<template #default="{ row }">
|
|
|
<span class="board-settings-range-text">
|
|
|
{{ formatChartValue(getSeriesRange(row.name).labelMin) }}
|
|
|
@@ -1182,7 +1266,7 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
</span>
|
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="操作" width="90">
|
|
|
+ <ZmTableColumn label="操作" width="66">
|
|
|
<template #default="{ row }">
|
|
|
<el-button size="small" link type="primary" @click="clearRangeDraft(row)">
|
|
|
清空
|
|
|
@@ -1298,6 +1382,14 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
color: #334155;
|
|
|
}
|
|
|
|
|
|
+.board-settings-line-name {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
.chart-main {
|
|
|
display: grid;
|
|
|
grid-template-columns: auto minmax(0, 1fr);
|