Files
fe.ems.vue3/src/views/perfManage/goldTarget/index.vue

1800 lines
48 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import * as echarts from 'echarts/core';
import {
TooltipComponent,
TooltipComponentOption,
GridComponent,
GridComponentOption,
LegendComponent,
LegendComponentOption,
DataZoomComponent,
DataZoomComponentOption,
} from 'echarts/components';
import { LineChart, LineSeriesOption } from 'echarts/charts';
import { UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import {
reactive,
ref,
onMounted,
toRaw,
markRaw,
nextTick,
onBeforeUnmount,
h,
watch,
} from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message, Modal, TableColumnType } from 'ant-design-vue/es';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import TableColumnsDnd from '@/components/TableColumnsDnd/index.vue';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import useNeInfoStore from '@/store/modules/neinfo';
import useI18n from '@/hooks/useI18n';
import { getKPITitle, listKPIData } from '@/api/perfManage/goldTarget';
import { parseDateToStr } from '@/utils/date-utils';
import { writeSheet } from '@/utils/execl-utils';
import saveAs from 'file-saver';
import { generateColorRGBA } from '@/utils/generate-utils';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import { useRoute } from 'vue-router';
import { LineOutlined } from '@ant-design/icons-vue';
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 neInfoStore = useNeInfoStore();
const route = useRoute();
const { t, currentLocale } = useI18n();
const ws = new WS();
echarts.use([
TooltipComponent,
GridComponent,
LegendComponent,
DataZoomComponent,
LineChart,
CanvasRenderer,
UniversalTransition,
]);
type EChartsOption = echarts.ComposeOption<
| TooltipComponentOption
| GridComponentOption
| LegendComponentOption
| DataZoomComponentOption
| LineSeriesOption
>;
/**图DOM节点实例对象 */
const kpiChartDom = ref<HTMLElement | undefined>(undefined);
/**图实例对象 */
const kpiChart = ref<any>(null);
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**记录开始结束时间 */
let queryRangePicker = ref<[string, string]>(['', '']);
//日期快捷选择
const ranges = ref([
{
label: t('views.perfManage.customTarget.toDay'),
value: [dayjs().startOf('day'), dayjs()],
},
{
label: t('views.perfManage.customTarget.ago1Hour'),
value: [
dayjs().subtract(1, 'hour').startOf('hour'),
dayjs().subtract(1, 'hour').endOf('hour'),
],
},
// {
// label: t('views.perfManage.customTarget.ago3Hour'),
// value: [dayjs().subtract(3, 'hours'), dayjs()],
// },
// {
// label: t('views.perfManage.customTarget.ago6Hour'),
// value: [dayjs().subtract(6, 'hours'), dayjs()],
// },
{
label: t('views.perfManage.customTarget.ago1Day'),
value: [
dayjs().subtract(1, 'day').startOf('day'),
dayjs().subtract(1, 'day').endOf('day'),
],
},
{
label: t('views.perfManage.customTarget.ago7Day'),
value: [
dayjs().subtract(7, 'day').startOf('day'),
dayjs().subtract(1, 'day').endOf('day'),
],
},
{
label: t('views.perfManage.customTarget.ago15Day'),
value: [
dayjs().subtract(15, 'day').startOf('day'),
dayjs().subtract(1, 'day').endOf('day'),
],
},
]);
/**表格字段列 */
let tableColumns = ref<any[]>([]);
/**表格字段列排序 */
let tableColumnsDnd = ref<any[]>([]);
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => t('common.tablePaginationTotal', { total }),
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
},
});
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: Record<string, any>[];
/**显示表格 */
showTable: boolean;
};
/**表格状态 */
let tableState: TabeStateType = reactive({
tableColumns: [],
loading: false,
size: 'middle',
seached: true,
data: [],
showTable: false,
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**选中的网元信息 */
let selectedNes = ref<{ neType: string; neId: string }[]>([]);
/**查询参数 */
let queryParams: any = reactive({
/**网元类型 */
neType: '',
/**网元标识列表 */
neIds: [] as string[],
/**颗粒度 */
interval: 900,
/**开始时间 */
startTime: '',
/**结束时间 */
endTime: '',
/**排序字段 */
sortField: 'timeGroup',
/**排序方式 */
sortOrder: 'desc',
});
/**表格分页、排序、筛选变化时触发操作, 排序方式,取值为 ascend descend */
function fnTableChange(pagination: any, filters: any, sorter: any, extra: any) {
const { columnKey, order } = sorter;
if (!order) return;
if (order.startsWith(queryParams.sortOrder)) return;
if (order) {
queryParams.sortField = columnKey;
queryParams.sortOrder = order.replace('end', '');
} else {
queryParams.sortOrder = 'asc';
}
fnGetList();
}
/**对象信息状态类型 */
type StateType = {
/**网元类型 */
neType: string;
/**选中的网元ID列表 */
neIds: string[];
/**图表实时统计 */
chartRealTime: boolean;
/**图表标签选择 */
chartLegendSelectedFlag: boolean;
};
/**对象信息状态 */
let state: StateType = reactive({
neType: '',
neIds: [],
chartRealTime: false,
chartLegendSelectedFlag: true,
});
// 存储每个指标的临时固定颜色
const kpiColors = new Map<string, string>();
//legend表格数据
const kpiStats: any = ref([]);
// 添加表格列定义
const statsColumns: TableColumnType<any>[] = [
{
title: '',
key: 'icon',
width: 50,
customRender: ({ record }: { record: any }) => {
return h(LineOutlined, {
style: {
color: kpiColors.get(record.kpiId) || '#000', // 使用与折线图相同的颜色
fontSize: '30px', // 增大图标尺寸到30px
fontWeight: 'bold', // 加粗
},
});
},
},
{
title: t('views.perfManage.kpiOverView.kpiName'),
dataIndex: 'title',
key: 'title',
width: '65%',
},
{
title: t('views.perfManage.kpiOverView.totalValue'),
dataIndex: 'total',
key: 'total',
width: '24%',
sortDirections: ['ascend', 'descend'],
},
{
title: t('views.perfManage.kpiOverView.avgValue'),
dataIndex: 'avg',
key: 'avg',
width: '24%',
sortDirections: ['ascend', 'descend'],
},
{
title: t('views.perfManage.kpiOverView.maxValue'),
dataIndex: 'max',
key: 'max',
width: '17%',
sortDirections: ['ascend', 'descend'],
},
{
title: t('views.perfManage.kpiOverView.minValue'),
dataIndex: 'min',
key: 'min',
width: '17%',
sortDirections: ['ascend', 'descend'],
},
];
/**
* 数据列表导出
*/
function fnRecordExport() {
Modal.confirm({
title: 'Tip',
content: t('views.perfManage.goldTarget.exportSure'),
onOk() {
const key = 'exportKPI';
message.loading({ content: t('common.loading'), key });
if (tableState.data.length <= 0) {
message.error({
content: t('views.perfManage.goldTarget.exportEmpty'),
key,
duration: 2,
});
return;
}
const tableColumnsTitleArr: string[] = [];
const tableColumnsKeyArr: string[] = [];
for (const columns of tableColumnsDnd.value) {
tableColumnsTitleArr.push(`${columns.title}`);
tableColumnsKeyArr.push(`${columns.key}`);
}
const kpiDataArr = [];
for (const item of tableState.data) {
const kpiData: Record<string, any> = {};
const keys = Object.keys(item);
for (let i = 0; i <= tableColumnsKeyArr.length; i++) {
for (const key of keys) {
if (tableColumnsKeyArr[i] === key) {
const title = tableColumnsTitleArr[i];
if (key === 'timeGroup') {
kpiData[title] = parseDateToStr(item[key]);
} else {
kpiData[title] = item[key];
}
}
}
}
kpiDataArr.push(kpiData);
}
writeSheet(kpiDataArr, 'KPI', { header: tableColumnsTitleArr })
.then(fileBlob => saveAs(fileBlob, `kpi_data_${Date.now()}.xlsx`))
.finally(() => {
message.success({
content: t('common.msgSuccess', { msg: t('common.export') }),
key,
duration: 2,
});
});
},
});
}
/**可选网元列表 */
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() {
// 当前语言
var language = currentLocale.value.split('_')[0];
if (language === 'zh') language = 'cn';
// 获取表头文字,只传递纯网元类型
getKPITitle(state.neType)
.then(res => {
//处理getKPITitle返回的结果
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
//检查值
tableColumns.value = []; //设为空数组
const columns: any[] = []; //初始化,构建新表头
for (const item of res.data) {
//遍历res.data
const kpiDisplay = item[`${language}Title`]; //提取标题kpiDisplay和ID标识kpiValue
const kpiValue = item[`kpiId`];
columns.push({
//
title: kpiDisplay,
dataIndex: kpiValue,
align: 'left',
key: kpiValue,
resizable: true,
width: 100,
minWidth: 150,
maxWidth: 300,
});
}
columns.push({
title: t('views.perfManage.perfData.neName'),
dataIndex: 'neName',
key: 'neName',
align: 'left',
width: 100,
});
columns.push({
title: t('views.perfManage.goldTarget.time'),
dataIndex: 'timeGroup',
align: 'left',
fixed: 'right',
key: 'timeGroup',
sorter: true,
width: 100,
});
nextTick(() => {
tableColumns.value = columns;
});
return true;
} else {
message.warning({
content: t('common.getInfoFail'),
duration: 2,
});
return false;
}
})
.then(result => {
//result是前一个.then返回的值(true or false)
result && fnGetList();
});
}
/**查询数据列表 */
async function fnGetList() {
if (tableState.loading) return;
tableState.loading = 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 [];
});
});
const results = await Promise.all(promises);
allNeData = results.flat();
tablePagination.total = allNeData.length;
tableState.data = allNeData;
// 调用图表渲染函数
fnRanderChartData();
// 封装legend表格数据
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}`, // 使用组合ID来唯一标识指标-网元对
title: `${columns.title}(${neId})`, // 在标题中显示网元ID
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,
});
}
}
return true;
} catch (error) {
console.error('Error fetching data:', error);
message.error({
content: t('common.getInfoFail'),
duration: 2,
});
return false;
} finally {
tableState.loading = false;
}
}
/**切换显示类型 图或表格 */
function fnChangShowType() {
tableState.showTable = !tableState.showTable;
}
/**绘制图表 */
function fnRanderChart() {
const container: HTMLElement | undefined = kpiChartDom.value; //获取图表容器DOM元素
if (!container) return; //若没有,则退出函数
kpiChart.value = markRaw(echarts.init(container, 'light'));
//初始化Echarts图表实例应用light主题并赋值给kpiChart.valuemarkRaw是vue函数用于标记对象为不可响应
const option: any = {
//定义图表的配置对象tooltip的出发方式为axis
tooltip: {
trigger: 'axis',
position: function (pt: any) {
return [pt[0], '10%'];
},
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: {
//x类别轴
type: 'category',
boundaryGap: false,
data: [], // 数据x轴
axisLabel: {
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
splitLine: {
show: true,
lineStyle: {
color: getSplitLineColor(),
},
},
},
yAxis: {
//y类别轴
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: [], // 数据y轴
};
kpiChart.value.setOption(option); //设置图表配置项应用到kpiChart实例上
// 创建 ResizeObserver 实例 监听图表容器大小变化,并在变化时调整图表大小
var observer = new ResizeObserver(entries => {
if (kpiChart.value) {
kpiChart.value.resize();
}
});
// 监听元素大小变化
observer.observe(container);
}
/**图表标签选择 */
let chartLegendSelected: Record<string, boolean> = {};
/**图表配置数据x轴 */
let chartDataXAxisData: string[] = [];
/**图表配置数据y轴 */
let chartDataYSeriesData: Record<string, any>[] = [];
/**图表数据渲染 */
function fnRanderChartData() {
if (kpiChart.value == null || tableState.data.length <= 0) {
return;
}
// 重置
chartLegendSelected = {};
chartDataXAxisData = [];
chartDataYSeriesData = [];
// 为每个网元的每个指标创建一条线
for (const neId of state.neIds) {
for (const columns of tableColumns.value) {
if (
columns.key === 'neName' ||
columns.key === 'startIndex' ||
columns.key === 'timeGroup'
) {
continue;
}
const kpiId = `${columns.key}_${neId}`;
const seriesName = `${columns.title}(${neId})`;
// 获取或生成颜色
const color = kpiColors.get(kpiId) || generateColorRGBA();
kpiColors.set(kpiId, color);
const seriesData = {
name: seriesName,
key: kpiId,
type: 'line',
symbol: 'none',
symbolSize: 6,
smooth: 0.6,
showSymbol: true,
sampling: 'lttb',
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)'),
},
]),
},
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;
});
}
// 如果有选中的行,只显示选中的指标
if (selectedRows.value.length > 0) {
Object.keys(chartLegendSelected).forEach(key => {
chartLegendSelected[key] = false;
});
kpiStats.value.forEach((item: KPIStats) => {
if (selectedRows.value.includes(item.kpiId)) {
chartLegendSelected[item.title] = true;
}
});
}
// 更新图表
kpiChart.value.setOption(
{
legend: {
selected: chartLegendSelected,
},
xAxis: {
type: 'category',
boundaryGap: false,
axisLabel: {
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
splitLine: {
show: true,
lineStyle: {
color: getSplitLineColor(),
},
},
data: chartDataXAxisData,
},
series: chartDataYSeriesData,
},
{
replaceMerge: ['xAxis', 'series'],
}
);
}
/**图表折线显示全部 */
function fnLegendSelected(bool: any) {
// 清空选中状态
selectedRows.value = [];
// 更新所有图例的显示状态
Object.keys(chartLegendSelected).forEach(key => {
chartLegendSelected[key] = bool;
});
// 更新图表设置
kpiChart.value.setOption({
legend: {
selected: chartLegendSelected,
},
});
}
/**图表实时统计 */
function fnRealTimeSwitch(bool: any) {
if (bool) {
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 = {
url: '/ws',
params: {
/**订阅通道组
* 指标(GroupID:10_neType_neId)
* 为所有选中的网元创建订阅
*/
subGroupID: state.neIds
.map(neId => `10_${state.neType}_${neId}`)
.join(','),
},
onmessage: wsMessage,
onerror: wsError,
};
ws.connect(options);
} else {
tableState.seached = true;
ws.close();
}
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
// 添加 ChartDataItem 接口
interface ChartDataItem {
date: string; // 存储完整的时间字符串
[kpiId: string]: string | number; // 动态指标
}
// 添加图表数据响应式数组
const chartData = ref<ChartDataItem[]>([]);
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
// 解析订阅组ID获取网元类型和ID
const [_, neType, neId] = data.groupId.split('_');
if (!neType || !neId) return;
// kpiEvent 黄金指标指标事件
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);
tablePagination.total++;
// 非对应网元类型
if (neType !== queryParams.neType) return;
// 限制数据量
if (tableState.data.length > 100) {
tableState.data.pop();
}
// 更新图表数据
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;
}
const kpiId = `${columns.key}_${neId}`;
const seriesName = `${columns.title}(${neId})`;
// 获取或生成颜色
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(
{
xAxis: {
type: 'category',
boundaryGap: false,
axisLabel: {
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
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 selectedRows = ref<string[]>([]);
// 修改行点击处理函数
function handleRowClick(record: KPIStats) {
const index = selectedRows.value.indexOf(record.kpiId);
// 如果已经选中,取消选中
if (index > -1) {
selectedRows.value.splice(index, 1);
} else {
// 添加新的选中项
selectedRows.value.push(record.kpiId);
// 如果是第一个选中项,重置所有图例为不显示
if (selectedRows.value.length === 1) {
Object.keys(chartLegendSelected).forEach(key => {
chartLegendSelected[key] = false;
});
}
}
// 更新图例可见性
if (selectedRows.value.length === 0) {
// 如果没有选中项,显示所有图例
Object.keys(chartLegendSelected).forEach(key => {
chartLegendSelected[key] = true;
});
} else {
// 如果有选中项,只显示选中的图例
kpiStats.value.forEach((item: KPIStats) => {
const isSelected = selectedRows.value.includes(item.kpiId);
chartLegendSelected[item.title] = isSelected;
});
}
// 更新图表设置
kpiChart.value.setOption({
legend: {
selected: chartLegendSelected,
},
});
}
// 添加一个函数来获取当前主题下的网格线颜色
function getSplitLineColor() {
return document.documentElement.getAttribute('data-theme') === 'dark'
? '#333333'
: '#E8E8E8'; // 亮色模式返回 undefined使用默认颜色
}
// 监听主题变化
watch(
() => layoutStore.proConfig.theme, // 监听的值
newValue => {
if (kpiChart.value) {
const splitLineColor = getSplitLineColor();
// 绘制图数据
kpiChart.value.setOption({
tooltip: {
trigger: 'axis',
position: function (pt: any) {
return [pt[0], '10%'];
},
confine: true, // 限制 tooltip 显示范围
backgroundColor:
newValue === 'dark'
? 'rgba(48, 48, 48, 0.8)'
: 'rgba(255, 255, 255, 0.9)',
borderColor: newValue === 'dark' ? '#555' : '#ddd',
textStyle: {
color: newValue === 'dark' ? '#CACADA' : '#333',
},
},
xAxis: {
axisLabel: {
color: newValue === 'dark' ? '#CACADA' : '#333',
},
splitLine: {
show: true,
lineStyle: {
color: splitLineColor,
},
},
},
yAxis: {
axisLabel: {
color: newValue === 'dark' ? '#CACADA' : '#333',
},
splitLine: {
show: true,
lineStyle: {
color: splitLineColor,
},
},
},
});
}
}
);
/**点击搜索按钮 */
function handleSearch() {
// 检查是否选择了网元类型和网元ID
if (!state.neType) {
message.warning({
content: 'Please select the network element type first',
duration: 2,
});
return;
}
if (state.neIds.length === 0) {
message.warning({
content: 'Please select at least one network element',
duration: 2,
});
return;
}
// 更新查询参数
queryParams.neType = state.neType;
queryParams.neIds = [...state.neIds];
queryParams.startTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
// 获取指标名,成功后会自动获取指标值
fnGetListTitle();
}
onMounted(() => {
// 目前支持的 AMF AUSF MME MOCNGW NSSF SMF UDM UPF PCF
// 获取网元网元列表
neInfoStore.fnNelist().then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
// 过滤不可用的网元
neCascaderOptions.value = neInfoStore.getNeCascaderOptions.filter(
(item: any) => {
return !['OMC', 'NSSF', 'NEF', 'NRF', 'LMF', 'N3IWF'].includes(
item.value
);
}
);
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
// 无查询参数neType时 默认选择UPF
const queryNeType = (route.query.neType as string) || 'UPF';
const item = neCascaderOptions.value.find(s => s.value === queryNeType);
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];
state.neIds = [info.neId];
queryParams.neIds = [info.neId];
selectedNes.value = [
{
neType: item.value,
neId: info.neId,
},
];
} else {
const item = neCascaderOptions.value[0];
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];
state.neIds = [info.neId];
queryParams.neIds = [info.neId];
selectedNes.value = [
{
neType: item.value,
neId: info.neId,
},
];
}
// 查询当前小时
const now = new Date();
now.setMinutes(0, 0, 0);
// 设置起始时间为整点前一小时
const startTime = new Date(now);
startTime.setHours(now.getHours() - 1);
queryRangePicker.value[0] = `${startTime.getTime()}`;
// 设置结束时间为整点
const endTime = new Date(now);
endTime.setMinutes(59, 59, 59);
queryRangePicker.value[1] = `${endTime.getTime()}`;
// 绘制空图表
fnRanderChart();
// 自动执行一次搜索,获取默认网元的数据
handleSearch();
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
});
});
onBeforeUnmount(() => {
ws.close();
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParamsFrom" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="4" :md="8" :xs="24">
<a-form-item name="neType" :label="t('views.ne.common.neType')">
<a-select
v-model:value="state.neType"
:options="neCascaderOptions"
:placeholder="t('common.selectPlease')"
@change="handleNeTypeChange"
:field-names="{
label: 'label',
value: 'value',
}"
:allow-clear="false"
/>
</a-form-item>
</a-col>
<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
:label="t('views.perfManage.goldTarget.timeFrame')"
name="timeFrame"
>
<a-range-picker
v-model:value="queryRangePicker"
bordered
:presets="ranges"
:allow-clear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
value-format="x"
style="width: 100%"
></a-range-picker>
</a-form-item>
</a-col>
<a-col :lg="4" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.goldTarget.interval')"
name="interval"
>
<a-select
v-model:value="queryParams.interval"
:placeholder="t('common.selectPlease')"
:options="[
{ label: '5S', value: 5 },
{ label: '1M', value: 60 },
{ label: '5M', value: 300 },
{ label: '15M', value: 900 },
{ label: '30M', value: 1800 },
{ label: '60M', value: 3600 },
]"
/>
</a-form-item>
</a-col>
<a-col :lg="2" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button
type="primary"
:loading="tableState.loading"
@click.prevent="handleSearch"
>
<template #icon>
<SearchOutlined />
</template>
{{ t('common.search') }}
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
:loading="tableState.loading"
@click.prevent="fnChangShowType()"
>
<template #icon>
<AreaChartOutlined />
</template>
{{
tableState.showTable
? t('views.perfManage.goldTarget.kpiChartTitle')
: t('views.perfManage.goldTarget.kpiTableTitle')
}}
</a-button>
<a-button
type="dashed"
:loading="tableState.loading"
@click.prevent="fnRecordExport()"
v-show="tableState.showTable"
>
<template #icon>
<ExportOutlined />
</template>
{{ t('common.export') }}
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center" v-show="tableState.showTable">
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon>
<ReloadOutlined />
</template>
</a-button>
</a-tooltip>
<TableColumnsDnd
v-if="tableColumns.length > 0"
:columns="tableColumns"
v-model:columns-dnd="tableColumnsDnd"
></TableColumnsDnd>
<a-tooltip>
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown trigger="click" placement="bottomRight">
<a-button type="text">
<template #icon>
<ColumnHeightOutlined />
</template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">
{{ t('common.size.default') }}
</a-menu-item>
<a-menu-item key="middle">
{{ t('common.size.middle') }}
</a-menu-item>
<a-menu-item key="small">
{{ t('common.size.small') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
<a-form layout="inline" v-show="!tableState.showTable">
<!-- <a-form-item
:label="t('views.perfManage.goldTarget.showChartSelected')"
name="chartLegendSelectedFlag"
>
<a-switch
:disabled="tableState.loading"
v-model:checked="state.chartLegendSelectedFlag"
:checked-children="t('common.switch.open')"
:un-checked-children="t('common.switch.shut')"
@change="fnLegendSelected"
size="small"
/>
</a-form-item> -->
<a-form-item
:label="t('views.perfManage.goldTarget.realTimeData')"
name="chartRealTime"
>
<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>
</template>
<!-- 表格列表 -->
<a-table
v-show="tableState.showTable"
class="table"
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"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'timeGroup'">
{{ parseDateToStr(+record.timeGroup) }}
</template>
</template>
</a-table>
<!-- 图表 -->
<div style="padding: 24px" v-show="!tableState.showTable">
<div
ref="kpiChartDom"
class="chart-container"
style="height: 450px; width: 100%"
></div>
<div class="table-container">
<a-table
:columns="statsColumns"
:data-source="kpiStats"
:pagination="false"
:scroll="{ y: 250 }"
size="small"
:custom-row="
(record:any) => ({
onClick: () => handleRowClick(record),
class: selectedRows.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>
{{ t('views.perfManage.kpiOverView.totalValueTip') }}
</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>
{{ t('views.perfManage.kpiOverView.avgValueTip') }}
</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>
{{ t('views.perfManage.kpiOverView.maxValueTip') }}
</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>
{{ t('views.perfManage.kpiOverView.minValueTip') }}
</span>
</template>
<InfoCircleOutlined />
</a-tooltip>
</span>
</template>
</template>
</a-table>
</div>
</div>
</a-card>
</PageContainer>
</template>
<style scoped>
.chart-container {
height: 800px;
width: 100%;
}
.table-container {
height: 282px;
width: 100%;
margin-top: 20px;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* 表格布局相关样式 */
:deep(.ant-table-wrapper),
:deep(.ant-table),
:deep(.ant-table-container),
:deep(.ant-table-content) {
flex: 1;
display: flex;
flex-direction: column;
}
:deep(.ant-table-body) {
flex: 1;
overflow-y: auto !important;
min-height: 0;
}
/* 表格行和表头样式 */
:deep(.ant-table-thead tr th),
:deep(.ant-table-tbody tr td) {
padding: 8px;
height: 40px;
}
/* 美化滚动条样式 */
:deep(.ant-table-body::-webkit-scrollbar) {
width: 6px;
height: 6px;
}
:deep(.ant-table-body::-webkit-scrollbar-thumb) {
background: #ccc;
border-radius: 3px;
}
:deep(.ant-table-body::-webkit-scrollbar-track) {
background: #f1f1f1;
border-radius: 3px;
}
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-thumb) {
background: #4c4c4c;
}
[data-theme='dark'] :deep(.ant-table-body::-webkit-scrollbar-track) {
background: #2a2a2a;
}
/* 选中行样式 */
:deep(.selected-row) {
background-color: rgba(24, 144, 255, 0.1);
}
[data-theme='dark'] :deep(.selected-row) {
background-color: rgba(24, 144, 255, 0.2);
}
/* 鼠标悬停样式 */
:deep(.ant-table-tbody tr:hover) {
cursor: pointer;
}
</style>