Merge remote-tracking branch 'origin/main' into multi-tenant

This commit is contained in:
lai
2024-12-06 18:37:56 +08:00
181 changed files with 6304 additions and 5882 deletions

View File

@@ -1,18 +1,17 @@
<script setup lang="ts">
import { reactive, onMounted, ref, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { Form, message, Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { ProModal } from 'antdv-pro-modal';
import { Form, message, Modal } 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 { ColumnsType } from 'ant-design-vue/es/table';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
import useNeInfoStore from '@/store/modules/neinfo';
import { parseObjLineToHump } from '@/utils/parse-utils';
import {
addCustom,
delCustom,
getCustom,
listCustom,
updateCustom,
} from '@/api/perfManage/customTarget';
@@ -91,6 +90,11 @@ let tableColumns: ColumnsType = [
dataIndex: 'title',
align: 'center',
},
{
title: t('views.perfManage.customTarget.expression'),
dataIndex: 'exprAlias',
align: 'center',
},
{
title: t('views.perfManage.customTarget.description'),
dataIndex: 'description',
@@ -150,7 +154,9 @@ function fnTableSize({ key }: MenuInfo) {
function fnRecordDelete(row: Record<string, any>) {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.perfManage.customTarget.delCustomTip', { num: row.id }),
content: t('views.perfManage.customTarget.delCustomTip', {
num: row.kpiId,
}),
onOk() {
const key = 'delThreshold';
message.loading({ content: t('common.loading'), key });
@@ -208,9 +214,9 @@ function fnGetList(pageNum?: number) {
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
openByView: boolean;
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
openByEdit: boolean;
/**标题 */
title: string;
/**网元类型设备对象 */
@@ -219,6 +225,8 @@ type ModalStateType = {
neTypPerformance: Record<string, any>[];
/**已选择性能测量项 */
selectedPre: string[];
/** 元素选择*/
elemSelect: any;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
@@ -227,16 +235,16 @@ type ModalStateType = {
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
visibleByEdit: false,
openByView: false,
openByEdit: false,
title: '',
neType: [],
neTypPerformance: [],
selectedPre: [],
elemSelect: '',
from: {
id: undefined,
neType: 'UDM',
kpiId: '',
title: '',
expression: '',
status: 'Active',
@@ -276,13 +284,6 @@ const modalStateFrom = Form.useForm(
t('common.unableNull'),
},
],
kpiId: [
{
required: true,
message:
t('views.perfManage.customTarget.kpiId') + t('common.unableNull'),
},
],
title: [
{
required: true,
@@ -303,6 +304,7 @@ const modalStateFrom = Form.useForm(
/**性能测量数据集选择初始 value:neType*/
function fnSelectPerformanceInit(value: any) {
modalState.from.expression = '';
modalState.elemSelect = '';
modalState.neTypPerformance = [
{
value: 'granularity',
@@ -340,13 +342,14 @@ function fnModalVisibleByEdit(row?: any, id?: any) {
if (!id) {
modalStateFrom.resetFields();
modalState.title = t('views.perfManage.customTarget.addCustom');
modalState.visibleByEdit = true;
modalState.openByEdit = true;
fnSelectPerformanceInit(modalState.from.neType);
} else {
fnSelectPerformanceInit(row.neType);
modalState.from = Object.assign(modalState.from, row);
modalState.from.expression = modalState.from.exprAlias;
modalState.title = t('views.perfManage.customTarget.editCustom');
modalState.visibleByEdit = true;
modalState.openByEdit = true;
}
}
@@ -357,7 +360,34 @@ function fnModalVisibleByEdit(row?: any, id?: any) {
function fnModalOk() {
modalStateFrom
.validate()
.then(e => {
.then((e: any) => {
const matches = modalState.from.expression.match(/'([^']+)'/g); // 提取单引号内容
// 替换为对应的 value
let result = modalState.from.expression;
if (matches) {
for (const match of matches) {
const valueToReplace = match.slice(1, -1); // 去掉单引号
const found = modalState.neTypPerformance.find(
(item: any) => item.label === valueToReplace
);
if (found) {
result = result.replace(match, `'${found.value}'`); // 替换为对应的 value
} else {
message.error(
t('views.perfManage.customTarget.expressionErrorTip', {
kpiId: valueToReplace,
}),
3
);
return;
}
}
} else {
message.error(t('views.perfManage.customTarget.expressionNoIdTip'), 3);
return false;
}
modalState.from.expression = result;
const from = toRaw(modalState.from);
//return false;
modalState.confirmLoading = true;
@@ -395,7 +425,7 @@ function fnModalOk() {
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdit = false;
modalState.openByEdit = false;
modalState.confirmLoading = false;
modalStateFrom.resetFields();
modalState.neType = [];
@@ -406,12 +436,20 @@ function fnModalCancel() {
* 选择性能指标,填充进当前计算公式的值
*/
function fnSelectPer(s: any, option: any) {
modalState.from.expression += `'${s}'`;
console.log(option);
modalState.from.expression += `'${option.label}'`;
}
function fnSelectSymbol(s: any) {
modalState.from.expression += s;
}
function fnChangeUnit(value: any) {
if (value === '%' && modalState.from.expression) {
modalState.from.expression = `(${modalState.from.expression})*100`;
}
}
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
onMounted(() => {
@@ -465,10 +503,7 @@ onMounted(() => {
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.neType')"
name="neType "
>
<a-form-item :label="t('views.ne.common.neType')" name="neType ">
<a-auto-complete
v-model:value="queryParams.neType"
:options="neCascaderOptions"
@@ -609,7 +644,7 @@ onMounted(() => {
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
v-model:open="modalState.openByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@@ -621,7 +656,7 @@ onMounted(() => {
:label-col="{ span: 6 }"
:label-wrap="true"
>
<a-row :gutter="16">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.neType')"
@@ -638,102 +673,6 @@ onMounted(() => {
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.customTarget.title')"
name="title"
v-bind="modalStateFrom.validateInfos.title"
>
<a-input
v-model:value="modalState.from.title"
:maxlength="255"
allow-clear
>
</a-input>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.customTarget.kpiId')"
name="kpiId"
v-bind="modalStateFrom.validateInfos.kpiId"
>
<a-input
v-model:value="modalState.from.kpiId"
:maxlength="16"
allow-clear
>
</a-input>
</a-form-item>
</a-col>
</a-row>
<a-form-item
:label="t('views.perfManage.customTarget.expression')"
name="expression"
:label-col="{ span: 3 }"
v-bind="modalStateFrom.validateInfos.expression"
>
<a-input
v-model:value="modalState.from.expression"
:maxlength="1024"
allow-clear
>
</a-input>
</a-form-item>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
name="perSelect"
:label="t('views.perfManage.customTarget.symbol')"
>
<a-select
placeholder="Please select"
:options="modalStateFromOption.symbolJson"
@select="fnSelectSymbol"
></a-select>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
name="perSelect"
:label="t('views.perfManage.customTarget.element')"
>
<a-select
placeholder="Please select"
:options="modalState.neTypPerformance"
@select="fnSelectPer"
></a-select>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.customTarget.unit')"
name="expression"
v-bind="modalStateFrom.validateInfos.unit"
>
<a-auto-complete
v-model:value="modalState.from.unit"
:options="[
{
label: 'Mbps',
value: 'Mbps',
},
{
label: '%',
value: '%',
},
]"
></a-auto-complete>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.customTarget.status')"
@@ -758,6 +697,96 @@ onMounted(() => {
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item
:label="t('views.perfManage.customTarget.title')"
name="title"
:label-col="{ span: 3 }"
v-bind="modalStateFrom.validateInfos.title"
>
<a-input
v-model:value="modalState.from.title"
:maxlength="255"
allow-clear
>
</a-input>
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="left">{{
t('views.perfManage.customTarget.expressionModal')
}}</a-divider>
<a-form-item
:label="t('views.perfManage.customTarget.expression')"
name="expression"
:label-col="{ span: 3 }"
v-bind="modalStateFrom.validateInfos.expression"
>
<a-input
v-model:value="modalState.from.expression"
:disabled="modalState.from.id"
:maxlength="1024"
autocomplete="off"
allow-clear
>
</a-input>
</a-form-item>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
name="elemSelect"
:label="t('views.perfManage.customTarget.element')"
>
<a-select
v-model:value="modalState.elemSelect"
placeholder="Please select"
:options="modalState.neTypPerformance"
@select="fnSelectPer"
></a-select>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
name="symbol"
:label="t('views.perfManage.customTarget.symbol')"
>
<a-select
placeholder="Please select"
:options="modalStateFromOption.symbolJson"
@select="fnSelectSymbol"
></a-select>
</a-form-item>
</a-col>
</a-row>
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.customTarget.unit')"
name="expression"
v-bind="modalStateFrom.validateInfos.unit"
:help="t('views.perfManage.customTarget.TourDes4')"
>
<a-auto-complete
v-model:value="modalState.from.unit"
@change="fnChangeUnit"
:options="[
{
label: 'Mbps',
value: 'Mbps',
},
{
label: '%',
value: '%',
},
]"
>
</a-auto-complete>
</a-form-item>
</a-col>
</a-row>
<a-divider></a-divider>
<a-form-item
:label="t('views.perfManage.customTarget.description')"

View File

@@ -22,12 +22,13 @@ import {
markRaw,
nextTick,
onBeforeUnmount,
h,
watch
} from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } from 'ant-design-vue/lib';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
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,
@@ -42,6 +43,9 @@ 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();
@@ -78,10 +82,10 @@ let neCascaderOptions = ref<Record<string, any>[]>([]);
let queryRangePicker = ref<[string, string]>(['', '']);
/**表格字段列 */
let tableColumns = ref<ColumnsType>([]);
let tableColumns = ref<any[]>([]);
/**表格字段列排序 */
let tableColumnsDnd = ref<ColumnsType>([]);
let tableColumnsDnd = ref<any[]>([]);
/**表格分页器参数 */
let tablePagination = reactive({
@@ -185,9 +189,70 @@ type StateType = {
let state: StateType = reactive({
neType: [],
chartRealTime: false,
chartLegendSelectedFlag: 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'],
},
];
/**
* 数据列表导出
*/
@@ -251,14 +316,18 @@ function fnGetListTitle() {
// 获取表头文字
getKPITitle(state.neType[0])
.then(res => {//处理getKPITitle返回的结果
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {//检查值
tableColumns.value = [];//设为空数组
const columns: ColumnsType = [];//初始化,构建新表头
for (const item of res.data) {//遍历res.data
const kpiDisplay = item[`${language}Title`];//提取标题kpiDisplay和ID标识kpiValue
.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({//
columns.push({
//
title: kpiDisplay,
dataIndex: kpiValue,
align: 'left',
@@ -297,7 +366,8 @@ function fnGetListTitle() {
return false;
}
})
.then(result => {//result是前一个.then返回的值(true or false)
.then(result => {
//result是前一个.then返回的值(true or false)
result && fnGetList();
});
}
@@ -316,6 +386,7 @@ function fnGetList() {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
tablePagination.total = res.data.length;
tableState.data = res.data;
return true;
}
return false;
@@ -323,6 +394,38 @@ function fnGetList() {
.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
});
}
}
});
}
@@ -334,27 +437,33 @@ function fnChangShowType() {
/**绘制图表 */
function fnRanderChart() {
const container: HTMLElement | undefined = kpiChartDom.value;//获取图表容器DOM元素
if (!container) return;//若没有,则退出函数
const container: HTMLElement | undefined = kpiChartDom.value; //获取图表容器DOM元素
if (!container) return; //若没有,则退出函数
kpiChart.value = markRaw(echarts.init(container, 'light'));
//初始化Echarts图表实例应用light主题并赋值给kpiChart.valuemarkRaw是vue函数用于标记对象为不可响应
const option: EChartsOption = {//定义图表的配置对象tooltip的出发方式为axis
//初始化Echarts图表实例应用light主题并赋值给kpiChart.valuemarkRaw是vue函数用于标记对象为不可响应
const option: any = {
//定义图表的配置对象tooltip的出发方式为axis
tooltip: {
trigger: 'axis',
position: function (pt: any) {
return [pt[0], '10%'];
},
confine: true, // 限制 tooltip 显示范围
},
xAxis: {//x类别轴
xAxis: {
//x类别轴
type: 'category',
boundaryGap: false,
data: [], // 数据x轴
},
yAxis: {//y类别轴
yAxis: {
//y类别轴
type: 'value',
boundaryGap: [0, '100%'],
},
legend: {//图例垂直滚动
legend: {
show: false,
//图例垂直滚动
type: 'scroll',
orient: 'vertical',
top: 40,
@@ -367,25 +476,17 @@ function fnRanderChart() {
icon: 'circle',
selected: {},
},
grid: {//网格区域边距
left: '10%',
right: '30%',
bottom: '20%',
grid: {
//网格区域边距
left: '7%',
right: '7%',
bottom: '7%',
containLabel: true,
},
dataZoom: [
{//启用图表的数据缩放范围90%-100%
type: 'inside',
start: 90,
end: 100,
},
{
start: 90,
end: 100,
},
],
series: [], // 数据y轴
};
kpiChart.value.setOption(option);//设置图表配置项应用到kpiChart实例上
kpiChart.value.setOption(option); //设置图表配置项应用到kpiChart实例上
// 创建 ResizeObserver 实例 监听图表容器大小变化,并在变化时调整图表大小
var observer = new ResizeObserver(entries => {
@@ -423,12 +524,16 @@ function fnRanderChartData() {
) {
continue;
}
const color = generateColorRGBA();
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,
@@ -452,11 +557,9 @@ function fnRanderChartData() {
// 用降序就反转
let orgData = tableState.data;
console.log(orgData);
if (queryParams.sortOrder === 'desc') {
orgData = orgData.toReversed();
}
for (const item of orgData) {
chartDataXAxisData.push(parseDateToStr(+item['timeGroup']));
const keys = Object.keys(item);
@@ -480,8 +583,32 @@ function fnRanderChartData() {
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,
},
{
@@ -577,6 +704,113 @@ function wsMessage(res: Record<string, any>) {
});
}
// 添加一个变量来跟踪当前选中的行
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
// 获取网元网元列表
@@ -614,11 +848,16 @@ onMounted(() => {
}
// 查询当前小时
const nowDate: Date = new Date();
nowDate.setMinutes(0, 0, 0);
queryRangePicker.value[0] = `${nowDate.getTime()}`;
nowDate.setMinutes(59, 59, 59);
queryRangePicker.value[1] = `${nowDate.getTime()}`;
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();
@@ -639,71 +878,42 @@ onBeforeUnmount(() => {
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<a-card v-show="tableState.seached" :bordered="false" :body-style="{ marginBottom: '24px', paddingBottom: 0 }">
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParamsFrom" layout="horizontal">
<a-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 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 :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 :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>
<a-button type="primary" :loading="tableState.loading" @click.prevent="fnGetListTitle()">
<template #icon>
<SearchOutlined />
</template>
{{ t('common.search') }}
</a-button>
</a-space>
@@ -717,24 +927,18 @@ onBeforeUnmount(() => {
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
:loading="tableState.loading"
@click.prevent="fnChangShowType()"
>
<template #icon> <AreaChartOutlined /> </template>
<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"
>
<a-button type="dashed" :loading="tableState.loading" @click.prevent="fnRecordExport()"
v-show="tableState.showTable">
<template #icon>
<ExportOutlined />
</template>
@@ -749,26 +953,23 @@ onBeforeUnmount(() => {
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
<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>
<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>
<template #icon>
<ColumnHeightOutlined />
</template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu :selected-keys="[tableState.size as string]" @click="fnTableSize">
<a-menu-item key="default">
{{ t('common.size.default') }}
</a-menu-item>
@@ -784,7 +985,7 @@ onBeforeUnmount(() => {
</a-tooltip>
</a-space>
<a-form layout="inline" v-show="!tableState.showTable">
<a-form-item
<!-- <a-form-item
:label="t('views.perfManage.goldTarget.showChartSelected')"
name="chartLegendSelectedFlag"
>
@@ -796,38 +997,20 @@ onBeforeUnmount(() => {
@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-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 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) }}
@@ -837,10 +1020,94 @@ onBeforeUnmount(() => {
<!-- 图表 -->
<div style="padding: 24px" v-show="!tableState.showTable">
<div ref="kpiChartDom" style="height: 450px; width: 100%"></div>
<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' : ''
})
" />
</div>
</div>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>
<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>

View File

@@ -22,9 +22,11 @@ import {
markRaw,
nextTick,
onBeforeUnmount,
h,
watch,
} from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } from 'ant-design-vue/es';
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';
@@ -41,13 +43,18 @@ 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 } 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,
@@ -79,17 +86,20 @@ let neCascaderOptions = ref<Record<string, any>[]>([]);
let queryRangePicker = ref<[string, string]>(['', '']);
/**时间选择 */
let ranges = ref<Record<string, [Dayjs, Dayjs]>>({
[t('views.perfManage.customTarget.sixHoursAgo')]: [
dayjs().subtract(6, 'hours'),
dayjs(),
],
[t('views.perfManage.customTarget.threeHoursAgo')]: [
dayjs().subtract(3, 'hours'),
dayjs(),
],
[t('views.monitor.monitor.today')]: [dayjs().startOf('day'), dayjs()],
});
const ranges = ref([
{
label: t('views.perfManage.customTarget.sixHoursAgo'),
value: [dayjs().subtract(6, 'hours'), dayjs()],
},
{
label: t('views.perfManage.customTarget.threeHoursAgo'),
value: [dayjs().subtract(3, 'hours'), dayjs()],
},
{
label: t('views.monitor.monitor.today'),
value: [dayjs().startOf('day'), dayjs()],
},
]);
/**表格字段列 */
let tableColumns = ref<any[]>([]);
@@ -197,9 +207,69 @@ type StateType = {
let state: StateType = reactive({
neType: [],
chartRealTime: false,
chartLegendSelectedFlag: false,
chartLegendSelectedFlag: true,
});
// 存储每个指标的临时固定颜色
const kpiColors = new Map<string, string>();
//legend表格数据
const kpiStats: any = ref([]);
// 添加一个函数来获取当前主题下的网格线颜色
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',
width: '65%',
},
{
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'],
},
];
/**
* 数据列表导出
*/
@@ -261,6 +331,8 @@ 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 => {
@@ -273,6 +345,7 @@ function fnGetListTitle() {
tableState.data = [];
tableColumns.value = [];
tableColumnsDnd.value = [];
kpiStats.value = []; //清空数据
fnRanderChartData();
return false;
}
@@ -342,6 +415,18 @@ function fnGetList() {
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 = [];
kpiStats.value = []; //清空数据
fnRanderChartData();
return false;
}
return true;
}
return false;
@@ -349,6 +434,37 @@ function fnGetList() {
.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
});
}
}
});
}
@@ -370,6 +486,18 @@ function fnRanderChart() {
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',
@@ -381,6 +509,7 @@ function fnRanderChart() {
boundaryGap: [0, '100%'],
},
legend: {
show: false,
type: 'scroll',
orient: 'vertical',
top: 40,
@@ -394,21 +523,12 @@ function fnRanderChart() {
selected: {},
},
grid: {
left: '10%',
right: '30%',
bottom: '20%',
//网格区域边距
left: '7%',
right: '7%',
bottom: '7%',
containLabel: true,
},
dataZoom: [
{
type: 'inside',
start: 90,
end: 100,
},
{
start: 90,
end: 100,
},
],
series: [], // 数据y轴
};
kpiChart.value.setOption(option);
@@ -449,12 +569,16 @@ function fnRanderChartData() {
) {
continue;
}
const color = generateColorRGBA();
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,
@@ -483,18 +607,21 @@ function fnRanderChartData() {
}
for (const item of orgData) {
chartDataXAxisData.push(item['timeGroup']);
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']);
}
}
}
}
// console.log(queryParams.sortOrder, chartLegendSelected);
// console.log(chartDataXAxisData, chartDataYSeriesData);
chartDataXAxisData = Array.from(new Set(chartDataXAxisData));
//console.log(queryParams.sortOrder, chartDataXAxisData);
//console.log(chartDataXAxisData, chartDataYSeriesData);
// 绘制图数据
kpiChart.value.setOption(
@@ -505,8 +632,32 @@ function fnRanderChartData() {
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,
},
{
@@ -515,18 +666,6 @@ function fnRanderChartData() {
);
}
/**图表折线显示全部 */
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) {
@@ -602,57 +741,177 @@ function wsMessage(res: Record<string, any>) {
});
}
// 添加一个变量来跟踪当前选中的行
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,
},
});
}
// 监听主题变化
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 => {
listCustom({ status: 'Active' }).then((res: any) => {
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 (!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) || '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();
}
);
if (neCascaderOptions.value.length === 0) {
} else {
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);
queryRangePicker.value[0] = `${now.getTime()}`;
now.setMinutes(59, 59, 59);
queryRangePicker.value[1] = `${now.getTime()}`;
fnGetListTitle();
// 绘图
fnRanderChart();
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
});
@@ -665,51 +924,31 @@ onBeforeUnmount(() => {
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<a-card v-show="tableState.seached" :bordered="false" :body-style="{ marginBottom: '24px', paddingBottom: 0 }">
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParamsFrom" layout="horizontal">
<a-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-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"
:ranges="ranges"
style="width: 100%"
></a-range-picker>
<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>
<a-button type="primary" :loading="tableState.loading" @click.prevent="fnGetListTitle()">
<template #icon>
<SearchOutlined />
</template>
{{ t('common.search') }}
</a-button>
</a-space>
@@ -723,24 +962,18 @@ onBeforeUnmount(() => {
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button
type="primary"
:loading="tableState.loading"
@click.prevent="fnChangShowType()"
>
<template #icon> <AreaChartOutlined /> </template>
<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"
>
<a-button type="dashed" :loading="tableState.loading" @click.prevent="fnRecordExport()"
v-show="tableState.showTable">
<template #icon>
<ExportOutlined />
</template>
@@ -755,26 +988,23 @@ onBeforeUnmount(() => {
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
<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>
<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>
<template #icon>
<ColumnHeightOutlined />
</template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu :selected-keys="[tableState.size as string]" @click="fnTableSize">
<a-menu-item key="default">
{{ t('common.size.default') }}
</a-menu-item>
@@ -790,7 +1020,7 @@ onBeforeUnmount(() => {
</a-tooltip>
</a-space>
<a-form layout="inline" v-show="!tableState.showTable">
<a-form-item
<!-- <a-form-item
:label="t('views.perfManage.goldTarget.showChartSelected')"
name="chartLegendSelectedFlag"
>
@@ -802,46 +1032,112 @@ onBeforeUnmount(() => {
@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-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 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" style="height: 450px; width: 100%"></div>
<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' : ''
})
" />
</div>
</div>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>
<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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { SizeType } from 'ant-design-vue/es/config-provider';
import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/es/table';
import { parseDateToStr } from '@/utils/date-utils';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { listperfData } from '@/api/perfManage/perfData';

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { reactive, onMounted, ref, toRaw } from 'vue';
import { reactive, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { Form, message, Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { ProModal } from 'antdv-pro-modal';
import { Form, message, Modal } 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 { ColumnsType } from 'ant-design-vue/es/table';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
import useDictStore from '@/store/modules/dict';
@@ -211,9 +212,9 @@ function fnGetList(pageNum?: number) {
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
openByView: boolean;
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
openByEdit: boolean;
/**标题 */
title: string;
/**网元类型设备对象 */
@@ -228,8 +229,8 @@ type ModalStateType = {
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
visibleByEdit: false,
openByView: false,
openByEdit: false,
title: '',
neType: [],
neTypPerformance: [],
@@ -305,7 +306,7 @@ function fnModalVisibleByEdit(id?: string) {
if (!id) {
modalStateFrom.resetFields();
modalState.title = t('views.perfManage.perfThreshold.addThre');
modalState.visibleByEdit = true;
modalState.openByEdit = true;
} else {
if (modalState.confirmLoading) return;
const hide = message.loading(t('common.loading'), 0);
@@ -317,7 +318,7 @@ function fnModalVisibleByEdit(id?: string) {
fnSelectPerformanceInit(res.data.neType);
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = t('views.perfManage.perfThreshold.editThre');
modalState.visibleByEdit = true;
modalState.openByEdit = true;
} else {
message.error(t('views.perfManage.perfThreshold.errorThreInfo'), 3);
}
@@ -345,7 +346,7 @@ function fnModalOk() {
content: t('common.msgSuccess', { msg: modalState.title }),
duration: 3,
});
modalState.visibleByEdit = false;
modalState.openByEdit = false;
modalStateFrom.resetFields();
} else {
message.error({
@@ -370,7 +371,7 @@ function fnModalOk() {
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdit = false;
modalState.openByEdit = false;
modalState.confirmLoading = false;
modalStateFrom.resetFields();
modalState.neType = [];
@@ -512,10 +513,7 @@ onMounted(() => {
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.neType')"
name="neType "
>
<a-form-item :label="t('views.ne.common.neType')" name="neType ">
<a-auto-complete
v-model:value="queryParams.neType"
:options="useNeInfoStore().getNeSelectOtions"
@@ -676,14 +674,14 @@ onMounted(() => {
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
:open="modalState.openByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form name="modalStateFrom" layout="horizontal">
<a-row :gutter="16">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.neType')"
@@ -716,7 +714,7 @@ onMounted(() => {
</a-select>
</a-form-item>
<a-row :gutter="16">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.perfThreshold.thresholdValue')"

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { reactive, onMounted, ref, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { Form, message, Modal } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { ProModal } from 'antdv-pro-modal';
import { Form, message, Modal } 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 { ColumnsType } from 'ant-design-vue/es/table';
import { parseDateToStr } from '@/utils/date-utils';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
@@ -244,9 +245,9 @@ function fnGetList(pageNum?: number) {
/**对话框对象信息状态类型 */
type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
openByView: boolean;
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
openByEdit: boolean;
/**标题 */
title: string;
/**选择后清空 */
@@ -272,8 +273,8 @@ type ModalStateType = {
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByView: false,
visibleByEdit: false,
openByView: false,
openByEdit: false,
title: '',
initPeriods: [],
neType: [],
@@ -453,7 +454,7 @@ function fnModalVisibleByVive(id: string) {
modalState.timeRangePicker = [res.data.startTime, res.data.endTime];
modalState.from = Object.assign(modalState.from, res.data);
modalState.title = t('views.perfManage.taskManage.viewTask');
modalState.visibleByView = true;
modalState.openByView = true;
} else {
message.error(t('views.perfManage.taskManage.errorTaskInfo'), 3);
}
@@ -468,7 +469,7 @@ function fnModalVisibleByEdit(id?: string) {
if (!id) {
modalStateFrom.resetFields();
modalState.title = t('views.perfManage.taskManage.addTask');
modalState.visibleByEdit = true;
modalState.openByEdit = true;
} else {
if (modalState.confirmLoading) return;
const hide = message.loading(t('common.loading'), 0);
@@ -528,7 +529,7 @@ function fnModalVisibleByEdit(id?: string) {
modalState.from.periods = [];
}
modalState.title = t('views.perfManage.taskManage.editTask');
modalState.visibleByEdit = true;
modalState.openByEdit = true;
} else {
message.error(t('views.perfManage.taskManage.errorTaskInfo'), 3);
}
@@ -556,7 +557,7 @@ function fnModalOk() {
content: t('common.msgSuccess', { msg: modalState.title }),
duration: 3,
});
modalState.visibleByEdit = false;
modalState.openByEdit = false;
modalStateFrom.resetFields();
} else {
message.error({
@@ -590,8 +591,8 @@ function updateStep(str: any) {
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByView = false;
modalState.visibleByEdit = false;
modalState.openByView = false;
modalState.openByEdit = false;
modalState.confirmLoading = false;
modalStateFrom.resetFields();
modalState.timeRangePicker = ['', ''];
@@ -724,10 +725,7 @@ onMounted(() => {
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.neType')"
name="neType "
>
<a-form-item :label="t('views.ne.common.neType')" name="neType ">
<a-auto-complete
v-model:value="queryParams.neType"
:options="neInfoStore.getNeSelectOtions"
@@ -883,17 +881,14 @@ onMounted(() => {
<ProModal
:drag="true"
:width="800"
:visible="modalState.visibleByView"
:open="modalState.openByView"
:title="modalState.title"
@cancel="fnModalCancel"
>
<a-form layout="horizontal">
<a-row :gutter="16">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.neType')"
name="neType"
>
<a-form-item :label="t('views.ne.common.neType')" name="neType">
<a-cascader
:value="modalState.neType"
:options="neInfoStore.getNeCascaderOptions"
@@ -926,7 +921,7 @@ onMounted(() => {
{{ modalState.neTypPerformanceList }}
</a-form-item>
<a-row :gutter="16">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
name="granulOption"
@@ -949,7 +944,7 @@ onMounted(() => {
{{ modalState.initPeriods }}
</a-form-item>
<a-row :gutter="16">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.taskManage.plan')"
@@ -997,14 +992,14 @@ onMounted(() => {
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
:open="modalState.openByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form name="modalStateFrom" layout="horizontal">
<a-row :gutter="16">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.ne.common.neType')"
@@ -1059,7 +1054,7 @@ onMounted(() => {
></a-range-picker>
</a-form-item>
<a-row :gutter="16">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.taskManage.granulOption')"
@@ -1102,7 +1097,7 @@ onMounted(() => {
/>
</a-form-item>
<a-row :gutter="16">
<a-row>
<a-col :lg="12" :md="12" :xs="24">
<a-form-item
:label="t('views.perfManage.taskManage.plan')"
@@ -1127,10 +1122,7 @@ onMounted(() => {
</a-col>
</a-row>
<a-form-item
:label="t('views.traceManage.task.remark')"
name="comment"
>
<a-form-item :label="t('views.traceManage.task.remark')" name="comment">
<a-textarea
v-model:value="modalState.from.comment"
:auto-size="{ minRows: 2, maxRows: 6 }"