实时推送&绘制特性点

This commit is contained in:
caozehui
2026-05-06 10:53:30 +08:00
parent 71d80e67f1
commit 5ca5d73f98
3 changed files with 138 additions and 268 deletions

View File

@@ -1,4 +1,4 @@
<template> <template>
<el-card class="dip-chart-card" shadow="never"> <el-card class="dip-chart-card" shadow="never">
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
@@ -17,9 +17,9 @@
导出数据 导出数据
</el-button> </el-button>
<el-button <el-button
v-if="!props.autoDrawCurve"
type="primary" type="primary"
plain plain
:loading="curveLoading"
class="draw-curve-button" class="draw-curve-button"
@click="drawCharacteristicCurve" @click="drawCharacteristicCurve"
> >
@@ -41,7 +41,6 @@ import { ElMessage } from 'element-plus'
import { Document, Download } from '@element-plus/icons-vue' import { Document, Download } from '@element-plus/icons-vue'
import * as XLSX from 'xlsx' import * as XLSX from 'xlsx'
import MyEchart from '@/components/echarts/line/index.vue' import MyEchart from '@/components/echarts/line/index.vue'
import { getFreqConverterSCurve } from '@/api/device/freqConverter'
type ChartPointStatus = 'pass' | 'fail' type ChartPointStatus = 'pass' | 'fail'
@@ -62,16 +61,19 @@ const props = defineProps<{
selectedMapping?: Record<string, any> | null selectedMapping?: Record<string, any> | null
webMsgSend?: any webMsgSend?: any
resultData?: any resultData?: any
autoDrawCurve?: boolean
}>() }>()
const STATUS_COLOR_MAP: Record<ChartPointStatus, string> = { const STATUS_COLOR_MAP: Record<ChartPointStatus, string> = {
pass: '#1d4ed8', pass: '#4e73df',
fail: '#111111' fail: '#4b4b4b'
} }
const CHARACTERISTIC_POINT_COLOR = '#ff4d4f'
const chartPoints = ref<ChartPoint[]>([]) const chartPoints = ref<ChartPoint[]>([])
const characteristicCurveData = ref<Array<[number, number]>>([]) const characteristicCurveData = ref<Array<[number, number]>>([])
const curveLoading = ref(false) const characteristicCurveVisible = ref(false)
const chartRef = ref<any>(null) const chartRef = ref<any>(null)
const selectedMappingText = computed(() => { const selectedMappingText = computed(() => {
@@ -90,21 +92,23 @@ const positiveDurations = computed(() => {
}) })
const xAxisMin = computed(() => { const xAxisMin = computed(() => {
if (!positiveDurations.value.length) { return 0.001
return 0.001 // if (!positiveDurations.value.length) {
} // return 0.001
// }
const minValue = Math.min(...positiveDurations.value) //
return Math.min(0.001, Number(minValue.toFixed(3))) // const minValue = Math.min(...positiveDurations.value)
// return Math.min(0.001, Number(minValue.toFixed(3)))
}) })
const xAxisMax = computed(() => { const xAxisMax = computed(() => {
if (!positiveDurations.value.length) { return 1000
return 60 // if (!positiveDurations.value.length) {
} // return 60
// }
const maxValue = Math.max(...positiveDurations.value) //
return Math.max(Number((maxValue * 1.05).toFixed(3)), 60) // const maxValue = Math.max(...positiveDurations.value)
// return Math.max(Number((maxValue * 1.05).toFixed(3)), 60)
}) })
const sortedChartPoints = computed(() => { const sortedChartPoints = computed(() => {
@@ -127,105 +131,34 @@ const sortedCharacteristicCurveData = computed(() => {
}) })
}) })
const clampValue = (value: number, min: number, max: number) => {
return Math.min(max, Math.max(min, value))
}
const buildExtendedCurvePoint = (points: Array<[number, number]>, targetDuration: number) => {
if (!points.length || !Number.isFinite(targetDuration) || targetDuration <= 0) {
return null as [number, number] | null
}
const lastPoint = points[points.length - 1]
if (!lastPoint || targetDuration === lastPoint[0]) {
return null as [number, number] | null
}
if (points.length === 1) {
return [targetDuration, lastPoint[1]] as [number, number]
}
const recentDistinctPoints: Array<[number, number]> = []
for (let index = points.length - 1; index >= 0 && recentDistinctPoints.length < 3; index -= 1) {
const point = points[index]
if (recentDistinctPoints.some(item => item[0] === point[0])) {
continue
}
recentDistinctPoints.unshift(point)
}
if (recentDistinctPoints.length < 2) {
return [targetDuration, lastPoint[1]] as [number, number]
}
const toLogDuration = (value: number) => Math.log10(value)
const getSlope = (start: [number, number], end: [number, number]) => {
const deltaX = toLogDuration(end[0]) - toLogDuration(start[0])
if (!Number.isFinite(deltaX) || deltaX === 0) {
return null
}
return (end[1] - start[1]) / deltaX
}
let slope = getSlope(
recentDistinctPoints[recentDistinctPoints.length - 2],
recentDistinctPoints[recentDistinctPoints.length - 1]
)
if (recentDistinctPoints.length >= 3) {
const previousSlope = getSlope(recentDistinctPoints[0], recentDistinctPoints[1])
if (previousSlope !== null && slope !== null) {
slope = previousSlope * 0.35 + slope * 0.65
}
}
if (slope === null) {
return [targetDuration, lastPoint[1]] as [number, number]
}
const deltaX = toLogDuration(targetDuration) - toLogDuration(lastPoint[0])
if (!Number.isFinite(deltaX)) {
return null as [number, number] | null
}
const predictedResidualVoltage = lastPoint[1] + slope * deltaX
const nearbyResidualVoltages = recentDistinctPoints.map(item => item[1])
const minResidualVoltage = Math.max(0, Math.min(...nearbyResidualVoltages) - 15)
const maxResidualVoltage = Math.min(100, Math.max(...nearbyResidualVoltages) + 15)
return [
targetDuration,
Number(clampValue(predictedResidualVoltage, minResidualVoltage, maxResidualVoltage).toFixed(2))
] as [number, number]
}
const inferredCharacteristicCurvePoint = computed(() => {
return buildExtendedCurvePoint(sortedCharacteristicCurveData.value, xAxisMax.value)
})
const solidCharacteristicCurveSeriesData = computed(() => { const solidCharacteristicCurveSeriesData = computed(() => {
return sortedCharacteristicCurveData.value.map(item => [item[0], item[1], 'Backend point']) if (!characteristicCurveVisible.value) {
}) return [] as Array<[number, number, string]>
const dashedCharacteristicCurveSeriesData = computed(() => {
const lastPoint = sortedCharacteristicCurveData.value[sortedCharacteristicCurveData.value.length - 1]
const extensionPoint = inferredCharacteristicCurvePoint.value
if (!lastPoint || !extensionPoint) {
return []
} }
return [ return sortedCharacteristicCurveData.value.map(item => [item[0], item[1], '特性曲线'])
{ })
value: [lastPoint[0], lastPoint[1], 'Backend point'],
symbol: 'none' const characteristicCurvePointSeriesData = computed(() => {
}, return sortedCharacteristicCurveData.value.map(item => ({
{ value: [item[0], item[1], '特性点']
value: [extensionPoint[0], extensionPoint[1], 'Inferred point'] }))
} })
]
const passPointSeriesData = computed(() => {
return sortedChartPoints.value
.filter(item => item.status === 'pass')
.map(item => ({
value: [item.duration, item.residualVoltage, getStatusText(item.status)]
}))
})
const failPointSeriesData = computed(() => {
return sortedChartPoints.value
.filter(item => item.status === 'fail')
.map(item => ({
value: [item.duration, item.residualVoltage, getStatusText(item.status)]
}))
}) })
const hasChartData = computed(() => { const hasChartData = computed(() => {
@@ -374,65 +307,6 @@ const normalizePoint = (source: Record<string, any>): ChartPoint | null => {
} }
} }
const normalizeCurveData = (source: unknown) => {
if (!Array.isArray(source)) {
return [] as Array<[number, number]>
}
return source
.map(item => {
if (Array.isArray(item) && item.length >= 2) {
const duration = Number(item[0])
const residualVoltage = Number(item[1])
if (Number.isFinite(duration) && Number.isFinite(residualVoltage) && duration > 0) {
return [duration, residualVoltage] as [number, number]
}
}
if (item && typeof item === 'object') {
const record = item as Record<string, any>
const tolerant = normalizeTolerantValue(record.tolerant)
if (tolerant !== null && tolerant !== 2) {
return null
}
const duration = normalizeDuration(record)
const residualVoltage = normalizeResidualVoltageValue(record)
if (duration !== null && residualVoltage !== null && duration > 0) {
return [duration, residualVoltage] as [number, number]
}
}
return null
})
.filter((item): item is [number, number] => !!item)
}
const extractCurveData = (payload: any) => {
if (!payload) {
return [] as Array<[number, number]>
}
const candidates = [
payload,
payload?.data,
payload?.data?.records,
payload?.data?.points,
payload?.points,
payload?.records,
payload?.list
]
for (const candidate of candidates) {
const normalized = normalizeCurveData(candidate)
if (normalized.length) {
return normalized
}
}
return [] as Array<[number, number]>
}
const extractCharacteristicCurvePoints = (payload: any) => { const extractCharacteristicCurvePoints = (payload: any) => {
const result: Array<[number, number]> = [] const result: Array<[number, number]> = []
const seen = new Set<string>() const seen = new Set<string>()
@@ -472,6 +346,22 @@ const extractCharacteristicCurvePoints = (payload: any) => {
return result return result
} }
const mergeCharacteristicCurvePoints = (points: Array<[number, number]>) => {
if (!points.length) {
return
}
const existingPointMap = new Map(
characteristicCurveData.value.map(item => [`${item[0]}|${item[1]}`, item] as const)
)
points.forEach(item => {
existingPointMap.set(`${item[0]}|${item[1]}`, item)
})
characteristicCurveData.value = Array.from(existingPointMap.values())
}
const extractPoints = (payload: any) => { const extractPoints = (payload: any) => {
const result: ChartPoint[] = [] const result: ChartPoint[] = []
const seen = new Set<string>() const seen = new Set<string>()
@@ -511,35 +401,20 @@ const extractPoints = (payload: any) => {
return result return result
} }
const drawCharacteristicCurve = async () => { const updateCharacteristicCurveVisibility = () => {
const freqConverterId = props.selectedMapping?.freqConverterId if (props.autoDrawCurve) {
if (!freqConverterId) { characteristicCurveVisible.value = characteristicCurveData.value.length > 0
ElMessage.warning('未获取到变频器ID') }
}
const drawCharacteristicCurve = () => {
if (!sortedCharacteristicCurveData.value.length) {
characteristicCurveVisible.value = false
ElMessage.warning('暂无特性曲线点')
return return
} }
curveLoading.value = true characteristicCurveVisible.value = true
try {
const result = await getFreqConverterSCurve({
converterId: freqConverterId
})
const normalizedCurveData = extractCurveData(result)
if (!normalizedCurveData.length) {
characteristicCurveData.value = []
ElMessage.warning('未获取到特性曲线数据')
return
}
characteristicCurveData.value = normalizedCurveData
} catch (error) {
console.error('draw characteristic curve failed:', error)
characteristicCurveData.value = []
} finally {
curveLoading.value = false
}
} }
const downloadChartImage = async () => { const downloadChartImage = async () => {
@@ -577,24 +452,24 @@ const exportChartData = () => {
if (sortedChartPoints.value.length) { if (sortedChartPoints.value.length) {
const pointSheet = XLSX.utils.json_to_sheet( const pointSheet = XLSX.utils.json_to_sheet(
sortedChartPoints.value.map((item, index) => ({ sortedChartPoints.value.map((item, index) => ({
'序号': index + 1, 序号: index + 1,
'持续时间_s': item.duration, 持续时间_s: item.duration,
'暂降幅值_%': item.residualVoltage, 残余电压_pct: item.residualVoltage,
'状态': getStatusText(item.status) 状态: getStatusText(item.status)
})) }))
) )
XLSX.utils.book_append_sheet(workbook, pointSheet, '暂降点') XLSX.utils.book_append_sheet(workbook, pointSheet, '耐受点')
} }
if (sortedCharacteristicCurveData.value.length) { if (sortedCharacteristicCurveData.value.length) {
const curveSheet = XLSX.utils.json_to_sheet( const curveSheet = XLSX.utils.json_to_sheet(
sortedCharacteristicCurveData.value.map((item, index) => ({ sortedCharacteristicCurveData.value.map((item, index) => ({
'序号': index + 1, 序号: index + 1,
'持续时间_s': item[0], 持续时间_s: item[0],
'暂降幅值_%': item[1] 残余电压_pct: item[1]
})) }))
) )
XLSX.utils.book_append_sheet(workbook, curveSheet, '特性曲线') XLSX.utils.book_append_sheet(workbook, curveSheet, '特性')
} }
const workbookBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' }) const workbookBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
@@ -642,7 +517,7 @@ const chartOptions = computed(() => {
legend: { legend: {
top: 0, top: 0,
right: 0, right: 0,
data: ['特性测试曲线'] data: ['特性曲线', '特性点', '耐受点', '不耐受点']
}, },
xAxis: { xAxis: {
type: 'log', type: 'log',
@@ -676,7 +551,7 @@ const chartOptions = computed(() => {
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
name: '暂降幅值', name: '残余电压',
min: 0, min: 0,
max: 100, max: 100,
interval: 10, interval: 10,
@@ -704,46 +579,45 @@ const chartOptions = computed(() => {
dataZoom: [], dataZoom: [],
series: [ series: [
{ {
name: '特性测试曲线', name: '特性曲线',
type: 'line', type: 'line',
smooth: true, smooth: true,
showSymbol: true, showSymbol: false,
symbolSize: 7,
lineStyle: { lineStyle: {
color: '#ff2a2a', color: CHARACTERISTIC_POINT_COLOR,
width: 3 width: 3
}, },
itemStyle: { itemStyle: {
color: '#ff2a2a' color: CHARACTERISTIC_POINT_COLOR
}, },
data: solidCharacteristicCurveSeriesData.value data: solidCharacteristicCurveSeriesData.value
}, },
{ {
name: '特性曲线延伸', name: '特性',
type: 'line',
smooth: true,
showSymbol: true,
symbolSize: 7,
lineStyle: {
color: '#ff2a2a',
width: 3,
type: 'dashed'
},
itemStyle: {
color: '#ff2a2a'
},
data: dashedCharacteristicCurveSeriesData.value
},
{
name: '暂降点',
type: 'scatter', type: 'scatter',
symbolSize: 10, symbolSize: 10,
data: chartPoints.value.map(item => ({ itemStyle: {
value: [item.duration, item.residualVoltage, getStatusText(item.status)], color: CHARACTERISTIC_POINT_COLOR
itemStyle: { },
color: STATUS_COLOR_MAP[item.status] data: characteristicCurvePointSeriesData.value
} },
})) {
name: '耐受点',
type: 'scatter',
symbolSize: 10,
itemStyle: {
color: STATUS_COLOR_MAP.pass
},
data: passPointSeriesData.value
},
{
name: '不耐受点',
type: 'scatter',
symbolSize: 10,
itemStyle: {
color: STATUS_COLOR_MAP.fail
},
data: failPointSeriesData.value
} }
], ],
options: { options: {
@@ -752,6 +626,14 @@ const chartOptions = computed(() => {
} }
}) })
watch(
() => props.autoDrawCurve,
newValue => {
characteristicCurveVisible.value = newValue ? characteristicCurveData.value.length > 0 : false
},
{ immediate: true }
)
watch( watch(
() => props.webMsgSend, () => props.webMsgSend,
newValue => { newValue => {
@@ -760,29 +642,21 @@ watch(
} }
const nextPoints = extractPoints(newValue) const nextPoints = extractPoints(newValue)
if (!nextPoints.length) { if (nextPoints.length) {
const nextCurvePoints = extractCharacteristicCurvePoints(newValue) const existingPointMap = new Map(
if (nextCurvePoints.length) { chartPoints.value.map(item => [`${item.duration}|${item.residualVoltage}`, item] as const)
characteristicCurveData.value = nextCurvePoints )
}
return nextPoints.forEach(item => {
const key = `${item.duration}|${item.residualVoltage}`
existingPointMap.set(key, item)
})
chartPoints.value = Array.from(existingPointMap.values())
} }
const existingPointMap = new Map( mergeCharacteristicCurvePoints(extractCharacteristicCurvePoints(newValue))
chartPoints.value.map(item => [`${item.duration}|${item.residualVoltage}`, item] as const) updateCharacteristicCurveVisibility()
)
nextPoints.forEach(item => {
const key = `${item.duration}|${item.residualVoltage}`
existingPointMap.set(key, item)
})
chartPoints.value = Array.from(existingPointMap.values())
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) {
characteristicCurveData.value = nextCurvePoints
}
}, },
{ deep: true } { deep: true }
) )
@@ -795,10 +669,8 @@ watch(
} }
chartPoints.value = extractPoints(newValue) chartPoints.value = extractPoints(newValue)
const nextCurvePoints = extractCharacteristicCurvePoints(newValue) characteristicCurveData.value = extractCharacteristicCurvePoints(newValue)
if (nextCurvePoints.length) { updateCharacteristicCurveVisibility()
characteristicCurveData.value = nextCurvePoints
}
}, },
{ deep: true, immediate: true } { deep: true, immediate: true }
) )
@@ -808,6 +680,7 @@ watch(
() => { () => {
chartPoints.value = [] chartPoints.value = []
characteristicCurveData.value = [] characteristicCurveData.value = []
characteristicCurveVisible.value = false
} }
) )
</script> </script>
@@ -856,11 +729,6 @@ watch(
color: var(--el-text-color-secondary); color: var(--el-text-color-secondary);
} }
.draw-curve-button {
margin-top: -2px;
flex-shrink: 0;
}
.chart-wrapper { .chart-wrapper {
height: 400px; height: 400px;
} }

View File

@@ -13,6 +13,7 @@
v-if="dialogVisible && selectedMapping" v-if="dialogVisible && selectedMapping"
:selected-mapping="selectedMapping" :selected-mapping="selectedMapping"
:result-data="resultPayload" :result-data="resultPayload"
:auto-draw-curve="true"
/> />
<el-empty v-else-if="!loading" description="暂无检测结果" /> <el-empty v-else-if="!loading" description="暂无检测结果" />

View File

@@ -30,6 +30,7 @@
:selected-mapping="selectedMapping" :selected-mapping="selectedMapping"
:web-msg-send="webSocketMessage" :web-msg-send="webSocketMessage"
:result-data="historyResultData" :result-data="historyResultData"
:auto-draw-curve="false"
/> />
</div> </div>