|
@@ -142,6 +142,14 @@ interface ChartData {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const chartData = shallowRef<ChartData>({})
|
|
const chartData = shallowRef<ChartData>({})
|
|
|
|
|
+
|
|
|
|
|
+// 【修复前】每条 MQTT 消息只在当前 payload 内查找 1001(悬重)和 1008(悬轻)。
|
|
|
|
|
+// 如果设备把两个测点拆成两条消息上报,当前消息缺少的那一侧会被当成 0。
|
|
|
|
|
+// 例如:只收到悬轻 0 时,旧逻辑会计算成 0 - 0 = 0,形成错误的“掉零”点。
|
|
|
|
|
+//
|
|
|
|
|
+// 【修复后】保存两侧最近一次真实收到的有效点。后续只收到一侧时,使用另一侧最近值参与计算,
|
|
|
|
|
+// 在还没有拿到两侧数据之前则不生成组合点,避免用虚假的 0 代替“暂无数据”。
|
|
|
|
|
+const latestSuspensionPoints = new Map<string, ChartPoint>()
|
|
|
const resizeChart = () => chart?.resize()
|
|
const resizeChart = () => chart?.resize()
|
|
|
|
|
|
|
|
watch(isConnected, (newVal) => {
|
|
watch(isConnected, (newVal) => {
|
|
@@ -309,6 +317,34 @@ function getPointTimestamp(value: unknown) {
|
|
|
return Number.isFinite(parsedValue) ? parsedValue : Date.now()
|
|
return Number.isFinite(parsedValue) ? parsedValue : Date.now()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function normalizeSuspensionTimestamp(timestamp: number) {
|
|
|
|
|
+ // 图表 tooltip 只展示到秒,因此将组合曲线的毫秒统一归入对应的秒桶。
|
|
|
|
|
+ // 【修复前】14:26:26.000 和 14:26:26.500 会被 appendChartPoint 视为两个不同点,
|
|
|
|
|
+ // 但 tooltip 都显示为 14:26:26,视觉上就像同一秒出现了两个值。
|
|
|
|
|
+ // 【修复后】同一秒使用同一个时间戳,后到的组合结果覆盖先到的结果。
|
|
|
|
|
+ return Math.floor(timestamp / 1000) * 1000
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getCombinedSuspensionPoint(pointMap: Map<string, ChartPoint>) {
|
|
|
|
|
+ // 当前 payload 可能只包含 1001 或 1008 的其中一个,因此先更新缓存,
|
|
|
|
|
+ // 再从缓存中读取两侧最近一次有效值,而不是只依赖当前 payload。
|
|
|
|
|
+ for (const identifier of [SUSPENSION_WEIGHT_IDENTIFIER, SUSPENSION_LIGHT_IDENTIFIER]) {
|
|
|
|
|
+ const point = pointMap.get(identifier)
|
|
|
|
|
+ if (point) latestSuspensionPoints.set(identifier, point)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const weightPoint = latestSuspensionPoints.get(SUSPENSION_WEIGHT_IDENTIFIER)
|
|
|
|
|
+ const lightPoint = latestSuspensionPoints.get(SUSPENSION_LIGHT_IDENTIFIER)
|
|
|
|
|
+
|
|
|
|
|
+ // 两侧都没有数据时不能计算;返回 null 后,本次不会追加组合曲线点。
|
|
|
|
|
+ if (!weightPoint || !lightPoint) return null
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ts: normalizeSuspensionTimestamp(Math.max(weightPoint.ts, lightPoint.ts)),
|
|
|
|
|
+ value: weightPoint.value - lightPoint.value
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
type SeriesRange = ReturnType<typeof getSeriesRange>
|
|
type SeriesRange = ReturnType<typeof getSeriesRange>
|
|
|
|
|
|
|
|
function mapChartPoint({ ts, value }: ChartPoint, range: SeriesRange) {
|
|
function mapChartPoint({ ts, value }: ChartPoint, range: SeriesRange) {
|
|
@@ -660,19 +696,33 @@ function updateDimensionValues(valueMap: Map<string, number>) {
|
|
|
|
|
|
|
|
function handleMessageUpdate(_topic: string, payload: any) {
|
|
function handleMessageUpdate(_topic: string, payload: any) {
|
|
|
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : []
|
|
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : []
|
|
|
|
|
+
|
|
|
const valueMap = new Map<string, number>()
|
|
const valueMap = new Map<string, number>()
|
|
|
const pointMap = new Map<string, ChartPoint>()
|
|
const pointMap = new Map<string, ChartPoint>()
|
|
|
const updatedNames = new Set<string>()
|
|
const updatedNames = new Set<string>()
|
|
|
|
|
|
|
|
list.forEach((item) => {
|
|
list.forEach((item) => {
|
|
|
const identity = String(item.id ?? item.identity ?? '')
|
|
const identity = String(item.id ?? item.identity ?? '')
|
|
|
- const numberValue = Number(item.value ?? item.logValue ?? 0)
|
|
|
|
|
|
|
+ // 【修复前】item.value ?? item.logValue ?? 0 会把缺少数值的 MQTT 项伪装成 0。
|
|
|
|
|
+ // 【修复后】缺少 value/logValue 时直接忽略该项;真正上报的数值 0 仍然会被保留。
|
|
|
|
|
+ const rawValue = item.value ?? item.logValue
|
|
|
|
|
+ const numberValue = Number(rawValue)
|
|
|
|
|
|
|
|
- if (!identity || !Number.isFinite(numberValue)) return
|
|
|
|
|
|
|
+ if (
|
|
|
|
|
+ !identity ||
|
|
|
|
|
+ rawValue === null ||
|
|
|
|
|
+ rawValue === undefined ||
|
|
|
|
|
+ rawValue === '' ||
|
|
|
|
|
+ !Number.isFinite(numberValue)
|
|
|
|
|
+ ) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
valueMap.set(identity, numberValue)
|
|
valueMap.set(identity, numberValue)
|
|
|
const point = {
|
|
const point = {
|
|
|
- ts: getPointTimestamp(item.remark),
|
|
|
|
|
|
|
+ // 【修复前】详情页只读取 remark;如果后端返回 readTime/ts/time,就会走 Date.now()。
|
|
|
|
|
+ // 【修复后】按设备消息可能使用的字段依次取值,保证点的时间尽量来自设备实际上报时间。
|
|
|
|
|
+ ts: getPointTimestamp(item.remark || item.readTime || item.ts || item.time),
|
|
|
value: numberValue
|
|
value: numberValue
|
|
|
}
|
|
}
|
|
|
pointMap.set(identity, point)
|
|
pointMap.set(identity, point)
|
|
@@ -692,15 +742,9 @@ function handleMessageUpdate(_topic: string, payload: any) {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
if (suspensionWeightAndLightEnabled.value) {
|
|
if (suspensionWeightAndLightEnabled.value) {
|
|
|
- const weightPoint = pointMap.get(SUSPENSION_WEIGHT_IDENTIFIER)
|
|
|
|
|
- const lightPoint = pointMap.get(SUSPENSION_LIGHT_IDENTIFIER)
|
|
|
|
|
-
|
|
|
|
|
- if (weightPoint || lightPoint) {
|
|
|
|
|
- const combinedPoint = {
|
|
|
|
|
- ts: Math.max(weightPoint?.ts ?? 0, lightPoint?.ts ?? 0),
|
|
|
|
|
- value: (weightPoint?.value ?? 0) - (lightPoint?.value ?? 0)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const combinedPoint = getCombinedSuspensionPoint(pointMap)
|
|
|
|
|
|
|
|
|
|
+ if (combinedPoint) {
|
|
|
valueMap.set(SUSPENSION_WEIGHT_IDENTIFIER, combinedPoint.value)
|
|
valueMap.set(SUSPENSION_WEIGHT_IDENTIFIER, combinedPoint.value)
|
|
|
|
|
|
|
|
const dimension = dimensions.value.find(
|
|
const dimension = dimensions.value.find(
|
|
@@ -711,6 +755,10 @@ function handleMessageUpdate(_topic: string, payload: any) {
|
|
|
appendChartPoint(dimension.name, combinedPoint)
|
|
appendChartPoint(dimension.name, combinedPoint)
|
|
|
updatedNames.add(dimension.name)
|
|
updatedNames.add(dimension.name)
|
|
|
}
|
|
}
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 组合尚未完整时,不让当前 payload 中的单侧原始值覆盖页面上的上一条完整组合值。
|
|
|
|
|
+ valueMap.delete(SUSPENSION_WEIGHT_IDENTIFIER)
|
|
|
|
|
+ valueMap.delete(SUSPENSION_LIGHT_IDENTIFIER)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -749,16 +797,28 @@ function mergeSuspensionHistory(weightPoints: ChartPoint[], lightPoints: ChartPo
|
|
|
const mergedPoints = new Map<number, { weight?: number; light?: number }>()
|
|
const mergedPoints = new Map<number, { weight?: number; light?: number }>()
|
|
|
|
|
|
|
|
weightPoints.forEach((point) => {
|
|
weightPoints.forEach((point) => {
|
|
|
- mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), weight: point.value })
|
|
|
|
|
|
|
+ // 历史接口的两条曲线可能存在毫秒差,例如悬重 26.100、悬轻 26.800;
|
|
|
|
|
+ // 先按展示秒归桶,才能把同一采样秒的两侧数据配对起来。
|
|
|
|
|
+ const timestamp = normalizeSuspensionTimestamp(point.ts)
|
|
|
|
|
+ mergedPoints.set(timestamp, { ...mergedPoints.get(timestamp), weight: point.value })
|
|
|
})
|
|
})
|
|
|
lightPoints.forEach((point) => {
|
|
lightPoints.forEach((point) => {
|
|
|
- mergedPoints.set(point.ts, { ...mergedPoints.get(point.ts), light: point.value })
|
|
|
|
|
|
|
+ const timestamp = normalizeSuspensionTimestamp(point.ts)
|
|
|
|
|
+ mergedPoints.set(timestamp, { ...mergedPoints.get(timestamp), light: point.value })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- return Array.from(mergedPoints, ([ts, value]) => ({
|
|
|
|
|
- ts,
|
|
|
|
|
- value: (value.weight ?? 0) - (value.light ?? 0)
|
|
|
|
|
- })).sort((a, b) => a.ts - b.ts)
|
|
|
|
|
|
|
+ return Array.from(mergedPoints, ([ts, value]) => {
|
|
|
|
|
+ // 【修复前】任意一侧缺失都会使用 ?? 0 继续计算,产生无法代表真实状态的组合值。
|
|
|
|
|
+ // 【修复后】缺少任意一侧就不生成历史点,宁可没有点,也不显示伪造的 0。
|
|
|
|
|
+ if (value.weight === undefined || value.light === undefined) return null
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ ts,
|
|
|
|
|
+ value: value.weight - value.light
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .filter((point): point is ChartPoint => point !== null)
|
|
|
|
|
+ .sort((a, b) => a.ts - b.ts)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function initLoadChartData({
|
|
async function initLoadChartData({
|
|
@@ -775,6 +835,10 @@ async function initLoadChartData({
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
chartData.value = Object.fromEntries(dimensions.value.map((item) => [item.name, []]))
|
|
chartData.value = Object.fromEntries(dimensions.value.map((item) => [item.name, []]))
|
|
|
|
|
+
|
|
|
|
|
+ // 切换时间范围、重置图表或重新加载历史数据时,必须清理上一段数据的最近值缓存,
|
|
|
|
|
+ // 否则新时间范围的第一条单侧 MQTT 消息可能会和旧时间范围的数据错误组合。
|
|
|
|
|
+ latestSuspensionPoints.clear()
|
|
|
chartLoading.value = true
|
|
chartLoading.value = true
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
@@ -803,10 +867,19 @@ async function initLoadChartData({
|
|
|
selectedDate.value[1]
|
|
selectedDate.value[1]
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- chartData.value[item.name] = mergeSuspensionHistory(
|
|
|
|
|
- normalizeHistoryPoints(res),
|
|
|
|
|
- normalizeHistoryPoints(lightRes)
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ const weightPoints = normalizeHistoryPoints(res)
|
|
|
|
|
+ const lightPoints = normalizeHistoryPoints(lightRes)
|
|
|
|
|
+ const latestWeightPoint = weightPoints[weightPoints.length - 1]
|
|
|
|
|
+ const latestLightPoint = lightPoints[lightPoints.length - 1]
|
|
|
|
|
+
|
|
|
|
|
+ if (latestWeightPoint) {
|
|
|
|
|
+ latestSuspensionPoints.set(SUSPENSION_WEIGHT_IDENTIFIER, latestWeightPoint)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (latestLightPoint) {
|
|
|
|
|
+ latestSuspensionPoints.set(SUSPENSION_LIGHT_IDENTIFIER, latestLightPoint)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ chartData.value[item.name] = mergeSuspensionHistory(weightPoints, lightPoints)
|
|
|
} else {
|
|
} else {
|
|
|
chartData.value[item.name] = (((res as any[]) ?? []) as any[])
|
|
chartData.value[item.name] = (((res as any[]) ?? []) as any[])
|
|
|
.sort((a, b) => a.ts - b.ts)
|
|
.sort((a, b) => a.ts - b.ts)
|
|
@@ -918,7 +991,7 @@ async function exportChart() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const MAX_XLSX_TIME_COLUMNS = 16_383
|
|
|
|
|
|
|
+const MAX_XLSX_DATA_COLUMNS = 16_383
|
|
|
|
|
|
|
|
function getSelectedExportDimensions() {
|
|
function getSelectedExportDimensions() {
|
|
|
return dimensions.value.filter((item) => selectedDimension.value[item.name])
|
|
return dimensions.value.filter((item) => selectedDimension.value[item.name])
|
|
@@ -933,18 +1006,24 @@ function buildExportRows(selectedDimensions: Dimensions[]) {
|
|
|
)
|
|
)
|
|
|
).sort((a, b) => a - b)
|
|
).sort((a, b) => a - b)
|
|
|
|
|
|
|
|
|
|
+ // 导出矩阵转置为“每行一个时间点、每列一个属性”:
|
|
|
|
|
+ // 第一列是时间,后续列是选中的属性,便于按时间顺序查看和继续处理数据。
|
|
|
|
|
+ const valuesByDimension = new Map(
|
|
|
|
|
+ selectedDimensions.map((dimension) => [
|
|
|
|
|
+ dimension.name,
|
|
|
|
|
+ new Map((chartData.value[dimension.name] || []).map((point) => [point.ts, point.value]))
|
|
|
|
|
+ ])
|
|
|
|
|
+ )
|
|
|
const rows: Array<Array<string | number | Date | null>> = [
|
|
const rows: Array<Array<string | number | Date | null>> = [
|
|
|
- ['属性名称', ...timestamps.map((timestamp) => new Date(timestamp))]
|
|
|
|
|
|
|
+ ['时间', ...selectedDimensions.map((dimension) => dimension.name)]
|
|
|
]
|
|
]
|
|
|
|
|
|
|
|
- selectedDimensions.forEach((dimension) => {
|
|
|
|
|
- const valuesByTimestamp = new Map(
|
|
|
|
|
- (chartData.value[dimension.name] || []).map((point) => [point.ts, point.value])
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
|
|
+ timestamps.forEach((timestamp) => {
|
|
|
rows.push([
|
|
rows.push([
|
|
|
- dimension.name,
|
|
|
|
|
- ...timestamps.map((timestamp) => valuesByTimestamp.get(timestamp) ?? null)
|
|
|
|
|
|
|
+ new Date(timestamp),
|
|
|
|
|
+ ...selectedDimensions.map(
|
|
|
|
|
+ (dimension) => valuesByDimension.get(dimension.name)?.get(timestamp) ?? null
|
|
|
|
|
+ )
|
|
|
])
|
|
])
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -966,18 +1045,20 @@ async function exportChartData() {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (timestamps.length > MAX_XLSX_TIME_COLUMNS) {
|
|
|
|
|
- message.warning(`时间点共 ${timestamps.length} 个,请缩短时间范围后再导出`)
|
|
|
|
|
|
|
+ if (selectedDimensions.length > MAX_XLSX_DATA_COLUMNS) {
|
|
|
|
|
+ message.warning(
|
|
|
|
|
+ `属性共 ${selectedDimensions.length} 个,超过 Excel 列数限制,请减少属性后再导出`
|
|
|
|
|
+ )
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
exportDataLoading.value = true
|
|
exportDataLoading.value = true
|
|
|
try {
|
|
try {
|
|
|
const worksheet = XLSX.utils.aoa_to_sheet(rows, { cellDates: true })
|
|
const worksheet = XLSX.utils.aoa_to_sheet(rows, { cellDates: true })
|
|
|
- worksheet['!cols'] = [{ wch: 24 }, ...timestamps.map(() => ({ wch: 20 }))]
|
|
|
|
|
|
|
+ worksheet['!cols'] = [{ wch: 22 }, ...selectedDimensions.map(() => ({ wch: 20 }))]
|
|
|
|
|
|
|
|
timestamps.forEach((_, index) => {
|
|
timestamps.forEach((_, index) => {
|
|
|
- const cell = worksheet[XLSX.utils.encode_cell({ r: 0, c: index + 1 })]
|
|
|
|
|
|
|
+ const cell = worksheet[XLSX.utils.encode_cell({ r: index + 1, c: 0 })]
|
|
|
if (cell) cell.z = 'yyyy-mm-dd hh:mm:ss'
|
|
if (cell) cell.z = 'yyyy-mm-dd hh:mm:ss'
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -1029,8 +1110,6 @@ function openRangeDialog(targetName?: string) {
|
|
|
name: item.name,
|
|
name: item.name,
|
|
|
identifier: item.identifier,
|
|
identifier: item.identifier,
|
|
|
color: item.color,
|
|
color: item.color,
|
|
|
- minValue: normalizeRangeValue(item.minValue),
|
|
|
|
|
- maxValue: normalizeRangeValue(item.maxValue),
|
|
|
|
|
periodSecond: normalizeRangeValue(item.periodSecond),
|
|
periodSecond: normalizeRangeValue(item.periodSecond),
|
|
|
changeValue: normalizeRangeValue(item.changeValue),
|
|
changeValue: normalizeRangeValue(item.changeValue),
|
|
|
axisMin: normalizeRangeValue(item.axisMin),
|
|
axisMin: normalizeRangeValue(item.axisMin),
|
|
@@ -1041,23 +1120,17 @@ function openRangeDialog(targetName?: string) {
|
|
|
|
|
|
|
|
async function saveRangeSetting(
|
|
async function saveRangeSetting(
|
|
|
item: Dimensions,
|
|
item: Dimensions,
|
|
|
- minValue?: number,
|
|
|
|
|
- maxValue?: number,
|
|
|
|
|
periodSecondValue?: number,
|
|
periodSecondValue?: number,
|
|
|
changeValueValue?: number,
|
|
changeValueValue?: number,
|
|
|
axisMinValue?: number,
|
|
axisMinValue?: number,
|
|
|
axisMaxValue?: number
|
|
axisMaxValue?: number
|
|
|
) {
|
|
) {
|
|
|
- const min = normalizeRangeValue(minValue)
|
|
|
|
|
- const max = normalizeRangeValue(maxValue)
|
|
|
|
|
const periodSecond = normalizeRangeValue(periodSecondValue)
|
|
const periodSecond = normalizeRangeValue(periodSecondValue)
|
|
|
const changeValue = normalizeRangeValue(changeValueValue)
|
|
const changeValue = normalizeRangeValue(changeValueValue)
|
|
|
const axisMin = normalizeRangeValue(axisMinValue)
|
|
const axisMin = normalizeRangeValue(axisMinValue)
|
|
|
const axisMax = normalizeRangeValue(axisMaxValue)
|
|
const axisMax = normalizeRangeValue(axisMaxValue)
|
|
|
|
|
|
|
|
if (
|
|
if (
|
|
|
- min === undefined &&
|
|
|
|
|
- max === undefined &&
|
|
|
|
|
periodSecond === undefined &&
|
|
periodSecond === undefined &&
|
|
|
changeValue === undefined &&
|
|
changeValue === undefined &&
|
|
|
axisMin === undefined &&
|
|
axisMin === undefined &&
|
|
@@ -1077,26 +1150,10 @@ async function saveRangeSetting(
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if ((min === undefined) !== (max === undefined)) {
|
|
|
|
|
- throw new Error(`${item.name} 的告警上限和告警下限需要同时填写`)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (min !== undefined && max !== undefined && min > max) {
|
|
|
|
|
- throw new Error(`${item.name} 的告警下限不能大于告警上限`)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (min !== undefined && max !== undefined && min === max) {
|
|
|
|
|
- throw new Error(`${item.name} 的告警上限和告警下限不能相等`)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
if ((periodSecond === undefined) !== (changeValue === undefined)) {
|
|
if ((periodSecond === undefined) !== (changeValue === undefined)) {
|
|
|
throw new Error(`${item.name} 的检查时间和超限范围需要同时填写`)
|
|
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)) {
|
|
if (periodSecond !== undefined && (!Number.isInteger(periodSecond) || periodSecond <= 0)) {
|
|
|
throw new Error(`${item.name} 的检查时间必须为大于 0 的整数秒`)
|
|
throw new Error(`${item.name} 的检查时间必须为大于 0 的整数秒`)
|
|
|
}
|
|
}
|
|
@@ -1114,8 +1171,8 @@ async function saveRangeSetting(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const body = {
|
|
const body = {
|
|
|
- minValue: min ?? null,
|
|
|
|
|
- maxValue: max ?? null,
|
|
|
|
|
|
|
+ minValue: null,
|
|
|
|
|
+ maxValue: null,
|
|
|
periodSecond: periodSecond ?? null,
|
|
periodSecond: periodSecond ?? null,
|
|
|
changeValue: changeValue ?? null,
|
|
changeValue: changeValue ?? null,
|
|
|
axisMin: axisMin ?? null,
|
|
axisMin: axisMin ?? null,
|
|
@@ -1130,8 +1187,8 @@ async function saveRangeSetting(
|
|
|
const res = await IotDeviceApi.saveMaxMin(body)
|
|
const res = await IotDeviceApi.saveMaxMin(body)
|
|
|
|
|
|
|
|
if (res.id) item.id = res.id
|
|
if (res.id) item.id = res.id
|
|
|
- item.minValue = min
|
|
|
|
|
- item.maxValue = max
|
|
|
|
|
|
|
+ item.minValue = undefined
|
|
|
|
|
+ item.maxValue = undefined
|
|
|
item.periodSecond = periodSecond
|
|
item.periodSecond = periodSecond
|
|
|
item.changeValue = changeValue
|
|
item.changeValue = changeValue
|
|
|
item.axisMin = axisMin
|
|
item.axisMin = axisMin
|
|
@@ -1146,8 +1203,6 @@ async function handleRangeDialogSave() {
|
|
|
|
|
|
|
|
await saveRangeSetting(
|
|
await saveRangeSetting(
|
|
|
item,
|
|
item,
|
|
|
- draft.minValue,
|
|
|
|
|
- draft.maxValue,
|
|
|
|
|
draft.periodSecond,
|
|
draft.periodSecond,
|
|
|
draft.changeValue,
|
|
draft.changeValue,
|
|
|
draft.axisMin,
|
|
draft.axisMin,
|
|
@@ -1166,8 +1221,6 @@ async function handleRangeDialogSave() {
|
|
|
function handleRangeDialogReset() {
|
|
function handleRangeDialogReset() {
|
|
|
rangeSettingDrafts.value = rangeSettingDrafts.value.map((item) => ({
|
|
rangeSettingDrafts.value = rangeSettingDrafts.value.map((item) => ({
|
|
|
...item,
|
|
...item,
|
|
|
- minValue: undefined,
|
|
|
|
|
- maxValue: undefined,
|
|
|
|
|
periodSecond: undefined,
|
|
periodSecond: undefined,
|
|
|
changeValue: undefined,
|
|
changeValue: undefined,
|
|
|
axisMin: undefined,
|
|
axisMin: undefined,
|
|
@@ -1176,8 +1229,6 @@ function handleRangeDialogReset() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function clearRangeDraft(row: RangeSettingDraft) {
|
|
function clearRangeDraft(row: RangeSettingDraft) {
|
|
|
- row.minValue = undefined
|
|
|
|
|
- row.maxValue = undefined
|
|
|
|
|
row.periodSecond = undefined
|
|
row.periodSecond = undefined
|
|
|
row.changeValue = undefined
|
|
row.changeValue = undefined
|
|
|
row.axisMin = undefined
|
|
row.axisMin = undefined
|
|
@@ -1190,6 +1241,7 @@ onMounted(() => {
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
chartLoadVersion += 1
|
|
chartLoadVersion += 1
|
|
|
|
|
+ latestSuspensionPoints.clear()
|
|
|
destroy()
|
|
destroy()
|
|
|
cancelRealtimeChartUpdate()
|
|
cancelRealtimeChartUpdate()
|
|
|
window.removeEventListener('resize', resizeChart)
|
|
window.removeEventListener('resize', resizeChart)
|
|
@@ -1279,22 +1331,6 @@ onUnmounted(() => {
|
|
|
<span v-else class="dimension-card__value">
|
|
<span v-else class="dimension-card__value">
|
|
|
{{ item.value }}
|
|
{{ item.value }}
|
|
|
</span>
|
|
</span>
|
|
|
-
|
|
|
|
|
- <span
|
|
|
|
|
- v-if="item.minValue !== undefined || item.maxValue !== undefined"
|
|
|
|
|
- class="dimension-card__ranges">
|
|
|
|
|
- <span v-if="item.maxValue !== undefined" class="range-pill range-pill--max">
|
|
|
|
|
- <i class="i-material-symbols:arrow-upward-alt-rounded"></i>
|
|
|
|
|
- <span>告警上限</span>
|
|
|
|
|
- <strong>{{ formatChartValue(item.maxValue) }}</strong>
|
|
|
|
|
- </span>
|
|
|
|
|
-
|
|
|
|
|
- <span v-if="item.minValue !== undefined" class="range-pill range-pill--min">
|
|
|
|
|
- <i class="i-material-symbols:arrow-downward-alt-rounded"></i>
|
|
|
|
|
- <span>告警下限</span>
|
|
|
|
|
- <strong>{{ formatChartValue(item.minValue) }}</strong>
|
|
|
|
|
- </span>
|
|
|
|
|
- </span>
|
|
|
|
|
</span>
|
|
</span>
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -1389,17 +1425,17 @@ onUnmounted(() => {
|
|
|
|
|
|
|
|
<el-dialog
|
|
<el-dialog
|
|
|
v-model="rangeDialogVisible"
|
|
v-model="rangeDialogVisible"
|
|
|
- title="告警阈值、检查时间、超限范围与 Y 轴范围"
|
|
|
|
|
|
|
+ title="检查时间、超限范围与 Y 轴范围"
|
|
|
width="min(1140px, calc(100vw - 32px))"
|
|
width="min(1140px, calc(100vw - 32px))"
|
|
|
append-to-body
|
|
append-to-body
|
|
|
destroy-on-close>
|
|
destroy-on-close>
|
|
|
<ZmTable
|
|
<ZmTable
|
|
|
:data="rangeSettingDrafts"
|
|
:data="rangeSettingDrafts"
|
|
|
:loading="false"
|
|
:loading="false"
|
|
|
- :max-height="520"
|
|
|
|
|
|
|
+ :max-height="420"
|
|
|
:show-border="true"
|
|
:show-border="true"
|
|
|
:fit="false">
|
|
:fit="false">
|
|
|
- <ZmTableColumn label="曲线" width="210" fixed="left" align="left">
|
|
|
|
|
|
|
+ <ZmTableColumn label="曲线" fixed="left" align="left">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
|
<span
|
|
<span
|
|
@@ -1411,25 +1447,7 @@ onUnmounted(() => {
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="告警下限" width="115">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-input-number
|
|
|
|
|
- v-model="row.minValue"
|
|
|
|
|
- class="!w-full"
|
|
|
|
|
- :controls="false"
|
|
|
|
|
- placeholder="未设置" />
|
|
|
|
|
- </template>
|
|
|
|
|
- </ZmTableColumn>
|
|
|
|
|
- <ZmTableColumn label="告警上限" width="115">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-input-number
|
|
|
|
|
- v-model="row.maxValue"
|
|
|
|
|
- class="!w-full"
|
|
|
|
|
- :controls="false"
|
|
|
|
|
- placeholder="未设置" />
|
|
|
|
|
- </template>
|
|
|
|
|
- </ZmTableColumn>
|
|
|
|
|
- <ZmTableColumn label="检查时间(秒)" width="125">
|
|
|
|
|
|
|
+ <ZmTableColumn label="检查时间(秒)">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
<el-input-number
|
|
|
v-model="row.periodSecond"
|
|
v-model="row.periodSecond"
|
|
@@ -1437,10 +1455,10 @@ onUnmounted(() => {
|
|
|
:controls="false"
|
|
:controls="false"
|
|
|
:min="1"
|
|
:min="1"
|
|
|
:precision="0"
|
|
:precision="0"
|
|
|
- placeholder="仅当前值" />
|
|
|
|
|
|
|
+ placeholder="未设置" />
|
|
|
</template>
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="超限范围" width="115">
|
|
|
|
|
|
|
+ <ZmTableColumn label="超限范围">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
<el-input-number
|
|
|
v-model="row.changeValue"
|
|
v-model="row.changeValue"
|
|
@@ -1450,7 +1468,7 @@ onUnmounted(() => {
|
|
|
placeholder="0" />
|
|
placeholder="0" />
|
|
|
</template>
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="Y 轴下限" width="110">
|
|
|
|
|
|
|
+ <ZmTableColumn label="Y 轴下限">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
<el-input-number
|
|
|
v-model="row.axisMin"
|
|
v-model="row.axisMin"
|
|
@@ -1459,7 +1477,7 @@ onUnmounted(() => {
|
|
|
:placeholder="formatChartValue(getSeriesRange(row.name).labelMin)" />
|
|
:placeholder="formatChartValue(getSeriesRange(row.name).labelMin)" />
|
|
|
</template>
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="Y 轴上限" width="110">
|
|
|
|
|
|
|
+ <ZmTableColumn label="Y 轴上限">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<el-input-number
|
|
<el-input-number
|
|
|
v-model="row.axisMax"
|
|
v-model="row.axisMax"
|
|
@@ -1468,7 +1486,7 @@ onUnmounted(() => {
|
|
|
:placeholder="formatChartValue(getSeriesRange(row.name).labelMax)" />
|
|
:placeholder="formatChartValue(getSeriesRange(row.name).labelMax)" />
|
|
|
</template>
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="当前 Y 轴范围" width="120">
|
|
|
|
|
|
|
+ <ZmTableColumn label="当前 Y 轴范围">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<span class="text-xs text-slate-500">
|
|
<span class="text-xs text-slate-500">
|
|
|
{{ formatChartValue(getSeriesRange(row.name).labelMin) }}
|
|
{{ formatChartValue(getSeriesRange(row.name).labelMin) }}
|
|
@@ -1477,7 +1495,7 @@ onUnmounted(() => {
|
|
|
</span>
|
|
</span>
|
|
|
</template>
|
|
</template>
|
|
|
</ZmTableColumn>
|
|
</ZmTableColumn>
|
|
|
- <ZmTableColumn label="操作" width="66">
|
|
|
|
|
|
|
+ <ZmTableColumn label="操作">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<el-button size="small" link type="primary" @click="clearRangeDraft(row)">
|
|
<el-button size="small" link type="primary" @click="clearRangeDraft(row)">
|
|
|
清空
|
|
清空
|
|
@@ -1683,50 +1701,6 @@ onUnmounted(() => {
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.dimension-card__ranges {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
- align-items: flex-end;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- max-width: 100%;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.range-pill {
|
|
|
|
|
- display: inline-flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 3px;
|
|
|
|
|
- height: 20px;
|
|
|
|
|
- padding: 0 6px;
|
|
|
|
|
- font-size: 10px;
|
|
|
|
|
- font-weight: 800;
|
|
|
|
|
- line-height: 1;
|
|
|
|
|
- border: 1px solid;
|
|
|
|
|
- border-radius: 999px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.range-pill i {
|
|
|
|
|
- width: 12px;
|
|
|
|
|
- height: 12px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.range-pill strong {
|
|
|
|
|
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
|
|
|
- font-size: 11px;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.range-pill--max {
|
|
|
|
|
- color: #047857;
|
|
|
|
|
- background: rgb(236 253 245 / 78%);
|
|
|
|
|
- border-color: rgb(167 243 208 / 86%);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.range-pill--min {
|
|
|
|
|
- color: #be123c;
|
|
|
|
|
- background: rgb(255 241 242 / 78%);
|
|
|
|
|
- border-color: rgb(254 205 211 / 88%);
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
.chart-panel {
|
|
.chart-panel {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|