Files
fe.ems.vue3/src/views/dashboard/overview2/index.vue
2025-08-28 09:52:25 +08:00

877 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import svgBase from '@/assets/svg/base.svg';
import svgUserIMS from '@/assets/svg/userIMS.svg';
import svgUserSMF from '@/assets/svg/userSMF.svg';
import useI18n from '@/hooks/useI18n';
import Topology from './components/Topology/index.vue';
import NeResources from './components/NeResources/index.vue';
import UserActivity from './components/UserActivity/index.vue';
import IMSActivity from './components/IMSActivity/index.vue';
import AlarnTypeBar from './components/AlarnTypeBar/index.vue';
import UPFFlow from './components/UPFFlow/index.vue';
import { listUDMSub } from '@/api/neData/udm_sub';
import { listSMFSubNum } from '@/api/neData/smf';
import { listIMSSessionNum } from '@/api/neData/ims';
import { listAMFNblist } from '@/api/neData/amf';
import { listMMENblist } from '@/api/neData/mme';
import {
graphNodeClickID,
graphState,
notNeNodes,
graphNodeStateNum,
neStateRequestMap,
} from './hooks/useTopology';
import { upfTotalFlow, upfTFActive } from './hooks/useUPFTotalFlow';
import { useFullscreen } from '@vueuse/core';
import useWS from './hooks/useWS';
import useAppStore from '@/store/modules/app';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { useRouter } from 'vue-router';
import useNeListStore from '@/store/modules/ne_list';
import { message } from 'ant-design-vue';
import { upfWhoId } from './hooks/useWS';
import { listAMFNbStatelist } from '@/api/neData/amf';
import { listMMENbStatelist } from '@/api/neData/mme';
const neListStore = useNeListStore();
const router = useRouter();
const appStore = useAppStore();
const { t } = useI18n();
const { wsSend, userActivitySend, upfTFSend, reSendUPF } = useWS();
/**概览状态类型 */
type SkimStateType = {
/**UDM签约用户数量 */
udmSubNum: number;
/**SMF在线用户数 */
smfUeNum: number;
/**IMS在线用户数 */
imsUeNum: number;
/**5G基站数量 */
gnbNum: number;
/**5G在线用户数量 */
gnbUeNum: number;
/**4G基站数量 */
enbNum: number;
/**4G在线用户数量 */
enbUeNum: number;
/**5G用户总数量 */
gNbSumNum: number;
/**4G用户总数量 */
eNbSumNum: number;
};
/**概览状态信息 */
let skimState: SkimStateType = reactive({
udmSubNum: 0,
smfUeNum: 0,
imsUeNum: 0,
gnbNum: 0,
gnbUeNum: 0,
enbNum: 0,
enbUeNum: 0,
gNbSumNum: 0,
eNbSumNum: 0,
});
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**总览节点 */
const viewportDom = ref<HTMLElement | null>(null);
const { isFullscreen, toggle } = useFullscreen(viewportDom);
let initFlag = false;
/**10s调度器 */
const interval10s = ref<any>(null);
/**5s调度器 */
const interval5s = ref<any>(null);
/**查询网元状态 */
function fnGetNeState() {
// 获取节点状态
for (const node of graphState.data.nodes) {
if (notNeNodes.includes(node.id)) continue;
const neInfoList = node.neInfoList || [];
if (neInfoList.length === 0) continue;
for (const neInfo of neInfoList) {
if (!neInfo.neType || !neInfo.neId) continue;
wsSend({
requestId: `neState_${neInfo.neType}_${neInfo.neId}`,
type: 'ne_state',
data: {
neType: neInfo.neType,
neId: neInfo.neId,
},
});
}
}
}
/**获取概览信息 */
async function fnGetSkim() {
let tempGnbSumNum = 0;
let tempEnbSumNum = 0;
const neHandlers = new Map([
// [
// 'UDM',
// {
// request: (neId: string) =>
// listUDMSub({ neId: neId, pageNum: 1, pageSize: 1 }),
// process: (res: any) =>
// res.code === RESULT_CODE_SUCCESS &&
// (skimState.udmSubNum += res.data.total),
// },
// ],
[
'SMF',
{
request: (neId: string) => listSMFSubNum(neId),
process: (res: any) => {
if (
res.code === RESULT_CODE_SUCCESS &&
typeof res.data === 'number'
) {
skimState.smfUeNum += res.data;
}
},
},
],
[
'IMS',
{
request: (neId: string) => listIMSSessionNum(neId),
process: (res: any) => {
if (
res.code === RESULT_CODE_SUCCESS &&
typeof res.data === 'number'
) {
skimState.imsUeNum += res.data;
}
},
},
],
// [
// 'AMF',
// {
// request: (neId: string) => listAMFNblist({ neId }),
// process: (res: any) => {
// if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// skimState.gnbNum += res.data.length;
// skimState.gnbUeNum += res.data.reduce(
// (sum: number, item: any) => sum + item.ueNum,
// 0
// );
// }
// },
// },
// ],
[
'AMF',
{
request: (neId: string) => listAMFNblist({ neId }),
process: async (res: any, neId: any) => {
console.log(neId);
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
skimState.gnbNum += res.data.length;
skimState.gnbUeNum += res.data.reduce(
(sum: number, item: any) => sum + item.ueNum,
0
);
const amfNbRes = await listAMFNbStatelist({ neId });
console.log(amfNbRes);
if (
amfNbRes.code === RESULT_CODE_SUCCESS &&
Array.isArray(amfNbRes.data)
) {
// skimState.gNbSumNum += amfNbRes.data.length;
tempGnbSumNum += amfNbRes.data.length;
}
}
},
},
],
// [
// 'MME',
// {
// request: (neId: string) => listMMENblist({ neId }),
// process: (res: any) => {
// if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// skimState.enbNum += res.data.length;
// skimState.enbUeNum += res.data.reduce(
// (sum: number, item: any) => sum + item.ueNum,
// 0
// );
// }
// },
// },
// ],
[
'MME',
{
request: (neId: string) => listMMENblist({ neId }),
process: async (res: any, neId: any) => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
skimState.enbNum += res.data.length;
skimState.enbUeNum += res.data.reduce(
(sum: number, item: any) => sum + item.ueNum,
0
);
const mmeNbRes = await listMMENbStatelist({ neId });
console.log(mmeNbRes);
if (
mmeNbRes.code === RESULT_CODE_SUCCESS &&
Array.isArray(mmeNbRes.data)
) {
// skimState.eNbSumNum += mmeNbRes.data.length;
console.log(mmeNbRes);
tempEnbSumNum += mmeNbRes.data.length;
}
}
},
},
],
]);
console.log(neCascaderOptions);
const requests = neCascaderOptions.value.flatMap(
(ne: any) =>
ne.children
?.map((child: any) => {
console.log(child.neId);
const handler = neHandlers.get(child.neType);
return handler
? {
promise: handler.request(child.neId),
process: handler.process,
neId: child.neId, // 这里加上neId
}
: null;
})
.filter(Boolean) || []
);
const results = await Promise.allSettled(requests.map(r => r.promise));
// 重置
Object.assign(skimState, {
//udmSubNum: 0,
smfUeNum: 0,
imsUeNum: 0,
gnbNum: 0,
gnbUeNum: 0,
enbNum: 0,
enbUeNum: 0,
});
// results.forEach((result, index) => {
// if (result.status === 'fulfilled') {
// requests[index].process(result.value);
// } else {
// requests[index].process(0);
// }
// });
const processPromises = results.map((result: any, index: any) => {
const req = requests[index];
if (result.status === 'fulfilled') {
return req.process(result.value, req.neId);
} else {
return req.process(0, req.neId);
}
});
await Promise.all(processPromises);
skimState.gNbSumNum = tempGnbSumNum;
skimState.eNbSumNum = tempEnbSumNum;
// UDM
listUDMSub({ neId: udmNeId.value, pageNum: 1, pageSize: 1 }).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
skimState.udmSubNum = res.data.total;
}
});
}
/**初始数据函数 */
function loadData() {
fnGetNeState(); // 获取网元状态
userActivitySend();
upfTFSend('0');
upfTFSend('7');
upfTFSend('30');
clearInterval(interval10s.value);
interval10s.value = setInterval(() => {
if (!interval10s.value || !initFlag) return;
if (upfTFActive.value === '0') {
upfTFActive.value = '7';
} else if (upfTFActive.value === '7') {
upfTFActive.value = '30';
} else if (upfTFActive.value === '30') {
upfTFActive.value = '0';
}
upfTFSend('0');
upfTFSend('7');
upfTFSend('30');
}, 10_000);
clearInterval(interval5s.value);
interval5s.value = setInterval(() => {
if (!interval5s.value || !initFlag) return;
fnGetSkim(); // 获取概览信息
fnGetNeState(); // 获取网元状态
}, 10_000);
}
/**栏目信息跳转 */
function fnToRouter(name: string, query?: any) {
router.push({ name, query });
}
/**网元参数 */
let neOtions = ref<Record<string, any>[]>([]);
// UPF实时流量下拉框选择
function fnSelectNe(value: any, option: any) {
upfWhoId.value = value;
reSendUPF(value);
// upfTotalFlow.value.map((item: any) => {
// item.requestFlag = false;
// });
for (var key in upfTotalFlow.value) {
upfTotalFlow.value[key].requestFlag = false;
}
// loadData();
}
// UPF实时流量下拉菜单选择
function fnSelectUPF(e: any) {
upfWhoId.value = e.key;
reSendUPF(e.key);
for (var key in upfTotalFlow.value) {
upfTotalFlow.value[key].requestFlag = false;
}
}
let udmNeId = ref<string>('001');
let udmOtions = ref<Record<string, any>[]>([]);
let onlineOtions = ref<Record<string, any>[]>([]);
/**用户数量-选择UDM */
async function fnSelectUDM(e: any) {
udmNeId.value = e.key;
try {
const res = await listUDMSub({
neId: udmNeId.value,
pageNum: 1,
pageSize: 1,
});
console.log(res);
// listUDMSub({ neId: udmNeId.value, pageNum: 1, pageSize: 1 }).then(res => {
if (
res.code === RESULT_CODE_SUCCESS &&
typeof res.data.total === 'number'
) {
skimState.udmSubNum = res.data.total;
console.log(res);
} else {
skimState.udmSubNum = 0;
}
// }).catch(() => {
// skimState.udmSubNum = 0;
// });
} catch (error) {
skimState.udmSubNum = 0;
}
}
/**资源控制-选择NE */
function fnSelectNeRe(e: any) {
console.log(e);
graphNodeClickID.value = e.key;
}
//
// 定义一个方法返回 views 容器
const getPopupContainer = () => {
// 使用 ref 或其他方式来引用你的 views 容器
// 如果 views 容器直接在这个组件内部,你可以使用 ref
// 但在这个例子中,我们假设它是通过类名来获取的
return document.querySelector('.viewport');
};
onMounted(() => {
// 获取网元网元列表
neListStore.neCascaderOptions.forEach(item => {
console.log(item);
if (item.value === 'UPF') {
neOtions.value = JSON.parse(JSON.stringify(item.children));
}
if (item.value === 'UDM') {
udmOtions.value = JSON.parse(JSON.stringify(item.children));
}
});
if (neOtions.value.length > 0) {
fnSelectNe(neOtions.value[0].value, neOtions.value[0]);
}
if (udmOtions.value.length > 0) {
fnSelectUDM({ key: udmOtions.value[0].value });
}
// if (onlineArr.length > 0) {
// fnSelectNeRe({ key: onlineArr[0].value });
// }
// 过滤不可用的网元
neCascaderOptions.value = neListStore.getNeCascaderOptions.filter(
(item: any) => {
return ['UDM', 'SMF', 'IMS', 'AMF', 'MME'].includes(item.value);
}
);
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
//online Ne
let onlineArr: Record<string, any>[] = [];
// UDM
neListStore.neList.forEach((v: any) => {
if (
v.status &&
[
'UDM',
'UPF',
'AUSF',
'PCF',
'SMF',
'AMF',
'OMC',
'SMSC',
'IMS',
'MME',
].includes(v.neType)
) {
onlineArr.push({
value: v.neType + '_' + v.neId,
label: v.neName,
rmUid: v.rmUid,
});
}
});
onlineOtions.value = onlineArr;
initFlag = true;
fnGetSkim().then(() => {
loadData();
});
});
onBeforeUnmount(() => {
clearInterval(interval10s.value);
interval10s.value = null;
clearInterval(interval5s.value);
interval5s.value = null;
initFlag = false;
});
</script>
<template>
<div class="viewport" ref="viewportDom">
<div class="brand">
<div
class="brand-title"
@click="toggle"
:title="t('views.dashboard.overview.fullscreen')"
>
{{ t('views.dashboard.overview.title') }}
<FullscreenExitOutlined v-if="isFullscreen" />
<FullscreenOutlined v-else />
</div>
<div class="brand-desc">{{ appStore.appName }}</div>
</div>
<div class="column">
<div class="skim panel">
<div class="inner">
<h3 class="leftright">
<span class="title">
<IdcardOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.skim.userTitle') }}
</span>
</h3>
<div class="data">
<div
class="item toRouter"
:title="t('views.dashboard.overview.toRouter')"
v-if="neListStore.fnHasNe(['udm'])"
>
<div @click="fnToRouter('UdmSub_2001')">
<UserOutlined
style="color: #4096ff; margin-right: 8px; font-size: 1.1rem"
/>
{{ skimState.udmSubNum || 0 }}
</div>
<span>
<a-dropdown
:trigger="['click']"
:get-Popup-Container="getPopupContainer"
>
<div class="toDeep-text">
{{ t('views.dashboard.overview.skim.users') }}
<DownOutlined style="margin-left: 12px; font-size: 12px" />
</div>
<template #overlay>
<a-menu @click="fnSelectUDM">
<a-menu-item
v-for="v in udmOtions"
:key="v.value"
:disabled="udmNeId === v.value"
>
{{ v.label }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('ImsSub_2004')"
:title="t('views.dashboard.overview.toRouter')"
style="margin: 0 12px"
v-perms:has="['dashboard:overview:imsUeNum']"
v-if="neListStore.fnHasNe(['ims'])"
>
<div>
<img :src="svgUserIMS" style="width: 18px; margin-right: 8px" />
{{ skimState.imsUeNum || 0 }}
</div>
<span>
{{ t('views.dashboard.overview.skim.imsUeNum') }}
</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('SmfSub_2005')"
:title="t('views.dashboard.overview.toRouter')"
v-perms:has="['dashboard:overview:smfUeNum']"
v-if="neListStore.fnHasNe(['smf'])"
>
<div>
<img :src="svgUserSMF" style="width: 18px; margin-right: 8px" />
{{ skimState.smfUeNum || 0 }}
</div>
<span>
{{ t('views.dashboard.overview.skim.smfUeNum') }}
</span>
</div>
</div>
</div>
</div>
<!--告警统计-->
<div class="alarmType panel">
<div class="inner">
<h3
class="toRouter leftright"
:title="t('views.dashboard.overview.toRouter')"
>
<span class="title" @click="fnToRouter('HistoryAlarm_2097')">
<PieChartOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.alarmTypeBar.alarmSum') }}
</span>
</h3>
<div class="chart">
<AlarnTypeBar />
</div>
</div>
</div>
<!-- 用户行为 -->
<div class="userActivity panel">
<div class="inner">
<h3 class="leftright">
<span class="title">
<WhatsAppOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.userActivity.title') }}
</span>
</h3>
<div class="chart">
<UserActivity />
</div>
</div>
</div>
</div>
<div class="column" style="flex: 4; margin: 1.333rem 0.833rem 0">
<!-- 实时流量 -->
<div class="upfFlow panel">
<div class="inner">
<h3 class="centerStyle">
<span class="title">
<div
class="toRouter"
@click="fnToRouter('GoldTarget_2104')"
:title="t('views.dashboard.overview.toRouter')"
>
<AreaChartOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.upfFlow.title') }}
</div>
&nbsp;&nbsp;&nbsp;&nbsp;
<a-dropdown
:trigger="['click']"
:get-Popup-Container="getPopupContainer"
>
<div class="toDeep-text">
{{
neOtions.find(item => item.value === upfWhoId)?.label ||
'Select UPF'
}}
<DownOutlined style="margin-left: -2px; font-size: 12px" />
</div>
<template #overlay>
<a-menu @click="fnSelectUPF">
<a-menu-item v-for="v in neOtions" :key="v.value">
{{ v.label }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</span>
</h3>
<div class="chart">
<UPFFlow />
</div>
</div>
</div>
<!-- 网络拓扑 -->
<div class="topology panel">
<div class="inner">
<h3
class="toRouter centerStyle"
@click="fnToRouter('TopologyArchitecture_2128')"
:title="t('views.dashboard.overview.toRouter')"
>
<span class="title">
<ApartmentOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.topology.title') }}
</span>
</h3>
<div class="chart">
<Topology />
</div>
</div>
</div>
</div>
<div class="column">
<!-- 基站信息 -->
<div
class="skim panel base"
v-perms:has="['dashboard:overview:gnbBase']"
v-if="neListStore.fnHasNe(['amf'])"
>
<div class="inner">
<h3 class="leftright">
<span class="title">
<GlobalOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.skim.nodeBInfo') }}
</span>
</h3>
<div class="data" style="margin-top: 20px">
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'AMF' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<img
:src="svgBase"
style="width: 18px; margin-right: 8px; height: 2rem"
/>
{{ skimState.gNbSumNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.gnbSumBase') }}</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'AMF' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<img
:src="svgBase"
style="width: 18px; margin-right: 8px; height: 2rem"
/>
{{ skimState.gnbNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.gnbBase') }}</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2096', { neType: 'AMF' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<UserOutlined
style="color: #4096ff; margin-right: 8px; font-size: 1.1rem"
/>
{{ skimState.gnbUeNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.gnbUeNum') }}</span>
</div>
</div>
</div>
</div>
<div
class="skim panel base"
v-perms:has="['dashboard:overview:enbBase']"
v-if="neListStore.fnHasNe(['mme'])"
>
<div class="inner">
<h3></h3>
<div class="data" style="margin-top: 40px">
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'MME' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<img
:src="svgBase"
style="width: 18px; margin-right: 8px; height: 2rem"
/>
{{ skimState.eNbSumNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.enbSumBase') }}</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'MME' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<img
:src="svgBase"
style="width: 18px; margin-right: 8px; height: 2rem"
/>
{{ skimState.enbNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.enbBase') }}</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'MME' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<UserOutlined
style="color: #4096ff; margin-right: 8px; font-size: 1.1rem"
/>
{{ skimState.enbUeNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.enbUeNum') }}</span>
</div>
</div>
</div>
</div>
<!-- 资源情况 -->
<div class="resources panel">
<div class="inner">
<h3 class="resources leftright">
<span class="title">
<DashboardOutlined
style="color: #68d8fe; font-size: 20px"
/>&nbsp;&nbsp;
<div style="margin-left: -3px">
{{ t('views.dashboard.overview.resources.title') }}
</div>
<a-dropdown
:trigger="['click']"
:get-Popup-Container="getPopupContainer"
>
<div class="toDeep-text">
{{ graphNodeClickID }}
<DownOutlined style="margin-left: -2px; font-size: 12px" />
</div>
<template #overlay>
<a-menu @click="fnSelectNeRe">
<a-menu-item v-for="v in onlineOtions" :key="v.value">
{{ v.label }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</span>
</h3>
<div class="chart">
<NeResources />
</div>
</div>
</div>
<!-- IMS用户行为 -->
<div class="userActivity panel">
<div class="inner">
<h3 class="leftright">
<span class="title">
<WhatsAppOutlined
style="color: #68d8fe; font-size: 20px"
/>&nbsp;&nbsp;
{{ t('views.dashboard.overview.userActivity.imsTitle') }}
</span>
</h3>
<div class="chart">
<IMSActivity />
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
@import url('./css/index.css');
.toDeep {
--editor-background-color: blue;
}
.toDeep :deep(.ant-select-selector) {
background-color: #050f23;
border: none;
}
.toDeep :deep(.ant-select-arrow) {
color: #4c9bfd;
}
.toDeep :deep(.ant-select-selection-item) {
color: #4c9bfd;
}
.toDeep-text {
color: #4c9bfd !important;
font-size: 0.844rem !important;
position: relative !important;
line-height: 2rem !important;
white-space: nowrap !important;
text-align: start !important;
text-overflow: ellipsis !important;
overflow: hidden !important;
}
</style>