feat: mt版本移动过来的的告警/仪表/性能大屏页面
@@ -194,33 +194,13 @@ export async function exportAll(query: Record<string, any>) {
|
||||
* @returns bolb
|
||||
*/
|
||||
export async function origGet() {
|
||||
let totalSQL = `select count(*) as value,orig_severity as name from alarm WHERE alarm_status='1' and orig_severity!='Event' group by orig_severity`;
|
||||
|
||||
// 发起请求
|
||||
const result = await request({
|
||||
url: `/api/rest/databaseManagement/v1/select/omc_db/alarm`,
|
||||
return await request({
|
||||
url: `/neData/alarm/count/severity`,
|
||||
method: 'GET',
|
||||
params: {
|
||||
SQL: totalSQL,
|
||||
alarmStatus: 'Active',
|
||||
},
|
||||
timeout: 30_000,
|
||||
});
|
||||
////
|
||||
|
||||
// 解析数据
|
||||
if (result.code === RESULT_CODE_SUCCESS) {
|
||||
const itemData = result.data.data;
|
||||
if (Array.isArray(itemData)) {
|
||||
const v = itemData[0]['alarm'];
|
||||
if (Array.isArray(v)) {
|
||||
result.data = v;
|
||||
}
|
||||
if (v === null) {
|
||||
result.data = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -228,34 +208,13 @@ export async function origGet() {
|
||||
* @param filterFlag 查询参数
|
||||
* @returns object
|
||||
*/
|
||||
export async function top3Sel(filterFlag?: string) {
|
||||
let filter = ` WHERE alarm_status='1'and orig_severity='${filterFlag}'`;
|
||||
if (!filterFlag) filter = "WHERE alarm_status='1'";
|
||||
|
||||
let top3SQL = `select count(*) as value,ne_type as name from alarm ${filter} and orig_severity!='Event' group by ne_type ORDER BY value desc limit 0,3 `;
|
||||
|
||||
// 发起请求
|
||||
const result = await request({
|
||||
url: `/api/rest/databaseManagement/v1/select/omc_db/alarm`,
|
||||
export async function top3Sel() {
|
||||
return await request({
|
||||
url: `/neData/alarm/count/ne`,
|
||||
method: 'GET',
|
||||
params: {
|
||||
SQL: top3SQL,
|
||||
alarmStatus: 'Active',
|
||||
top: 3,
|
||||
},
|
||||
timeout: 30_000,
|
||||
});
|
||||
|
||||
// 解析数据
|
||||
if (result.code === RESULT_CODE_SUCCESS) {
|
||||
const itemData = result.data.data;
|
||||
if (Array.isArray(itemData)) {
|
||||
const v = itemData[0]['alarm'];
|
||||
if (Array.isArray(v)) {
|
||||
result.data = v;
|
||||
}
|
||||
if (v === null) {
|
||||
result.data = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -78,9 +78,9 @@ const alarmTypeType = ref<any>([
|
||||
|
||||
/**告警类型Top数据 */
|
||||
const alarmTypeTypeTop = ref<any>([
|
||||
{ name: 'AMF', value: 0 },
|
||||
{ name: 'UDM', value: 0 },
|
||||
{ name: 'SMF', value: 0 },
|
||||
{ neType: 'AMF', total: 0 },
|
||||
{ neType: 'UDM', total: 0 },
|
||||
{ neType: 'SMF', total: 0 },
|
||||
]);
|
||||
|
||||
//
|
||||
@@ -92,7 +92,7 @@ function initPicture() {
|
||||
if (res0.code === RESULT_CODE_SUCCESS && Array.isArray(res0.data)) {
|
||||
for (const item of res0.data) {
|
||||
let index = 0;
|
||||
switch (item.name) {
|
||||
switch (item.severity) {
|
||||
case 'Critical':
|
||||
index = 0;
|
||||
break;
|
||||
@@ -109,7 +109,7 @@ function initPicture() {
|
||||
// index = 4;
|
||||
// break;
|
||||
}
|
||||
alarmTypeType.value[index].value = Number(item.value);
|
||||
alarmTypeType.value[index].value = Number(item.total);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +119,7 @@ function initPicture() {
|
||||
alarmTypeTypeTop.value = alarmTypeTypeTop.value
|
||||
.concat(res1.data)
|
||||
.sort((a: any, b: any) => {
|
||||
return b.value - a.value;
|
||||
return b.total - a.total;
|
||||
})
|
||||
.slice(0, 3);
|
||||
}
|
||||
@@ -210,7 +210,7 @@ function initPicture() {
|
||||
{ offset: 1, color: '#2f54eb' },
|
||||
]), // 渐变
|
||||
},
|
||||
data: alarmTypeTypeTop.value,
|
||||
data: alarmTypeTypeTop.value.map((item: any) => item.total),
|
||||
},
|
||||
],
|
||||
// 柱状图设置
|
||||
@@ -237,7 +237,7 @@ function initPicture() {
|
||||
show: false,
|
||||
},
|
||||
inverse: true,
|
||||
data: alarmTypeTypeTop.value.map((item: any) => item.name),
|
||||
data: alarmTypeTypeTop.value.map((item: any) => item.neType),
|
||||
axisLabel: {
|
||||
color: '#A7D6F4',
|
||||
fontSize: 14,
|
||||
|
||||
284
src/views/dashboard/overview2/components/AlarnTypeBar/index.vue
Normal file
@@ -0,0 +1,284 @@
|
||||
<script setup lang="ts">
|
||||
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<HTMLElement | undefined>(undefined);
|
||||
|
||||
/**图实例对象 */
|
||||
const alarmTypeBarChart = ref<any>(null);
|
||||
|
||||
/**告警类型数据 */
|
||||
const alarmTypeType = ref<any>([
|
||||
{
|
||||
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<any>([
|
||||
{ neType: 'AMF', total: 0 },
|
||||
{ neType: 'UDM', total: 0 },
|
||||
{ neType: 'SMF', total: 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.severity) {
|
||||
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.total);
|
||||
}
|
||||
}
|
||||
}
|
||||
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.total - a.total;
|
||||
})
|
||||
.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.map((item: any) => item.total),
|
||||
},
|
||||
],
|
||||
// 柱状图设置
|
||||
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.neType),
|
||||
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();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="alarmTypeBar" class="chart-container"></div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chart-container {
|
||||
/* 设置图表容器大小和位置 */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
268
src/views/dashboard/overview2/components/IMSActivity/index.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<script setup lang="ts">
|
||||
import { parseDuration, parseDateToStr } from '@/utils/date-utils';
|
||||
import { eventData, eventId } from '../../hooks/useUserActivity';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import useDictStore from '@/store/modules/dict';
|
||||
import { onMounted, reactive } from 'vue';
|
||||
const { t } = useI18n();
|
||||
const { getDict } = useDictStore();
|
||||
|
||||
/**字典数据 */
|
||||
let dict: {
|
||||
/**CDR SIP响应代码类别类型 */
|
||||
cdrSipCode: DictType[];
|
||||
/**CDR 呼叫类型 */
|
||||
cdrCallType: DictType[];
|
||||
/**UE 事件认证代码类型 */
|
||||
ueAauthCode: DictType[];
|
||||
/**UE 事件类型 */
|
||||
ueEventType: DictType[];
|
||||
/**UE 事件CM状态 */
|
||||
ueEventCmState: DictType[];
|
||||
/**CDR SIP响应代码类别类型 */
|
||||
cdrSipCodeCause: DictType[];
|
||||
} = reactive({
|
||||
cdrSipCode: [],
|
||||
cdrCallType: [],
|
||||
ueAauthCode: [],
|
||||
ueEventType: [],
|
||||
ueEventCmState: [],
|
||||
cdrSipCodeCause: [],
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 初始字典数据
|
||||
Promise.allSettled([
|
||||
getDict('cdr_sip_code'),
|
||||
getDict('cdr_call_type'),
|
||||
getDict('ue_auth_code'),
|
||||
getDict('ue_event_type'),
|
||||
getDict('ue_event_cm_state'),
|
||||
getDict('cdr_sip_code_cause'),
|
||||
]).then(resArr => {
|
||||
if (resArr[0].status === 'fulfilled') {
|
||||
dict.cdrSipCode = resArr[0].value;
|
||||
}
|
||||
if (resArr[1].status === 'fulfilled') {
|
||||
dict.cdrCallType = resArr[1].value;
|
||||
}
|
||||
if (resArr[2].status === 'fulfilled') {
|
||||
dict.ueAauthCode = resArr[2].value;
|
||||
}
|
||||
if (resArr[3].status === 'fulfilled') {
|
||||
dict.ueEventType = resArr[3].value;
|
||||
}
|
||||
if (resArr[4].status === 'fulfilled') {
|
||||
dict.ueEventCmState = resArr[4].value;
|
||||
}
|
||||
if (resArr[5].status === 'fulfilled') {
|
||||
dict.cdrSipCodeCause = resArr[5].value;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="activty">
|
||||
<template v-for="item in eventData" :key="item.eId">
|
||||
<!-- CDR事件IMS -->
|
||||
<div class="card-cdr" :class="{ active: item.eId === eventId }" v-if="item.eType === 'ims_cdr'">
|
||||
<div class="card-cdr-item">
|
||||
<div>
|
||||
{{ t('views.dashboard.overview.userActivity.type') }}:
|
||||
<span>
|
||||
<DictTag :options="dict.cdrCallType" :value="item.data.callType" />
|
||||
</span>
|
||||
</div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="card-cdr-item">
|
||||
<div>
|
||||
{{ t('views.dashboard.overview.userActivity.caller') }}:
|
||||
<span :title="item.data.callerParty">
|
||||
{{ item.data.callerParty }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ t('views.dashboard.overview.userActivity.called') }}:
|
||||
<span :title="item.data.calledParty">
|
||||
{{ item.data.calledParty }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.data.callType !== 'sms'">
|
||||
{{ t('views.dashboard.overview.userActivity.duration') }}:
|
||||
<span>{{ parseDuration(item.data.callDuration) }}</span>
|
||||
</div>
|
||||
<div v-else></div>
|
||||
</div>
|
||||
<div>
|
||||
{{ t('views.dashboard.overview.userActivity.time') }}:
|
||||
<span :title="item.data.releaseTime">
|
||||
{{
|
||||
typeof item.data.releaseTime === 'number'
|
||||
? parseDateToStr(+item.data.releaseTime * 1000)
|
||||
: parseDateToStr(item.data.releaseTime)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ t('views.dashboard.overview.userActivity.result') }}:
|
||||
<span v-if="item.data.callType !== 'sms'">
|
||||
<DictTag :options="dict.cdrSipCode" :value="item.data.cause" value-default="0" />
|
||||
<DictTag :options="dict.cdrSipCodeCause" :value="item.data.cause" value-default="0" />
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ t('views.dashboard.overview.userActivity.resultOK') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.activty {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: 94%;
|
||||
color: #61a8ff;
|
||||
font-size: 0.75rem;
|
||||
|
||||
& .card-ue {
|
||||
border: 1px #61a8ff solid;
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-bottom: 0.3rem;
|
||||
line-height: 1rem;
|
||||
|
||||
& span {
|
||||
color: #68d8fe;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
&>div {
|
||||
width: 50%;
|
||||
white-space: nowrap;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&-w33 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
&>div {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .card-cdr {
|
||||
border: 1px #61a8ff solid;
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-bottom: 0.3rem;
|
||||
line-height: 1rem;
|
||||
|
||||
& span {
|
||||
color: #68d8fe;
|
||||
}
|
||||
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
&>div {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .active {
|
||||
color: #faad14;
|
||||
border: 1px #faad14 solid;
|
||||
animation: backInRight 0.3s alternate;
|
||||
|
||||
& span {
|
||||
color: #faad14;
|
||||
}
|
||||
}
|
||||
|
||||
/* 兼容当行显示字内容 */
|
||||
@media (max-width: 1720px) {
|
||||
& .card-cdr {
|
||||
&-item {
|
||||
display: block;
|
||||
|
||||
&>div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .card-ue {
|
||||
&-item {
|
||||
display: block;
|
||||
|
||||
&>div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 修改滚动条的样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
/* 设置滚动条宽度 */
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: #101129;
|
||||
/* 设置滚动条轨道背景颜色 */
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #28293f;
|
||||
/* 设置滚动条滑块颜色 */
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #68d8fe;
|
||||
/* 设置鼠标悬停时滚动条滑块颜色 */
|
||||
}
|
||||
|
||||
@keyframes backInRight {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
-webkit-transform: translateX(2000px) scale(0.7);
|
||||
transform: translateX(2000px) scale(0.7);
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 0.7;
|
||||
-webkit-transform: translateX(0) scale(0.7);
|
||||
transform: translateX(0) scale(0.7);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
352
src/views/dashboard/overview2/components/NeResources/index.vue
Normal file
@@ -0,0 +1,352 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, nextTick, watch } from 'vue';
|
||||
import * as echarts from 'echarts/core';
|
||||
import { GridComponent, GridComponentOption, TooltipComponent } from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { graphNodeClickID, graphNodeState } from '../../hooks/useTopology';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { markRaw } from 'vue';
|
||||
// 引入液体填充图表
|
||||
import 'echarts-liquidfill';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
echarts.use([GridComponent, TooltipComponent, CanvasRenderer]);
|
||||
|
||||
type EChartsOption = echarts.ComposeOption<GridComponentOption>;
|
||||
|
||||
/**图DOM节点实例对象 */
|
||||
const neResourcesDom = ref<HTMLElement | undefined>(undefined);
|
||||
|
||||
/**图实例对象 */
|
||||
const neResourcesChart = ref<any>(null);
|
||||
|
||||
// 当前选中的网元ID
|
||||
const currentNeId = ref('');
|
||||
|
||||
// 资源数据
|
||||
const resourceData = ref({
|
||||
neCpu: 1,
|
||||
sysCpu: 1,
|
||||
sysMem: 1,
|
||||
sysDisk: 1
|
||||
});
|
||||
|
||||
// 获取颜色
|
||||
function getColorByValue(value: number) {
|
||||
if (value >= 70) {
|
||||
return ['#f5222d', '#ff7875']; // 红色
|
||||
} else if (value >= 30) {
|
||||
return ['#2f54eb', '#597ef7']; // 蓝色
|
||||
} else {
|
||||
return ['#52c41a', '#95de64']; // 绿色
|
||||
}
|
||||
}
|
||||
|
||||
/**图数据 */
|
||||
const optionData: any = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}: {c}%'
|
||||
},
|
||||
grid: {
|
||||
top: '10%',
|
||||
bottom: '5%',
|
||||
left: '5%',
|
||||
right: '5%',
|
||||
containLabel: true
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: 'liquidFill',
|
||||
radius: '50%',
|
||||
center: ['15%', '35%'],
|
||||
data: [resourceData.value.neCpu / 100],
|
||||
name: t('views.dashboard.overview.resources.neCpu'),
|
||||
color: getColorByValue(resourceData.value.neCpu),
|
||||
backgroundStyle: {
|
||||
color: 'rgba(10, 60, 160, 0.1)'
|
||||
},
|
||||
label: {
|
||||
normal: {
|
||||
formatter: () => {
|
||||
return `${t('views.dashboard.overview.resources.neCpu')}\n${resourceData.value.neCpu}%`;
|
||||
},
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
show: true,
|
||||
borderDistance: 2,
|
||||
itemStyle: {
|
||||
borderColor: '#0a3ca0',
|
||||
borderWidth: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'liquidFill',
|
||||
radius: '50%',
|
||||
center: ['85%', '35%'],
|
||||
data: [resourceData.value.sysCpu / 100],
|
||||
name: t('views.dashboard.overview.resources.sysCpu'),
|
||||
color: getColorByValue(resourceData.value.sysCpu),
|
||||
backgroundStyle: {
|
||||
color: 'rgba(10, 60, 160, 0.1)'
|
||||
},
|
||||
label: {
|
||||
normal: {
|
||||
formatter: () => {
|
||||
return `${t('views.dashboard.overview.resources.sysCpu')}\n${resourceData.value.sysCpu}%`;
|
||||
},
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
show: true,
|
||||
borderDistance: 2,
|
||||
itemStyle: {
|
||||
borderColor: '#0a3ca0',
|
||||
borderWidth: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'liquidFill',
|
||||
radius: '50%',
|
||||
center: ['35%', '65%'],
|
||||
data: [resourceData.value.sysMem / 100],
|
||||
name: t('views.dashboard.overview.resources.sysMem'),
|
||||
color: getColorByValue(resourceData.value.sysMem),
|
||||
backgroundStyle: {
|
||||
color: 'rgba(10, 60, 160, 0.1)'
|
||||
},
|
||||
label: {
|
||||
normal: {
|
||||
formatter: () => {
|
||||
return `${t('views.dashboard.overview.resources.sysMem')}\n${resourceData.value.sysMem}%`;
|
||||
},
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
show: true,
|
||||
borderDistance: 2,
|
||||
itemStyle: {
|
||||
borderColor: '#0a3ca0',
|
||||
borderWidth: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'liquidFill',
|
||||
radius: '50%',
|
||||
center: ['65%', '65%'],
|
||||
data: [resourceData.value.sysDisk / 100],
|
||||
name: t('views.dashboard.overview.resources.sysDisk'),
|
||||
color: getColorByValue(resourceData.value.sysDisk),
|
||||
backgroundStyle: {
|
||||
color: 'rgba(10, 60, 160, 0.1)'
|
||||
},
|
||||
label: {
|
||||
normal: {
|
||||
formatter: () => {
|
||||
return `${t('views.dashboard.overview.resources.sysDisk')}\n${resourceData.value.sysDisk}%`;
|
||||
},
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: '#fff'
|
||||
}
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
show: true,
|
||||
borderDistance: 2,
|
||||
itemStyle: {
|
||||
borderColor: '#0a3ca0',
|
||||
borderWidth: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**图数据渲染 */
|
||||
function handleRanderChart(
|
||||
container: HTMLElement | undefined,
|
||||
option: any
|
||||
) {
|
||||
if (!container) return;
|
||||
neResourcesChart.value = markRaw(echarts.init(container));
|
||||
option && neResourcesChart.value.setOption(option);
|
||||
|
||||
// 创建 ResizeObserver 实例
|
||||
var observer = new ResizeObserver(entries => {
|
||||
if (neResourcesChart.value) {
|
||||
neResourcesChart.value.resize();
|
||||
}
|
||||
});
|
||||
// 监听元素大小变化
|
||||
observer.observe(container);
|
||||
}
|
||||
|
||||
function fnChangeData(data: any[], itemID: string) {
|
||||
const neType = itemID.split('_')[0];
|
||||
const neID = itemID.split('_')[1];
|
||||
currentNeId.value = neID;
|
||||
|
||||
let info = data.find((item: any) => item.id === neType);
|
||||
if (!info || !info.neStateMap[neID]?.online) return;
|
||||
|
||||
let sysCpuUsage = 0;
|
||||
let nfCpuUsage = 0;
|
||||
if (info.neStateMap[neID].cpu) {
|
||||
nfCpuUsage = info.neStateMap[neID].cpu.nfCpuUsage;
|
||||
const nfCpu = +(info.neStateMap[neID].cpu.nfCpuUsage / 100);
|
||||
nfCpuUsage = +nfCpu.toFixed(2);
|
||||
if (nfCpuUsage > 100) {
|
||||
nfCpuUsage = 100;
|
||||
}
|
||||
|
||||
sysCpuUsage = info.neStateMap[neID].cpu.sysCpuUsage;
|
||||
let sysCpu = +(info.neStateMap[neID].cpu.sysCpuUsage / 100);
|
||||
sysCpuUsage = +sysCpu.toFixed(2);
|
||||
if (sysCpuUsage > 100) {
|
||||
sysCpuUsage = 100;
|
||||
}
|
||||
}
|
||||
|
||||
let sysMemUsage = 0;
|
||||
if (info.neStateMap[neID].mem) {
|
||||
const men = info.neStateMap[neID].mem.sysMemUsage;
|
||||
sysMemUsage = +(men / 100).toFixed(2);
|
||||
if (sysMemUsage > 100) {
|
||||
sysMemUsage = 100;
|
||||
}
|
||||
}
|
||||
|
||||
let sysDiskUsage = 0;
|
||||
if (info.neStateMap[neID].disk && Array.isArray(info.neStateMap[neID].disk.partitionInfo)) {
|
||||
let disks: any[] = info.neStateMap[neID].disk.partitionInfo;
|
||||
disks = disks.sort((a, b) => +b.used - +a.used);
|
||||
if (disks.length > 0) {
|
||||
const { total, used } = disks[0];
|
||||
sysDiskUsage = +((used / total) * 100).toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
resourceData.value = {
|
||||
neCpu: nfCpuUsage,
|
||||
sysCpu: sysCpuUsage,
|
||||
sysMem: sysMemUsage,
|
||||
sysDisk: sysDiskUsage
|
||||
};
|
||||
|
||||
// 更新图表数据
|
||||
neResourcesChart.value.setOption({
|
||||
series: [
|
||||
{
|
||||
data: [resourceData.value.neCpu / 100],
|
||||
color: getColorByValue(resourceData.value.neCpu),
|
||||
label: {
|
||||
normal: {
|
||||
formatter: () => {
|
||||
return `${t('views.dashboard.overview.resources.neCpu')}\n${resourceData.value.neCpu}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
data: [resourceData.value.sysCpu / 100],
|
||||
color: getColorByValue(resourceData.value.sysCpu),
|
||||
label: {
|
||||
normal: {
|
||||
formatter: () => {
|
||||
return `${t('views.dashboard.overview.resources.sysCpu')}\n${resourceData.value.sysCpu}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
data: [resourceData.value.sysMem / 100],
|
||||
color: getColorByValue(resourceData.value.sysMem),
|
||||
label: {
|
||||
normal: {
|
||||
formatter: () => {
|
||||
return `${t('views.dashboard.overview.resources.sysMem')}\n${resourceData.value.sysMem}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
data: [resourceData.value.sysDisk / 100],
|
||||
color: getColorByValue(resourceData.value.sysDisk),
|
||||
label: {
|
||||
normal: {
|
||||
formatter: () => {
|
||||
return `${t('views.dashboard.overview.resources.sysDisk')}\n${resourceData.value.sysDisk}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
graphNodeState,
|
||||
v => {
|
||||
fnChangeData(v, graphNodeClickID.value);
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
watch(graphNodeClickID, v => {
|
||||
fnChangeData(graphNodeState.value, v);
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
handleRanderChart(neResourcesDom.value, optionData);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="resource-panel">
|
||||
<div ref="neResourcesDom" class="chart"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.resource-panel {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: -32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.panel-title {
|
||||
font-size: 14px;
|
||||
color: #00fcff;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid rgba(10, 115, 255, 0.3);
|
||||
}
|
||||
|
||||
.chart {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
337
src/views/dashboard/overview2/components/Topology/index.vue
Normal file
@@ -0,0 +1,337 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||
import { listAllNeInfo } from '@/api/ne/neInfo';
|
||||
import { message } from 'ant-design-vue/es';
|
||||
import { getGraphData } from '@/api/monitor/topology';
|
||||
import { Graph, GraphData, Tooltip } from '@antv/g6';
|
||||
import { parseBasePath } from '@/plugins/file-static-url';
|
||||
import { edgeLineAnimateState } from '@/views/monitor/topologyBuild/hooks/registerEdge';
|
||||
import { nodeImageAnimateState } from '@/views/monitor/topologyBuild/hooks/registerNode';
|
||||
import {
|
||||
graphG6,
|
||||
graphState,
|
||||
graphNodeClickID,
|
||||
notNeNodes,
|
||||
} from '../../hooks/useTopology';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
const { t } = useI18n();
|
||||
|
||||
/**图DOM节点实例对象 */
|
||||
const graphG6Dom = ref<HTMLElement | undefined>(undefined);
|
||||
|
||||
/**图节点展示 */
|
||||
const graphNodeTooltip = new Tooltip({
|
||||
offsetX: 10,
|
||||
offsetY: 20,
|
||||
getContent(evt) {
|
||||
if (!evt) return t('views.monitor.topologyBuild.graphNotInfo');
|
||||
const { id, label, neState, neInfoList, neStateMap }: any = evt.item?.getModel();
|
||||
//console.log(neInfoList,neState,neInfoList);
|
||||
if (notNeNodes.includes(id)) {
|
||||
return `<div><span>${label || id}</span></div>`;
|
||||
}
|
||||
if (!neState) {
|
||||
return `<div><span>${label || id}</span></div>`;
|
||||
}
|
||||
|
||||
// 获取同类型网元列表
|
||||
const sameTypeNes = neInfoList || [];
|
||||
|
||||
// 如果没有网元或只有一个网元,显示原来的信息
|
||||
if (sameTypeNes.length <= 1) {
|
||||
return `
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 200px;
|
||||
"
|
||||
>
|
||||
<div><strong>${t('views.monitor.topology.state')}:</strong><span>
|
||||
${
|
||||
neState.online
|
||||
? t('views.monitor.topology.normalcy')
|
||||
: t('views.monitor.topology.exceptions')
|
||||
}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.refreshTime')}:</strong><span>
|
||||
${neState.refreshTime ?? '--'}
|
||||
</span></div>
|
||||
<div>========================</div>
|
||||
<div><strong>ID:</strong><span>${neState.neId}</span></div>
|
||||
<div><strong>${t('views.monitor.topology.name')}:</strong><span>
|
||||
${neState.neName ?? '--'}
|
||||
</span></div>
|
||||
<div><strong>IP:</strong><span>${neState.neIP ?? '--'}</span></div>
|
||||
<div><strong>${t('views.monitor.topology.version')}:</strong><span>
|
||||
${neState.version ?? '--'}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.serialNum')}:</strong><span>
|
||||
${neState.sn ?? '--'}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.expiryDate')}:</strong><span>
|
||||
${neState.expire ?? '--'}
|
||||
</span></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 如果有多个网元,聚合显示
|
||||
let content = `
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 300px;
|
||||
"
|
||||
>
|
||||
<div><strong>${t('views.monitor.topology.state')}:</strong><span>
|
||||
${
|
||||
neState.online
|
||||
? t('views.monitor.topology.normalcy')
|
||||
: t('views.monitor.topology.exceptions')
|
||||
}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.refreshTime')}:</strong><span>
|
||||
${neState.refreshTime ?? '--'}
|
||||
</span></div>
|
||||
<div>========================</div>`;
|
||||
|
||||
// 为每个同类型网元添加信息
|
||||
sameTypeNes.forEach((ne: any, index: number) => {
|
||||
// 获取该网元的状态信息
|
||||
const neStateInfo = neStateMap?.[ne.neId] ||
|
||||
(ne.neId === neState.neId ? neState : {});
|
||||
|
||||
content += `
|
||||
<div style="margin-top: 8px;"><strong>${t('views.monitor.topology.name')}:${ne.neName || id + '_' + ne.neId}</strong></div>
|
||||
<div><strong>ID:</strong><span>${ne.neId || '--'}</span></div>
|
||||
<div><strong>IP:</strong><span>${neStateInfo.neIP || ne.neIP || '--'}</span></div>
|
||||
<div><strong>${t('views.monitor.topology.version')}:</strong><span>
|
||||
${neStateInfo.version || ne.version || '--'}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.serialNum')}:</strong><span>
|
||||
${neStateInfo.sn || ne.sn || '--'}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.expiryDate')}:</strong><span>
|
||||
${neStateInfo.expire || ne.expire || '--'}
|
||||
</span></div>
|
||||
<div><strong>${t('views.monitor.topology.state')}:</strong><span>
|
||||
${
|
||||
neStateInfo.online !== undefined
|
||||
? (neStateInfo.online
|
||||
? t('views.monitor.topology.normalcy')
|
||||
: t('views.monitor.topology.exceptions'))
|
||||
: 'undefined'
|
||||
}
|
||||
</span></div>
|
||||
${index < sameTypeNes.length - 1 ? '<div>------------------------</div>' : ''}
|
||||
`;
|
||||
});
|
||||
|
||||
content += '</div>';
|
||||
return content;
|
||||
},
|
||||
itemTypes: ['node'],
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**图数据渲染 */
|
||||
function handleRanderGraph(
|
||||
container: HTMLElement | undefined,
|
||||
data: GraphData
|
||||
) {
|
||||
if (!container) return;
|
||||
const { clientHeight, clientWidth } = container;
|
||||
|
||||
edgeLineAnimateState();
|
||||
nodeImageAnimateState();
|
||||
|
||||
const graph = new Graph({
|
||||
container: container,
|
||||
width: clientWidth,
|
||||
height: clientHeight - 36,
|
||||
fitCenter: true,
|
||||
fitView: true,
|
||||
fitViewPadding: [20],
|
||||
autoPaint: true,
|
||||
modes: {
|
||||
// default: ['drag-canvas', 'zoom-canvas'],
|
||||
},
|
||||
groupByTypes: false,
|
||||
nodeStateStyles: {
|
||||
selected: {
|
||||
fill: 'transparent',
|
||||
},
|
||||
},
|
||||
plugins: [graphNodeTooltip],
|
||||
animate: true, // 是否使用动画过度,默认为 false
|
||||
animateCfg: {
|
||||
duration: 500, // Number,一次动画的时长
|
||||
easing: 'linearEasing', // String,动画函数
|
||||
},
|
||||
});
|
||||
graph.data(data);
|
||||
graph.render();
|
||||
|
||||
|
||||
graphG6.value = graph;
|
||||
|
||||
// 创建 ResizeObserver 实例
|
||||
var observer = new ResizeObserver(function (entries) {
|
||||
// 当元素大小发生变化时触发回调函数
|
||||
entries.forEach(function (entry) {
|
||||
if (!graphG6.value) {
|
||||
return;
|
||||
}
|
||||
graphG6.value.changeSize(
|
||||
entry.contentRect.width,
|
||||
entry.contentRect.height - 20
|
||||
);
|
||||
graphG6.value.fitCenter();
|
||||
});
|
||||
});
|
||||
// 监听元素大小变化
|
||||
observer.observe(container);
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图组数据渲染到画布
|
||||
* @param reload 是否重载数据
|
||||
*/
|
||||
function fnGraphDataLoad(reload: boolean = false) {
|
||||
Promise.all([
|
||||
getGraphData(graphState.group),
|
||||
listAllNeInfo({
|
||||
bandStatus: false,
|
||||
}),
|
||||
])
|
||||
.then(resArr => {
|
||||
const graphRes = resArr[0];
|
||||
const neRes = resArr[1];
|
||||
if (
|
||||
graphRes.code === RESULT_CODE_SUCCESS &&
|
||||
Array.isArray(graphRes.data.nodes) &&
|
||||
graphRes.data.nodes.length > 0 &&
|
||||
neRes.code === RESULT_CODE_SUCCESS &&
|
||||
Array.isArray(neRes.data) &&
|
||||
neRes.data.length > 0
|
||||
) {
|
||||
return {
|
||||
graphData: graphRes.data,
|
||||
neList: neRes.data,
|
||||
};
|
||||
} else {
|
||||
message.warning({
|
||||
content: t('views.monitor.topology.noData'),
|
||||
duration: 5,
|
||||
});
|
||||
}
|
||||
})
|
||||
.then(res => {
|
||||
if (!res) return;
|
||||
const { combos, edges, nodes } = res.graphData;
|
||||
|
||||
// 按网元类型分组
|
||||
const neTypeMap = new Map();
|
||||
res.neList.forEach(ne => {
|
||||
if (!ne.neType) return;
|
||||
if (!neTypeMap.has(ne.neType)) {
|
||||
neTypeMap.set(ne.neType, []);
|
||||
}
|
||||
neTypeMap.get(ne.neType).push(ne);
|
||||
});
|
||||
|
||||
// 节点过滤
|
||||
const nf: Record<string, any>[] = nodes.filter(
|
||||
(node: Record<string, any>) => {
|
||||
Reflect.set(node, 'neState', { online: false });
|
||||
Reflect.set(node, 'neStateMap', {}); // 初始化状态映射
|
||||
|
||||
// 图片路径处理
|
||||
if (node.img) node.img = parseBasePath(node.img);
|
||||
if (node.icon.show && node.icon?.img){
|
||||
node.icon.img = parseBasePath(node.icon.img);
|
||||
}
|
||||
|
||||
// 遍历是否有网元数据
|
||||
const nodeID: string = node.id;
|
||||
|
||||
// 处理非网元节点
|
||||
if (notNeNodes.includes(nodeID)) {
|
||||
return true;
|
||||
}
|
||||
//(neTypeMap.get(nodeID),nodeID,node.neState)
|
||||
// 处理网元节点
|
||||
if (neTypeMap.has(nodeID)) {
|
||||
// all NeInfo
|
||||
Reflect.set(node, 'neInfoList', neTypeMap.get(nodeID));
|
||||
|
||||
Reflect.set(node, 'neInfo', neTypeMap.get(nodeID)[0] || {});
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
// 边过滤
|
||||
const ef: Record<string, any>[] = edges.filter(
|
||||
(edge: Record<string, any>) => {
|
||||
const edgeSource: string = edge.source;
|
||||
const edgeTarget: string = edge.target;
|
||||
const hasNeS = nf.some(n => n.id === edgeSource);
|
||||
const hasNeT = nf.some(n => n.id === edgeTarget);
|
||||
if (hasNeS && hasNeT) {
|
||||
return true;
|
||||
}
|
||||
if (hasNeS && notNeNodes.includes(edgeTarget)) {
|
||||
return true;
|
||||
}
|
||||
if (hasNeT && notNeNodes.includes(edgeSource)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
// 分组过滤
|
||||
combos.forEach((combo: Record<string, any>) => {
|
||||
const comboChildren: Record<string, any>[] = combo.children;
|
||||
combo.children = comboChildren.filter(c => nf.some(n => n.id === c.id));
|
||||
return combo;
|
||||
});
|
||||
|
||||
// 图数据
|
||||
graphState.data = { combos, edges: ef, nodes: nf };
|
||||
})
|
||||
.finally(() => {
|
||||
if (graphState.data.length < 0) return;
|
||||
// 重载数据
|
||||
if (reload) {
|
||||
graphG6.value.read(graphState.data);
|
||||
} else {
|
||||
handleRanderGraph(graphG6Dom.value, graphState.data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fnGraphDataLoad(false);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="graphG6Dom" class="chart"></div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
291
src/views/dashboard/overview2/components/UPFFlow/index.vue
Normal file
@@ -0,0 +1,291 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { listKPIData } from '@/api/perfManage/goldTarget';
|
||||
import * as echarts from 'echarts/core';
|
||||
import {
|
||||
TooltipComponent,
|
||||
TooltipComponentOption,
|
||||
GridComponent,
|
||||
GridComponentOption,
|
||||
LegendComponent,
|
||||
LegendComponentOption,
|
||||
} from 'echarts/components';
|
||||
import { LineChart, LineSeriesOption } from 'echarts/charts';
|
||||
import { UniversalTransition } from 'echarts/features';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { markRaw } from 'vue';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||
import { upfFlowData, upfFlowParse } from '../../hooks/useUPFTotalFlow';
|
||||
import { upfWhoId } from '../../hooks/useWS';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
echarts.use([
|
||||
TooltipComponent,
|
||||
GridComponent,
|
||||
LegendComponent,
|
||||
LineChart,
|
||||
CanvasRenderer,
|
||||
UniversalTransition,
|
||||
]);
|
||||
|
||||
type EChartsOption = echarts.ComposeOption<
|
||||
| TooltipComponentOption
|
||||
| GridComponentOption
|
||||
| LegendComponentOption
|
||||
| LineSeriesOption
|
||||
>;
|
||||
|
||||
/**图DOM节点实例对象 */
|
||||
const upfFlow = ref<HTMLElement | undefined>(undefined);
|
||||
|
||||
/**图实例对象 */
|
||||
const upfFlowChart = ref<any>(null);
|
||||
|
||||
function fnDesign(container: HTMLElement | undefined, option: EChartsOption) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
if (!upfFlowChart.value) {
|
||||
upfFlowChart.value = markRaw(echarts.init(container, 'light'));
|
||||
}
|
||||
|
||||
option && upfFlowChart.value.setOption(option);
|
||||
|
||||
// 创建 ResizeObserver 实例
|
||||
var observer = new ResizeObserver(entries => {
|
||||
if (upfFlowChart.value) {
|
||||
upfFlowChart.value.resize();
|
||||
}
|
||||
});
|
||||
// 监听元素大小变化
|
||||
observer.observe(container);
|
||||
}
|
||||
|
||||
//渲染速率图
|
||||
function handleRanderChart() {
|
||||
const { lineXTime, lineYUp, lineYDown } = upfFlowData.value;
|
||||
|
||||
var yAxisSeries: any = [
|
||||
{
|
||||
name: t('views.dashboard.overview.upfFlow.up'),
|
||||
type: 'line',
|
||||
color: 'rgba(250, 219, 20)',
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(250, 219, 20, .5)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(250, 219, 20, 0.5)',
|
||||
},
|
||||
]),
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10,
|
||||
},
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
formatter: '{b}',
|
||||
data: lineYUp,
|
||||
},
|
||||
{
|
||||
name: t('views.dashboard.overview.upfFlow.down'),
|
||||
type: 'line',
|
||||
color: 'rgba(92, 123, 217)',
|
||||
smooth: true,
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{
|
||||
offset: 0,
|
||||
color: 'rgba(92, 123, 217, .5)',
|
||||
},
|
||||
{
|
||||
offset: 1,
|
||||
color: 'rgba(92, 123, 217, 0.5)',
|
||||
},
|
||||
]),
|
||||
shadowColor: 'rgba(0, 0, 0, 0.1)',
|
||||
shadowBlur: 10,
|
||||
},
|
||||
symbol: 'circle',
|
||||
symbolSize: 5,
|
||||
formatter: '{b}',
|
||||
data: lineYDown,
|
||||
},
|
||||
];
|
||||
|
||||
const optionData: EChartsOption = {
|
||||
tooltip: {
|
||||
show: true, //是否显示提示框组件
|
||||
trigger: 'axis',
|
||||
//formatter:'{a0}:{c0}<br>{a1}:{c1}'
|
||||
formatter: function (param: any) {
|
||||
var tip = '';
|
||||
if (param !== null && param.length > 0) {
|
||||
tip += param[0].name + '<br />';
|
||||
for (var i = 0; i < param.length; i++) {
|
||||
tip +=
|
||||
param[i].marker +
|
||||
param[i].seriesName +
|
||||
': ' +
|
||||
param[i].value +
|
||||
'<br />';
|
||||
}
|
||||
}
|
||||
return tip;
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: yAxisSeries.map((s: any) => s.name),
|
||||
textStyle: {
|
||||
fontSize: 12,
|
||||
color: 'rgb(0,253,255,0.6)',
|
||||
},
|
||||
left: 'center', // 设置图例居中
|
||||
},
|
||||
grid: {
|
||||
top: '14%',
|
||||
left: '4%',
|
||||
right: '4%',
|
||||
bottom: '16%',
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: lineXTime,
|
||||
axisLabel: {
|
||||
formatter: function (params: any) {
|
||||
return params;
|
||||
},
|
||||
fontSize: 14,
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgb(0,253,255,0.6)',
|
||||
},
|
||||
},
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
name: '(Mbps)',
|
||||
nameTextStyle: {
|
||||
fontSize: 12, // 设置文字距离x轴的距离
|
||||
padding: [0, -10, 0, 0], // 设置名称在x轴方向上的偏移
|
||||
},
|
||||
type: 'value',
|
||||
// splitNumber: 4,
|
||||
min: 0,
|
||||
//max: 300,
|
||||
axisLabel: {
|
||||
formatter: '{value}',
|
||||
},
|
||||
splitLine: {
|
||||
lineStyle: {
|
||||
color: 'rgb(23,255,243,0.3)',
|
||||
},
|
||||
},
|
||||
axisLine: {
|
||||
lineStyle: {
|
||||
color: 'rgb(0,253,255,0.6)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
series: yAxisSeries,
|
||||
};
|
||||
fnDesign(upfFlow.value, optionData);
|
||||
}
|
||||
|
||||
/**查询初始UPF数据 */
|
||||
function fnGetInitData() {
|
||||
// 查询5分钟前的
|
||||
const nowDate = new Date().getTime();
|
||||
|
||||
listKPIData({
|
||||
neType: 'UPF',
|
||||
neId: upfWhoId.value,
|
||||
startTime: nowDate - 5 * 60 * 1000,
|
||||
endTime: nowDate,
|
||||
|
||||
interval: 5, // 5秒
|
||||
sortField: 'timeGroup',
|
||||
sortOrder: 'asc',
|
||||
})
|
||||
.then(res => {
|
||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
|
||||
for (const item of res.data) {
|
||||
upfFlowParse(item);
|
||||
}
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
handleRanderChart();
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => upfFlowData.value,
|
||||
v => {
|
||||
if (upfFlowChart.value == null) return;
|
||||
upfFlowChart.value.setOption({
|
||||
xAxis: {
|
||||
data: v.lineXTime,
|
||||
},
|
||||
series: [
|
||||
{
|
||||
data: v.lineYUp,
|
||||
},
|
||||
{
|
||||
data: v.lineYDown,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
fnGetInitData();
|
||||
|
||||
// setInterval(() => {
|
||||
// upfFlowData.value.lineXTime.push(parseDateToStr(new Date()));
|
||||
// const upN3 = parseSizeFromKbs(+145452, 5);
|
||||
// upfFlowData.value.lineYUp.push(upN3[0]);
|
||||
// const downN6 = parseSizeFromKbs(+232343, 5);
|
||||
// upfFlowData.value.lineYDown.push(downN6[0]);
|
||||
|
||||
// upfFlowChart.value.setOption({
|
||||
// xAxis: {
|
||||
// data: upfFlowData.value.lineXTime,
|
||||
// },
|
||||
// series: [
|
||||
// {
|
||||
// data: upfFlowData.value.lineYUp,
|
||||
// },
|
||||
// {
|
||||
// data: upfFlowData.value.lineYDown,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// }, 5000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="upfFlow" class="chart-container"></div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.chart-container {
|
||||
/* 设置图表容器大小和位置 */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
324
src/views/dashboard/overview2/components/UserActivity/index.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<script setup lang="ts">
|
||||
import { parseDuration, parseDateToStr } from '@/utils/date-utils';
|
||||
import { eventData, eventId } from '../../hooks/useUserActivity';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
import useDictStore from '@/store/modules/dict';
|
||||
import { onMounted, reactive } from 'vue';
|
||||
const { t } = useI18n();
|
||||
const { getDict } = useDictStore();
|
||||
|
||||
/**字典数据 */
|
||||
let dict: {
|
||||
/**CDR SIP响应代码类别类型 */
|
||||
cdrSipCode: DictType[];
|
||||
/**CDR 呼叫类型 */
|
||||
cdrCallType: DictType[];
|
||||
/**UE 事件认证代码类型 */
|
||||
ueAauthCode: DictType[];
|
||||
/**UE 事件类型 */
|
||||
ueEventType: DictType[];
|
||||
/**UE 事件CM状态 */
|
||||
ueEventCmState: DictType[];
|
||||
/**CDR SIP响应代码类别类型 */
|
||||
cdrSipCodeCause: DictType[];
|
||||
} = reactive({
|
||||
cdrSipCode: [],
|
||||
cdrCallType: [],
|
||||
ueAauthCode: [],
|
||||
ueEventType: [],
|
||||
ueEventCmState: [],
|
||||
cdrSipCodeCause: [],
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 初始字典数据
|
||||
Promise.allSettled([
|
||||
getDict('cdr_sip_code'),
|
||||
getDict('cdr_call_type'),
|
||||
getDict('ue_auth_code'),
|
||||
getDict('ue_event_type'),
|
||||
getDict('ue_event_cm_state'),
|
||||
getDict('cdr_sip_code_cause'),
|
||||
]).then(resArr => {
|
||||
if (resArr[0].status === 'fulfilled') {
|
||||
dict.cdrSipCode = resArr[0].value;
|
||||
}
|
||||
if (resArr[1].status === 'fulfilled') {
|
||||
dict.cdrCallType = resArr[1].value;
|
||||
}
|
||||
if (resArr[2].status === 'fulfilled') {
|
||||
dict.ueAauthCode = resArr[2].value;
|
||||
}
|
||||
if (resArr[3].status === 'fulfilled') {
|
||||
dict.ueEventType = resArr[3].value;
|
||||
}
|
||||
if (resArr[4].status === 'fulfilled') {
|
||||
dict.ueEventCmState = resArr[4].value;
|
||||
}
|
||||
if (resArr[5].status === 'fulfilled') {
|
||||
dict.cdrSipCodeCause = resArr[5].value;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="activty">
|
||||
<template v-for="item in eventData" :key="item.eId">
|
||||
<!-- UE事件AMF -->
|
||||
<div
|
||||
class="card-ue"
|
||||
:class="{ active: item.eId === eventId }"
|
||||
v-if="item.eType === 'amf_ue'"
|
||||
>
|
||||
<div class="card-ue-item">
|
||||
<div>
|
||||
{{ t('views.dashboard.overview.userActivity.type') }}:
|
||||
<span>
|
||||
<DictTag :options="dict.ueEventType" :value="item.type" />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
IMSI: <span :title="item.data.imsi">{{ item.data.imsi }}</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div class="card-ue-w33" v-if="item.type === 'auth-result'">
|
||||
<div>
|
||||
GNB ID: <span>{{ item.data.gNBID }}</span>
|
||||
</div>
|
||||
<div>
|
||||
Cell ID: <span>{{ item.data.cellID }}</span>
|
||||
</div>
|
||||
<div>
|
||||
TAC ID: <span>{{ item.data.tacID }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ t('views.dashboard.overview.userActivity.time') }}:
|
||||
<template v-if="item.data?.time">
|
||||
{{ parseDateToStr(item.data.time) }}
|
||||
</template>
|
||||
<template v-else-if="item.data?.timestamp">
|
||||
{{ parseDateToStr(+item.data.timestamp * 1000) }}
|
||||
</template>
|
||||
<template v-else> - </template>
|
||||
</div>
|
||||
<div v-if="item.type === 'auth-result'">
|
||||
{{ t('views.dashboard.overview.userActivity.result') }}:
|
||||
<span>
|
||||
<DictTag :options="dict.ueAauthCode" :value="item.data.result" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.type === 'detach'">
|
||||
{{ t('views.dashboard.overview.userActivity.result') }}:
|
||||
<span>{{ t('views.dashboard.overview.userActivity.resultOK') }}</span>
|
||||
</div>
|
||||
<div class="card-ue-w33" v-if="item.type === 'cm-state'">
|
||||
{{ t('views.dashboard.overview.userActivity.result') }}:
|
||||
<span>
|
||||
<DictTag :options="dict.ueEventCmState" :value="item.data.result" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- UE事件MME -->
|
||||
<div
|
||||
class="card-ue"
|
||||
:class="{ active: item.eId === eventId }"
|
||||
v-if="item.eType === 'mme_ue'"
|
||||
>
|
||||
<div class="card-ue-item">
|
||||
<div>
|
||||
{{ t('views.dashboard.overview.userActivity.type') }}:
|
||||
<span v-if="item.type === 'cm-state'">
|
||||
{{
|
||||
dict.ueEventType
|
||||
.find(s => s.value === item.type)
|
||||
?.label.replace('CM', 'ECM')
|
||||
}}
|
||||
</span>
|
||||
<span v-else>
|
||||
<DictTag :options="dict.ueEventType" :value="item.type" />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
IMSI: <span :title="item.data?.imsi">{{ item.data?.imsi }}</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
|
||||
<div class="card-ue-w33" v-if="item.type === 'auth-result'">
|
||||
<div>
|
||||
ENB ID: <span>{{ item.data.eNBID }}</span>
|
||||
</div>
|
||||
<div>
|
||||
Cell ID: <span>{{ item.data.cellID }}</span>
|
||||
</div>
|
||||
<div>
|
||||
TAC ID: <span>{{ item.data.tacID }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{{ t('views.dashboard.overview.userActivity.time') }}:
|
||||
<template v-if="item.data?.time">
|
||||
{{ parseDateToStr(item.data.time) }}
|
||||
</template>
|
||||
<template v-else-if="item.data?.timestamp">
|
||||
{{
|
||||
typeof item.data?.timestamp === 'number'
|
||||
? parseDateToStr(+item.data?.timestamp * 1000)
|
||||
: parseDateToStr(item.data?.timestamp)
|
||||
}}
|
||||
</template>
|
||||
<template v-else> - </template>
|
||||
</div>
|
||||
<div v-if="item.type === 'auth-result'">
|
||||
{{ t('views.dashboard.overview.userActivity.result') }}:
|
||||
<span>
|
||||
<DictTag :options="dict.ueAauthCode" :value="item.data.result" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="item.type === 'detach'">
|
||||
{{ t('views.dashboard.overview.userActivity.result') }}:
|
||||
<span>{{ t('views.dashboard.overview.userActivity.resultOK') }}</span>
|
||||
</div>
|
||||
<div class="card-ue-w33" v-if="item.type === 'cm-state'">
|
||||
{{ t('views.dashboard.overview.userActivity.result') }}:
|
||||
<span>
|
||||
<DictTag :options="dict.ueEventCmState" :value="item.data.result" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.activty {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: 94%;
|
||||
color: #61a8ff;
|
||||
font-size: 0.75rem;
|
||||
& .card-ue {
|
||||
border: 1px #61a8ff solid;
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-bottom: 0.3rem;
|
||||
line-height: 1rem;
|
||||
& span {
|
||||
color: #68d8fe;
|
||||
}
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
& > div {
|
||||
width: 50%;
|
||||
white-space: nowrap;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
&-w33 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
& > div {
|
||||
width: 33%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .card-cdr {
|
||||
border: 1px #61a8ff solid;
|
||||
border-radius: 4px;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin-bottom: 0.3rem;
|
||||
line-height: 1rem;
|
||||
& span {
|
||||
color: #68d8fe;
|
||||
}
|
||||
&-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
& > div {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
text-align: start;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .active {
|
||||
color: #faad14;
|
||||
border: 1px #faad14 solid;
|
||||
animation: backInRight 0.3s alternate;
|
||||
& span {
|
||||
color: #faad14;
|
||||
}
|
||||
}
|
||||
|
||||
/* 兼容当行显示字内容 */
|
||||
@media (max-width: 1720px) {
|
||||
& .card-cdr {
|
||||
&-item {
|
||||
display: block;
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
& .card-ue {
|
||||
&-item {
|
||||
display: block;
|
||||
& > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 修改滚动条的样式 */
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px; /* 设置滚动条宽度 */
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background-color: #101129; /* 设置滚动条轨道背景颜色 */
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #28293f; /* 设置滚动条滑块颜色 */
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #68d8fe; /* 设置鼠标悬停时滚动条滑块颜色 */
|
||||
}
|
||||
|
||||
@keyframes backInRight {
|
||||
0% {
|
||||
opacity: 0.7;
|
||||
-webkit-transform: translateX(2000px) scale(0.7);
|
||||
transform: translateX(2000px) scale(0.7);
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 0.7;
|
||||
-webkit-transform: translateX(0) scale(0.7);
|
||||
transform: translateX(0) scale(0.7);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
399
src/views/dashboard/overview2/css/index.css
Normal file
@@ -0,0 +1,399 @@
|
||||
.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-image: url(../images/bj.png);
|
||||
height: 100vh;
|
||||
margin-bottom: -20px;
|
||||
background-size:80% 80%;
|
||||
background-attachment:fixed;
|
||||
-webkit-background-size: cover;
|
||||
}
|
||||
|
||||
|
||||
.column {
|
||||
flex: 3;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 边框 */
|
||||
.panel {
|
||||
box-sizing: border-box;
|
||||
border: 2px solid rgba(252, 252, 252, 0);
|
||||
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;
|
||||
}
|
||||
|
||||
.leftright {
|
||||
width: 100%;
|
||||
min-height: 2.5rem;
|
||||
background: url(../images/title.png) no-repeat center center;
|
||||
background-size: 100%;
|
||||
color: #4c9bfd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
padding: 0.5rem 1.2rem;
|
||||
border-radius: 0.7rem 0.7rem 0 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
text-shadow: 0 1px 4px #000a;
|
||||
flex-wrap: nowrap;
|
||||
/* 保证内容不换行且居中 */
|
||||
}
|
||||
|
||||
.leftright .title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.centerStyle {
|
||||
width: 100%;
|
||||
min-height: 2.5rem;
|
||||
background: url(../images/title.png) no-repeat center center;
|
||||
background-size: 90%;
|
||||
color: #4c9bfd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
padding: 0.5rem 1.2rem;
|
||||
border-radius: 0.7rem 0.7rem 0 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
text-shadow: 0 1px 4px #000a;
|
||||
flex-wrap: nowrap;
|
||||
/* 保证内容不换行且居中 */
|
||||
}
|
||||
|
||||
.centerStyle .title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* 总览标题 */
|
||||
.brand {
|
||||
background-image: url(../images/newBrand.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: 0rem;
|
||||
}
|
||||
|
||||
/* 网络拓扑 */
|
||||
.topology {
|
||||
/* min-height: 27.8rem; */
|
||||
height: 46.4%;
|
||||
flex: 1;
|
||||
}
|
||||
.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: 11.25%;
|
||||
}
|
||||
|
||||
.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%;
|
||||
flex: 1;
|
||||
}
|
||||
.userActivity .inner .chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* 流量统计 */
|
||||
.upfFlowTotal1 {
|
||||
/* min-height: 7.5rem; */
|
||||
height: 14.4%;
|
||||
}
|
||||
.upfFlowTotal1 .inner h3 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.upfFlowTotal1 .inner h3 .filter {
|
||||
display: flex;
|
||||
}
|
||||
.upfFlowTotal1 .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;
|
||||
}
|
||||
.upfFlowTotal1 .inner h3 .filter span:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
.upfFlowTotal1 .inner h3 .filter span:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
.upfFlowTotal1 .inner h3 .filter span.active {
|
||||
color: #fff;
|
||||
font-size: 0.833rem;
|
||||
}
|
||||
.upfFlowTotal1 .inner .chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
.upfFlowTotal1 .inner .chart .data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
height: 60%;
|
||||
}
|
||||
.upfFlowTotal1 .inner .chart .data .item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
.upfFlowTotal1 .inner .chart .data .item h4 {
|
||||
font-size: 1.467rem;
|
||||
color: #fff;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.upfFlowTotal1 .inner .chart .data .item span {
|
||||
color: #4c9bfd;
|
||||
font-size: 0.867rem;
|
||||
}
|
||||
|
||||
|
||||
/* 流量统计 */
|
||||
.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: 24.4%;
|
||||
}
|
||||
.resources .inner .chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
/* 告警统计 */
|
||||
.alarmType {
|
||||
/* min-height: 25rem; */
|
||||
height: 35%;
|
||||
}
|
||||
.alarmType .inner .chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 跳转鼠标悬浮 */
|
||||
.toRouter:hover {
|
||||
cursor: pointer;
|
||||
color: #fff !important;
|
||||
}
|
||||
.toRouter:hover > *,
|
||||
.toRouter:hover > * > * {
|
||||
color: #fff !important;
|
||||
}
|
||||
197
src/views/dashboard/overview2/hooks/useTopology.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
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<Record<string, any>>({
|
||||
/**当前图组名 */
|
||||
group: '5GC System Architecture',
|
||||
/**图数据 */
|
||||
data: {
|
||||
combos: [],
|
||||
edges: [],
|
||||
nodes: [],
|
||||
},
|
||||
});
|
||||
|
||||
/**图实例对象 */
|
||||
export const graphG6 = ref<any>(null);
|
||||
|
||||
/**图点击选择 */
|
||||
export const graphNodeClickID = ref<string>('UPF_001');
|
||||
|
||||
/**图节点网元信息状态 */
|
||||
export const graphNodeState = computed(() =>{
|
||||
return graphState.data.nodes.map((item: any) => ({
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
neInfo: item.neInfo,
|
||||
neState: item.neState,
|
||||
neInfoList:item.neInfoList,
|
||||
neStateMap: item.neStateMap,
|
||||
}))
|
||||
}
|
||||
|
||||
);
|
||||
|
||||
/**图节点网元状态数量 */
|
||||
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<Map<string, boolean>>(new Map());
|
||||
|
||||
/**neStateParse 网元状态 数据解析 */
|
||||
export function neStateParse(neType: string, data: Record<string, any>,neId: string) {
|
||||
// console.log('neStateParse',neType, data, neId);
|
||||
|
||||
const { combos, edges, nodes } = graphState.data;
|
||||
|
||||
const node = nodes.find((item: Record<string, any>) => item.id === neType);
|
||||
//console.log('neStateParse',node);
|
||||
|
||||
if (!node) return;
|
||||
|
||||
// 初始化状态映射
|
||||
if (!node.neStateMap) node.neStateMap = {};
|
||||
|
||||
// 更新网元状态
|
||||
const newNeState :any = {
|
||||
...data, // 先展开data对象
|
||||
refreshTime: parseDateToStr(data.refreshTime, 'HH:mm:ss'),
|
||||
online: !!data.cpu,
|
||||
neId: neId
|
||||
};
|
||||
// 如果是001,更新节点状态。neInfo为主要的网元信息
|
||||
if (node.neInfo && node.neInfo.neId === neId) {
|
||||
Object.assign(node.neState, newNeState);
|
||||
}
|
||||
|
||||
//console.log(node.neState)
|
||||
// 无论是否为主要网元,都更新状态映射
|
||||
node.neStateMap[neId] = {...newNeState};
|
||||
// 通过 ID 查询节点实例
|
||||
const item = graphG6.value.findById(node.id);
|
||||
if (item) {
|
||||
// 检查当前节点下所有网元的状态
|
||||
const allStates = Object.values(node.neStateMap);
|
||||
// 判断状态颜色
|
||||
let stateColor = '#52c41a'; // 默认绿色(所有网元都正常)
|
||||
if (allStates.some((state: any) => !state.online)) {
|
||||
// 如果有任何一个网元不正常
|
||||
stateColor = allStates.every((state: any) => !state.online) ? '#f5222d' : '#faad14'; // 红色(全部不正常)或黄色(部分不正常)
|
||||
}
|
||||
|
||||
// 图片类型不能填充
|
||||
if (node.type && node.type.startsWith('image')) {
|
||||
// 更新节点
|
||||
if (node.label !== newNeState.neType) {
|
||||
graphG6.value.updateItem(item, {
|
||||
label: newNeState.neType,
|
||||
});
|
||||
}
|
||||
// 设置状态
|
||||
graphG6.value.setItemState(item, 'top-right-dot', stateColor);
|
||||
} else {
|
||||
// 更新节点
|
||||
graphG6.value.updateItem(item, {
|
||||
label: newNeState.neType,
|
||||
style: {
|
||||
fill: stateColor, // 填充色
|
||||
stroke: stateColor, // 填充色
|
||||
},
|
||||
});
|
||||
// 设置状态
|
||||
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_001';
|
||||
neStateRequestMap.value = new Map();
|
||||
}
|
||||
110
src/views/dashboard/overview2/hooks/useUPFTotalFlow.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
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<FDType>({
|
||||
lineXTime: [],
|
||||
lineYUp: [],
|
||||
lineYDown: [],
|
||||
cap: 0,
|
||||
});
|
||||
|
||||
/**UPF-流量数据 数据解析 */
|
||||
export function upfFlowParse(data: Record<string, string>) {
|
||||
upfFlowData.value.lineXTime.push(parseDateToStr(+data['timeGroup'], 'HH:mm:ss'));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
type TFType = {
|
||||
/**上行 N3 */
|
||||
up: number;
|
||||
upFrom: string;
|
||||
/**下行 N6 */
|
||||
down: number;
|
||||
downFrom: string;
|
||||
/**请求标记 */
|
||||
requestFlag: boolean;
|
||||
};
|
||||
|
||||
/**UPF-总流量数 */
|
||||
export const upfTotalFlow = ref<Record<string, TFType>>({
|
||||
'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<string, number>) {
|
||||
let { up, down } = data;
|
||||
upfTotalFlow.value[day] = {
|
||||
up: up,
|
||||
upFrom: parseSizeFromBits(up),
|
||||
down: down,
|
||||
downFrom: parseSizeFromBits(down),
|
||||
requestFlag: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**UPF-总流量数 选中 */
|
||||
export const upfTFActive = ref<string>('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';
|
||||
}
|
||||
144
src/views/dashboard/overview2/hooks/useUserActivity.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { ref } from 'vue';
|
||||
|
||||
/**ueEventAMFParse UE会话事件AMF 数据解析 */
|
||||
function ueEventAMFParse(
|
||||
item: Record<string, any>
|
||||
): false | Record<string, any> {
|
||||
let evData: Record<string, any> = 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<string, any>
|
||||
): false | Record<string, any> {
|
||||
let evData: Record<string, any> = 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<string, any>
|
||||
): false | Record<string, any> {
|
||||
let evData: Record<string, any> = 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<string, any> = 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 > 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<string, any> = 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<Record<string, any>[]>([]);
|
||||
/**CDR+UE事件总量 */
|
||||
export const eventTotal = ref<number>(0);
|
||||
/**CDR/UE事件推送id */
|
||||
export const eventId = ref<string>('');
|
||||
|
||||
/**属性复位 */
|
||||
export function userActivityReset() {
|
||||
eventData.value = [];
|
||||
eventTotal.value = 0;
|
||||
eventId.value = '';
|
||||
}
|
||||
224
src/views/dashboard/overview2/hooks/useWS.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { RESULT_CODE_ERROR } from '@/constants/result-constants';
|
||||
import { OptionsType, WS } from '@/plugins/ws-websocket';
|
||||
import { onBeforeUnmount, ref } from 'vue';
|
||||
import {
|
||||
eventData,
|
||||
eventListParse,
|
||||
eventItemParseAndPush,
|
||||
userActivityReset,
|
||||
} from './useUserActivity';
|
||||
import {
|
||||
upfTotalFlow,
|
||||
upfTFParse,
|
||||
upfFlowParse,
|
||||
upfTotalFlowReset,
|
||||
} from './useUPFTotalFlow';
|
||||
import { topologyReset, neStateParse, neStateRequestMap } from './useTopology';
|
||||
import PQueue from 'p-queue';
|
||||
|
||||
/**UPF-的Id */
|
||||
export const upfWhoId = ref<any>('');
|
||||
|
||||
/**UPF-的RmUid */
|
||||
export const upfWhoRmUid = ref<any>('');
|
||||
|
||||
/**websocket连接 */
|
||||
export default function useWS() {
|
||||
const ws = new WS();
|
||||
const queue = new PQueue({ concurrency: 1, autoStart: true });
|
||||
|
||||
/**发消息 */
|
||||
function wsSend(data: Record<string, any>) {
|
||||
ws.send(data);
|
||||
}
|
||||
|
||||
/**接收数据后回调 */
|
||||
function wsMessage(res: Record<string, any>) {
|
||||
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];
|
||||
const neId = requestId.split('_')[2];
|
||||
neStateParse(neType, data,neId);
|
||||
return;
|
||||
}
|
||||
|
||||
// 普通信息
|
||||
switch (requestId) {
|
||||
// AMF_UE会话事件
|
||||
case 'amf_1010_001':
|
||||
if (Array.isArray(data.rows)) {
|
||||
eventListParse('amf_ue', data);
|
||||
eventData.value.sort((a, b) => b.eTime - a.eTime);
|
||||
}
|
||||
break;
|
||||
// MME_UE会话事件
|
||||
case 'mme_1011_001':
|
||||
if (Array.isArray(data.rows)) {
|
||||
eventListParse('mme_ue', data);
|
||||
eventData.value.sort((a, b) => b.eTime - a.eTime);
|
||||
}
|
||||
break;
|
||||
// IMS_CDR会话事件
|
||||
case 'ims_1005_001':
|
||||
if (Array.isArray(data.rows)) {
|
||||
eventListParse('ims_cdr', data);
|
||||
eventData.value.sort((a, b) => b.eTime - a.eTime);
|
||||
}
|
||||
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 '10_UPF_' + upfWhoId.value:
|
||||
if (data.data) {
|
||||
upfFlowParse(data.data);
|
||||
}
|
||||
break;
|
||||
// AMF_UE会话事件
|
||||
case '1010_001':
|
||||
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_001',
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**重新发送至UPF 10_UPF_neId */
|
||||
function reSendUPF(neId: string) {
|
||||
upfWhoId.value = neId;
|
||||
//初始时时无需还原全部属性以及关闭
|
||||
if (ws.state() === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
// userActivityReset();
|
||||
upfTotalFlowReset();
|
||||
neStateRequestMap.value = new Map();
|
||||
//topologyReset();
|
||||
}
|
||||
const options: OptionsType = {
|
||||
url: '/ws',
|
||||
params: {
|
||||
/**订阅通道组
|
||||
*
|
||||
* 指标UPF (GroupID:10_neType_neId)
|
||||
* AMF_UE会话事件(GroupID:1010_neId)
|
||||
* MME_UE会话事件(GroupID:1011_neId)
|
||||
* IMS_CDR会话事件(GroupID:1005_neId)
|
||||
*/
|
||||
subGroupID: '10_UPF_' + neId + ',1010_001,1011_001,1005_001',
|
||||
},
|
||||
onmessage: wsMessage,
|
||||
onerror: (ev: any) => {
|
||||
console.error(ev);
|
||||
},
|
||||
};
|
||||
ws.connect(options);
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ws.close();
|
||||
userActivityReset();
|
||||
upfTotalFlowReset();
|
||||
topologyReset();
|
||||
upfWhoRmUid.value = '';
|
||||
});
|
||||
|
||||
return {
|
||||
wsSend,
|
||||
userActivitySend,
|
||||
upfTFSend,
|
||||
reSendUPF,
|
||||
};
|
||||
}
|
||||
BIN
src/views/dashboard/overview2/images/bj.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
src/views/dashboard/overview2/images/border.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/views/dashboard/overview2/images/brand.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/views/dashboard/overview2/images/line.png
Normal file
|
After Width: | Height: | Size: 237 B |
BIN
src/views/dashboard/overview2/images/newBrand.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/views/dashboard/overview2/images/rect.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/views/dashboard/overview2/images/title.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
783
src/views/dashboard/overview2/index.vue
Normal file
@@ -0,0 +1,783 @@
|
||||
<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 { listIMSSessionNum } from '@/api/neData/ims';
|
||||
import { listUDMSub } from '@/api/neData/udm_sub';
|
||||
import { listAMFNblist } from '@/api/neData/amf';
|
||||
import { listMMENblist } from '@/api/neData/mme';
|
||||
import { listSMFSubNum } from '@/api/neData/smf';
|
||||
import { graphNodeClickID, graphState, notNeNodes } 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 neInfoStore = 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.total),
|
||||
// },
|
||||
// ],
|
||||
[
|
||||
'SMF',
|
||||
{
|
||||
request: (neId: string) => listSMFSubNum(neId),
|
||||
process: (res: any) =>
|
||||
res.code === RESULT_CODE_SUCCESS && (skimState.smfUeNum += res.data),
|
||||
},
|
||||
],
|
||||
[
|
||||
'IMS',
|
||||
{
|
||||
request: (neId: string) => listIMSSessionNum(neId),
|
||||
process: (res: any) =>
|
||||
res.code === RESULT_CODE_SUCCESS && (skimState.imsUeNum += res.data),
|
||||
},
|
||||
],
|
||||
[
|
||||
'AMF',
|
||||
{
|
||||
request: (neId: string) => listAMFNblist({ neId }),
|
||||
process: async (res: any, neId: any) => {
|
||||
if (res.code === RESULT_CODE_SUCCESS) {
|
||||
skimState.gnbNum += res.total;
|
||||
skimState.gnbUeNum += res.rows.reduce(
|
||||
(sum: number, item: any) => sum + item.ueNum,
|
||||
0
|
||||
);
|
||||
const amfNbRes = await listAMFNbStatelist({ neId });
|
||||
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: async (res: any, neId: 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 mmeNbRes = await listMMENbStatelist({ neId });
|
||||
if (
|
||||
mmeNbRes.code === RESULT_CODE_SUCCESS &&
|
||||
Array.isArray(mmeNbRes.data)
|
||||
) {
|
||||
// skimState.eNbSumNum += mmeNbRes.data.length;
|
||||
tempEnbSumNum += mmeNbRes.data.length;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
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,
|
||||
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,
|
||||
});
|
||||
const processPromises = results.map((result, index) => {
|
||||
const req = requests[index];
|
||||
if (result.status === 'fulfilled') {
|
||||
return req.process(result.value, req.neId);
|
||||
} else {
|
||||
return req.process(0, req.neId);
|
||||
}
|
||||
});
|
||||
|
||||
// 等待所有 process 执行完再赋值gNbSumNum等
|
||||
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.total;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**初始数据函数 */
|
||||
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 || !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();
|
||||
}
|
||||
|
||||
let udmNeId = ref<string>('001');
|
||||
let udmOtions = ref<Record<string, any>[]>([]);
|
||||
let onlineOtions = ref<Record<string, any>[]>([]);
|
||||
|
||||
/**用户数量-选择UDM */
|
||||
function fnSelectUDM(e: any) {
|
||||
udmNeId.value = e.key;
|
||||
listUDMSub({ neId: udmNeId.value, pageNum: 1, pageSize: 1 }).then(res => {
|
||||
if (res.code === RESULT_CODE_SUCCESS) {
|
||||
skimState.udmSubNum = res.total;
|
||||
}
|
||||
});
|
||||
}
|
||||
/**资源控制-选择NE */
|
||||
function fnSelectNeRe(e: any) {
|
||||
graphNodeClickID.value = e.key;
|
||||
}
|
||||
//
|
||||
// 定义一个方法返回 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) {
|
||||
// UPF
|
||||
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]);
|
||||
}
|
||||
//online Ne
|
||||
let onlineArr: Record<string, any>[] = [];
|
||||
|
||||
// UDM
|
||||
let arr1: Record<string, any>[] = [];
|
||||
res.data.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,
|
||||
});
|
||||
}
|
||||
if (v.neType === 'UDM') {
|
||||
arr1.push({ value: v.neId, label: v.neName, rmUid: v.rmUid });
|
||||
}
|
||||
});
|
||||
udmOtions.value = arr1;
|
||||
onlineOtions.value = onlineArr;
|
||||
if (arr1.length > 0) {
|
||||
fnSelectUDM({ key: arr1[0].value });
|
||||
}
|
||||
|
||||
if (onlineArr.length > 0) {
|
||||
fnSelectNeRe({ key: onlineArr[0].value });
|
||||
}
|
||||
|
||||
// 过滤不可用的网元
|
||||
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(() => {
|
||||
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" />
|
||||
{{ t('views.dashboard.overview.skim.userTitle') }}
|
||||
</span>
|
||||
</h3>
|
||||
<div class="data">
|
||||
<div
|
||||
class="item toRouter"
|
||||
:title="t('views.dashboard.overview.toRouter')"
|
||||
>
|
||||
<div @click="fnToRouter('Sub_2010')">
|
||||
<UserOutlined
|
||||
style="color: #4096ff; margin-right: 8px; font-size: 1.1rem"
|
||||
/>
|
||||
{{ skimState.udmSubNum }}
|
||||
</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('Ims_2080')"
|
||||
:title="t('views.dashboard.overview.toRouter')"
|
||||
style="margin: 0 12px"
|
||||
v-perms:has="['dashboard:overview:imsUeNum']"
|
||||
>
|
||||
<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')"
|
||||
v-perms:has="['dashboard:overview:smfUeNum']"
|
||||
>
|
||||
<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="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" />
|
||||
{{ 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" />
|
||||
{{ 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="toRouter centerStyle"
|
||||
:title="t('views.dashboard.overview.toRouter')"
|
||||
>
|
||||
<span class="title">
|
||||
<div @click="fnToRouter('GoldTarget_2104')">
|
||||
<AreaChartOutlined style="color: #68d8fe" />
|
||||
{{ t('views.dashboard.overview.upfFlow.title') }}
|
||||
</div>
|
||||
|
||||
<a-select
|
||||
v-model:value="upfWhoId"
|
||||
:options="neOtions"
|
||||
:get-Popup-Container="getPopupContainer"
|
||||
class="toDeep"
|
||||
style="color: #fff"
|
||||
@change="fnSelectNe"
|
||||
/>
|
||||
</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" />
|
||||
{{ 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']">
|
||||
<div class="inner">
|
||||
<h3 class="leftright">
|
||||
<span class="title">
|
||||
<GlobalOutlined style="color: #68d8fe" />
|
||||
{{ t('views.dashboard.overview.skim.nodeBInfo') }}
|
||||
</span>
|
||||
</h3>
|
||||
<div class="data" style="margin-top: 20px">
|
||||
<div
|
||||
class="item toRouter"
|
||||
@click="fnToRouter('BaseStation_2096', { 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_2096', { 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']">
|
||||
<div class="inner">
|
||||
<h3></h3>
|
||||
<div class="data" style="margin-top: 40px">
|
||||
<div
|
||||
class="item toRouter"
|
||||
@click="fnToRouter('BaseStation_2096', { 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_2096', { 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_2096', { 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"
|
||||
/>
|
||||
<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"
|
||||
/>
|
||||
{{ 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>
|
||||
BIN
src/views/faultManage/alarm-overview/images/bg.png
Normal file
|
After Width: | Height: | Size: 257 KiB |