Files
fe.ems.vue3/src/views/dashboard/overview/index.vue
2024-12-20 18:31:28 +08:00

612 lines
19 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 AlarnTypeBar from './components/AlarnTypeBar/index.vue';
import UPFFlow from './components/UPFFlow/index.vue';
import { listUDMSub } from '@/api/neData/udm_sub';
import { listUENumBySMF } from '@/api/neUser/smf';
import { listUENumByIMS } from '@/api/neUser/ims';
import { listBase5G } from '@/api/neUser/base5G';
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 useNeInfoStore from '@/store/modules/neinfo';
import { message } from 'ant-design-vue';
import { upfWhoId } from './hooks/useWS';
const neInfoStore = useNeInfoStore();
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;
};
/**概览状态信息 */
let skimState: SkimStateType = reactive({
udmSubNum: 0,
smfUeNum: 0,
imsUeNum: 0,
gnbNum: 0,
gnbUeNum: 0,
enbNum: 0,
enbUeNum: 0,
});
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**总览节点 */
const viewportDom = ref<HTMLElement | null>(null);
const { isFullscreen, toggle } = useFullscreen(viewportDom);
/**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 { neType, neId } = node.neInfo;
if (!neType || !neId) continue;
// 请求标记检查避免重复发送
if (neStateRequestMap.value.get(neType)) continue;
neStateRequestMap.value.set(neType, true);
wsSend({
requestId: `neState_${neType}_${neId}`,
type: 'ne_state',
data: {
neType: neType,
neId: neId,
},
});
}
}
/**获取概览信息 */
async function fnGetSkim() {
// console.log(neCascaderOptions.value);
// const resArr = await Promise.allSettled([
// listUDMSub({
// neid: '001',
// pageNum: 1,
// pageSize: 1,
// }),
// listUENumBySMF('001'),
// listUENumByIMS('001'),
// listBase5G({
// neType: 'AMF',
// neId: '001',
// }),
// listBase5G({
// neType: 'MME',
// neId: '001',
// }),
// ]);
// if (resArr[0].status === 'fulfilled') {
// const res0 = resArr[0].value;
// if (res0.code === RESULT_CODE_SUCCESS) {
// skimState.udmSubNum = res0.total;
// }
// }
// if (resArr[1].status === 'fulfilled') {
// const res1 = resArr[1].value;
// if (res1.code === RESULT_CODE_SUCCESS) {
// skimState.smfUeNum = res1.data;
// }
// }
// if (resArr[2].status === 'fulfilled') {
// const res2 = resArr[2].value;
// if (res2.code === RESULT_CODE_SUCCESS) {
// skimState.imsUeNum = res2.data;
// }
// }
// if (resArr[3].status === 'fulfilled') {
// const res3 = resArr[3].value;
// if (res3.code === RESULT_CODE_SUCCESS) {
// skimState.gnbNum = res3.total;
// skimState.gnbUeNum = 0;
// res3.rows.map((item: any) => {
// skimState.gnbUeNum += item.ueNum;
// });
// }
// }
// if (resArr[4].status === 'fulfilled') {
// const res4 = resArr[4].value;
// if (res4.code === RESULT_CODE_SUCCESS) {
// skimState.enbNum = res4.total;
// skimState.enbUeNum = 0;
// res4.rows.map((item: any) => {
// skimState.enbUeNum += item.ueNum;
// });
// }
// }
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.total)
}],
['SMF', {
request: (neId: string) => listUENumBySMF(neId),
process: (res: any) => res.code === RESULT_CODE_SUCCESS && (skimState.smfUeNum += res.data)
}],
['IMS', {
request: (neId: string) => listUENumByIMS(neId),
process: (res: any) => res.code === RESULT_CODE_SUCCESS && (skimState.imsUeNum += res.data)
}],
['AMF', {
request: (neId: string) => listBase5G({ neType: 'AMF', neId }),
process: (res: any) => {
if (res.code === RESULT_CODE_SUCCESS) {
skimState.gnbNum += res.total;
skimState.gnbUeNum += res.rows.reduce((sum: number, item: any) => sum + item.ueNum, 0);
}
}
}],
['MME', {
request: (neId: string) => listBase5G({ neType: 'MME', neId }),
process: (res: any) => {
if (res.code === RESULT_CODE_SUCCESS) {
skimState.enbNum += res.total;
skimState.enbUeNum += res.rows.reduce((sum: number, item: any) => sum + item.ueNum, 0);
}
}
}]
]);
const requests = neCascaderOptions.value.flatMap((ne: any) =>
ne.children?.map((child: any) => {
const handler = neHandlers.get(child.neType);
return handler ? {
promise: handler.request(child.neId),
process: handler.process
} : 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);
}
});
}
/**初始数据函数 */
function loadData() {
fnGetNeState(); // 获取网元状态
userActivitySend();
upfTFSend('0');
upfTFSend('7');
upfTFSend('30');
clearInterval(interval10s.value);
interval10s.value = setInterval(() => {
if (!interval10s.value) return;
if (upfTFActive.value === '0') {
upfTFSend('7');
upfTFActive.value = '7';
} else if (upfTFActive.value === '7') {
upfTFSend('30');
upfTFActive.value = '30';
} else if (upfTFActive.value === '30') {
upfTFSend('0');
upfTFActive.value = '0';
}
}, 10_000);
clearInterval(interval5s.value);
interval5s.value = setInterval(() => {
if (!interval5s.value) return;
fnGetSkim(); // 获取概览信息
fnGetNeState(); // 获取网元状态
}, 5_000);
}
/**栏目信息跳转 */
function fnToRouter(name: string, query?: any) {
router.push({ name, query });
}
/**网元参数 */
let neOtions = ref<Record<string, any>[]>([]);
/**UPF网元Id */
let queryParams = reactive({
/**45G类型 */
neRealId: '',
});
// UPF实时流量下拉框选择
function fnSelectNe(value: any, option: any) {
queryParams.neRealId = value;
upfWhoId.value = value;
reSendUPF(option.rmUid);
// upfTotalFlow.value.map((item: any) => {
// item.requestFlag = false;
// });
for (var key in upfTotalFlow.value) {
upfTotalFlow.value[key].requestFlag = false;
}
loadData();
}
// 定义一个方法返回 views 容器
const getPopupContainer = () => {
// 使用 ref 或其他方式来引用你的 views 容器
// 如果 views 容器直接在这个组件内部,你可以使用 ref
// 但在这个例子中,我们假设它是通过类名来获取的
return document.querySelector('.viewport');
};
onMounted(() => {
neInfoStore.fnNelist().then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
let arr: Record<string, any>[] = [];
res.data.forEach(i => {
if (i.neType === 'UPF') {
arr.push({ value: i.neId, label: i.neName, rmUid: i.rmUid });
}
});
neOtions.value = arr;
if (arr.length > 0) {
//queryParams.neRealId = arr[0].value;
fnSelectNe(arr[0].value, arr[0]);
}
// 过滤不可用的网元
neCascaderOptions.value = neInfoStore.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;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
})
.finally(() => {
fnGetSkim().then(() => {
loadData();
})
})
});
onBeforeUnmount(() => {
clearInterval(interval10s.value);
interval10s.value = null;
clearInterval(interval5s.value);
interval5s.value = null;
});
</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>
<IdcardOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.skim.userTitle') }}
</h3>
<div class="data">
<div class="item toRouter" @click="fnToRouter('Sub_2010')" :title="t('views.dashboard.overview.toRouter')">
<div>
<UserOutlined style="color: #4096ff; margin-right: 8px; font-size: 1.1rem" />
{{ skimState.udmSubNum }}
</div>
<span>
{{ t('views.dashboard.overview.skim.users') }}
</span>
</div>
<div class="item toRouter" @click="fnToRouter('Ims_2080')" :title="t('views.dashboard.overview.toRouter')"
style="margin: 0 12px">
<div>
<img :src="svgUserIMS" style="width: 18px; margin-right: 8px" />
{{ skimState.imsUeNum }}
</div>
<span>
{{ t('views.dashboard.overview.skim.imsUeNum') }}
</span>
</div>
<div class="item toRouter" @click="fnToRouter('Ue_2081')" :title="t('views.dashboard.overview.toRouter')">
<div>
<img :src="svgUserSMF" style="width: 18px; margin-right: 8px" />
{{ skimState.smfUeNum }}
</div>
<span>
{{ t('views.dashboard.overview.skim.smfUeNum') }}
</span>
</div>
</div>
</div>
</div>
<div class="skim panel base">
<div class="inner">
<h3>
<GlobalOutlined style="color: #68d8fe" />&nbsp;&nbsp; 5G
{{ t('views.dashboard.overview.skim.baseTitle') }}
</h3>
<div class="data">
<div class="item toRouter" @click="fnToRouter('Base5G_2082', { 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('Base5G_2082', { 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">
<div class="inner">
<h3>
<GlobalOutlined style="color: #68d8fe" />&nbsp;&nbsp; 4G
{{ t('views.dashboard.overview.skim.baseTitle') }}
</h3>
<div class="data">
<div class="item toRouter" @click="fnToRouter('Base5G_2082', { 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('Base5G_2082', { 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="userActivity panel">
<div class="inner">
<h3>
<WhatsAppOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.userActivity.title') }}
</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="toRouter"
:title="t('views.dashboard.overview.toRouter')"
style="display: flex; align-items: center"
>
<AreaChartOutlined style="color: #68d8fe" />&nbsp;&nbsp;
<span @click="fnToRouter('GoldTarget_2104')">{{
t('views.dashboard.overview.upfFlow.title')
}}</span>
<a-select
v-model:value="queryParams.neRealId"
:options="neOtions"
:get-Popup-Container="getPopupContainer"
class="toDeep"
style="width: 100px; color: #fff; margin-left: auto"
@change="fnSelectNe"
/>
</h3>
<div class="chart">
<UPFFlow />
</div>
</div>
</div>
<!-- 网络拓扑 -->
<div class="topology panel">
<div class="inner">
<h3 class="toRouter" @click="fnToRouter('TopologyArchitecture_2128')"
:title="t('views.dashboard.overview.toRouter')">
<span>
<ApartmentOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.topology.title') }}
</span>
<span>
{{ t('views.dashboard.overview.topology.normal') }}:
<span class="normal"> {{ graphNodeStateNum[0] }} </span>
{{ t('views.dashboard.overview.topology.abnormal') }}:
<span class="abnormal"> {{ graphNodeStateNum[1] }} </span>
</span>
</h3>
<div class="chart">
<Topology />
</div>
</div>
</div>
</div>
<div class="column">
<!-- 流量统计 -->
<div class="upfFlowTotal panel">
<div class="inner">
<h3>
<span>
<SwapOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.upfFlowTotal.title') }}
</span>
<!-- 筛选 -->
<div class="filter">
<span :data-key="v" :class="{ active: upfTFActive === v }" v-for="v in ['0', '7', '30']" :key="v" @click="() => {
upfTFActive = v;
}
">
{{
v === '0'
? '24' + t('common.units.hour')
: v + t('common.units.day')
}}
</span>
</div>
</h3>
<div class="chart">
<!-- 数据 -->
<div class="data">
<div class="item">
<span>
<ArrowUpOutlined style="color: #597ef7" />
{{ t('views.dashboard.overview.upfFlowTotal.up') }}
</span>
<h4>{{ upfTotalFlow[upfTFActive].upFrom }}</h4>
</div>
<div class="item">
<span>
<ArrowDownOutlined style="color: #52c41a" />
{{ t('views.dashboard.overview.upfFlowTotal.down') }}
</span>
<h4>{{ upfTotalFlow[upfTFActive].downFrom }}</h4>
</div>
</div>
</div>
</div>
</div>
<!-- 告警统计 -->
<div class="alarmType panel">
<div class="inner">
<h3 class="toRouter" @click="fnToRouter('HistoryAlarm_2097')" :title="t('views.dashboard.overview.toRouter')">
<PieChartOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.alarmTypeBar.alarmSum') }}
</h3>
<div class="chart">
<AlarnTypeBar />
</div>
</div>
</div>
<!-- 资源情况 -->
<div class="resources panel">
<div class="inner">
<h3>
<DashboardOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.resources.title') }}
{{ graphNodeClickID }}
</h3>
<div class="chart">
<NeResources />
</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: #101129;
border: none;
}
.toDeep :deep(.ant-select-arrow) {
color: #fff;
}
.toDeep :deep(.ant-select-selection-item) {
color: #fff;
}
</style>