Compare commits
9 Commits
2.2410.3-2
...
2.2410.4-2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33a8ce97d3 | ||
|
|
6ee9d464fb | ||
|
|
df5072bae7 | ||
|
|
e12dce1f0f | ||
|
|
d0457fc285 | ||
|
|
63d32f0a39 | ||
|
|
cf5d08aaab | ||
|
|
7ad566d74f | ||
|
|
c312186d91 |
@@ -11,7 +11,7 @@ VITE_APP_NAME = "Core Network OMC"
|
||||
VITE_APP_CODE = "OMC"
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION = "2.241018"
|
||||
VITE_APP_VERSION = "2.241102"
|
||||
|
||||
# 接口基础URL地址-不带/后缀
|
||||
VITE_API_BASE_URL = "/omc-api"
|
||||
|
||||
@@ -11,7 +11,7 @@ VITE_APP_NAME = "Core Network OMC"
|
||||
VITE_APP_CODE = "OMC"
|
||||
|
||||
# 应用版本
|
||||
VITE_APP_VERSION = "2.241018"
|
||||
VITE_APP_VERSION = "2.241102"
|
||||
|
||||
# 接口基础URL地址-不带/后缀
|
||||
VITE_API_BASE_URL = "/omc-api"
|
||||
|
||||
@@ -567,6 +567,8 @@ export default {
|
||||
rowInfo: "Info",
|
||||
type: "Type",
|
||||
duration: "Duration",
|
||||
seizureTime: "Call Start Time",
|
||||
releaseTime: "Hangup Time",
|
||||
caller: "Caller",
|
||||
called: "Called",
|
||||
result: "Result",
|
||||
@@ -1085,6 +1087,13 @@ export default {
|
||||
"layout2": "Layout 2",
|
||||
"layout3": "Layout 3"
|
||||
},
|
||||
kpiOverView:{
|
||||
"kpiChartTitle":"Overview of NE metrics",
|
||||
"changeLine":"Change to Line Charts",
|
||||
"changeBar":"Change to Bar Charts",
|
||||
"chooseShowMetrics":"Select the metric you want to display",
|
||||
"chooseMetrics":"Select an indicator",
|
||||
},
|
||||
},
|
||||
traceManage: {
|
||||
analysis: {
|
||||
|
||||
@@ -567,6 +567,8 @@ export default {
|
||||
rowInfo: "记录信息",
|
||||
type: "记录类型",
|
||||
duration: "通话时长",
|
||||
seizureTime: "呼叫开始时间",
|
||||
releaseTime: "挂断结束时间",
|
||||
caller: "主叫",
|
||||
called: "被叫",
|
||||
result: "结果",
|
||||
@@ -1085,6 +1087,14 @@ export default {
|
||||
"layout2": "布局2",
|
||||
"layout3": "布局3"
|
||||
},
|
||||
kpiOverView:{
|
||||
"kpiChartTitle":"网元指标概览",
|
||||
"changeLine":"切换为折线图",
|
||||
"changeBar":"切换为柱状图",
|
||||
"chooseShowMetrics":"选择需要显示的指标",
|
||||
"chooseMetrics":"选择指标",
|
||||
|
||||
},
|
||||
},
|
||||
traceManage: {
|
||||
analysis: {
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
import { PageContainer } from 'antdv-pro-layout';
|
||||
import { ColumnsType } from 'ant-design-vue/lib/table';
|
||||
import { message } from 'ant-design-vue/lib';
|
||||
import { reactive, toRaw, ref, onMounted, onBeforeUnmount, markRaw } from 'vue';
|
||||
import { listMain } from '@/api/index';
|
||||
import { reactive, ref, onMounted, onBeforeUnmount, markRaw } from 'vue';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { TooltipComponent } from 'echarts/components';
|
||||
import { GaugeChart } from 'echarts/charts';
|
||||
@@ -15,6 +14,8 @@ import { LabelLayout } from 'echarts/features';
|
||||
import { useRoute } from 'vue-router';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import useDictStore from '@/store/modules/dict';
|
||||
import { listAllNeInfo } from '@/api/ne/neInfo';
|
||||
import { parseDateToStr } from '@/utils/date-utils';
|
||||
const { getDict } = useDictStore();
|
||||
const appStore = useAppStore();
|
||||
const route = useRoute();
|
||||
@@ -52,44 +53,55 @@ let indexColor = ref<DictType[]>([
|
||||
let tableColumns: ColumnsType = [
|
||||
{
|
||||
title: t('views.index.object'),
|
||||
dataIndex: 'name',
|
||||
align: 'center',
|
||||
key: 'status',
|
||||
dataIndex: 'neName',
|
||||
align: 'left',
|
||||
},
|
||||
{
|
||||
title: t('views.index.realNeStatus'),
|
||||
dataIndex: 'status',
|
||||
align: 'center',
|
||||
customRender(opt) {
|
||||
if (opt.value == 'Normal') return t('views.index.normal');
|
||||
return t('views.index.abnormal');
|
||||
},
|
||||
dataIndex: 'serverState',
|
||||
align: 'left',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: t('views.index.reloadTime'),
|
||||
dataIndex: 'refresh',
|
||||
align: 'center',
|
||||
dataIndex: 'serverState',
|
||||
align: 'left',
|
||||
customRender(opt) {
|
||||
if (opt.value?.refreshTime) return parseDateToStr(opt.value?.refreshTime);
|
||||
return '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('views.index.version'),
|
||||
dataIndex: 'version',
|
||||
align: 'center',
|
||||
dataIndex: 'serverState',
|
||||
align: 'left',
|
||||
customRender(opt) {
|
||||
return opt.value?.version || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('views.index.serialNum'),
|
||||
dataIndex: 'serialNum',
|
||||
align: 'center',
|
||||
dataIndex: 'serverState',
|
||||
align: 'left',
|
||||
customRender(opt) {
|
||||
return opt.value?.sn || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('views.index.expiryDate'),
|
||||
dataIndex: 'expiryDate',
|
||||
align: 'center',
|
||||
dataIndex: 'serverState',
|
||||
align: 'left',
|
||||
customRender(opt) {
|
||||
return opt.value?.expire || '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('views.index.ipAddress'),
|
||||
dataIndex: 'ipAddress',
|
||||
key: 'groupName',
|
||||
align: 'center',
|
||||
dataIndex: 'serverState',
|
||||
align: 'left',
|
||||
customRender(opt) {
|
||||
return opt.value?.neIP || '-';
|
||||
},
|
||||
},
|
||||
];
|
||||
/**表格状态类型 */
|
||||
@@ -130,12 +142,8 @@ type nfStateType = {
|
||||
hostName: string;
|
||||
/**操作系统信息 */
|
||||
osInfo: string;
|
||||
/**数据库信息 */
|
||||
dbInfo: string;
|
||||
/**IP地址 */
|
||||
ipAddress: string;
|
||||
/**端口 */
|
||||
port: number;
|
||||
/**版本 */
|
||||
version: string;
|
||||
/**CPU利用率 */
|
||||
@@ -154,9 +162,7 @@ type nfStateType = {
|
||||
let pronInfo: nfStateType = reactive({
|
||||
hostName: '5gc',
|
||||
osInfo: 'Linux 5gc 4.15.0-112-generic 2020 x86_64 GNU/Linux',
|
||||
dbInfo: 'db v9.9.9',
|
||||
ipAddress: '-',
|
||||
port: 33030,
|
||||
version: '-',
|
||||
cpuUse: '-',
|
||||
memoryUse: '-',
|
||||
@@ -169,8 +175,8 @@ let pronInfo: nfStateType = reactive({
|
||||
function fnGetList(one: boolean) {
|
||||
if (tableState.loading) return;
|
||||
one && (tableState.loading = true);
|
||||
listMain().then(res => {
|
||||
tableState.data = res;
|
||||
listAllNeInfo({ bandStatus: true }).then(res => {
|
||||
tableState.data = res.data;
|
||||
tableState.loading = false;
|
||||
var rightNum = 0;
|
||||
var errorNum = 0;
|
||||
@@ -253,17 +259,14 @@ const closeDrawer = () => {
|
||||
function rowClick(record: any, index: any) {
|
||||
return {
|
||||
onClick: (event: any) => {
|
||||
if (
|
||||
toRaw(record).status == '异常' ||
|
||||
toRaw(record).status == 'Abnormal'
|
||||
) {
|
||||
let pronData = JSON.parse(JSON.stringify(record.serverState));
|
||||
if (!pronData.online) {
|
||||
message.error(t('views.index.neStatus'), 2);
|
||||
return false;
|
||||
} else {
|
||||
let pronData = toRaw(record);
|
||||
const totalMemInKB = pronData.memUsage?.totalMem;
|
||||
const nfUsedMemInKB = pronData.memUsage?.nfUsedMem;
|
||||
const sysMemUsageInKB = pronData.memUsage?.sysMemUsage;
|
||||
const totalMemInKB = pronData.mem?.totalMem;
|
||||
const nfUsedMemInKB = pronData.mem?.nfUsedMem;
|
||||
const sysMemUsageInKB = pronData.mem?.sysMemUsage;
|
||||
|
||||
// 将KB转换为MB
|
||||
const totalMemInMB = Math.round((totalMemInKB / 1024) * 100) / 100;
|
||||
@@ -273,19 +276,17 @@ function rowClick(record: any, index: any) {
|
||||
|
||||
//渲染详细信息
|
||||
pronInfo = {
|
||||
hostName: pronData.hostName,
|
||||
osInfo: pronData.osInfo,
|
||||
dbInfo: pronData.dbInfo,
|
||||
ipAddress: pronData.ipAddress,
|
||||
port: pronData.port,
|
||||
hostName: pronData.hostname,
|
||||
osInfo: pronData.os,
|
||||
ipAddress: pronData.neIP,
|
||||
version: pronData.version,
|
||||
cpuUse:
|
||||
pronData.name +
|
||||
pronData.neName +
|
||||
':' +
|
||||
pronData.cpuUsage?.nfCpuUsage / 100 +
|
||||
pronData.cpu?.nfCpuUsage / 100 +
|
||||
'%; ' +
|
||||
'SYS:' +
|
||||
pronData.cpuUsage?.sysCpuUsage / 100 +
|
||||
pronData.cpu?.sysCpuUsage / 100 +
|
||||
'%',
|
||||
memoryUse:
|
||||
'Total:' +
|
||||
@@ -298,8 +299,8 @@ function rowClick(record: any, index: any) {
|
||||
sysMemUsageInMB +
|
||||
'MB',
|
||||
capability: pronData.capability,
|
||||
serialNum: pronData.serialNum,
|
||||
expiryDate: pronData.expiryDate,
|
||||
serialNum: pronData.sn,
|
||||
expiryDate: pronData.expire,
|
||||
};
|
||||
}
|
||||
visible.value = true;
|
||||
@@ -388,11 +389,11 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<div v-if="record.status == '正常' || record.status == 'Normal'">
|
||||
<a-tag color="blue">{{ record.name }}</a-tag>
|
||||
<div v-if="record.serverState.online">
|
||||
<a-tag color="blue">{{ t('views.index.normal') }}</a-tag>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-tag color="pink">{{ record.name }}</a-tag>
|
||||
<a-tag color="pink">{{ t('views.index.abnormal') }}</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -161,6 +161,13 @@ let tableColumns: ColumnsType = [
|
||||
return cdrJSON.calledParty;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('views.dashboard.cdr.result'),
|
||||
dataIndex: 'cdrJSON',
|
||||
key: 'cause',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: t('views.dashboard.cdr.duration'),
|
||||
dataIndex: 'cdrJSON',
|
||||
@@ -175,20 +182,29 @@ let tableColumns: ColumnsType = [
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('views.dashboard.cdr.result'),
|
||||
title: t('views.dashboard.cdr.seizureTime'),
|
||||
dataIndex: 'cdrJSON',
|
||||
key: 'cause',
|
||||
align: 'left',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: t('views.dashboard.cdr.time'),
|
||||
dataIndex: 'cdrJSON',
|
||||
align: 'center',
|
||||
width: 150,
|
||||
width: 200,
|
||||
customRender(opt) {
|
||||
const cdrJSON = opt.value;
|
||||
return parseDateToStr(+cdrJSON.releaseTime * 1000);
|
||||
if (typeof cdrJSON.seizureTime === 'number') {
|
||||
return parseDateToStr(+cdrJSON.seizureTime * 1000);
|
||||
}
|
||||
return cdrJSON.seizureTime;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('views.dashboard.cdr.releaseTime'),
|
||||
dataIndex: 'cdrJSON',
|
||||
align: 'left',
|
||||
width: 200,
|
||||
customRender(opt) {
|
||||
const cdrJSON = opt.value;
|
||||
if (typeof cdrJSON.releaseTime === 'number') {
|
||||
return parseDateToStr(+cdrJSON.releaseTime * 1000);
|
||||
}
|
||||
return cdrJSON.releaseTime;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -725,61 +741,73 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</template>
|
||||
<template #expandedRowRender="{ record }">
|
||||
<div style="width: 46%; padding-left: 32px; padding-bottom: 16px">
|
||||
<a-divider orientation="left">
|
||||
{{ t('views.dashboard.cdr.cdrInfo') }}
|
||||
</a-divider>
|
||||
<div>
|
||||
<span>{{ t('views.ne.common.neName') }}: </span>
|
||||
<span>{{ record.neName }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.ne.common.rmUid') }}: </span>
|
||||
<span>{{ record.rmUID }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.time') }}: </span>
|
||||
<span>{{ parseDateToStr(+record.timestamp * 1000) }}</span>
|
||||
</div>
|
||||
<a-divider orientation="left">
|
||||
{{ t('views.dashboard.cdr.rowInfo') }}
|
||||
</a-divider>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.type') }}: </span>
|
||||
<DictTag
|
||||
:options="dict.cdrCallType"
|
||||
:value="record.cdrJSON.callType"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.duration') }}: </span>
|
||||
<span v-if="record.cdrJSON.callType !== 'sms'">
|
||||
{{ parseDuration(record.cdrJSON.callDuration) }}
|
||||
</span>
|
||||
<span v-else> - </span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.caller') }}: </span>
|
||||
<span>{{ record.cdrJSON.callerParty }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.called') }}: </span>
|
||||
<span>{{ record.cdrJSON.calledParty }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.result') }}: </span>
|
||||
<span v-if="record.cdrJSON.callType !== 'sms'">
|
||||
<a-row :gutter="16">
|
||||
<a-col :lg="5" :md="12" :xs="24">
|
||||
<a-divider orientation="left">
|
||||
{{ t('views.dashboard.cdr.cdrInfo') }}
|
||||
</a-divider>
|
||||
<div>
|
||||
<span>{{ t('views.ne.common.neName') }}: </span>
|
||||
<span>{{ record.neName }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.ne.common.rmUid') }}: </span>
|
||||
<span>{{ record.rmUID }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.time') }}: </span>
|
||||
<span>{{ parseDateToStr(+record.timestamp * 1000) }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :lg="6" :md="12" :xs="24">
|
||||
<a-divider orientation="left">
|
||||
{{ t('views.dashboard.cdr.rowInfo') }}
|
||||
</a-divider>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.type') }}: </span>
|
||||
<DictTag
|
||||
:options="dict.cdrSipCode"
|
||||
:value="record.cdrJSON.cause"
|
||||
value-default="0"
|
||||
:options="dict.cdrCallType"
|
||||
:value="record.cdrJSON.callType"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('views.dashboard.cdr.resultOk') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.duration') }}: </span>
|
||||
<span v-if="record.cdrJSON.callType !== 'sms'">
|
||||
{{ parseDuration(record.cdrJSON.callDuration) }}
|
||||
</span>
|
||||
<span v-else> - </span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.caller') }}: </span>
|
||||
<span>{{ record.cdrJSON.callerParty }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.called') }}: </span>
|
||||
<span>{{ record.cdrJSON.calledParty }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.result') }}: </span>
|
||||
<span v-if="record.cdrJSON.callType !== 'sms'">
|
||||
<DictTag
|
||||
:options="dict.cdrSipCode"
|
||||
:value="record.cdrJSON.cause"
|
||||
value-default="0"
|
||||
/>
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('views.dashboard.cdr.resultOk') }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.seizureTime') }}: </span>
|
||||
<span>{{ record.cdrJSON.seizureTime }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>{{ t('views.dashboard.cdr.releaseTime') }}: </span>
|
||||
<span>{{ record.cdrJSON.releaseTime }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
@@ -316,8 +316,8 @@ function fnModalOk() {
|
||||
.then(e => {
|
||||
modalState.confirmLoading = true;
|
||||
const from = toRaw(modalState.from);
|
||||
from.neId = queryParams.neId || '-';
|
||||
from.algoIndex = `${from.algoIndex}`;
|
||||
from.neId = queryParams.neId || '-';
|
||||
const result = from.id
|
||||
? updateUDMAuth(from)
|
||||
: from.num === 1
|
||||
@@ -484,33 +484,27 @@ function fnRecordDelete(imsi: string) {
|
||||
/**
|
||||
* UDM鉴权用户勾选导出
|
||||
*/
|
||||
function fnRecordExport(type: string = 'txt') {
|
||||
function fnRecordExport(type: string = 'txt') {
|
||||
const selectLen = tableState.selectedRowKeys.length;
|
||||
if (selectLen <= 0) return;
|
||||
const rows: Record<string, any>[] = tableState.data.filter(
|
||||
(row: Record<string, any>) =>
|
||||
tableState.selectedRowKeys.indexOf(row.imsi) >= 0
|
||||
);
|
||||
|
||||
let content = '';
|
||||
if (type == 'txt') {
|
||||
for (const row of rows) {
|
||||
const opc = row.opc === '-' ? '' : `,${row.opc}`;
|
||||
content += `${row.imsi},${row.ki},${row.algoIndex},${row.amf}${opc}\r\n`;
|
||||
}
|
||||
}
|
||||
if (type == 'csv') {
|
||||
content = `IMSI,ki,Algo Index,AMF,OPC\r\n`;
|
||||
for (const row of rows) {
|
||||
const opc = row.opc === '-' ? '' : `,${row.opc}`;
|
||||
content += `${row.imsi},${row.ki},${row.algoIndex},${row.amf}${opc}\r\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||
saveAs(blob, `UDMAuth_${Date.now()}.${type}`);
|
||||
const neId = queryParams.neId;
|
||||
if (!neId) return;
|
||||
const hide = message.loading(t('common.loading'), 0);
|
||||
exportUDMAuth({ type: type, neId: neId, imsis: tableState.selectedRowKeys })
|
||||
.then(res => {
|
||||
if (res.code === RESULT_CODE_SUCCESS) {
|
||||
message.success(t('common.msgSuccess', { msg: t('common.export') }), 3);
|
||||
saveAs(res.data, `UDMAuth_select_${Date.now()}.${type}`);
|
||||
} else {
|
||||
message.error(`${res.msg}`, 3);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hide();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**列表导出全部数据 */
|
||||
function fnExportList(type: string) {
|
||||
const neId = queryParams.neId;
|
||||
|
||||
@@ -780,9 +780,6 @@ onMounted(() => {
|
||||
ok-text="TXT"
|
||||
ok-type="default"
|
||||
@confirm="fnExportList('txt')"
|
||||
:show-cancel="false"
|
||||
cancel-text="CSV"
|
||||
@cancel="fnExportList('csv')"
|
||||
>
|
||||
<a-button type="dashed">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
|
||||
@@ -625,6 +625,10 @@ function fnModalOk() {
|
||||
.map((item: number) => `${item}`.padStart(2, '0'))
|
||||
.join('');
|
||||
|
||||
from.activeTime = `${from.activeTime}`;
|
||||
from.rfspIndex = `${from.rfspIndex}`;
|
||||
from.regTimer = `${from.regTimer}`;
|
||||
from.ueUsageType = `${from.ueUsageType}`;
|
||||
from.neId = queryParams.neId || '-';
|
||||
const result = from.id
|
||||
? updateUDMSub(from)
|
||||
@@ -827,49 +831,30 @@ function fnRecordDelete(imsi: string) {
|
||||
/**
|
||||
* UDM签约用户导出
|
||||
*/
|
||||
function fnRecordExport(type: string = 'txt') {
|
||||
function fnRecordExport(type: string = 'txt') {
|
||||
const selectLen = tableState.selectedRowKeys.length;
|
||||
if (selectLen <= 0) return;
|
||||
const rows: Record<string, any>[] = tableState.data.filter(
|
||||
(row: Record<string, any>) =>
|
||||
tableState.selectedRowKeys.indexOf(row.imsi) >= 0
|
||||
);
|
||||
|
||||
let content = '';
|
||||
if (type == 'txt') {
|
||||
for (const row of rows) {
|
||||
const epsDat = [
|
||||
row.epsFlag,
|
||||
row.epsOdb,
|
||||
row.hplmnOdb,
|
||||
row.ard,
|
||||
row.epstpl,
|
||||
row.contextId,
|
||||
row.apnContext,
|
||||
row.staticIp,
|
||||
].join(',');
|
||||
content += `${row.imsi},${row.msisdn},${row.ambr},${row.nssai},${row.arfb},${row.sar},${row.rat},${row.cn},${row.smfSel},${row.smData},${epsDat}\r\n`;
|
||||
}
|
||||
}
|
||||
if (type == 'csv') {
|
||||
content = `imsi,msisdn,ambr,nssai,arfb,sar,rat,cn,smf_sel,sm_dat,eps_dat\r\n`;
|
||||
for (const row of rows) {
|
||||
const epsDat = [
|
||||
row.epsFlag,
|
||||
row.epsOdb,
|
||||
row.hplmnOdb,
|
||||
row.ard,
|
||||
row.epstpl,
|
||||
row.contextId,
|
||||
row.apnContext,
|
||||
row.staticIp,
|
||||
].join(',');
|
||||
content += `${row.imsi},${row.msisdn},${row.ambr},${row.nssai},${row.arfb},${row.sar},${row.rat},${row.cn},${row.smfSel},${row.smData},${epsDat}\r\n`;
|
||||
}
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
||||
saveAs(blob, `UDMSub_${Date.now()}.${type}`);
|
||||
const neId = queryParams.neId;
|
||||
if (!neId) return;
|
||||
const hide = message.loading(t('common.loading'), 0);
|
||||
exportUDMSub({ type: type, neId: neId, imsis: tableState.selectedRowKeys })
|
||||
.then(res => {
|
||||
if (res.code === RESULT_CODE_SUCCESS) {
|
||||
message.success({
|
||||
content: t('common.msgSuccess', { msg: t('common.export') }),
|
||||
duration: 2,
|
||||
});
|
||||
saveAs(res.data, `UDMSub_select_${Date.now()}.${type}`);
|
||||
} else {
|
||||
message.error({
|
||||
content: `${res.msg}`,
|
||||
duration: 2,
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
hide();
|
||||
});
|
||||
}
|
||||
|
||||
/**列表导出 */
|
||||
|
||||
@@ -1,16 +1,791 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { PageContainer } from 'antdv-pro-layout';
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { LegendComponent } from 'echarts/components';
|
||||
import { LineChart, BarChart } from 'echarts/charts';
|
||||
import { GridComponent, TooltipComponent, TitleComponent } from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { getKPITitle, listKPIData } from '@/api/perfManage/goldTarget';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||
import { generateColorRGBA } from '@/utils/generate-utils';
|
||||
import { BarChartOutlined, LineChartOutlined, UnorderedListOutlined, DownOutlined, MoreOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
// 在这里定义 ChartDataItem 接口
|
||||
interface ChartDataItem {
|
||||
date: string; // 将存储完整的时间字符串,包含时分秒
|
||||
[kpiId: string]: string | number; // 动态指标
|
||||
}
|
||||
|
||||
echarts.use([
|
||||
LineChart,
|
||||
BarChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
TitleComponent,
|
||||
CanvasRenderer,
|
||||
LegendComponent
|
||||
]);
|
||||
// WebSocket连接
|
||||
const ws = ref<WS | null>(null);
|
||||
|
||||
//日期范围响应式变量
|
||||
const dateRange = ref<[string, string]>([
|
||||
dayjs().startOf('day').valueOf().toString(),
|
||||
dayjs().valueOf().toString()
|
||||
]);
|
||||
//实时数据状态
|
||||
const isRealtime = ref(false);
|
||||
//图表数据响应式数组
|
||||
const chartData = ref<ChartDataItem[]>([]);
|
||||
|
||||
//储存Echarts的实例的变量
|
||||
let chart: echarts.ECharts | null = null;
|
||||
|
||||
//observer 变量 监听图表容器大小
|
||||
let observer: ResizeObserver | null = null;
|
||||
|
||||
//日期变化时更新图表数据
|
||||
const handleDateChange = (
|
||||
value: [string, string] | [Dayjs, Dayjs],
|
||||
dateStrings: [string, string]
|
||||
) => {
|
||||
if (!dateStrings[0] || !dateStrings[1]) {
|
||||
console.warn('Invalid date strings:', dateStrings);
|
||||
return;
|
||||
}
|
||||
|
||||
dateRange.value = [
|
||||
dayjs(dateStrings[0]).valueOf().toString(),
|
||||
dayjs(dateStrings[1]).valueOf().toString()
|
||||
];
|
||||
fetchChartData();
|
||||
};
|
||||
|
||||
//切换实时数据
|
||||
const toggleRealtime = () => {
|
||||
fnRealTimeSwitch(isRealtime.value);
|
||||
};
|
||||
|
||||
// 定义所有网元类型
|
||||
const ALL_NE_TYPES = ['AMF', 'SMF', 'UPF', 'MME', 'IMS', 'SMSC'] as const;
|
||||
type NeType = typeof ALL_NE_TYPES[number];
|
||||
|
||||
// 定义要筛选的指标 ID,按网元类型组织
|
||||
const TARGET_KPI_IDS: Record<NeType, string[]> = {
|
||||
AMF: ['AMF.02', 'AMF.03', 'AMF.A.07', 'AMF.A.08'],
|
||||
SMF: ['SMF.02', 'SMF.03', 'SMF.04', 'SMF.05'],
|
||||
UPF: ['UPF.03', 'UPF.04', 'UPF.05', 'UPF.06'],
|
||||
MME: ['MME.A.01', 'MME.A.02', 'MME.A.03'],
|
||||
IMS: ['SCSCF.01', 'SCSCF.02', 'SCSCF.05', 'SCSCF.06'],
|
||||
SMSC: ['SMSC.A.01', 'SMSC.A.02', 'SMSC.A.03']
|
||||
};
|
||||
|
||||
// 实时数据开关函数
|
||||
const fnRealTimeSwitch = (bool: boolean) => {
|
||||
if (bool) {
|
||||
if(!ws.value) {
|
||||
ws.value = new WS();
|
||||
}
|
||||
chartData.value = [];
|
||||
|
||||
const options: OptionsType = {
|
||||
url: '/ws',
|
||||
params: {
|
||||
subGroupID: ALL_NE_TYPES.map(type => `10_${type}_001`).join(','),
|
||||
},
|
||||
onmessage: wsMessage,
|
||||
onerror: wsError,
|
||||
};
|
||||
ws.value.connect(options);
|
||||
} else if(ws.value) {
|
||||
ws.value.close();//断开链接
|
||||
ws.value = null;//清空链接
|
||||
}
|
||||
}
|
||||
|
||||
// 接收数据后错误回调
|
||||
const wsError = () => {
|
||||
message.error(t('common.websocketError'));
|
||||
}
|
||||
|
||||
// 接收数据后回调
|
||||
const wsMessage = (res: Record<string, any>) => {
|
||||
const { code, data } = res;
|
||||
if (code === RESULT_CODE_ERROR) {
|
||||
console.warn(res.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data?.groupId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const kpiEvent = data.data;
|
||||
if (!kpiEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构造新的数据点
|
||||
const newData: ChartDataItem = {
|
||||
date: kpiEvent.timeGroup
|
||||
? kpiEvent.timeGroup.toString()
|
||||
: Date.now().toString()
|
||||
};
|
||||
|
||||
// 只添加已选中的指标的数据
|
||||
selectedKPIs.value.forEach(kpiId => {
|
||||
if (kpiEvent[kpiId] !== undefined) {
|
||||
newData[kpiId] = Number(kpiEvent[kpiId]);
|
||||
} else {
|
||||
newData[kpiId] = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 更新数据
|
||||
updateChartData(newData);
|
||||
};
|
||||
|
||||
// 获取图表数据方法
|
||||
const fetchChartData = async () => {
|
||||
if (kpiColumns.value.length === 0) {
|
||||
console.warn('No KPI columns available');
|
||||
updateChart();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const [startTime, endTime] = dateRange.value;
|
||||
|
||||
if (!startTime || !endTime) {
|
||||
console.warn('Invalid date range:', dateRange.value);
|
||||
return;
|
||||
}
|
||||
|
||||
const allData: any[] = [];
|
||||
|
||||
// 使用 ALL_NE_TYPES 遍历网元类型
|
||||
for (const neType of ALL_NE_TYPES) {
|
||||
const params = {
|
||||
neType,
|
||||
neId: '001',
|
||||
startTime: String(startTime),
|
||||
endTime: String(endTime),
|
||||
sortField: 'timeGroup',
|
||||
sortOrder: 'asc',
|
||||
interval: 5,
|
||||
kpiIds: TARGET_KPI_IDS[neType].join(',')
|
||||
};
|
||||
|
||||
|
||||
|
||||
const res = await listKPIData(params);
|
||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||
allData.push(...res.data);
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间分组合数据
|
||||
const groupedData = new Map<string, any>();
|
||||
allData.forEach(item => {
|
||||
const timeKey = item.timeGroup;
|
||||
if (!groupedData.has(timeKey)) {
|
||||
groupedData.set(timeKey, { timeGroup: timeKey });
|
||||
}
|
||||
const existingData = groupedData.get(timeKey);
|
||||
Object.assign(existingData, item);
|
||||
});
|
||||
|
||||
// 直接将处理后的数据赋值给 chartData.value
|
||||
chartData.value = Array.from(groupedData.values())
|
||||
.sort((a, b) => Number(a.timeGroup) - Number(b.timeGroup))
|
||||
.map(item => {
|
||||
const dataItem: ChartDataItem = {
|
||||
date: item.timeGroup.toString(),
|
||||
};
|
||||
kpiColumns.value.forEach(kpi => {
|
||||
dataItem[kpi.kpiId] = Number(item[kpi.kpiId]) || 0;
|
||||
});
|
||||
return dataItem;
|
||||
});
|
||||
|
||||
updateChart();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch chart data:', error);
|
||||
message.error(t('common.getInfoFail'));
|
||||
}
|
||||
};
|
||||
|
||||
// 添加一个 Map 来存储每个指标的临时固定颜色
|
||||
const kpiColors = new Map<string, string>();
|
||||
|
||||
// 定义图表类型的响应式变量
|
||||
const chartType = ref<'line' | 'bar'>('line');
|
||||
|
||||
// 添加切换图表类型的方法
|
||||
const toggleChartType = () => {
|
||||
chartType.value = chartType.value === 'line' ? 'bar' : 'line';
|
||||
updateChart();
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chart || !kpiColumns.value.length) return;//首先检查图表实例和指标是否存在
|
||||
//过滤出已选择的指标列
|
||||
const filteredColumns = kpiColumns.value.filter(col => selectedKPIs.value.includes(col.kpiId));
|
||||
const legendData = filteredColumns.map(item => item.title);//创建图例数据数组,包含所有选中的指标的标题
|
||||
//为每个选中的指标创建一个系列配置
|
||||
const series = filteredColumns.map(item => {
|
||||
const color = kpiColors.get(item.kpiId) || generateColorRGBA();
|
||||
if (!kpiColors.has(item.kpiId)) {
|
||||
kpiColors.set(item.kpiId, color);//保持指标颜色的临时一致性
|
||||
}
|
||||
|
||||
return {
|
||||
name: item.title,
|
||||
type: chartType.value, // 使用当前选择的图表类型
|
||||
data: chartData.value.length > 0
|
||||
? chartData.value.map(dataItem => dataItem[item.kpiId] || 0)
|
||||
: [0],
|
||||
smooth: chartType.value === 'line', // 只在折线图时使用平滑
|
||||
symbol: chartType.value === 'line' ? 'circle' : undefined, // 只在折线图时显示标记
|
||||
symbolSize: chartType.value === 'line' ? 6 : undefined,
|
||||
showSymbol: chartType.value === 'line',
|
||||
itemStyle: { color }
|
||||
};
|
||||
});
|
||||
//图表配置对象
|
||||
const option = {
|
||||
title: {
|
||||
text: t('views.perfManage.kpiOverView.kpiChartTitle'),
|
||||
left: 'center'
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
position: function(pt: any) {
|
||||
return [pt[0], '10%'];
|
||||
},
|
||||
},
|
||||
legend: {//图例配置
|
||||
data: legendData,
|
||||
type: 'scroll',
|
||||
orient: 'horizontal',
|
||||
top: 25,
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
},
|
||||
selected: Object.fromEntries(legendData.map(name => [name, true])),
|
||||
show: true,
|
||||
left: 'center',
|
||||
width: '80%',
|
||||
height: 50,
|
||||
padding: [5, 10],
|
||||
},
|
||||
grid: { //网格配置
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: 100,
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: { //x轴配置
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: chartData.value.length > 0
|
||||
? chartData.value.map(item => {
|
||||
// 将时间戳转换为包含时分秒的格式
|
||||
return dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss');
|
||||
})
|
||||
: [''],
|
||||
axisLabel: {
|
||||
formatter: (value: string) => {
|
||||
// 自定义 x 轴标签的显示格式
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
rotate: 0,
|
||||
interval: 'auto', // 自动计算显示间隔
|
||||
align: 'right'
|
||||
}
|
||||
},
|
||||
yAxis: { // y轴配置
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
formatter: '{value}'
|
||||
},
|
||||
// 添加自动计算的分割段数
|
||||
splitNumber: 5,
|
||||
// 添加自动计算的最小/最大值围
|
||||
scale: true
|
||||
},
|
||||
series: series //配置数据
|
||||
};
|
||||
|
||||
chart.setOption(option);//使用新的配置更新图表
|
||||
chart.resize();//调整图表大小以适应容器
|
||||
|
||||
// 如果已经有 observer,先断开连接
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
|
||||
// 创建新的 ResizeObserver
|
||||
observer = new ResizeObserver(() => {
|
||||
if (chart) {
|
||||
chart.resize();
|
||||
}
|
||||
});
|
||||
|
||||
// 观察图表容器
|
||||
const container = document.getElementById('chartContainer');
|
||||
if (container) {
|
||||
observer.observe(container);
|
||||
}
|
||||
};
|
||||
|
||||
//钩子函数
|
||||
onMounted(async () => {
|
||||
try {
|
||||
// 获取所有网元的指标
|
||||
await fetchSpecificKPI();
|
||||
await nextTick();
|
||||
|
||||
const container = document.getElementById('chartContainer');
|
||||
if (container && !chart) {
|
||||
chart = echarts.init(container);
|
||||
|
||||
if (kpiColumns.value.length > 0) {
|
||||
|
||||
updateChart();
|
||||
await fetchChartData();
|
||||
} else {
|
||||
console.warn('No KPI columns available after fetching');
|
||||
}
|
||||
} else if (chart) {
|
||||
console.warn('Chart already initialized, skipping initialization');
|
||||
} else {
|
||||
console.error('Chart container not found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize:', error);
|
||||
message.error(t('common.initFail'));
|
||||
}
|
||||
});
|
||||
|
||||
// 定义指列类型
|
||||
interface KPIColumn {
|
||||
title: string;
|
||||
dataIndex: string;
|
||||
key: string;
|
||||
kpiId: string;
|
||||
neType: string; // 添加网元类型字段
|
||||
}
|
||||
|
||||
// 存储指标列信
|
||||
const kpiColumns = ref<KPIColumn[]>([]);
|
||||
// 添加选中指标的的状态
|
||||
const selectedKPIs = ref<string[]>([]);
|
||||
// 添加对话框可见性状态
|
||||
const isModalVisible = ref(false);
|
||||
// 添加临时存储下拉框选择的数组
|
||||
const tempSelectedKPIs = ref<string[]>([]);
|
||||
|
||||
// 添加一个变量保存打开对话框时的选择状态
|
||||
const originalSelectedKPIs = ref<string[]>([]);
|
||||
|
||||
// 打开对话框的方法
|
||||
const showKPISelector = () => {
|
||||
// 保存当前的选择状态
|
||||
originalSelectedKPIs.value = [...selectedKPIs.value];
|
||||
|
||||
// 初始化临时选择为当前已选择的其他指标
|
||||
const primaryKPIs = Object.values(TARGET_KPI_IDS).flat();
|
||||
tempSelectedKPIs.value = selectedKPIs.value.filter(kpiId =>
|
||||
!primaryKPIs.includes(kpiId)
|
||||
);
|
||||
isModalVisible.value = true;
|
||||
};
|
||||
|
||||
// 保存选中指标到 localStorage 的方法
|
||||
const saveSelectedKPIs = () => {
|
||||
localStorage.setItem('selectedKPIs', JSON.stringify(selectedKPIs.value));
|
||||
};
|
||||
|
||||
// 取消按钮的处理方法
|
||||
const handleModalCancel = () => {
|
||||
// 恢复到打开对话框时的选择状态
|
||||
selectedKPIs.value = [...originalSelectedKPIs.value];
|
||||
// 清空临时选择
|
||||
tempSelectedKPIs.value = [];
|
||||
isModalVisible.value = false;
|
||||
};
|
||||
|
||||
// 确认按钮的处理方法
|
||||
const handleModalOk = () => {
|
||||
// 获取主要指标列表
|
||||
const primaryKPIs = Object.values(TARGET_KPI_IDS).flat();
|
||||
|
||||
// 获取当前在主界面选中的主要指标
|
||||
const selectedPrimaryKPIs = selectedKPIs.value.filter(kpiId =>
|
||||
primaryKPIs.includes(kpiId)
|
||||
);
|
||||
|
||||
// 合并选中的主要指标和临时选中的其他指标
|
||||
selectedKPIs.value = Array.from(new Set([
|
||||
...selectedPrimaryKPIs, // 只包含已选中的主要指标
|
||||
...tempSelectedKPIs.value // 临时选中的其他指标
|
||||
]));
|
||||
|
||||
// 清空临时选择和原始选择
|
||||
tempSelectedKPIs.value = [];
|
||||
originalSelectedKPIs.value = [];
|
||||
|
||||
// 保存选择并更新图表
|
||||
saveSelectedKPIs();
|
||||
updateChart();
|
||||
isModalVisible.value = false;
|
||||
};
|
||||
|
||||
// 获取网元指标
|
||||
const fetchSpecificKPI = async () => {
|
||||
const language = currentLocale.value.split('_')[0] === 'zh' ? 'cn' : currentLocale.value.split('_')[0];
|
||||
|
||||
try {
|
||||
let allKPIs: KPIColumn[] = [];
|
||||
|
||||
// 1. 获取所有网元的全部指标
|
||||
for (const neType of ALL_NE_TYPES) {
|
||||
const res = await getKPITitle(neType.toUpperCase());
|
||||
|
||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||
// 转换指标格式
|
||||
const formattedKPIs = res.data.map(item => ({
|
||||
title: item[`${language}Title`],
|
||||
dataIndex: item.kpiId,
|
||||
key: item.kpiId,
|
||||
kpiId: item.kpiId,
|
||||
neType: neType // 添加网元类型信息
|
||||
}));
|
||||
|
||||
// 添加到所有指标数组
|
||||
allKPIs = [...allKPIs, ...formattedKPIs];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 更新所有指标到 kpiColumns
|
||||
kpiColumns.value = allKPIs;
|
||||
|
||||
// 3. 尝试加载保存的选择
|
||||
const savedKPIs = localStorage.getItem('selectedKPIs');
|
||||
if (savedKPIs) {
|
||||
// 确保保存的选择仍然存在于当前指标中
|
||||
const validSavedKPIs = JSON.parse(savedKPIs).filter(
|
||||
(kpiId: string) => kpiColumns.value.some(col => col.kpiId === kpiId)
|
||||
);
|
||||
if (validSavedKPIs.length > 0) {
|
||||
selectedKPIs.value = validSavedKPIs;
|
||||
} else {
|
||||
// 如果没有有效的保存选择,则默认选择<E98089><E68BA9>要指标
|
||||
selectedKPIs.value = Object.values(TARGET_KPI_IDS).flat();
|
||||
}
|
||||
} else {
|
||||
// 如果没有保存的选择,则默认选择重要指标
|
||||
selectedKPIs.value = Object.values(TARGET_KPI_IDS).flat();
|
||||
}
|
||||
|
||||
if (kpiColumns.value.length === 0) {
|
||||
console.warn('No KPIs found');
|
||||
} else {
|
||||
console.log(`Found ${kpiColumns.value.length} total KPIs`);
|
||||
}
|
||||
|
||||
return kpiColumns.value;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch KPI titles:', error);
|
||||
message.error(t('common.getInfoFail'));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// onUnmounted 钩子
|
||||
onUnmounted(() => {
|
||||
if(ws.value && ws.value.state() === WebSocket.OPEN) {
|
||||
ws.value.close();
|
||||
}
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
observer = null;
|
||||
}
|
||||
if (chart) {
|
||||
chart.dispose();
|
||||
chart = null;
|
||||
}
|
||||
// 可选:在组件卸载时保存选择
|
||||
saveSelectedKPIs();
|
||||
});
|
||||
|
||||
const { t, currentLocale } = useI18n();
|
||||
|
||||
// 更新图表数据方法
|
||||
const updateChartData = (newData: ChartDataItem) => {
|
||||
chartData.value.push(newData);
|
||||
if (chartData.value.length > 100) {
|
||||
chartData.value.shift();
|
||||
}
|
||||
|
||||
if (chart) {
|
||||
chart.setOption({
|
||||
xAxis: {
|
||||
data: chartData.value.map(item =>
|
||||
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
|
||||
)
|
||||
},
|
||||
series: selectedKPIs.value.map(kpiId => ({
|
||||
type: chartType.value, // 使用当前选择的图表类型
|
||||
data: chartData.value.map(item => item[kpiId] || 0)
|
||||
}))
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// groupedKPIs 计算属性,使用 TARGET_KPI_IDS 来分组过滤
|
||||
const groupedKPIs = computed(() => {
|
||||
const groups: Record<string, KPIColumn[]> = {};
|
||||
|
||||
ALL_NE_TYPES.forEach(neType => {
|
||||
// 使用 TARGET_KPI_IDS 中定义的指标 ID 来过滤
|
||||
const targetIds = TARGET_KPI_IDS[neType];
|
||||
groups[neType] = kpiColumns.value.filter(kpi =>
|
||||
targetIds.includes(kpi.kpiId)
|
||||
);
|
||||
});
|
||||
return groups;
|
||||
});
|
||||
|
||||
// 计算其他指标
|
||||
const secondaryKPIs = computed(() => {
|
||||
const groups: Record<string, KPIColumn[]> = {};
|
||||
|
||||
if (kpiColumns.value.length === 0) {
|
||||
console.warn('No KPI columns available');
|
||||
return groups;
|
||||
}
|
||||
|
||||
ALL_NE_TYPES.forEach(neType => {
|
||||
// 获取当前网元类型的主要指标 ID
|
||||
const primaryIds = TARGET_KPI_IDS[neType];
|
||||
|
||||
// 从所有指标中筛选出当前网元其他指标
|
||||
groups[neType] = kpiColumns.value.filter(kpi => {
|
||||
// 检查是否不在主要指标列表中
|
||||
const isNotPrimary = !primaryIds.includes(kpi.kpiId);
|
||||
|
||||
// 检查是否属于当前网元类型
|
||||
// 使用 getKPITitle API 返回的原始数据中的网元类型信息
|
||||
const isCurrentNeType = kpi.neType === neType;
|
||||
|
||||
return isCurrentNeType && isNotPrimary;
|
||||
});
|
||||
});
|
||||
return groups;
|
||||
});
|
||||
|
||||
// 添加处理其他指标选择变化的方法
|
||||
const handleSecondaryKPIChange = (kpiId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
// 如果选中,将指标 ID 添加到临时列表
|
||||
if (!tempSelectedKPIs.value.includes(kpiId)) {
|
||||
tempSelectedKPIs.value = [...tempSelectedKPIs.value, kpiId];
|
||||
}
|
||||
} else {
|
||||
// 如果取消选中,从临时列表中移除指标 ID
|
||||
tempSelectedKPIs.value = tempSelectedKPIs.value.filter(id => id !== kpiId);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-card :bordered="false" :body-style="{ padding: '0px' }">
|
||||
<h1>kpiOverView</h1>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
<div class="kpi-overview">
|
||||
<div class="controls">
|
||||
<a-range-picker
|
||||
v-model:value="dateRange"
|
||||
:show-time="{ format: 'HH:mm:ss' }"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
:value-format="'x'"
|
||||
:disabled="isRealtime"
|
||||
@change="handleDateChange"
|
||||
/>
|
||||
<a-button @click="showKPISelector">
|
||||
<template #icon>
|
||||
<unordered-list-outlined />
|
||||
</template>
|
||||
{{t('views.perfManage.kpiOverView.chooseMetrics')}}
|
||||
</a-button>
|
||||
<a-button @click="toggleChartType">
|
||||
<template #icon>
|
||||
<bar-chart-outlined v-if="chartType === 'line'" />
|
||||
<line-chart-outlined v-else />
|
||||
</template>
|
||||
{{ chartType === 'line' ? t('views.perfManage.kpiOverView.changeLine') : t('views.perfManage.kpiOverView.changeBar') }}
|
||||
</a-button>
|
||||
<a-form-item :label="isRealtime ? t('views.dashboard.cdr.realTimeDataStart') : t('views.dashboard.cdr.realTimeDataStop')">
|
||||
<a-switch
|
||||
v-model:checked="isRealtime"
|
||||
@change="toggleRealtime"
|
||||
/>
|
||||
</a-form-item>
|
||||
</div>
|
||||
<div id="chartContainer" class="chart-container"></div>
|
||||
|
||||
<!-- 修改指标选择对话框 -->
|
||||
<a-modal
|
||||
v-model:visible="isModalVisible"
|
||||
title="选择要显示的指标"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
width="800px"
|
||||
:bodyStyle="{ maxHeight: '600px', overflow: 'auto' }"
|
||||
>
|
||||
<a-checkbox-group v-model:value="selectedKPIs">
|
||||
<div class="kpi-checkbox-list">
|
||||
<a-card
|
||||
v-for="neType in ALL_NE_TYPES"
|
||||
:key="neType"
|
||||
class="ne-type-card"
|
||||
:bordered="false"
|
||||
>
|
||||
<template #title>
|
||||
<span class="card-title">{{ neType.toUpperCase() }}</span>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-dropdown v-if="secondaryKPIs[neType]?.length" trigger="click">
|
||||
<a-button type="link" size="small">
|
||||
<more-outlined />
|
||||
<down-outlined />
|
||||
<span class="secondary-count">({{ secondaryKPIs[neType].length }})</span>
|
||||
</a-button>
|
||||
<template >
|
||||
<div class="secondary-kpi-menu" @click.stop>
|
||||
<div
|
||||
v-for="kpi in secondaryKPIs[neType]"
|
||||
:key="kpi.kpiId"
|
||||
class="secondary-kpi-item"
|
||||
@click.stop
|
||||
>
|
||||
<a-checkbox
|
||||
:value="kpi.kpiId"
|
||||
:checked="tempSelectedKPIs.includes(kpi.kpiId)"
|
||||
@change="(e) => handleSecondaryKPIChange(kpi.kpiId, e.target.checked)"
|
||||
@click.stop
|
||||
>
|
||||
{{ kpi.title }}
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<div class="ne-type-items">
|
||||
<div
|
||||
v-for="kpi in groupedKPIs[neType]"
|
||||
:key="kpi.kpiId"
|
||||
class="kpi-checkbox-item"
|
||||
>
|
||||
<a-checkbox :value="kpi.kpiId">
|
||||
{{ kpi.title }}
|
||||
</a-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</a-checkbox-group>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style scoped>
|
||||
/* 基础布局样式 */
|
||||
.kpi-overview {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: calc(100vh - 160px);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 指标选择对话框样式 */
|
||||
.kpi-checkbox-list {
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 网元指标列表样式 */
|
||||
.ne-type-items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.kpi-checkbox-item {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 其他指标下拉菜单样式 */
|
||||
.secondary-kpi-menu {
|
||||
background: #fff;
|
||||
padding: 4px 0;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 6px -4px rgba(0,0,0,.12), 0 6px 16px 0 rgba(0,0,0,.08);
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.secondary-kpi-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 组件统一样式 */
|
||||
:deep(.ant-form-item),
|
||||
:deep(.ant-picker),
|
||||
:deep(.ant-btn) {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ant-checkbox-wrapper) {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 添加次要指标数量的样式 */
|
||||
.secondary-count {
|
||||
margin-left: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user