1597 lines
44 KiB
Vue
1597 lines
44 KiB
Vue
<script lang="ts" setup>
|
||
import { ref, onMounted, onUnmounted, nextTick, computed, h } from 'vue';
|
||
|
||
|
||
import * as echarts from 'echarts/core';
|
||
import {
|
||
GridComponent,
|
||
TooltipComponent,
|
||
TitleComponent,
|
||
LegendComponent,
|
||
} from 'echarts/components';
|
||
import { LineChart } from 'echarts/charts';
|
||
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 { parseSizeFromKbs } from '@/utils/parse-utils';
|
||
import { LineOutlined, InfoCircleOutlined, EyeOutlined, EyeInvisibleOutlined,UpOutlined } from '@ant-design/icons-vue';
|
||
import { TableColumnType } from 'ant-design-vue';
|
||
import useNeInfoStore from '@/store/modules/neinfo';
|
||
const { t, currentLocale } = useI18n();
|
||
//定义KPI接口
|
||
interface KPIBase {
|
||
kpiId: string;
|
||
title: string;
|
||
}
|
||
//继承接口
|
||
interface KPIColumn extends KPIBase {
|
||
dataIndex: string;
|
||
key: string;
|
||
neType: string;
|
||
}
|
||
// 在这里定义 ChartDataItem 接口
|
||
interface ChartDataItem {
|
||
date: string; // 将存储完整的时间字符串包含时分秒
|
||
[kpiId: string]: string | number; // 动态指标
|
||
}
|
||
const tableLoading = ref(false);
|
||
const rangeLoading = ref(false);
|
||
//网元类型定义
|
||
const ALL_NE_TYPES = ['AMF', 'UPF', 'IMS','MME'] as const;
|
||
type NeType = (typeof ALL_NE_TYPES)[number];
|
||
|
||
echarts.use([
|
||
LineChart,
|
||
GridComponent,
|
||
TooltipComponent,
|
||
TitleComponent,
|
||
CanvasRenderer,
|
||
LegendComponent,
|
||
]);
|
||
// WebSocket连接
|
||
const ws = ref<WS | null>(null);
|
||
|
||
//时间选择
|
||
const ranges = ref([
|
||
{
|
||
label: t('views.perfManage.customTarget.toDay'),
|
||
value: [dayjs().startOf('day'), dayjs()],
|
||
},
|
||
{
|
||
label: t('views.perfManage.customTarget.ago1Hour'),
|
||
value: [
|
||
dayjs().subtract(1, 'hour').startOf('hour'),
|
||
dayjs().subtract(1, 'hour').endOf('hour'),
|
||
],
|
||
},
|
||
// {
|
||
// label: t('views.perfManage.customTarget.ago3Hour'),
|
||
// value: [dayjs().subtract(3, 'hours'), dayjs()],
|
||
// },
|
||
// {
|
||
// label: t('views.perfManage.customTarget.ago6Hour'),
|
||
// value: [dayjs().subtract(6, 'hours'), dayjs()],
|
||
// },
|
||
{
|
||
label: t('views.perfManage.customTarget.ago1Day'),
|
||
value: [
|
||
dayjs().subtract(1, 'day').startOf('day'),
|
||
dayjs().subtract(1, 'day').endOf('day'),
|
||
],
|
||
},
|
||
{
|
||
label: t('views.perfManage.customTarget.ago7Day'),
|
||
value: [
|
||
dayjs().subtract(7, 'day').startOf('day'),
|
||
dayjs().subtract(1, 'day').endOf('day'),
|
||
],
|
||
},
|
||
{
|
||
label: t('views.perfManage.customTarget.ago15Day'),
|
||
value: [
|
||
dayjs().subtract(15, 'day').startOf('day'),
|
||
dayjs().subtract(1, 'day').endOf('day'),
|
||
],
|
||
},
|
||
]);
|
||
//日期范围响应式变量
|
||
const dateRange = ref<[string, string]>([
|
||
dayjs().subtract(1, 'hour').startOf('hour').valueOf().toString(), // 上一小时开始
|
||
dayjs().startOf('hour').add(1, 'hour').valueOf().toString(), // 当前小时结束
|
||
]);
|
||
//实时数据状态
|
||
const isRealtime = ref(false);
|
||
//图表显示状态
|
||
const isChartVisible = ref(true);
|
||
//图表数据响应式数组
|
||
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 toggleChartVisibility = () => {
|
||
isChartVisible.value = !isChartVisible.value;
|
||
// 当图表重新显示时,需要重新调整大小
|
||
if (isChartVisible.value && chart) {
|
||
nextTick(() => {
|
||
chart?.resize();
|
||
});
|
||
}
|
||
};
|
||
|
||
// 定义要筛选的指标 ID,按网元类型组织
|
||
const TARGET_KPI_IDS: Record<NeType, string[]> = {
|
||
AMF: ['AMF.02', 'AMF.03'],
|
||
UPF: ['UPF.04', 'UPF.05'],
|
||
IMS: ['SCSCF.03', 'SCSCF.04', 'SCSCF.05', 'SCSCF.06', 'SCSCF.07', 'SCSCF.08'],
|
||
MME: ['MME.A.01','MME.A.02'],
|
||
|
||
// 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'],
|
||
};
|
||
|
||
// 原始KPI标题(用于内部计算)
|
||
const BASE_KPI_TITLE: Record<string, string> = {
|
||
'AMF.02': '5G Registration Request',
|
||
'AMF.03': '5G Registration Success',
|
||
'UPF.04': 'UPF Downlink Throughput',
|
||
'UPF.05': 'UPF Uplink Throughput',
|
||
'SCSCF.03': 'IMS Registration Success',
|
||
'SCSCF.04': 'IMS Registration Request',
|
||
'SCSCF.05': 'MO Call Success',
|
||
'SCSCF.06': 'MO Call Attempt',
|
||
'SCSCF.07': 'MT Call Success',
|
||
'SCSCF.08': 'MT Call Attempt',
|
||
'MME.A.01':'4G Registration Request',
|
||
'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();
|
||
|
||
// 添加网元列表相关变量
|
||
const neList = ref<Record<NeType, { neId: string; neName: string }[]>>({
|
||
AMF: [],
|
||
UPF: [],
|
||
IMS: [],
|
||
MME: [],
|
||
});
|
||
|
||
// 添加获取网元列表的函数
|
||
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,
|
||
});
|
||
}
|
||
});
|
||
|
||
} 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) => {
|
||
if (bool) {
|
||
if (!chart) {
|
||
isRealtime.value = false;
|
||
return;
|
||
}
|
||
if (!ws.value) {
|
||
ws.value = new WS();
|
||
}
|
||
// 只清空图表数据,不影响表格统计数据
|
||
chartData.value = [];
|
||
|
||
const options: OptionsType = {
|
||
url: '/ws',
|
||
params: {
|
||
// 为所有网元创建订阅
|
||
subGroupID: ALL_NE_TYPES.flatMap(type =>
|
||
neList.value[type].map(ne => `10_${type}_${ne.neId}`)
|
||
).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>) => {
|
||
if (!chart) {
|
||
return;
|
||
}
|
||
const { code, data } = res;
|
||
if (code === RESULT_CODE_ERROR || !data?.groupId) {
|
||
return;
|
||
}
|
||
|
||
// 解析订阅组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(),
|
||
};
|
||
|
||
// 为每个网元的每个计算型指标添加数据
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 更新图表数据(只影响图表,不影响表格)
|
||
updateChartData(newData);
|
||
};
|
||
|
||
// 添加数据处理函数
|
||
const processChartData = (rawData: any[]) => {
|
||
const groupedData = new Map<string, any>(); //数据按时间分组
|
||
rawData.forEach(item => {
|
||
//合并相同时间点的数据
|
||
const timeKey = item.timeGroup;
|
||
if (!groupedData.has(timeKey)) {
|
||
//按时间排序
|
||
groupedData.set(timeKey, { timeGroup: timeKey });
|
||
}
|
||
Object.assign(groupedData.get(timeKey), item);
|
||
});
|
||
|
||
return Array.from(groupedData.values())
|
||
.sort((a, b) => Number(a.timeGroup) - Number(b.timeGroup))
|
||
.map(item => {
|
||
//转换成图表需要的格式
|
||
const dataItem: ChartDataItem = { date: item.timeGroup.toString() };
|
||
selectedKPIs.value.forEach(kpiId => {
|
||
dataItem[kpiId] = Number(item[kpiId]) || 0;
|
||
});
|
||
return dataItem;
|
||
});
|
||
};
|
||
// 获取图表数据方法
|
||
const fetchChartData = async () => {
|
||
if (kpiColumns.value.length === 0) {
|
||
updateChart();
|
||
return;
|
||
}
|
||
rangeLoading.value = true;
|
||
try {
|
||
const [startTime, endTime] = dateRange.value;
|
||
if (!startTime || !endTime) {
|
||
console.warn('Invalid date range:', dateRange.value);
|
||
return;
|
||
}
|
||
|
||
// 创建并行请求数组
|
||
const requests = [];
|
||
for (const neType of ALL_NE_TYPES) {
|
||
for (const ne of neList.value[neType]) {
|
||
const params = {
|
||
neType,
|
||
neId: ne.neId,
|
||
startTime: String(startTime),
|
||
endTime: String(endTime),
|
||
sortField: 'timeGroup',
|
||
sortOrder: 'asc',
|
||
interval: 60 * 15,
|
||
kpiIds: TARGET_KPI_IDS[neType].join(','),
|
||
};
|
||
|
||
requests.push(
|
||
listKPIData(params).then(res => {
|
||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||
return res.data.map(item => ({
|
||
...item,
|
||
_neId: ne.neId,
|
||
neName: `${neType}-${ne.neId}`,
|
||
}));
|
||
}
|
||
return [];
|
||
})
|
||
);
|
||
}
|
||
}
|
||
|
||
// 并行执行所有请求
|
||
const results = await Promise.all(requests);
|
||
const allData = results.flat();
|
||
|
||
// 按时间分组处理数据
|
||
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]) {
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return dataItem;
|
||
});
|
||
|
||
updateChart();
|
||
} catch (error) {
|
||
console.error('Failed to fetch chart data:', error);
|
||
message.error(t('common.getInfoFail'));
|
||
} finally {
|
||
rangeLoading.value = false;
|
||
}
|
||
};
|
||
|
||
// 存储每个指标的临时固定颜色
|
||
const kpiColors = new Map<string, string>();
|
||
|
||
// 更新图表类型
|
||
const getSeriesConfig = () => ({
|
||
symbol: 'none',
|
||
symbolSize: 6,
|
||
smooth: 0.6,
|
||
showSymbol: true,
|
||
});
|
||
// 添加一个函数来获取当前主题下的网格线颜色
|
||
const getSplitLineColor = () => {
|
||
return document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#333333'
|
||
: '#E8E8E8'; // 亮色模式返回 undefined,使用默认颜色
|
||
};
|
||
|
||
// 添加主题变化的观察器
|
||
const themeObserver = new MutationObserver(() => {
|
||
if (chart) {
|
||
//清空颜色缓存
|
||
kpiColors.clear();
|
||
// 获取当前的主题色
|
||
const splitLineColor = getSplitLineColor();
|
||
|
||
// 重新设置完整的图表配置并触发重新渲染
|
||
requestAnimationFrame(() => {
|
||
// 先生成所有新的颜色并存储
|
||
selectedKPIs.value.forEach(kpiId => {
|
||
const color = generateColorRGBA();
|
||
kpiColors.set(kpiId, color);
|
||
});
|
||
// 使用存储的颜色更新图表系列
|
||
const series = selectedKPIs.value
|
||
.map(kpiId => {
|
||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||
if (!kpi) return null;
|
||
|
||
return {
|
||
name: kpi.title,
|
||
type: 'line',
|
||
data: chartData.value.map(item => item[kpiId] || 0),
|
||
itemStyle: { color: kpiColors.get(kpiId) },
|
||
...getSeriesConfig(),
|
||
};
|
||
})
|
||
.filter(Boolean);
|
||
const option = {
|
||
title: {
|
||
text: t('views.perfManage.kpiOverView.kpiChartTitle'),
|
||
left: 'center',
|
||
textStyle: {
|
||
color:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
},
|
||
xAxis: {
|
||
// 保持现有的 xAxis 配置
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: chartData.value.map(item =>
|
||
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||
),
|
||
axisLabel: {
|
||
color:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
color: splitLineColor,
|
||
},
|
||
},
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
formatter: '{value}',
|
||
color:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
splitNumber: 5,
|
||
scale: true,
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
color: splitLineColor,
|
||
},
|
||
},
|
||
// 根据指标类型显示单位
|
||
name: getYAxisUnit(),
|
||
nameTextStyle: {
|
||
fontSize: 12,
|
||
padding: [0, 0, 0, 0],
|
||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
},
|
||
legend: {
|
||
show: false,
|
||
selected: Object.fromEntries(
|
||
selectedKPIs.value.map(kpiId => [
|
||
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
|
||
selectedRows.value.length === 0
|
||
? true
|
||
: selectedRows.value.includes(kpiId),
|
||
])
|
||
),
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
confine: true, // 限制 tooltip 显示范围
|
||
backgroundColor:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? 'rgba(48, 48, 48, 0.8)'
|
||
: 'rgba(255, 255, 255, 0.9)',
|
||
borderColor:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#555'
|
||
: '#ddd',
|
||
textStyle: {
|
||
color:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);',
|
||
},
|
||
// 重新设置系列数据
|
||
series,
|
||
};
|
||
|
||
// 使用新的配置更新图表
|
||
chart!.setOption(option, true);
|
||
// 强制重新渲染
|
||
chart!.resize();
|
||
// 更新统计数据表格(不重新获取数据)
|
||
nextTick(() => {
|
||
fnInitKpiStatsData();
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
const updateChart = () => {
|
||
if (!chart || !kpiColumns.value.length) return;
|
||
|
||
const series = [];
|
||
for (const neType of ALL_NE_TYPES) {
|
||
for (const ne of neList.value[neType]) {
|
||
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);
|
||
|
||
series.push({
|
||
name: `${kpi.title}(${ne.neName})`,
|
||
type: 'line',
|
||
data: chartData.value.map(item => item[key] || 0),
|
||
itemStyle: { color },
|
||
symbol: 'none',
|
||
symbolSize: 6,
|
||
smooth: 0.6,
|
||
showSymbol: true,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
const option = {
|
||
title: {
|
||
text: t('views.perfManage.kpiOverView.kpiChartTitle'),
|
||
left: 'center',
|
||
// 添加文字颜色配置,根据主题切换
|
||
textStyle: {
|
||
color:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
confine: true,
|
||
backgroundColor:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? 'rgba(48, 48, 48, 0.8)'
|
||
: 'rgba(255, 255, 255, 0.9)',
|
||
borderColor:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#555'
|
||
: '#ddd',
|
||
textStyle: {
|
||
color:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);',
|
||
},
|
||
legend: {
|
||
data: series.map(s => s.name),
|
||
type: 'scroll',
|
||
orient: 'horizontal',
|
||
top: 25,
|
||
textStyle: {
|
||
fontSize: 12,
|
||
},
|
||
selected: Object.fromEntries(
|
||
kpiStats.value.map(item => [
|
||
item.title,
|
||
selectedRows.value.length === 0 ||
|
||
selectedRows.value.includes(item.kpiId),
|
||
])
|
||
),
|
||
show: false,
|
||
left: 'center',
|
||
width: '80%',
|
||
height: 50,
|
||
padding: [5, 10],
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true,
|
||
},
|
||
xAxis: {
|
||
// 指定x轴类型为类目轴,适用于离散的类目数据
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: chartData.value.map(item =>
|
||
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||
),
|
||
axisLabel: {
|
||
// formatter: '{value}',
|
||
color:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
color: getSplitLineColor(),
|
||
},
|
||
},
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
formatter: '{value}',
|
||
color:
|
||
document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
// 添加自计算的分割段数
|
||
splitNumber: 5,
|
||
// 添加自动计算的最小/最大值范围
|
||
scale: true,
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: {
|
||
color: getSplitLineColor(),
|
||
},
|
||
},
|
||
// 根据指标类型显示单位
|
||
name: getYAxisUnit(),
|
||
nameTextStyle: {
|
||
fontSize: 12,
|
||
padding: [0, 0, 0, 0],
|
||
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||
? '#CACADA'
|
||
: '#333',
|
||
},
|
||
},
|
||
series: series, //配置数据
|
||
};
|
||
if (chart) {
|
||
requestAnimationFrame(() => {
|
||
if (chart) {
|
||
// 添加额外的空值检查
|
||
chart.setOption(option, true);
|
||
chart.resize();
|
||
}
|
||
});
|
||
}
|
||
};
|
||
|
||
//钩子函数
|
||
onMounted(async () => {
|
||
try {
|
||
// 获取网元列表和指标信息(这两个很快)
|
||
await fetchNeList();
|
||
await fetchSpecificKPI();
|
||
await nextTick();
|
||
|
||
// 立即初始化表格结构,显示标题行(空的统计值)
|
||
fnInitKpiStatsData();
|
||
|
||
// 初始化图表
|
||
const container = document.getElementById('chartContainer');
|
||
if (container && !chart) {
|
||
chart = echarts.init(container);
|
||
|
||
if (kpiColumns.value.length > 0) {
|
||
await fetchChartData();
|
||
updateChart();
|
||
} else {
|
||
console.warn('No KPI columns available after fetching');
|
||
}
|
||
|
||
// 创建 ResizeObserver 实例监听图表容器大小变化
|
||
observer = new ResizeObserver(() => {
|
||
if (chart) {
|
||
chart.resize();
|
||
}
|
||
});
|
||
// 监听元素大小变化
|
||
observer.observe(container);
|
||
} else if (chart) {
|
||
console.warn('Chart already initialized, skipping initialization');
|
||
} else {
|
||
console.error('Chart container not found');
|
||
}
|
||
|
||
// 添加主题观察器
|
||
themeObserver.observe(document.documentElement, {
|
||
attributes: true,
|
||
attributeFilter: ['data-theme'],
|
||
});
|
||
|
||
// 异步获取统计数据(这个慢,放在最后)
|
||
nextTick(() => {
|
||
fnGetKpiStatsData();
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error('Failed to initialize:', error);
|
||
message.error(t('common.initFail'));
|
||
}
|
||
});
|
||
|
||
// 存储指标列信
|
||
const kpiColumns = ref<KPIColumn[]>([]);
|
||
// 添加选中指标的的状态
|
||
const selectedKPIs = ref<string[]>(Object.values(CALCULATED_KPIS).flat().map(kpi => kpi.id));
|
||
|
||
// 获取网元指标
|
||
const fetchSpecificKPI = async () => {
|
||
const language =
|
||
currentLocale.value.split('_')[0] === 'zh'
|
||
? 'cn'
|
||
: currentLocale.value.split('_')[0];
|
||
|
||
try {
|
||
let allKPIs: KPIColumn[] = [];
|
||
|
||
// 为每个网元类型创建计算型KPI列
|
||
for (const neType of ALL_NE_TYPES) {
|
||
const calculatedKPIs = getCalculatedKPIsForNeType(neType);
|
||
|
||
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(CALCULATED_KPIS).flat().map(kpi => kpi.id);
|
||
|
||
if (kpiColumns.value.length === 0) {
|
||
console.warn('No KPIs found');
|
||
} else {
|
||
// console.log(`Found ${kpiColumns.value.length} total KPIs`);
|
||
}
|
||
|
||
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;
|
||
}
|
||
// 断开主题观察器
|
||
themeObserver.disconnect();
|
||
});
|
||
|
||
// 实时数据更新图表数据方法
|
||
const updateChartData = (newData: ChartDataItem) => {
|
||
if (!chart) {
|
||
return;
|
||
}
|
||
chartData.value.push(newData);
|
||
if (chartData.value.length > 100) {
|
||
//100改为50
|
||
chartData.value.shift(); //大于100条时删除最早的数据
|
||
}
|
||
//使用try-catch包裹图表更新逻辑
|
||
try {
|
||
requestAnimationFrame(() => {
|
||
if (!chart) return;
|
||
const option = {
|
||
xAxis: {
|
||
data: chartData.value.map(item =>
|
||
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||
),
|
||
},
|
||
series: ALL_NE_TYPES.flatMap(type =>
|
||
neList.value[type].map(ne =>
|
||
getCalculatedKPIsForNeType(type).map(kpi => {
|
||
const key = `${kpi.id}_${ne.neId}`;
|
||
return {
|
||
type: 'line',
|
||
data: chartData.value.map(item => item[key] || 0),
|
||
name: `${kpi.title}(${ne.neName})`,
|
||
};
|
||
})
|
||
)
|
||
).flat(),
|
||
};
|
||
chart.setOption(option);
|
||
});
|
||
|
||
// 实时数据只更新图表,不影响统计表格数据
|
||
} catch (error) {
|
||
console.error('Failed to update chart:', error);
|
||
}
|
||
};
|
||
|
||
// 添加一个接口定义指标统计数据的类型
|
||
interface KPIStats {
|
||
kpiId: string;
|
||
title: string;
|
||
last1Day: number | string;
|
||
last7Days: number | string;
|
||
last30Days: number | string;
|
||
}
|
||
|
||
// 添加计算属性,用于计算每个指标的统计数据
|
||
// 将 kpiStats 从计算属性改为响应式引用
|
||
const kpiStats = ref<KPIStats[]>([]);
|
||
|
||
/**初始化关键指标统计表格数据 */
|
||
function fnInitKpiStatsData() {
|
||
// 先初始化表格,显示指标×网元的列表和默认值
|
||
kpiStats.value = [];
|
||
|
||
for (const neType of ALL_NE_TYPES) {
|
||
for (const ne of neList.value[neType]) {
|
||
const calculatedKPIs = getCalculatedKPIsForNeType(neType);
|
||
for (const kpi of calculatedKPIs) {
|
||
kpiStats.value.push({
|
||
kpiId: `${kpi.id}_${ne.neId}`,
|
||
title: `${kpi.title}(${ne.neName})`,
|
||
last1Day: '', // 空白显示,loading状态表示正在获取数据
|
||
last7Days: '',
|
||
last30Days: '',
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**获取关键指标近期统计数据 */
|
||
async function fnGetKpiStatsData() {
|
||
if (!kpiColumns.value.length) {
|
||
return;
|
||
}
|
||
|
||
tableLoading.value = true;
|
||
const now = new Date();
|
||
const now_ms = now.getTime();
|
||
const day1_start = now_ms - (1 * 24 * 60 * 60 * 1000); // 1天前
|
||
const day7_start = now_ms - (7 * 24 * 60 * 60 * 1000); // 7天前
|
||
const day30_start = now_ms - (30 * 24 * 60 * 60 * 1000); // 30天前
|
||
|
||
try {
|
||
// 创建所有网元的并发请求
|
||
const requests = [];
|
||
|
||
for (const neType of ALL_NE_TYPES) {
|
||
for (const ne of neList.value[neType]) {
|
||
const params = {
|
||
neType,
|
||
neId: ne.neId,
|
||
startTime: String(day30_start),
|
||
endTime: String(now_ms),
|
||
sortField: 'timeGroup',
|
||
sortOrder: 'asc',
|
||
interval: 60 * 15,
|
||
kpiIds: TARGET_KPI_IDS[neType].join(','),
|
||
};
|
||
|
||
// 添加到并发请求数组
|
||
requests.push(
|
||
listKPIData(params).then(res => ({
|
||
neType,
|
||
neId: ne.neId,
|
||
success: res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data),
|
||
data: res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data) ? res.data : []
|
||
})).catch(error => {
|
||
console.error(`获取网元${ne.neId}统计数据失败:`, error);
|
||
return {
|
||
neType,
|
||
neId: ne.neId,
|
||
success: false,
|
||
data: []
|
||
};
|
||
})
|
||
);
|
||
}
|
||
}
|
||
|
||
// 并发执行所有请求
|
||
const results = await Promise.all(requests);
|
||
|
||
// 处理所有结果
|
||
results.forEach(result => {
|
||
const { neType, neId, success, data } = result;
|
||
|
||
if (success && data.length > 0) {
|
||
// 为每个计算型指标计算统计值
|
||
const calculatedKPIs = getCalculatedKPIsForNeType(neType as NeType);
|
||
for (const kpi of calculatedKPIs) {
|
||
const key = `${kpi.id}_${neId}`;
|
||
|
||
// 根据时间范围筛选有效数据(非零数据)
|
||
const data1Day = data.filter((item: any) => {
|
||
const itemTime = Number(item.timeGroup);
|
||
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);
|
||
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);
|
||
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;
|
||
|
||
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));
|
||
}
|
||
};
|
||
|
||
// 更新对应的统计数据
|
||
const statsIndex = kpiStats.value.findIndex((item: any) => item.kpiId === key);
|
||
if (statsIndex !== -1) {
|
||
kpiStats.value[statsIndex].last1Day = calculateValue(data1Day);
|
||
kpiStats.value[statsIndex].last7Days = calculateValue(data7Days);
|
||
kpiStats.value[statsIndex].last30Days = calculateValue(data30Days);
|
||
}
|
||
}
|
||
} else {
|
||
// 如果获取失败,保持空白显示
|
||
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 = '';
|
||
kpiStats.value[statsIndex].last7Days = '';
|
||
kpiStats.value[statsIndex].last30Days = '';
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 更新图表显示
|
||
updateChartLegendSelect();
|
||
} catch (error) {
|
||
console.error('获取统计数据失败:', error);
|
||
// 如果获取失败,保持空白显示
|
||
for (const statsItem of kpiStats.value) {
|
||
statsItem.last1Day = '';
|
||
statsItem.last7Days = '';
|
||
statsItem.last30Days = '';
|
||
}
|
||
} finally {
|
||
tableLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 添加一个计算函数来更新统计数据(保持向后兼容)
|
||
const updateKpiStats = () => {
|
||
if (!kpiColumns.value.length) {
|
||
kpiStats.value = [];
|
||
return;
|
||
}
|
||
|
||
// 只初始化统计数据表格结构,不获取数据
|
||
fnInitKpiStatsData();
|
||
};
|
||
|
||
// 添加表列定义
|
||
const statsColumns: TableColumnType<KPIStats>[] = [
|
||
{
|
||
title: '',
|
||
key: 'icon',
|
||
width: 50,
|
||
customRender: ({ record }: { record: KPIStats }) => {
|
||
// 获取当前主题下的颜色
|
||
// 直接使用 kpiColors 中存储的颜色
|
||
const color = kpiColors.get(record.kpiId);
|
||
return h(LineOutlined, {
|
||
style: {
|
||
color: color || '#000', // 使用与折线图相同的颜色
|
||
fontSize: '30px', // 增大图标尺寸到30px
|
||
fontWeight: 'bold', // 加粗
|
||
},
|
||
});
|
||
},
|
||
},
|
||
{
|
||
title: t('views.perfManage.kpiOverView.kpiName'),
|
||
dataIndex: 'title',
|
||
key: 'title',
|
||
width: '40%',
|
||
},
|
||
{
|
||
title: () => h('div', { style: { display: 'flex', alignItems: 'right', gap: '4px' } }, [
|
||
h('span', t('views.perfManage.customTarget.ago1')),
|
||
h(InfoCircleOutlined, {
|
||
title: t('views.perfManage.kpiOverView.tips'),
|
||
}),
|
||
]),
|
||
dataIndex: 'last1Day',
|
||
key: 'last1Day',
|
||
width: '20%',
|
||
sortDirections: ['ascend', 'descend'],
|
||
customRender: ({ record }: { record: KPIStats }) => {
|
||
const value = record.last1Day;
|
||
// 如果是空值,直接显示空白
|
||
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}`;
|
||
}
|
||
},
|
||
},
|
||
{
|
||
title: t('views.perfManage.customTarget.ago7'),
|
||
dataIndex: 'last7Days',
|
||
key: 'last7Days',
|
||
width: '20%',
|
||
sortDirections: ['ascend', 'descend'],
|
||
customRender: ({ record }: { record: KPIStats }) => {
|
||
const value = record.last7Days;
|
||
// 如果是空值,直接显示空白
|
||
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}`;
|
||
}
|
||
},
|
||
},
|
||
{
|
||
title: t('views.perfManage.customTarget.ago30'),
|
||
dataIndex: 'last30Days',
|
||
key: 'last30Days',
|
||
width: '20%',
|
||
sortDirections: ['ascend', 'descend'],
|
||
customRender: ({ record }: { record: KPIStats }) => {
|
||
const value = record.last30Days;
|
||
// 如果是空值,直接显示空白
|
||
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}`;
|
||
}
|
||
},
|
||
},
|
||
];
|
||
|
||
// 将 selectedRow 改为 selectedRows 数组
|
||
const selectedRows = ref<string[]>([]);
|
||
|
||
// 修改处理行点击的方法
|
||
const handleRowClick = (record: KPIStats) => {
|
||
const index = selectedRows.value.indexOf(record.kpiId);
|
||
if (index > -1) {
|
||
// 如果已经选中,则取消选中
|
||
selectedRows.value.splice(index, 1);
|
||
} else {
|
||
// 添加新的选中项
|
||
selectedRows.value.push(record.kpiId);
|
||
}
|
||
// 更新图表显示
|
||
updateChartLegendSelect();
|
||
};
|
||
|
||
// 修改更新图表图例选中态的方法
|
||
const updateChartLegendSelect = () => {
|
||
if (!chart) return;
|
||
|
||
const legendSelected = Object.fromEntries(
|
||
kpiStats.value.map(item => [
|
||
item.title,
|
||
selectedRows.value.length === 0 ||
|
||
selectedRows.value.includes(item.kpiId),
|
||
])
|
||
);
|
||
|
||
chart.setOption({
|
||
legend: {
|
||
selected: legendSelected,
|
||
},
|
||
});
|
||
};
|
||
|
||
// 修改表格行的自定义配置
|
||
const tableRowConfig = computed(() => {
|
||
return (record: KPIStats) => ({
|
||
onClick: () => handleRowClick(record),
|
||
class: selectedRows.value.includes(record.kpiId) ? 'selected-row' : '',
|
||
});
|
||
});
|
||
</script>
|
||
<template>
|
||
<div class="kpi-overview">
|
||
<div class="controls">
|
||
<a-range-picker
|
||
v-model:value="dateRange"
|
||
:show-time="{ format: 'HH:mm:ss' }"
|
||
format="YYYY-MM-DD HH:mm:ss"
|
||
:value-format="'x'"
|
||
:disabled="isRealtime"
|
||
:presets="ranges"
|
||
:loading="rangeLoading"
|
||
@change="handleDateChange"
|
||
/>
|
||
<a-form-item
|
||
:label="
|
||
isRealtime
|
||
? t('views.dashboard.cdr.realTimeDataStop')
|
||
: t('views.dashboard.cdr.realTimeDataStart')
|
||
"
|
||
>
|
||
<a-switch v-model:checked="isRealtime" @change="toggleRealtime" />
|
||
</a-form-item>
|
||
<!-- <a-button-->
|
||
<!-- type="primary"-->
|
||
<!-- @click="toggleChartVisibility"-->
|
||
<!-- class="chart-toggle-btn"-->
|
||
<!-- :title="isChartVisible ? 'hide' : 'show'"-->
|
||
<!-- >-->
|
||
<!-- <DownOutlined v-if="isChartVisible" />-->
|
||
<!-- <UpOutlined v-else />-->
|
||
<!-- <span style="margin-left: 4px;">{{ isChartVisible ? 'Foldable the chart' : 'Expand the chart' }}</span>-->
|
||
<!-- </a-button>-->
|
||
</div>
|
||
<div class="chart-wrapper">
|
||
<a-button
|
||
type="link"
|
||
@click="toggleChartVisibility"
|
||
class="chart-toggle-btn"
|
||
:title="isChartVisible ? 'hide' : 'show'"
|
||
>
|
||
<DownOutlined v-if="isChartVisible" />
|
||
<RightOutlined v-else />
|
||
<span style="margin-left: 4px;">{{ isChartVisible ? 'Expand Graph' : 'Collapse Graph' }}</span>
|
||
</a-button>
|
||
<div
|
||
id="chartContainer"
|
||
class="chart-container"
|
||
:style="{ display: isChartVisible ? 'block' : 'none' }"
|
||
></div>
|
||
<div class="table-container">
|
||
<a-table
|
||
:columns="statsColumns"
|
||
:data-source="kpiStats"
|
||
:pagination="false"
|
||
:scroll="{ y: 250 }"
|
||
size="small"
|
||
:loading="tableLoading"
|
||
:custom-row="tableRowConfig"
|
||
>
|
||
</a-table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<style scoped>
|
||
/* 基础布局样式 */
|
||
.kpi-overview {
|
||
padding: 20px;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
gap: 16px;
|
||
margin-bottom: 20px;
|
||
align-items: center;
|
||
}
|
||
|
||
.chart-wrapper {
|
||
border-radius: 4px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||
|
||
}
|
||
|
||
[data-theme='light'] .chart-wrapper {
|
||
background: #fff;
|
||
}
|
||
|
||
[data-theme='dark'] .chart-wrapper {
|
||
background: #1f1f1f;
|
||
}
|
||
|
||
.chart-container {
|
||
height: 450px;
|
||
width: 100%;
|
||
|
||
}
|
||
|
||
.table-container {
|
||
height: 282px;
|
||
width: 100%;
|
||
margin-top: 20px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 图表隐藏时,表格容器的高度调整 */
|
||
.chart-wrapper:has(.chart-container[style*="display: none"]) .table-container {
|
||
height: 500px;
|
||
margin-top: 0;
|
||
}
|
||
|
||
/* 表格布局相关样式 */
|
||
:deep(.ant-table-wrapper),
|
||
:deep(.ant-table),
|
||
:deep(.ant-table-container),
|
||
:deep(.ant-table-content) {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.ant-table-body) {
|
||
flex: 1;
|
||
overflow-y: auto !important;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* 表格行和表头样式 */
|
||
:deep(.ant-table-thead tr th),
|
||
:deep(.ant-table-tbody tr td) {
|
||
padding: 8px;
|
||
height: 40px;
|
||
}
|
||
|
||
/* 组件统一样式 */
|
||
: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-table-body::-webkit-scrollbar) {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
|
||
:deep(.ant-table-body::-webkit-scrollbar-thumb) {
|
||
background: #ccc;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
:deep(.ant-table-body::-webkit-scrollbar-track) {
|
||
background: #f1f1f1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-thumb) {
|
||
background: #4c4c4c;
|
||
}
|
||
|
||
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-track) {
|
||
background: #2a2a2a;
|
||
}
|
||
|
||
/* 选中行样式 */
|
||
:deep(.selected-row) {
|
||
background-color: rgba(24, 144, 255, 0.1) !important;
|
||
}
|
||
|
||
[data-theme='dark'] :deep(.selected-row) {
|
||
background-color: rgba(24, 144, 255, 0.2) !important;
|
||
}
|
||
|
||
/* 选中行悬停效果 */
|
||
:deep(.selected-row:hover) {
|
||
background-color: rgba(24, 144, 255, 0.2) !important;
|
||
}
|
||
|
||
:deep(.ant-checkbox-wrapper) {
|
||
width: 100%;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 图表切换按钮样式 */
|
||
.chart-toggle-btn {
|
||
margin-left: 8px;
|
||
}
|
||
</style>
|