波形解析相关20260417
This commit is contained in:
@@ -13,94 +13,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="info-section">
|
<div class="info-section">
|
||||||
<div class="feature-header">向量信息</div>
|
<WaveformVectorInfo
|
||||||
<div v-if="hasVectorData" class="vector-content">
|
:vector-parse-result="vectorParseResult"
|
||||||
<div class="vector-summary-grid">
|
:last-vector-parse-error-message="lastVectorParseErrorMessage"
|
||||||
<div v-for="item in vectorSummaryItems" :key="item.label" class="summary-item vector-summary-item">
|
:active-vector-channel-name="activeVectorChannelName"
|
||||||
<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>
|
||||||
|
|
||||||
<div class="feature-header">特征值</div>
|
<div class="feature-header">特征值</div>
|
||||||
@@ -127,12 +44,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from 'vue'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import type { Waveform } from '@/api/tools/waveform/interface'
|
import type { Waveform } from '@/api/tools/waveform/interface'
|
||||||
import type { FeatureCardItem, SummaryItem } from './types'
|
import type { FeatureCardItem, SummaryItem } from './types'
|
||||||
|
import WaveformVectorInfo from './WaveformVectorInfo.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
hasParsedWaveform: boolean
|
hasParsedWaveform: boolean
|
||||||
summaryItems: SummaryItem[]
|
summaryItems: SummaryItem[]
|
||||||
featureCards: FeatureCardItem[]
|
featureCards: FeatureCardItem[]
|
||||||
@@ -141,188 +57,6 @@ const props = defineProps<{
|
|||||||
lastVectorParseErrorMessage: string
|
lastVectorParseErrorMessage: string
|
||||||
activeVectorChannelName: 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>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
@@ -379,8 +113,7 @@ const activeHarmonicRows = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.empty-text,
|
.empty-text,
|
||||||
.empty-inline,
|
.empty-inline {
|
||||||
.vector-placeholder {
|
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-regular);
|
||||||
@@ -391,29 +124,23 @@ const activeHarmonicRows = computed(() => {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-body {
|
.info-body,
|
||||||
gap: 8px;
|
.info-section {
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-section,
|
|
||||||
.vector-content,
|
|
||||||
.vector-panel-body,
|
|
||||||
.vector-tab-content {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-body {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.summary-grid {
|
.summary-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vector-summary-grid,
|
|
||||||
.vector-card-grid,
|
|
||||||
.sequence-grid,
|
|
||||||
.feature-grid {
|
.feature-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -421,9 +148,7 @@ const activeHarmonicRows = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.summary-item,
|
.summary-item,
|
||||||
.feature-card,
|
.feature-card {
|
||||||
.vector-placeholder,
|
|
||||||
.harmonic-list {
|
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: 1px solid var(--el-border-color-lighter);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -450,10 +175,6 @@ const activeHarmonicRows = computed(() => {
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feature-header--nested {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feature-card {
|
.feature-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -469,55 +190,6 @@ const activeHarmonicRows = computed(() => {
|
|||||||
color: var(--el-text-color-regular);
|
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) {
|
@media (max-width: 1200px) {
|
||||||
.summary-grid {
|
.summary-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
@@ -526,15 +198,8 @@ const activeHarmonicRows = computed(() => {
|
|||||||
|
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.summary-grid,
|
.summary-grid,
|
||||||
.vector-summary-grid,
|
|
||||||
.vector-card-grid,
|
|
||||||
.sequence-grid,
|
|
||||||
.feature-grid {
|
.feature-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.harmonic-row {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
<template>
|
||||||
|
<div class="waveform-vector-info">
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</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'
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: 'WaveformVectorInfo'
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
vectorParseResult: Waveform.WaveComtradeVectorResultVO | null
|
||||||
|
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">
|
||||||
|
.waveform-vector-info,
|
||||||
|
.vector-content,
|
||||||
|
.vector-panel-body,
|
||||||
|
.vector-tab-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text,
|
||||||
|
.empty-inline,
|
||||||
|
.vector-placeholder {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: var(--el-color-danger);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vector-summary-grid,
|
||||||
|
.vector-card-grid,
|
||||||
|
.sequence-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item,
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-label,
|
||||||
|
.feature-card-title,
|
||||||
|
.feature-header {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-value {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-header {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-header--nested {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
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: 992px) {
|
||||||
|
.vector-summary-grid,
|
||||||
|
.vector-card-grid,
|
||||||
|
.sequence-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.harmonic-row {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user