1226 lines
33 KiB
Vue
1226 lines
33 KiB
Vue
<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';
|
||
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]>(['', '']);
|
||
|
||
/**表格字段列 */
|
||
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: '',
|
||
/**颗粒度 */
|
||
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[];
|
||
/**图表实时统计 */
|
||
chartRealTime: boolean;
|
||
/**图表标签选择 */
|
||
chartLegendSelectedFlag: boolean;
|
||
};
|
||
|
||
/**对象信息状态 */
|
||
let state: StateType = reactive({
|
||
neType: [],
|
||
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%',
|
||
sorter: (a: any, b: any) => a.total - b.total,
|
||
sortDirections: ['ascend', 'descend'],
|
||
},
|
||
{
|
||
title: t('views.perfManage.kpiOverView.avgValue'),
|
||
dataIndex: 'avg',
|
||
key: 'avg',
|
||
width: '24%',
|
||
sorter: (a: any, b: any) => a.avg - b.avg,
|
||
sortDirections: ['ascend', 'descend'],
|
||
},
|
||
{
|
||
title: t('views.perfManage.kpiOverView.maxValue'),
|
||
dataIndex: 'max',
|
||
key: 'max',
|
||
width: '17%',
|
||
sorter: (a: any, b: any) => a.max - b.max, // 添加排序函数
|
||
sortDirections: ['ascend', 'descend'],
|
||
},
|
||
{
|
||
title: t('views.perfManage.kpiOverView.minValue'),
|
||
dataIndex: 'min',
|
||
key: 'min',
|
||
width: '17%',
|
||
sorter: (a: any, b: any) => a.min - b.min, // 添加排序函数
|
||
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];
|
||
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 fnGetListTitle() {
|
||
// 当前语言
|
||
var language = currentLocale.value.split('_')[0];
|
||
if (language === 'zh') language = 'cn';
|
||
|
||
// 获取表头文字
|
||
getKPITitle(state.neType[0])
|
||
.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();
|
||
});
|
||
}
|
||
|
||
/**查询数据列表 */
|
||
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];
|
||
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;
|
||
}
|
||
return false;
|
||
})
|
||
.then(result => {
|
||
if (result) {
|
||
fnRanderChartData();
|
||
//封装legend表格数据
|
||
kpiStats.value = [];
|
||
for (const columns of tableColumns.value) {
|
||
if (
|
||
columns.key === 'neName' ||
|
||
columns.key === 'startIndex' ||
|
||
columns.key === 'timeGroup'
|
||
) {
|
||
continue;
|
||
}
|
||
|
||
const values = tableState.data.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,
|
||
title: columns.title,
|
||
max: values.length > 0 ? Math.max(...values) : 0,
|
||
min: values.length > 0 ? Math.min(...values) : 0,
|
||
avg: avg,
|
||
total: total,
|
||
});
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**切换显示类型 图或表格 */
|
||
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.value,markRaw是vue函数,用于标记对象为不可响应
|
||
const option: any = {
|
||
//定义图表的配置对象,tooltip的出发方式为axis
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
position: function (pt: any) {
|
||
return [pt[0], '10%'];
|
||
},
|
||
confine: true, // 限制 tooltip 显示范围
|
||
},
|
||
xAxis: {
|
||
//x类别轴
|
||
type: 'category',
|
||
boundaryGap: false,
|
||
data: [], // 数据x轴
|
||
},
|
||
yAxis: {
|
||
//y类别轴
|
||
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); //设置图表配置项,应用到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 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) {
|
||
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);
|
||
|
||
// 绘制图数据
|
||
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 fnLegendSelected(bool: any) {
|
||
for (const key of Object.keys(chartLegendSelected)) {
|
||
chartLegendSelected[key] = bool;
|
||
}
|
||
kpiChart.value.setOption({
|
||
legend: {
|
||
selected: chartLegendSelected,
|
||
},
|
||
});
|
||
}
|
||
|
||
/**图表实时统计 */
|
||
function fnRealTimeSwitch(bool: any) {
|
||
if (bool) {
|
||
tableState.seached = false;
|
||
// 建立链接
|
||
const options: OptionsType = {
|
||
url: '/ws',
|
||
params: {
|
||
/**订阅通道组
|
||
*
|
||
* 指标(GroupID:10_neType_neId)
|
||
*/
|
||
subGroupID: `10_${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(parseDateToStr(+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[]>([]);
|
||
|
||
// 添加处理行点击的方法
|
||
function handleRowClick(record: any) {
|
||
const index = selectedRow.value.indexOf(record.kpiId);
|
||
|
||
// 如果已经选中,取消选中
|
||
if (index > -1) {
|
||
selectedRow.value.splice(index, 1);
|
||
chartLegendSelected[record.title] = false;
|
||
} else {
|
||
// 添加新的选中项
|
||
selectedRow.value.push(record.kpiId);
|
||
|
||
// 如果只有一个选中项,重置为 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,
|
||
},
|
||
});
|
||
}
|
||
|
||
// 添加一个函数来获取当前主题下的网格线颜色
|
||
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,
|
||
},
|
||
},
|
||
},
|
||
});
|
||
}
|
||
}
|
||
);
|
||
|
||
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) {
|
||
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"
|
||
: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"
|
||
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="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
|
||
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"
|
||
:cache-id="`kpiTarget_${state.neType[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 => ({
|
||
onClick: () => handleRowClick(record),
|
||
class: selectedRow.includes(record.kpiId) ? 'selected-row' : '',
|
||
})
|
||
"
|
||
>
|
||
<template #headerCell="{ column }">
|
||
<template v-if="column.key === 'total'">
|
||
<span>
|
||
{{ t('views.perfManage.kpiOverView.totalValue') }}
|
||
<a-tooltip placement="bottom">
|
||
<template #title>
|
||
<span>Sum within Time Range</span>
|
||
</template>
|
||
<InfoCircleOutlined />
|
||
</a-tooltip>
|
||
</span>
|
||
</template>
|
||
<template v-if="column.key === 'avg'">
|
||
<span>
|
||
{{ t('views.perfManage.kpiOverView.avgValue') }}
|
||
<a-tooltip placement="bottom">
|
||
<template #title>
|
||
<span>Average value over the time range</span>
|
||
</template>
|
||
<InfoCircleOutlined />
|
||
</a-tooltip>
|
||
</span>
|
||
</template>
|
||
<template v-if="column.key === 'max'">
|
||
<span>
|
||
{{ t('views.perfManage.kpiOverView.maxValue') }}
|
||
<a-tooltip placement="bottom">
|
||
<template #title>
|
||
<span>Maximum value in time range</span>
|
||
</template>
|
||
<InfoCircleOutlined />
|
||
</a-tooltip>
|
||
</span>
|
||
</template>
|
||
<template v-if="column.key === 'min'">
|
||
<span>
|
||
{{ t('views.perfManage.kpiOverView.minValue') }}
|
||
<a-tooltip placement="bottom">
|
||
<template #title>
|
||
<span>Minimum value in the time range</span>
|
||
</template>
|
||
<InfoCircleOutlined />
|
||
</a-tooltip>
|
||
</span>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</div>
|
||
</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>
|