波形解析相关
This commit is contained in:
@@ -34,7 +34,10 @@
|
||||
:has-parsed-waveform="hasParsedWaveform"
|
||||
:summary-items="summaryItems"
|
||||
:feature-cards="featureCards"
|
||||
:vector-parse-result="vectorParseResult"
|
||||
:last-parse-error-message="lastParseErrorMessage"
|
||||
:last-vector-parse-error-message="lastVectorParseErrorMessage"
|
||||
:active-vector-channel-name="activeWaveDetail?.channelName || ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,8 +45,9 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { parseComtradeApi } from '@/api/tools/waveform'
|
||||
import { parseComtradeApi, parseComtradeVectorApi } from '@/api/tools/waveform'
|
||||
import type { Waveform } from '@/api/tools/waveform/interface'
|
||||
import WaveformInfoPanel from './components/WaveformInfoPanel.vue'
|
||||
import WaveformToolbar from './components/WaveformToolbar.vue'
|
||||
@@ -89,7 +93,9 @@ const isParsing = ref(false)
|
||||
const selectedCfgFile = ref<File | null>(null)
|
||||
const selectedDatFile = ref<File | null>(null)
|
||||
const waveformParseResult = ref<Waveform.WaveComtradeResultVO | null>(null)
|
||||
const vectorParseResult = ref<Waveform.WaveComtradeVectorResultVO | null>(null)
|
||||
const lastParseErrorMessage = ref('')
|
||||
const lastVectorParseErrorMessage = ref('')
|
||||
const waveformFileAccept = '.cfg,.dat'
|
||||
|
||||
const trendTabs: LabelValueOption<TrendTabValue>[] = [
|
||||
@@ -257,6 +263,13 @@ const formatDuration = (value?: number) => {
|
||||
return `${Number(numberValue.toFixed(3))} ms`
|
||||
}
|
||||
|
||||
const formatWaveformTime = (value?: string) => {
|
||||
if (!value) return '--'
|
||||
|
||||
const parsedValue = dayjs(value)
|
||||
return parsedValue.isValid() ? parsedValue.format('YYYY-MM-DD HH:mm:ss.SSS') : value
|
||||
}
|
||||
|
||||
const buildChannelLabel = (detail: Waveform.WaveDataDetail, index: number) => {
|
||||
if (detail.title) return detail.title
|
||||
if (detail.channelName && detail.unit) return `${detail.channelName} (${detail.unit})`
|
||||
@@ -310,22 +323,96 @@ const currentTrendColors = computed(() => {
|
||||
|
||||
const hasWaveformData = computed(() => activeTrendPayload.value.series.length > 0)
|
||||
|
||||
const SYMMETRIC_AXIS_SPLIT_COUNT = 6
|
||||
const REGULAR_AXIS_SPLIT_COUNT = 5
|
||||
|
||||
const getAxisPrecision = (step: number) => {
|
||||
if (!Number.isFinite(step) || step >= 1) return 0
|
||||
|
||||
const stepText = `${step}`
|
||||
if (stepText.includes('e-')) {
|
||||
return Number(stepText.split('e-')[1] || 0)
|
||||
}
|
||||
|
||||
return stepText.split('.')[1]?.length || 0
|
||||
}
|
||||
|
||||
const normalizeAxisValue = (value: number, precision: number) => {
|
||||
const factor = 10 ** precision
|
||||
const normalizedValue = Math.round(value * factor) / factor
|
||||
return Object.is(normalizedValue, -0) ? 0 : normalizedValue
|
||||
}
|
||||
|
||||
const getNiceAxisInterval = (value: number) => {
|
||||
if (!Number.isFinite(value) || value <= 0) return 1
|
||||
|
||||
const exponent = Math.floor(Math.log10(value))
|
||||
const magnitude = 10 ** exponent
|
||||
const normalized = value / magnitude
|
||||
const candidates = [1, 2, 2.5, 5, 10]
|
||||
const closestCandidate = candidates.reduce((currentBest, currentValue) => {
|
||||
return Math.abs(currentValue - normalized) < Math.abs(currentBest - normalized) ? currentValue : currentBest
|
||||
}, candidates[0])
|
||||
|
||||
return closestCandidate * magnitude
|
||||
}
|
||||
|
||||
const roundAxisBoundary = (value: number, interval: number, isMin: boolean) => {
|
||||
const precision = getAxisPrecision(interval)
|
||||
const stepCount = isMin ? Math.floor(value / interval) : Math.ceil(value / interval)
|
||||
return normalizeAxisValue(stepCount * interval, precision)
|
||||
}
|
||||
|
||||
const formatAxisLabel = (value: number, precision: number) => {
|
||||
if (!Number.isFinite(value)) return ''
|
||||
return `${normalizeAxisValue(value, precision)}`
|
||||
}
|
||||
|
||||
const buildTrendAxisConfig = (trendPayload: WaveformTrendPayload) => {
|
||||
const min = trendPayload.min
|
||||
const max = trendPayload.max
|
||||
const valueGap = Number.isFinite(min) && Number.isFinite(max) ? Math.max((max! - min!) * 0.1, 1) : undefined
|
||||
const min = Number(trendPayload.min)
|
||||
const max = Number(trendPayload.max)
|
||||
|
||||
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
||||
return {
|
||||
name: trendPayload.unit || ''
|
||||
}
|
||||
}
|
||||
|
||||
const crossesZero = min < 0 && max > 0
|
||||
const splitCount = crossesZero ? SYMMETRIC_AXIS_SPLIT_COUNT : REGULAR_AXIS_SPLIT_COUNT
|
||||
const baseValue = crossesZero ? Math.max(Math.abs(min), Math.abs(max)) : max - min
|
||||
const interval = getNiceAxisInterval(baseValue / (crossesZero ? splitCount / 2 : splitCount))
|
||||
const precision = getAxisPrecision(interval)
|
||||
|
||||
let axisMin = roundAxisBoundary(min, interval, true)
|
||||
let axisMax = roundAxisBoundary(max, interval, false)
|
||||
|
||||
if (crossesZero) {
|
||||
const axisBound = Math.max(Math.abs(axisMin), Math.abs(axisMax))
|
||||
axisMin = normalizeAxisValue(-axisBound, precision)
|
||||
axisMax = normalizeAxisValue(axisBound, precision)
|
||||
}
|
||||
|
||||
if (axisMin === axisMax) {
|
||||
axisMin = normalizeAxisValue(axisMin - interval, precision)
|
||||
axisMax = normalizeAxisValue(axisMax + interval, precision)
|
||||
}
|
||||
|
||||
return {
|
||||
name: trendPayload.unit || '',
|
||||
...(valueGap !== undefined && min !== undefined && max !== undefined
|
||||
? {
|
||||
min: min - valueGap,
|
||||
max: max + valueGap
|
||||
}
|
||||
: {})
|
||||
min: axisMin,
|
||||
max: axisMax,
|
||||
interval,
|
||||
splitNumber: crossesZero ? SYMMETRIC_AXIS_SPLIT_COUNT : Math.max(Math.round((axisMax - axisMin) / interval), 1),
|
||||
minInterval: interval,
|
||||
maxInterval: interval,
|
||||
// 跨零波形按 0 对称出刻度,避免边界出现 164 / -165 这类不均匀标签。
|
||||
axisLabel: {
|
||||
hideOverlap: true,
|
||||
formatter: (value: number) => formatAxisLabel(value, precision)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const buildTimeAxisLabelFormatter = (timeLabels: string[]) => {
|
||||
const lastIndex = timeLabels.length - 1
|
||||
|
||||
@@ -360,14 +447,14 @@ const buildTrendChartOptions = (
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
top: 10,
|
||||
top: 0,
|
||||
right: 12
|
||||
},
|
||||
grid: {
|
||||
top: '18px',
|
||||
left: '24px',
|
||||
right: showTimeAxis ? '32px' : '24px',
|
||||
bottom: showTimeAxis ? '22px' : '12px'
|
||||
bottom: showTimeAxis ? '16px' : '6px'
|
||||
},
|
||||
xAxis: {
|
||||
data: trendPayload.timeLabels,
|
||||
@@ -381,6 +468,8 @@ const buildTrendChartOptions = (
|
||||
},
|
||||
axisLine: {
|
||||
show: showTimeAxis,
|
||||
// 时间轴固定在图底部,避免 y 轴包含 0 时横轴穿过波形区域显得过粗。
|
||||
onZero: false,
|
||||
lineStyle: {
|
||||
color: axisLineColor
|
||||
}
|
||||
@@ -389,7 +478,7 @@ const buildTrendChartOptions = (
|
||||
show: showTimeAxis,
|
||||
hideOverlap: false,
|
||||
interval: 0,
|
||||
margin: showTimeAxis ? 6 : 0,
|
||||
margin: showTimeAxis ? 9 : 0,
|
||||
color: axisTextColor,
|
||||
formatter: buildTimeAxisLabelFormatter(trendPayload.timeLabels)
|
||||
},
|
||||
@@ -445,18 +534,15 @@ const summaryItems = computed<SummaryItem[]>(() => {
|
||||
const detail = activeWaveDetail.value
|
||||
|
||||
return [
|
||||
{ label: '测点名称', value: waveData?.monitorName || '--' },
|
||||
{ label: '事件时间', value: waveData?.time || '--' },
|
||||
{ label: '录波开始', value: cfgData?.timeStart || '--' },
|
||||
{ label: '触发时间', value: cfgData?.timeTrige || '--' },
|
||||
{ label: '录波开始', value: formatWaveformTime(cfgData?.timeStart) },
|
||||
{ label: '触发时间', value: formatWaveformTime(cfgData?.timeTrige) },
|
||||
{ label: '采样率', value: cfgData?.finalSampleRate ? `${cfgData.finalSampleRate} Hz` : '--' },
|
||||
{ label: '总通道数', value: cfgData?.nChannelNum ?? '--' },
|
||||
{ label: '当前通道', value: detail ? buildChannelLabel(detail, activeChannelIndex.value) : '--' },
|
||||
{ label: '单位', value: detail?.unit || '--' },
|
||||
{ label: '相别数量', value: waveData?.iPhasic ?? '--' },
|
||||
{ label: 'PT / CT', value: `${formatNumber(waveData?.pt, 2)} / ${formatNumber(waveData?.ct, 2)}` },
|
||||
{ label: '数据类型', value: valueModeLabelMap[activeValueMode.value] },
|
||||
{ label: '文件编码', value: cfgData?.strBinType || '--' }
|
||||
{ label: '数据类型', value: valueModeLabelMap[activeValueMode.value] }
|
||||
]
|
||||
})
|
||||
|
||||
@@ -480,7 +566,9 @@ const resetSelectedWaveformFiles = () => {
|
||||
selectedCfgFile.value = null
|
||||
selectedDatFile.value = null
|
||||
waveformParseResult.value = null
|
||||
vectorParseResult.value = null
|
||||
lastParseErrorMessage.value = ''
|
||||
lastVectorParseErrorMessage.value = ''
|
||||
}
|
||||
|
||||
const handleWaveformFileChange = async (event: Event) => {
|
||||
@@ -524,24 +612,50 @@ const loadWaveformData = async (cfgFile: File, datFile: File) => {
|
||||
isParsing.value = true
|
||||
activeChannelIndex.value = 0
|
||||
lastParseErrorMessage.value = ''
|
||||
lastVectorParseErrorMessage.value = ''
|
||||
|
||||
const result = await parseComtradeApi({
|
||||
cfgFile,
|
||||
datFile
|
||||
})
|
||||
// 波形与向量结果共用同一组文件,这里并行请求以缩短右侧联动展示等待时间。
|
||||
const [waveformResult, vectorResult] = await Promise.allSettled([
|
||||
parseComtradeApi({
|
||||
cfgFile,
|
||||
datFile
|
||||
}),
|
||||
parseComtradeVectorApi({
|
||||
cfgFile,
|
||||
datFile,
|
||||
parseType: 3
|
||||
})
|
||||
])
|
||||
|
||||
waveformParseResult.value = result.data
|
||||
} catch (error) {
|
||||
waveformParseResult.value = null
|
||||
lastParseErrorMessage.value = getWaveformParseErrorMessage(error)
|
||||
if (waveformResult.status === 'fulfilled') {
|
||||
waveformParseResult.value = waveformResult.value.data
|
||||
} else {
|
||||
waveformParseResult.value = null
|
||||
lastParseErrorMessage.value = getWaveformParseErrorMessage(waveformResult.reason)
|
||||
|
||||
console.error('[waveform] parseComtrade failed', {
|
||||
cfgFileName: cfgFile.name,
|
||||
cfgFileSize: cfgFile.size,
|
||||
datFileName: datFile.name,
|
||||
datFileSize: datFile.size,
|
||||
error
|
||||
})
|
||||
console.error('[waveform] parseComtrade failed', {
|
||||
cfgFileName: cfgFile.name,
|
||||
cfgFileSize: cfgFile.size,
|
||||
datFileName: datFile.name,
|
||||
datFileSize: datFile.size,
|
||||
error: waveformResult.reason
|
||||
})
|
||||
}
|
||||
|
||||
if (vectorResult.status === 'fulfilled') {
|
||||
vectorParseResult.value = vectorResult.value.data
|
||||
} else {
|
||||
vectorParseResult.value = null
|
||||
lastVectorParseErrorMessage.value = getWaveformParseErrorMessage(vectorResult.reason)
|
||||
|
||||
console.error('[waveform] parseComtradeVector failed', {
|
||||
cfgFileName: cfgFile.name,
|
||||
cfgFileSize: cfgFile.size,
|
||||
datFileName: datFile.name,
|
||||
datFileSize: datFile.size,
|
||||
error: vectorResult.reason
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
isParsing.value = false
|
||||
}
|
||||
@@ -586,7 +700,7 @@ const downloadTrendData = () => {
|
||||
|
||||
.waveform-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(360px, 0.95fr);
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(300px, 0.72fr);
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -600,10 +714,3 @@ const downloadTrendData = () => {
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user