feat(waveform): 添加全通道波形数据显示功能
- 实现全通道模式下的波形数据展示 - 添加通道选择器支持全部/单个通道切换 - 新增全通道趋势分组数据结构 - 重构波形数据获取逻辑支持多通道模式 - 更新图表配置支持动态图例显示控制 - 完善波形数据导出功能支持全通道数据 - 优化工具栏界面适配新的通道选择功能
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user