From 6ce5a24614a21210027df6ca4b01b49298021065 Mon Sep 17 00:00:00 2001 From: zhongzm Date: Tue, 9 Sep 2025 10:25:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=85=B3=E9=94=AE=E4=BB=AA=E8=A1=A8?= =?UTF-8?q?=E7=9B=98=E6=98=BE=E7=A4=BA=E6=8C=87=E6=A0=87=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/perfManage/kpiOverView/index.vue | 382 +++++++++++++++++---- 1 file changed, 309 insertions(+), 73 deletions(-) diff --git a/src/views/perfManage/kpiOverView/index.vue b/src/views/perfManage/kpiOverView/index.vue index ba746e8a..98f70183 100644 --- a/src/views/perfManage/kpiOverView/index.vue +++ b/src/views/perfManage/kpiOverView/index.vue @@ -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 = { // SMSC: ['SMSC.A.01', 'SMSC.A.02', 'SMSC.A.03'], }; -const KPI_TITLE: Record = { +// 原始KPI标题(用于内部计算) +const BASE_KPI_TITLE: Record = { 'AMF.02': '5G Registration Request', 'AMF.03': '5G Registration Success', 'UPF.04': 'UPF Downlink Throughput', @@ -183,6 +185,143 @@ const KPI_TITLE: Record = { '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 = { + 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 = {}; +// 初始化显示标题 +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): 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) => { 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([]); // 添加选中指标的的状态 -const selectedKPIs = ref(Object.values(TARGET_KPI_IDS).flat()); +const selectedKPIs = ref(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, - neType: neType, - }; - }); - - allKPIs = [...allKPIs, ...filteredKPIs]; + for (const kpi of calculatedKPIs) { + allKPIs.push({ + title: kpi.title, + dataIndex: kpi.id, + key: kpi.id, + kpiId: kpi.id, + neType: neType, + }); } } 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[] = [ if (value === '' || value === null || value === undefined) { return ''; } - // 关键指标多为次数类,使用累计值 - return `${value} `; + // 根据指标类型显示不同单位 + 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[] = [ if (value === '' || value === null || value === undefined) { return ''; } - // 关键指标多为次数类,使用累计值 - return `${value} `; + // 根据指标类型显示不同单位 + 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[] = [ if (value === '' || value === null || value === undefined) { return ''; } - // 关键指标多为次数类,使用累计值 - return `${value} `; + // 根据指标类型显示不同单位 + 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}`; + } }, }, ];