|
|
@@ -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,
|
|
|
@@ -100,10 +101,6 @@ interface ThresholdAlarm {
|
|
|
|
|
|
const thresholdAlarms = shallowRef<Record<string, ThresholdAlarm>>({})
|
|
|
const activeThresholdAlarms = computed(() => Object.values(thresholdAlarms.value))
|
|
|
-const pendingThresholdStarts = new Map<
|
|
|
- string,
|
|
|
- { direction: ThresholdAlarm['direction']; startedAt: number }
|
|
|
->()
|
|
|
|
|
|
function getThresholdAlarmText(alarm: ThresholdAlarm) {
|
|
|
const directionText = alarm.direction === 'high' ? '高于上限' : '低于下限'
|
|
|
@@ -113,14 +110,14 @@ function getThresholdAlarmText(alarm: ThresholdAlarm) {
|
|
|
alarm.range > 0
|
|
|
? `(阈值 ${formatChartValue(alarm.baseThreshold)}${suffix},超限范围 ${formatChartValue(alarm.range)}${suffix})`
|
|
|
: ''
|
|
|
- const durationText = alarm.duration > 0 ? `并持续 ${formatChartValue(alarm.duration)} 秒` : ''
|
|
|
+ const durationText = alarm.duration > 0 ? `(最近 ${formatChartValue(alarm.duration)} 秒内)` : ''
|
|
|
+ const valueText = alarm.duration > 0 ? '出现' : '当前'
|
|
|
|
|
|
- return `${alarm.name}:当前 ${formatChartValue(alarm.value)}${suffix},${directionText} ${formatChartValue(alarm.threshold)}${suffix}${durationText}${rangeText}`
|
|
|
+ return `${alarm.name}:${durationText}${valueText} ${formatChartValue(alarm.value)}${suffix},${directionText} ${formatChartValue(alarm.threshold)}${suffix}${rangeText}`
|
|
|
}
|
|
|
|
|
|
function clearThresholdAlarms() {
|
|
|
thresholdAlarms.value = {}
|
|
|
- pendingThresholdStarts.clear()
|
|
|
setAlarmActive(false)
|
|
|
}
|
|
|
|
|
|
@@ -159,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) {
|
|
|
@@ -168,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
|
|
|
|
|
|
@@ -473,55 +471,33 @@ function updateDimensionValues(valueMap: Map<string, number>) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-function updateThresholdAlarm(valueMap: Map<string, number>) {
|
|
|
+function updateThresholdAlarm(pointMap: Map<string, ChartPoint>) {
|
|
|
const nextAlarms = { ...thresholdAlarms.value }
|
|
|
- const now = Date.now()
|
|
|
|
|
|
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.alarmDuration) ?? 0)
|
|
|
- const range = Math.max(0, normalizeRangeValue(item.alarmRange) ?? 0)
|
|
|
+ const duration = Math.max(0, normalizeRangeValue(item.periodSecond) ?? 0)
|
|
|
+ const range = Math.max(0, normalizeRangeValue(item.changeValue) ?? 0)
|
|
|
const alarmKey = `${item.identifier}|${item.name}`
|
|
|
- const highThreshold = max === undefined ? undefined : max + range
|
|
|
- const lowThreshold = min === undefined ? undefined : min - range
|
|
|
- const direction =
|
|
|
- highThreshold !== undefined && value > highThreshold
|
|
|
- ? 'high'
|
|
|
- : lowThreshold !== undefined && value < lowThreshold
|
|
|
- ? 'low'
|
|
|
- : undefined
|
|
|
-
|
|
|
- if (!direction) {
|
|
|
- pendingThresholdStarts.delete(alarmKey)
|
|
|
- delete nextAlarms[alarmKey]
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const pending = pendingThresholdStarts.get(alarmKey)
|
|
|
- const startedAt = pending?.direction === direction ? pending.startedAt : now
|
|
|
- pendingThresholdStarts.set(alarmKey, { direction, startedAt })
|
|
|
+ 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 (now - startedAt < duration * 1000) {
|
|
|
+ if (!alarm) {
|
|
|
delete nextAlarms[alarmKey]
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- const threshold = direction === 'high' ? highThreshold! : lowThreshold!
|
|
|
- const baseThreshold = direction === 'high' ? max! : min!
|
|
|
-
|
|
|
nextAlarms[alarmKey] = {
|
|
|
key: alarmKey,
|
|
|
name: item.name,
|
|
|
- value,
|
|
|
- threshold,
|
|
|
- baseThreshold,
|
|
|
+ ...alarm,
|
|
|
duration,
|
|
|
range,
|
|
|
- direction,
|
|
|
suffix: item.suffix
|
|
|
}
|
|
|
})
|
|
|
@@ -938,8 +914,8 @@ function buildRangeSettingDrafts() {
|
|
|
color: item.color,
|
|
|
minValue: normalizeRangeValue(item.minValue),
|
|
|
maxValue: normalizeRangeValue(item.maxValue),
|
|
|
- alarmDuration: normalizeRangeValue(item.alarmDuration),
|
|
|
- alarmRange: normalizeRangeValue(item.alarmRange),
|
|
|
+ periodSecond: normalizeRangeValue(item.periodSecond),
|
|
|
+ changeValue: normalizeRangeValue(item.changeValue),
|
|
|
axisMin: normalizeRangeValue(item.axisMin),
|
|
|
axisMax: normalizeRangeValue(item.axisMax)
|
|
|
}))
|
|
|
@@ -978,23 +954,23 @@ async function saveRangeSetting(
|
|
|
item: Dimensions,
|
|
|
minValue?: number,
|
|
|
maxValue?: number,
|
|
|
- alarmDurationValue?: number,
|
|
|
- alarmRangeValue?: number,
|
|
|
+ periodSecondValue?: number,
|
|
|
+ changeValueValue?: number,
|
|
|
axisMinValue?: number,
|
|
|
axisMaxValue?: number
|
|
|
) {
|
|
|
const min = normalizeRangeValue(minValue)
|
|
|
const max = normalizeRangeValue(maxValue)
|
|
|
- const alarmDuration = normalizeRangeValue(alarmDurationValue)
|
|
|
- const alarmRange = normalizeRangeValue(alarmRangeValue)
|
|
|
+ const periodSecond = normalizeRangeValue(periodSecondValue)
|
|
|
+ const changeValue = normalizeRangeValue(changeValueValue)
|
|
|
const axisMin = normalizeRangeValue(axisMinValue)
|
|
|
const axisMax = normalizeRangeValue(axisMaxValue)
|
|
|
|
|
|
if (
|
|
|
min === undefined &&
|
|
|
max === undefined &&
|
|
|
- alarmDuration === undefined &&
|
|
|
- alarmRange === undefined &&
|
|
|
+ periodSecond === undefined &&
|
|
|
+ changeValue === undefined &&
|
|
|
axisMin === undefined &&
|
|
|
axisMax === undefined
|
|
|
) {
|
|
|
@@ -1004,8 +980,8 @@ async function saveRangeSetting(
|
|
|
|
|
|
item.minValue = undefined
|
|
|
item.maxValue = undefined
|
|
|
- item.alarmDuration = undefined
|
|
|
- item.alarmRange = undefined
|
|
|
+ item.periodSecond = undefined
|
|
|
+ item.changeValue = undefined
|
|
|
item.axisMin = undefined
|
|
|
item.axisMax = undefined
|
|
|
item.id = undefined
|
|
|
@@ -1024,19 +1000,19 @@ async function saveRangeSetting(
|
|
|
throw new Error(`${item.name} 的告警上限和告警下限不能相等`)
|
|
|
}
|
|
|
|
|
|
- if ((alarmDuration === undefined) !== (alarmRange === undefined)) {
|
|
|
- throw new Error(`${item.name} 的告警持续时间和超限范围需要同时填写`)
|
|
|
+ if ((periodSecond === undefined) !== (changeValue === undefined)) {
|
|
|
+ throw new Error(`${item.name} 的检查时间和超限范围需要同时填写`)
|
|
|
}
|
|
|
|
|
|
- if ((alarmDuration !== undefined || alarmRange !== undefined) && min === undefined) {
|
|
|
+ if ((periodSecond !== undefined || changeValue !== undefined) && min === undefined) {
|
|
|
throw new Error(`${item.name} 需要先设置告警上限和告警下限`)
|
|
|
}
|
|
|
|
|
|
- if (alarmDuration !== undefined && (!Number.isInteger(alarmDuration) || alarmDuration <= 0)) {
|
|
|
- throw new Error(`${item.name} 的告警持续时间必须为大于 0 的整数秒`)
|
|
|
+ if (periodSecond !== undefined && (!Number.isInteger(periodSecond) || periodSecond <= 0)) {
|
|
|
+ throw new Error(`${item.name} 的检查时间必须为大于 0 的整数秒`)
|
|
|
}
|
|
|
|
|
|
- if (alarmRange !== undefined && alarmRange < 0) {
|
|
|
+ if (changeValue !== undefined && changeValue < 0) {
|
|
|
throw new Error(`${item.name} 的超限范围不能小于 0`)
|
|
|
}
|
|
|
|
|
|
@@ -1051,8 +1027,8 @@ async function saveRangeSetting(
|
|
|
const body = {
|
|
|
minValue: min ?? null,
|
|
|
maxValue: max ?? null,
|
|
|
- alarmDuration: alarmDuration ?? null,
|
|
|
- alarmRange: alarmRange ?? null,
|
|
|
+ periodSecond: periodSecond ?? null,
|
|
|
+ changeValue: changeValue ?? null,
|
|
|
axisMin: axisMin ?? null,
|
|
|
axisMax: axisMax ?? null,
|
|
|
deviceId: props.id,
|
|
|
@@ -1067,8 +1043,8 @@ async function saveRangeSetting(
|
|
|
if (res.id) item.id = res.id
|
|
|
item.minValue = min
|
|
|
item.maxValue = max
|
|
|
- item.alarmDuration = alarmDuration
|
|
|
- item.alarmRange = alarmRange
|
|
|
+ item.periodSecond = periodSecond
|
|
|
+ item.changeValue = changeValue
|
|
|
item.axisMin = axisMin
|
|
|
item.axisMax = axisMax
|
|
|
}
|
|
|
@@ -1086,8 +1062,8 @@ async function handleSettingsSave() {
|
|
|
item,
|
|
|
draft.minValue,
|
|
|
draft.maxValue,
|
|
|
- draft.alarmDuration,
|
|
|
- draft.alarmRange,
|
|
|
+ draft.periodSecond,
|
|
|
+ draft.changeValue,
|
|
|
draft.axisMin,
|
|
|
draft.axisMax
|
|
|
)
|
|
|
@@ -1109,8 +1085,8 @@ function handleRangeDialogReset() {
|
|
|
...item,
|
|
|
minValue: undefined,
|
|
|
maxValue: undefined,
|
|
|
- alarmDuration: undefined,
|
|
|
- alarmRange: undefined,
|
|
|
+ periodSecond: undefined,
|
|
|
+ changeValue: undefined,
|
|
|
axisMin: undefined,
|
|
|
axisMax: undefined
|
|
|
}))
|
|
|
@@ -1119,8 +1095,8 @@ function handleRangeDialogReset() {
|
|
|
function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
row.minValue = undefined
|
|
|
row.maxValue = undefined
|
|
|
- row.alarmDuration = undefined
|
|
|
- row.alarmRange = undefined
|
|
|
+ row.periodSecond = undefined
|
|
|
+ row.changeValue = undefined
|
|
|
row.axisMin = undefined
|
|
|
row.axisMax = undefined
|
|
|
}
|
|
|
@@ -1191,7 +1167,7 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
<el-dialog
|
|
|
v-model="settingsDialogVisible"
|
|
|
title="设备设置"
|
|
|
- width="94vw"
|
|
|
+ width="min(1140px, calc(100vw - 32px))"
|
|
|
class="board-settings-dialog"
|
|
|
modal-class="board-settings-overlay"
|
|
|
append-to-body
|
|
|
@@ -1207,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"
|
|
|
@@ -1228,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"
|
|
|
@@ -1237,28 +1218,28 @@ function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
placeholder="未设置" />
|
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="持续时间(秒)" width="150">
|
|
|
+ <ZmTableColumn label="检查时间(秒)" width="125">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
|
- v-model="row.alarmDuration"
|
|
|
+ v-model="row.periodSecond"
|
|
|
class="!w-full"
|
|
|
:controls="false"
|
|
|
:min="1"
|
|
|
:precision="0"
|
|
|
- placeholder="立即告警" />
|
|
|
+ placeholder="仅当前值" />
|
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="超限范围" width="140">
|
|
|
+ <ZmTableColumn label="超限范围" width="115">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
|
- v-model="row.alarmRange"
|
|
|
+ v-model="row.changeValue"
|
|
|
class="!w-full"
|
|
|
:controls="false"
|
|
|
:min="0"
|
|
|
placeholder="0" />
|
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="Y 轴下限" width="140">
|
|
|
+ <ZmTableColumn label="Y 轴下限" width="110">
|
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
|
v-model="row.axisMin"
|
|
|
@@ -1267,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"
|
|
|
@@ -1276,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) }}
|
|
|
@@ -1285,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)">
|
|
|
清空
|
|
|
@@ -1401,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);
|