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.

302 lines
9.6 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 displacement-board-page">
<DisplacementBoardFilter
: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="setDisplacementParam"
@query="handleQuery"
/>
<el-row :gutter="16" class="mt-4 displacement-grid">
<el-col :xs="24" :lg="7" :xl="6">
<DisplacementBoardDeviceTree :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 displacement-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, type DisplacementAdvancedPageVO } from '@/api/ems/report/displacementBoard';
import HelpButton from '../components/DisplacementHelpButton.vue';
import DisplacementBoardFilter from '../components/DisplacementBoardFilter.vue';
import DisplacementBoardDeviceTree from '../components/DisplacementBoardDeviceTree.vue';
import { ADVANCED_HELP } from '../components/helpContent';
import { useChartResize } from '../components/useChartResize';
import { useDisplacementBoardQueryState } from '../components/useDisplacementBoardQueryState';
import { toNumber, displacementMetricOptions } from '../components/displacementBoardShared';
defineOptions({ name: 'DisplacementBoardAdvanced' });
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,
setDisplacementParam,
buildQuery
} = useDisplacementBoardQueryState();
const advancedData = ref<DisplacementAdvancedPageVO>({
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: [...displacementMetricOptions.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 getDisplacementAdvancedData(buildQuery());
advancedData.value = data || {
sankeyNodes: [],
sankeyLinks: [],
treemapItems: [],
parallelAxes: [],
parallelSeries: []
};
await nextTick();
renderCharts();
} finally {
loading.value = false;
}
};
onMounted(() => {
void Promise.all([loadTree(), handleQuery()]);
});
</script>
<style scoped lang="scss">
@import '../components/displacementBoardReport.scss';
</style>