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

@@ -53,8 +53,6 @@ const route = useRoute();
const { t, currentLocale } = useI18n(); const { t, currentLocale } = useI18n();
const ws = new WS(); const ws = new WS();
echarts.use([ echarts.use([
TooltipComponent, TooltipComponent,
GridComponent, GridComponent,
@@ -449,20 +447,19 @@ function fnGetList() {
return item[columns.key] ? Number(item[columns.key]) : 0; return item[columns.key] ? Number(item[columns.key]) : 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 const avg = values.length > 0 ? Number((total / values.length).toFixed(2)) : 0;
? Number((total / values.length).toFixed(2))
: 0;
kpiStats.value.push({ kpiStats.value.push({
kpiId: columns.key, kpiId: columns.key,
title: columns.title, title: columns.title,
max: values.length > 0 ? Math.max(...values) : 0, max: values.length > 0 ? Math.max(...values) : 0,
min: values.length > 0 ? Math.min(...values) : 0, min: values.length > 0 ? Math.min(...values) : 0,
avg avg,
}); });
} }
} }
@@ -487,16 +484,19 @@ function fnRanderChart() {
return [pt[0], '10%']; return [pt[0], '10%'];
}, },
confine: true, // 限制 tooltip 显示范围 confine: true, // 限制 tooltip 显示范围
backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark' backgroundColor:
document.documentElement.getAttribute('data-theme') === 'dark'
? 'rgba(48, 48, 48, 0.8)' ? 'rgba(48, 48, 48, 0.8)'
: 'rgba(255, 255, 255, 0.9)', : 'rgba(255, 255, 255, 0.9)',
borderColor: document.documentElement.getAttribute('data-theme') === 'dark' borderColor:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#555' ? '#555'
: '#ddd', : '#ddd',
textStyle: { textStyle: {
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
}, },
}, },
xAxis: { xAxis: {
@@ -633,29 +633,31 @@ function fnRanderChartData() {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
axisLabel: { axisLabel: {
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
}, },
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: getSplitLineColor() color: getSplitLineColor(),
} },
}, },
data: chartDataXAxisData, data: chartDataXAxisData,
}, },
yAxis: { yAxis: {
axisLabel: { axisLabel: {
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
}, },
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: getSplitLineColor() color: getSplitLineColor(),
} },
}, },
}, },
series: chartDataYSeriesData, series: chartDataYSeriesData,
@@ -784,59 +786,49 @@ function handleRowClick(record: any) {
// 监听主题变化 // 监听主题变化
watch( watch(
() => layoutStore.proConfig.theme, // 监听的值 () => layoutStore.proConfig.theme, // 监听的值
(newValue) => { newValue => {
if (kpiChart.value) { if (kpiChart.value) {
const splitLineColor = getSplitLineColor(); const splitLineColor = getSplitLineColor();
// 绘制图数据 // 绘制图数据
kpiChart.value.setOption( kpiChart.value.setOption({
{
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
position: function (pt: any) { position: function (pt: any) {
return [pt[0], '10%']; return [pt[0], '10%'];
}, },
confine: true, // 限制 tooltip 显示范围 confine: true, // 限制 tooltip 显示范围
backgroundColor: newValue === 'dark' backgroundColor:
newValue === 'dark'
? 'rgba(48, 48, 48, 0.8)' ? 'rgba(48, 48, 48, 0.8)'
: 'rgba(255, 255, 255, 0.9)', : 'rgba(255, 255, 255, 0.9)',
borderColor: newValue === 'dark' borderColor: newValue === 'dark' ? '#555' : '#ddd',
? '#555'
: '#ddd',
textStyle: { textStyle: {
color: newValue === 'dark' color: newValue === 'dark' ? '#CACADA' : '#333',
? '#CACADA'
: '#333'
}, },
}, },
xAxis: { xAxis: {
axisLabel: { axisLabel: {
color: newValue === 'dark' color: newValue === 'dark' ? '#CACADA' : '#333',
? '#CACADA'
: '#333'
}, },
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: splitLineColor color: splitLineColor,
} },
} },
}, },
yAxis: { yAxis: {
axisLabel: { axisLabel: {
color: newValue === 'dark' color: newValue === 'dark' ? '#CACADA' : '#333',
? '#CACADA'
: '#333'
}, },
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: splitLineColor color: splitLineColor,
} },
} },
} },
} });
);
} }
} }
); );
@@ -924,28 +916,50 @@ onBeforeUnmount(() => {
<template> <template>
<PageContainer> <PageContainer>
<a-card v-show="tableState.seached" :bordered="false" :body-style="{ marginBottom: '24px', paddingBottom: 0 }"> <a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 --> <!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParamsFrom" layout="horizontal"> <a-form :model="queryParams" name="queryParamsFrom" layout="horizontal">
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24"> <a-col :lg="6" :md="12" :xs="24">
<a-form-item name="neType" :label="t('views.ne.common.neType')"> <a-form-item name="neType" :label="t('views.ne.common.neType')">
<a-cascader v-model:value="state.neType" :options="neCascaderOptions" :allow-clear="false" <a-cascader
:placeholder="t('common.selectPlease')" /> v-model:value="state.neType"
:options="neCascaderOptions"
:allow-clear="false"
:placeholder="t('common.selectPlease')"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :lg="10" :md="12" :xs="24"> <a-col :lg="10" :md="12" :xs="24">
<a-form-item :label="t('views.perfManage.goldTarget.timeFrame')" name="timeFrame"> <a-form-item
<a-range-picker v-model:value="queryRangePicker" bordered :allow-clear="false" :label="t('views.perfManage.goldTarget.timeFrame')"
:show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" value-format="x" :presets="ranges" name="timeFrame"
style="width: 100%"></a-range-picker> >
<a-range-picker
v-model:value="queryRangePicker"
bordered
:allow-clear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
value-format="x"
:presets="ranges"
style="width: 100%"
></a-range-picker>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :lg="2" :md="12" :xs="24"> <a-col :lg="2" :md="12" :xs="24">
<a-form-item> <a-form-item>
<a-space :size="8"> <a-space :size="8">
<a-button type="primary" :loading="tableState.loading" @click.prevent="fnGetListTitle()"> <a-button
type="primary"
:loading="tableState.loading"
@click.prevent="fnGetListTitle()"
>
<template #icon> <template #icon>
<SearchOutlined /> <SearchOutlined />
</template> </template>
@@ -962,7 +976,11 @@ onBeforeUnmount(() => {
<!-- 插槽-卡片左侧侧 --> <!-- 插槽-卡片左侧侧 -->
<template #title> <template #title>
<a-space :size="8" align="center"> <a-space :size="8" align="center">
<a-button type="primary" :loading="tableState.loading" @click.prevent="fnChangShowType()"> <a-button
type="primary"
:loading="tableState.loading"
@click.prevent="fnChangShowType()"
>
<template #icon> <template #icon>
<AreaChartOutlined /> <AreaChartOutlined />
</template> </template>
@@ -972,8 +990,12 @@ onBeforeUnmount(() => {
: t('views.perfManage.goldTarget.kpiTableTitle') : t('views.perfManage.goldTarget.kpiTableTitle')
}} }}
</a-button> </a-button>
<a-button type="dashed" :loading="tableState.loading" @click.prevent="fnRecordExport()" <a-button
v-show="tableState.showTable"> type="dashed"
:loading="tableState.loading"
@click.prevent="fnRecordExport()"
v-show="tableState.showTable"
>
<template #icon> <template #icon>
<ExportOutlined /> <ExportOutlined />
</template> </template>
@@ -993,8 +1015,12 @@ onBeforeUnmount(() => {
</template> </template>
</a-button> </a-button>
</a-tooltip> </a-tooltip>
<TableColumnsDnd v-if="tableColumns.length > 0" :cache-id="`kpiTarget_${state.neType[0]}`" <TableColumnsDnd
:columns="tableColumns" v-model:columns-dnd="tableColumnsDnd"></TableColumnsDnd> v-if="tableColumns.length > 0"
:cache-id="`kpiTarget_${state.neType[0]}`"
:columns="tableColumns"
v-model:columns-dnd="tableColumnsDnd"
></TableColumnsDnd>
<a-tooltip> <a-tooltip>
<template #title>{{ t('common.sizeText') }}</template> <template #title>{{ t('common.sizeText') }}</template>
<a-dropdown trigger="click" placement="bottomRight"> <a-dropdown trigger="click" placement="bottomRight">
@@ -1004,7 +1030,10 @@ onBeforeUnmount(() => {
</template> </template>
</a-button> </a-button>
<template #overlay> <template #overlay>
<a-menu :selected-keys="[tableState.size as string]" @click="fnTableSize"> <a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default"> <a-menu-item key="default">
{{ t('common.size.default') }} {{ t('common.size.default') }}
</a-menu-item> </a-menu-item>
@@ -1033,32 +1062,108 @@ onBeforeUnmount(() => {
size="small" size="small"
/> />
</a-form-item> --> </a-form-item> -->
<a-form-item :label="t('views.perfManage.goldTarget.realTimeData')" name="chartRealTime"> <a-form-item
<a-switch :disabled="tableState.loading" v-model:checked="state.chartRealTime" :label="t('views.perfManage.goldTarget.realTimeData')"
:checked-children="t('common.switch.open')" :un-checked-children="t('common.switch.shut')" name="chartRealTime"
@change="fnRealTimeSwitch" size="small" /> >
<a-switch
:disabled="tableState.loading"
v-model:checked="state.chartRealTime"
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
@change="fnRealTimeSwitch"
size="small"
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
</template> </template>
<!-- 表格列表 --> <!-- 表格列表 -->
<a-table v-show="tableState.showTable" class="table" row-key="id" :columns="tableColumnsDnd" <a-table
:loading="tableState.loading" :data-source="tableState.data" :size="tableState.size" v-show="tableState.showTable"
:pagination="tablePagination" :scroll="{ x: tableColumnsDnd.length * 200, y: 'calc(100vh - 480px)' }" class="table"
@resizeColumn="(w: number, col: any) => (col.width = w)" :show-expand-column="false" @change="fnTableChange"> row-key="id"
:columns="tableColumnsDnd"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: tableColumnsDnd.length * 200, y: 'calc(100vh - 480px)' }"
@resizeColumn="(w: number, col: any) => (col.width = w)"
:show-expand-column="false"
@change="fnTableChange"
>
</a-table> </a-table>
<!-- 图表 --> <!-- 图表 -->
<div style="padding: 24px" v-show="!tableState.showTable"> <div style="padding: 24px" v-show="!tableState.showTable">
<div ref="kpiChartDom" class="chart-container" style="height: 450px; width: 100%"></div> <div
ref="kpiChartDom"
class="chart-container"
style="height: 450px; width: 100%"
></div>
<div class="table-container"> <div class="table-container">
<a-table :columns="statsColumns" :data-source="kpiStats" :pagination="false" :scroll="{ y: 250 }" size="small" <a-table
:custom-row="record => ({ :columns="statsColumns"
:data-source="kpiStats"
:pagination="false"
:scroll="{ y: 250 }"
size="small"
:custom-row="
record => ({
onClick: () => handleRowClick(record), onClick: () => handleRowClick(record),
class: selectedRow.includes(record.kpiId) ? 'selected-row' : '' class: selectedRow.includes(record.kpiId) ? 'selected-row' : '',
}) })
" /> "
>
<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> </div>
</a-card> </a-card>

View File

@@ -1,8 +1,20 @@
<script setup lang="ts"> <script setup lang="ts">
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { onMounted, reactive, ref, markRaw, nextTick, onUnmounted, watch, h } from 'vue'; import {
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants'; onMounted,
reactive,
ref,
markRaw,
nextTick,
onUnmounted,
watch,
h,
} from 'vue';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import { SizeType } from 'ant-design-vue/es/config-provider'; import { SizeType } from 'ant-design-vue/es/config-provider';
import { listKPIData, getKPITitle } from '@/api/perfManage/goldTarget'; import { listKPIData, getKPITitle } from '@/api/perfManage/goldTarget';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
@@ -23,12 +35,19 @@ const { t, currentLocale } = useI18n();
const neInfoStore = useNeInfoStore(); const neInfoStore = useNeInfoStore();
//日期快捷选择 //日期快捷选择
const ranges = ref([ const ranges = ref([
{label:t('views.perfManage.customTarget.sixHoursAgo'),value:[dayjs().subtract(6, 'hours'), {
dayjs(),]}, label: t('views.perfManage.customTarget.sixHoursAgo'),
{label:t('views.perfManage.customTarget.threeHoursAgo'),value:[dayjs().subtract(3, 'hours'), value: [dayjs().subtract(6, 'hours'), dayjs()],
dayjs(),]}, },
{label:t('views.monitor.monitor.today'),value:[dayjs().startOf('day'), dayjs()]}, {
]) label: t('views.perfManage.customTarget.threeHoursAgo'),
value: [dayjs().subtract(3, 'hours'), dayjs()],
},
{
label: t('views.monitor.monitor.today'),
value: [dayjs().startOf('day'), dayjs()],
},
]);
//WebSocket连接 //WebSocket连接
const ws = ref<WS | null>(null); const ws = ref<WS | null>(null);
@@ -40,13 +59,25 @@ const handleRealTimeSwitch = (checked: any) => {
}; };
// 定义所有网元类型 // 定义所有网元类型
const ALL_NE_TYPES = ['ims', 'amf', 'udm', 'upf','smf', 'pcf', 'mme', 'mocngw', 'smsc', 'cbc', 'ausf'] as const; const ALL_NE_TYPES = [
'ims',
'amf',
'udm',
'upf',
'smf',
'pcf',
'mme',
'mocngw',
'smsc',
'cbc',
'ausf',
] as const;
type AllChartType = (typeof ALL_NE_TYPES)[number] & string; type AllChartType = (typeof ALL_NE_TYPES)[number] & string;
// 在 ALL_NE_TYPES 定义之后添加 小写转大写 // 在 ALL_NE_TYPES 定义之后添加 小写转大写
const neTypeOptions = ALL_NE_TYPES.map(type => ({ const neTypeOptions = ALL_NE_TYPES.map(type => ({
label: type.toUpperCase(), label: type.toUpperCase(),
value: type value: type,
})); }));
// 使用 ref 来使 networkElementTypes 变为响应式,并使用 ALL_NE_TYPES 初始化 // 使用 ref 来使 networkElementTypes 变为响应式,并使用 ALL_NE_TYPES 初始化
@@ -59,7 +90,9 @@ const selectedNeTypes = ref<AllChartType[]>([]);
const latestSelectedTypes = ref<AllChartType[]>([]); const latestSelectedTypes = ref<AllChartType[]>([]);
// watch 监控函数 // watch 监控函数
watch(selectedNeTypes, async (newTypes, oldTypes) => { watch(
selectedNeTypes,
async (newTypes, oldTypes) => {
networkElementTypes.value = [...newTypes]; networkElementTypes.value = [...newTypes];
latestSelectedTypes.value = [...newTypes]; latestSelectedTypes.value = [...newTypes];
@@ -96,7 +129,8 @@ watch(selectedNeTypes, async (newTypes, oldTypes) => {
for (const type of newTypes) { for (const type of newTypes) {
try { try {
// 只有当图表不存在或者是新增的类型时才初始化 // 只有当图表不存在或者是新增的类型时才初始化
const needsInit = !chartStates[type].chart.value || addedTypes.includes(type); const needsInit =
!chartStates[type].chart.value || addedTypes.includes(type);
if (needsInit) { if (needsInit) {
await fetchKPITitle(type); await fetchKPITitle(type);
@@ -125,7 +159,9 @@ watch(selectedNeTypes, async (newTypes, oldTypes) => {
const options: OptionsType = { const options: OptionsType = {
url: '/ws', url: '/ws',
params: { params: {
subGroupID: newTypes.map(type => `10_${type.toUpperCase()}_001`).join(','), subGroupID: newTypes
.map(type => `10_${type.toUpperCase()}_001`)
.join(','),
}, },
onmessage: wsMessage, onmessage: wsMessage,
onerror: wsError, onerror: wsError,
@@ -135,19 +171,24 @@ watch(selectedNeTypes, async (newTypes, oldTypes) => {
// 保存选中的网元类型到本地存储 // 保存选中的网元类型到本地存储
localStorage.setItem('selectedNeTypes', JSON.stringify(newTypes)); localStorage.setItem('selectedNeTypes', JSON.stringify(newTypes));
}, { deep: true }); },
{ deep: true }
);
// 防抖函数 // 防抖函数
useDebounceFn(() => { useDebounceFn(() => {
// 比较当前选择和最新选择 // 比较当前选择和最新选择
if (JSON.stringify(latestSelectedTypes.value) !== JSON.stringify(selectedNeTypes.value)) { if (
JSON.stringify(latestSelectedTypes.value) !==
JSON.stringify(selectedNeTypes.value)
) {
// 如果不一致,以最新选择为准 // 如果不一致,以最新选择为准
selectedNeTypes.value = latestSelectedTypes.value; selectedNeTypes.value = latestSelectedTypes.value;
} }
const newTypes = selectedNeTypes.value; const newTypes = selectedNeTypes.value;
// 确保 chartStates 包含新的网元类型 // 确保 chartStates 包含新的网元类型
newTypes.forEach((type) => { newTypes.forEach(type => {
if (!chartStates[type]) { if (!chartStates[type]) {
chartStates[type] = createChartState(type); chartStates[type] = createChartState(type);
} }
@@ -168,7 +209,7 @@ const initCharts = async () => {
const savedChartStates = new Map(Object.entries(chartStates)); const savedChartStates = new Map(Object.entries(chartStates));
// 清除不再需要的图表 // 清除不再需要的图表
Object.keys(chartStates).forEach((key) => { Object.keys(chartStates).forEach(key => {
if (!networkElementTypes.value.includes(key as AllChartType)) { if (!networkElementTypes.value.includes(key as AllChartType)) {
const state = chartStates[key as AllChartType]; const state = chartStates[key as AllChartType];
if (state.chart.value) { if (state.chart.value) {
@@ -226,7 +267,9 @@ const createChartState = (neType: AllChartType) => {
const chartDom = ref<HTMLElement | null>(null); const chartDom = ref<HTMLElement | null>(null);
const chart = ref<echarts.ECharts | null>(null); const chart = ref<echarts.ECharts | null>(null);
const observer = ref<ResizeObserver | null>(null); const observer = ref<ResizeObserver | null>(null);
const currentTheme = ref<string | null>(document.documentElement.getAttribute('data-theme')); const currentTheme = ref<string | null>(
document.documentElement.getAttribute('data-theme')
);
const themeObserver = ref<MutationObserver | null>(null); const themeObserver = ref<MutationObserver | null>(null);
return { return {
@@ -252,7 +295,10 @@ const createChartState = (neType: AllChartType) => {
}; };
// 图表类型状态 // 图表类型状态
const chartStates: Record<AllChartType, ReturnType<typeof createChartState>> = Object.fromEntries( const chartStates: Record<
AllChartType,
ReturnType<typeof createChartState>
> = Object.fromEntries(
networkElementTypes.value.map(type => [type, createChartState(type)]) networkElementTypes.value.map(type => [type, createChartState(type)])
) as Record<AllChartType, ReturnType<typeof createChartState>>; ) as Record<AllChartType, ReturnType<typeof createChartState>>;
@@ -263,14 +309,19 @@ interface RangePicker extends Record<AllChartType, [string, string]> {
// 日期选择器状态 // 日期选择器状态
const rangePicker = reactive<RangePicker>({ const rangePicker = reactive<RangePicker>({
...Object.fromEntries(networkElementTypes.value.map(type => [ ...(Object.fromEntries(
networkElementTypes.value.map(type => [
type, type,
[ [
dayjs().subtract(1, 'hour').startOf('hour').valueOf().toString(), // 上一小时开始 dayjs().subtract(1, 'hour').startOf('hour').valueOf().toString(), // 上一小时开始
dayjs().startOf('hour').add(1, 'hour').valueOf().toString(), // 当前小时结束 dayjs().startOf('hour').add(1, 'hour').valueOf().toString(), // 当前小时结束
] ],
])) as Record<AllChartType, [string, string]>, ])
placeholder: [t('views.monitor.monitor.startTime'), t('views.monitor.monitor.endTime')] as [string, string], ) as Record<AllChartType, [string, string]>),
placeholder: [
t('views.monitor.monitor.startTime'),
t('views.monitor.monitor.endTime'),
] as [string, string],
}); });
// 可复用的图表初始化函数 // 可复用的图表初始化函数
@@ -284,7 +335,8 @@ const initChart = (type: AllChartType) => {
} }
// 更新当前主题 // 更新当前主题
state.currentTheme.value = document.documentElement.getAttribute('data-theme'); state.currentTheme.value =
document.documentElement.getAttribute('data-theme');
// 等待 DOM 更新 // 等待 DOM 更新
await nextTick(); await nextTick();
@@ -292,10 +344,14 @@ const initChart = (type: AllChartType) => {
const container = state.chartDom.value; const container = state.chartDom.value;
if (!container) { if (!container) {
if (retries > 0) { if (retries > 0) {
console.warn(`Chart container for ${type} not found, retrying... (${retries} attempts left)`); console.warn(
`Chart container for ${type} not found, retrying... (${retries} attempts left)`
);
setTimeout(() => tryInit(retries - 1), 100); setTimeout(() => tryInit(retries - 1), 100);
} else { } else {
console.error(`Chart container for ${type} not found after multiple attempts`); console.error(
`Chart container for ${type} not found after multiple attempts`
);
} }
return; return;
} }
@@ -314,12 +370,19 @@ const initChart = (type: AllChartType) => {
if (!state.chart.value) return; if (!state.chart.value) return;
const splitLineColor = getSplitLineColor(); const splitLineColor = getSplitLineColor();
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; const isDark =
document.documentElement.getAttribute('data-theme') === 'dark';
const option = { const option = {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
position: function (point: number[], _params: any, _dom: HTMLElement, _rect: any, size: { viewSize: number[], contentSize: number[] }) { position: function (
point: number[],
_params: any,
_dom: HTMLElement,
_rect: any,
size: { viewSize: number[]; contentSize: number[] }
) {
const [x] = point; const [x] = point;
const [viewWidth] = size.viewSize; const [viewWidth] = size.viewSize;
const [tooltipWidth] = size.contentSize; const [tooltipWidth] = size.contentSize;
@@ -338,17 +401,19 @@ const initChart = (type: AllChartType) => {
}, },
axisPointer: { axisPointer: {
type: 'line', type: 'line',
z: 0 z: 0,
}, },
className: `chart-tooltip-${type}`, className: `chart-tooltip-${type}`,
z: 1000, z: 1000,
extraCssText: 'z-index: 1000; pointer-events: none;', extraCssText: 'z-index: 1000; pointer-events: none;',
confine: true, confine: true,
backgroundColor: isDark ? 'rgba(48, 48, 48, 0.8)' : 'rgba(255, 255, 255, 0.9)', backgroundColor: isDark
? 'rgba(48, 48, 48, 0.8)'
: 'rgba(255, 255, 255, 0.9)',
borderColor: isDark ? '#555' : '#ddd', borderColor: isDark ? '#555' : '#ddd',
textStyle: { textStyle: {
color: isDark ? '#CACADA' : '#333' color: isDark ? '#CACADA' : '#333',
} },
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',
@@ -356,30 +421,30 @@ const initChart = (type: AllChartType) => {
data: state.chartDataXAxisData, data: state.chartDataXAxisData,
axisLabel: { axisLabel: {
formatter: '{value}', formatter: '{value}',
color: isDark ? '#CACADA' : '#333' color: isDark ? '#CACADA' : '#333',
}, },
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: splitLineColor color: splitLineColor,
} },
} },
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
boundaryGap: [0, '100%'], boundaryGap: [0, '100%'],
axisLabel: { axisLabel: {
formatter: '{value}', formatter: '{value}',
color: isDark ? '#CACADA' : '#333' color: isDark ? '#CACADA' : '#333',
}, },
splitNumber: 5, splitNumber: 5,
scale: true, scale: true,
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: splitLineColor color: splitLineColor,
} },
} },
}, },
legend: { legend: {
type: 'scroll', type: 'scroll',
@@ -392,7 +457,9 @@ const initChart = (type: AllChartType) => {
show: false, show: false,
icon: 'circle', icon: 'circle',
selected: state.chartLegendSelected, selected: state.chartLegendSelected,
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.9)', backgroundColor: isDark
? 'rgba(0, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.9)',
borderColor: isDark ? '#555' : '#ddd', borderColor: isDark ? '#555' : '#ddd',
}, },
grid: { grid: {
@@ -401,7 +468,9 @@ const initChart = (type: AllChartType) => {
bottom: '15%', bottom: '15%',
}, },
series: state.chartDataYSeriesData.map(series => { series: state.chartDataYSeriesData.map(series => {
const color = state.seriesColors.get(series.customKey as string) || generateColorRGBA(); const color =
state.seriesColors.get(series.customKey as string) ||
generateColorRGBA();
return { return {
...series, ...series,
itemStyle: { color }, itemStyle: { color },
@@ -428,13 +497,14 @@ const initChart = (type: AllChartType) => {
} }
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
state.currentTheme.value = document.documentElement.getAttribute('data-theme'); state.currentTheme.value =
document.documentElement.getAttribute('data-theme');
updateChartTheme(type); updateChartTheme(type);
}); });
observer.observe(document.documentElement, { observer.observe(document.documentElement, {
attributes: true, attributes: true,
attributeFilter: ['data-theme'] attributeFilter: ['data-theme'],
}); });
// 正确地设置 ref 的值 // 正确地设置 ref 的值
@@ -505,7 +575,7 @@ function fnRealTimeSwitch(bool: boolean) {
if (state.chart.value) { if (state.chart.value) {
state.chart.value.setOption({ state.chart.value.setOption({
xAxis: { data: [] }, xAxis: { data: [] },
series: state.chartDataYSeriesData series: state.chartDataYSeriesData,
}); });
} }
}); });
@@ -513,7 +583,9 @@ function fnRealTimeSwitch(bool: boolean) {
const options: OptionsType = { const options: OptionsType = {
url: '/ws', url: '/ws',
params: { params: {
subGroupID: selectedNeTypes.value.map(type => `10_${type.toUpperCase()}_001`).join(','), subGroupID: selectedNeTypes.value
.map(type => `10_${type.toUpperCase()}_001`)
.join(','),
}, },
onmessage: wsMessage, onmessage: wsMessage,
onerror: wsError, onerror: wsError,
@@ -534,7 +606,6 @@ function fnRealTimeSwitch(bool: boolean) {
// 接收数据后错误回调 // 接收数据后错误回调
function wsError() { function wsError() {
message.error(t('common.websocketError')); message.error(t('common.websocketError'));
} }
@@ -559,7 +630,9 @@ function wsMessage(res: Record<string, any>) {
return; return;
} }
const newTime = parseDateToStr(kpiEvent.timeGroup ? +kpiEvent.timeGroup : Date.now()); const newTime = parseDateToStr(
kpiEvent.timeGroup ? +kpiEvent.timeGroup : Date.now()
);
// 只有在实时数据模式下才更新图表 // 只有在实时数据模式下才更新图表
if (realTimeEnabled.value) { if (realTimeEnabled.value) {
@@ -596,7 +669,7 @@ function wsMessage(res: Record<string, any>) {
if (state.chart.value) { if (state.chart.value) {
state.chart.value.setOption({ state.chart.value.setOption({
xAxis: { data: state.chartDataXAxisData }, xAxis: { data: state.chartDataXAxisData },
series: state.chartDataYSeriesData as SeriesOption[] series: state.chartDataYSeriesData as SeriesOption[],
}); });
} }
@@ -629,7 +702,8 @@ const renderChart = async (type: AllChartType) => {
// 处理数据 // 处理数据
for (const column of state.tableColumns.value) { for (const column of state.tableColumns.value) {
if (['neName', 'startIndex', 'timeGroup'].includes(column.key as string)) continue; if (['neName', 'startIndex', 'timeGroup'].includes(column.key as string))
continue;
// 检查是否已有该指标的颜色,如果没有才生成新颜色 // 检查是否已有该指标的颜色,如果没有才生成新颜色
let color = state.seriesColors.get(column.key as string); let color = state.seriesColors.get(column.key as string);
@@ -672,11 +746,14 @@ const renderChart = async (type: AllChartType) => {
// 更新图表时考虑当前选中状态 // 更新图表时考虑当前选中状态
const legendSelected: Record<string, boolean> = {}; const legendSelected: Record<string, boolean> = {};
state.chartDataYSeriesData.forEach(series => { state.chartDataYSeriesData.forEach(series => {
const title = state.tableColumns.value.find(col => col.key === series.customKey)?.title || series.customKey; const title =
state.tableColumns.value.find(col => col.key === series.customKey)
?.title || series.customKey;
if (typeof title === 'string') { if (typeof title === 'string') {
// 如果没有选中的指标或选中列表为空,显示所有指标 // 如果没有选中的指标或选中列表为空,显示所有指标
// 否则只显示选中的指标 // 否则只显示选中的指标
legendSelected[title] = !currentSelectedKpiIds.length || legendSelected[title] =
!currentSelectedKpiIds.length ||
currentSelectedKpiIds.includes(series.customKey as string); currentSelectedKpiIds.includes(series.customKey as string);
} }
}); });
@@ -687,14 +764,17 @@ const renderChart = async (type: AllChartType) => {
legend: { legend: {
selected: legendSelected, selected: legendSelected,
textStyle: { textStyle: {
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#646A73', : '#646A73',
}, },
backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark' backgroundColor:
document.documentElement.getAttribute('data-theme') === 'dark'
? 'rgba(0, 0, 0, 0.2)' ? 'rgba(0, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.9)', : 'rgba(255, 255, 255, 0.9)',
borderColor: document.documentElement.getAttribute('data-theme') === 'dark' borderColor:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#555' ? '#555'
: '#ddd', : '#ddd',
}, },
@@ -704,16 +784,17 @@ const renderChart = async (type: AllChartType) => {
boundaryGap: false, boundaryGap: false,
axisLabel: { axisLabel: {
formatter: '{value}', formatter: '{value}',
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
}, },
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: getSplitLineColor() color: getSplitLineColor(),
} },
} },
}, },
series: state.chartDataYSeriesData as SeriesOption[], series: state.chartDataYSeriesData as SeriesOption[],
}, },
@@ -721,10 +802,12 @@ const renderChart = async (type: AllChartType) => {
); );
}; };
// 获网元指标据 // 获网元指标据
const fetchKPITitle = async (type: AllChartType) => { const fetchKPITitle = async (type: AllChartType) => {
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 { try {
const res = await getKPITitle(type.toUpperCase()); const res = await getKPITitle(type.toUpperCase());
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
@@ -742,7 +825,7 @@ const fetchKPITitle = async (type: AllChartType) => {
fixed: 'left', fixed: 'left',
customRender: ({ text }: { text: string }) => { customRender: ({ text }: { text: string }) => {
return dayjs(Number(text)).format('YYYY-MM-DD HH:mm:ss'); return dayjs(Number(text)).format('YYYY-MM-DD HH:mm:ss');
} },
}, },
...res.data.map(item => { ...res.data.map(item => {
const kpiId = item.kpiId; const kpiId = item.kpiId;
@@ -761,7 +844,7 @@ const fetchKPITitle = async (type: AllChartType) => {
minWidth: 150, minWidth: 150,
maxWidth: 300, maxWidth: 300,
}; };
}) }),
]; ];
// 更新颜色映射 // 更新颜色映射
@@ -805,11 +888,14 @@ const themeObserver = new MutationObserver(() => {
// 重新计算图例选中状态 // 重新计算图例选中状态
const legendSelected: Record<string, boolean> = {}; const legendSelected: Record<string, boolean> = {};
state.chartDataYSeriesData.forEach(series => { state.chartDataYSeriesData.forEach(series => {
const title = state.tableColumns.value.find(col => col.key === series.customKey)?.title || series.customKey; const title =
state.tableColumns.value.find(col => col.key === series.customKey)
?.title || series.customKey;
if (typeof title === 'string') { if (typeof title === 'string') {
// 如果没有选中的指标或选中列表为空,显示所有指标 // 如果没有选中的指标或选中列表为空,显示所有指标
// 否则只显示选中的指标 // 否则只显示选中的指标
legendSelected[title] = !currentSelectedKpiIds.length || legendSelected[title] =
!currentSelectedKpiIds.length ||
currentSelectedKpiIds.includes(series.customKey as string); currentSelectedKpiIds.includes(series.customKey as string);
} }
}); });
@@ -817,7 +903,13 @@ const themeObserver = new MutationObserver(() => {
const option = { const option = {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
position: function (point: number[], _params: any, _dom: HTMLElement, _rect: any, size: { viewSize: number[], contentSize: number[] }) { position: function (
point: number[],
_params: any,
_dom: HTMLElement,
_rect: any,
size: { viewSize: number[]; contentSize: number[] }
) {
const [x] = point; const [x] = point;
const [viewWidth] = size.viewSize; const [viewWidth] = size.viewSize;
const [tooltipWidth] = size.contentSize; const [tooltipWidth] = size.contentSize;
@@ -836,58 +928,64 @@ const themeObserver = new MutationObserver(() => {
}, },
axisPointer: { axisPointer: {
type: 'line', type: 'line',
z: 0 z: 0,
}, },
className: `chart-tooltip-${state.neType}`, className: `chart-tooltip-${state.neType}`,
z: 1000, z: 1000,
extraCssText: 'z-index: 1000; pointer-events: none;', extraCssText: 'z-index: 1000; pointer-events: none;',
confine: true, confine: true,
backgroundColor: isDark ? 'rgba(48, 48, 48, 0.8)' : 'rgba(255, 255, 255, 0.9)', backgroundColor: isDark
? 'rgba(48, 48, 48, 0.8)'
: 'rgba(255, 255, 255, 0.9)',
borderColor: isDark ? '#555' : '#ddd', borderColor: isDark ? '#555' : '#ddd',
textStyle: { textStyle: {
color: isDark ? '#CACADA' : '#333' color: isDark ? '#CACADA' : '#333',
} },
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',
boundaryGap: false, boundaryGap: false,
data: state.chartDataXAxisData, data: state.chartDataXAxisData,
axisLabel: { axisLabel: {
color: isDark ? '#CACADA' : '#333' color: isDark ? '#CACADA' : '#333',
}, },
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: splitLineColor color: splitLineColor,
} },
} },
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
boundaryGap: [0, '100%'], boundaryGap: [0, '100%'],
axisLabel: { axisLabel: {
color: isDark ? '#CACADA' : '#333' color: isDark ? '#CACADA' : '#333',
}, },
splitNumber: 5, splitNumber: 5,
scale: true, scale: true,
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: splitLineColor color: splitLineColor,
} },
} },
}, },
legend: { legend: {
show: false, show: false,
selected: legendSelected, selected: legendSelected,
textStyle: { textStyle: {
color: isDark ? '#CACADA' : '#646A73' color: isDark ? '#CACADA' : '#646A73',
}, },
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.9)', backgroundColor: isDark
? 'rgba(0, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.9)',
borderColor: isDark ? '#555' : '#ddd', borderColor: isDark ? '#555' : '#ddd',
}, },
series: state.chartDataYSeriesData.map(series => { series: state.chartDataYSeriesData.map(series => {
const color = state.seriesColors.get(series.customKey as string) as string; const color = state.seriesColors.get(
series.customKey as string
) as string;
return { return {
...series, ...series,
type: 'line', type: 'line',
@@ -902,14 +1000,14 @@ const themeObserver = new MutationObserver(() => {
{ offset: 1, color: color.replace(')', ',0.3)') }, { offset: 1, color: color.replace(')', ',0.3)') },
]), ]),
}, },
data: series.data data: series.data,
}; };
}) }),
}; };
chart.setOption(option, { chart.setOption(option, {
notMerge: false, notMerge: false,
lazyUpdate: false lazyUpdate: false,
}); });
// 强制重新渲染 // 强制重新渲染
@@ -945,7 +1043,9 @@ onMounted(async () => {
// 从本地存储中读取选中的网元类型 // 从本地存储中读取选中的网元类型
const savedSelectedNeTypes = localStorage.getItem('selectedNeTypes'); const savedSelectedNeTypes = localStorage.getItem('selectedNeTypes');
if (savedSelectedNeTypes) { if (savedSelectedNeTypes) {
const parsedSelectedNeTypes = JSON.parse(savedSelectedNeTypes) as AllChartType[]; const parsedSelectedNeTypes = JSON.parse(
savedSelectedNeTypes
) as AllChartType[];
selectedNeTypes.value = parsedSelectedNeTypes; selectedNeTypes.value = parsedSelectedNeTypes;
networkElementTypes.value = parsedSelectedNeTypes; networkElementTypes.value = parsedSelectedNeTypes;
} else { } else {
@@ -957,7 +1057,7 @@ onMounted(async () => {
// 添加主题观察器 // 添加主题观察器
themeObserver.observe(document.documentElement, { themeObserver.observe(document.documentElement, {
attributes: true, attributes: true,
attributeFilter: ['data-theme'] attributeFilter: ['data-theme'],
}); });
await initCharts(); await initCharts();
@@ -968,7 +1068,7 @@ onUnmounted(() => {
if (ws.value && ws.value.state() === WebSocket.OPEN) { if (ws.value && ws.value.state() === WebSocket.OPEN) {
ws.value.close(); ws.value.close();
} }
Object.values(chartStates).forEach((state) => { Object.values(chartStates).forEach(state => {
if (state.chart.value) { if (state.chart.value) {
state.chart.value.dispose(); state.chart.value.dispose();
} }
@@ -996,7 +1096,8 @@ const getKpiStats = (type: AllChartType) => {
const state = chartStates[type]; const state = chartStates[type];
if (!state?.chartDataYSeriesData) return []; if (!state?.chartDataYSeriesData) return [];
return state.chartDataYSeriesData.map(series => { return state.chartDataYSeriesData
.map(series => {
const kpiId = series.customKey as string; const kpiId = series.customKey as string;
const column = state.tableColumns.value.find(col => col.key === kpiId); const column = state.tableColumns.value.find(col => col.key === kpiId);
if (!column) return null; if (!column) return null;
@@ -1009,13 +1110,16 @@ const getKpiStats = (type: AllChartType) => {
title: column.title as string, title: column.title as string,
max: values.length > 0 ? Math.max(...values) : 0, max: values.length > 0 ? Math.max(...values) : 0,
min: values.length > 0 ? Math.min(...values) : 0, min: values.length > 0 ? Math.min(...values) : 0,
neType: type neType: type,
}; };
}).filter((item): item is KPIStats => item !== null); })
.filter((item): item is KPIStats => item !== null);
}; };
// 修改选中行的状态改为数组存储个选中的指标ID // 修改选中行的状态改为数组存储个选中的指标ID
const selectedRows = ref<Record<AllChartType, string[]>>({} as Record<AllChartType, string[]>); const selectedRows = ref<Record<AllChartType, string[]>>(
{} as Record<AllChartType, string[]>
);
// 修改处理行点击的方法,支持多选 // 修改处理行点击的方法,支持多选
const handleRowClick = (record: KPIStats, type: AllChartType) => { const handleRowClick = (record: KPIStats, type: AllChartType) => {
@@ -1041,7 +1145,10 @@ const handleRowClick = (record: KPIStats, type: AllChartType) => {
}; };
// 修改更新图表图例选中的方法,支持多选 // 修改更新图表图例选中的方法,支持多选
const updateChartLegendSelect = (type: AllChartType, selectedKpiIds?: string[]) => { const updateChartLegendSelect = (
type: AllChartType,
selectedKpiIds?: string[]
) => {
const state = chartStates[type]; const state = chartStates[type];
const chart = state?.chart?.value; const chart = state?.chart?.value;
if (!chart) return; if (!chart) return;
@@ -1053,11 +1160,15 @@ const updateChartLegendSelect = (type: AllChartType, selectedKpiIds?: string[])
const legendSelected: Record<string, boolean> = {}; const legendSelected: Record<string, boolean> = {};
state.chartDataYSeriesData.forEach(series => { state.chartDataYSeriesData.forEach(series => {
const title = state.tableColumns.value.find(col => col.key === series.customKey)?.title || series.customKey; const title =
state.tableColumns.value.find(col => col.key === series.customKey)
?.title || series.customKey;
if (typeof title === 'string') { if (typeof title === 'string') {
// 如果没有选中的指标或选中列表为空,显示所有指标 // 如果没有选中的指标或选中列表为空,显示所有指标
// 否则只显示选中的指标 // 否则只显示选中的指标
legendSelected[title] = !selectedKpiIds?.length || selectedKpiIds.includes(series.customKey as string); legendSelected[title] =
!selectedKpiIds?.length ||
selectedKpiIds.includes(series.customKey as string);
} }
}); });
@@ -1070,9 +1181,11 @@ const updateChartLegendSelect = (type: AllChartType, selectedKpiIds?: string[])
textStyle: { textStyle: {
color: isDark ? '#CACADA' : '#646A73', color: isDark ? '#CACADA' : '#646A73',
}, },
backgroundColor: isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(255, 255, 255, 0.9)', backgroundColor: isDark
? 'rgba(0, 0, 0, 0.2)'
: 'rgba(255, 255, 255, 0.9)',
borderColor: isDark ? '#555' : '#ddd', borderColor: isDark ? '#555' : '#ddd',
} },
}); });
}; };
@@ -1092,9 +1205,9 @@ const statsColumns: TableColumnType<KPIStats>[] = [
color: color || '#000', color: color || '#000',
fontSize: '40px', fontSize: '40px',
fontWeight: 'bold', fontWeight: 'bold',
} as Record<string, string> } as Record<string, string>,
}); });
} },
}, },
{ {
title: t('views.perfManage.kpiOverView.kpiName'), title: t('views.perfManage.kpiOverView.kpiName'),
@@ -1117,7 +1230,7 @@ const statsColumns: TableColumnType<KPIStats>[] = [
width: '30%', width: '30%',
sorter: (a: KPIStats, b: KPIStats) => a.min - b.min, sorter: (a: KPIStats, b: KPIStats) => a.min - b.min,
sortDirections: ['ascend', 'descend'] as ('ascend' | 'descend')[], sortDirections: ['ascend', 'descend'] as ('ascend' | 'descend')[],
} },
]; ];
// 1. 添加 tab 切换处理函数 // 1. 添加 tab 切换处理函数
@@ -1142,7 +1255,13 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => {
<a-form layout="horizontal"> <a-form layout="horizontal">
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :lg="4" :md="24" :xs="24"> <a-col :lg="4" :md="24" :xs="24">
<a-form-item :label="realTimeEnabled ? t('views.dashboard.cdr.realTimeDataStop'):t('views.dashboard.cdr.realTimeDataStart')"> <a-form-item
:label="
realTimeEnabled
? t('views.dashboard.cdr.realTimeDataStop')
: t('views.dashboard.cdr.realTimeDataStart')
"
>
<a-switch <a-switch
v-model:checked="realTimeEnabled" v-model:checked="realTimeEnabled"
@change="handleRealTimeSwitch" @change="handleRealTimeSwitch"
@@ -1150,7 +1269,10 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => {
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :lg="20" :md="24" :xs="24"> <a-col :lg="20" :md="24" :xs="24">
<a-form-item :label="t('views.ne.common.neType')" class="ne-type-select"> <a-form-item
:label="t('views.ne.common.neType')"
class="ne-type-select"
>
<a-checkbox-group <a-checkbox-group
v-model:value="selectedNeTypes" v-model:value="selectedNeTypes"
:options="neTypeOptions" :options="neTypeOptions"
@@ -1192,11 +1314,19 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => {
</template> </template>
<div class="card-content"> <div class="card-content">
<div class="chart-container"> <div class="chart-container">
<div :ref="el => { if (el && chartStates[type]) chartStates[type].chartDom.value = el as HTMLElement }"></div> <div
:ref="el => { if (el && chartStates[type]) chartStates[type].chartDom.value = el as HTMLElement }"
></div>
</div> </div>
<div class="table-container"> <div class="table-container">
<a-tabs default-active-key="stats" @change="(key:any) => handleTabChange(key, type)"> <a-tabs
<a-tab-pane key="stats" :tab="t('views.perfManage.kpiKeyTarget.statistics')"> default-active-key="stats"
@change="(key:any) => handleTabChange(key, type)"
>
<a-tab-pane
key="stats"
:tab="t('views.perfManage.kpiKeyTarget.statistics')"
>
<a-table <a-table
:columns="statsColumns" :columns="statsColumns"
:data-source="getKpiStats(type)" :data-source="getKpiStats(type)"
@@ -1204,13 +1334,67 @@ const handleTabChange = async (activeKey: string, type: AllChartType) => {
size="small" size="small"
:scroll="{ y: 'true' }" :scroll="{ y: 'true' }"
:loading="chartStates[type].tableState.loading" :loading="chartStates[type].tableState.loading"
:custom-row="(record) => ({ :custom-row="
record => ({
onClick: () => handleRowClick(record, type), onClick: () => handleRowClick(record, type),
class: selectedRows[type]?.includes(record.kpiId) ? 'selected-row' : '' class: selectedRows[type]?.includes(record.kpiId)
})" ? 'selected-row'
/> : '',
})
"
>
<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>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="raw" :tab="t('views.perfManage.kpiKeyTarget.rawData')"> <a-tab-pane
key="raw"
:tab="t('views.perfManage.kpiKeyTarget.rawData')"
>
<a-table <a-table
:columns="chartStates[type].tableColumns.value" :columns="chartStates[type].tableColumns.value"
:data-source="chartStates[type].tableState.data" :data-source="chartStates[type].tableState.data"

View File

@@ -1,13 +1,21 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick, computed, h } 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 } 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,} 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';
@@ -35,7 +43,7 @@ const tableLoading = ref(false);
const rangeLoading = ref(false); const rangeLoading = ref(false);
//网元类型定义 //网元类型定义
const ALL_NE_TYPES = ['AMF', 'SMF', 'UPF', 'MME', 'IMS', 'SMSC'] as const; const ALL_NE_TYPES = ['AMF', 'SMF', 'UPF', 'MME', 'IMS', 'SMSC'] as const;
type NeType= typeof ALL_NE_TYPES[number]; type NeType = (typeof ALL_NE_TYPES)[number];
echarts.use([ echarts.use([
LineChart, LineChart,
@@ -61,12 +69,19 @@ const ws = ref<WS | null>(null);
// [t('views.monitor.monitor.today')]: [dayjs().startOf('day'), dayjs()], // [t('views.monitor.monitor.today')]: [dayjs().startOf('day'), dayjs()],
// }); // });
const ranges = ref([ const ranges = ref([
{label:t('views.perfManage.customTarget.sixHoursAgo'),value:[dayjs().subtract(6, 'hours'), {
dayjs(),]}, label: t('views.perfManage.customTarget.sixHoursAgo'),
{label:t('views.perfManage.customTarget.threeHoursAgo'),value:[dayjs().subtract(3, 'hours'), value: [dayjs().subtract(6, 'hours'), dayjs()],
dayjs(),]}, },
{label:t('views.monitor.monitor.today'),value:[dayjs().startOf('day'), 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]>([ const dateRange = ref<[string, string]>([
dayjs().subtract(1, 'hour').startOf('hour').valueOf().toString(), // 上一小时开始 dayjs().subtract(1, 'hour').startOf('hour').valueOf().toString(), // 上一小时开始
@@ -181,18 +196,20 @@ const wsMessage = (res:Record<string,any>)=>{
const processChartData = (rawData: any[]) => { 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; const timeKey = item.timeGroup;
if (!groupedData.has(timeKey)) {//按时间排序 if (!groupedData.has(timeKey)) {
//按时间排序
groupedData.set(timeKey, { timeGroup: timeKey }); groupedData.set(timeKey, { timeGroup: timeKey });
} }
Object.assign(groupedData.get(timeKey), item); Object.assign(groupedData.get(timeKey), item);
}); });
return Array.from(groupedData.values()) return Array.from(groupedData.values())
.sort((a, b) => Number(a.timeGroup) - Number(b.timeGroup)) .sort((a, b) => Number(a.timeGroup) - Number(b.timeGroup))
.map(item => {//转换成图表需要的格式 .map(item => {
//转换成图表需要的格式
const dataItem: ChartDataItem = { date: item.timeGroup.toString() }; const dataItem: ChartDataItem = { date: item.timeGroup.toString() };
selectedKPIs.value.forEach(kpiId => { selectedKPIs.value.forEach(kpiId => {
dataItem[kpiId] = Number(item[kpiId]) || 0; dataItem[kpiId] = Number(item[kpiId]) || 0;
@@ -216,7 +233,7 @@ const fetchChartData = async () => {
} }
// 创建并行请求数组 // 创建并行请求数组
const requests = ALL_NE_TYPES.map(async (neType) => { const requests = ALL_NE_TYPES.map(async neType => {
const params = { const params = {
neType, neType,
neId: '001', neId: '001',
@@ -289,7 +306,8 @@ const themeObserver = new MutationObserver(() => {
kpiColors.set(kpiId, color); kpiColors.set(kpiId, color);
}); });
// 使用存储的颜色更新图表系列 // 使用存储的颜色更新图表系列
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);
if (!kpi) return null; if (!kpi) return null;
@@ -300,16 +318,18 @@ const themeObserver = new MutationObserver(() => {
itemStyle: { color: kpiColors.get(kpiId) }, itemStyle: { color: kpiColors.get(kpiId) },
...getSeriesConfig(), ...getSeriesConfig(),
}; };
}).filter(Boolean); })
.filter(Boolean);
const option = { const option = {
title: { title: {
text: t('views.perfManage.kpiOverView.kpiChartTitle'), text: t('views.perfManage.kpiOverView.kpiChartTitle'),
left: 'center', left: 'center',
textStyle: { textStyle: {
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
} },
}, },
xAxis: { xAxis: {
// 保持现有的 xAxis 配置 // 保持现有的 xAxis 配置
@@ -319,61 +339,68 @@ const themeObserver = new MutationObserver(() => {
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss') dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
), ),
axisLabel: { axisLabel: {
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
}, },
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: splitLineColor color: splitLineColor,
} },
} },
}, },
yAxis: { yAxis: {
type: 'value', type: 'value',
axisLabel: { axisLabel: {
formatter: '{value}', formatter: '{value}',
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
}, },
splitNumber: 5, splitNumber: 5,
scale: true, scale: true,
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: splitLineColor color: splitLineColor,
} },
} },
}, },
legend: { legend: {
show: false, show: false,
selected: Object.fromEntries( selected: Object.fromEntries(
selectedKPIs.value.map(kpiId => [ selectedKPIs.value.map(kpiId => [
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || 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: { tooltip: {
trigger: 'axis', trigger: 'axis',
confine: true, // 限制 tooltip 显示范围 confine: true, // 限制 tooltip 显示范围
backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark' backgroundColor:
document.documentElement.getAttribute('data-theme') === 'dark'
? 'rgba(48, 48, 48, 0.8)' ? 'rgba(48, 48, 48, 0.8)'
: 'rgba(255, 255, 255, 0.9)', : 'rgba(255, 255, 255, 0.9)',
borderColor: document.documentElement.getAttribute('data-theme') === 'dark' borderColor:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#555' ? '#555'
: '#ddd', : '#ddd',
textStyle: { textStyle: {
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#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,
}; };
// 使用新的配置更新图表 // 使用新的配置更新图表
@@ -393,7 +420,8 @@ const updateChart = () => {
//获取图表配置 //获取图表配置
const commonConfig = getSeriesConfig(); 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);
if (!kpi) return null; if (!kpi) return null;
//为每个KPI分配临时的固定颜色 //为每个KPI分配临时的固定颜色
@@ -407,7 +435,8 @@ const updateChart = () => {
itemStyle: { color }, itemStyle: { color },
...commonConfig, ...commonConfig,
}; };
}).filter(Boolean); })
.filter(Boolean);
const option = { const option = {
title: { title: {
@@ -415,29 +444,34 @@ const updateChart = () => {
left: 'center', left: 'center',
// 添加文字颜色配置,根据主题切换 // 添加文字颜色配置,根据主题切换
textStyle: { textStyle: {
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
} },
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: 'axis',
confine: true, // 限制 tooltip 显示范围 confine: true, // 限制 tooltip 显示范围
backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark' backgroundColor:
document.documentElement.getAttribute('data-theme') === 'dark'
? 'rgba(48, 48, 48, 0.8)' ? 'rgba(48, 48, 48, 0.8)'
: 'rgba(255, 255, 255, 0.9)', : 'rgba(255, 255, 255, 0.9)',
borderColor: document.documentElement.getAttribute('data-theme') === 'dark' borderColor:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#555' ? '#555'
: '#ddd', : '#ddd',
textStyle: { textStyle: {
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#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: { legend: {
data: selectedKPIs.value.map(kpiId => data: selectedKPIs.value.map(
kpiId =>
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId
), ),
type: 'scroll', type: 'scroll',
@@ -449,7 +483,9 @@ const updateChart = () => {
selected: Object.fromEntries( selected: Object.fromEntries(
selectedKPIs.value.map(kpiId => [ selectedKPIs.value.map(kpiId => [
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || 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, show: false,
@@ -470,15 +506,16 @@ const updateChart = () => {
type: 'category', type: 'category',
axisLabel: { axisLabel: {
formatter: '{value}', formatter: '{value}',
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
}, },
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: getSplitLineColor() color: getSplitLineColor(),
} },
}, },
//控制坐标轴两边留白 //控制坐标轴两边留白
// 当为折线图时(isLine为true)时不留白,柱状图时留白 // 当为折线图时(isLine为true)时不留白,柱状图时留白
@@ -519,9 +556,10 @@ const updateChart = () => {
type: 'value', type: 'value',
axisLabel: { axisLabel: {
formatter: '{value}', formatter: '{value}',
color: document.documentElement.getAttribute('data-theme') === 'dark' color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA' ? '#CACADA'
: '#333' : '#333',
}, },
// 添加自计算的分割段数 // 添加自计算的分割段数
splitNumber: 5, splitNumber: 5,
@@ -530,9 +568,9 @@ const updateChart = () => {
splitLine: { splitLine: {
show: true, show: true,
lineStyle: { lineStyle: {
color: getSplitLineColor() color: getSplitLineColor(),
} },
} },
}, },
series: series, //配置数据 series: series, //配置数据
}; };
@@ -561,8 +599,6 @@ const updateChart = () => {
} }
}; };
//钩子函数 //钩子函数
onMounted(async () => { onMounted(async () => {
try { try {
@@ -588,7 +624,7 @@ onMounted(async () => {
// 添加主题观察器 // 添加主题观察器
themeObserver.observe(document.documentElement, { themeObserver.observe(document.documentElement, {
attributes: true, attributes: true,
attributeFilter: ['data-theme'] attributeFilter: ['data-theme'],
}); });
} catch (error) { } catch (error) {
console.error('Failed to initialize:', error); console.error('Failed to initialize:', error);
@@ -603,7 +639,8 @@ const selectedKPIs = ref<string[]>(Object.values(TARGET_KPI_IDS).flat());
// 获取网元指标 // 获取网元指标
const fetchSpecificKPI = async () => { const fetchSpecificKPI = async () => {
const language = currentLocale.value.split('_')[0] === 'zh' const language =
currentLocale.value.split('_')[0] === 'zh'
? 'cn' ? 'cn'
: currentLocale.value.split('_')[0]; : currentLocale.value.split('_')[0];
@@ -668,7 +705,8 @@ const updateChartData = (newData: ChartDataItem) => {
return; return;
} }
chartData.value.push(newData); chartData.value.push(newData);
if (chartData.value.length > 100) {//100改为50 if (chartData.value.length > 100) {
//100改为50
chartData.value.shift(); //大于100条时删除最早的数据 chartData.value.shift(); //大于100条时删除最早的数据
} }
//使用try-catch包裹图表更新逻辑 //使用try-catch包裹图表更新逻辑
@@ -697,7 +735,6 @@ const updateChartData = (newData: ChartDataItem) => {
} }
}; };
// 添加一个接口定义指标统计数据的类型 // 添加一个接口定义指标统计数据的类型
interface KPIStats { interface KPIStats {
kpiId: string; kpiId: string;
@@ -718,7 +755,8 @@ const updateKpiStats = () => {
kpiStats.value = []; kpiStats.value = [];
return; return;
} }
kpiStats.value = selectedKPIs.value.map(kpiId => { kpiStats.value = selectedKPIs.value
.map(kpiId => {
// 找到对应的KPI标题 // 找到对应的KPI标题
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId); const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
if (!kpi) return null; if (!kpi) return null;
@@ -727,12 +765,13 @@ const updateKpiStats = () => {
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 const avg =
? Number((total / values.length).toFixed(2)) values.length > 0 ? Number((total / values.length).toFixed(2)) : 0;
: 0;
return { return {
kpiId: kpiId, kpiId: kpiId,
@@ -740,12 +779,12 @@ const updateKpiStats = () => {
max: Math.max(...values), max: Math.max(...values),
min: Math.min(...values), min: Math.min(...values),
avg: avg, avg: avg,
total: total total: total,
}; };
}).filter((item): item is KPIStats => item !== null); })
.filter((item): item is KPIStats => item !== null);
}; };
// 添加表列定义 // 添加表列定义
const statsColumns: TableColumnType<KPIStats>[] = [ const statsColumns: TableColumnType<KPIStats>[] = [
{ {
@@ -761,9 +800,9 @@ const statsColumns: TableColumnType<KPIStats>[] = [
color: color || '#000', // 使用与折线图相同的颜色 color: color || '#000', // 使用与折线图相同的颜色
fontSize: '30px', // 增大图标尺寸到30px fontSize: '30px', // 增大图标尺寸到30px
fontWeight: 'bold', // 加粗 fontWeight: 'bold', // 加粗
} },
}); });
} },
}, },
{ {
title: t('views.perfManage.kpiOverView.kpiName'), title: t('views.perfManage.kpiOverView.kpiName'),
@@ -829,14 +868,16 @@ const updateChartLegendSelect = () => {
const legendSelected = Object.fromEntries( const legendSelected = Object.fromEntries(
selectedKPIs.value.map(kpiId => [ selectedKPIs.value.map(kpiId => [
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || 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({ chart.setOption({
legend: { legend: {
selected: legendSelected selected: legendSelected,
} },
}); });
}; };
@@ -844,7 +885,7 @@ const updateChartLegendSelect = () => {
const tableRowConfig = computed(() => { const tableRowConfig = computed(() => {
return (record: KPIStats) => ({ return (record: KPIStats) => ({
onClick: () => handleRowClick(record), onClick: () => handleRowClick(record),
class: selectedRows.value.includes(record.kpiId) ? 'selected-row' : '' class: selectedRows.value.includes(record.kpiId) ? 'selected-row' : '',
}); });
}); });
</script> </script>
@@ -882,7 +923,54 @@ const tableRowConfig = computed(() => {
size="small" size="small"
:loading="tableLoading" :loading="tableLoading"
:custom-row="tableRowConfig" :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> </div>
</div> </div>