feat:关键指标、黄金指标仪表盘多网元实现

This commit is contained in:
zhongzm
2025-05-22 15:00:27 +08:00
parent f42b921a50
commit 1aa1b471a5
2 changed files with 934 additions and 406 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ import { OptionsType, WS } from '@/plugins/ws-websocket';
import { generateColorRGBA } from '@/utils/generate-utils'; import { generateColorRGBA } from '@/utils/generate-utils';
import { LineOutlined } from '@ant-design/icons-vue'; import { LineOutlined } from '@ant-design/icons-vue';
import { TableColumnType } from 'ant-design-vue'; import { TableColumnType } from 'ant-design-vue';
import useNeInfoStore from '@/store/modules/neinfo';
const { t, currentLocale } = useI18n(); const { t, currentLocale } = useI18n();
//定义KPI接口 //定义KPI接口
interface KPIBase { interface KPIBase {
@@ -147,6 +148,50 @@ const KPI_TITLE: Record<string, string> = {
'SCSCF.08': 'MT Call Attempt', 'SCSCF.08': 'MT Call Attempt',
}; };
// 添加网元信息 store
const neInfoStore = useNeInfoStore();
// 添加网元列表相关变量
const neList = ref<Record<NeType, {neId: string, neName: string}[]>>({
AMF: [],
UPF: [],
IMS: []
});
// 添加获取网元列表的函数
const fetchNeList = async () => {
try {
const res = await neInfoStore.fnNelist();
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// 初始化网元列表
ALL_NE_TYPES.forEach(type => {
neList.value[type] = [];
});
// 过滤并分类网元
res.data.forEach(ne => {
const neType = ne.neType as NeType;
if (ALL_NE_TYPES.includes(neType)) {
neList.value[neType].push({
neId: ne.neId,
neName: ne.neName
});
}
});
console.log('网元列表获取成功:', neList.value);
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
} catch (error) {
console.error('Failed to fetch NE list:', error);
message.error(t('common.getInfoFail'));
}
};
// 实时数据开关函数 // 实时数据开关函数
const fnRealTimeSwitch = (bool: boolean) => { const fnRealTimeSwitch = (bool: boolean) => {
if (bool) { if (bool) {
@@ -163,15 +208,18 @@ const fnRealTimeSwitch = (bool: boolean) => {
const options: OptionsType = { const options: OptionsType = {
url: '/ws', url: '/ws',
params: { params: {
subGroupID: ALL_NE_TYPES.map(type => `10_${type}_001`).join(','), // 为所有网元创建订阅
subGroupID: ALL_NE_TYPES.flatMap(type =>
neList.value[type].map(ne => `10_${type}_${ne.neId}`)
).join(','),
}, },
onmessage: wsMessage, onmessage: wsMessage,
onerror: wsError, onerror: wsError,
}; };
ws.value.connect(options); ws.value.connect(options);
} else if (ws.value) { } else if (ws.value) {
ws.value.close(); //断开链接 ws.value.close();
ws.value = null; //清空链接 ws.value = null;
tableLoading.value = false; tableLoading.value = false;
} }
}; };
@@ -181,26 +229,6 @@ const wsError = () => {
message.error(t('common.websocketError')); message.error(t('common.websocketError'));
}; };
const handleWebSocketMessage = (kpiEvent: any) => {
if (!kpiEvent) return;
// 构造新的数据点
const newData: ChartDataItem = {
date: kpiEvent.timeGroup?.toString() || Date.now().toString(),
};
// 添加已选中的指标的数据
selectedKPIs.value.forEach(kpiId => {
newData[kpiId] = Number(kpiEvent[kpiId]) || 0;
});
// 更新数据
updateChartData(newData);
if (tableLoading.value) {
tableLoading.value = false;
}
};
//成功回调
const wsMessage = (res: Record<string, any>) => { const wsMessage = (res: Record<string, any>) => {
if (!chart) { if (!chart) {
return; return;
@@ -210,8 +238,32 @@ const wsMessage = (res: Record<string, any>) => {
tableLoading.value = false; tableLoading.value = false;
return; return;
} }
handleWebSocketMessage(data.data);
// 解析订阅组ID获取网元类型和ID
const [_, neType, neId] = data.groupId.split('_');
if (!neType || !neId) return;
const kpiEvent = data.data;
if (!kpiEvent) return;
// 构造新的数据点
const newData: ChartDataItem = {
date: kpiEvent.timeGroup?.toString() || Date.now().toString(),
};
// 为每个网元的每个指标添加数据
for (const kpiId of TARGET_KPI_IDS[neType as NeType] || []) {
const key = `${kpiId}_${neId}`;
newData[key] = Number(kpiEvent[kpiId]) || 0;
}
// 更新数据
updateChartData(newData);
if (tableLoading.value) {
tableLoading.value = false;
}
}; };
// 添加数据处理函数 // 添加数据处理函数
const processChartData = (rawData: any[]) => { const processChartData = (rawData: any[]) => {
const groupedData = new Map<string, any>(); //数据按时间分组 const groupedData = new Map<string, any>(); //数据按时间分组
@@ -252,35 +304,66 @@ const fetchChartData = async () => {
} }
// 创建并行请求数组 // 创建并行请求数组
const requests = ALL_NE_TYPES.map(async neType => { const requests = [];
const params = { for (const neType of ALL_NE_TYPES) {
neType, for (const ne of neList.value[neType]) {
neId: '001', const params = {
startTime: String(startTime), neType,
endTime: String(endTime), neId: ne.neId,
sortField: 'timeGroup', startTime: String(startTime),
sortOrder: 'asc', endTime: String(endTime),
interval: 60 * 15, sortField: 'timeGroup',
kpiIds: TARGET_KPI_IDS[neType].join(','), sortOrder: 'asc',
}; interval: 60 * 15,
kpiIds: TARGET_KPI_IDS[neType].join(','),
};
try { requests.push(
const res = await listKPIData(params); listKPIData(params).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
return res.data; return res.data.map(item => ({
} ...item,
return []; _neId: ne.neId,
} catch (error) { neName: `${neType}-${ne.neId}`
console.error(`Failed to fetch data for ${neType}:`, error); }));
return []; }
return [];
})
);
} }
}); }
// 并行执行所有请求 // 并行执行所有请求
const results = await Promise.all(requests); const results = await Promise.all(requests);
const allData = results.flat(); const allData = results.flat();
chartData.value = processChartData(allData); // 按时间分组处理数据
const groupedData = new Map<string, any>();
allData.forEach(item => {
const timeKey = item.timeGroup;
if (!groupedData.has(timeKey)) {
groupedData.set(timeKey, { timeGroup: timeKey });
}
Object.assign(groupedData.get(timeKey), item);
});
// 转换为图表数据格式
chartData.value = Array.from(groupedData.values())
.sort((a, b) => Number(a.timeGroup) - Number(b.timeGroup))
.map(item => {
const dataItem: ChartDataItem = { date: item.timeGroup.toString() };
// 为每个网元的每个指标添加数据
for (const neType of ALL_NE_TYPES) {
for (const ne of neList.value[neType]) {
for (const kpiId of TARGET_KPI_IDS[neType]) {
const key = `${kpiId}_${ne.neId}`;
dataItem[key] = Number(item[kpiId]) || 0;
}
}
}
return dataItem;
});
updateChart(); updateChart();
updateKpiStats(); updateKpiStats();
} catch (error) { } catch (error) {
@@ -436,26 +519,31 @@ const themeObserver = new MutationObserver(() => {
const updateChart = () => { const updateChart = () => {
if (!chart || !kpiColumns.value.length) return; if (!chart || !kpiColumns.value.length) return;
//获取图表配置
const commonConfig = getSeriesConfig();
//构建数据系列
const series = selectedKPIs.value
.map(kpiId => {
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (!kpi) return null;
//为每个KPI分配临时的固定颜色
const color = kpiColors.get(kpiId) || generateColorRGBA();
kpiColors.set(kpiId, color);
return { const series = [];
name: kpi.title, for (const neType of ALL_NE_TYPES) {
type: 'line', for (const ne of neList.value[neType]) {
data: chartData.value.map(item => item[kpiId] || 0), for (const kpiId of TARGET_KPI_IDS[neType]) {
itemStyle: { color }, const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
...commonConfig, if (!kpi) continue;
};
}) const key = `${kpiId}_${ne.neId}`;
.filter(Boolean); const color = kpiColors.get(key) || generateColorRGBA();
kpiColors.set(key, color);
series.push({
name: `${kpi.title}(${ne.neId})`,
type: 'line',
data: chartData.value.map(item => item[key] || 0),
itemStyle: { color },
symbol: 'none',
symbolSize: 6,
smooth: 0.6,
showSymbol: true,
});
}
}
}
const option = { const option = {
title: { title: {
@@ -463,36 +551,29 @@ const updateChart = () => {
left: 'center', left: 'center',
// 添加文字颜色配置,根据主题切换 // 添加文字颜色配置,根据主题切换
textStyle: { textStyle: {
color: color: document.documentElement.getAttribute('data-theme') === 'dark'
document.documentElement.getAttribute('data-theme') === 'dark' ? '#CACADA'
? '#CACADA' : '#333',
: '#333',
}, },
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
confine: true, // 限制 tooltip 显示范围 confine: true,
backgroundColor: backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark'
document.documentElement.getAttribute('data-theme') === 'dark' ? 'rgba(48, 48, 48, 0.8)'
? 'rgba(48, 48, 48, 0.8)' : 'rgba(255, 255, 255, 0.9)',
: 'rgba(255, 255, 255, 0.9)', borderColor: document.documentElement.getAttribute('data-theme') === 'dark'
borderColor: ? '#555'
document.documentElement.getAttribute('data-theme') === 'dark' : '#ddd',
? '#555'
: '#ddd',
textStyle: { textStyle: {
color: color: document.documentElement.getAttribute('data-theme') === 'dark'
document.documentElement.getAttribute('data-theme') === 'dark' ? '#CACADA'
? '#CACADA' : '#333',
: '#333',
}, },
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);', extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);',
}, },
legend: { legend: {
data: selectedKPIs.value.map( data: series.map(s => s.name),
kpiId =>
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId
),
type: 'scroll', type: 'scroll',
orient: 'horizontal', orient: 'horizontal',
top: 25, top: 25,
@@ -500,11 +581,9 @@ const updateChart = () => {
fontSize: 12, fontSize: 12,
}, },
selected: Object.fromEntries( selected: Object.fromEntries(
selectedKPIs.value.map(kpiId => [ kpiStats.value.map(item => [
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId, item.title,
selectedRows.value.length === 0 selectedRows.value.length === 0 || selectedRows.value.includes(item.kpiId)
? true
: selectedRows.value.includes(kpiId),
]) ])
), ),
show: false, show: false,
@@ -514,17 +593,20 @@ const updateChart = () => {
padding: [5, 10], padding: [5, 10],
}, },
grid: { grid: {
//网格配置 left: '3%',
left: '6%', right: '4%',
right: '6%',
bottom: '3%', bottom: '3%',
containLabel: true, containLabel: true,
}, },
xAxis: { xAxis: {
// 指定x轴类型为类目轴适用于离散的类目数据 // 指定x轴类型为类目轴适用于离散的类目数据
type: 'category', type: 'category',
boundaryGap: false,
data: chartData.value.map(item =>
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
),
axisLabel: { axisLabel: {
formatter: '{value}', // formatter: '{value}',
color: color:
document.documentElement.getAttribute('data-theme') === 'dark' document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
@@ -536,49 +618,14 @@ const updateChart = () => {
color: getSplitLineColor(), color: getSplitLineColor(),
}, },
}, },
//控制坐标轴两边留白
// 当为折线图时(isLine为true)时不留白,柱状图时留白
// 这样可以让折线图从原点开始,柱状图有合适的间距
boundaryGap: false,
// 设置x轴的数据
// 将时间戳转换为格式化的时间字符串
data: chartData.value.map(item =>
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
),
//设置坐标轴刻度标签的样式
// axisLabel: {
// interval: function(index: number, value: string) {
// const currentTime = dayjs(value);
// const minutes = currentTime.minute();
// const seconds = currentTime.second();
//
// // 始终显示小时的起始和结束时间点
// if ((minutes === 0 && seconds === 0) ||
// (minutes === 59 && seconds === 59)) {
// return true;
// }
//
// // 对于中间的时间点,使用 auto 的逻辑
// if (index % Math.ceil(chartData.value.length / 6) === 0) {
// return true;
// }
//
// return false;
// },
// rotate: 0,
// align: 'center',
// hideOverlap: true
// }
}, },
yAxis: { yAxis: {
// y轴配置
type: 'value', type: 'value',
axisLabel: { axisLabel: {
formatter: '{value}', formatter: '{value}',
color: color: document.documentElement.getAttribute('data-theme') === 'dark'
document.documentElement.getAttribute('data-theme') === 'dark' ? '#CACADA'
? '#CACADA' : '#333',
: '#333',
}, },
// 添加自计算的分割段数 // 添加自计算的分割段数
splitNumber: 5, splitNumber: 5,
@@ -595,32 +642,19 @@ const updateChart = () => {
}; };
if (chart) { if (chart) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
chart!.setOption(option, true); //使用新的配置更新图表 if (chart) { // 添加额外的空值检查
chart!.resize(); //调整图表大小适应容器 chart.setOption(option, true);
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 () => { onMounted(async () => {
try { try {
// 获取网元列表
await fetchNeList();
// 获取所有网元的指标 // 获取所有网元的指标
await fetchSpecificKPI(); await fetchSpecificKPI();
await nextTick(); await nextTick();
@@ -635,6 +669,15 @@ onMounted(async () => {
} else { } else {
console.warn('No KPI columns available after fetching'); console.warn('No KPI columns available after fetching');
} }
// 创建 ResizeObserver 实例监听图表容器大小变化
observer = new ResizeObserver(() => {
if (chart) {
chart.resize();
}
});
// 监听元素大小变化
observer.observe(container);
} else if (chart) { } else if (chart) {
console.warn('Chart already initialized, skipping initialization'); console.warn('Chart already initialized, skipping initialization');
} else { } else {
@@ -672,21 +715,24 @@ const fetchSpecificKPI = async () => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
const titleIDs = Object.values(TARGET_KPI_IDS).flat(); const titleIDs = Object.values(TARGET_KPI_IDS).flat();
const formattedKPIs = res.data.map(item => { // 只获取 TARGET_KPI_IDS 中定义的指标
let title = item[`${language}Title`]; const filteredKPIs = res.data
if (titleIDs.includes(item.kpiId)) { .filter(item => TARGET_KPI_IDS[neType].includes(item.kpiId))
title = KPI_TITLE[item.kpiId] || title; .map(item => {
} let title = item[`${language}Title`];
return { if (titleIDs.includes(item.kpiId)) {
title: title, title = KPI_TITLE[item.kpiId] || title;
dataIndex: item.kpiId, }
key: item.kpiId, return {
kpiId: item.kpiId, title: title,
neType: neType, dataIndex: item.kpiId,
}; key: item.kpiId,
}); kpiId: item.kpiId,
neType: neType,
}
});
allKPIs = [...allKPIs, ...formattedKPIs]; allKPIs = [...allKPIs, ...filteredKPIs];
} }
} }
@@ -745,14 +791,19 @@ const updateChartData = (newData: ChartDataItem) => {
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss') dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
), ),
}, },
series: selectedKPIs.value.map(kpiId => { series: ALL_NE_TYPES.flatMap(type =>
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId); neList.value[type].map(ne =>
return { TARGET_KPI_IDS[type].map(kpiId => {
type: 'line', const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
data: chartData.value.map(item => item[kpiId] || 0), const key = `${kpiId}_${ne.neId}`;
name: kpi?.title || kpiId, return {
}; type: 'line',
}), data: chartData.value.map(item => item[key] || 0),
name: `${kpi?.title || kpiId}(${ne.neId})`,
};
})
)
).flat(),
}; };
chart.setOption(option); chart.setOption(option);
}); });
@@ -778,52 +829,39 @@ const kpiStats = ref<KPIStats[]>([]);
// 添加一个计算函数来更新统计数据 // 添加一个计算函数来更新统计数据
const updateKpiStats = () => { const updateKpiStats = () => {
if (!chartData.value.length || !kpiColumns.value.length) { if (!chartData.value.length || !kpiColumns.value.length) {
kpiStats.value = selectedKPIs.value.map(kpiId => { kpiStats.value = [];
// 找到对应的KPI标题
let title = kpiId;
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (kpi) {
title = kpi.title;
}
return {
kpiId: kpiId,
title: title,
max: 0,
min: 0,
avg: 0,
total: 0,
};
});
return; return;
} }
kpiStats.value = selectedKPIs.value
.map(kpiId => {
// 找到对应的KPI标题
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (!kpi) return null;
// 获取该指标的所有数值 kpiStats.value = [];
const values = chartData.value.map(item => Number(item[kpiId]) || 0); for (const neType of ALL_NE_TYPES) {
for (const ne of neList.value[neType]) {
for (const kpiId of TARGET_KPI_IDS[neType]) {
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (!kpi) continue;
// 计算总值 const key = `${kpiId}_${ne.neId}`;
const total = Number( const values = chartData.value.map(item => Number(item[key]) || 0);
values.reduce((sum, val) => sum + val, 0).toFixed(2)
);
// 计算平均值 if (values.length === 0) continue;
const avg =
values.length > 0 ? Number((total / values.length).toFixed(2)) : 0;
return { const total = Number(values.reduce((sum, val) => sum + val, 0).toFixed(2));
kpiId: kpiId, const avg = Number((total / values.length).toFixed(2));
title: kpi.title,
max: Math.max(...values), kpiStats.value.push({
min: Math.min(...values), kpiId: key,
avg: avg, title: `${kpi.title}(${ne.neId})`,
total: total, max: Math.max(...values),
}; min: Math.min(...values),
}) avg: avg,
.filter((item): item is KPIStats => item !== null); total: total,
});
}
}
}
// 更新图表显示
updateChartLegendSelect();
}; };
// 添加表列定义 // 添加表列定义
@@ -903,11 +941,9 @@ const updateChartLegendSelect = () => {
if (!chart) return; if (!chart) return;
const legendSelected = Object.fromEntries( const legendSelected = Object.fromEntries(
selectedKPIs.value.map(kpiId => [ kpiStats.value.map(item => [
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId, item.title,
selectedRows.value.length === 0 selectedRows.value.length === 0 || selectedRows.value.includes(item.kpiId)
? true
: selectedRows.value.includes(kpiId),
]) ])
); );
@@ -1037,6 +1073,9 @@ const tableRowConfig = computed(() => {
border-radius: 4px; border-radius: 4px;
padding: 20px; padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
height: calc(100vh - 200px);
display: flex;
flex-direction: column;
} }
[data-theme='light'] .chart-wrapper { [data-theme='light'] .chart-wrapper {
@@ -1048,8 +1087,10 @@ const tableRowConfig = computed(() => {
} }
.chart-container { .chart-container {
height: 400px; height: 450px;
width: 100%; width: 100%;
min-height: 400px;
position: relative;
} }
.table-container { .table-container {