Files
fe.ems.vue3/src/views/perfManage/kpiCReport/index.vue
2025-09-18 16:27:23 +08:00

1444 lines
40 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 { listCustom } from '@/api/perfManage/customTarget';
import { listCustomData } from '@/api/perfManage/customData';
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 { LineOutlined, InfoCircleOutlined, EyeOutlined, EyeInvisibleOutlined, UpOutlined } from '@ant-design/icons-vue';
import { useRoute } from 'vue-router';
import dayjs, { Dayjs } from 'dayjs';
import useLayoutStore from '@/store/modules/layout';
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 queryParams: any = reactive({
/**网元类型 */
neType: '',
/**网元标识 */
neId: '',
/**开始时间 */
startTime: '',
/**结束时间 */
endTime: '',
/**排序字段 */
sortField: 'created_at',
/**排序方式 */
sortOrder: 'asc',
});
/**表格分页、排序、筛选变化时触发操作, 排序方式,取值为 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[];
/**图表实时统计 */
chartRealTime: boolean;
/**图表标签选择 */
chartLegendSelectedFlag: boolean;
};
/**对象信息状态 */
let state: StateType = reactive({
neType: [],
chartRealTime: false,
chartLegendSelectedFlag: true,
});
// 存储每个指标的临时固定颜色
const kpiColors = new Map<string, string>();
//legend表格数据
const kpiStats: any = ref([]);
// 统计表格loading状态
const statsTableLoading = ref(false);
/**图表显示状态 */
const isChartVisible = ref(true);
// 添加一个函数来获取当前主题下的网格线颜色
function getSplitLineColor() {
return document.documentElement.getAttribute('data-theme') === 'dark'
? '#333333'
: '#E8E8E8'; // 亮色模式返回 undefined使用默认颜色
}
// 添加表格列定义
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',
},
{
title: () => h('div', { style: { display: 'flex', alignItems: 'center', gap: '4px' } }, [
h('span', t('views.perfManage.customTarget.ago1')),
h(InfoCircleOutlined, {
style: { cursor: 'pointer' },
title: t('views.perfManage.kpiOverView.tips'),
}),
]),
dataIndex: 'last1Day',
key: 'last1Day',
width: '210px',
sortDirections: ['ascend', 'descend'],
customRender: ({ record }: { record: any }) => {
const unit = record.unit || '';
const value = record.last1Day;
// 如果是空值,直接显示空白
if (value === '' || value === null || value === undefined) {
return '';
}
if (unit.includes('%') || unit === 'Mbps') {
return `${value} `;
} else {
return `${value} `;
}
},
},
{
title: t('views.perfManage.customTarget.ago7'),
dataIndex: 'last7Days',
key: 'last7Days',
width: '200px',
sortDirections: ['ascend', 'descend'],
customRender: ({ record }: { record: any }) => {
const unit = record.unit || '';
const value = record.last7Days;
// 如果是空值,直接显示空白
if (value === '' || value === null || value === undefined) {
return '';
}
if (unit.includes('%') || unit === 'Mbps') {
return `${value} `;
} else {
return `${value} `;
}
},
},
{
title: t('views.perfManage.customTarget.ago30'),
dataIndex: 'last30Days',
key: 'last30Days',
width: '200px',
sortDirections: ['ascend', 'descend'],
customRender: ({ record }: { record: any }) => {
const unit = record.unit || '';
const value = record.last30Days;
// 如果是空值,直接显示空白
if (value === '' || value === null || value === undefined) {
return '';
}
if (unit.includes('%') || unit === 'Mbps') {
return `${value} `;
} else {
return `${value} `;
}
},
},
];
/**
* 数据列表导出
*/
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,
});
});
},
});
}
/**初始化统计表格数据 */
function fnInitStatsData() {
// 先初始化表格,显示指标列表和默认值
kpiStats.value = [];
for (const columns of tableColumns.value) {
if (
columns.key === 'neName' ||
columns.key === 'startIndex' ||
columns.key === 'timeGroup'
) {
continue;
}
kpiStats.value.push({
kpiId: columns.key,
title: columns.title,
unit: columns.unit,
last1Day: '', // 空白显示loading状态表示正在获取数据
last7Days: '',
last30Days: '',
});
}
}
/**获取近期统计数据 */
async function fnGetStatsData() {
if (!state.neType[0]) return;
statsTableLoading.value = true;
const now = new Date();
// 创建并发请求(虽然这里只有一个网元,但保持与其他界面的一致性)
const startTime = new Date(now);
startTime.setDate(now.getDate() - 30);
startTime.setHours(0, 0, 0, 0);
const endTime = new Date(now);
endTime.setHours(23, 59, 59, 999);
const params = {
neType: state.neType[0],
neId: state.neType[1],
startTime: startTime.getTime().toString(),
endTime: endTime.getTime().toString(),
sortField: 'created_at',
sortOrder: 'asc',
};
try {
const res = await listCustomData(params);
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// 计算时间边界
const now_ms = now.getTime();
const day1_start = now_ms - (1 * 24 * 60 * 60 * 1000); // 1天前
const day7_start = now_ms - (7 * 24 * 60 * 60 * 1000); // 7天前
const day30_start = now_ms - (30 * 24 * 60 * 60 * 1000); // 30天前
// 为每个指标计算统计值
for (const columns of tableColumns.value) {
if (
columns.key === 'neName' ||
columns.key === 'startIndex' ||
columns.key === 'timeGroup'
) {
continue;
}
const unit = columns.unit || '';
const isAverageType = unit.includes('%') || unit === 'Mbps';
// 根据时间范围筛选非零数据
const data1Day = res.data.filter((item: any) => {
const itemTime = new Date(item.timeGroup).getTime();
const value = item[columns.key] ? Number(item[columns.key]) : 0;
return itemTime >= day1_start && value !== 0;
});
const data7Days = res.data.filter((item: any) => {
const itemTime = new Date(item.timeGroup).getTime();
const value = item[columns.key] ? Number(item[columns.key]) : 0;
return itemTime >= day7_start && value !== 0;
});
const data30Days = res.data.filter((item: any) => {
const itemTime = new Date(item.timeGroup).getTime();
const value = item[columns.key] ? Number(item[columns.key]) : 0;
return itemTime >= day30_start && value !== 0;
});
// 计算统计值(只对非零数据进行计算)
const calculateValue = (dataArray: any[]) => {
if (dataArray.length === 0) return 0;
const values = dataArray.map((item: any) => Number(item[columns.key]));
if (isAverageType) {
// 百分比和速率值使用平均值
return Number((values.reduce((sum, val) => sum + val, 0) / values.length).toFixed(2));
} else {
// 普通值使用累加值
return Number(values.reduce((sum, val) => sum + val, 0).toFixed(2));
}
};
// 更新对应的统计数据
const statsIndex = kpiStats.value.findIndex((item: any) => item.kpiId === columns.key);
if (statsIndex !== -1) {
kpiStats.value[statsIndex].last1Day = calculateValue(data1Day);
kpiStats.value[statsIndex].last7Days = calculateValue(data7Days);
kpiStats.value[statsIndex].last30Days = calculateValue(data30Days);
}
}
}
} catch (error) {
console.error('获取统计数据失败:', error);
// 如果获取失败,保持空白显示
for (const statsItem of kpiStats.value) {
statsItem.last1Day = '';
statsItem.last7Days = '';
statsItem.last30Days = '';
}
} finally {
statsTableLoading.value = false;
}
}
/**查询数据列表表头 */
function fnGetListTitle() {
// 当前语言
var language = currentLocale.value.split('_')[0];
if (language === 'zh') language = 'cn';
if (!state.neType[0]) return false;
// 获取表头文字
listCustom({ neType: state.neType[0], status: 'Active' })
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length === 0) {
message.error({
content: t('views.perfManage.customTarget.kpiIdTip'),
duration: 2,
});
tableState.data = [];
tableColumns.value = [];
tableColumnsDnd.value = [];
kpiStats.value = []; //清空数据,因为没有可用指标
fnRanderChartData();
return false;
}
tableColumns.value = [];
const columns: any[] = [];
for (const item of res.data) {
const kpiDisplay = item[`unit`]
? item[`title`] + `(${item['unit']})`
: item[`title`];
const kpiValue = item[`kpiId`];
columns.push({
title: kpiDisplay,
dataIndex: kpiValue,
align: 'left',
key: kpiValue,
unit: item[`unit`],
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;
// 立即初始化统计表格,显示指标列表和默认值
fnInitStatsData();
});
return true;
} else {
message.warning({
content: t('common.getInfoFail'),
duration: 2,
});
return false;
}
})
.then(result => {
if (result) {
fnGetList();
// 统计数据将在 fnGetList() 成功后获取,避免重复调用
}
});
}
/**查询数据列表 */
function fnGetList() {
if (tableState.loading) return;
tableState.loading = true;
queryParams.neType = state.neType[0];
queryParams.neId = state.neType[1];
queryParams.startTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listCustomData(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;
if (!res.data.length) {
message.warning({
content: t('common.noData'),
duration: 2,
});
tableState.data = [];
// tableColumns.value = [];
// tableColumnsDnd.value = [];
// 即使没有图表数据,也要确保联动表格显示指标和默认值
if (kpiStats.value.length === 0) {
fnInitStatsData();
}
fnRanderChartData();
return false;
}
return true;
}
return false;
})
.then(result => {
if (result) {
fnRanderChartData();
// 图表数据获取成功后,开始获取统计数据(此时统计表格已经显示默认值)
fnGetStatsData();
}
});
}
/**切换显示类型 图或表格 */
function fnChangShowType() {
tableState.showTable = !tableState.showTable;
}
/**切换图表显示状态 */
function toggleChartVisibility() {
isChartVisible.value = !isChartVisible.value;
// 当图表重新显示时,需要重新调整大小
if (isChartVisible.value && kpiChart.value) {
nextTick(() => {
kpiChart.value?.resize();
});
}
}
/**绘制图表 */
function fnRanderChart() {
const container: HTMLElement | undefined = kpiChartDom.value;
if (!container) return;
kpiChart.value = markRaw(echarts.init(container, 'light'));
const option: EChartsOption = {
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: {
type: 'category',
boundaryGap: false,
data: [], // 数据x轴
},
yAxis: {
type: 'value',
boundaryGap: [0, '100%'],
},
legend: {
show: false,
type: 'scroll',
orient: 'vertical',
top: 40,
right: 20,
itemWidth: 20,
itemGap: 25,
textStyle: {
color: '#646A73',
},
icon: 'circle',
selected: {},
},
grid: {
//网格区域边距
left: '7%',
right: '7%',
bottom: '7%',
containLabel: true,
},
series: [], // 数据y轴
};
kpiChart.value.setOption(option);
// 创建 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 (var columns of tableColumns.value) {
if (
columns.key === 'neName' ||
columns.key === 'startIndex' ||
columns.key === 'timeGroup'
) {
continue;
}
const color = kpiColors.get(columns.key) || generateColorRGBA();
kpiColors.set(columns.key, color);
chartDataYSeriesData.push({
name: `${columns.title}`,
key: `${columns.key}`,
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: [],
});
chartLegendSelected[`${columns.title}`] = state.chartLegendSelectedFlag;
}
// 用降序就反转
let orgData = tableState.data;
if (queryParams.sortOrder === 'desc') {
orgData = orgData.toReversed();
}
for (const item of orgData) {
const keys = Object.keys(item);
//console.log(keys,item);
for (const y of chartDataYSeriesData) {
for (const key of keys) {
if (y.key === key) {
y.data.push(+item[key]);
chartDataXAxisData.push(item['timeGroup']);
}
}
}
}
chartDataXAxisData = Array.from(new Set(chartDataXAxisData));
//console.log(queryParams.sortOrder, chartDataXAxisData);
//console.log(chartDataXAxisData, chartDataYSeriesData);
// 绘制图数据
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,
},
yAxis: {
axisLabel: {
color:
document.documentElement.getAttribute('data-theme') === 'dark'
? '#CACADA'
: '#333',
},
splitLine: {
show: true,
lineStyle: {
color: getSplitLineColor(),
},
},
},
series: chartDataYSeriesData,
},
{
replaceMerge: ['xAxis', 'series'],
}
);
}
/**图表实时统计 */
function fnRealTimeSwitch(bool: any) {
if (bool) {
tableState.seached = false;
// 确保统计数据已获取(如果表格为空则先初始化再获取数据)
if (kpiStats.value.length === 0) {
fnInitStatsData();
fnGetStatsData();
}
// 建立链接
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* 指标(GroupID:10_neType_neId)
*/
subGroupID: `20_${queryParams.neType}_${queryParams.neId}`,
},
onmessage: wsMessage,
onerror: wsError,
};
ws.connect(options);
} else {
tableState.seached = true;
ws.close();
}
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
/**接收数据后回调 */
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;
}
// kpiEvent 黄金指标指标事件
const kpiEvent = data.data;
tableState.data.unshift(kpiEvent);
tablePagination.total++;
// 非对应网元类型
if (kpiEvent.neType !== queryParams.neType) return;
for (const key of Object.keys(data.data)) {
const v = kpiEvent[key];
// x轴
if (key === 'timeGroup') {
// chartDataXAxisData.shift();
chartDataXAxisData.push(v);
continue;
}
// y轴
const yItem = chartDataYSeriesData.find(item => item.key === key);
if (yItem) {
// yItem.data.shift();
yItem.data.push(+v);
}
}
// 绘制图数据
kpiChart.value.setOption({
xAxis: {
data: chartDataXAxisData,
},
series: chartDataYSeriesData,
});
}
// 添加一个变量来跟踪当前选中的行
const selectedRow = ref<string[]>([]);
const selectedUnit = ref<string | null>(null);
// 添加处理行点击的方法
function handleRowClick(record: any) {
const index = selectedRow.value.indexOf(record.kpiId);
// 如果已经选中,取消选中
if (index > -1) {
selectedRow.value.splice(index, 1);
chartLegendSelected[record.title] = false;
// 如果取消选中的是最后一个,重置 selectedUnit
if (selectedRow.value.length === 0) {
selectedUnit.value = null;
}
} else {
// 检查单位是否一致
if (selectedUnit.value && selectedUnit.value !== record.unit) {
message.error(
`${t('views.perfManage.customTarget.unitSelect')} ${selectedUnit.value}`
);
return;
}
// 添加新的选中项
selectedRow.value.push(record.kpiId);
// 设置选中的单位
if (!selectedUnit.value) {
selectedUnit.value = record.unit;
}
// 如果只有一个选中项,重置为 false
if (selectedRow.value.length === 1) {
Object.keys(chartLegendSelected).forEach(key => {
chartLegendSelected[key] = false;
});
}
chartLegendSelected[record.title] = true;
}
// 如果没有选中项,设置所有图例为 true
if (selectedRow.value.length === 0) {
Object.keys(chartLegendSelected).forEach(key => {
chartLegendSelected[key] = true;
});
}
// 更新图表设置
kpiChart.value.setOption({
legend: {
selected: chartLegendSelected,
},
});
}
// 监听主题变化
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,
},
},
},
});
}
}
);
onMounted(() => {
// 目前支持的 AMF AUSF MME MOCNGW NSSF SMF UDM UPF PCF
// 获取网元网元列表
listCustom({ status: 'Active' }).then((res: any) => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (!res.data.length) {
message.warning({
content: '无可用的自定义指标,请先添加自定义指标',
duration: 2,
});
return false;
}
let typeArr: any = [];
res.data.forEach((item: any) => {
typeArr.push(item.neType);
});
typeArr = Array.from(new Set(typeArr));
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 typeArr.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) || 'IMS';
const item = neCascaderOptions.value.find(
s => s.value === queryNeType
);
if (item && item.children) {
const info = item.children[0];
state.neType = [info.neType, info.neId];
queryParams.neType = info.neType;
queryParams.neId = info.neId;
} else {
const info = neCascaderOptions.value[0].children[0];
state.neType = [info.neType, info.neId];
queryParams.neType = info.neType;
queryParams.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()}`;
fnGetListTitle();
// 绘图
fnRanderChart();
}
} 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="6" :md="12" :xs="24">
<a-form-item name="neType" :label="t('views.ne.common.neType')">
<a-cascader
v-model:value="state.neType"
:options="neCascaderOptions"
:allow-clear="false"
@change="fnGetListTitle()"
:placeholder="t('common.selectPlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="10" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.goldTarget.timeFrame')"
name="timeFrame"
>
<a-range-picker
v-model:value="queryRangePicker"
bordered
:allow-clear="false"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
value-format="x"
:presets="ranges"
style="width: 100%"
></a-range-picker>
</a-form-item>
</a-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="fnGetListTitle()"
>
<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
v-show="!tableState.showTable"
type="default"
@click="toggleChartVisibility"
class="chart-toggle-btn"
:title="isChartVisible ? 'hide' : 'show'"
>
<DownOutlined v-if="isChartVisible" />
<RightOutlined v-else />
<span style="margin-left: 4px;">{{ isChartVisible ? 'Expand Graph' : 'Collapse Graph' }}</span>
</a-button>
<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"
>
</a-table>
<!-- 图表 -->
<div style="padding: 24px" v-show="!tableState.showTable">
<div
ref="kpiChartDom"
class="chart-container"
style="height: 450px; width: 100%"
:style="{ display: isChartVisible ? 'block' : 'none' }"
></div>
<div class="table-container">
<a-table
:columns="statsColumns"
:data-source="kpiStats"
:pagination="false"
:scroll="{ y: 250 }"
size="small"
:loading="statsTableLoading"
:custom-row="(record:any) => ({
onClick: () => handleRowClick(record),
class: selectedRow.includes(record.kpiId) ? 'selected-row' : '',
})
"
>
</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;
}
/* 图表隐藏时,表格容器的高度调整 */
.chart-container[style*="display: none"] + .table-container {
height: 500px;
margin-top: 0;
}
/* 表格布局相关样式 */
: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;
}
/* 图表切换按钮样式 */
.chart-toggle-btn {
margin-left: 8px;
}
</style>