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.

224 lines
7.2 KiB
Vue

<template>
<div v-loading="loading" class="app-container">
<VibrationBoardFilter
:selection-label="queryForm.selectionLabel"
:time-range="daterangeRecordTime"
:sampling-interval="queryForm.samplingInterval"
:vibration-param="queryForm.vibrationParam"
show-metric
@update:time-range="setTimeRange"
@update:sampling-interval="setSamplingInterval"
@update:vibration-param="setVibrationParam"
@query="handleQuery"
/>
<el-row :gutter="16" class="mt-4">
<el-col :span="6"
><el-card
><div class="kpi-label">采样记录数</div>
<div class="kpi-value">{{ toNumber(overviewData.sampleCount) }}</div></el-card
></el-col
>
<el-col :span="6"
><el-card
><div class="kpi-label">设备数量</div>
<div class="kpi-value">{{ toNumber(overviewData.deviceCount) }}</div></el-card
></el-col
>
<el-col :span="6"
><el-card
><div class="kpi-label">主看指标</div>
<div class="kpi-value">{{ overviewData.metricLabel || '--' }}</div></el-card
></el-col
>
<el-col :span="6"
><el-card
><div class="kpi-label">覆盖率</div>
<div class="kpi-value">{{ formatPercent(toNumber(overviewData.coverageRate)) }}</div></el-card
></el-col
>
</el-row>
<el-row :gutter="16" class="mt-4">
<el-col :span="6" v-for="item in overviewData.metricCards || []" :key="item.field">
<el-card class="metric-card" :style="{ borderTopColor: metricColorMap[item.field || ''] || '#5b8ff9' }">
<div class="kpi-label">{{ item.label }}</div>
<div class="kpi-value">
{{ formatValue(item.latest) }}<span class="unit">{{ item.unit }}</span>
</div>
<div class="metric-foot">均值 {{ formatValue(item.avg) }}{{ item.unit }}</div>
<div class="metric-foot">峰值 {{ formatValue(item.max) }}{{ item.unit }}</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="16" class="mt-4">
<el-col :span="6">
<VibrationBoardDeviceTree :loading="treeLoading" :tree-data="monitorTreeOptions" :tree-props="treeProps" @node-click="handleTreeNodeClick" />
</el-col>
<el-col :span="18">
<el-row :gutter="16">
<el-col :span="12"
><el-card><template #header>仪表盘总览</template><Chart ref="gaugeChart" class="chart-lg" /></el-card
></el-col>
<el-col :span="12"
><el-card
><template #header>{{ hasMultiDevice ? '设备排名对比图' : '主看指标分布图' }}</template
><Chart ref="rankChart" class="chart-lg" /></el-card
></el-col>
</el-row>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import Chart from '@/components/Charts/Chart.vue';
import { getVibrationOverviewData, type VibrationOverviewPageVO } from '@/api/ems/report/vibrationBoard';
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
import { vibrationMetricOptions } from '../components/vibrationBoardShared';
defineOptions({ name: 'VibrationBoardOverview' });
const gaugeChart = ref<any>();
const rankChart = ref<any>();
const {
loading,
treeLoading,
monitorTreeOptions,
treeProps,
daterangeRecordTime,
queryForm,
deviceCount,
hasMultiDevice,
loadTree,
handleTreeNodeClick,
setTimeRange,
setSamplingInterval,
setVibrationParam,
buildQuery
} = useVibrationBoardQueryState();
const overviewData = ref<VibrationOverviewPageVO>({ metricCards: [], gaugeItems: [], deviceRanks: [] });
const metricColorMap = Object.fromEntries(vibrationMetricOptions.map((item) => [item.field, item.color]));
const toNumber = (value: number | string | undefined) => Number(value ?? 0);
const formatValue = (value: number | string | undefined) => (value === null || value === undefined || value === '' ? '--' : Number(value).toFixed(2));
const formatPercent = (value: number) => `${(Number(value || 0) * 100).toFixed(1)}%`;
const renderCharts = () => {
const gaugeItems = overviewData.value.gaugeItems || [];
const deviceRanks = overviewData.value.deviceRanks || [];
if (gaugeItems.length) {
gaugeChart.value?.setData({
series: gaugeItems.map((item, index) => ({
type: 'gauge',
min: 0,
max: toNumber(item.maxValue),
radius: '78%',
center: [
['18%', '55%'],
['50%', '55%'],
['82%', '55%']
][index] || ['50%', '55%'],
progress: { show: true, width: 12, itemStyle: { color: ['#5b8ff9', '#36cfc9', '#f59e0b'][index] || '#5b8ff9' } },
axisLine: { lineStyle: { width: 12 } },
pointer: { show: false },
axisTick: { show: false },
splitLine: { length: 10 },
axisLabel: { distance: 16, fontSize: 10 },
title: { offsetCenter: [0, '82%'], fontSize: 12 },
detail: { offsetCenter: [0, '22%'], fontSize: 18, formatter: `${formatValue(item.value)} ${item.unit || ''}` },
data: [{ value: toNumber(item.value), name: item.name }]
}))
});
} else {
// 空结果时同步清空仪表盘,避免 KPI 已回到空态但图表仍停留在上一轮查询。
gaugeChart.value?.setData(null);
}
if (hasMultiDevice.value) {
if (!deviceRanks.length) {
rankChart.value?.setData(null);
return;
}
rankChart.value?.setData({
tooltip: { trigger: 'axis' },
xAxis: { type: 'value' },
yAxis: { type: 'category', data: deviceRanks.map((item) => item.monitorName) },
series: [
{
type: 'bar',
data: deviceRanks.map((item) => toNumber(item.avg)),
itemStyle: { color: metricColorMap[queryForm.value.vibrationParam] || '#5b8ff9' }
}
]
});
return;
}
const stats = overviewData.value.primaryMetricStats;
if (!stats) {
rankChart.value?.setData(null);
return;
}
rankChart.value?.setData({
xAxis: { type: 'category', data: ['最小值', '均值', '最大值'] },
yAxis: { type: 'value', name: overviewData.value.unit || '' },
series: [
{
type: 'bar',
data: [toNumber(stats.min), toNumber(stats.avg), toNumber(stats.max)],
itemStyle: { color: metricColorMap[queryForm.value.vibrationParam] || '#5b8ff9' }
}
]
});
};
const handleQuery = async () => {
loading.value = true;
try {
const { data } = await getVibrationOverviewData(buildQuery());
overviewData.value = data || { metricCards: [], gaugeItems: [], deviceRanks: [] };
await nextTick();
renderCharts();
} finally {
loading.value = false;
}
};
onMounted(async () => {
await loadTree();
await handleQuery();
});
</script>
<style scoped>
.kpi-label {
color: #5b6573;
font-size: 13px;
}
.kpi-value {
margin-top: 12px;
font-size: 26px;
font-weight: 700;
color: #1f2329;
}
.metric-card {
border-top: 4px solid #5b8ff9;
}
.metric-foot {
margin-top: 8px;
font-size: 12px;
color: #86909c;
}
.unit {
margin-left: 4px;
font-size: 14px;
}
.chart-lg {
height: 420px;
}
</style>