新建监控功能

This commit is contained in:
2026-04-23 11:09:06 +08:00
parent 7dee9092dc
commit 287de846a6
28 changed files with 6263 additions and 703 deletions

View File

@@ -1,266 +0,0 @@
# parseComtrade API 文档
## 1. 接口概述
- 接口名称:解析 COMTRADE 波形文件
- Controller[WaveController.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/controller/WaveController.java)
- 方法:`parseComtrade`
- 请求路径:`POST /wave/parseComtrade`
- Content-Type`multipart/form-data`
- 返回类型:`HttpResult<WaveComtradeResultVO>`
用途说明:
- 上传一组 COMTRADE `cfg/dat` 文件
- 解析原始波形数据
- 按请求决定是否补充 RMS 数据、前端查看明细和特征值结果
## 2. 请求参数
### 2.1 文件参数
| 参数名 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `cfgFile` | file | 是 | COMTRADE 配置文件 `.cfg` |
| `datFile` | file | 是 | COMTRADE 数据文件 `.dat` |
### 2.2 表单参数
参数定义来源:[WaveComtradeParseParam.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/param/WaveComtradeParseParam.java)
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `parseType` | integer | 否 | `1` | 解析类型:`0` 高级算法采样率 32-128`1` 普通展示,`2` App 抽点,`3` 原始波形 |
| `ptType` | integer | 否 | `0` | PT 接线方式:`0` 星形,`1` 三角,`2` 开口三角 |
| `pt` | number | 否 | `1` | PT 变比 |
| `ct` | number | 否 | `1` | CT 变比 |
| `monitorName` | string | 否 | `未命名测点` | 测点名称 |
| `calculateRms` | boolean | 否 | `true` | 是否计算 RMS |
| `buildDetails` | boolean | 否 | `true` | 是否构建前端查看明细 |
| `calculateEigenvalue` | boolean | 否 | `false` | 是否计算特征值 |
| `dynamicThreshold` | boolean | 否 | `true` | 特征值是否使用浮动门槛 |
## 3. 请求示例
```bash
curl -X POST "http://localhost:8080/wave/parseComtrade" \
-F "cfgFile=@D:/00-B7-8D-00-E4-09/1_20260321_201458_748.CFG" \
-F "datFile=@D:/00-B7-8D-00-E4-09/1_20260321_201458_748.DAT" \
-F "parseType=1" \
-F "ptType=0" \
-F "pt=1" \
-F "ct=1" \
-F "monitorName=监测点1" \
-F "calculateRms=true" \
-F "buildDetails=true" \
-F "calculateEigenvalue=true" \
-F "dynamicThreshold=true"
```
## 4. 响应结构
### 4.1 外层响应
Controller 返回的是 `HttpResult<WaveComtradeResultVO>`。当前仓库内未展开 `HttpResult` 类型源码,本接口文档只对业务 `data` 部分做精确定义。
业务数据类型来源:[WaveComtradeResultVO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/vo/WaveComtradeResultVO.java)
### 4.2 data 字段定义
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `waveData` | object | 波形基础数据 |
| `waveDataDetails` | array | 前端查看明细,`buildDetails=true` 时返回 |
| `eigenvalues` | array | 特征值结果,`calculateEigenvalue=true` 时返回 |
## 5. 业务对象说明
### 5.1 waveData
定义来源:[WaveDataDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/WaveDataDTO.java)
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `comtradeCfgDTO` | object | CFG 解析结果 |
| `waveTitle` | array<string> | 波形标题,例如 `["Time","UA相","UB相"]` |
| `channelNames` | array<string> | 通道名称列表 |
| `listWaveData` | array<array<number>> | 原始波形数据,首列为时间,后续列为相电压/电流值 |
| `listRmsData` | array<array<number>> | RMS 波形数据,`calculateRms=true` 时可用 |
| `listRmsMinData` | array<array<number>> | RMS 最小值摘要 |
| `iPhasic` | integer | 相别数量 |
| `ptType` | integer | PT 接线方式 |
| `pt` | number | PT 变比 |
| `ct` | number | CT 变比 |
| `time` | string | 事件发生时刻 |
| `monitorName` | string | 测点名称 |
### 5.2 comtradeCfgDTO
定义来源:[ComtradeCfgDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/ComtradeCfgDTO.java)
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `nChannelNum` | integer | 通道总数 |
| `nPhasic` | integer | 相别数量 |
| `nAnalogNum` | integer | 模拟量通道数 |
| `nDigitalNum` | integer | 开关量通道数 |
| `timeStart` | string/date | 录波开始时间 |
| `timeTrige` | string/date | 触发时间 |
| `lstAnalogDTO` | array | 模拟量通道配置 |
| `lstDigitalDTO` | array | 开关量通道配置 |
| `nRates` | integer | 采样率分段数 |
| `lstRate` | array | 采样率分段配置 |
| `firstTime` | string/date | 首个触发时间对象 |
| `firstMs` | integer | 首个触发毫秒值 |
| `nPush` | integer | 触发前推点数 |
| `finalSampleRate` | integer | 最终采样率 |
| `nAllWaveNum` | integer | 总周波数 |
| `strBinType` | string | 文件编码类型,例如 `BINARY` |
### 5.3 waveDataDetails
定义来源:[WaveDataDetail.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/bo/WaveDataDetail.java)
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `instantData` | object | 瞬时波形数据 |
| `rmsData` | object | RMS 波形数据 |
| `a` | string | A 相名称 |
| `b` | string | B 相名称 |
| `c` | string | C 相名称 |
| `channelName` | string | 通道名称 |
| `unit` | string | 单位 |
| `isOpen` | boolean | 是否开口三角模式 |
| `title` | string | 当前图标题 |
| `colors` | array<string> | 曲线颜色 |
其中 `instantData``rmsData` 结构一致,定义分别来自:
- [InstantData.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/bo/InstantData.java)
- [RmsData.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/bo/RmsData.java)
公共字段:
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `max` | number | 当前曲线最大值 |
| `min` | number | 当前曲线最小值 |
| `aValue` | array<array<number>> | A 相点位 |
| `bValue` | array<array<number>> | B 相点位 |
| `cValue` | array<array<number>> | C 相点位 |
### 5.4 eigenvalues
定义来源:[EigenvalueDTO.java](D:/Work/SourceCode/CN_Tool/tools/wave-tool/src/main/java/com/njcn/gather/tool/wave/pojo/dto/EigenvalueDTO.java)
| 字段名 | 类型 | 说明 |
| --- | --- | --- |
| `amplitude` | number | 特征幅值百分比 |
| `residualVoltage` | number | 残余电压 |
| `ratedVoltage` | number | 额定电压 |
| `durationTime` | number | 持续时间 |
## 6. 成功响应示例
以下示例基于真实样本文件联测结果整理,长数组做了截断展示。
```json
{
"code": "SUCCESS",
"message": "成功",
"data": {
"waveData": {
"comtradeCfgDTO": {
"nChannelNum": 6,
"nPhasic": 3,
"nAnalogNum": 6,
"nDigitalNum": 0,
"timeStart": "2026-03-21 20:14:58.648",
"timeTrige": "2026-03-21 20:14:58.748",
"nRates": 1,
"firstMs": 748,
"nPush": 100,
"finalSampleRate": 512,
"nAllWaveNum": 30,
"strBinType": "BINARY"
},
"waveTitle": ["Time", "UA相", "UB相", "UC相", "IA相", "IB相", "IC相"],
"channelNames": ["/", "U1", "U2", "U3", "I1", "I2", "I3"],
"listWaveData": {
"count": 15616,
"first": [-100.0, -146.56, -76.9, -76.9, -0.13, 0.01, -0.2],
"last": [509.96, 148.02, 69.73, 69.75, 0.16, 0.01, 0.15]
},
"listRmsData": {
"count": 15616,
"first": [-100.0, 104.94, 104.22, 104.23, 0.27, 0.01, 0.28],
"last": [509.96, 105.6, 105.1, 105.12, 0.24, 0.01, 0.24]
},
"listRmsMinData": [
[40.74, 41.2],
[362.19, 0.01]
],
"iPhasic": 3,
"ptType": 0,
"pt": 1.0,
"ct": 1.0,
"time": "2026-03-21 20:14:58.748",
"monitorName": "监测点1"
},
"waveDataDetails": [
{
"channelName": "U1",
"unit": "kV",
"a": "A相",
"b": "B相",
"c": "C相",
"isOpen": false
},
{
"channelName": "I1",
"unit": "A",
"a": "A相",
"b": "B相",
"c": "C相",
"isOpen": false
}
],
"eigenvalues": [
{
"amplitude": 0.3926178,
"residualVoltage": 41.200005,
"ratedVoltage": 104.936676,
"durationTime": 48.632812
},
{
"amplitude": 0.4067544,
"residualVoltage": 42.390152,
"ratedVoltage": 104.21559,
"durationTime": 54.492188
},
{
"amplitude": 0.40674016,
"residualVoltage": 42.396355,
"ratedVoltage": 104.2345,
"durationTime": 54.492188
}
]
}
}
```
## 7. 失败场景
基于当前代码,常见失败场景包括:
| 场景 | 说明 |
| --- | --- |
| `cfgFile``datFile` 未上传 | 返回业务异常提示“cfg 或 dat 文件不能为空” |
| CFG 文件格式错误 | 返回 CFG 解析失败 |
| DAT 文件为空或格式错误 | 返回 DAT 解析失败 |
| COMTRADE 解析过程中出现异常 | 返回“COMTRADE 波形解析失败” |
## 8. 备注
- 当前接口已经移除图片生成相关参数,不再支持 `generateInstantImage``generateRmsImage` 等旧字段。
- 当前接口文档只覆盖 `parseComtrade`,其他波形文本解析接口请单独编写。

View File

@@ -14,6 +14,7 @@
<div class="summary-value">{{ item.value }}</div>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="向量信息" name="vector">
@@ -23,27 +24,16 @@
:last-vector-parse-error-message="lastVectorParseErrorMessage"
:active-vector-channel-name="activeVectorChannelName"
/>
</div>
</el-tab-pane>
</el-tabs>
<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 class="empty-text">接口联调完成后右侧会展示波形信息和向量信息</div>
<div v-if="lastParseErrorMessage" class="empty-text error-text">最近一次解析失败{{ lastParseErrorMessage }}</div>
</div>
</div>
@@ -53,13 +43,12 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { Waveform } from '@/api/tools/waveform/interface'
import type { FeatureCardItem, SummaryItem } from './types'
import type { SummaryItem } from './types'
import WaveformVectorInfo from './WaveformVectorInfo.vue'
defineProps<{
hasParsedWaveform: boolean
summaryItems: SummaryItem[]
featureCards: FeatureCardItem[]
vectorParseResult: Waveform.WaveComtradeVectorResultVO | null
lastParseErrorMessage: string
lastVectorParseErrorMessage: string
@@ -143,18 +132,40 @@ const activeInfoTab = ref('waveform')
}
.info-body {
overflow: auto;
overflow: hidden;
}
.info-tabs {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
flex-direction: column;
width: 100%;
flex-shrink: 0;
}
.info-tabs :deep(.el-tabs__header) {
margin-bottom: 8px;
}
.info-tabs :deep(.el-tabs__content) {
flex: 1;
min-height: 0;
overflow: hidden;
}
.info-tabs :deep(.el-tab-pane) {
height: 100%;
}
.info-tab-panel {
flex: 1;
height: 100%;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
}
.info-tabs :deep(.el-tabs__item) {
font-size: 13px;
}
@@ -165,23 +176,15 @@ const activeInfoTab = ref('waveform')
gap: 8px;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.summary-item,
.feature-card {
.summary-item {
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 {
.summary-label {
font-size: 13px;
font-weight: 600;
color: var(--el-text-color-primary);
@@ -195,24 +198,7 @@ const activeInfoTab = ref('waveform')
word-break: break-all;
}
.feature-header {
margin-top: 2px;
}
.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);
}
@media (max-width: 1200px) {
.summary-grid {
@@ -221,8 +207,7 @@ const activeInfoTab = ref('waveform')
}
@media (max-width: 992px) {
.summary-grid,
.feature-grid {
.summary-grid {
grid-template-columns: 1fr;
}
}

View File

@@ -9,7 +9,9 @@
placeholder="请选择同一组.cfg和.dat文件"
class="file-input"
/>
<el-button type="primary" :loading="isParsing" @click="openWaveformFilePicker">选择波形</el-button>
<el-button type="primary" :icon="FolderOpened" :loading="isParsing" @click="openWaveformFilePicker">
选择波形
</el-button>
<input
ref="waveformFileInputRef"
type="file"
@@ -59,13 +61,16 @@
</el-radio-group>
</div>
<el-button type="primary" :disabled="!hasWaveformData" @click="emit('download')">下载数据</el-button>
<el-button type="primary" :icon="Download" :disabled="!hasWaveformData" @click="emit('download')">
下载数据
</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Download, FolderOpened } from '@element-plus/icons-vue'
import { ref } from 'vue'
import type { DisplayMode, LabelValueOption, ValueMode, WaveformDetailOption } from './types'

View File

@@ -28,6 +28,7 @@
</el-tabs>
<div v-if="activeCycle" class="vector-tab-content">
<div class="feature-header feature-header--nested">基础指标</div>
<div v-if="phaseMetricColumns.length" class="phase-metric-table">
<div class="phase-metric-row phase-metric-row--header" :style="phaseMetricGridStyle">
<span class="phase-metric-cell phase-metric-cell--label">指标</span>
@@ -83,23 +84,8 @@
/>
</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 v-if="activeHarmonicRows.length" class="harmonic-chart-card">
<div ref="harmonicChartRef" class="harmonic-chart" />
</div>
<div v-else class="empty-inline">当前相未返回谐波结果</div>
</div>
@@ -112,8 +98,9 @@
</template>
<script setup lang="ts">
import { computed, ref, watch, type CSSProperties } from 'vue'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue'
import dayjs from 'dayjs'
import * as echarts from 'echarts'
import type { Waveform } from '@/api/tools/waveform/interface'
import type { SummaryItem } from './types'
@@ -163,26 +150,12 @@ const formatWaveformTime = (value?: string) => {
return parsedValue.isValid() ? parsedValue.format('YYYY-MM-DD HH:mm:ss.SSS') : value
}
const formatWaveValue = (value: unknown, unit?: string) => {
const formatWaveValue = (value: unknown) => {
const formattedValue = formatNumber(value)
if (formattedValue === '--') return '--'
return unit ? `${formattedValue} ${unit}` : formattedValue
return 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 buildMetricLabel = (label: string, unit?: string) => {
return unit ? `${label} (${unit})` : label
@@ -190,13 +163,15 @@ const buildMetricLabel = (label: string, unit?: string) => {
const phaseMetricConfigs: Array<{
label: string
getValue: (phase: Waveform.WavePhaseVectorDTO, unit?: string) => string
unit?: string
useGroupUnit?: boolean
getValue: (phase: Waveform.WavePhaseVectorDTO) => string
}> = [
{ label: '总有效值', getValue: (phase, unit) => formatWaveValue(phase.totalRms, unit) },
{ label: '基波幅值', getValue: (phase, unit) => formatWaveValue(phase.fundamentalAmplitude, unit) },
{ label: '基波有效值', getValue: (phase, unit) => formatWaveValue(phase.fundamentalRms, unit) },
{ label: '基波相角', getValue: phase => formatPhaseAngle(phase.fundamentalPhaseAngle) },
{ label: '谐波畸变率', getValue: phase => formatPercentValue(phase.harmonicDistortionRate) }
{ label: '总有效值', useGroupUnit: true, getValue: phase => formatWaveValue(phase.totalRms) },
{ label: '基波幅值', useGroupUnit: true, getValue: phase => formatWaveValue(phase.fundamentalAmplitude) },
{ label: '基波有效值', useGroupUnit: true, getValue: phase => formatWaveValue(phase.fundamentalRms) },
{ label: '基波相角', unit: '°', getValue: phase => formatWaveValue(phase.fundamentalPhaseAngle) },
{ label: '谐波畸变率', unit: '%', getValue: phase => formatWaveValue(phase.harmonicDistortionRate) }
]
const buildVectorGroupKey = (group: Waveform.WaveVectorGroupDTO, index: number) => {
@@ -299,15 +274,15 @@ const phaseMetricColumns = computed<PhaseMetricColumn[]>(() => {
})
const phaseMetricGridStyle = computed<CSSProperties>(() => ({
gridTemplateColumns: `96px repeat(${Math.max(phaseMetricColumns.value.length, 1)}, minmax(96px, 1fr))`
gridTemplateColumns: `128px repeat(${Math.max(phaseMetricColumns.value.length, 1)}, minmax(96px, 1fr))`
}))
const sequenceMetricGridStyle = computed<CSSProperties>(() => ({
gridTemplateColumns: `132px repeat(${Math.max(sequenceMetricColumns.value.length, 1)}, minmax(0, 1fr))`
gridTemplateColumns: `88px repeat(${Math.max(sequenceMetricColumns.value.length, 1)}, minmax(64px, 1fr))`
}))
const unbalanceMetricGridStyle = computed<CSSProperties>(() => ({
gridTemplateColumns: '156px minmax(0, 1fr)'
gridTemplateColumns: '128px minmax(72px, 1fr)'
}))
const phaseMetricRows = computed<PhaseMetricRow[]>(() => {
@@ -315,11 +290,11 @@ const phaseMetricRows = computed<PhaseMetricRow[]>(() => {
// 相量基础指标按“指标为行、相别为列”转置,减少 A/B/C 三相重复标签。
return phaseMetricConfigs.map(config => ({
label: config.label,
label: buildMetricLabel(config.label, config.useGroupUnit ? activeGroup?.unit : config.unit),
values: activePhaseVectors.value.reduce<Record<string, string>>((result, phase, index) => {
const column = phaseMetricColumns.value[index]
if (column) {
result[column.key] = config.getValue(phase, activeGroup?.unit)
result[column.key] = config.getValue(phase)
}
return result
}, {})
@@ -380,11 +355,158 @@ const unbalanceMetricRows = computed<MetricRow[]>(() => {
]
})
const activeHarmonicRows = computed(() => {
const activeHarmonicRows = computed<Waveform.WaveHarmonicDTO[]>(() => {
if (!activePhaseVector.value) return []
return activePhaseVector.value.harmonicVoltageContentRates || activePhaseVector.value.harmonicCurrentAmplitudes || []
})
const harmonicChartRef = ref<HTMLDivElement>()
let harmonicChart: echarts.ECharts | null = null
let harmonicResizeObserver: ResizeObserver | null = null
const normalizeChartValue = (value: unknown) => {
const numberValue = Number(value)
return Number.isFinite(numberValue) ? Number(numberValue.toFixed(3)) : null
}
const harmonicChartOption = computed<echarts.EChartsOption>(() => {
const unit = activeVectorGroup.value?.unit || ''
const categories = activeHarmonicRows.value.map(item => `${item.harmonicOrder ?? '--'}`)
const hasRate = activeHarmonicRows.value.some(item => Number.isFinite(Number(item.rate)))
const valueAxisName = unit ? `幅值 / 有效值 (${unit})` : '幅值 / 有效值'
const series: echarts.SeriesOption[] = [
{
name: unit ? `幅值 (${unit})` : '幅值',
type: 'bar',
barMaxWidth: 16,
data: activeHarmonicRows.value.map(item => normalizeChartValue(item.amplitude))
},
{
name: unit ? `有效值 (${unit})` : '有效值',
type: 'bar',
barMaxWidth: 16,
data: activeHarmonicRows.value.map(item => normalizeChartValue(item.rms))
}
]
if (hasRate) {
series.push({
name: '占比 (%)',
type: 'bar',
yAxisIndex: 1,
barMaxWidth: 16,
data: activeHarmonicRows.value.map(item => normalizeChartValue(item.rate))
})
}
return {
color: ['#2f80ed', '#07ccca', '#ffbf00'],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
confine: true
},
legend: {
top: 0,
right: 8,
itemWidth: 12,
itemHeight: 8,
textStyle: { color: '#606266', fontSize: 12 }
},
grid: {
top: 36,
right: hasRate ? 52 : 16,
bottom: 42,
left: 12,
containLabel: true
},
xAxis: {
type: 'category',
name: '谐波次数',
nameGap: 22,
data: categories,
axisTick: { show: false },
axisLabel: { color: '#606266', fontSize: 11 },
axisLine: { lineStyle: { color: '#dcdfe6' } }
},
yAxis: [
{
type: 'value',
name: valueAxisName,
nameTextStyle: { color: '#606266', fontSize: 11 },
axisLabel: { color: '#606266', fontSize: 11 },
splitLine: { lineStyle: { color: '#ebeef5', type: 'dashed' } }
},
{
type: 'value',
name: '%',
show: hasRate,
nameTextStyle: { color: '#606266', fontSize: 11 },
axisLabel: { color: '#606266', fontSize: 11 },
splitLine: { show: false }
}
],
dataZoom: [
{
type: 'inside',
start: 0,
end: Math.min(100, activeHarmonicRows.value.length > 18 ? 36 : 100)
},
{
type: 'slider',
height: 12,
bottom: 12,
start: 0,
end: Math.min(100, activeHarmonicRows.value.length > 18 ? 36 : 100)
}
],
series
}
})
const resizeHarmonicChart = () => {
if (!harmonicChart || !harmonicChartRef.value || harmonicChartRef.value.offsetHeight === 0) return
harmonicChart.resize()
}
const renderHarmonicChart = async () => {
await nextTick()
if (!harmonicChartRef.value || !activeHarmonicRows.value.length) {
harmonicChart?.dispose()
harmonicChart = null
harmonicResizeObserver?.disconnect()
harmonicResizeObserver = null
return
}
if (!harmonicChart) {
harmonicChart = echarts.init(harmonicChartRef.value)
}
harmonicChart.setOption(harmonicChartOption.value, true)
if (!harmonicResizeObserver) {
harmonicResizeObserver = new ResizeObserver(() => resizeHarmonicChart())
harmonicResizeObserver.observe(harmonicChartRef.value)
}
resizeHarmonicChart()
}
onMounted(() => {
renderHarmonicChart()
})
onBeforeUnmount(() => {
harmonicResizeObserver?.disconnect()
harmonicChart?.dispose()
})
watch(harmonicChartOption, () => {
renderHarmonicChart()
})
</script>
<style scoped lang="scss">
@@ -397,6 +519,12 @@ const activeHarmonicRows = computed(() => {
gap: 8px;
}
.waveform-vector-info {
flex: none;
min-height: auto;
overflow: visible;
}
.empty-text,
.empty-inline,
.vector-placeholder {
@@ -425,7 +553,7 @@ const activeHarmonicRows = computed(() => {
.summary-item,
.feature-card,
.vector-placeholder,
.harmonic-list {
.harmonic-chart-card {
padding: 10px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 4px;
@@ -488,6 +616,11 @@ const activeHarmonicRows = computed(() => {
border-bottom: 1px solid var(--el-border-color-lighter);
}
.phase-metric-table--fit .phase-metric-cell {
padding-right: 8px;
padding-left: 8px;
}
.phase-metric-table--fit .phase-metric-row {
width: 100%;
min-width: 0;
@@ -509,12 +642,20 @@ const activeHarmonicRows = computed(() => {
font-size: 12px;
line-height: 1.4;
color: var(--el-text-color-regular);
text-align: right;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
border-right: 1px solid var(--el-border-color-lighter);
}
.phase-metric-cell--label {
overflow: visible;
text-align: left;
text-overflow: clip;
white-space: normal;
word-break: keep-all;
}
.phase-metric-row--header .phase-metric-cell {
color: var(--el-text-color-primary);
}
@@ -523,9 +664,6 @@ const activeHarmonicRows = computed(() => {
border-right: 0;
}
.phase-metric-cell--label {
text-align: left;
}
.vector-tabs {
width: 100%;
@@ -544,36 +682,14 @@ const activeHarmonicRows = computed(() => {
font-size: 13px;
}
.harmonic-list {
display: flex;
flex-direction: column;
gap: 0;
max-height: 240px;
overflow: auto;
padding: 0;
.harmonic-chart-card {
height: 280px;
padding: 10px 8px 4px;
}
.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);
.harmonic-chart {
width: 100%;
height: 100%;
}
@media (max-width: 1200px) {
@@ -585,7 +701,7 @@ const activeHarmonicRows = computed(() => {
@media (min-width: 993px) {
.vector-tab-content {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-columns: minmax(0, 1.35fr) minmax(200px, 0.65fr);
align-items: start;
}
@@ -594,29 +710,30 @@ const activeHarmonicRows = computed(() => {
}
.vector-tab-content > :first-child,
.vector-tab-content > :nth-child(n + 6) {
.vector-tab-content > :nth-child(2),
.vector-tab-content > :nth-child(n + 7) {
grid-column: 1 / -1;
}
.vector-tab-content > :nth-child(2) {
grid-column: 1;
grid-row: 2;
}
.vector-tab-content > :nth-child(3) {
grid-column: 1;
grid-row: 3;
}
.vector-tab-content > :nth-child(4) {
grid-column: 2;
grid-row: 2;
grid-column: 1;
grid-row: 4;
}
.vector-tab-content > :nth-child(5) {
grid-column: 2;
grid-row: 3;
}
.vector-tab-content > :nth-child(6) {
grid-column: 2;
grid-row: 4;
}
}
@media (max-width: 992px) {
@@ -626,8 +743,6 @@ const activeHarmonicRows = computed(() => {
grid-template-columns: 1fr;
}
.harmonic-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
</style>

View File

@@ -33,7 +33,6 @@
<WaveformInfoPanel
:has-parsed-waveform="hasParsedWaveform"
:summary-items="summaryItems"
:feature-cards="featureCards"
:vector-parse-result="vectorParseResult"
:last-parse-error-message="lastParseErrorMessage"
:last-vector-parse-error-message="lastVectorParseErrorMessage"
@@ -54,7 +53,6 @@ import WaveformToolbar from './components/WaveformToolbar.vue'
import WaveformTrendPanel from './components/WaveformTrendPanel.vue'
import type {
DisplayMode,
FeatureCardItem,
LabelValueOption,
SingleChannelTrendOption,
SummaryItem,
@@ -218,7 +216,6 @@ const activeWaveDetail = computed(() => {
})
const activeWaveData = computed(() => waveformParseResult.value?.waveData)
const eigenvalueList = computed(() => waveformParseResult.value?.eigenvalues || [])
const normalizeRatio = (value?: number) => {
const ratio = Number(value)
@@ -251,18 +248,6 @@ const formatNumber = (value: unknown, fractionDigits = 3) => {
return `${Number(numberValue.toFixed(fractionDigits))}`
}
const formatPercentage = (value?: number) => {
const numberValue = Number(value)
if (!Number.isFinite(numberValue)) return '--'
return `${Number((numberValue * 100).toFixed(2))}%`
}
const formatDuration = (value?: number) => {
const numberValue = Number(value)
if (!Number.isFinite(numberValue)) return '--'
return `${Number(numberValue.toFixed(3))} ms`
}
const formatWaveformTime = (value?: string) => {
if (!value) return '--'
@@ -546,18 +531,6 @@ const summaryItems = computed<SummaryItem[]>(() => {
]
})
const featureCards = computed<FeatureCardItem[]>(() => {
return eigenvalueList.value.map((item, index) => ({
title: `特征值 ${index + 1}`,
rows: [
{ label: '幅值占比', value: formatPercentage(item.amplitude) },
{ label: '残余电压', value: formatNumber(item.residualVoltage) },
{ label: '额定电压', value: formatNumber(item.ratedVoltage) },
{ label: '持续时间', value: formatDuration(item.durationTime) }
]
}))
})
const getFileBaseName = (fileName: string) => {
return fileName.replace(/\.[^.]+$/, '').toLowerCase()
}