Files
fe.ems.vue3/src/views/perfManage/kpiOverView/index.vue
2025-01-17 15:30:22 +08:00

1101 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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 { LineOutlined } from '@ant-design/icons-vue';
import { TableColumnType } from 'ant-design-vue';
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', 'SMF', 'UPF', 'MME', 'IMS', 'SMSC'] as const;
type NeType = (typeof ALL_NE_TYPES)[number];
echarts.use([
LineChart,
GridComponent,
TooltipComponent,
TitleComponent,
CanvasRenderer,
LegendComponent,
]);
// WebSocket连接
const ws = ref<WS | null>(null);
//时间选择
// let ranges = ref<Record<string, [Dayjs, Dayjs]>>({
// [t('views.perfManage.customTarget.sixHoursAgo')]: [
// dayjs().subtract(6, 'hours'),
// dayjs(),
// ],
// [t('views.perfManage.customTarget.threeHoursAgo')]: [
// dayjs().subtract(3, 'hours'),
// dayjs(),
// ],
// [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()],
},
]);
//日期范围响应式变量
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 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);
};
// 定义要筛选的指标 ID按网元类型组织
const TARGET_KPI_IDS: Record<NeType, string[]> = {
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'],
};
// 实时数据开关函数
const fnRealTimeSwitch = (bool: boolean) => {
if (bool) {
if (!chart) {
isRealtime.value = false;
return;
}
if (!ws.value) {
ws.value = new WS();
}
chartData.value = [];
tableLoading.value = true;
const options: OptionsType = {
url: '/ws',
params: {
subGroupID: ALL_NE_TYPES.map(type => `10_${type}_001`).join(','),
},
onmessage: wsMessage,
onerror: wsError,
};
ws.value.connect(options);
} else if (ws.value) {
ws.value.close(); //断开链接
ws.value = null; //清空链接
tableLoading.value = false;
}
};
// 接收数据后错误回调
const wsError = () => {
message.error(t('common.websocketError'));
};
const handleWebSocketMessage = (kpiEvent: any) => {
if (!kpiEvent) return;
// 构造新的数据点
const newData: ChartDataItem = {
date: kpiEvent.timeGroup?.toString() || Date.now().toString(),
};
// 添加已选中的指标的数据
selectedKPIs.value.forEach(kpiId => {
newData[kpiId] = Number(kpiEvent[kpiId]) || 0;
});
// 更新数据
updateChartData(newData);
if (tableLoading.value) {
tableLoading.value = false;
}
};
//成功回调
const wsMessage = (res: Record<string, any>) => {
if (!chart) {
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>(); //数据按时间分组
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;
}
tableLoading.value = true;
rangeLoading.value = true;
try {
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 params = {
neType,
neId: '001',
startTime: String(startTime),
endTime: String(endTime),
sortField: 'timeGroup',
sortOrder: 'asc',
interval: 60 * 15,
kpiIds: TARGET_KPI_IDS[neType].join(','),
};
try {
const res = await listKPIData(params);
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
return res.data;
}
return [];
} catch (error) {
console.error(`Failed to fetch data for ${neType}:`, error);
return [];
}
});
// 并行执行所有请求
const results = await Promise.all(requests);
const allData = results.flat();
chartData.value = processChartData(allData);
updateChart();
updateKpiStats();
} catch (error) {
console.error('Failed to fetch chart data:', error);
message.error(t('common.getInfoFail'));
} finally {
tableLoading.value = false;
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,
},
},
},
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(() => {
updateKpiStats();
});
});
}
});
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);
return {
name: kpi.title,
type: 'line',
data: chartData.value.map(item => item[kpiId] || 0),
itemStyle: { color },
...commonConfig,
};
})
.filter(Boolean);
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, // 限制 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);',
},
legend: {
data: selectedKPIs.value.map(
kpiId =>
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId
),
type: 'scroll',
orient: 'horizontal',
top: 25,
textStyle: {
fontSize: 12,
},
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),
])
),
show: false,
left: 'center',
width: '80%',
height: 50,
padding: [5, 10],
},
grid: {
//网格配置
left: '6%',
right: '6%',
bottom: '3%',
containLabel: true,
},
xAxis: {
// 指定x轴类型为类目轴适用于离散的类目数据
type: 'category',
axisLabel: {
formatter: '{value}',
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
splitLine: {
show: true,
lineStyle: {
color: getSplitLineColor(),
},
},
//控制坐标轴两边留白
// 当为折线图时(isLine为true)时不留白,柱状图时留白
// 这样可以让折线图从原点开始,柱状图有合适的间距
boundaryGap: false,
// 设置x轴的数据
// 将时间戳转换为格式化的时间字符串
data: chartData.value.map(item =>
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
),
//设置坐标轴刻度标签的样式
// axisLabel: {
// interval: function(index: number, value: string) {
// const currentTime = dayjs(value);
// const minutes = currentTime.minute();
// const seconds = currentTime.second();
//
// // 始终显示小时的起始和结束时间点
// if ((minutes === 0 && seconds === 0) ||
// (minutes === 59 && seconds === 59)) {
// return true;
// }
//
// // 对于中间的时间点,使用 auto 的逻辑
// if (index % Math.ceil(chartData.value.length / 6) === 0) {
// return true;
// }
//
// return false;
// },
// rotate: 0,
// align: 'center',
// hideOverlap: true
// }
},
yAxis: {
// y轴配置
type: 'value',
axisLabel: {
formatter: '{value}',
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
// 添加自计算的分割段数
splitNumber: 5,
// 添加自动计算的最小/最大值范围
scale: true,
splitLine: {
show: true,
lineStyle: {
color: getSplitLineColor(),
},
},
},
series: series, //配置数据
};
if (chart) {
requestAnimationFrame(() => {
chart!.setOption(option, true); //使用新的配置更新图表
chart!.resize(); //调整图表大小适应容器
});
}
// 如果已经有 observer先断开连接
if (observer) {
observer.disconnect();
}
// 创建新的 ResizeObserver
observer = new ResizeObserver(() => {
if (chart) {
chart.resize();
}
});
// 观察图表容器
const container = document.getElementById('chartContainer');
if (container) {
observer.observe(container);
}
};
//钩子函数
onMounted(async () => {
try {
// 获取所有网元的指标
await fetchSpecificKPI();
await nextTick();
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');
}
} 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'],
});
} catch (error) {
console.error('Failed to initialize:', error);
message.error(t('common.initFail'));
}
});
// 存储指标列信
const kpiColumns = ref<KPIColumn[]>([]);
// 添加选中指标的的状态
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];
try {
let allKPIs: KPIColumn[] = [];
// 获取所有网元的指标
for (const neType of ALL_NE_TYPES) {
const res = await getKPITitle(neType.toUpperCase());
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
const formattedKPIs = res.data.map(item => ({
title: item[`${language}Title`],
dataIndex: item.kpiId,
key: item.kpiId,
kpiId: item.kpiId,
neType: neType,
}));
allKPIs = [...allKPIs, ...formattedKPIs];
}
}
kpiColumns.value = allKPIs;
// 直接使用重要指标
selectedKPIs.value = Object.values(TARGET_KPI_IDS).flat();
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: selectedKPIs.value.map(kpiId => {
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
return {
type: 'line',
data: chartData.value.map(item => item[kpiId] || 0),
name: kpi?.title || kpiId,
};
}),
};
chart.setOption(option);
});
} catch (error) {
console.error('Failed to update chart:', error);
}
};
// 添加一个接口定义指标统计数据的类型
interface KPIStats {
kpiId: string;
title: string;
max: number;
min: number;
avg: number;
total: number;
}
// 添加计算属性,用于计算每个指标的最大值和最小值
// 将 kpiStats 从计算属性改为响应式引用
const kpiStats = ref<KPIStats[]>([]);
// 添加一个计算函数来更新统计数据
const updateKpiStats = () => {
if (!chartData.value.length || !kpiColumns.value.length) {
kpiStats.value = [];
return;
}
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 total = Number(
values.reduce((sum, val) => sum + val, 0).toFixed(2)
);
// 计算平均值
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);
};
// 添加表列定义
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: '50%',
},
{
title: t('views.perfManage.kpiOverView.totalValue'),
dataIndex: 'total',
key: 'total',
width: '12%',
sorter: (a: KPIStats, b: KPIStats) => a.total - b.total,
sortDirections: ['ascend', 'descend'],
},
{
title: t('views.perfManage.kpiOverView.avgValue'),
dataIndex: 'avg',
key: 'avg',
width: '12%',
sorter: (a: KPIStats, b: KPIStats) => a.avg - b.avg,
sortDirections: ['ascend', 'descend'],
},
{
title: t('views.perfManage.kpiOverView.maxValue'),
dataIndex: 'max',
key: 'max',
width: '12%',
sorter: (a: KPIStats, b: KPIStats) => a.max - b.max, // 添加排序函数
sortDirections: ['ascend', 'descend'],
},
{
title: t('views.perfManage.kpiOverView.minValue'),
dataIndex: 'min',
key: 'min',
width: '12%',
sorter: (a: KPIStats, b: KPIStats) => a.min - b.min,
sortDirections: ['ascend', 'descend'],
},
];
// 将 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(
selectedKPIs.value.map(kpiId => [
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
selectedRows.value.length === 0
? true
: selectedRows.value.includes(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>
</div>
<div class="chart-wrapper">
<div id="chartContainer" class="chart-container"></div>
<div class="table-container">
<a-table
:columns="statsColumns"
:data-source="kpiStats"
:pagination="false"
:scroll="{ y: 250 }"
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>
</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: 400px;
width: 100%;
}
.table-container {
height: 282px;
width: 100%;
margin-top: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 表格布局相关样式 */
: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;
}
[data-theme='dark'] :deep(.selected-row:hover) {
background-color: rgba(24, 144, 255, 0.3) !important;
}
/* 鼠标悬停样式 */
:deep(.ant-table-tbody tr:hover) {
cursor: pointer;
}
</style>