feat:自定义网元指标概览
This commit is contained in:
@@ -1090,6 +1090,12 @@ export default {
|
|||||||
"layout2": "Layout 2",
|
"layout2": "Layout 2",
|
||||||
"layout3": "Layout 3"
|
"layout3": "Layout 3"
|
||||||
},
|
},
|
||||||
|
kpiOverView:{
|
||||||
|
"changeLine":"Change to Line Charts",
|
||||||
|
"changeBar":"Change to Bar Charts",
|
||||||
|
"chooseShowMetrics":"Select the metric you want to display",
|
||||||
|
"chooseMetrics":"Select an indicator",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
traceManage: {
|
traceManage: {
|
||||||
analysis: {
|
analysis: {
|
||||||
|
|||||||
@@ -1090,6 +1090,12 @@ export default {
|
|||||||
"layout2": "布局2",
|
"layout2": "布局2",
|
||||||
"layout3": "布局3"
|
"layout3": "布局3"
|
||||||
},
|
},
|
||||||
|
kpiOverView:{
|
||||||
|
"changeLine":"切换为折线图",
|
||||||
|
"changeBar":"切换为柱状图",
|
||||||
|
"chooseShowMetrics":"选择需要显示的指标",
|
||||||
|
"chooseMetrics":"选择指标",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
traceManage: {
|
traceManage: {
|
||||||
analysis: {
|
analysis: {
|
||||||
|
|||||||
@@ -1,16 +1,738 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import { onMounted } from 'vue';
|
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue';
|
||||||
import { PageContainer } from 'antdv-pro-layout';
|
import * as echarts from 'echarts/core';
|
||||||
|
import { LegendComponent } from 'echarts/components';
|
||||||
|
import { LineChart, BarChart } from 'echarts/charts';
|
||||||
|
import {
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
TitleComponent,
|
||||||
|
} from 'echarts/components';
|
||||||
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
|
import { getKPITitle, listKPIData } from '@/api/perfManage/goldTarget';
|
||||||
|
import useI18n from '@/hooks/useI18n';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import {
|
||||||
|
RESULT_CODE_ERROR,
|
||||||
|
RESULT_CODE_SUCCESS,
|
||||||
|
} from '@/constants/result-constants';
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||||
|
import { generateColorRGBA } from '@/utils/generate-utils';
|
||||||
|
import {
|
||||||
|
BarChartOutlined,
|
||||||
|
LineChartOutlined,
|
||||||
|
UnorderedListOutlined,
|
||||||
|
} from '@ant-design/icons-vue';
|
||||||
|
|
||||||
onMounted(() => {});
|
// 在这里定义 ChartDataItem 接口
|
||||||
|
interface ChartDataItem {
|
||||||
|
date: string; // 将存储完整的时间字符串,包含时分秒
|
||||||
|
[kpiId: string]: string | number; // 动态指标值
|
||||||
|
}
|
||||||
|
|
||||||
|
echarts.use([
|
||||||
|
LineChart,
|
||||||
|
BarChart,
|
||||||
|
GridComponent,
|
||||||
|
TooltipComponent,
|
||||||
|
TitleComponent,
|
||||||
|
CanvasRenderer,
|
||||||
|
LegendComponent,
|
||||||
|
]);
|
||||||
|
// WebSocket连接
|
||||||
|
const ws = ref<WS | null>(null);
|
||||||
|
|
||||||
|
//日期范围响应式变量
|
||||||
|
const dateRange = ref<[string, string]>([
|
||||||
|
dayjs().startOf('day').valueOf().toString(),
|
||||||
|
dayjs().valueOf().toString(),
|
||||||
|
]);
|
||||||
|
//实时数据状态
|
||||||
|
const isRealtime = ref(false);
|
||||||
|
//图表数据响应式数组
|
||||||
|
const chartData = ref<ChartDataItem[]>([]);
|
||||||
|
|
||||||
|
//储存Echarts的实例的变量
|
||||||
|
let chart: echarts.ECharts | null = null;
|
||||||
|
|
||||||
|
//observer 变量 监听图表容器大小
|
||||||
|
let observer: ResizeObserver | null = null;
|
||||||
|
|
||||||
|
//日期变化时触发数据变化时触发
|
||||||
|
const handleDateChange = (
|
||||||
|
value: [string, string] | [Dayjs, Dayjs],
|
||||||
|
dateStrings: [string, string]
|
||||||
|
) => {
|
||||||
|
if (!dateStrings[0] || !dateStrings[1]) {
|
||||||
|
console.warn('Invalid date strings:', dateStrings);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dateRange.value = [
|
||||||
|
dayjs(dateStrings[0]).valueOf().toString(),
|
||||||
|
dayjs(dateStrings[1]).valueOf().toString(),
|
||||||
|
];
|
||||||
|
|
||||||
|
fetchChartData();
|
||||||
|
};
|
||||||
|
|
||||||
|
//切换实时数据方法
|
||||||
|
const toggleRealtime = () => {
|
||||||
|
fnRealTimeSwitch(isRealtime.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 定义所有网元类型
|
||||||
|
const ALL_NE_TYPES = ['AMF', 'SMF', 'UPF', 'MME', 'IMS', 'SMSC'] as const;
|
||||||
|
type NeType = (typeof ALL_NE_TYPES)[number];
|
||||||
|
|
||||||
|
// 定义要筛选的指标 ID,按网元类型组织
|
||||||
|
const TARGET_KPI_IDS: Record<NeType, string[]> = {
|
||||||
|
AMF: ['AMF.02', 'AMF.03', 'AMF.A.07', 'AMF.A.08'],
|
||||||
|
SMF: ['SMF.02', 'SMF.03', 'SMF.04', 'SMF.05'],
|
||||||
|
UPF: ['UPF.03', 'UPF.04', 'UPF.05', 'UPF.06'],
|
||||||
|
MME: ['MME.A.01', 'MME.A.02', 'MME.A.03'],
|
||||||
|
IMS: ['SCSCF.01', 'SCSCF.02', 'SCSCF.05', 'SCSCF.06'],
|
||||||
|
SMSC: ['SMSC.A.01', 'SMSC.A.02', 'SMSC.A.03'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 实时数据开关函数
|
||||||
|
const fnRealTimeSwitch = (bool: boolean) => {
|
||||||
|
if (bool) {
|
||||||
|
if (!ws.value) {
|
||||||
|
ws.value = new WS();
|
||||||
|
}
|
||||||
|
chartData.value = [];
|
||||||
|
|
||||||
|
const options: OptionsType = {
|
||||||
|
url: '/ws',
|
||||||
|
params: {
|
||||||
|
subGroupID: ALL_NE_TYPES.map(type => `10_${type}_001`).join(','),
|
||||||
|
},
|
||||||
|
onmessage: wsMessage,
|
||||||
|
onerror: wsError,
|
||||||
|
};
|
||||||
|
ws.value.connect(options);
|
||||||
|
} else if (ws.value) {
|
||||||
|
ws.value.close(); //断开链接
|
||||||
|
ws.value = null; //清空链接
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 接收数据后错误回调
|
||||||
|
const wsError = () => {
|
||||||
|
message.error(t('common.websocketError'));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 收数据回调
|
||||||
|
const wsMessage = (res: Record<string, any>) => {
|
||||||
|
const { code, data } = res;
|
||||||
|
if (code === RESULT_CODE_ERROR) {
|
||||||
|
console.warn(res.msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data?.groupId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const kpiEvent = data.data;
|
||||||
|
if (!kpiEvent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构造新的数据点
|
||||||
|
const newData: ChartDataItem = {
|
||||||
|
date: kpiEvent.timeGroup
|
||||||
|
? kpiEvent.timeGroup.toString()
|
||||||
|
: Date.now().toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 只添加已选中的指标数据
|
||||||
|
selectedKPIs.value.forEach(kpiId => {
|
||||||
|
if (kpiEvent[kpiId] !== undefined) {
|
||||||
|
newData[kpiId] = Number(kpiEvent[kpiId]);
|
||||||
|
} else {
|
||||||
|
newData[kpiId] = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
updateChartData(newData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取图表数据
|
||||||
|
const fetchChartData = async () => {
|
||||||
|
if (kpiColumns.value.length === 0) {
|
||||||
|
console.warn('No KPI columns available');
|
||||||
|
updateChart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [startTime, endTime] = dateRange.value;
|
||||||
|
|
||||||
|
if (!startTime || !endTime) {
|
||||||
|
console.warn('Invalid date range:', dateRange.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allData: any[] = [];
|
||||||
|
|
||||||
|
// 使用 ALL_NE_TYPES 遍历网元类型
|
||||||
|
for (const neType of ALL_NE_TYPES) {
|
||||||
|
const params = {
|
||||||
|
neType,
|
||||||
|
neId: '001',
|
||||||
|
startTime: String(startTime),
|
||||||
|
endTime: String(endTime),
|
||||||
|
sortField: 'timeGroup',
|
||||||
|
sortOrder: 'asc',
|
||||||
|
interval: 5,
|
||||||
|
kpiIds: TARGET_KPI_IDS[neType].join(','),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await listKPIData(params);
|
||||||
|
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||||
|
allData.push(...res.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按时间分组合并数据
|
||||||
|
const groupedData = new Map<string, any>();
|
||||||
|
allData.forEach(item => {
|
||||||
|
const timeKey = item.timeGroup;
|
||||||
|
if (!groupedData.has(timeKey)) {
|
||||||
|
groupedData.set(timeKey, { timeGroup: timeKey });
|
||||||
|
}
|
||||||
|
const existingData = groupedData.get(timeKey);
|
||||||
|
Object.assign(existingData, item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 直接将处理后的数据赋值给 chartData.value
|
||||||
|
chartData.value = Array.from(groupedData.values())
|
||||||
|
.sort((a, b) => Number(a.timeGroup) - Number(b.timeGroup))
|
||||||
|
.map(item => {
|
||||||
|
const dataItem: ChartDataItem = {
|
||||||
|
date: item.timeGroup.toString(),
|
||||||
|
};
|
||||||
|
kpiColumns.value.forEach(kpi => {
|
||||||
|
dataItem[kpi.kpiId] = Number(item[kpi.kpiId]) || 0;
|
||||||
|
});
|
||||||
|
return dataItem;
|
||||||
|
});
|
||||||
|
|
||||||
|
updateChart();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch chart data:', error);
|
||||||
|
message.error(t('common.getInfoFail'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加一个 Map 来存储每个指标的固定颜色
|
||||||
|
const kpiColors = new Map<string, string>();
|
||||||
|
|
||||||
|
// 加图表类型的响应式变量
|
||||||
|
const chartType = ref<'line' | 'bar'>('line');
|
||||||
|
|
||||||
|
// 添加切换图表类型的方法
|
||||||
|
const toggleChartType = () => {
|
||||||
|
chartType.value = chartType.value === 'line' ? 'bar' : 'line';
|
||||||
|
updateChart();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改 updateChart 函数中的系列配置
|
||||||
|
const updateChart = () => {
|
||||||
|
if (!chart || !kpiColumns.value.length) return;
|
||||||
|
|
||||||
|
const filteredColumns = kpiColumns.value.filter(col =>
|
||||||
|
selectedKPIs.value.includes(col.kpiId)
|
||||||
|
);
|
||||||
|
const legendData = filteredColumns.map(item => item.title);
|
||||||
|
|
||||||
|
const series = filteredColumns.map(item => {
|
||||||
|
const color = kpiColors.get(item.kpiId) || generateColorRGBA();
|
||||||
|
if (!kpiColors.has(item.kpiId)) {
|
||||||
|
kpiColors.set(item.kpiId, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: item.title,
|
||||||
|
type: chartType.value, // 使用当前选择的图表类型
|
||||||
|
data:
|
||||||
|
chartData.value.length > 0
|
||||||
|
? chartData.value.map(dataItem => dataItem[item.kpiId] || 0)
|
||||||
|
: [0],
|
||||||
|
smooth: chartType.value === 'line', // 只在折线图时使用平滑
|
||||||
|
symbol: chartType.value === 'line' ? 'circle' : undefined, // 只在折线图时显示标记
|
||||||
|
symbolSize: chartType.value === 'line' ? 6 : undefined,
|
||||||
|
showSymbol: chartType.value === 'line',
|
||||||
|
itemStyle: { color },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const option = {
|
||||||
|
title: {
|
||||||
|
text: '网元指标概览',
|
||||||
|
left: 'center',
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt: any) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: legendData,
|
||||||
|
type: 'scroll',
|
||||||
|
orient: 'horizontal',
|
||||||
|
top: 25,
|
||||||
|
textStyle: {
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
selected: Object.fromEntries(legendData.map(name => [name, true])),
|
||||||
|
show: true,
|
||||||
|
left: 'center',
|
||||||
|
width: '80%',
|
||||||
|
height: 50,
|
||||||
|
padding: [5, 10],
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
top: 100,
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data:
|
||||||
|
chartData.value.length > 0
|
||||||
|
? chartData.value.map(item => {
|
||||||
|
// 将时间戳转换为包含时分秒的格式
|
||||||
|
return dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
})
|
||||||
|
: [''],
|
||||||
|
axisLabel: {
|
||||||
|
formatter: (value: string) => {
|
||||||
|
// 自定义 x 轴标签的显示格式
|
||||||
|
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
},
|
||||||
|
rotate: 0,
|
||||||
|
interval: 'auto', // 自动计算显示间隔
|
||||||
|
align: 'right',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
axisLabel: {
|
||||||
|
formatter: '{value}',
|
||||||
|
},
|
||||||
|
// 添加自动计算的分割段数
|
||||||
|
splitNumber: 5,
|
||||||
|
// 添加自动计算的最小/最大值围
|
||||||
|
scale: true,
|
||||||
|
},
|
||||||
|
series: series,
|
||||||
|
};
|
||||||
|
|
||||||
|
chart.setOption(option);
|
||||||
|
chart.resize();
|
||||||
|
|
||||||
|
// 如果已经有 observer,先断开连接
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的 ResizeObserver
|
||||||
|
observer = new ResizeObserver(() => {
|
||||||
|
if (chart) {
|
||||||
|
chart.resize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 观察图表容器
|
||||||
|
const container = document.getElementById('chartContainer');
|
||||||
|
if (container) {
|
||||||
|
observer.observe(container);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//钩子函数
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 获取所有网元的指标
|
||||||
|
await fetchSpecificKPI();
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const container = document.getElementById('chartContainer');
|
||||||
|
if (container && !chart) {
|
||||||
|
chart = echarts.init(container);
|
||||||
|
|
||||||
|
if (kpiColumns.value.length > 0) {
|
||||||
|
updateChart();
|
||||||
|
await fetchChartData();
|
||||||
|
} else {
|
||||||
|
console.warn('No KPI columns available after fetching');
|
||||||
|
}
|
||||||
|
} else if (chart) {
|
||||||
|
console.warn('Chart already initialized, skipping initialization');
|
||||||
|
} else {
|
||||||
|
console.error('Chart container not found');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize:', error);
|
||||||
|
message.error(t('common.initFail'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 定义指标列类型
|
||||||
|
interface KPIColumn {
|
||||||
|
title: string;
|
||||||
|
dataIndex: string;
|
||||||
|
key: string;
|
||||||
|
kpiId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 存储指标列信
|
||||||
|
const kpiColumns = ref<KPIColumn[]>([]);
|
||||||
|
// 添加选中标的的状态
|
||||||
|
const selectedKPIs = ref<string[]>([]);
|
||||||
|
// 添加对话框可见性状态
|
||||||
|
const isModalVisible = ref(false);
|
||||||
|
|
||||||
|
// 打开对话框
|
||||||
|
const showKPISelector = () => {
|
||||||
|
isModalVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加保存选中指标到 localStorage 的方法
|
||||||
|
const saveSelectedKPIs = () => {
|
||||||
|
localStorage.setItem('selectedKPIs', JSON.stringify(selectedKPIs.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 窗口确认按钮
|
||||||
|
const handleModalOk = () => {
|
||||||
|
saveSelectedKPIs(); // 保存选择
|
||||||
|
updateChart();
|
||||||
|
isModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消按钮
|
||||||
|
const handleModalCancel = () => {
|
||||||
|
isModalVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取网元指标
|
||||||
|
const fetchSpecificKPI = async () => {
|
||||||
|
const language =
|
||||||
|
currentLocale.value.split('_')[0] === 'zh'
|
||||||
|
? 'cn'
|
||||||
|
: currentLocale.value.split('_')[0];
|
||||||
|
|
||||||
|
try {
|
||||||
|
let allKPIs: KPIColumn[] = [];
|
||||||
|
|
||||||
|
// 使用 ALL_NE_TYPES 遍历网元类型
|
||||||
|
for (const neType of ALL_NE_TYPES) {
|
||||||
|
const res = await getKPITitle(neType);
|
||||||
|
|
||||||
|
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||||
|
// 过滤当前网元类型的指标
|
||||||
|
const filteredKPIs = res.data.filter(item =>
|
||||||
|
TARGET_KPI_IDS[neType].some(
|
||||||
|
targetId =>
|
||||||
|
item.kpiId === targetId || // 完全匹配
|
||||||
|
item.kpiId.startsWith(targetId) // 前缀匹配
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 转换并添加到总指标列表
|
||||||
|
const formattedKPIs = filteredKPIs.map(item => ({
|
||||||
|
title: item[`${language}Title`],
|
||||||
|
dataIndex: item.kpiId,
|
||||||
|
key: item.kpiId,
|
||||||
|
kpiId: item.kpiId,
|
||||||
|
}));
|
||||||
|
allKPIs = [...allKPIs, ...formattedKPIs];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新指标列信息
|
||||||
|
kpiColumns.value = allKPIs;
|
||||||
|
|
||||||
|
// 尝试加载保存的选择
|
||||||
|
const savedKPIs = localStorage.getItem('selectedKPIs');
|
||||||
|
if (savedKPIs) {
|
||||||
|
// 确保保存的选择仍然存在于前指标中
|
||||||
|
const validSavedKPIs = JSON.parse(savedKPIs).filter((kpiId: string) =>
|
||||||
|
kpiColumns.value.some(col => col.kpiId === kpiId)
|
||||||
|
);
|
||||||
|
if (validSavedKPIs.length > 0) {
|
||||||
|
selectedKPIs.value = validSavedKPIs;
|
||||||
|
} else {
|
||||||
|
// 如果没有有效的保存选择,则默认全选
|
||||||
|
selectedKPIs.value = kpiColumns.value.map(col => col.kpiId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有保存的选择,则默认全选
|
||||||
|
selectedKPIs.value = kpiColumns.value.map(col => col.kpiId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kpiColumns.value.length === 0) {
|
||||||
|
console.warn('No matching KPIs found');
|
||||||
|
} else {
|
||||||
|
console.log(
|
||||||
|
`Found ${kpiColumns.value.length} total KPIs:`,
|
||||||
|
kpiColumns.value.map(kpi => kpi.kpiId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return kpiColumns.value;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch KPI titles:', error);
|
||||||
|
message.error(t('common.getInfoFail'));
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确保只保留一个 onUnmounted 钩子
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (ws.value && ws.value.state() === WebSocket.OPEN) {
|
||||||
|
ws.value.close();
|
||||||
|
}
|
||||||
|
if (observer) {
|
||||||
|
observer.disconnect();
|
||||||
|
observer = null;
|
||||||
|
}
|
||||||
|
if (chart) {
|
||||||
|
chart.dispose();
|
||||||
|
chart = null;
|
||||||
|
}
|
||||||
|
// 可选:在组件卸载时保存选择
|
||||||
|
saveSelectedKPIs();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { t, currentLocale } = useI18n();
|
||||||
|
|
||||||
|
// 修改 updateChartData 方法,只更新数据而不重新渲染整个图表
|
||||||
|
const updateChartData = (newData: ChartDataItem) => {
|
||||||
|
chartData.value.push(newData);
|
||||||
|
if (chartData.value.length > 100) {
|
||||||
|
chartData.value.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chart) {
|
||||||
|
chart.setOption({
|
||||||
|
xAxis: {
|
||||||
|
data: chartData.value.map(item =>
|
||||||
|
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
),
|
||||||
|
},
|
||||||
|
series: selectedKPIs.value.map(kpiId => ({
|
||||||
|
type: chartType.value, // 使用当前选择的图表类型
|
||||||
|
data: chartData.value.map(item => item[kpiId] || 0),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改 groupedKPIs 计算属性,使用 TARGET_KPI_IDS 来分组
|
||||||
|
const groupedKPIs = computed(() => {
|
||||||
|
const groups: Record<string, KPIColumn[]> = {};
|
||||||
|
|
||||||
|
ALL_NE_TYPES.forEach(neType => {
|
||||||
|
// 使用 TARGET_KPI_IDS 中定义的指标 ID 来过滤
|
||||||
|
const targetIds = TARGET_KPI_IDS[neType];
|
||||||
|
groups[neType] = kpiColumns.value.filter(kpi =>
|
||||||
|
targetIds.some(
|
||||||
|
targetId =>
|
||||||
|
kpi.kpiId === targetId || // 完全匹配
|
||||||
|
kpi.kpiId.startsWith(targetId) // 前缀匹配
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return groups;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PageContainer>
|
<div class="kpi-overview">
|
||||||
<a-card :bordered="false" :body-style="{ padding: '0px' }">
|
<div class="controls">
|
||||||
<h1>kpiOverView</h1>
|
<a-range-picker
|
||||||
</a-card>
|
v-model:value="dateRange"
|
||||||
</PageContainer>
|
:show-time="{ format: 'HH:mm:ss' }"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:value-format="'x'"
|
||||||
|
:disabled="isRealtime"
|
||||||
|
@change="handleDateChange"
|
||||||
|
/>
|
||||||
|
<a-button @click="showKPISelector">
|
||||||
|
<template #icon>
|
||||||
|
<unordered-list-outlined />
|
||||||
|
</template>
|
||||||
|
{{ t('views.perfManage.kpiOverView.chooseMetrics') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button @click="toggleChartType">
|
||||||
|
<template #icon>
|
||||||
|
<bar-chart-outlined v-if="chartType === 'line'" />
|
||||||
|
<line-chart-outlined v-else />
|
||||||
|
</template>
|
||||||
|
{{
|
||||||
|
chartType === 'line'
|
||||||
|
? t('views.perfManage.kpiOverView.changeLine')
|
||||||
|
: t('views.perfManage.kpiOverView.changeBar')
|
||||||
|
}}
|
||||||
|
</a-button>
|
||||||
|
<a-form-item
|
||||||
|
:label="
|
||||||
|
isRealtime
|
||||||
|
? t('views.dashboard.cdr.realTimeDataStart')
|
||||||
|
: t('views.dashboard.cdr.realTimeDataStop')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-switch v-model:checked="isRealtime" @change="toggleRealtime" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
<div id="chartContainer" class="chart-container"></div>
|
||||||
|
|
||||||
|
<!-- 修改指标选择对话框 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="isModalVisible"
|
||||||
|
:title="t('views.perfManage.kpiOverView.chooseShowMetrics')"
|
||||||
|
@ok="handleModalOk"
|
||||||
|
@cancel="handleModalCancel"
|
||||||
|
width="800px"
|
||||||
|
:bodyStyle="{ maxHeight: '600px', overflow: 'auto' }"
|
||||||
|
>
|
||||||
|
<a-checkbox-group v-model:value="selectedKPIs">
|
||||||
|
<div class="kpi-checkbox-list">
|
||||||
|
<div
|
||||||
|
v-for="neType in ALL_NE_TYPES"
|
||||||
|
:key="neType"
|
||||||
|
class="ne-type-group"
|
||||||
|
>
|
||||||
|
<div class="ne-type-title">{{ neType.toUpperCase() }}</div>
|
||||||
|
<div class="ne-type-items">
|
||||||
|
<div
|
||||||
|
v-for="kpi in groupedKPIs[neType]"
|
||||||
|
:key="kpi.kpiId"
|
||||||
|
class="kpi-checkbox-item"
|
||||||
|
>
|
||||||
|
<a-checkbox :value="kpi.kpiId">
|
||||||
|
{{ kpi.title }}
|
||||||
|
</a-checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-checkbox-group>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style scoped>
|
||||||
|
.kpi-overview {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 控制区域样式 */
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 图表容器样式 */
|
||||||
|
.chart-container {
|
||||||
|
height: calc(100vh - 160px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 指标选择对话框样式 */
|
||||||
|
.kpi-checkbox-list {
|
||||||
|
padding: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 网元分组样式 */
|
||||||
|
.ne-type-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ne-type-title {
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #fafafa;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ne-type-items {
|
||||||
|
padding: 16px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-checkbox-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 深度选择器样式 */
|
||||||
|
:deep(.ant-modal-body) {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 组件高度统一样式 */
|
||||||
|
:deep(.ant-form-item),
|
||||||
|
:deep(.ant-picker),
|
||||||
|
:deep(.ant-btn) {
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item-label) {
|
||||||
|
padding-right: 8px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-checkbox-wrapper) {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user