diff --git a/frontend/src/api/event/eventList/index.ts b/frontend/src/api/event/eventList/index.ts new file mode 100644 index 0000000..945a78b --- /dev/null +++ b/frontend/src/api/event/eventList/index.ts @@ -0,0 +1,14 @@ +import http from '@/api' +import type { EventList } from './interface' + +export const getTransientEventPage = (params: EventList.TransientPageParams) => { + return http.post>('/event/list/transient/page', params) +} + +export const getTransientEventDetail = (eventId: string) => { + return http.get(`/event/list/transient/${eventId}`) +} + +export const exportTransientEvents = (params: EventList.TransientPageParams) => { + return http.downloadWithHeaders('/event/list/transient/export', params) +} diff --git a/frontend/src/api/event/eventList/interface/index.ts b/frontend/src/api/event/eventList/interface/index.ts new file mode 100644 index 0000000..86b599f --- /dev/null +++ b/frontend/src/api/event/eventList/interface/index.ts @@ -0,0 +1,50 @@ +import type { ReqPage } from '@/api/interface' + +export namespace EventList { + export interface PageResult { + records: T[] + current: number + size: number + total: number + pages?: number + } + + export interface TransientPageParams extends ReqPage { + startTimeStart?: string + startTimeEnd?: string + eventType?: string + phase?: string + eventDescribe?: string + durationMin?: number + durationMax?: number + featureAmplitudeMin?: number + featureAmplitudeMax?: number + fileFlag?: number + dealFlag?: number + lineIds?: string[] + engineeringName?: string + projectName?: string + equipmentName?: string + lineName?: string + } + + export interface TransientEventRecord { + eventId: string + measurementPointId?: string + eventType?: string + equipmentName?: string + engineeringName?: string + projectName?: string + startTime?: string + lineName?: string + eventDescribe?: string + sagsource?: string + phase?: string + duration?: number + featureAmplitude?: number + wavePath?: string + fileFlag?: number + dealFlag?: number + createTime?: string + } +} diff --git a/frontend/src/api/tools/addLedger/index.ts b/frontend/src/api/tools/addLedger/index.ts index 5ba34da..30ef811 100644 --- a/frontend/src/api/tools/addLedger/index.ts +++ b/frontend/src/api/tools/addLedger/index.ts @@ -170,3 +170,21 @@ export const getAvailableLineNos = (params: AddLedger.AvailableLineNoParams) => export const deleteAddLedgerNode = (params: AddLedger.DeleteNodeParams) => { return requestAddLedger('delete', '/node', params) } + +export const getAddLedgerDictTypeList = (params: AddLedger.DictTypeListParams) => { + return http.post | AddLedger.DictTypeRecord[]>('/dictType/list', params, { + loading: false + }) +} + +export const getAddLedgerDictDataList = (params: AddLedger.DictDataListParams) => { + return http.post | AddLedger.DictDataRecord[]>('/dictData/listByTypeId', params, { + loading: false + }) +} + +export const getAddLedgerDictDataById = (id: string) => { + return http.post('/dictData/getDicDataById', id, { + loading: false + }) +} diff --git a/frontend/src/api/tools/addLedger/interface/index.ts b/frontend/src/api/tools/addLedger/interface/index.ts index 9a99a46..43f377f 100644 --- a/frontend/src/api/tools/addLedger/interface/index.ts +++ b/frontend/src/api/tools/addLedger/interface/index.ts @@ -107,4 +107,34 @@ export namespace AddLedger { label: string value: T } + + export interface PageResult { + records?: T[] + } + + export interface DictTypeListParams { + code: string + pageNum: number + pageSize: number + } + + export interface DictTypeRecord { + id?: string + name?: string + code?: string + state?: number + } + + export interface DictDataListParams { + typeId: string + pageNum: number + pageSize: number + } + + export interface DictDataRecord { + id?: string + name?: string + code?: string + state?: number + } } diff --git a/frontend/src/api/tools/waveform/index.ts b/frontend/src/api/tools/waveform/index.ts index c4b831a..126b471 100644 --- a/frontend/src/api/tools/waveform/index.ts +++ b/frontend/src/api/tools/waveform/index.ts @@ -11,6 +11,7 @@ export const parseComtradeApi = (params: Waveform.ParseComtradeParams) => { formData.append('cfgFile', params.cfgFile) formData.append('datFile', params.datFile) + appendFormDataValue(formData, 'parseType', params.parseType) return http.post(`/wave/parseComtrade`, formData, { headers: { 'Content-Type': 'multipart/form-data' } diff --git a/frontend/src/api/tools/waveform/interface/index.ts b/frontend/src/api/tools/waveform/interface/index.ts index fb0fa34..e68dc8a 100644 --- a/frontend/src/api/tools/waveform/interface/index.ts +++ b/frontend/src/api/tools/waveform/interface/index.ts @@ -39,6 +39,8 @@ export namespace Waveform { c?: string channelName?: string unit?: string + totalChannels?: number + phaseCount?: number isOpen?: boolean title?: string colors?: string[] @@ -68,10 +70,14 @@ export namespace Waveform { listRmsMinData?: number[][] iPhasic?: number ptType?: number + szValueType?: string pt?: number ct?: number time?: string monitorName?: string + unit?: string + totalChannels?: number + phaseCount?: number } export interface EigenvalueDTO { @@ -141,5 +147,8 @@ export namespace Waveform { waveData?: WaveDataDTO waveDataDetails?: WaveDataDetail[] eigenvalues?: EigenvalueDTO[] + unit?: string + totalChannels?: number + phaseCount?: number } } diff --git a/frontend/src/routers/modules/dynamicRouter.ts b/frontend/src/routers/modules/dynamicRouter.ts index acc1a12..8b11af1 100644 --- a/frontend/src/routers/modules/dynamicRouter.ts +++ b/frontend/src/routers/modules/dynamicRouter.ts @@ -16,6 +16,8 @@ const STATIC_ROUTE_NAMES = new Set([ 'toolWaveform', 'toolMmsMapping', 'toolAddData', + 'toolAddLedger', + 'eventList', 'systemMonitor', 'diskMonitor', '403', diff --git a/frontend/src/routers/modules/staticRouter.ts b/frontend/src/routers/modules/staticRouter.ts index 8b5906f..078d409 100644 --- a/frontend/src/routers/modules/staticRouter.ts +++ b/frontend/src/routers/modules/staticRouter.ts @@ -75,6 +75,15 @@ export const staticRouter: RouteRecordRaw[] = [ title: '数据台账' } }, + { + path: '/eventList/index', + name: 'eventList', + alias: ['/event/eventList'], + component: () => import('@/views/event/eventList/index.vue'), + meta: { + title: '事件列表' + } + }, { path: '/403', name: '403', diff --git a/frontend/src/views/event/eventList/index.vue b/frontend/src/views/event/eventList/index.vue new file mode 100644 index 0000000..604a238 --- /dev/null +++ b/frontend/src/views/event/eventList/index.vue @@ -0,0 +1,309 @@ + + + + + diff --git a/frontend/src/views/tools/addLedger/API_DEBUG.md b/frontend/src/views/tools/addLedger/API_DEBUG.md new file mode 100644 index 0000000..a1a4840 --- /dev/null +++ b/frontend/src/views/tools/addLedger/API_DEBUG.md @@ -0,0 +1,484 @@ +# add-ledger API 调试文档 + +## 1. 基础信息 + +| 项 | 值 | +|---|---| +| 服务模块 | `tools/add-ledger` | +| 默认本地地址 | `http://127.0.0.1:18192` | +| 接口前缀 | `/addLedger` | +| 数据格式 | `application/json;charset=UTF-8` | +| 认证方式 | 走全局认证过滤器,除登录和 Swagger 外均需携带访问令牌 | + +请求头: + +```http +Authorization: Bearer ${accessToken} +Content-Type: application/json;charset=UTF-8 +``` + +说明: + +- `Authorization` 的真实 Header 名称和前缀来自公共组件 `SecurityConstants`,当前过滤器按 `SecurityConstants.AUTHORIZATION_KEY` 和 `SecurityConstants.AUTHORIZATION_PREFIX` 校验。 +- 以下响应示例只固定 `data` 结构;`code`、`message` 以 `HttpResultUtil` 和 `CommonResponseEnum.SUCCESS` 的运行时序列化结果为准。 +- 编辑项目、设备、测点时不支持搬迁父节点;如果请求中传父级 ID,后端仍以当前台账节点已有父级关系为准。 +- 删除父节点前端必须二次确认;后端收到删除请求后直接执行级联软删除。 + +统一成功响应结构示例: + +```json +{ + "code": "SUCCESS_CODE", + "message": "SUCCESS_MESSAGE", + "data": {} +} +``` + +## 2. 调试顺序建议 + +1. 调用登录接口获取 `accessToken`。 +2. 新增工程。 +3. 使用工程 ID 新增项目。 +4. 使用项目 ID 新增设备。 +5. 查询设备可用线路号。 +6. 使用设备 ID 和可用线路号新增测点。 +7. 调用台账树和详情接口核对层级与字段。 +8. 调用删除接口验证软删除和级联软删除。 + +## 3. 接口总览 + +| 方法 | 路径 | 说明 | +|---|---|---| +| `GET` | `/addLedger/tree` | 查询台账树 | +| `GET` | `/addLedger/detail` | 查询节点详情 | +| `POST` | `/addLedger/engineering/save` | 新增或保存工程 | +| `POST` | `/addLedger/project/save` | 新增或保存项目 | +| `POST` | `/addLedger/equipment/save` | 新增或保存设备 | +| `POST` | `/addLedger/line/save` | 新增或保存测点 | +| `GET` | `/addLedger/line/availableLineNos` | 查询设备可用线路号 | +| `DELETE` | `/addLedger/node` | 删除节点并级联软删除子节点 | + +## 4. 查询台账树 + +### 4.1 请求 + +```http +GET /addLedger/tree?keyword=测试 +``` + +Query 参数: + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `keyword` | `String` | 否 | 按 `cs_ledger.Name` 模糊查询 | + +curl: + +```bash +curl -X GET "http://127.0.0.1:18192/addLedger/tree?keyword=测试" \ + -H "Authorization: Bearer ${accessToken}" +``` + +### 4.2 响应 + +```json +{ + "code": "SUCCESS_CODE", + "message": "SUCCESS_MESSAGE", + "data": [ + { + "id": "engineeringId", + "parentId": "0", + "parentIds": "0", + "name": "测试工程", + "level": 0, + "sort": 0, + "children": [ + { + "id": "projectId", + "parentId": "engineeringId", + "parentIds": "0,engineeringId", + "name": "测试项目", + "level": 1, + "sort": 0, + "children": [] + } + ] + } + ] +} +``` + +## 5. 查询节点详情 + +### 5.1 请求 + +```http +GET /addLedger/detail?id=engineeringId&level=0 +``` + +Query 参数: + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `id` | `String` | 是 | 节点 ID,与业务表主键一致 | +| `level` | `Integer` | 是 | `0` 工程,`1` 项目,`2` 设备,`3` 测点 | + +curl: + +```bash +curl -X GET "http://127.0.0.1:18192/addLedger/detail?id=engineeringId&level=0" \ + -H "Authorization: Bearer ${accessToken}" +``` + +### 5.2 响应 + +工程详情: + +```json +{ + "code": "SUCCESS_CODE", + "message": "SUCCESS_MESSAGE", + "data": { + "id": "engineeringId", + "level": 0, + "parentId": "0", + "parentIds": "0", + "name": "测试工程", + "sort": 0, + "province": "320000000000", + "city": "320100000000", + "description": "工程描述" + } +} +``` + +## 6. 新增或保存工程 + +### 6.1 请求 + +```http +POST /addLedger/engineering/save +``` + +Body 参数: + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `id` | `String` | 否 | 为空时新增;非空时编辑 | +| `name` | `String` | 是 | 工程名称,同步为树节点名称 | +| `province` | `String` | 否 | 省 | +| `city` | `String` | 否 | 市 | +| `description` | `String` | 否 | 描述 | + +新增示例: + +```bash +curl -X POST "http://127.0.0.1:18192/addLedger/engineering/save" \ + -H "Authorization: Bearer ${accessToken}" \ + -H "Content-Type: application/json;charset=UTF-8" \ + -d "{\"name\":\"测试工程\",\"province\":\"320000000000\",\"city\":\"320100000000\",\"description\":\"工程描述\"}" +``` + +编辑示例: + +```json +{ + "id": "engineeringId", + "name": "测试工程-修改", + "province": "320000000000", + "city": "320100000000", + "description": "工程描述修改" +} +``` + +### 6.2 响应 + +```json +{ + "code": "SUCCESS_CODE", + "message": "SUCCESS_MESSAGE", + "data": { + "id": "engineeringId", + "level": 0, + "parentId": "0", + "parentIds": "0", + "name": "测试工程", + "province": "320000000000", + "city": "320100000000", + "description": "工程描述" + } +} +``` + +## 7. 新增或保存项目 + +### 7.1 请求 + +```http +POST /addLedger/project/save +``` + +Body 参数: + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `id` | `String` | 否 | 为空时新增;非空时编辑 | +| `engineeringId` | `String` | 新增必填 | 父级工程 ID | +| `name` | `String` | 是 | 项目名称,同步为树节点名称 | +| `area` | `String` | 否 | 相对位置 | +| `description` | `String` | 否 | 项目描述 | + +新增示例: + +```json +{ + "engineeringId": "engineeringId", + "name": "测试项目", + "area": "灿能园区", + "description": "项目描述" +} +``` + +curl: + +```bash +curl -X POST "http://127.0.0.1:18192/addLedger/project/save" \ + -H "Authorization: Bearer ${accessToken}" \ + -H "Content-Type: application/json;charset=UTF-8" \ + -d "{\"engineeringId\":\"engineeringId\",\"name\":\"测试项目\",\"area\":\"灿能园区\",\"description\":\"项目描述\"}" +``` + +## 8. 新增或保存设备 + +### 8.1 请求 + +```http +POST /addLedger/equipment/save +``` + +Body 参数: + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `id` | `String` | 否 | 为空时新增;非空时编辑 | +| `projectId` | `String` | 新增必填 | 父级项目 ID | +| `name` | `String` | 是 | 装置名称,同步为树节点名称 | +| `ndid` | `String` | 是 | 网络设备 ID | +| `mac` | `String` | 是 | 装置 MAC 地址 | +| `devType` | `String` | 否 | 装置类型,保存字典数据 ID | +| `devModel` | `String` | 是 | 装置型号,保存字典数据 ID | +| `devAccessMethod` | `String` | 否 | 装置接入方式 | +| `nodeId` | `String` | 否 | 前置服务器 IP | +| `nodeProcess` | `Integer` | 否 | 前置进程号 | +| `upgrade` | `Integer` | 否 | 是否支持升级,`0` 否,`1` 是,默认 `0` | + +新增示例: + +```json +{ + "projectId": "projectId", + "name": "测试设备", + "ndid": "00B78D000001", + "mac": "00:B7:8D:00:00:01", + "devType": "dictDataIdForDeviceType", + "devModel": "dictDataIdForDeviceModel", + "devAccessMethod": "MQTT", + "nodeId": "127.0.0.1", + "nodeProcess": 1, + "upgrade": 0 +} +``` + +curl: + +```bash +curl -X POST "http://127.0.0.1:18192/addLedger/equipment/save" \ + -H "Authorization: Bearer ${accessToken}" \ + -H "Content-Type: application/json;charset=UTF-8" \ + -d "{\"projectId\":\"projectId\",\"name\":\"测试设备\",\"ndid\":\"00B78D000001\",\"mac\":\"00:B7:8D:00:00:01\",\"devModel\":\"dictDataIdForDeviceModel\",\"devAccessMethod\":\"MQTT\",\"upgrade\":0}" +``` + +### 8.2 默认值 + +新增设备时后端默认填充: + +| 字段 | 默认值 | +|---|---| +| `run_status` | `1` | +| `status` | `1` | +| `process` | `4` | +| `usage_status` | `1` | +| `sort` | `0` | +| `upgrade` | 未传时为 `0` | + +## 9. 查询设备可用线路号 + +### 9.1 请求 + +```http +GET /addLedger/line/availableLineNos?deviceId=equipmentId&lineId=lineId +``` + +Query 参数: + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `deviceId` | `String` | 是 | 设备 ID | +| `lineId` | `String` | 否 | 编辑测点时传入当前测点 ID | + +curl: + +```bash +curl -X GET "http://127.0.0.1:18192/addLedger/line/availableLineNos?deviceId=equipmentId" \ + -H "Authorization: Bearer ${accessToken}" +``` + +### 9.2 响应 + +```json +{ + "code": "SUCCESS_CODE", + "message": "SUCCESS_MESSAGE", + "data": [1, 2, 3, 4, 5] +} +``` + +## 10. 新增或保存测点 + +### 10.1 请求 + +```http +POST /addLedger/line/save +``` + +Body 参数: + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `lineId` | `String` | 否 | 为空时新增;非空时编辑 | +| `deviceId` | `String` | 新增必填 | 父级设备 ID | +| `name` | `String` | 是 | 监测点名,同步为树节点名称 | +| `lineNo` | `Integer` | 是 | 设备线路,范围 `1-20` | +| `conType` | `Integer` | 是 | 接线方式,`0` 星型,`1` 角型,`2` V 型 | +| `volGrade` | `Decimal` | 是 | 电压等级,仅支持 `0.38`、`10`、`35`、`110`、`220`、`500` | +| `position` | `String` | 否 | 安装位置 | +| `ctRatio` | `Decimal` | 是 | CT 一次额定值,非负数 | +| `ct2Ratio` | `Decimal` | 是 | CT 二次额定值,非负数 | +| `ptRatio` | `Decimal` | 是 | PT 一次额定值,非负数 | +| `pt2Ratio` | `Decimal` | 是 | PT 二次额定值,非负数 | +| `shortCircuitCapacity` | `Decimal` | 否 | 最小短路容量,非负数 | +| `basicCapacity` | `Decimal` | 否 | 基准短路容量,非负数 | +| `protocolCapacity` | `Decimal` | 否 | 用户协议容量,非负数 | +| `devCapacity` | `Decimal` | 否 | 供电设备容量,非负数 | +| `monitorObj` | `String` | 否 | 监测对象类型 | +| `isGovern` | `Integer` | 否 | 是否治理,默认 `0` | +| `monitorUser` | `String` | 否 | 敏感用户 ID | +| `isImportant` | `Integer` | 否 | 是否主要监测点,默认 `0` | + +新增示例: + +```json +{ + "deviceId": "equipmentId", + "name": "测试测点1", + "lineNo": 1, + "conType": 0, + "volGrade": 10, + "position": "positionDictDataId", + "ctRatio": 200, + "ct2Ratio": 1, + "ptRatio": 10, + "pt2Ratio": 0.1, + "shortCircuitCapacity": 10, + "basicCapacity": 10, + "protocolCapacity": 10, + "devCapacity": 10, + "monitorObj": "monitorObjDictDataId", + "isGovern": 0, + "monitorUser": "", + "isImportant": 0 +} +``` + +curl: + +```bash +curl -X POST "http://127.0.0.1:18192/addLedger/line/save" \ + -H "Authorization: Bearer ${accessToken}" \ + -H "Content-Type: application/json;charset=UTF-8" \ + -d "{\"deviceId\":\"equipmentId\",\"name\":\"测试测点1\",\"lineNo\":1,\"conType\":0,\"volGrade\":10,\"ctRatio\":200,\"ct2Ratio\":1,\"ptRatio\":10,\"pt2Ratio\":0.1,\"isGovern\":0,\"isImportant\":0}" +``` + +### 10.2 校验规则 + +- `lineNo` 必须在 `1-20` 之间。 +- 同设备下正常测点的 `lineNo` 不可重复。 +- `conType` 只能是 `0`、`1`、`2`。 +- `volGrade` 只能是 `0.38`、`10`、`35`、`110`、`220`、`500`。 +- CT/PT 字段必填且不能为负数。 +- 容量字段传入时不能为负数。 + +## 11. 删除节点 + +### 11.1 请求 + +```http +DELETE /addLedger/node?id=engineeringId&level=0 +``` + +Query 参数: + +| 参数 | 类型 | 必填 | 说明 | +|---|---|---|---| +| `id` | `String` | 是 | 节点 ID | +| `level` | `Integer` | 是 | `0` 工程,`1` 项目,`2` 设备,`3` 测点 | + +curl: + +```bash +curl -X DELETE "http://127.0.0.1:18192/addLedger/node?id=engineeringId&level=0" \ + -H "Authorization: Bearer ${accessToken}" +``` + +### 11.2 响应 + +```json +{ + "code": "SUCCESS_CODE", + "message": "SUCCESS_MESSAGE", + "data": true +} +``` + +### 11.3 删除影响范围 + +| 删除层级 | 后端处理 | +|---|---| +| 工程 | 软删除工程、下属项目、设备、测点和对应台账节点 | +| 项目 | 软删除项目、下属设备、测点和对应台账节点 | +| 设备 | 软删除设备、下属测点和对应台账节点 | +| 测点 | 软删除测点和对应台账节点 | + +软删除字段: + +| 表 | 字段 | 删除值 | +|---|---|---| +| `cs_engineering` | `status` | `0` | +| `cs_project` | `status` | `0` | +| `cs_equipment_delivery` | `run_status` | `0` | +| `cs_line` | `status` | `0` | +| `cs_ledger` | `State` | `0` | + +## 12. 常见错误 + +| 场景 | 典型错误信息 | +|---|---| +| 未传 token 或 token 格式错误 | 全局过滤器返回 token 解析失败 | +| 新增项目未传有效工程 ID | `台账节点不存在或已删除` | +| 新增设备未传有效项目 ID | `台账节点不存在或已删除` | +| 新增测点未传有效设备 ID | `台账节点不存在或已删除` | +| 测点线路号重复 | `同设备下 line_no 不可重复` | +| 测点线路号超范围 | `line_no 必须在 1-20 之间` | +| 接线方式非法 | `conType 只能是 0、1、2` | +| 电压等级非法 | `vol_grade 只能是 0.38、10、35、110、220、500` | diff --git a/frontend/src/views/tools/addLedger/components/EngineeringForm.vue b/frontend/src/views/tools/addLedger/components/EngineeringForm.vue index f583cea..d865e0b 100644 --- a/frontend/src/views/tools/addLedger/components/EngineeringForm.vue +++ b/frontend/src/views/tools/addLedger/components/EngineeringForm.vue @@ -3,10 +3,8 @@
工程配置
-
维护工程基础信息,并从工程下继续新增项目。
-
- 新增项目 +
保存工程 删除工程 @@ -14,24 +12,32 @@
- + - + - + - + @@ -39,8 +45,8 @@ + + diff --git a/frontend/src/views/tools/addLedger/components/LedgerTreePanel.vue b/frontend/src/views/tools/addLedger/components/LedgerTreePanel.vue index 57a170b..741f975 100644 --- a/frontend/src/views/tools/addLedger/components/LedgerTreePanel.vue +++ b/frontend/src/views/tools/addLedger/components/LedgerTreePanel.vue @@ -3,8 +3,18 @@
台账树
- 新增工程 - 刷新 + + + + + 刷新 +
@@ -24,10 +34,34 @@ > @@ -37,7 +71,7 @@ @@ -672,19 +1127,35 @@ onMounted(async () => { grid-template-columns: 320px minmax(0, 1fr); gap: 12px; width: 100%; + height: 100%; min-height: 0; } .add-ledger-main { display: flex; flex-direction: column; + position: relative; min-width: 0; min-height: 0; - gap: 10px; + gap: 6px; } -.detail-alert { - flex: none; +.add-ledger-toolbar { + position: absolute; + top: 0; + left: 50%; + z-index: 3; + transform: translateX(-50%); +} + +.add-ledger-toolbar :deep(.el-button) { + height: 28px; + padding: 0 14px; + font-size: 12px; +} + +.add-ledger-toolbar :deep(.el-button + .el-button) { + margin-left: 0; } .add-ledger-empty { @@ -698,6 +1169,12 @@ onMounted(async () => { color: var(--el-text-color-regular); } +.add-ledger-empty :deep(.el-button.is-circle) { + width: 28px; + height: 28px; + padding: 0; +} + .empty-title { font-size: 16px; font-weight: 600; diff --git a/frontend/src/views/tools/waveform/check-value-mode-contract.mjs b/frontend/src/views/tools/waveform/check-value-mode-contract.mjs new file mode 100644 index 0000000..db3b0e6 --- /dev/null +++ b/frontend/src/views/tools/waveform/check-value-mode-contract.mjs @@ -0,0 +1,144 @@ +/* eslint-env node */ +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const currentDir = path.dirname(fileURLToPath(import.meta.url)) +const viewFile = path.join(currentDir, 'index.vue') +const toolbarFile = path.join(currentDir, 'components', 'WaveformToolbar.vue') +const trendPanelFile = path.join(currentDir, 'components', 'WaveformTrendPanel.vue') +const interfaceFile = path.join(currentDir, '..', '..', '..', 'api', 'tools', 'waveform', 'interface', 'index.ts') + +const viewSource = fs.readFileSync(viewFile, 'utf8') +const toolbarSource = fs.readFileSync(toolbarFile, 'utf8') +const trendPanelSource = fs.readFileSync(trendPanelFile, 'utf8') +const interfaceSource = fs.readFileSync(interfaceFile, 'utf8') + +const expectations = [ + ['wave data supports szValueType', /interface\s+WaveDataDTO\s*{[\s\S]*szValueType\?:\s*string/], + ['wave data detail supports unit', /interface\s+WaveDataDetail\s*{[\s\S]*unit\?:\s*string/], + ['wave data detail supports totalChannels', /interface\s+WaveDataDetail\s*{[\s\S]*totalChannels\?:\s*number/], + ['wave data detail supports phaseCount', /interface\s+WaveDataDetail\s*{[\s\S]*phaseCount\?:\s*number/], + ['wave data supports unit fallback', /interface\s+WaveDataDTO\s*{[\s\S]*unit\?:\s*string/], + ['wave data supports totalChannels fallback', /interface\s+WaveDataDTO\s*{[\s\S]*totalChannels\?:\s*number/], + ['wave data supports phaseCount fallback', /interface\s+WaveDataDTO\s*{[\s\S]*phaseCount\?:\s*number/], + ['parse result supports root unit', /interface\s+WaveComtradeResultVO\s*{[\s\S]*unit\?:\s*string/], + ['parse result supports root totalChannels', /interface\s+WaveComtradeResultVO\s*{[\s\S]*totalChannels\?:\s*number/], + ['parse result supports root phaseCount', /interface\s+WaveComtradeResultVO\s*{[\s\S]*phaseCount\?:\s*number/], + [ + 'summary detail falls back to first normalized detail', + /summaryWaveDetail[\s\S]*activeWaveDetail\.value\s*\|\|\s*normalizedWaveDetails\.value\[0\]/ + ], + [ + 'normalized detail keeps backend unit fallback', + /unit:\s*detail\?\.unit\s*\|\|\s*waveData\?\.unit\s*\|\|\s*parseResult\?\.unit\s*\|\|\s*''/ + ], + [ + 'normalized detail keeps backend totalChannels', + /totalChannels:\s*detail\?\.totalChannels\s*\?\?\s*waveData\?\.totalChannels\s*\?\?\s*parseResult\?\.totalChannels/ + ], + [ + 'normalized detail keeps backend phaseCount', + /phaseCount:\s*detail\?\.phaseCount\s*\?\?\s*waveData\?\.phaseCount\s*\?\?\s*parseResult\?\.phaseCount/ + ], + [ + 'summary total channels prefers backend detail field', + /detail\?\.totalChannels\s*\?\?\s*summaryDetail\?\.totalChannels\s*\?\?\s*waveData\?\.totalChannels\s*\?\?\s*parseResult\?\.totalChannels\s*\?\?\s*cfgData\?\.nChannelNum/ + ], + [ + 'summary phase count prefers backend detail field', + /detail\?\.phaseCount\s*\?\?\s*summaryDetail\?\.phaseCount\s*\?\?\s*waveData\?\.phaseCount\s*\?\?\s*parseResult\?\.phaseCount\s*\?\?\s*waveData\?\.iPhasic/ + ], + [ + 'display unit prefers backend detail unit', + /const\s+resolveSummaryUnit[\s\S]*detail\?\.unit[\s\S]*return\s+detail\.unit[\s\S]*return\s+resolveDisplayUnit\(detail\)/ + ], + ['summary unit falls back to root unit', /label:\s*'单位'[\s\S]*summaryDetail[\s\S]*parseResult\?\.unit\s*\|\|\s*'--'/], + ['source value mode state exists', /sourceValueMode\s*=\s*ref\('secondary'\)/], + ['P maps to primary value mode', /value\s*===\s*'P'[\s\S]*return\s*'primary'/], + ['S maps to secondary value mode', /value\s*===\s*'S'[\s\S]*return\s*'secondary'/], + [ + 'parsed waveform defaults to primary display mode', + /sourceValueMode\.value\s*=\s*resolveSourceValueMode[\s\S]*activeValueMode\.value\s*=\s*'primary'/ + ], + ['reset waveform defaults to primary display mode', /resetSelectedWaveformFiles[\s\S]*activeValueMode\.value\s*=\s*'primary'/], + ['current channel detects ampere unit first', /isCurrentChannel[\s\S]*unit\s*===\s*'A'[\s\S]*return\s*true/], + ['current display unit stays A', /resolveDisplayUnit[\s\S]*isCurrentChannel\(detail\)[\s\S]*return\s*'A'/], + [ + 'voltage channel primary display unit is kV', + /resolveDisplayUnit[\s\S]*activeValueMode\.value\s*===\s*'primary'\s*\?\s*'kV'\s*:\s*'V'/ + ], + ['same source and display mode keep scale 1', /sourceValueMode\.value\s*===\s*activeValueMode\.value[\s\S]*return\s*1/], + [ + 'secondary voltage source converts to primary kV by multiplying ratio and dividing 1000', + /sourceValueMode\.value\s*===\s*'secondary'[\s\S]*return\s*isVoltageChannel\(detail\)\s*\?\s*normalizedRatio\s*\/\s*1000\s*:\s*normalizedRatio/ + ], + [ + 'primary voltage source converts to secondary V by multiplying 1000 and dividing ratio', + /return\s*isVoltageChannel\(detail\)\s*\?\s*1000\s*\/\s*normalizedRatio\s*:\s*1\s*\/\s*normalizedRatio/ + ], + ['trend payload uses display unit', /unit:\s*resolveDisplayUnit\(detail\)/], + ['temporary PT ratio state exists', /temporaryPtRatio\s*=\s*ref/], + ['temporary CT ratio state exists', /temporaryCtRatio\s*=\s*ref/], + [ + 'value scale reads temporary ratios before parsed ratios', + /isCurrentChannel\(detail\)\s*\?\s*temporaryCtRatio\.value\s*:\s*temporaryPtRatio\.value/ + ], + [ + 'parsed waveform resets temporary ratios from wave data', + /temporaryPtRatio\.value\s*=\s*parseResult\.waveData\?\.pt[\s\S]*temporaryCtRatio\.value\s*=\s*parseResult\.waveData\?\.ct/ + ], + ['toolbar exposes PT CT adjustment event', /'update-waveform-ratio':\s*\[value:\s*WaveformRatioValue\]/], + [ + 'ratio configuration button uses icon and text', + // + ], + [ + 'ratio configuration button follows value mode switch', + /class="value-mode-switch"[\s\S]*<\/el-radio-group>[\s\S]*:icon="Setting"[\s\S]*参数配置/ + ], + ['toolbar does not render PT CT label text', /
PT\/CT<\/div>/, true], + ['summary does not render PT CT label text', /label:\s*'PT \/ CT'/, true], + [ + 'summary renders current PT ratio value', + /label:\s*'PT变比'[\s\S]*formatNumber\(temporaryPtRatio\.value\s*\?\?\s*waveData\?\.pt/ + ], + [ + 'summary renders current CT ratio value', + /label:\s*'CT变比'[\s\S]*formatNumber\(temporaryCtRatio\.value\s*\?\?\s*waveData\?\.ct/ + ], + [ + 'ratio dialog edits PT above CT', + /label="PT变比"[\s\S]*v-model="ratioForm\.ptPrimary"[\s\S]*ratio-separator[\s\S]*v-model="ratioForm\.ptSecondary"[\s\S]*label="CT变比"[\s\S]*v-model="ratioForm\.ctPrimary"[\s\S]*ratio-separator[\s\S]*v-model="ratioForm\.ctSecondary"/ + ], + [ + 'ratio confirm calculates final ratios by division', + /pt:\s*Number\(ratioForm\.ptPrimary\)\s*\/\s*Number\(ratioForm\.ptSecondary\)[\s\S]*ct:\s*Number\(ratioForm\.ctPrimary\)\s*\/\s*Number\(ratioForm\.ctSecondary\)/ + ], + [ + 'trend tools first group contains zoom reset and pan only', + /key:\s*'viewport'[\s\S]*x-zoom-in[\s\S]*x-zoom-out[\s\S]*y-zoom-in[\s\S]*y-zoom-out[\s\S]*box-zoom[\s\S]*reset[\s\S]*pan[\s\S]*key:\s*'marker-view'/ + ], + [ + 'trend tools second group contains mark and fullscreen only', + /key:\s*'marker-view'[\s\S]*mark[\s\S]*fullscreen[\s\S]*key:\s*'export'/ + ], + ['trend tools third group contains image and data downloads', /key:\s*'export'[\s\S]*download-image[\s\S]*download-data/], + ['trend tool group separator is dashed vertical line', /\.trend-tool-group\s*\+\s*\.trend-tool-group[\s\S]*border-left:\s*1px\s+dashed/] +] + +const combinedSource = `${viewSource}\n${toolbarSource}\n${trendPanelSource}\n${interfaceSource}` +const failures = expectations.filter(([, pattern, shouldBeMissing]) => { + const exists = pattern.test(combinedSource) + return shouldBeMissing ? exists : !exists +}) + +if (failures.length) { + console.error('waveform value mode contract check failed:') + for (const [name] of failures) { + console.error(`- ${name}`) + } + process.exit(1) +} + +console.log('waveform value mode contract check passed') diff --git a/frontend/src/views/tools/waveform/components/WaveformToolbar.vue b/frontend/src/views/tools/waveform/components/WaveformToolbar.vue index 9b56a14..76f4fef 100644 --- a/frontend/src/views/tools/waveform/components/WaveformToolbar.vue +++ b/frontend/src/views/tools/waveform/components/WaveformToolbar.vue @@ -22,6 +22,23 @@
+
+
解析类型
+ + + +
+
波形通道
+ +
+ + 参数配置 + +
+ + + + +
+ + : + +
+
+ +
+ + : + +
+
+
+ +
', + '', + '', + '', + '', + buildTrendExportHeaderRows(columns), + '', + '', + buildTrendExportRows(timeLabels, columns), + '', + '
', + '', + '' + ].join('') +} + const downloadTrendData = () => { if (!hasWaveformData.value) { ElMessage.warning('暂无可导出的波形数据') return } - const trendPayload = activeTrendPayload.value - const allChannelPayloads = normalizedWaveDetails.value - .map((detail, index) => ({ - label: buildChannelLabel(detail, index), - payload: buildTrendPayload(detail, activeTrendTab.value, getValueScale(detail)) - })) - .filter(item => item.payload.series.length > 0) - const exportPayload = isAllChannelsActive.value ? allChannelPayloads[0]?.payload : trendPayload + const exportGroups = ( + isAllChannelsActive.value + ? normalizedWaveDetails.value.map((detail, index) => buildTrendExportPayloadGroup(detail, index)) + : [buildTrendExportPayloadGroup(activeWaveDetail.value, activeChannelIndex.value as number)] + ).filter(hasTrendExportSeries) + const timeLabels = resolveTrendExportTimeLabels(exportGroups) + const exportColumns = [ + ...exportGroups.flatMap(item => buildTrendExportColumns(item, 'instant')), + ...exportGroups.flatMap(item => buildTrendExportColumns(item, 'rms')) + ] - if (!exportPayload) { + if (!timeLabels.length || !exportColumns.length) { ElMessage.warning('暂无可导出的波形数据') return } - const header = ['时间', ...trendPayload.series.map(item => item.name)] - const allChannelHeader = [ - '时间', - ...allChannelPayloads.flatMap(item => item.payload.series.map(series => `${item.label}-${series.name}`)) - ] - const rows = exportPayload.timeLabels.map((time, index) => { - if (isAllChannelsActive.value) { - return [ - time, - ...allChannelPayloads.flatMap(item => item.payload.series.map(series => series.data[index] ?? '')) - ] - } - - return [time, ...trendPayload.series.map(item => item.data[index] ?? '')] - }) - - const csvContent = [isAllChannelsActive.value ? allChannelHeader : header, ...rows] - .map(row => row.join(',')) - .join('\n') - const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' }) + const excelContent = buildTrendExportExcelHtml(timeLabels, exportColumns) + const blob = new Blob([`\uFEFF${excelContent}`], { type: 'application/vnd.ms-excel;charset=utf-8;' }) const blobUrl = URL.createObjectURL(blob) const exportFile = document.createElement('a') - const fileName = buildTrendExportFileName('csv') + const fileName = buildTrendExportFileName('xls', false) exportFile.style.display = 'none' exportFile.download = fileName