波形解析相关

This commit is contained in:
2026-04-16 20:20:52 +08:00
parent 9b43f45808
commit 649418a51c
9 changed files with 1213 additions and 460 deletions

View File

@@ -0,0 +1,183 @@
<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>
<div class="summary-value">{{ item.value }}</div>
</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">
<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>
<div v-else class="panel-body">
<div class="empty-block">
<div class="empty-title">暂无解析信息</div>
<div class="empty-text">接口联调完成后右侧会展示波形摘要和特征值</div>
<div v-if="lastParseErrorMessage" class="empty-text error-text">最近一次解析失败{{ lastParseErrorMessage }}</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import type { FeatureCardItem, SummaryItem } from './types'
defineProps<{
hasParsedWaveform: boolean
summaryItems: SummaryItem[]
featureCards: FeatureCardItem[]
lastParseErrorMessage: string
}>()
</script>
<style scoped lang="scss">
.waveform-panel {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding: 16px;
overflow: hidden;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
}
.panel-header {
margin-bottom: 16px;
flex-shrink: 0;
}
.panel-body {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.empty-block {
display: flex;
flex: 1;
min-height: 0;
padding: 12px;
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--cn-color-canvas-bg);
}
.empty-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.empty-text,
.empty-inline,
.panel-tip {
font-size: 13px;
line-height: 1.6;
color: var(--el-text-color-regular);
}
.error-text {
color: var(--el-color-danger);
word-break: break-all;
}
.info-body {
gap: 12px;
overflow: auto;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.summary-item,
.feature-card {
padding: 12px;
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: 4px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.feature-card {
display: flex;
flex-direction: column;
gap: 8px;
}
.feature-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
font-size: 13px;
color: var(--el-text-color-regular);
}
@media (max-width: 992px) {
.summary-grid,
.feature-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,215 @@
<template>
<div class="page-header">
<div class="page-title">波形查看</div>
<div class="action-row">
<div class="file-select-row">
<el-input
:model-value="selectedWaveformFileName"
readonly
placeholder="请选择同一组.cfg和.dat文件"
class="file-input"
/>
<el-button type="primary" :loading="isParsing" @click="openWaveformFilePicker">选择波形</el-button>
<input
ref="waveformFileInputRef"
type="file"
:accept="waveformFileAccept"
multiple
class="waveform-file-input"
@change="handleWaveformFileChange"
/>
</div>
<div class="toolbar-actions">
<div v-if="channelOptions.length" class="toolbar-item">
<div class="toolbar-label">波形通道</div>
<el-select
:model-value="activeChannelIndex"
class="channel-select"
placeholder="选择通道"
@update:model-value="handleChannelChange"
>
<el-option v-for="item in channelOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="toolbar-item">
<div class="toolbar-label">功能显示</div>
<el-select
:model-value="activeDisplayMode"
class="display-mode-select"
placeholder="选择显示模式"
@update:model-value="handleDisplayModeChange"
>
<el-option
v-for="item in displayModeOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="toolbar-item">
<div class="toolbar-label">数值类型</div>
<el-radio-group :model-value="activeValueMode" class="value-mode-switch" @update:model-value="handleValueModeChange">
<el-radio-button v-for="item in valueModeOptions" :key="item.value" :label="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
<el-button type="primary" :disabled="!hasWaveformData" @click="emit('download')">下载数据</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { DisplayMode, LabelValueOption, ValueMode, WaveformDetailOption } from './types'
defineProps<{
selectedWaveformFileName: string
isParsing: boolean
waveformFileAccept: string
channelOptions: WaveformDetailOption[]
activeChannelIndex: number
displayModeOptions: LabelValueOption<DisplayMode>[]
activeDisplayMode: DisplayMode
valueModeOptions: LabelValueOption<ValueMode>[]
activeValueMode: ValueMode
hasWaveformData: boolean
}>()
const emit = defineEmits<{
'update:activeChannelIndex': [value: number]
'update:activeDisplayMode': [value: DisplayMode]
'update:activeValueMode': [value: ValueMode]
'waveform-file-change': [event: Event]
download: []
}>()
const waveformFileInputRef = ref<HTMLInputElement>()
const openWaveformFilePicker = () => {
if (!waveformFileInputRef.value) return
waveformFileInputRef.value.value = ''
waveformFileInputRef.value.click()
}
const handleWaveformFileChange = (event: Event) => {
emit('waveform-file-change', event)
}
const handleChannelChange = (value: number) => {
emit('update:activeChannelIndex', value)
}
const handleDisplayModeChange = (value: DisplayMode) => {
emit('update:activeDisplayMode', value)
}
const handleValueModeChange = (value: string | number | boolean | undefined) => {
if (value === 'primary' || value === 'secondary') {
emit('update:activeValueMode', value)
}
}
</script>
<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);
}
.action-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.file-select-row,
.toolbar-actions,
.toolbar-item {
display: flex;
gap: 12px;
align-items: center;
min-width: 0;
}
.toolbar-actions {
flex-shrink: 0;
}
.toolbar-label,
.file-select-row :deep(.el-input__inner),
.file-select-row :deep(.el-button),
.toolbar-actions :deep(.el-radio-button__inner),
.toolbar-actions :deep(.el-button),
.toolbar-actions :deep(.el-input__inner),
.toolbar-actions :deep(.el-select__placeholder),
.toolbar-actions :deep(.el-select__selected-item) {
font-size: 13px;
}
.file-input {
width: 360px;
max-width: 100%;
}
.channel-select {
width: 220px;
}
.display-mode-select {
width: 160px;
}
.value-mode-switch {
min-width: 150px;
}
.waveform-file-input {
display: none;
}
@media (max-width: 992px) {
.action-row,
.toolbar-actions {
flex-direction: column;
align-items: stretch;
}
.toolbar-item,
.file-select-row {
width: 100%;
}
.channel-select,
.display-mode-select,
.file-input,
.value-mode-switch {
width: 100%;
max-width: none;
}
}
@media (max-width: 768px) {
.page-title {
font-size: 17px;
}
}
</style>

View File

@@ -0,0 +1,181 @@
<template>
<section class="waveform-panel">
<div class="panel-header">
<el-tabs :model-value="activeTrendTab" class="trend-tabs" @update:model-value="handleTrendTabChange">
<el-tab-pane v-for="item in trendTabs" :key="item.value" :label="item.label" :name="item.value" />
</el-tabs>
</div>
<div class="panel-body">
<div v-if="hasWaveformData && activeDisplayMode === 'multi-channel'" class="chart-container">
<LineChart :options="activeTrendOptions" />
</div>
<div v-else-if="hasWaveformData" class="single-channel-list">
<div
v-for="item in singleChannelTrendOptionsList"
:key="item.key"
class="single-channel-card"
:class="{ 'single-channel-card--with-axis': item.isLastChart }"
>
<div class="single-channel-chart">
<LineChart :options="item.options" :group="item.group" />
</div>
</div>
</div>
<div v-else class="empty-block">
<div class="empty-title">暂无波形数据</div>
<div class="empty-text">请选择同一组 `.cfg` `.dat` 文件后自动解析并展示</div>
<div v-if="lastParseErrorMessage" class="empty-text error-text">最近一次解析失败{{ lastParseErrorMessage }}</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import LineChart from '@/components/echarts/line/index.vue'
import type { DisplayMode, LabelValueOption, SingleChannelTrendOption, TrendTabValue } from './types'
defineProps<{
hasWaveformData: boolean
activeDisplayMode: DisplayMode
activeTrendTab: TrendTabValue
trendTabs: LabelValueOption<TrendTabValue>[]
activeTrendOptions: Record<string, unknown>
singleChannelTrendOptionsList: SingleChannelTrendOption[]
lastParseErrorMessage: string
}>()
const emit = defineEmits<{
'update:activeTrendTab': [value: TrendTabValue]
}>()
const handleTrendTabChange = (value: string | number) => {
emit('update:activeTrendTab', value as TrendTabValue)
}
</script>
<style scoped lang="scss">
.waveform-panel {
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
padding: 16px;
overflow: hidden;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
}
.panel-header {
margin-bottom: 16px;
flex-shrink: 0;
}
.trend-tabs {
width: 100%;
flex-shrink: 0;
}
.trend-tabs :deep(.el-tabs__header) {
margin-bottom: 0;
}
.trend-tabs :deep(.el-tabs__nav-wrap::after) {
background-color: var(--el-border-color-light);
}
.trend-tabs :deep(.el-tabs__item) {
font-size: 13px;
}
.panel-body {
display: flex;
flex: 1;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.chart-container,
.empty-block {
display: flex;
flex: 1;
min-height: 0;
padding: 12px;
overflow: hidden;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
}
.empty-block {
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
background: var(--cn-color-canvas-bg);
}
.single-channel-list {
display: flex;
flex: 1;
flex-direction: column;
gap: 12px;
min-height: 0;
overflow: hidden;
}
.single-channel-card {
display: flex;
flex: 1;
flex-direction: column;
gap: 8px;
min-height: 0;
padding: 12px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
background: var(--cn-color-canvas-bg);
overflow: hidden;
}
.single-channel-card--with-axis {
flex: 1.18;
}
.single-channel-chart {
flex: 1;
min-height: 0;
}
.empty-title {
font-size: 14px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.empty-text {
font-size: 13px;
line-height: 1.6;
color: var(--el-text-color-regular);
}
.error-text {
color: var(--el-color-danger);
word-break: break-all;
}
@media (max-width: 768px) {
.trend-tabs {
width: 100%;
}
.trend-tabs :deep(.el-tabs__nav) {
width: 100%;
}
.trend-tabs :deep(.el-tabs__item) {
flex: 1;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,33 @@
export type TrendTabValue = 'instant' | 'rms'
export type ValueMode = 'primary' | 'secondary'
export type DisplayMode = 'single-channel' | 'multi-channel'
export interface LabelValueOption<T extends string | number = string | number> {
label: string
value: T
}
export interface WaveformDetailOption {
label: string
value: number
}
export interface SingleChannelTrendOption {
key: string
group: string
isLastChart?: boolean
options: Record<string, unknown>
}
export interface SummaryItem {
label: string
value: string | number
}
export interface FeatureCardItem {
title: string
rows: Array<{
label: string
value: string
}>
}