From f9ed6c6245006e1e0083f6af7c6a5deacdfa40e7 Mon Sep 17 00:00:00 2001 From: yexb <553699424@qq.com> Date: Mon, 18 May 2026 08:46:42 +0800 Subject: [PATCH] =?UTF-8?q?refactor(event):=20=E9=87=8D=E6=9E=84=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E5=88=97=E8=A1=A8=E5=92=8C=E7=A8=B3=E6=80=81=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=A7=86=E5=9B=BE=E7=BB=84=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将事件列表页面逻辑拆分为 EventListTable 组件 - 新增 MeasurementPointDialog 和 VoltageToleranceDialog 弹窗组件 - 重构稳态数据视图为主工作台组件 SteadyTrendWorkbench - 移除不再使用的相别参数和相关逻辑 - 更新事件详情工具函数和接口参数映射 - 优化波形查看功能的数据传递方式 - 修正事件描述字段命名和严重程度解析逻辑 --- AGENTS.md | 2 +- BOOT-INF/classes/application.yml | 125 +++++ frontend/src/api/event/eventList/index.ts | 7 +- .../api/event/eventList/interface/index.ts | 12 +- .../steady/steadyDataView/interface/index.ts | 1 - .../check-search-layout-contract.mjs | 42 -- .../eventList/check-visible-contract.mjs | 61 --- .../eventList/components/EventListTable.vue | 469 ++++++++++++++++++ .../components/MeasurementPointDialog.vue | 46 ++ .../components/VoltageToleranceDialog.vue | 150 ++++++ .../check-display-contract.mjs | 12 +- .../{ => contracts}/check-export-contract.mjs | 34 +- .../check-query-params-contract.mjs | 18 +- .../{ => contracts}/check-route-contract.mjs | 6 +- .../check-search-layout-contract.mjs | 78 +++ .../check-time-range-contract.mjs | 2 +- .../contracts/check-visible-contract.mjs | 85 ++++ .../check-voltage-tolerance-contract.mjs | 79 +++ .../check-waveform-view-contract.mjs | 57 +++ frontend/src/views/event/eventList/index.vue | 379 ++------------ .../event/eventList/utils/detailItems.ts | 2 +- .../views/event/eventList/utils/display.ts | 12 + .../eventList/{ => utils}/eventTimeRange.ts | 2 +- .../event/eventList/utils/queryParams.ts | 17 +- .../src/views/event/eventList/utils/status.ts | 6 + .../event/eventList/utils/voltageTolerance.ts | 221 +++++++++ .../SteadyIndicatorFloatingPanel.vue | 115 +++++ .../components/SteadyIndicatorTree.vue | 2 +- .../components/SteadyLedgerTree.vue | 2 +- .../components/SteadyTrendToolbar.vue | 149 +++--- .../components/SteadyTrendWorkbench.vue | 131 +++++ .../{ => contracts}/check-route-contract.mjs | 2 +- .../contracts/check-selection-contract.mjs | 34 ++ .../{ => contracts}/check-trend-contract.mjs | 48 +- .../check-visible-contract.mjs | 24 +- .../src/views/steady/steadyDataView/index.vue | 192 +------ .../steadyDataView/utils/selectionRules.ts | 24 +- .../steadyDataView/utils/trendPayload.ts | 12 +- frontend/src/views/tools/waveform/index.vue | 38 +- 39 files changed, 1943 insertions(+), 755 deletions(-) create mode 100644 BOOT-INF/classes/application.yml delete mode 100644 frontend/src/views/event/eventList/check-search-layout-contract.mjs delete mode 100644 frontend/src/views/event/eventList/check-visible-contract.mjs create mode 100644 frontend/src/views/event/eventList/components/EventListTable.vue create mode 100644 frontend/src/views/event/eventList/components/MeasurementPointDialog.vue create mode 100644 frontend/src/views/event/eventList/components/VoltageToleranceDialog.vue rename frontend/src/views/event/eventList/{ => contracts}/check-display-contract.mjs (80%) rename frontend/src/views/event/eventList/{ => contracts}/check-export-contract.mjs (64%) rename frontend/src/views/event/eventList/{ => contracts}/check-query-params-contract.mjs (62%) rename frontend/src/views/event/eventList/{ => contracts}/check-route-contract.mjs (86%) create mode 100644 frontend/src/views/event/eventList/contracts/check-search-layout-contract.mjs rename frontend/src/views/event/eventList/{ => contracts}/check-time-range-contract.mjs (99%) create mode 100644 frontend/src/views/event/eventList/contracts/check-visible-contract.mjs create mode 100644 frontend/src/views/event/eventList/contracts/check-voltage-tolerance-contract.mjs create mode 100644 frontend/src/views/event/eventList/contracts/check-waveform-view-contract.mjs rename frontend/src/views/event/eventList/{ => utils}/eventTimeRange.ts (79%) create mode 100644 frontend/src/views/event/eventList/utils/voltageTolerance.ts create mode 100644 frontend/src/views/steady/steadyDataView/components/SteadyIndicatorFloatingPanel.vue create mode 100644 frontend/src/views/steady/steadyDataView/components/SteadyTrendWorkbench.vue rename frontend/src/views/steady/steadyDataView/{ => contracts}/check-route-contract.mjs (96%) create mode 100644 frontend/src/views/steady/steadyDataView/contracts/check-selection-contract.mjs rename frontend/src/views/steady/steadyDataView/{ => contracts}/check-trend-contract.mjs (63%) rename frontend/src/views/steady/steadyDataView/{ => contracts}/check-visible-contract.mjs (66%) diff --git a/AGENTS.md b/AGENTS.md index 77bee55..2e5eb16 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,7 @@ - `components/` 放当前页面专属展示块、弹窗、表格、工具栏和信息面板。组件通过 props / emits 与入口页通信,不直接越级调用页面接口状态。 - `utils/` 放当前页面专属纯函数或弱状态工具,包括请求参数构造、接口返回归一化、树节点/表单模型转换、枚举选项、时间/数值格式化、图表坐标和导出数据拼装等。 - 页面级类型优先复用 `api/**/interface/`;只服务页面内部组件的 UI 类型可放在当前页面 `components/types.ts` 或 `utils/*.ts`,不要扩散到全局类型。 -- 页面级 contract 脚本可放在对应页面目录下,验证结构拆分后的关键业务约束;当常量或逻辑从 `index.vue` 移到 `utils/` 时,需要同步更新脚本扫描范围。 +- 页面级 contract 脚本统一放在对应页面目录下的 `contracts/`,用于验证结构拆分后的关键业务约束;不要继续把 `check-*.mjs` 散放在页面根目录。脚本移动到 `contracts/` 后,需要同步修正 `index.vue`、`components/`、`utils/`、`api/**/interface/` 等扫描路径。 - 复杂页面拆分优先顺序:先抽纯数据转换和常量,再抽独立展示块,最后再考虑组合式函数;不要在一次需求中顺手改变视觉、接口字段、交互流程或持久化时机。 - `waveform` 趋势图规则仍以本文件后续“趋势图”章节为准,图表坐标、线宽、多图对齐等计算逻辑拆分后也必须保留对应 contract 检查。 diff --git a/BOOT-INF/classes/application.yml b/BOOT-INF/classes/application.yml new file mode 100644 index 0000000..d604bb2 --- /dev/null +++ b/BOOT-INF/classes/application.yml @@ -0,0 +1,125 @@ +server: + port: 18092 +spring: + application: + name: entrance + datasource: + druid: + driver-class-name: com.mysql.cj.jdbc.Driver +# url: jdbc:mysql://192.168.1.24:13306/pqs91002?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true +# username: root +# password: njcnpqs + url: jdbc:mysql://192.168.1.24:13306/pqs91002?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true + username: root + password: njcnpqs + #初始化建立物理连接的个数、最小、最大连接数 + initial-size: 5 + min-idle: 5 + max-active: 50 + #获取连接最大等待时间,单位毫秒 + max-wait: 60000 + #链接保持空间而不被驱逐的最长时间,单位毫秒 + min-evictable-idle-time-millis: 300000 + validation-query: select 1 + test-while-idle: true + test-on-borrow: false + test-on-return: false + pool-prepared-statements: true + max-pool-prepared-statement-per-connection-size: 20 + +#mybatis配置信息 +mybatis-plus: + mapper-locations: classpath*:com/njcn/**/mapping/*.xml + #别名扫描 + type-aliases-package: com.njcn.gather.system.dictionary.pojo.po,com.njcn.gather.machine.pojo.po + configuration: + #驼峰命名 + map-underscore-to-camel-case: true + #配置sql日志输出 + # log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + #关闭日志输出 + log-impl: org.apache.ibatis.logging.nologging.NoLoggingImpl + global-config: + db-config: + #指定主键生成策略 + id-type: assign_uuid + + +socket: + source: + ip: 127.0.0.1 + port: 62000 + device: + ip: 127.0.0.1 + port: 61000 +# source: +# ip: 192.168.1.121 +# port: 10086 +# device: +# ip: 192.168.1.121 +# port: 61000 + +webSocket: + port: 7777 + +#源参数下发,暂态数据默认值 +Dip: + #暂态前时间(s) + fPreTime: 2f + #写入时间(s) + fRampIn: 0.001f + #写出时间(s) + fRampOut: 0.001f + #暂态后时间(s) + fAfterTime: 3f + + +Flicker: + waveFluType: CPM + waveType: SQU + fDutyCycle: 50f + +log: + homeDir: D:\logs + commonLevel: info +report: + template: D:\template + reportDir: D:\report + dateFormat: yyyy年MM月dd日 +data: + homeDir: D:\data +qr: + cloud: http://pqmcc.com:18082/api/file + dev: + name: njcn + password: Pqs@12345678 + port: 21 + path: /etc/qrc.bin + gcDev: + name: root + password: Pqs@12345678 + port: 21 + path: /emmc/qrc.bin + +db: + type: mysql + + +# 比对录波需要的配置,晚点再做优化 +# 系统配置 +power-quality: + # 文件读取配置 + reading: + encoding: GBK # 文件编码(支持中文) + + # 计算参数 + calculation: + sampling: + default-rate: 256 # 默认采样率(每周波采样点数) + harmonic-times: 50 # 谐波次数 + ib-add: false # 电流基波叠加标志 + uharm-add: false # 电压谐波叠加标志 +# 激活配置 +activate: + private-key: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcUyYhVqczGxblL+o/xZzF/8nf+LjrfUE/dS1aRHM7uMDD0cgCArhjtfneFePrMxt+Z7W8yNBzSarub8qsfhaVNikV7Es7oaeTygfjQXTi2n4AFkir3fM07J08RpWhl5M8f8uWTCuvFUYAw00gq55typqmnbkmJa2VIUy/iQf+cMCP7abz4/jNhUzUR3qA7TV4oMRgTdIEDUp63YF8dOC+JH8XxYrCVeHXV6fLCwmesdMzl0lB2VTEKMfLbXhOmF5g7P9y/16VCcN8UBuZlbyYfn+GAxJOSbeHi5HshOKfoSuD7Jz+3WQZpNavOWjIFExKIU38/CvnJCOP7XBCqpSTAgMBAAECggEAYeWokWRE3TpvwiOZnUpR/aVMdVi75a3ROL5XIpqPV61B+t/bU3cEpl0GF9C5pUeiRi0IoStZb3mI9D1KPW/REKyUWkhabQO1gFYbTnRlkNOn6MILzKX4cwJjDaZeeo4EBPU7N+qHyOOXrU6hdH5FfxhMdV983ajm5eeuupxER1C2kAcIklTeVpTX6EKOgZb5LBp5ssOVm2P42pOauvcRozRcvZmqnErXmukv0H4l3EVNt4rHpTn9riHUC63e8JfiYzVaF6zuNUxv6nHEft0/SRMw11XSTnNfDzcKqgjz6ksFBS/6eQQYKESk+ONC53HUuYHFAknkwsPupDCT2W8FIQKBgQDLHT/xCU3nxGr4vFKBDNaO2D5oK20ECbBO4oDvLWWmQG7f+6TsMy8PgVdMnoL4RfqGlwFAKEpS6KVFHnBVqnNEhcdy9uCI7x7Xx8UnyUtxj1EDTm76uta9Ki9OrlqB6tImDM9+Ya3vGktW37ht4WOx2OsJRhG1dbf6RLwFlH7DWwKBgQDFBxvi5I1BR6hg6Tj7xd2SqOT2Y+BED3xuSYENhWbmMhLJDResaB7mjztbxlYaY2mOE0holWm2uDmVFFhMh4jYXik4hYH8nmDzq9mDpZCZ9pyjYqnAP8THoAa8EbgrUWB8A6BPH4iL3KbMnBfBKY0pIr2xrvnjQjNBAgta7KDRKQKBgCe6oe4wxrdF2TKsC2tIqpMoQxS3Icy/ZGgZr+SYuaBKTCWtoDW/UT40K3JGMxIDBhzbXphBCUCsVt9tM8Xd4EwP6tJW7dZ7B0pnve2pVwNwaAVAiz6p2yUHIle+jN+Koe5lZRSwYIg7WW81tWpwwsJfzqFyvjYDP6hJV4mz4ROvAoGAaRcdnKvjXApomShMqJ4lTPChD3q+SA8qg3jZSOj6tZXHx00gb2kp8jg7pPvpOTIFPy6x1Ha9aCRjMk0ju84fA6lVuzwa1S907wOehUVuF3Eeo1cgy9Y3k3KbpPyeixxgpkUY4JslLdSHc2NemD0dee951qhJyRmqVOZOQDUuoeECgYEAqBw2cAFk3vM97WY06TSldGA8ajVHx3BYRjj+zl62NTQthy8fw3tqxb3c5e8toOmZWKjZvDhg2TRLhsDDQWEYg3LZG87REqVIjgEPcpjNLidjygGX8n3JF2o0O5I/EMvl0s/+LVQONfduOBvhwDqr8QNisbLsyneiAq7umewMolo=" + public-key: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnFMmIVanMxsW5S/qP8Wcxf/J3/i4631BP3UtWkRzO7jAw9HIAgK4Y7X53hXj6zMbfme1vMjQc0mq7m/KrH4WlTYpFexLO6Gnk8oH40F04tp+ABZIq93zNOydPEaVoZeTPH/LlkwrrxVGAMNNIKuebcqapp25JiWtlSFMv4kH/nDAj+2m8+P4zYVM1Ed6gO01eKDEYE3SBA1Ket2BfHTgviR/F8WKwlXh11enywsJnrHTM5dJQdlUxCjHy214TpheYOz/cv9elQnDfFAbmZW8mH5/hgMSTkm3h4uR7ITin6Erg+yc/t1kGaTWrzloyBRMSiFN/Pwr5yQjj+1wQqqUkwIDAQAB" diff --git a/frontend/src/api/event/eventList/index.ts b/frontend/src/api/event/eventList/index.ts index 5ea061d..99c2e42 100644 --- a/frontend/src/api/event/eventList/index.ts +++ b/frontend/src/api/event/eventList/index.ts @@ -1,5 +1,6 @@ import http from '@/api' import type { EventList } from './interface' +import type { Waveform } from '@/api/tools/waveform/interface' export const getTransientEventPage = (params: EventList.TransientPageParams) => { return http.post>('/event/list/transient/page', params) @@ -9,10 +10,14 @@ export const getTransientEventDetail = (eventId: string) => { return http.get(`/event/list/transient/${eventId}`) } +export const getTransientEventWave = (eventId: string) => { + return http.get(`/event/list/transient/${eventId}/wave`) +} + export const exportTransientEvents = (params: EventList.TransientPageParams) => { return http.downloadWithHeaders('/event/list/transient/export', params) } export const exportTransientWaveforms = (params: EventList.TransientWaveformExportParams) => { - return http.downloadWithHeaders('/event/list/transient/waveform/export', params) + return http.downloadWithHeaders('/event/list/transient/wave/export', params) } diff --git a/frontend/src/api/event/eventList/interface/index.ts b/frontend/src/api/event/eventList/interface/index.ts index 6477313..6fab85e 100644 --- a/frontend/src/api/event/eventList/interface/index.ts +++ b/frontend/src/api/event/eventList/interface/index.ts @@ -14,11 +14,13 @@ export namespace EventList { startTimeEnd?: string eventType?: string phase?: string - eventDescribe?: string + event_describe?: string durationMin?: number durationMax?: number featureAmplitudeMin?: number featureAmplitudeMax?: number + severityMin?: number + severityMax?: number fileFlag?: number dealFlag?: number lineIds?: string[] @@ -38,19 +40,15 @@ export namespace EventList { eventType?: string eventTypeName?: string equipmentName?: string + mac?: string engineeringName?: string projectName?: string startTime?: string lineName?: string event_describe?: string - eventDescribe?: string - eventDescription?: string - eventDesc?: string - description?: string - describe?: string - remark?: string sagsource?: string phase?: string + severity?: number duration?: number featureAmplitude?: number wavePath?: string diff --git a/frontend/src/api/steady/steadyDataView/interface/index.ts b/frontend/src/api/steady/steadyDataView/interface/index.ts index 68f1d60..a0bb8c0 100644 --- a/frontend/src/api/steady/steadyDataView/interface/index.ts +++ b/frontend/src/api/steady/steadyDataView/interface/index.ts @@ -40,7 +40,6 @@ export namespace SteadyDataView { lineIds: string[] indicatorCodes: string[] statTypes: SteadyTrendStatType[] - phases: string[] timeStart: string timeEnd: string bucket?: string diff --git a/frontend/src/views/event/eventList/check-search-layout-contract.mjs b/frontend/src/views/event/eventList/check-search-layout-contract.mjs deleted file mode 100644 index 4dbc01a..0000000 --- a/frontend/src/views/event/eventList/check-search-layout-contract.mjs +++ /dev/null @@ -1,42 +0,0 @@ -/* 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 pageFile = path.join(currentDir, 'index.vue') -const source = fs.readFileSync(pageFile, 'utf8') - -const expectations = [ - ['search grid keeps five fields on wide event list screens', /:search-col="\{\s*xs:\s*1,\s*sm:\s*2,\s*md:\s*2,\s*lg:\s*5,\s*xl:\s*5\s*\}"/], - ['event time table column keeps occurrence time label', /prop:\s*'startTime',\s*label:\s*'发生时刻'/], - ['event time search label is shortened to time', /search:\s*\{\s*label:\s*'时间',\s*key:\s*'startTimeRange'/], - ['event time search field only takes one grid column', /key:\s*'startTimeRange',\s*span:\s*1,/], - ['event time search imports shared TimePeriodSearch component', /import TimePeriodSearch from '@\/views\/components\/TimePeriodSearch\/index\.vue'/], - ['event time search imports shared period helpers', /from '@\/views\/components\/TimePeriodSearch\/timePeriod'/], - ['event time search renders shared TimePeriodSearch', /h\(TimePeriodSearch,[\s\S]*unit:\s*eventTimeUnit\.value,[\s\S]*modelValue:\s*eventTimeBaseDate\.value/], - [ - 'event list requests preserve explicit time range and fallback to current visible range', - /const resolveCurrentSearchParams[\s\S]*startTimeRange:\s*params\.startTimeRange\s*\?\?\s*buildTimePeriodRange\(eventTimeUnit\.value,\s*eventTimeBaseDate\.value\)[\s\S]*getTransientEventPage\(buildEventQueryParams\(resolveCurrentSearchParams\(params\)\)\)/ - ], - [ - 'event export button is named explicitly', - /事件导出<\/el-button>/ - ], - [ - 'event export uses the same current search params as the table request', - /exportTransientEvents,[\s\S]*buildEventQueryParams\(resolveCurrentSearchParams\(searchParam\)\)/ - ] -] - -const failures = expectations.filter(([, pattern]) => !pattern.test(source)) - -if (failures.length) { - console.error('eventList search layout contract check failed:') - for (const [name] of failures) { - console.error(`- ${name}`) - } - process.exit(1) -} - -console.log('eventList search layout contract check passed') diff --git a/frontend/src/views/event/eventList/check-visible-contract.mjs b/frontend/src/views/event/eventList/check-visible-contract.mjs deleted file mode 100644 index 879368f..0000000 --- a/frontend/src/views/event/eventList/check-visible-contract.mjs +++ /dev/null @@ -1,61 +0,0 @@ -/* 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 pageFile = path.join(currentDir, 'index.vue') -const queryFile = path.join(currentDir, 'utils', 'queryParams.ts') -const source = fs.readFileSync(pageFile, 'utf8') -const querySource = fs.readFileSync(queryFile, 'utf8') - -const expectations = [ - [ - 'waveform selection column is rendered near the left side', - /prop:\s*'waveformSelection'[\s\S]*label:\s*'波形选择'[\s\S]*headerRender:\s*renderWaveformSelectionHeader[\s\S]*render:\s*renderWaveformSelectionCell/ - ], - [ - 'waveform selection requires event id, waveform flag and waveform path', - /const isWaveformExportable[\s\S]*Boolean\(row\.eventId\)[\s\S]*Number\(row\.fileFlag\)\s*===\s*1[\s\S]*Boolean\(row\.wavePath\)/ - ], - [ - 'table keeps requested visible event column order', - /label:\s*'发生时刻'[\s\S]*label:\s*'监测点名称'[\s\S]*label:\s*'暂降\/暂升幅值\(%\)'[\s\S]*label:\s*'持续时间\(s\)'[\s\S]*label:\s*'事件类型'[\s\S]*label:\s*'相别'/ - ], - [ - 'event type column displays name but searches by eventType code', - /prop:\s*'eventTypeName'[\s\S]*label:\s*'事件类型'[\s\S]*enum:\s*eventTypeOptions[\s\S]*fieldNames:\s*\{\s*label:\s*'name',\s*value:\s*'code'\s*\}[\s\S]*isFilterEnum:\s*false[\s\S]*search:\s*\{[\s\S]*key:\s*'eventType'[\s\S]*el:\s*'select'/ - ], - [ - 'event description defaults hidden in table columns', - /prop:\s*'event_describe'[\s\S]*label:\s*'事件描述'[\s\S]*isShow:\s*false/ - ], - ['event location defaults hidden in table columns', /prop:\s*'sagsource'[\s\S]*label:\s*'事件发生位置'[\s\S]*isShow:\s*false/], - ['waveform status defaults hidden in table columns', /prop:\s*'fileFlag'[\s\S]*label:\s*'波形文件状态'[\s\S]*isShow:\s*false/], - ['monitor point is rendered as a clickable link', /prop:\s*'lineName'[\s\S]*type:\s*'primary'[\s\S]*link:\s*true[\s\S]*handleViewMeasurementPoint\(row\)/], - ['measurement point dialog is present', /measurementPointDialogVisible[\s\S]*title="监测点信息"/], - ['operation switches between view waveform and supplement waveform', /Number\(row\.fileFlag\)\s*===\s*1[\s\S]*查看波形[\s\S]*波形补招/], - ['waveform status search uses custom render instead of select', /renderFileFlagSearch[\s\S]*prop:\s*'fileFlag'[\s\S]*search:\s*\{[\s\S]*render:\s*renderFileFlagSearch/], - ['phase column is not a search field', /prop:\s*'phase'[\s\S]*search:[\s\S]*prop:\s*'event_describe'/.test(source) === false], - [ - 'event description is not a search field', - /prop:\s*'event_describe'[\s\S]*label:\s*'事件描述'[\s\S]*search:[\s\S]*prop:\s*'sagsource'/.test(source) === false - ], - ['ledger names are searched through one keyword field', /ledgerKeyword[\s\S]*label:\s*'台账关键字'/], - ['query params fan out ledger keyword to ledger name fields', /ledgerKeyword[\s\S]*engineeringName[\s\S]*projectName[\s\S]*equipmentName[\s\S]*lineName/.test(querySource)] -] - -const failures = expectations.filter(([, pattern]) => { - if (typeof pattern === 'boolean') return !pattern - return !pattern.test(source) -}) - -if (failures.length) { - console.error('eventList visible contract check failed:') - for (const [name] of failures) { - console.error(`- ${name}`) - } - process.exit(1) -} - -console.log('eventList visible contract check passed') diff --git a/frontend/src/views/event/eventList/components/EventListTable.vue b/frontend/src/views/event/eventList/components/EventListTable.vue new file mode 100644 index 0000000..8924a9d --- /dev/null +++ b/frontend/src/views/event/eventList/components/EventListTable.vue @@ -0,0 +1,469 @@ + + + + + diff --git a/frontend/src/views/event/eventList/components/MeasurementPointDialog.vue b/frontend/src/views/event/eventList/components/MeasurementPointDialog.vue new file mode 100644 index 0000000..949c400 --- /dev/null +++ b/frontend/src/views/event/eventList/components/MeasurementPointDialog.vue @@ -0,0 +1,46 @@ + + + diff --git a/frontend/src/views/event/eventList/components/VoltageToleranceDialog.vue b/frontend/src/views/event/eventList/components/VoltageToleranceDialog.vue new file mode 100644 index 0000000..8865a30 --- /dev/null +++ b/frontend/src/views/event/eventList/components/VoltageToleranceDialog.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/frontend/src/views/event/eventList/check-display-contract.mjs b/frontend/src/views/event/eventList/contracts/check-display-contract.mjs similarity index 80% rename from frontend/src/views/event/eventList/check-display-contract.mjs rename to frontend/src/views/event/eventList/contracts/check-display-contract.mjs index ba6926f..a8533d3 100644 --- a/frontend/src/views/event/eventList/check-display-contract.mjs +++ b/frontend/src/views/event/eventList/contracts/check-display-contract.mjs @@ -23,16 +23,22 @@ fs.mkdirSync(tempDir, { recursive: true }) const tempModulePath = path.join(tempDir, 'display.mjs') fs.writeFileSync(tempModulePath, transpiled, 'utf8') -const { resolveEventDescription, resolveEventTypeName } = await import(pathToFileURL(tempModulePath).href) +const { resolveEventDescription, resolveEventSeverity, resolveEventTypeName } = await import(pathToFileURL(tempModulePath).href) assert.equal(resolveEventDescription({ event_describe: '电压暂降' }), '电压暂降') assert.equal(resolveEventDescription({ event_describe: '' }), '--') -assert.equal(resolveEventDescription({ eventDescribe: '驼峰描述' }), '--') -assert.equal(resolveEventDescription({ eventDescription: '描述字段' }), '--') assert.equal(resolveEventDescription({ eventType: 'VOLTAGE_SAG' }), '--') assert.equal(resolveEventDescription({}), '--') assert.equal(resolveEventDescription(null), '--') +assert.equal(resolveEventSeverity({ severity: 72.5 }), '72.5') +assert.equal(resolveEventSeverity({ severity: 0 }), '0') +assert.equal(resolveEventSeverity({ severity: -1 }), '-') +assert.equal(resolveEventSeverity({ severity: '-2' }), '-') +assert.equal(resolveEventSeverity({ severity: '' }), '-') +assert.equal(resolveEventSeverity({}), '-') +assert.equal(resolveEventSeverity(null), '-') + const eventTypeOptions = [ { id: 'c5ce588cb76fba90c4510000000000000', name: '电压暂降', code: 'VOLTAGE_SAG' }, { id: 'a26e588cb76fba90c4510000000000000', name: '电压暂升', code: 'VOLTAGE_SWELL', value: 'SWELL' } diff --git a/frontend/src/views/event/eventList/check-export-contract.mjs b/frontend/src/views/event/eventList/contracts/check-export-contract.mjs similarity index 64% rename from frontend/src/views/event/eventList/check-export-contract.mjs rename to frontend/src/views/event/eventList/contracts/check-export-contract.mjs index 94cb995..eefacbf 100644 --- a/frontend/src/views/event/eventList/check-export-contract.mjs +++ b/frontend/src/views/event/eventList/contracts/check-export-contract.mjs @@ -4,29 +4,39 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const currentDir = path.dirname(fileURLToPath(import.meta.url)) -const pageFile = path.join(currentDir, 'index.vue') -const apiFile = path.resolve(currentDir, '../../../api/event/eventList/index.ts') -const interfaceFile = path.resolve(currentDir, '../../../api/event/eventList/interface/index.ts') -const source = fs.readFileSync(pageFile, 'utf8') +const pageFile = path.join(currentDir, '..', 'index.vue') +const componentDir = path.join(currentDir, '..', 'components') +const apiFile = path.resolve(currentDir, '../../../../api/event/eventList/index.ts') +const interfaceFile = path.resolve(currentDir, '../../../../api/event/eventList/interface/index.ts') + +const pageSource = fs.readFileSync(pageFile, 'utf8') +const componentSource = fs.existsSync(componentDir) + ? fs + .readdirSync(componentDir) + .filter(file => file.endsWith('.vue')) + .map(file => fs.readFileSync(path.join(componentDir, file), 'utf8')) + .join('\n') + : '' +const source = `${pageSource}\n${componentSource}` const apiSource = fs.readFileSync(apiFile, 'utf8') const interfaceSource = fs.readFileSync(interfaceFile, 'utf8') const eventExportButtonBlocks = (source.match(//g) || []).filter( - block => /@click="handleEventExport"/.test(block) && />事件导出<\/el-button>/.test(block) + block => /@click="handleEventExport"/.test(block) ) const expectations = [ ['waveform export api is imported by the page', /import \{[\s\S]*exportTransientWaveforms[\s\S]*\} from '@\/api\/event\/eventList'/], - ['waveform export button is present and disabled without selection', /波形导出<\/el-button>/], - ['event export button is named explicitly', /事件导出<\/el-button>/], + ['waveform export button is present and disabled without selection', //], + ['event export button is wired explicitly', //], ['event export button does not depend on waveform selection', eventExportButtonBlocks.some(block => !/selectedWaveformRows/.test(block))], ['waveform selected rows are tracked independently', /const selectedWaveformRows\s*=\s*ref\(\[\]\)/], - ['waveform export payload uses event ids only', /eventIds:\s*exportableRows\.map\(row => row\.eventId\)/], - ['waveform export uses server filename download hook', /useDownloadWithServerFileName\(exportTransientWaveforms,\s*'事件波形导出'/], + ['waveform export payload uses event ids only', /eventIds:\s*rows\.map\(row => row\.eventId\)/], + ['waveform export uses server filename download hook', /useDownloadWithServerFileName\(exportTransientWaveforms/], ['waveform selection clear helper exists', /const clearWaveformSelection\s*=\s*\(\)\s*=>\s*\{[\s\S]*selectedWaveformRows\.value\s*=\s*\[\][\s\S]*\}/], - ['table request clears waveform selection before reload', /const getTableList\s*=\s*\([^)]*\)\s*=>\s*\{[\s\S]*clearWaveformSelection\(\)[\s\S]*return getTransientEventPage/], - ['search reset clears waveform selection', /const handleSearchReset\s*=\s*\(\)\s*=>\s*\{[\s\S]*clearWaveformSelection\(\)[\s\S]*syncEventTimeRange\(\)/], + ['table request clears waveform selection before reload', /const getTableList\s*=\s*\([^)]*\)\s*=>\s*\{[\s\S]*clearWaveformSelection\(\)[\s\S]*props\.requestApi/], + ['search reset clears waveform selection', /const handleSearchReset\s*=\s*\(\)\s*=>\s*\{[\s\S]*clearWaveformSelection\(\)[\s\S]*commitEventTimeRange\(\{ shouldSearch: true \}\)/], ['waveform export api method exists', /export const exportTransientWaveforms\s*=\s*\(params:\s*EventList\.TransientWaveformExportParams\)/.test(apiSource)], - ['waveform export api path is stable', /downloadWithHeaders\('\/event\/list\/transient\/waveform\/export',\s*params\)/.test(apiSource)], + ['waveform export api path follows API_DEBUG.md', /downloadWithHeaders\('\/event\/list\/transient\/wave\/export',\s*params\)/.test(apiSource)], ['waveform export params type contains eventIds', /export interface TransientWaveformExportParams\s*\{[\s\S]*eventIds:\s*string\[\][\s\S]*\}/.test(interfaceSource)] ] diff --git a/frontend/src/views/event/eventList/check-query-params-contract.mjs b/frontend/src/views/event/eventList/contracts/check-query-params-contract.mjs similarity index 62% rename from frontend/src/views/event/eventList/check-query-params-contract.mjs rename to frontend/src/views/event/eventList/contracts/check-query-params-contract.mjs index 23c89ab..da5b8b8 100644 --- a/frontend/src/views/event/eventList/check-query-params-contract.mjs +++ b/frontend/src/views/event/eventList/contracts/check-query-params-contract.mjs @@ -28,10 +28,26 @@ const { buildEventQueryParams } = await import(pathToFileURL(tempModulePath).hre const params = buildEventQueryParams({ pageNum: 2, pageSize: 20, - eventType: 'VOLTAGE_SAG' + eventType: 'VOLTAGE_SAG', + startTimeRange: ['2025-12-01 00:00:00.000', '2025-12-31 23:59:59.999'], + featureAmplitudeMin: '10', + featureAmplitudeMax: '90', + durationMin: '0.02', + durationMax: '10', + severityMin: '1', + severityMax: '5' }) assert.equal(params.eventType, 'VOLTAGE_SAG') +assert.equal(params.startTimeStart, '2025-12-01 00:00:00') +assert.equal(params.startTimeEnd, '2025-12-31 23:59:59') +assert.equal(params.featureAmplitudeMin, 10) +assert.equal(params.featureAmplitudeMax, 90) +assert.equal(params.durationMin, 0.02) +assert.equal(params.durationMax, 10) +assert.equal(params.severityMin, 1) +assert.equal(params.severityMax, 5) assert.equal(Object.hasOwn(params, 'eventTypeCode'), false) +assert.equal(source.includes(['event', 'Describe'].join('')), false) console.log('eventList query params contract passed') diff --git a/frontend/src/views/event/eventList/check-route-contract.mjs b/frontend/src/views/event/eventList/contracts/check-route-contract.mjs similarity index 86% rename from frontend/src/views/event/eventList/check-route-contract.mjs rename to frontend/src/views/event/eventList/contracts/check-route-contract.mjs index 9b61b85..116b828 100644 --- a/frontend/src/views/event/eventList/check-route-contract.mjs +++ b/frontend/src/views/event/eventList/contracts/check-route-contract.mjs @@ -4,11 +4,11 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const currentDir = path.dirname(fileURLToPath(import.meta.url)) -const routersDir = path.join(currentDir, '..', '..', '..', 'routers', 'modules') +const routersDir = path.join(currentDir, '..', '..', '..', '..', 'routers', 'modules') const staticRouterFile = path.join(routersDir, 'staticRouter.ts') const dynamicRouterFile = path.join(routersDir, 'dynamicRouter.ts') -const authStoreFile = path.join(currentDir, '..', '..', '..', 'stores', 'modules', 'auth.ts') -const subMenuFile = path.join(currentDir, '..', '..', '..', 'layouts', 'components', 'Menu', 'SubMenu.vue') +const authStoreFile = path.join(currentDir, '..', '..', '..', '..', 'stores', 'modules', 'auth.ts') +const subMenuFile = path.join(currentDir, '..', '..', '..', '..', 'layouts', 'components', 'Menu', 'SubMenu.vue') const staticRouterSource = fs.readFileSync(staticRouterFile, 'utf8') const dynamicRouterSource = fs.readFileSync(dynamicRouterFile, 'utf8') diff --git a/frontend/src/views/event/eventList/contracts/check-search-layout-contract.mjs b/frontend/src/views/event/eventList/contracts/check-search-layout-contract.mjs new file mode 100644 index 0000000..3db24a8 --- /dev/null +++ b/frontend/src/views/event/eventList/contracts/check-search-layout-contract.mjs @@ -0,0 +1,78 @@ +/* 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 pageFile = path.join(currentDir, '..', 'index.vue') +const componentFile = path.join(currentDir, '..', 'components', 'EventListTable.vue') +const pageSource = fs.readFileSync(pageFile, 'utf8') +const componentSource = fs.existsSync(componentFile) ? fs.readFileSync(componentFile, 'utf8') : '' +const source = `${pageSource}\n${componentSource}` + +const expectations = [ + ['page renders extracted event list table component', // + ], + [ + 'event export uses the same current search params as the table request', + /const handleEventExport[\s\S]*resolveCurrentSearchParams\(searchParam\)/ + ], + [ + 'number range search keeps min and max inputs on one line', + /event-number-range-search[\s\S]*flex-wrap:\s*nowrap;/ + ], + [ + 'number range inputs pass compact width through render props', + /const numberRangeInputStyle[\s\S]*flex:\s*'0 0 72px'[\s\S]*width:\s*'72px'[\s\S]*h\(ElInputNumber,\s*\{[\s\S]*style:\s*numberRangeInputStyle[\s\S]*h\(ElInputNumber,\s*\{[\s\S]*style:\s*numberRangeInputStyle/ + ], + [ + 'number range separator keeps three pixel side margin', + /const numberRangeSeparatorStyle[\s\S]*flex:\s*'0 0 auto'[\s\S]*margin:\s*'0 3px'[\s\S]*class:\s*'event-number-range-separator',\s*style:\s*numberRangeSeparatorStyle/ + ] +] + +const failures = expectations.filter(([, pattern]) => { + if (typeof pattern === 'boolean') return !pattern + return !pattern.test(source) +}) + +if (failures.length) { + console.error('eventList search layout contract check failed:') + for (const [name] of failures) { + console.error(`- ${name}`) + } + process.exit(1) +} + +console.log('eventList search layout contract check passed') diff --git a/frontend/src/views/event/eventList/check-time-range-contract.mjs b/frontend/src/views/event/eventList/contracts/check-time-range-contract.mjs similarity index 99% rename from frontend/src/views/event/eventList/check-time-range-contract.mjs rename to frontend/src/views/event/eventList/contracts/check-time-range-contract.mjs index 0daeef6..c7724ea 100644 --- a/frontend/src/views/event/eventList/check-time-range-contract.mjs +++ b/frontend/src/views/event/eventList/contracts/check-time-range-contract.mjs @@ -5,7 +5,7 @@ import { pathToFileURL } from 'node:url' import ts from 'typescript' const sharedModulePath = path.resolve('src/views/components/TimePeriodSearch/timePeriod.ts') -const eventModulePath = path.resolve('src/views/event/eventList/eventTimeRange.ts') +const eventModulePath = path.resolve('src/views/event/eventList/utils/eventTimeRange.ts') if (!fs.existsSync(sharedModulePath)) { throw new Error('TimePeriodSearch/timePeriod.ts must provide shared time range helpers') diff --git a/frontend/src/views/event/eventList/contracts/check-visible-contract.mjs b/frontend/src/views/event/eventList/contracts/check-visible-contract.mjs new file mode 100644 index 0000000..61da020 --- /dev/null +++ b/frontend/src/views/event/eventList/contracts/check-visible-contract.mjs @@ -0,0 +1,85 @@ +/* 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 pageFile = path.join(currentDir, '..', 'index.vue') +const componentDir = path.join(currentDir, '..', 'components') +const queryFile = path.join(currentDir, '..', 'utils', 'queryParams.ts') +const interfaceFile = path.resolve(currentDir, '../../../../api/event/eventList/interface/index.ts') + +const read = file => fs.readFileSync(file, 'utf8') +const pageSource = read(pageFile) +const componentSource = fs.existsSync(componentDir) + ? fs + .readdirSync(componentDir) + .filter(file => file.endsWith('.vue')) + .map(file => read(path.join(componentDir, file))) + .join('\n') + : '' +const source = `${pageSource}\n${componentSource}` +const querySource = read(queryFile) +const interfaceSource = read(interfaceFile) +const measurementPointItemsBlock = source.match(/const measurementPointItems[\s\S]*?\n\]/)?.[0] || '' + +const expectations = [ + ['page imports extracted event list table component', /EventListTable/], + ['page imports extracted measurement point dialog component', /MeasurementPointDialog/], + ['event list table component exists', fs.existsSync(path.join(componentDir, 'EventListTable.vue'))], + ['measurement point dialog component exists', fs.existsSync(path.join(componentDir, 'MeasurementPointDialog.vue'))], + [ + 'waveform selection column is rendered near the left side', + /prop:\s*'waveformSelection'[\s\S]*fixed:\s*'left'[\s\S]*headerRender:\s*renderWaveformSelectionHeader[\s\S]*render:\s*renderWaveformSelectionCell/ + ], + [ + 'waveform selection requires event id, waveform flag and waveform path', + /const isWaveformExportable[\s\S]*Boolean\(row\.eventId\)[\s\S]*Number\(row\.fileFlag\)\s*===\s*1[\s\S]*Boolean\(row\.wavePath\)/ + ], + [ + 'table keeps requested visible event column order', + /prop:\s*'startTime'[\s\S]*prop:\s*'lineName'[\s\S]*prop:\s*'featureAmplitude'[\s\S]*prop:\s*'duration'[\s\S]*prop:\s*'eventTypeName'[\s\S]*prop:\s*'severity'[\s\S]*prop:\s*'phase'/ + ], + ['severity column renders through severity resolver', /prop:\s*'severity'[\s\S]*render:\s*\(\{ row \}\)\s*=>\s*resolveEventSeverity\(row\)/], + [ + 'event type column displays name but searches by eventType code', + /prop:\s*'eventTypeName'[\s\S]*enum:\s*props\.eventTypeOptions[\s\S]*fieldNames:\s*\{\s*label:\s*'name',\s*value:\s*'code'\s*\}[\s\S]*isFilterEnum:\s*false[\s\S]*search:\s*\{[\s\S]*key:\s*'eventType'[\s\S]*el:\s*'select'/ + ], + ['event description defaults hidden in table columns', /prop:\s*'event_describe'[\s\S]*isShow:\s*false/], + ['event location defaults hidden in table columns', /prop:\s*'sagsource'[\s\S]*isShow:\s*false/], + ['waveform status defaults hidden in table columns', /prop:\s*'fileFlag'[\s\S]*isShow:\s*false/], + ['monitor point is rendered as a clickable link', /prop:\s*'lineName'[\s\S]*type:\s*'primary'[\s\S]*link:\s*true[\s\S]*viewMeasurementPoint/], + ['measurement point dialog is present', /\s*resolvePhaseText\(row\.phase\)/], + ['waveform status search uses custom render instead of select', /renderFileFlagSearch[\s\S]*prop:\s*'fileFlag'[\s\S]*search:\s*\{[\s\S]*render:\s*renderFileFlagSearch/], + ['phase column is not a search field', /prop:\s*'phase'[\s\S]*search:[\s\S]*prop:\s*'event_describe'/.test(source) === false], + [ + 'event description is not a search field', + /prop:\s*'event_describe'[\s\S]*search:[\s\S]*prop:\s*'sagsource'/.test(source) === false + ], + ['ledger names are searched through one keyword field', /prop:\s*'ledgerKeyword'[\s\S]*search:\s*\{[\s\S]*order:\s*3/], + ['query params fan out ledger keyword to ledger name fields', /ledgerKeyword[\s\S]*engineeringName[\s\S]*projectName[\s\S]*equipmentName[\s\S]*lineName/.test(querySource)] +] + +const failures = expectations.filter(([, pattern]) => { + if (typeof pattern === 'boolean') return !pattern + return !pattern.test(source) +}) + +if (failures.length) { + console.error('eventList visible contract check failed:') + for (const [name] of failures) { + console.error(`- ${name}`) + } + process.exit(1) +} + +console.log('eventList visible contract check passed') diff --git a/frontend/src/views/event/eventList/contracts/check-voltage-tolerance-contract.mjs b/frontend/src/views/event/eventList/contracts/check-voltage-tolerance-contract.mjs new file mode 100644 index 0000000..7d2e797 --- /dev/null +++ b/frontend/src/views/event/eventList/contracts/check-voltage-tolerance-contract.mjs @@ -0,0 +1,79 @@ +import assert from 'node:assert/strict' +import fs from 'node:fs' +import path from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import ts from 'typescript' + +const currentDir = path.dirname(fileURLToPath(import.meta.url)) +const utilsFile = path.resolve(currentDir, '../utils/voltageTolerance.ts') +const tableFile = path.resolve(currentDir, '../components/EventListTable.vue') +const dialogFile = path.resolve(currentDir, '../components/VoltageToleranceDialog.vue') +const pageFile = path.resolve(currentDir, '../index.vue') + +if (!fs.existsSync(utilsFile)) { + throw new Error('eventList voltage tolerance helpers must be extracted to utils/voltageTolerance.ts') +} + +if (!fs.existsSync(dialogFile)) { + throw new Error('eventList voltage tolerance dialog must be extracted to components/VoltageToleranceDialog.vue') +} + +const source = fs.readFileSync(utilsFile, 'utf8') +const transpiled = ts.transpileModule(source, { + compilerOptions: { + module: ts.ModuleKind.ES2020, + target: ts.ScriptTarget.ES2020 + } +}).outputText + +const tempDir = path.resolve('node_modules/.cache/event-list-contract') +fs.mkdirSync(tempDir, { recursive: true }) +const tempModulePath = path.join(tempDir, 'voltageTolerance.mjs') +fs.writeFileSync(tempModulePath, transpiled, 'utf8') + +const { + VOLTAGE_TOLERANCE_STANDARDS, + evaluateVoltageTolerance, + resolveVoltageTolerancePoint, + buildVoltageToleranceChartOption +} = await import(pathToFileURL(tempModulePath).href) + +assert.deepEqual( + VOLTAGE_TOLERANCE_STANDARDS.map(item => item.key), + ['itic', 'semiF47'] +) + +assert.deepEqual(resolveVoltageTolerancePoint({ duration: 0.5, featureAmplitude: 70 }), { + durationSeconds: 0.5, + voltagePercent: 70 +}) +assert.equal(resolveVoltageTolerancePoint({ duration: null, featureAmplitude: 70 }), null) +assert.equal(resolveVoltageTolerancePoint({ duration: 0.5, featureAmplitude: undefined }), null) + +assert.equal(evaluateVoltageTolerance('semiF47', { duration: 0.2, featureAmplitude: 50 })?.tolerable, true) +assert.equal(evaluateVoltageTolerance('semiF47', { duration: 0.5, featureAmplitude: 65 })?.tolerable, false) +assert.equal(evaluateVoltageTolerance('itic', { duration: 10, featureAmplitude: 80 })?.tolerable, true) +assert.equal(evaluateVoltageTolerance('itic', { duration: 10, featureAmplitude: 75 })?.tolerable, false) +assert.equal(evaluateVoltageTolerance('itic', { duration: 0, featureAmplitude: 75 }), null) + +const option = buildVoltageToleranceChartOption('itic', { duration: 10, featureAmplitude: 75 }) +assert.equal(option.xAxis.type, 'log') +assert.ok(option.series.some(item => item.name === '事件点')) +assert.ok(option.series.some(item => item.name.includes('ITIC'))) + +const tableSource = fs.readFileSync(tableFile, 'utf8') +const pageSource = fs.readFileSync(pageFile, 'utf8') +const dialogSource = fs.readFileSync(dialogFile, 'utf8') + +assert.match(tableSource, /ITIC\/SEMI F47/) +assert.match(tableSource, /viewVoltageTolerance/) +assert.match(pageSource, /VoltageToleranceDialog/) +assert.match(pageSource, /voltageToleranceDialogVisible/) +assert.match(dialogSource, /\s*\{[\s\S]*sessionStorage\.getItem\(EVENT_LIST_WAVEFORM_SESSION_KEY\)[\s\S]*applyWaveformParseResult/.test(waveformSource) + ], + [ + 'waveform page removes consumed event list waveform data', + /sessionStorage\.removeItem\(EVENT_LIST_WAVEFORM_SESSION_KEY\)/.test(waveformSource) + ] +] + +const failures = expectations.filter(([, pattern]) => { + if (typeof pattern === 'boolean') return !pattern + return !pattern.test(source) +}) + +if (failures.length) { + console.error('eventList waveform view contract check failed:') + for (const [name] of failures) { + console.error(`- ${name}`) + } + process.exit(1) +} + +console.log('eventList waveform view contract check passed') diff --git a/frontend/src/views/event/eventList/index.vue b/frontend/src/views/event/eventList/index.vue index 7626b74..a0e52a9 100644 --- a/frontend/src/views/event/eventList/index.vue +++ b/frontend/src/views/event/eventList/index.vue @@ -1,347 +1,70 @@ @@ -406,8 +135,4 @@ const handleWaveformExport = () => { .event-list-page :deep(.el-descriptions__cell) { word-break: break-all; } - -.event-list-page :deep(.event-file-flag-search) { - width: 100%; -} diff --git a/frontend/src/views/event/eventList/utils/detailItems.ts b/frontend/src/views/event/eventList/utils/detailItems.ts index d20742c..9e1f214 100644 --- a/frontend/src/views/event/eventList/utils/detailItems.ts +++ b/frontend/src/views/event/eventList/utils/detailItems.ts @@ -1,5 +1,5 @@ import type { EventList } from '@/api/event/eventList/interface' -import { formatEventOccurrenceTime } from '../eventTimeRange' +import { formatEventOccurrenceTime } from './eventTimeRange' import { resolveEventDescription } from './display' import { resolveDealFlagText, resolveFileFlagText } from './status' diff --git a/frontend/src/views/event/eventList/utils/display.ts b/frontend/src/views/event/eventList/utils/display.ts index e9c32c9..be5ce0f 100644 --- a/frontend/src/views/event/eventList/utils/display.ts +++ b/frontend/src/views/event/eventList/utils/display.ts @@ -22,6 +22,18 @@ export const resolveEventDescription = (row: EventRecordLike) => { return description || '--' } +export const resolveEventSeverity = (row: EventRecordLike) => { + if (!row) return '-' + + const severityText = resolveOptionalText((row as Record).severity) + if (!severityText) return '-' + + const severity = Number(severityText) + if (!Number.isFinite(severity) || severity < 0) return '-' + + return String(severity) +} + export const resolveEventTypeName = (row: EventRecordLike, eventTypeOptions: EventTypeOptionLike[] = []) => { if (!row) return '/' diff --git a/frontend/src/views/event/eventList/eventTimeRange.ts b/frontend/src/views/event/eventList/utils/eventTimeRange.ts similarity index 79% rename from frontend/src/views/event/eventList/eventTimeRange.ts rename to frontend/src/views/event/eventList/utils/eventTimeRange.ts index 60cb9af..64003e3 100644 --- a/frontend/src/views/event/eventList/eventTimeRange.ts +++ b/frontend/src/views/event/eventList/utils/eventTimeRange.ts @@ -4,7 +4,7 @@ export const formatEventOccurrenceTime = (value: unknown) => { const text = String(value).trim() if (!text) return '--' - // 发生时刻直接承载事件定位精度:小数秒按接口原始值展示,不补零、不裁剪。 + // 发生时刻直接承载接口返回精度:有毫秒就展示,没有毫秒不在前端合成。 const matched = text.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.(\d+))?$/) if (!matched) return text diff --git a/frontend/src/views/event/eventList/utils/queryParams.ts b/frontend/src/views/event/eventList/utils/queryParams.ts index ea9595f..62c77f8 100644 --- a/frontend/src/views/event/eventList/utils/queryParams.ts +++ b/frontend/src/views/event/eventList/utils/queryParams.ts @@ -11,6 +11,13 @@ const resolveOptionalText = (value: unknown) => { return text || undefined } +const resolveEventTimeText = (value: unknown) => { + const text = resolveOptionalText(value) + if (!text) return undefined + + return text.replace(/\.\d+$/, '') +} + const resolveOptionalNumber = (value: unknown) => { if (value === null || value === undefined || value === '') return undefined const parsed = Number(value) @@ -30,10 +37,16 @@ export const buildEventQueryParams = (params: EventSearchParams = {}) => { return pruneEmptyParams({ pageNum: params.pageNum, pageSize: params.pageSize, - startTimeStart: resolveOptionalText(timeRange[0]), - startTimeEnd: resolveOptionalText(timeRange[1]), + startTimeStart: resolveEventTimeText(timeRange[0]), + startTimeEnd: resolveEventTimeText(timeRange[1]), eventType: resolveOptionalText(params.eventType), phase: resolveOptionalText(params.phase), + durationMin: resolveOptionalNumber(params.durationMin), + durationMax: resolveOptionalNumber(params.durationMax), + featureAmplitudeMin: resolveOptionalNumber(params.featureAmplitudeMin), + featureAmplitudeMax: resolveOptionalNumber(params.featureAmplitudeMax), + severityMin: resolveOptionalNumber(params.severityMin), + severityMax: resolveOptionalNumber(params.severityMax), fileFlag: resolveOptionalNumber(params.fileFlag), dealFlag: resolveOptionalNumber(params.dealFlag), engineeringName: ledgerKeyword, diff --git a/frontend/src/views/event/eventList/utils/status.ts b/frontend/src/views/event/eventList/utils/status.ts index e94ece3..efca462 100644 --- a/frontend/src/views/event/eventList/utils/status.ts +++ b/frontend/src/views/event/eventList/utils/status.ts @@ -5,6 +5,12 @@ export const phaseOptions = [ { label: '三相', value: 'ABC' } ] +export function resolvePhaseText(value: unknown) { + const text = value === null || value === undefined ? '' : String(value).trim() + const option = phaseOptions.find(item => item.value === text) + return option?.label.replace(' ', '') || text || '--' +} + export const fileFlagOptions = [ { label: '未招', value: 0 }, { label: '已招', value: 1 } diff --git a/frontend/src/views/event/eventList/utils/voltageTolerance.ts b/frontend/src/views/event/eventList/utils/voltageTolerance.ts new file mode 100644 index 0000000..c69f00c --- /dev/null +++ b/frontend/src/views/event/eventList/utils/voltageTolerance.ts @@ -0,0 +1,221 @@ +export type VoltageToleranceStandardKey = 'itic' | 'semiF47' + +export interface VoltageTolerancePoint { + durationSeconds: number + voltagePercent: number +} + +export interface VoltageToleranceStandard { + key: VoltageToleranceStandardKey + label: string + boundaryName: string + boundaryPoints: VoltageTolerancePoint[] +} + +export interface VoltageToleranceEvaluation { + standard: VoltageToleranceStandard + point: VoltageTolerancePoint + thresholdVoltagePercent: number + tolerable: boolean + marginPercent: number + statusText: string +} + +type EventVoltageSource = { + duration?: unknown + featureAmplitude?: unknown +} + +const iticBoundaryPoints: VoltageTolerancePoint[] = [ + { durationSeconds: 0.001, voltagePercent: 0 }, + { durationSeconds: 0.02, voltagePercent: 0 }, + { durationSeconds: 0.5, voltagePercent: 70 }, + { durationSeconds: 10, voltagePercent: 80 }, + { durationSeconds: 100, voltagePercent: 90 } +] + +const semiF47BoundaryPoints: VoltageTolerancePoint[] = [ + { durationSeconds: 0.001, voltagePercent: 50 }, + { durationSeconds: 0.2, voltagePercent: 50 }, + { durationSeconds: 0.5, voltagePercent: 70 }, + { durationSeconds: 1, voltagePercent: 80 }, + { durationSeconds: 100, voltagePercent: 80 } +] + +export const VOLTAGE_TOLERANCE_STANDARDS: VoltageToleranceStandard[] = [ + { + key: 'itic', + label: 'ITIC', + boundaryName: 'ITIC 可容忍边界', + boundaryPoints: iticBoundaryPoints + }, + { + key: 'semiF47', + label: 'SEMI F47', + boundaryName: 'SEMI F47 可容忍边界', + boundaryPoints: semiF47BoundaryPoints + } +] + +export const resolveVoltageToleranceStandard = (standardKey: VoltageToleranceStandardKey) => { + return VOLTAGE_TOLERANCE_STANDARDS.find(item => item.key === standardKey) || VOLTAGE_TOLERANCE_STANDARDS[0] +} + +const resolveFiniteNumber = (value: unknown) => { + if (value === null || value === undefined || value === '') return undefined + + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined +} + +export const resolveVoltageTolerancePoint = (event: EventVoltageSource): VoltageTolerancePoint | null => { + const durationSeconds = resolveFiniteNumber(event.duration) + const voltagePercent = resolveFiniteNumber(event.featureAmplitude) + + if (durationSeconds === undefined || voltagePercent === undefined) return null + if (durationSeconds <= 0 || voltagePercent < 0) return null + + return { + durationSeconds, + voltagePercent + } +} + +const interpolateVoltageThreshold = (boundaryPoints: VoltageTolerancePoint[], durationSeconds: number) => { + const sortedPoints = [...boundaryPoints].sort((a, b) => a.durationSeconds - b.durationSeconds) + const firstPoint = sortedPoints[0] + const lastPoint = sortedPoints[sortedPoints.length - 1] + + if (durationSeconds <= firstPoint.durationSeconds) return firstPoint.voltagePercent + if (durationSeconds >= lastPoint.durationSeconds) return lastPoint.voltagePercent + + const nextPointIndex = sortedPoints.findIndex(point => point.durationSeconds >= durationSeconds) + const previousPoint = sortedPoints[nextPointIndex - 1] + const nextPoint = sortedPoints[nextPointIndex] + + if (!previousPoint || !nextPoint) return lastPoint.voltagePercent + if (previousPoint.durationSeconds === nextPoint.durationSeconds) return nextPoint.voltagePercent + + // ITIC 与 SEMI F47 坐标图通常按时间对数轴展示,边界段按对数时间插值能与图上位置一致。 + const previousLogDuration = Math.log10(previousPoint.durationSeconds) + const nextLogDuration = Math.log10(nextPoint.durationSeconds) + const currentLogDuration = Math.log10(durationSeconds) + const ratio = (currentLogDuration - previousLogDuration) / (nextLogDuration - previousLogDuration) + + return previousPoint.voltagePercent + (nextPoint.voltagePercent - previousPoint.voltagePercent) * ratio +} + +export const evaluateVoltageTolerance = ( + standardKey: VoltageToleranceStandardKey, + event: EventVoltageSource +): VoltageToleranceEvaluation | null => { + const standard = resolveVoltageToleranceStandard(standardKey) + const point = resolveVoltageTolerancePoint(event) + + if (!point) return null + + const thresholdVoltagePercent = interpolateVoltageThreshold(standard.boundaryPoints, point.durationSeconds) + const marginPercent = point.voltagePercent - thresholdVoltagePercent + const tolerable = marginPercent >= 0 + + return { + standard, + point, + thresholdVoltagePercent, + tolerable, + marginPercent, + statusText: tolerable ? '可容忍' : '不可容忍' + } +} + +const formatChartPercent = (value: number) => { + return `${Number(value.toFixed(2))}%` +} + +export const buildVoltageToleranceChartOption = ( + standardKey: VoltageToleranceStandardKey, + event: EventVoltageSource +) => { + const standard = resolveVoltageToleranceStandard(standardKey) + const point = resolveVoltageTolerancePoint(event) + const evaluation = evaluateVoltageTolerance(standardKey, event) + const eventDurationMax = point ? Math.max(100, point.durationSeconds * 2) : 100 + const eventVoltageMax = point ? Math.max(120, Math.ceil(point.voltagePercent / 10) * 10 + 10) : 120 + + return { + color: ['#ff7e50', '#003078'], + tooltip: { + trigger: 'item', + formatter: (params: { seriesName?: string; data?: number[] }) => { + const [durationSeconds, voltagePercent] = params.data || [] + if (durationSeconds === undefined || voltagePercent === undefined) return '' + + return [ + params.seriesName || '', + `持续时间:${Number(durationSeconds.toFixed(4))} s`, + `电压百分比:${formatChartPercent(voltagePercent)}` + ].join('
') + } + }, + grid: { + left: 64, + right: 28, + top: 44, + bottom: 58, + containLabel: false + }, + xAxis: { + type: 'log', + name: '持续时间(s)', + min: 0.001, + max: eventDurationMax, + logBase: 10, + minorTick: { show: true }, + minorSplitLine: { show: true }, + axisLabel: { + formatter: (value: number) => Number(value.toPrecision(4)).toString() + } + }, + yAxis: { + type: 'value', + name: '电压百分比(%)', + min: 0, + max: eventVoltageMax, + axisLabel: { + formatter: '{value}%' + } + }, + series: [ + { + name: standard.boundaryName, + type: 'line', + smooth: false, + symbol: 'circle', + symbolSize: 6, + lineStyle: { + width: 2, + color: '#ff7e50' + }, + areaStyle: { + color: 'rgba(32, 178, 170, 0.08)', + origin: 'end' + }, + data: standard.boundaryPoints.map(item => [item.durationSeconds, item.voltagePercent]) + }, + { + name: '事件点', + type: 'scatter', + symbolSize: 14, + itemStyle: { + color: evaluation?.tolerable ? '#20b2aa' : '#a0522d' + }, + label: { + show: Boolean(point), + position: 'top', + formatter: evaluation?.statusText || '' + }, + data: point ? [[point.durationSeconds, point.voltagePercent]] : [] + } + ] + } +} diff --git a/frontend/src/views/steady/steadyDataView/components/SteadyIndicatorFloatingPanel.vue b/frontend/src/views/steady/steadyDataView/components/SteadyIndicatorFloatingPanel.vue new file mode 100644 index 0000000..08cd059 --- /dev/null +++ b/frontend/src/views/steady/steadyDataView/components/SteadyIndicatorFloatingPanel.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/frontend/src/views/steady/steadyDataView/components/SteadyIndicatorTree.vue b/frontend/src/views/steady/steadyDataView/components/SteadyIndicatorTree.vue index de5b9c3..bb22678 100644 --- a/frontend/src/views/steady/steadyDataView/components/SteadyIndicatorTree.vue +++ b/frontend/src/views/steady/steadyDataView/components/SteadyIndicatorTree.vue @@ -67,7 +67,7 @@ const normalizedTreeData = computed(() => { }) const handleCheck = () => { - const checkedNodes = (treeRef.value?.getCheckedNodes(false, true) || []) as SteadyDataView.SteadyIndicatorNode[] + const checkedNodes = (treeRef.value?.getCheckedNodes(false, false) || []) as SteadyDataView.SteadyIndicatorNode[] emit('change', collectLeafIndicators(checkedNodes)) } diff --git a/frontend/src/views/steady/steadyDataView/components/SteadyLedgerTree.vue b/frontend/src/views/steady/steadyDataView/components/SteadyLedgerTree.vue index 4769570..8f2d003 100644 --- a/frontend/src/views/steady/steadyDataView/components/SteadyLedgerTree.vue +++ b/frontend/src/views/steady/steadyDataView/components/SteadyLedgerTree.vue @@ -68,7 +68,7 @@ const handleKeywordChange = (value: string) => { } const handleCheck = () => { - emit('change', (treeRef.value?.getCheckedNodes(false, true) || []) as SteadyDataView.SteadyLedgerNode[]) + emit('change', (treeRef.value?.getCheckedNodes(false, false) || []) as SteadyDataView.SteadyLedgerNode[]) } diff --git a/frontend/src/views/steady/steadyDataView/components/SteadyTrendToolbar.vue b/frontend/src/views/steady/steadyDataView/components/SteadyTrendToolbar.vue index 334335b..50a155d 100644 --- a/frontend/src/views/steady/steadyDataView/components/SteadyTrendToolbar.vue +++ b/frontend/src/views/steady/steadyDataView/components/SteadyTrendToolbar.vue @@ -1,65 +1,67 @@