Compare commits

..
Author SHA1 Message Date
TsMask 7916cc5a54 feat: 指标Title管理页面功能 2025-08-21 11:10:51 +08:00
TsMask 54468e2728 style: sgwc导出超时180s 2025-08-19 16:07:43 +08:00
TsMask 91ce2d7737 fix: 增加多个导出操作的超时时间至180秒 2025-08-18 19:07:41 +08:00
zhongzm 40705c6863 feat:中英文补充 2025-08-15 19:04:53 +08:00
TsMask c43e57a5d8 chore: 更新版本号 2.2508.2 2025-08-15 18:49:10 +08:00
TsMask 037fb18e06 fix: 给config.js加随机数避免缓存 2025-08-15 09:48:36 +08:00
TsMask 8eaddc8e79 fix: 第三方用户不可删除和修改密码 2025-08-14 10:48:40 +08:00
TsMask a7b539cd36 feat: 第三方登录认证功能和管理页 2025-08-12 09:54:37 +08:00
TsMask 0514c58b63 style:: 系统操作日志详情json格式化显示 2025-08-11 15:39:02 +08:00
TsMask 6a558788c7 fix: 网元信息更新禁止修改neType neId 2025-08-11 11:42:47 +08:00
zhongzm 253e36073d feat:用户数重置0 注释 2025-08-11 11:23:46 +08:00
zhongzm 3d7543074d feat:用户数网元切换显示 2025-08-11 11:15:51 +08:00
zhongzm 91175acd31 feat:用户数累加问题修复 2025-08-11 10:40:36 +08:00
zhongzm a7c2d25c5d feat:液体图中浮标显示错误修复 2025-08-08 19:53:36 +08:00
zhongzm cd495ab817 feat:基站数修复以及资源模块下拉框修复 2025-08-08 19:41:52 +08:00
TsMask 3229f21bcf fix: 告警事件时间显示不是时间格式字符串 2025-08-08 18:26:46 +08:00
zhongzm 33ffbbe4b2 feat:透明柱状图自适应,数值标签定位 2025-08-08 17:36:25 +08:00
zhongzm 2b4ce5f1c4 feat:告警数据参数调整 2025-08-08 17:15:20 +08:00
TsMask 8a31265743 chore: 更新版本号 2.2508.1 2025-08-08 12:12:08 +08:00
zhongzm 2492555a69 feat:主动呼叫改正在通话 2025-08-08 11:36:15 +08:00
zhongzm 6644ef2608 feat:网元切换清除缓存数据 2025-08-08 11:35:50 +08:00
zhongzm 1359aa8994 feat:调整顺序 2025-08-07 19:15:40 +08:00
TsMask bffd3fe791 Merge branch 'main-v2' of http://192.168.2.166:3180/OMC/ems_frontend_vue3 into main-v2 2025-08-07 15:49:49 +08:00
TsMask c020598678 fix: 看板切换upf的流量统计显示 2025-08-07 15:49:47 +08:00
zhongzm 7a7a0938ca feat:语音仪表盘中英文适配 2025-08-06 17:35:03 +08:00
TsMask 48a4497c14 fix: 看板2用户数量显示不出 2025-08-05 19:51:06 +08:00
TsMask 1342a37d50 style: 修改广播帮助的数量提示 2025-08-05 18:51:33 +08:00
TsMask 67dc3a0389 fix: MML日志列表显示 2025-08-05 17:28:03 +08:00
TsMask 7901b34c04 style: 系统用户账号简单4位长度 2025-08-05 17:27:18 +08:00
TsMask 6aeb645d88 fix: CBC创建时间格式显示错误 2025-08-05 17:26:20 +08:00
TsMask 501fc4fd07 style: 网元概览隐藏容量字段显示 2025-08-05 15:25:34 +08:00
TsMask b16a91ca74 fix: 关闭网元reload操作,只有amf smf upf udm四个网元支持,改用mml发送 2025-08-05 11:23:55 +08:00
TsMask 37691e5349 fix: MML命令为空判断不发送 2025-08-04 18:27:24 +08:00
TsMask e4136a27ee fix: UDM鉴权更新ki长度提示错误无法发送请求 2025-08-04 16:41:08 +08:00
TsMask 27bcf0ce6b chore: 更新版本号 2.2507.4 2025-08-01 18:23:02 +08:00
TsMask 6ca7181bce fix: cbc表格列宽设置 2025-08-01 17:51:22 +08:00
zhongzm a0ae414ec6 feat:数据采集说明 2025-08-01 17:33:06 +08:00
zhongzm ef3368ded9 feat:修复界面溢出 2025-08-01 16:58:00 +08:00
zhongzm abdd58fc81 feat:底部边距 2025-08-01 16:52:58 +08:00
zhongzm 6572fd13b1 feat:告警模块优化 2025-08-01 16:44:20 +08:00
lai e190379468 Merge branch 'main-v2' of http://192.168.2.166:3180/OMC/ems_frontend_vue3 into main-v2 2025-08-01 15:54:34 +08:00
lai 6b2682b3d3 新增cbc界面 2025-08-01 15:49:01 +08:00
zhongzm bbc27c757b feat:数据处理修复,显示优化 2025-08-01 15:04:19 +08:00
zhongzm cf88c86b75 Merge remote-tracking branch 'origin/main-v2' into main-v2 2025-07-30 18:34:50 +08:00
zhongzm aa16a7356f feat:参数值推送显示修复 2025-07-30 18:34:39 +08:00
TsMask e982f09874 chore: 更新版本号 2.2507.3 2025-07-25 20:31:15 +08:00
TsMask 9e69e76670 style: 将UDM鉴权导出按钮隐藏 2025-07-24 19:21:00 +08:00
TsMask ad373961e4 fix:: 自定义指标数值格式处理,导出表格修复 2025-07-23 10:56:59 +08:00
TsMask 26da73654f fix: 仪表盘2用户事件显示不正常 2025-07-22 18:51:41 +08:00
TsMask 09b7d806ea fix:: 自定义指标数值格式化保留3位小数 2025-07-22 18:50:57 +08:00
TsMask b5400b0dd8 feat: 参数配置AMF导入Index字段存在更新,不存在默认新增 2025-07-21 17:13:10 +08:00
TsMask b5b9e763aa feat: 参数配置刷新无法正常打开,导入白名单字段格式转换 2025-07-21 15:40:30 +08:00
TsMask fb20e40e67 chore: 更新版本号 2.2507.2 2025-07-18 21:28:49 +08:00
TsMask a19427e953 fix: 指标页面数据获取异常接口调整 2025-07-18 21:24:58 +08:00
TsMask a76c595c4c del: 删除无用文件 2025-07-18 21:23:24 +08:00
zhongzm ee55be5fcc feat:参数和接口修改问题 2025-07-18 18:56:02 +08:00
zhongzm 96c1f54b71 feat:综合仪表盘修改迁移以及版本兼容修复 2025-07-18 17:27:12 +08:00
zhongzm 8b6805fdb0 feat:综合仪表盘修改初版 2025-07-18 15:19:05 +08:00
TsMask f5cdad08c5 fix: 注释掉echarts-liquidfill 2025-07-16 15:03:43 +08:00
TsMask 10a5ed15bf feat: mt版本移动过来的的告警/仪表/性能大屏页面 2025-07-16 14:51:41 +08:00
TsMask ab7a466879 fix: 基站状态记录和smscCDR导出改为get行为 2025-07-15 19:54:02 +08:00
TsMask 82b1340906 fix: 基站状态拓扑图显示失败 2025-07-15 19:53:08 +08:00
TsMask 46ace81d9d fix: 调整UE数据返回参数 2025-07-15 19:52:38 +08:00
TsMask 73993eb4ce fix: 参数配置列表项不记录勾选状态 2025-07-15 19:51:54 +08:00
TsMask dd9bcaa7ce fix: 调整告警类型参数值 2025-07-15 19:51:20 +08:00
TsMask 27b2cfb449 feat: 调整MML命令请求接口 2025-07-15 19:50:09 +08:00
TsMask 3eae546aa4 fix: CDR返回信息调整 2025-07-15 19:49:18 +08:00
TsMask 818f91ad89 feat: 调整nssf/n3iwf接口调用 2025-07-15 19:47:46 +08:00
TsMask 594e439b43 fix: 调整安装UE_POOL改到基础项 2025-07-10 14:30:07 +08:00
TsMask a0ae95345c fix: 调整安装UE_POOL改到基础项 2025-07-10 11:23:27 +08:00
TsMask 2e23709fe4 feat: HLR跟踪页面标识为UDM 2025-07-07 10:43:50 +08:00
TsMask c80e7f17de chore: 更新版本号 2.2507.1 2025-07-05 10:28:27 +08:00
TsMask 799eacc9fa chore: 更新版本号 2.2507.1 2025-07-05 10:17:34 +08:00
TsMask 8e769a99e9 fix: 修复listCacheKey函数中的URL路径错误 2025-07-02 18:49:16 +08:00
TsMask 93ea37bbc1 fix: SMF-DATA图表日期显示不完整 2025-07-02 18:10:09 +08:00
TsMask e3f70640b8 fix: 跟踪任务编号的传入获取信息 2025-07-02 15:27:45 +08:00
TsMask 2181d51999 fix: 参数配置可见visible属性判断 2025-06-30 16:17:07 +08:00
TsMask a2a7ec3857 chore: 更新版本号 2.2506.4 2025-06-27 19:33:51 +08:00
TsMask 1566392c19 feat: AMF/MME对应的IMEI白名单模板去除index 2025-06-24 17:13:57 +08:00
TsMask 5704ff09c3 fix: UDM-IMS数据批量新增/批量删除命令调整 2025-06-24 17:13:18 +08:00
TsMask 673b8f9ad2 chore: 更新版本号 2.250620 2025-06-20 19:30:30 +08:00
TsMask b6468eecab feat: UDM签约cnType新增可选1仅5G/2仅4G 2025-06-20 14:54:53 +08:00
TsMask b8c6761fbf feat: 给AMF/MME对应的IMEI白名单添加批量导入和批量删除功能 2025-06-20 14:39:53 +08:00
TsMask d6aa4086e2 feat: UDM voup/ims导入失败文件下载 2025-06-16 18:47:13 +08:00
TsMask f513b47fb8 chore: 更新版本号 2.250613 2025-06-13 20:12:08 +08:00
TsMask eb1dccd642 fix: 数值控件属性maxlength拼写错误 2025-06-12 19:40:25 +08:00
TsMask 94ba520f5d fix: 空格内容无法编辑,提示信息显示方向 2025-06-12 10:34:05 +08:00
TsMask d3649ea784 style: 抓包文件目录列表显示文件大小 2025-06-11 20:35:35 +08:00
TsMask 2f7c22aac2 chore: 更新版本号 2.250607 2025-06-07 16:29:29 +08:00
TsMask 2f8ed1d821 fix: 请求响应码常量,身份信息更换逻辑优化 2025-06-07 16:28:18 +08:00
TsMask d84a6626c1 fix: 更新tcpdump路径从/tmp到/usr/local以符合新目录结构 2025-06-06 15:06:52 +08:00
121 changed files with 13613 additions and 1207 deletions

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ VITE_APP_NAME = "Core Network OMC"
VITE_APP_CODE = "OMC"
# 应用版本
VITE_APP_VERSION = "2.250530"
VITE_APP_VERSION = "local-dev"
# 接口基础URL地址-不带/后缀
VITE_API_BASE_URL = "/omc-api"
+1 -1
View File
@@ -11,7 +11,7 @@ VITE_APP_NAME = "Core Network OMC"
VITE_APP_CODE = "OMC"
# 应用版本
VITE_APP_VERSION = "2.250530"
VITE_APP_VERSION = "local-prod"
# 接口基础URL地址-不带/后缀
VITE_API_BASE_URL = "/omc-api"
+77
View File
@@ -1,5 +1,82 @@
# 版本发布日志
## 2.2508.2-20250815
- 优化 给config.js加随机数避免缓存
- 新增 第三方登录认证功能和管理页,第三方用户不可删除和修改密码
- 优化 系统操作日志详情json格式化显示
- 优化 网元信息更新禁止修改neType neId
- 修复 用户数累加问题修复,液体图中浮标显示错误修复
- 修复 基站数修复以及资源模块下拉框修复
- 修复 告警事件时间显示不是时间格式字符串
- 修复 透明柱状图自适应,数值标签定位
- 修复 告警数据参数调整
## 2.2508.1-20250808
- 修复 UDM鉴权更新ki长度提示错误无法发送请求
- 优化 MML命令为空判断不发送,MML日志列表显示
- 优化 关闭网元reload操作,只有amf smf upf udm四个网元支持,改用mml发送
- 优化 网元概览隐藏容量字段显示,CBC创建时间格式显示错误
- 优化 系统用户账号简单4位长度
- 修复 看板2用户数量显示不出,修改广播帮助的数量提示
- 优化 性能栏目相关页面主动呼叫改正在通话,网元切换清除缓存数据
## 2.2507.4-20250801
- 新增 cbc界面
- 修复 数据处理修复,显示优化
- 修复 告警模块优化,底部边距,数据采集说明
## 2.2507.3-20250725
- 优化 将UDM鉴权导出按钮隐藏
- 修复 自定义指标数值格式处理,导出表格修复
- 修复 仪表盘2用户事件显示不正常
- 修复 自定义指标数值格式化保留3位小数
- 优化 参数配置AMF导入Index字段存在更新,不存在默认新增
## 2.2507.2-20250718
- 优化 变更nssf/n3iwf接口调用
- 优化 调整告警类型参数值,参数配置列表项不记录勾选状态
- 优化 调整UE数据返回参数
- 修复 基站状态拓扑图显示失败
- 修复 基站状态记录和smscCDR导出改为get行为
- 新增 mt版本移动过来的的告警/仪表/性能大屏页面
- 移除 删除无用文件
- 修复 指标页面数据获取异常接口调整
## 2.2507.1-20250705
- 修复 缓存管理列表key查询URL路径错误
- 优化 SMF-Date显示图表日期显示不完整,改日期格式
- 修复 跟踪任务编号的传入获取信息不一致
- 修复 参数配置可见visible属性判断
## 2.2506.4-20250627
- 修复 UDM-IMS数据批量新增/批量删除命令调整
- 优化 AMF/MME对应的IMEI白名单模板去除index
## 2.2506.3-20250620
- 新增 UDM签约cnType新增可选1仅5G/2仅4G
- 新增 给AMF/MME对应的IMEI白名单添加批量导入和批量删除功能
- 新增 UDM voup/ims导入失败文件下载
## 2.2506.2-20250613
- 修复 数值控件属性maxlength拼写错误
- 修复 空格内容无法编辑,提示信息显示方向
- 优化 抓包文件目录列表显示文件大小
## 2.2506.1-20250607
- 优化 请求响应码常量,身份信息更换逻辑优化
- 修复 更新tcpdump路径从/tmp到/usr/local以符合新目录结构
## 2.2505.4-20250530
- 优化 网元参数配置修改tooltip位置为bottomLeft防止遮挡
+8 -4
View File
@@ -2,14 +2,18 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="google" content="notranslate">
<meta name="google" content="notranslate" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>loading...</title>
<link rel="icon" href="/favicon.ico" />
<link rel="preload" href="/loading.js" as="script">
<link rel="preload" href="/loading.js" as="script" />
<script async src="/loading.js"></script>
<link rel="preload" href="/config.js" as="script">
<script async src="/config.js"></script>
<script>
var script = document.createElement('script');
script.src = '/config.js?nocache=' + new Date().getTime();
script.async = true;
document.head.appendChild(script);
</script>
</head>
<body>
<div id="app"></div>
+1
View File
@@ -29,6 +29,7 @@
"crypto-js": "4.2.0",
"dayjs": "1.11.13",
"echarts": "5.6.0",
"echarts-liquidfill": "^3.1.0",
"file-saver": "2.0.5",
"grid-layout-plus": "1.0.6",
"intl-tel-input": "25.2.0",
+67
View File
@@ -1,4 +1,5 @@
import { request } from '@/plugins/http-fetch';
import { sessionGet } from '@/utils/cache-session-utils';
/**
* 登录方法
@@ -87,3 +88,69 @@ export function getCaptchaImage() {
whithToken: false,
});
}
/**
* 登录认证源
* @returns object
*/
export function getLoginSource() {
return request({
url: '/auth/login/source',
method: 'GET',
whithToken: false,
});
}
/**
* LDAP登录
* @returns object
*/
export function loginLDAP(data: Record<string, string>) {
return request({
url: '/auth/login/ldap',
method: 'POST',
data: data,
whithToken: false,
});
}
/**
* SMTP登录
* @returns object
*/
export function loginSMTP(data: Record<string, string>) {
return request({
url: '/auth/login/smtp',
method: 'POST',
data: data,
whithToken: false,
});
}
/**
* 登录认证源OAuth2跳转登录URL
* @returns object
*/
export function loginOAuth2URL(state: string): string {
// 兼容旧前端可改配置文件
const baseUrl = import.meta.env.PROD
? sessionGet('baseUrl') || import.meta.env.VITE_API_BASE_URL
: import.meta.env.VITE_API_BASE_URL;
return `${baseUrl}/auth/login/oauth2/authorize?state=${state}`;
}
/**
* 登录认证源OAuth2认证登录
* @returns object
*/
export function loginOAuth2Token(code: string, state: string) {
return request({
url: '/auth/login/oauth2/token',
method: 'POST',
data: {
code,
state,
},
whithToken: false,
});
}
+75
View File
@@ -0,0 +1,75 @@
import { request } from '@/plugins/http-fetch';
/**
* CBC列表
* @param query 查询参数
* @returns object
*/
export function listCBC(query: Record<string, any>) {
return request({
url: `/neData/cbc/message/list`,
method: 'GET',
params: query,
timeout: 30_000,
});
}
/**
* CBC签约用户新增
* @param data 签约对象
* @returns object
*/
export function addCBC(data: Record<string, any>) {
return request({
url: `/neData/cbc/message?neId=${data.neId}`,
method: 'POST',
data: data,
timeout: 180_000,
});
}
export function updateCBCStatus(data:any) {
return request({
url: `/neData/cbc/message/${data.id}/${data.status}?neId=${data.neId}`,
method: 'PUT',
timeout: 180_000,
});
}
export function updateCBC(data:any) {
return request({
url: `/neData/cbc/message/${data.id}?neId=${data.neId}`,
method: 'PUT',
data,
timeout: 180_000,
});
}
/**
* CBC删除
* @param data 签约对象
* @returns object
*/
export function delCBC(neId: string, id: string) {
return request({
url: `/neData/cbc/message/${id}?neId=${neId}`,
method: 'DELETE',
timeout: 180_000,
});
}
-35
View File
@@ -1,35 +0,0 @@
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
RESULT_MSG_ERROR,
} from '@/constants/result-constants';
import { language, request } from '@/plugins/http-fetch';
/**
* 更新网元配置重新载入
* @param neType 网元类型
* @param neId 网元ID
* @returns
*/
export async function updateNeConfigReload(neType: string, neId: string) {
// 发起请求
const result = await request({
url: `/api/rest/operationManagement/v1/elementType/${neType}/objectType/mml?ne_id=${neId}`,
method: 'POST',
data: { mml: ['reload'] },
timeout: 180_000,
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
const v = result.data.data[0];
const str = v.toLowerCase();
if (str.indexOf('ok') !== -1) {
delete result.data;
} else if (str.indexOf('success') !== -1) {
delete result.data;
} else {
return { code: RESULT_CODE_ERROR, msg: RESULT_MSG_ERROR[language] };
}
}
return result;
}
+10 -51
View File
@@ -12,7 +12,7 @@ export async function getActiveAlarmTotal() {
url: `/neData/alarm/list`,
method: 'GET',
params: {
alarmStatus: '1',
alarmStatus: 'Active',
sortField: 'event_time',
sortOrder: 'desc',
pageNum: 1,
@@ -127,7 +127,7 @@ export function exportAlarm(params: Record<string, any>) {
method: 'GET',
params: params,
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
@@ -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;
}
-55
View File
@@ -1,55 +0,0 @@
import { CACHE_SESSION_CRYPTO_API } from '@/constants/cache-keys-constants';
import { sessionGet } from '@/utils/cache-session-utils';
import { request } from '@/plugins/http-fetch';
/**
* 获取下拉框数据
* @returns object
*/
export function getBakFile() {
return request({
url: '/lm/table/list',
method: 'GET',
});
}
/**
* 获取对应类型的文件列表
* @param query 查询参数
* @returns object
*/
export function getBakFileList(query: Record<string, any>) {
return request({
url: '/lm/file/list',
method: 'GET',
params: query,
});
}
/**
* 下载远端文件
* @param query 查询参数
* @returns object
*/
export function downFile(query: Record<string, any>) {
return request({
url: `/lm/file/${query.fileName}`,
method: 'GET',
params: query,
responseType: 'blob',
timeout: 180_000,
});
}
/**
* 删除远端获取文件
* @param query 查询参数
* @returns object
*/
export function delFile(query: Record<string, any>) {
return request({
url: `/lm/file/${query.fileName}`,
method: 'DELETE',
params: query,
});
}
-68
View File
@@ -1,68 +0,0 @@
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { request } from '@/plugins/http-fetch';
import { parseObjLineToHump } from '@/utils/parse-utils';
/**
* 查询日志列表
* @param query 查询参数
* @returns object
*/
export async function listMML(query: Record<string, any>) {
let totalSQL = 'select count(*) as total from mml_log where 1=1 ';
let rowsSQL = 'select * from mml_log where 1=1 ';
// 查询
let querySQL = '';
if (query.accountName) {
querySQL += ` and user like '%${query.accountName}%' `;
}
if (query.beginTime) {
querySQL += ` and log_time >= '${query.beginTime}' `;
}
if (query.endTime) {
querySQL += ` and log_time <= '${query.endTime}' `;
}
// 排序
let sortSql = ' order by log_time ';
if (query.sortOrder === 'asc') {
sortSql += ' asc ';
} else {
sortSql += ' desc ';
}
// 分页
const pageNum = (query.pageNum - 1) * query.pageSize;
const limtSql = ` limit ${pageNum},${query.pageSize} `;
// 发起请求
const result = await request({
url: `/api/rest/databaseManagement/v1/select/omc_db/mml_log`,
method: 'GET',
params: {
totalSQL: totalSQL + querySQL,
rowsSQL: rowsSQL + querySQL + sortSql + limtSql,
},
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS) {
const data = {
data: { total: 0, rows: [] as any },
code: result.code,
msg: result.msg,
};
result.data.data.forEach((item: any) => {
const itemData = item['mml_log'];
if (Array.isArray(itemData)) {
if (itemData.length === 1 && itemData[0]['total'] >= 0) {
data.data.total = itemData[0]['total'];
} else {
data.data.rows = itemData.map(v => parseObjLineToHump(v));
}
}
});
return data;
}
return result;
}
-71
View File
@@ -1,71 +0,0 @@
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { request } from '@/plugins/http-fetch';
import { parseObjLineToHump } from '@/utils/parse-utils';
/**
* 查询日志列表
* @param query 查询参数
* @returns object
*/
export async function listSecurityLog(query: Record<string, any>) {
let totalSQL = 'select count(*) as total from security_log where 1=1 ';
let rowsSQL = 'select * from security_log where 1=1 ';
// 查询
let querySQL = '';
if (query.accountName) {
querySQL += ` and account_name like '%${query.accountName}%' `;
}
if (query.opType) {
querySQL += ` and op_type = '${query.opType}' `;
}
if (query.beginTime) {
querySQL += ` and op_time >= '${query.beginTime}' `;
}
if (query.endTime) {
querySQL += ` and op_time <= '${query.endTime}' `;
}
// 排序
let sortSql = ' order by op_time ';
if (query.sortOrder === 'asc') {
sortSql += ' asc ';
} else {
sortSql += ' desc ';
}
// 分页
const pageNum = (query.pageNum - 1) * query.pageSize;
const limtSql = ` limit ${pageNum},${query.pageSize} `;
// 发起请求
const result = await request({
url: `/api/rest/databaseManagement/v1/select/omc_db/security_log`,
method: 'GET',
params: {
totalSQL: totalSQL + querySQL,
rowsSQL: rowsSQL + querySQL + sortSql + limtSql,
},
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS) {
const data = {
data: { total: 0, rows: [] as any },
code: result.code,
msg: result.msg,
};
result.data.data.forEach((item: any) => {
const itemData = item['security_log'];
if (Array.isArray(itemData)) {
if (itemData.length === 1 && itemData[0]['total'] >= 0) {
data.data.total = itemData[0]['total'];
} else {
data.data.rows = itemData.map(v => parseObjLineToHump(v));
}
}
});
return data;
}
return result;
}
+15 -23
View File
@@ -1,6 +1,4 @@
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { request } from '@/plugins/http-fetch';
import { parseObjLineToHump } from '@/utils/parse-utils';
/**
* 查询网元可用cmd命令
@@ -8,22 +6,17 @@ import { parseObjLineToHump } from '@/utils/parse-utils';
* @returns object
*/
export async function getMMLByNE(neType: string) {
// 发起请求
const result = await request({
url: `/api/rest/databaseManagement/v1/elementType/omc_db/objectType/mml_system`,
return request({
url: '/tool/mml/system/list',
method: 'GET',
params: {
SQL: `select * from mml_system where ne_type = '${neType}' and status = 'Active'`,
neType: neType,
status: 'Active',
pageNum: 1,
pageSize: 1000,
},
timeout: 60_000,
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
let data = result.data.data[0];
return Object.assign(result, {
data: parseObjLineToHump(data['mml_system']),
});
}
return result;
}
/**
@@ -40,16 +33,15 @@ export async function sendMMlByNE(
objectType: string,
cmdArr: string[]
) {
// 发起请求
const result = await request({
url: `/api/rest/operationManagement/v1/elementType/${neType}/objectType/${objectType}?ne_id=${neId}`,
return request({
url: '/tool/mml/command',
method: 'POST',
data: { mml: cmdArr },
data: {
neType: neType,
neId: neId,
type: objectType,
command: cmdArr,
},
timeout: 180_000,
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
result.data = result.data.data;
}
return result;
}
+15 -23
View File
@@ -1,28 +1,21 @@
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { request } from '@/plugins/http-fetch';
import { parseObjLineToHump } from '@/utils/parse-utils';
/**
* 查询UDM可用cmd命令
* @returns object
*/
export async function getMMLByUDM() {
// 发起请求
const result = await request({
url: `/api/rest/databaseManagement/v1/elementType/omc_db/objectType/mml_subscriber`,
return request({
url: '/tool/mml/subscriber/list',
method: 'GET',
params: {
SQL: `select * from mml_subscriber where ne_type = 'UDM' and status = 'Active'`,
neType: 'UDM',
status: 'Active',
pageNum: 1,
pageSize: 1000,
},
timeout: 60_000,
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
let data = result.data.data[0];
return Object.assign(result, {
data: parseObjLineToHump(data['mml_subscriber']),
});
}
return result;
}
/**
@@ -32,16 +25,15 @@ export async function getMMLByUDM() {
* @returns
*/
export async function sendMMlByUDM(neId: string, cmdArr: string[]) {
// 发起请求
const result = await request({
url: `/api/rest/operationManagement/v1/elementType/UDM/objectType/mml?ne_id=${neId}`,
return request({
url: '/tool/mml/command',
method: 'POST',
data: { mml: cmdArr },
data: {
neType: 'UDM',
neId: neId,
type: 'General',
command: cmdArr,
},
timeout: 180_000,
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
result.data = result.data.data;
}
return result;
}
+1 -1
View File
@@ -29,7 +29,7 @@ export function listCacheName() {
*/
export function listCacheKey(cacheName: string) {
return request({
url: `/monitor/cache//keys`,
url: `/monitor/cache/keys`,
method: 'GET',
params: { cacheName },
});
+5 -5
View File
@@ -29,16 +29,16 @@ export function delAMFDataUE(ueIds: string | number) {
/**
* AMF-UE会话列表导出
* @param data 查询列表条件
* @param query 查询列表条件
* @returns object
*/
export function exportAMFDataUE(data: Record<string, any>) {
export function exportAMFDataUE(query: Record<string, any>) {
return request({
url: '/neData/amf/ue/export',
method: 'POST',
data,
method: 'GET',
params: query,
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
+5 -5
View File
@@ -29,16 +29,16 @@ export function delIMSDataCDR(cdrIds: string | number) {
/**
* IMS-CDR会话列表导出
* @param data 查询列表条件
* @param query 查询列表条件
* @returns object
*/
export function exportIMSDataCDR(data: Record<string, any>) {
export function exportIMSDataCDR(query: Record<string, any>) {
return request({
url: '/neData/ims/cdr/export',
method: 'POST',
data,
method: 'GET',
params: query,
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
+5 -5
View File
@@ -29,16 +29,16 @@ export function delMMEDataUE(ueIds: string | number) {
/**
* MME-UE会话列表导出
* @param data 查询列表条件
* @param query 查询列表条件
* @returns object
*/
export function exportMMEDataUE(data: Record<string, any>) {
export function exportMMEDataUE(query: Record<string, any>) {
return request({
url: '/neData/mme/ue/export',
method: 'POST',
data,
method: 'GET',
params: query,
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
+5 -5
View File
@@ -16,15 +16,15 @@ export function listNBState(query: Record<string, any>) {
/**
* 历史记录列表导出
* @param data 查询列表条件
* @param query 查询列表条件
* @returns object
*/
export function exportNBState(data: Record<string, any>) {
export function exportNBState(query: Record<string, any>) {
return request({
url: '/neData/nb-state/export',
method: 'POST',
data,
method: 'GET',
params: query,
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
+1 -1
View File
@@ -64,7 +64,7 @@ export function exportPCFRule(data: Record<string, any>) {
method: 'GET',
params: data,
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
+5 -5
View File
@@ -29,15 +29,15 @@ export function delSGWCDataCDR(cdrIds: string | number) {
/**
* SGWC-CDR会话列表导出
* @param data 查询列表条件
* @param query 查询列表条件
* @returns object
*/
export function exportSGWCDataCDR(data: Record<string, any>) {
export function exportSGWCDataCDR(query: Record<string, any>) {
return request({
url: '/neData/sgwc/cdr/export',
method: 'POST',
data,
method: 'GET',
params: query,
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
+5 -5
View File
@@ -29,16 +29,16 @@ export function delSMFDataCDR(cdrIds: string | number) {
/**
* SMF-CDR会话列表导出
* @param data 查询列表条件
* @param query 查询列表条件
* @returns object
*/
export function exportSMFDataCDR(data: Record<string, any>) {
export function exportSMFDataCDR(query: Record<string, any>) {
return request({
url: '/neData/smf/cdr/export',
method: 'POST',
data,
method: 'GET',
params: query,
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
+5 -5
View File
@@ -29,15 +29,15 @@ export function delSMSCDataCDR(cdrIds: string | number) {
/**
* SMSC-CDR会话列表导出
* @param data 查询列表条件
* @param query 查询列表条件
* @returns object
*/
export function exportSMSCDataCDR(data: Record<string, any>) {
export function exportSMSCDataCDR(query: Record<string, any>) {
return request({
url: '/neData/smsc/cdr/export',
method: 'POST',
data,
method: 'GET',
params: query,
responseType: 'blob',
timeout: 60_000,
timeout: 180_000,
});
}
+14 -4
View File
@@ -71,13 +71,16 @@ export function batchAddUDMVolteIMS(data: Record<string, any>, num: number) {
/**
* UDMVolteIMS用户删除
* @param data 签约对象
* @param neId 网元ID
* @param imsi_msisdn IMSI/MSISDN
* @param tag 标签 0-voip 1-volte
* @returns object
*/
export function delUDMVolteIMS(neId: string, imsi: string) {
export function delUDMVolteIMS(neId: string, imsi_msisdn: string, tag: string) {
return request({
url: `/neData/udm/volte-ims/${neId}/${imsi}`,
url: `/neData/udm/volte-ims/${neId}/${imsi_msisdn}`,
method: 'DELETE',
params: { volte: tag },
timeout: 180_000,
});
}
@@ -87,12 +90,19 @@ export function delUDMVolteIMS(neId: string, imsi: string) {
* @param neId 网元ID
* @param imsi IMSI
* @param num 数量
* @param tag 标签 0-voip 1-volte
* @returns object
*/
export function batchDelUDMVolteIMS(neId: string, imsi: string, num: number) {
export function batchDelUDMVolteIMS(
neId: string,
imsi: string,
num: number,
tag: string
) {
return request({
url: `/neData/udm/volte-ims/${neId}/${imsi}/${num}`,
method: 'DELETE',
params: { volte: tag },
timeout: 180_000,
});
}
+5 -27
View File
@@ -3,36 +3,14 @@ import { request } from '@/plugins/http-fetch';
import { parseObjLineToHump } from '@/utils/parse-utils';
/**
* 查询列表
* @param query 查询参数
* N3IWF-在线订阅用户列表信息
* @param query 查询参数 {imsi}
* @returns object
*/
export async function listN3iwf(query: Record<string, any>) {
const result = await request({
url: '/api/rest/ueManagement/v1/elementType/n3iwf/objectType/ueInfo',
export function listN3IWFSubList(query: Record<string, any>) {
return request({
url: '/neData/n3iwf/sub/list',
method: 'GET',
params: query,
});
const data = {
data: { total: 0, rows: [] as any },
code: result.code,
msg: result.msg,
};
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
const rows = parseObjLineToHump(result.data.data);
data.data.total = rows.length;
data.data.rows = rows;
}
// 模拟数据
// data.rows = [
// {
// "activeTime": "2023-11-29 06:35:43",
// "imsi": "460302072701181",
// "nai": "0460302072701181@nai.epc.mnc030.mcc460.3gppnetwork.org",
// "regState": 1
// }
// ]
return data;
}
+15 -15
View File
@@ -3,24 +3,24 @@ import { request } from '@/plugins/http-fetch';
import { parseObjLineToHump } from '@/utils/parse-utils';
/**
* 查询列表
* NSSF-在线订阅用户列表信息
* @param query 查询参数 {imsi}
* @returns object
*/
export async function listNSSF() {
const result = await request({
url: '/api/rest/ueManagement/v1/elementType/nssf/objectType/subscriptions',
export function listNSSFSubList() {
return request({
url: '/neData/nssf/sub/list',
method: 'GET',
});
let data = {
data: { total: 0, rows: [] as any },
code: result.code,
msg: result.msg,
};
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
const rows = parseObjLineToHump(result.data.data);
data.data.total = rows.length;
data.data.rows = rows;
}
return data;
/**
* NSSF-可用AMF列表信息
* @returns object
*/
export function listNSSFAmfList() {
return request({
url: '/neData/nssf/amf/list',
method: 'GET',
});
}
-43
View File
@@ -1,43 +0,0 @@
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { request } from '@/plugins/http-fetch';
import { parseObjLineToHump } from '@/utils/parse-utils';
/**
* 查询列表
* @returns object
*/
export async function listNSSFAMF() {
const result = await request({
url: '/api/rest/ueManagement/v1/elementType/nssf/objectType/availableAMFs',
method: 'GET',
});
const data = {
data: { total: 0, rows: [] as any },
code: result.code,
msg: result.msg,
};
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
const rows = parseObjLineToHump(result.data.data);
data.data.total = rows.length;
data.data.rows = rows;
}
// let testData = {
// total:0,
// rows: [
// {
// nfId: '001',
// amfSetId: '001',
// },
// {
// nfId: '002',
// amfSetId: '[001,002]',
// },
// ],
// code: 1,
// msg:'',
// };
//return testData;
return data;
}
+1 -1
View File
@@ -8,7 +8,7 @@ import { request } from '@/plugins/http-fetch';
export async function listCustomData(query: Record<string, any>) {
// 发起请求
const result = await request({
url: `/pm/kpiC/report`,
url: `/neData/kpic/data`,
method: 'GET',
params: query,
timeout: 60_000,
+6 -20
View File
@@ -1,30 +1,16 @@
import { request } from '@/plugins/http-fetch';
/**
* 新 查询自定义指标
* 查询自定义指标
* @param query 查询参数
* @returns object
*/
export async function listCustom(query?: Record<string, any>) {
// 发起请求
const result = await request({
url: `/pm/kpiC/title/totalList`,
return await request({
url: `/neData/kpic/title/list`,
method: 'GET',
params: query,
});
return result;
}
/**
* 查询自定义指标详细
* @param id 网元ID
* @returns object
*/
export async function getCustom(id: string | number) {
return request({
url: `/pm/kpiC/title/${id}`,
method: 'GET',
});
}
/**
@@ -34,7 +20,7 @@ export async function getCustom(id: string | number) {
*/
export function addCustom(data: Record<string, any>) {
return request({
url: `/pm/kpiC/title`,
url: `/neData/kpic/title`,
method: 'POST',
data: data,
});
@@ -47,7 +33,7 @@ export function addCustom(data: Record<string, any>) {
*/
export function updateCustom(data: Record<string, any>) {
return request({
url: `/pm/kpiC/title/${data.id}`,
url: `/neData/kpic/title`,
method: 'PUT',
data: data,
});
@@ -59,7 +45,7 @@ export function updateCustom(data: Record<string, any>) {
*/
export async function delCustom(data: Record<string, any>) {
return request({
url: `/pm/kpiC/title/${data.id}`,
url: `/neData/kpic/title?id=${data.id}`,
method: 'DELETE',
});
}
+51
View File
@@ -0,0 +1,51 @@
import { request } from '@/plugins/http-fetch';
/**
* 查询统计指标
* @param query 查询参数
* @returns object
*/
export async function listKPITitle(query?: Record<string, any>) {
return await request({
url: `/neData/kpi/title/list`,
method: 'GET',
params: query,
});
}
/**
* 新增统计指标
* @param data 网元对象
* @returns object
*/
export function addKPITitle(data: Record<string, any>) {
return request({
url: `/neData/kpi/title`,
method: 'POST',
data: data,
});
}
/**
* 修改统计指标
* @param data 网元对象
* @returns object
*/
export function updateKPITitle(data: Record<string, any>) {
return request({
url: `/neData/kpi/title`,
method: 'PUT',
data: data,
});
}
/**
* 删除统计指标
* @returns object
*/
export async function delKPITitle(id: string) {
return request({
url: `/neData/kpi/title?id=${id}`,
method: 'DELETE',
});
}
+64
View File
@@ -0,0 +1,64 @@
import { request } from '@/plugins/http-fetch';
/**
* 查询登录源列表
* @param query 查询参数
* @returns object
*/
export function listLoginSource(query: Record<string, any>) {
return request({
url: '/system/login-source/list',
method: 'GET',
params: query,
});
}
/**
* 查询登录源详细
* @param id 登录源ID
* @returns object
*/
export function getLoginSource(id: string | number) {
return request({
url: `/system/login-source/${id}`,
method: 'GET',
});
}
/**
* 新增登录源
* @param data 登录源对象
* @returns object
*/
export function addLoginSource(data: Record<string, any>) {
return request({
url: '/system/login-source',
method: 'POST',
data: data,
});
}
/**
* 修改登录源
* @param data 登录源对象
* @returns object
*/
export function updateLoginSource(data: Record<string, any>) {
return request({
url: '/system/login-source',
method: 'PUT',
data: data,
});
}
/**
* 认证源删除
* @param id 登录源ID
* @returns object
*/
export function delLoginSource(id: string | number) {
return request({
url: `/system/login-source/${id}`,
method: 'DELETE',
});
}
+25
View File
@@ -0,0 +1,25 @@
import { request } from '@/plugins/http-fetch';
// 更新网元配置重新载入
export function updateNeConfigReload(neType: string, neId: string) {
return request({
url: '/tool/mml/command',
method: 'POST',
data: {
neType: neType,
neId: neId,
type: 'General',
command: ['reload'],
},
timeout: 180_000,
});
}
// MMML日志列表
export function mmlLogList(query: Record<string, any>) {
return request({
url: '/tool/mml/log/list',
method: 'GET',
params: query,
});
}
+12
View File
@@ -19,6 +19,18 @@ export const RESULT_MSG_SUCCESS: Record<string, string> = {
/**响应-code错误失败 */
export const RESULT_CODE_ERROR = 400001;
/**响应-code身份认证失败或者过期 */
export const RESULT_CODE_AUTH = 401001;
/**响应-code无效身份信息 */
export const RESULT_CODE_AUTH_INVALID = 401002;
/**响应-code令牌字符为空 */
export const RESULT_CODE_AUTH_NOTOKEN = 401003;
/**响应-code设备指纹信息不匹配 */
export const RESULT_CODE_AUTH_DEVICE = 401004;
/**响应-code错误异常 */
export const RESULT_CODE_EXCEPTION = 500001;
+139 -6
View File
@@ -261,12 +261,16 @@ export default {
login: {
tabPane1: 'Account password login',
tabPane2: 'Login with phone number',
registerBtn: 'Register an account',
registerBtn: 'Register Account',
loginBtn: 'Sign In',
loginSuccess: 'Login Successful',
loginMethod: 'Other login methods:',
loginMethodWX: 'WeChat Scan Login',
loginMethodQQ: 'QQ Scan Code Login',
otherMethod: 'Other Methods',
backBtn: 'Back',
backBtnLogin: 'Return Login',
authorizedNotfound: 'Authorized Not Found',
authorizedFailed: 'Authorized Failed',
authorizedSuccess: 'Authorized Successful',
redirectHome: 'Redirect to home page in {i} seconds',
},
register: {
registerBtn: 'Register',
@@ -357,6 +361,54 @@ export default {
description: "No data yet, try refreshing",
},
},
cbc:{
cbe:{
neType:'CBC Object',
title: ' CBC Event',
delTip: 'Confirm deletion of the event data item numbered [{num}]?',
eventName: 'Event Name',
startTime: 'Start Time',
endTime: 'End Time',
repetitionPeriod: 'Repetition Period',
numOfBcast: 'Number of Broadcasts',
msgPWSType: 'Message Type',
messageId: 'Message ID',
displayMode: 'Display Mode',
geoScope:' Geographic Scope',
emergencyUserAlert: 'Emergency User Alert',
activatePopup: 'Activate Popup',
warningType: 'Warning Type',
language:' Language',
warningMessageText:' Broadcast Content',
status: 'Status',
warningAreaType: 'Warning Area Type',
taiListTip:'TAI List cannot be empty',
taiSonTip:'TAI List each item MCC, MNC, TAC cannot be empty',
eutraListTip:'EUTRA CellId List cannot be empty',
eutraSonTip:'EUTRA CellId List each item MCC, MNC, CellId cannot be empty',
nrTip:'NR CellId List cannot be empty',
nrSonTip:'NR CellId List each item MCC, MNC, CellId cannot be empty',
areaTip:'Area ID List cannot be empty',
areaSonTip:'Area ID List each item AreaID cannot be empty',
messageIdProfile:'Message ID Profile',
serialNumProfile:'Serial Num Profile',
warningTypeProfile:'Warning Type Profile',
warningMessageProfile:'Warning Message Profile',
etws:'Earthquake and Tsunami Warning System',
cmas:'Commercial Mobile Alert System',
createdAt:'Create Time',
eventNameHelp:'If it is CMAS, the recommended prefix is cmas_xxx. If it is ETWS, the recommended prefix is etws_xxx',
repetitionPeriodHelp:'Unit is seconds',
numOfBcastHelp:'Number of Broadcasts',
cellListTip:'CellId List cannot be empty',
cellListSonTip:'MCC、MNC、 Cannot be empty, and at least one EUTRA CellId/NR CellId must be filled in',
letupSure:'Do you confirm the status of the broadcast event with the modification number 【{id}】?',
tacHelp:'The TAC value is a decimal string, separated by ";" for multiple TAC values.',
cellIdHelp:'The CellId value is a hexadecimal string, separated by ";" for multiple CellId values.',
areaId:'The areaId value is a hexadecimal string.',
detail:'Detail',
}
},
dashboard: {
overview:{
title: "Core Network Dashboard",
@@ -368,10 +420,13 @@ export default {
imsUeNum: "VoNR/VoLTE",
smfUeNum: "Data Sessions",
gnbBase: "Online gNodeB",
gnbSumBase: "Total gNodeB",
enbBase: "Online eNodeB",
enbSumBase: "Total eNodeB",
gnbUeNum:'5G Active Users',
enbUeNum:'4G Active Users',
baseTitle:'Online Information',
nodeBInfo: 'NodeB Information',
},
upfFlow:{
title: "UPF Throughput",
@@ -401,6 +456,7 @@ export default {
},
userActivity: {
title: "User Activity",
imsTitle: "IMS Activity",
type: "Type",
duration: "Duration",
caller: "Caller",
@@ -443,6 +499,14 @@ export default {
sgwcVolumeGPRSUplink: 'GPRS Uplink',
sgwcVolumeGPRSDownlink: 'GPRS Downlink',
},
chart:{
charttitle:'Data Usage Report',
uplink:'Uplink(Byte)',
downlink:'Downlink(Byte)',
datausage:'Data Usage',
totaluplink:'Total Uplink',
totaldownlink:'Total Downlink',
},
ue: {
eventType: "Event Type",
realTimeDataStart: "Turn on real-time data",
@@ -877,6 +941,10 @@ export default {
rfspTip:'RFSP index, in NG-RAN, the index of a specific RRM configuration, parameter between 0 and 127',
ueTypeTip: 'Operator-defined subscriber UE Usage Type, integer, parameter between 0 and 127',
cnFlag: 'Whether to enable 5G Core Network service',
cnFlag0: 'No Access Allowed',
cnFlag1: 'Access Only 5G',
cnFlag2: 'Access Only 4G',
cnFlag3: 'Access 4G/5G',
epsFlagTip: 'Whether to enable 4G EPS service',
contextIdTip: 'To sign up for an APN Context ID, you must select it from the APN Context list.',
apnContextTip: 'The list of APNs available to the phone, up to six, is defined in the HSS.',
@@ -1049,6 +1117,21 @@ export default {
"chooseShowMetrics":"Select the metric you want to display",
"chooseMetrics":"Select an indicator",
},
voiceOverView:{
"voiceTitle":"Voice Calls Dashboard",
"tips":"Data and voice statistics per minute",
"ne":"NE",
"now":"now",
"last":"last",
"calls":"Calls",
"activeCall":"active calls",
"callMOMT":"calls MO/MT",
"failedcall":"failed calls",
"registration":"Registrations",
"activeregistration":"active registrations",
"registrationsuccess":"registration success",
"failedregistration":"failed registrations total",
},
},
traceManage: {
pcap: {
@@ -1633,11 +1716,12 @@ export default {
userName: 'Nick Name',
permission: 'Role',
className: 'Department',
userType: 'User Type',
loginIp: 'Login Address',
loginTime: 'Login Time',
status: 'Status',
userNameTip:'The account number can only contain strings of uppercase letters, lowercase letters and numbers with a minimum length of 6 digits',
passwdTip:'Please enter the correct password format',
userNameTip:'Please enter the correct username format no less than 4 digits',
passwdTip:'Please enter the correct password format no less than 6 digits',
nickNameTip:'Nicknames no less than 2 digits',
emailTip:'Please enter the correct email address',
phoneTip:'Please enter the correct phone number',
@@ -1695,6 +1779,55 @@ export default {
refreshCacheTip: "Are you sure you want to refresh the parameter configuration cache?",
refreshCacheOk: "Refresh Cache Successful",
},
loginSource: {
uid: "UID",
name: "Name",
namePlease: 'Please enter the authentication source name correctly',
icon: "Icon",
iconPlease: 'You can enter the image link or upload the image path address',
type: "Type",
activeFlag: "Status",
remark: "Remark",
createTime: "Create Time",
updateTime: "Update Time",
ldapUrl: "Server Address",
ldapUrlPlease: 'Please enter the LDAP server address correctly',
ldapBaseDN: "Base DN",
baseDnPlease: 'Please enter the LDAP base DN correctly',
ldapUserFilter: "User Filter",
userFilterPlease: 'Please enter the LDAP user filter correctly',
ldapBindDN: "Bind DN",
ldapBindPassword: "Bind Password",
smtpHost: 'Server Address',
smtpHostPlease: 'Please enter the SMTP server address correctly',
smtpPort: 'Port Number',
smtpPortPlease: 'Please enter the SMTP port number correctly',
oauth2ClientID: 'Client ID',
oauth2ClientIDPlease: 'Please enter the OAuth2 client ID correctly',
oauth2ClientSecret: 'Client Secret',
oauth2ClientSecretPlease: 'Please enter the OAuth2 client secret correctly',
oauth2AuthURL: 'Authorization URL',
oauth2AuthURLPlease: 'Please enter the OAuth2 authorization URL correctly',
oauth2TokenURL: 'Token URL',
oauth2TokenURLPlease: 'Please enter the OAuth2 token URL correctly',
oauth2UserURL: 'User Info URL',
oauth2UserURLPlease: 'Please enter the OAuth2 user info URL correctly',
oauth2AccountField: 'Account Field',
oauth2AccountFieldPlease: 'Please enter the OAuth2 account field correctly',
oauth2Scopes: 'Scopes',
oauth2ScopesPlease: 'Please enter the OAuth2 scopes correctly',
oauth2RedirectURL: 'Redirect URL',
oauth2RedirectURLPlease: 'Please enter the OAuth2 redirect URL correctly',
oauth2RedirectURLTip: 'Please jump to the specified path (omchost/#/login/oauth2), redirect with code and state address parameters',
uploadFileTip: 'Confirm to upload the authentication source icon?',
uploadFileOk: 'Authentication source icon upload successful',
uploadFileErr: 'Authentication source icon upload failed',
viewInfoErr: "Failed to get authentication source information",
addInfo: "Add Authentication Source",
editInfo: "Modify Authentication Source",
delTip: "Confirm deleting the authentication source number [{num}] data item?",
delOk: "Deleted Successfully",
},
setting: {
charMaxLen: 'characters length',
saveSubmit: 'Submit&Save',
+141 -8
View File
@@ -264,9 +264,13 @@ export default {
registerBtn: '注册账号',
loginBtn: '登录',
loginSuccess: '登录成功',
loginMethod: '其他登录方式:',
loginMethodWX: '微信扫一扫登录',
loginMethodQQ: 'QQ扫码登录',
otherMethod: '其他方式',
backBtn: '返回',
backBtnLogin: '返回登录',
authorizedNotfound: '授权无效',
authorizedFailed: '授权失败',
authorizedSuccess: '授权成功',
redirectHome: '{i} 秒后跳转主页',
},
register: {
registerBtn: '注册',
@@ -357,6 +361,54 @@ export default {
description: "暂无数据,尝试刷新看看",
},
},
cbc:{
cbe:{
neType:'CBC网元对象',
title: '广播事件',
delTip: '确认删除编号为【{num}】的CBC事件吗?',
eventName: '事件名称',
startTime: '开始时间',
endTime: '结束时间',
repetitionPeriod: '广播周期',
numOfBcast: '广播次数',
msgPWSType: '消息类型',
messageId: '消息编号',
displayMode: '显示模式',
geoScope:'广播覆盖范围',
emergencyUserAlert: '紧急用户提示',
activatePopup: '弹窗提示',
warningType: '预警类型标识',
language:'语言',
warningMessageText:'广播内容',
status: '状态',
warningAreaType: '预警区域类型',
taiListTip:'TAI List 不能为空',
taiSonTip:'TAI List 每项的 MCC、MNC、TAC 都不能为空',
eutraListTip:'EUTRA CellId List 不能为空',
eutraSonTip:'EUTRA CellId List 每项的 MCC、MNC、CellId 都不能为空',
nrTip:'NR CellId List 不能为空',
nrSonTip:'NR CellId List 每项的 MCC、MNC、CellId 都不能为空',
areaTip:'Area ID List 不能为空',
areaSonTip:'Area ID List 每项的 AreaID 都不能为空',
messageIdProfile:'消息标识配置',
serialNumProfile:'序列号配置',
warningTypeProfile:'预警类型配置',
warningMessageProfile:'预警消息配置',
etws:'地震海啸预警',
cmas:'公共预警广播',
createdAt:'创建时间',
eventNameHelp:'如果为CMAS,推荐前缀为cmas_xxx,如果为ETWS,推荐前缀为etws_xxx',
repetitionPeriodHelp:'单位是秒',
numOfBcastHelp:'广播帮助的数量',
cellListTip:'CellId List不能为空',
cellListSonTip:'MCC、MNC、不能为空以及EUTRA CellId/NR CellId至少填写一个',
letupSure:'确认修改编号为 【{id}】 的广播事件的状态嘛?',
tacHelp:'TAC值是十进制字符串,通过";"分隔多个TAC值',
cellIdHelp:'CellId值是十六进制字符串,通过";"分隔多个CellId值',
areaId:'areaId值是十六进制字符串',
detail:'详情',
}
},
dashboard: {
overview:{
title: "核心网系统看板",
@@ -367,11 +419,14 @@ export default {
userTitle:'用户信息',
imsUeNum: "IMS 会话数",
smfUeNum: "Data 会话数",
gnbBase: "5G 基站数",
gnbBase: "5G 基站在线数",
gnbSumBase: "5G 基站总数",
gnbUeNum:'5G 用户数',
enbBase: "4G 基站数",
enbBase: "4G 基站在线数",
enbSumBase: "4G 基站总数",
enbUeNum:'4G 用户数',
baseTitle:'在线信息',
nodeBInfo: '基站信息',
},
upfFlow:{
title: "用户面吞吐量",
@@ -401,6 +456,7 @@ export default {
},
userActivity: {
title: "用户活动",
imsTitle: "IMS 活动",
type: "类型",
duration: "时长",
caller: "主叫",
@@ -443,6 +499,14 @@ export default {
sgwcVolumeGPRSUplink: 'GPRS 上行链路',
sgwcVolumeGPRSDownlink: 'GPRS 下行链路',
},
chart:{
charttitle:'数据使用报告',
uplink:'上行流量(B)',
downlink:'下行流量(B)',
datausage:'流量使用',
totaluplink:'总上行流量',
totaldownlink:'总下行流量',
},
ue: {
eventType: "事件类型",
realTimeDataStart: "开启实时数据",
@@ -877,6 +941,10 @@ export default {
rfspTip:'RFSP 索引,在 NG-RAN 中,特定 RRM 配置的索引,参数介于0到127之间',
ueTypeTip: '运营商定义的用户 UE Usage Type,整型,参数介于0到127之间',
cnFlag: '是否开启 5G Core Network 服务',
cnFlag0: '不允许接入',
cnFlag1: '只能接入 5G',
cnFlag2: '只能接入 4G',
cnFlag3: '允许接入 4G/5G',
epsFlagTip: '是否开启 4G EPS 服务',
contextIdTip: '签约APN 上下文ID,必须从APN Context list 中选择。',
apnContextTip: '手机可用的APN列表,最多六个,在HSS中定义。',
@@ -1049,6 +1117,21 @@ export default {
"chooseShowMetrics":"选择需要显示的指标",
"chooseMetrics":"选择指标",
},
voiceOverView:{
"voiceTitle":"语音通话仪表盘",
"tips":"每分钟数据语音统计",
"ne":"网元",
"now":"现在",
"last":"过去",
"calls":"呼叫",
"activeCall":"正在通话",
"callMOMT":"呼叫 主叫接通率/被叫接通率",
"failedcall":"失败呼叫",
"registration":"注册",
"activeregistration":"主动注册",
"registrationsuccess":"注册成功率",
"failedregistration":"失败注册",
},
},
traceManage: {
pcap: {
@@ -1631,13 +1714,14 @@ export default {
userNum: '用户编号',
account: '登录账号',
userName: '用户昵称',
permission: '用户权限',
permission: '用户角色',
className: '部门名称',
userType: '用户类型',
loginIp: '登录地址',
loginTime: '登录时间',
status: '用户状态',
userNameTip:'账号只能包含大写字母、小写字母和数字的字符串,长度至少为6位',
passwdTip:'请输入正确的密码格式',
userNameTip:'请输入正确的用户名格式不少于4位',
passwdTip:'请输入正确的密码格式不少于6位',
nickNameTip:'昵称不少于2位',
emailTip:'请输入正确的邮箱地址',
phoneTip:'请输入正确的手机号码',
@@ -1695,6 +1779,55 @@ export default {
refreshCacheTip: "确定要刷新参数配置缓存吗?",
refreshCacheOk: "刷新缓存成功",
},
loginSource: {
uid: "唯一标识",
name: "名称",
namePlease: '请正确输入认证源名称',
icon: "图标",
iconPlease: '可填入图片链接或上传图片路径地址',
type: "类型",
activeFlag: "状态",
remark: "备注说明",
createTime: "创建时间",
updateTime: "更新时间",
ldapUrl: "服务器地址",
ldapUrlPlease: '请正确输入LDAP 服务器地址',
ldapBaseDN: "基础DN",
baseDnPlease: '请正确输入LDAP 基础DN',
ldapUserFilter: "用户过滤",
userFilterPlease: '请正确输入LDAP 用户过滤',
ldapBindDN: "绑定DN",
ldapBindPassword: "绑定密码",
smtpHost: '服务器地址',
smtpHostPlease: '请正确输入SMTP 服务器地址',
smtpPort: '端口号',
smtpPortPlease: '请正确输入SMTP 端口号',
oauth2ClientID: '客户端ID',
oauth2ClientIDPlease: '请正确输入OAuth2 客户端ID',
oauth2ClientSecret: '客户端密钥',
oauth2ClientSecretPlease: '请正确输入OAuth2 客户端密钥',
oauth2AuthURL: '授权URL',
oauth2AuthURLPlease: '请正确输入OAuth2 授权URL',
oauth2TokenURL: '令牌URL',
oauth2TokenURLPlease: '请正确输入OAuth2 令牌URL',
oauth2UserURL: '用户信息URL',
oauth2UserURLPlease: '请正确输入OAuth2 用户信息URL',
oauth2AccountField: '账号字段',
oauth2AccountFieldPlease: '请正确输入OAuth2 账号字段',
oauth2Scopes: '作用域',
oauth2ScopesPlease: '请正确输入OAuth2 作用域',
oauth2RedirectURL: '重定向URL',
oauth2RedirectURLPlease: '请正确输入OAuth2 重定向URL',
oauth2RedirectURLTip: '请跳转指定路径(omchost/#/login/oauth2), 重定向携带code和state地址参数',
uploadFileTip: '确认要上传认证源图标吗?',
uploadFileOk: '认证源图标上传成功',
uploadFileErr: '认证源图标上传失败',
viewInfoErr: "获取认证源信息失败",
addInfo: "添加认证源",
editInfo: "修改认证源",
delTip: "确认删除认证源编号为 【{num}】 的数据项?",
delOk: "删除成功",
},
setting: {
charMaxLen: '位字符长度',
saveSubmit: '提交保存',
+6 -1
View File
@@ -80,7 +80,12 @@ function fnChangeLocale(e: any) {
<a-space :size="12" align="center">
<a-tooltip placement="bottomRight">
<template #title>{{ t('loayouts.rightContent.alarm') }}</template>
<a-button type="text" style="color: inherit" @click="fnClickAlarm">
<a-button
type="text"
style="color: inherit"
@click="fnClickAlarm"
v-perms:has="['faultManage:active-alarm:index']"
>
<template #icon>
<a-badge
:count="useAlarmStore().activeAlarmTotal"
+31 -23
View File
@@ -22,6 +22,10 @@ import {
APP_DATA_API_KEY,
} from '@/constants/app-constants';
import {
RESULT_CODE_AUTH,
RESULT_CODE_AUTH_DEVICE,
RESULT_CODE_AUTH_INVALID,
RESULT_CODE_AUTH_NOTOKEN,
RESULT_CODE_ENCRYPT,
RESULT_CODE_ERROR,
RESULT_CODE_EXCEPTION,
@@ -34,7 +38,7 @@ import {
RESULT_MSG_TIMEOUT,
RESULT_MSG_URL_RESUBMIT,
} from '@/constants/result-constants';
import { decryptAES, encryptAES } from '@/utils/encrypt-utils';
import { decryptAES, encryptAES, hexMD5 } from '@/utils/encrypt-utils';
import { localGet } from '@/utils/cache-local-utils';
import { refreshToken } from '@/api/auth';
@@ -52,9 +56,7 @@ export type ResultType = {
/**防止重复提交类型 */
type RepeatSubmitType = {
/**请求地址 */
url: string;
/**请求数据 */
/**请求数据MD5 */
data: string;
/**请求时间 */
time: number;
@@ -158,19 +160,19 @@ function beforeRequest(options: OptionsType): OptionsType | Promise<any> {
['POST', 'PUT'].includes(options.method)
) {
const requestObj: RepeatSubmitType = {
data: hexMD5(
JSON.stringify({
url: options.url,
data: JSON.stringify(options.data) || '',
})
),
time: Date.now(),
};
const sessionObj: RepeatSubmitType = sessionGetJSON(CACHE_SESSION_FATCH);
if (sessionObj) {
const { url, data, time } = sessionObj;
const { data, time } = sessionObj;
const interval = 3000; // 间隔时间(ms),小于此时间视为重复提交
if (
requestObj.url === url &&
requestObj.data === data &&
requestObj.time - time < interval
) {
if (requestObj.data === data && requestObj.time - time < interval) {
const message = RESULT_MSG_URL_RESUBMIT[language];
return Promise.resolve({
code: RESULT_CODE_ERROR,
@@ -232,27 +234,33 @@ async function beforeResponse(
): Promise<any> {
// console.log('请求后的拦截', res);
// 登录失效时,移除授权令牌并重新刷新页面
// 登录失效时,移除访问令牌并重新请求
if (res.code === 401001) {
const result = await refreshToken(getRefreshToken());
// 移除授权令牌并重新刷新页面
function clearToken() {
delAccessToken();
delRefreshToken();
window.location.reload();
}
// 令牌失效时
if (res.code === RESULT_CODE_AUTH) {
const refreshTokenStr = getRefreshToken();
if (!refreshTokenStr) {
clearToken();
}
const result = await refreshToken(refreshTokenStr);
// 更新访问令牌和刷新令牌
if (result.code === RESULT_CODE_SUCCESS) {
setAccessToken(result.data.accessToken, result.data.refreshExpiresIn);
setRefreshToken(result.data.refreshToken, result.data.refreshExpiresIn);
return await request(options);
} else if (result.code === RESULT_CODE_AUTH_DEVICE) {
clearToken();
} else {
debugger
console.warn(result)
// delAccessToken();
// delRefreshToken();
window.location.reload();
// clearToken();
}
}
if ([401002, 401003].includes(res.code)) {
delAccessToken();
delRefreshToken();
window.location.reload();
// 令牌解析错误
if ([RESULT_CODE_AUTH_INVALID, RESULT_CODE_AUTH_NOTOKEN].includes(res.code)) {
clearToken();
}
// 响应数据解密
+1 -1
View File
@@ -49,7 +49,7 @@ router.afterEach((to, from, failure) => {
/**无Token可访问页面地址白名单 */
const WHITE_LIST: string[] = [
'/login',
'/auth-redirect',
'/login/oauth2',
'/help',
'/register',
'/quick-start',
+7 -2
View File
@@ -58,7 +58,7 @@ export const constantRoutes: RouteRecordRaw[] = [
{
path: '/trace-task-hlr',
name: 'TraceTaskHLR', // 跟踪任务HLR
meta: { title: 'router.traceTaskHLR', neType: ['HLR'] },
meta: { title: 'router.traceTaskHLR', neType: ['UDM', 'HLR'] },
component: () => import('@/views/traceManage/task-hlr/index.vue'),
},
{
@@ -74,11 +74,16 @@ export const constantRoutes: RouteRecordRaw[] = [
component: () => import('@/views/tool/lockScreen/index.vue'),
},
{
path: '/login/oauth2',
name: 'LoginOAuth2', // 第三方认证重定向
component: () => import('@/views/login/oauth2.vue'),
},
{
path: '/login',
name: 'Login', // 登录页
meta: { title: 'router.login' },
component: () => import('@/views/login.vue'),
component: () => import('@/views/login/index.vue'),
},
{
path: '/register',
+5 -4
View File
@@ -13,7 +13,7 @@ import { parseUrlPath } from '@/plugins/file-static-url';
/**用户信息类型 */
type UserInfo = {
/**用户ID */
/**是否强制修改密码 */
forcePasswdChange: boolean;
/**用户ID */
userId: string;
@@ -33,8 +33,8 @@ type UserInfo = {
email: string;
/**用户性别 */
sex: string | undefined;
/**其他信息 */
profile: Record<string, any>;
/**用户类型 */
userType: string;
};
const useUserStore = defineStore('user', {
@@ -49,7 +49,7 @@ const useUserStore = defineStore('user', {
phone: '',
email: '',
sex: undefined,
profile: {},
userType: 'System',
}),
getters: {
/**
@@ -132,6 +132,7 @@ const useUserStore = defineStore('user', {
this.phone = user.phone;
this.email = user.email;
this.sex = user.sex;
this.userType = user.userType;
// 验证返回的roles是否是一个非空数组
if (Array.isArray(roles) && roles.length > 0) {
+9
View File
@@ -48,3 +48,12 @@ export function decryptAES(ciphertext: string, aeskey: string): string {
}
return '';
}
/**
* MD5 编码
* @param message 字符串信息
* @returns hax码
*/
export function hexMD5(message: string): string {
return CryptoJS.MD5(message).toString(CryptoJS.enc.Hex);
}
+2 -1
View File
@@ -20,8 +20,9 @@ export const regExpPort =
* 有效账号格式
*
* 账号只能包含大写字母、小写字母和数字的字符串,长度至少为6位
* /^[A-Za-z0-9]{6,}$/
*/
export const regExpUserName = /^[A-Za-z0-9]{6,}$/;
export const regExpUserName = /^.{4,}$/;
/**
* 有效密码格式
+3
View File
@@ -6,8 +6,10 @@ import ResetPasswd from './components/reset-passwd.vue';
import StyleLayout from './components/style-layout.vue';
import { useRoute } from 'vue-router';
import useAppStore from '@/store/modules/app';
import useUserStore from '@/store/modules/user';
import useI18n from '@/hooks/useI18n';
const appStore = useAppStore();
const userStore = useUserStore();
const route = useRoute();
const { t } = useI18n();
@@ -43,6 +45,7 @@ onActivated(() => {
<a-tab-pane
key="reset-passwd"
:tab="t('views.account.settings.resetPasswd')"
v-if="userStore.userType === 'System'"
>
<ResetPasswd></ResetPasswd>
</a-tab-pane>
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -369,9 +369,9 @@ onBeforeUnmount(() => {
<a-descriptions-item :label="t('views.index.version')">
{{ serverState.version }}
</a-descriptions-item>
<a-descriptions-item :label="t('views.index.capability')">
<!-- <a-descriptions-item :label="t('views.index.capability')">
{{ serverState.capability }}
</a-descriptions-item>
</a-descriptions-item> -->
<!-- <a-descriptions-item :label="t('views.index.cpuUse')">
{{ serverState.cpuUse }}
</a-descriptions-item>
+7 -7
View File
@@ -174,7 +174,7 @@ let tableColumns: ColumnsType = [
if (record?.time) {
return record.time;
}
return parseDateToStr(+record.timestamp * 1000);
return parseDateToStr(record.timestamp);
},
},
{
@@ -685,16 +685,16 @@ onBeforeUnmount(() => {
<DictTag :options="dict.ueEventType" :value="record.eventType" />
</template>
<template v-if="column.key === 'result'">
<span v-if="record.eventType === 'auth-result'">
<span v-if="record.eventType === 'Auth'">
<DictTag
:options="dict.ueAauthCode"
:value="record.eventJSON.result"
/>
</span>
<span v-if="record.eventType === 'detach'">
<span v-if="record.eventType === 'Detach'">
<span>{{ t('views.dashboard.ue.resultOk') }}</span>
</span>
<span v-if="record.eventType === 'cm-state'">
<span v-if="record.eventType === 'CM'">
<DictTag
:options="dict.ueEventCmState"
:value="record.eventJSON.result"
@@ -766,16 +766,16 @@ onBeforeUnmount(() => {
</div>
<div>
<span>{{ t('views.dashboard.ue.result') }}: </span>
<span v-if="record.eventType === 'auth-result'">
<span v-if="record.eventType === 'Auth'">
<DictTag
:options="dict.ueAauthCode"
:value="record.eventJSON.result"
/>
</span>
<span v-if="record.eventType === 'detach'">
<span v-if="record.eventType === 'Detach'">
{{ t('views.dashboard.ue.resultOk') }}
</span>
<span v-if="record.eventType === 'cm-state'">
<span v-if="record.eventType === 'CM'">
<DictTag
:options="dict.ueEventCmState"
:value="record.eventJSON.result"
+6 -11
View File
@@ -290,14 +290,11 @@ function fnTableSelectedRowKeys(keys: (string | number)[]) {
type ModalStateType = {
/**确定按钮 loading */
confirmLoading: boolean;
/**最大ID值 */
maxId: number;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
confirmLoading: false,
maxId: 0,
});
/**
@@ -399,11 +396,6 @@ function fnGetList(pageNum?: number) {
return item;
});
// 取最大值ID用作实时累加
if (total > 0) {
modalState.maxId = Number(rows[0].id);
}
}
tableState.loading = false;
});
@@ -490,15 +482,18 @@ function wsMessage(res: Record<string, any>) {
// cdrEvent CDR会话事件
if (data.groupId === `1005_${queryParams.neId}`) {
const cdrEvent = data.data;
let cdrJSON = {};
try {
cdrJSON = JSON.parse(cdrEvent.cdrJSON);
} catch (e) {}
queue.add(async () => {
modalState.maxId += 1;
tableState.data.unshift({
id: modalState.maxId,
id: cdrEvent.id,
neType: cdrEvent.neType,
neName: cdrEvent.neName,
rmUID: cdrEvent.rmUID,
timestamp: cdrEvent.timestamp,
cdrJSON: cdrEvent.CDR,
cdrJSON: cdrJSON,
});
tablePagination.total += 1;
if (tableState.data.length > 100) {
+8 -8
View File
@@ -175,7 +175,7 @@ let tableColumns: ColumnsType = [
if (record?.time) {
return record.time;
}
return parseDateToStr(+record.timestamp * 1000);
return parseDateToStr(record.timestamp);
},
},
{
@@ -460,7 +460,7 @@ onMounted(() => {
if (resArr[1].status === 'fulfilled') {
const ueEventType: any[] = JSON.parse(JSON.stringify(resArr[1].value));
dict.ueEventType = ueEventType.map(item => {
if (item.value === 'cm-state') {
if (item.value === 'CM') {
item.label = item.label.replace('CM', 'ECM');
}
return item;
@@ -693,16 +693,16 @@ onBeforeUnmount(() => {
<DictTag :options="dict.ueEventType" :value="record.eventType" />
</template>
<template v-if="column.key === 'result'">
<span v-if="record.eventType === 'auth-result'">
<span v-if="record.eventType === 'Auth'">
<DictTag
:options="dict.ueAauthCode"
:value="record.eventJSON.result"
/>
</span>
<span v-if="record.eventType === 'detach'">
<span v-if="record.eventType === 'Detach'">
<span>{{ t('views.dashboard.ue.resultOk') }}</span>
</span>
<span v-if="record.eventType === 'cm-state'">
<span v-if="record.eventType === 'CM'">
<DictTag
:options="dict.ueEventCmState"
:value="record.eventJSON.result"
@@ -774,16 +774,16 @@ onBeforeUnmount(() => {
</div>
<div>
<span>{{ t('views.dashboard.ue.result') }}: </span>
<span v-if="record.eventType === 'auth-result'">
<span v-if="record.eventType === 'Auth'">
<DictTag
:options="dict.ueAauthCode"
:value="record.eventJSON.result"
/>
</span>
<span v-if="record.eventType === 'detach'">
<span v-if="record.eventType === 'Detach'">
{{ t('views.dashboard.ue.resultOk') }}
</span>
<span v-if="record.eventType === 'cm-state'">
<span v-if="record.eventType === 'CM'">
<DictTag
:options="dict.ueEventCmState"
:value="record.eventJSON.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,
@@ -152,38 +152,35 @@ onMounted(() => {
<div></div>
</div>
<div class="card-ue-w33" v-if="item.type === 'auth-result'">
<div class="card-ue-w33" v-if="item.type === 'Auth'">
<div>
GNB ID: <span>{{ item.data.gNBID }}</span>
GNB ID: <span>{{ item.data.nbId }}</span>
</div>
<div>
Cell ID: <span>{{ item.data.cellID }}</span>
Cell ID: <span>{{ item.data.cellId }}</span>
</div>
<div>
TAC ID: <span>{{ item.data.tacID }}</span>
TAC ID: <span>{{ item.data.tac }}</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 v-if="item.data?.recordTime">
{{ parseDateToStr(item.data.recordTime) }}
</template>
<template v-else> - </template>
</div>
<div v-if="item.type === 'auth-result'">
<div v-if="item.type === 'Auth'">
{{ t('views.dashboard.overview.userActivity.result') }}:&nbsp;
<span>
<DictTag :options="dict.ueAauthCode" :value="item.data.result" />
</span>
</div>
<div v-if="item.type === 'detach'">
<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'">
<div class="card-ue-w33" v-if="item.type === 'CM'">
{{ t('views.dashboard.overview.userActivity.result') }}:&nbsp;
<span>
<DictTag :options="dict.ueEventCmState" :value="item.data.result" />
@@ -199,7 +196,7 @@ onMounted(() => {
<div class="card-ue-item">
<div>
{{ t('views.dashboard.overview.userActivity.type') }}:&nbsp;
<span v-if="item.type === 'cm-state'">
<span v-if="item.type === 'CM'">
{{
dict.ueEventType
.find(s => s.value === item.type)
@@ -216,42 +213,35 @@ onMounted(() => {
<div></div>
</div>
<div class="card-ue-w33" v-if="item.type === 'auth-result'">
<div class="card-ue-w33" v-if="item.type === 'Auth'">
<div>
ENB ID: <span>{{ item.data.eNBID }}</span>
ENB ID: <span>{{ item.data.nbId }}</span>
</div>
<div>
Cell ID: <span>{{ item.data.cellID }}</span>
Cell ID: <span>{{ item.data.cellId }}</span>
</div>
<div>
TAC ID: <span>{{ item.data.tacID }}</span>
TAC ID: <span>{{ item.data.tac }}</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 v-if="item.data?.recordTime">
{{ parseDateToStr(item.data.recordTime) }}
</template>
<template v-else> - </template>
</div>
<div v-if="item.type === 'auth-result'">
<div v-if="item.type === 'Auth'">
{{ t('views.dashboard.overview.userActivity.result') }}:&nbsp;
<span>
<DictTag :options="dict.ueAauthCode" :value="item.data.result" />
</span>
</div>
<div v-if="item.type === 'detach'">
<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'">
<div class="card-ue-w33" v-if="item.type === 'CM'">
{{ t('views.dashboard.overview.userActivity.result') }}:&nbsp;
<span>
<DictTag :options="dict.ueEventCmState" :value="item.data.result" />
+7 -10
View File
@@ -19,9 +19,6 @@ 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();
@@ -71,13 +68,13 @@ export default function useWS() {
}
break;
//UPF-总流量数
case 'upf_001_0':
case `upf_${upfWhoId.value}_0`:
upfTFParse('0', data);
break;
case 'upf_001_7':
case `upf_${upfWhoId.value}_7`:
upfTFParse('7', data);
break;
case 'upf_001_30':
case `upf_${upfWhoId.value}_30`:
upfTFParse('30', data);
break;
}
@@ -87,7 +84,7 @@ export default function useWS() {
}
switch (data.groupId) {
// kpiEvent 指标UPF
case '10_UPF_' + upfWhoId.value:
case `10_UPF_${upfWhoId.value}`:
if (data.data) {
upfFlowParse(data.data);
}
@@ -122,11 +119,11 @@ export default function useWS() {
upfTotalFlow.value[day].requestFlag = true;
ws.send({
requestId: `upf_001_${day}`,
requestId: `upf_${upfWhoId.value}_${day}`,
type: 'upf_tf',
data: {
neType: 'UPF',
neId: '001',
neId: upfWhoId.value,
day: Number(day),
},
});
@@ -197,7 +194,7 @@ export default function useWS() {
* MME_UE会话事件(GroupID:1011_neId)
* IMS_CDR会话事件(GroupID:1005_neId)
*/
subGroupID: '10_UPF_' + neId + ',1010_001,1011_001,1005_001',
subGroupID: `10_UPF_${neId},1010_001,1011_001,1005_001`,
},
onmessage: wsMessage,
onerror: (ev: any) => {
@@ -0,0 +1,392 @@
<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;
console.log(res0)
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);
console.log(item)
console.log(alarmTypeType.value[0])
}
}
}
if (resArr[1].status === 'fulfilled') {
const res1 = resArr[1].value;
if (res1.code === RESULT_CODE_SUCCESS && Array.isArray(res1.data)) {
console.log(res1.data)
alarmTypeTypeTop.value = res1.data
.sort((a: any, b: any) => b.value - a.value)
.slice(0, 3);
}
console.log(alarmTypeTypeTop.value)
}
})
.then(() => {
// 计算最大值用于标准化
const maxAlarmValue = Math.max(...alarmTypeType.value.map((item: any) => item.value || 0), 1);
const maxTopValue = Math.max(...alarmTypeTypeTop.value.map((item: any) => item.total || 0), 1);
const optionData: EChartsOption = {
title: [
{
text: 'Top3',
left: 'center',
top: '48%',
textStyle: {
color: '#fff',
fontSize: 16,
fontWeight: 'bold',
},
},
],
grid: [
{ // 主图
top: '5%',
left: '20%',
right: '10%',
height: '38%'
},
{ // Top3
top: '58%',
left: '20%',
right: '10%',
height: '32%'
}
],
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: function(params: any) {
if (Array.isArray(params) && params.length > 0) {
const param = params[0];
// 获取真实值
let realValue = 0;
if (param.seriesIndex === 0 || param.seriesIndex === 1) {
// 告警类型背景或实际值
realValue = alarmTypeType.value[param.dataIndex]?.value || 0;
} else if (param.seriesIndex === 2 || param.seriesIndex === 3) {
// Top3背景或实际值
realValue = alarmTypeTypeTop.value[param.dataIndex]?.total || 0;
}
return `${param.name}: ${realValue}`;
}
return '';
},
},
legend: {
show: false
},
xAxis: [
{
type: 'value',
gridIndex: 0,
show: false,
max: maxAlarmValue
},
{
type: 'value',
gridIndex: 1,
show: false,
max: maxTopValue
}
],
yAxis: [
{
type: 'category',
gridIndex: 0,
data: alarmTypeType.value.map((item: any) => item.name),
axisLabel: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold',
interval: 0 // 确保显示所有标签
},
axisLine: { show: false },
axisTick: { show: false },
inverse: true
},
{
type: 'category',
gridIndex: 1,
data: alarmTypeTypeTop.value.map((item: any) => item.neType),
axisLabel: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold',
interval: 0 // 确保显示所有标签
},
axisLine: { show: false },
axisTick: { show: false },
inverse: true
}
],
series: [
// 告警类型透明背景柱状图
{
type: 'bar',
xAxisIndex: 0,
yAxisIndex: 0,
barWidth: 16,
barGap: '-100%', // 重叠显示
itemStyle: {
borderRadius: [0, 8, 8, 0],
color: 'rgba(255,255,255,0.1)',
borderColor: '#666',
borderWidth: 1
},
data: Array(alarmTypeType.value.length).fill(maxAlarmValue),
zlevel: 1,
silent: true, // 不响应鼠标事件
label: {
show: true,
position: 'right',
color: '#fff',
fontWeight: 'bold',
fontSize: 14,
formatter: (params: any) => {
const realValue = alarmTypeType.value[params.dataIndex]?.value || 0;
return `${realValue}`;
},
}
},
// 告警类型实际值柱状图
{
type: 'bar',
xAxisIndex: 0,
yAxisIndex: 0,
barWidth: 16,
barGap: '-100%', // 重叠显示
itemStyle: {
borderRadius: [0, 8, 8, 0],
color: function (params: any) {
// 如果值为0,不显示颜色条
if (params.value === 0) {
return 'transparent';
}
// 渐变色
const colorArr = [
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#f5222d' },
{ offset: 1, color: '#fa8c16' }
]),
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#fa8c16' },
{ offset: 1, color: '#fadb14' }
]),
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#fadb14' },
{ offset: 1, color: '#1677ff' }
]),
new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#1677ff' },
{ offset: 1, color: '#00fcff' }
])
];
return colorArr[params.dataIndex] || colorArr[3];
}
},
label: { show: false }, // 实际值柱状图不显示标签
data: alarmTypeType.value.map((item: any) => item.value || 0),
zlevel: 2
},
// Top3透明背景柱状图
{
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
barWidth: 16,
barGap: '-100%', // 重叠显示
itemStyle: {
borderRadius: [0, 20, 20, 0],
color: 'rgba(255,255,255,0.1)',
borderColor: '#666',
borderWidth: 1
},
data: Array(alarmTypeTypeTop.value.length).fill(maxTopValue),
zlevel: 1,
silent: true, // 不响应鼠标事件
label: {
show: true,
position: 'right',
color: '#fff',
fontWeight: 'bold',
fontSize: 14,
formatter: (params: any) => {
const realValue = alarmTypeTypeTop.value[params.dataIndex]?.total || 0;
return `${realValue}`;
},
}
},
// Top3实际值柱状图
{
name: 'Top3',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
barWidth: 16,
barGap: '-100%', // 重叠显示
itemStyle: {
borderRadius: [0, 20, 20, 0],
color: function (params: any) {
// 如果值为0,不显示颜色条
if (params.value === 0) {
return 'transparent';
}
return new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#f0f5ff' },
{ offset: 0.5, color: '#adc6ff' },
{ offset: 1, color: '#2f54eb' },
]);
}
},
label: { show: false }, // 实际值柱状图不显示标签
data: alarmTypeTypeTop.value.map((item: any) => item.total || 0),
zlevel: 2
}
]
};
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>
@@ -0,0 +1,284 @@
<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') }}:&nbsp;
<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') }}:&nbsp;
<span>{{ parseDuration(item.data.callDuration) }}</span>
</div>
<div v-else></div>
</div>
<div>
{{ t('views.dashboard.overview.userActivity.time') }}:&nbsp;
<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') }}:&nbsp;
<span v-if="item.data.callType !== 'sms'">
<DictTag
:options="dict.cdrSipCode"
:value="item.data.cause"
value-default="0"
/>
&nbsp;-&nbsp;
<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>
@@ -0,0 +1,369 @@
<script setup lang="ts">
import { onMounted, ref, nextTick, watch } from 'vue';
import * as echarts from 'echarts/core';
import { GridComponent, 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]);
/**图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: '{a}: {c}%',
formatter: (params: any) => {
return `${params.seriesName}: ${(params.value * 100).toFixed(2)}%`;
},
},
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>
@@ -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>
@@ -0,0 +1,290 @@
<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: '001',
beginTime: 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>
@@ -0,0 +1,314 @@
<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') }}:&nbsp;
<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'">
<div>
GNB ID: <span>{{ item.data.nbId }}</span>
</div>
<div>
Cell ID: <span>{{ item.data.cellId }}</span>
</div>
<div>
TAC ID: <span>{{ item.data.tac }}</span>
</div>
</div>
<div>
{{ t('views.dashboard.overview.userActivity.time') }}:
<template v-if="item.data?.recordTime">
{{ parseDateToStr(item.data.recordTime) }}
</template>
<template v-else> - </template>
</div>
<div v-if="item.type === 'Auth'">
{{ t('views.dashboard.overview.userActivity.result') }}:&nbsp;
<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'">
{{ t('views.dashboard.overview.userActivity.result') }}:&nbsp;
<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') }}:&nbsp;
<span v-if="item.type === 'CM'">
{{
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'">
<div>
ENB ID: <span>{{ item.data.nbId }}</span>
</div>
<div>
Cell ID: <span>{{ item.data.cellId }}</span>
</div>
<div>
TAC ID: <span>{{ item.data.tac }}</span>
</div>
</div>
<div>
{{ t('views.dashboard.overview.userActivity.time') }}:
<template v-if="item.data?.recordTime">
{{ parseDateToStr(item.data.recordTime) }}
</template>
<template v-else> - </template>
</div>
<div v-if="item.type === 'Auth'">
{{ t('views.dashboard.overview.userActivity.result') }}:&nbsp;
<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'">
{{ t('views.dashboard.overview.userActivity.result') }}:&nbsp;
<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>
+506
View File
@@ -0,0 +1,506 @@
.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:
linear-gradient(rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.75)),
url(../images/bj.png);
height: 100vh;
margin-bottom: -20px;
background-size:80% 80%;
background-attachment:fixed;
-webkit-background-size: cover;
}
.column {
flex: 2;
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;
}
/* 使用伪元素创建L形角框 */
/* 添加L形角框边框效果 */
.panel::before,
.panel::after {
content: '';
position: absolute;
pointer-events: none;
z-index: 10;
display: none; /* 隐藏所有角框 */
}
/* 左上角L形 - 相对于inner的实际边界 */
.panel::before {
top: -2.125rem;
left: -5.5rem;
width: 10px;
height: 10px;
border-top: 3px solid #4c9bfd;
border-left: 3px solid #4c9bfd;
}
/* 右下角L形 - 相对于inner的实际边界 */
.panel::after {
bottom: -0.875rem;
right: -1.583rem;
width: 10px;
height: 10px;
border-bottom: 3px solid #4c9bfd;
border-right: 3px solid #4c9bfd;
}
/* 右上角L形 - 相对于inner的实际边界 */
.panel .inner::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 10px;
height: 10px;
border-top: 3px solid #4c9bfd;
border-right: 3px solid #4c9bfd;
pointer-events: none;
z-index: 10;
display: none; /* 隐藏所有角框 */
}
/* 左下角L形 - 相对于inner的实际边界 */
.panel .inner::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 10px;
height: 10px;
border-bottom: 3px solid #4c9bfd;
border-left: 3px solid #4c9bfd;
pointer-events: none;
z-index: 10;
display: none; /* 隐藏所有角框 */
}
/* 为base模块调整角框显示 */
/* 第一个base模块:只显示上方角框(左上角和右上角) */
.skim.panel.base:first-of-type::after,
.skim.panel.base:first-of-type .inner::after {
display: none;
}
/* 第二个base模块:只显示下方角框(左下角和右下角) */
.skim.panel.base:not(:first-of-type)::before,
.skim.panel.base:not(:first-of-type) .inner::before {
display: none;
}
.panel .inner {
/* 装内容 */
/* height: 60px; */
position: absolute;
top: -2.125rem;
right: -1.583rem;
bottom: -0.875rem;
left: -5.5rem;
padding: 1rem 0.3rem;
}
.panel h3 {
font-size: 0.833rem;
color: #fff;
background: rgba(76, 155, 253, 0.1); /* 为所有模块标题添加淡蓝色背景 */
padding: 0.5rem 1.2rem;
border-radius: 0.7rem 0.7rem 0 0;
margin: 0;
box-sizing: border-box;
}
.leftright {
width: 100%;
min-height: 2.5rem;
/*background: url(../images/title.png) no-repeat center center;*/
background: rgba(76, 155, 253, 0.1); /* 添加淡蓝色背景 */
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: 1.2rem;
padding-left: 0;
}
.centerStyle {
width: 100%;
min-height: 2.5rem;
/*background: url(../images/title.png) no-repeat center center;*/
background: rgba(76, 155, 253, 0.1); /* 添加淡蓝色背景 */
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: 1.2rem;
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;
background: rgba(76, 155, 253, 0.1); /* 为网络拓扑模块标题添加淡蓝色背景 */
padding: 0.5rem 1.2rem;
border-radius: 0.7rem 0.7rem 0 0;
margin: 0;
box-sizing: border-box;
}
.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: 0.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: 90%;/*生效了底部边距*/
margin-top: 1rem;
padding-bottom: 1rem;
box-sizing: border-box;/*不知道为啥没生效*/
}
/* 流量统计 */
.upfFlowTotal1 {
/* min-height: 7.5rem; */
height: 14.4%;
}
.upfFlowTotal1 .inner h3 {
display: flex;
justify-content: space-between;
background: rgba(76, 155, 253, 0.1); /* 为流量统计模块标题添加淡蓝色背景 */
padding: 0.5rem 1.2rem;
border-radius: 0.7rem 0.7rem 0 0;
margin: 0;
box-sizing: border-box;
}
.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;
background: rgba(76, 155, 253, 0.1); /* 为流量统计模块标题添加淡蓝色背景 */
padding: 0.5rem 1.2rem;
border-radius: 0.7rem 0.7rem 0 0;
margin: 0;
box-sizing: border-box;
}
.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: 2rem;
}
/* 告警统计 */
.alarmType {
/* min-height: 25rem; */
height: 35%;
}
.alarmType .inner .chart {
width: 100%;
height: 100%;
}
/* 为base模块的第二个模块(4G部分)添加标题样式 */
.skim.panel.base:not(:first-of-type) h3 {
background: transparent; /* 移除背景,避免阴影问题 */
padding: 0;
margin: 0;
min-height: 0;
display: none; /* 完全隐藏空的h3元素 */
}
/* 跳转鼠标悬浮 */
.toRouter:hover {
cursor: pointer;
color: #fff !important;
}
.toRouter:hover > *,
.toRouter:hover > * > * {
color: #fff !important;
}
@@ -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();
}
@@ -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';
}
@@ -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 = '';
}
@@ -0,0 +1,222 @@
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>('');
/**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>) {
//console.log(res);
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 网元状态
if (requestId && requestId.startsWith('neState')) {
const neType = requestId.split('_')[1];
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_${upfWhoId.value}_0`:
upfTFParse('0', data);
break;
case `upf_${upfWhoId.value}_7`:
upfTFParse('7', data);
break;
case `upf_${upfWhoId.value}_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_${upfWhoId.value}_${day}`,
type: 'upf_tf',
data: {
neType: 'UPF',
neId: upfWhoId.value,
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();
upfWhoId.value = '';
});
return {
wsSend,
userActivitySend,
upfTFSend,
reSendUPF,
};
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

+875
View File
@@ -0,0 +1,875 @@
<script setup lang="ts">
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import svgBase from '@/assets/svg/base.svg';
import svgUserIMS from '@/assets/svg/userIMS.svg';
import svgUserSMF from '@/assets/svg/userSMF.svg';
import useI18n from '@/hooks/useI18n';
import Topology from './components/Topology/index.vue';
import NeResources from './components/NeResources/index.vue';
import UserActivity from './components/UserActivity/index.vue';
import IMSActivity from './components/IMSActivity/index.vue';
import AlarnTypeBar from './components/AlarnTypeBar/index.vue';
import UPFFlow from './components/UPFFlow/index.vue';
import { listUDMSub } from '@/api/neData/udm_sub';
import { listSMFSubNum } from '@/api/neData/smf';
import { listIMSSessionNum } from '@/api/neData/ims';
import { listAMFNblist } from '@/api/neData/amf';
import { listMMENblist } from '@/api/neData/mme';
import {
graphNodeClickID,
graphState,
notNeNodes,
graphNodeStateNum,
neStateRequestMap,
} from './hooks/useTopology';
import { upfTotalFlow, upfTFActive } from './hooks/useUPFTotalFlow';
import { useFullscreen } from '@vueuse/core';
import useWS from './hooks/useWS';
import useAppStore from '@/store/modules/app';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { useRouter } from 'vue-router';
import useNeListStore from '@/store/modules/ne_list';
import { message } from 'ant-design-vue';
import { upfWhoId } from './hooks/useWS';
import { listAMFNbStatelist } from '@/api/neData/amf';
import { listMMENbStatelist } from '@/api/neData/mme';
const neListStore = useNeListStore();
const router = useRouter();
const appStore = useAppStore();
const { t } = useI18n();
const { wsSend, userActivitySend, upfTFSend, reSendUPF } = useWS();
/**概览状态类型 */
type SkimStateType = {
/**UDM签约用户数量 */
udmSubNum: number;
/**SMF在线用户数 */
smfUeNum: number;
/**IMS在线用户数 */
imsUeNum: number;
/**5G基站数量 */
gnbNum: number;
/**5G在线用户数量 */
gnbUeNum: number;
/**4G基站数量 */
enbNum: number;
/**4G在线用户数量 */
enbUeNum: number;
/**5G用户总数量 */
gNbSumNum: number;
/**4G用户总数量 */
eNbSumNum: number;
};
/**概览状态信息 */
let skimState: SkimStateType = reactive({
udmSubNum: 0,
smfUeNum: 0,
imsUeNum: 0,
gnbNum: 0,
gnbUeNum: 0,
enbNum: 0,
enbUeNum: 0,
gNbSumNum: 0,
eNbSumNum: 0,
});
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**总览节点 */
const viewportDom = ref<HTMLElement | null>(null);
const { isFullscreen, toggle } = useFullscreen(viewportDom);
let initFlag = false;
/**10s调度器 */
const interval10s = ref<any>(null);
/**5s调度器 */
const interval5s = ref<any>(null);
/**查询网元状态 */
function fnGetNeState() {
// 获取节点状态
for (const node of graphState.data.nodes) {
if (notNeNodes.includes(node.id)) continue;
const neInfoList = node.neInfoList || [];
if (neInfoList.length === 0) continue;
for (const neInfo of neInfoList) {
if (!neInfo.neType || !neInfo.neId) continue;
wsSend({
requestId: `neState_${neInfo.neType}_${neInfo.neId}`,
type: 'ne_state',
data: {
neType: neInfo.neType,
neId: neInfo.neId,
},
});
}
}
}
/**获取概览信息 */
async function fnGetSkim() {
let tempGnbSumNum = 0;
let tempEnbSumNum = 0;
const neHandlers = new Map([
// [
// 'UDM',
// {
// request: (neId: string) =>
// listUDMSub({ neId: neId, pageNum: 1, pageSize: 1 }),
// process: (res: any) =>
// res.code === RESULT_CODE_SUCCESS &&
// (skimState.udmSubNum += res.data.total),
// },
// ],
[
'SMF',
{
request: (neId: string) => listSMFSubNum(neId),
process: (res: any) => {
if (
res.code === RESULT_CODE_SUCCESS &&
typeof res.data === 'number'
) {
skimState.smfUeNum += res.data;
}
},
},
],
[
'IMS',
{
request: (neId: string) => listIMSSessionNum(neId),
process: (res: any) => {
if (
res.code === RESULT_CODE_SUCCESS &&
typeof res.data === 'number'
) {
skimState.imsUeNum += res.data;
}
},
},
],
// [
// 'AMF',
// {
// request: (neId: string) => listAMFNblist({ neId }),
// process: (res: any) => {
// if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// skimState.gnbNum += res.data.length;
// skimState.gnbUeNum += res.data.reduce(
// (sum: number, item: any) => sum + item.ueNum,
// 0
// );
// }
// },
// },
// ],
[
'AMF',
{
request: (neId: string) => listAMFNblist({ neId }),
process: async (res: any, neId: any) => {
console.log(neId)
if (res.code === RESULT_CODE_SUCCESS&& Array.isArray(res.data)) {
skimState.gnbNum += res.data.length;
skimState.gnbUeNum += res.data.reduce(
(sum: number, item: any) => sum + item.ueNum,
0
);
const amfNbRes = await listAMFNbStatelist({ neId });
console.log(amfNbRes)
if (
amfNbRes.code === RESULT_CODE_SUCCESS &&
Array.isArray(amfNbRes.data)
) {
// skimState.gNbSumNum += amfNbRes.data.length;
tempGnbSumNum += amfNbRes.data.length;
}
}
},
},
],
// [
// 'MME',
// {
// request: (neId: string) => listMMENblist({ neId }),
// process: (res: any) => {
// if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// skimState.enbNum += res.data.length;
// skimState.enbUeNum += res.data.reduce(
// (sum: number, item: any) => sum + item.ueNum,
// 0
// );
// }
// },
// },
// ],
[
'MME',
{
request: (neId: string) => listMMENblist({ neId }),
process: async (res: any, neId: any) => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
skimState.enbNum += res.data.length;
skimState.enbUeNum += res.data.reduce(
(sum: number, item: any) => sum + item.ueNum,
0
);
const mmeNbRes = await listMMENbStatelist({ neId });
console.log(mmeNbRes)
if (
mmeNbRes.code === RESULT_CODE_SUCCESS &&
Array.isArray(mmeNbRes.data)
) {
// skimState.eNbSumNum += mmeNbRes.data.length;
console.log(mmeNbRes)
tempEnbSumNum += mmeNbRes.data.length;
}
}
},
},
],
]);
console.log(neCascaderOptions)
const requests = neCascaderOptions.value.flatMap(
(ne: any) =>
ne.children
?.map((child: any) => {
console.log(child.neId)
const handler = neHandlers.get(child.neType);
return handler
? {
promise: handler.request(child.neId),
process: handler.process,
neId: child.neId, // 这里加上neId
}
: null;
})
.filter(Boolean) || []
);
const results = await Promise.allSettled(requests.map(r => r.promise));
// 重置
Object.assign(skimState, {
//udmSubNum: 0,
smfUeNum: 0,
imsUeNum: 0,
gnbNum: 0,
gnbUeNum: 0,
enbNum: 0,
enbUeNum: 0,
});
// results.forEach((result, index) => {
// if (result.status === 'fulfilled') {
// requests[index].process(result.value);
// } else {
// requests[index].process(0);
// }
// });
const processPromises = results.map((result: any, index: any) => {
const req = requests[index];
if (result.status === 'fulfilled') {
return req.process(result.value, req.neId);
} else {
return req.process(0, req.neId);
}
});
await Promise.all(processPromises);
skimState.gNbSumNum = tempGnbSumNum;
skimState.eNbSumNum = tempEnbSumNum;
// UDM
listUDMSub({ neId: udmNeId.value, pageNum: 1, pageSize: 1 }).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
skimState.udmSubNum = res.data.total;
}
});
}
/**初始数据函数 */
function loadData() {
fnGetNeState(); // 获取网元状态
userActivitySend();
upfTFSend('0');
upfTFSend('7');
upfTFSend('30');
clearInterval(interval10s.value);
interval10s.value = setInterval(() => {
if (!interval10s.value || !initFlag) return;
if (upfTFActive.value === '0') {
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();
}
// UPF实时流量下拉菜单选择
function fnSelectUPF(e: any) {
upfWhoId.value = e.key;
reSendUPF(e.key);
for (var key in upfTotalFlow.value) {
upfTotalFlow.value[key].requestFlag = false;
}
}
let udmNeId = ref<string>('001');
let udmOtions = ref<Record<string, any>[]>([]);
let onlineOtions = ref<Record<string, any>[]>([]);
/**用户数量-选择UDM */
async function fnSelectUDM(e: any) {
udmNeId.value = e.key;
try {
const res = await listUDMSub({
neId: udmNeId.value,
pageNum: 1,
pageSize: 1,
});
console.log(res)
// listUDMSub({ neId: udmNeId.value, pageNum: 1, pageSize: 1 }).then(res => {
if (res.code === RESULT_CODE_SUCCESS && typeof res.data.total === 'number') {
skimState.udmSubNum = res.data.total;
console.log(res)
} else {
skimState.udmSubNum = 0;
}
// }).catch(() => {
// skimState.udmSubNum = 0;
// });
} catch (error) {
skimState.udmSubNum = 0;
}
}
/**资源控制-选择NE */
function fnSelectNeRe(e: any) {
console.log(e)
graphNodeClickID.value = e.key;
}
//
// 定义一个方法返回 views 容器
const getPopupContainer = () => {
// 使用 ref 或其他方式来引用你的 views 容器
// 如果 views 容器直接在这个组件内部,你可以使用 ref
// 但在这个例子中,我们假设它是通过类名来获取的
return document.querySelector('.viewport');
};
onMounted(() => {
// 获取网元网元列表
neListStore.neCascaderOptions.forEach(item => {
console.log(item)
if (item.value === 'UPF') {
neOtions.value = JSON.parse(JSON.stringify(item.children));
}
if (item.value === 'UDM') {
udmOtions.value = JSON.parse(JSON.stringify(item.children));
}
});
if (neOtions.value.length > 0) {
fnSelectNe(neOtions.value[0].value, neOtions.value[0]);
}
if (udmOtions.value.length > 0) {
fnSelectUDM({ key: udmOtions.value[0].value });
}
// if (onlineArr.length > 0) {
// fnSelectNeRe({ key: onlineArr[0].value });
// }
// 过滤不可用的网元
neCascaderOptions.value = neListStore.getNeCascaderOptions.filter(
(item: any) => {
return ['UDM', 'SMF', 'IMS', 'AMF', 'MME'].includes(item.value);
}
);
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
//online Ne
let onlineArr: Record<string, any>[] = [];
// UDM
neListStore.neList.forEach((v: any) => {
if (
v.status &&
[
'UDM',
'UPF',
'AUSF',
'PCF',
'SMF',
'AMF',
'OMC',
'SMSC',
'IMS',
'MME',
].includes(v.neType)
) {
onlineArr.push({
value: v.neType + '_' + v.neId,
label: v.neName,
rmUid: v.rmUid,
});
}
});
onlineOtions.value = onlineArr;
initFlag = true;
fnGetSkim().then(() => {
loadData();
});
});
onBeforeUnmount(() => {
clearInterval(interval10s.value);
interval10s.value = null;
clearInterval(interval5s.value);
interval5s.value = null;
initFlag = false;
});
</script>
<template>
<div class="viewport" ref="viewportDom">
<div class="brand">
<div
class="brand-title"
@click="toggle"
:title="t('views.dashboard.overview.fullscreen')"
>
{{ t('views.dashboard.overview.title') }}
<FullscreenExitOutlined v-if="isFullscreen" />
<FullscreenOutlined v-else />
</div>
<div class="brand-desc">{{ appStore.appName }}</div>
</div>
<div class="column">
<div class="skim panel">
<div class="inner">
<h3 class="leftright">
<span class="title">
<IdcardOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.skim.userTitle') }}
</span>
</h3>
<div class="data">
<div
class="item toRouter"
:title="t('views.dashboard.overview.toRouter')"
v-if="neListStore.fnHasNe(['udm'])"
>
<div @click="fnToRouter('UdmSub_2001')">
<UserOutlined
style="color: #4096ff; margin-right: 8px; font-size: 1.1rem"
/>
{{ skimState.udmSubNum || 0 }}
</div>
<span>
<a-dropdown
:trigger="['click']"
:get-Popup-Container="getPopupContainer"
>
<div class="toDeep-text">
{{ t('views.dashboard.overview.skim.users') }}
<DownOutlined style="margin-left: 12px; font-size: 12px" />
</div>
<template #overlay>
<a-menu @click="fnSelectUDM">
<a-menu-item
v-for="v in udmOtions"
:key="v.value"
:disabled="udmNeId === v.value"
>
{{ v.label }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('ImsSub_2004')"
:title="t('views.dashboard.overview.toRouter')"
style="margin: 0 12px"
v-perms:has="['dashboard:overview:imsUeNum']"
v-if="neListStore.fnHasNe(['ims'])"
>
<div>
<img :src="svgUserIMS" style="width: 18px; margin-right: 8px" />
{{ skimState.imsUeNum || 0 }}
</div>
<span>
{{ t('views.dashboard.overview.skim.imsUeNum') }}
</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('SmfSub_2005')"
:title="t('views.dashboard.overview.toRouter')"
v-perms:has="['dashboard:overview:smfUeNum']"
v-if="neListStore.fnHasNe(['smf'])"
>
<div>
<img :src="svgUserSMF" style="width: 18px; margin-right: 8px" />
{{ skimState.smfUeNum || 0 }}
</div>
<span>
{{ t('views.dashboard.overview.skim.smfUeNum') }}
</span>
</div>
</div>
</div>
</div>
<!--告警统计-->
<div class="alarmType panel">
<div class="inner">
<h3
class="toRouter leftright"
:title="t('views.dashboard.overview.toRouter')"
>
<span class="title" @click="fnToRouter('HistoryAlarm_2097')">
<PieChartOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.alarmTypeBar.alarmSum') }}
</span>
</h3>
<div class="chart">
<AlarnTypeBar />
</div>
</div>
</div>
<!-- 用户行为 -->
<div class="userActivity panel">
<div class="inner">
<h3 class="leftright">
<span class="title">
<WhatsAppOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.userActivity.title') }}
</span>
</h3>
<div class="chart">
<UserActivity />
</div>
</div>
</div>
</div>
<div class="column" style="flex: 4; margin: 1.333rem 0.833rem 0">
<!-- 实时流量 -->
<div class="upfFlow panel">
<div class="inner">
<h3 class="centerStyle">
<span class="title">
<div
class="toRouter"
@click="fnToRouter('GoldTarget_2104')"
:title="t('views.dashboard.overview.toRouter')"
>
<AreaChartOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.upfFlow.title') }}
</div>
&nbsp;&nbsp;&nbsp;&nbsp;
<a-dropdown
:trigger="['click']"
:get-Popup-Container="getPopupContainer"
>
<div class="toDeep-text">
{{
neOtions.find(item => item.value === upfWhoId)?.label ||
'Select UPF'
}}
<DownOutlined style="margin-left: -2px; font-size: 12px" />
</div>
<template #overlay>
<a-menu @click="fnSelectUPF">
<a-menu-item v-for="v in neOtions" :key="v.value">
{{ v.label }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</span>
</h3>
<div class="chart">
<UPFFlow />
</div>
</div>
</div>
<!-- 网络拓扑 -->
<div class="topology panel">
<div class="inner">
<h3
class="toRouter centerStyle"
@click="fnToRouter('TopologyArchitecture_2128')"
:title="t('views.dashboard.overview.toRouter')"
>
<span class="title">
<ApartmentOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.topology.title') }}
</span>
</h3>
<div class="chart">
<Topology />
</div>
</div>
</div>
</div>
<div class="column">
<!-- 基站信息 -->
<div
class="skim panel base"
v-perms:has="['dashboard:overview:gnbBase']"
v-if="neListStore.fnHasNe(['amf'])"
>
<div class="inner">
<h3 class="leftright">
<span class="title">
<GlobalOutlined style="color: #68d8fe" />&nbsp;&nbsp;
{{ t('views.dashboard.overview.skim.nodeBInfo') }}
</span>
</h3>
<div class="data" style="margin-top: 20px">
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'AMF' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<img
:src="svgBase"
style="width: 18px; margin-right: 8px; height: 2rem"
/>
{{ skimState.gNbSumNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.gnbSumBase') }}</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'AMF' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<img
:src="svgBase"
style="width: 18px; margin-right: 8px; height: 2rem"
/>
{{ skimState.gnbNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.gnbBase') }}</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2096', { neType: 'AMF' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<UserOutlined
style="color: #4096ff; margin-right: 8px; font-size: 1.1rem"
/>
{{ skimState.gnbUeNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.gnbUeNum') }}</span>
</div>
</div>
</div>
</div>
<div
class="skim panel base"
v-perms:has="['dashboard:overview:enbBase']"
v-if="neListStore.fnHasNe(['mme'])"
>
<div class="inner">
<h3></h3>
<div class="data" style="margin-top: 40px">
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'MME' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<img
:src="svgBase"
style="width: 18px; margin-right: 8px; height: 2rem"
/>
{{ skimState.eNbSumNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.enbSumBase') }}</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'MME' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<img
:src="svgBase"
style="width: 18px; margin-right: 8px; height: 2rem"
/>
{{ skimState.enbNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.enbBase') }}</span>
</div>
<div
class="item toRouter"
@click="fnToRouter('BaseStation_2007', { neType: 'MME' })"
:title="t('views.dashboard.overview.toRouter')"
>
<div style="align-items: flex-start">
<UserOutlined
style="color: #4096ff; margin-right: 8px; font-size: 1.1rem"
/>
{{ skimState.enbUeNum }}
</div>
<span>{{ t('views.dashboard.overview.skim.enbUeNum') }}</span>
</div>
</div>
</div>
</div>
<!-- 资源情况 -->
<div class="resources panel">
<div class="inner">
<h3 class="resources leftright">
<span class="title">
<DashboardOutlined
style="color: #68d8fe; font-size: 20px"
/>&nbsp;&nbsp;
<div style="margin-left: -3px">
{{ t('views.dashboard.overview.resources.title') }}:
</div>
<a-dropdown
:trigger="['click']"
:get-Popup-Container="getPopupContainer"
>
<div class="toDeep-text">
{{ graphNodeClickID }}
<DownOutlined style="margin-left: -2px; font-size: 12px" />
</div>
<template #overlay>
<a-menu @click="fnSelectNeRe">
<a-menu-item v-for="v in onlineOtions" :key="v.value">
{{ v.label }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</span>
</h3>
<div class="chart">
<NeResources />
</div>
</div>
</div>
<!-- IMS用户行为 -->
<div class="userActivity panel">
<div class="inner">
<h3 class="leftright">
<span class="title">
<WhatsAppOutlined
style="color: #68d8fe; font-size: 20px"
/>&nbsp;&nbsp;
{{ t('views.dashboard.overview.userActivity.imsTitle') }}
</span>
</h3>
<div class="chart">
<IMSActivity />
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
@import url('./css/index.css');
.toDeep {
--editor-background-color: blue;
}
.toDeep :deep(.ant-select-selector) {
background-color: #050f23;
border: none;
}
.toDeep :deep(.ant-select-arrow) {
color: #4c9bfd;
}
.toDeep :deep(.ant-select-selection-item) {
color: #4c9bfd;
}
.toDeep-text {
color: #4c9bfd !important;
font-size: 0.844rem !important;
position: relative !important;
line-height: 2rem !important;
white-space: nowrap !important;
text-align: start !important;
text-overflow: ellipsis !important;
overflow: hidden !important;
}
</style>
+6 -11
View File
@@ -260,14 +260,11 @@ function fnTableSelectedRowKeys(keys: (string | number)[]) {
type ModalStateType = {
/**确定按钮 loading */
confirmLoading: boolean;
/**最大ID值 */
maxId: number;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
confirmLoading: false,
maxId: 0,
});
/**
@@ -369,11 +366,6 @@ function fnGetList(pageNum?: number) {
return item;
});
// 取最大值ID用作实时累加
if (total > 0) {
modalState.maxId = Number(rows[0].id);
}
}
tableState.loading = false;
});
@@ -460,15 +452,18 @@ function wsMessage(res: Record<string, any>) {
// cdrEvent CDR会话事件
if (data.groupId === `1008_${queryParams.neId}`) {
const cdrEvent = data.data;
let cdrJSON = {};
try {
cdrJSON = JSON.parse(cdrEvent.cdrJSON);
} catch (e) {}
queue.add(async () => {
modalState.maxId += 1;
tableState.data.unshift({
id: modalState.maxId,
id: cdrEvent.id,
neType: cdrEvent.neType,
neName: cdrEvent.neName,
rmUID: cdrEvent.rmUID,
timestamp: cdrEvent.timestamp,
cdrJSON: cdrEvent.CDR,
cdrJSON: cdrJSON,
});
tablePagination.total += 1;
if (tableState.data.length > 100) {
+6 -11
View File
@@ -287,14 +287,11 @@ function fnTableSelectedRowKeys(keys: (string | number)[]) {
type ModalStateType = {
/**确定按钮 loading */
confirmLoading: boolean;
/**最大ID值 */
maxId: number;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
confirmLoading: false,
maxId: 0,
});
/**
@@ -396,11 +393,6 @@ function fnGetList(pageNum?: number) {
return item;
});
// 取最大值ID用作实时累加
if (total > 0) {
modalState.maxId = Number(rows[0].id);
}
}
tableState.loading = false;
});
@@ -487,15 +479,18 @@ function wsMessage(res: Record<string, any>) {
// cdrEvent CDR会话事件
if (data.groupId === `1006_${queryParams.neId}`) {
const cdrEvent = data.data;
let cdrJSON = {};
try {
cdrJSON = JSON.parse(cdrEvent.cdrJSON);
} catch (e) {}
queue.add(async () => {
modalState.maxId += 1;
tableState.data.unshift({
id: modalState.maxId,
id: cdrEvent.id,
neType: cdrEvent.neType,
neName: cdrEvent.neName,
rmUID: cdrEvent.rmUID,
timestamp: cdrEvent.timestamp,
cdrJSON: cdrEvent.CDR,
cdrJSON: cdrJSON,
});
tablePagination.total += 1;
if (tableState.data.length > 100) {
+12 -8
View File
@@ -37,6 +37,7 @@ import { parseSizeFromByte } from '@/utils/parse-utils';
import { message } from 'ant-design-vue';
import useNeListStore from '@/store/modules/ne_list';
import dayjs, { Dayjs } from 'dayjs';
import { parseDateToStr } from '@/utils/date-utils';
const { t, currentLocale } = useI18n();
const neListStore = useNeListStore();
const ws = new WS();
@@ -48,7 +49,7 @@ let cdrChart: echarts.ECharts | null = null;
/**图表配置 */
const option = {
title: {
text: 'Data Usage Report',
text: t('views.dashboard.chart.charttitle'),
left: 'left',
},
tooltip: {
@@ -148,12 +149,12 @@ const option = {
],
yAxis: [
{
name: 'Downlink (Byte)',
name: t('views.dashboard.chart.downlink'),
type: 'value',
},
{
gridIndex: 1,
name: 'Uplink (Byte)',
name: t('views.dashboard.chart.uplink'),
type: 'value',
inverse: true,
},
@@ -381,7 +382,7 @@ function fnRanderChartDataLoad() {
break;
}
// 时间
const dataTime = item.cdrJSON.invocationTimestamp;
const dataTime = parseDateToStr(item.cdrJSON.invocationTimestamp);
const listOfMultipleUnitUsage = item.cdrJSON.listOfMultipleUnitUsage;
if (
!Array.isArray(listOfMultipleUnitUsage) ||
@@ -504,6 +505,10 @@ function fnRealTime() {
// cdrEvent CDR会话事件
if (data.groupId === `1006_${queryParams.neId}`) {
const cdrEvent = data.data;
let cdrJSON: any = {};
try {
cdrJSON = JSON.parse(cdrEvent.cdrJSON);
} catch (e) {}
// 对应结束时间内
if (queryParams.endTime) {
const endTime = Math.round(queryParams.endTime / 1000);
@@ -511,7 +516,6 @@ function fnRealTime() {
return;
}
}
const cdrJSON = cdrEvent.CDR;
if (!cdrJSON.invocationTimestamp) {
return;
}
@@ -683,11 +687,11 @@ onBeforeUnmount(() => {
<!-- 图数据 -->
<div ref="cdrChartDom" style="height: 600px; width: 100%"></div>
<a-descriptions title="Data Usage" bordered :column="2">
<a-descriptions-item label="Total Uplink">
<a-descriptions :title="t('views.dashboard.chart.datausage')" bordered :column="2">
<a-descriptions-item :label="t('views.dashboard.chart.totaluplink')">
{{ state.dataUsage[0] }}
</a-descriptions-item>
<a-descriptions-item label="Total Downlink">
<a-descriptions-item :label="t('views.dashboard.chart.totaldownlink')">
{{ state.dataUsage[1] }}
</a-descriptions-item>
</a-descriptions>
+6 -11
View File
@@ -254,14 +254,11 @@ function fnTableSelectedRowKeys(keys: (string | number)[]) {
type ModalStateType = {
/**确定按钮 loading */
confirmLoading: boolean;
/**最大ID值 */
maxId: number;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
confirmLoading: false,
maxId: 0,
});
/**
@@ -363,11 +360,6 @@ function fnGetList(pageNum?: number) {
return item;
});
// 取最大值ID用作实时累加
if (total > 0) {
modalState.maxId = Number(rows[0].id);
}
}
tableState.loading = false;
});
@@ -454,15 +446,18 @@ function wsMessage(res: Record<string, any>) {
// cdrEvent CDR会话事件
if (data.groupId === `1007_${queryParams.neId}`) {
const cdrEvent = data.data;
let cdrJSON = {};
try {
cdrJSON = JSON.parse(cdrEvent.cdrJSON);
} catch (e) {}
queue.add(async () => {
modalState.maxId += 1;
tableState.data.unshift({
id: modalState.maxId,
id: cdrEvent.id,
neType: cdrEvent.neType,
neName: cdrEvent.neName,
rmUID: cdrEvent.rmUID,
timestamp: cdrEvent.timestamp,
cdrJSON: cdrEvent.CDR,
cdrJSON: cdrJSON,
});
tablePagination.total += 1;
if (tableState.data.length > 100) {
+3 -3
View File
@@ -70,7 +70,7 @@ let rangePickerPresets = ref([
/**查询参数 */
let queryParams = reactive({
alarmStatus: 1,
alarmStatus: 'Active',
sortField: 'event_time',
sortOrder: 'desc',
/**告警设备类型 */
@@ -102,7 +102,7 @@ let queryParams = reactive({
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
alarmStatus: 1,
alarmStatus: 'Active',
/**告警设备类型 */
neType: '',
/**告警网元名称 */
@@ -386,7 +386,7 @@ let modalState: ModalStateType = reactive({
objectName: '',
locationInfo: '',
province: '',
alarmStatus: '',
alarmStatus: 'Active',
specificProblemId: '',
specificProblem: '',
addInfo: '',
Binary file not shown.

After

Width:  |  Height:  |  Size: 257 KiB

File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -680,7 +680,7 @@ onMounted(() => {
:label="t('views.faultManage.activeAlarm.eventTime')"
name="eventTime"
>
{{ modalState.from.eventTime }}
{{ parseDateToStr(modalState.from.eventTime) }}
</a-form-item>
</a-col>
</a-row>
@@ -61,7 +61,7 @@ let rangePickerPresets = ref([
/**查询参数 */
let queryParams = reactive({
alarmStatus: 0,
alarmStatus: 'Clear',
sortField: 'event_time',
sortOrder: 'desc',
/**告警设备类型 */
@@ -91,7 +91,7 @@ let queryParams = reactive({
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
alarmStatus: 0,
alarmStatus: 'Clear',
/**告警设备类型 */
neType: '',
/**告警网元名称 */
@@ -317,7 +317,7 @@ let modalState: ModalStateType = reactive({
objectName: '',
locationInfo: '',
province: '',
alarmStatus: '',
alarmStatus: 'Clear',
specificProblemId: '',
specificProblem: '',
addInfo: '',
+8 -10
View File
@@ -6,7 +6,7 @@ import { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/es/table';
import { parseDateToStr } from '@/utils/date-utils';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { listMML } from '@/api/logManage/mml';
import { mmlLogList } from '@/api/tool/mml';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
@@ -16,7 +16,7 @@ let queryRangePicker = ref<[string, string]>(['', '']);
/**查询参数 */
let queryParams = reactive({
/**登录账号 */
accountName: '',
user: '',
/**记录时间 */
beginTime: '',
endTime: '',
@@ -105,8 +105,8 @@ let tableColumns: ColumnsType = reactive([
},
{
title: t('views.logManage.mml.MML'),
dataIndex: 'mml',
key: 'mml',
dataIndex: 'command',
key: 'command',
align: 'left',
ellipsis: true,
},
@@ -157,7 +157,7 @@ function fnGetList(pageNum?: number) {
}
queryParams.beginTime = queryRangePicker.value[0];
queryParams.endTime = queryRangePicker.value[1];
listMML(toRaw(queryParams)).then(res => {
mmlLogList(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
const { total, rows } = res.data;
tablePagination.total = total;
@@ -192,13 +192,11 @@ onMounted(() => {
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="6" :md="12" :xs="24">
<a-form-item
:label="t('views.logManage.mml.account')"
name="accountName"
>
<a-form-item :label="t('views.logManage.mml.account')" name="user">
<a-input
v-model:value="queryParams.accountName"
v-model:value="queryParams.user"
:allow-clear="true"
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
</a-col>
-398
View File
@@ -1,398 +0,0 @@
<script lang="ts" setup>
import svgLight from '@/assets/svg/light.svg';
import svgDark from '@/assets/svg/dark.svg';
import { message } from 'ant-design-vue/es';
import { reactive, onMounted, computed, toRaw } from 'vue';
import useUserStore from '@/store/modules/user';
import useAppStore from '@/store/modules/app';
import { getCaptchaImage } from '@/api/auth';
import { useRouter, useRoute } from 'vue-router';
import useI18n from '@/hooks/useI18n';
import useLayoutStore from '@/store/modules/layout';
import { viewTransitionTheme } from 'antdv-pro-layout';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { parseUrlPath } from '@/plugins/file-static-url';
const { t, changeLocale, optionsLocale } = useI18n();
const layoutStore = useLayoutStore();
const appStore = useAppStore();
const router = useRouter();
const route = useRoute();
let state = reactive({
/**表单属性 */
from: {
/**账号 */
username: '',
/**密码 */
password: '',
/**验证码 */
code: '',
/**验证码uuid */
uuid: '',
},
/**表单提交点击状态 */
fromClick: false,
/**验证码状态 */
captcha: {
/**验证码开关 */
enabled: false,
/**验证码图片地址 */
codeImg: '',
},
/**验证码点击状态 */
captchaClick: false,
});
/**表单验证通过 */
function fnFinish() {
state.fromClick = true;
// 发送请求
useUserStore()
.fnLogin(toRaw(state.from))
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('views.login.loginSuccess'), 1);
/**登录后重定向页面 */
const redirectPath = route.query?.redirect || '/index';
router.push({ path: redirectPath as string });
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
state.fromClick = false;
// 刷新验证码
if (state.captcha.enabled) {
state.from.code = '';
fnGetCaptcha();
}
});
}
/**
* 获取验证码
*/
function fnGetCaptcha() {
if (state.captchaClick) return;
state.captchaClick = true;
getCaptchaImage()
.then(res => {
if (res.code !== RESULT_CODE_SUCCESS) {
message.warning({
content: `${res.msg}`,
duration: 3,
});
return;
}
const { enabled, img, uuid } = res.data;
state.captcha.enabled = Boolean(enabled);
if (state.captcha.enabled) {
state.captcha.codeImg = img;
state.from.uuid = uuid;
}
if (res.data?.text) {
state.from.code = res.data.text;
}
})
.finally(() => {
state.captchaClick = false;
});
}
// 判断是否有背景地址
const calcBG = computed(() => {
const bgURL = parseUrlPath(appStore.loginBackground);
if (bgURL && bgURL !== '#') {
return {
backgroundImage: `url('${bgURL}')`,
backgroundPosition: 'center',
backgroundSize: 'cover',
};
}
return undefined;
});
/**
* 国际化翻译转换
*/
function fnLocale() {
let title = route.meta.title as string;
if (title.indexOf('router.') !== -1) {
title = t(title);
}
appStore.setTitle(title);
}
/**改变主题色 */
function fnClickTheme(e: any) {
viewTransitionTheme(isDarkMode => {
layoutStore.changeConf('theme', isDarkMode ? 'light' : 'dark');
}, e);
}
onMounted(() => {
fnLocale();
fnGetCaptcha();
});
/**改变多语言 */
function fnChangeLocale(e: any) {
changeLocale(e.key);
}
</script>
<template>
<div class="container" :style="calcBG">
<section v-show="!calcBG">
<div class="animation animation1"></div>
<div class="animation animation2"></div>
<div class="animation animation3"></div>
<div class="animation animation4"></div>
<div class="animation animation5"></div>
</section>
<a-card :bordered="false" class="login-card">
<div class="title">
{{ t('common.title') }}
</div>
<a-form :model="state.from" name="stateFrom" @finish="fnFinish">
<a-form-item
name="username"
:rules="[
{
required: true,
min: 2,
max: 30,
message: t('valid.userNamePlease'),
},
]"
>
<a-input
v-model:value="state.from.username"
size="large"
:placeholder="t('valid.userNameHit')"
:maxlength="30"
>
<template #prefix>
<UserOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[
{
required: true,
min: 6,
max: 26,
message: t('valid.passwordPlease'),
},
]"
>
<a-input-password
v-model:value="state.from.password"
size="large"
:placeholder="t('valid.passwordHit')"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-row v-if="state.captcha.enabled">
<a-col :span="16">
<a-form-item
name="code"
:rules="[
{
required: true,
min: 1,
message: t('valid.codePlease'),
},
]"
>
<a-input
v-model:value="state.from.code"
size="large"
:placeholder="t('valid.codeHit')"
:maxlength="6"
>
<template #prefix>
<RobotOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="8">
<a-image
:alt="t('valid.codeHit')"
style="cursor: pointer; border-radius: 2px"
width="100px"
height="40px"
:preview="false"
:src="state.captcha.codeImg"
@click="fnGetCaptcha"
/>
</a-col>
</a-row>
<a-row
justify="space-between"
align="middle"
style="margin-bottom: 16px"
>
<a-col :span="12" v-if="appStore.registerUser">
<a-button
type="link"
target="_self"
:title="t('views.login.registerBtn')"
@click="() => router.push({ name: 'Register' })"
>
{{ t('views.login.registerBtn') }}
</a-button>
</a-col>
</a-row>
<a-button
block
type="primary"
size="large"
html-type="submit"
:loading="state.fromClick"
>
{{ t('views.login.loginBtn') }}
</a-button>
<a-row justify="end" align="middle" style="margin-top: 18px">
<a-col :span="3">
<a-tooltip placement="bottomRight">
<template #title>{{ t('loayouts.rightContent.theme') }}</template>
<a-button type="text" @click="fnClickTheme">
<template #icon>
<img
v-if="layoutStore.proConfig.theme === 'dark'"
:src="svgDark"
class="theme-icon"
/>
<img v-else :src="svgLight" class="theme-icon" />
</template>
</a-button>
</a-tooltip>
</a-col>
<a-col :span="3" v-if="appStore.i18nOpen">
<a-dropdown trigger="click" placement="bottomRight">
<a-button type="text">
<template #icon> <TranslationOutlined /> </template>
</a-button>
<template #overlay>
<a-menu @click="fnChangeLocale">
<a-menu-item v-for="opt in optionsLocale" :key="opt.value">
{{ opt.label }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
</template>
<style lang="less" scoped>
.container {
position: relative;
width: 100%;
min-height: 100%;
padding-top: 164px;
// background: url('./../assets/black_dot.png') 0% 0% / 14px 14px repeat;
background-image: url(/background/light.jpg);
background-repeat: no-repeat;
background-size: cover;
background-position: center center;
.animation {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #1be1f6;
&.animation1 {
left: 15%;
top: 70%;
animation: slashStar 2s ease-in-out 0.3s infinite;
}
&.animation2 {
left: 34%;
top: 35%;
animation: slashStar 2s ease-in-out 1.2s infinite;
}
&.animation3 {
left: 10%;
top: 8%;
animation: slashStar 2s ease-in-out 0.5s infinite;
}
&.animation4 {
left: 68%;
top: 68%;
animation: slashStar 2s ease-in-out 0.8s infinite;
}
&.animation5 {
left: 87%;
top: 30%;
animation: slashStar 2s ease-in-out 1.5s infinite;
}
}
@keyframes slashStar {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
[data-theme='dark'] .container {
background-image: url(/background/dark.jpg);
background-color: #141414;
}
.login-card {
width: 368px;
min-width: 260px;
margin: 0 auto;
margin-left: 60%;
border-radius: 6px;
& .title {
text-align: left;
margin-bottom: 18px;
color: #141414;
font-weight: 600;
font-size: 18px;
}
& .prefix-icon {
color: #8c8c8c;
font-size: 16px;
}
}
[data-theme='dark'] .login-card {
& .title {
color: #999;
}
}
@media (max-width: 992px) {
.login-card {
margin: 0 auto;
}
}
</style>
+339
View File
@@ -0,0 +1,339 @@
<script setup lang="ts">
import svgLight from '@/assets/svg/light.svg';
import svgDark from '@/assets/svg/dark.svg';
import { reactive, toRaw, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { viewTransitionTheme } from 'antdv-pro-layout';
import { useRouter, useRoute } from 'vue-router';
import useI18n from '@/hooks/useI18n';
import useUserStore from '@/store/modules/user';
import useAppStore from '@/store/modules/app';
import useLayoutStore from '@/store/modules/layout';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { getCaptchaImage } from '@/api/auth';
import {
loginSourceState,
fnClickLoginSource,
} from '@/views/login/hooks/useLoginSource';
const { t, changeLocale, optionsLocale } = useI18n();
const layoutStore = useLayoutStore();
const appStore = useAppStore();
const router = useRouter();
const route = useRoute();
let state = reactive({
/**表单属性 */
from: {
/**账号 */
username: '',
/**密码 */
password: '',
/**验证码 */
code: '',
/**验证码uuid */
uuid: '',
},
/**表单提交点击状态 */
fromClick: false,
/**验证码状态 */
captcha: {
/**验证码开关 */
enabled: false,
/**验证码图片地址 */
codeImg: '',
},
/**验证码点击状态 */
captchaClick: false,
});
/**表单验证通过 */
function fnFinish() {
state.fromClick = true;
// 发送请求
useUserStore()
.fnLogin(toRaw(state.from))
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('views.login.loginSuccess'), 1);
/**登录后重定向页面 */
const redirectPath = route.query?.redirect || '/index';
router.push({ path: redirectPath as string });
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
state.fromClick = false;
// 刷新验证码
if (state.captcha.enabled) {
state.from.code = '';
fnGetCaptcha();
}
});
}
/**
* 获取验证码
*/
function fnGetCaptcha() {
if (state.captchaClick) return;
state.captchaClick = true;
getCaptchaImage()
.then(res => {
if (res.code !== RESULT_CODE_SUCCESS) {
message.warning({
content: `${res.msg}`,
duration: 3,
});
return;
}
const { enabled, img, uuid } = res.data;
state.captcha.enabled = Boolean(enabled);
if (state.captcha.enabled) {
state.captcha.codeImg = img;
state.from.uuid = uuid;
}
if (res.data?.text) {
state.from.code = res.data.text;
}
})
.finally(() => {
state.captchaClick = false;
});
}
/**
* 国际化翻译转换
*/
function fnLocale() {
let title = route.meta.title as string;
if (title.indexOf('router.') !== -1) {
title = t(title);
}
appStore.setTitle(title);
}
/**改变主题色 */
function fnClickTheme(e: any) {
viewTransitionTheme(isDarkMode => {
layoutStore.changeConf('theme', isDarkMode ? 'light' : 'dark');
}, e);
}
/**改变多语言 */
function fnChangeLocale(e: any) {
changeLocale(e.key);
}
onMounted(() => {
fnLocale();
fnGetCaptcha();
});
</script>
<template>
<a-card :bordered="false" class="login-card">
<div class="title">
{{ t('common.title') }}
</div>
<a-form :model="state.from" name="stateFrom" @finish="fnFinish">
<a-form-item
name="username"
:rules="[
{
required: true,
min: 2,
max: 30,
message: t('valid.userNamePlease'),
},
]"
>
<a-input
v-model:value="state.from.username"
size="large"
:placeholder="t('valid.userNameHit')"
:maxlength="30"
>
<template #prefix>
<UserOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[
{
required: true,
min: 6,
max: 26,
message: t('valid.passwordPlease'),
},
]"
>
<a-input-password
v-model:value="state.from.password"
size="large"
:placeholder="t('valid.passwordHit')"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-row v-if="state.captcha.enabled">
<a-col :span="16">
<a-form-item
name="code"
:rules="[
{
required: true,
min: 1,
message: t('valid.codePlease'),
},
]"
>
<a-input
v-model:value="state.from.code"
size="large"
:placeholder="t('valid.codeHit')"
:maxlength="6"
>
<template #prefix>
<RobotOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
</a-col>
<a-col :span="8">
<a-image
:alt="t('valid.codeHit')"
style="cursor: pointer; border-radius: 2px"
width="100px"
height="40px"
:preview="false"
:src="state.captcha.codeImg"
@click="fnGetCaptcha"
/>
</a-col>
</a-row>
<a-button
block
type="primary"
size="large"
html-type="submit"
:loading="state.fromClick"
>
{{ t('views.login.loginBtn') }}
</a-button>
<template v-if="loginSourceState.list.length > 0">
<a-divider :plain="true">
{{ t('views.login.otherMethod') }}
</a-divider>
<a-row
justify="center"
align="middle"
:wrap="false"
:gutter="16"
style="margin-top: 18px"
>
<a-col v-for="s in loginSourceState.list" :key="s.uid">
<a-tooltip placement="top">
<template #title>{{ s.name }}</template>
<a-button
type="text"
shape="circle"
@click="fnClickLoginSource(s)"
>
<template #icon>
<a-avatar v-if="s.icon" :src="s.icon" :alt="s.name" />
<a-avatar v-else :alt="s.name">{{ s.name }}</a-avatar>
</template>
</a-button>
</a-tooltip>
</a-col>
</a-row>
</template>
<a-row justify="end" align="middle" style="margin-top: 18px">
<a-col :span="12" v-if="appStore.registerUser">
<a-button
type="link"
target="_self"
:title="t('views.login.registerBtn')"
@click="() => router.push({ name: 'Register' })"
>
{{ t('views.login.registerBtn') }}
</a-button>
</a-col>
<a-col :span="3" v-if="appStore.i18nOpen">
<a-dropdown trigger="click" placement="bottomRight">
<a-button type="text">
<template #icon> <TranslationOutlined /> </template>
</a-button>
<template #overlay>
<a-menu @click="fnChangeLocale">
<a-menu-item v-for="opt in optionsLocale" :key="opt.value">
{{ opt.label }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-col>
<a-col :span="3">
<a-tooltip placement="bottomRight">
<template #title>{{ t('loayouts.rightContent.theme') }}</template>
<a-button type="text" @click="fnClickTheme">
<template #icon>
<img
v-if="layoutStore.proConfig.theme === 'dark'"
:src="svgDark"
class="theme-icon"
/>
<img v-else :src="svgLight" class="theme-icon" />
</template>
</a-button>
</a-tooltip>
</a-col>
</a-row>
</a-form>
</a-card>
</template>
<style lang="less" scoped>
.login-card {
width: 368px;
min-width: 260px;
margin: 0 auto;
margin-left: 60%;
border-radius: 6px;
& .title {
text-align: left;
margin-bottom: 18px;
color: #141414;
font-weight: 600;
font-size: 18px;
}
& .prefix-icon {
color: #8c8c8c;
font-size: 16px;
}
}
[data-theme='dark'] .login-card {
& .title {
color: #999;
}
}
@media (max-width: 992px) {
.login-card {
margin: 0 auto;
}
}
</style>
@@ -0,0 +1,163 @@
<script setup lang="ts">
import { reactive, toRaw } from 'vue';
import { message } from 'ant-design-vue';
import { useRouter, useRoute } from 'vue-router';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { loginLDAP } from '@/api/auth';
import {
loginSourceState,
fnClickLoginBack,
} from '@/views/login/hooks/useLoginSource';
import { setAccessToken, setRefreshToken } from '@/plugins/auth-token';
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
let state = reactive({
/**表单属性 */
from: {
/**账号 */
username: '',
/**密码 */
password: '',
/**认证uid */
uid: loginSourceState.selct.uid,
},
/**表单提交点击状态 */
fromClick: false,
});
/**表单验证通过 */
function fnFinish() {
state.fromClick = true;
// 发送请求
loginLDAP(toRaw(state.from))
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('views.login.loginSuccess'), 1);
setAccessToken(res.data.accessToken, res.data.refreshExpiresIn);
setRefreshToken(res.data.refreshToken, res.data.refreshExpiresIn);
/**登录后重定向页面 */
const redirectPath = route.query?.redirect || '/index';
router.push({ path: redirectPath as string });
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
state.fromClick = false;
});
}
</script>
<template>
<a-card :bordered="false" class="login-card">
<div class="title">
{{ loginSourceState.selct.name }}
</div>
<a-form :model="state.from" name="stateFrom" @finish="fnFinish">
<a-form-item
name="username"
:rules="[
{
required: true,
min: 2,
max: 30,
message: t('valid.userNamePlease'),
},
]"
>
<a-input
v-model:value="state.from.username"
size="large"
:placeholder="t('valid.userNameHit')"
:maxlength="30"
>
<template #prefix>
<UserOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[
{
required: true,
min: 6,
max: 26,
message: t('valid.passwordPlease'),
},
]"
>
<a-input-password
v-model:value="state.from.password"
size="large"
:placeholder="t('valid.passwordHit')"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-button
block
type="primary"
size="large"
html-type="submit"
:loading="state.fromClick"
>
{{ t('views.login.loginBtn') }}
</a-button>
<a-button
block
size="large"
:disabled="state.fromClick"
style="margin-top: 24px"
@click="fnClickLoginBack"
>
{{ t('views.login.backBtn') }}
</a-button>
</a-form>
</a-card>
</template>
<style lang="less" scoped>
.login-card {
width: 368px;
min-width: 260px;
margin: 0 auto;
margin-left: 60%;
border-radius: 6px;
& .title {
text-align: left;
margin-bottom: 18px;
color: #141414;
font-weight: 600;
font-size: 18px;
}
& .prefix-icon {
color: #8c8c8c;
font-size: 16px;
}
}
[data-theme='dark'] .login-card {
& .title {
color: #999;
}
}
@media (max-width: 992px) {
.login-card {
margin: 0 auto;
}
}
</style>
@@ -0,0 +1,163 @@
<script setup lang="ts">
import { reactive, toRaw } from 'vue';
import { message } from 'ant-design-vue';
import { useRouter, useRoute } from 'vue-router';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { loginSMTP } from '@/api/auth';
import {
loginSourceState,
fnClickLoginBack,
} from '@/views/login/hooks/useLoginSource';
import { setAccessToken, setRefreshToken } from '@/plugins/auth-token';
const { t } = useI18n();
const router = useRouter();
const route = useRoute();
let state = reactive({
/**表单属性 */
from: {
/**账号 */
username: '',
/**密码 */
password: '',
/**认证uid */
uid: loginSourceState.selct.uid,
},
/**表单提交点击状态 */
fromClick: false,
});
/**表单验证通过 */
function fnFinish() {
state.fromClick = true;
// 发送请求
loginSMTP(toRaw(state.from))
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('views.login.loginSuccess'), 1);
setAccessToken(res.data.accessToken, res.data.refreshExpiresIn);
setRefreshToken(res.data.refreshToken, res.data.refreshExpiresIn);
/**登录后重定向页面 */
const redirectPath = route.query?.redirect || '/index';
router.push({ path: redirectPath as string });
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
state.fromClick = false;
});
}
</script>
<template>
<a-card :bordered="false" class="login-card">
<div class="title">
{{ loginSourceState.selct.name }}
</div>
<a-form :model="state.from" name="stateFrom" @finish="fnFinish">
<a-form-item
name="username"
:rules="[
{
required: true,
min: 2,
max: 30,
message: t('valid.userNamePlease'),
},
]"
>
<a-input
v-model:value="state.from.username"
size="large"
:placeholder="t('valid.userNameHit')"
:maxlength="30"
>
<template #prefix>
<UserOutlined class="prefix-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[
{
required: true,
min: 6,
max: 26,
message: t('valid.passwordPlease'),
},
]"
>
<a-input-password
v-model:value="state.from.password"
size="large"
:placeholder="t('valid.passwordHit')"
:maxlength="26"
>
<template #prefix>
<LockOutlined class="prefix-icon" />
</template>
</a-input-password>
</a-form-item>
<a-button
block
type="primary"
size="large"
html-type="submit"
:loading="state.fromClick"
>
{{ t('views.login.loginBtn') }}
</a-button>
<a-button
block
size="large"
:disabled="state.fromClick"
style="margin-top: 24px"
@click="fnClickLoginBack"
>
{{ t('views.login.backBtn') }}
</a-button>
</a-form>
</a-card>
</template>
<style lang="less" scoped>
.login-card {
width: 368px;
min-width: 260px;
margin: 0 auto;
margin-left: 60%;
border-radius: 6px;
& .title {
text-align: left;
margin-bottom: 18px;
color: #141414;
font-weight: 600;
font-size: 18px;
}
& .prefix-icon {
color: #8c8c8c;
font-size: 16px;
}
}
[data-theme='dark'] .login-card {
& .title {
color: #999;
}
}
@media (max-width: 992px) {
.login-card {
margin: 0 auto;
}
}
</style>
+82
View File
@@ -0,0 +1,82 @@
import { getLoginSource, loginOAuth2URL } from '@/api/auth';
import { parseUrlPath } from '@/plugins/file-static-url';
import { defineAsyncComponent, nextTick, reactive, shallowRef } from 'vue';
// 异步加载组件
const cardLogin = defineAsyncComponent(
() => import('../components/CardLogin.vue')
);
// 当前组件
export const currentComponent = shallowRef(cardLogin);
/**登录认证源类型 */
type LoginSourceType = {
/**认证标识 */
uid: string;
/**名称 */
name: string;
/**类型 */
type: string;
/**图标 */
icon: string;
};
/**登录认证源 */
export const loginSourceState = reactive({
list: [] as LoginSourceType[],
selct: {
uid: '',
name: '',
type: '',
icon: '',
},
});
/**获取登录认证源 */
export function fnGetLoginSource() {
getLoginSource().then(res => {
loginSourceState.list = res.data.map((item: Record<string, string>) => ({
...item,
icon: parseUrlPath(item.icon),
}));
});
}
/**点击登录认证源 */
export function fnClickLoginSource(item: LoginSourceType) {
loginSourceState.selct = { ...item };
let loadComponent: any = undefined;
switch (item.type) {
case 'LDAP':
loadComponent = defineAsyncComponent(
() => import('../components/CardLoginForLDAP.vue')
);
break;
case 'SMTP':
loadComponent = defineAsyncComponent(
() => import('../components/CardLoginForSMTP.vue')
);
break;
case 'OAuth2':
const redirectUri = loginOAuth2URL(item.uid);
window.location.href = redirectUri;
// window.open(redirectUri, '_blank');
break;
default:
fnClickLoginBack();
}
if (loadComponent) {
nextTick(() => {
currentComponent.value = loadComponent;
});
}
}
/**点击登录返回 */
export function fnClickLoginBack() {
nextTick(() => {
currentComponent.value = cardLogin;
});
}
+105
View File
@@ -0,0 +1,105 @@
<script lang="ts" setup>
import { computed, onMounted } from 'vue';
import useAppStore from '@/store/modules/app';
import { parseUrlPath } from '@/plugins/file-static-url';
import {
currentComponent,
fnGetLoginSource,
} from '@/views/login/hooks/useLoginSource';
const appStore = useAppStore();
// 判断是否有背景地址
const calcBG = computed(() => {
const bgURL = parseUrlPath(appStore.loginBackground);
if (bgURL && bgURL !== '#') {
return {
backgroundImage: `url('${bgURL}')`,
backgroundPosition: 'center',
backgroundSize: 'cover',
};
}
return undefined;
});
onMounted(() => {
// 获取登录认证源
fnGetLoginSource();
});
</script>
<template>
<div class="container" :style="calcBG">
<section v-show="!calcBG">
<div class="animation animation1"></div>
<div class="animation animation2"></div>
<div class="animation animation3"></div>
<div class="animation animation4"></div>
<div class="animation animation5"></div>
</section>
<component :is="currentComponent" />
</div>
</template>
<style lang="less" scoped>
.container {
position: relative;
width: 100%;
min-height: 100%;
padding-top: 164px;
// background: url('./../assets/black_dot.png') 0% 0% / 14px 14px repeat;
background-image: url(/background/light.jpg);
background-repeat: no-repeat;
background-size: cover;
background-position: center center;
.animation {
position: absolute;
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #1be1f6;
&.animation1 {
left: 15%;
top: 70%;
animation: slashStar 2s ease-in-out 0.3s infinite;
}
&.animation2 {
left: 34%;
top: 35%;
animation: slashStar 2s ease-in-out 1.2s infinite;
}
&.animation3 {
left: 10%;
top: 8%;
animation: slashStar 2s ease-in-out 0.5s infinite;
}
&.animation4 {
left: 68%;
top: 68%;
animation: slashStar 2s ease-in-out 0.8s infinite;
}
&.animation5 {
left: 87%;
top: 30%;
animation: slashStar 2s ease-in-out 1.5s infinite;
}
}
@keyframes slashStar {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
[data-theme='dark'] .container {
background-image: url(/background/dark.jpg);
background-color: #141414;
}
</style>
+85
View File
@@ -0,0 +1,85 @@
<script setup lang="ts">
import { onMounted, reactive } from 'vue';
import { useRouter } from 'vue-router';
import useI18n from '@/hooks/useI18n';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import {
delAccessToken,
delRefreshToken,
setAccessToken,
setRefreshToken,
} from '@/plugins/auth-token';
import { loginOAuth2Token } from '@/api/auth';
const router = useRouter();
const { t } = useI18n();
const info = reactive({
status: 'warning' as 'error' | 'warning' | 'success',
title: t('views.login.authorizedNotfound'),
subTitle: '',
});
function fnToLogin() {
router.replace({ name: 'Login' }).finally(() => {
delAccessToken();
delRefreshToken();
location.replace(location.origin);
});
}
onMounted(() => {
// 从URL参数中获取code和state
const params = new URLSearchParams(location.search);
const code = params.get('code');
const state = params.get('state');
if (code && state) {
loginOAuth2Token(code, state).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
info.status = 'success';
info.title = t('views.login.authorizedSuccess');
info.subTitle = t('views.login.redirectHome', { i: 3 });
setAccessToken(res.data.accessToken, res.data.refreshExpiresIn);
setRefreshToken(res.data.refreshToken, res.data.refreshExpiresIn);
// 3秒后跳转主页
let i = 2;
const timer = setInterval(() => {
info.subTitle = t('views.login.redirectHome', { i });
i--;
if (i <= -1) {
clearInterval(timer);
/**登录后跳转主页 */
router.replace({ name: 'Index' }).finally(() => {
location.replace(location.origin);
});
}
}, 1000);
} else {
info.status = 'error';
info.title = t('views.login.authorizedFailed');
info.subTitle = res.msg;
}
});
} else {
// info.subTitle = "未获取到code和state参数";
info.subTitle = "code and state parameters not obtained";
}
});
</script>
<template>
<a-result
:status="info.status"
:title="info.title"
v-model:subTitle="info.subTitle"
>
<template #extra>
<a-button
type="default"
v-if="info.status !== 'success'"
@click="fnToLogin"
>
{{ t('views.login.backBtnLogin') }}
</a-button>
</template>
</a-result>
</template>
+11 -8
View File
@@ -48,7 +48,7 @@ let state: StateType = reactive({
key: '',
operation: '',
object: '',
objectType: 'mml',
objectType: 'General',
param: [],
},
from: {
@@ -103,7 +103,7 @@ function fnCleanFrom() {
key: '',
operation: '',
object: '',
objectType: 'mml',
objectType: 'General',
param: [],
};
state.from = {};
@@ -171,6 +171,9 @@ function fnSendMML() {
}
cmdArr = value.split(';');
}
if (cmdArr.length === 1 && cmdArr[0] === '') {
return;
}
// 发送
state.from.sendLoading = true;
@@ -342,7 +345,7 @@ function fnNeChange(keys: any, _: any) {
key: '',
operation: '',
object: '',
objectType: 'mml',
objectType: 'General',
param: {},
};
fnGetList();
@@ -354,12 +357,12 @@ function fnGetList() {
const neType = state.neType[0];
state.mmlNeType = neType;
getMMLByNE(neType).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
// 构建自动完成筛选结构
const autoCompleteArr: Record<string, any>[] = [];
// 构建树结构
const treeArr: Record<string, any>[] = [];
for (const item of res.data) {
for (const item of res.data.rows) {
const id = item['id'];
const object = item['object'];
const objectType = item['objectType'];
@@ -540,7 +543,7 @@ onMounted(() => {
// 获取网元网元列表
neCascaderOptions.value = neListStore.getNeCascaderOptions.filter(
(item: any) => {
return !['OMC'].includes(item.value); // 过滤不可用的网元
return !['OMC', 'CBC', 'SGWC'].includes(item.value); // 过滤不可用的网元
}
);
if (neCascaderOptions.value.length === 0) {
@@ -680,10 +683,10 @@ onMounted(() => {
v-model:value="state.mmlSelect.objectType"
style="width: 20%"
>
<a-select-option value="mml">
<a-select-option value="General">
{{ t('views.mmlManage.neOperate.mml') }}
</a-select-option>
<a-select-option value="mml2">
<a-select-option value="Standard">
{{ t('views.mmlManage.neOperate.mml2') }}
</a-select-option>
</a-select>
+2 -2
View File
@@ -342,12 +342,12 @@ function ruleVerification(
/**查询可选命令列表 */
function fnGetList() {
getMMLByUDM().then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
// 构建自动完成筛选结构
const autoCompleteArr: Record<string, any>[] = [];
// 构建树结构
const treeArr: Record<string, any>[] = [];
for (const item of res.data) {
for (const item of res.data.rows) {
const id = item['id'];
const object = item['object'];
const operation = item['operation'];
@@ -0,0 +1,85 @@
import { delNeConfigData } from '@/api/ne/neConfig';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { message } from 'ant-design-vue';
import { reactive, toRaw } from 'vue';
/**
* 批量删除array
* @param param 父级传入 { t, neTypeSelect, fnActiveConfigNode }
* @returns
*/
export default function useArrayBatch({
t,
neTypeSelect,
fnActiveConfigNode,
}: any) {
/**状态属性 */
const batchState = reactive({
open: false,
loading: false, //批量删除
paramName: '',
startIndex: 1,
num: 1,
});
/**对话框表格信息导入弹出窗口 */
function modalBatchOpen(paramName: string) {
batchState.paramName = paramName;
batchState.open = true;
}
function modalBatchClose() {
if (batchState.loading) {
message.error({
content: 'Delete is in progress, please wait for it to complete',
duration: 3,
});
return;
}
batchState.open = false;
batchState.loading = false;
batchState.startIndex = 1;
batchState.num = 1;
fnActiveConfigNode('#');
}
async function modalBatchOk() {
let okNum = 0;
let failNum = 0;
const endIndex = batchState.startIndex + batchState.num - 1;
for (let i = endIndex; i >= batchState.startIndex; i--) {
const res = await delNeConfigData({
neType: neTypeSelect.value[0],
neId: neTypeSelect.value[1],
paramName: batchState.paramName,
loc: `${i}`,
});
if (res.code === RESULT_CODE_SUCCESS) {
okNum++;
} else {
failNum++;
break;
}
}
if (okNum > 0) {
message.success({
content: `Successfully deleted ${okNum} items`,
duration: 3,
});
}
if (failNum > 0) {
message.error({
content: `Delete failed, please check the index range`,
duration: 3,
});
}
modalBatchClose();
}
return {
batchState,
modalBatchOpen,
modalBatchClose,
modalBatchOk,
};
}
@@ -0,0 +1,208 @@
import { addNeConfigData, editNeConfigData } from '@/api/ne/neConfig';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { readSheet } from '@/utils/execl-utils';
import { message } from 'ant-design-vue';
import { reactive } from 'vue';
import saveAs from 'file-saver';
/**
* 导入文件加array
* @param param 父级传入 { t, neTypeSelect, arrayState, fnActiveConfigNode }
* @returns
*/
export default function useArrayImport({
t,
neTypeSelect,
arrayState,
fnActiveConfigNode,
}: any) {
/**网元导入模板解析 */
const m: Record<string, any> = {
AMF: {
imeiWhitelist: {
filename: 'import_amf_imeiWhitelist_template',
fileetx: '.xlsx',
itemKey: 'index',
item: (row: Record<string, any>) => {
const index = row['Index'] || 0;
return {
imeiPrefixValue: `${row['IMEI Prefix']}`,
index: parseInt(index),
};
},
},
whitelist: {
filename: 'import_amf_whitelist_template',
fileetx: '.xlsx',
itemKey: 'index',
item: (row: Record<string, any>) => {
const index = row['Index'] || 0;
return {
imsiValue: `${row['IMSI Value']}`,
imeiValue: `${row['IMEI Value/Prefix']}`,
index: parseInt(index),
};
},
},
},
MME: {
white_list: {
filename: 'import_mme_imeiWhitelist_template',
fileetx: '.xlsx',
itemKey: 'index',
item: (row: Record<string, any>) => {
const index = row['Index'] || 0;
return {
imei: `${row['IMEI']}`,
index: parseInt(index),
};
},
},
},
};
/**状态属性 */
const importState = reactive({
open: false,
msgArr: [] as string[],
loading: false, //开始导入
itemKey: '', // 解析item的key
item: null as any, // 解析item方法
paramName: '',
filename: '',
fileetx: '',
});
/**对话框表格信息导入弹出窗口 */
function modalImportOpen(neType: string, paramName: string) {
const tmpM = m[neType][paramName];
importState.itemKey = tmpM.itemKey;
importState.item = tmpM.item;
importState.paramName = paramName;
importState.filename = tmpM.filename;
importState.fileetx = tmpM.fileetx;
importState.open = true;
}
function modalImportClose() {
if (importState.loading) {
message.error({
content: 'Import is in progress, please wait for it to complete',
duration: 3,
});
return;
}
importState.open = false;
importState.msgArr = [];
importState.loading = false;
fnActiveConfigNode('#');
}
/**对话框表格信息导入上传 */
async function modalImportUpload(file: File) {
const hide = message.loading(t('common.loading'), 0);
const [neType, neId] = neTypeSelect.value;
importState.msgArr = [];
// 获取最大index
let index = 0;
if (arrayState.columnsData.length <= 0) {
index = 0;
} else {
const last = arrayState.columnsData[arrayState.columnsData.length - 1];
index = last.index.value + 1;
}
const reader = new FileReader();
reader.onload = function (e: any) {
const arrayBuffer = e.target.result;
readSheet(arrayBuffer).then(async rows => {
if (rows.length <= 0) {
hide();
message.error({
content: t('views.neData.baseStation.importDataEmpty'),
duration: 3,
});
return;
}
// 开始导入
importState.loading = true;
for (const row of rows) {
const rowItem = importState.item(row);
const rowKey = rowItem[importState.itemKey] || -1;
let result: any = null;
// 检查index是否定义
const has = arrayState.columnsData.find(
(item: any) => item[importState.itemKey].value === rowKey
);
if (has) {
// 已定义则更新
rowItem.index = has.index.value;
result = await editNeConfigData({
neType: neType,
neId: neId,
paramName: importState.paramName,
paramData: rowItem,
loc: `${rowItem.index}`,
});
let msg = `index:${rowItem.index} update fail`;
if (result.code === RESULT_CODE_SUCCESS) {
msg = `index:${rowItem.index} update success`;
}
importState.msgArr.push(msg);
} else {
// 未定义则新增
result = await addNeConfigData({
neType: neType,
neId: neId,
paramName: importState.paramName,
paramData: Object.assign(rowItem, { index }),
loc: `${index}`,
});
let msg = `index:${index} add fail`;
if (result.code === RESULT_CODE_SUCCESS) {
msg = `index:${index} add success`;
index += 1;
}
importState.msgArr.push(msg);
}
}
hide();
importState.loading = false;
});
};
reader.onerror = function (e) {
hide();
console.error('reader file error:', e);
};
reader.readAsArrayBuffer(file);
}
/**对话框表格信息导入模板 */
function modalImportTemplate() {
const hide = message.loading(t('common.loading'), 0);
const baseUrl = import.meta.env.VITE_HISTORY_BASE_URL;
const templateUrl = `${
baseUrl.length === 1 && baseUrl.indexOf('/') === 0
? ''
: baseUrl.indexOf('/') === -1
? '/' + baseUrl
: baseUrl
}/neDataImput`;
saveAs(
`${templateUrl}/${importState.filename}${importState.fileetx}`,
`${importState.filename}_${Date.now()}${importState.fileetx}`
);
hide();
}
return {
importState,
modalImportOpen,
modalImportClose,
modalImportUpload,
modalImportTemplate,
};
}
+160 -11
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { reactive, ref, onMounted, toRaw, watch } from 'vue';
import { useRoute } from 'vue-router';
import { PageContainer } from 'antdv-pro-layout';
import { ProModal } from 'antdv-pro-modal';
import { message } from 'ant-design-vue/es';
@@ -12,9 +13,12 @@ import useOptions from './hooks/useOptions';
import useConfigList from './hooks/useConfigList';
import useConfigArray from './hooks/useConfigArray';
import useConfigArrayChild from './hooks/useConfigArrayChild';
import useArrayImport from './hooks/useArrayImport';
import useArrayBatchDel from './hooks/useArrayBatchDel';
import { getAllNeConfig, getNeConfigData } from '@/api/ne/neConfig';
const neListStore = useNeListStore();
const { t } = useI18n();
const route = useRoute();
const { ruleVerification, smfByUPFIdLoadData, smfByUPFIdOptions } = useOptions({
t,
});
@@ -219,16 +223,26 @@ function fnGetNeConfig() {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
const arr = [];
for (const item of res.data) {
// 是否可见
if (!item['visible']) {
item['visible'] = 'public';
} else if (item['visible'] == 'hide') {
continue;
}
// 权限控制
let paramPerms: string[] = [];
if (item.paramPerms) {
paramPerms = item.paramPerms.split(',');
} else {
paramPerms = ['post', 'put', 'delete'];
}
const title = item.paramDisplay;
// 处理字符串开头特殊字符
item.paramDisplay = title.replace(/[└─]+/, '');
arr.push({
...item,
children: undefined,
title: item.paramDisplay,
title: title,
key: item.paramName,
paramPerms,
});
@@ -362,6 +376,21 @@ const {
arrayEditClose,
});
const {
importState,
modalImportOpen,
modalImportClose,
modalImportUpload,
modalImportTemplate,
} = useArrayImport({ t, neTypeSelect, arrayState, fnActiveConfigNode });
const { batchState, modalBatchOpen, modalBatchClose, modalBatchOk } =
useArrayBatchDel({
t,
neTypeSelect,
fnActiveConfigNode,
});
onMounted(() => {
// 获取网元网元列表
neCascaderOptions.value = neListStore.getNeCascaderOptions.filter(
@@ -377,10 +406,20 @@ onMounted(() => {
return;
}
// 默认选择AMF
const item = neCascaderOptions.value.find(s => s.value === 'AMF');
const queryNeType = (route.query.neType as string) || 'AMF';
const queryNeId = (route.query.neId as string) || '001';
const item = neCascaderOptions.value.find(s => s.value === queryNeType);
if (item && item.children) {
const info = item.children[0];
const info = item.children.find((s: any) => s.neId === queryNeId);
if (info) {
neTypeSelect.value = [info.neType, info.neId];
} else {
// 默认取第一个网元ID
const info = item.children[0];
if (info) {
neTypeSelect.value = [info.neType, info.neId];
}
}
} else {
const info = neCascaderOptions.value[0].children[0];
neTypeSelect.value = [info.neType, info.neId];
@@ -428,7 +467,7 @@ onMounted(() => {
<a-card
size="small"
:bordered="false"
:body-style="{ maxHeight: '600px', 'overflow-y': 'auto' }"
:body-style="{ maxHeight: '680px', 'overflow-y': 'auto' }"
:loading="treeState.selectLoading"
>
<template #title>
@@ -575,9 +614,11 @@ onMounted(() => {
@dblclick="listEdit(record)"
>
<template v-if="record['type'] === 'enum'">
{{ JSON.parse(record['filter'])[text] || '&nbsp;' }}
{{ JSON.parse(record['filter'])[text] }}
</template>
<template v-else>{{ `${text}` || '&nbsp;' }}</template>
<template v-else>{{ `${text}` }}</template>
&nbsp;
<!-- 空格占位 -->
<EditOutlined
class="editable-cell__icon"
@click="listEdit(record)"
@@ -604,7 +645,7 @@ onMounted(() => {
:size="arrayState.size"
:pagination="tablePagination"
:bordered="true"
:scroll="{ x: arrayState.columnsDnd.length * 200, y: 480 }"
:scroll="{ x: arrayState.columnsDnd.length * 200, y: '500px' }"
@resizeColumn="(w:number, col:any) => (col.width = w)"
:show-expand-column="false"
v-model:expanded-row-keys="arrayState.arrayChildExpandKeys"
@@ -623,10 +664,41 @@ onMounted(() => {
</a-button>
<TableColumnsDnd
type="ghost"
:cache-id="treeState.selectNode.key"
:columns="treeState.selectNode.paramPerms.includes('get') ? [...arrayState.columns.filter((s:any)=>s.key !== 'index')] : arrayState.columns"
v-model:columns-dnd="arrayState.columnsDnd"
></TableColumnsDnd>
<!-- 特殊导入删除-->
<template
v-if="
['AMF', 'MME'].includes(neTypeSelect[0]) &&
['white_list', 'imeiWhitelist', 'whitelist'].includes(
treeState.selectNode.paramName
)
"
>
<a-button
@click.prevent="
modalImportOpen(
neTypeSelect[0],
treeState.selectNode.paramName
)
"
size="small"
>
<template #icon><ImportOutlined /></template>
{{ t('common.import') }}
</a-button>
<a-button
danger
@click.prevent="
modalBatchOpen(treeState.selectNode.paramName)
"
size="small"
>
<template #icon><DeleteOutlined /></template>
{{ t('views.neData.common.batchDelText') }}
</a-button>
</template>
</a-space>
</template>
@@ -698,7 +770,7 @@ onMounted(() => {
{{ JSON.parse(text['filter'])[text.value] }}
</template>
<template v-else>
{{ `${text.value}` || '&nbsp;' }}
{{ `${text.value}` }}
</template>
</div>
</div>
@@ -734,7 +806,6 @@ onMounted(() => {
</a-button>
<TableColumnsDnd
type="ghost"
:cache-id="`${treeState.selectNode.key}:${arrayChildState.loc}`"
:columns="[...arrayChildState.columns]"
v-model:columns-dnd="arrayChildState.columnsDnd"
v-if="arrayChildState.loc"
@@ -784,7 +855,7 @@ onMounted(() => {
{{ JSON.parse(text['filter'])[text.value] }}
</template>
<template v-else>
{{ `${text.value}` || '&nbsp;' }}
{{ `${text.value}` }}
</template>
</div>
</div>
@@ -904,6 +975,84 @@ onMounted(() => {
</a-form-item>
</a-form>
</ProModal>
<!-- 上传导入表格数据文件框 -->
<UploadModal
:title="t('common.import')"
@upload="modalImportUpload"
@close="modalImportClose"
v-model:open="importState.open"
:ext="['.xls', '.xlsx']"
:size="10"
>
<template #default>
<a-row justify="space-between" align="middle">
<a-col :span="12"> </a-col>
<a-col :span="6">
<a-button
type="link"
:title="t('views.system.user.downloadObj')"
@click.prevent="modalImportTemplate"
>
{{ t('views.system.user.downloadObj') }}
</a-button>
</a-col>
</a-row>
<a-textarea
:disabled="true"
:hidden="importState.msgArr.length <= 0"
:value="importState.msgArr.join('\r\n')"
:auto-size="{ minRows: 2, maxRows: 8 }"
style="background-color: transparent; color: rgba(0, 0, 0, 0.85)"
/>
</template>
</UploadModal>
<!-- 批量删除框 -->
<ProModal
:drag="true"
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
:open="batchState.open"
:title="t('views.neData.common.batchDelText')"
:confirm-loading="batchState.loading"
@ok="modalBatchOk"
@cancel="modalBatchClose"
>
<a-form
name="batchStateForm"
:model="batchState"
layout="horizontal"
:label-col="{ span: 6 }"
:labelWrap="true"
>
<a-row>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item label="Start Index" name="startIndex" required>
<a-input-number
v-model:value="batchState.startIndex"
style="width: 100%"
allow-clear
:min="0"
:maxlength="5"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item label="Num" name="num" required>
<a-input-number
v-model:value="batchState.num"
style="width: 100%"
:min="1"
:maxlength="5"
placeholder="<=500"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
</a-form>
</ProModal>
</PageContainer>
</template>
@@ -399,6 +399,7 @@ onMounted(() => {
v-model:value="modalState.from.neType"
:options="NE_TYPE_LIST.map(v => ({ value: v }))"
@change="fnNeTypeChange"
:disabled="!!modalState.from.id"
>
<a-input
allow-clear
@@ -451,6 +452,7 @@ onMounted(() => {
:placeholder="t('common.inputPlease')"
:maxlength="32"
@change="fnNeIdChange"
:disabled="!!modalState.from.id"
>
<template #prefix>
<a-tooltip placement="topLeft">
+1 -1
View File
@@ -2,7 +2,7 @@ import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { Modal, message } from 'ant-design-vue/es';
import useI18n from '@/hooks/useI18n';
import { useRouter } from 'vue-router';
import { updateNeConfigReload } from '@/api/configManage/configParam';
import { updateNeConfigReload } from '@/api/tool/mml';
import { serviceNeAction } from '@/api/ne/neInfo';
import useMaskStore from '@/store/modules/mask';
+1 -6
View File
@@ -588,12 +588,7 @@ onMounted(() => {
<CloseSquareOutlined />
{{ t('views.ne.common.stop') }}
</a-menu-item>
<a-menu-item
key="reload"
v-if="
!['OMC', 'PCF', 'IMS', 'MME'].includes(record.neType)
"
>
<a-menu-item key="reload" v-if="false">
<SyncOutlined />
{{ t('views.ne.common.reload') }}
</a-menu-item>
@@ -165,6 +165,21 @@ watch(
placeholder="1-65535"
></a-input>
</a-form-item>
<a-form-item label="UE_POOL" name="external.ue_pool">
<a-input
v-model:value="fromState.external.ue_pool"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> UE IP and mask </template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
</a-col>
@@ -244,28 +259,6 @@ watch(
</a-select>
</a-form-item>
</a-col>
<a-col
:lg="12"
:md="12"
:xs="24"
v-if="fromState.external.upf_type === 'LightUPF'"
>
<a-form-item label="UE_POOL" name="external.ue_pool">
<a-input
v-model:value="fromState.external.ue_pool"
allow-clear
:placeholder="t('common.inputPlease')"
:maxlength="50"
>
<template #prefix>
<a-tooltip placement="topLeft">
<template #title> UE IP and maxk </template>
<InfoCircleOutlined style="opacity: 0.45; color: inherit" />
</a-tooltip>
</template>
</a-input>
</a-form-item>
</a-col>
<a-col
:lg="12"
:md="12"
+1 -1
View File
@@ -380,7 +380,7 @@ async function fnGraphDataBase() {
edges: [],
};
// 添加基础网元
for (const item of neListStore.getNeSelectOtions) {
for (const item of neListStore.getNeCascaderOptions) {
if ('OMC' === item.value) {
if (item.children?.length === 0) continue;
// 是否存在OMC保证唯一
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff. Show more