diff --git a/practical_training/views/dashboard/imsCDR/index.vue b/practical_training/views/dashboard/imsCDR/index.vue index 3405700a..be212ab3 100644 --- a/practical_training/views/dashboard/imsCDR/index.vue +++ b/practical_training/views/dashboard/imsCDR/index.vue @@ -11,7 +11,6 @@ import { RESULT_CODE_SUCCESS, } from '@/constants/result-constants'; import useDictStore from '@/store/modules/dict'; -import useNeInfoStore from '@/store/modules/neinfo'; import { delIMSDataCDR, exportIMSDataCDR, @@ -38,9 +37,6 @@ let dict: { cdrCallType: [], }); -/**网元可选 */ -let neOtions = ref[]>([]); - /**开始结束时间 */ let queryRangePicker = ref<[string, string]>(['', '']); @@ -468,31 +464,6 @@ onMounted(() => { if (resArr[1].status === 'fulfilled') { dict.cdrCallType = resArr[1].value; } - } - ); - // 获取网元网元列表 - useNeInfoStore() - .fnNelist() - .then(res => { - if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - if (res.data.length > 0) { - let arr: Record[] = []; - res.data.forEach(i => { - if (i.neType === 'IMS') { - arr.push({ value: i.neId, label: i.neName }); - } - }); - neOtions.value = arr; - if (arr.length > 0) { - queryParams.neId = arr[0].value; - } - } - } else { - message.warning({ - content: t('common.noData'), - duration: 2, - }); - } }) .finally(() => { // 获取列表数据 @@ -517,15 +488,6 @@ onBeforeUnmount(() => { - - - - - { > - + diff --git a/practical_training/views/dashboard/mmeUE/index.vue b/practical_training/views/dashboard/mmeUE/index.vue index b6f4f842..b95d71ab 100644 --- a/practical_training/views/dashboard/mmeUE/index.vue +++ b/practical_training/views/dashboard/mmeUE/index.vue @@ -11,7 +11,6 @@ import { RESULT_CODE_SUCCESS, } from '@/constants/result-constants'; import useDictStore from '@/store/modules/dict'; -import useNeInfoStore from '@/store/modules/neinfo'; import { listMMEDataUE, delMMEDataUE, exportMMEDataUE } from '@/api/neData/mme'; import { parseDateToStr } from '@/utils/date-utils'; import { OptionsType, WS } from '@/plugins/ws-websocket'; @@ -23,9 +22,6 @@ const { getDict } = useDictStore(); const ws = new WS(); const queue = new PQueue({ concurrency: 1, autoStart: true }); -/**网元可选 */ -let neOtions = ref[]>([]); - /**字典数据 */ let dict: { /**UE 事件认证代码类型 */ @@ -411,47 +407,23 @@ onMounted(() => { getDict('ue_auth_code'), getDict('ue_event_type'), getDict('ue_event_cm_state'), - ]).then(resArr => { - if (resArr[0].status === 'fulfilled') { - dict.ueAauthCode = resArr[0].value; - } - if (resArr[1].status === 'fulfilled') { - const ueEventType: any[] = JSON.parse(JSON.stringify(resArr[1])); - dict.ueEventType = ueEventType.map(item => { - if (item.value === 'cm-state') { - item.label = item.label.replace('CM', 'ECM'); - } - return item; - }); - } - if (resArr[2].status === 'fulfilled') { - dict.ueEventCmState = resArr[2].value; - } - }); - - // 获取网元网元列表 - useNeInfoStore() - .fnNelist() - .then(res => { - if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - if (res.data.length > 0) { - let arr: Record[] = []; - res.data.forEach(i => { - if (i.neType === 'MME') { - arr.push({ value: i.neId, label: i.neName }); - } - }); - neOtions.value = arr; - if (arr.length > 0) { - queryParams.neId = arr[0].value; + ]) + .then(resArr => { + if (resArr[0].status === 'fulfilled') { + dict.ueAauthCode = resArr[0].value; + } + if (resArr[1].status === 'fulfilled') { + const ueEventType: any[] = JSON.parse(JSON.stringify(resArr[1])); + dict.ueEventType = ueEventType.map(item => { + if (item.value === 'cm-state') { + item.label = item.label.replace('CM', 'ECM'); } - } - } else { - message.warning({ - content: t('common.noData'), - duration: 2, + return item; }); } + if (resArr[2].status === 'fulfilled') { + dict.ueEventCmState = resArr[2].value; + } }) .finally(() => { // 获取列表数据 @@ -476,15 +448,6 @@ onBeforeUnmount(() => { - - - - - { > - + +import * as echarts from 'echarts/core'; +import { + TitleComponent, + TitleComponentOption, + TooltipComponent, + TooltipComponentOption, + GridComponent, + GridComponentOption, + LegendComponent, + LegendComponentOption, +} from 'echarts/components'; +import { + PieChart, + PieSeriesOption, + BarChart, + BarSeriesOption, +} from 'echarts/charts'; +import { LabelLayout } from 'echarts/features'; +import { CanvasRenderer } from 'echarts/renderers'; + +import { markRaw, onMounted, ref } from 'vue'; +import { origGet, top3Sel } from '@/api/faultManage/actAlarm'; +import useI18n from '@/hooks/useI18n'; +import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; + +const { t } = useI18n(); + +echarts.use([ + TitleComponent, + TooltipComponent, + GridComponent, + LegendComponent, + PieChart, + BarChart, + CanvasRenderer, + LabelLayout, +]); + +type EChartsOption = echarts.ComposeOption< + | TitleComponentOption + | TooltipComponentOption + | GridComponentOption + | LegendComponentOption + | PieSeriesOption + | BarSeriesOption +>; + +/**图DOM节点实例对象 */ +const alarmTypeBar = ref(undefined); + +/**图实例对象 */ +const alarmTypeBarChart = ref(null); + +/**告警类型数据 */ +const alarmTypeType = ref([ + { + value: 0, + name: t('views.index.Critical'), + }, + { + value: 0, + name: t('views.index.Major'), + }, + { + value: 0, + name: t('views.index.Minor'), + }, + { + value: 0, + name: t('views.index.Warning'), + }, + // { + // value: 0, + // name: t('views.index.Event'), + // }, +]); + +/**告警类型Top数据 */ +const alarmTypeTypeTop = ref([ + { name: 'AMF', value: 0 }, + { name: 'UDM', value: 0 }, + { name: 'SMF', value: 0 }, +]); + +// +function initPicture() { + Promise.allSettled([origGet(), top3Sel()]) + .then(resArr => { + if (resArr[0].status === 'fulfilled') { + const res0 = resArr[0].value; + if (res0.code === RESULT_CODE_SUCCESS && Array.isArray(res0.data)) { + for (const item of res0.data) { + let index = 0; + switch (item.name) { + case 'Critical': + index = 0; + break; + case 'Major': + index = 1; + break; + case 'Minor': + index = 2; + break; + case 'Warning': + index = 3; + break; + // case 'Event': + // index = 4; + // break; + } + alarmTypeType.value[index].value = Number(item.value); + } + } + } + if (resArr[1].status === 'fulfilled') { + const res1 = resArr[1].value; + if (res1.code === RESULT_CODE_SUCCESS && Array.isArray(res1.data)) { + alarmTypeTypeTop.value = alarmTypeTypeTop.value + .concat(res1.data) + .sort((a: any, b: any) => { + return b.value - a.value; + }) + .slice(0, 3); + } + } + }) + .then(() => { + const optionData: EChartsOption = { + title: [ + { + show: false, + }, + { + text: t('views.dashboard.overview.alarmTypeBar.topTitle'), + textStyle: { + color: '#fff', + fontSize: '14', + fontWeight: 400, + }, + top: '50%', + left: '0%', + }, + ], + tooltip: { + trigger: 'item', + formatter: '{b} : {c}', + }, + legend: { + orient: 'vertical', + right: '2%', + top: '12%', + data: alarmTypeType.value.map((item: any) => item.name), //label数组 + textStyle: { + color: '#A7D6F4', // 设置图例文字颜色 + }, + }, + grid: [ + { + top: '60%', + left: '15%', + right: '25%', + bottom: '10%', + }, + ], + series: [ + //饼图: + { + type: 'pie', + radius: '35%', + color: ['#f5222d', '#fa8c16', '#fadb14', '#1677ff', '#13c2c2'], + label: { + show: true, + position: 'inner', + formatter: (params: any) => { + if (!params.value) return ''; + return `${params.value}`; + }, + }, + labelLine: { + show: false, + }, + center: ['35%', '25%'], + data: alarmTypeType.value, + zlevel: 2, // 设置zlevel为1,使得柱状图在下层显示 + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)', + }, + }, + //柱状 + { + type: 'bar', + barWidth: 12, // 柱子宽度 + barCategoryGap: '30%', // 控制同一系列的柱间距离 + label: { + show: true, + position: 'right', // 位置 + color: '#A7D6F4', //淡蓝色 + fontSize: 14, + distance: 14, // label与柱子距离 + formatter: '{c}', + }, + itemStyle: { + borderRadius: [0, 20, 20, 0], // 圆角(左上、右上、右下、左下) + color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [ + { offset: 0, color: '#f0f5ff' }, + { offset: 0.5, color: '#adc6ff' }, + { offset: 1, color: '#2f54eb' }, + ]), // 渐变 + }, + data: alarmTypeTypeTop.value, + }, + ], + // 柱状图设置 + xAxis: [ + { + splitLine: { + show: false, + }, + type: 'value', + show: false, + }, + ], + yAxis: [ + { + splitLine: { + show: false, + }, + axisLine: { + //y轴 + show: false, + }, + type: 'category', + axisTick: { + show: false, + }, + inverse: true, + data: alarmTypeTypeTop.value.map((item: any) => item.name), + axisLabel: { + color: '#A7D6F4', + fontSize: 14, + }, + }, + ], + }; + + fnDesign(alarmTypeBar.value, optionData); + }); +} + +function fnDesign(container: HTMLElement | undefined, option: any) { + if (!container) return; + + alarmTypeBarChart.value = markRaw(echarts.init(container, 'light')); + option && alarmTypeBarChart.value.setOption(option); + + // 创建 ResizeObserver 实例 + var observer = new ResizeObserver(entries => { + if (alarmTypeBarChart.value) { + alarmTypeBarChart.value.resize(); + } + }); + // 监听元素大小变化 + observer.observe(container); +} + +onMounted(() => { + initPicture(); +}); + + + + + diff --git a/practical_training/views/dashboard/overview/components/NeResources/index.vue b/practical_training/views/dashboard/overview/components/NeResources/index.vue new file mode 100644 index 00000000..a50a6a20 --- /dev/null +++ b/practical_training/views/dashboard/overview/components/NeResources/index.vue @@ -0,0 +1,368 @@ + + + + + diff --git a/practical_training/views/dashboard/overview/components/Topology/index.vue b/practical_training/views/dashboard/overview/components/Topology/index.vue new file mode 100644 index 00000000..29fcde82 --- /dev/null +++ b/practical_training/views/dashboard/overview/components/Topology/index.vue @@ -0,0 +1,267 @@ + + + + + diff --git a/practical_training/views/dashboard/overview/components/UPFFlow/index.vue b/practical_training/views/dashboard/overview/components/UPFFlow/index.vue new file mode 100644 index 00000000..12694af7 --- /dev/null +++ b/practical_training/views/dashboard/overview/components/UPFFlow/index.vue @@ -0,0 +1,290 @@ + + + + + diff --git a/practical_training/views/dashboard/overview/components/UserActivity/index.vue b/practical_training/views/dashboard/overview/components/UserActivity/index.vue new file mode 100644 index 00000000..ae3e07a5 --- /dev/null +++ b/practical_training/views/dashboard/overview/components/UserActivity/index.vue @@ -0,0 +1,370 @@ + + + + + diff --git a/practical_training/views/dashboard/overview/css/index.css b/practical_training/views/dashboard/overview/css/index.css new file mode 100644 index 00000000..9b053c7c --- /dev/null +++ b/practical_training/views/dashboard/overview/css/index.css @@ -0,0 +1,277 @@ +.viewport { + /* 限定大小 */ + min-width: 1024px; + max-width: 1920px; + min-height: 780px; + margin: 0 auto; + position: relative; + display: flex; + padding: 5rem 0.833rem 0; + line-height: 1.15; + background-color: #101129; + height: 100vh; + margin-bottom: -20px; +} + +.column { + flex: 3; + position: relative; +} + +/* 边框 */ +.panel { + box-sizing: border-box; + border: 2px solid red; + border-image: url(../images/border.png) 51 38 21 132; + border-width: 2.125rem 1.583rem 0.875rem 5.5rem; + position: relative; + margin-bottom: 0.833rem; +} +.panel .inner { + /* 装内容 */ + /* height: 60px; */ + position: absolute; + top: -2.125rem; + right: -1.583rem; + bottom: -0.875rem; + left: -5.5rem; + padding: 1rem 1.5rem; +} +.panel h3 { + font-size: 0.833rem; + color: #fff; +} + +/* 总览标题 */ +.brand { + background-image: url(../images/brand.png); + background-repeat: no-repeat; + background-size: cover; + background-position: center center; + position: absolute; + top: 0.833rem; + left: 0; + right: 0; + width: 100%; + height: 5rem; + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; +} +.brand .brand-title { + color: #ffffff; + font-size: 1.4rem; + font-weight: 600; + padding-top: 1rem; + padding-bottom: 0.5rem; +} +.brand .brand-desc { + color: #d9d9d9; + font-size: 0.9rem; +} + +/* 实时流量 */ +.upfFlow { + /* min-height: 16rem; */ + height: 40%; +} +.upfFlow .inner .chart { + width: 100%; + height: 100%; + margin-top: 1rem; +} + +/* 网络拓扑 */ +.topology { + /* min-height: 27.8rem; */ + height: 56.4%; +} +.topology .inner h3 { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: baseline; +} +.topology .inner h3 .normal { + color: #52c41a; + font-size: 1.1rem; + margin-right: 8px; +} +.topology .inner h3 .abnormal { + color: #f5222d; + font-size: 1.1rem; +} +.topology .inner .chart { + width: 100%; + height: 100%; + margin-top: 1rem; +} + +/* 概览区域 */ +.skim { + /* min-height: 7.78rem; */ + height: 14.4%; +} +.skim .inner .data { + display: flex; + flex-direction: row; + align-items: center; + height: 90%; +} +.skim .inner .data .item { + display: flex; + flex-direction: column; + align-items: baseline; + width: 33%; +} +.skim .inner .data .item div { + font-size: 1.467rem; + color: #fff; + margin-bottom: 0; + display: flex; + align-items: baseline; + line-height: 2rem; +} +.skim .inner .data .item span { + color: #4c9bfd; + font-size: 0.833rem; + width: 100%; + position: relative; + line-height: 2rem; + white-space: nowrap; + text-align: start; + text-overflow: ellipsis; + overflow: hidden; +} +.skim .inner .data .item span::before { + content: ' '; + position: absolute; + top: 2px; + left: 0; + right: 0; + bottom: 0; + z-index: 0; + background-image: linear-gradient(to right, #fff, #fff0); + height: 1px; + border-radius: 4px; +} + +/* 概览区域 衍生基站信息 */ +.skim.base { + height: 12%; +} + +.skim.base .inner .data { + display: flex; + flex-direction: row; + align-items: center; + height: 75%; +} +.skim.base .inner .data .item { + display: flex; + flex-direction: column; + align-items: baseline; + width: 50%; +} + +/* 用户行为 */ +.userActivity { + /* min-height: 35.8rem; */ + height: 54.6%; +} +.userActivity .inner .chart { + width: 100%; + height: 100%; + margin-top: 1rem; +} + +/* 流量统计 */ +.upfFlowTotal { + /* min-height: 7.5rem; */ + height: 14.4%; +} +.upfFlowTotal .inner h3 { + display: flex; + justify-content: space-between; +} +.upfFlowTotal .inner h3 .filter { + display: flex; +} +.upfFlowTotal .inner h3 .filter span { + display: block; + height: 0.75rem; + line-height: 1; + padding: 0 0.75rem; + color: #1950c4; + font-size: 0.75rem; + border-right: 0.083rem solid #00f2f1; + cursor: pointer; +} +.upfFlowTotal .inner h3 .filter span:first-child { + padding-left: 0; +} +.upfFlowTotal .inner h3 .filter span:last-child { + border-right: none; +} +.upfFlowTotal .inner h3 .filter span.active { + color: #fff; + font-size: 0.833rem; +} +.upfFlowTotal .inner .chart { + width: 100%; + height: 100%; + margin-top: 0.1rem; +} +.upfFlowTotal .inner .chart .data { + display: flex; + flex-direction: column; + justify-content: space-around; + height: 60%; +} +.upfFlowTotal .inner .chart .data .item { + display: flex; + justify-content: space-between; + align-items: baseline; +} +.upfFlowTotal .inner .chart .data .item h4 { + font-size: 1.467rem; + color: #fff; + margin-bottom: 0; +} +.upfFlowTotal .inner .chart .data .item span { + color: #4c9bfd; + font-size: 0.867rem; +} + +/* 资源情况 */ +.resources { + /* min-height: 18rem; */ + height: 34.4%; +} +.resources .inner .chart { + width: 100%; + height: 100%; + margin-top: 1rem; +} + +/* 告警统计 */ +.alarmType { + /* min-height: 25rem; */ + height: 46%; +} +.alarmType .inner .chart { + width: 100%; + height: 100%; + margin-top: 1rem; +} + +/* 跳转鼠标悬浮 */ +.toRouter:hover { + cursor: pointer; + color: #fff !important; +} +.toRouter:hover > *, +.toRouter:hover > * > * { + color: #fff !important; +} diff --git a/practical_training/views/dashboard/overview/hooks/useTopology.ts b/practical_training/views/dashboard/overview/hooks/useTopology.ts new file mode 100644 index 00000000..64c01c68 --- /dev/null +++ b/practical_training/views/dashboard/overview/hooks/useTopology.ts @@ -0,0 +1,172 @@ +import { parseDateToStr } from '@/utils/date-utils'; +import { computed, reactive, ref } from 'vue'; + +/**非网元元素 */ +export const notNeNodes = [ + '5GC', + 'DN', + 'UE', + 'Base', + 'lan', + 'lan1', + 'lan2', + 'lan3', + 'lan4', + 'lan5', + 'lan6', + 'lan7', + 'LAN', + 'NR', +]; + +/**图状态 */ +export const graphState = reactive>({ + /**当前图组名 */ + group: '5GC System Architecture', + /**图数据 */ + data: { + combos: [], + edges: [], + nodes: [], + }, +}); + +/**图实例对象 */ +export const graphG6 = ref(null); + +/**图点击选择 */ +export const graphNodeClickID = ref('UPF'); + +/**图节点网元信息状态 */ +export const graphNodeState = computed(() => + graphState.data.nodes.map((item: any) => ({ + id: item.id, + label: item.label, + neInfo: item.neInfo, + neState: item.neState, + })) +); + +/**图节点网元状态数量 */ +export const graphNodeStateNum = computed(() => { + let normal = 0; + let abnormal = 0; + for (const item of graphState.data.nodes) { + const neId = item.neState.neId; + if (neId) { + if (item.neState.online) { + normal += 1; + } else { + abnormal += 1; + } + } + } + return [normal, abnormal]; +}); + +/**网元状态请求标记 */ +export const neStateRequestMap = ref>(new Map()); + +/**neStateParse 网元状态 数据解析 */ +export function neStateParse(neType: string, data: Record) { + const { combos, edges, nodes } = graphState.data; + const node = nodes.find((item: Record) => item.id === neType); + + // 更新网元状态 + const newNeState = Object.assign(node.neState, data, { + refreshTime: parseDateToStr(data.refreshTime, 'HH:mm:ss'), + online: !!data.cpu, + }); + + // 通过 ID 查询节点实例 + const item = graphG6.value.findById(node.id); + if (item) { + const stateColor = newNeState.online ? '#52c41a' : '#f5222d'; // 状态颜色 + // 图片类型不能填充 + if (node.type.startsWith('image')) { + // 更新节点 + if (node.label !== newNeState.neName) { + graphG6.value.updateItem(item, { + label: newNeState.neName, + }); + } + // 设置状态 + graphG6.value.setItemState(item, 'top-right-dot', stateColor); + } else { + // 更新节点 + graphG6.value.updateItem(item, { + label: newNeState.neName, + // neState: newNeState, + style: { + fill: stateColor, // 填充色 + stroke: stateColor, // 填充色 + }, + // labelCfg: { + // style: { + // fill: '#ffffff', // 标签文本色 + // }, + // }, + }); + // 设置状态 + graphG6.value.setItemState(item, 'stroke', newNeState.online); + } + } + + // 设置边状态 + for (const edge of edges) { + const edgeSource: string = edge.source; + const edgeTarget: string = edge.target; + const neS = nodes.find((n: any) => n.id === edgeSource); + const neT = nodes.find((n: any) => n.id === edgeTarget); + // console.log(neS, edgeSource, neT, edgeTarget); + + if (neS && neT) { + // 通过 ID 查询节点实例 + // const item = graphG6.value.findById(edge.id); + // console.log( + // `${edgeSource} - ${edgeTarget}`, + // neS.neState.online && neT.neState.online + // ); + // const stateColor = neS.neState.online && neT.neState.online ? '#000000' : '#ff4d4f'; // 状态颜色 + // 更新边 + // graphG6.value.updateItem(item, { + // label: `${edgeSource} - ${edgeTarget}`, + // style: { + // stroke: stateColor, // 填充色 + // }, + // labelCfg: { + // style: { + // fill: '#ffffff', // 标签文本色 + // }, + // }, + // }); + // 设置状态 + graphG6.value.setItemState( + edge.id, + 'circle-move', + neS.neState.online && neT.neState.online + ); + } + if (neS && notNeNodes.includes(edgeTarget)) { + graphG6.value.setItemState(edge.id, 'line-dash', neS.neState.online); + } + if (neT && notNeNodes.includes(edgeSource)) { + graphG6.value.setItemState(edge.id, 'line-dash', neT.neState.online); + } + } + + // 请求标记复位 + neStateRequestMap.value.set(neType, false); +} + +/**属性复位 */ +export function topologyReset() { + graphState.data = { + combos: [], + edges: [], + nodes: [], + }; + graphG6.value = null; + graphNodeClickID.value = 'UPF'; + neStateRequestMap.value = new Map(); +} diff --git a/practical_training/views/dashboard/overview/hooks/useUPFTotalFlow.ts b/practical_training/views/dashboard/overview/hooks/useUPFTotalFlow.ts new file mode 100644 index 00000000..29aebbe9 --- /dev/null +++ b/practical_training/views/dashboard/overview/hooks/useUPFTotalFlow.ts @@ -0,0 +1,115 @@ +import { parseDateToStr } from '@/utils/date-utils'; +import { parseSizeFromBits, parseSizeFromKbs } from '@/utils/parse-utils'; +import { ref } from 'vue'; + +type FDType = { + /**时间 */ + lineXTime: string[]; + /**上行 N3 */ + lineYUp: number[]; + /**下行 N6 */ + lineYDown: number[]; + /**容量 */ + cap: number; +}; + +/**UPF-流量数据 */ +export const upfFlowData = ref({ + lineXTime: [], + lineYUp: [], + lineYDown: [], + cap: 0, +}); + +/**UPF-流量数据 数据解析 */ +export function upfFlowParse(data: Record) { + upfFlowData.value.lineXTime.push(parseDateToStr(+data['timeGroup'])); + const upN3 = parseSizeFromKbs(+data['UPF.03'], 5); + upfFlowData.value.lineYUp.push(upN3[0]); + const downN6 = parseSizeFromKbs(+data['UPF.06'], 5); + upfFlowData.value.lineYDown.push(downN6[0]); + upfFlowData.value.cap += 1; + // 超过 25 弹出 + if (upfFlowData.value.cap > 25) { + upfFlowData.value.lineXTime.shift(); + upfFlowData.value.lineYUp.shift(); + upfFlowData.value.lineYDown.shift(); + upfFlowData.value.cap -= 1; + } + // UPF-总流量数0天 当天24小时 + upfTFParse('0', { + up: upfTotalFlow.value['0'].up + +data['UPF.03'], + down: upfTotalFlow.value['0'].down + +data['UPF.06'], + }); +} + +type TFType = { + /**上行 N3 */ + up: number; + upFrom: string; + /**下行 N6 */ + down: number; + downFrom: string; + /**请求标记 */ + requestFlag: boolean; +}; + +/**UPF-总流量数 */ +export const upfTotalFlow = ref>({ + '0': { + up: 0, + upFrom: '0 B', + down: 0, + downFrom: '0 B', + requestFlag: false, + }, + '7': { + up: 0, + upFrom: '0 B', + down: 0, + downFrom: '0 B', + requestFlag: false, + }, + '30': { + up: 0, + upFrom: '0 B', + down: 0, + downFrom: '0 B', + requestFlag: false, + }, +}); + +/**UPF-总流量数 数据解析 */ +export function upfTFParse(day: string, data: Record) { + let { up, down } = data; + upfTotalFlow.value[day] = { + up: up, + upFrom: parseSizeFromBits(up), + down: down, + downFrom: parseSizeFromBits(down), + requestFlag: false, + }; +} + +/**UPF-总流量数 选中 */ +export const upfTFActive = ref('0'); + +/**属性复位 */ +export function upfTotalFlowReset() { + upfFlowData.value = { + lineXTime: [], + lineYUp: [], + lineYDown: [], + cap: 0, + }; + for (const key of Object.keys(upfTotalFlow.value)) { + upfTotalFlow.value[key] = { + up: 0, + upFrom: '0 B', + down: 0, + downFrom: '0 B', + requestFlag: false, + }; + } + upfTFActive.value = '0'; +} diff --git a/practical_training/views/dashboard/overview/hooks/useUserActivity.ts b/practical_training/views/dashboard/overview/hooks/useUserActivity.ts new file mode 100644 index 00000000..5e307552 --- /dev/null +++ b/practical_training/views/dashboard/overview/hooks/useUserActivity.ts @@ -0,0 +1,148 @@ +import { ref } from 'vue'; + +/**ueEventAMFParse UE会话事件AMF 数据解析 */ +function ueEventAMFParse( + item: Record +): false | Record { + let evData: Record = item.eventJSON; + if (typeof evData === 'string') { + try { + evData = JSON.parse(evData); + } catch (error) { + console.error(error); + } + } + + return { + eType: 'amf_ue', + eId: `amf_ue_${item.id}_${Date.now()}`, + eTime: +item.timestamp, + id: item.id, + type: item.eventType, + data: evData, + }; +} + +/**ueEventMMEParse UE会话事件MME 数据解析 */ +function ueEventMMEParse( + item: Record +): false | Record { + let evData: Record = item.eventJSON; + if (typeof evData === 'string') { + try { + evData = JSON.parse(evData); + } catch (error) { + console.error(error); + } + } + + return { + eType: 'mme_ue', + eId: `mme_ue_${item.id}_${Date.now()}`, + eTime: +item.timestamp, + id: item.id, + type: item.eventType, + data: evData, + }; +} + +/**cdrEventIMSParse CDR会话事件IMS 数据解析 */ +function cdrEventIMSParse( + item: Record +): false | Record { + let evData: Record = item.cdrJSON || item.CDR; + if (typeof evData === 'string') { + try { + evData = JSON.parse(evData); + } catch (error) { + console.error(error); + return false; + } + } + + // 指定显示CDR类型MOC/MTSM + if (!['MOC', 'MTSM'].includes(evData.recordType)) { + return false; + } + + return { + eType: 'ims_cdr', + eId: `ims_cdr_${item.id}_${Date.now()}`, + eTime: +item.timestamp, + id: item.id, + data: evData, + }; +} + +/**eventListParse 事件列表解析 */ +export function eventListParse( + type: 'ims_cdr' | 'amf_ue' | 'mme_ue', + data: any +) { + eventTotal.value += data.total; + for (const item of data.rows) { + let v: false | Record = false; + if (type === 'ims_cdr') { + v = cdrEventIMSParse(item); + } + if (type === 'amf_ue') { + v = ueEventAMFParse(item); + } + if (type === 'mme_ue') { + v = ueEventMMEParse(item); + } + + if (v) { + eventData.value.push(v); + } + } + + // 有数据进行排序 + if (eventData.value.length > 5) { + eventData.value.sort((a, b) => b.eTime - a.eTime); + } + if (eventData.value.length > 0) { + eventId.value = eventData.value[0].eId; + } +} + +/**eventItemParseAndPush 事件项解析并添加 */ +export async function eventItemParseAndPush( + type: 'ims_cdr' | 'amf_ue' | 'mme_ue', + item: any +) { + let v: false | Record = false; + if (type === 'ims_cdr') { + v = cdrEventIMSParse(item); + } + if (type === 'amf_ue') { + v = ueEventAMFParse(item); + } + if (type === 'mme_ue') { + v = ueEventMMEParse(item); + } + + if (v) { + eventData.value.unshift(v); + eventTotal.value += 1; + eventId.value = v.eId; + await new Promise(resolve => setTimeout(resolve, 800)); + if (eventData.value.length > 20) { + eventData.value.pop(); + } + } +} + +/**CDR+UE事件数据 */ +export const eventData = ref[]>([]); +/**CDR+UE事件总量 */ +export const eventTotal = ref(0); +/**CDR/UE事件推送id */ +export const eventId = ref(''); + +/**属性复位 */ +export function userActivityReset() { + eventData.value = []; + eventTotal.value = 0; + eventId.value = ''; +} diff --git a/practical_training/views/dashboard/overview/hooks/useWS.ts b/practical_training/views/dashboard/overview/hooks/useWS.ts new file mode 100644 index 00000000..df85c983 --- /dev/null +++ b/practical_training/views/dashboard/overview/hooks/useWS.ts @@ -0,0 +1,204 @@ +import { RESULT_CODE_ERROR } from '@/constants/result-constants'; +import { OptionsType, WS } from '@/plugins/ws-websocket'; +import { onBeforeUnmount, onMounted } from 'vue'; +import { + eventListParse, + eventItemParseAndPush, + userActivityReset, +} from './useUserActivity'; +import { + upfTotalFlow, + upfTFParse, + upfFlowParse, + upfTotalFlowReset, +} from './useUPFTotalFlow'; +import { topologyReset, neStateParse } from './useTopology'; +import PQueue from 'p-queue'; + +/**websocket连接 */ +export default function useWS() { + const ws = new WS(); + const queue = new PQueue({ concurrency: 1, autoStart: true }); + + /**发消息 */ + function wsSend(data: Record) { + ws.send(data); + } + + /**接收数据后回调 */ + function wsMessage(res: Record) { + // console.log(res); + const { code, requestId, data } = res; + if (code === RESULT_CODE_ERROR) { + console.warn(res.msg); + return; + } + + // 网元状态 + if (requestId && requestId.startsWith('neState')) { + const neType = requestId.split('_')[1]; + neStateParse(neType, data); + return; + } + + // 普通信息 + switch (requestId) { + // AMF_UE会话事件 + case 'amf_1010': + if (Array.isArray(data.rows)) { + eventListParse('amf_ue', data); + } + break; + // MME_UE会话事件 + case 'mme_1011_001': + if (Array.isArray(data.rows)) { + eventListParse('mme_ue', data); + } + break; + // IMS_CDR会话事件 + case 'ims_1005_001': + if (Array.isArray(data.rows)) { + eventListParse('ims_cdr', data); + } + break; + //UPF-总流量数 + case 'upf_001_0': + upfTFParse('0', data); + break; + case 'upf_001_7': + upfTFParse('7', data); + break; + case 'upf_001_30': + upfTFParse('30', data); + break; + } + + // 订阅组信息 + if (!data?.groupId) { + return; + } + switch (data.groupId) { + // kpiEvent 指标UPF + case '12_001': + if (data.data) { + upfFlowParse(data.data); + } + break; + // AMF_UE会话事件 + case '1010': + if (data.data) { + queue.add(() => eventItemParseAndPush('amf_ue', data.data)); + } + break; + // MME_UE会话事件 + case '1011_001': + if (data.data) { + queue.add(() => eventItemParseAndPush('mme_ue', data.data)); + } + break; + // IMS_CDR会话事件 + case '1005_001': + if (data.data) { + queue.add(() => eventItemParseAndPush('ims_cdr', data.data)); + } + break; + } + } + + /**UPF-总流量数 发消息*/ + function upfTFSend(day: '0' | '7' | '30') { + // 请求标记检查避免重复发送 + if (upfTotalFlow.value[day].requestFlag) { + return; + } + upfTotalFlow.value[day].requestFlag = true; + + ws.send({ + requestId: `upf_001_${day}`, + type: 'upf_tf', + data: { + neType: 'UPF', + neId: '001', + day: Number(day), + }, + }); + } + + /**userActivitySend 用户行为事件基础列表数据 发消息*/ + function userActivitySend() { + // AMF_UE会话事件 + ws.send({ + requestId: 'amf_1010', + type: 'amf_ue', + data: { + neType: 'AMF', + neId: '001', + sortField: 'timestamp', + sortOrder: 'desc', + pageNum: 1, + pageSize: 20, + }, + }); + // MME_UE会话事件 + ws.send({ + requestId: 'mme_1011_001', + type: 'mme_ue', + data: { + neType: 'MME', + neId: '001', + sortField: 'timestamp', + sortOrder: 'desc', + pageNum: 1, + pageSize: 20, + }, + }); + // IMS_CDR会话事件 + ws.send({ + requestId: 'ims_1005_001', + type: 'ims_cdr', + data: { + neType: 'IMS', + neId: '001', + recordType: 'MOC', + sortField: 'timestamp', + sortOrder: 'desc', + pageNum: 1, + pageSize: 20, + }, + }); + } + + onMounted(() => { + const options: OptionsType = { + url: '/ws', + params: { + /**订阅通道组 + * + * 指标UPF (GroupID:12_neId) + * AMF_UE会话事件(GroupID:1010) + * MME_UE会话事件(GroupID:1011_neId) + * IMS_CDR会话事件(GroupID:1005_neId) + */ + subGroupID: '12_001,1010,1011_001,1005_001', + }, + onmessage: wsMessage, + onerror: (ev: any) => { + console.error(ev); + }, + }; + ws.connect(options); + }); + + onBeforeUnmount(() => { + ws.close(); + userActivityReset(); + upfTotalFlowReset(); + topologyReset(); + }); + + return { + wsSend, + userActivitySend, + upfTFSend, + }; +} diff --git a/practical_training/views/dashboard/overview/images/border.png b/practical_training/views/dashboard/overview/images/border.png new file mode 100644 index 00000000..b854cea4 Binary files /dev/null and b/practical_training/views/dashboard/overview/images/border.png differ diff --git a/practical_training/views/dashboard/overview/images/brand.png b/practical_training/views/dashboard/overview/images/brand.png new file mode 100644 index 00000000..2f6fb6a0 Binary files /dev/null and b/practical_training/views/dashboard/overview/images/brand.png differ diff --git a/practical_training/views/dashboard/overview/images/line.png b/practical_training/views/dashboard/overview/images/line.png new file mode 100644 index 00000000..34e99aca Binary files /dev/null and b/practical_training/views/dashboard/overview/images/line.png differ diff --git a/practical_training/views/dashboard/overview/images/rect.png b/practical_training/views/dashboard/overview/images/rect.png new file mode 100644 index 00000000..6c0ebf00 Binary files /dev/null and b/practical_training/views/dashboard/overview/images/rect.png differ diff --git a/practical_training/views/dashboard/overview/index.vue b/practical_training/views/dashboard/overview/index.vue new file mode 100644 index 00000000..728d974c --- /dev/null +++ b/practical_training/views/dashboard/overview/index.vue @@ -0,0 +1,493 @@ + + + + + diff --git a/practical_training/views/dashboard/smfCDR/index.vue b/practical_training/views/dashboard/smfCDR/index.vue index 23c098f1..828d935f 100644 --- a/practical_training/views/dashboard/smfCDR/index.vue +++ b/practical_training/views/dashboard/smfCDR/index.vue @@ -5,7 +5,6 @@ import { Modal, message } 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 useNeInfoStore from '@/store/modules/neinfo'; import useI18n from '@/hooks/useI18n'; import { RESULT_CODE_ERROR, @@ -24,9 +23,6 @@ const { t } = useI18n(); const ws = new WS(); const queue = new PQueue({ concurrency: 1, autoStart: true }); -/**网元可选 */ -let neOtions = ref[]>([]); - /**开始结束时间 */ let queryRangePicker = ref<[string, string]>(['', '']); @@ -458,34 +454,8 @@ function wsMessage(res: Record) { } onMounted(() => { - // 获取网元网元列表 - useNeInfoStore() - .fnNelist() - .then(res => { - if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - if (res.data.length > 0) { - let arr: Record[] = []; - res.data.forEach(i => { - if (i.neType === 'SMF') { - arr.push({ value: i.neId, label: i.neName }); - } - }); - neOtions.value = arr; - if (arr.length > 0) { - queryParams.neId = arr[0].value; - } - } - } else { - message.warning({ - content: t('common.noData'), - duration: 2, - }); - } - }) - .finally(() => { - // 获取列表数据 - fnGetList(); - }); + // 获取列表数据 + fnGetList(); }); onBeforeUnmount(() => { @@ -505,15 +475,6 @@ onBeforeUnmount(() => { - - - - - []>([]); - /**开始结束时间 */ let queryRangePicker = ref<[string, string]>(['', '']); @@ -434,33 +431,10 @@ function wsMessage(res: Record) { onMounted(() => { // 初始字典数据 - Promise.allSettled([getDict('cdr_cause_code')]).then(resArr => { - if (resArr[0].status === 'fulfilled') { - dict.cdrCauseCode = resArr[0].value; - } - }); - // 获取网元网元列表 - useNeInfoStore() - .fnNelist() - .then(res => { - if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - if (res.data.length > 0) { - let arr: Record[] = []; - res.data.forEach(i => { - if (i.neType === 'SMSC') { - arr.push({ value: i.neId, label: i.neName }); - } - }); - neOtions.value = arr; - if (arr.length > 0) { - queryParams.neId = arr[0].value; - } - } - } else { - message.warning({ - content: t('common.noData'), - duration: 2, - }); + Promise.allSettled([getDict('cdr_cause_code')]) + .then(resArr => { + if (resArr[0].status === 'fulfilled') { + dict.cdrCauseCode = resArr[0].value; } }) .finally(() => { @@ -486,15 +460,6 @@ onBeforeUnmount(() => { - - - - - { > - + diff --git a/src/views/dashboard/imsCDR/index.vue b/src/views/dashboard/imsCDR/index.vue index 3405700a..be212ab3 100644 --- a/src/views/dashboard/imsCDR/index.vue +++ b/src/views/dashboard/imsCDR/index.vue @@ -11,7 +11,6 @@ import { RESULT_CODE_SUCCESS, } from '@/constants/result-constants'; import useDictStore from '@/store/modules/dict'; -import useNeInfoStore from '@/store/modules/neinfo'; import { delIMSDataCDR, exportIMSDataCDR, @@ -38,9 +37,6 @@ let dict: { cdrCallType: [], }); -/**网元可选 */ -let neOtions = ref[]>([]); - /**开始结束时间 */ let queryRangePicker = ref<[string, string]>(['', '']); @@ -468,31 +464,6 @@ onMounted(() => { if (resArr[1].status === 'fulfilled') { dict.cdrCallType = resArr[1].value; } - } - ); - // 获取网元网元列表 - useNeInfoStore() - .fnNelist() - .then(res => { - if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - if (res.data.length > 0) { - let arr: Record[] = []; - res.data.forEach(i => { - if (i.neType === 'IMS') { - arr.push({ value: i.neId, label: i.neName }); - } - }); - neOtions.value = arr; - if (arr.length > 0) { - queryParams.neId = arr[0].value; - } - } - } else { - message.warning({ - content: t('common.noData'), - duration: 2, - }); - } }) .finally(() => { // 获取列表数据 @@ -517,15 +488,6 @@ onBeforeUnmount(() => { - - - - - { > - + diff --git a/src/views/dashboard/mmeUE/index.vue b/src/views/dashboard/mmeUE/index.vue index b6f4f842..b95d71ab 100644 --- a/src/views/dashboard/mmeUE/index.vue +++ b/src/views/dashboard/mmeUE/index.vue @@ -11,7 +11,6 @@ import { RESULT_CODE_SUCCESS, } from '@/constants/result-constants'; import useDictStore from '@/store/modules/dict'; -import useNeInfoStore from '@/store/modules/neinfo'; import { listMMEDataUE, delMMEDataUE, exportMMEDataUE } from '@/api/neData/mme'; import { parseDateToStr } from '@/utils/date-utils'; import { OptionsType, WS } from '@/plugins/ws-websocket'; @@ -23,9 +22,6 @@ const { getDict } = useDictStore(); const ws = new WS(); const queue = new PQueue({ concurrency: 1, autoStart: true }); -/**网元可选 */ -let neOtions = ref[]>([]); - /**字典数据 */ let dict: { /**UE 事件认证代码类型 */ @@ -411,47 +407,23 @@ onMounted(() => { getDict('ue_auth_code'), getDict('ue_event_type'), getDict('ue_event_cm_state'), - ]).then(resArr => { - if (resArr[0].status === 'fulfilled') { - dict.ueAauthCode = resArr[0].value; - } - if (resArr[1].status === 'fulfilled') { - const ueEventType: any[] = JSON.parse(JSON.stringify(resArr[1])); - dict.ueEventType = ueEventType.map(item => { - if (item.value === 'cm-state') { - item.label = item.label.replace('CM', 'ECM'); - } - return item; - }); - } - if (resArr[2].status === 'fulfilled') { - dict.ueEventCmState = resArr[2].value; - } - }); - - // 获取网元网元列表 - useNeInfoStore() - .fnNelist() - .then(res => { - if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - if (res.data.length > 0) { - let arr: Record[] = []; - res.data.forEach(i => { - if (i.neType === 'MME') { - arr.push({ value: i.neId, label: i.neName }); - } - }); - neOtions.value = arr; - if (arr.length > 0) { - queryParams.neId = arr[0].value; + ]) + .then(resArr => { + if (resArr[0].status === 'fulfilled') { + dict.ueAauthCode = resArr[0].value; + } + if (resArr[1].status === 'fulfilled') { + const ueEventType: any[] = JSON.parse(JSON.stringify(resArr[1])); + dict.ueEventType = ueEventType.map(item => { + if (item.value === 'cm-state') { + item.label = item.label.replace('CM', 'ECM'); } - } - } else { - message.warning({ - content: t('common.noData'), - duration: 2, + return item; }); } + if (resArr[2].status === 'fulfilled') { + dict.ueEventCmState = resArr[2].value; + } }) .finally(() => { // 获取列表数据 @@ -476,15 +448,6 @@ onBeforeUnmount(() => { - - - - - { > - + { {{ t('views.dashboard.overview.upfFlowTotal.up') }} -

{{ upfTotalFlow[upfTFActive].upFrom }}

+

{{ upfTotalFlow[upfTFActive].up }}

{{ t('views.dashboard.overview.upfFlowTotal.down') }} -

{{ upfTotalFlow[upfTFActive].downFrom }}

+

{{ upfTotalFlow[upfTFActive].down }}

diff --git a/src/views/dashboard/smfCDR/index.vue b/src/views/dashboard/smfCDR/index.vue index 23c098f1..828d935f 100644 --- a/src/views/dashboard/smfCDR/index.vue +++ b/src/views/dashboard/smfCDR/index.vue @@ -5,7 +5,6 @@ import { Modal, message } 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 useNeInfoStore from '@/store/modules/neinfo'; import useI18n from '@/hooks/useI18n'; import { RESULT_CODE_ERROR, @@ -24,9 +23,6 @@ const { t } = useI18n(); const ws = new WS(); const queue = new PQueue({ concurrency: 1, autoStart: true }); -/**网元可选 */ -let neOtions = ref[]>([]); - /**开始结束时间 */ let queryRangePicker = ref<[string, string]>(['', '']); @@ -458,34 +454,8 @@ function wsMessage(res: Record) { } onMounted(() => { - // 获取网元网元列表 - useNeInfoStore() - .fnNelist() - .then(res => { - if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - if (res.data.length > 0) { - let arr: Record[] = []; - res.data.forEach(i => { - if (i.neType === 'SMF') { - arr.push({ value: i.neId, label: i.neName }); - } - }); - neOtions.value = arr; - if (arr.length > 0) { - queryParams.neId = arr[0].value; - } - } - } else { - message.warning({ - content: t('common.noData'), - duration: 2, - }); - } - }) - .finally(() => { - // 获取列表数据 - fnGetList(); - }); + // 获取列表数据 + fnGetList(); }); onBeforeUnmount(() => { @@ -505,15 +475,6 @@ onBeforeUnmount(() => { - - - - - []>([]); - /**开始结束时间 */ let queryRangePicker = ref<[string, string]>(['', '']); @@ -434,33 +431,10 @@ function wsMessage(res: Record) { onMounted(() => { // 初始字典数据 - Promise.allSettled([getDict('cdr_cause_code')]).then(resArr => { - if (resArr[0].status === 'fulfilled') { - dict.cdrCauseCode = resArr[0].value; - } - }); - // 获取网元网元列表 - useNeInfoStore() - .fnNelist() - .then(res => { - if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) { - if (res.data.length > 0) { - let arr: Record[] = []; - res.data.forEach(i => { - if (i.neType === 'SMSC') { - arr.push({ value: i.neId, label: i.neName }); - } - }); - neOtions.value = arr; - if (arr.length > 0) { - queryParams.neId = arr[0].value; - } - } - } else { - message.warning({ - content: t('common.noData'), - duration: 2, - }); + Promise.allSettled([getDict('cdr_cause_code')]) + .then(resArr => { + if (resArr[0].status === 'fulfilled') { + dict.cdrCauseCode = resArr[0].value; } }) .finally(() => { @@ -486,15 +460,6 @@ onBeforeUnmount(() => { - - - - - { >
- +