style: KPI数据表格头提示信息

This commit is contained in:
TsMask
2025-01-17 15:30:22 +08:00
parent 8adf2a3dd0
commit 806cbbd9ed
3 changed files with 849 additions and 472 deletions

View File

@@ -1,13 +1,21 @@
<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 {
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 { 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';
@@ -16,12 +24,12 @@ import { LineOutlined } from '@ant-design/icons-vue';
import { TableColumnType } from 'ant-design-vue';
const { t, currentLocale } = useI18n();
//定义KPI接口
interface KPIBase{
interface KPIBase {
kpiId: string;
title: string;
}
//继承接口
interface KPIColumn extends KPIBase{
interface KPIColumn extends KPIBase {
dataIndex: string;
key: string;
neType: string;
@@ -34,8 +42,8 @@ interface ChartDataItem {
const tableLoading = ref(false);
const rangeLoading = ref(false);
//网元类型定义
const ALL_NE_TYPES = ['AMF','SMF','UPF','MME','IMS','SMSC'] as const;
type NeType= typeof ALL_NE_TYPES[number];
const ALL_NE_TYPES = ['AMF', 'SMF', 'UPF', 'MME', 'IMS', 'SMSC'] as const;
type NeType = (typeof ALL_NE_TYPES)[number];
echarts.use([
LineChart,
@@ -61,12 +69,19 @@ const ws = ref<WS | null>(null);
// [t('views.monitor.monitor.today')]: [dayjs().startOf('day'), dayjs()],
// });
const ranges = ref([
{label:t('views.perfManage.customTarget.sixHoursAgo'),value:[dayjs().subtract(6, 'hours'),
dayjs(),]},
{label:t('views.perfManage.customTarget.threeHoursAgo'),value:[dayjs().subtract(3, 'hours'),
dayjs(),]},
{label:t('views.monitor.monitor.today'),value:[dayjs().startOf('day'), dayjs()]},
])
{
label: t('views.perfManage.customTarget.sixHoursAgo'),
value: [dayjs().subtract(6, 'hours'), dayjs()],
},
{
label: t('views.perfManage.customTarget.threeHoursAgo'),
value: [dayjs().subtract(3, 'hours'), dayjs()],
},
{
label: t('views.monitor.monitor.today'),
value: [dayjs().startOf('day'), dayjs()],
},
]);
//日期范围响应式变量
const dateRange = ref<[string, string]>([
dayjs().subtract(1, 'hour').startOf('hour').valueOf().toString(), // 上一小时开始
@@ -118,8 +133,8 @@ const TARGET_KPI_IDS: Record<NeType, string[]> = {
// 实时数据开关函数
const fnRealTimeSwitch = (bool: boolean) => {
if (bool) {
if(!chart){
isRealtime.value=false;
if (!chart) {
isRealtime.value = false;
return;
}
if (!ws.value) {
@@ -127,7 +142,7 @@ const fnRealTimeSwitch = (bool: boolean) => {
}
chartData.value = [];
tableLoading.value =true;
tableLoading.value = true;
const options: OptionsType = {
url: '/ws',
params: {
@@ -149,8 +164,8 @@ const wsError = () => {
message.error(t('common.websocketError'));
};
const handleWebSocketMessage = (kpiEvent:any)=>{
if(!kpiEvent)return;
const handleWebSocketMessage = (kpiEvent: any) => {
if (!kpiEvent) return;
// 构造新的数据点
const newData: ChartDataItem = {
@@ -159,40 +174,42 @@ const handleWebSocketMessage = (kpiEvent:any)=>{
// 添加已选中的指标的数据
selectedKPIs.value.forEach(kpiId => {
newData[kpiId] = Number(kpiEvent[kpiId])||0;
newData[kpiId] = Number(kpiEvent[kpiId]) || 0;
});
// 更新数据
updateChartData(newData);
if(tableLoading.value){
tableLoading.value=false;
if (tableLoading.value) {
tableLoading.value = false;
}
};
//成功回调
const wsMessage = (res:Record<string,any>)=>{
if(!chart){
const wsMessage = (res: Record<string, any>) => {
if (!chart) {
return;
}
const{code,data}=res;
if(code===RESULT_CODE_ERROR||!data?.groupId)return;
const { code, data } = res;
if (code === RESULT_CODE_ERROR || !data?.groupId) return;
handleWebSocketMessage(data.data);
};
// 添加数据处理函数
const processChartData = (rawData: any[]) => {
const groupedData = new Map<string, any>();//数据按时间分组
const groupedData = new Map<string, any>(); //数据按时间分组
rawData.forEach(item => {//合并相同时间点的数据
rawData.forEach(item => {
//合并相同时间点的数据
const timeKey = item.timeGroup;
if (!groupedData.has(timeKey)) {//按时间排序
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 => {//转换成图表需要的格式
.map(item => {
//转换成图表需要的格式
const dataItem: ChartDataItem = { date: item.timeGroup.toString() };
selectedKPIs.value.forEach(kpiId => {
dataItem[kpiId] = Number(item[kpiId]) || 0;
@@ -202,21 +219,21 @@ const processChartData = (rawData: any[]) => {
};
// 获取图表数据方法
const fetchChartData = async () => {
if(kpiColumns.value.length===0){
if (kpiColumns.value.length === 0) {
updateChart();
return;
}
tableLoading.value=true;
rangeLoading.value=true;
tableLoading.value = true;
rangeLoading.value = true;
try {
const[startTime,endTime]=dateRange.value;
const [startTime, endTime] = dateRange.value;
if (!startTime || !endTime) {
console.warn('Invalid date range:', dateRange.value);
return;
}
// 创建并行请求数组
const requests = ALL_NE_TYPES.map(async (neType) => {
const requests = ALL_NE_TYPES.map(async neType => {
const params = {
neType,
neId: '001',
@@ -224,7 +241,7 @@ const fetchChartData = async () => {
endTime: String(endTime),
sortField: 'timeGroup',
sortOrder: 'asc',
interval: 60*15,
interval: 60 * 15,
kpiIds: TARGET_KPI_IDS[neType].join(','),
};
@@ -250,9 +267,9 @@ const fetchChartData = async () => {
} catch (error) {
console.error('Failed to fetch chart data:', error);
message.error(t('common.getInfoFail'));
}finally {
tableLoading.value=false;
rangeLoading.value=false;
} finally {
tableLoading.value = false;
rangeLoading.value = false;
}
};
@@ -263,14 +280,14 @@ const kpiColors = new Map<string, string>();
const getSeriesConfig = () => ({
symbol: 'none',
symbolSize: 6,
smooth:0.6,
smooth: 0.6,
showSymbol: true,
});
// 添加一个函数来获取当前主题下的网格线颜色
const getSplitLineColor = () => {
return document.documentElement.getAttribute('data-theme') === 'dark'
? '#333333'
: '#E8E8E8'; // 亮色模式返回 undefined使用默认颜色
: '#E8E8E8'; // 亮色模式返回 undefined使用默认颜色
};
// 添加主题变化的观察器
@@ -289,27 +306,30 @@ const themeObserver = new MutationObserver(() => {
kpiColors.set(kpiId, color);
});
// 使用存储的颜色更新图表系列
const series = selectedKPIs.value.map(kpiId => {
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (!kpi) return null;
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);
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'
}
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
},
xAxis: {
// 保持现有的 xAxis 配置
@@ -319,61 +339,68 @@ const themeObserver = new MutationObserver(() => {
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
),
axisLabel: {
color: document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333'
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
splitLine: {
show: true,
lineStyle: {
color: splitLineColor
}
}
color: splitLineColor,
},
},
},
yAxis: {
type: 'value',
axisLabel: {
formatter: '{value}',
color: document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333'
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
splitNumber: 5,
scale: true,
splitLine: {
show: true,
lineStyle: {
color: splitLineColor
}
}
color: splitLineColor,
},
},
},
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)
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',
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'
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);'
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);',
},
// 重新设置系列数据
series
series,
};
// 使用新的配置更新图表
@@ -390,24 +417,26 @@ const themeObserver = new MutationObserver(() => {
const updateChart = () => {
if (!chart || !kpiColumns.value.length) return;
//获取图表配置
//获取图表配置
const commonConfig = getSeriesConfig();
//构建数据系列
const series = selectedKPIs.value.map(kpiId => {
const kpi = kpiColumns.value.find(col=>col.kpiId ===kpiId);
if (!kpi) return null;
//为每个KPI分配临时的固定颜色
const color = kpiColors.get(kpiId)||generateColorRGBA();
kpiColors.set(kpiId, color);
//构建数据系列
const series = selectedKPIs.value
.map(kpiId => {
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (!kpi) return null;
//为每个KPI分配临时的固定颜色
const color = kpiColors.get(kpiId) || generateColorRGBA();
kpiColors.set(kpiId, color);
return {
name: kpi.title,
type: 'line',
data: chartData.value.map(item=>item[kpiId]||0),
itemStyle: { color },
...commonConfig,
};
}).filter(Boolean);
return {
name: kpi.title,
type: 'line',
data: chartData.value.map(item => item[kpiId] || 0),
itemStyle: { color },
...commonConfig,
};
})
.filter(Boolean);
const option = {
title: {
@@ -415,30 +444,35 @@ const updateChart = () => {
left: 'center',
// 添加文字颜色配置,根据主题切换
textStyle: {
color: document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333'
}
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
},
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',
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'
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);'
extraCssText: 'box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);',
},
legend: {
data: selectedKPIs.value.map(kpiId =>
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId
data: selectedKPIs.value.map(
kpiId =>
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId
),
type: 'scroll',
orient: 'horizontal',
@@ -449,7 +483,9 @@ const updateChart = () => {
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)
selectedRows.value.length === 0
? true
: selectedRows.value.includes(kpiId),
])
),
show: false,
@@ -470,15 +506,16 @@ const updateChart = () => {
type: 'category',
axisLabel: {
formatter: '{value}',
color: document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333'
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
splitLine: {
show: true,
lineStyle: {
color: getSplitLineColor()
}
color: getSplitLineColor(),
},
},
//控制坐标轴两边留白
// 当为折线图时(isLine为true)时不留白,柱状图时留白
@@ -486,7 +523,7 @@ const updateChart = () => {
boundaryGap: false,
// 设置x轴的数据
// 将时间戳转换为格式化的时间字符串
data:chartData.value.map(item=>
data: chartData.value.map(item =>
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
),
//设置坐标轴刻度标签的样式
@@ -519,9 +556,10 @@ const updateChart = () => {
type: 'value',
axisLabel: {
formatter: '{value}',
color: document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333'
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
// 添加自计算的分割段数
splitNumber: 5,
@@ -530,13 +568,13 @@ const updateChart = () => {
splitLine: {
show: true,
lineStyle: {
color: getSplitLineColor()
}
}
color: getSplitLineColor(),
},
},
},
series: series, //配置数据
};
if(chart) {
if (chart) {
requestAnimationFrame(() => {
chart!.setOption(option, true); //使用新的配置更新图表
chart!.resize(); //调整图表大小适应容器
@@ -561,8 +599,6 @@ const updateChart = () => {
}
};
//钩子函数
onMounted(async () => {
try {
@@ -588,7 +624,7 @@ onMounted(async () => {
// 添加主题观察器
themeObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
attributeFilter: ['data-theme'],
});
} catch (error) {
console.error('Failed to initialize:', error);
@@ -603,9 +639,10 @@ const selectedKPIs = ref<string[]>(Object.values(TARGET_KPI_IDS).flat());
// 获取网元指标
const fetchSpecificKPI = async () => {
const language = currentLocale.value.split('_')[0] === 'zh'
? 'cn'
: currentLocale.value.split('_')[0];
const language =
currentLocale.value.split('_')[0] === 'zh'
? 'cn'
: currentLocale.value.split('_')[0];
try {
let allKPIs: KPIColumn[] = [];
@@ -664,12 +701,13 @@ onUnmounted(() => {
// 实时数据更新图表数据方法
const updateChartData = (newData: ChartDataItem) => {
if(!chart){
if (!chart) {
return;
}
chartData.value.push(newData);
if (chartData.value.length > 100) {//100改为50
chartData.value.shift();//大于100条时删除最早的数据
if (chartData.value.length > 100) {
//100改为50
chartData.value.shift(); //大于100条时删除最早的数据
}
//使用try-catch包裹图表更新逻辑
try {
@@ -692,12 +730,11 @@ const updateChartData = (newData: ChartDataItem) => {
};
chart.setOption(option);
});
}catch (error){
} catch (error) {
console.error('Failed to update chart:', error);
}
};
// 添加一个接口定义指标统计数据的类型
interface KPIStats {
kpiId: string;
@@ -718,34 +755,36 @@ const updateKpiStats = () => {
kpiStats.value = [];
return;
}
kpiStats.value = selectedKPIs.value.map(kpiId => {
// 找到对应的KPI标题
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (!kpi) return null;
kpiStats.value = selectedKPIs.value
.map(kpiId => {
// 找到对应的KPI标题
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (!kpi) return null;
// 获取该指标的所有数值
const values = chartData.value.map(item => Number(item[kpiId]) || 0);
// 获取该指标的所有数值
const values = chartData.value.map(item => Number(item[kpiId]) || 0);
// 计算总值
const total = Number(values.reduce((sum, val) => sum + val, 0).toFixed(2));
// 计算总值
const total = Number(
values.reduce((sum, val) => sum + val, 0).toFixed(2)
);
// 计算平均值
const avg = values.length > 0
? Number((total / values.length).toFixed(2))
: 0;
// 计算平均值
const avg =
values.length > 0 ? Number((total / values.length).toFixed(2)) : 0;
return {
kpiId: kpiId,
title: kpi.title,
max: Math.max(...values),
min: Math.min(...values),
avg: avg,
total: total
};
}).filter((item): item is KPIStats => item !== null);
return {
kpiId: kpiId,
title: kpi.title,
max: Math.max(...values),
min: Math.min(...values),
avg: avg,
total: total,
};
})
.filter((item): item is KPIStats => item !== null);
};
// 添加表列定义
const statsColumns: TableColumnType<KPIStats>[] = [
{
@@ -761,9 +800,9 @@ const statsColumns: TableColumnType<KPIStats>[] = [
color: color || '#000', // 使用与折线图相同的颜色
fontSize: '30px', // 增大图标尺寸到30px
fontWeight: 'bold', // 加粗
}
},
});
}
},
},
{
title: t('views.perfManage.kpiOverView.kpiName'),
@@ -829,14 +868,16 @@ const updateChartLegendSelect = () => {
const legendSelected = Object.fromEntries(
selectedKPIs.value.map(kpiId => [
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
selectedRows.value.length === 0 ? true : selectedRows.value.includes(kpiId)
selectedRows.value.length === 0
? true
: selectedRows.value.includes(kpiId),
])
);
chart.setOption({
legend: {
selected: legendSelected
}
selected: legendSelected,
},
});
};
@@ -844,7 +885,7 @@ const updateChartLegendSelect = () => {
const tableRowConfig = computed(() => {
return (record: KPIStats) => ({
onClick: () => handleRowClick(record),
class: selectedRows.value.includes(record.kpiId) ? 'selected-row' : ''
class: selectedRows.value.includes(record.kpiId) ? 'selected-row' : '',
});
});
</script>
@@ -882,7 +923,54 @@ const tableRowConfig = computed(() => {
size="small"
:loading="tableLoading"
:custom-row="tableRowConfig"
/>
>
<template #headerCell="{ column }">
<template v-if="column.key === 'total'">
<span>
{{ t('views.perfManage.kpiOverView.totalValue') }}
<a-tooltip placement="bottom">
<template #title>
<span>Sum within Time Range</span>
</template>
<InfoCircleOutlined />
</a-tooltip>
</span>
</template>
<template v-if="column.key === 'avg'">
<span>
{{ t('views.perfManage.kpiOverView.avgValue') }}
<a-tooltip placement="bottom">
<template #title>
<span>Average value over the time range</span>
</template>
<InfoCircleOutlined />
</a-tooltip>
</span>
</template>
<template v-if="column.key === 'max'">
<span>
{{ t('views.perfManage.kpiOverView.maxValue') }}
<a-tooltip placement="bottom">
<template #title>
<span>Maximum value in time range</span>
</template>
<InfoCircleOutlined />
</a-tooltip>
</span>
</template>
<template v-if="column.key === 'min'">
<span>
{{ t('views.perfManage.kpiOverView.minValue') }}
<a-tooltip placement="bottom">
<template #title>
<span>Minimum value in the time range</span>
</template>
<InfoCircleOutlined />
</a-tooltip>
</span>
</template>
</template>
</a-table>
</div>
</div>
</div>
@@ -944,8 +1032,8 @@ const tableRowConfig = computed(() => {
}
/* 表格行和表头样式 */
:deep(.ant-table-thead tr th),
:deep(.ant-table-tbody tr td) {
:deep(.ant-table-thead tr th),
:deep(.ant-table-tbody tr td) {
padding: 8px;
height: 40px;
}