波形解析相关

This commit is contained in:
2026-04-17 08:10:46 +08:00
parent 649418a51c
commit e4051cf151
6 changed files with 621 additions and 83 deletions

View File

@@ -1,12 +1,10 @@
<template>
<template>
<section class="waveform-panel">
<div class="panel-header">
<div class="section-title">波形信息</div>
</div>
<div v-if="hasParsedWaveform" class="panel-body info-body">
<div class="panel-tip">当前接口未返回向量图坐标右侧展示解析摘要与特征值结果</div>
<div class="summary-grid">
<div v-for="item in summaryItems" :key="item.label" class="summary-item">
<div class="summary-label">{{ item.label }}</div>
@@ -14,6 +12,97 @@
</div>
</div>
<div class="info-section">
<div class="feature-header">向量信息</div>
<div v-if="hasVectorData" class="vector-content">
<div class="vector-summary-grid">
<div v-for="item in vectorSummaryItems" :key="item.label" class="summary-item vector-summary-item">
<div class="summary-label">{{ item.label }}</div>
<div class="summary-value">{{ item.value }}</div>
</div>
</div>
<el-tabs v-model="activeVectorGroupKey" class="vector-tabs">
<el-tab-pane
v-for="(group, index) in vectorGroups"
:key="buildVectorGroupKey(group, index)"
:label="group.channelName || `分组 ${index + 1}`"
:name="buildVectorGroupKey(group, index)"
/>
</el-tabs>
<div v-if="activeVectorGroup" class="vector-panel-body">
<el-tabs v-model="activeCycleKey" class="vector-tabs vector-cycle-tabs">
<el-tab-pane
v-for="(cycle, index) in activeVectorSeries"
:key="buildCycleKey(cycle, index)"
:label="buildCycleLabel(cycle, index)"
:name="buildCycleKey(cycle, index)"
/>
</el-tabs>
<div v-if="activeCycle" class="vector-tab-content">
<div v-if="phaseVectorCards.length" class="vector-card-grid">
<div v-for="item in phaseVectorCards" :key="item.title" class="feature-card">
<div class="feature-card-title">{{ item.title }}</div>
<div v-for="row in item.rows" :key="row.label" class="feature-row">
<span>{{ row.label }}</span>
<span>{{ row.value }}</span>
</div>
</div>
</div>
<div v-else class="empty-inline">当前周波未返回相量结果</div>
<div class="feature-header feature-header--nested">序分量与不平衡度</div>
<div class="vector-card-grid sequence-grid">
<div v-for="item in sequenceCards" :key="item.title" class="feature-card">
<div class="feature-card-title">{{ item.title }}</div>
<div v-for="row in item.rows" :key="row.label" class="feature-row">
<span>{{ row.label }}</span>
<span>{{ row.value }}</span>
</div>
</div>
</div>
<div class="feature-header feature-header--nested">谐波信息</div>
<el-tabs v-if="activePhaseVectors.length" v-model="activePhaseKey" class="vector-tabs vector-phase-tabs">
<el-tab-pane
v-for="(phase, index) in activePhaseVectors"
:key="buildPhaseKey(phase, index)"
:label="phase.phaseName || `相别 ${index + 1}`"
:name="buildPhaseKey(phase, index)"
/>
</el-tabs>
<div v-if="activeHarmonicRows.length" class="harmonic-list">
<div class="harmonic-row harmonic-row--header">
<span>次数</span>
<span>幅值</span>
<span>有效值</span>
<span>占比</span>
</div>
<div
v-for="item in activeHarmonicRows"
:key="`${activePhaseKey}-${item.harmonicOrder}`"
class="harmonic-row"
>
<span>{{ item.harmonicOrder ?? '--' }}</span>
<span>{{ formatWaveValue(item.amplitude, activeVectorGroup?.unit) }}</span>
<span>{{ formatWaveValue(item.rms, activeVectorGroup?.unit) }}</span>
<span>{{ formatPercentValue(item.rate) }}</span>
</div>
</div>
<div v-else class="empty-inline">当前相未返回谐波结果</div>
</div>
<div v-else class="empty-inline">当前分组未返回逐周波结果</div>
</div>
</div>
<div v-else class="vector-placeholder">向量信息待接口返回后展示</div>
<div v-if="lastVectorParseErrorMessage" class="empty-text error-text">
最近一次向量解析失败{{ lastVectorParseErrorMessage }}
</div>
</div>
<div class="feature-header">特征值</div>
<div v-if="featureCards.length" class="feature-grid">
<div v-for="item in featureCards" :key="item.title" class="feature-card">
@@ -30,7 +119,7 @@
<div v-else class="panel-body">
<div class="empty-block">
<div class="empty-title">暂无解析信息</div>
<div class="empty-text">接口联调完成后右侧会展示波形摘要和特征值</div>
<div class="empty-text">接口联调完成后右侧会展示波形信息向量信息和特征值</div>
<div v-if="lastParseErrorMessage" class="empty-text error-text">最近一次解析失败{{ lastParseErrorMessage }}</div>
</div>
</div>
@@ -38,14 +127,202 @@
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import dayjs from 'dayjs'
import type { Waveform } from '@/api/tools/waveform/interface'
import type { FeatureCardItem, SummaryItem } from './types'
defineProps<{
const props = defineProps<{
hasParsedWaveform: boolean
summaryItems: SummaryItem[]
featureCards: FeatureCardItem[]
vectorParseResult: Waveform.WaveComtradeVectorResultVO | null
lastParseErrorMessage: string
lastVectorParseErrorMessage: string
activeVectorChannelName: string
}>()
const activeVectorGroupKey = ref('')
const activeCycleKey = ref('')
const activePhaseKey = ref('')
const formatNumber = (value: unknown, fractionDigits = 3) => {
const numberValue = Number(value)
if (!Number.isFinite(numberValue)) return '--'
if (Number.isInteger(numberValue)) return `${numberValue}`
return `${Number(numberValue.toFixed(fractionDigits))}`
}
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 formatWaveValue = (value: unknown, unit?: string) => {
const formattedValue = formatNumber(value)
if (formattedValue === '--') return '--'
return unit ? `${formattedValue} ${unit}` : formattedValue
}
const formatPhaseAngle = (value: unknown) => {
const formattedValue = formatNumber(value)
return formattedValue === '--' ? '--' : `${formattedValue} °`
}
const formatPercentValue = (value: unknown) => {
const formattedValue = formatNumber(value)
return formattedValue === '--' ? '--' : `${formattedValue}%`
}
const formatCycleTime = (value: unknown) => {
const formattedValue = formatNumber(value)
return formattedValue === '--' ? '--' : `${formattedValue} ms`
}
const buildVectorGroupKey = (group: Waveform.WaveVectorGroupDTO, index: number) => {
return group.channelName || `group-${index}`
}
const buildCycleKey = (cycle: Waveform.WaveCycleVectorDTO, index: number) => {
return `${cycle.cycleIndex ?? index}`
}
const buildPhaseKey = (phase: Waveform.WavePhaseVectorDTO, index: number) => {
return phase.phaseName || `phase-${index}`
}
const buildCycleLabel = (cycle: Waveform.WaveCycleVectorDTO, index: number) => {
return `周波 ${Number(cycle.cycleIndex ?? index) + 1}`
}
const vectorGroups = computed(() => props.vectorParseResult?.vectorGroups || [])
const hasVectorData = computed(() => vectorGroups.value.length > 0)
watch(
[vectorGroups, () => props.activeVectorChannelName],
([groups, activeChannelName]) => {
if (!groups.length) {
activeVectorGroupKey.value = ''
return
}
const matchedIndex = groups.findIndex(item => item.channelName === activeChannelName)
const nextIndex = matchedIndex >= 0 ? matchedIndex : 0
activeVectorGroupKey.value = buildVectorGroupKey(groups[nextIndex], nextIndex)
},
{ immediate: true }
)
const activeVectorGroup = computed(() => {
return vectorGroups.value.find((item, index) => buildVectorGroupKey(item, index) === activeVectorGroupKey.value) || vectorGroups.value[0] || null
})
const activeVectorSeries = computed(() => activeVectorGroup.value?.vectorSeries || [])
watch(
activeVectorSeries,
series => {
if (!series.length) {
activeCycleKey.value = ''
return
}
if (series.some((item, index) => buildCycleKey(item, index) === activeCycleKey.value)) return
activeCycleKey.value = buildCycleKey(series[0], 0)
},
{ immediate: true }
)
const activeCycle = computed(() => {
return activeVectorSeries.value.find((item, index) => buildCycleKey(item, index) === activeCycleKey.value) || activeVectorSeries.value[0] || null
})
const activePhaseVectors = computed(() => activeCycle.value?.phaseVectors || [])
watch(
activePhaseVectors,
phaseVectors => {
if (!phaseVectors.length) {
activePhaseKey.value = ''
return
}
if (phaseVectors.some((item, index) => buildPhaseKey(item, index) === activePhaseKey.value)) return
activePhaseKey.value = buildPhaseKey(phaseVectors[0], 0)
},
{ immediate: true }
)
const activePhaseVector = computed(() => {
return activePhaseVectors.value.find((item, index) => buildPhaseKey(item, index) === activePhaseKey.value) || activePhaseVectors.value[0] || null
})
const vectorSummaryItems = computed<SummaryItem[]>(() => {
const activeGroup = activeVectorGroup.value
const currentCycle = activeCycle.value
return [
{ label: '测点名称', value: props.vectorParseResult?.monitorName || '--' },
{ label: '事件时间', value: formatWaveformTime(props.vectorParseResult?.time) },
{ label: '每周波采样点数', value: props.vectorParseResult?.samplePerCycle ?? '--' },
{ label: '可计算周波数', value: props.vectorParseResult?.cycleCount ?? '--' },
{ label: '当前分组', value: activeGroup?.channelName || '--' },
{ label: '当前周波', value: currentCycle ? buildCycleLabel(currentCycle, Number(currentCycle.cycleIndex ?? 0)) : '--' }
]
})
const phaseVectorCards = computed<FeatureCardItem[]>(() => {
const activeGroup = activeVectorGroup.value
return activePhaseVectors.value.map(item => ({
title: item.phaseName || '相量',
rows: [
{ label: '总有效值', value: formatWaveValue(item.totalRms, activeGroup?.unit) },
{ label: '基波幅值', value: formatWaveValue(item.fundamentalAmplitude, activeGroup?.unit) },
{ label: '基波有效值', value: formatWaveValue(item.fundamentalRms, activeGroup?.unit) },
{ label: '基波相角', value: formatPhaseAngle(item.fundamentalPhaseAngle) },
{ label: '谐波畸变率', value: formatPercentValue(item.harmonicDistortionRate) }
]
}))
})
const sequenceCards = computed<FeatureCardItem[]>(() => {
const activeGroup = activeVectorGroup.value
const currentCycle = activeCycle.value
const sequenceList = [currentCycle?.positiveSequence, currentCycle?.negativeSequence, currentCycle?.zeroSequence]
.filter(Boolean)
.map(item => ({
title: item?.sequenceName || '序分量',
rows: [
{ label: '幅值', value: formatWaveValue(item?.amplitude, activeGroup?.unit) },
{ label: '有效值', value: formatWaveValue(item?.rms, activeGroup?.unit) },
{ label: '相角', value: formatPhaseAngle(item?.phaseAngle) }
]
}))
return [
...sequenceList,
{
title: '不平衡度',
rows: [
{ label: '负序不平衡度', value: formatPercentValue(currentCycle?.unbalance?.negativeUnbalanceRate) },
{ label: '零序不平衡度', value: formatPercentValue(currentCycle?.unbalance?.zeroUnbalanceRate) },
{ label: '周波中点', value: formatCycleTime(currentCycle?.time) }
]
}
]
})
const activeHarmonicRows = computed(() => {
if (!activePhaseVector.value) return []
return activePhaseVector.value.harmonicVoltageContentRates || activePhaseVector.value.harmonicCurrentAmplitudes || []
})
</script>
<style scoped lang="scss">
@@ -54,7 +331,7 @@ defineProps<{
flex-direction: column;
min-width: 0;
min-height: 0;
padding: 16px;
padding: 12px;
overflow: hidden;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
@@ -62,7 +339,7 @@ defineProps<{
}
.panel-header {
margin-bottom: 16px;
margin-bottom: 12px;
flex-shrink: 0;
}
@@ -103,7 +380,7 @@ defineProps<{
.empty-text,
.empty-inline,
.panel-tip {
.vector-placeholder {
font-size: 13px;
line-height: 1.6;
color: var(--el-text-color-regular);
@@ -115,19 +392,39 @@ defineProps<{
}
.info-body {
gap: 12px;
gap: 8px;
overflow: auto;
}
.info-section,
.vector-content,
.vector-panel-body,
.vector-tab-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.vector-summary-grid,
.vector-card-grid,
.sequence-grid,
.feature-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
gap: 8px;
}
.summary-item,
.feature-card {
padding: 12px;
.feature-card,
.vector-placeholder,
.harmonic-list {
padding: 10px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
background: var(--cn-color-canvas-bg);
@@ -150,19 +447,17 @@ defineProps<{
}
.feature-header {
margin-top: 4px;
margin-top: 2px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
.feature-header--nested {
margin-top: 0;
}
.feature-card {
display: flex;
flex-direction: column;
gap: 8px;
gap: 6px;
}
.feature-row {
@@ -174,10 +469,72 @@ defineProps<{
color: var(--el-text-color-regular);
}
.vector-tabs {
width: 100%;
flex-shrink: 0;
}
.vector-tabs :deep(.el-tabs__header) {
margin-bottom: 0;
}
.vector-tabs :deep(.el-tabs__nav-wrap::after) {
background-color: var(--el-border-color-light);
}
.vector-tabs :deep(.el-tabs__item) {
font-size: 13px;
}
.harmonic-list {
display: flex;
flex-direction: column;
gap: 0;
max-height: 240px;
overflow: auto;
padding: 0;
}
.harmonic-row {
display: grid;
grid-template-columns: 52px repeat(3, minmax(0, 1fr));
gap: 8px;
padding: 8px 10px;
font-size: 12px;
color: var(--el-text-color-regular);
border-bottom: 1px solid var(--el-border-color-lighter);
}
.harmonic-row:last-child {
border-bottom: 0;
}
.harmonic-row--header {
position: sticky;
top: 0;
z-index: 1;
font-weight: 600;
color: var(--el-text-color-primary);
background: var(--el-bg-color);
}
@media (max-width: 1200px) {
.summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 992px) {
.summary-grid,
.vector-summary-grid,
.vector-card-grid,
.sequence-grid,
.feature-grid {
grid-template-columns: 1fr;
}
.harmonic-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>

View File

@@ -1,6 +1,5 @@
<template>
<div class="page-header">
<div class="page-title">波形查看</div>
<div class="action-row">
<div class="file-select-row">
@@ -122,16 +121,8 @@ const handleValueModeChange = (value: string | number | boolean | undefined) =>
<style scoped lang="scss">
.page-header {
display: flex;
flex-direction: column;
gap: 12px;
flex-shrink: 0;
}
.page-title {
font-size: 18px;
font-weight: 600;
line-height: 1.5;
color: var(--el-text-color-primary);
flex-shrink: 0;
}
.action-row {
@@ -207,9 +198,5 @@ const handleValueModeChange = (value: string | number | boolean | undefined) =>
}
}
@media (max-width: 768px) {
.page-title {
font-size: 17px;
}
}
</style>

View File

@@ -68,7 +68,7 @@ const handleTrendTabChange = (value: string | number) => {
}
.panel-header {
margin-bottom: 16px;
margin-bottom: 12px;
flex-shrink: 0;
}
@@ -102,7 +102,7 @@ const handleTrendTabChange = (value: string | number) => {
display: flex;
flex: 1;
min-height: 0;
padding: 12px;
padding: 8px;
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
@@ -112,7 +112,7 @@ const handleTrendTabChange = (value: string | number) => {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
gap: 4px;
background: var(--cn-color-canvas-bg);
}
@@ -120,7 +120,7 @@ const handleTrendTabChange = (value: string | number) => {
display: flex;
flex: 1;
flex-direction: column;
gap: 12px;
gap: 4px;
min-height: 0;
overflow: hidden;
}
@@ -129,9 +129,9 @@ const handleTrendTabChange = (value: string | number) => {
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
gap: 4px;
min-height: 0;
padding: 12px;
padding: 8px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
background: var(--cn-color-canvas-bg);
@@ -139,7 +139,7 @@ const handleTrendTabChange = (value: string | number) => {
}
.single-channel-card--with-axis {
flex: 1.18;
flex: 1.1;
}
.single-channel-chart {