feat(sys_oper_log): 新增操作日志备注字段

main
zangch@mesnac.com 1 month ago
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
});
}

@ -4,6 +4,7 @@ export interface OperLogQuery extends PageQuery {
operName: string;
businessType: string;
status: string;
operRemark: string;
orderByColumn: string;
isAsc: string;
}
@ -28,6 +29,7 @@ export interface OperLogVO extends BaseEntity {
errorMsg: string;
operTime: string;
costTime: number;
operRemark: string;
}
export interface OperLogForm {
@ -50,4 +52,5 @@ export interface OperLogForm {
errorMsg: string;
operTime: string;
costTime: number;
operRemark: string;
}

@ -108,7 +108,7 @@ const initFormData: EmsBaseLocationJsonForm = {
id: undefined,
json: undefined,
remark: undefined,
deptId: undefined
deptId: undefined,
}
const data = reactive<PageData<EmsBaseLocationJsonForm, EmsBaseLocationJsonQuery>>({
form: {...initFormData},

@ -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
};
};

@ -7,6 +7,9 @@
<el-form-item label="操作地址" prop="operIp">
<el-input v-model="queryParams.operIp" placeholder="请输入操作地址" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="备注" prop="operRemark">
<el-input v-model="queryParams.operRemark" placeholder="请输入备注" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="系统模块" prop="title">
<el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable @keyup.enter="handleQuery" />
</el-form-item>
@ -73,6 +76,7 @@
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="日志编号" align="center" prop="operId" />
<el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
<el-table-column label="备注" align="center" prop="operRemark" :show-overflow-tooltip="true" />
<el-table-column label="操作类型" align="center" prop="businessType">
<template #default="scope">
<dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
@ -178,6 +182,7 @@ const data = reactive<PageData<OperLogForm, OperLogQuery>>({
operName: '',
businessType: '',
status: '',
operRemark: '',
orderByColumn: defaultSort.value.prop,
isAsc: defaultSort.value.order
},

@ -16,6 +16,11 @@
<el-descriptions-item label="操作模块">
<template #default> {{ info.title }} / {{ typeFormat(info) }} </template>
</el-descriptions-item>
<el-descriptions-item v-if="info.operRemark" label="操作备注">
<template #default>
<span>{{ info.operRemark }}</span>
</template>
</el-descriptions-item>
<el-descriptions-item label="操作方法">
<template #default>
{{ info.method }}

Loading…
Cancel
Save