You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
639 lines
21 KiB
TypeScript
639 lines
21 KiB
TypeScript
|
3 months ago
|
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
|
||
|
|
};
|
||
|
|
};
|