新建监控功能
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
# generateAndSubmitIndexSelection 标准 API 调试文档
|
||||
|
||||
## 1. 接口信息
|
||||
|
||||
- 接口名称:上传 ICD 后直接生成并提交映射
|
||||
- Controller 方法:`generateAndSubmitIndexSelection`
|
||||
- 请求方式:`POST`
|
||||
- 请求路径:`/api/mms-mapping/generate-and-submit-index-selection`
|
||||
- Content-Type:`multipart/form-data`
|
||||
- 适用场景:前端或调试人员已经能直接给出 `indexSelection`,希望一次请求完成 ICD 解析、候选分析、索引校验和正式映射生成。
|
||||
|
||||
源码参考:
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/MappingController.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/MappingTaskService.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingRequestConverter.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingResponseConverter.java`
|
||||
|
||||
## 2. 处理链路
|
||||
|
||||
该接口内部按以下顺序执行:
|
||||
1. 读取上传的 `icdFile`。
|
||||
2. 解析 ICD,生成 `icdDocument`。
|
||||
3. 加载默认模板并分析候选索引。
|
||||
4. 读取同一次请求中的 `indexSelection`。
|
||||
5. 校验分组、标签、报告、`lnInst` 是否有效。
|
||||
6. 校验通过则生成正式映射 JSON;校验不通过则返回 `NEED_INDEX_SELECTION`。
|
||||
|
||||
因此,这个接口有两类主要调试目标:
|
||||
- 调试一次成功生成映射。
|
||||
- 调试为什么需要重新选择索引。
|
||||
|
||||
## 3. 请求定义
|
||||
|
||||
### 3.1 表单字段
|
||||
|
||||
| 字段 | 类型 | 是否必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `icdFile` | file | 是 | ICD 文件 |
|
||||
| `request` | json part | 是 | JSON 请求体,必须作为独立 part 传入 |
|
||||
|
||||
### 3.2 request JSON 字段
|
||||
|
||||
`request` 结构复用 `GenerateMappingFromIcdRequest`。
|
||||
|
||||
| 字段 | 类型 | 是否必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `version` | string | 否 | 输出版本号,空值时按服务端默认逻辑处理 |
|
||||
| `author` | string | 否 | 作者,空值时回退到模块默认作者 |
|
||||
| `saveToDisk` | boolean | 否 | 是否落盘保存生成结果 |
|
||||
| `prettyJson` | boolean | 否 | 是否返回格式化 JSON |
|
||||
| `outputDir` | string | 否 | 落盘目录,仅 `saveToDisk=true` 时有意义 |
|
||||
| `indexSelection` | array | 否 | 直接提交的索引绑定关系;为空时会返回 `NEED_INDEX_SELECTION` |
|
||||
|
||||
### 3.3 indexSelection 字段
|
||||
|
||||
| 字段 | 类型 | 是否必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `groupKey` | string | 建议必填 | 候选分组唯一键,建议直接使用候选接口返回值 |
|
||||
| `groupDesc` | string | 否 | 分组中文描述,用于兼容匹配 |
|
||||
| `bindings` | array | 是 | 当前分组下的绑定关系列表 |
|
||||
|
||||
### 3.4 bindings 子项字段
|
||||
|
||||
| 字段 | 类型 | 是否必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `reportName` | string | 是 | 报告名称 |
|
||||
| `dataSetName` | string | 是 | 数据集名称 |
|
||||
| `label` | string | 是 | 模板业务标签 |
|
||||
| `lnInst` | string | 是 | 最终绑定的 `lnInst` 数字 |
|
||||
|
||||
## 4. Postman 调试方式
|
||||
|
||||
### 4.1 请求设置
|
||||
|
||||
在 Postman 中选择:
|
||||
- Method:`POST`
|
||||
- Body:`form-data`
|
||||
|
||||
新增两个表单项:
|
||||
1. `icdFile`
|
||||
- 类型:`File`
|
||||
- 值:选择本地 ICD 文件
|
||||
2. `request`
|
||||
- 类型:`Text`
|
||||
- 值:填写 JSON 字符串
|
||||
|
||||
建议把 `request` 这个 part 的 Content-Type 显式设为 `application/json`。
|
||||
|
||||
### 4.2 request 示例
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"author": "system",
|
||||
"saveToDisk": false,
|
||||
"prettyJson": true,
|
||||
"outputDir": "",
|
||||
"indexSelection": [
|
||||
{
|
||||
"groupKey": "实时数据__DSSTHARM",
|
||||
"groupDesc": "实时数据",
|
||||
"bindings": [
|
||||
{
|
||||
"reportName": "brcbStHarm",
|
||||
"dataSetName": "dsStHarm",
|
||||
"label": "A相",
|
||||
"lnInst": "1"
|
||||
},
|
||||
{
|
||||
"reportName": "brcbStHarm",
|
||||
"dataSetName": "dsStHarm",
|
||||
"label": "B相",
|
||||
"lnInst": "2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 5. cURL 调试示例
|
||||
|
||||
### 5.1 成功生成示例
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/api/mms-mapping/generate-and-submit-index-selection" \
|
||||
-H "Accept: application/json" \
|
||||
-F "icdFile=@D:/data/demo.icd" \
|
||||
-F 'request={
|
||||
"version":"1.0",
|
||||
"author":"system",
|
||||
"saveToDisk":false,
|
||||
"prettyJson":true,
|
||||
"outputDir":"",
|
||||
"indexSelection":[
|
||||
{
|
||||
"groupKey":"实时数据__DSSTHARM",
|
||||
"groupDesc":"实时数据",
|
||||
"bindings":[
|
||||
{
|
||||
"reportName":"brcbStHarm",
|
||||
"dataSetName":"dsStHarm",
|
||||
"label":"A相",
|
||||
"lnInst":"1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};type=application/json'
|
||||
```
|
||||
|
||||
### 5.2 触发 NEED_INDEX_SELECTION 示例
|
||||
|
||||
将 `indexSelection` 置空,或故意传入非法 `label` / `lnInst`:
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/api/mms-mapping/generate-and-submit-index-selection" \
|
||||
-H "Accept: application/json" \
|
||||
-F "icdFile=@D:/data/demo.icd" \
|
||||
-F 'request={
|
||||
"version":"1.0",
|
||||
"author":"system",
|
||||
"saveToDisk":false,
|
||||
"prettyJson":true,
|
||||
"outputDir":"",
|
||||
"indexSelection":[]
|
||||
};type=application/json'
|
||||
```
|
||||
|
||||
## 6. 最小响应定义
|
||||
|
||||
该接口已经按场景裁剪返回值。
|
||||
|
||||
### 6.1 SUCCESS
|
||||
|
||||
只返回:
|
||||
- `status`
|
||||
- `message`
|
||||
- `mappingJson`
|
||||
- `savedPath`,仅 `saveToDisk=true` 且保存成功时返回
|
||||
- `problems`,仅存在问题时返回
|
||||
|
||||
### 6.2 NEED_INDEX_SELECTION
|
||||
|
||||
只返回:
|
||||
- `status`
|
||||
- `message`
|
||||
- `icdDocument`
|
||||
- `indexCandidates`
|
||||
- `problems`
|
||||
|
||||
### 6.3 FAILED
|
||||
|
||||
只返回:
|
||||
- `status`
|
||||
- `message`
|
||||
- `problems`,仅存在问题时返回
|
||||
|
||||
## 7. 响应示例
|
||||
|
||||
### 7.1 成功响应
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"message": "映射生成成功",
|
||||
"mappingJson": "{\"Version\":\"1.0\",\"Author\":\"system\"}"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 成功并落盘响应
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"message": "映射生成成功",
|
||||
"mappingJson": "{\"Version\":\"1.0\",\"Author\":\"system\"}",
|
||||
"savedPath": "D:/output/IED1-mapping-pretty.json"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 需要重新选择索引响应
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "NEED_INDEX_SELECTION",
|
||||
"message": "索引配置不合法,请根据候选信息完成标签与数字索引的绑定后重新提交",
|
||||
"icdDocument": {
|
||||
"fileName": "demo.icd",
|
||||
"iedName": "IED1",
|
||||
"ldInst": "LD0"
|
||||
},
|
||||
"indexCandidates": [
|
||||
{
|
||||
"groupKey": "实时数据__DSSTHARM",
|
||||
"groupDesc": "实时数据",
|
||||
"reportCount": 1,
|
||||
"templateLabels": ["A相", "B相", "C相"],
|
||||
"reports": [
|
||||
{
|
||||
"reportName": "brcbStHarm",
|
||||
"dataSetName": "dsStHarm",
|
||||
"reportDesc": "实时数据",
|
||||
"availableLnInstValues": ["1", "2", "3"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"problems": [
|
||||
"分组【实时数据】中 label【A相】不在模板候选中"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 失败响应
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "FAILED",
|
||||
"message": "ICD 解析失败:ICD 文件内容不能为空",
|
||||
"problems": [
|
||||
"ICD 文件内容不能为空"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 常见调试问题
|
||||
|
||||
### 8.1 `request` 没有按 JSON 传入
|
||||
|
||||
现象:
|
||||
- Spring 无法正确绑定 `@RequestPart("request")`
|
||||
- 返回 400 或参数解析异常
|
||||
|
||||
处理:
|
||||
- 确保 `request` 是单独的 multipart part
|
||||
- 建议显式设置 `type=application/json`
|
||||
|
||||
### 8.2 `indexSelection` 为空
|
||||
|
||||
现象:
|
||||
- 返回 `NEED_INDEX_SELECTION`
|
||||
- `message` 提示索引配置缺失
|
||||
|
||||
处理:
|
||||
- 先调用 `generateFromIcdCandidates`
|
||||
- 使用其返回的 `groupKey`、`templateLabels`、`availableLnInstValues` 重新组装绑定
|
||||
|
||||
### 8.3 `groupKey` 不匹配
|
||||
|
||||
现象:
|
||||
- `problems` 中出现“未找到分组配置”
|
||||
|
||||
处理:
|
||||
- 不要自己拼 `groupKey`
|
||||
- 直接使用候选接口返回值原样回传
|
||||
|
||||
### 8.4 `label` 不合法
|
||||
|
||||
现象:
|
||||
- `problems` 中出现“label 不在模板候选中”
|
||||
|
||||
处理:
|
||||
- `label` 必须取自当前分组的 `templateLabels`
|
||||
|
||||
### 8.5 `lnInst` 不合法
|
||||
|
||||
现象:
|
||||
- `problems` 中出现“lnInst 不在可选数字中”
|
||||
|
||||
处理:
|
||||
- `lnInst` 必须取自当前报告的 `availableLnInstValues`
|
||||
|
||||
### 8.6 `saveToDisk=true` 但没有 `savedPath`
|
||||
|
||||
可能原因:
|
||||
- 保存过程抛异常,接口直接返回 `FAILED`
|
||||
- 输出目录无权限或路径非法
|
||||
|
||||
处理:
|
||||
- 优先先用 `saveToDisk=false` 验证生成链路
|
||||
- 再单独验证落盘目录权限
|
||||
|
||||
## 9. 联调建议
|
||||
|
||||
- 首次联调建议先走两步:
|
||||
1. 先调用 `generateFromIcdCandidates`
|
||||
2. 根据候选结果确认 `groupKey`、`label`、`lnInst` 后,再调当前接口
|
||||
- 如果只是验证“提交链路”是否通,不建议一开始就打开 `saveToDisk`
|
||||
- 如果需要排查返回值最小化是否生效,重点看:
|
||||
- `SUCCESS` 不应再返回 `icdDocument`、`indexCandidates`
|
||||
- `NEED_INDEX_SELECTION` 不应返回 `mappingJson`
|
||||
- `FAILED` 不应返回候选或映射结果字段
|
||||
371
frontend/src/views/tools/mmsmapping/API-getIcdMmsJson.md
Normal file
371
frontend/src/views/tools/mmsmapping/API-getIcdMmsJson.md
Normal file
@@ -0,0 +1,371 @@
|
||||
# getIcdMmsJson 标准 API 调试文档
|
||||
|
||||
## 1. 文档范围
|
||||
|
||||
本文档用于说明 `mms-mapping` 模块统一调试接口 `getIcdMmsJson` 的标准调用方式、请求结构、响应规则和联调注意事项。
|
||||
|
||||
本文档内容以当前源码为准,主要对照以下实现:
|
||||
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/controller/MappingController.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingRequestConverter.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingResponseConverter.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/service/impl/MappingTaskServiceImpl.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/component/MappingGenerationService.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/param/GenerateMappingFromIcdRequest.java`
|
||||
- `tools/mms-mapping/src/main/java/com/njcn/gather/icd/mapping/pojo/vo/MappingTaskResponse.java`
|
||||
|
||||
说明:
|
||||
|
||||
- 本文档仅描述接口契约和调试方式,不改动业务代码。
|
||||
- 本次未执行 `mvn` 编译、打包或真实接口联调。
|
||||
- 如文档与运行结果冲突,以源码和实际部署配置为准。
|
||||
|
||||
## 2. 接口基本信息
|
||||
|
||||
| 项 | 说明 |
|
||||
| --- | --- |
|
||||
| 接口名称 | `getIcdMmsJson` |
|
||||
| 请求方法 | `POST` |
|
||||
| 请求路径 | `/api/mms-mapping/get-icd-mms-json` |
|
||||
| Content-Type | `multipart/form-data` |
|
||||
| 控制器入口 | `MappingController#getIcdMmsJson` |
|
||||
| 请求组成 | `icdFile` 文件 Part + `request` JSON Part |
|
||||
| 正常业务响应体 | `MappingTaskResponse` |
|
||||
|
||||
## 3. 接口职责
|
||||
|
||||
该接口是 `mms-mapping` 模块的统一调试入口,串联以下两个阶段:
|
||||
|
||||
1. 上传 ICD 文件并完成解析,生成 `icdDocument` 和 `indexCandidates`
|
||||
2. 根据 `request.indexSelection` 判断是否继续生成正式 `mappingJson`
|
||||
|
||||
接口行为分为三种典型结果:
|
||||
|
||||
1. `request.indexSelection` 未传或为空
|
||||
返回 `NEED_INDEX_SELECTION`,用于引导前端或调试人员先确认标签与 `lnInst` 的绑定关系。
|
||||
2. `request.indexSelection` 已传但校验不通过
|
||||
返回 `NEED_INDEX_SELECTION`,同时通过 `problems` 给出不合法原因,要求重新选择。
|
||||
3. `request.indexSelection` 校验通过
|
||||
返回 `SUCCESS`,输出正式 `mappingJson`,必要时同时落盘并返回 `savedPath`。
|
||||
|
||||
补充说明:
|
||||
|
||||
- 该接口每次都会重新解析上传的 ICD 文件,因此第二次调试仍然必须重新上传 ICD 文件。
|
||||
- 该接口正常进入业务编排后,返回体类型为 `MappingTaskResponse`。
|
||||
- 如果异常发生在控制器参数绑定或请求转换阶段,例如文件为空、Part 缺失、JSON Part 解析失败,则由全局异常处理器统一包装为 `HttpResult<String>`,而不是 `MappingTaskResponse`。
|
||||
|
||||
## 4. 请求规范
|
||||
|
||||
### 4.1 multipart/form-data Part 说明
|
||||
|
||||
| Part 名称 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `icdFile` | File | 是 | ICD 文件,不能为空 |
|
||||
| `request` | JSON Part | 是 | 生成参数,必须按 `application/json` 发送 |
|
||||
|
||||
说明:
|
||||
|
||||
- `request` Part 不能省略。即使第一次只想拿候选结果,也必须传一个最小 JSON。
|
||||
- `request.indexSelection` 可以省略或传空数组,此时接口只返回候选结果,不生成正式映射。
|
||||
|
||||
### 4.2 request JSON 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2026-04-22",
|
||||
"author": "debug-user",
|
||||
"saveToDisk": false,
|
||||
"prettyJson": true,
|
||||
"outputDir": "D:/temp/mms-output",
|
||||
"indexSelection": [
|
||||
{
|
||||
"groupKey": "harm",
|
||||
"groupDesc": "谐波数据",
|
||||
"bindings": [
|
||||
{
|
||||
"reportName": "brcbStHarm",
|
||||
"dataSetName": "dsStHarm",
|
||||
"label": "A相",
|
||||
"lnInst": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 request 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `version` | String | 否 | 输出版本号。未传或空白时,后端按当天日期补齐,格式为 `yyyy-MM-dd` |
|
||||
| `author` | String | 否 | 作者。未传或空白时,回退到配置项 `icd.mapping.default-author`,默认值为 `system` |
|
||||
| `saveToDisk` | boolean | 否 | 是否将生成结果写入磁盘 |
|
||||
| `prettyJson` | boolean | 否 | 是否输出格式化 JSON。`true` 为美化 JSON,`false` 为紧凑 JSON |
|
||||
| `outputDir` | String | 否 | 输出目录。未传或空白时,先回退到配置项 `icd.mapping.default-output-dir`;如果配置也为空,最终落到当前工作目录 |
|
||||
| `indexSelection` | Array | 否 | 标签与 `lnInst` 的最终绑定关系。未传或为空时,只返回候选结果 |
|
||||
|
||||
### 4.4 indexSelection 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `groupKey` | String | 是 | 分组唯一键,必须使用第一次响应里返回的原值 |
|
||||
| `groupDesc` | String | 否 | 分组中文描述,便于调试查看 |
|
||||
| `bindings` | Array | 是 | 当前业务分组下最终确认的绑定关系列表 |
|
||||
|
||||
### 4.5 bindings 字段说明
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| `reportName` | String | 是 | 绑定发生在哪个报告上,例如 `brcbStHarm` |
|
||||
| `dataSetName` | String | 是 | 绑定发生在哪个数据集上,例如 `dsStHarm` |
|
||||
| `label` | String | 是 | 业务标签,例如 `A相`、`最大值`、`实时数据` |
|
||||
| `lnInst` | String | 是 | 标签最终绑定到的逻辑节点实例值,例如 `1`、`2`、`3` |
|
||||
|
||||
## 5. 标准调试流程
|
||||
|
||||
### 5.1 第一次调试:只获取候选结果
|
||||
|
||||
用途:
|
||||
|
||||
- 上传 ICD 文件
|
||||
- 获取 `icdDocument`
|
||||
- 获取 `indexCandidates`
|
||||
- 确认每个业务分组下可选的 `reportName`、`dataSetName` 和 `availableLnInstValues`
|
||||
|
||||
调用要求:
|
||||
|
||||
- `request` Part 仍然必须传
|
||||
- `request.indexSelection` 可以不传,或传空数组
|
||||
|
||||
预期结果:
|
||||
|
||||
- `status = NEED_INDEX_SELECTION`
|
||||
- 响应中返回 `icdDocument`
|
||||
- 响应中返回 `indexCandidates`
|
||||
|
||||
### 5.2 第二次调试:带索引绑定生成正式结果
|
||||
|
||||
用途:
|
||||
|
||||
- 根据第一次返回的 `indexCandidates` 组装 `request.indexSelection`
|
||||
- 再次上传同一个 ICD 文件
|
||||
- 生成正式 `mappingJson`
|
||||
|
||||
调用要求:
|
||||
|
||||
- 必须继续上传 `icdFile`
|
||||
- `groupKey` 必须沿用第一次返回值
|
||||
- `reportName`、`dataSetName`、`lnInst` 必须与第一次返回的候选结果匹配
|
||||
|
||||
预期结果:
|
||||
|
||||
- `status = SUCCESS`
|
||||
- 响应中返回 `mappingJson`
|
||||
- 当 `saveToDisk = true` 时,响应中额外返回 `savedPath`
|
||||
|
||||
### 5.3 第二次调试但绑定不合法
|
||||
|
||||
适用场景:
|
||||
|
||||
- `groupKey` 与候选结果不匹配
|
||||
- `reportName` 或 `dataSetName` 不在候选集中
|
||||
- `lnInst` 不在 `availableLnInstValues` 内
|
||||
- 绑定关系缺失、不完整或结构错误
|
||||
|
||||
预期结果:
|
||||
|
||||
- `status = NEED_INDEX_SELECTION`
|
||||
- 响应中仍然返回 `icdDocument` 和 `indexCandidates`
|
||||
- `problems` 返回具体问题列表,要求重新确认绑定关系
|
||||
|
||||
## 6. 响应规范
|
||||
|
||||
### 6.1 正常业务响应体
|
||||
|
||||
接口正常进入业务编排后,统一返回 `MappingTaskResponse`。该对象使用了 `@JsonInclude(JsonInclude.Include.NON_EMPTY)`,空字段和空集合不会参与序列化。
|
||||
|
||||
基础字段说明:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `status` | Enum | 本次处理状态,可能为 `SUCCESS`、`NEED_INDEX_SELECTION`、`FAILED` |
|
||||
| `message` | String | 状态说明或错误提示 |
|
||||
| `icdDocument` | Object | 需要重新选择索引时返回的 ICD 解析结果 |
|
||||
| `mappingJson` | String | 正式生成成功后的映射 JSON 文本 |
|
||||
| `savedPath` | String | 结果已落盘时返回的绝对路径 |
|
||||
| `indexCandidates` | Array | 待绑定状态下返回的索引候选分组 |
|
||||
| `problems` | Array | 模板校验、候选分析或绑定校验问题 |
|
||||
|
||||
字段出现规则:
|
||||
|
||||
| 状态 | 必有字段 | 可能出现字段 |
|
||||
| --- | --- | --- |
|
||||
| `SUCCESS` | `status`、`message`、`mappingJson` | `savedPath`、`problems` |
|
||||
| `NEED_INDEX_SELECTION` | `status`、`message`、`icdDocument`、`indexCandidates` | `problems` |
|
||||
| `FAILED` | `status`、`message` | `problems` |
|
||||
|
||||
### 6.2 NEED_INDEX_SELECTION 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "NEED_INDEX_SELECTION",
|
||||
"message": "索引配置缺失,请根据候选信息完成标签与数字索引的绑定后重新提交",
|
||||
"icdDocument": {
|
||||
"fileName": "demo.icd",
|
||||
"iedName": "IED1",
|
||||
"ldInst": "LD0",
|
||||
"ldPrefix": "LD",
|
||||
"logicalNodes": [
|
||||
{
|
||||
"lnInst": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"indexCandidates": [
|
||||
{
|
||||
"groupKey": "harm",
|
||||
"groupDesc": "谐波数据",
|
||||
"reportCount": 1,
|
||||
"templateLabels": [
|
||||
"A相",
|
||||
"B相",
|
||||
"C相"
|
||||
],
|
||||
"reports": [
|
||||
{
|
||||
"reportName": "brcbStHarm",
|
||||
"dataSetName": "dsStHarm",
|
||||
"reportDesc": "谐波报告",
|
||||
"availableLnInstValues": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `icdDocument` 实际字段可能比示例更多。
|
||||
- 如果本次是“索引配置不合法”而不是“索引配置缺失”,通常还会返回 `problems`。
|
||||
|
||||
### 6.3 SUCCESS 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"message": "映射生成成功",
|
||||
"mappingJson": "{\n \"version\": \"2026-04-22\",\n \"author\": \"debug-user\",\n \"ied\": \"IED1\",\n \"ld\": \"LD\",\n \"instList\": []\n}"
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `mappingJson` 是字符串字段,字段值本身是一段 JSON 文本。
|
||||
- 当 `saveToDisk = true` 时,响应中还会额外返回 `savedPath`。
|
||||
|
||||
### 6.4 FAILED 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "FAILED",
|
||||
"message": "映射生成失败:加载 DefaultCfg.txt 失败:默认模板文件不存在:template/DefaultCfg.txt",
|
||||
"problems": [
|
||||
"加载 DefaultCfg.txt 失败:默认模板文件不存在:template/DefaultCfg.txt"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `FAILED` 主要对应服务编排阶段捕获到的运行异常,例如 ICD 解析、模板加载、映射生成、序列化或落盘失败。
|
||||
- 并非所有错误都会进入 `FAILED`。如果异常发生在控制器参数绑定或请求转换阶段,会走全局异常处理器,而不是这里的业务响应结构。
|
||||
|
||||
## 7. 全局异常响应说明
|
||||
|
||||
以下场景通常不会返回 `MappingTaskResponse`,而是由 `GlobalBusinessExceptionHandler` 统一包装:
|
||||
|
||||
- `icdFile` 缺失或为空
|
||||
- `request` Part 缺失
|
||||
- `request` Part 的 `Content-Type` 不是 `application/json`
|
||||
- `multipart/form-data` 结构不合法
|
||||
- JSON 反序列化失败或框架参数绑定失败
|
||||
|
||||
这类异常最终会包装为统一的 `HttpResult<String>` 响应,具体字段结构以全局公共响应定义为准,本文不展开其完整协议,只强调:
|
||||
|
||||
- 不能把这类错误等同理解为 `MappingTaskResponse.status = FAILED`
|
||||
- 联调时应先区分“业务响应体”与“全局异常包装”
|
||||
|
||||
## 8. 调试示例
|
||||
|
||||
### 8.1 curl 示例:第一次调用,只获取候选结果
|
||||
|
||||
```powershell
|
||||
curl.exe -X POST "http://localhost:8080/api/mms-mapping/get-icd-mms-json" `
|
||||
-H "Accept: application/json" `
|
||||
-F 'icdFile=@D:/data/demo.icd' `
|
||||
-F 'request={"prettyJson":true,"saveToDisk":false};type=application/json'
|
||||
```
|
||||
|
||||
### 8.2 curl 示例:第二次调用,带索引绑定直接生成 MMS JSON
|
||||
|
||||
```powershell
|
||||
curl.exe -X POST "http://localhost:8080/api/mms-mapping/get-icd-mms-json" `
|
||||
-H "Accept: application/json" `
|
||||
-F 'icdFile=@D:/data/demo.icd' `
|
||||
-F 'request={"version":"2026-04-22","author":"debug-user","prettyJson":true,"saveToDisk":false,"indexSelection":[{"groupKey":"harm","groupDesc":"谐波数据","bindings":[{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"A相","lnInst":"1"},{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"B相","lnInst":"2"},{"reportName":"brcbStHarm","dataSetName":"dsStHarm","label":"C相","lnInst":"3"}]}]};type=application/json'
|
||||
```
|
||||
|
||||
## 9. Postman 调试要点
|
||||
|
||||
1. `Body` 选择 `form-data`
|
||||
2. `icdFile` 类型选择 `File`
|
||||
3. `request` 保持文本输入,但该 Part 的 `Content-Type` 必须显式设置为 `application/json`
|
||||
4. 第一次调试不要省略 `request` Part,只是不传 `indexSelection`
|
||||
5. 第二次调试时必须继续上传 ICD 文件,并严格按第一次返回的候选结果组装绑定关系
|
||||
|
||||
## 10. 常见问题
|
||||
|
||||
### 10.1 为什么第一次调试也必须传 `request`
|
||||
|
||||
因为控制器方法签名使用的是 `@RequestPart("request") GenerateMappingFromIcdRequest request`,该 Part 本身就是必填参数。第一次调试可以只传最小 JSON,但不能完全省略。
|
||||
|
||||
### 10.2 为什么没有传 `indexSelection`,却没有返回 `FAILED`
|
||||
|
||||
这是接口设计的正常行为。`indexSelection` 缺失或为空时,业务语义不是“接口执行失败”,而是“还需要前端继续确认索引绑定”,因此返回的是 `NEED_INDEX_SELECTION`。
|
||||
|
||||
### 10.3 `saveToDisk=true` 但没有传 `outputDir`,结果会保存到哪里
|
||||
|
||||
处理顺序如下:
|
||||
|
||||
1. 先读取请求中的 `outputDir`
|
||||
2. 如果请求空白,则回退到配置项 `icd.mapping.default-output-dir`
|
||||
3. 如果配置项也为空,则最终落到当前工作目录
|
||||
|
||||
### 10.4 `version` 不传时会变成什么
|
||||
|
||||
后端在正式生成映射文档时,会把空白 `version` 自动补成当天日期,格式为 `yyyy-MM-dd`。
|
||||
|
||||
### 10.5 `mappingJson` 为什么是字符串,不是嵌套对象
|
||||
|
||||
因为当前响应结构中 `mappingJson` 定义为 `String`,接口返回的是一段已经序列化好的 JSON 文本,而不是再次展开后的对象结构。
|
||||
|
||||
### 10.6 什么情况下会返回 `problems`
|
||||
|
||||
`problems` 主要用于承载以下问题:
|
||||
|
||||
- 默认模板校验问题
|
||||
- 索引候选分析问题
|
||||
- `indexSelection` 绑定校验问题
|
||||
- 服务编排阶段捕获到的异常原因
|
||||
|
||||
## 11. 当前边界
|
||||
|
||||
- 当前文档仅覆盖 `getIcdMmsJson` 接口,不覆盖 `get-icd` 与 `get-mms-json` 的独立接口文档
|
||||
- 当前文档重点描述业务返回体与调试方式,不展开全局 `HttpResult` 的完整协议
|
||||
- 示例中的 `icdDocument`、`indexCandidates` 和 `mappingJson` 为结构化示意,实际字段数量与内容以运行结果为准
|
||||
756
frontend/src/views/tools/mmsmapping/DefaultCfg.txt
Normal file
756
frontend/src/views/tools/mmsmapping/DefaultCfg.txt
Normal file
@@ -0,0 +1,756 @@
|
||||
{
|
||||
"ReportList": [
|
||||
{
|
||||
"desc": "统计数据",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "DataStatFileMap",
|
||||
"DataSetList": [
|
||||
"dsStatisticData",
|
||||
"dsStHarm",
|
||||
"dsStIHarm",
|
||||
"dsStMMXU",
|
||||
"dsStMSQI"
|
||||
],
|
||||
"LnInstList": [
|
||||
"最大值",
|
||||
"最小值",
|
||||
"平均值",
|
||||
"95值",
|
||||
"方均根值",
|
||||
"间谐波最大值",
|
||||
"间谐波最小值",
|
||||
"间谐波平均值",
|
||||
"间谐波95值",
|
||||
"间谐波方均根值"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "波动闪变",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "FlickerFileMap",
|
||||
"DataSetList": [
|
||||
"dsFlickerData",
|
||||
"dsPST"
|
||||
],
|
||||
"LnInstList": [
|
||||
"波动闪变值"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "实时数据",
|
||||
"inst": "01",
|
||||
"TrgOps": "40",
|
||||
"Select": "DataRealFileMap",
|
||||
"DataSetList": [
|
||||
"dsRealTimeData",
|
||||
"dsRtHarm",
|
||||
"dsRtIHarm",
|
||||
"dsRtMMXU",
|
||||
"dsRtMSQI",
|
||||
"dsRtFre"
|
||||
],
|
||||
"LnInstList": [
|
||||
"实时数据",
|
||||
"间谐波实时数据"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "暂态事件",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "QVVR",
|
||||
"DataSetList": [
|
||||
"dsEveQVVR"
|
||||
],
|
||||
"LnInstList": [
|
||||
"电压变动A",
|
||||
"电压变动B",
|
||||
"电压变动C"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "录波状态",
|
||||
"inst": "01",
|
||||
"TrgOps": "96",
|
||||
"Select": "RDRE",
|
||||
"DataSetList": [
|
||||
"dsEveRDRE"
|
||||
],
|
||||
"LnInstList": [
|
||||
"录波文件"
|
||||
]
|
||||
}
|
||||
],
|
||||
"LnClassList": [
|
||||
{
|
||||
"desc": "基本数据",
|
||||
"nameList": [
|
||||
"MMXU"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "序分量值",
|
||||
"nameList": [
|
||||
"MSQI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "谐波/间谐波数据",
|
||||
"nameList": [
|
||||
"MHAI"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "波动闪变",
|
||||
"nameList": [
|
||||
"MFLK"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压变动",
|
||||
"nameList": [
|
||||
"QVVR"
|
||||
]
|
||||
}
|
||||
],
|
||||
"PhaseList": [
|
||||
{
|
||||
"desc": "无相别",
|
||||
"nameList": [
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "正序",
|
||||
"nameList": [
|
||||
"c1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "负序",
|
||||
"nameList": [
|
||||
"c2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "零序",
|
||||
"nameList": [
|
||||
"c3"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "A相",
|
||||
"nameList": [
|
||||
"phsA",
|
||||
"phsAHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "B相",
|
||||
"nameList": [
|
||||
"phsB",
|
||||
"phsBHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "C相",
|
||||
"nameList": [
|
||||
"phsC",
|
||||
"phsCHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "AB线",
|
||||
"nameList": [
|
||||
"phsAB",
|
||||
"phsABHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "BC线",
|
||||
"nameList": [
|
||||
"phsBC",
|
||||
"phsBCHar"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "CA线",
|
||||
"nameList": [
|
||||
"phsCA",
|
||||
"phsCAHar"
|
||||
]
|
||||
}
|
||||
],
|
||||
"MultiplierList": [
|
||||
{
|
||||
"multiplier": 1,
|
||||
"nameList": [
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"multiplier": 1000,
|
||||
"nameList": [
|
||||
"k"
|
||||
]
|
||||
}
|
||||
],
|
||||
"UnitList": [
|
||||
{
|
||||
"desc": "other",
|
||||
"nameList": [
|
||||
"null"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "v",
|
||||
"nameList": [
|
||||
"V"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "a",
|
||||
"nameList": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "w",
|
||||
"nameList": [
|
||||
"W",
|
||||
"VAr",
|
||||
"VA"
|
||||
]
|
||||
}
|
||||
],
|
||||
"TypeList": [
|
||||
{
|
||||
"desc": "值",
|
||||
"nameList": [
|
||||
"mag"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "角度",
|
||||
"nameList": [
|
||||
"ang"
|
||||
]
|
||||
}
|
||||
],
|
||||
"DataObjectsList": [
|
||||
{
|
||||
"desc": "非间谐波数据",
|
||||
"LnInstList": [
|
||||
"最大值",
|
||||
"最小值",
|
||||
"平均值",
|
||||
"95值",
|
||||
"实时数据"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "频率",
|
||||
"nameList": [
|
||||
"Hz"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压总有效值",
|
||||
"nameList": [
|
||||
"PPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压总有效值",
|
||||
"nameList": [
|
||||
"PhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总有效值",
|
||||
"nameList": [
|
||||
"A"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "有功功率",
|
||||
"nameList": [
|
||||
"W"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "无功功率",
|
||||
"nameList": [
|
||||
"VAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "视在功率",
|
||||
"nameList": [
|
||||
"VA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "功率因数",
|
||||
"nameList": [
|
||||
"PF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "位移功率因数",
|
||||
"nameList": [
|
||||
"DF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总有功功率",
|
||||
"nameList": [
|
||||
"TotW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总无功功率",
|
||||
"nameList": [
|
||||
"TotVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总视在功率",
|
||||
"nameList": [
|
||||
"TotVA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相功率因数",
|
||||
"nameList": [
|
||||
"TotPF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相位移功率因数",
|
||||
"nameList": [
|
||||
"TotDF"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "频率偏差",
|
||||
"nameList": [
|
||||
"HzDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压偏差",
|
||||
"nameList": [
|
||||
"PhVDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压偏差",
|
||||
"nameList": [
|
||||
"PPVDev"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "正序负序和零序电压",
|
||||
"nameList": [
|
||||
"SeqV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "正序负序和零序电流",
|
||||
"nameList": [
|
||||
"SeqA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压负序不平衡度",
|
||||
"nameList": [
|
||||
"ImbNgV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流负序不平衡度",
|
||||
"nameList": [
|
||||
"ImbNgA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压零序不平衡度",
|
||||
"nameList": [
|
||||
"ImbZroV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流零序不平衡度",
|
||||
"nameList": [
|
||||
"ImbZroA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压谐波总畸变率",
|
||||
"nameList": [
|
||||
"ThdPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压总偶次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdEvnPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压总奇次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdOddPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压谐波总畸变率",
|
||||
"nameList": [
|
||||
"ThdPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压总偶次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdEvnPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压总奇次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdOddPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HRPhV",
|
||||
"HPhVMag"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HRPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总偶次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdEvnA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流总奇次谐波畸变率",
|
||||
"nameList": [
|
||||
"ThdOddA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "谐波电流有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HA",
|
||||
"HAMag"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "谐波电压有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "2~50次谐波有功功率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "2~50次谐波无功功率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "2~50次谐波视在功率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 49,
|
||||
"nameList": [
|
||||
"HVA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总谐波视在功率",
|
||||
"nameList": [
|
||||
"TotHVA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总谐波无功功率",
|
||||
"nameList": [
|
||||
"TotHVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "三相总谐波有功功率",
|
||||
"nameList": [
|
||||
"TotHW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压基波有效值",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
"HFundPhV",
|
||||
"FundPhV"
|
||||
],
|
||||
"queueList":[
|
||||
"HPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压基波有效值",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
"HFundPPV"
|
||||
],
|
||||
"queueList":[
|
||||
"HPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电流基波有效值",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "基波有功功率",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HW"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "基波无功功率",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HVAr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "基波视在功率",
|
||||
"baseflag": 2,
|
||||
"queuecount": 49,
|
||||
"nameList": [
|
||||
|
||||
],
|
||||
"queueList":[
|
||||
"HVA"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "间谐波数据",
|
||||
"LnInstList": [
|
||||
"间谐波最大值",
|
||||
"间谐波最小值",
|
||||
"间谐波平均值",
|
||||
"间谐波95值",
|
||||
"间谐波实时数据"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "相电压间谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HPhV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压间谐波含有率序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HRPPV"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "间谐波电流有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "间谐波电压有效值序列",
|
||||
"baseflag": 1,
|
||||
"basecount": 50,
|
||||
"nameList": [
|
||||
"HRPhV"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压变动",
|
||||
"LnInstList": [
|
||||
"电压变动A",
|
||||
"电压变动B",
|
||||
"电压变动C"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "电压扰动事件启动",
|
||||
"nameList": [
|
||||
"VarStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂降事件启动",
|
||||
"nameList": [
|
||||
"DipStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂升事件启动",
|
||||
"nameList": [
|
||||
"SwlStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压中断事件启动",
|
||||
"nameList": [
|
||||
"IntrStr"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压扰动事件特征幅值",
|
||||
"nameList": [
|
||||
"VVa"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压扰动事件持续时间",
|
||||
"nameList": [
|
||||
"VVaTm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂降启动定值",
|
||||
"nameList": [
|
||||
"DipStrVal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压暂升启动定值",
|
||||
"nameList": [
|
||||
"SwlStrVal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "电压中断启动定值",
|
||||
"nameList": [
|
||||
"IntrStrVal"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "其余数据",
|
||||
"LnInstList": [
|
||||
"波动闪变值",
|
||||
"录波文件"
|
||||
],
|
||||
"ObjectList": [
|
||||
{
|
||||
"desc": "线电压短时闪变值",
|
||||
"nameList": [
|
||||
"PPPst"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压短时闪变值",
|
||||
"nameList": [
|
||||
"PhPst"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压长时闪变值",
|
||||
"nameList": [
|
||||
"PPPlt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压长时闪变值",
|
||||
"nameList": [
|
||||
"PhPlt"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压电压变动幅值",
|
||||
"nameList": [
|
||||
"PPFluc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压电压变动幅值",
|
||||
"nameList": [
|
||||
"PhFluc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "线电压电压变动频度",
|
||||
"nameList": [
|
||||
"PPFlucf"
|
||||
]
|
||||
},
|
||||
{
|
||||
"desc": "相电压电压变动频度",
|
||||
"nameList": [
|
||||
"PhFlucf"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div class="icd-document-tree">
|
||||
<el-tree
|
||||
v-if="treeNodes.length"
|
||||
:data="treeNodes"
|
||||
node-key="key"
|
||||
:indent="18"
|
||||
:default-expanded-keys="defaultExpandedKeys"
|
||||
:expand-on-click-node="false"
|
||||
class="icd-tree"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="icd-tree-node">
|
||||
<span class="icd-tree-node__label">{{ data.label }}</span>
|
||||
<span v-if="data.value !== undefined" class="icd-tree-node__value">{{ data.value }}</span>
|
||||
<span v-else class="icd-tree-node__summary">{{ data.summary }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
<div v-else class="icd-tree-empty">接口返回 `icdDocument` 后,会在这里以层级结构展示文档内容。</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
||||
|
||||
defineOptions({
|
||||
name: 'IcdDocumentTree'
|
||||
})
|
||||
|
||||
interface DocumentTreeNode {
|
||||
key: string
|
||||
label: string
|
||||
value?: string
|
||||
summary?: string
|
||||
children?: DocumentTreeNode[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
document: MmsMapping.IcdDocument | null
|
||||
}>()
|
||||
|
||||
const treeNodes = computed<DocumentTreeNode[]>(() => {
|
||||
if (!props.document) return []
|
||||
return [buildTreeNode('icdDocument', props.document, 'icdDocument')]
|
||||
})
|
||||
|
||||
const defaultExpandedKeys = computed(() => {
|
||||
const rootNode = treeNodes.value[0]
|
||||
if (!rootNode) return []
|
||||
|
||||
return [rootNode.key, ...(rootNode.children?.map(child => child.key) || [])]
|
||||
})
|
||||
|
||||
// 业务展示要求:按接口原始层级渲染 icdDocument,避免左侧信息再次退化成平铺文本。
|
||||
const buildTreeNode = (label: string, source: unknown, path: string): DocumentTreeNode => {
|
||||
if (Array.isArray(source)) {
|
||||
return {
|
||||
key: path,
|
||||
label,
|
||||
summary: `数组(${source.length})`,
|
||||
children: source.map((item, index) => buildTreeNode(`[${index}]`, item, `${path}.${index}`))
|
||||
}
|
||||
}
|
||||
|
||||
if (source && typeof source === 'object') {
|
||||
const entries = Object.entries(source as Record<string, unknown>).filter(([, value]) => value !== undefined)
|
||||
|
||||
return {
|
||||
key: path,
|
||||
label,
|
||||
summary: `对象(${entries.length})`,
|
||||
children: entries.map(([key, value]) => buildTreeNode(key, value, `${path}.${key}`))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: path,
|
||||
label,
|
||||
value: formatNodeValue(source)
|
||||
}
|
||||
}
|
||||
|
||||
const formatNodeValue = (source: unknown) => {
|
||||
if (source === null) return 'null'
|
||||
if (typeof source === 'string') return source || '""'
|
||||
return String(source)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.icd-document-tree {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.icd-tree {
|
||||
padding: 4px 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.icd-tree-node {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 2px 0;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.icd-tree-node__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #172033;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.icd-tree-node__summary,
|
||||
.icd-tree-node__value {
|
||||
font-size: 13px;
|
||||
color: #4b5563;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.icd-tree-node__value {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5f9;
|
||||
font-family: Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.icd-tree-empty {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
:deep(.icd-tree .el-tree-node__content) {
|
||||
min-height: 32px;
|
||||
height: auto;
|
||||
padding: 4px 0;
|
||||
align-items: flex-start;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.icd-tree .el-tree-node__expand-icon) {
|
||||
margin-top: 7px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
:deep(.icd-tree .el-tree-node:focus > .el-tree-node__content),
|
||||
:deep(.icd-tree .el-tree-node__content:hover) {
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
:deep(.icd-tree .el-tree-node__children) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<section class="mapping-panel config-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2 class="panel-title">请求配置</h2>
|
||||
<p class="panel-description">这里直接编辑 request.indexSelection,默认值会在选择 ICD 文件后自动生成。</p>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="showGenerateButton"
|
||||
type="primary"
|
||||
:icon="Connection"
|
||||
:loading="isSubmitting"
|
||||
:disabled="!canGenerate"
|
||||
@click="emit('generate')"
|
||||
>
|
||||
生成映射
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<div class="panel-section result-card">
|
||||
<el-alert v-if="jsonError" :title="jsonError" type="error" :closable="false" class="json-alert" />
|
||||
|
||||
<el-input
|
||||
type="textarea"
|
||||
class="index-selection-textarea"
|
||||
:model-value="indexSelectionJson"
|
||||
:disabled="isSubmitting"
|
||||
:rows="18"
|
||||
resize="none"
|
||||
placeholder="ICD 解析完成后,这里会自动填充 request.indexSelection,可继续直接编辑。"
|
||||
@update:model-value="value => emit('update:indexSelectionJson', String(value || ''))"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!hasDefaultJson" :description="emptyDescription" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Connection } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'MappingConfigPanel'
|
||||
})
|
||||
|
||||
defineProps<{
|
||||
indexSelectionJson: string
|
||||
isSubmitting: boolean
|
||||
canGenerate: boolean
|
||||
jsonError: string
|
||||
showGenerateButton: boolean
|
||||
hasDefaultJson: boolean
|
||||
emptyDescription: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:indexSelectionJson', value: string): void
|
||||
(event: 'generate'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mapping-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.config-panel {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.panel-description {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
}
|
||||
|
||||
.result-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.index-selection-textarea {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.index-selection-textarea :deep(.el-textarea) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.index-selection-textarea :deep(.el-textarea__inner) {
|
||||
height: 100%;
|
||||
font-family: Consolas, 'Courier New', monospace;
|
||||
line-height: 1.6;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.json-alert {
|
||||
margin: 16px 0 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mapping-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<section class="mapping-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2 class="panel-title">ICD 解析</h2>
|
||||
<p class="panel-description">选择 ICD 文件后仅保存当前文件,点击“解析 ICD”后才会向后台请求候选数据。</p>
|
||||
</div>
|
||||
<el-tag :type="requestStatusTagType" effect="light">{{ requestStatusText }}</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="panel-content">
|
||||
<div class="panel-section file-action-row">
|
||||
<div class="file-select-row">
|
||||
<el-input
|
||||
:model-value="selectedIcdFileName"
|
||||
readonly
|
||||
placeholder="请选择 `.icd`、`.cid`、`.scd` 或 `.xml` 文件"
|
||||
class="file-input"
|
||||
/>
|
||||
<el-button type="primary" :icon="FolderOpened" :loading="isSubmitting" @click="openIcdFilePicker">
|
||||
选择 ICD
|
||||
</el-button>
|
||||
<input
|
||||
ref="icdFileInputRef"
|
||||
class="hidden-file-input"
|
||||
type="file"
|
||||
:accept="icdFileAccept"
|
||||
@change="event => emit('file-change', event)"
|
||||
/>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
:icon="Search"
|
||||
:loading="isSubmitting"
|
||||
:disabled="!selectedIcdFileName"
|
||||
@click="emit('parse')"
|
||||
>
|
||||
解析 ICD
|
||||
</el-button>
|
||||
<el-button :icon="Delete" :disabled="!canReset" @click="emit('reset')">清空</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Delete, FolderOpened, Search } from '@element-plus/icons-vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'MappingRequestPanel'
|
||||
})
|
||||
|
||||
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
|
||||
|
||||
defineProps<{
|
||||
selectedIcdFileName: string
|
||||
isSubmitting: boolean
|
||||
icdFileAccept: string
|
||||
requestStatusText: string
|
||||
requestStatusTagType: TagType
|
||||
canReset: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'file-change', value: Event): void
|
||||
(event: 'parse'): void
|
||||
(event: 'reset'): void
|
||||
}>()
|
||||
|
||||
const icdFileInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const openIcdFilePicker = () => {
|
||||
icdFileInputRef.value?.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mapping-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.panel-description {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
}
|
||||
|
||||
.file-action-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.file-select-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
width: 360px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mapping-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.file-select-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<section class="mapping-panel">
|
||||
<div class="panel-header">
|
||||
<div>
|
||||
<h2 class="panel-title">调试输出</h2>
|
||||
<p class="panel-description">右侧展示最近一次接口返回的 mappingJson 和 problems,并支持导出当前映射摘要。</p>
|
||||
</div>
|
||||
<div class="panel-actions">
|
||||
<el-button plain :icon="Download" :disabled="!canExportMapping" @click="emit('export-mapping')">
|
||||
导出映射文件
|
||||
</el-button>
|
||||
<el-tag :type="responseStatusTagType" effect="light">{{ responseStatusText }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-content panel-content--fixed">
|
||||
<div class="panel-section result-card grow-card preview-tab-section">
|
||||
<el-tabs v-model="activeTabProxy" class="preview-tabs">
|
||||
<el-tab-pane label="映射摘要" name="mapping">
|
||||
<div class="preview-header preview-header--compact">
|
||||
<div class="preview-meta">{{ mappingMetaText }}</div>
|
||||
</div>
|
||||
<div class="mapping-json-scroll">
|
||||
<pre v-if="mappingJsonPreview" class="mapping-json-text">{{ mappingJsonPreview }}</pre>
|
||||
<el-empty v-else description="当前返回未包含 mappingJson" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="problemTabLabel" name="problem">
|
||||
<div class="problem-section">
|
||||
<div v-if="problemList.length" class="problem-list">
|
||||
<div v-for="(problem, index) in problemList" :key="`${index}-${problem}`" class="problem-item">
|
||||
<span class="problem-index">{{ index + 1 }}</span>
|
||||
<span class="problem-text">{{ problem }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else :description="problemEmptyText" />
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Download } from '@element-plus/icons-vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'MappingResultPanel'
|
||||
})
|
||||
|
||||
type TagType = 'success' | 'warning' | 'info' | 'primary' | 'danger'
|
||||
|
||||
const props = defineProps<{
|
||||
responseStatusText: string
|
||||
responseStatusTagType: TagType
|
||||
activeResultTab: 'mapping' | 'problem'
|
||||
mappingMetaText: string
|
||||
mappingJsonPreview: string
|
||||
problemTabLabel: string
|
||||
problemList: string[]
|
||||
problemEmptyText: string
|
||||
canExportMapping: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:activeResultTab', value: 'mapping' | 'problem'): void
|
||||
(event: 'export-mapping'): void
|
||||
}>()
|
||||
|
||||
const activeTabProxy = computed({
|
||||
get: () => props.activeResultTab,
|
||||
set: value => emit('update:activeResultTab', value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mapping-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
padding: 24px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.panel-description {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.panel-content--fixed {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-section {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
||||
}
|
||||
|
||||
.result-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.grow-card {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.preview-tab-section {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.preview-tabs {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.preview-tabs :deep(.el-tabs__header) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-tabs :deep(.el-tabs__nav-wrap::after) {
|
||||
background-color: #e5e7eb;
|
||||
}
|
||||
|
||||
.preview-tabs :deep(.el-tabs__item) {
|
||||
height: 36px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.preview-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.preview-tabs :deep(.el-tabs__content) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.preview-tabs :deep(.el-tab-pane) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.preview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview-header--compact {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.preview-meta {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.mapping-json-scroll,
|
||||
.problem-section {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.mapping-json-text {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
border: 1px solid #dbe3f0;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
font-family: Consolas, 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: #172033;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.problem-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.problem-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid #f3d19e;
|
||||
border-radius: 10px;
|
||||
background: #fff7ed;
|
||||
}
|
||||
|
||||
.problem-index {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
background: #f97316;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.problem-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #7c2d12;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mapping-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.panel-header,
|
||||
.panel-actions,
|
||||
.preview-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
101
frontend/src/views/tools/mmsmapping/utils/indexSelection.ts
Normal file
101
frontend/src/views/tools/mmsmapping/utils/indexSelection.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
|
||||
const normalizeRequiredString = (value: unknown, fieldPath: string) => {
|
||||
if (typeof value !== 'string' || !value.trim()) {
|
||||
throw new Error(`${fieldPath} 必须是非空字符串`)
|
||||
}
|
||||
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
const normalizeOptionalString = (value: unknown) => (typeof value === 'string' ? value.trim() : '')
|
||||
|
||||
const normalizeBindings = (value: unknown, groupIndex: number): MmsMapping.IndexSelectionBinding[] => {
|
||||
if (!Array.isArray(value) || !value.length) {
|
||||
throw new Error(`request.indexSelection[${groupIndex}].bindings 必须是非空数组`)
|
||||
}
|
||||
|
||||
return value.map((binding, bindingIndex) => {
|
||||
if (!isRecord(binding)) {
|
||||
throw new Error(`request.indexSelection[${groupIndex}].bindings[${bindingIndex}] 必须是对象`)
|
||||
}
|
||||
|
||||
return {
|
||||
reportName: normalizeRequiredString(
|
||||
binding.reportName,
|
||||
`request.indexSelection[${groupIndex}].bindings[${bindingIndex}].reportName`
|
||||
),
|
||||
dataSetName: normalizeRequiredString(
|
||||
binding.dataSetName,
|
||||
`request.indexSelection[${groupIndex}].bindings[${bindingIndex}].dataSetName`
|
||||
),
|
||||
label: normalizeRequiredString(
|
||||
binding.label,
|
||||
`request.indexSelection[${groupIndex}].bindings[${bindingIndex}].label`
|
||||
),
|
||||
lnInst: normalizeRequiredString(
|
||||
binding.lnInst,
|
||||
`request.indexSelection[${groupIndex}].bindings[${bindingIndex}].lnInst`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const buildDefaultIndexSelection = (
|
||||
candidateGroups: MmsMapping.IndexCandidateGroup[]
|
||||
): MmsMapping.IndexSelectionGroup[] =>
|
||||
candidateGroups
|
||||
.filter(candidate => candidate.groupKey?.trim())
|
||||
.map(candidate => {
|
||||
const defaultReport = (candidate.reports || []).find(
|
||||
report => report.reportName?.trim() && report.dataSetName?.trim()
|
||||
)
|
||||
const defaultLnInst = (defaultReport?.availableLnInstValues || []).find(item => item?.trim())?.trim() || ''
|
||||
|
||||
return {
|
||||
groupKey: candidate.groupKey!.trim(),
|
||||
groupDesc: candidate.groupDesc?.trim() || '',
|
||||
bindings: (candidate.templateLabels || [])
|
||||
.map(label => label?.trim() || '')
|
||||
.filter(Boolean)
|
||||
.map(label => ({
|
||||
reportName: defaultReport?.reportName?.trim() || '',
|
||||
dataSetName: defaultReport?.dataSetName?.trim() || '',
|
||||
label,
|
||||
lnInst: defaultLnInst
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
export const formatIndexSelectionJson = (value: MmsMapping.IndexSelectionGroup[]) => JSON.stringify(value, null, 4)
|
||||
|
||||
export const parseIndexSelectionJson = (source: string): MmsMapping.IndexSelectionGroup[] => {
|
||||
let parsed: unknown
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(source)
|
||||
} catch {
|
||||
throw new Error('request.indexSelection 不是合法 JSON')
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error('request.indexSelection 必须是数组')
|
||||
}
|
||||
|
||||
return parsed.map((group, groupIndex) => {
|
||||
if (!isRecord(group)) {
|
||||
throw new Error(`request.indexSelection[${groupIndex}] 必须是对象`)
|
||||
}
|
||||
|
||||
const groupDesc = normalizeOptionalString(group.groupDesc)
|
||||
|
||||
return {
|
||||
groupKey: normalizeRequiredString(group.groupKey, `request.indexSelection[${groupIndex}].groupKey`),
|
||||
groupDesc,
|
||||
bindings: normalizeBindings(group.bindings, groupIndex)
|
||||
}
|
||||
})
|
||||
}
|
||||
15
frontend/src/views/tools/mmsmapping/utils/requestPayload.ts
Normal file
15
frontend/src/views/tools/mmsmapping/utils/requestPayload.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { MmsMapping } from '@/api/tools/mmsmapping/interface'
|
||||
|
||||
export const DEFAULT_REQUEST_OPTIONS = {
|
||||
saveToDisk: false,
|
||||
prettyJson: true,
|
||||
outputDir: ''
|
||||
} satisfies Pick<MmsMapping.GetIcdMmsJsonRequestPayload, 'saveToDisk' | 'prettyJson' | 'outputDir'>
|
||||
|
||||
export const createBaseRequestPayload = (
|
||||
form: MmsMapping.BaseRequestForm
|
||||
): Omit<MmsMapping.GetIcdMmsJsonRequestPayload, 'indexSelection'> => ({
|
||||
version: form.version.trim() || '1.0',
|
||||
author: form.author.trim() || 'system',
|
||||
...DEFAULT_REQUEST_OPTIONS
|
||||
})
|
||||
Reference in New Issue
Block a user