实时推送&绘制特性点
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<template>
|
||||
<template>
|
||||
<el-card class="dip-chart-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
@@ -17,9 +17,9 @@
|
||||
导出数据
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!props.autoDrawCurve"
|
||||
type="primary"
|
||||
plain
|
||||
:loading="curveLoading"
|
||||
class="draw-curve-button"
|
||||
@click="drawCharacteristicCurve"
|
||||
>
|
||||
@@ -41,7 +41,6 @@ import { ElMessage } from 'element-plus'
|
||||
import { Document, Download } from '@element-plus/icons-vue'
|
||||
import * as XLSX from 'xlsx'
|
||||
import MyEchart from '@/components/echarts/line/index.vue'
|
||||
import { getFreqConverterSCurve } from '@/api/device/freqConverter'
|
||||
|
||||
type ChartPointStatus = 'pass' | 'fail'
|
||||
|
||||
@@ -62,16 +61,19 @@ const props = defineProps<{
|
||||
selectedMapping?: Record<string, any> | null
|
||||
webMsgSend?: any
|
||||
resultData?: any
|
||||
autoDrawCurve?: boolean
|
||||
}>()
|
||||
|
||||
const STATUS_COLOR_MAP: Record<ChartPointStatus, string> = {
|
||||
pass: '#1d4ed8',
|
||||
fail: '#111111'
|
||||
pass: '#4e73df',
|
||||
fail: '#4b4b4b'
|
||||
}
|
||||
|
||||
const CHARACTERISTIC_POINT_COLOR = '#ff4d4f'
|
||||
|
||||
const chartPoints = ref<ChartPoint[]>([])
|
||||
const characteristicCurveData = ref<Array<[number, number]>>([])
|
||||
const curveLoading = ref(false)
|
||||
const characteristicCurveVisible = ref(false)
|
||||
const chartRef = ref<any>(null)
|
||||
|
||||
const selectedMappingText = computed(() => {
|
||||
@@ -90,21 +92,23 @@ const positiveDurations = computed(() => {
|
||||
})
|
||||
|
||||
const xAxisMin = computed(() => {
|
||||
if (!positiveDurations.value.length) {
|
||||
return 0.001
|
||||
}
|
||||
|
||||
const minValue = Math.min(...positiveDurations.value)
|
||||
return Math.min(0.001, Number(minValue.toFixed(3)))
|
||||
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 xAxisMax = computed(() => {
|
||||
if (!positiveDurations.value.length) {
|
||||
return 60
|
||||
}
|
||||
|
||||
const maxValue = Math.max(...positiveDurations.value)
|
||||
return Math.max(Number((maxValue * 1.05).toFixed(3)), 60)
|
||||
return 1000
|
||||
// if (!positiveDurations.value.length) {
|
||||
// return 60
|
||||
// }
|
||||
//
|
||||
// const maxValue = Math.max(...positiveDurations.value)
|
||||
// return Math.max(Number((maxValue * 1.05).toFixed(3)), 60)
|
||||
})
|
||||
|
||||
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(() => {
|
||||
return sortedCharacteristicCurveData.value.map(item => [item[0], item[1], 'Backend point'])
|
||||
})
|
||||
|
||||
const dashedCharacteristicCurveSeriesData = computed(() => {
|
||||
const lastPoint = sortedCharacteristicCurveData.value[sortedCharacteristicCurveData.value.length - 1]
|
||||
const extensionPoint = inferredCharacteristicCurvePoint.value
|
||||
|
||||
if (!lastPoint || !extensionPoint) {
|
||||
return []
|
||||
if (!characteristicCurveVisible.value) {
|
||||
return [] as Array<[number, number, string]>
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
value: [lastPoint[0], lastPoint[1], 'Backend point'],
|
||||
symbol: 'none'
|
||||
},
|
||||
{
|
||||
value: [extensionPoint[0], extensionPoint[1], 'Inferred point']
|
||||
}
|
||||
]
|
||||
return sortedCharacteristicCurveData.value.map(item => [item[0], item[1], '特性曲线'])
|
||||
})
|
||||
|
||||
const characteristicCurvePointSeriesData = computed(() => {
|
||||
return sortedCharacteristicCurveData.value.map(item => ({
|
||||
value: [item[0], item[1], '特性点']
|
||||
}))
|
||||
})
|
||||
|
||||
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(() => {
|
||||
@@ -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 result: Array<[number, number]> = []
|
||||
const seen = new Set<string>()
|
||||
@@ -472,6 +346,22 @@ const extractCharacteristicCurvePoints = (payload: any) => {
|
||||
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 result: ChartPoint[] = []
|
||||
const seen = new Set<string>()
|
||||
@@ -511,35 +401,20 @@ const extractPoints = (payload: any) => {
|
||||
return result
|
||||
}
|
||||
|
||||
const drawCharacteristicCurve = async () => {
|
||||
const freqConverterId = props.selectedMapping?.freqConverterId
|
||||
if (!freqConverterId) {
|
||||
ElMessage.warning('未获取到变频器ID')
|
||||
const updateCharacteristicCurveVisibility = () => {
|
||||
if (props.autoDrawCurve) {
|
||||
characteristicCurveVisible.value = characteristicCurveData.value.length > 0
|
||||
}
|
||||
}
|
||||
|
||||
const drawCharacteristicCurve = () => {
|
||||
if (!sortedCharacteristicCurveData.value.length) {
|
||||
characteristicCurveVisible.value = false
|
||||
ElMessage.warning('暂无特性曲线点')
|
||||
return
|
||||
}
|
||||
|
||||
curveLoading.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
|
||||
}
|
||||
characteristicCurveVisible.value = true
|
||||
}
|
||||
|
||||
const downloadChartImage = async () => {
|
||||
@@ -577,24 +452,24 @@ const exportChartData = () => {
|
||||
if (sortedChartPoints.value.length) {
|
||||
const pointSheet = XLSX.utils.json_to_sheet(
|
||||
sortedChartPoints.value.map((item, index) => ({
|
||||
'序号': index + 1,
|
||||
'持续时间_s': item.duration,
|
||||
'暂降幅值_%': item.residualVoltage,
|
||||
'状态': getStatusText(item.status)
|
||||
序号: index + 1,
|
||||
持续时间_s: item.duration,
|
||||
残余电压_pct: item.residualVoltage,
|
||||
状态: getStatusText(item.status)
|
||||
}))
|
||||
)
|
||||
XLSX.utils.book_append_sheet(workbook, pointSheet, '暂降点')
|
||||
XLSX.utils.book_append_sheet(workbook, pointSheet, '耐受点')
|
||||
}
|
||||
|
||||
if (sortedCharacteristicCurveData.value.length) {
|
||||
const curveSheet = XLSX.utils.json_to_sheet(
|
||||
sortedCharacteristicCurveData.value.map((item, index) => ({
|
||||
'序号': index + 1,
|
||||
'持续时间_s': item[0],
|
||||
'暂降幅值_%': item[1]
|
||||
序号: index + 1,
|
||||
持续时间_s: item[0],
|
||||
残余电压_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' })
|
||||
@@ -642,7 +517,7 @@ const chartOptions = computed(() => {
|
||||
legend: {
|
||||
top: 0,
|
||||
right: 0,
|
||||
data: ['特性测试曲线']
|
||||
data: ['特性曲线', '特性点', '耐受点', '不耐受点']
|
||||
},
|
||||
xAxis: {
|
||||
type: 'log',
|
||||
@@ -676,7 +551,7 @@ const chartOptions = computed(() => {
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
name: '暂降幅值',
|
||||
name: '残余电压',
|
||||
min: 0,
|
||||
max: 100,
|
||||
interval: 10,
|
||||
@@ -704,46 +579,45 @@ const chartOptions = computed(() => {
|
||||
dataZoom: [],
|
||||
series: [
|
||||
{
|
||||
name: '特性测试曲线',
|
||||
name: '特性曲线',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: true,
|
||||
symbolSize: 7,
|
||||
showSymbol: false,
|
||||
lineStyle: {
|
||||
color: '#ff2a2a',
|
||||
color: CHARACTERISTIC_POINT_COLOR,
|
||||
width: 3
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#ff2a2a'
|
||||
color: CHARACTERISTIC_POINT_COLOR
|
||||
},
|
||||
data: solidCharacteristicCurveSeriesData.value
|
||||
},
|
||||
{
|
||||
name: '特性曲线延伸',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
showSymbol: true,
|
||||
symbolSize: 7,
|
||||
lineStyle: {
|
||||
color: '#ff2a2a',
|
||||
width: 3,
|
||||
type: 'dashed'
|
||||
},
|
||||
itemStyle: {
|
||||
color: '#ff2a2a'
|
||||
},
|
||||
data: dashedCharacteristicCurveSeriesData.value
|
||||
},
|
||||
{
|
||||
name: '暂降点',
|
||||
name: '特性点',
|
||||
type: 'scatter',
|
||||
symbolSize: 10,
|
||||
data: chartPoints.value.map(item => ({
|
||||
value: [item.duration, item.residualVoltage, getStatusText(item.status)],
|
||||
itemStyle: {
|
||||
color: STATUS_COLOR_MAP[item.status]
|
||||
}
|
||||
}))
|
||||
itemStyle: {
|
||||
color: CHARACTERISTIC_POINT_COLOR
|
||||
},
|
||||
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: {
|
||||
@@ -752,6 +626,14 @@ const chartOptions = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.autoDrawCurve,
|
||||
newValue => {
|
||||
characteristicCurveVisible.value = newValue ? characteristicCurveData.value.length > 0 : false
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.webMsgSend,
|
||||
newValue => {
|
||||
@@ -760,29 +642,21 @@ watch(
|
||||
}
|
||||
|
||||
const nextPoints = extractPoints(newValue)
|
||||
if (!nextPoints.length) {
|
||||
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
|
||||
if (nextCurvePoints.length) {
|
||||
characteristicCurveData.value = nextCurvePoints
|
||||
}
|
||||
return
|
||||
if (nextPoints.length) {
|
||||
const existingPointMap = new Map(
|
||||
chartPoints.value.map(item => [`${item.duration}|${item.residualVoltage}`, item] as const)
|
||||
)
|
||||
|
||||
nextPoints.forEach(item => {
|
||||
const key = `${item.duration}|${item.residualVoltage}`
|
||||
existingPointMap.set(key, item)
|
||||
})
|
||||
|
||||
chartPoints.value = Array.from(existingPointMap.values())
|
||||
}
|
||||
|
||||
const existingPointMap = new Map(
|
||||
chartPoints.value.map(item => [`${item.duration}|${item.residualVoltage}`, item] as const)
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
mergeCharacteristicCurvePoints(extractCharacteristicCurvePoints(newValue))
|
||||
updateCharacteristicCurveVisibility()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
@@ -795,10 +669,8 @@ watch(
|
||||
}
|
||||
|
||||
chartPoints.value = extractPoints(newValue)
|
||||
const nextCurvePoints = extractCharacteristicCurvePoints(newValue)
|
||||
if (nextCurvePoints.length) {
|
||||
characteristicCurveData.value = nextCurvePoints
|
||||
}
|
||||
characteristicCurveData.value = extractCharacteristicCurvePoints(newValue)
|
||||
updateCharacteristicCurveVisibility()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
@@ -808,6 +680,7 @@ watch(
|
||||
() => {
|
||||
chartPoints.value = []
|
||||
characteristicCurveData.value = []
|
||||
characteristicCurveVisible.value = false
|
||||
}
|
||||
)
|
||||
</script>
|
||||
@@ -856,11 +729,6 @@ watch(
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.draw-curve-button {
|
||||
margin-top: -2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
v-if="dialogVisible && selectedMapping"
|
||||
:selected-mapping="selectedMapping"
|
||||
:result-data="resultPayload"
|
||||
:auto-draw-curve="true"
|
||||
/>
|
||||
|
||||
<el-empty v-else-if="!loading" description="暂无检测结果" />
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
:selected-mapping="selectedMapping"
|
||||
:web-msg-send="webSocketMessage"
|
||||
:result-data="historyResultData"
|
||||
:auto-draw-curve="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user