|
|
<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>
|