feat:关键仪表盘显示指标修改

This commit is contained in:
zhongzm
2025-09-09 10:25:29 +08:00
parent 627f847d5e
commit 6ce5a24614

View File

@@ -22,6 +22,7 @@ import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import { generateColorRGBA } from '@/utils/generate-utils';
import { parseSizeFromKbs } from '@/utils/parse-utils';
import { LineOutlined, InfoCircleOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons-vue';
import { TableColumnType } from 'ant-design-vue';
import useNeInfoStore from '@/store/modules/neinfo';
@@ -168,7 +169,8 @@ const TARGET_KPI_IDS: Record<NeType, string[]> = {
// SMSC: ['SMSC.A.01', 'SMSC.A.02', 'SMSC.A.03'],
};
const KPI_TITLE: Record<string, string> = {
// 原始KPI标题用于内部计算
const BASE_KPI_TITLE: Record<string, string> = {
'AMF.02': '5G Registration Request',
'AMF.03': '5G Registration Success',
'UPF.04': 'UPF Downlink Throughput',
@@ -183,6 +185,143 @@ const KPI_TITLE: Record<string, string> = {
'MME.A.02':'4G Registration Success',
};
// 计算型KPI定义
interface CalculatedKPI {
id: string;
title: string;
numerator: string; // 分子指标ID
denominator: string; // 分母指标ID
isPercentage: boolean; // 是否为百分比
}
// 计算型KPI配置
const CALCULATED_KPIS: Record<NeType, CalculatedKPI[]> = {
AMF: [
{
id: 'AMF_5G_REG_SUCCESS_RATE',
title: '5G Registration Success Rate',
numerator: 'AMF.03',
denominator: 'AMF.02',
isPercentage: true
}
],
UPF: [
{
id: 'UPF.04',
title: 'UPF Downlink Throughput',
numerator: 'UPF.04',
denominator: '',
isPercentage: false
},
{
id: 'UPF.05',
title: 'UPF Uplink Throughput',
numerator: 'UPF.05',
denominator: '',
isPercentage: false
}
],
IMS: [
{
id: 'IMS_REG_SUCCESS_RATE',
title: 'IMS Registration Success Rate',
numerator: 'SCSCF.03',
denominator: 'SCSCF.04',
isPercentage: true
},
{
id: 'MO_CALL_SUCCESS_RATE',
title: 'MO Call Success Rate',
numerator: 'SCSCF.05',
denominator: 'SCSCF.06',
isPercentage: true
},
{
id: 'MT_CALL_SUCCESS_RATE',
title: 'MT Call Success Rate',
numerator: 'SCSCF.07',
denominator: 'SCSCF.08',
isPercentage: true
}
],
MME: [
{
id: 'MME_4G_REG_SUCCESS_RATE',
title: '4G Registration Success Rate',
numerator: 'MME.A.02',
denominator: 'MME.A.01',
isPercentage: true
}
]
};
// 显示用的KPI标题
const KPI_TITLE: Record<string, string> = {};
// 初始化显示标题
Object.values(CALCULATED_KPIS).flat().forEach(kpi => {
KPI_TITLE[kpi.id] = kpi.title;
});
// UPF吞吐量指标ID列表
const UPF_THROUGHPUT_KPIS = ['UPF.04', 'UPF.05'];
// 判断是否为UPF吞吐量指标
const isUPFThroughputKPI = (kpiId: string): boolean => {
return UPF_THROUGHPUT_KPIS.includes(kpiId);
};
// 格式化UPF吞吐量数据 - 根据数据来源使用不同的时间间隔
const formatUPFThroughput = (value: number, timeInterval: number = 900): number => {
if (value === 0 || value === null || value === undefined) return 0;
// 使用parseSizeFromKbs函数进行格式化转换为Mbps
const [formattedValue] = parseSizeFromKbs(value, timeInterval);
return Number(formattedValue);
};
// 判断当前选中的指标中是否包含UPF吞吐量指标
const hasUPFThroughputKPIs = (): boolean => {
return selectedKPIs.value.some(kpiId => isUPFThroughputKPI(kpiId));
};
// 计算KPI值的函数
const calculateKPIValue = (kpi: CalculatedKPI, rawData: Record<string, any>): number => {
if (!kpi.denominator) {
// 单一指标如UPF吞吐量
const value = Number(rawData[kpi.numerator]) || 0;
return isUPFThroughputKPI(kpi.id) ? formatUPFThroughput(value, 900) : value;
} else {
// 计算型指标(百分比)
const numerator = Number(rawData[kpi.numerator]) || 0;
const denominator = Number(rawData[kpi.denominator]) || 0;
if (denominator === 0) return 0;
const percentage = (numerator / denominator) * 100;
return Number(percentage.toFixed(2));
}
};
// 获取网元类型的所有计算型KPI
const getCalculatedKPIsForNeType = (neType: NeType): CalculatedKPI[] => {
return CALCULATED_KPIS[neType] || [];
};
// 获取Y轴单位
const getYAxisUnit = (): string => {
const allKPIs = Object.values(CALCULATED_KPIS).flat();
const hasUPF = allKPIs.some(kpi => isUPFThroughputKPI(kpi.id));
const hasPercentage = allKPIs.some(kpi => kpi.isPercentage);
if (hasUPF && hasPercentage) {
return ''; // 混合单位时不显示
} else if (hasUPF) {
return '(Mbps)';
} else if (hasPercentage) {
return '(%)';
}
return '';
};
// 添加网元信息 store
const neInfoStore = useNeInfoStore();
@@ -284,10 +423,19 @@ const wsMessage = (res: Record<string, any>) => {
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;
// 为每个网元的每个计算型指标添加数据
const calculatedKPIs = getCalculatedKPIsForNeType(neType as NeType);
for (const kpi of calculatedKPIs) {
const key = `${kpi.id}_${neId}`;
if (isUPFThroughputKPI(kpi.id)) {
// UPF吞吐量指标使用1分钟间隔
const rawValue = Number(kpiEvent[kpi.numerator]) || 0;
newData[key] = formatUPFThroughput(rawValue, 60);
} else {
// 其他计算型指标
newData[key] = calculateKPIValue(kpi, kpiEvent);
}
}
// 更新图表数据(只影响图表,不影响表格)
@@ -381,12 +529,21 @@ const fetchChartData = async () => {
.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;
const calculatedKPIs = getCalculatedKPIsForNeType(neType);
for (const kpi of calculatedKPIs) {
const key = `${kpi.id}_${ne.neId}`;
if (isUPFThroughputKPI(kpi.id)) {
// UPF吞吐量指标使用15分钟间隔
const rawValue = Number(item[kpi.numerator]) || 0;
dataItem[key] = formatUPFThroughput(rawValue, 900);
} else {
// 其他计算型指标
dataItem[key] = calculateKPIValue(kpi, item);
}
}
}
}
@@ -497,6 +654,15 @@ const themeObserver = new MutationObserver(() => {
color: splitLineColor,
},
},
// 根据指标类型显示单位
name: getYAxisUnit(),
nameTextStyle: {
fontSize: 12,
padding: [0, 0, 0, 0],
color: document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
},
legend: {
show: false,
@@ -550,11 +716,9 @@ const updateChart = () => {
const series = [];
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 calculatedKPIs = getCalculatedKPIsForNeType(neType);
for (const kpi of calculatedKPIs) {
const key = `${kpi.id}_${ne.neId}`;
const color = kpiColors.get(key) || generateColorRGBA();
kpiColors.set(key, color);
@@ -670,6 +834,15 @@ const updateChart = () => {
color: getSplitLineColor(),
},
},
// 根据指标类型显示单位
name: getYAxisUnit(),
nameTextStyle: {
fontSize: 12,
padding: [0, 0, 0, 0],
color: document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
},
series: series, //配置数据
};
@@ -741,7 +914,7 @@ onMounted(async () => {
// 存储指标列信
const kpiColumns = ref<KPIColumn[]>([]);
// 添加选中指标的的状态
const selectedKPIs = ref<string[]>(Object.values(TARGET_KPI_IDS).flat());
const selectedKPIs = ref<string[]>(Object.values(CALCULATED_KPIS).flat().map(kpi => kpi.id));
// 获取网元指标
const fetchSpecificKPI = async () => {
@@ -753,36 +926,24 @@ const fetchSpecificKPI = async () => {
try {
let allKPIs: KPIColumn[] = [];
// 获取所有网元的指标
// 为每个网元类型创建计算型KPI列
for (const neType of ALL_NE_TYPES) {
const res = await getKPITitle(neType.toUpperCase());
const calculatedKPIs = getCalculatedKPIsForNeType(neType);
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
const titleIDs = Object.values(TARGET_KPI_IDS).flat();
// 只获取 TARGET_KPI_IDS 中定义的指标
const filteredKPIs = res.data
.filter(item => TARGET_KPI_IDS[neType].includes(item.kpiId))
.map(item => {
let title = item[`${language}Title`];
if (titleIDs.includes(item.kpiId)) {
title = KPI_TITLE[item.kpiId] || title;
}
return {
title: title,
dataIndex: item.kpiId,
key: item.kpiId,
kpiId: item.kpiId,
for (const kpi of calculatedKPIs) {
allKPIs.push({
title: kpi.title,
dataIndex: kpi.id,
key: kpi.id,
kpiId: kpi.id,
neType: neType,
};
});
allKPIs = [...allKPIs, ...filteredKPIs];
}
}
kpiColumns.value = allKPIs;
// 直接使用重要指标
selectedKPIs.value = Object.values(TARGET_KPI_IDS).flat();
// 使用计算型指标
selectedKPIs.value = Object.values(CALCULATED_KPIS).flat().map(kpi => kpi.id);
if (kpiColumns.value.length === 0) {
console.warn('No KPIs found');
@@ -837,13 +998,12 @@ const updateChartData = (newData: ChartDataItem) => {
},
series: ALL_NE_TYPES.flatMap(type =>
neList.value[type].map(ne =>
TARGET_KPI_IDS[type].map(kpiId => {
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
const key = `${kpiId}_${ne.neId}`;
getCalculatedKPIsForNeType(type).map(kpi => {
const key = `${kpi.id}_${ne.neId}`;
return {
type: 'line',
data: chartData.value.map(item => item[key] || 0),
name: `${kpi?.title || kpiId}(${ne.neName})`,
name: `${kpi.title}(${ne.neName})`,
};
})
)
@@ -878,12 +1038,10 @@ function fnInitKpiStatsData() {
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 calculatedKPIs = getCalculatedKPIsForNeType(neType);
for (const kpi of calculatedKPIs) {
kpiStats.value.push({
kpiId: `${kpiId}_${ne.neId}`,
kpiId: `${kpi.id}_${ne.neId}`,
title: `${kpi.title}(${ne.neName})`,
last1Day: '', // 空白显示loading状态表示正在获取数据
last7Days: '',
@@ -952,39 +1110,86 @@ async function fnGetKpiStatsData() {
const { neType, neId, success, data } = result;
if (success && data.length > 0) {
// 为每个指标计算统计值
for (const kpiId of TARGET_KPI_IDS[neType as NeType]) {
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (!kpi) continue;
// 为每个计算型指标计算统计值
const calculatedKPIs = getCalculatedKPIsForNeType(neType as NeType);
for (const kpi of calculatedKPIs) {
const key = `${kpi.id}_${neId}`;
const key = `${kpiId}_${neId}`;
// 根据时间范围筛选非零数据
// 根据时间范围筛选有效数据(非零数据)
const data1Day = data.filter((item: any) => {
const itemTime = Number(item.timeGroup);
const value = item[kpiId] ? Number(item[kpiId]) : 0;
return itemTime >= day1_start && value !== 0;
if (itemTime < day1_start) return false;
// 检查相关指标是否有有效数值
if (kpi.isPercentage) {
// 百分比指标分母必须大于0
const denominator = Number(item[kpi.denominator]) || 0;
return denominator > 0;
} else {
// 其他指标数值必须大于0
const value = Number(item[kpi.numerator]) || 0;
return value > 0;
}
});
const data7Days = data.filter((item: any) => {
const itemTime = Number(item.timeGroup);
const value = item[kpiId] ? Number(item[kpiId]) : 0;
return itemTime >= day7_start && value !== 0;
if (itemTime < day7_start) return false;
// 检查相关指标是否有有效数值
if (kpi.isPercentage) {
// 百分比指标分母必须大于0
const denominator = Number(item[kpi.denominator]) || 0;
return denominator > 0;
} else {
// 其他指标数值必须大于0
const value = Number(item[kpi.numerator]) || 0;
return value > 0;
}
});
const data30Days = data.filter((item: any) => {
const itemTime = Number(item.timeGroup);
const value = item[kpiId] ? Number(item[kpiId]) : 0;
return itemTime >= day30_start && value !== 0;
if (itemTime < day30_start) return false;
// 检查相关指标是否有有效数值
if (kpi.isPercentage) {
// 百分比指标分母必须大于0
const denominator = Number(item[kpi.denominator]) || 0;
return denominator > 0;
} else {
// 其他指标数值必须大于0
const value = Number(item[kpi.numerator]) || 0;
return value > 0;
}
});
// 计算统计值(只对非零数据进行计算,关键指标多为次数类使用累计值
// 计算统计值(传入的数据已经是有效数据
const calculateValue = (dataArray: any[]) => {
if (dataArray.length === 0) return 0;
const values = dataArray.map((item: any) => Number(item[kpiId]));
// 关键指标多为次数类,使用累计
return Number(values.reduce((sum, val) => sum + val, 0).toFixed(2));
if (isUPFThroughputKPI(kpi.id)) {
// UPF吞吐量指标先格式化每个数据点然后计算平均
const values = dataArray.map((item: any) => Number(item[kpi.numerator]) || 0);
const formattedValues = values.map(val => formatUPFThroughput(val, 900));
const average = formattedValues.reduce((sum, val) => sum + val, 0) / formattedValues.length;
return Number(average.toFixed(2));
} else if (kpi.isPercentage) {
// 百分比指标计算平均成功率数据已经过滤分母都大于0
const percentages = dataArray.map((item: any) => {
const numerator = Number(item[kpi.numerator]) || 0;
const denominator = Number(item[kpi.denominator]) || 0;
return (numerator / denominator) * 100;
});
const average = percentages.reduce((sum, val) => sum + val, 0) / percentages.length;
return Number(average.toFixed(2));
} else {
// 其他指标:累计值(数据已经过滤,都是非零值)
const values = dataArray.map((item: any) => Number(item[kpi.numerator]) || 0);
const sum = values.reduce((sum, val) => sum + val, 0);
return Number(sum.toFixed(2));
}
};
// 更新对应的统计数据
@@ -997,8 +1202,9 @@ async function fnGetKpiStatsData() {
}
} else {
// 如果获取失败,保持空白显示
for (const kpiId of TARGET_KPI_IDS[neType as NeType]) {
const key = `${kpiId}_${neId}`;
const calculatedKPIs = getCalculatedKPIsForNeType(neType as NeType);
for (const kpi of calculatedKPIs) {
const key = `${kpi.id}_${neId}`;
const statsIndex = kpiStats.value.findIndex((item: any) => item.kpiId === key);
if (statsIndex !== -1) {
kpiStats.value[statsIndex].last1Day = '';
@@ -1077,8 +1283,18 @@ const statsColumns: TableColumnType<KPIStats>[] = [
if (value === '' || value === null || value === undefined) {
return '';
}
// 关键指标多为次数类,使用累计值
// 根据指标类型显示不同单位
const kpiId = record.kpiId.split('_')[0];
const isUPFThroughput = isUPFThroughputKPI(kpiId);
const isPercentage = Object.values(CALCULATED_KPIS).flat().find(kpi => kpi.id === kpiId)?.isPercentage;
if (isUPFThroughput) {
return `${value} Mbps`;
} else if (isPercentage) {
return `${value}%`;
} else {
return `${value}`;
}
},
},
{
@@ -1093,8 +1309,18 @@ const statsColumns: TableColumnType<KPIStats>[] = [
if (value === '' || value === null || value === undefined) {
return '';
}
// 关键指标多为次数类,使用累计值
// 根据指标类型显示不同单位
const kpiId = record.kpiId.split('_')[0];
const isUPFThroughput = isUPFThroughputKPI(kpiId);
const isPercentage = Object.values(CALCULATED_KPIS).flat().find(kpi => kpi.id === kpiId)?.isPercentage;
if (isUPFThroughput) {
return `${value} Mbps`;
} else if (isPercentage) {
return `${value}%`;
} else {
return `${value}`;
}
},
},
{
@@ -1109,8 +1335,18 @@ const statsColumns: TableColumnType<KPIStats>[] = [
if (value === '' || value === null || value === undefined) {
return '';
}
// 关键指标多为次数类,使用累计值
// 根据指标类型显示不同单位
const kpiId = record.kpiId.split('_')[0];
const isUPFThroughput = isUPFThroughputKPI(kpiId);
const isPercentage = Object.values(CALCULATED_KPIS).flat().find(kpi => kpi.id === kpiId)?.isPercentage;
if (isUPFThroughput) {
return `${value} Mbps`;
} else if (isPercentage) {
return `${value}%`;
} else {
return `${value}`;
}
},
},
];