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.

267 lines
9.4 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div v-loading="loading" class="app-container vibration-board-page">
<VibrationBoardFilter
:selection-label="queryForm.selectionLabel"
:time-range="daterangeRecordTime"
:sampling-interval="queryForm.samplingInterval"
@update:time-range="setTimeRange"
@update:sampling-interval="setSamplingInterval"
@query="handleQuery"
/>
<el-row :gutter="16" class="mt-4 vibration-grid">
<el-col :xs="24" :lg="7" :xl="6">
<VibrationBoardDeviceTree :loading="treeLoading" :tree-data="monitorTreeOptions" :tree-props="treeProps" @node-click="handleTreeNodeClick" />
</el-col>
<el-col :xs="24" :lg="17" :xl="18">
<el-alert
type="info"
:closable="false"
show-icon
class="section-tip"
title="对比分析建议选择设备组或全部位移设备,这样横向排名与散点聚类才更有参考价值。"
/>
<el-row :gutter="16" class="vibration-grid">
<el-col :xs="24" :xl="12">
<el-card shadow="never" class="chart-card">
<template #header>
<div class="card-header">
<div class="card-title-group">
<div class="card-title">设备排名对比图</div>
<div class="card-subtitle">同时对比位移均值和最新值,适合快速筛出“当前状态已经偏离历史常态”的设备。</div>
</div>
<HelpButton title="设备排名对比图" :content="COMPARISON_HELP" />
</div>
</template>
<div ref="rankChartRef" class="chart-container-lg" />
</el-card>
</el-col>
<el-col :xs="24" :xl="12">
<el-card shadow="never" class="chart-card">
<template #header>
<div class="card-header">
<div class="card-title-group">
<div class="card-title">设备散点图</div>
<div class="card-subtitle">横轴看均值纵轴看峰值气泡越大表示样本量越充足判断更稳</div>
</div>
<HelpButton title="设备散点图" :content="COMPARISON_HELP" />
</div>
</template>
<div ref="scatterChartRef" class="chart-container-lg" />
</el-card>
</el-col>
</el-row>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import * as echarts from 'echarts';
import { getDisplacementComparisonData, type DisplacementComparisonPageVO } from '@/api/ems/report/displacementBoard';
import HelpButton from '../components/HelpButton.vue';
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
import { COMPARISON_HELP } from '../components/helpContent';
import { useChartResize } from '../components/useChartResize';
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
import { buildCategoryDataZoom, getVibrationMetricOption, toNumber, vibrationMetricOptions } from '../components/vibrationBoardShared';
defineOptions({ name: 'VibrationBoardComparison' });
const rankChartRef = ref<HTMLElement>();
const scatterChartRef = ref<HTMLElement>();
const { getChart } = useChartResize(rankChartRef, scatterChartRef);
const {
loading,
treeLoading,
monitorTreeOptions,
treeProps,
daterangeRecordTime,
queryForm,
loadTree,
handleTreeNodeClick,
setTimeRange,
setSamplingInterval,
buildQuery
} = useVibrationBoardQueryState();
const comparisonData = ref<DisplacementComparisonPageVO>({ rankItems: [], scatterItems: [] });
const metricColorMap = Object.fromEntries(vibrationMetricOptions.map((item) => [item.field, item.color]));
const activeMetric = computed(() => getVibrationMetricOption(comparisonData.value.metricField));
const renderCharts = () => {
const rankChart = getChart(rankChartRef);
const scatterChart = getChart(scatterChartRef);
const unit = comparisonData.value.unit || activeMetric.value.unit;
const rankItems = comparisonData.value.rankItems || [];
const scatterItems = comparisonData.value.scatterItems || [];
const currentMetricColor = metricColorMap[comparisonData.value.metricField || ''] || activeMetric.value.color;
if (rankChart) {
if (!rankItems.length) {
rankChart.clear();
} else {
const categoryNames = rankItems.map((item) => item.monitorName || item.monitorId || '--');
rankChart.setOption(
{
color: [currentMetricColor, '#36cfc9'],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any[]) => {
const rows = params.map((item) => `${item.marker}${item.seriesName}: <b>${Number(item.value).toFixed(2)} ${unit}</b>`);
return [`<b>${params[0]?.name || '--'}</b>`, ...rows].join('<br/>');
}
},
legend: {
top: 8,
data: ['均值', '最新值'],
textStyle: { color: '#475569' }
},
grid: { left: 150, right: 28, top: 56, bottom: 20 },
xAxis: {
type: 'value',
name: unit,
splitLine: { lineStyle: { type: 'dashed', color: 'rgba(148, 163, 184, 0.35)' } },
axisLabel: { color: '#64748b' }
},
yAxis: {
type: 'category',
data: categoryNames,
axisLabel: { color: '#334155', width: 110, overflow: 'truncate' }
},
dataZoom: buildCategoryDataZoom(categoryNames.length),
series: [
{
name: '均值',
type: 'bar',
barMaxWidth: 16,
data: rankItems.map((item) => toNumber(item.avg)),
itemStyle: {
borderRadius: [0, 6, 6, 0],
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: currentMetricColor },
{ offset: 1, color: `${currentMetricColor}55` }
])
}
},
{
name: '最新值',
type: 'bar',
barMaxWidth: 16,
data: rankItems.map((item) => toNumber(item.latest)),
itemStyle: {
borderRadius: [0, 6, 6, 0],
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#36cfc9' },
{ offset: 1, color: '#a7f3d0' }
])
}
}
]
},
true
);
}
}
if (scatterChart) {
if (!scatterItems.length) {
scatterChart.clear();
return;
}
const sampleCounts = scatterItems.map((item) => toNumber(item.sampleCount));
scatterChart.setOption(
{
tooltip: {
formatter: (params: any) =>
[
`<b>${params.value[3]}</b>`,
`均值: ${Number(params.value[0]).toFixed(2)} ${unit}`,
`峰值: ${Number(params.value[1]).toFixed(2)} ${unit}`,
`采样量: ${params.value[2]}`
].join('<br/>')
},
visualMap: {
min: sampleCounts.length ? Math.min(...sampleCounts) : 0,
max: sampleCounts.length ? Math.max(...sampleCounts) : 0,
dimension: 2,
orient: 'horizontal',
left: 'center',
bottom: 6,
calculable: true,
text: ['采样量高', '采样量低'],
inRange: {
symbolSize: [14, 42],
color: ['#dbeafe', currentMetricColor]
}
},
grid: { left: 60, right: 24, top: 24, bottom: 80 },
xAxis: {
type: 'value',
name: `均值(${unit}`,
splitLine: { lineStyle: { type: 'dashed', color: 'rgba(148, 163, 184, 0.35)' } },
axisLabel: { color: '#64748b' }
},
yAxis: {
type: 'value',
name: `峰值(${unit}`,
splitLine: { lineStyle: { type: 'dashed', color: 'rgba(148, 163, 184, 0.35)' } },
axisLabel: { color: '#64748b' }
},
series: [
{
name: '设备散点',
type: 'scatter',
emphasis: { focus: 'series' },
label: {
show: true,
position: 'top',
color: '#334155',
fontSize: 11,
formatter: (params: any) => params.value[3]
},
itemStyle: {
shadowBlur: 12,
shadowColor: 'rgba(15, 23, 42, 0.14)'
},
data: scatterItems.map((item) => [
toNumber(item.avg),
toNumber(item.max),
toNumber(item.sampleCount),
item.monitorName || item.monitorId || '--'
])
}
]
},
true
);
}
};
const handleQuery = async () => {
loading.value = true;
try {
const { data } = await getDisplacementComparisonData(buildQuery());
comparisonData.value = data || { rankItems: [], scatterItems: [] };
await nextTick();
// 对比页经常在切换设备树后快速重复查询,这里统一在下一帧重绘,避免旧尺寸和旧图层残留。
renderCharts();
} finally {
loading.value = false;
}
};
onMounted(async () => {
await loadTree();
await handleQuery();
});
</script>
<style scoped lang="scss">
@import '../components/vibrationBoardReport.scss';
</style>