feat(sys_oper_log): 新增操作日志备注字段
parent
cb4f23b59c
commit
a1d4069c86
@ -0,0 +1,60 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type {
|
||||||
|
EfficiencyAnalysisVO,
|
||||||
|
EnergyAbnormalAlertReportVO,
|
||||||
|
EmsActionResponse,
|
||||||
|
InstrumentConditionReportVO,
|
||||||
|
MeterBalanceReportVO,
|
||||||
|
RecordIotenvInstantReportQuery
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
export interface MeterBalanceReportQuery extends RecordIotenvInstantReportQuery {
|
||||||
|
rootMonitorId?: string;
|
||||||
|
aggregationType?: 'LAST' | 'AVG' | 'SUM' | string;
|
||||||
|
imbalanceThreshold?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EfficiencyAnalysisQuery extends RecordIotenvInstantReportQuery {
|
||||||
|
metricCodes?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EnergyAbnormalAlertReportQuery extends RecordIotenvInstantReportQuery {
|
||||||
|
alarmStatus?: number;
|
||||||
|
pushStatus?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询仪表工况分析
|
||||||
|
export function getInstrumentConditionReport(data: RecordIotenvInstantReportQuery): Promise<EmsActionResponse<InstrumentConditionReportVO>> {
|
||||||
|
return request({
|
||||||
|
url: '/ems/report/iotAdvanced/instrumentCondition',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询计量平衡报表
|
||||||
|
export function getMeterBalanceReport(data: MeterBalanceReportQuery): Promise<EmsActionResponse<MeterBalanceReportVO>> {
|
||||||
|
return request({
|
||||||
|
url: '/ems/report/iotAdvanced/meterBalance',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询综合运行能效分析
|
||||||
|
export function getEfficiencyAnalysisReport(data: EfficiencyAnalysisQuery): Promise<EmsActionResponse<EfficiencyAnalysisVO>> {
|
||||||
|
return request({
|
||||||
|
url: '/ems/report/iotAdvanced/efficiencyAnalysis',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询用能异常报警报表
|
||||||
|
export function getEnergyAbnormalAlertReport(data: EnergyAbnormalAlertReportQuery): Promise<EmsActionResponse<EnergyAbnormalAlertReportVO>> {
|
||||||
|
return request({
|
||||||
|
url: '/ems/report/iotAdvanced/energyAbnormalAlert',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,502 @@
|
|||||||
|
import type { RecordIotenvInstantVO } from '@/api/ems/types';
|
||||||
|
import * as echarts from 'echarts';
|
||||||
|
|
||||||
|
export interface CurveChartExpose {
|
||||||
|
chart?: unknown;
|
||||||
|
setData?: (option: echarts.EChartsOption) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IotCurveMetricHint {
|
||||||
|
key?: string;
|
||||||
|
label?: string;
|
||||||
|
unit?: string;
|
||||||
|
aliases?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IotCurveMetricDefinition {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
unit: string;
|
||||||
|
aliases: string[];
|
||||||
|
color: string;
|
||||||
|
areaStartColor: string;
|
||||||
|
areaEndColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NormalizedIotCurveRecord extends RecordIotenvInstantVO {
|
||||||
|
__timeKey: string;
|
||||||
|
__sortValue: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_METRIC_CATALOG: IotCurveMetricDefinition[] = [
|
||||||
|
{
|
||||||
|
key: 'temperature',
|
||||||
|
label: '温度',
|
||||||
|
unit: '℃',
|
||||||
|
aliases: ['temperature', 'tempreture'],
|
||||||
|
color: '#ff7a45',
|
||||||
|
areaStartColor: 'rgba(255, 122, 69, 0.28)',
|
||||||
|
areaEndColor: 'rgba(255, 122, 69, 0.02)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'humidity',
|
||||||
|
label: '湿度',
|
||||||
|
unit: '%',
|
||||||
|
aliases: ['humidity'],
|
||||||
|
color: '#14b8a6',
|
||||||
|
areaStartColor: 'rgba(20, 184, 166, 0.26)',
|
||||||
|
areaEndColor: 'rgba(20, 184, 166, 0.02)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'noise',
|
||||||
|
label: '噪声',
|
||||||
|
unit: 'dB',
|
||||||
|
aliases: ['noise'],
|
||||||
|
color: '#8b5cf6',
|
||||||
|
areaStartColor: 'rgba(139, 92, 246, 0.24)',
|
||||||
|
areaEndColor: 'rgba(139, 92, 246, 0.02)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'illuminance',
|
||||||
|
label: '照度',
|
||||||
|
unit: 'Lux',
|
||||||
|
aliases: ['illuminance'],
|
||||||
|
color: '#f59e0b',
|
||||||
|
areaStartColor: 'rgba(245, 158, 11, 0.24)',
|
||||||
|
areaEndColor: 'rgba(245, 158, 11, 0.02)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'concentration',
|
||||||
|
label: '浓度',
|
||||||
|
unit: 'ppm',
|
||||||
|
aliases: ['concentration'],
|
||||||
|
color: '#ef4444',
|
||||||
|
areaStartColor: 'rgba(239, 68, 68, 0.24)',
|
||||||
|
areaEndColor: 'rgba(239, 68, 68, 0.02)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vibrationSpeed',
|
||||||
|
label: '振动速度',
|
||||||
|
unit: 'mm/s',
|
||||||
|
aliases: ['vibrationSpeed', 'speed'],
|
||||||
|
color: '#2563eb',
|
||||||
|
areaStartColor: 'rgba(37, 99, 235, 0.24)',
|
||||||
|
areaEndColor: 'rgba(37, 99, 235, 0.02)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vibrationDisplacement',
|
||||||
|
label: '振动位移',
|
||||||
|
unit: 'um',
|
||||||
|
aliases: ['vibrationDisplacement', 'displacement'],
|
||||||
|
color: '#0f766e',
|
||||||
|
areaStartColor: 'rgba(15, 118, 110, 0.24)',
|
||||||
|
areaEndColor: 'rgba(15, 118, 110, 0.02)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vibrationAcceleration',
|
||||||
|
label: '振动加速度',
|
||||||
|
unit: 'g',
|
||||||
|
aliases: ['vibrationAcceleration', 'acceleration'],
|
||||||
|
color: '#db2777',
|
||||||
|
areaStartColor: 'rgba(219, 39, 119, 0.24)',
|
||||||
|
areaEndColor: 'rgba(219, 39, 119, 0.02)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'vibrationTemp',
|
||||||
|
label: '振动温度',
|
||||||
|
unit: '℃',
|
||||||
|
aliases: ['vibrationTemp'],
|
||||||
|
color: '#f97316',
|
||||||
|
areaStartColor: 'rgba(249, 115, 22, 0.24)',
|
||||||
|
areaEndColor: 'rgba(249, 115, 22, 0.02)'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const toNumberOrNull = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const nextValue = Number(value);
|
||||||
|
return Number.isFinite(nextValue) ? nextValue : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const uniqueStrings = (values: Array<string | undefined | null>) => {
|
||||||
|
return Array.from(
|
||||||
|
new Set(
|
||||||
|
values
|
||||||
|
.map((item) => String(item || '').trim())
|
||||||
|
.filter((item) => item.length > 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createFallbackMetricDefinition = (hint: IotCurveMetricHint, index: number): IotCurveMetricDefinition => {
|
||||||
|
const colorPalette = [
|
||||||
|
['#2563eb', 'rgba(37, 99, 235, 0.24)', 'rgba(37, 99, 235, 0.02)'],
|
||||||
|
['#14b8a6', 'rgba(20, 184, 166, 0.24)', 'rgba(20, 184, 166, 0.02)'],
|
||||||
|
['#f97316', 'rgba(249, 115, 22, 0.24)', 'rgba(249, 115, 22, 0.02)'],
|
||||||
|
['#8b5cf6', 'rgba(139, 92, 246, 0.24)', 'rgba(139, 92, 246, 0.02)'],
|
||||||
|
['#ef4444', 'rgba(239, 68, 68, 0.24)', 'rgba(239, 68, 68, 0.02)']
|
||||||
|
];
|
||||||
|
const [color, areaStartColor, areaEndColor] = colorPalette[index % colorPalette.length];
|
||||||
|
const fallbackKey = String(hint.key || hint.aliases?.[0] || `metric_${index + 1}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: fallbackKey,
|
||||||
|
label: String(hint.label || fallbackKey),
|
||||||
|
unit: String(hint.unit || ''),
|
||||||
|
aliases: uniqueStrings([fallbackKey, ...(hint.aliases || [])]),
|
||||||
|
color,
|
||||||
|
areaStartColor,
|
||||||
|
areaEndColor
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const mergeMetricCatalog = (metricHints: IotCurveMetricHint[] = []) => {
|
||||||
|
const catalogMap = new Map<string, IotCurveMetricDefinition>();
|
||||||
|
|
||||||
|
BASE_METRIC_CATALOG.forEach((item) => {
|
||||||
|
catalogMap.set(item.key, item);
|
||||||
|
});
|
||||||
|
|
||||||
|
metricHints.forEach((hint, index) => {
|
||||||
|
const key = String(hint.key || hint.aliases?.[0] || '').trim();
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const previous = catalogMap.get(key);
|
||||||
|
if (previous) {
|
||||||
|
catalogMap.set(key, {
|
||||||
|
...previous,
|
||||||
|
label: hint.label || previous.label,
|
||||||
|
unit: hint.unit || previous.unit,
|
||||||
|
aliases: uniqueStrings([key, ...(previous.aliases || []), ...(hint.aliases || [])])
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catalogMap.set(key, createFallbackMetricDefinition(hint, index));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(catalogMap.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveTimeKey = (record: RecordIotenvInstantVO) => {
|
||||||
|
return String(record?.recodeTime || record?.collectTime || '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveSortValue = (timeKey: string) => {
|
||||||
|
const timestamp = new Date(timeKey).getTime();
|
||||||
|
return Number.isFinite(timestamp) ? timestamp : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizeIotCurveRecord = (record: RecordIotenvInstantVO): NormalizedIotCurveRecord => {
|
||||||
|
const timeKey = resolveTimeKey(record);
|
||||||
|
return {
|
||||||
|
...record,
|
||||||
|
__timeKey: timeKey,
|
||||||
|
__sortValue: resolveSortValue(timeKey)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readMetricValue = (record: Record<string, unknown>, metric: IotCurveMetricDefinition) => {
|
||||||
|
for (const alias of metric.aliases) {
|
||||||
|
const metricValue = toNumberOrNull(record[alias]);
|
||||||
|
if (metricValue !== null) {
|
||||||
|
return metricValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildMetricHintsByNode = (node?: Record<string, unknown> | null) => {
|
||||||
|
if (!node) {
|
||||||
|
return [] as IotCurveMetricHint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const metricCode = String(node.metricCode || '').trim();
|
||||||
|
const metricName = String(node.metricName || node.pointType || '').trim();
|
||||||
|
const unitName = String(node.unitName || '').trim();
|
||||||
|
if (!metricCode) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里优先把树节点元数据也转成候选指标,后续即使后端新增类型,只要字段名与 metricCode 对上就能直接出图。
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: metricCode,
|
||||||
|
label: metricName || String(node.label || metricCode),
|
||||||
|
unit: unitName,
|
||||||
|
aliases: [metricCode]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveIotCurveMetrics = (
|
||||||
|
records: Array<Record<string, unknown>>,
|
||||||
|
metricHints: IotCurveMetricHint[] = []
|
||||||
|
) => {
|
||||||
|
const catalog = mergeMetricCatalog(metricHints);
|
||||||
|
const preferredKeys = uniqueStrings(metricHints.map((item) => item.key));
|
||||||
|
const detectedMetrics = catalog.filter((item) => records.some((record) => readMetricValue(record, item) !== null));
|
||||||
|
|
||||||
|
if (!records.length) {
|
||||||
|
return catalog.filter((item) => preferredKeys.includes(item.key));
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultMap = new Map<string, IotCurveMetricDefinition>();
|
||||||
|
preferredKeys.forEach((key) => {
|
||||||
|
const metric = catalog.find((item) => item.key === key);
|
||||||
|
if (metric && detectedMetrics.some((item) => item.key === key)) {
|
||||||
|
resultMap.set(key, metric);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
detectedMetrics.forEach((item) => {
|
||||||
|
resultMap.set(item.key, item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(resultMap.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const extractRealtimeIotRecords = (payload: unknown): RecordIotenvInstantVO[] => {
|
||||||
|
if (!payload) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (Array.isArray(payload)) {
|
||||||
|
return payload as RecordIotenvInstantVO[];
|
||||||
|
}
|
||||||
|
if (typeof payload !== 'object') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const source = payload as Record<string, unknown>;
|
||||||
|
const candidateKeys = ['data', 'rows', 'records', 'list', 'payload'];
|
||||||
|
for (const key of candidateKeys) {
|
||||||
|
if (Array.isArray(source[key])) {
|
||||||
|
return source[key] as RecordIotenvInstantVO[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (resolveTimeKey(source as RecordIotenvInstantVO)) {
|
||||||
|
return [source as RecordIotenvInstantVO];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeRealtimeWindow = (
|
||||||
|
currentRecords: NormalizedIotCurveRecord[],
|
||||||
|
incomingRecords: RecordIotenvInstantVO[],
|
||||||
|
windowSize: number
|
||||||
|
) => {
|
||||||
|
const mergedMap = new Map<string, NormalizedIotCurveRecord>();
|
||||||
|
[...currentRecords, ...incomingRecords.map(normalizeIotCurveRecord)].forEach((item) => {
|
||||||
|
if (!item.__timeKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const uniqueKey = `${item.monitorId || item.monitorCode || ''}_${item.__timeKey}`;
|
||||||
|
const previous = mergedMap.get(uniqueKey);
|
||||||
|
mergedMap.set(uniqueKey, {
|
||||||
|
...previous,
|
||||||
|
...item
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const normalizedWindowSize = Math.max(1, Number(windowSize) || 1);
|
||||||
|
return Array.from(mergedMap.values())
|
||||||
|
.sort((left, right) => {
|
||||||
|
if (left.__sortValue !== right.__sortValue) {
|
||||||
|
return left.__sortValue - right.__sortValue;
|
||||||
|
}
|
||||||
|
return left.__timeKey.localeCompare(right.__timeKey);
|
||||||
|
})
|
||||||
|
.slice(-normalizedWindowSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildIotCurveOption = ({
|
||||||
|
metric,
|
||||||
|
monitorName,
|
||||||
|
records,
|
||||||
|
premium = false,
|
||||||
|
subtitle = ''
|
||||||
|
}: {
|
||||||
|
metric: IotCurveMetricDefinition;
|
||||||
|
monitorName: string;
|
||||||
|
records: Array<Record<string, unknown>>;
|
||||||
|
premium?: boolean;
|
||||||
|
subtitle?: string;
|
||||||
|
}): echarts.EChartsOption => {
|
||||||
|
const titleText = `${monitorName || '设备'} ${metric.label}曲线`;
|
||||||
|
const xData = records.map((item) => String(item.__timeKey || item.recodeTime || item.collectTime || ''));
|
||||||
|
const seriesData = records.map((item) => readMetricValue(item, metric));
|
||||||
|
|
||||||
|
return {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
animation: premium,
|
||||||
|
animationDuration: premium ? 420 : 0,
|
||||||
|
animationDurationUpdate: premium ? 320 : 0,
|
||||||
|
title: {
|
||||||
|
text: titleText,
|
||||||
|
subtext: subtitle,
|
||||||
|
left: 'center',
|
||||||
|
top: 10,
|
||||||
|
textStyle: {
|
||||||
|
color: premium ? '#0f172a' : '#0f172a',
|
||||||
|
fontSize: premium ? 18 : 16,
|
||||||
|
fontWeight: 600
|
||||||
|
},
|
||||||
|
subtextStyle: {
|
||||||
|
color: premium ? '#64748b' : '#64748b',
|
||||||
|
fontSize: 12
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: premium ? '18%' : '16%',
|
||||||
|
bottom: 56,
|
||||||
|
left: 68,
|
||||||
|
right: 28
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
backgroundColor: premium ? 'rgba(15, 23, 42, 0.92)' : 'rgba(15, 23, 42, 0.88)',
|
||||||
|
borderWidth: 0,
|
||||||
|
textStyle: {
|
||||||
|
color: '#ffffff'
|
||||||
|
},
|
||||||
|
axisPointer: {
|
||||||
|
type: 'line',
|
||||||
|
label: {
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
right: 20,
|
||||||
|
top: 16,
|
||||||
|
itemWidth: 14,
|
||||||
|
itemHeight: 8,
|
||||||
|
textStyle: {
|
||||||
|
color: '#475569'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataZoom: [
|
||||||
|
{
|
||||||
|
type: 'inside',
|
||||||
|
filterMode: 'none'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'slider',
|
||||||
|
height: 22,
|
||||||
|
bottom: 14,
|
||||||
|
borderColor: 'transparent',
|
||||||
|
backgroundColor: premium ? 'rgba(148, 163, 184, 0.16)' : 'rgba(148, 163, 184, 0.14)',
|
||||||
|
fillerColor: premium ? 'rgba(59, 130, 246, 0.18)' : 'rgba(59, 130, 246, 0.14)',
|
||||||
|
handleStyle: {
|
||||||
|
color: metric.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: xData,
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#cbd5e1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#64748b',
|
||||||
|
hideOverlap: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: metric.unit ? `${metric.label}(${metric.unit})` : metric.label,
|
||||||
|
nameTextStyle: {
|
||||||
|
color: '#475569'
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
lineStyle: {
|
||||||
|
color: 'rgba(148, 163, 184, 0.18)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisTick: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: '#cbd5e1'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
axisLabel: {
|
||||||
|
color: '#64748b'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: metric.unit ? `${metric.label}(${metric.unit})` : metric.label,
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
connectNulls: true,
|
||||||
|
showSymbol: records.length <= 80,
|
||||||
|
symbol: 'circle',
|
||||||
|
symbolSize: premium ? 7 : 6,
|
||||||
|
lineStyle: {
|
||||||
|
width: premium ? 3 : 2,
|
||||||
|
color: metric.color
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
color: metric.color,
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
borderWidth: premium ? 2 : 1
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{ offset: 0, color: metric.areaStartColor },
|
||||||
|
{ offset: 1, color: metric.areaEndColor }
|
||||||
|
])
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
focus: 'series'
|
||||||
|
},
|
||||||
|
data: seriesData
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectCurveCharts = (
|
||||||
|
charts: Array<
|
||||||
|
| {
|
||||||
|
chart?: unknown;
|
||||||
|
}
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
>,
|
||||||
|
groupId: string
|
||||||
|
) => {
|
||||||
|
const chartInstances = charts
|
||||||
|
.map((item) => item?.chart)
|
||||||
|
.filter((item): item is echarts.ECharts => Boolean(item) && typeof (item as echarts.ECharts).setOption === 'function');
|
||||||
|
|
||||||
|
if (chartInstances.length < 2) {
|
||||||
|
return () => undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
chartInstances.forEach((item) => {
|
||||||
|
item.group = groupId;
|
||||||
|
});
|
||||||
|
echarts.connect(groupId);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
echarts.disconnect(groupId);
|
||||||
|
chartInstances.forEach((item) => {
|
||||||
|
if (item.group === groupId) {
|
||||||
|
item.group = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,638 @@
|
|||||||
|
import { computed, nextTick, reactive, ref, watch } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { parseTime } from '@/utils/ruoyi';
|
||||||
|
import { getMonitorInfoTree } from '@/api/ems/base/baseMonitorInfo';
|
||||||
|
import { getRecordIotenvInstantReportData } from '@/api/ems/record/recordIotenvInstant';
|
||||||
|
import type { RecordIotenvInstantVO } from '@/api/ems/types';
|
||||||
|
import {
|
||||||
|
buildMetricHintsByNode,
|
||||||
|
normalizeIotCurveRecord,
|
||||||
|
readMetricValue,
|
||||||
|
resolveIotCurveMetrics,
|
||||||
|
type IotCurveMetricDefinition,
|
||||||
|
type IotCurveMetricHint,
|
||||||
|
type NormalizedIotCurveRecord
|
||||||
|
} from './iotCurveShared';
|
||||||
|
|
||||||
|
export interface IotReportMetricStats {
|
||||||
|
latest: number | null;
|
||||||
|
avg: number | null;
|
||||||
|
max: number | null;
|
||||||
|
p75: number | null;
|
||||||
|
p90: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IotReportDeviceStat {
|
||||||
|
monitorId: string;
|
||||||
|
monitorName: string;
|
||||||
|
latest: number;
|
||||||
|
avg: number;
|
||||||
|
max: number;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IotReportScopeOption {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
deviceCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IotTreeLeafNode extends Record<string, unknown> {
|
||||||
|
code?: string;
|
||||||
|
label?: string;
|
||||||
|
type?: unknown;
|
||||||
|
__rootKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IotReportSelection {
|
||||||
|
compareScope: string;
|
||||||
|
monitorId: string;
|
||||||
|
monitorIds: string[];
|
||||||
|
monitorName: string;
|
||||||
|
selectionLabel: string;
|
||||||
|
nodeId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const createDefaultTimeRange = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const start = new Date(now.getTime() - 2 * 60 * 60 * 1000);
|
||||||
|
return [parseTime(start, '{y}-{m}-{d} {h}:{i}:{s}'), parseTime(now, '{y}-{m}-{d} {h}:{i}:{s}')];
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveNodeKey = (node: Record<string, unknown>) => {
|
||||||
|
return String(node.id || node.code || node.label || Math.random());
|
||||||
|
};
|
||||||
|
|
||||||
|
const cloneTreeByRootKeys = (nodes: Array<Record<string, unknown>>, selectedKeys: string[]) => {
|
||||||
|
return (nodes || []).filter((item) => selectedKeys.includes(resolveNodeKey(item)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLeafNodes = (nodes: Array<Record<string, unknown>>, rootKey?: string): IotTreeLeafNode[] => {
|
||||||
|
let result: IotTreeLeafNode[] = [];
|
||||||
|
(nodes || []).forEach((item) => {
|
||||||
|
const currentRootKey = rootKey || resolveNodeKey(item);
|
||||||
|
if (Array.isArray(item.children) && item.children.length > 0) {
|
||||||
|
result = result.concat(getLeafNodes(item.children as Array<Record<string, unknown>>, currentRootKey));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (item.code) {
|
||||||
|
result.push({
|
||||||
|
...item,
|
||||||
|
__rootKey: currentRootKey
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildDeviceDisplayMap = (nodes: Array<Record<string, unknown>>) => {
|
||||||
|
return getLeafNodes(nodes).reduce((accumulator, item) => {
|
||||||
|
const key = String(item.code || '');
|
||||||
|
if (!key) {
|
||||||
|
return accumulator;
|
||||||
|
}
|
||||||
|
accumulator[key] = {
|
||||||
|
label: String(item.label || key),
|
||||||
|
type: item.type
|
||||||
|
};
|
||||||
|
return accumulator;
|
||||||
|
}, {} as Record<string, { label: string; type: unknown }>);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findRootKeyByNodeId = (nodes: Array<Record<string, unknown>>, targetId: string, rootKey = ''): string => {
|
||||||
|
for (const item of nodes || []) {
|
||||||
|
const currentRootKey = rootKey || resolveNodeKey(item);
|
||||||
|
if (String(item.id || '') === String(targetId || '')) {
|
||||||
|
return currentRootKey;
|
||||||
|
}
|
||||||
|
if (Array.isArray(item.children) && item.children.length > 0) {
|
||||||
|
const matched = findRootKeyByNodeId(item.children as Array<Record<string, unknown>>, targetId, currentRootKey);
|
||||||
|
if (matched) {
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSelectionByLeafNodes = (leafNodes: IotTreeLeafNode[], selectionLabel: string, compareScope = ''): IotReportSelection => {
|
||||||
|
const monitorIds = leafNodes.map((item) => String(item.code || '')).filter((item) => item);
|
||||||
|
const firstNode = leafNodes[0];
|
||||||
|
return {
|
||||||
|
compareScope: compareScope || (monitorIds.length > 1 ? 'group' : 'single'),
|
||||||
|
monitorId: monitorIds.length === 1 ? monitorIds[0] : '',
|
||||||
|
monitorIds,
|
||||||
|
monitorName: monitorIds.length === 1 ? String(firstNode?.label || selectionLabel) : selectionLabel,
|
||||||
|
selectionLabel,
|
||||||
|
nodeId: String(firstNode?.id || '')
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isValidNumber = (value: unknown) => value !== null && value !== undefined && !Number.isNaN(Number(value));
|
||||||
|
|
||||||
|
export const average = (values: number[]) => {
|
||||||
|
if (!values.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return values.reduce((sum, item) => sum + item, 0) / values.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const percentile = (sortedValues: number[], ratio: number) => {
|
||||||
|
if (!sortedValues.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const index = Math.min(sortedValues.length - 1, Math.max(0, Math.ceil(sortedValues.length * ratio) - 1));
|
||||||
|
return sortedValues[index];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getBands = (values: number[]) => {
|
||||||
|
const sortedValues = [...values].sort((left, right) => left - right);
|
||||||
|
return {
|
||||||
|
low: percentile(sortedValues, 0.33),
|
||||||
|
high: percentile(sortedValues, 0.66),
|
||||||
|
avg: average(sortedValues),
|
||||||
|
p75: percentile(sortedValues, 0.75),
|
||||||
|
p90: percentile(sortedValues, 0.9)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatValue = (value: unknown) => {
|
||||||
|
if (!isValidNumber(value)) {
|
||||||
|
return '--';
|
||||||
|
}
|
||||||
|
return Number(value).toFixed(2);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatPercent = (value: unknown) => {
|
||||||
|
return `${(Number(value || 0) * 100).toFixed(1)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const normalizePercent = (value: number | null, max: number | null) => {
|
||||||
|
if (!isValidNumber(value) || !isValidNumber(max) || Number(max) <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Number((((value || 0) / (max || 1)) * 100).toFixed(2));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ceilNumber = (value: number) => Math.ceil(value * 10) / 10;
|
||||||
|
|
||||||
|
export const useIotReportWorkbench = () => {
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const treeLoading = ref(false);
|
||||||
|
const reportReady = ref(false);
|
||||||
|
const monitorTreeOptions = ref<Array<Record<string, unknown>>>([]);
|
||||||
|
const deviceDisplayMap = ref<Record<string, { label: string; type: unknown }>>({});
|
||||||
|
const selectedScopeKeys = ref<string[]>([]);
|
||||||
|
const currentTreeNodeId = ref('');
|
||||||
|
const rawRecords = ref<NormalizedIotCurveRecord[]>([]);
|
||||||
|
const daterangeRecordTime = ref<string[]>(createDefaultTimeRange());
|
||||||
|
|
||||||
|
const queryForm = reactive({
|
||||||
|
compareScope: 'single',
|
||||||
|
monitorId: '',
|
||||||
|
monitorIds: [] as string[],
|
||||||
|
monitorName: '',
|
||||||
|
selectionLabel: '当前分析范围',
|
||||||
|
samplingInterval: 5,
|
||||||
|
primaryMetricField: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const leafNodeMap = computed(() => {
|
||||||
|
return getLeafNodes(monitorTreeOptions.value).reduce((accumulator, item) => {
|
||||||
|
const key = String(item.code || '');
|
||||||
|
if (key) {
|
||||||
|
accumulator[key] = item;
|
||||||
|
}
|
||||||
|
return accumulator;
|
||||||
|
}, {} as Record<string, IotTreeLeafNode>);
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedLeafNodes = computed(() => {
|
||||||
|
const currentIds = queryForm.monitorIds.length ? queryForm.monitorIds : queryForm.monitorId ? [queryForm.monitorId] : [];
|
||||||
|
return currentIds.map((item) => leafNodeMap.value[item]).filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
|
const metricHints = computed<IotCurveMetricHint[]>(() => {
|
||||||
|
return selectedLeafNodes.value.flatMap((item) => buildMetricHintsByNode(item));
|
||||||
|
});
|
||||||
|
|
||||||
|
const metricConfigs = computed<IotCurveMetricDefinition[]>(() => {
|
||||||
|
return resolveIotCurveMetrics(rawRecords.value, metricHints.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const primaryMetric = computed<IotCurveMetricDefinition | null>(() => {
|
||||||
|
if (!metricConfigs.value.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const matched = metricConfigs.value.find((item) => item.key === queryForm.primaryMetricField);
|
||||||
|
return matched || metricConfigs.value[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const availablePrimaryMetricOptions = computed(() => metricConfigs.value);
|
||||||
|
|
||||||
|
const deviceCount = computed(() => {
|
||||||
|
if (queryForm.monitorIds.length > 0) {
|
||||||
|
return queryForm.monitorIds.length;
|
||||||
|
}
|
||||||
|
return queryForm.monitorId ? 1 : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasMultiDevice = computed(() => {
|
||||||
|
return queryForm.monitorIds.length > 1 || ['group', 'all'].includes(queryForm.compareScope);
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableScopeOptions = computed<IotReportScopeOption[]>(() => {
|
||||||
|
return (monitorTreeOptions.value || []).map((item) => ({
|
||||||
|
key: resolveNodeKey(item),
|
||||||
|
label: String(item.label || '未命名分组'),
|
||||||
|
deviceCount: getLeafNodes([item]).length
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredMonitorTreeOptions = computed(() => {
|
||||||
|
if (!selectedScopeKeys.value.length) {
|
||||||
|
return monitorTreeOptions.value;
|
||||||
|
}
|
||||||
|
return cloneTreeByRootKeys(monitorTreeOptions.value, selectedScopeKeys.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const monitorTypeLabel = computed(() => {
|
||||||
|
return metricConfigs.value.length ? metricConfigs.value.map((item) => item.label).join(' / ') : '待识别';
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectionSummary = computed(() => {
|
||||||
|
const summaryMap = {
|
||||||
|
all: '多分组联合分析',
|
||||||
|
group: '设备组分析',
|
||||||
|
single: '单设备分析'
|
||||||
|
};
|
||||||
|
return summaryMap[queryForm.compareScope] || '分析中';
|
||||||
|
});
|
||||||
|
|
||||||
|
const energySelectionSummary = computed(() => {
|
||||||
|
if (!selectedScopeKeys.value.length) {
|
||||||
|
return '当前未限定分组,默认展示全部范围';
|
||||||
|
}
|
||||||
|
const labels = availableScopeOptions.value.filter((item) => selectedScopeKeys.value.includes(item.key)).map((item) => item.label);
|
||||||
|
return `当前分组:${labels.join(' / ')}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceSelectionSummary = computed(() => {
|
||||||
|
return queryForm.selectionLabel ? `分析范围:${queryForm.selectionLabel}` : '分析范围待确认';
|
||||||
|
});
|
||||||
|
|
||||||
|
const analysisObjectValue = computed(() => {
|
||||||
|
if (hasMultiDevice.value) {
|
||||||
|
return `${deviceCount.value} 台设备`;
|
||||||
|
}
|
||||||
|
return queryForm.monitorName || queryForm.selectionLabel || '--';
|
||||||
|
});
|
||||||
|
|
||||||
|
const analysisObjectFoot = computed(() => {
|
||||||
|
return hasMultiDevice.value ? '当前报表按设备组/联合范围聚合分析' : '当前报表按单设备时间轨迹分析';
|
||||||
|
});
|
||||||
|
|
||||||
|
const metricDescription = computed(() => {
|
||||||
|
return metricConfigs.value.length ? metricConfigs.value.map((item) => item.label).join('、') : '等待识别有效指标';
|
||||||
|
});
|
||||||
|
|
||||||
|
const primaryMetricFoot = computed(() => {
|
||||||
|
return primaryMetric.value ? `当前主看指标用于排名、筛选与小时均值分析` : '请选择有效主看指标';
|
||||||
|
});
|
||||||
|
|
||||||
|
const getMonitorDisplayName = (monitorId: string) => {
|
||||||
|
const cachedItem = deviceDisplayMap.value[monitorId];
|
||||||
|
return cachedItem?.label || monitorId || '--';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShortDisplayName = (monitorId: string) => {
|
||||||
|
const displayName = getMonitorDisplayName(monitorId);
|
||||||
|
return displayName.length <= 10 ? displayName : `${displayName.slice(0, 9)}...`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSeriesTimeAxis = () => {
|
||||||
|
return Array.from(new Set(rawRecords.value.map((item) => item.__timeKey).filter((item) => item)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMetricValues = (metric: IotCurveMetricDefinition, monitorId = '') => {
|
||||||
|
return rawRecords.value
|
||||||
|
.filter((item) => !monitorId || String(item.monitorId || item.monitorCode || '') === monitorId)
|
||||||
|
.map((item) => readMetricValue(item, metric))
|
||||||
|
.filter((item): item is number => item !== null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMetricStats = (metric: IotCurveMetricDefinition, monitorId = ''): IotReportMetricStats => {
|
||||||
|
const values = getMetricValues(metric, monitorId);
|
||||||
|
if (!values.length) {
|
||||||
|
return {
|
||||||
|
latest: null,
|
||||||
|
avg: null,
|
||||||
|
max: null,
|
||||||
|
p75: null,
|
||||||
|
p90: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const sortedValues = [...values].sort((left, right) => left - right);
|
||||||
|
return {
|
||||||
|
latest: values[values.length - 1],
|
||||||
|
avg: average(values),
|
||||||
|
max: sortedValues[sortedValues.length - 1],
|
||||||
|
p75: percentile(sortedValues, 0.75),
|
||||||
|
p90: percentile(sortedValues, 0.9)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const metricCards = computed(() => {
|
||||||
|
return metricConfigs.value.slice(0, 4).map((metric) => {
|
||||||
|
const stats = getMetricStats(metric);
|
||||||
|
return {
|
||||||
|
...metric,
|
||||||
|
latest: stats.latest,
|
||||||
|
avg: stats.avg,
|
||||||
|
max: stats.max
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const deviceStats = computed<IotReportDeviceStat[]>(() => {
|
||||||
|
if (!primaryMetric.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const deviceMap = {} as Record<string, number[]>;
|
||||||
|
rawRecords.value.forEach((item) => {
|
||||||
|
const monitorId = String(item.monitorId || item.monitorCode || '');
|
||||||
|
const metricValue = readMetricValue(item, primaryMetric.value as IotCurveMetricDefinition);
|
||||||
|
if (!monitorId || metricValue === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!deviceMap[monitorId]) {
|
||||||
|
deviceMap[monitorId] = [];
|
||||||
|
}
|
||||||
|
deviceMap[monitorId].push(metricValue);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Object.keys(deviceMap)
|
||||||
|
.map((monitorId) => {
|
||||||
|
const values = deviceMap[monitorId];
|
||||||
|
const sortedValues = [...values].sort((left, right) => left - right);
|
||||||
|
return {
|
||||||
|
monitorId,
|
||||||
|
monitorName: getMonitorDisplayName(monitorId),
|
||||||
|
latest: values[values.length - 1],
|
||||||
|
avg: average(values),
|
||||||
|
max: sortedValues[sortedValues.length - 1],
|
||||||
|
count: values.length
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((left, right) => right.avg - left.avg);
|
||||||
|
});
|
||||||
|
|
||||||
|
const coverageRatio = computed(() => {
|
||||||
|
if (!rawRecords.value.length || !metricConfigs.value.length) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const totalPoints = rawRecords.value.length * metricConfigs.value.length;
|
||||||
|
const validPoints = metricConfigs.value.reduce((sum, metric) => sum + getMetricValues(metric).length, 0);
|
||||||
|
return totalPoints ? validPoints / totalPoints : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadMonitorTree = async () => {
|
||||||
|
treeLoading.value = true;
|
||||||
|
try {
|
||||||
|
// 这里不再硬编码能源类型编号,直接读取当前项目可用树结构,后续新增/删减类型不需要改页面。
|
||||||
|
const response = await getMonitorInfoTree({});
|
||||||
|
monitorTreeOptions.value = response.data || [];
|
||||||
|
deviceDisplayMap.value = buildDeviceDisplayMap(monitorTreeOptions.value);
|
||||||
|
} finally {
|
||||||
|
treeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildSelectionByMonitorIds = (monitorIds: string[]) => {
|
||||||
|
const leafNodes = monitorIds.map((item) => leafNodeMap.value[item]).filter(Boolean);
|
||||||
|
if (!leafNodes.length) {
|
||||||
|
return {
|
||||||
|
compareScope: 'all',
|
||||||
|
monitorId: '',
|
||||||
|
monitorIds: [],
|
||||||
|
monitorName: '',
|
||||||
|
selectionLabel: '当前分析范围',
|
||||||
|
nodeId: ''
|
||||||
|
} as IotReportSelection;
|
||||||
|
}
|
||||||
|
const compareScope = leafNodes.length > 1 ? 'group' : 'single';
|
||||||
|
const selectionLabel = leafNodes.length > 1 ? `选中设备组(${leafNodes.length}台)` : String(leafNodes[0].label || '当前设备');
|
||||||
|
return buildSelectionByLeafNodes(leafNodes, selectionLabel, compareScope);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applySelection = (selection: IotReportSelection, shouldQuery = true) => {
|
||||||
|
queryForm.compareScope = selection.compareScope;
|
||||||
|
queryForm.monitorId = selection.monitorId;
|
||||||
|
queryForm.monitorIds = selection.monitorIds;
|
||||||
|
queryForm.monitorName = selection.monitorName;
|
||||||
|
queryForm.selectionLabel = selection.selectionLabel;
|
||||||
|
currentTreeNodeId.value = selection.nodeId;
|
||||||
|
if (shouldQuery) {
|
||||||
|
void handleQuery();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applySelectionByScopeKeys = (shouldQuery = true) => {
|
||||||
|
const selectedRoots = cloneTreeByRootKeys(monitorTreeOptions.value, selectedScopeKeys.value);
|
||||||
|
if (!selectedRoots.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const leafNodes = getLeafNodes(selectedRoots);
|
||||||
|
const selectionLabel =
|
||||||
|
selectedRoots.length === 1 ? `${String(selectedRoots[0].label || '当前分组')}设备组` : `${selectedRoots.map((item) => item.label).join(' / ')}联合分析`;
|
||||||
|
const compareScope = selectedRoots.length > 1 ? 'all' : '';
|
||||||
|
applySelection(buildSelectionByLeafNodes(leafNodes, selectionLabel, compareScope), shouldQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
const initQueryFromRoute = () => {
|
||||||
|
const { query } = route;
|
||||||
|
const selectionCacheKey = String(query.selectionKey || 'recordIotenvInstantDashboardSelection');
|
||||||
|
const selectionCacheText = window.sessionStorage.getItem(selectionCacheKey);
|
||||||
|
let selectionCache = {} as Record<string, unknown>;
|
||||||
|
try {
|
||||||
|
selectionCache = selectionCacheText ? JSON.parse(selectionCacheText) : {};
|
||||||
|
} catch (error) {
|
||||||
|
selectionCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const monitorIds = String(query.monitorIds || '')
|
||||||
|
.split(',')
|
||||||
|
.map((item) => item.trim())
|
||||||
|
.filter((item) => item);
|
||||||
|
|
||||||
|
queryForm.compareScope = String(query.compareScope || (monitorIds.length > 1 ? 'group' : 'single'));
|
||||||
|
queryForm.monitorId = String(query.monitorId || '');
|
||||||
|
queryForm.monitorIds = monitorIds;
|
||||||
|
queryForm.monitorName = String(query.monitorName || query.monitorId || '');
|
||||||
|
queryForm.selectionLabel = String(query.selectionLabel || selectionCache.selectionLabel || query.monitorName || '当前分析范围');
|
||||||
|
queryForm.samplingInterval = Number(query.samplingInterval || 5);
|
||||||
|
queryForm.primaryMetricField = String(query.primaryMetricField || '');
|
||||||
|
|
||||||
|
const beginRecordTime = String(query.beginRecordTime || '');
|
||||||
|
const endRecordTime = String(query.endRecordTime || '');
|
||||||
|
if (beginRecordTime && endRecordTime) {
|
||||||
|
daterangeRecordTime.value = [beginRecordTime, endRecordTime];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyInitialSelection = () => {
|
||||||
|
if (!monitorTreeOptions.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const routeIds = queryForm.monitorIds.length ? queryForm.monitorIds : queryForm.monitorId ? [queryForm.monitorId] : [];
|
||||||
|
if (routeIds.length) {
|
||||||
|
const selection = buildSelectionByMonitorIds(routeIds);
|
||||||
|
if (selection.monitorIds.length) {
|
||||||
|
selectedScopeKeys.value = Array.from(new Set(routeIds.map((item) => leafNodeMap.value[item]?.__rootKey).filter(Boolean) as string[]));
|
||||||
|
applySelection(selection, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedScopeKeys.value = [resolveNodeKey(monitorTreeOptions.value[0])];
|
||||||
|
applySelectionByScopeKeys(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildRequestQuery = () => {
|
||||||
|
const query: Record<string, unknown> = {
|
||||||
|
samplingInterval: queryForm.samplingInterval,
|
||||||
|
params: {
|
||||||
|
beginRecordTime: daterangeRecordTime.value[0],
|
||||||
|
endRecordTime: daterangeRecordTime.value[1]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (queryForm.monitorIds.length > 1) {
|
||||||
|
query.monitorIds = queryForm.monitorIds;
|
||||||
|
} else {
|
||||||
|
query.monitorId = queryForm.monitorIds[0] || queryForm.monitorId;
|
||||||
|
}
|
||||||
|
return query;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQuery = async () => {
|
||||||
|
if (!deviceCount.value) {
|
||||||
|
ElMessage.warning('当前没有可分析的设备范围');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!daterangeRecordTime.value || daterangeRecordTime.value.length !== 2) {
|
||||||
|
ElMessage.warning('请选择完整的记录时间范围');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
reportReady.value = false;
|
||||||
|
try {
|
||||||
|
const { data = [] } = await getRecordIotenvInstantReportData(buildRequestQuery());
|
||||||
|
rawRecords.value = (data as RecordIotenvInstantVO[]).map((item) => normalizeIotCurveRecord(item));
|
||||||
|
if (metricConfigs.value.length && !metricConfigs.value.some((item) => item.key === queryForm.primaryMetricField)) {
|
||||||
|
queryForm.primaryMetricField = metricConfigs.value[0].key;
|
||||||
|
}
|
||||||
|
reportReady.value = rawRecords.value.length > 0 && metricConfigs.value.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('报表加载失败,请稍后重试');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScopeChange = (value: string[]) => {
|
||||||
|
if (!value || !value.length) {
|
||||||
|
nextTick(() => {
|
||||||
|
selectedScopeKeys.value = availableScopeOptions.value.length ? [availableScopeOptions.value[0].key] : [];
|
||||||
|
});
|
||||||
|
ElMessage.warning('至少保留一个分组用于分析');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applySelectionByScopeKeys();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTreeNodeClick = (data: Record<string, unknown>) => {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rootKey = findRootKeyByNodeId(monitorTreeOptions.value, String(data.id || ''));
|
||||||
|
if (rootKey) {
|
||||||
|
selectedScopeKeys.value = [rootKey];
|
||||||
|
}
|
||||||
|
if (Array.isArray(data.children) && data.children.length > 0) {
|
||||||
|
applySelection(buildSelectionByLeafNodes(getLeafNodes([data]), `${String(data.label || '当前节点')}设备组`, 'group'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const monitorId = String(data.code || '');
|
||||||
|
applySelection(buildSelectionByMonitorIds([monitorId]));
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
metricConfigs,
|
||||||
|
(value) => {
|
||||||
|
if (!value.length) {
|
||||||
|
queryForm.primaryMetricField = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!value.some((item) => item.key === queryForm.primaryMetricField)) {
|
||||||
|
queryForm.primaryMetricField = value[0].key;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialize = async () => {
|
||||||
|
initQueryFromRoute();
|
||||||
|
await loadMonitorTree();
|
||||||
|
applyInitialSelection();
|
||||||
|
await handleQuery();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
availablePrimaryMetricOptions,
|
||||||
|
availableScopeOptions,
|
||||||
|
analysisObjectFoot,
|
||||||
|
analysisObjectValue,
|
||||||
|
buildSeriesTimeAxis,
|
||||||
|
coverageRatio,
|
||||||
|
currentTreeNodeId,
|
||||||
|
daterangeRecordTime,
|
||||||
|
deviceCount,
|
||||||
|
deviceDisplayMap,
|
||||||
|
deviceSelectionSummary,
|
||||||
|
deviceStats,
|
||||||
|
energySelectionSummary,
|
||||||
|
filteredMonitorTreeOptions,
|
||||||
|
formatPercent,
|
||||||
|
formatValue,
|
||||||
|
getBands,
|
||||||
|
getMetricStats,
|
||||||
|
getMetricValues,
|
||||||
|
getMonitorDisplayName,
|
||||||
|
getShortDisplayName,
|
||||||
|
handleQuery,
|
||||||
|
handleScopeChange,
|
||||||
|
handleTreeNodeClick,
|
||||||
|
hasMultiDevice,
|
||||||
|
initialize,
|
||||||
|
loading,
|
||||||
|
metricCards,
|
||||||
|
metricConfigs,
|
||||||
|
metricDescription,
|
||||||
|
monitorTreeOptions,
|
||||||
|
monitorTypeLabel,
|
||||||
|
primaryMetric,
|
||||||
|
primaryMetricFoot,
|
||||||
|
queryForm,
|
||||||
|
rawRecords,
|
||||||
|
reportReady,
|
||||||
|
selectedScopeKeys,
|
||||||
|
selectionSummary,
|
||||||
|
treeLoading,
|
||||||
|
availableMetricHints: metricHints
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue