新建监控功能
This commit is contained in:
@@ -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`,其他波形文本解析接口请单独编写。
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user