|
|
|
|
|
<template>
|
|
|
|
|
|
<div v-loading="loading" class="app-container vibration-board-page">
|
|
|
|
|
|
<VibrationBoardFilter
|
|
|
|
|
|
:selection-label="queryForm.selectionLabel"
|
|
|
|
|
|
:time-range="daterangeRecordTime"
|
|
|
|
|
|
:sampling-interval="queryForm.samplingInterval"
|
|
|
|
|
|
:vibration-param="queryForm.vibrationParam"
|
|
|
|
|
|
@update:time-range="setTimeRange"
|
|
|
|
|
|
@update:sampling-interval="setSamplingInterval"
|
|
|
|
|
|
@update:vibration-param="setVibrationParam"
|
|
|
|
|
|
@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="bandTip" />
|
|
|
|
|
|
|
|
|
|
|
|
<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="ADVANCED_HELP" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div ref="sankeyChartRef" class="chart-container-xl" />
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<el-row :gutter="16" class="mt-4 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="ADVANCED_HELP" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div ref="treemapChartRef" 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 { getDisplacementAdvancedData as getVibrationAdvancedData, type VibrationAdvancedPageVO } from '@/api/ems/report/displacementBoard';
|
|
|
|
|
|
import HelpButton from '../components/HelpButton.vue';
|
|
|
|
|
|
import VibrationBoardFilter from '../components/VibrationBoardFilter.vue';
|
|
|
|
|
|
import VibrationBoardDeviceTree from '../components/VibrationBoardDeviceTree.vue';
|
|
|
|
|
|
import { ADVANCED_HELP } from '../components/helpContent';
|
|
|
|
|
|
import { useChartResize } from '../components/useChartResize';
|
|
|
|
|
|
import { useVibrationBoardQueryState } from '../components/useVibrationBoardQueryState';
|
|
|
|
|
|
import { toNumber, vibrationMetricOptions } from '../components/vibrationBoardShared';
|
|
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'VibrationBoardAdvanced' });
|
|
|
|
|
|
|
|
|
|
|
|
const sankeyChartRef = ref<HTMLElement>();
|
|
|
|
|
|
const treemapChartRef = ref<HTMLElement>();
|
|
|
|
|
|
const parallelChartRef = ref<HTMLElement>();
|
|
|
|
|
|
const { getChart } = useChartResize(sankeyChartRef, treemapChartRef, parallelChartRef);
|
|
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
loading,
|
|
|
|
|
|
treeLoading,
|
|
|
|
|
|
monitorTreeOptions,
|
|
|
|
|
|
treeProps,
|
|
|
|
|
|
daterangeRecordTime,
|
|
|
|
|
|
queryForm,
|
|
|
|
|
|
loadTree,
|
|
|
|
|
|
handleTreeNodeClick,
|
|
|
|
|
|
setTimeRange,
|
|
|
|
|
|
setSamplingInterval,
|
|
|
|
|
|
setVibrationParam,
|
|
|
|
|
|
buildQuery
|
|
|
|
|
|
} = useVibrationBoardQueryState();
|
|
|
|
|
|
|
|
|
|
|
|
const advancedData = ref<VibrationAdvancedPageVO>({
|
|
|
|
|
|
sankeyNodes: [],
|
|
|
|
|
|
sankeyLinks: [],
|
|
|
|
|
|
treemapItems: [],
|
|
|
|
|
|
parallelAxes: [],
|
|
|
|
|
|
parallelSeries: []
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const riskBandColorMap: Record<string, string> = {
|
|
|
|
|
|
高位: '#ef4444',
|
|
|
|
|
|
关注: '#f59e0b',
|
|
|
|
|
|
平稳: '#10b981'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const bandTip = computed(() => {
|
|
|
|
|
|
const lowBandUpper = advancedData.value.lowBandUpper;
|
|
|
|
|
|
const focusBandUpper = advancedData.value.focusBandUpper;
|
|
|
|
|
|
if (lowBandUpper === undefined || focusBandUpper === undefined) {
|
|
|
|
|
|
return '高级分析会根据当前位移指标的风险分带,对设备群做迁移与占比分析。';
|
|
|
|
|
|
}
|
|
|
|
|
|
return `当前风险分带建议:平稳 ≤ ${lowBandUpper},关注区 ≤ ${focusBandUpper},其上视为高位。`;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const getNodeColor = (name?: string) => {
|
|
|
|
|
|
if (!name) {
|
|
|
|
|
|
return '#5b8ff9';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (name.includes('高')) {
|
|
|
|
|
|
return riskBandColorMap['高位'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (name.includes('关注') || name.includes('预警')) {
|
|
|
|
|
|
return riskBandColorMap['关注'];
|
|
|
|
|
|
}
|
|
|
|
|
|
return riskBandColorMap['平稳'];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const renderCharts = () => {
|
|
|
|
|
|
const sankeyChart = getChart(sankeyChartRef);
|
|
|
|
|
|
const treemapChart = getChart(treemapChartRef);
|
|
|
|
|
|
const parallelChart = getChart(parallelChartRef);
|
|
|
|
|
|
const sankeyNodes = advancedData.value.sankeyNodes || [];
|
|
|
|
|
|
const sankeyLinks = advancedData.value.sankeyLinks || [];
|
|
|
|
|
|
const treemapItems = advancedData.value.treemapItems || [];
|
|
|
|
|
|
const parallelAxes = advancedData.value.parallelAxes || [];
|
|
|
|
|
|
const parallelSeries = advancedData.value.parallelSeries || [];
|
|
|
|
|
|
|
|
|
|
|
|
if (sankeyChart) {
|
|
|
|
|
|
if (!sankeyNodes.length || !sankeyLinks.length) {
|
|
|
|
|
|
sankeyChart.clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sankeyChart.setOption(
|
|
|
|
|
|
{
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'item',
|
|
|
|
|
|
formatter: (params: any) => {
|
|
|
|
|
|
if (params.dataType === 'edge') {
|
|
|
|
|
|
return `${params.data.source} → ${params.data.target}<br/>流量: <b>${params.data.value}</b>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
return `${params.name}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'sankey',
|
|
|
|
|
|
left: 16,
|
|
|
|
|
|
right: 24,
|
|
|
|
|
|
top: 24,
|
|
|
|
|
|
bottom: 16,
|
|
|
|
|
|
nodeGap: 26,
|
|
|
|
|
|
emphasis: { focus: 'adjacency' },
|
|
|
|
|
|
data: sankeyNodes.map((item) => ({
|
|
|
|
|
|
name: item.name,
|
|
|
|
|
|
itemStyle: { color: getNodeColor(item.name) }
|
|
|
|
|
|
})),
|
|
|
|
|
|
links: sankeyLinks.map((item) => ({
|
|
|
|
|
|
source: item.source,
|
|
|
|
|
|
target: item.target,
|
|
|
|
|
|
value: toNumber(item.value)
|
|
|
|
|
|
})),
|
|
|
|
|
|
lineStyle: {
|
|
|
|
|
|
color: 'gradient',
|
|
|
|
|
|
curveness: 0.52,
|
|
|
|
|
|
opacity: 0.5
|
|
|
|
|
|
},
|
|
|
|
|
|
label: { color: '#334155', fontSize: 12 }
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (treemapChart) {
|
|
|
|
|
|
if (!treemapItems.length) {
|
|
|
|
|
|
treemapChart.clear();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
treemapChart.setOption(
|
|
|
|
|
|
{
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
formatter: (params: any) =>
|
|
|
|
|
|
`${params.name}<br/>值: <b>${Number(params.value).toFixed(2)}</b><br/>风险带: <b>${params.data.levelTag || '--'}</b>`
|
|
|
|
|
|
},
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: 'treemap',
|
|
|
|
|
|
roam: false,
|
|
|
|
|
|
breadcrumb: { show: false },
|
|
|
|
|
|
visibleMin: 8,
|
|
|
|
|
|
data: treemapItems.map((item) => ({
|
|
|
|
|
|
name: item.name,
|
|
|
|
|
|
value: toNumber(item.value),
|
|
|
|
|
|
levelTag: item.levelTag,
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: riskBandColorMap[item.levelTag || ''] || '#5b8ff9',
|
|
|
|
|
|
borderColor: '#fff',
|
|
|
|
|
|
borderWidth: 2,
|
|
|
|
|
|
gapWidth: 2
|
|
|
|
|
|
}
|
|
|
|
|
|
})),
|
|
|
|
|
|
upperLabel: { show: false },
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
color: '#fff',
|
|
|
|
|
|
formatter: (params: any) => `${params.name}\n${Number(params.value).toFixed(1)}`
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
},
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (parallelChart) {
|
|
|
|
|
|
if (!parallelAxes.length || !parallelSeries.length) {
|
|
|
|
|
|
parallelChart.clear();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
parallelChart.setOption(
|
|
|
|
|
|
{
|
|
|
|
|
|
color: [...vibrationMetricOptions.map((item) => item.color), '#8b5cf6', '#64748b'] as string[],
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'item'
|
|
|
|
|
|
},
|
|
|
|
|
|
legend: {
|
|
|
|
|
|
type: 'scroll',
|
|
|
|
|
|
top: 8,
|
|
|
|
|
|
data: parallelSeries.map((item) => item.monitorName || item.monitorId || '--'),
|
|
|
|
|
|
textStyle: { color: '#475569' }
|
|
|
|
|
|
},
|
|
|
|
|
|
parallelAxis: parallelAxes.map((item) => ({
|
|
|
|
|
|
dim: toNumber(item.dim),
|
|
|
|
|
|
name: item.name,
|
|
|
|
|
|
max: Math.max(toNumber(item.max), 1),
|
|
|
|
|
|
nameLocation: 'end',
|
|
|
|
|
|
nameGap: 18,
|
|
|
|
|
|
axisLabel: { color: '#64748b' }
|
|
|
|
|
|
})),
|
|
|
|
|
|
parallel: {
|
|
|
|
|
|
left: 52,
|
|
|
|
|
|
right: 24,
|
|
|
|
|
|
top: 64,
|
|
|
|
|
|
bottom: 42,
|
|
|
|
|
|
parallelAxisDefault: {
|
|
|
|
|
|
type: 'value',
|
|
|
|
|
|
axisLine: { lineStyle: { color: '#cbd5e1' } },
|
|
|
|
|
|
axisTick: { show: false },
|
|
|
|
|
|
splitLine: { lineStyle: { type: 'dashed', color: 'rgba(148, 163, 184, 0.3)' } }
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
series: parallelSeries.map((item) => ({
|
|
|
|
|
|
name: item.monitorName || item.monitorId || '--',
|
|
|
|
|
|
type: 'parallel',
|
|
|
|
|
|
smooth: 0.2,
|
|
|
|
|
|
lineStyle: { width: 1.6, opacity: 0.42 },
|
|
|
|
|
|
emphasis: { lineStyle: { width: 3, opacity: 0.95 } },
|
|
|
|
|
|
data: [(item.values || []).map((value) => toNumber(value))]
|
|
|
|
|
|
}))
|
|
|
|
|
|
},
|
|
|
|
|
|
true
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleQuery = async () => {
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await getVibrationAdvancedData(buildQuery());
|
|
|
|
|
|
advancedData.value = data || {
|
|
|
|
|
|
sankeyNodes: [],
|
|
|
|
|
|
sankeyLinks: [],
|
|
|
|
|
|
treemapItems: [],
|
|
|
|
|
|
parallelAxes: [],
|
|
|
|
|
|
parallelSeries: []
|
|
|
|
|
|
};
|
|
|
|
|
|
await nextTick();
|
|
|
|
|
|
renderCharts();
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await loadTree();
|
|
|
|
|
|
await handleQuery();
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
@import '../components/vibrationBoardReport.scss';
|
|
|
|
|
|
</style>
|