波形解析相关
This commit is contained in:
@@ -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>
|
||||
215
frontend/src/views/tools/waveform/components/WaveformToolbar.vue
Normal file
215
frontend/src/views/tools/waveform/components/WaveformToolbar.vue
Normal 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>
|
||||
@@ -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>
|
||||
33
frontend/src/views/tools/waveform/components/types.ts
Normal file
33
frontend/src/views/tools/waveform/components/types.ts
Normal 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
|
||||
}>
|
||||
}
|
||||
Reference in New Issue
Block a user