From 498dad2b15c238a31ddd56301c45693908a9f663 Mon Sep 17 00:00:00 2001 From: zch Date: Thu, 9 Apr 2026 15:42:23 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(=E6=8C=AF=E5=8A=A8=E7=9C=8B=E6=9D=BF):?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E6=8C=87=E6=A0=87=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E6=97=B6=E9=98=88=E5=80=BC=E6=9C=AA=E9=87=8D=E7=BD=AE=E5=92=8C?= =?UTF-8?q?=E5=9B=BE=E8=A1=A8=E6=AE=8B=E7=95=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复质量页继承隐藏指标过滤条件的问题 - 优化散点图符号大小计算逻辑 - 切换指标时自动重置异常检测阈值 - 查询结果为空时主动清空图表实例 - 重构查询状态管理为模块级共享 --- .../report/vibrationBoard/anomaly/index.vue | 9 +- .../vibrationBoard/comparison/index.vue | 2 +- .../components/useVibrationBoardQueryState.ts | 91 ++++++++++++++----- .../vibrationBoard/distribution/index.vue | 9 ++ .../report/vibrationBoard/quality/index.vue | 3 +- 5 files changed, 90 insertions(+), 24 deletions(-) diff --git a/src/views/ems/report/vibrationBoard/anomaly/index.vue b/src/views/ems/report/vibrationBoard/anomaly/index.vue index 59746f2..f200731 100644 --- a/src/views/ems/report/vibrationBoard/anomaly/index.vue +++ b/src/views/ems/report/vibrationBoard/anomaly/index.vue @@ -8,7 +8,7 @@ show-metric @update:time-range="setTimeRange" @update:sampling-interval="setSamplingInterval" - @update:vibration-param="setVibrationParam" + @update:vibration-param="handleMetricChange" @query="handleQuery" /> @@ -132,6 +132,7 @@ const { setTimeRange, setSamplingInterval, setVibrationParam, + resetAnomalyThresholds, buildQuery } = useVibrationBoardQueryState(); @@ -151,6 +152,12 @@ const applyThresholdDefaults = (data?: VibrationAnomalyPageVO) => { queryForm.value.stddevThreshold = Number(data.stddevThreshold ?? queryForm.value.stddevThreshold ?? 0); }; +const handleMetricChange = (value: string) => { + // 切换指标后先清空旧阈值覆盖,再触发查询,这样后端才能按新指标返回正确默认阈值。 + resetAnomalyThresholds(); + setVibrationParam(value); +}; + const handleQuery = async () => { loading.value = true; try { diff --git a/src/views/ems/report/vibrationBoard/comparison/index.vue b/src/views/ems/report/vibrationBoard/comparison/index.vue index 8365c89..2509c2b 100644 --- a/src/views/ems/report/vibrationBoard/comparison/index.vue +++ b/src/views/ems/report/vibrationBoard/comparison/index.vue @@ -98,7 +98,7 @@ const renderCharts = () => { series: [ { type: 'scatter', - symbolSize: (value: any[]) => Math.max(12, Math.min(30, value[2] * 2)), + symbolSize: (value: any[]) => Math.max(12, Math.min(40, 8 + Math.log(toNumber(value[2]) + 1) * 3)), itemStyle: { color: metricColorMap[comparisonData.value.metricField || queryForm.value.vibrationParam] || '#5b8ff9', opacity: 0.8 }, label: { show: true, formatter: (params: any) => params.value[3], position: 'top', fontSize: 11 }, data: (comparisonData.value.scatterItems || []).map((item) => [ diff --git a/src/views/ems/report/vibrationBoard/components/useVibrationBoardQueryState.ts b/src/views/ems/report/vibrationBoard/components/useVibrationBoardQueryState.ts index bbd750f..352b84d 100644 --- a/src/views/ems/report/vibrationBoard/components/useVibrationBoardQueryState.ts +++ b/src/views/ems/report/vibrationBoard/components/useVibrationBoardQueryState.ts @@ -2,30 +2,61 @@ import { getMonitorInfoTree } from '@/api/ems/base/baseMonitorInfo'; import type { VibrationBoardQuery } from '@/api/ems/types'; import { createDefaultTimeRange } from './vibrationBoardShared'; +type VibrationBoardQueryForm = { + compareScope: string; + monitorId: string; + monitorIds: string[]; + selectionLabel: string; + samplingInterval: number; + vibrationParam: string; + highThreshold: number | undefined; + warningThreshold: number | undefined; + minContinuousSamples: number | undefined; + rapidRiseThreshold: number | undefined; + stddevThreshold: number | undefined; +}; + +type BuildQueryOptions = { + includeVibrationParam?: boolean; +}; + +const createDefaultQueryForm = (defaultMetric = 'vibrationSpeed'): VibrationBoardQueryForm => ({ + compareScope: 'all', + monitorId: '', + monitorIds: [], + selectionLabel: '全部振动设备', + samplingInterval: 5, + vibrationParam: defaultMetric, + highThreshold: undefined, + warningThreshold: undefined, + minContinuousSamples: undefined, + rapidRiseThreshold: undefined, + stddevThreshold: undefined +}); + +// 模块级缓存:同一浏览器会话内 7 个页面共享设备树与查询状态,避免重复请求和切页丢筛选。 +const sharedState = reactive({ + treeLoaded: false, + monitorTreeOptions: [] as any[], + deviceDisplayMap: {} as Record, + daterangeRecordTime: createDefaultTimeRange() as string[], + queryForm: createDefaultQueryForm() +}); + export function useVibrationBoardQueryState(defaultMetric = 'vibrationSpeed') { - const state = reactive({ + const localState = reactive({ loading: false, treeLoading: false, - monitorTreeOptions: [] as any[], - treeProps: { label: 'label', children: 'children' }, - deviceDisplayMap: {} as Record, - daterangeRecordTime: createDefaultTimeRange() as string[], - queryForm: { - compareScope: 'all', - monitorId: '', - monitorIds: [] as string[], - selectionLabel: '全部振动设备', - samplingInterval: 5, - vibrationParam: defaultMetric, - highThreshold: undefined as number | undefined, - warningThreshold: undefined as number | undefined, - minContinuousSamples: undefined as number | undefined, - rapidRiseThreshold: undefined as number | undefined, - stddevThreshold: undefined as number | undefined - } + treeProps: { label: 'label', children: 'children' } }); - const { loading, treeLoading, monitorTreeOptions, treeProps, deviceDisplayMap, daterangeRecordTime, queryForm } = toRefs(state); + // 仅在首次进入页面时应用默认主看指标,后续页面切换沿用用户最新选择。 + if (!sharedState.queryForm.vibrationParam) { + sharedState.queryForm.vibrationParam = defaultMetric; + } + + const { loading, treeLoading, treeProps } = toRefs(localState); + const { monitorTreeOptions, deviceDisplayMap, daterangeRecordTime, queryForm } = toRefs(sharedState); const deviceCount = computed(() => queryForm.value.monitorIds?.length || (queryForm.value.monitorId ? 1 : 0)); const hasMultiDevice = computed(() => deviceCount.value > 1 || ['group', 'all'].includes(queryForm.value.compareScope)); @@ -68,12 +99,27 @@ export function useVibrationBoardQueryState(defaultMetric = 'vibrationSpeed') { queryForm.value.vibrationParam = value; }; + const resetAnomalyThresholds = () => { + // 主看指标切换后必须回到“未覆盖”状态,让后端按新指标重新下发默认阈值, + // 否则会把上一指标的阈值误当成当前指标的用户自定义值。 + queryForm.value.highThreshold = undefined; + queryForm.value.warningThreshold = undefined; + queryForm.value.minContinuousSamples = undefined; + queryForm.value.rapidRiseThreshold = undefined; + queryForm.value.stddevThreshold = undefined; + }; + const loadTree = async () => { + // 已加载且有缓存时直接复用,避免 7 个页面重复打同一个树接口。 + if (sharedState.treeLoaded) { + return; + } treeLoading.value = true; try { const response = await getMonitorInfoTree({ monitorType: 10 }); monitorTreeOptions.value = response.data || []; deviceDisplayMap.value = buildDeviceDisplayMap(monitorTreeOptions.value); + sharedState.treeLoaded = true; if (!queryForm.value.monitorIds.length && !queryForm.value.monitorId) { applySelection(buildSelection(getLeafNodes(monitorTreeOptions.value), '全部振动设备', 'all')); } @@ -91,9 +137,11 @@ export function useVibrationBoardQueryState(defaultMetric = 'vibrationSpeed') { applySelection(buildSelection([data], data.label, 'single')); }; - const buildQuery = (): VibrationBoardQuery => ({ + const buildQuery = (options?: BuildQueryOptions): VibrationBoardQuery => ({ samplingInterval: queryForm.value.samplingInterval, - vibrationParam: queryForm.value.vibrationParam, + // 质量页不展示主看指标时,必须禁掉这个隐藏筛选条件, + // 否则会继承其它页面的指标选择,悄悄改变质量统计口径。 + vibrationParam: options?.includeVibrationParam === false ? undefined : queryForm.value.vibrationParam, beginRecordTime: daterangeRecordTime.value[0], endRecordTime: daterangeRecordTime.value[1], highThreshold: queryForm.value.highThreshold, @@ -125,6 +173,7 @@ export function useVibrationBoardQueryState(defaultMetric = 'vibrationSpeed') { setTimeRange, setSamplingInterval, setVibrationParam, + resetAnomalyThresholds, buildQuery, getMonitorDisplayName }; diff --git a/src/views/ems/report/vibrationBoard/distribution/index.vue b/src/views/ems/report/vibrationBoard/distribution/index.vue index 2355dbc..f132438 100644 --- a/src/views/ems/report/vibrationBoard/distribution/index.vue +++ b/src/views/ems/report/vibrationBoard/distribution/index.vue @@ -82,6 +82,13 @@ const distributionData = ref({ const toNumber = (value: number | string | undefined) => Number(value ?? 0); +const clearCharts = () => { + intervalChart.value?.setData(null); + histogramChart.value?.setData(null); + calendarChart.value?.setData(null); + hourlyChart.value?.setData(null); +}; + const renderCharts = () => { const intervalData = distributionData.value.intervalBuckets || []; const histogramData = distributionData.value.histogramBuckets || []; @@ -89,6 +96,8 @@ const renderCharts = () => { const hourlyData = (distributionData.value.hourlyHeatmap || []).map((item) => [toNumber(item.statHour), item.statDate, toNumber(item.avgValue)]); const days = [...new Set(hourlyData.map((item) => item[1] as string))].sort(); if (!intervalData.length && !histogramData.length && !calendarData.length && !hourlyData.length) { + // 查询结果为空时必须主动清空实例,否则会残留上一轮图表数据误导用户。 + clearCharts(); return; } diff --git a/src/views/ems/report/vibrationBoard/quality/index.vue b/src/views/ems/report/vibrationBoard/quality/index.vue index f5787cc..c2604cc 100644 --- a/src/views/ems/report/vibrationBoard/quality/index.vue +++ b/src/views/ems/report/vibrationBoard/quality/index.vue @@ -87,7 +87,8 @@ const renderChart = () => { const handleQuery = async () => { loading.value = true; try { - const { data } = await getVibrationQualityData(buildQuery()); + // 质量页统计的是整批振动样本的数据完整性,不能继承其它页面隐藏的主看指标过滤口径。 + const { data } = await getVibrationQualityData(buildQuery({ includeVibrationParam: false })); qualityData.value = data || { metricQualityItems: [] }; await nextTick(); renderChart(); From 08f83fb91ef0643a826c7684e6a2324175286ebb Mon Sep 17 00:00:00 2001 From: zch Date: Thu, 9 Apr 2026 15:42:46 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(=E4=BB=AA=E8=A1=A8=E7=9B=98):=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=E7=A9=BA=E6=95=B0=E6=8D=AE=E6=97=B6=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E6=B8=85=E7=A9=BA=E5=9B=BE=E8=A1=A8=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 当查询结果为空时,确保仪表盘和排名图表被清空,避免显示旧数据 --- .../ems/report/vibrationBoard/overview/index.vue | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/views/ems/report/vibrationBoard/overview/index.vue b/src/views/ems/report/vibrationBoard/overview/index.vue index 839d21c..67be81b 100644 --- a/src/views/ems/report/vibrationBoard/overview/index.vue +++ b/src/views/ems/report/vibrationBoard/overview/index.vue @@ -133,9 +133,16 @@ const renderCharts = () => { data: [{ value: toNumber(item.value), name: item.name }] })) }); + } else { + // 空结果时同步清空仪表盘,避免 KPI 已回到空态但图表仍停留在上一轮查询。 + gaugeChart.value?.setData(null); } if (hasMultiDevice.value) { + if (!deviceRanks.length) { + rankChart.value?.setData(null); + return; + } rankChart.value?.setData({ tooltip: { trigger: 'axis' }, xAxis: { type: 'value' }, @@ -152,7 +159,10 @@ const renderCharts = () => { } const stats = overviewData.value.primaryMetricStats; - if (!stats) return; + if (!stats) { + rankChart.value?.setData(null); + return; + } rankChart.value?.setData({ xAxis: { type: 'category', data: ['最小值', '均值', '最大值'] }, yAxis: { type: 'value', name: overviewData.value.unit || '' }, From 099a61b80e76634064ff2b41dc800b19367f88e2 Mon Sep 17 00:00:00 2001 From: zch Date: Thu, 9 Apr 2026 15:43:19 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=89=A9?= =?UTF-8?q?=E8=81=94=E7=BD=91=E6=8A=A5=E8=A1=A8=E6=A8=A1=E5=9D=97=E5=8F=8A?= =?UTF-8?q?=E5=AE=9E=E6=97=B6=E5=91=8A=E8=AD=A6=E5=A4=84=E7=90=86=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增多个物联网报表页面,包括趋势总览、质量诊断、设备对比等报表视图 添加实时告警处理弹窗组件,支持告警展示、SOP步骤查看和处理操作 实现计量平衡、能效分析和仪表工况等高级报表功能 引入新的图表组件和工具函数,优化报表数据展示效果 重构告警规则表单,增强通知组选择器的可读性 --- src/components/alarm/RealtimeAlarmModal.vue | 482 +++++++ src/utils/alarmRealtime.ts | 88 ++ .../ems/record/recordAlarmRule/index.vue | 9 +- .../ems/report/components/AlarmTrendPanel.vue | 99 ++ .../components/EfficiencyScoreBoard.vue | 127 ++ .../report/components/IotReportWorkbench.vue | 1256 +++++++++++++++++ .../components/MeterBalanceTopology.vue | 104 ++ .../index.vue | 346 +++++ .../energyAbnormalAlertReport/index.vue | 318 +++++ .../instrumentConditionReport/index.vue | 510 +++++++ .../report/iotDeviceCompareReport/index.vue | 15 + .../ems/report/iotQualityReport/index.vue | 15 + src/views/ems/report/iotTrendReport/index.vue | 15 + .../ems/report/meterBalanceReport/index.vue | 385 +++++ .../ems/report/realtimeIOTCurve/index.vue | 988 +++++++++++++ 15 files changed, 4756 insertions(+), 1 deletion(-) create mode 100644 src/components/alarm/RealtimeAlarmModal.vue create mode 100644 src/utils/alarmRealtime.ts create mode 100644 src/views/ems/report/components/AlarmTrendPanel.vue create mode 100644 src/views/ems/report/components/EfficiencyScoreBoard.vue create mode 100644 src/views/ems/report/components/IotReportWorkbench.vue create mode 100644 src/views/ems/report/components/MeterBalanceTopology.vue create mode 100644 src/views/ems/report/comprehensiveEnergyEfficiencyReport/index.vue create mode 100644 src/views/ems/report/energyAbnormalAlertReport/index.vue create mode 100644 src/views/ems/report/instrumentConditionReport/index.vue create mode 100644 src/views/ems/report/iotDeviceCompareReport/index.vue create mode 100644 src/views/ems/report/iotQualityReport/index.vue create mode 100644 src/views/ems/report/iotTrendReport/index.vue create mode 100644 src/views/ems/report/meterBalanceReport/index.vue create mode 100644 src/views/ems/report/realtimeIOTCurve/index.vue diff --git a/src/components/alarm/RealtimeAlarmModal.vue b/src/components/alarm/RealtimeAlarmModal.vue new file mode 100644 index 0000000..6f9b88a --- /dev/null +++ b/src/components/alarm/RealtimeAlarmModal.vue @@ -0,0 +1,482 @@ + + + + + diff --git a/src/utils/alarmRealtime.ts b/src/utils/alarmRealtime.ts new file mode 100644 index 0000000..e749924 --- /dev/null +++ b/src/utils/alarmRealtime.ts @@ -0,0 +1,88 @@ +import type { EmsRecordAlarmDataVO } from '@/api/ems/types'; + +export const EMS_REALTIME_ALARM_EVENT = 'ems:alarm:realtime'; +export const EMS_REALTIME_ALARM_PAYLOAD_TYPE = 'ems_alarm_realtime'; + +export interface EmsRealtimeAlarmEnvelope { + eventType: typeof EMS_REALTIME_ALARM_PAYLOAD_TYPE; + channel?: 'SSE' | 'WEBSOCKET' | 'UNKNOWN'; + source?: string; + generatedAt?: string; + alarms: EmsRecordAlarmDataVO[]; +} + +interface LegacyRealtimeAlarmEnvelope { + eventType?: string; + source?: string; + eventTime?: string; + payload?: { + records?: EmsRecordAlarmDataVO[]; + }; +} + +export interface ParsedAlarmMessage { + envelope: EmsRealtimeAlarmEnvelope | null; + displayMessage: string; +} + +export const useAlarmRealtimeBus = () => useEventBus(EMS_REALTIME_ALARM_EVENT); + +const isRealtimeAlarmEnvelope = (value: unknown): value is EmsRealtimeAlarmEnvelope => { + if (!value || typeof value !== 'object') { + return false; + } + const envelope = value as EmsRealtimeAlarmEnvelope; + return envelope.eventType === EMS_REALTIME_ALARM_PAYLOAD_TYPE && Array.isArray(envelope.alarms); +}; + +const normalizeLegacyEnvelope = (value: unknown): EmsRealtimeAlarmEnvelope | null => { + if (!value || typeof value !== 'object') { + return null; + } + const legacyEnvelope = value as LegacyRealtimeAlarmEnvelope; + const records = legacyEnvelope.payload?.records; + if (legacyEnvelope.eventType !== 'EMS_REALTIME_ALARM_BATCH' || !Array.isArray(records)) { + return null; + } + return { + eventType: EMS_REALTIME_ALARM_PAYLOAD_TYPE, + source: legacyEnvelope.source, + generatedAt: legacyEnvelope.eventTime, + alarms: records + }; +}; + +export const parseAlarmRealtimeMessage = (rawMessage: unknown): ParsedAlarmMessage => { + const fallbackMessage = typeof rawMessage === 'string' ? rawMessage : JSON.stringify(rawMessage ?? ''); + if (typeof rawMessage !== 'string') { + return { + envelope: null, + displayMessage: fallbackMessage + }; + } + try { + const parsed = JSON.parse(rawMessage); + const envelope = isRealtimeAlarmEnvelope(parsed) ? parsed : normalizeLegacyEnvelope(parsed); + if (!envelope) { + return { + envelope: null, + displayMessage: fallbackMessage + }; + } + const firstAlarm = envelope.alarms[0]; + const alarmCount = envelope.alarms.length; + const displayMessage = + alarmCount > 1 + ? `收到 ${alarmCount} 条实时告警,已进入待处置队列` + : `收到实时告警:${firstAlarm?.alarmTitle || firstAlarm?.monitorName || firstAlarm?.monitorId || '未命名告警'}`; + return { + envelope, + displayMessage + }; + } catch { + return { + envelope: null, + displayMessage: fallbackMessage + }; + } +}; diff --git a/src/views/ems/record/recordAlarmRule/index.vue b/src/views/ems/record/recordAlarmRule/index.vue index 0a1662b..db3035a 100644 --- a/src/views/ems/record/recordAlarmRule/index.vue +++ b/src/views/ems/record/recordAlarmRule/index.vue @@ -226,7 +226,14 @@ - + diff --git a/src/views/ems/report/components/AlarmTrendPanel.vue b/src/views/ems/report/components/AlarmTrendPanel.vue new file mode 100644 index 0000000..ad01f77 --- /dev/null +++ b/src/views/ems/report/components/AlarmTrendPanel.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/views/ems/report/components/EfficiencyScoreBoard.vue b/src/views/ems/report/components/EfficiencyScoreBoard.vue new file mode 100644 index 0000000..76f1b27 --- /dev/null +++ b/src/views/ems/report/components/EfficiencyScoreBoard.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/src/views/ems/report/components/IotReportWorkbench.vue b/src/views/ems/report/components/IotReportWorkbench.vue new file mode 100644 index 0000000..9b3340d --- /dev/null +++ b/src/views/ems/report/components/IotReportWorkbench.vue @@ -0,0 +1,1256 @@ + + + + + diff --git a/src/views/ems/report/components/MeterBalanceTopology.vue b/src/views/ems/report/components/MeterBalanceTopology.vue new file mode 100644 index 0000000..540c397 --- /dev/null +++ b/src/views/ems/report/components/MeterBalanceTopology.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/src/views/ems/report/comprehensiveEnergyEfficiencyReport/index.vue b/src/views/ems/report/comprehensiveEnergyEfficiencyReport/index.vue new file mode 100644 index 0000000..3306354 --- /dev/null +++ b/src/views/ems/report/comprehensiveEnergyEfficiencyReport/index.vue @@ -0,0 +1,346 @@ + + + + + diff --git a/src/views/ems/report/energyAbnormalAlertReport/index.vue b/src/views/ems/report/energyAbnormalAlertReport/index.vue new file mode 100644 index 0000000..9e0429f --- /dev/null +++ b/src/views/ems/report/energyAbnormalAlertReport/index.vue @@ -0,0 +1,318 @@ + + + + + diff --git a/src/views/ems/report/instrumentConditionReport/index.vue b/src/views/ems/report/instrumentConditionReport/index.vue new file mode 100644 index 0000000..064fd08 --- /dev/null +++ b/src/views/ems/report/instrumentConditionReport/index.vue @@ -0,0 +1,510 @@ + + + + + diff --git a/src/views/ems/report/iotDeviceCompareReport/index.vue b/src/views/ems/report/iotDeviceCompareReport/index.vue new file mode 100644 index 0000000..23b5c7a --- /dev/null +++ b/src/views/ems/report/iotDeviceCompareReport/index.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/views/ems/report/iotQualityReport/index.vue b/src/views/ems/report/iotQualityReport/index.vue new file mode 100644 index 0000000..db9b4a4 --- /dev/null +++ b/src/views/ems/report/iotQualityReport/index.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/views/ems/report/iotTrendReport/index.vue b/src/views/ems/report/iotTrendReport/index.vue new file mode 100644 index 0000000..1112466 --- /dev/null +++ b/src/views/ems/report/iotTrendReport/index.vue @@ -0,0 +1,15 @@ + + + diff --git a/src/views/ems/report/meterBalanceReport/index.vue b/src/views/ems/report/meterBalanceReport/index.vue new file mode 100644 index 0000000..fd4dcac --- /dev/null +++ b/src/views/ems/report/meterBalanceReport/index.vue @@ -0,0 +1,385 @@ + + + + + diff --git a/src/views/ems/report/realtimeIOTCurve/index.vue b/src/views/ems/report/realtimeIOTCurve/index.vue new file mode 100644 index 0000000..d3c6565 --- /dev/null +++ b/src/views/ems/report/realtimeIOTCurve/index.vue @@ -0,0 +1,988 @@ + + + + +