feat(waveform): 添加全通道波形数据显示功能

- 实现全通道模式下的波形数据展示
- 添加通道选择器支持全部/单个通道切换
- 新增全通道趋势分组数据结构
- 重构波形数据获取逻辑支持多通道模式
- 更新图表配置支持动态图例显示控制
- 完善波形数据导出功能支持全通道数据
- 优化工具栏界面适配新的通道选择功能
This commit is contained in:
2026-05-06 16:35:48 +08:00
parent bf9f3719a4
commit 407ab0a7f6
4 changed files with 246 additions and 53 deletions

View File

@@ -1,4 +1,4 @@
<template>
<template>
<div class="table-box waveform-page">
<WaveformToolbar
:selected-waveform-file-name="selectedWaveformFileName"
@@ -26,6 +26,8 @@
:trend-tabs="trendTabs"
:active-trend-options="activeTrendOptions"
:single-channel-trend-options-list="singleChannelTrendOptionsList"
:all-channel-trend-groups="allChannelTrendGroups"
:is-all-channels-active="isAllChannelsActive"
:last-parse-error-message="lastParseErrorMessage"
@update:active-trend-tab="activeTrendTab = $event"
/>
@@ -52,6 +54,8 @@ import WaveformInfoPanel from './components/WaveformInfoPanel.vue'
import WaveformToolbar from './components/WaveformToolbar.vue'
import WaveformTrendPanel from './components/WaveformTrendPanel.vue'
import type {
AllChannelTrendGroup,
ChannelSelectValue,
DisplayMode,
LabelValueOption,
SingleChannelTrendOption,
@@ -80,13 +84,15 @@ interface WaveformTrendPayload {
interface TrendChartLayoutOptions {
showTimeAxis?: boolean
showLegend?: boolean
}
const activeTrendTab = ref<TrendTabValue>('instant')
const activeValueMode = ref<ValueMode>('primary')
const activeDisplayMode = ref<DisplayMode>('single-channel')
const activeChannelIndex = ref(0)
const activeChannelIndex = ref<ChannelSelectValue>('all')
const singleChannelTrendChartGroup = 'waveform-single-channel-sync'
const allChannelTrendChartGroup = 'waveform-all-channel-sync'
const isParsing = ref(false)
const selectedCfgFile = ref<File | null>(null)
const selectedDatFile = ref<File | null>(null)
@@ -205,13 +211,19 @@ const normalizedWaveDetails = computed<Waveform.WaveDataDetail[]>(() => {
})
const channelOptions = computed<WaveformDetailOption[]>(() => {
return normalizedWaveDetails.value.map((item, index) => ({
const detailOptions = normalizedWaveDetails.value.map((item, index) => ({
label: buildChannelLabel(item, index),
value: index
}))
return detailOptions.length ? [{ label: '全部', value: 'all' }, ...detailOptions] : []
})
const isAllChannelsActive = computed(() => activeChannelIndex.value === 'all')
const activeWaveDetail = computed(() => {
if (typeof activeChannelIndex.value !== 'number') return null
return normalizedWaveDetails.value[activeChannelIndex.value] || normalizedWaveDetails.value[0] || null
})
@@ -226,13 +238,15 @@ const isCurrentChannel = (channelName?: string) => {
return (channelName || '').toUpperCase().startsWith('I')
}
const activeValueScale = computed(() => {
const getValueScale = (detail: Waveform.WaveDataDetail | null) => {
if (activeValueMode.value === 'secondary') return 1
const waveData = activeWaveData.value
const ratio = isCurrentChannel(activeWaveDetail.value?.channelName) ? waveData?.ct : waveData?.pt
const ratio = isCurrentChannel(detail?.channelName) ? waveData?.ct : waveData?.pt
return normalizeRatio(ratio)
})
}
const activeValueScale = computed(() => getValueScale(activeWaveDetail.value))
const safeNumber = (value: unknown) => {
const numberValue = Number(value)
@@ -262,7 +276,11 @@ const buildChannelLabel = (detail: Waveform.WaveDataDetail, index: number) => {
return `通道 ${index + 1}`
}
const buildTrendPayload = (detail: Waveform.WaveDataDetail | null, trendTab: TrendTabValue, scale: number): WaveformTrendPayload => {
const buildTrendPayload = (
detail: Waveform.WaveDataDetail | null,
trendTab: TrendTabValue,
scale: number
): WaveformTrendPayload => {
const trendData = trendTab === 'instant' ? detail?.instantData : detail?.rmsData
const aName = detail?.a || 'A相'
const bName = detail?.b || 'B相'
@@ -301,12 +319,18 @@ const activeTrendPayload = computed(() => {
return buildTrendPayload(activeWaveDetail.value, activeTrendTab.value, activeValueScale.value)
})
const currentTrendColors = computed(() => {
const customColors = activeWaveDetail.value?.colors?.filter(Boolean) || []
const getTrendColors = (detail: Waveform.WaveDataDetail | null) => {
const customColors = detail?.colors?.filter(Boolean) || []
return customColors.length >= 3 ? customColors.slice(0, 3) : defaultPhaseColors
})
}
const hasWaveformData = computed(() => activeTrendPayload.value.series.length > 0)
const currentTrendColors = computed(() => getTrendColors(activeWaveDetail.value))
const hasWaveformData = computed(() => {
if (isAllChannelsActive.value) return allChannelTrendGroups.value.length > 0
return activeTrendPayload.value.series.length > 0
})
const SYMMETRIC_AXIS_SPLIT_COUNT = 6
const REGULAR_AXIS_SPLIT_COUNT = 5
@@ -408,13 +432,40 @@ const buildTimeAxisLabelFormatter = (timeLabels: string[]) => {
}
}
interface TrendTooltipParam {
axisValue?: string | number
marker?: string
seriesName?: string
value?: number | string
}
const buildTrendTooltipFormatter = (unit: string) => {
return (params: TrendTooltipParam | TrendTooltipParam[]) => {
const paramList = Array.isArray(params) ? params : [params]
const firstParam = paramList[0]
const timeValue = firstParam?.axisValue
const valueRows = paramList
.map(item => {
const marker = item.marker || ''
const seriesName = item.seriesName || ''
const valueText = unit ? `${formatNumber(item.value)} ${unit}` : formatNumber(item.value)
return `<div>${marker}${seriesName}<span style="float:right;margin-left:12px;font-weight:600;">${valueText}</span></div>`
})
.join('')
const timeText = timeValue === undefined ? '--' : `${formatNumber(timeValue, 2)} ms`
return `${valueRows}<div style="margin-top:4px;">时间<span style="float:right;margin-left:12px;">${timeText}</span></div>`
}
}
const buildTrendChartOptions = (
trendPayload: WaveformTrendPayload,
seriesList: WaveformSeriesItem[],
chartColors = currentTrendColors.value,
layoutOptions: TrendChartLayoutOptions = {}
) => {
const { showTimeAxis = true } = layoutOptions
const { showTimeAxis = true, showLegend = true } = layoutOptions
return {
tooltip: {
@@ -427,16 +478,15 @@ const buildTrendChartOptions = (
width: 1
}
},
valueFormatter: (value: number) => {
return trendPayload.unit ? `${formatNumber(value)} ${trendPayload.unit}` : formatNumber(value)
}
formatter: buildTrendTooltipFormatter(trendPayload.unit)
},
legend: {
show: showLegend,
top: 0,
right: 12
},
grid: {
top: '18px',
top: showLegend ? '18px' : '6px',
left: '24px',
right: showTimeAxis ? '32px' : '24px',
bottom: showTimeAxis ? '16px' : '6px'
@@ -493,24 +543,68 @@ const activeTrendOptions = computed<Record<string, unknown>>(() => {
})
// 单通道模式按相别拆成多个图,便于分别观察各通道波形。
const singleChannelTrendOptionsList = computed<SingleChannelTrendOption[]>(() => {
const trendPayload = activeTrendPayload.value
const buildSingleChannelTrendOptionsList = (
detail: Waveform.WaveDataDetail | null,
detailIndex: number,
chartGroup: string,
resolveShowTimeAxis = (seriesIndex: number, seriesCount: number) => seriesIndex === seriesCount - 1
): SingleChannelTrendOption[] => {
const trendPayload = buildTrendPayload(detail, activeTrendTab.value, getValueScale(detail))
const trendColors = getTrendColors(detail)
// 单通道下每张图只保留一个 series需要单独指定对应相色。
return trendPayload.series.map((item, index) => ({
key: item.name,
group: singleChannelTrendChartGroup,
isLastChart: index === trendPayload.series.length - 1,
options: buildTrendChartOptions(
trendPayload,
[item],
[currentTrendColors.value[index] || defaultPhaseColors[index]],
{
// 仅最后一张图显示时间轴,并通过卡片高度微调补偿其额外占用空间。
showTimeAxis: index === trendPayload.series.length - 1
return trendPayload.series.map((item, index) => {
const showTimeAxis = resolveShowTimeAxis(index, trendPayload.series.length)
return {
key: `${detailIndex}-${item.name}`,
group: chartGroup,
isLastChart: showTimeAxis,
options: buildTrendChartOptions(trendPayload, [item], [trendColors[index] || defaultPhaseColors[index]], {
// 单通道下每张图只有一条曲线,图例信息与图表标题重复。
showTimeAxis,
showLegend: false
})
}
})
}
const singleChannelTrendOptionsList = computed<SingleChannelTrendOption[]>(() => {
return buildSingleChannelTrendOptionsList(activeWaveDetail.value, -1, singleChannelTrendChartGroup)
})
const allChannelTrendGroups = computed<AllChannelTrendGroup[]>(() => {
const availableDetails = normalizedWaveDetails.value
.map((detail, index) => {
const trendPayload = buildTrendPayload(detail, activeTrendTab.value, getValueScale(detail))
return {
detail,
index,
trendPayload
}
)
}))
})
.filter(item => item.trendPayload.series.length > 0)
return availableDetails.map((item, groupIndex) => {
const isLastGroup = groupIndex === availableDetails.length - 1
return {
key: `${item.index}-${buildChannelLabel(item.detail, item.index)}`,
title: buildChannelLabel(item.detail, item.index),
singleChannelOptionsList: buildSingleChannelTrendOptionsList(
item.detail,
item.index,
allChannelTrendChartGroup,
(seriesIndex, seriesCount) => isLastGroup && seriesIndex === seriesCount - 1
),
multiChannelOptions: buildTrendChartOptions(
item.trendPayload,
item.trendPayload.series,
getTrendColors(item.detail)
)
}
})
})
const summaryItems = computed<SummaryItem[]>(() => {
@@ -523,7 +617,14 @@ const summaryItems = computed<SummaryItem[]>(() => {
{ 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: isAllChannelsActive.value
? '全部'
: detail
? buildChannelLabel(detail, activeChannelIndex.value as number)
: '--'
},
{ label: '单位', value: detail?.unit || '--' },
{ label: '相别数量', value: waveData?.iPhasic ?? '--' },
{ label: 'PT / CT', value: `${formatNumber(waveData?.pt, 2)} / ${formatNumber(waveData?.ct, 2)}` },
@@ -583,7 +684,7 @@ const handleWaveformFileChange = async (event: Event) => {
const loadWaveformData = async (cfgFile: File, datFile: File) => {
try {
isParsing.value = true
activeChannelIndex.value = 0
activeChannelIndex.value = 'all'
lastParseErrorMessage.value = ''
lastVectorParseErrorMessage.value = ''
@@ -641,16 +742,46 @@ const downloadTrendData = () => {
}
const trendPayload = activeTrendPayload.value
const allChannelPayloads = normalizedWaveDetails.value
.map((detail, index) => ({
label: buildChannelLabel(detail, index),
payload: buildTrendPayload(detail, activeTrendTab.value, getValueScale(detail))
}))
.filter(item => item.payload.series.length > 0)
const exportPayload = isAllChannelsActive.value ? allChannelPayloads[0]?.payload : trendPayload
if (!exportPayload) {
ElMessage.warning('暂无可导出的波形数据')
return
}
const header = ['时间', ...trendPayload.series.map(item => item.name)]
const rows = trendPayload.timeLabels.map((time, index) => {
const allChannelHeader = [
'时间',
...allChannelPayloads.flatMap(item => item.payload.series.map(series => `${item.label}-${series.name}`))
]
const rows = exportPayload.timeLabels.map((time, index) => {
if (isAllChannelsActive.value) {
return [
time,
...allChannelPayloads.flatMap(item => item.payload.series.map(series => series.data[index] ?? ''))
]
}
return [time, ...trendPayload.series.map(item => item.data[index] ?? '')]
})
const csvContent = [header, ...rows].map(row => row.join(',')).join('\n')
const csvContent = [isAllChannelsActive.value ? allChannelHeader : header, ...rows]
.map(row => row.join(','))
.join('\n')
const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' })
const blobUrl = URL.createObjectURL(blob)
const exportFile = document.createElement('a')
const channelLabel = activeWaveDetail.value ? buildChannelLabel(activeWaveDetail.value, activeChannelIndex.value) : '波形'
const channelLabel = isAllChannelsActive.value
? '全部'
: activeWaveDetail.value
? buildChannelLabel(activeWaveDetail.value, activeChannelIndex.value as number)
: '波形'
const fileName = `波形查看_${channelLabel}_${valueModeLabelMap[activeValueMode.value]}_${trendLabelMap[activeTrendTab.value]}.csv`
exportFile.style.display = 'none'
@@ -686,4 +817,3 @@ const downloadTrendData = () => {
}
}
</style>