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 { 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) => { return String(node.id || node.code || node.label || Math.random()); }; const cloneTreeByRootKeys = (nodes: Array>, selectedKeys: string[]) => { return (nodes || []).filter((item) => selectedKeys.includes(resolveNodeKey(item))); }; const getLeafNodes = (nodes: Array>, 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>, currentRootKey)); return; } if (item.code) { result.push({ ...item, __rootKey: currentRootKey }); } }); return result; }; const buildDeviceDisplayMap = (nodes: Array>) => { 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); }; const findRootKeyByNodeId = (nodes: Array>, 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>, 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>>([]); const deviceDisplayMap = ref>({}); const selectedScopeKeys = ref([]); const currentTreeNodeId = ref(''); const rawRecords = ref([]); const daterangeRecordTime = ref(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); }); 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(() => { return selectedLeafNodes.value.flatMap((item) => buildMetricHintsByNode(item)); }); const metricConfigs = computed(() => { return resolveIotCurveMetrics(rawRecords.value, metricHints.value); }); const primaryMetric = computed(() => { 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(() => { 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(() => { if (!primaryMetric.value) { return []; } const deviceMap = {} as Record; 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; 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 = { 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) => { 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 }; };