|
|
@@ -149,8 +149,8 @@ const chartData = shallowRef<ChartData>({})
|
|
|
// 如果设备把两个测点拆成两条消息上报,当前消息缺少的那一侧会被当成 0。
|
|
|
// 例如:只收到悬轻 0 时,旧逻辑会计算成 0 - 0 = 0,形成错误的“掉零”点。
|
|
|
//
|
|
|
-// 【修复后】保存两侧最近一次真实收到的有效点。后续只收到一侧时,使用另一侧最近值参与计算,
|
|
|
-// 在还没有拿到两侧数据之前则不生成组合点,避免用虚假的 0 代替“暂无数据”。
|
|
|
+// 保存两侧最近一次有效点。尚未收到另一侧时显示单侧值,
|
|
|
+// 悬重为正向、悬轻为负向;两侧齐全后再计算差值。
|
|
|
const latestSuspensionPoints = new Map<string, ChartPoint>()
|
|
|
const resizeChart = () => chart?.resize()
|
|
|
|
|
|
@@ -167,6 +167,7 @@ async function loadDimensions() {
|
|
|
const gatewaySource = [
|
|
|
...(((await IotDeviceApi.getIotDeviceTds(Number(query.id))) as IotTdItem[]) ?? [])
|
|
|
]
|
|
|
+ .map((item) => ({ ...item, identifier: String(item.identifier).trim() }))
|
|
|
.sort((a, b) => (b.modelOrder ?? 0) - (a.modelOrder ?? 0))
|
|
|
.map(mapToDimension)
|
|
|
const gatewayIdentifiers = new Set(gatewaySource.map((item) => String(item.identifier)))
|
|
|
@@ -175,10 +176,17 @@ async function loadDimensions() {
|
|
|
gatewayIdentifiers.has(SUSPENSION_WEIGHT_IDENTIFIER) &&
|
|
|
gatewayIdentifiers.has(SUSPENSION_LIGHT_IDENTIFIER)
|
|
|
|
|
|
+ // 属性是否存在以 td 的完整列表为准,悬轻值为空也仍启用合并。
|
|
|
+ const weightDimension = gatewaySource.find(
|
|
|
+ (item) => item.identifier === SUSPENSION_WEIGHT_IDENTIFIER
|
|
|
+ )
|
|
|
+ if (weightDimension) {
|
|
|
+ weightDimension.name = suspensionWeightAndLightEnabled.value
|
|
|
+ ? SUSPENSION_COMBINED_NAME
|
|
|
+ : '悬重'
|
|
|
+ }
|
|
|
+
|
|
|
if (suspensionWeightAndLightEnabled.value) {
|
|
|
- const weightDimension = gatewaySource.find(
|
|
|
- (item) => String(item.identifier) === SUSPENSION_WEIGHT_IDENTIFIER
|
|
|
- )
|
|
|
const lightDimension = gatewaySource.find(
|
|
|
(item) => String(item.identifier) === SUSPENSION_LIGHT_IDENTIFIER
|
|
|
)
|
|
|
@@ -188,8 +196,6 @@ async function loadDimensions() {
|
|
|
if (weightDimension && Number.isFinite(weightValue) && Number.isFinite(lightValue)) {
|
|
|
weightDimension.value = weightValue - lightValue
|
|
|
}
|
|
|
-
|
|
|
- if (weightDimension) weightDimension.name = SUSPENSION_COMBINED_NAME
|
|
|
}
|
|
|
|
|
|
const gateway = gatewaySource.filter(
|
|
|
@@ -319,15 +325,15 @@ function getPointTimestamp(value: unknown) {
|
|
|
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,视觉上就像同一秒出现了两个值。
|
|
|
- // 【修复后】同一秒使用同一个时间戳,后到的组合结果覆盖先到的结果。
|
|
|
+function normalizeChartTimestamp(timestamp: number) {
|
|
|
+ // 所有曲线使用相同的秒级坐标,避免组合曲线与其他曲线因毫秒差拆成两个 tooltip。
|
|
|
return Math.floor(timestamp / 1000) * 1000
|
|
|
}
|
|
|
|
|
|
function getCombinedSuspensionPoint(pointMap: Map<string, ChartPoint>) {
|
|
|
+ if (!pointMap.has(SUSPENSION_WEIGHT_IDENTIFIER) && !pointMap.has(SUSPENSION_LIGHT_IDENTIFIER)) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
// 当前 payload 可能只包含 1001 或 1008 的其中一个,因此先更新缓存,
|
|
|
// 再从缓存中读取两侧最近一次有效值,而不是只依赖当前 payload。
|
|
|
for (const identifier of [SUSPENSION_WEIGHT_IDENTIFIER, SUSPENSION_LIGHT_IDENTIFIER]) {
|
|
|
@@ -338,12 +344,16 @@ function getCombinedSuspensionPoint(pointMap: Map<string, ChartPoint>) {
|
|
|
const weightPoint = latestSuspensionPoints.get(SUSPENSION_WEIGHT_IDENTIFIER)
|
|
|
const lightPoint = latestSuspensionPoints.get(SUSPENSION_LIGHT_IDENTIFIER)
|
|
|
|
|
|
- // 两侧都没有数据时不能计算;返回 null 后,本次不会追加组合曲线点。
|
|
|
- if (!weightPoint || !lightPoint) return null
|
|
|
+ if (!weightPoint && !lightPoint) return null
|
|
|
|
|
|
return {
|
|
|
- ts: normalizeSuspensionTimestamp(Math.max(weightPoint.ts, lightPoint.ts)),
|
|
|
- value: weightPoint.value - lightPoint.value
|
|
|
+ ts: normalizeChartTimestamp(Math.max(weightPoint?.ts ?? 0, lightPoint?.ts ?? 0)),
|
|
|
+ value:
|
|
|
+ weightPoint && lightPoint
|
|
|
+ ? weightPoint.value - lightPoint.value
|
|
|
+ : weightPoint
|
|
|
+ ? weightPoint.value
|
|
|
+ : -lightPoint!.value
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -485,6 +495,7 @@ function updateSingleSeries(name: string) {
|
|
|
{
|
|
|
id: name,
|
|
|
name,
|
|
|
+ showSymbol: chartData.value[name]?.length === 1,
|
|
|
data: buildSeriesData(name)
|
|
|
}
|
|
|
]
|
|
|
@@ -504,6 +515,7 @@ function updateSeriesByNames(names: string[]) {
|
|
|
series: names.map((name) => ({
|
|
|
id: name,
|
|
|
name,
|
|
|
+ showSymbol: chartData.value[name]?.length === 1,
|
|
|
data: buildSeriesData(name)
|
|
|
}))
|
|
|
})
|
|
|
@@ -534,6 +546,7 @@ function flushRealtimeChartUpdate() {
|
|
|
series: names.map((name) => ({
|
|
|
id: name,
|
|
|
name,
|
|
|
+ showSymbol: chartData.value[name]?.length === 1,
|
|
|
data: buildSeriesData(name)
|
|
|
}))
|
|
|
})
|
|
|
@@ -659,7 +672,7 @@ function renderChart() {
|
|
|
type: 'line',
|
|
|
sampling: 'lttb',
|
|
|
smooth: 0.24,
|
|
|
- showSymbol: false,
|
|
|
+ showSymbol: chartData.value[item.name]?.length === 1,
|
|
|
connectNulls: true,
|
|
|
endLabel: {
|
|
|
show: true,
|
|
|
@@ -696,7 +709,13 @@ function updateDimensionValues(valueMap: Map<string, number>) {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-function handleMessageUpdate(_topic: string, payload: any) {
|
|
|
+function handleMessageUpdate(topic: string, payload: any) {
|
|
|
+ console.log('[监控详情 MQTT 返回数据]', {
|
|
|
+ deviceCode: data.value.deviceCode,
|
|
|
+ topic,
|
|
|
+ receivedAt: dayjs().format('YYYY-MM-DD HH:mm:ss.SSS'),
|
|
|
+ data: payload
|
|
|
+ })
|
|
|
const list = Array.isArray(payload) ? payload : Array.isArray(payload?.data) ? payload.data : []
|
|
|
|
|
|
const valueMap = new Map<string, number>()
|
|
|
@@ -724,7 +743,9 @@ function handleMessageUpdate(_topic: string, payload: any) {
|
|
|
const point = {
|
|
|
// 【修复前】详情页只读取 remark;如果后端返回 readTime/ts/time,就会走 Date.now()。
|
|
|
// 【修复后】按设备消息可能使用的字段依次取值,保证点的时间尽量来自设备实际上报时间。
|
|
|
- ts: getPointTimestamp(item.remark || item.readTime || item.ts || item.time),
|
|
|
+ ts: normalizeChartTimestamp(
|
|
|
+ getPointTimestamp(item.remark || item.readTime || item.ts || item.time)
|
|
|
+ ),
|
|
|
value: numberValue
|
|
|
}
|
|
|
pointMap.set(identity, point)
|
|
|
@@ -758,7 +779,7 @@ function handleMessageUpdate(_topic: string, payload: any) {
|
|
|
updatedNames.add(dimension.name)
|
|
|
}
|
|
|
} else {
|
|
|
- // 组合尚未完整时,不让当前 payload 中的单侧原始值覆盖页面上的上一条完整组合值。
|
|
|
+ // 当前消息没有有效的悬重或悬轻数据时,不更新组合值。
|
|
|
valueMap.delete(SUSPENSION_WEIGHT_IDENTIFIER)
|
|
|
valueMap.delete(SUSPENSION_LIGHT_IDENTIFIER)
|
|
|
}
|
|
|
@@ -786,13 +807,24 @@ async function startRealtimeMqtt() {
|
|
|
function normalizeHistoryPoints(data: unknown): ChartPoint[] {
|
|
|
if (!Array.isArray(data)) return []
|
|
|
|
|
|
- return data
|
|
|
+ const points = data
|
|
|
+ .filter(
|
|
|
+ (item) => item.value !== null && item.value !== undefined && String(item.value).trim() !== ''
|
|
|
+ )
|
|
|
.map((item) => ({
|
|
|
ts: Number(item.ts),
|
|
|
value: Number(item.value)
|
|
|
}))
|
|
|
.filter((item) => Number.isFinite(item.ts) && Number.isFinite(item.value))
|
|
|
.sort((a, b) => a.ts - b.ts)
|
|
|
+
|
|
|
+ // 同一秒保留最后采样值,历史和实时均使用同一套坐标精度。
|
|
|
+ const pointsBySecond = new Map<number, ChartPoint>()
|
|
|
+ points.forEach((point) => {
|
|
|
+ const ts = normalizeChartTimestamp(point.ts)
|
|
|
+ pointsBySecond.set(ts, { ts, value: point.value })
|
|
|
+ })
|
|
|
+ return Array.from(pointsBySecond.values())
|
|
|
}
|
|
|
|
|
|
function mergeSuspensionHistory(weightPoints: ChartPoint[], lightPoints: ChartPoint[]) {
|
|
|
@@ -801,26 +833,34 @@ function mergeSuspensionHistory(weightPoints: ChartPoint[], lightPoints: ChartPo
|
|
|
weightPoints.forEach((point) => {
|
|
|
// 历史接口的两条曲线可能存在毫秒差,例如悬重 26.100、悬轻 26.800;
|
|
|
// 先按展示秒归桶,才能把同一采样秒的两侧数据配对起来。
|
|
|
- const timestamp = normalizeSuspensionTimestamp(point.ts)
|
|
|
+ const timestamp = normalizeChartTimestamp(point.ts)
|
|
|
mergedPoints.set(timestamp, { ...mergedPoints.get(timestamp), weight: point.value })
|
|
|
})
|
|
|
lightPoints.forEach((point) => {
|
|
|
- const timestamp = normalizeSuspensionTimestamp(point.ts)
|
|
|
+ const timestamp = normalizeChartTimestamp(point.ts)
|
|
|
mergedPoints.set(timestamp, { ...mergedPoints.get(timestamp), light: point.value })
|
|
|
})
|
|
|
|
|
|
- return Array.from(mergedPoints, ([ts, value]) => {
|
|
|
- // 【修复前】任意一侧缺失都会使用 ?? 0 继续计算,产生无法代表真实状态的组合值。
|
|
|
- // 【修复后】缺少任意一侧就不生成历史点,宁可没有点,也不显示伪造的 0。
|
|
|
- if (value.weight === undefined || value.light === undefined) return null
|
|
|
+ let latestWeight: number | undefined
|
|
|
+ let latestLight: number | undefined
|
|
|
|
|
|
- return {
|
|
|
- ts,
|
|
|
- value: value.weight - value.light
|
|
|
- }
|
|
|
- })
|
|
|
- .filter((point): point is ChartPoint => point !== null)
|
|
|
- .sort((a, b) => a.ts - b.ts)
|
|
|
+ return Array.from(mergedPoints)
|
|
|
+ .sort(([a], [b]) => a - b)
|
|
|
+ .map(([ts, value]) => {
|
|
|
+ // 按时间保留最近值,允许两侧分开上报;尚未收到另一侧时显示单侧值。
|
|
|
+ if (value.weight !== undefined) latestWeight = value.weight
|
|
|
+ if (value.light !== undefined) latestLight = value.light
|
|
|
+
|
|
|
+ return {
|
|
|
+ ts,
|
|
|
+ value:
|
|
|
+ latestWeight !== undefined && latestLight !== undefined
|
|
|
+ ? latestWeight - latestLight
|
|
|
+ : latestWeight !== undefined
|
|
|
+ ? latestWeight
|
|
|
+ : -latestLight!
|
|
|
+ }
|
|
|
+ })
|
|
|
}
|
|
|
|
|
|
async function initLoadChartData({
|
|
|
@@ -883,9 +923,7 @@ async function initLoadChartData({
|
|
|
|
|
|
chartData.value[item.name] = mergeSuspensionHistory(weightPoints, lightPoints)
|
|
|
} else {
|
|
|
- chartData.value[item.name] = (((res as any[]) ?? []) as any[])
|
|
|
- .sort((a, b) => a.ts - b.ts)
|
|
|
- .map((item) => ({ ts: item.ts, value: item.value }))
|
|
|
+ chartData.value[item.name] = normalizeHistoryPoints(res)
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error(error)
|