feat:关键指标、黄金指标仪表盘多网元实现
This commit is contained in:
@@ -45,6 +45,9 @@ import { OptionsType, WS } from '@/plugins/ws-websocket';
|
|||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { LineOutlined } from '@ant-design/icons-vue';
|
import { LineOutlined } from '@ant-design/icons-vue';
|
||||||
import useLayoutStore from '@/store/modules/layout';
|
import useLayoutStore from '@/store/modules/layout';
|
||||||
|
import { SelectValue } from 'ant-design-vue/es/select';
|
||||||
|
import type { DefaultOptionType } from 'ant-design-vue/es/select';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
const neInfoStore = useNeInfoStore();
|
const neInfoStore = useNeInfoStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@@ -141,12 +144,15 @@ function fnTableSize({ key }: MenuInfo) {
|
|||||||
tableState.size = key as SizeType;
|
tableState.size = key as SizeType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**选中的网元信息 */
|
||||||
|
let selectedNes = ref<{neType: string, neId: string}[]>([]);
|
||||||
|
|
||||||
/**查询参数 */
|
/**查询参数 */
|
||||||
let queryParams: any = reactive({
|
let queryParams: any = reactive({
|
||||||
/**网元类型 */
|
/**网元类型 */
|
||||||
neType: '',
|
neType: '',
|
||||||
/**网元标识 */
|
/**网元标识列表 */
|
||||||
neId: '',
|
neIds: [] as string[],
|
||||||
/**颗粒度 */
|
/**颗粒度 */
|
||||||
interval: 900,
|
interval: 900,
|
||||||
/**开始时间 */
|
/**开始时间 */
|
||||||
@@ -178,7 +184,9 @@ function fnTableChange(pagination: any, filters: any, sorter: any, extra: any) {
|
|||||||
/**对象信息状态类型 */
|
/**对象信息状态类型 */
|
||||||
type StateType = {
|
type StateType = {
|
||||||
/**网元类型 */
|
/**网元类型 */
|
||||||
neType: string[];
|
neType: string;
|
||||||
|
/**选中的网元ID列表 */
|
||||||
|
neIds: string[];
|
||||||
/**图表实时统计 */
|
/**图表实时统计 */
|
||||||
chartRealTime: boolean;
|
chartRealTime: boolean;
|
||||||
/**图表标签选择 */
|
/**图表标签选择 */
|
||||||
@@ -187,7 +195,8 @@ type StateType = {
|
|||||||
|
|
||||||
/**对象信息状态 */
|
/**对象信息状态 */
|
||||||
let state: StateType = reactive({
|
let state: StateType = reactive({
|
||||||
neType: [],
|
neType: '',
|
||||||
|
neIds: [],
|
||||||
chartRealTime: false,
|
chartRealTime: false,
|
||||||
chartLegendSelectedFlag: true,
|
chartLegendSelectedFlag: true,
|
||||||
});
|
});
|
||||||
@@ -304,14 +313,80 @@ function fnRecordExport() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**可选网元列表 */
|
||||||
|
let availableNeIds = ref<{label: string, value: string}[]>([]);
|
||||||
|
|
||||||
|
// 添加类型定义
|
||||||
|
interface KPIStats {
|
||||||
|
kpiId: string;
|
||||||
|
title: string;
|
||||||
|
rawKpiId: string;
|
||||||
|
rawKpiTitle: string;
|
||||||
|
neId: string;
|
||||||
|
max: number;
|
||||||
|
min: number;
|
||||||
|
avg: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**处理网元类型变化 */
|
||||||
|
function handleNeTypeChange(value: SelectValue, option: DefaultOptionType | DefaultOptionType[]) {
|
||||||
|
if (!value) {
|
||||||
|
state.neType = '';
|
||||||
|
state.neIds = [];
|
||||||
|
availableNeIds.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.neType = value as string;
|
||||||
|
queryParams.neType = value as string;
|
||||||
|
state.neIds = []; // 清空已选网元
|
||||||
|
|
||||||
|
// 根据选择的网元类型更新可选的网元ID列表
|
||||||
|
const neTypeOption = neCascaderOptions.value.find(item => item.value === value);
|
||||||
|
if (neTypeOption && neTypeOption.children && neTypeOption.children.length > 0) {
|
||||||
|
availableNeIds.value = neTypeOption.children.map((ne: any) => ({
|
||||||
|
label: `${ne.label}`,
|
||||||
|
value: ne.neId
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 默认选择第一个网元
|
||||||
|
if (availableNeIds.value.length > 0) {
|
||||||
|
state.neIds = [availableNeIds.value[0].value];
|
||||||
|
queryParams.neIds = [...state.neIds];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
availableNeIds.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**处理网元ID变化 */
|
||||||
|
function handleNeIdsChange(value: SelectValue, option: DefaultOptionType | DefaultOptionType[]) {
|
||||||
|
if (!value || (Array.isArray(value) && value.length === 0)) {
|
||||||
|
state.neIds = [];
|
||||||
|
queryParams.neIds = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = Array.isArray(value) ? value : [value];
|
||||||
|
state.neIds = values as string[];
|
||||||
|
queryParams.neIds = [...values as string[]];
|
||||||
|
|
||||||
|
// 更新选中的网元信息
|
||||||
|
selectedNes.value = (values as string[]).map(neId => ({
|
||||||
|
neType: state.neType,
|
||||||
|
neId: neId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
/**查询数据列表表头 */
|
/**查询数据列表表头 */
|
||||||
function fnGetListTitle() {
|
function fnGetListTitle() {
|
||||||
// 当前语言
|
// 当前语言
|
||||||
var language = currentLocale.value.split('_')[0];
|
var language = currentLocale.value.split('_')[0];
|
||||||
if (language === 'zh') language = 'cn';
|
if (language === 'zh') language = 'cn';
|
||||||
|
|
||||||
// 获取表头文字
|
// 获取表头文字,只传递纯网元类型
|
||||||
getKPITitle(state.neType[0])
|
getKPITitle(state.neType)
|
||||||
.then(res => {
|
.then(res => {
|
||||||
//处理getKPITitle返回的结果
|
//处理getKPITitle返回的结果
|
||||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||||
@@ -369,29 +444,51 @@ function fnGetListTitle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**查询数据列表 */
|
/**查询数据列表 */
|
||||||
function fnGetList() {
|
async function fnGetList() {
|
||||||
if (tableState.loading) return;
|
if (tableState.loading) return;
|
||||||
tableState.loading = true;
|
tableState.loading = true;
|
||||||
queryParams.neType = state.neType[0];
|
|
||||||
queryParams.neId = state.neType[1];
|
|
||||||
queryParams.startTime = queryRangePicker.value[0];
|
|
||||||
queryParams.endTime = queryRangePicker.value[1];
|
|
||||||
listKPIData(toRaw(queryParams))
|
|
||||||
.then(res => {
|
|
||||||
tableState.loading = false;
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
|
||||||
tablePagination.total = res.data.length;
|
|
||||||
tableState.data = res.data;
|
|
||||||
|
|
||||||
return true;
|
try {
|
||||||
|
// 获取所有网元数据
|
||||||
|
let allNeData: any[] = [];
|
||||||
|
|
||||||
|
// 每个选中的网元ID发送一个请求
|
||||||
|
const promises = state.neIds.map(neId => {
|
||||||
|
const params = {
|
||||||
|
...queryParams,
|
||||||
|
neId: neId,
|
||||||
|
startTime: queryRangePicker.value[0],
|
||||||
|
endTime: queryRangePicker.value[1]
|
||||||
|
};
|
||||||
|
// 不需要将neIds发送到后端
|
||||||
|
delete params.neIds;
|
||||||
|
|
||||||
|
return listKPIData(toRaw(params)).then(res => {
|
||||||
|
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||||
|
// 添加网元ID标识,方便后续处理
|
||||||
|
return res.data.map(item => ({
|
||||||
|
...item,
|
||||||
|
_neId: neId,
|
||||||
|
neName: `${state.neType}-${neId}`
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return false;
|
return [];
|
||||||
})
|
});
|
||||||
.then(result => {
|
});
|
||||||
if (result) {
|
|
||||||
|
const results = await Promise.all(promises);
|
||||||
|
allNeData = results.flat();
|
||||||
|
|
||||||
|
tablePagination.total = allNeData.length;
|
||||||
|
tableState.data = allNeData;
|
||||||
|
|
||||||
|
// 调用图表渲染函数
|
||||||
fnRanderChartData();
|
fnRanderChartData();
|
||||||
|
|
||||||
// 封装legend表格数据
|
// 封装legend表格数据
|
||||||
kpiStats.value = [];
|
kpiStats.value = [];
|
||||||
|
|
||||||
|
// 为每个指标和每个网元创建统计数据
|
||||||
for (const columns of tableColumns.value) {
|
for (const columns of tableColumns.value) {
|
||||||
if (
|
if (
|
||||||
columns.key === 'neName' ||
|
columns.key === 'neName' ||
|
||||||
@@ -401,7 +498,14 @@ function fnGetList() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const values = tableState.data.map((item: any) => {
|
// 处理每个网元
|
||||||
|
for (const neId of state.neIds) {
|
||||||
|
// 过滤该网元的数据
|
||||||
|
const neData = tableState.data.filter(item => item._neId === neId);
|
||||||
|
|
||||||
|
if (neData.length === 0) continue;
|
||||||
|
|
||||||
|
const values = neData.map((item: any) => {
|
||||||
return item[columns.key] ? Number(item[columns.key]) : 0;
|
return item[columns.key] ? Number(item[columns.key]) : 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -415,8 +519,11 @@ function fnGetList() {
|
|||||||
values.length > 0 ? Number((total / values.length).toFixed(2)) : 0;
|
values.length > 0 ? Number((total / values.length).toFixed(2)) : 0;
|
||||||
|
|
||||||
kpiStats.value.push({
|
kpiStats.value.push({
|
||||||
kpiId: columns.key,
|
kpiId: `${columns.key}_${neId}`, // 使用组合ID来唯一标识指标-网元对
|
||||||
title: columns.title,
|
title: `${columns.title}(${neId})`, // 在标题中显示网元ID
|
||||||
|
rawKpiId: columns.key,
|
||||||
|
rawKpiTitle: columns.title,
|
||||||
|
neId: neId,
|
||||||
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,
|
avg: avg,
|
||||||
@@ -424,7 +531,18 @@ function fnGetList() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
message.error({
|
||||||
|
content: t('common.getInfoFail'),
|
||||||
|
duration: 2,
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
tableState.loading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**切换显示类型 图或表格 */
|
/**切换显示类型 图或表格 */
|
||||||
@@ -446,17 +564,53 @@ function fnRanderChart() {
|
|||||||
return [pt[0], '10%'];
|
return [pt[0], '10%'];
|
||||||
},
|
},
|
||||||
confine: true, // 限制 tooltip 显示范围
|
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',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
//x类别轴
|
//x类别轴
|
||||||
type: 'category',
|
type: 'category',
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
data: [], // 数据x轴
|
data: [], // 数据x轴
|
||||||
|
axisLabel: {
|
||||||
|
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
? '#CACADA'
|
||||||
|
: '#333',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: getSplitLineColor(),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
yAxis: {
|
yAxis: {
|
||||||
//y类别轴
|
//y类别轴
|
||||||
type: 'value',
|
type: 'value',
|
||||||
boundaryGap: [0, '100%'],
|
boundaryGap: [0, '100%'],
|
||||||
|
axisLabel: {
|
||||||
|
formatter: '{value}',
|
||||||
|
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
? '#CACADA'
|
||||||
|
: '#333',
|
||||||
|
},
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: true,
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: getSplitLineColor(),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
legend: {
|
legend: {
|
||||||
show: false,
|
show: false,
|
||||||
@@ -475,12 +629,11 @@ function fnRanderChart() {
|
|||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
//网格区域边距
|
//网格区域边距
|
||||||
left: '7%',
|
left: '3%',
|
||||||
right: '7%',
|
right: '4%',
|
||||||
bottom: '7%',
|
bottom: '3%',
|
||||||
containLabel: true,
|
containLabel: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
series: [], // 数据y轴
|
series: [], // 数据y轴
|
||||||
};
|
};
|
||||||
kpiChart.value.setOption(option); //设置图表配置项,应用到kpiChart实例上
|
kpiChart.value.setOption(option); //设置图表配置项,应用到kpiChart实例上
|
||||||
@@ -504,7 +657,7 @@ let chartDataYSeriesData: Record<string, any>[] = [];
|
|||||||
|
|
||||||
/**图表数据渲染 */
|
/**图表数据渲染 */
|
||||||
function fnRanderChartData() {
|
function fnRanderChartData() {
|
||||||
if (kpiChart.value == null && tableState.data.length <= 0) {
|
if (kpiChart.value == null || tableState.data.length <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -513,6 +666,8 @@ function fnRanderChartData() {
|
|||||||
chartDataXAxisData = [];
|
chartDataXAxisData = [];
|
||||||
chartDataYSeriesData = [];
|
chartDataYSeriesData = [];
|
||||||
|
|
||||||
|
// 为每个网元的每个指标创建一条线
|
||||||
|
for (const neId of state.neIds) {
|
||||||
for (const columns of tableColumns.value) {
|
for (const columns of tableColumns.value) {
|
||||||
if (
|
if (
|
||||||
columns.key === 'neName' ||
|
columns.key === 'neName' ||
|
||||||
@@ -521,11 +676,17 @@ function fnRanderChartData() {
|
|||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const color = kpiColors.get(columns.key) || generateColorRGBA();
|
|
||||||
kpiColors.set(columns.key, color);
|
const kpiId = `${columns.key}_${neId}`;
|
||||||
chartDataYSeriesData.push({
|
const seriesName = `${columns.title}(${neId})`;
|
||||||
name: `${columns.title}`,
|
|
||||||
key: `${columns.key}`,
|
// 获取或生成颜色
|
||||||
|
const color = kpiColors.get(kpiId) || generateColorRGBA();
|
||||||
|
kpiColors.set(kpiId, color);
|
||||||
|
|
||||||
|
const seriesData = {
|
||||||
|
name: seriesName,
|
||||||
|
key: kpiId,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
symbol: 'none',
|
symbol: 'none',
|
||||||
symbolSize: 6,
|
symbolSize: 6,
|
||||||
@@ -547,31 +708,44 @@ function fnRanderChartData() {
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
data: [],
|
data: [] as (number | null)[],
|
||||||
|
};
|
||||||
|
|
||||||
|
chartDataYSeriesData.push(seriesData);
|
||||||
|
|
||||||
|
// 默认所有指标都显示
|
||||||
|
chartLegendSelected[seriesName] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有时间点并格式化
|
||||||
|
const timePoints = [...new Set(tableState.data.map(item => item.timeGroup))].sort();
|
||||||
|
chartDataXAxisData = timePoints.map(time => parseDateToStr(+time));
|
||||||
|
|
||||||
|
// 填充数据
|
||||||
|
for (const series of chartDataYSeriesData) {
|
||||||
|
const [kpiKey, neId] = series.key.split('_');
|
||||||
|
const neData = tableState.data.filter(item => item._neId === neId);
|
||||||
|
series.data = timePoints.map(time => {
|
||||||
|
const dataPoint = neData.find(item => item.timeGroup === time);
|
||||||
|
return dataPoint ? +dataPoint[kpiKey] : null;
|
||||||
});
|
});
|
||||||
chartLegendSelected[`${columns.title}`] = state.chartLegendSelectedFlag;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用降序就反转
|
// 如果有选中的行,只显示选中的指标
|
||||||
let orgData = tableState.data;
|
if (selectedRows.value.length > 0) {
|
||||||
if (queryParams.sortOrder === 'desc') {
|
Object.keys(chartLegendSelected).forEach(key => {
|
||||||
orgData = orgData.toReversed();
|
chartLegendSelected[key] = false;
|
||||||
}
|
});
|
||||||
for (const item of orgData) {
|
|
||||||
chartDataXAxisData.push(parseDateToStr(+item['timeGroup']));
|
|
||||||
const keys = Object.keys(item);
|
|
||||||
for (const y of chartDataYSeriesData) {
|
|
||||||
for (const key of keys) {
|
|
||||||
if (y.key === key) {
|
|
||||||
y.data.push(+item[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// console.log(queryParams.sortOrder, chartLegendSelected);
|
|
||||||
// console.log(chartDataXAxisData, chartDataYSeriesData);
|
|
||||||
|
|
||||||
// 绘制图数据
|
kpiStats.value.forEach((item: KPIStats) => {
|
||||||
|
if (selectedRows.value.includes(item.kpiId)) {
|
||||||
|
chartLegendSelected[item.title] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表
|
||||||
kpiChart.value.setOption(
|
kpiChart.value.setOption(
|
||||||
{
|
{
|
||||||
legend: {
|
legend: {
|
||||||
@@ -581,10 +755,7 @@ function fnRanderChartData() {
|
|||||||
type: 'category',
|
type: 'category',
|
||||||
boundaryGap: false,
|
boundaryGap: false,
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
color:
|
color: document.documentElement.getAttribute('data-theme') === 'dark' ? '#CACADA' : '#333',
|
||||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
|
||||||
? '#CACADA'
|
|
||||||
: '#333',
|
|
||||||
},
|
},
|
||||||
splitLine: {
|
splitLine: {
|
||||||
show: true,
|
show: true,
|
||||||
@@ -594,20 +765,6 @@ function fnRanderChartData() {
|
|||||||
},
|
},
|
||||||
data: chartDataXAxisData,
|
data: chartDataXAxisData,
|
||||||
},
|
},
|
||||||
yAxis: {
|
|
||||||
axisLabel: {
|
|
||||||
color:
|
|
||||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
|
||||||
? '#CACADA'
|
|
||||||
: '#333',
|
|
||||||
},
|
|
||||||
splitLine: {
|
|
||||||
show: true,
|
|
||||||
lineStyle: {
|
|
||||||
color: getSplitLineColor(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
series: chartDataYSeriesData,
|
series: chartDataYSeriesData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -618,9 +775,15 @@ function fnRanderChartData() {
|
|||||||
|
|
||||||
/**图表折线显示全部 */
|
/**图表折线显示全部 */
|
||||||
function fnLegendSelected(bool: any) {
|
function fnLegendSelected(bool: any) {
|
||||||
for (const key of Object.keys(chartLegendSelected)) {
|
// 清空选中状态
|
||||||
|
selectedRows.value = [];
|
||||||
|
|
||||||
|
// 更新所有图例的显示状态
|
||||||
|
Object.keys(chartLegendSelected).forEach(key => {
|
||||||
chartLegendSelected[key] = bool;
|
chartLegendSelected[key] = bool;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
// 更新图表设置
|
||||||
kpiChart.value.setOption({
|
kpiChart.value.setOption({
|
||||||
legend: {
|
legend: {
|
||||||
selected: chartLegendSelected,
|
selected: chartLegendSelected,
|
||||||
@@ -632,15 +795,99 @@ function fnLegendSelected(bool: any) {
|
|||||||
function fnRealTimeSwitch(bool: any) {
|
function fnRealTimeSwitch(bool: any) {
|
||||||
if (bool) {
|
if (bool) {
|
||||||
tableState.seached = false;
|
tableState.seached = false;
|
||||||
|
// 清空图表数据
|
||||||
|
chartData.value = [];
|
||||||
|
chartDataXAxisData = [];
|
||||||
|
chartDataYSeriesData = [];
|
||||||
|
|
||||||
|
// 重新初始化图表配置
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
position: function (pt: any) {
|
||||||
|
return [pt[0], '10%'];
|
||||||
|
},
|
||||||
|
confine: true,
|
||||||
|
backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
? 'rgba(48, 48, 48, 0.8)'
|
||||||
|
: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
borderColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
? '#555'
|
||||||
|
: '#ddd',
|
||||||
|
textStyle: {
|
||||||
|
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
? '#CACADA'
|
||||||
|
: '#333',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: [],
|
||||||
|
axisLabel: {
|
||||||
|
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
? '#CACADA'
|
||||||
|
: '#333',
|
||||||
|
},
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: getSplitLineColor(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
boundaryGap: [0, '100%'],
|
||||||
|
axisLabel: {
|
||||||
|
formatter: '{value}',
|
||||||
|
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
|
? '#CACADA'
|
||||||
|
: '#333',
|
||||||
|
},
|
||||||
|
splitNumber: 5,
|
||||||
|
scale: true,
|
||||||
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: getSplitLineColor(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
type: 'scroll',
|
||||||
|
orient: 'vertical',
|
||||||
|
top: 40,
|
||||||
|
right: 20,
|
||||||
|
itemWidth: 20,
|
||||||
|
itemGap: 25,
|
||||||
|
textStyle: {
|
||||||
|
color: '#646A73',
|
||||||
|
},
|
||||||
|
icon: 'circle',
|
||||||
|
selected: {},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
left: '3%',
|
||||||
|
right: '4%',
|
||||||
|
bottom: '3%',
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
kpiChart.value.setOption(option);
|
||||||
|
|
||||||
// 建立链接
|
// 建立链接
|
||||||
const options: OptionsType = {
|
const options: OptionsType = {
|
||||||
url: '/ws',
|
url: '/ws',
|
||||||
params: {
|
params: {
|
||||||
/**订阅通道组
|
/**订阅通道组
|
||||||
*
|
|
||||||
* 指标(GroupID:10_neType_neId)
|
* 指标(GroupID:10_neType_neId)
|
||||||
|
* 为所有选中的网元创建订阅
|
||||||
*/
|
*/
|
||||||
subGroupID: `10_${queryParams.neType}_${queryParams.neId}`,
|
subGroupID: state.neIds.map(neId => `10_${state.neType}_${neId}`).join(','),
|
||||||
},
|
},
|
||||||
onmessage: wsMessage,
|
onmessage: wsMessage,
|
||||||
onerror: wsError,
|
onerror: wsError,
|
||||||
@@ -658,6 +905,15 @@ function wsError(ev: any) {
|
|||||||
console.error(ev);
|
console.error(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加 ChartDataItem 接口
|
||||||
|
interface ChartDataItem {
|
||||||
|
date: string; // 存储完整的时间字符串
|
||||||
|
[kpiId: string]: string | number; // 动态指标
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加图表数据响应式数组
|
||||||
|
const chartData = ref<ChartDataItem[]>([]);
|
||||||
|
|
||||||
/**接收数据后回调 */
|
/**接收数据后回调 */
|
||||||
function wsMessage(res: Record<string, any>) {
|
function wsMessage(res: Record<string, any>) {
|
||||||
const { code, requestId, data } = res;
|
const { code, requestId, data } = res;
|
||||||
@@ -670,69 +926,221 @@ function wsMessage(res: Record<string, any>) {
|
|||||||
if (!data?.groupId) {
|
if (!data?.groupId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析订阅组ID获取网元类型和ID
|
||||||
|
const [_, neType, neId] = data.groupId.split('_');
|
||||||
|
if (!neType || !neId) return;
|
||||||
|
|
||||||
// kpiEvent 黄金指标指标事件
|
// kpiEvent 黄金指标指标事件
|
||||||
const kpiEvent = data.data;
|
const kpiEvent = data.data;
|
||||||
|
if (!kpiEvent) return;
|
||||||
|
|
||||||
|
// 构造新的数据点
|
||||||
|
const newData: ChartDataItem = {
|
||||||
|
date: kpiEvent.timeGroup?.toString() || Date.now().toString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 为每个网元的每个指标添加数据
|
||||||
|
for (const columns of tableColumns.value) {
|
||||||
|
if (
|
||||||
|
columns.key === 'neName' ||
|
||||||
|
columns.key === 'startIndex' ||
|
||||||
|
columns.key === 'timeGroup'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const key = `${columns.key}_${neId}`;
|
||||||
|
// 确保从 kpiEvent 中获取正确的指标值
|
||||||
|
const value = kpiEvent[columns.key];
|
||||||
|
newData[key] = value !== undefined ? Number(value) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到数据列表
|
||||||
tableState.data.unshift(kpiEvent);
|
tableState.data.unshift(kpiEvent);
|
||||||
tablePagination.total++;
|
tablePagination.total++;
|
||||||
|
|
||||||
// 非对应网元类型
|
// 非对应网元类型
|
||||||
if (kpiEvent.neType !== queryParams.neType) return;
|
if (neType !== queryParams.neType) return;
|
||||||
|
|
||||||
for (const key of Object.keys(data.data)) {
|
// 限制数据量
|
||||||
const v = kpiEvent[key];
|
if (tableState.data.length > 100) {
|
||||||
// x轴
|
tableState.data.pop();
|
||||||
if (key === 'timeGroup') {
|
}
|
||||||
// chartDataXAxisData.shift();
|
|
||||||
chartDataXAxisData.push(parseDateToStr(+v));
|
// 更新图表数据
|
||||||
|
chartData.value.unshift(newData);
|
||||||
|
if (chartData.value.length > 100) {
|
||||||
|
chartData.value.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 requestAnimationFrame 更新图表
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!kpiChart.value) return;
|
||||||
|
|
||||||
|
// 重新生成 series 数据
|
||||||
|
const series = [];
|
||||||
|
for (const neId of state.neIds) {
|
||||||
|
for (const columns of tableColumns.value) {
|
||||||
|
if (
|
||||||
|
columns.key === 'neName' ||
|
||||||
|
columns.key === 'startIndex' ||
|
||||||
|
columns.key === 'timeGroup'
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// y轴
|
|
||||||
const yItem = chartDataYSeriesData.find(item => item.key === key);
|
const kpiId = `${columns.key}_${neId}`;
|
||||||
if (yItem) {
|
const seriesName = `${columns.title}(${neId})`;
|
||||||
// yItem.data.shift();
|
|
||||||
yItem.data.push(+v);
|
// 获取或生成颜色
|
||||||
|
const color = kpiColors.get(kpiId) || generateColorRGBA();
|
||||||
|
kpiColors.set(kpiId, color);
|
||||||
|
|
||||||
|
// 获取该网元该指标的所有数据点
|
||||||
|
const dataPoints = chartData.value.map(item => {
|
||||||
|
const value = item[kpiId];
|
||||||
|
return value !== undefined ? Number(value) : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
series.push({
|
||||||
|
name: seriesName,
|
||||||
|
type: 'line',
|
||||||
|
data: dataPoints,
|
||||||
|
symbol: 'none',
|
||||||
|
symbolSize: 6,
|
||||||
|
smooth: 0.6,
|
||||||
|
showSymbol: true,
|
||||||
|
itemStyle: {
|
||||||
|
color: color,
|
||||||
|
},
|
||||||
|
areaStyle: {
|
||||||
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||||
|
{
|
||||||
|
offset: 0,
|
||||||
|
color: color.replace(')', ',0.8)'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
offset: 1,
|
||||||
|
color: color.replace(')', ',0.3)'),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制图数据
|
// 更新图表
|
||||||
kpiChart.value.setOption({
|
kpiChart.value.setOption(
|
||||||
|
{
|
||||||
xAxis: {
|
xAxis: {
|
||||||
data: chartDataXAxisData,
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
axisLabel: {
|
||||||
|
color: document.documentElement.getAttribute('data-theme') === 'dark' ? '#CACADA' : '#333',
|
||||||
},
|
},
|
||||||
series: chartDataYSeriesData,
|
splitLine: {
|
||||||
|
show: true,
|
||||||
|
lineStyle: {
|
||||||
|
color: getSplitLineColor(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: chartData.value.map(item => parseDateToStr(+item.date)),
|
||||||
|
},
|
||||||
|
series: series,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
replaceMerge: ['xAxis', 'series'],
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 更新统计数据
|
||||||
|
updateKpiStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加更新统计数据的函数
|
||||||
|
function updateKpiStats() {
|
||||||
|
kpiStats.value = [];
|
||||||
|
|
||||||
|
// 为每个指标和每个网元创建统计数据
|
||||||
|
for (const columns of tableColumns.value) {
|
||||||
|
if (
|
||||||
|
columns.key === 'neName' ||
|
||||||
|
columns.key === 'startIndex' ||
|
||||||
|
columns.key === 'timeGroup'
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每个网元
|
||||||
|
for (const neId of state.neIds) {
|
||||||
|
// 过滤该网元的数据
|
||||||
|
const neData = tableState.data.filter(item => item._neId === neId);
|
||||||
|
|
||||||
|
if (neData.length === 0) continue;
|
||||||
|
|
||||||
|
const values = neData.map((item: any) => {
|
||||||
|
return item[columns.key] ? Number(item[columns.key]) : 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;
|
||||||
|
|
||||||
|
kpiStats.value.push({
|
||||||
|
kpiId: `${columns.key}_${neId}`,
|
||||||
|
title: `${columns.title}(${neId})`,
|
||||||
|
rawKpiId: columns.key,
|
||||||
|
rawKpiTitle: columns.title,
|
||||||
|
neId: neId,
|
||||||
|
max: values.length > 0 ? Math.max(...values) : 0,
|
||||||
|
min: values.length > 0 ? Math.min(...values) : 0,
|
||||||
|
avg: avg,
|
||||||
|
total: total,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加一个变量来跟踪当前选中的行
|
// 添加一个变量来跟踪当前选中的行
|
||||||
const selectedRow = ref<string[]>([]);
|
const selectedRows = ref<string[]>([]);
|
||||||
|
|
||||||
// 添加处理行点击的方法
|
// 修改行点击处理函数
|
||||||
function handleRowClick(record: any) {
|
function handleRowClick(record: KPIStats) {
|
||||||
const index = selectedRow.value.indexOf(record.kpiId);
|
const index = selectedRows.value.indexOf(record.kpiId);
|
||||||
|
|
||||||
// 如果已经选中,取消选中
|
// 如果已经选中,取消选中
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
selectedRow.value.splice(index, 1);
|
selectedRows.value.splice(index, 1);
|
||||||
chartLegendSelected[record.title] = false;
|
|
||||||
} else {
|
} else {
|
||||||
// 添加新的选中项
|
// 添加新的选中项
|
||||||
selectedRow.value.push(record.kpiId);
|
selectedRows.value.push(record.kpiId);
|
||||||
|
|
||||||
// 如果只有一个选中项,重置为 false
|
// 如果是第一个选中项,重置所有图例为不显示
|
||||||
if (selectedRow.value.length === 1) {
|
if (selectedRows.value.length === 1) {
|
||||||
Object.keys(chartLegendSelected).forEach(key => {
|
Object.keys(chartLegendSelected).forEach(key => {
|
||||||
chartLegendSelected[key] = false;
|
chartLegendSelected[key] = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
chartLegendSelected[record.title] = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有选中项,设置所有图例为 true
|
// 更新图例可见性
|
||||||
if (selectedRow.value.length === 0) {
|
if (selectedRows.value.length === 0) {
|
||||||
|
// 如果没有选中项,显示所有图例
|
||||||
Object.keys(chartLegendSelected).forEach(key => {
|
Object.keys(chartLegendSelected).forEach(key => {
|
||||||
chartLegendSelected[key] = true;
|
chartLegendSelected[key] = true;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// 如果有选中项,只显示选中的图例
|
||||||
|
kpiStats.value.forEach((item: KPIStats) => {
|
||||||
|
const isSelected = selectedRows.value.includes(item.kpiId);
|
||||||
|
chartLegendSelected[item.title] = isSelected;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新图表设置
|
// 更新图表设置
|
||||||
@@ -800,6 +1208,35 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**点击搜索按钮 */
|
||||||
|
function handleSearch() {
|
||||||
|
// 检查是否选择了网元类型和网元ID
|
||||||
|
if (!state.neType) {
|
||||||
|
message.warning({
|
||||||
|
content: t('views.perfManage.goldTarget.selectNeTypeFirst') || '请先选择网元类型',
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.neIds.length === 0) {
|
||||||
|
message.warning({
|
||||||
|
content: t('views.perfManage.goldTarget.selectNeIdsFirst') || '请选择至少一个网元',
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新查询参数
|
||||||
|
queryParams.neType = state.neType;
|
||||||
|
queryParams.neIds = [...state.neIds];
|
||||||
|
queryParams.startTime = queryRangePicker.value[0];
|
||||||
|
queryParams.endTime = queryRangePicker.value[1];
|
||||||
|
|
||||||
|
// 获取指标名,成功后会自动获取指标值
|
||||||
|
fnGetListTitle();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 目前支持的 AMF AUSF MME MOCNGW NSSF SMF UDM UPF PCF
|
// 目前支持的 AMF AUSF MME MOCNGW NSSF SMF UDM UPF PCF
|
||||||
// 获取网元网元列表
|
// 获取网元网元列表
|
||||||
@@ -821,19 +1258,48 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无查询参数neType时 默认选择UPF
|
// 无查询参数neType时 默认选择UPF
|
||||||
const queryNeType = (route.query.neType as string) || 'UPF';
|
const queryNeType = (route.query.neType as string) || 'UPF';
|
||||||
const item = neCascaderOptions.value.find(s => s.value === queryNeType);
|
const item = neCascaderOptions.value.find(s => s.value === queryNeType);
|
||||||
if (item && item.children) {
|
if (item && item.children) {
|
||||||
|
// 设置网元类型
|
||||||
|
state.neType = item.value;
|
||||||
|
queryParams.neType = item.value;
|
||||||
|
|
||||||
|
// 获取该类型下的网元ID列表
|
||||||
|
availableNeIds.value = item.children.map((ne: any) => ({
|
||||||
|
label: `${ne.label}`,
|
||||||
|
value: ne.neId
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 默认选择第一个网元
|
||||||
const info = item.children[0];
|
const info = item.children[0];
|
||||||
state.neType = [info.neType, info.neId];
|
state.neIds = [info.neId];
|
||||||
queryParams.neType = info.neType;
|
queryParams.neIds = [info.neId];
|
||||||
queryParams.neId = info.neId;
|
selectedNes.value = [{
|
||||||
|
neType: item.value,
|
||||||
|
neId: info.neId
|
||||||
|
}];
|
||||||
} else {
|
} else {
|
||||||
const info = neCascaderOptions.value[0].children[0];
|
const item = neCascaderOptions.value[0];
|
||||||
state.neType = [info.neType, info.neId];
|
state.neType = item.value;
|
||||||
queryParams.neType = info.neType;
|
queryParams.neType = item.value;
|
||||||
queryParams.neId = info.neId;
|
|
||||||
|
// 获取该类型下的网元ID列表
|
||||||
|
availableNeIds.value = item.children.map((ne: any) => ({
|
||||||
|
label: `${ne.label}`,
|
||||||
|
value: ne.neId
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 默认选择第一个网元
|
||||||
|
const info = item.children[0];
|
||||||
|
state.neIds = [info.neId];
|
||||||
|
queryParams.neIds = [info.neId];
|
||||||
|
selectedNes.value = [{
|
||||||
|
neType: item.value,
|
||||||
|
neId: info.neId
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询当前小时
|
// 查询当前小时
|
||||||
@@ -847,9 +1313,12 @@ onMounted(() => {
|
|||||||
const endTime = new Date(now);
|
const endTime = new Date(now);
|
||||||
endTime.setMinutes(59, 59, 59);
|
endTime.setMinutes(59, 59, 59);
|
||||||
queryRangePicker.value[1] = `${endTime.getTime()}`;
|
queryRangePicker.value[1] = `${endTime.getTime()}`;
|
||||||
fnGetListTitle();
|
|
||||||
// 绘图
|
// 绘制空图表
|
||||||
fnRanderChart();
|
fnRanderChart();
|
||||||
|
|
||||||
|
// 自动执行一次搜索,获取默认网元的数据
|
||||||
|
handleSearch();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message.warning({
|
message.warning({
|
||||||
@@ -875,17 +1344,35 @@ onBeforeUnmount(() => {
|
|||||||
<!-- 表格搜索栏 -->
|
<!-- 表格搜索栏 -->
|
||||||
<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="4" :md="8" :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
|
<a-select
|
||||||
v-model:value="state.neType"
|
v-model:value="state.neType"
|
||||||
:options="neCascaderOptions"
|
:options="neCascaderOptions"
|
||||||
:allow-clear="false"
|
|
||||||
:placeholder="t('common.selectPlease')"
|
:placeholder="t('common.selectPlease')"
|
||||||
|
@change="handleNeTypeChange"
|
||||||
|
:field-names="{
|
||||||
|
label: 'label',
|
||||||
|
value: 'value'
|
||||||
|
}"
|
||||||
|
:allow-clear="false"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :lg="10" :md="12" :xs="24">
|
<a-col :lg="6" :md="12" :xs="24">
|
||||||
|
<a-form-item name="neIds" :label="t('views.ne.common.neName')">
|
||||||
|
<a-select
|
||||||
|
v-model:value="state.neIds"
|
||||||
|
:options="availableNeIds"
|
||||||
|
:placeholder="t('common.selectPlease')"
|
||||||
|
@change="handleNeIdsChange"
|
||||||
|
mode="multiple"
|
||||||
|
:max-tag-count="2"
|
||||||
|
:allow-clear="false"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :lg="8" :md="12" :xs="24">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
:label="t('views.perfManage.goldTarget.timeFrame')"
|
:label="t('views.perfManage.goldTarget.timeFrame')"
|
||||||
name="timeFrame"
|
name="timeFrame"
|
||||||
@@ -926,7 +1413,7 @@ onBeforeUnmount(() => {
|
|||||||
<a-button
|
<a-button
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="tableState.loading"
|
:loading="tableState.loading"
|
||||||
@click.prevent="fnGetListTitle()"
|
@click.prevent="handleSearch"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
@@ -1085,7 +1572,7 @@ onBeforeUnmount(() => {
|
|||||||
:custom-row="
|
:custom-row="
|
||||||
(record:any) => ({
|
(record:any) => ({
|
||||||
onClick: () => handleRowClick(record),
|
onClick: () => handleRowClick(record),
|
||||||
class: selectedRow.includes(record.kpiId) ? 'selected-row' : '',
|
class: selectedRows.includes(record.kpiId) ? 'selected-row' : '',
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { OptionsType, WS } from '@/plugins/ws-websocket';
|
|||||||
import { generateColorRGBA } from '@/utils/generate-utils';
|
import { generateColorRGBA } from '@/utils/generate-utils';
|
||||||
import { LineOutlined } from '@ant-design/icons-vue';
|
import { LineOutlined } from '@ant-design/icons-vue';
|
||||||
import { TableColumnType } from 'ant-design-vue';
|
import { TableColumnType } from 'ant-design-vue';
|
||||||
|
import useNeInfoStore from '@/store/modules/neinfo';
|
||||||
const { t, currentLocale } = useI18n();
|
const { t, currentLocale } = useI18n();
|
||||||
//定义KPI接口
|
//定义KPI接口
|
||||||
interface KPIBase {
|
interface KPIBase {
|
||||||
@@ -147,6 +148,50 @@ const KPI_TITLE: Record<string, string> = {
|
|||||||
'SCSCF.08': 'MT Call Attempt',
|
'SCSCF.08': 'MT Call Attempt',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 添加网元信息 store
|
||||||
|
const neInfoStore = useNeInfoStore();
|
||||||
|
|
||||||
|
// 添加网元列表相关变量
|
||||||
|
const neList = ref<Record<NeType, {neId: string, neName: string}[]>>({
|
||||||
|
AMF: [],
|
||||||
|
UPF: [],
|
||||||
|
IMS: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加获取网元列表的函数
|
||||||
|
const fetchNeList = async () => {
|
||||||
|
try {
|
||||||
|
const res = await neInfoStore.fnNelist();
|
||||||
|
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||||
|
// 初始化网元列表
|
||||||
|
ALL_NE_TYPES.forEach(type => {
|
||||||
|
neList.value[type] = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 过滤并分类网元
|
||||||
|
res.data.forEach(ne => {
|
||||||
|
const neType = ne.neType as NeType;
|
||||||
|
if (ALL_NE_TYPES.includes(neType)) {
|
||||||
|
neList.value[neType].push({
|
||||||
|
neId: ne.neId,
|
||||||
|
neName: ne.neName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('网元列表获取成功:', neList.value);
|
||||||
|
} else {
|
||||||
|
message.warning({
|
||||||
|
content: t('common.noData'),
|
||||||
|
duration: 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch NE list:', error);
|
||||||
|
message.error(t('common.getInfoFail'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 实时数据开关函数
|
// 实时数据开关函数
|
||||||
const fnRealTimeSwitch = (bool: boolean) => {
|
const fnRealTimeSwitch = (bool: boolean) => {
|
||||||
if (bool) {
|
if (bool) {
|
||||||
@@ -163,15 +208,18 @@ const fnRealTimeSwitch = (bool: boolean) => {
|
|||||||
const options: OptionsType = {
|
const options: OptionsType = {
|
||||||
url: '/ws',
|
url: '/ws',
|
||||||
params: {
|
params: {
|
||||||
subGroupID: ALL_NE_TYPES.map(type => `10_${type}_001`).join(','),
|
// 为所有网元创建订阅
|
||||||
|
subGroupID: ALL_NE_TYPES.flatMap(type =>
|
||||||
|
neList.value[type].map(ne => `10_${type}_${ne.neId}`)
|
||||||
|
).join(','),
|
||||||
},
|
},
|
||||||
onmessage: wsMessage,
|
onmessage: wsMessage,
|
||||||
onerror: wsError,
|
onerror: wsError,
|
||||||
};
|
};
|
||||||
ws.value.connect(options);
|
ws.value.connect(options);
|
||||||
} else if (ws.value) {
|
} else if (ws.value) {
|
||||||
ws.value.close(); //断开链接
|
ws.value.close();
|
||||||
ws.value = null; //清空链接
|
ws.value = null;
|
||||||
tableLoading.value = false;
|
tableLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -181,26 +229,6 @@ const wsError = () => {
|
|||||||
message.error(t('common.websocketError'));
|
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>) => {
|
const wsMessage = (res: Record<string, any>) => {
|
||||||
if (!chart) {
|
if (!chart) {
|
||||||
return;
|
return;
|
||||||
@@ -210,8 +238,32 @@ const wsMessage = (res: Record<string, any>) => {
|
|||||||
tableLoading.value = false;
|
tableLoading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleWebSocketMessage(data.data);
|
|
||||||
|
// 解析订阅组ID获取网元类型和ID
|
||||||
|
const [_, neType, neId] = data.groupId.split('_');
|
||||||
|
if (!neType || !neId) return;
|
||||||
|
|
||||||
|
const kpiEvent = data.data;
|
||||||
|
if (!kpiEvent) return;
|
||||||
|
|
||||||
|
// 构造新的数据点
|
||||||
|
const newData: ChartDataItem = {
|
||||||
|
date: kpiEvent.timeGroup?.toString() || Date.now().toString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 为每个网元的每个指标添加数据
|
||||||
|
for (const kpiId of TARGET_KPI_IDS[neType as NeType] || []) {
|
||||||
|
const key = `${kpiId}_${neId}`;
|
||||||
|
newData[key] = Number(kpiEvent[kpiId]) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
updateChartData(newData);
|
||||||
|
if (tableLoading.value) {
|
||||||
|
tableLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 添加数据处理函数
|
// 添加数据处理函数
|
||||||
const processChartData = (rawData: any[]) => {
|
const processChartData = (rawData: any[]) => {
|
||||||
const groupedData = new Map<string, any>(); //数据按时间分组
|
const groupedData = new Map<string, any>(); //数据按时间分组
|
||||||
@@ -252,10 +304,12 @@ const fetchChartData = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建并行请求数组
|
// 创建并行请求数组
|
||||||
const requests = ALL_NE_TYPES.map(async neType => {
|
const requests = [];
|
||||||
|
for (const neType of ALL_NE_TYPES) {
|
||||||
|
for (const ne of neList.value[neType]) {
|
||||||
const params = {
|
const params = {
|
||||||
neType,
|
neType,
|
||||||
neId: '001',
|
neId: ne.neId,
|
||||||
startTime: String(startTime),
|
startTime: String(startTime),
|
||||||
endTime: String(endTime),
|
endTime: String(endTime),
|
||||||
sortField: 'timeGroup',
|
sortField: 'timeGroup',
|
||||||
@@ -264,23 +318,52 @@ const fetchChartData = async () => {
|
|||||||
kpiIds: TARGET_KPI_IDS[neType].join(','),
|
kpiIds: TARGET_KPI_IDS[neType].join(','),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
requests.push(
|
||||||
const res = await listKPIData(params);
|
listKPIData(params).then(res => {
|
||||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||||
return res.data;
|
return res.data.map(item => ({
|
||||||
|
...item,
|
||||||
|
_neId: ne.neId,
|
||||||
|
neName: `${neType}-${ne.neId}`
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
} catch (error) {
|
})
|
||||||
console.error(`Failed to fetch data for ${neType}:`, error);
|
);
|
||||||
return [];
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// 并行执行所有请求
|
// 并行执行所有请求
|
||||||
const results = await Promise.all(requests);
|
const results = await Promise.all(requests);
|
||||||
const allData = results.flat();
|
const allData = results.flat();
|
||||||
|
|
||||||
chartData.value = processChartData(allData);
|
// 按时间分组处理数据
|
||||||
|
const groupedData = new Map<string, any>();
|
||||||
|
allData.forEach(item => {
|
||||||
|
const timeKey = item.timeGroup;
|
||||||
|
if (!groupedData.has(timeKey)) {
|
||||||
|
groupedData.set(timeKey, { timeGroup: timeKey });
|
||||||
|
}
|
||||||
|
Object.assign(groupedData.get(timeKey), item);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 转换为图表数据格式
|
||||||
|
chartData.value = Array.from(groupedData.values())
|
||||||
|
.sort((a, b) => Number(a.timeGroup) - Number(b.timeGroup))
|
||||||
|
.map(item => {
|
||||||
|
const dataItem: ChartDataItem = { date: item.timeGroup.toString() };
|
||||||
|
// 为每个网元的每个指标添加数据
|
||||||
|
for (const neType of ALL_NE_TYPES) {
|
||||||
|
for (const ne of neList.value[neType]) {
|
||||||
|
for (const kpiId of TARGET_KPI_IDS[neType]) {
|
||||||
|
const key = `${kpiId}_${ne.neId}`;
|
||||||
|
dataItem[key] = Number(item[kpiId]) || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataItem;
|
||||||
|
});
|
||||||
|
|
||||||
updateChart();
|
updateChart();
|
||||||
updateKpiStats();
|
updateKpiStats();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -436,26 +519,31 @@ const themeObserver = new MutationObserver(() => {
|
|||||||
|
|
||||||
const updateChart = () => {
|
const updateChart = () => {
|
||||||
if (!chart || !kpiColumns.value.length) return;
|
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 {
|
const series = [];
|
||||||
name: kpi.title,
|
for (const neType of ALL_NE_TYPES) {
|
||||||
|
for (const ne of neList.value[neType]) {
|
||||||
|
for (const kpiId of TARGET_KPI_IDS[neType]) {
|
||||||
|
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||||
|
if (!kpi) continue;
|
||||||
|
|
||||||
|
const key = `${kpiId}_${ne.neId}`;
|
||||||
|
const color = kpiColors.get(key) || generateColorRGBA();
|
||||||
|
kpiColors.set(key, color);
|
||||||
|
|
||||||
|
series.push({
|
||||||
|
name: `${kpi.title}(${ne.neId})`,
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: chartData.value.map(item => item[kpiId] || 0),
|
data: chartData.value.map(item => item[key] || 0),
|
||||||
itemStyle: { color },
|
itemStyle: { color },
|
||||||
...commonConfig,
|
symbol: 'none',
|
||||||
};
|
symbolSize: 6,
|
||||||
})
|
smooth: 0.6,
|
||||||
.filter(Boolean);
|
showSymbol: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const option = {
|
const option = {
|
||||||
title: {
|
title: {
|
||||||
@@ -463,36 +551,29 @@ const updateChart = () => {
|
|||||||
left: 'center',
|
left: 'center',
|
||||||
// 添加文字颜色配置,根据主题切换
|
// 添加文字颜色配置,根据主题切换
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color:
|
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
|
||||||
? '#CACADA'
|
? '#CACADA'
|
||||||
: '#333',
|
: '#333',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
trigger: 'axis',
|
trigger: 'axis',
|
||||||
confine: true, // 限制 tooltip 显示范围
|
confine: true,
|
||||||
backgroundColor:
|
backgroundColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
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:
|
borderColor: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
|
||||||
? '#555'
|
? '#555'
|
||||||
: '#ddd',
|
: '#ddd',
|
||||||
textStyle: {
|
textStyle: {
|
||||||
color:
|
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
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(
|
data: series.map(s => s.name),
|
||||||
kpiId =>
|
|
||||||
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId
|
|
||||||
),
|
|
||||||
type: 'scroll',
|
type: 'scroll',
|
||||||
orient: 'horizontal',
|
orient: 'horizontal',
|
||||||
top: 25,
|
top: 25,
|
||||||
@@ -500,11 +581,9 @@ const updateChart = () => {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
selected: Object.fromEntries(
|
selected: Object.fromEntries(
|
||||||
selectedKPIs.value.map(kpiId => [
|
kpiStats.value.map(item => [
|
||||||
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
|
item.title,
|
||||||
selectedRows.value.length === 0
|
selectedRows.value.length === 0 || selectedRows.value.includes(item.kpiId)
|
||||||
? true
|
|
||||||
: selectedRows.value.includes(kpiId),
|
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
show: false,
|
show: false,
|
||||||
@@ -514,17 +593,20 @@ const updateChart = () => {
|
|||||||
padding: [5, 10],
|
padding: [5, 10],
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
//网格配置
|
left: '3%',
|
||||||
left: '6%',
|
right: '4%',
|
||||||
right: '6%',
|
|
||||||
bottom: '3%',
|
bottom: '3%',
|
||||||
containLabel: true,
|
containLabel: true,
|
||||||
},
|
},
|
||||||
xAxis: {
|
xAxis: {
|
||||||
// 指定x轴类型为类目轴,适用于离散的类目数据
|
// 指定x轴类型为类目轴,适用于离散的类目数据
|
||||||
type: 'category',
|
type: 'category',
|
||||||
|
boundaryGap: false,
|
||||||
|
data: chartData.value.map(item =>
|
||||||
|
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
),
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
formatter: '{value}',
|
// formatter: '{value}',
|
||||||
color:
|
color:
|
||||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
? '#CACADA'
|
? '#CACADA'
|
||||||
@@ -536,47 +618,12 @@ const updateChart = () => {
|
|||||||
color: getSplitLineColor(),
|
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: {
|
yAxis: {
|
||||||
// y轴配置
|
|
||||||
type: 'value',
|
type: 'value',
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
formatter: '{value}',
|
formatter: '{value}',
|
||||||
color:
|
color: document.documentElement.getAttribute('data-theme') === 'dark'
|
||||||
document.documentElement.getAttribute('data-theme') === 'dark'
|
|
||||||
? '#CACADA'
|
? '#CACADA'
|
||||||
: '#333',
|
: '#333',
|
||||||
},
|
},
|
||||||
@@ -595,32 +642,19 @@ const updateChart = () => {
|
|||||||
};
|
};
|
||||||
if (chart) {
|
if (chart) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
chart!.setOption(option, true); //使用新的配置更新图表
|
if (chart) { // 添加额外的空值检查
|
||||||
chart!.resize(); //调整图表大小适应容器
|
chart.setOption(option, true);
|
||||||
});
|
|
||||||
}
|
|
||||||
// 如果已经有 observer,先断开连接
|
|
||||||
if (observer) {
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建新的 ResizeObserver
|
|
||||||
observer = new ResizeObserver(() => {
|
|
||||||
if (chart) {
|
|
||||||
chart.resize();
|
chart.resize();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 观察图表容器
|
|
||||||
const container = document.getElementById('chartContainer');
|
|
||||||
if (container) {
|
|
||||||
observer.observe(container);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//钩子函数
|
//钩子函数
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
|
// 获取网元列表
|
||||||
|
await fetchNeList();
|
||||||
// 获取所有网元的指标
|
// 获取所有网元的指标
|
||||||
await fetchSpecificKPI();
|
await fetchSpecificKPI();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
@@ -635,6 +669,15 @@ onMounted(async () => {
|
|||||||
} else {
|
} else {
|
||||||
console.warn('No KPI columns available after fetching');
|
console.warn('No KPI columns available after fetching');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建 ResizeObserver 实例监听图表容器大小变化
|
||||||
|
observer = new ResizeObserver(() => {
|
||||||
|
if (chart) {
|
||||||
|
chart.resize();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 监听元素大小变化
|
||||||
|
observer.observe(container);
|
||||||
} else if (chart) {
|
} else if (chart) {
|
||||||
console.warn('Chart already initialized, skipping initialization');
|
console.warn('Chart already initialized, skipping initialization');
|
||||||
} else {
|
} else {
|
||||||
@@ -672,7 +715,10 @@ const fetchSpecificKPI = async () => {
|
|||||||
|
|
||||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||||
const titleIDs = Object.values(TARGET_KPI_IDS).flat();
|
const titleIDs = Object.values(TARGET_KPI_IDS).flat();
|
||||||
const formattedKPIs = res.data.map(item => {
|
// 只获取 TARGET_KPI_IDS 中定义的指标
|
||||||
|
const filteredKPIs = res.data
|
||||||
|
.filter(item => TARGET_KPI_IDS[neType].includes(item.kpiId))
|
||||||
|
.map(item => {
|
||||||
let title = item[`${language}Title`];
|
let title = item[`${language}Title`];
|
||||||
if (titleIDs.includes(item.kpiId)) {
|
if (titleIDs.includes(item.kpiId)) {
|
||||||
title = KPI_TITLE[item.kpiId] || title;
|
title = KPI_TITLE[item.kpiId] || title;
|
||||||
@@ -683,10 +729,10 @@ const fetchSpecificKPI = async () => {
|
|||||||
key: item.kpiId,
|
key: item.kpiId,
|
||||||
kpiId: item.kpiId,
|
kpiId: item.kpiId,
|
||||||
neType: neType,
|
neType: neType,
|
||||||
};
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
allKPIs = [...allKPIs, ...formattedKPIs];
|
allKPIs = [...allKPIs, ...filteredKPIs];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,14 +791,19 @@ const updateChartData = (newData: ChartDataItem) => {
|
|||||||
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
series: selectedKPIs.value.map(kpiId => {
|
series: ALL_NE_TYPES.flatMap(type =>
|
||||||
|
neList.value[type].map(ne =>
|
||||||
|
TARGET_KPI_IDS[type].map(kpiId => {
|
||||||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||||
|
const key = `${kpiId}_${ne.neId}`;
|
||||||
return {
|
return {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: chartData.value.map(item => item[kpiId] || 0),
|
data: chartData.value.map(item => item[key] || 0),
|
||||||
name: kpi?.title || kpiId,
|
name: `${kpi?.title || kpiId}(${ne.neId})`,
|
||||||
};
|
};
|
||||||
}),
|
})
|
||||||
|
)
|
||||||
|
).flat(),
|
||||||
};
|
};
|
||||||
chart.setOption(option);
|
chart.setOption(option);
|
||||||
});
|
});
|
||||||
@@ -778,52 +829,39 @@ const kpiStats = ref<KPIStats[]>([]);
|
|||||||
// 添加一个计算函数来更新统计数据
|
// 添加一个计算函数来更新统计数据
|
||||||
const updateKpiStats = () => {
|
const updateKpiStats = () => {
|
||||||
if (!chartData.value.length || !kpiColumns.value.length) {
|
if (!chartData.value.length || !kpiColumns.value.length) {
|
||||||
kpiStats.value = selectedKPIs.value.map(kpiId => {
|
kpiStats.value = [];
|
||||||
// 找到对应的KPI标题
|
|
||||||
let title = kpiId;
|
|
||||||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
|
||||||
if (kpi) {
|
|
||||||
title = kpi.title;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
kpiId: kpiId,
|
|
||||||
title: title,
|
|
||||||
max: 0,
|
|
||||||
min: 0,
|
|
||||||
avg: 0,
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
kpiStats.value = selectedKPIs.value
|
|
||||||
.map(kpiId => {
|
kpiStats.value = [];
|
||||||
// 找到对应的KPI标题
|
for (const neType of ALL_NE_TYPES) {
|
||||||
|
for (const ne of neList.value[neType]) {
|
||||||
|
for (const kpiId of TARGET_KPI_IDS[neType]) {
|
||||||
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
const kpi = kpiColumns.value.find(col => col.kpiId === kpiId);
|
||||||
if (!kpi) return null;
|
if (!kpi) continue;
|
||||||
|
|
||||||
// 获取该指标的所有数值
|
const key = `${kpiId}_${ne.neId}`;
|
||||||
const values = chartData.value.map(item => Number(item[kpiId]) || 0);
|
const values = chartData.value.map(item => Number(item[key]) || 0);
|
||||||
|
|
||||||
// 计算总值
|
if (values.length === 0) continue;
|
||||||
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 =
|
const avg = Number((total / values.length).toFixed(2));
|
||||||
values.length > 0 ? Number((total / values.length).toFixed(2)) : 0;
|
|
||||||
|
|
||||||
return {
|
kpiStats.value.push({
|
||||||
kpiId: kpiId,
|
kpiId: key,
|
||||||
title: kpi.title,
|
title: `${kpi.title}(${ne.neId})`,
|
||||||
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);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新图表显示
|
||||||
|
updateChartLegendSelect();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加表列定义
|
// 添加表列定义
|
||||||
@@ -903,11 +941,9 @@ const updateChartLegendSelect = () => {
|
|||||||
if (!chart) return;
|
if (!chart) return;
|
||||||
|
|
||||||
const legendSelected = Object.fromEntries(
|
const legendSelected = Object.fromEntries(
|
||||||
selectedKPIs.value.map(kpiId => [
|
kpiStats.value.map(item => [
|
||||||
kpiColumns.value.find(col => col.kpiId === kpiId)?.title || kpiId,
|
item.title,
|
||||||
selectedRows.value.length === 0
|
selectedRows.value.length === 0 || selectedRows.value.includes(item.kpiId)
|
||||||
? true
|
|
||||||
: selectedRows.value.includes(kpiId),
|
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1037,6 +1073,9 @@ const tableRowConfig = computed(() => {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
height: calc(100vh - 200px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme='light'] .chart-wrapper {
|
[data-theme='light'] .chart-wrapper {
|
||||||
@@ -1048,8 +1087,10 @@ const tableRowConfig = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chart-container {
|
.chart-container {
|
||||||
height: 400px;
|
height: 450px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 400px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-container {
|
.table-container {
|
||||||
|
|||||||
Reference in New Issue
Block a user