feat:关键指标概览界面重构(grafana标准)
This commit is contained in:
@@ -1096,6 +1096,9 @@ export default {
|
|||||||
"layout3": "Layout 3"
|
"layout3": "Layout 3"
|
||||||
},
|
},
|
||||||
kpiOverView:{
|
kpiOverView:{
|
||||||
|
"kpiName":"NE Metrics Name",
|
||||||
|
"maxValue":"Max Value",
|
||||||
|
"minValue":"Min Value",
|
||||||
"kpiChartTitle":"Overview of NE metrics",
|
"kpiChartTitle":"Overview of NE metrics",
|
||||||
"changeLine":"Change to Line Charts",
|
"changeLine":"Change to Line Charts",
|
||||||
"changeBar":"Change to Bar Charts",
|
"changeBar":"Change to Bar Charts",
|
||||||
|
|||||||
@@ -1096,6 +1096,9 @@ export default {
|
|||||||
"layout3": "布局3"
|
"layout3": "布局3"
|
||||||
},
|
},
|
||||||
kpiOverView:{
|
kpiOverView:{
|
||||||
|
"kpiName":"指标名",
|
||||||
|
"maxValue":"最大值",
|
||||||
|
"minValue":"最小值",
|
||||||
"kpiChartTitle":"网元指标概览",
|
"kpiChartTitle":"网元指标概览",
|
||||||
"changeLine":"切换为折线图",
|
"changeLine":"切换为折线图",
|
||||||
"changeBar":"切换为柱状图",
|
"changeBar":"切换为柱状图",
|
||||||
|
|||||||
@@ -1,53 +1,20 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue';
|
import { ref, onMounted, onUnmounted, nextTick, computed, h } from 'vue';
|
||||||
import * as echarts from 'echarts/core';
|
import * as echarts from 'echarts/core';
|
||||||
import { GridComponent, TooltipComponent, TitleComponent,LegendComponent } from 'echarts/components';
|
import { GridComponent, TooltipComponent, TitleComponent,LegendComponent } from 'echarts/components';
|
||||||
import { LineChart, BarChart } from 'echarts/charts';
|
import { LineChart } from 'echarts/charts';
|
||||||
import { CanvasRenderer } from 'echarts/renderers';
|
import { CanvasRenderer } from 'echarts/renderers';
|
||||||
import { getKPITitle, listKPIData } from '@/api/perfManage/goldTarget';
|
import { getKPITitle, listKPIData } from '@/api/perfManage/goldTarget';
|
||||||
import useI18n from '@/hooks/useI18n';
|
import useI18n from '@/hooks/useI18n';
|
||||||
import { message, TourProps } from 'ant-design-vue';
|
import { message,} from 'ant-design-vue';
|
||||||
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||||
import { generateColorRGBA } from '@/utils/generate-utils';
|
import { generateColorRGBA } from '@/utils/generate-utils';
|
||||||
import { BarChartOutlined, LineChartOutlined, UnorderedListOutlined, MoreOutlined } from '@ant-design/icons-vue';
|
import { LineOutlined } from '@ant-design/icons-vue';
|
||||||
//配置漫游引导变量
|
|
||||||
const open = ref<boolean>(false);
|
|
||||||
|
|
||||||
const handleOpen = (val: boolean): void => {
|
|
||||||
open.value = val;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ref1=ref<any>(null);
|
|
||||||
const ref2=ref<any>(null);
|
|
||||||
const ref3=ref<any>(null);
|
|
||||||
const ref4=ref<any>(null);
|
|
||||||
|
|
||||||
const steps: TourProps['steps']=[
|
|
||||||
{
|
|
||||||
title:'日期范围选择器',
|
|
||||||
description: '选择要查看数据的时间范围建议范围一个小时',
|
|
||||||
target: () => ref1.value && ref1.value.$el,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title:'选择指标按钮',
|
|
||||||
description: '在这里勾选想要查看的指标',
|
|
||||||
target: () => ref2.value && ref2.value.$el,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title:'切换图表类型',
|
|
||||||
description: '点击切换另一种图表查看数据概览',
|
|
||||||
target: () => ref3.value && ref3.value.$el,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title:'实时数据开关',
|
|
||||||
description: '打开开关观看实时数据',
|
|
||||||
target: () => ref4.value && ref4.value.$el,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
const { t, currentLocale } = useI18n();
|
||||||
//定义KPI接口
|
//定义KPI接口
|
||||||
interface KPIBase{
|
interface KPIBase{
|
||||||
kpiId: string;
|
kpiId: string;
|
||||||
@@ -61,7 +28,7 @@ interface KPIColumn extends KPIBase{
|
|||||||
}
|
}
|
||||||
// 在这里定义 ChartDataItem 接口
|
// 在这里定义 ChartDataItem 接口
|
||||||
interface ChartDataItem {
|
interface ChartDataItem {
|
||||||
date: string; // 将存储完整的时间字符串,包含时分秒
|
date: string; // 将存储完整的时间字符串包含时分秒
|
||||||
[kpiId: string]: string | number; // 动态指标
|
[kpiId: string]: string | number; // 动态指标
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +38,6 @@ type NeType= typeof ALL_NE_TYPES[number];
|
|||||||
|
|
||||||
echarts.use([
|
echarts.use([
|
||||||
LineChart,
|
LineChart,
|
||||||
BarChart,
|
|
||||||
GridComponent,
|
GridComponent,
|
||||||
TooltipComponent,
|
TooltipComponent,
|
||||||
TitleComponent,
|
TitleComponent,
|
||||||
@@ -169,7 +135,7 @@ const handleWebSocketMessage = (kpiEvent:any)=>{
|
|||||||
date: kpiEvent.timeGroup?.toString() || Date.now().toString(),
|
date: kpiEvent.timeGroup?.toString() || Date.now().toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 只添加已选中的指标的数据
|
// 添加已选中的指标的数据
|
||||||
selectedKPIs.value.forEach(kpiId => {
|
selectedKPIs.value.forEach(kpiId => {
|
||||||
newData[kpiId] = Number(kpiEvent[kpiId])||0;
|
newData[kpiId] = Number(kpiEvent[kpiId])||0;
|
||||||
});
|
});
|
||||||
@@ -261,31 +227,17 @@ const fetchChartData = async () => {
|
|||||||
// 存储每个指标的临时固定颜色
|
// 存储每个指标的临时固定颜色
|
||||||
const kpiColors = new Map<string, string>();
|
const kpiColors = new Map<string, string>();
|
||||||
|
|
||||||
// 定义图表类型的响应式变量
|
|
||||||
const chartType = ref<'line' | 'bar'>('line');
|
|
||||||
|
|
||||||
// 切换图表类型的方法
|
|
||||||
const toggleChartType = () => {
|
|
||||||
chartType.value = chartType.value === 'line' ? 'bar' : 'line';
|
|
||||||
updateChart();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新图表类型
|
// 更新图表类型
|
||||||
const getCommonSeriesConfig=(isLine:boolean)=>({
|
const getSeriesConfig = () => ({
|
||||||
symbol: isLine ? 'circle' : undefined,
|
symbol: 'circle',
|
||||||
symbolSize: isLine ? 6 : undefined,
|
symbolSize: 6,
|
||||||
showSymbol: isLine,
|
showSymbol: true,
|
||||||
barWidth: !isLine ? 20 : undefined,
|
|
||||||
barGap: !isLine ? '30%' : undefined,
|
|
||||||
animation: !isLine,
|
|
||||||
animationDuration: 300,
|
|
||||||
animationEasing: 'cubicOut',
|
|
||||||
});
|
});
|
||||||
const updateChart = () => {
|
|
||||||
if (!chart || !kpiColumns.value.length) return; //首先检查图表实例和指标是否存在
|
|
||||||
|
|
||||||
const isLine = chartType.value==='line';
|
const updateChart = () => {
|
||||||
const commonConfig = getCommonSeriesConfig(isLine);
|
if (!chart || !kpiColumns.value.length) return;
|
||||||
|
|
||||||
|
const commonConfig = getSeriesConfig();
|
||||||
|
|
||||||
const series = selectedKPIs.value.map(kpiId => {
|
const series = selectedKPIs.value.map(kpiId => {
|
||||||
const kpi = kpiColumns.value.find(col=>col.kpiId ===kpiId);
|
const kpi = kpiColumns.value.find(col=>col.kpiId ===kpiId);
|
||||||
@@ -295,13 +247,13 @@ const updateChart = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
name: kpi.title,
|
name: kpi.title,
|
||||||
type: chartType.value, // 使用当前选择的图表类型
|
type: 'line',
|
||||||
data:chartData.value.map(item=>item[kpiId]||0),
|
data: chartData.value.map(item=>item[kpiId]||0),
|
||||||
itemStyle: { color },
|
itemStyle: { color },
|
||||||
...commonConfig,
|
...commonConfig,
|
||||||
};
|
};
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
//图表配置对象
|
|
||||||
const option = {
|
const option = {
|
||||||
title: {
|
title: {
|
||||||
text: t('views.perfManage.kpiOverView.kpiChartTitle'),
|
text: t('views.perfManage.kpiOverView.kpiChartTitle'),
|
||||||
@@ -320,16 +272,22 @@ const updateChart = () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
//图例配置
|
data: selectedKPIs.value.map(kpiId =>
|
||||||
data: selectedKPIs.value.map(kpiId=>kpiColumns.value.find(col=>col.kpiId===kpiId)?.title),
|
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId
|
||||||
|
),
|
||||||
type: 'scroll',
|
type: 'scroll',
|
||||||
orient: 'horizontal',
|
orient: 'horizontal',
|
||||||
top: 25,
|
top: 25,
|
||||||
textStyle: {
|
textStyle: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
selected:Object.fromEntries(selectedKPIs.value.map(kpiId=>[kpiId,true])),
|
selected: Object.fromEntries(
|
||||||
show: true,
|
selectedKPIs.value.map(kpiId => [
|
||||||
|
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
|
||||||
|
selectedRow.value ? kpiId === selectedRow.value : true
|
||||||
|
])
|
||||||
|
),
|
||||||
|
show: false,
|
||||||
left: 'center',
|
left: 'center',
|
||||||
width: '80%',
|
width: '80%',
|
||||||
height: 50,
|
height: 50,
|
||||||
@@ -349,11 +307,11 @@ const updateChart = () => {
|
|||||||
//控制坐标轴两边留白
|
//控制坐标轴两边留白
|
||||||
// 当为折线图时(isLine为true)时不留白,柱状图时留白
|
// 当为折线图时(isLine为true)时不留白,柱状图时留白
|
||||||
// 这样可以让折线图从原点开始,柱状图有合适的间距
|
// 这样可以让折线图从原点开始,柱状图有合适的间距
|
||||||
boundaryGap: isLine,
|
boundaryGap: false,
|
||||||
// 设置x轴的数据
|
// 设置x轴的数据
|
||||||
// 将时间戳转换为格式化的时间字符串
|
// 将时间戳转换为格式化的时间字符串
|
||||||
data:chartData.value.map(item=>
|
data:chartData.value.map(item=>
|
||||||
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||||||
),
|
),
|
||||||
//设置坐标轴刻度标签的样式
|
//设置坐标轴刻度标签的样式
|
||||||
},
|
},
|
||||||
@@ -363,7 +321,7 @@ const updateChart = () => {
|
|||||||
axisLabel: {
|
axisLabel: {
|
||||||
formatter: '{value}',
|
formatter: '{value}',
|
||||||
},
|
},
|
||||||
// 添加自动计算的分割段数
|
// 添加自<EFBFBD><EFBFBD><EFBFBD>计算的分割段数
|
||||||
splitNumber: 5,
|
splitNumber: 5,
|
||||||
// 添加自动计算的最小/最大值范围
|
// 添加自动计算的最小/最大值范围
|
||||||
scale: true,
|
scale: true,
|
||||||
@@ -373,7 +331,7 @@ const updateChart = () => {
|
|||||||
if(chart) {
|
if(chart) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
chart!.setOption(option, true); //使用新的配置更新图表
|
chart!.setOption(option, true); //使用新的配置更新图表
|
||||||
chart!.resize(); //调整图表大小以适应容器
|
chart!.resize(); //调整图表大小适应容器
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 如果已经有 observer,先断开连接
|
// 如果已经有 observer,先断开连接
|
||||||
@@ -399,7 +357,6 @@ const updateChart = () => {
|
|||||||
|
|
||||||
//钩子函数
|
//钩子函数
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
open.value=true;
|
|
||||||
try {
|
try {
|
||||||
// 获取所有网元的指标
|
// 获取所有网元的指标
|
||||||
await fetchSpecificKPI();
|
await fetchSpecificKPI();
|
||||||
@@ -429,121 +386,37 @@ onMounted(async () => {
|
|||||||
// 存储指标列信
|
// 存储指标列信
|
||||||
const kpiColumns = ref<KPIColumn[]>([]);
|
const kpiColumns = ref<KPIColumn[]>([]);
|
||||||
// 添加选中指标的的状态
|
// 添加选中指标的的状态
|
||||||
const selectedKPIs = ref<string[]>([]);
|
const selectedKPIs = ref<string[]>(Object.values(TARGET_KPI_IDS).flat());
|
||||||
// 添加对话框可见性状态
|
|
||||||
const isModalVisible = ref(false);
|
|
||||||
// 添加临时存储下拉框选择的数组
|
|
||||||
const tempSelectedKPIs = ref<string[]>([]);
|
|
||||||
|
|
||||||
// 添加一个变量保存打开对话框时的选择状态
|
|
||||||
const originalSelectedKPIs = ref<string[]>([]);
|
|
||||||
|
|
||||||
// 打开对话框的方法
|
|
||||||
const showKPISelector = () => {
|
|
||||||
|
|
||||||
// 保存当前的选择状态
|
|
||||||
originalSelectedKPIs.value = [...selectedKPIs.value];
|
|
||||||
|
|
||||||
// 初始化临时选择为当前已选择的其他指标
|
|
||||||
const primaryKPIs = Object.values(TARGET_KPI_IDS).flat();
|
|
||||||
tempSelectedKPIs.value = selectedKPIs.value.filter(
|
|
||||||
kpiId => !primaryKPIs.includes(kpiId)
|
|
||||||
);
|
|
||||||
isModalVisible.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 保存选中指标到 localStorage 的方法
|
|
||||||
const saveSelectedKPIs = () => {
|
|
||||||
localStorage.setItem('selectedKPIs', JSON.stringify(selectedKPIs.value));
|
|
||||||
};
|
|
||||||
|
|
||||||
// 取消按钮的处理方法
|
|
||||||
const handleModalCancel = () => {
|
|
||||||
// 恢复到打开对话框时的选择状态
|
|
||||||
selectedKPIs.value = [...originalSelectedKPIs.value];
|
|
||||||
// 清空临时选择
|
|
||||||
tempSelectedKPIs.value = [];
|
|
||||||
isModalVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 确认按钮的处理方法
|
|
||||||
const handleModalOk = () => {
|
|
||||||
|
|
||||||
// 获取主要指标列表
|
|
||||||
const primaryKPIs = Object.values(TARGET_KPI_IDS).flat();
|
|
||||||
|
|
||||||
// 获取当前在主界面选中的主要指标
|
|
||||||
const selectedPrimaryKPIs = selectedKPIs.value.filter(kpiId =>
|
|
||||||
primaryKPIs.includes(kpiId)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 合并选中的主要指标和临时选中的其他指标
|
|
||||||
selectedKPIs.value = Array.from(
|
|
||||||
new Set([
|
|
||||||
...selectedPrimaryKPIs, // 只包含已选中的主要指标
|
|
||||||
...tempSelectedKPIs.value, // 临时选中的其他指标
|
|
||||||
])
|
|
||||||
);
|
|
||||||
|
|
||||||
// 清空临时选择和原始选择
|
|
||||||
tempSelectedKPIs.value = [];
|
|
||||||
originalSelectedKPIs.value = [];
|
|
||||||
|
|
||||||
// 保存选择并更新图表
|
|
||||||
saveSelectedKPIs();
|
|
||||||
updateChart();
|
|
||||||
isModalVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 获取网元指标
|
// 获取网元指标
|
||||||
const fetchSpecificKPI = async () => {
|
const fetchSpecificKPI = async () => {
|
||||||
const language =
|
const language = currentLocale.value.split('_')[0] === 'zh'
|
||||||
currentLocale.value.split('_')[0] === 'zh'
|
? 'cn'
|
||||||
? 'cn'
|
: currentLocale.value.split('_')[0];
|
||||||
: currentLocale.value.split('_')[0];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let allKPIs: KPIColumn[] = [];
|
let allKPIs: KPIColumn[] = [];
|
||||||
|
|
||||||
// 1. 获取所有网元的全部指标
|
// 获取所有网元的指标
|
||||||
for (const neType of ALL_NE_TYPES) {
|
for (const neType of ALL_NE_TYPES) {
|
||||||
const res = await getKPITitle(neType.toUpperCase());
|
const res = await getKPITitle(neType.toUpperCase());
|
||||||
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||||
// 转换指标格式
|
|
||||||
const formattedKPIs = res.data.map(item => ({
|
const formattedKPIs = res.data.map(item => ({
|
||||||
title: item[`${language}Title`],
|
title: item[`${language}Title`],
|
||||||
dataIndex: item.kpiId,
|
dataIndex: item.kpiId,
|
||||||
key: item.kpiId,
|
key: item.kpiId,
|
||||||
kpiId: item.kpiId,
|
kpiId: item.kpiId,
|
||||||
neType: neType, // 添加网元类型信息
|
neType: neType,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 添加到所有指标数组
|
|
||||||
allKPIs = [...allKPIs, ...formattedKPIs];
|
allKPIs = [...allKPIs, ...formattedKPIs];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 更新所有指标到 kpiColumns
|
|
||||||
kpiColumns.value = allKPIs;
|
kpiColumns.value = allKPIs;
|
||||||
|
// 直接使用重要指标
|
||||||
// 3. 尝试加载保存的选择
|
selectedKPIs.value = Object.values(TARGET_KPI_IDS).flat();
|
||||||
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 = Object.values(TARGET_KPI_IDS).flat();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果没有保存的选择,则默认选择重要指标
|
|
||||||
selectedKPIs.value = Object.values(TARGET_KPI_IDS).flat();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (kpiColumns.value.length === 0) {
|
if (kpiColumns.value.length === 0) {
|
||||||
console.warn('No KPIs found');
|
console.warn('No KPIs found');
|
||||||
@@ -572,12 +445,8 @@ onUnmounted(() => {
|
|||||||
chart.dispose();
|
chart.dispose();
|
||||||
chart = null;
|
chart = null;
|
||||||
}
|
}
|
||||||
// 可选:在组件卸载时保存选择
|
|
||||||
saveSelectedKPIs();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { t, currentLocale } = useI18n();
|
|
||||||
|
|
||||||
// 更新图表数据方法
|
// 更新图表数据方法
|
||||||
const updateChartData = (newData: ChartDataItem) => {
|
const updateChartData = (newData: ChartDataItem) => {
|
||||||
if(!chart){
|
if(!chart){
|
||||||
@@ -600,7 +469,7 @@ const updateChartData = (newData: ChartDataItem) => {
|
|||||||
series: selectedKPIs.value.map(kpiId => {
|
series: selectedKPIs.value.map(kpiId => {
|
||||||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||||
return {
|
return {
|
||||||
type: chartType.value,
|
type: 'line',
|
||||||
data: chartData.value.map(item => item[kpiId] || 0),
|
data: chartData.value.map(item => item[kpiId] || 0),
|
||||||
name: kpi?.title || kpiId,
|
name: kpi?.title || kpiId,
|
||||||
};
|
};
|
||||||
@@ -614,66 +483,111 @@ const updateChartData = (newData: ChartDataItem) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// groupedKPIs 计算属性,使用 TARGET_KPI_IDS 来分组过滤
|
// 添加一个接口定义指标统计数据的类型
|
||||||
const groupedKPIs = computed(() => {
|
interface KPIStats {
|
||||||
const groups: Record<string, KPIColumn[]> = {};
|
kpiId: string;
|
||||||
|
title: string;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
}
|
||||||
|
|
||||||
ALL_NE_TYPES.forEach(neType => {
|
// 添加计算属性,用于计算每个指标的最大值和最小值
|
||||||
// 使用 TARGET_KPI_IDS 中定义的指标 ID 来过滤
|
const kpiStats = computed((): KPIStats[] => {
|
||||||
const targetIds = TARGET_KPI_IDS[neType];
|
if (!chartData.value.length || !kpiColumns.value.length) return [];
|
||||||
groups[neType] = kpiColumns.value.filter(kpi =>
|
|
||||||
targetIds.includes(kpi.kpiId)
|
return selectedKPIs.value.map(kpiId => {
|
||||||
);
|
// 找到对应的KPI标题
|
||||||
});
|
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||||
return groups;
|
if (!kpi) return null;
|
||||||
|
|
||||||
|
// 获取该指标的所有数值
|
||||||
|
const values = chartData.value.map(item => Number(item[kpiId]) || 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
kpiId: kpiId,
|
||||||
|
title: kpi.title,
|
||||||
|
max: Math.max(...values),
|
||||||
|
min: Math.min(...values)
|
||||||
|
};
|
||||||
|
}).filter((item): item is KPIStats => item !== null);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 计算其他指标
|
// 添加表格列定义
|
||||||
const secondaryKPIs = computed(() => {
|
const statsColumns = [
|
||||||
const groups: Record<string, KPIColumn[]> = {};
|
{
|
||||||
|
title: '',
|
||||||
if (kpiColumns.value.length === 0) {
|
key: 'icon',
|
||||||
console.warn('No KPI columns available');
|
width: 50,
|
||||||
return groups;
|
customRender: ({ record }: { record: KPIStats }) => {
|
||||||
}
|
return h(LineOutlined, {
|
||||||
|
style: {
|
||||||
ALL_NE_TYPES.forEach(neType => {
|
color: kpiColors.get(record.kpiId) || '#000', // 使用与折线图相同的颜色
|
||||||
// 获取当前网元类型的主要指标 ID
|
fontSize: '30px', // 增大图标尺寸到30px
|
||||||
const primaryIds = TARGET_KPI_IDS[neType];
|
fontWeight: 'bold', // 加粗
|
||||||
// 从所有指标中筛选出当前网元其他指标
|
}
|
||||||
groups[neType] = kpiColumns.value.filter(kpi => {
|
});
|
||||||
return kpi.neType === neType && !primaryIds.includes(kpi.kpiId);
|
|
||||||
// 检查是否不在主要指标列表中
|
|
||||||
// const isNotPrimary = !primaryIds.includes(kpi.kpiId);
|
|
||||||
//
|
|
||||||
// // 检查是否属于当前网元类型
|
|
||||||
// // 使用 getKPITitle API 返回的原始数据中的网元类型信息
|
|
||||||
// const isCurrentNeType = kpi.neType === neType;
|
|
||||||
//
|
|
||||||
// return isCurrentNeType && isNotPrimary;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return groups;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加处理其他指标选择变化的方法
|
|
||||||
const handleSecondaryKPIChange = (kpiId: string, checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
// 如果选中,将指标 ID 添加到临时列表
|
|
||||||
if (!tempSelectedKPIs.value.includes(kpiId)) {
|
|
||||||
tempSelectedKPIs.value = [...tempSelectedKPIs.value, kpiId];
|
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
// 如果取消选中,从临时列表中移除指标 ID
|
{
|
||||||
tempSelectedKPIs.value = tempSelectedKPIs.value.filter(id => id !== kpiId);
|
title: t('views.perfManage.kpiOverView.kpiName'),
|
||||||
|
dataIndex: 'title',
|
||||||
|
key: 'title',
|
||||||
|
width: '65%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('views.perfManage.kpiOverView.maxValue'),
|
||||||
|
dataIndex: 'max',
|
||||||
|
key: 'max',
|
||||||
|
width: '17%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('views.perfManage.kpiOverView.minValue'),
|
||||||
|
dataIndex: 'min',
|
||||||
|
key: 'min',
|
||||||
|
width: '17%',
|
||||||
}
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 添加一个变量来跟踪当前选中的行
|
||||||
|
const selectedRow = ref<string | null>(null);
|
||||||
|
|
||||||
|
// 添加处理行点击的方法
|
||||||
|
const handleRowClick = (record: KPIStats) => {
|
||||||
|
if (selectedRow.value === record.kpiId) {
|
||||||
|
// 如果点击的是当前选中的行,则取消选中
|
||||||
|
selectedRow.value = null;
|
||||||
|
// 更新图表,显示所有指标
|
||||||
|
updateChartLegendSelect();
|
||||||
|
} else {
|
||||||
|
// 选中新行
|
||||||
|
selectedRow.value = record.kpiId;
|
||||||
|
// 更新图表,只显示选中的指标
|
||||||
|
updateChartLegendSelect(record.kpiId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加更新表图例选中态的方法
|
||||||
|
const updateChartLegendSelect = (selectedKpiId?: string) => {
|
||||||
|
if (!chart) return;
|
||||||
|
|
||||||
|
const legendSelected = Object.fromEntries(
|
||||||
|
selectedKPIs.value.map(kpiId => [
|
||||||
|
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
|
||||||
|
selectedKpiId ? kpiId === selectedKpiId : true
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
chart.setOption({
|
||||||
|
legend: {
|
||||||
|
selected: legendSelected
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="kpi-overview">
|
<div class="kpi-overview">
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<a-range-picker
|
<a-range-picker
|
||||||
ref="ref1"
|
|
||||||
v-model:value="dateRange"
|
v-model:value="dateRange"
|
||||||
:show-time="{ format: 'HH:mm:ss' }"
|
:show-time="{ format: 'HH:mm:ss' }"
|
||||||
format="YYYY-MM-DD HH:mm:ss"
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
@@ -681,23 +595,6 @@ const handleSecondaryKPIChange = (kpiId: string, checked: boolean) => {
|
|||||||
:disabled="isRealtime"
|
:disabled="isRealtime"
|
||||||
@change="handleDateChange"
|
@change="handleDateChange"
|
||||||
/>
|
/>
|
||||||
<a-button ref="ref2" @click="showKPISelector">
|
|
||||||
<template #icon>
|
|
||||||
<unordered-list-outlined />
|
|
||||||
</template>
|
|
||||||
{{ t('views.perfManage.kpiOverView.chooseMetrics') }}
|
|
||||||
</a-button>
|
|
||||||
<a-button ref="ref3" @click="toggleChartType">
|
|
||||||
<template #icon>
|
|
||||||
<bar-chart-outlined v-if="chartType === 'line'" />
|
|
||||||
<line-chart-outlined v-else />
|
|
||||||
</template>
|
|
||||||
{{
|
|
||||||
chartType === 'line'
|
|
||||||
? t('views.perfManage.kpiOverView.changeBar')
|
|
||||||
: t('views.perfManage.kpiOverView.changeLine')
|
|
||||||
}}
|
|
||||||
</a-button>
|
|
||||||
<a-form-item
|
<a-form-item
|
||||||
:label="
|
:label="
|
||||||
isRealtime
|
isRealtime
|
||||||
@@ -705,79 +602,25 @@ const handleSecondaryKPIChange = (kpiId: string, checked: boolean) => {
|
|||||||
: t('views.dashboard.cdr.realTimeDataStart')
|
: t('views.dashboard.cdr.realTimeDataStart')
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<a-switch ref="ref4" v-model:checked="isRealtime" @change="toggleRealtime" />
|
<a-switch v-model:checked="isRealtime" @change="toggleRealtime" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-tour :open="open" :steps="steps" @close="handleOpen(false)">
|
|
||||||
<template #indicatorsRender="{ current, total }">
|
|
||||||
<span>{{ current + 1 }} / {{ total }}</span>
|
|
||||||
</template>
|
|
||||||
</a-tour>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="chartContainer" class="chart-container"></div>
|
<div class="chart-wrapper">
|
||||||
<a-modal
|
<div id="chartContainer" class="chart-container"></div>
|
||||||
v-model:visible="isModalVisible"
|
<div class="table-container">
|
||||||
:title="t('views.perfManage.kpiOverView.chooseShowMetrics')"
|
<a-table
|
||||||
@ok="handleModalOk"
|
:columns="statsColumns"
|
||||||
@cancel="handleModalCancel"
|
:data-source="kpiStats"
|
||||||
width="800px"
|
:pagination="false"
|
||||||
:bodyStyle="{ maxHeight: '600px', overflow: 'auto' }"
|
:scroll="{ y: 250 }"
|
||||||
>
|
size="small"
|
||||||
<a-checkbox-group v-model:value="selectedKPIs" >
|
:custom-row="(record) => ({
|
||||||
<div class="kpi-checkbox-list">
|
onClick: () => handleRowClick(record),
|
||||||
<a-card
|
class: record.kpiId === selectedRow ? 'selected-row' : ''
|
||||||
v-for="neType in ALL_NE_TYPES"
|
})"
|
||||||
:key="neType"
|
/>
|
||||||
class="ne-type-card"
|
</div>
|
||||||
:bordered="false"
|
</div>
|
||||||
>
|
|
||||||
{{neType.toUpperCase()}}
|
|
||||||
<template #title>
|
|
||||||
<span class="card-title">{{ neType.toUpperCase() }}</span>
|
|
||||||
</template>
|
|
||||||
<template #extra>
|
|
||||||
<a-dropdown v-if="secondaryKPIs[neType]?.length" :trigger="['click']">
|
|
||||||
<template #overlay>
|
|
||||||
<div class="secondary-kpi-menu" @click.stop>
|
|
||||||
<div class="secondary-kpi-list">
|
|
||||||
<div
|
|
||||||
v-for="kpi in secondaryKPIs[neType]"
|
|
||||||
:key="kpi.kpiId"
|
|
||||||
class="secondary-kpi-item"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<a-checkbox
|
|
||||||
:value="kpi.kpiId"
|
|
||||||
:checked="tempSelectedKPIs.includes(kpi.kpiId)"
|
|
||||||
@change="(e:any) => handleSecondaryKPIChange(kpi.kpiId, e.target.checked)"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
{{ kpi.title }}
|
|
||||||
</a-checkbox>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<a-button type="link" size="small" class="more-metrics-btn" ref="ref6">
|
|
||||||
<more-outlined />
|
|
||||||
<span class="secondary-count">({{ secondaryKPIs[neType].length }})</span>
|
|
||||||
</a-button>
|
|
||||||
</a-dropdown>
|
|
||||||
</template>
|
|
||||||
<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>
|
|
||||||
</a-card>
|
|
||||||
</div>
|
|
||||||
</a-checkbox-group>
|
|
||||||
</a-modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -792,102 +635,55 @@ const handleSecondaryKPIChange = (kpiId: string, checked: boolean) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-wrapper {
|
||||||
height: calc(100vh - 210px);/*调整图表大小*/
|
border-radius: 4px;
|
||||||
width: 100%;
|
padding: 20px;
|
||||||
}
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
/* 网元指标列表样式 */
|
|
||||||
.ne-type-items {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-checkbox-item {
|
[data-theme='light'] .chart-wrapper {
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 100%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
/* 其他指标下拉菜单样式 */
|
|
||||||
.secondary-kpi-menu {
|
|
||||||
border-radius: 4px;
|
|
||||||
box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
|
||||||
0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
|
||||||
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
|
||||||
min-width: 240px;
|
|
||||||
max-width: 320px;
|
|
||||||
}
|
|
||||||
[data-theme='light'] .secondary-kpi-menu {
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
[data-theme='dark'] .secondary-kpi-menu {
|
|
||||||
|
[data-theme='dark'] .chart-wrapper {
|
||||||
background: #1f1f1f;
|
background: #1f1f1f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-kpi-list {
|
.chart-container {
|
||||||
max-height: 300px;
|
height: calc(100vh - 550px);
|
||||||
overflow-y: auto;
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-kpi-list::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-kpi-list::-webkit-scrollbar-thumb {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-kpi-list::-webkit-scrollbar-track {
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-kpi-item {
|
|
||||||
padding: 8px 12px;
|
|
||||||
transition: background-color 0.3s;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.more-metrics-btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
color: #1890ff;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.more-metrics-btn:hover {
|
|
||||||
color: #40a9ff;
|
|
||||||
background: rgba(24, 144, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-count {
|
|
||||||
margin-left: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 优化复选框样式 */
|
|
||||||
:deep(.ant-checkbox-wrapper) {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
height: 282px;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
display: flex;
|
||||||
white-space: nowrap;
|
flex-direction: column;
|
||||||
padding: 4px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-checkbox-wrapper:hover) {
|
/* 表格布局相关样式 */
|
||||||
color: #1890ff;
|
:deep(.ant-table-wrapper),
|
||||||
|
:deep(.ant-table),
|
||||||
|
:deep(.ant-table-container),
|
||||||
|
:deep(.ant-table-content) {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-checkbox-checked .ant-checkbox-inner) {
|
:deep(.ant-table-body) {
|
||||||
background-color: #1890ff;
|
flex: 1;
|
||||||
border-color: #1890ff;
|
overflow-y: auto !important;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格行和表头样式 */
|
||||||
|
:deep(.ant-table-thead tr th),
|
||||||
|
:deep(.ant-table-tbody tr td) {
|
||||||
|
padding: 8px;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 组件统一样式 */
|
/* 组件统一样式 */
|
||||||
@@ -903,10 +699,41 @@ const handleSecondaryKPIChange = (kpiId: string, checked: boolean) => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ant-checkbox-wrapper) {
|
/* 美化滚动条样式 */
|
||||||
width: 100%;
|
:deep(.ant-table-body::-webkit-scrollbar) {
|
||||||
overflow: hidden;
|
width: 6px;
|
||||||
text-overflow: ellipsis;
|
height: 6px;
|
||||||
white-space: nowrap;
|
}
|
||||||
|
|
||||||
|
: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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='dark'] :deep(.selected-row) {
|
||||||
|
background-color: rgba(24, 144, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标悬停样式 */
|
||||||
|
:deep(.ant-table-tbody tr:hover) {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user