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.

300 lines
9.6 KiB
Vue

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