87 Commits

Author SHA1 Message Date
TsMask
33a8ce97d3 chore: 更新版本号 2.241102 2024-11-02 15:47:18 +08:00
TsMask
6ee9d464fb feat: PCF导出有取消操作 2024-11-02 15:46:26 +08:00
TsMask
df5072bae7 fix: CDR-IMS显示呼叫-挂断时间 2024-11-02 15:46:05 +08:00
zhongzm
e12dce1f0f feat:网元指标添加其他指标选项 优化样式 2024-10-31 18:35:26 +08:00
TsMask
d0457fc285 fix: UDM鉴权签约用户勾选导出 2024-10-31 16:32:14 +08:00
zhongzm
63d32f0a39 feat:自定义网元指标概览 2024-10-31 10:30:16 +08:00
TsMask
cf5d08aaab chore: 更新版本号 2.241028 2024-10-28 16:53:21 +08:00
TsMask
7ad566d74f fix: 网元总览接口变更 2024-10-28 16:52:41 +08:00
TsMask
c312186d91 fix: UDM签约数据参数类型转换字符串参数 2024-10-28 16:52:05 +08:00
lai
72fd372fe0 增加关闭按钮 2024-10-24 10:40:54 +08:00
lai
acdadcbb6f 增加Network Function IPv4地址 2024-10-24 10:40:20 +08:00
lai
7a49de71ea 修复 修改Event Type的label 2024-10-24 10:38:28 +08:00
zhongzm
56e4419e77 Revert "Revert "fix:中英提示修复""
This reverts commit 3abb4dd4bd.
2024-10-24 10:12:51 +08:00
zhongzm
3abb4dd4bd Revert "fix:中英提示修复"
This reverts commit a45243390b.
2024-10-24 10:12:09 +08:00
zhongzm
726a284ab5 Merge remote-tracking branch 'origin/main' 2024-10-24 10:11:28 +08:00
zhongzm
a45243390b fix:中英提示修复 2024-10-24 10:11:08 +08:00
TsMask
1faed9bc3d style: 关键指标概览页面占位 2024-10-23 18:48:29 +08:00
TsMask
9bd700eeb7 feat: PCF补充增加online和offline字段 2024-10-23 10:44:15 +08:00
zhongzm
5cc3b9c8cf fix:css样式报错修复 2024-10-22 18:54:35 +08:00
zhongzm
208895c7d5 feat:快速布局功能以及ws连接修复 2024-10-22 16:01:53 +08:00
TsMask
46578ce97b feat: 快速开站SMSC的IP赋值 2024-10-18 20:16:06 +08:00
TsMask
0ff5bd5e20 style: UDM用户数据根据网元类型变更刷新列表 2024-10-18 11:42:21 +08:00
TsMask
f08e637e69 fix: 版权信息文本长度128 2024-10-18 11:34:37 +08:00
TsMask
a600e056b8 chore: 更新版本号 2.241018 2024-10-18 10:36:06 +08:00
zhongzm
671c80972e fix:多选改checkbox 2024-10-18 10:32:55 +08:00
TsMask
d07230b582 Merge remote-tracking branch 'origin/main' into lichang 2024-10-18 10:20:54 +08:00
TsMask
35c24407ac Merge remote-tracking branch 'origin/lichang' 2024-10-18 10:18:48 +08:00
TsMask
cf33756548 Merge remote-tracking branch 'origin/main' into lichang 2024-10-18 10:17:53 +08:00
TsMask
1ef98298bc style: 移除port/dbinfo/capability属性信息 2024-10-17 19:55:09 +08:00
zhongzm
b1c2a95ec4 Merge remote-tracking branch 'origin/lichang' into lichang 2024-10-17 19:54:55 +08:00
zhongzm
147b2fad8d fix:网元响应式数组添加防抖 2024-10-17 19:54:40 +08:00
TsMask
b629088406 Merge remote-tracking branch 'origin/lichang' 2024-10-17 19:51:43 +08:00
TsMask
430a067280 Merge remote-tracking branch 'origin/main' into lichang 2024-10-17 18:27:09 +08:00
TsMask
8a71e8f773 Merge remote-tracking branch 'origin/lichang' 2024-10-17 18:26:49 +08:00
lai
ff556ce1ec 添加首页加载状态 2024-10-17 18:18:37 +08:00
zhongzm
9ed7aed4b4 feat:自定义布局保存,WS数据追加 2024-10-17 18:07:39 +08:00
TsMask
9e14297488 Merge remote-tracking branch 'origin/main' into lichang 2024-10-17 15:55:15 +08:00
TsMask
8e70706ed5 fix: 网元日志实时查看组件参数调整 2024-10-17 15:07:04 +08:00
TsMask
3e0529cf87 fix: 终端SSH视图组件调整参数配置外部地址 2024-10-17 15:06:34 +08:00
TsMask
91af2bed92 feat: 工具iperf/ping功能页面 2024-10-17 15:05:21 +08:00
lai
1ecefb91dc 增加导出时携带完整搜索条件 2024-10-17 14:51:55 +08:00
TsMask
72d9895902 fix: UDM用户数据按查询条件导出 2024-10-17 11:39:17 +08:00
lai
41fa214137 完善自定义首页设置 2024-10-17 10:30:21 +08:00
TsMask
1565f25a03 del: 移除debugger标记 2024-10-17 10:29:39 +08:00
lai
c5c2926d99 修改告警导出异常 2024-10-17 10:29:22 +08:00
lai
55456f9220 自定义主页 2024-10-16 19:28:05 +08:00
TsMask
cf1686c348 Merge branch 'lichang' of http://192.168.2.166:3180/OMC/ems_frontend_vue3 into lichang 2024-10-16 16:47:01 +08:00
TsMask
f7833bcd9f fix: UDM数据load失败无法重试 2024-10-16 16:46:57 +08:00
TsMask
5a621053a4 feat: 网元连接配置UDM支持Redis 2024-10-16 16:46:14 +08:00
lai
2a6451ef2a 更改首页代码文件路径 2024-10-16 15:11:26 +08:00
lai
f1b440c8dd 补充 2024-10-16 14:50:47 +08:00
lai
a67e54ca6e 补充 2024-10-16 14:23:32 +08:00
lai
53d9e63c36 新增自定义首页 2024-10-16 14:12:31 +08:00
zhongzm
b4623d19e5 fix:关键指标界面拖拽块设为图标,优化样式 2024-10-15 18:26:02 +08:00
zhongzm
700bff6e38 fix:关键指标界面自定义布局功能 2024-10-15 17:48:35 +08:00
TsMask
d77c4e43d4 feat: 信令跟踪保活续期 2024-10-15 15:14:38 +08:00
TsMask
6e11d2b16a feat: 网元日志文件获取查看,抓包单独查看 2024-10-15 14:55:41 +08:00
TsMask
405842bc0b fix: 看板用户事件AMF订阅编号无neId 2024-10-15 14:38:29 +08:00
zhongzm
bf8d7f2124 fix:代码优化-方法封装-拖拽保存-大小自适应 2024-10-14 18:52:47 +08:00
TsMask
ba98b37306 feat: 优化PCF参数可选请求数据处理 2024-10-12 19:10:52 +08:00
TsMask
aa8ed65fd8 del: 删除旧License页面相关接口请求 2024-10-12 19:10:21 +08:00
TsMask
936a4410b3 del: 删除旧参数配置页面相关接口请求 2024-10-12 19:10:04 +08:00
TsMask
58ec76f9e5 Merge remote-tracking branch 'origin/main' into lichang 2024-10-12 15:43:09 +08:00
TsMask
c1a77c8e48 chore: 更新版本号 241012 2024-10-12 15:42:29 +08:00
TsMask
477e8e4631 feat: 工具iperf/ping页面占位 2024-10-12 15:39:53 +08:00
TsMask
4f9d65a3a7 feat: UDM签约支持MICO和RAT修改 2024-10-12 15:07:06 +08:00
TsMask
b1799d8ccb style: 首页-网元详细信息-删除数据库以及端口字段显示 2024-10-12 09:50:57 +08:00
TsMask
86833e7d6b feat: 关键指标报表页面 2024-10-11 18:48:46 +08:00
TsMask
59cf57898b Merge branch 'lichang' of http://192.168.2.166:3180/OMC/ems_frontend_vue3 into lichang 2024-10-11 18:14:31 +08:00
zhongzm
fb9382e3a0 Merge remote-tracking branch 'origin/lichang' into lichang 2024-10-11 18:05:32 +08:00
zhongzm
dae4697cd2 feat:多图表网元指标界面实时数据连接修复和拖拽功能实现 2024-10-11 17:57:51 +08:00
TsMask
1b2e892f74 fix: 看板总流量24小时切换类型声明 2024-10-11 16:50:46 +08:00
TsMask
c66c640f75 fix: 看板总流量24小时实时累加 2024-10-11 15:47:26 +08:00
TsMask
30849416b6 fix: 工具ps/net页面定时器清除 2024-10-11 14:12:57 +08:00
TsMask
5edcee8da5 style: 隐藏跳转主机添加页面 2024-10-11 14:12:08 +08:00
TsMask
311beed2a7 fix: UPF总量数据格式化单位问题 2024-10-11 14:11:18 +08:00
TsMask
eb5fdfb635 fix: 修复工具ps/net资源列表 2024-10-11 09:53:13 +08:00
TsMask
78bcde9ef2 style: 信令跟踪根据状态隐藏操作 2024-10-10 21:06:56 +08:00
lai
630e2a16ad 限制自定义指标输入长度 2024-10-10 19:45:45 +08:00
TsMask
e1fe031f25 Merge branch 'lichang' of http://192.168.2.166:3180/OMC/ems_frontend_vue3 into lichang 2024-10-09 18:56:28 +08:00
zhongzm
855ba7dc9e feat:net界面修复:F12后的报错消除 2024-10-09 12:04:16 +08:00
TsMask
3a72e73d5d style: 拓扑图smsc图标 2024-10-09 10:53:47 +08:00
TsMask
4cb13a1419 feat: 网元快速安装添加SMSC的ip填充 2024-10-09 09:50:31 +08:00
TsMask
8dd84a5255 fix: 网元抓包loading状态禁止重复开始任务 2024-10-08 16:49:14 +08:00
TsMask
c0e62f48b7 fix: wiregasm去除gz压缩文件 2024-10-01 14:10:22 +08:00
TsMask
b992225e28 fix: 网元快速安装多语言识别 2024-10-01 13:02:06 +08:00
TsMask
2f04562a34 feat: 信令跟踪功能页面 2024-09-30 21:02:01 +08:00
72 changed files with 183894 additions and 3948 deletions

View File

@@ -11,7 +11,7 @@ VITE_APP_NAME = "Core Network OMC"
VITE_APP_CODE = "OMC"
# 应用版本
VITE_APP_VERSION = "2.240927"
VITE_APP_VERSION = "2.241102"
# 接口基础URL地址-不带/后缀
VITE_API_BASE_URL = "/omc-api"

View File

@@ -11,7 +11,7 @@ VITE_APP_NAME = "Core Network OMC"
VITE_APP_CODE = "OMC"
# 应用版本
VITE_APP_VERSION = "2.240927"
VITE_APP_VERSION = "2.241102"
# 接口基础URL地址-不带/后缀
VITE_API_BASE_URL = "/omc-api"

View File

@@ -30,6 +30,7 @@
"dayjs": "^1.11.11",
"echarts": "~5.5.0",
"file-saver": "^2.0.5",
"grid-layout-plus": "^1.0.5",
"intl-tel-input": "^23.8.1",
"js-base64": "^3.7.7",
"js-cookie": "^3.0.5",

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

178925
public/wiregasm/wiregasm.data Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -16,12 +16,12 @@ const fetchPackages = async () => {
console.log('Fetching packages');
let [wasmBuffer, dataBuffer] = await Promise.all([
await inflateRemoteBuffer(
'/wiregasm/wiregasm.wasm.gz'
// 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.wasm.gz'
'/wiregasm/wiregasm.wasm'
// 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.wasm'
),
await inflateRemoteBuffer(
'/wiregasm/wiregasm.data.gz'
// 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.data.gz'
'/wiregasm/wiregasm.data'
// 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.data'
),
]);

View File

@@ -1,55 +0,0 @@
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
RESULT_MSG_ERROR,
} from '@/constants/result-constants';
import { language, request } from '@/plugins/http-fetch';
import { parseObjLineToHump } from '@/utils/parse-utils';
/**
* 查询配置详细
* @param tag 配置ID
* @returns object
*/
export async function getConfigInfo(tag: string) {
// 发起请求
const result = await request({
url: `/api/rest/databaseManagement/v1/omc_db/config`,
method: 'get',
params: {
SQL: `SELECT * FROM config WHERE config_tag = '${tag}'`,
},
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
let data = result.data.data[0];
return Object.assign(result, {
data: parseObjLineToHump(data['config'][0]),
});
}
return result;
}
/**
* 修改配置
* @param data 配置对象
* @returns object
*/
export async function updateConfig(tag: string, data: Record<string, any>) {
const result = await request({
url: `/api/rest/databaseManagement/v1/omc_db/config?WHERE=config_tag='${tag}'`,
method: 'put',
data: { data },
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && result.data.data) {
let rows = result.data.data.affectedRows;
if (rows) {
delete result.data;
return result;
} else {
return { code: RESULT_CODE_ERROR, msg: RESULT_MSG_ERROR[language] };
}
}
return result;
}

View File

@@ -2,315 +2,8 @@ import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
RESULT_MSG_ERROR,
RESULT_MSG_SUCCESS,
} from '@/constants/result-constants';
import { language, request } from '@/plugins/http-fetch';
import { parseObjLineToHump } from '@/utils/parse-utils';
/**
* 查询配置参数标签栏
* @param neType 网元类型
* @returns object
*/
export async function getParamConfigTopTab(neType: string) {
// 发起请求
const result = await request({
url: `/api/rest/databaseManagement/v1/elementType/omc_db/objectType/param_config`,
method: 'get',
params: {
SQL: `SELECT id,top_display,top_tag,method FROM param_config WHERE ne_type = '${neType}' ORDER BY id ASC`,
},
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
let data = result.data.data[0];
data = data['param_config'];
if (Array.isArray(data)) {
return Object.assign(result, {
data: parseObjLineToHump(data),
});
}
return Object.assign(result, {
data: [],
});
}
return result;
}
/**
* 查询配置参数标签栏对应信息和规则
* @param neType 网元类型
* @param topTag
* @param neId
* @returns object { wrRule, dataArr }
*/
async function getParamConfigInfoAndRule(
neType: string,
topTag: string,
neId: string
) {
return await Promise.allSettled([
// 获取参数规则
request({
url: `/api/rest/databaseManagement/v1/elementType/omc_db/objectType/param_config`,
method: 'get',
params: {
SQL: `SELECT param_json FROM param_config WHERE ne_type = '${neType}' AND top_tag='${topTag}'`,
},
}),
// 获取对应信息
request({
url: `/api/rest/systemManagement/v1/elementType/${neType.toLowerCase()}/objectType/config/${topTag}`,
method: 'get',
params: {
ne_id: neId,
},
}),
]).then(resArr => {
let wrRule: Record<string, any> = {};
// 规则数据
if (resArr[0].status === 'fulfilled') {
const itemV = resArr[0].value;
// 解析数据
if (
itemV.code === RESULT_CODE_SUCCESS &&
Array.isArray(itemV.data?.data)
) {
let itemData = itemV.data.data;
const data = itemData[0]['param_config'];
if (Array.isArray(data)) {
const v = data[0]['param_json'];
try {
itemData = parseObjLineToHump(JSON.parse(v));
wrRule = itemData;
} catch (error) {
console.error(error);
}
}
}
}
let dataArr: Record<string, any>[] = [];
// 对应信息
if (resArr[1].status === 'fulfilled') {
const itemV = resArr[1].value;
// 解析数据
if (
itemV.code === RESULT_CODE_SUCCESS &&
Array.isArray(itemV.data?.data)
) {
let itemData = itemV.data.data;
dataArr = parseObjLineToHump(itemData);
}
}
return { wrRule, dataArr };
});
}
/**
* 查询配置参数标签栏对应信息-表单结构处理
* @param neType 网元类型
* @param topTag
* @param neId
* @returns object
*/
export async function getParamConfigInfoForm(
neType: string,
topTag: string,
neId: string
) {
const { wrRule, dataArr } = await getParamConfigInfoAndRule(
neType,
topTag,
neId
);
// 拼装数据
const result = {
code: RESULT_CODE_SUCCESS,
msg: RESULT_MSG_SUCCESS,
data: {
type: 'list' as 'list' | 'array',
data: [] as Record<string, any>[],
dataRule: {},
},
};
// kv单列表
if (Reflect.has(wrRule, 'list')) {
result.data.type = 'list';
const ruleArr = Object.freeze(wrRule['list']);
// 列表项数据
const dataList = [];
for (const item of dataArr) {
for (const key in item) {
// 规则为准
for (const rule of ruleArr) {
if (rule['name'] === key) {
const ruleItem = Object.assign({ optional: 'true' }, rule, {
value: item[key],
});
dataList.push(ruleItem);
break;
}
}
}
}
result.data.data = dataList;
}
// 多列表
if (Reflect.has(wrRule, 'array')) {
result.data.type = 'array';
const ruleArr = Object.freeze(wrRule['array']);
// 列表项数据
const dataArray = [];
for (const item of dataArr) {
const index = item['index'];
let record: Record<string, any>[] = [];
for (const key in item) {
// 规则为准
for (const rule of ruleArr) {
if (rule['name'] === key) {
const ruleItem = Object.assign({ optional: 'true' }, rule, {
value: item[key],
});
record.push(ruleItem);
break;
}
}
}
dataArray.push({ title: `Index-${index}`, key: index, record });
}
result.data.data = dataArray;
// 无数据时,用于新增
result.data.dataRule = { title: `Index-0`, key: 0, record: ruleArr };
}
return result;
}
/**
* 查询配置参数标签栏对应信息
* @param neType 网元类型
* @param topTag
* @param neId
* @returns object
*/
export async function getParamConfigInfo(
neType: string,
topTag: string,
neId: string
) {
// 发起请求
const result = await request({
url: `/api/rest/systemManagement/v1/elementType/${neType.toLowerCase()}/objectType/config/${topTag}`,
method: 'get',
params: {
ne_id: neId,
},
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
return Object.assign(result, {
data: parseObjLineToHump(result.data.data),
});
}
return result;
}
/**
* 查询配置参数标签栏对应信息子节点
* @param neType 网元类型
* @param topTag
* @param neId
* @param loc 子节点index/字段) 1/dnnList
* @returns
*/
export async function getParamConfigInfoChild(
neType: string,
topTag: string,
neId: string,
loc: string
) {
// 发起请求
const result = await request({
url: `/api/rest/systemManagement/v1/elementType/${neType.toLowerCase()}/objectType/config/${topTag}`,
method: 'get',
params: {
ne_id: neId,
loc: loc,
},
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS && Array.isArray(result.data.data)) {
return Object.assign(result, {
data: parseObjLineToHump(result.data.data),
});
}
return result;
}
/**
* 修改配置参数标签栏对应信息
* @param args 对象 {neType,neId,topTag,loc}
* @param data 对象 {修改的数据kv}
* @returns object
*/
export function updateParamConfigInfo(
type: 'list' | 'array',
args: Record<string, any>,
data: Record<string, any>
) {
let url = `/api/rest/systemManagement/v1/elementType/${args.neType.toLowerCase()}/objectType/config/${
args.topTag
}?ne_id=${args.neId}`;
// 多列表需要loc
if (type === 'array') {
url += `&loc=${args.loc}`;
}
return request({
url,
method: 'put',
data: data,
});
}
/**
* 新增配置参数标签栏对应信息
* @param args 对象 {neType,neId,topTag,loc}
* @param data 行记录对象
* @returns object
*/
export function addParamConfigInfo(
args: Record<string, any>,
data: Record<string, any>
) {
return request({
url: `/api/rest/systemManagement/v1/elementType/${args.neType.toLowerCase()}/objectType/config/${
args.topTag
}?ne_id=${args.neId}&loc=${args.loc}`,
method: 'post',
data: data,
});
}
/**
* 删除配置参数标签栏对应信息
* @param args 对象 {neType,neId,topTag,loc}
* loc 多层表的定位信息{index0}/{paraName1}/{index1}
* @param data 行记录对象
* @returns object
*/
export function delParamConfigInfo(args: Record<string, any>) {
return request({
url: `/api/rest/systemManagement/v1/elementType/${args.neType.toLowerCase()}/objectType/config/${
args.topTag
}?ne_id=${args.neId}&loc=${args.loc}`,
method: 'delete',
});
}
/**
* 更新网元配置重新载入
@@ -343,141 +36,50 @@ export async function updateNeConfigReload(neType: string, neId: string) {
/**
* 从参数配置PCF中获取对应信息提供给PCC用户策略输入框
* @param neType 网元类型
* @param topTag
* @param neId
* @returns object { wrRule, dataArr }
* @returns object {pccRules,sessionRules,qosTemplate,headerEnrichTemplate,serviceAreaRestriction}
*/
export async function getPCCRule(neId: any) {
return await Promise.allSettled([
// 获取参数规则
request({
url: `/api/rest/systemManagement/v1/elementType/pcf/objectType/config/pccRules`,
method: 'get',
params: {
ne_id: neId,
},
timeout: 1_000,
}),
// 获取对应信息
request({
url: `/api/rest/systemManagement/v1/elementType/pcf/objectType/config/sessionRules`,
method: 'get',
params: {
ne_id: neId,
},
timeout: 1_000,
}),
request({
url: `/api/rest/systemManagement/v1/elementType/pcf/objectType/config/qosTemplate`,
method: 'get',
params: {
ne_id: neId,
},
timeout: 1_000,
}),
request({
url: `/api/rest/systemManagement/v1/elementType/pcf/objectType/config/headerEnrichTemplate`,
method: 'get',
params: {
ne_id: neId,
},
timeout: 1_000,
}),
request({
url: `/api/rest/systemManagement/v1/elementType/pcf/objectType/config/serviceAreaRestriction`,
method: 'get',
params: {
ne_id: neId,
},
timeout: 1_000,
}),
]).then(resArr => {
let pccJson: any = new Map();
let sessJson: any = new Map();
let qosJson: any = new Map();
let headerJson: any = new Map();
let sarJson: any = new Map();
const paramNameArr = [
'pccRules',
'sessionRules',
'qosTemplate',
'headerEnrichTemplate',
'serviceAreaRestriction',
];
const reqArr = [];
for (const paramName of paramNameArr) {
reqArr.push(
request({
url: `/ne/config/data`,
params: { neType: 'PCF', neId, paramName },
method: 'get',
})
);
}
return await Promise.allSettled(reqArr).then(resArr => {
// 规则数据
if (resArr[0].status === 'fulfilled') {
const itemV = resArr[0].value;
// 解析数据
if (
itemV.code === RESULT_CODE_SUCCESS &&
Array.isArray(itemV.data?.data)
) {
let itemData = itemV.data.data;
itemData.forEach((item: any) => {
pccJson.set(item.ruleId, { value: item.ruleId, label: item.ruleId });
});
}
}
if (resArr[1].status === 'fulfilled') {
const itemV = resArr[1].value;
// 解析数据
if (
itemV.code === RESULT_CODE_SUCCESS &&
Array.isArray(itemV.data?.data)
) {
let itemData = itemV.data.data;
itemData.forEach((item: any) => {
sessJson.set(item.ruleId, { value: item.ruleId, label: item.ruleId });
});
}
}
if (resArr[2].status === 'fulfilled') {
const itemV = resArr[2].value;
// 解析数据
if (
itemV.code === RESULT_CODE_SUCCESS &&
Array.isArray(itemV.data?.data)
) {
let itemData = itemV.data.data;
itemData.forEach((item: any) => {
qosJson.set(item.qosId, { value: item.qosId, label: item.qosId });
});
}
}
if (resArr[3].status === 'fulfilled') {
const itemV = resArr[3].value;
// 解析数据
if (
itemV.code === RESULT_CODE_SUCCESS &&
Array.isArray(itemV.data?.data)
) {
let itemData = itemV.data.data;
itemData.forEach((item: any) => {
headerJson.set(item.templateName, {
value: item.templateName,
label: item.templateName,
const obj: any = {};
resArr.forEach((item, i: number) => {
if (item.status === 'fulfilled') {
const res = item.value;
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
const key = paramNameArr[i];
obj[key] = res.data.map((item: any) => {
if ('qosTemplate' === key) {
return { value: item.qosId, label: item.qosId };
}
if ('headerEnrichTemplate' === key) {
return { value: item.templateName, label: item.templateName };
}
if ('serviceAreaRestriction' === key) {
return { value: item.name, label: item.name };
}
return { value: item.ruleId, label: item.ruleId };
});
});
}
}
}
if (resArr[4].status === 'fulfilled') {
const itemV = resArr[4].value;
// 解析数据
if (
itemV.code === RESULT_CODE_SUCCESS &&
Array.isArray(itemV.data?.data)
) {
let itemData = itemV.data.data;
itemData.forEach((item: any) => {
sarJson.set(item.name, { value: item.name, label: item.name });
});
}
}
pccJson = Array.from(pccJson.values());
sessJson = Array.from(sessJson.values());
qosJson = Array.from(qosJson.values());
headerJson = Array.from(headerJson.values());
sarJson = Array.from(sarJson.values());
return { pccJson, sessJson, qosJson, headerJson, sarJson };
});
return obj;
});
}

View File

@@ -1,72 +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 listLicense(query: Record<string, any>) {
let totalSQL = 'select count(id) as total from ne_license ';
let rowsSQL = ' select * from ne_license ';
// 查询
let querySQL = 'where 1=1';
if (query.neType) {
querySQL += ` and ne_type like '%${query.neType}%' `;
}
// 分页
const pageNum = (query.pageNum - 1) * query.pageSize;
const limtSql = ` order by create_time desc limit ${pageNum},${query.pageSize} `;
// 发起请求
const result = await request({
url: `/api/rest/databaseManagement/v1/select/omc_db/ne_license`,
method: 'get',
params: {
totalSQL: totalSQL + querySQL,
rowsSQL: rowsSQL + querySQL + limtSql,
},
});
// 解析数据
if (result.code === RESULT_CODE_SUCCESS) {
const data: DataList = {
total: 0,
rows: [],
code: result.code,
msg: result.msg,
};
result.data.data.forEach((item: any) => {
const itemData = item['ne_license'];
if (Array.isArray(itemData)) {
if (itemData.length === 1 && itemData[0]['total'] >= 0) {
data.total = itemData[0]['total'];
} else {
data.rows = itemData.map(v => parseObjLineToHump(v));
}
}
});
return data;
}
return result;
}
/**
* 上传文件
* @param data 表单数据对象
* @returns object
*/
export function uploadLicense(data: FormData) {
return request({
url: `/api/rest/systemManagement/v1/elementType/${data.get(
'nfType'
)}/objectType/license?neId=${data.get('nfId')}`,
method: 'post',
data,
dataType: 'form-data',
timeout: 180_000,
});
}

20
src/api/tool/iperf.ts Normal file
View File

@@ -0,0 +1,20 @@
import { request } from '@/plugins/http-fetch';
// iperf 版本信息
export function iperfV(data: Record<string, string>) {
return request({
url: '/tool/iperf/v',
method: 'get',
params: data,
});
}
// iperf 软件安装
export function iperfI(data: Record<string, string>) {
return request({
url: '/tool/iperf/i',
method: 'post',
data: data,
timeout: 60_000,
});
}

View File

@@ -1,7 +1,7 @@
import { request } from '@/plugins/http-fetch';
/**
* 查询文件列表列表
* 查询网元端文件列表
* @param query 查询参数
* @returns object
*/
@@ -14,7 +14,7 @@ export function listNeFiles(query: Record<string, any>) {
}
/**
* 从网元获取文件
* 从网元到本地获取文件
* @param query 查询参数
* @returns object
*/
@@ -27,3 +27,24 @@ export function getNeFile(query: Record<string, any>) {
timeout: 180_000,
});
}
// 从网元到本地获取目录压缩为ZIP
export function getNeDirZip(data: Record<string, any>) {
return request({
url: '/ne/action/pullDirZip',
method: 'get',
params: data,
responseType: 'blob',
timeout: 60_000,
});
}
// 查看网元端文件内容
export function getNeViewFile(data: Record<string, any>) {
return request({
url: '/ne/action/viewFile',
method: 'get',
params: data,
timeout: 60_000,
});
}

10
src/api/tool/ping.ts Normal file
View File

@@ -0,0 +1,10 @@
import { request } from '@/plugins/http-fetch';
// ping 网元端版本信息
export function pingV(data: Record<string, string>) {
return request({
url: '/tool/ping/v',
method: 'get',
params: data,
});
}

64
src/api/trace/packet.ts Normal file
View File

@@ -0,0 +1,64 @@
import { request } from '@/plugins/http-fetch';
/**
* 信令跟踪网卡设备列表
* @returns
*/
export function packetDevices() {
return request({
url: '/trace/packet/devices',
method: 'get',
});
}
/**
* 信令跟踪开始
* @param data 对象
* @returns
*/
export function packetStart(data: Record<string, any>) {
return request({
url: '/trace/packet/start',
method: 'post',
data: data,
});
}
/**
* 信令跟踪结束
* @param data 对象
* @returns
*/
export function packetStop(taskNo: string) {
return request({
url: '/trace/packet/stop',
method: 'post',
data: { taskNo },
});
}
/**
* 信令跟踪过滤
* @param data 对象
* @returns
*/
export function packetFilter(taskNo: string, expr: string) {
return request({
url: '/trace/packet/filter',
method: 'put',
data: { taskNo, expr },
});
}
/**
* 信令跟踪续期保活
* @param data 对象
* @returns
*/
export function packetKeep(taskNo: string, duration: number = 120) {
return request({
url: '/trace/packet/keep-alive',
method: 'put',
data: { taskNo, duration },
});
}

View File

@@ -6,6 +6,7 @@ export function dumpStart(data: Record<string, string>) {
url: '/trace/tcpdump/start',
method: 'post',
data: data,
timeout: 60_000,
});
}
@@ -15,16 +16,6 @@ export function dumpStop(data: Record<string, string>) {
url: '/trace/tcpdump/stop',
method: 'post',
data: data,
});
}
// 网元抓包PACP 下载
export function dumpDownload(data: Record<string, any>) {
return request({
url: '/trace/tcpdump/download',
method: 'get',
params: data,
responseType: 'blob',
timeout: 60_000,
});
}
@@ -35,5 +26,6 @@ export function traceUPF(data: Record<string, string>) {
url: '/trace/tcpdump/upf',
method: 'post',
data: data,
timeout: 60_000,
});
}

View File

@@ -13,6 +13,11 @@ const props = defineProps({
type: String,
required: true,
},
/**ws连接地址必传 如/ws/view */
url: {
type: String,
required: true,
},
/**网元类型,必传 */
neType: {
type: String,
@@ -33,6 +38,11 @@ const props = defineProps({
type: Number,
default: 40,
},
/**ws发送requestId前缀 如ssh_id */
prefix: {
type: String,
default: 'ssh',
},
});
/**终端输入DOM节点实例对象 */
@@ -148,13 +158,18 @@ function wsMessage(res: Record<string, any>) {
if (parts.length > 0) {
let text = parts[parts.length - 1];
// 找到最后输出标记
const lestIndex = text.lastIndexOf('\u001b[?2004h\u001b]0;');
let lestIndex = text.lastIndexOf('\u001b[?2004h\u001b]0;');
if (lestIndex !== -1) {
text = text.substring(0, lestIndex);
}
if (text === '' || text === '\r\n' || text.startsWith("^C\r\n") ) {
if (text === '' || text === '\r\n' || text.startsWith('^C\r\n')) {
return;
}
// 是否还有最后输出标记
lestIndex = text.lastIndexOf('\u001b[?2004h');
if (lestIndex !== -1) {
text = text.substring(0, lestIndex);
}
// console.log({ parts, text });
terminal.value.write(text);
return;
@@ -168,7 +183,7 @@ onMounted(() => {
if (props.neType && props.neId) {
// 建立链接
const options: OptionsType = {
url: '/ws/view',
url: props.url,
params: {
neType: props.neType,
neId: props.neId,
@@ -185,7 +200,7 @@ onMounted(() => {
});
onBeforeUnmount(() => {
ws.close();
if (ws.state() === WebSocket.OPEN) ws.close();
});
// 给组件设置属性 ref="xxxTerminal"
@@ -200,7 +215,7 @@ defineExpose({
/**发送命令 */
send: (type: string, data: Record<string, any>) => {
ws.send({
requestId: `ssh_${props.id}`,
requestId: `${props.prefix}_${props.id}`,
type,
data,
});
@@ -208,7 +223,7 @@ defineExpose({
/**模拟按下 Ctrl+C */
ctrlC: () => {
ws.send({
requestId: `ssh_${props.id}`,
requestId: `${props.prefix}_${props.id}`,
type: 'ctrl-c',
});
},

View File

@@ -567,6 +567,8 @@ export default {
rowInfo: "Info",
type: "Type",
duration: "Duration",
seizureTime: "Call Start Time",
releaseTime: "Hangup Time",
caller: "Caller",
called: "Called",
result: "Result",
@@ -685,11 +687,12 @@ export default {
addrPlease: "Please fill in the host IP address correctly",
port: "Port",
portPlease: "Please fill in the host port number correctly",
user: "Login User",
userPlease: "Please fill in the host login user correctly",
user: "User",
userPlease: "Please fill in the host user correctly",
database: "DataBase",
authMode: "Auth Mode",
password: "Password",
passwordPlease: "Please fill in the host login password correctly",
passwordPlease: "Please fill in the host password correctly",
privateKey: "Private Key",
privateKeyPlease: "Please fill in the private key characters correctly ~/.ssh/id_rsa",
passPhrase: "Private Key Cipher",
@@ -1071,7 +1074,26 @@ export default {
element:'Element',
granularity:'Granularity',
unit:'Unit',
}
},
kpiKeyTarget:{
"fullWidthLayout":"Full Width",
"twoColumnLayout":"Two Column",
"saveLayout": "Save Layout",
"restoreSaved": "Restore Layout",
"saveSuccess": " '{name}' saved successfully",
"restoreSavedSuccess": " '{name}' restored successfully",
"noSavedLayout": "No saved layout found for '{name}'",
"layout1": "Layout 1",
"layout2": "Layout 2",
"layout3": "Layout 3"
},
kpiOverView:{
"kpiChartTitle":"Overview of NE metrics",
"changeLine":"Change to Line Charts",
"changeBar":"Change to Bar Charts",
"chooseShowMetrics":"Select the metric you want to display",
"chooseMetrics":"Select an indicator",
},
},
traceManage: {
analysis: {
@@ -1110,8 +1132,8 @@ export default {
fileUPFTip: 'UPF internal packet capture and analysis packet',
textStart: "Start",
textStop: "Stop",
textLog: "Log",
textLogMsg: "Log Info",
textLog: "LogFile",
textLogMsg: "LogFile Info",
textDown: "Download",
downTip: "Are you sure you want to download the {title} capture data file?",
downOk: "{title} file download complete",
@@ -1287,7 +1309,7 @@ export default {
},
exportFile:{
fileName:'File Source',
downTip: "Confirm the download file name is [{fileName}] File?",
downTip: "Confirm the download file name is [{fileName}] File?",
downTipErr: "Failed to get file",
deleteTip: "Confirm the delete file name is [{fileName}] File?",
deleteTipErr: "Failed to delete file",
@@ -1782,6 +1804,10 @@ export default {
reset: "System Reset",
resetInstruction: "A system reset will erase all data in the current system, please proceed with caution!!!!",
resetTipContent: 'Are you sure you want to clear all data from the current system and insist on continuing?',
homeInstruction:'Set the home page',
home: 'Home Page',
homeTip:'Do you want to submit the current interface as the system interface?',
homeSet:'Home Page Settings',
},
role:{
allScopeOptions:'All data permissions',
@@ -2112,7 +2138,7 @@ export default {
realTimeStop:"Stop",
realTime:"Real Time Speed",
pid:"PID",
name:"APP Name",
name:"Program name",
username:"User Name",
runTime:"Run Time",
numThreads:"Thread",
@@ -2121,13 +2147,11 @@ export default {
diskWrite:"Disk Write",
},
net:{
PID:"PID",
name:"name",
localAddr:"localAddr",
remoteAddr:"remoteAddr",
status:"status",
type:"type",
port:"port",
localAddr:"Local Address",
remoteAddr:"Foreign Address",
status:"State",
proto:"Proto",
port:"Port",
},
},
},

View File

@@ -567,6 +567,8 @@ export default {
rowInfo: "记录信息",
type: "记录类型",
duration: "通话时长",
seizureTime: "呼叫开始时间",
releaseTime: "挂断结束时间",
caller: "主叫",
called: "被叫",
result: "结果",
@@ -686,10 +688,11 @@ export default {
port: "端口",
portPlease: "请正确填写主机端口号",
user: "用户名",
userPlease: "请正确填写主机登录用户",
userPlease: "请正确填写主机用户",
database: "数据库",
authMode: "认证模式",
password: "密码",
passwordPlease: "请正确填写主机登录密码",
passwordPlease: "请正确填写主机密码",
privateKey: "私钥",
privateKeyPlease: "请正确填写私钥字符内容 ~/.ssh/id_rsa",
passPhrase: "私钥密码",
@@ -1071,7 +1074,27 @@ export default {
element:'元素',
granularity:'颗粒度',
unit:'单位',
}
},
kpiKeyTarget:{
"fullWidthLayout":"全宽布局",
"twoColumnLayout":"两列布局",
"saveLayout": "保存布局",
"restoreSaved": "恢复布局",
"saveSuccess": " {name} 保存成功",
"restoreSavedSuccess": " {name} 恢复成功",
"noSavedLayout": "没有找到保存的布局 {name}",
"layout1": "布局1",
"layout2": "布局2",
"layout3": "布局3"
},
kpiOverView:{
"kpiChartTitle":"网元指标概览",
"changeLine":"切换为折线图",
"changeBar":"切换为柱状图",
"chooseShowMetrics":"选择需要显示的指标",
"chooseMetrics":"选择指标",
},
},
traceManage: {
analysis: {
@@ -1110,8 +1133,8 @@ export default {
fileUPFTip: 'UPF内部抓包分析包',
textStart: "开始",
textStop: "停止",
textLog: "日志",
textLogMsg: "日志信息",
textLog: "日志文件",
textLogMsg: "日志文件信息",
textDown: "下载",
downTip: "确认要下载 {title} 抓包数据文件吗?",
downOk: "{title} 文件下载完成",
@@ -1126,7 +1149,7 @@ export default {
stopNotRun: "{title} 任务未运行",
},
task: {
traceId: '跟踪编号',
traceId: '跟踪编号',
trackType: '跟踪类型',
trackTypePlease: '请选择跟踪类型',
creater: '创建人',
@@ -1287,7 +1310,7 @@ export default {
},
exportFile:{
fileName:'文件来源',
downTip: "确认下载文件名为 【{fileName}】 文件?",
downTip: "确认下载文件名为 【{fileName}】 文件?",
downTipErr: "文件获取失败",
deleteTip: "确认删除文件名为 【{fileName}】 文件?",
deleteTipErr: "文件删除失败",
@@ -1782,6 +1805,10 @@ export default {
reset: "系统重置",
resetInstruction: "系统重置将会清除当前系统内所有数据,请谨慎操作!!!",
resetTipContent: '确认要清除当前系统内所有数据并坚持继续吗?',
homeInstruction:'设置系统首页界面',
home: '系统首页',
homeTip:'确认要提交当前界面为系统界面吗?',
homeSet:'系统首页设置',
},
role:{
allScopeOptions:'全部数据权限',
@@ -2105,7 +2132,7 @@ export default {
hostSelectMore: "加载更多 {num}",
hostSelectHeader: "主机列表",
},
ps:{
ps:{
realTimeHigh:"高",
realTimeLow:"低",
realTimeRegular:"常规",
@@ -2121,13 +2148,11 @@ export default {
diskWrite:"磁盘写入",
},
net:{
PID:"PID",
name:"名称",
localAddr:"localAddr",
remoteAddr:"remoteAddr",
localAddr:"本地地址",
remoteAddr:"远程地址",
status:"状态",
type:"类型",
port:"口",
proto:"协议",
port:"口",
},
},
},

View File

@@ -187,7 +187,6 @@ function beforeRequest(options: OptionsType): OptionsType | Promise<any> {
const separator = options.url.includes('?') ? '&' : '?';
// 请求加密
if (options.crypto) {
debugger;
const data = encryptAES(JSON.stringify(paramStr), APP_DATA_API_KEY);
options.url += `${separator}data=${encodeURIComponent(data)}`;
} else {

View File

@@ -172,15 +172,16 @@ export function parseSizeFromKbs(sizeByte: number, timeInterval: number): any {
/**
* 字节数转换单位
* @param bits 字节Bit大小
* @returns MB
* @param bits 字节Bit大小 64009540 = 512.08 MB
* @returns xx B / KB / MB / GB / TB / PB / EB / ZB / YB
*/
export function parseSizeFromBits(bits: number | string): string {
bits = Number(bits) || 0;
if (bits <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
bits = bits * 8;
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const unitIndex = Math.floor(Math.log2(bits) / 10);
const value = (bits / Math.pow(1024, unitIndex)).toFixed(2);
const value = (bits / Math.pow(1000, unitIndex)).toFixed(2);
const unti = units[unitIndex];
return `${value} ${unti}`;
}

View File

@@ -1,165 +0,0 @@
import useI18n from '@/hooks/useI18n';
import { regExpIPv4, regExpIPv6, validURL } from '@/utils/regular-utils';
export default function useOptions() {
const { t } = useI18n();
/**规则校验 */
function ruleVerification(row: Record<string, any>): (string | boolean)[] {
let result = [true, ''];
const type = row.type;
const value = row.value;
const filter = row.filter;
const display = row.display;
// 子嵌套的不检查
if (row.array) {
return result;
}
// 可选的同时没有值不检查
if (row.optional === 'true' && !value) {
return result;
}
switch (type) {
case 'int':
// filter: "0~128"
if (filter && filter.indexOf('~') !== -1) {
const filterArr = filter.split('~');
const minInt = parseInt(filterArr[0]);
const maxInt = parseInt(filterArr[1]);
const valueInt = parseInt(value);
if (valueInt < minInt || valueInt > maxInt) {
return [
false,
t('views.configManage.configParamForm.requireInt', {
display,
filter,
}),
];
}
}
break;
case 'ipv4':
if (!regExpIPv4.test(value)) {
return [
false,
t('views.configManage.configParamForm.requireIpv4', { display }),
];
}
break;
case 'ipv6':
if (!regExpIPv6.test(value)) {
return [
false,
t('views.configManage.configParamForm.requireIpv6', { display }),
];
}
break;
case 'enum':
if (filter && filter.indexOf('{') === 1) {
let filterJson: Record<string, any> = {};
try {
filterJson = JSON.parse(filter); //string---json
} catch (error) {
console.error(error);
}
if (!Object.keys(filterJson).includes(`${value}`)) {
return [
false,
t('views.configManage.configParamForm.requireEnum', { display }),
];
}
}
break;
case 'bool':
// filter: '{"0":"false", "1":"true"}'
if (filter && filter.indexOf('{') === 1) {
let filterJson: Record<string, any> = {};
try {
filterJson = JSON.parse(filter); //string---json
} catch (error) {
console.error(error);
}
if (!Object.values(filterJson).includes(`${value}`)) {
return [
false,
t('views.configManage.configParamForm.requireBool', { display }),
];
}
}
break;
case 'string':
// filter: "0~128"
// 字符串长度判断
if (filter && filter.indexOf('~') !== -1) {
try {
const filterArr = filter.split('~');
let rule = new RegExp(
'^\\S{' + filterArr[0] + ',' + filterArr[1] + '}$'
);
if (!rule.test(value)) {
return [
false,
t('views.configManage.configParamForm.requireString', {
display,
}),
];
}
} catch (error) {
console.error(error);
}
}
// 字符串http判断
if (value.startsWith('http')) {
try {
if (!validURL(value)) {
return [
false,
t('views.configManage.configParamForm.requireString', {
display,
}),
];
}
} catch (error) {
console.error(error);
}
}
break;
case 'regex':
// filter: "^[0-9]{3}$"
if (filter) {
try {
let regex = new RegExp(filter);
if (!regex.test(value)) {
return [
false,
t('views.configManage.configParamForm.requireString', {
display,
}),
];
}
} catch (error) {
console.error(error);
}
}
break;
default:
return [
false,
t('views.configManage.configParamForm.requireUn', { display }),
];
}
return result;
}
return { ruleVerification };
}

View File

@@ -1,22 +0,0 @@
import { getParamConfigInfo } from '@/api/configManage/configParam';
import { ref } from 'vue';
export default function useSMFOptions() {
/**upfId可选择 */
const optionsUPFIds = ref<{ value: string; label: string }[]>([]);
/**初始加载upfId */
function initUPFIds() {
getParamConfigInfo('smf', 'upfConfig', '001').then(res => {
optionsUPFIds.value = [];
for (const s of res.data) {
optionsUPFIds.value.push({
value: s.id,
label: s.id,
});
}
});
}
return { initUPFIds, optionsUPFIds };
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,500 +0,0 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { Form, message } from 'ant-design-vue/lib';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { MenuInfo } from 'ant-design-vue/lib/menu/src/interface';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { uploadLicense, listLicense } from '@/api/configManage/license';
import useI18n from '@/hooks/useI18n';
import useNeInfoStore from '@/store/modules/neinfo';
import { FileType } from 'ant-design-vue/lib/upload/interface';
import { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface';
import { parseDateToStr } from '@/utils/date-utils';
const neInfoStore = useNeInfoStore();
const { t } = useI18n();
/**查询参数 */
let queryParams = reactive({
/**网元类型 */
neType: '',
/**当前页数 */
pageNum: 1,
/**每页条数 */
pageSize: 20,
});
/**查询参数重置 */
function fnQueryReset() {
queryParams = Object.assign(queryParams, {
neType: '',
pageNum: 1,
pageSize: 20,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
fnGetList();
}
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: true,
data: [],
selectedRowKeys: [],
});
/**表格字段列 */
let tableColumns: ColumnsType = [
{
title: t('views.configManage.license.neType'),
dataIndex: 'neType',
align: 'center',
width: 2,
},
{
title: t('views.configManage.license.neId'),
dataIndex: 'neId',
align: 'center',
width: 2,
},
{
title: t('views.configManage.license.serialNum'),
dataIndex: 'serialNum',
align: 'center',
width: 3,
},
{
title: t('views.configManage.license.comment'),
dataIndex: 'remark',
align: 'center',
width: 5,
},
{
title: t('views.configManage.license.createTime'),
dataIndex: 'createTime',
align: 'center',
customRender(opt) {
if (!opt.value) return '';
return parseDateToStr(opt.value);
},
width: 2,
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => t('common.tablePaginationTotal', { total }),
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
queryParams.pageNum = page;
queryParams.pageSize = pageSize;
fnGetList();
},
});
/**表格紧凑型变更操作 */
function fnTableSize({ key }: MenuInfo) {
tableState.size = key as SizeType;
}
/**查询信息列表, pageNum初始页数 */
function fnGetList(pageNum?: number) {
if (tableState.loading) return;
tableState.loading = true;
if (pageNum) {
queryParams.pageNum = pageNum;
}
listLicense(toRaw(queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
// 取消勾选
if (tableState.selectedRowKeys.length > 0) {
tableState.selectedRowKeys = [];
}
tablePagination.total = res.total;
tableState.data = res.rows;
if (
tablePagination.total <=
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}
}
tableState.loading = false;
});
}
/**对话框对象信息状态类型 */
type ModalStateType = {
/**新增框或修改框是否显示 */
visibleByEdit: boolean;
/**网元版本历史框是否显示 */
visibleByHistory: boolean;
/**标题 */
title: string;
/**表单数据 */
from: Record<string, any>;
/**确定按钮 loading */
confirmLoading: boolean;
};
/**对话框对象信息状态 */
let modalState: ModalStateType = reactive({
visibleByEdit: false,
visibleByHistory: false,
title: '任务设置',
from: {
neType: undefined,
comment: '',
file: undefined,
fileList: [],
},
confirmLoading: false,
});
/**
* 对话框弹出显示为 新增或者修改
* @param noticeId 网元id, 不传为新增
*/
function fnModalVisibleByEdit() {
modalState.title = t('common.uploadText');
modalState.visibleByEdit = true;
}
/**对话框内表单属性和校验规则 */
const modalStateFrom = Form.useForm(
modalState.from,
reactive({
neType: [
{
required: true,
message: t('views.configManage.license.neTypePlease'),
},
],
comment: [
{
required: true,
message: t('views.configManage.license.updateCommentPlease'),
},
],
file: [
{
required: true,
message: t('views.configManage.license.updateFilePlease'),
},
],
})
);
/**
* 对话框弹出确认执行函数
* 进行表达规则校验
*/
function fnModalOk() {
modalStateFrom
.validate()
.then(e => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
let formData = new FormData();
formData.append('nfType', from.neType[0]);
formData.append('nfId', from.neType[1]);
formData.append('comment', from.comment);
formData.append('file', from.file);
const hide = message.loading(t('common.loading'), 0);
uploadLicense(formData)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', { msg: modalState.title }),
duration: 3,
});
modalState.visibleByEdit = false;
modalStateFrom.resetFields();
} else {
message.error({
content: `${res.msg}`,
duration: 3,
});
}
})
.finally(() => {
hide();
modalState.confirmLoading = false;
// 获取列表数据
fnGetList();
});
})
.catch(e => {
message.error(t('common.errorFields', { num: e.errorFields.length }), 3);
});
}
/**
* 对话框弹出关闭执行函数
* 进行表达规则校验
*/
function fnModalCancel() {
modalState.visibleByEdit = false;
modalState.visibleByHistory = false;
modalStateFrom.resetFields();
}
/**上传前检查或转换压缩 */
function fnBeforeUploadFile(file: FileType) {
if (modalState.confirmLoading) return false;
const fileName = file.name;
const suff = fileName.substring(fileName.lastIndexOf('.'));
if (!['.ini'].includes(suff)) {
message.error(
t('views.configManage.softwareManage.onlyAble', { fileText: '(.ini)' }),
3
);
return false;
}
return true;
}
/**上传文件 */
function fnUploadFile(up: UploadRequestOption) {
// 改为完成状态
const file = modalState.from.fileList[0];
file.percent = 100;
file.status = 'done';
// 预置到表单
modalState.from.file = up.file;
}
onMounted(() => {
// 获取网元网元列表
neInfoStore.fnNelist().then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// 获取列表数据
fnGetList();
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
});
});
</script>
<template>
<PageContainer>
<a-card
v-show="tableState.seached"
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<!-- 表格搜索栏 -->
<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.configManage.license.neType')"
name="neType "
>
<a-auto-complete
v-model:value="queryParams.neType"
:options="neInfoStore.getNeSelectOtions"
allow-clear
:placeholder="t('views.configManage.license.neTypePlease')"
/>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnGetList(1)">
<template #icon><SearchOutlined /></template>
{{ t('common.search') }}
</a-button>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
{{ t('common.reset') }}
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8" align="center">
<a-button type="primary" @click.prevent="fnModalVisibleByEdit()">
<template #icon><UploadOutlined /></template>
{{ t('common.uploadText') }}
</a-button>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8" align="center">
<a-tooltip>
<template #title>{{ t('common.searchBarText') }}</template>
<a-switch
v-model:checked="tableState.seached"
:checked-children="t('common.switch.show')"
:un-checked-children="t('common.switch.hide')"
size="small"
/>
</a-tooltip>
<a-tooltip>
<template #title>{{ t('common.reloadText') }}</template>
<a-button type="text" @click.prevent="fnGetList()">
<template #icon><ReloadOutlined /></template>
</a-button>
</a-tooltip>
<a-tooltip>
<template #title>{{ t('common.sizeText') }}</template>
<a-dropdown trigger="click" placement="bottomRight">
<a-button type="text">
<template #icon><ColumnHeightOutlined /></template>
</a-button>
<template #overlay>
<a-menu
:selected-keys="[tableState.size as string]"
@click="fnTableSize"
>
<a-menu-item key="default">
{{ t('common.size.default') }}
</a-menu-item>
<a-menu-item key="middle">
{{ t('common.size.middle') }}
</a-menu-item>
<a-menu-item key="small">
{{ t('common.size.small') }}
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-tooltip>
</a-space>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:pagination="tablePagination"
:scroll="{ x: 1000 }"
>
</a-table>
</a-card>
<!-- 上传框 -->
<ProModal
:drag="true"
:width="800"
:destroyOnClose="true"
:keyboard="false"
:mask-closable="false"
:visible="modalState.visibleByEdit"
:title="modalState.title"
:confirm-loading="modalState.confirmLoading"
@ok="fnModalOk"
@cancel="fnModalCancel"
>
<a-form name="modalStateFrom" layout="horizontal">
<a-form-item
:label="t('views.configManage.license.neType')"
name="neType"
v-bind="modalStateFrom.validateInfos.neType"
>
<a-cascader
v-model:value="modalState.from.neType"
:options="useNeInfoStore().getNeCascaderOptions"
:allow-clear="false"
:placeholder="t('views.configManage.license.neTypePlease')"
/>
</a-form-item>
<a-form-item
:label="t('views.configManage.license.updateComment')"
name="comment"
v-bind="modalStateFrom.validateInfos.comment"
>
<a-textarea
v-model:value="modalState.from.comment"
:maxlength="200"
:show-count="true"
:placeholder="t('views.configManage.license.updateCommentPlease')"
/>
</a-form-item>
<a-form-item
:label="t('views.configManage.license.updateFile')"
name="file"
v-bind="modalStateFrom.validateInfos.file"
>
<a-upload
name="file"
v-model:file-list="modalState.from.fileList"
accept=".ini"
list-type="text"
:max-count="1"
:show-upload-list="true"
:before-upload="fnBeforeUploadFile"
:custom-request="fnUploadFile"
>
<a-button type="default" :loading="modalState.confirmLoading">
{{ t('views.configManage.license.selectFile') }}
</a-button>
</a-upload>
</a-form-item>
</a-form>
</ProModal>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;
}
</style>

View File

@@ -0,0 +1,438 @@
<script setup lang="ts">
import { PageContainer } from 'antdv-pro-layout';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { message } from 'ant-design-vue/lib';
import { reactive, ref, onMounted, onBeforeUnmount, markRaw } from 'vue';
import useI18n from '@/hooks/useI18n';
import { TooltipComponent } from 'echarts/components';
import { GaugeChart } from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';
import * as echarts from 'echarts/core';
import { TitleComponent, LegendComponent } from 'echarts/components';
import { PieChart } from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { useRoute } from 'vue-router';
import useAppStore from '@/store/modules/app';
import useDictStore from '@/store/modules/dict';
import { listAllNeInfo } from '@/api/ne/neInfo';
import { parseDateToStr } from '@/utils/date-utils';
const { getDict } = useDictStore();
const appStore = useAppStore();
const route = useRoute();
const { t } = useI18n();
echarts.use([
TooltipComponent,
GaugeChart,
TitleComponent,
LegendComponent,
PieChart,
CanvasRenderer,
LabelLayout,
]);
/**图DOM节点实例对象 */
const statusBar = ref<HTMLElement | undefined>(undefined);
/**图实例对象 */
const statusBarChart = ref<any>(null);
/**网元状态字典数据 */
let indexColor = ref<DictType[]>([
{ label: 'Normal', value: 'normal', tagType: '', tagClass: '#91cc75' },
{
label: 'Abnormal',
value: 'abnormal',
tagType: '',
tagClass: '#ee6666',
},
]);
/**表格字段列 */
//customRender(){} ----单元格处理
let tableColumns: ColumnsType = [
{
title: t('views.index.object'),
dataIndex: 'neName',
align: 'left',
},
{
title: t('views.index.realNeStatus'),
dataIndex: 'serverState',
align: 'left',
key: 'status',
},
{
title: t('views.index.reloadTime'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
if (opt.value?.refreshTime) return parseDateToStr(opt.value?.refreshTime);
return '-';
},
},
{
title: t('views.index.version'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
return opt.value?.version || '-';
},
},
{
title: t('views.index.serialNum'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
return opt.value?.sn || '-';
},
},
{
title: t('views.index.expiryDate'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
return opt.value?.expire || '-';
},
},
{
title: t('views.index.ipAddress'),
dataIndex: 'serverState',
align: 'left',
customRender(opt) {
return opt.value?.neIP || '-';
},
},
];
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: string;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: false,
data: [],
selectedRowKeys: [],
});
/**表格状态 */
let nfInfo: any = reactive({
obj: 'OMC',
version: appStore.version,
status: t('views.index.normal'),
outTimeDate: '',
serialNum: appStore.serialNum,
});
/**表格状态类型 */
type nfStateType = {
/**主机名 */
hostName: string;
/**操作系统信息 */
osInfo: string;
/**IP地址 */
ipAddress: string;
/**版本 */
version: string;
/**CPU利用率 */
cpuUse: string;
/**内存使用 */
memoryUse: string;
/**用户容量 */
capability: number;
/**序列号 */
serialNum: string;
/**许可证到期日期 */
/* selectedRowKeys: (string | number)[];*/
expiryDate: string;
};
/**网元详细信息 */
let pronInfo: nfStateType = reactive({
hostName: '5gc',
osInfo: 'Linux 5gc 4.15.0-112-generic 2020 x86_64 GNU/Linux',
ipAddress: '-',
version: '-',
cpuUse: '-',
memoryUse: '-',
capability: 0,
serialNum: '-',
expiryDate: '-',
});
/**查询网元状态列表 */
function fnGetList(one: boolean) {
if (tableState.loading) return;
one && (tableState.loading = true);
listAllNeInfo({ bandStatus: true }).then(res => {
tableState.data = res.data;
tableState.loading = false;
var rightNum = 0;
var errorNum = 0;
// if (res.length) nfInfo.serialNum = res[0].serialNum;
for (let i = 0; i < res.length; i++) {
if (res[i].status == '正常' || res[i].status == 'Normal') {
rightNum++;
} else {
errorNum++;
}
}
const optionData: any = {
title: {
text: '',
subtext: '',
left: 'center',
},
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
color: indexColor.value.map(item => item.tagClass),
series: [
{
name: t('views.index.realNeStatus'),
type: 'pie',
radius: '70%',
center: ['50%', '50%'],
data: [
{ value: rightNum, name: t('views.index.normal') },
{ value: errorNum, name: t('views.index.abnormal') },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
label: {},
},
],
};
fnDesign(statusBar.value, optionData);
});
}
function fnDesign(container: HTMLElement | undefined, option: any) {
if (!container) return;
if (!statusBarChart.value) {
statusBarChart.value = markRaw(echarts.init(container, 'light'));
}
option && statusBarChart.value.setOption(option);
// 创建 ResizeObserver 实例
var observer = new ResizeObserver(entries => {
if (statusBarChart.value) {
statusBarChart.value.resize();
}
});
// 监听元素大小变化
observer.observe(container);
}
/**抽屉 网元详细信息 */
const visible = ref(false);
const closeDrawer = () => {
visible.value = false;
};
/**抽屉 网元详细信息 */
/**监听表格行事件*/
function rowClick(record: any, index: any) {
return {
onClick: (event: any) => {
let pronData = JSON.parse(JSON.stringify(record.serverState));
if (!pronData.online) {
message.error(t('views.index.neStatus'), 2);
return false;
} else {
const totalMemInKB = pronData.mem?.totalMem;
const nfUsedMemInKB = pronData.mem?.nfUsedMem;
const sysMemUsageInKB = pronData.mem?.sysMemUsage;
// 将KB转换为MB
const totalMemInMB = Math.round((totalMemInKB / 1024) * 100) / 100;
const nfUsedMemInMB = Math.round((nfUsedMemInKB / 1024) * 100) / 100;
const sysMemUsageInMB =
Math.round((sysMemUsageInKB / 1024) * 100) / 100;
//渲染详细信息
pronInfo = {
hostName: pronData.hostname,
osInfo: pronData.os,
ipAddress: pronData.neIP,
version: pronData.version,
cpuUse:
pronData.neName +
':' +
pronData.cpu?.nfCpuUsage / 100 +
'%; ' +
'SYS:' +
pronData.cpu?.sysCpuUsage / 100 +
'%',
memoryUse:
'Total:' +
totalMemInMB +
'MB; ' +
pronData.name +
':' +
nfUsedMemInMB +
'MB; SYS:' +
sysMemUsageInMB +
'MB',
capability: pronData.capability,
serialNum: pronData.sn,
expiryDate: pronData.expire,
};
}
visible.value = true;
},
};
}
let timer: any;
/**
* 国际化翻译转换
*/
function fnLocale() {
let title = route.meta.title as string;
if (title.indexOf('router.') !== -1) {
title = t(title);
}
appStore.setTitle(title);
}
onMounted(() => {
getDict('index_status')
.then(res => {
if (res.length > 0) {
indexColor.value = res;
}
})
.finally(() => {
fnLocale();
fnGetList(true);
timer = setInterval(() => fnGetList(false), 10000); // 每隔10秒执行一次
});
});
// 在组件卸载之前清除定时器
onBeforeUnmount(() => {
clearInterval(timer);
});
</script>
<template>
<PageContainer :breadcrumb="{}">
<div>
<a-drawer :visible="visible" @close="closeDrawer" :width="700">
<a-descriptions bordered :column="1" :label-style="{ width: '160px' }">
<a-descriptions-item :label="t('views.index.hostName')">{{
pronInfo.hostName
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.osInfo')">{{
pronInfo.osInfo
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.ipAddress')">{{
pronInfo.ipAddress
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.version')">{{
pronInfo.version
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.cpuUse')">{{
pronInfo.cpuUse
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.memoryUse')">{{
pronInfo.memoryUse
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.serialNum')">{{
pronInfo.serialNum
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.expiryDate')">{{
pronInfo.expiryDate
}}</a-descriptions-item>
</a-descriptions>
</a-drawer>
</div>
<a-row :gutter="16">
<a-col :lg="14" :md="16" :xs="24">
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:pagination="false"
:scroll="{ x: true }"
:customRow="rowClick"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<div v-if="record.serverState.online">
<a-tag color="blue">{{ t('views.index.normal') }}</a-tag>
</div>
<div v-else>
<a-tag color="pink">{{ t('views.index.abnormal') }}</a-tag>
</div>
</template>
</template>
</a-table>
</a-col>
<a-col :lg="10" :md="8" :xs="24">
<a-card :title="t('views.index.runStatus')" style="margin-bottom: 16px">
<div style="width: 100%; min-height: 200px" ref="statusBar"></div>
</a-card>
<a-card :title="t('views.index.mark')" style="margin-top: 16px">
<a-descriptions
bordered
:column="1"
:label-style="{ width: '160px' }"
>
<a-descriptions-item :label="t('views.index.object')">{{
nfInfo.obj
}}</a-descriptions-item>
<template v-if="nfInfo.obj === 'OMC'">
<a-descriptions-item :label="t('views.index.versionNum')">{{
nfInfo.version
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.systemStatus')">{{
nfInfo.status
}}</a-descriptions-item>
</template>
<template v-else>
<a-descriptions-item :label="t('views.index.serialNum')">{{
nfInfo.serialNum
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.expiryDate')">{{
nfInfo.outTimeDate
}}</a-descriptions-item>
</template>
</a-descriptions>
</a-card>
</a-col>
</a-row>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -425,8 +425,6 @@ function fnGetList(pageNum?: number) {
(queryParams.pageNum - 1) * tablePagination.pageSize &&
queryParams.pageNum !== 1
) {
debugger;
tableState.loading = false;
fnGetList(queryParams.pageNum - 1);
}

View File

@@ -161,6 +161,13 @@ let tableColumns: ColumnsType = [
return cdrJSON.calledParty;
},
},
{
title: t('views.dashboard.cdr.result'),
dataIndex: 'cdrJSON',
key: 'cause',
align: 'left',
width: 150,
},
{
title: t('views.dashboard.cdr.duration'),
dataIndex: 'cdrJSON',
@@ -175,20 +182,29 @@ let tableColumns: ColumnsType = [
},
},
{
title: t('views.dashboard.cdr.result'),
title: t('views.dashboard.cdr.seizureTime'),
dataIndex: 'cdrJSON',
key: 'cause',
align: 'left',
width: 150,
},
{
title: t('views.dashboard.cdr.time'),
dataIndex: 'cdrJSON',
align: 'center',
width: 150,
width: 200,
customRender(opt) {
const cdrJSON = opt.value;
return parseDateToStr(+cdrJSON.releaseTime * 1000);
if (typeof cdrJSON.seizureTime === 'number') {
return parseDateToStr(+cdrJSON.seizureTime * 1000);
}
return cdrJSON.seizureTime;
},
},
{
title: t('views.dashboard.cdr.releaseTime'),
dataIndex: 'cdrJSON',
align: 'left',
width: 200,
customRender(opt) {
const cdrJSON = opt.value;
if (typeof cdrJSON.releaseTime === 'number') {
return parseDateToStr(+cdrJSON.releaseTime * 1000);
}
return cdrJSON.releaseTime;
},
},
{
@@ -725,61 +741,73 @@ onBeforeUnmount(() => {
</template>
</template>
<template #expandedRowRender="{ record }">
<div style="width: 46%; padding-left: 32px; padding-bottom: 16px">
<a-divider orientation="left">
{{ t('views.dashboard.cdr.cdrInfo') }}
</a-divider>
<div>
<span>{{ t('views.ne.common.neName') }}: </span>
<span>{{ record.neName }}</span>
</div>
<div>
<span>{{ t('views.ne.common.rmUid') }}: </span>
<span>{{ record.rmUID }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.time') }}: </span>
<span>{{ parseDateToStr(+record.timestamp * 1000) }}</span>
</div>
<a-divider orientation="left">
{{ t('views.dashboard.cdr.rowInfo') }}
</a-divider>
<div>
<span>{{ t('views.dashboard.cdr.type') }}: </span>
<DictTag
:options="dict.cdrCallType"
:value="record.cdrJSON.callType"
/>
</div>
<div>
<span>{{ t('views.dashboard.cdr.duration') }}: </span>
<span v-if="record.cdrJSON.callType !== 'sms'">
{{ parseDuration(record.cdrJSON.callDuration) }}
</span>
<span v-else> - </span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.caller') }}: </span>
<span>{{ record.cdrJSON.callerParty }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.called') }}: </span>
<span>{{ record.cdrJSON.calledParty }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.result') }}: </span>
<span v-if="record.cdrJSON.callType !== 'sms'">
<a-row :gutter="16">
<a-col :lg="5" :md="12" :xs="24">
<a-divider orientation="left">
{{ t('views.dashboard.cdr.cdrInfo') }}
</a-divider>
<div>
<span>{{ t('views.ne.common.neName') }}: </span>
<span>{{ record.neName }}</span>
</div>
<div>
<span>{{ t('views.ne.common.rmUid') }}: </span>
<span>{{ record.rmUID }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.time') }}: </span>
<span>{{ parseDateToStr(+record.timestamp * 1000) }}</span>
</div>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-divider orientation="left">
{{ t('views.dashboard.cdr.rowInfo') }}
</a-divider>
<div>
<span>{{ t('views.dashboard.cdr.type') }}: </span>
<DictTag
:options="dict.cdrSipCode"
:value="record.cdrJSON.cause"
value-default="0"
:options="dict.cdrCallType"
:value="record.cdrJSON.callType"
/>
</span>
<span v-else>
{{ t('views.dashboard.cdr.resultOk') }}
</span>
</div>
</div>
</div>
<div>
<span>{{ t('views.dashboard.cdr.duration') }}: </span>
<span v-if="record.cdrJSON.callType !== 'sms'">
{{ parseDuration(record.cdrJSON.callDuration) }}
</span>
<span v-else> - </span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.caller') }}: </span>
<span>{{ record.cdrJSON.callerParty }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.called') }}: </span>
<span>{{ record.cdrJSON.calledParty }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.result') }}: </span>
<span v-if="record.cdrJSON.callType !== 'sms'">
<DictTag
:options="dict.cdrSipCode"
:value="record.cdrJSON.cause"
value-default="0"
/>
</span>
<span v-else>
{{ t('views.dashboard.cdr.resultOk') }}
</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.seizureTime') }}: </span>
<span>{{ record.cdrJSON.seizureTime }}</span>
</div>
<div>
<span>{{ t('views.dashboard.cdr.releaseTime') }}: </span>
<span>{{ record.cdrJSON.releaseTime }}</span>
</div>
</a-col>
</a-row>
</template>
</a-table>
</a-card>

View File

@@ -25,6 +25,9 @@ const queue = new PQueue({ concurrency: 1, autoStart: true });
/**网元可选 */
let neOtions = ref<Record<string, any>[]>([]);
/**event Type */
let mmeEventType = ref<DictType[]>([]);
/**字典数据 */
let dict: {
/**UE 事件认证代码类型 */
@@ -415,11 +418,13 @@ onMounted(() => {
dict.ueAauthCode = resArr[0].value;
}
if (resArr[1].status === 'fulfilled') {
dict.ueEventType = resArr[1].value.map(item => {
if (item.value === 'cm-state') {
item.label = item.label.replace('CM', 'ECM');
resArr[1].value.map(item => {
const realJson = JSON.parse(JSON.stringify(item));
if (realJson.value === 'cm-state') {
realJson.label = realJson.label.replace('CM', 'ECM');
}
return item;
mmeEventType.value.push(realJson);
});
}
if (resArr[2].status === 'fulfilled') {
@@ -491,7 +496,7 @@ onBeforeUnmount(() => {
<a-select
v-model:value="eventTypes"
mode="multiple"
:options="dict.ueEventType"
:options="mmeEventType"
:placeholder="t('common.selectPlease')"
@change="fnQueryEventTypeChange"
></a-select>
@@ -648,7 +653,7 @@ onBeforeUnmount(() => {
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'eventType'">
<DictTag :options="dict.ueEventType" :value="record.eventType" />
<DictTag :options="mmeEventType" :value="record.eventType" />
</template>
<template v-if="column.key === 'result'">
<span v-if="record.eventType === 'auth-result'">
@@ -705,7 +710,7 @@ onBeforeUnmount(() => {
</div>
<div>
<span>{{ t('views.dashboard.ue.eventType') }}: </span>
<DictTag :options="dict.ueEventType" :value="record.eventType" />
<DictTag :options="mmeEventType" :value="record.eventType" />
</div>
<div>
<span>{{ t('views.dashboard.ue.result') }}: </span>

View File

@@ -157,22 +157,26 @@ async function fnGetSkim() {
/**初始数据函数 */
function loadData() {
fnGetNeState(); // 获取网元状态
userActivitySend();
upfTFSend(0);
upfTFSend(7);
upfTFSend(30);
userActivitySend();
upfTFSend('0');
upfTFSend('7');
upfTFSend('30');
clearInterval(interval10s.value);
interval10s.value = setInterval(() => {
upfTFActive.value = upfTFActive.value >= 2 ? 0 : upfTFActive.value + 1;
if (upfTFActive.value === 0) {
upfTFSend(7);
} else if (upfTFActive.value === 1) {
upfTFSend(30);
} else if (upfTFActive.value === 2) {
upfTFSend(0);
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(() => {
fnGetSkim(); // 获取概览信息
fnGetNeState(); // 获取网元状态
@@ -425,12 +429,12 @@ function fnRightClick() {
<div class="filter">
<span
:data-key="v"
:class="{ active: upfTFActive === i }"
v-for="(v, i) in ['0', '7', '30']"
:class="{ active: upfTFActive === v }"
v-for="v in ['0', '7', '30']"
:key="v"
@click="
() => {
upfTFActive = i;
upfTFActive = v;
}
"
>
@@ -450,14 +454,14 @@ function fnRightClick() {
<ArrowUpOutlined style="color: #597ef7" />
{{ t('views.dashboard.overview.upfFlowTotal.up') }}
</span>
<h4>{{ upfTotalFlow[upfTFActive].up }}</h4>
<h4>{{ upfTotalFlow[upfTFActive].upFrom }}</h4>
</div>
<div class="item">
<span>
<ArrowDownOutlined style="color: #52c41a" />
{{ t('views.dashboard.overview.upfFlowTotal.down') }}
</span>
<h4>{{ upfTotalFlow[upfTFActive].down }}</h4>
<h4>{{ upfTotalFlow[upfTFActive].downFrom }}</h4>
</div>
</div>
</div>

View File

@@ -36,47 +36,63 @@ export function upfFlowParse(data: Record<string, string>) {
upfFlowData.value.lineYDown.shift();
upfFlowData.value.cap -= 1;
}
// UPF-总流量数0天 当天24小时
upfTFParse('0', {
up: upfTotalFlow.value['0'].up + +data['UPF.03'],
down: upfTotalFlow.value['0'].down + +data['UPF.06'],
});
}
type TFType = {
/**上行 N3 */
up: string;
up: number;
upFrom: string;
/**下行 N6 */
down: string;
down: number;
downFrom: string;
/**请求标记 */
requestFlag: boolean;
};
/**UPF-总流量数 */
export const upfTotalFlow = ref<TFType[]>([
// 0天 当天24小时
{
up: '0 B',
down: '0 B',
export const upfTotalFlow = ref<Record<string, TFType>>({
'0': {
up: 0,
upFrom: '0 B',
down: 0,
downFrom: '0 B',
requestFlag: false,
},
{
up: '0 B',
down: '0 B',
'7': {
up: 0,
upFrom: '0 B',
down: 0,
downFrom: '0 B',
requestFlag: false,
},
{
up: '0 B',
down: '0 B',
'30': {
up: 0,
upFrom: '0 B',
down: 0,
downFrom: '0 B',
requestFlag: false,
},
]);
});
/**UPF-总流量数 数据解析 */
export function upfTFParse(data: Record<string, string>) {
export function upfTFParse(day: string, data: Record<string, number>) {
let { up, down } = data;
up = parseSizeFromBits(up);
down = parseSizeFromBits(down);
return { up, down };
upfTotalFlow.value[day] = {
up: up,
upFrom: parseSizeFromBits(up),
down: down,
downFrom: parseSizeFromBits(down),
requestFlag: false,
};
}
/**UPF-总流量数 选中 */
export const upfTFActive = ref<number>(0);
export const upfTFActive = ref<string>('0');
/**属性复位 */
export function upfTotalFlowReset() {
@@ -86,23 +102,14 @@ export function upfTotalFlowReset() {
lineYDown: [],
cap: 0,
};
upfTotalFlow.value = [
// 0天 当天24小时
{
up: '0 B',
down: '0 B',
for (const key of Object.keys(upfTotalFlow.value)) {
upfTotalFlow.value[key] = {
up: 0,
upFrom: '0 B',
down: 0,
downFrom: '0 B',
requestFlag: false,
},
{
up: '0 B',
down: '0 B',
requestFlag: false,
},
{
up: '0 B',
down: '0 B',
requestFlag: false,
},
];
upfTFActive.value = 0;
};
}
upfTFActive.value = '0';
}

View File

@@ -25,12 +25,6 @@ export default function useWS() {
ws.send(data);
}
/**接收数据后回调 */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
// console.log(res);
@@ -50,7 +44,7 @@ export default function useWS() {
// 普通信息
switch (requestId) {
// AMF_UE会话事件
case 'amf_1010_001':
case 'amf_1010':
if (Array.isArray(data.rows)) {
eventListParse('amf_ue', data);
}
@@ -69,22 +63,13 @@ export default function useWS() {
break;
//UPF-总流量数
case 'upf_001_0':
const v0 = upfTFParse(data);
upfTotalFlow.value[0].up = v0.up;
upfTotalFlow.value[0].down = v0.down;
upfTotalFlow.value[0].requestFlag = false;
upfTFParse('0', data);
break;
case 'upf_001_7':
const v7 = upfTFParse(data);
upfTotalFlow.value[1].up = v7.up;
upfTotalFlow.value[1].down = v7.down;
upfTotalFlow.value[1].requestFlag = false;
upfTFParse('7', data);
break;
case 'upf_001_30':
const v30 = upfTFParse(data);
upfTotalFlow.value[2].up = v30.up;
upfTotalFlow.value[2].down = v30.down;
upfTotalFlow.value[2].requestFlag = false;
upfTFParse('30', data);
break;
}
@@ -100,7 +85,7 @@ export default function useWS() {
}
break;
// AMF_UE会话事件
case '1010_001':
case '1010':
if (data.data) {
queue.add(() => eventItemParseAndPush('amf_ue', data.data));
}
@@ -121,20 +106,12 @@ export default function useWS() {
}
/**UPF-总流量数 发消息*/
function upfTFSend(day: 0 | 7 | 30) {
function upfTFSend(day: '0' | '7' | '30') {
// 请求标记检查避免重复发送
let index = 0;
if (day === 0) {
index = 0;
} else if (day === 7) {
index = 1;
} else if (day === 30) {
index = 2;
}
if (upfTotalFlow.value[index].requestFlag) {
if (upfTotalFlow.value[day].requestFlag) {
return;
}
upfTotalFlow.value[index].requestFlag = true;
upfTotalFlow.value[day].requestFlag = true;
ws.send({
requestId: `upf_001_${day}`,
@@ -142,7 +119,7 @@ export default function useWS() {
data: {
neType: 'UPF',
neId: '001',
day: day,
day: Number(day),
},
});
}
@@ -151,7 +128,7 @@ export default function useWS() {
function userActivitySend() {
// AMF_UE会话事件
ws.send({
requestId: 'amf_1010_001',
requestId: 'amf_1010',
type: 'amf_ue',
data: {
neType: 'AMF',
@@ -198,14 +175,16 @@ export default function useWS() {
/**订阅通道组
*
* 指标UPF (GroupID:12_neId)
* AMF_UE会话事件(GroupID:1010_neId)
* AMF_UE会话事件(GroupID:1010)
* MME_UE会话事件(GroupID:1011_neId)
* IMS_CDR会话事件(GroupID:1005_neId)
*/
subGroupID: '12_001,1010_001,1011_001,1005_001',
subGroupID: '12_001,1010,1011_001,1005_001',
},
onmessage: wsMessage,
onerror: wsError,
onerror: (ev: any) => {
console.error(ev);
},
};
ws.connect(options);
});

View File

@@ -156,19 +156,21 @@ async function fnGetSkim() {
function loadData() {
fnGetNeState(); // 获取网元状态
userActivitySend();
upfTFSend(0);
upfTFSend(7);
upfTFSend(30);
upfTFSend('0');
upfTFSend('7');
upfTFSend('30');
clearInterval(interval10s.value);
interval10s.value = setInterval(() => {
upfTFActive.value = upfTFActive.value >= 2 ? 0 : upfTFActive.value + 1;
if (upfTFActive.value === 0) {
upfTFSend(7);
} else if (upfTFActive.value === 1) {
upfTFSend(30);
} else if (upfTFActive.value === 2) {
upfTFSend(0);
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);
@@ -411,12 +413,12 @@ onBeforeUnmount(() => {
<div class="filter">
<span
:data-key="v"
:class="{ active: upfTFActive === i }"
v-for="(v, i) in ['0', '7', '30']"
:class="{ active: upfTFActive === v }"
v-for="v in ['0', '7', '30']"
:key="v"
@click="
() => {
upfTFActive = i;
upfTFActive = v;
}
"
>
@@ -436,14 +438,14 @@ onBeforeUnmount(() => {
<ArrowUpOutlined style="color: #597ef7" />
{{ t('views.dashboard.overview.upfFlowTotal.up') }}
</span>
<h4>{{ upfTotalFlow[upfTFActive].up }}</h4>
<h4>{{ upfTotalFlow[upfTFActive].upFrom }}</h4>
</div>
<div class="item">
<span>
<ArrowDownOutlined style="color: #52c41a" />
{{ t('views.dashboard.overview.upfFlowTotal.down') }}
</span>
<h4>{{ upfTotalFlow[upfTFActive].down }}</h4>
<h4>{{ upfTotalFlow[upfTFActive].downFrom }}</h4>
</div>
</div>
</div>

View File

@@ -813,6 +813,13 @@ onBeforeUnmount(() => {
}}
</span>
</div>
<div>
<span>Network Function IPv4: </span>
<span>{{
record.cdrJSON.nFunctionConsumerInformation
.networkFunctionIPv4Address
}}</span>
</div>
</a-col>
</a-row>
</template>

View File

@@ -612,13 +612,16 @@ function fnShowSet() {
}
// key替换中文title
function mapKeysWithReduce(data: any, titleMapping: any) {
return data.map((item:any) => {
return Object.keys(item).reduce((newItem:any, key:any) => {
function mapKeysWithReduce(data: any[], titleMapping: Record<string, string>) {
return data.map((item: any) => {
if (typeof item !== 'object' || item === null) {
return item; // 如果不是对象,直接返回原值
}
return Object.keys(item).reduce((newItem: Record<string, any>, key: string) => {
const title = titleMapping[key] || key;
newItem[title] = item[key];
return newItem;
});
}, {}); // 确保初始值是一个空对象
});
}
@@ -673,6 +676,7 @@ function fnExportAll() {
return filteredObj;
});
console.log(mappArr);
res.data = mapKeysWithReduce(res.data, mappArr);
writeSheet(res.data, 'alarm', sortData).then(fileBlob => {

View File

@@ -427,13 +427,16 @@ function fnCancelConfirm() {
}
// key替换中文title
function mapKeysWithReduce(data: any, titleMapping: any) {
return data.map((item:any) => {
return Object.keys(item).reduce((newItem:any, key:any) => {
function mapKeysWithReduce(data: any[], titleMapping: Record<string, string>) {
return data.map((item: any) => {
if (typeof item !== 'object' || item === null) {
return item;
}
return Object.keys(item).reduce((newItem: Record<string, any>, key: string) => {
const title = titleMapping[key] || key;
newItem[title] = item[key];
return newItem;
});
}, {});
});
}

View File

@@ -1,446 +1,77 @@
<script setup lang="ts">
import { PageContainer } from 'antdv-pro-layout';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { message } from 'ant-design-vue/lib';
import { reactive, toRaw, ref, onMounted, onBeforeUnmount, markRaw } from 'vue';
import { listMain } from '@/api/index';
import useI18n from '@/hooks/useI18n';
import { TooltipComponent } from 'echarts/components';
import { GaugeChart } from 'echarts/charts';
import { CanvasRenderer } from 'echarts/renderers';
import * as echarts from 'echarts/core';
import { TitleComponent, LegendComponent } from 'echarts/components';
import { PieChart } from 'echarts/charts';
import { LabelLayout } from 'echarts/features';
import { useRoute } from 'vue-router';
import useAppStore from '@/store/modules/app';
import useDictStore from '@/store/modules/dict';
const { getDict } = useDictStore();
const appStore = useAppStore();
const route = useRoute();
const { t } = useI18n();
import {
defineAsyncComponent,
onMounted,
ref,
shallowRef,
type Component,
} from 'vue';
import { getConfigKey, changeValue } from '@/api/system/config';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
echarts.use([
TooltipComponent,
GaugeChart,
TitleComponent,
LegendComponent,
PieChart,
CanvasRenderer,
LabelLayout,
]);
const currentComponent = shallowRef<Component | null>(null);
/**图DOM节点实例对象 */
const statusBar = ref<HTMLElement | undefined>(undefined);
const spinning = ref<boolean>(true);
/**图实例对象 */
const statusBarChart = ref<any>(null);
/**网元状态字典数据 */
let indexColor = ref<DictType[]>([
{ label: 'Normal', value: 'normal', tagType: '', tagClass: '#91cc75' },
{
label: 'Abnormal',
value: 'abnormal',
tagType: '',
tagClass: '#ee6666',
},
]);
/**表格字段列 */
//customRender(){} ----单元格处理
let tableColumns: ColumnsType = [
{
title: t('views.index.object'),
dataIndex: 'name',
align: 'center',
key: 'status',
},
{
title: t('views.index.realNeStatus'),
dataIndex: 'status',
align: 'center',
customRender(opt) {
if (opt.value == 'Normal') return t('views.index.normal');
return t('views.index.abnormal');
},
},
{
title: t('views.index.reloadTime'),
dataIndex: 'refresh',
align: 'center',
},
{
title: t('views.index.version'),
dataIndex: 'version',
align: 'center',
},
{
title: t('views.index.serialNum'),
dataIndex: 'serialNum',
align: 'center',
},
{
title: t('views.index.expiryDate'),
dataIndex: 'expiryDate',
align: 'center',
},
{
title: t('views.index.ipAddress'),
dataIndex: 'ipAddress',
key: 'groupName',
align: 'center',
},
];
/**表格状态类型 */
type TabeStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: string;
/**搜索栏 */
seached: boolean;
/**记录数据 */
data: object[];
/**勾选记录 */
selectedRowKeys: (string | number)[];
};
/**表格状态 */
let tableState: TabeStateType = reactive({
loading: false,
size: 'middle',
seached: false,
data: [],
selectedRowKeys: [],
});
/**表格状态 */
let nfInfo: any = reactive({
obj: 'OMC',
version: appStore.version,
status: t('views.index.normal'),
outTimeDate: '',
serialNum: appStore.serialNum,
});
/**表格状态类型 */
type nfStateType = {
/**主机名 */
hostName: string;
/**操作系统信息 */
osInfo: string;
/**数据库信息 */
dbInfo: string;
/**IP地址 */
ipAddress: string;
/**端口 */
port: number;
/**版本 */
version: string;
/**CPU利用率 */
cpuUse: string;
/**内存使用 */
memoryUse: string;
/**用户容量 */
capability: number;
/**序列号 */
serialNum: string;
/**许可证到期日期 */
/* selectedRowKeys: (string | number)[];*/
expiryDate: string;
};
/**网元详细信息 */
let pronInfo: nfStateType = reactive({
hostName: '5gc',
osInfo: 'Linux 5gc 4.15.0-112-generic 2020 x86_64 GNU/Linux',
dbInfo: 'db v9.9.9',
ipAddress: '-',
port: 33030,
version: '-',
cpuUse: '-',
memoryUse: '-',
capability: 0,
serialNum: '-',
expiryDate: '-',
});
/**查询网元状态列表 */
function fnGetList(one: boolean) {
if (tableState.loading) return;
one && (tableState.loading = true);
listMain().then(res => {
tableState.data = res;
tableState.loading = false;
var rightNum = 0;
var errorNum = 0;
// if (res.length) nfInfo.serialNum = res[0].serialNum;
for (let i = 0; i < res.length; i++) {
if (res[i].status == '正常' || res[i].status == 'Normal') {
rightNum++;
} else {
errorNum++;
}
}
const optionData: any = {
title: {
text: '',
subtext: '',
left: 'center',
},
tooltip: {
trigger: 'item',
},
legend: {
orient: 'vertical',
left: 'left',
},
color: indexColor.value.map(item => item.tagClass),
series: [
{
name: t('views.index.realNeStatus'),
type: 'pie',
radius: '70%',
center: ['50%', '50%'],
data: [
{ value: rightNum, name: t('views.index.normal') },
{ value: errorNum, name: t('views.index.abnormal') },
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)',
},
},
label: {},
},
],
};
fnDesign(statusBar.value, optionData);
});
}
function fnDesign(container: HTMLElement | undefined, option: any) {
if (!container) return;
if (!statusBarChart.value) {
statusBarChart.value = markRaw(echarts.init(container, 'light'));
}
option && statusBarChart.value.setOption(option);
// 创建 ResizeObserver 实例
var observer = new ResizeObserver(entries => {
if (statusBarChart.value) {
statusBarChart.value.resize();
}
});
// 监听元素大小变化
observer.observe(container);
}
/**抽屉 网元详细信息 */
const visible = ref(false);
const closeDrawer = () => {
visible.value = false;
};
/**抽屉 网元详细信息 */
/**监听表格行事件*/
function rowClick(record: any, index: any) {
return {
onClick: (event: any) => {
if (
toRaw(record).status == '异常' ||
toRaw(record).status == 'Abnormal'
) {
message.error(t('views.index.neStatus'), 2);
return false;
} else {
let pronData = toRaw(record);
const totalMemInKB = pronData.memUsage?.totalMem;
const nfUsedMemInKB = pronData.memUsage?.nfUsedMem;
const sysMemUsageInKB = pronData.memUsage?.sysMemUsage;
// 将KB转换为MB
const totalMemInMB = Math.round((totalMemInKB / 1024) * 100) / 100;
const nfUsedMemInMB = Math.round((nfUsedMemInKB / 1024) * 100) / 100;
const sysMemUsageInMB =
Math.round((sysMemUsageInKB / 1024) * 100) / 100;
//渲染详细信息
pronInfo = {
hostName: pronData.hostName,
osInfo: pronData.osInfo,
dbInfo: pronData.dbInfo,
ipAddress: pronData.ipAddress,
port: pronData.port,
version: pronData.version,
cpuUse:
pronData.name +
':' +
pronData.cpuUsage?.nfCpuUsage / 100 +
'%; ' +
'SYS:' +
pronData.cpuUsage?.sysCpuUsage / 100 +
'%',
memoryUse:
'Total:' +
totalMemInMB +
'MB; ' +
pronData.name +
':' +
nfUsedMemInMB +
'MB; SYS:' +
sysMemUsageInMB +
'MB',
capability: pronData.capability,
serialNum: pronData.serialNum,
expiryDate: pronData.expiryDate,
};
}
visible.value = true;
},
};
}
let timer: any;
/**匹配views里面所有的.vue或.tsx文件 */
const views = import.meta.glob('../views/**/*.{vue,tsx}') as Record<
string,
() => Promise<Component>
>;
/**
* 国际化翻译转换
* 查找页面模块
* @param dirName 组件路径
* @returns 路由懒加载函数
*/
function fnLocale() {
let title = route.meta.title as string;
if (title.indexOf('router.') !== -1) {
title = t(title);
function findView(dirName: string): () => Promise<Component> {
for (const dir in views) {
let viewDirName = '';
const component = dir.match(/\/(.+)\.(vue|tsx)/);
if (component && component.length === 3) {
viewDirName = component[1];
}
if (viewDirName === dirName) {
return views[dir];
}
}
appStore.setTitle(title);
return () => import('@/views/configManage/neOverview/index.vue');
}
onMounted(() => {
getDict('index_status')
.then(res => {
if (res.length > 0) {
indexColor.value = res;
//获取当前系统设置的首页路径
getConfigKey('sys.homePage').then(res => {
spinning.value = false;
if (res.code === RESULT_CODE_SUCCESS && res.data) {
console.log(spinning);
if (res.data) {
const asyncComponent = findView(`${res.data}`);
currentComponent.value = defineAsyncComponent(asyncComponent);
}
})
.finally(() => {
fnLocale();
fnGetList(true);
timer = setInterval(() => fnGetList(false), 10000); // 每隔10秒执行一次
});
});
// 在组件卸载之前清除定时器
onBeforeUnmount(() => {
clearInterval(timer);
} else {
currentComponent.value = defineAsyncComponent(
() => import('@/views/configManage/neOverview/index.vue')
);
}
});
});
</script>
<template>
<PageContainer :breadcrumb="{}">
<div>
<a-drawer :visible="visible" @close="closeDrawer" :width="700">
<a-descriptions bordered :column="1" :label-style="{ width: '160px' }">
<a-descriptions-item :label="t('views.index.hostName')">{{
pronInfo.hostName
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.osInfo')">{{
pronInfo.osInfo
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.dbInfo')">{{
pronInfo.dbInfo
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.ipAddress')">{{
pronInfo.ipAddress
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.port')">{{
pronInfo.port
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.version')">{{
pronInfo.version
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.cpuUse')">{{
pronInfo.cpuUse
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.memoryUse')">{{
pronInfo.memoryUse
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.capability')">{{
pronInfo.capability
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.serialNum')">{{
pronInfo.serialNum
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.expiryDate')">{{
pronInfo.expiryDate
}}</a-descriptions-item>
</a-descriptions>
</a-drawer>
</div>
<a-row :gutter="16">
<a-col :lg="14" :md="16" :xs="24">
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:pagination="false"
:scroll="{ x: true }"
:customRow="rowClick"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<div v-if="record.status == '正常' || record.status == 'Normal'">
<a-tag color="blue">{{ record.name }}</a-tag>
</div>
<div v-else>
<a-tag color="pink">{{ record.name }}</a-tag>
</div>
</template>
</template>
</a-table>
</a-col>
<a-col :lg="10" :md="8" :xs="24">
<a-card :title="t('views.index.runStatus')" style="margin-bottom: 16px">
<div style="width: 100%; min-height: 200px" ref="statusBar"></div>
</a-card>
<a-card :title="t('views.index.mark')" style="margin-top: 16px">
<a-descriptions
bordered
:column="1"
:label-style="{ width: '160px' }"
>
<a-descriptions-item :label="t('views.index.object')">{{
nfInfo.obj
}}</a-descriptions-item>
<template v-if="nfInfo.obj === 'OMC'">
<a-descriptions-item :label="t('views.index.versionNum')">{{
nfInfo.version
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.systemStatus')">{{
nfInfo.status
}}</a-descriptions-item>
</template>
<template v-else>
<a-descriptions-item :label="t('views.index.serialNum')">{{
nfInfo.serialNum
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.expiryDate')">{{
nfInfo.outTimeDate
}}</a-descriptions-item>
</template>
</a-descriptions>
</a-card>
</a-col>
</a-row>
</PageContainer>
<a-spin
size="large"
:spinning="spinning"
style="
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 362px;
"
>
<component :is="currentComponent" />
</a-spin>
</template>
<style lang="less" scoped></style>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { reactive, onMounted, watch, ref, nextTick } from 'vue';
import { reactive, watch, ref } from 'vue';
import { ProModal } from 'antdv-pro-modal';
import TerminalSSHView from '@/components/TerminalSSHView/index.vue';
import useI18n from '@/hooks/useI18n';
@@ -86,19 +86,20 @@ function fnInit() {
function fnReload() {
if (!viewTerminal.value) return;
viewTerminal.value.ctrlC();
if (state.form.showType !== 'lines') {
state.form.lines = 10;
} else {
state.form.char = 0;
}
viewTerminal.value.clear();
viewTerminal.value.send('tail', {
filePath: props.filePath,
lines: state.form.lines,
char: state.form.char,
follow: state.form.follow,
});
setTimeout(() => {
viewTerminal.value.send('tail', {
filePath: props.filePath,
lines: state.form.lines,
char: state.form.char,
follow: state.form.follow,
});
}, 1000);
}
</script>
@@ -122,9 +123,11 @@ function fnReload() {
<TerminalSSHView
ref="viewTerminal"
:id="`V${Date.now()}`"
style="height: calc(100% - 36px)"
prefix="tail"
url="/ws/view"
:ne-type="neType"
:ne-id="neId"
style="height: calc(100% - 36px)"
@connect="fnInit()"
></TerminalSSHView>
<!-- 命令控制属性 -->

View File

@@ -204,11 +204,13 @@ function fnDirCD(dir: string, index?: number) {
/**网元类型选择对应修改 */
function fnNeChange(keys: any, _: any) {
if (!Array.isArray(keys)) return;
const neType = keys[0];
const neId = keys[1];
// 不是同类型时需要重新加载
if (Array.isArray(keys) && queryParams.neType !== keys[0]) {
const neType = keys[0];
if (queryParams.neType !== neType || queryParams.neId !== neId) {
queryParams.neType = neType;
queryParams.neId = keys[1];
queryParams.neId = neId;
if (neType === 'IMS') {
nePathArr.value = ['/var/log/ims'];
queryParams.search = '';

View File

@@ -40,7 +40,7 @@ let dict: {
* 测试主机连接
*/
function fnHostTest(row: Record<string, any>) {
if (modalState.confirmLoading || !row.addr) return;
if (modalState.confirmLoading || !row.addr || !row.port) return;
modalState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
testNeHost(row)
@@ -124,8 +124,8 @@ let modalState: ModalStateType = reactive({
addr: '',
port: 22,
user: 'omcuser',
authMode: '0',
password: 'a9tU53r',
authMode: '2',
password: '',
privateKey: '',
passPhrase: '',
remark: '',
@@ -283,11 +283,11 @@ function fnModalCancel() {
/**表单修改网元类型 */
function fnNeTypeChange(v: any) {
const hostsLen = modalState.from.hosts.length;
// 网元默认只含22和4100
if (hostsLen === 3 && v !== 'UPF') {
if (modalState.from.hosts.length === 3) {
modalState.from.hosts.pop();
}
const hostsLen = modalState.from.hosts.length;
// UPF标准版本可支持5002
if (hostsLen === 2 && v === 'UPF') {
modalState.from.hosts.push({
@@ -295,11 +295,27 @@ function fnNeTypeChange(v: any) {
hostType: 'telnet',
groupId: '1',
title: 'Telnet_NE_5002',
addr: '',
addr: modalState.from.ip,
port: 5002,
user: 'user',
user: 'admin',
authMode: '0',
password: 'user',
password: 'admin',
remark: '',
});
}
// UDM可支持6379
if (hostsLen === 2 && v === 'UDM') {
modalState.from.hosts.push({
hostId: undefined,
hostType: 'redis',
groupId: '1',
title: 'REDIS_NE_6379',
addr: modalState.from.ip,
port: 6379,
user: 'udmdb',
authMode: '0',
password: 'helloearth',
dbName: '0',
remark: '',
});
}
@@ -626,8 +642,13 @@ onMounted(() => {
(s:any) => !(s.hostType === 'telnet' && modalState.from.neType === 'OMC')
)"
:key="host.title"
:header="`${host.hostType.toUpperCase()} ${host.port}`"
>
<template #header>
<span v-if="host.hostType === 'redis'"> DB {{ host.port }} </span>
<span v-else>
{{ `${host.hostType.toUpperCase()} ${host.port}` }}
</span>
</template>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.neHost.addr')">
@@ -654,7 +675,22 @@ onMounted(() => {
</a-col>
</a-row>
<a-row :gutter="16">
<a-form-item
v-if="host.hostType === 'telnet'"
:label="t('views.ne.neHost.user')"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-input
v-model:value="host.user"
allow-clear
:maxlength="32"
:placeholder="t('common.inputPlease')"
>
</a-input>
</a-form-item>
<a-row :gutter="16" v-if="host.hostType === 'ssh'">
<a-col :lg="12" :md="12" :xs="24">
<a-form-item :label="t('views.ne.neHost.user')">
<a-input
@@ -672,7 +708,6 @@ onMounted(() => {
v-model:value="host.authMode"
default-value="0"
:options="dict.neHostAuthMode"
:disabled="host.hostType === 'telnet'"
>
</a-select>
</a-form-item>
@@ -692,7 +727,6 @@ onMounted(() => {
>
</a-input-password>
</a-form-item>
<template v-if="host.authMode === '1'">
<a-form-item
:label="t('views.ne.neHost.privateKey')"
@@ -722,6 +756,21 @@ onMounted(() => {
</a-form-item>
</template>
<a-form-item
v-if="host.hostType === 'mysql'"
:label="t('views.ne.neHost.database')"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-input
v-model:value="host.dbName"
allow-clear
:maxlength="32"
:placeholder="t('common.inputPlease')"
>
</a-input>
</a-form-item>
<a-form-item
:label="t('common.remark')"
:label-col="{ span: 3 }"
@@ -736,6 +785,7 @@ onMounted(() => {
/>
</a-form-item>
<!-- 测试 -->
<a-form-item
:label="t('views.ne.neHost.test')"
name="test"

View File

@@ -229,11 +229,11 @@ function fnModalOk() {
* 表单修改网元类型
*/
function fnNeTypeChange(v: any) {
const hostsLen = modalState.from.hosts.length;
// 网元默认只含22和4100
if (hostsLen === 3 && v !== 'UPF') {
if (modalState.from.hosts.length === 3) {
modalState.from.hosts.pop();
}
const hostsLen = modalState.from.hosts.length;
// UPF标准版本可支持5002
if (hostsLen === 2 && v === 'UPF') {
modalState.from.hosts.push({
@@ -249,6 +249,22 @@ function fnNeTypeChange(v: any) {
remark: '',
});
}
// UDM可支持6379
if (hostsLen === 2 && v === 'UDM') {
modalState.from.hosts.push({
hostId: undefined,
hostType: 'redis',
groupId: '1',
title: 'REDIS_NE_6379',
addr: modalState.from.ip,
port: 6379,
user: 'udmdb',
authMode: '0',
password: 'helloearth',
dbName: '0',
remark: '',
});
}
}
/**
@@ -268,7 +284,7 @@ function fnStepPrev() {
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.stepPrevTip'),
onOk() {
fnRestStepState();
fnRestStepState(t);
fnToStepName('Start');
},
});

View File

@@ -1,18 +1,14 @@
<script setup lang="ts">
import { Modal, TableColumnsType, message } from 'ant-design-vue/lib';
import { defineAsyncComponent, onMounted, reactive, ref, toRaw } from 'vue';
import { Modal, message } from 'ant-design-vue/lib';
import { defineAsyncComponent, onMounted, reactive, toRaw } from 'vue';
import { fnToStepName, stepState } from '../hooks/useStep';
import useI18n from '@/hooks/useI18n';
import { listNeVersion, operateNeVersion } from '@/api/ne/neVersion';
import { operateNeVersion } from '@/api/ne/neVersion';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import {
addNeSoftware,
listNeSoftware,
newNeVersion,
} from '@/api/ne/neSoftware';
import { listNeSoftware, newNeVersion } from '@/api/ne/neSoftware';
import { parseDateToStr } from '@/utils/date-utils';
import { ColumnsType } from 'ant-design-vue/lib/table';
const { t } = useI18n();

View File

@@ -82,7 +82,7 @@ function fnStepEnd() {
title: t('common.tipTitle'),
content: t('views.ne.neQuickSetup.licenseEndTip'),
onOk() {
fnRestStepState();
fnRestStepState(t);
},
});
}

View File

@@ -70,6 +70,7 @@ let fromState = ref({
nef_ip: '172.16.5.210',
mme_ip: '172.16.5.220',
n3iwf_ip: '172.16.5.230',
smsc_ip: '172.16.5.240',
},
});

View File

@@ -102,6 +102,9 @@ export function usePara5G() {
case 'N3IWF':
state.from.sbi.n3iwf_ip = item.ip;
break;
case 'SMSC':
state.from.sbi.smsc_ip = item.ip;
break;
}
}
}

View File

@@ -52,6 +52,7 @@ export function fnRestStepState(t?: any) {
stepState.current = -1;
stepState.neHost = {};
stepState.neInfo = {};
// 多语言翻译
if (t) {
stepState.steps = [
@@ -104,8 +105,5 @@ export function useStep({ t }: any) {
}
);
onMounted(() => {
fnRestStepState(t);
});
return { currentComponent };
}

View File

@@ -24,6 +24,7 @@ watch(
);
onMounted(() => {
fnRestStepState(t);
fnReloadData();
});

View File

@@ -316,8 +316,8 @@ function fnModalOk() {
.then(e => {
modalState.confirmLoading = true;
const from = toRaw(modalState.from);
from.neId = queryParams.neId || '-';
from.algoIndex = `${from.algoIndex}`;
from.neId = queryParams.neId || '-';
const result = from.id
? updateUDMAuth(from)
: from.num === 1
@@ -484,43 +484,34 @@ function fnRecordDelete(imsi: string) {
/**
* UDM鉴权用户勾选导出
*/
function fnRecordExport(type: string = 'txt') {
function fnRecordExport(type: string = 'txt') {
const selectLen = tableState.selectedRowKeys.length;
if (selectLen <= 0) return;
const rows: Record<string, any>[] = tableState.data.filter(
(row: Record<string, any>) =>
tableState.selectedRowKeys.indexOf(row.imsi) >= 0
);
let content = '';
if (type == 'txt') {
for (const row of rows) {
const opc = row.opc === '-' ? '' : `,${row.opc}`;
content += `${row.imsi},${row.ki},${row.algoIndex},${row.amf}${opc}\r\n`;
}
}
if (type == 'csv') {
content = `IMSI,ki,Algo Index,AMF,OPC\r\n`;
for (const row of rows) {
const opc = row.opc === '-' ? '' : `,${row.opc}`;
content += `${row.imsi},${row.ki},${row.algoIndex},${row.amf}${opc}\r\n`;
}
}
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
saveAs(blob, `UDMAuth_${Date.now()}.${type}`);
const neId = queryParams.neId;
if (!neId) return;
const hide = message.loading(t('common.loading'), 0);
exportUDMAuth({ type: type, neId: neId, imsis: tableState.selectedRowKeys })
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.msgSuccess', { msg: t('common.export') }), 3);
saveAs(res.data, `UDMAuth_select_${Date.now()}.${type}`);
} else {
message.error(`${res.msg}`, 3);
}
})
.finally(() => {
hide();
});
}
/**列表导出全部数据 */
function fnExportList(type: string) {
const neId = queryParams.neId;
if (!neId) return;
const hide = message.loading(t('common.loading'), 0);
exportUDMAuth({
neId: neId,
type: type,
})
exportUDMAuth(Object.assign({ type: type }, queryParams))
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.msgSuccess', { msg: t('common.export') }), 3);
@@ -558,6 +549,9 @@ function fnLoadData() {
fnQueryReset();
}, timerS * 1000);
} else {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
message.error({
content: t('common.getInfoFail'),
duration: 3,
@@ -737,6 +731,7 @@ onMounted(() => {
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
@change="fnGetList(1)"
/>
</a-form-item>
</a-col>
@@ -818,7 +813,7 @@ onMounted(() => {
{{ t('views.neUser.auth.import') }}
</a-button>
<a-popconfirm
<!-- <a-popconfirm
:title="t('views.neUser.auth.exportConfirm')"
placement="topRight"
ok-text="TXT"
@@ -827,6 +822,13 @@ onMounted(() => {
:show-cancel="false"
cancel-text="CSV"
@cancel="fnExportList('csv')"
> -->
<a-popconfirm
:title="t('views.neUser.auth.exportConfirm')"
placement="topRight"
ok-text="TXT"
ok-type="default"
@confirm="fnExportList('txt')"
>
<a-button type="dashed">
<template #icon><ExportOutlined /></template>

View File

@@ -29,12 +29,12 @@ const { t } = useI18n();
let neOtions = ref<Record<string, any>[]>([]);
/**表单中多选的OPTION */
const pcfRuleOption = reactive({
pccOpt: [],
sessOpt: [],
qosOpt: [],
headerOpt: [],
sarOpt: [],
const pcfRuleOption = ref<Record<string, any[]>>({
pccRules: [],
sessionRules: [],
qosTemplate: [],
headerEnrichTemplate: [],
serviceAreaRestriction: [],
});
/**查询参数 */
@@ -103,7 +103,7 @@ let tableColumns = ref<TableColumnsType>([
title: 'SAR',
dataIndex: 'sar',
align: 'left',
width: 50,
width: 150,
},
{
title: 'RFSP',
@@ -121,34 +121,54 @@ let tableColumns = ref<TableColumnsType>([
title: 'QoS Audio',
dataIndex: 'qosAudio',
align: 'left',
width: 100,
},
{
title: 'Online Billing', // 在线计费
dataIndex: 'online',
align: 'left',
width: 120,
customRender(opt) {
const status = +opt.value;
return status ? 'Enable' : 'Disable';
},
},
{
title: 'Offline Billing', // 离线计费
dataIndex: 'offline',
align: 'left',
width: 120,
customRender(opt) {
const status = +opt.value;
return status ? 'Enable' : 'Disable';
},
},
{
title: 'PCC Rules',
dataIndex: 'pccRules',
align: 'left',
resizable: true,
width: 150,
minWidth: 100,
maxWidth: 300,
},
{
title: 'PCC Rules',
dataIndex: 'pccRules',
align: 'left',
width: 120,
},
{
title: 'SESS Rules',
dataIndex: 'sessRules',
align: 'left',
width: 120,
width: 150,
},
{
title: 'HDR Enrich',
dataIndex: 'hdrEnrich',
align: 'left',
width: 100,
width: 150,
},
{
title: 'UE Policy',
dataIndex: 'uePolicy',
align: 'left',
width: 100,
width: 150,
},
{
title: t('common.operate'),
@@ -258,12 +278,8 @@ const modalStateFrom = Form.useForm(
*/
function fnModalVisibleByEdit(row?: Record<string, any>) {
getPCCRule(queryParams.neId)
.then((res: any) => {
pcfRuleOption.pccOpt = res.pccJson;
pcfRuleOption.sessOpt = res.sessJson;
pcfRuleOption.qosOpt = res.qosJson;
pcfRuleOption.headerOpt = res.headerJson;
pcfRuleOption.sarOpt = res.sarJson;
.then((data: any) => {
pcfRuleOption.value = data;
})
.finally(() => {
modalState.isBatch = false;
@@ -438,12 +454,9 @@ function fnModalCancel() {
*/
function fnModalVisibleByBatch(type: 'delete' | 'add' | 'update') {
getPCCRule(queryParams.neId)
.then((res: any) => {
pcfRuleOption.pccOpt = res.pccJson;
pcfRuleOption.sessOpt = res.sessJson;
pcfRuleOption.qosOpt = res.qosJson;
pcfRuleOption.headerOpt = res.headerJson;
pcfRuleOption.sarOpt = res.sarJson;
.then((data: any) => {
pcfRuleOption.value = data;
console.log(data);
})
.finally(() => {
modalStateFrom.resetFields(); //重置表单
@@ -767,9 +780,6 @@ onMounted(() => {
ok-text="TXT"
ok-type="default"
@confirm="fnExportList('txt')"
:show-cancel="false"
cancel-text="CSV"
@cancel="fnExportList('csv')"
>
<a-button type="dashed">
<template #icon><ExportOutlined /></template>
@@ -1021,7 +1031,7 @@ onMounted(() => {
v-model:value="modalState.from.pccRules"
allow-clear
mode="tags"
:options="pcfRuleOption.pccOpt"
:options="pcfRuleOption.pccRules"
:title="t('views.neUser.pcf.pccRuleTip')"
/>
</a-form-item>
@@ -1032,7 +1042,7 @@ onMounted(() => {
v-model:value="modalState.from.sessRules"
allow-clear
mode="tags"
:options="pcfRuleOption.sessOpt"
:options="pcfRuleOption.sessionRules"
:title="t('views.neUser.pcf.sessRuleTip')"
/>
</a-form-item>
@@ -1045,7 +1055,7 @@ onMounted(() => {
<a-auto-complete
v-model:value="modalState.from.qosAudio"
allow-clear
:options="pcfRuleOption.qosOpt"
:options="pcfRuleOption.qosTemplate"
:filter-option="filterOption"
/>
</a-form-item>
@@ -1055,7 +1065,7 @@ onMounted(() => {
<a-auto-complete
v-model:value="modalState.from.qosVideo"
allow-clear
:options="pcfRuleOption.qosOpt"
:options="pcfRuleOption.qosTemplate"
:filter-option="filterOption"
/>
</a-form-item>
@@ -1068,7 +1078,7 @@ onMounted(() => {
<a-auto-complete
v-model:value="modalState.from.hdrEnrich"
allow-clear
:options="pcfRuleOption.headerOpt"
:options="pcfRuleOption.headerEnrichTemplate"
:filter-option="filterOption"
/>
</a-form-item>
@@ -1099,7 +1109,7 @@ onMounted(() => {
<a-auto-complete
v-model:value="modalState.from.sar"
allow-clear
:options="pcfRuleOption.sarOpt"
:options="pcfRuleOption.serviceAreaRestriction"
:filter-option="filterOption"
/>
</a-form-item>

View File

@@ -118,12 +118,6 @@ let tableColumns = ref<ColumnsType>([
align: 'center',
width: 100,
},
{
title: 'RAT',
dataIndex: 'rat',
align: 'center',
width: 50,
},
{
title: 'Forbidden Areas',
dataIndex: 'arfb',
@@ -252,19 +246,24 @@ let modalState: ModalStateType = reactive({
title: 'UDM签约用户',
from: {
id: undefined,
num: 1,
msisdn: '',
neId: '',
imsi: '',
msisdn: '',
// amDat
ambr: 'def_ambr',
nssai: 'def_nssai',
rat: '0',
rat: '0', // 0x00:VIRTUAL 0x01:WLAN 0x02:EUTRA 0x03:NR
arfb: 'def_arfb',
sar: 'def_sar',
cn: '3',
smData: '',
smfSel: 'def_snssai',
epsDat: '',
neId: '',
cnType: '3', // 0x00:EPC和5GC 0x01:5GC 0x02:EPC 0x03:EPC+5GC
rfspIndex: 1,
regTimer: 12000,
ueUsageType: 1,
activeTime: 1000,
mico: '0',
odbPs: '1',
groupId: '-',
// epsDat
epsFlag: '1',
epsOdb: [2],
hplmnOdb: [3, 4],
@@ -273,9 +272,12 @@ let modalState: ModalStateType = reactive({
contextId: '1',
apnContext: [1, 2, 0, 0, 0, 0],
staticIp: '-',
rfsp: 1,
ueType: 1,
//
smData: '',
smfSel: 'def_snssai',
cag: 'def_cag',
// 非字段
num: 1,
remark: '',
},
BatchDelForm: {
@@ -623,6 +625,10 @@ function fnModalOk() {
.map((item: number) => `${item}`.padStart(2, '0'))
.join('');
from.activeTime = `${from.activeTime}`;
from.rfspIndex = `${from.rfspIndex}`;
from.regTimer = `${from.regTimer}`;
from.ueUsageType = `${from.ueUsageType}`;
from.neId = queryParams.neId || '-';
const result = from.id
? updateUDMSub(from)
@@ -825,50 +831,30 @@ function fnRecordDelete(imsi: string) {
/**
* UDM签约用户导出
*/
function fnRecordExport(type: string = 'txt') {
function fnRecordExport(type: string = 'txt') {
const selectLen = tableState.selectedRowKeys.length;
if (selectLen <= 0) return;
const rows: Record<string, any>[] = tableState.data.filter(
(row: Record<string, any>) =>
tableState.selectedRowKeys.indexOf(row.imsi) >= 0
);
let content = '';
if (type == 'txt') {
for (const row of rows) {
debugger;
const epsDat = [
row.epsFlag,
row.epsOdb,
row.hplmnOdb,
row.ard,
row.epstpl,
row.contextId,
row.apnContext,
row.staticIp,
].join(',');
content += `${row.imsi},${row.msisdn},${row.ambr},${row.nssai},${row.arfb},${row.sar},${row.rat},${row.cn},${row.smfSel},${row.smData},${epsDat}\r\n`;
}
}
if (type == 'csv') {
content = `imsi,msisdn,ambr,nssai,arfb,sar,rat,cn,smf_sel,sm_dat,eps_dat\r\n`;
for (const row of rows) {
const epsDat = [
row.epsFlag,
row.epsOdb,
row.hplmnOdb,
row.ard,
row.epstpl,
row.contextId,
row.apnContext,
row.staticIp,
].join(',');
content += `${row.imsi},${row.msisdn},${row.ambr},${row.nssai},${row.arfb},${row.sar},${row.rat},${row.cn},${row.smfSel},${row.smData},${epsDat}\r\n`;
}
}
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
saveAs(blob, `UDMSub_${Date.now()}.${type}`);
const neId = queryParams.neId;
if (!neId) return;
const hide = message.loading(t('common.loading'), 0);
exportUDMSub({ type: type, neId: neId, imsis: tableState.selectedRowKeys })
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', { msg: t('common.export') }),
duration: 2,
});
saveAs(res.data, `UDMSub_select_${Date.now()}.${type}`);
} else {
message.error({
content: `${res.msg}`,
duration: 2,
});
}
})
.finally(() => {
hide();
});
}
/**列表导出 */
@@ -877,10 +863,7 @@ function fnExportList(type: string) {
if (!neId) return;
const key = 'exportSub';
message.loading({ content: t('common.loading'), key });
exportUDMSub({
neId: neId,
type: type,
}).then(res => {
exportUDMSub(Object.assign({ type: type }, queryParams)).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', { msg: t('common.export') }),
@@ -922,6 +905,9 @@ function fnLoadData() {
fnQueryReset();
}, timerS * 1000);
} else {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
message.error({
content: t('common.getInfoFail'),
duration: 3,
@@ -1116,6 +1102,7 @@ onMounted(() => {
v-model:value="queryParams.neId"
:options="neOtions"
:placeholder="t('common.selectPlease')"
@change="fnGetList(1)"
/>
</a-form-item>
</a-col>
@@ -1211,7 +1198,7 @@ onMounted(() => {
{{ t('views.neUser.sub.import') }}
</a-button>
<a-popconfirm
<!-- <a-popconfirm
:title="t('views.neUser.sub.exportConfirm')"
placement="topRight"
ok-text="TXT"
@@ -1220,6 +1207,13 @@ onMounted(() => {
:show-cancel="false"
cancel-text="CSV"
@cancel="fnExportList('csv')"
> -->
<a-popconfirm
:title="t('views.neUser.sub.exportConfirm')"
placement="topRight"
ok-text="TXT"
ok-type="default"
@confirm="fnExportList('txt')"
>
<a-button type="dashed">
<template #icon>
@@ -1333,7 +1327,7 @@ onMounted(() => {
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'cnFlag'">
{{
record.cn === '3'
['1', '3'].includes(record.cnType)
? t('views.neUser.sub.enable')
: t('views.neUser.sub.disable')
}}
@@ -1599,7 +1593,7 @@ onMounted(() => {
name="cnFlag"
:help="t('views.neUser.sub.cnFlag')"
>
<a-select v-model:value="modalState.from.cn">
<a-select v-model:value="modalState.from.cnType">
<a-select-option value="3">
{{ t('views.neUser.sub.enable') }}
</a-select-option>
@@ -1753,7 +1747,7 @@ onMounted(() => {
name="mico"
:help="t('views.neUser.sub.micoTip')"
>
<a-select value="1">
<a-select v-model:value="modalState.from.mico">
<a-select-option value="1">
{{ t('views.neUser.sub.enable') }}
</a-select-option>
@@ -1764,9 +1758,19 @@ onMounted(() => {
</a-form-item>
</a-col>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item label="5G UE Usage Type" name="ueType">
<a-form-item label="5G RAT Mode" name="rat">
<a-select v-model:value="modalState.from.rat">
<a-select-option value="0">VIRTUAL</a-select-option>
<a-select-option value="1">WLAN</a-select-option>
<a-select-option value="2">EUTRA</a-select-option>
<a-select-option value="3">NR</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item label="5G UE Usage Type" name="ueUsageType">
<a-input-number
v-model:value="modalState.from.ueType"
v-model:value="modalState.from.ueUsageType"
style="width: 100%"
:min="0"
:max="127"
@@ -1786,9 +1790,9 @@ onMounted(() => {
</a-form-item>
</a-col>
<a-col :lg="24" :md="24" :xs="24">
<a-form-item label="5G RFSP Index" name="rfsp">
<a-form-item label="5G RFSP Index" name="rfspIndex">
<a-input-number
v-model:value="modalState.from.rfsp"
v-model:value="modalState.from.rfspIndex"
style="width: 100%"
:min="0"
:max="127"

View File

@@ -647,7 +647,11 @@ onMounted(() => {
name="title"
v-bind="modalStateFrom.validateInfos.title"
>
<a-input v-model:value="modalState.from.title" allow-clear>
<a-input
v-model:value="modalState.from.title"
:maxlength="255"
allow-clear
>
</a-input>
</a-form-item>
</a-col>
@@ -657,7 +661,11 @@ onMounted(() => {
name="kpiId"
v-bind="modalStateFrom.validateInfos.kpiId"
>
<a-input v-model:value="modalState.from.kpiId" allow-clear>
<a-input
v-model:value="modalState.from.kpiId"
:maxlength="16"
allow-clear
>
</a-input>
</a-form-item>
</a-col>
@@ -669,7 +677,11 @@ onMounted(() => {
:label-col="{ span: 3 }"
v-bind="modalStateFrom.validateInfos.expression"
>
<a-input v-model:value="modalState.from.expression" allow-clear>
<a-input
v-model:value="modalState.from.expression"
:maxlength="1024"
allow-clear
>
</a-input>
</a-form-item>

View File

@@ -251,14 +251,14 @@ function fnGetListTitle() {
// 获取表头文字
getKPITitle(state.neType[0])
.then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
tableColumns.value = [];
const columns: ColumnsType = [];
for (const item of res.data) {
const kpiDisplay = item[`${language}Title`];
.then(res => {//处理getKPITitle返回的结果
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {//检查值
tableColumns.value = [];//设为空数组
const columns: ColumnsType = [];//初始化,构建新表头
for (const item of res.data) {//遍历res.data
const kpiDisplay = item[`${language}Title`];//提取标题kpiDisplay和ID标识kpiValue
const kpiValue = item[`kpiId`];
columns.push({
columns.push({//
title: kpiDisplay,
dataIndex: kpiValue,
align: 'left',
@@ -297,7 +297,7 @@ function fnGetListTitle() {
return false;
}
})
.then(result => {
.then(result => {//result是前一个.then返回的值(true or false)
result && fnGetList();
});
}
@@ -334,27 +334,27 @@ function fnChangShowType() {
/**绘制图表 */
function fnRanderChart() {
const container: HTMLElement | undefined = kpiChartDom.value;
if (!container) return;
const container: HTMLElement | undefined = kpiChartDom.value;//获取图表容器DOM元素
if (!container) return;//若没有,则退出函数
kpiChart.value = markRaw(echarts.init(container, 'light'));
const option: EChartsOption = {
//初始化Echarts图表实例应用light主题并赋值给kpiChart.valuemarkRaw是vue函数用于标记对象为不可响应
const option: EChartsOption = {//定义图表的配置对象tooltip的出发方式为axis
tooltip: {
trigger: 'axis',
position: function (pt: any) {
return [pt[0], '10%'];
},
},
xAxis: {
xAxis: {//x类别轴
type: 'category',
boundaryGap: false,
data: [], // 数据x轴
},
yAxis: {
yAxis: {//y类别轴
type: 'value',
boundaryGap: [0, '100%'],
},
legend: {
legend: {//图例垂直滚动
type: 'scroll',
orient: 'vertical',
top: 40,
@@ -367,13 +367,13 @@ function fnRanderChart() {
icon: 'circle',
selected: {},
},
grid: {
grid: {//网格区域边距
left: '10%',
right: '30%',
bottom: '20%',
},
dataZoom: [
{
{//启用图表的数据缩放范围90%-100%
type: 'inside',
start: 90,
end: 100,
@@ -385,9 +385,9 @@ function fnRanderChart() {
],
series: [], // 数据y轴
};
kpiChart.value.setOption(option);
kpiChart.value.setOption(option);//设置图表配置项应用到kpiChart实例上
// 创建 ResizeObserver 实例
// 创建 ResizeObserver 实例 监听图表容器大小变化,并在变化时调整图表大小
var observer = new ResizeObserver(entries => {
if (kpiChart.value) {
kpiChart.value.resize();
@@ -452,6 +452,7 @@ function fnRanderChartData() {
// 用降序就反转
let orgData = tableState.data;
console.log(orgData);
if (queryParams.sortOrder === 'desc') {
orgData = orgData.toReversed();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,791 @@
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue';
import * as echarts from 'echarts/core';
import { LegendComponent } from 'echarts/components';
import { LineChart, BarChart } from 'echarts/charts';
import { GridComponent, TooltipComponent, TitleComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';
import { getKPITitle, listKPIData } from '@/api/perfManage/goldTarget';
import useI18n from '@/hooks/useI18n';
import { message, Modal } from 'ant-design-vue';
import { RESULT_CODE_ERROR, RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import { generateColorRGBA } from '@/utils/generate-utils';
import { BarChartOutlined, LineChartOutlined, UnorderedListOutlined, DownOutlined, MoreOutlined } from '@ant-design/icons-vue';
// 在这里定义 ChartDataItem 接口
interface ChartDataItem {
date: string; // 将存储完整的时间字符串,包含时分秒
[kpiId: string]: string | number; // 动态指标
}
echarts.use([
LineChart,
BarChart,
GridComponent,
TooltipComponent,
TitleComponent,
CanvasRenderer,
LegendComponent
]);
// WebSocket连接
const ws = ref<WS | null>(null);
//日期范围响应式变量
const dateRange = ref<[string, string]>([
dayjs().startOf('day').valueOf().toString(),
dayjs().valueOf().toString()
]);
//实时数据状态
const isRealtime = ref(false);
//图表数据响应式数组
const chartData = ref<ChartDataItem[]>([]);
//储存Echarts的实例的变量
let chart: echarts.ECharts | null = null;
//observer 变量 监听图表容器大小
let observer: ResizeObserver | null = null;
//日期变化时更新图表数据
const handleDateChange = (
value: [string, string] | [Dayjs, Dayjs],
dateStrings: [string, string]
) => {
if (!dateStrings[0] || !dateStrings[1]) {
console.warn('Invalid date strings:', dateStrings);
return;
}
dateRange.value = [
dayjs(dateStrings[0]).valueOf().toString(),
dayjs(dateStrings[1]).valueOf().toString()
];
fetchChartData();
};
//切换实时数据
const toggleRealtime = () => {
fnRealTimeSwitch(isRealtime.value);
};
// 定义所有网元类型
const ALL_NE_TYPES = ['AMF', 'SMF', 'UPF', 'MME', 'IMS', 'SMSC'] as const;
type NeType = typeof ALL_NE_TYPES[number];
// 定义要筛选的指标 ID按网元类型组织
const TARGET_KPI_IDS: Record<NeType, string[]> = {
AMF: ['AMF.02', 'AMF.03', 'AMF.A.07', 'AMF.A.08'],
SMF: ['SMF.02', 'SMF.03', 'SMF.04', 'SMF.05'],
UPF: ['UPF.03', 'UPF.04', 'UPF.05', 'UPF.06'],
MME: ['MME.A.01', 'MME.A.02', 'MME.A.03'],
IMS: ['SCSCF.01', 'SCSCF.02', 'SCSCF.05', 'SCSCF.06'],
SMSC: ['SMSC.A.01', 'SMSC.A.02', 'SMSC.A.03']
};
// 实时数据开关函数
const fnRealTimeSwitch = (bool: boolean) => {
if (bool) {
if(!ws.value) {
ws.value = new WS();
}
chartData.value = [];
const options: OptionsType = {
url: '/ws',
params: {
subGroupID: ALL_NE_TYPES.map(type => `10_${type}_001`).join(','),
},
onmessage: wsMessage,
onerror: wsError,
};
ws.value.connect(options);
} else if(ws.value) {
ws.value.close();//断开链接
ws.value = null;//清空链接
}
}
// 接收数据后错误回调
const wsError = () => {
message.error(t('common.websocketError'));
}
// 接收数据后回调
const wsMessage = (res: Record<string, any>) => {
const { code, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
if (!data?.groupId) {
return;
}
const kpiEvent = data.data;
if (!kpiEvent) {
return;
}
// 构造新的数据点
const newData: ChartDataItem = {
date: kpiEvent.timeGroup
? kpiEvent.timeGroup.toString()
: Date.now().toString()
};
// 只添加已选中的指标的数据
selectedKPIs.value.forEach(kpiId => {
if (kpiEvent[kpiId] !== undefined) {
newData[kpiId] = Number(kpiEvent[kpiId]);
} else {
newData[kpiId] = 0;
}
});
// 更新数据
updateChartData(newData);
};
// 获取图表数据方法
const fetchChartData = async () => {
if (kpiColumns.value.length === 0) {
console.warn('No KPI columns available');
updateChart();
return;
}
try {
const [startTime, endTime] = dateRange.value;
if (!startTime || !endTime) {
console.warn('Invalid date range:', dateRange.value);
return;
}
const allData: any[] = [];
// 使用 ALL_NE_TYPES 遍历网元类型
for (const neType of ALL_NE_TYPES) {
const params = {
neType,
neId: '001',
startTime: String(startTime),
endTime: String(endTime),
sortField: 'timeGroup',
sortOrder: 'asc',
interval: 5,
kpiIds: TARGET_KPI_IDS[neType].join(',')
};
const res = await listKPIData(params);
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
allData.push(...res.data);
}
}
// 按时间分组合数据
const groupedData = new Map<string, any>();
allData.forEach(item => {
const timeKey = item.timeGroup;
if (!groupedData.has(timeKey)) {
groupedData.set(timeKey, { timeGroup: timeKey });
}
const existingData = groupedData.get(timeKey);
Object.assign(existingData, item);
});
// 直接将处理后的数据赋值给 chartData.value
chartData.value = Array.from(groupedData.values())
.sort((a, b) => Number(a.timeGroup) - Number(b.timeGroup))
.map(item => {
const dataItem: ChartDataItem = {
date: item.timeGroup.toString(),
};
kpiColumns.value.forEach(kpi => {
dataItem[kpi.kpiId] = Number(item[kpi.kpiId]) || 0;
});
return dataItem;
});
updateChart();
} catch (error) {
console.error('Failed to fetch chart data:', error);
message.error(t('common.getInfoFail'));
}
};
// 添加一个 Map 来存储每个指标的临时固定颜色
const kpiColors = new Map<string, string>();
// 定义图表类型的响应式变量
const chartType = ref<'line' | 'bar'>('line');
// 添加切换图表类型的方法
const toggleChartType = () => {
chartType.value = chartType.value === 'line' ? 'bar' : 'line';
updateChart();
};
// 更新图表
const updateChart = () => {
if (!chart || !kpiColumns.value.length) return;//首先检查图表实例和指标是否存在
//过滤出已选择的指标列
const filteredColumns = kpiColumns.value.filter(col => selectedKPIs.value.includes(col.kpiId));
const legendData = filteredColumns.map(item => item.title);//创建图例数据数组,包含所有选中的指标的标题
//为每个选中的指标创建一个系列配置
const series = filteredColumns.map(item => {
const color = kpiColors.get(item.kpiId) || generateColorRGBA();
if (!kpiColors.has(item.kpiId)) {
kpiColors.set(item.kpiId, color);//保持指标颜色的临时一致性
}
return {
name: item.title,
type: chartType.value, // 使用当前选择的图表类型
data: chartData.value.length > 0
? chartData.value.map(dataItem => dataItem[item.kpiId] || 0)
: [0],
smooth: chartType.value === 'line', // 只在折线图时使用平滑
symbol: chartType.value === 'line' ? 'circle' : undefined, // 只在折线图时显示标记
symbolSize: chartType.value === 'line' ? 6 : undefined,
showSymbol: chartType.value === 'line',
itemStyle: { color }
};
});
//图表配置对象
const option = {
title: {
text: t('views.perfManage.kpiOverView.kpiChartTitle'),
left: 'center'
},
tooltip: {
trigger: 'axis',
position: function(pt: any) {
return [pt[0], '10%'];
},
},
legend: {//图例配置
data: legendData,
type: 'scroll',
orient: 'horizontal',
top: 25,
textStyle: {
fontSize: 12
},
selected: Object.fromEntries(legendData.map(name => [name, true])),
show: true,
left: 'center',
width: '80%',
height: 50,
padding: [5, 10],
},
grid: { //网格配置
left: '3%',
right: '4%',
bottom: '3%',
top: 100,
containLabel: true
},
xAxis: { //x轴配置
type: 'category',
boundaryGap: false,
data: chartData.value.length > 0
? chartData.value.map(item => {
// 将时间戳转换为包含时分秒的格式
return dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss');
})
: [''],
axisLabel: {
formatter: (value: string) => {
// 自定义 x 轴标签的显示格式
return dayjs(value).format('YYYY-MM-DD HH:mm:ss');
},
rotate: 0,
interval: 'auto', // 自动计算显示间隔
align: 'right'
}
},
yAxis: { // y轴配置
type: 'value',
axisLabel: {
formatter: '{value}'
},
// 添加自动计算的分割段数
splitNumber: 5,
// 添加自动计算的最小/最大值围
scale: true
},
series: series //配置数据
};
chart.setOption(option);//使用新的配置更新图表
chart.resize();//调整图表大小以适应容器
// 如果已经有 observer先断开连接
if (observer) {
observer.disconnect();
}
// 创建新的 ResizeObserver
observer = new ResizeObserver(() => {
if (chart) {
chart.resize();
}
});
// 观察图表容器
const container = document.getElementById('chartContainer');
if (container) {
observer.observe(container);
}
};
//钩子函数
onMounted(async () => {
try {
// 获取所有网元的指标
await fetchSpecificKPI();
await nextTick();
const container = document.getElementById('chartContainer');
if (container && !chart) {
chart = echarts.init(container);
if (kpiColumns.value.length > 0) {
updateChart();
await fetchChartData();
} else {
console.warn('No KPI columns available after fetching');
}
} else if (chart) {
console.warn('Chart already initialized, skipping initialization');
} else {
console.error('Chart container not found');
}
} catch (error) {
console.error('Failed to initialize:', error);
message.error(t('common.initFail'));
}
});
// 定义指列类型
interface KPIColumn {
title: string;
dataIndex: string;
key: string;
kpiId: string;
neType: string; // 添加网元类型字段
}
// 存储指标列信
const kpiColumns = ref<KPIColumn[]>([]);
// 添加选中指标的的状态
const selectedKPIs = ref<string[]>([]);
// 添加对话框可见性状态
const isModalVisible = ref(false);
// 添加临时存储下拉框选择的数组
const tempSelectedKPIs = ref<string[]>([]);
// 添加一个变量保存打开对话框时的选择状态
const originalSelectedKPIs = ref<string[]>([]);
// 打开对话框的方法
const showKPISelector = () => {
// 保存当前的选择状态
originalSelectedKPIs.value = [...selectedKPIs.value];
// 初始化临时选择为当前已选择的其他指标
const primaryKPIs = Object.values(TARGET_KPI_IDS).flat();
tempSelectedKPIs.value = selectedKPIs.value.filter(kpiId =>
!primaryKPIs.includes(kpiId)
);
isModalVisible.value = true;
};
// 保存选中指标到 localStorage 的方法
const saveSelectedKPIs = () => {
localStorage.setItem('selectedKPIs', JSON.stringify(selectedKPIs.value));
};
// 取消按钮的处理方法
const handleModalCancel = () => {
// 恢复到打开对话框时的选择状态
selectedKPIs.value = [...originalSelectedKPIs.value];
// 清空临时选择
tempSelectedKPIs.value = [];
isModalVisible.value = false;
};
// 确认按钮的处理方法
const handleModalOk = () => {
// 获取主要指标列表
const primaryKPIs = Object.values(TARGET_KPI_IDS).flat();
// 获取当前在主界面选中的主要指标
const selectedPrimaryKPIs = selectedKPIs.value.filter(kpiId =>
primaryKPIs.includes(kpiId)
);
// 合并选中的主要指标和临时选中的其他指标
selectedKPIs.value = Array.from(new Set([
...selectedPrimaryKPIs, // 只包含已选中的主要指标
...tempSelectedKPIs.value // 临时选中的其他指标
]));
// 清空临时选择和原始选择
tempSelectedKPIs.value = [];
originalSelectedKPIs.value = [];
// 保存选择并更新图表
saveSelectedKPIs();
updateChart();
isModalVisible.value = false;
};
// 获取网元指标
const fetchSpecificKPI = async () => {
const language = currentLocale.value.split('_')[0] === 'zh' ? 'cn' : currentLocale.value.split('_')[0];
try {
let allKPIs: KPIColumn[] = [];
// 1. 获取所有网元的全部指标
for (const neType of ALL_NE_TYPES) {
const res = await getKPITitle(neType.toUpperCase());
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// 转换指标格式
const formattedKPIs = res.data.map(item => ({
title: item[`${language}Title`],
dataIndex: item.kpiId,
key: item.kpiId,
kpiId: item.kpiId,
neType: neType // 添加网元类型信息
}));
// 添加到所有指标数组
allKPIs = [...allKPIs, ...formattedKPIs];
}
}
// 2. 更新所有指标到 kpiColumns
kpiColumns.value = allKPIs;
// 3. 尝试加载保存的选择
const savedKPIs = localStorage.getItem('selectedKPIs');
if (savedKPIs) {
// 确保保存的选择仍然存在于当前指标中
const validSavedKPIs = JSON.parse(savedKPIs).filter(
(kpiId: string) => kpiColumns.value.some(col => col.kpiId === kpiId)
);
if (validSavedKPIs.length > 0) {
selectedKPIs.value = validSavedKPIs;
} else {
// 如果没有有效的保存选择则默认选择<E98089><E68BA9>要指标
selectedKPIs.value = Object.values(TARGET_KPI_IDS).flat();
}
} else {
// 如果没有保存的选择,则默认选择重要指标
selectedKPIs.value = Object.values(TARGET_KPI_IDS).flat();
}
if (kpiColumns.value.length === 0) {
console.warn('No KPIs found');
} else {
console.log(`Found ${kpiColumns.value.length} total KPIs`);
}
return kpiColumns.value;
} catch (error) {
console.error('Failed to fetch KPI titles:', error);
message.error(t('common.getInfoFail'));
return [];
}
};
// onUnmounted 钩子
onUnmounted(() => {
if(ws.value && ws.value.state() === WebSocket.OPEN) {
ws.value.close();
}
if (observer) {
observer.disconnect();
observer = null;
}
if (chart) {
chart.dispose();
chart = null;
}
// 可选:在组件卸载时保存选择
saveSelectedKPIs();
});
const { t, currentLocale } = useI18n();
// 更新图表数据方法
const updateChartData = (newData: ChartDataItem) => {
chartData.value.push(newData);
if (chartData.value.length > 100) {
chartData.value.shift();
}
if (chart) {
chart.setOption({
xAxis: {
data: chartData.value.map(item =>
dayjs(Number(item.date)).format('YYYY-MM-DD HH:mm:ss')
)
},
series: selectedKPIs.value.map(kpiId => ({
type: chartType.value, // 使用当前选择的图表类型
data: chartData.value.map(item => item[kpiId] || 0)
}))
});
}
};
// groupedKPIs 计算属性,使用 TARGET_KPI_IDS 来分组过滤
const groupedKPIs = computed(() => {
const groups: Record<string, KPIColumn[]> = {};
ALL_NE_TYPES.forEach(neType => {
// 使用 TARGET_KPI_IDS 中定义的指标 ID 来过滤
const targetIds = TARGET_KPI_IDS[neType];
groups[neType] = kpiColumns.value.filter(kpi =>
targetIds.includes(kpi.kpiId)
);
});
return groups;
});
// 计算其他指标
const secondaryKPIs = computed(() => {
const groups: Record<string, KPIColumn[]> = {};
if (kpiColumns.value.length === 0) {
console.warn('No KPI columns available');
return groups;
}
ALL_NE_TYPES.forEach(neType => {
// 获取当前网元类型的主要指标 ID
const primaryIds = TARGET_KPI_IDS[neType];
// 从所有指标中筛选出当前网元其他指标
groups[neType] = kpiColumns.value.filter(kpi => {
// 检查是否不在主要指标列表中
const isNotPrimary = !primaryIds.includes(kpi.kpiId);
// 检查是否属于当前网元类型
// 使用 getKPITitle API 返回的原始数据中的网元类型信息
const isCurrentNeType = kpi.neType === neType;
return isCurrentNeType && isNotPrimary;
});
});
return groups;
});
// 添加处理其他指标选择变化的方法
const handleSecondaryKPIChange = (kpiId: string, checked: boolean) => {
if (checked) {
// 如果选中,将指标 ID 添加到临时列表
if (!tempSelectedKPIs.value.includes(kpiId)) {
tempSelectedKPIs.value = [...tempSelectedKPIs.value, kpiId];
}
} else {
// 如果取消选中,从临时列表中移除指标 ID
tempSelectedKPIs.value = tempSelectedKPIs.value.filter(id => id !== kpiId);
}
};
</script>
<template>
<div class="kpi-overview">
<div class="controls">
<a-range-picker
v-model:value="dateRange"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
:value-format="'x'"
:disabled="isRealtime"
@change="handleDateChange"
/>
<a-button @click="showKPISelector">
<template #icon>
<unordered-list-outlined />
</template>
{{t('views.perfManage.kpiOverView.chooseMetrics')}}
</a-button>
<a-button @click="toggleChartType">
<template #icon>
<bar-chart-outlined v-if="chartType === 'line'" />
<line-chart-outlined v-else />
</template>
{{ chartType === 'line' ? t('views.perfManage.kpiOverView.changeLine') : t('views.perfManage.kpiOverView.changeBar') }}
</a-button>
<a-form-item :label="isRealtime ? t('views.dashboard.cdr.realTimeDataStart') : t('views.dashboard.cdr.realTimeDataStop')">
<a-switch
v-model:checked="isRealtime"
@change="toggleRealtime"
/>
</a-form-item>
</div>
<div id="chartContainer" class="chart-container"></div>
<!-- 修改指标选择对话框 -->
<a-modal
v-model:visible="isModalVisible"
title="选择要显示的指标"
@ok="handleModalOk"
@cancel="handleModalCancel"
width="800px"
:bodyStyle="{ maxHeight: '600px', overflow: 'auto' }"
>
<a-checkbox-group v-model:value="selectedKPIs">
<div class="kpi-checkbox-list">
<a-card
v-for="neType in ALL_NE_TYPES"
:key="neType"
class="ne-type-card"
:bordered="false"
>
<template #title>
<span class="card-title">{{ neType.toUpperCase() }}</span>
</template>
<template #extra>
<a-dropdown v-if="secondaryKPIs[neType]?.length" trigger="click">
<a-button type="link" size="small">
<more-outlined />
<down-outlined />
<span class="secondary-count">({{ secondaryKPIs[neType].length }})</span>
</a-button>
<template >
<div class="secondary-kpi-menu" @click.stop>
<div
v-for="kpi in secondaryKPIs[neType]"
:key="kpi.kpiId"
class="secondary-kpi-item"
@click.stop
>
<a-checkbox
:value="kpi.kpiId"
:checked="tempSelectedKPIs.includes(kpi.kpiId)"
@change="(e) => handleSecondaryKPIChange(kpi.kpiId, e.target.checked)"
@click.stop
>
{{ kpi.title }}
</a-checkbox>
</div>
</div>
</template>
</a-dropdown>
</template>
<div class="ne-type-items">
<div
v-for="kpi in groupedKPIs[neType]"
:key="kpi.kpiId"
class="kpi-checkbox-item"
>
<a-checkbox :value="kpi.kpiId">
{{ kpi.title }}
</a-checkbox>
</div>
</div>
</a-card>
</div>
</a-checkbox-group>
</a-modal>
</div>
</template>
<style scoped>
/* 基础布局样式 */
.kpi-overview {
padding: 20px;
}
.controls {
display: flex;
gap: 16px;
margin-bottom: 20px;
align-items: center;
}
.chart-container {
height: calc(100vh - 160px);
width: 100%;
}
/* 指标选择对话框样式 */
.kpi-checkbox-list {
padding: 8px;
width: 100%;
}
/* 网元指标列表样式 */
.ne-type-items {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.kpi-checkbox-item {
padding: 8px 12px;
border-radius: 4px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 其他指标下拉菜单样式 */
.secondary-kpi-menu {
background: #fff;
padding: 4px 0;
border-radius: 2px;
box-shadow: 0 3px 6px -4px rgba(0,0,0,.12), 0 6px 16px 0 rgba(0,0,0,.08);
max-height: 300px;
overflow-y: auto;
min-width: 200px;
}
.secondary-kpi-item {
padding: 8px 12px;
cursor: pointer;
user-select: none;
}
/* 组件统一样式 */
:deep(.ant-form-item),
:deep(.ant-picker),
:deep(.ant-btn) {
height: 32px;
}
:deep(.ant-form-item) {
margin-bottom: 0;
display: flex;
align-items: center;
}
:deep(.ant-checkbox-wrapper) {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 添加次要指标数量的样式 */
.secondary-count {
margin-left: 4px;
font-size: 12px;
}
</style>

View File

@@ -1,16 +0,0 @@
<script setup lang="ts">
import { onMounted } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
onMounted(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Perf Report</h1>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -193,7 +193,6 @@ function fnTableSelectedRows(
_: (string | number)[],
rows: Record<string, string>[]
) {
//debugger
tableState.selectedRowKeys = rows.map(item => item.loginId);
// 针对单个登录账号解锁
if (rows.length === 1) {

View File

@@ -139,7 +139,7 @@ let tableColumns: any = [
* 测试主机连接
*/
function fnHostTest(row: Record<string, any>) {
if (tabState.confirmLoading || !row.addr) return;
if (tabState.confirmLoading || !row.addr || !row.port) return;
tabState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0);
testNeHost(row)
@@ -187,19 +187,19 @@ function fnHostAuthorized(row: Record<string, any>) {
* 表单修改网元类型
*/
function fnNeTypeChange(v: any, data: any) {
const hostsLen = data.hosts.length;
// 网元默认只含22和4100
if (hostsLen === 3 && v !== 'UPF') {
data.hosts.pop();
if (modalState.from.hosts.length === 3) {
modalState.from.hosts.pop();
}
const hostsLen = modalState.from.hosts.length;
// UPF标准版本可支持5002
if (hostsLen === 2 && v === 'UPF') {
data.hosts.push({
modalState.from.hosts.push({
hostId: undefined,
hostType: 'telnet',
groupId: '1',
title: 'Telnet_NE_5002',
addr: '',
addr: modalState.from.ip,
port: 5002,
user: 'admin',
authMode: '0',
@@ -207,6 +207,22 @@ function fnNeTypeChange(v: any, data: any) {
remark: '',
});
}
// UDM可支持6379
if (hostsLen === 2 && v === 'UDM') {
modalState.from.hosts.push({
hostId: undefined,
hostType: 'redis',
groupId: '1',
title: 'REDIS_NE_6379',
addr: modalState.from.ip,
port: 6379,
user: 'udmdb',
authMode: '0',
password: 'helloearth',
dbName: '0',
remark: '',
});
}
}
//打开新增或修改界面

View File

@@ -147,6 +147,9 @@ function fnGetList() {
case 'N3IWF':
state.from.sbi.n3iwf_ip = item.ip;
break;
case 'SMSC':
state.from.sbi.smsc_ip = item.ip;
break;
}
}
}

View File

@@ -71,7 +71,7 @@ onMounted(() => {
<a-input
v-model:value="state.copyright"
allow-clear
:maxlength="40"
:maxlength="128"
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item>
@@ -101,7 +101,7 @@ onMounted(() => {
<a-typography>
<a-typography-paragraph>
{{ t('views.system.setting.sysCopyrightLimitation') }}
<a-typography-text mark>40</a-typography-text>
<a-typography-text mark>128</a-typography-text>
{{ t('views.system.setting.charMaxLen') }}
</a-typography-paragraph>
</a-typography>

View File

@@ -0,0 +1,129 @@
<script lang="ts" setup>
import { Modal, message } from 'ant-design-vue/lib';
import { onMounted, reactive, toRaw } from 'vue';
import useI18n from '@/hooks/useI18n';
import { listMenu } from '@/api/system/menu';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { getConfigKey, changeValue } from '@/api/system/config';
const { t } = useI18n();
type StateType = {
edite: boolean;
loading: boolean;
open: boolean;
default: any;
options: any;
};
let state: StateType = reactive({
edite: false,
loading: false,
open: false,
default: '',
options: [],
});
/**提交保存 */
function fnSave() {
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.system.setting.homeTip'),
onOk() {
// 发送请求
const hide = message.loading(t('common.loading'), 0);
state.loading = true;
changeValue({ key: 'sys.homePage', value: state.default })
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('views.system.setting.saveSuccess'), 3);
fnEdit(false);
} else {
message.error(res.msg, 3);
}
})
.finally(() => {
state.loading = false;
hide();
});
},
});
}
/**进入可编辑 */
function fnEdit(v: boolean) {
state.edite = v;
state.open = v;
}
onMounted(() => {
listMenu(toRaw({ status: 1 })).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// 过滤旧前端菜单以及不是菜单类型以及路径为空
res.data = res.data
.filter(i => i.perms !== 'page' && i.menuType === 'M' && i.component)
.map((item: any) => {
state.options.push({
label: item.menuName,
value: item.component,
});
});
}
});
//获取当前系统设置的首页路径 111为configID
getConfigKey('sys.homePage').then(res => {
if (res.code === RESULT_CODE_SUCCESS && res.data) {
state.default = res.data;
}
});
});
</script>
<template>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="24" style="margin-bottom: 30px">
<template v-if="state.edite">
<a-form-item :label="t('views.system.setting.home')">
<a-select
ref="select"
v-model:value="state.default"
style="width: 240px"
:disabled="false"
:options="state.options"
></a-select>
</a-form-item>
<a-button type="primary" @click="fnSave">
{{ t('views.system.setting.saveSubmit') }}
</a-button>
<a-button style="margin-left: 10px" @click="fnEdit(false)">
{{ t('common.cancel') }}
</a-button>
</template>
<template v-else>
<a-form-item :label="t('views.system.setting.home')">
<a-select
ref="select"
v-model:value="state.default"
style="width: 240px"
:disabled="true"
:options="state.options"
></a-select>
</a-form-item>
<a-button type="dashed" @click="fnEdit(true)">
{{ t('common.editText') }}
</a-button>
</template>
</a-col>
<a-col :lg="12" :md="12" :xs="24">
<a-typography>
<a-typography-paragraph>
{{ t('views.system.setting.homeInstruction') }}
</a-typography-paragraph>
</a-typography>
</a-col>
</a-row>
</template>
<style lang="less" scoped></style>

View File

@@ -8,6 +8,7 @@ import ChangeHelpDoc from './components/change-help-doc.vue';
import ChangeOfficialUrl from './components/change-official-url.vue';
import ChangeI18n from './components/change-i18n.vue';
import SystemReset from './components/system-reset.vue';
import ChangeHome from './components/change-home-index.vue';
import useI18n from '@/hooks/useI18n';
const { t } = useI18n();
</script>
@@ -49,6 +50,10 @@ const { t } = useI18n();
</a-divider>
<ChangeI18n></ChangeI18n>
</div>
<a-divider orientation="left">
{{ t('views.system.setting.homeSet') }}
</a-divider>
<ChangeHome></ChangeHome>
<a-divider orientation="left">
{{ t('views.system.setting.reset') }}
</a-divider>

View File

@@ -0,0 +1,432 @@
<script setup lang="ts">
import { reactive, onMounted, onBeforeUnmount, ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } from 'ant-design-vue/lib';
import useI18n from '@/hooks/useI18n';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import TerminalSSHView from '@/components/TerminalSSHView/index.vue';
import useNeInfoStore from '@/store/modules/neinfo';
import { iperfI, iperfV } from '@/api/tool/iperf';
const neInfoStore = useNeInfoStore();
const { t } = useI18n();
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**状态对象 */
let state = reactive({
/**初始化 */
initialized: false,
/**运行中 */
running: false,
/**版本信息 */
versionInfo: [],
/**网元类型 */
neType: [],
/**数据类型 */
dataType: 'options' as 'options' | 'command',
/**ws参数 */
params: {
neType: '',
neId: '',
cols: 120,
rows: 40,
},
/**ws数据 */
data: {
command: '', // 命令字符串
client: true, // 服务端或客户端,默认服务端
// Server or Client
port: 5201, // 服务端口
interval: 1, // 每次报告之间的时间间隔,单位为秒
// Server
oneOff: false, // 只进行一次连接
// Client
host: '', // 客户端连接到的服务端IP地址
udp: false, // use UDP rather than TCP
time: 10, // 以秒为单位的传输时间(默认为 10 秒)
reverse: false, // 以反向模式运行(服务器发送,客户端接收)
window: '300k', // 设置窗口大小/套接字缓冲区大小
},
});
/**连接发送 */
async function fnIPerf3() {
const [neType, neId] = state.neType;
if (!neType || !neId) {
message.warning({
content: 'No Found NE Type',
duration: 2,
});
return;
}
if (state.dataType === 'options' && state.data.host === '') {
message.warning({
content: 'Please fill in the Host',
duration: 2,
});
return;
}
if (state.dataType === 'command' && state.data.command === '') {
message.warning({
content: 'Please fill in the Command',
duration: 2,
});
return;
}
if (state.initialized) {
fnResend();
return;
}
state.params.neType = neType;
state.params.neId = neId;
const resVersion = await iperfV({ neType, neId });
if (resVersion.code !== RESULT_CODE_SUCCESS) {
Modal.confirm({
title: t('common.tipTitle'),
content: 'Not found if iperf is installed',
onOk: () => fnInstall(),
});
return;
} else {
state.versionInfo = resVersion.data;
}
state.initialized = true;
}
/**触发安装iperf3 */
function fnInstall() {
const key = 'iperfI';
message.loading({ content: t('common.loading'), key });
const { neType, neId } = state.params;
iperfI({ neType, neId }).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: 'install success',
key,
duration: 2,
});
} else {
message.error({
content: 'install fail',
key,
duration: 2,
});
}
});
}
/**终端实例 */
const toolTerminal = ref();
/**重置并停止 */
function fnReset() {
if (!toolTerminal.value) return;
toolTerminal.value.ctrlC();
// toolTerminal.value.clear();
state.running = false;
}
/**重载发送 */
function fnResend() {
if (!toolTerminal.value) return;
state.running = true;
toolTerminal.value.ctrlC();
toolTerminal.value.clear();
setTimeout(() => {
const data = JSON.parse(JSON.stringify(state.data));
if (state.dataType === 'options') data.command = '';
toolTerminal.value.send('iperf3', data);
}, 1000);
}
/**终端初始连接*/
function fnConnect() {
fnResend();
}
/**终端消息监听*/
function fnMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
if (!requestId) return;
let lestIndex = data.lastIndexOf('unable to');
if (lestIndex !== -1) {
state.running = false;
return;
}
lestIndex = data.lastIndexOf('iperf Done.');
if (lestIndex !== -1) {
state.running = false;
return;
}
}
/**钩子函数,界面打开初始化*/
onMounted(() => {
// 获取网元网元列表
neInfoStore.fnNelist().then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
// 过滤不可用的网元
for (const item of neInfoStore.getNeCascaderOptions) {
neCascaderOptions.value.push(item);
}
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
});
});
/**钩子函数,界面关闭*/
onBeforeUnmount(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8">
<span>
{{ t('views.ne.common.neType') }}:
<a-cascader
v-model:value="state.neType"
:options="neCascaderOptions"
:placeholder="t('common.selectPlease')"
:disabled="state.running"
/>
</span>
<a-radio-group
v-model:value="state.dataType"
button-style="solid"
:disabled="state.running"
>
<a-radio-button value="options">Options</a-radio-button>
<a-radio-button value="command">Command</a-radio-button>
</a-radio-group>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8">
<a-button
@click.prevent="fnIPerf3()"
type="primary"
:loading="state.running"
>
<template #icon><PlayCircleOutlined /></template>
{{ state.running ? 'Running' : 'Launch' }}
</a-button>
<a-button v-if="state.running" @click.prevent="fnReset()" danger>
<template #icon><CloseCircleOutlined /></template>
Stop
</a-button>
<!-- 版本信息 -->
<a-popover
trigger="click"
placement="bottomRight"
v-if="state.versionInfo.length > 0"
>
<template #content>
<div v-for="v in state.versionInfo">{{ v }}</div>
</template>
<InfoCircleOutlined />
</a-popover>
</a-space>
</template>
<!-- options -->
<a-form
v-if="state.dataType === 'options'"
:model="state.data"
name="queryParams"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
style="padding: 12px"
>
<a-divider orientation="left">Server or Client</a-divider>
<a-row>
<a-col :lg="6" :md="6" :xs="12">
<a-form-item label="Port" name="port">
<a-input-number
v-model:value="state.data.port"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1024"
:max="65535"
></a-input-number> </a-form-item
></a-col>
<a-col :lg="6" :md="6" :xs="12">
<a-form-item label="Interval" name="interval">
<a-input-number
v-model:value="state.data.interval"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="30"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
<a-divider orientation="left">
<a-radio-group
v-model:value="state.data.client"
:disabled="state.running"
>
<a-radio :value="true">Client</a-radio>
<a-radio :value="false">Server</a-radio>
</a-radio-group>
</a-divider>
<template v-if="state.data.client">
<a-form-item
label="Host"
name="host"
help="run in client mode, connecting to <host>"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-input
v-model:value="state.data.host"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
>
</a-input>
</a-form-item>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="UDP"
name="udp"
help="use UDP rather than TCP"
>
<a-switch
v-model:checked="state.data.udp"
:disabled="state.running"
/>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Reverse"
name="reverse"
help="run in reverse mode (server sends, client receives)"
>
<a-switch
v-model:checked="state.data.reverse"
:disabled="state.running"
/>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Time"
name="time"
help="time in seconds to transmit for (default 10 secs)"
>
<a-input-number
v-model:value="state.data.time"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="60"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Window"
name="window"
help="set window size / socket buffer size"
>
<a-input
v-model:value="state.data.window"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
></a-input>
</a-form-item>
</a-col>
</a-row>
</template>
<a-row :gutter="16" v-else>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="OneOff"
name="oneOff"
help=" handle one client connection then exit"
>
<a-switch
v-model:checked="state.data.oneOff"
:disabled="state.running"
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- command -->
<div v-else style="padding: 12px">
<a-auto-complete
v-model:value="state.data.command"
:disabled="state.running"
:dropdown-match-select-width="500"
style="width: 100%"
>
<a-input
addon-before="iperf3"
placeholder="Client: -c 172.5.16.100 -p 5201 or Server: -s -p 5201"
/>
</a-auto-complete>
</div>
<!-- 运行过程 -->
<TerminalSSHView
v-if="state.initialized"
ref="toolTerminal"
:id="`V${Date.now()}`"
prefix="iperf3"
url="/tool/iperf/run"
:ne-type="state.params.neType"
:ne-id="state.params.neId"
:rows="state.params.rows"
:cols="state.params.cols"
style="height: 400px"
@connect="fnConnect"
@message="fnMessage"
></TerminalSSHView>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -1,272 +1,336 @@
<script setup lang="ts">
import { reactive ,toRaw, onMounted } from 'vue';
import { reactive, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { SizeType } from 'ant-design-vue/lib/config-provider';
import { ColumnsType} from 'ant-design-vue/lib/table';
import { ColumnsType } from 'ant-design-vue/lib/table';
import useI18n from '@/hooks/useI18n';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import {
RESULT_CODE_ERROR
} from '@/constants/result-constants';
import { RESULT_CODE_ERROR } from '@/constants/result-constants';
const { t } = useI18n();
const ws = new WS();
//查询数据
/**表单查询参数 */
let queryParams = reactive({
pid: undefined,
name:"",
port:undefined,
flag: false,
changeTime:5000,
name: '',
port: undefined,
});
//临时缓存
let queryParams2 = reactive({
pid: undefined,
name:"",
port:undefined,
flag: false,
changeTime:5000,
})
//时间粒度
let timeOptions =[
{label:t('views.tool.ps.fastSpeed'),value:3000},
{label:t('views.tool.ps.normalSpeed'),value:5000},
{label:t('views.tool.ps.slowSpeed'),value:10000},
];
/**钩子函数,界面打开初始化*/
onMounted(() =>{
fnRealTime()//建立连接
extracted()//先立刻发送请求获取第一次的数据
queryReset2(false)//设置定时器
/**状态对象 */
let state = reactive({
/**调度器 */
interval: null as any,
/**刷新周期 */
intervalTime: 5_000,
/**查询参数 */
query: {
pid: undefined,
name: '',
port: undefined,
},
});
/**查询按钮**/
function queryTime(){//将表单中的数据传递给缓存数据(缓存数据自动同步请求信息中)
queryParams2.pid=queryParams.pid;
queryParams2.port=queryParams.port;
queryParams2.name=queryParams.name;
//queryParams.flag = true
queryParams2.flag=true
queryParams.pid=undefined;
queryParams.name="";
queryParams.port=undefined;
}
/**重置按钮**/
function queryReset(){
queryParams.flag = false
queryParams2.flag = false
}
let s:any = null
/**定时器**/
function queryReset2(c:boolean){
if(c){
clearInterval(s)//清理旧定时器
ws.close();//断开原来的wb连接
fnRealTime();//建立新的实时数据连接
/**接收数据后回调(成功) */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 建联时发送请求
if (!requestId && data.clientId) {
fnGetList();
return;
}
// 收到消息数据
if (requestId.startsWith('net_')) {
// 将数据填入表格
if (Array.isArray(data)) {
if (tableState.loading) {
tableState.loading = false;
}
tableState.data = data;
} else {
tableState.data = [];
}
}
s = setInterval(()=>{//设置新的定时器s
extracted();
},queryParams.changeTime)
}
/**刷新频率改变**/
function fnRealTime2() {//时间粒度改变时触发
queryReset2(true)//改变定时器
}
/**
* 实时数据
*/
function fnRealTime() {
/**实时数据*/
function fnRealTime(reLink: boolean) {
if (reLink) {
ws.close();
}
const options: OptionsType = {
url: '/ws',
onmessage: wsMessage,
onerror: wsError,
onerror: (ev: any) => {
// 接收数据后回调
console.error(ev);
},
};
//建立连接
ws.connect(options);
}
/**接收数据后回调(失败) */
function wsError(ev: any) {
// 接收数据后回调
console.error(ev);
/**调度器周期变更*/
function fnIntervalChange(v: any) {
clearInterval(state.interval);
state.interval = null;
const timer = parseInt(v);
if (timer > 1_000) {
state.intervalTime = v;
fnGetList();
}
}
/**接收数据后回调(成功) */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;//获取数据
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 处理数据组成ip : port
let processedData: any;
/**查询列表 */
function fnGetList() {
if (tableState.loading || ws.state() === -1) return;
tableState.loading = true;
const msg = {
requestId: `net_${state.interval}`,
type: 'net',
data: state.query,
};
// 首发
ws.send(msg);
// 定时刷新数据
state.interval = setInterval(() => {
msg.data = JSON.parse(JSON.stringify(state.query));
ws.send(msg);
}, state.intervalTime);
}
processedData = data.map((item:any) => {
const localAddr = `${item.localaddr.ip} : ${item.localaddr.port}`;
const remoteAddr = `${item.remoteaddr.ip} : ${item.remoteaddr.port}`;
return { ...item, localAddr, remoteAddr };
/**查询参数传入 */
function fnQuery() {
state.query = JSON.parse(JSON.stringify(queryParams));
nextTick(() => {
ws.send({
requestId: `net_${state.interval}`,
type: 'net',
data: state.query,
});
if (requestId) {
tableState.data = processedData; // 将数据填入表格
}
}
function extracted() {//将表单各条件值取出并打包在data中发送请求
const {pid,name, port, flag} = toRaw(queryParams2)//从queryParams中取出各属性值
let data = {} // { 'PID': PID, 'name': name, 'port': port }
if (flag){//若flag为真则把值封装进data
data = { 'pid': pid, 'name': name, 'port': port }
}
//发送请求
ws.send({
'requestId': 'dxxx',
'type': 'net',
'data': data,
});
}
/**表格状态 */
let tableState: TableStateType = reactive({
loading: false,
size: 'large',
data: [],
});
/**查询参数重置 */
function fnQueryReset() {
Object.assign(queryParams, {
pid: undefined,
name: '',
port: undefined,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
// 重置查询条件
Object.assign(state.query, {
pid: undefined,
name: '',
port: undefined,
});
}
/**表格状态类型 */
type TableStateType = {
/**加载等待 */
loading: boolean;
/**紧凑型 */
size: SizeType;
/**记录数据 */
data: object[];
};
/**表格状态 */
let tableState: TableStateType = reactive({
loading: false,
data: [],
});
/**表格字段列 */
const tableColumns: ColumnsType<any> = [
{
title: t('views.tool.net.PID'),
title: t('views.tool.ps.pid'),
dataIndex: 'pid',
align: 'center',
width: 50,
sorter:{//PID排序
compare:(a:any, b:any)=>a.pid-b.pid,
multiple:1,
}
align: 'right',
width: 100,
sorter: {
compare: (a: any, b: any) => a.pid - b.pid,
multiple: 3,
},
},
{
title: t('views.tool.net.name'),
dataIndex: 'name',
align: 'center',
title: t('views.tool.net.proto'),
dataIndex: 'type',
align: 'left',
width: 100,
customRender(opt) {
return `${opt.value}`.toUpperCase();
},
},
{
title: t('views.tool.net.localAddr'),
dataIndex: 'localAddr',
align: 'center',
width: 70,
align: 'left',
width: 150,
customRender(opt) {
const { ip, port } = opt.value;
return port !== 0 ? `${ip}:${port}` : `${ip}`;
},
},
{
title: t('views.tool.net.remoteAddr'),
dataIndex:'remoteAddr',
align: 'center',
width: 100,
dataIndex: 'remoteAddr',
align: 'left',
width: 150,
customRender(opt) {
const { ip, port } = opt.value;
return port !== 0 ? `${ip}:${port}` : `${ip}`;
},
},
{
title: t('views.tool.net.status'),
dataIndex: 'status',
align: 'center',
width: 70,
align: 'left',
width: 120,
},
{
title: t('views.tool.net.type'),
dataIndex: 'type',
align: 'center',
width: 100,
title: t('views.tool.ps.name'),
dataIndex: 'name',
align: 'left',
},
];
/**表格分页器参数 */
let tablePagination = reactive({
/**当前页数 */
current: 1,
/**每页条数 */
pageSize: 20,
/**默认的每页条数 */
defaultPageSize: 20,
/**指定每页可以显示多少条 */
pageSizeOptions: ['10', '20', '50', '100'],
/**只有一页时是否隐藏分页器 */
hideOnSinglePage: false,
/**是否可以快速跳转至某页 */
showQuickJumper: true,
/**是否可以改变 pageSize */
showSizeChanger: true,
/**数据总数 */
total: 0,
showTotal: (total: number) => t('common.tablePaginationTotal', { total }),
onChange: (page: number, pageSize: number) => {
tablePagination.current = page;
tablePagination.pageSize = pageSize;
},
});
/**钩子函数,界面打开初始化*/
onMounted(() => {
fnRealTime(false);
});
/**钩子函数,界面关闭*/
onBeforeUnmount(() => {
clearInterval(state.interval);
state.interval = null;
ws.close();
});
</script>
<template>
<PageContainer>
<a-card
:bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }"
>
<a-form :model="queryParams" name="formParams" layout="horizontal">
<!-- 表格搜索栏 -->
<a-form :model="queryParams" name="queryParams" layout="horizontal">
<a-row :gutter="16">
<a-col :lg="3" :md="6" :xs="12">
<a-form-item :label="t('views.tool.ps.changeTime')" name="changeTime">
<a-select
v-model:value="queryParams.changeTime"
:options="timeOptions"
:placeholder="t('common.selectPlease')"
@change='fnRealTime2'
/>
</a-form-item></a-col>
<a-col :lg="3" :md="6" :xs="12">
<a-form-item :label="t('views.tool.ps.PID')" name="pid">
<a-col :lg="4" :md="6" :xs="12">
<a-form-item :label="t('views.tool.ps.pid')" name="pid">
<a-input-number
v-model:value="queryParams.pid"
allow-clear
:placeholder="t('common.inputPlease')"
style='width: 100%'
style="width: 100%"
></a-input-number>
</a-form-item></a-col>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.tool.net.name')" name="name">
<a-form-item :label="t('views.tool.ps.name')" name="name">
<a-input
v-model:value="queryParams.name"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item></a-col>
</a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.tool.net.port')" name="port">
<a-input
<a-form-item :label="t('views.tool.net.port')" name="port">
<a-input-number
v-model:value="queryParams.port"
allow-clear
:placeholder="t('common.inputPlease')"
></a-input>
</a-form-item></a-col>
<a-col :lg="3" :md="4" :xs="5">
<a-form-item>
<a-button type="primary" @click='queryTime()'>
<template #icon><SearchOutlined /></template>
{{ t('common.search') }}
</a-button>
<a-button type="default" @click.prevent="queryReset()" style='left: 20px;'>
<template #icon><ClearOutlined /></template>
{{ t('common.reset') }}
</a-button>
style="width: 100%"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="4" :md="12" :xs="24">
<a-form-item>
<a-space :size="8">
<a-button type="primary" @click.prevent="fnQuery()">
<template #icon><SearchOutlined /></template>
{{ t('common.search') }}
</a-button>
<a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
{{ t('common.reset') }}
</a-button>
</a-space>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:loading="tableState.loading"
:data-source="tableState.data"
:size="tableState.size"
:scroll="{ y: 'calc(100vh - 480px)' }"
:pagination='false'
></a-table>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-form layout="inline">
<a-form-item :label="t('views.tool.ps.realTime')" name="realTime">
<a-select
v-model:value="state.intervalTime"
:options="[
{ label: t('views.tool.ps.realTimeHigh'), value: 3_000 },
{ label: t('views.tool.ps.realTimeRegular'), value: 5_000 },
{ label: t('views.tool.ps.realTimeLow'), value: 10_000 },
{ label: t('views.tool.ps.realTimeStop'), value: -1 },
]"
:placeholder="t('common.selectPlease')"
@change="fnIntervalChange"
style="width: 100px"
/>
</a-form-item>
</a-form>
</template>
<!-- 表格列表 -->
<a-table
class="table"
row-key="id"
:columns="tableColumns"
:pagination="tablePagination"
:loading="tableState.loading"
:data-source="tableState.data"
size="small"
:scroll="{ x: tableColumns.length * 120 }"
></a-table>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped>
.table :deep(.ant-pagination) {
padding: 0 24px;

View File

@@ -0,0 +1,374 @@
<script setup lang="ts">
import { reactive, onMounted, onBeforeUnmount, ref, toRaw } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message } from 'ant-design-vue/lib';
import useI18n from '@/hooks/useI18n';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import TerminalSSHView from '@/components/TerminalSSHView/index.vue';
import useNeInfoStore from '@/store/modules/neinfo';
import { pingV } from '@/api/tool/ping';
const neInfoStore = useNeInfoStore();
const { t } = useI18n();
/**网元参数 */
let neCascaderOptions = ref<Record<string, any>[]>([]);
/**状态对象 */
let state = reactive({
/**初始化 */
initialized: false,
/**运行中 */
running: false,
/**版本信息 */
versionInfo: '',
/**网元类型 */
neType: [],
/**数据类型 */
dataType: 'options' as 'options' | 'command',
/**ws参数 */
params: {
neType: '',
neId: '',
cols: 120,
rows: 40,
},
/**ws数据 */
data: {
command: '', // 命令字符串
desAddr: '8.8.8.8', // dns name or ip address
// Options
interval: 1, // seconds between sending each packet
ttl: 255, // define time to live
count: 4, // <count> 次回复后停止
size: 56, // 使用 <size> 作为要发送的数据字节数
timeout: 10, // seconds time to wait for response
},
});
/**连接发送 */
async function fnPing() {
const [neType, neId] = state.neType;
if (!neType || !neId) {
message.warning({
content: 'No Found NE Type',
duration: 2,
});
return;
}
if (state.dataType === 'options' && state.data.desAddr === '') {
message.warning({
content: 'Please fill in the Destination',
duration: 2,
});
return;
}
if (state.dataType === 'command' && state.data.command === '') {
message.warning({
content: 'Please fill in the Command',
duration: 2,
});
return;
}
if (state.initialized) {
fnResend();
return;
}
state.params.neType = neType;
state.params.neId = neId;
const resVersion = await pingV({ neType, neId });
if (resVersion.code !== RESULT_CODE_SUCCESS) {
message.warning({
content: 'No Found ping iputils',
duration: 2,
});
return;
} else {
state.versionInfo = resVersion.data;
}
state.initialized = true;
}
/**终端实例 */
const toolTerminal = ref();
/**重置并停止 */
function fnReset() {
if (!toolTerminal.value) return;
toolTerminal.value.ctrlC();
// toolTerminal.value.clear();
state.running = false;
}
/**重载发送 */
function fnResend() {
if (!toolTerminal.value) return;
state.running = true;
toolTerminal.value.ctrlC();
toolTerminal.value.clear();
setTimeout(() => {
const data = JSON.parse(JSON.stringify(state.data));
if (state.dataType === 'options') data.command = '';
toolTerminal.value.send('ping', data);
}, 1000);
}
/**终端初始连接*/
function fnConnect() {
fnResend();
}
/**终端消息监听*/
function fnMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
if (!requestId) return;
let lestIndex = data.lastIndexOf('ping statistics ---');
if (lestIndex !== -1) {
state.running = false;
return;
}
lestIndex = data.lastIndexOf('failure');
if (lestIndex !== -1) {
state.running = false;
return;
}
}
/**钩子函数,界面打开初始化*/
onMounted(() => {
// 获取网元网元列表
neInfoStore.fnNelist().then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
if (res.data.length > 0) {
// 过滤不可用的网元
for (const item of neInfoStore.getNeCascaderOptions) {
neCascaderOptions.value.push(item);
}
if (neCascaderOptions.value.length === 0) {
message.warning({
content: t('common.noData'),
duration: 2,
});
return;
}
}
} else {
message.warning({
content: t('common.noData'),
duration: 2,
});
}
});
});
/**钩子函数,界面关闭*/
onBeforeUnmount(() => {});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<!-- 插槽-卡片左侧侧 -->
<template #title>
<a-space :size="8">
<span>
{{ t('views.ne.common.neType') }}:
<a-cascader
v-model:value="state.neType"
:options="neCascaderOptions"
:placeholder="t('common.selectPlease')"
:disabled="state.running"
/>
</span>
<a-radio-group
v-model:value="state.dataType"
button-style="solid"
:disabled="state.running"
>
<a-radio-button value="options">Options</a-radio-button>
<a-radio-button value="command">Command</a-radio-button>
</a-radio-group>
</a-space>
</template>
<!-- 插槽-卡片右侧 -->
<template #extra>
<a-space :size="8">
<a-button
@click.prevent="fnPing()"
type="primary"
:loading="state.running"
>
<template #icon><PlayCircleOutlined /></template>
{{ state.running ? 'Running' : 'Launch' }}
</a-button>
<a-button v-if="state.running" @click.prevent="fnReset()" danger>
<template #icon><CloseCircleOutlined /></template>
Stop
</a-button>
<!-- 版本信息 -->
<a-popover
trigger="click"
placement="bottomRight"
v-if="state.versionInfo"
>
<template #content>
{{ state.versionInfo }}
</template>
<InfoCircleOutlined />
</a-popover>
</a-space>
</template>
<!-- options -->
<a-form
v-if="state.dataType === 'options'"
:model="state.data"
name="queryParams"
layout="horizontal"
:label-col="{ span: 6 }"
:label-wrap="true"
style="padding: 12px"
>
<a-form-item
label="Destination"
name="desAddr"
help="dns name or ip address"
:label-col="{ span: 3 }"
:label-wrap="true"
>
<a-input
v-model:value="state.data.desAddr"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
>
</a-input>
</a-form-item>
<a-row :gutter="16">
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Interval"
name="Interval"
help="seconds between sending each packet"
>
<a-input-number
v-model:value="state.data.interval"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="120"
>
</a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item label="TTL" name="ttl" help="define time to live">
<a-input-number
v-model:value="state.data.ttl"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="255"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Count"
name="count"
help="stop after <count> replies"
>
<a-input-number
v-model:value="state.data.count"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="120"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Size"
name="size"
help="use <size> as number of data bytes to be sent"
>
<a-input-number
v-model:value="state.data.size"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="128"
></a-input-number>
</a-form-item>
</a-col>
<a-col :lg="12" :md="12" :xs="12">
<a-form-item
label="Deadline"
name="timeout"
help="reply wait <deadline> in seconds"
>
<a-input-number
v-model:value="state.data.timeout"
:disabled="state.running"
allow-clear
:placeholder="t('common.inputPlease')"
style="width: 100%"
:min="1"
:max="60"
></a-input-number>
</a-form-item>
</a-col>
</a-row>
</a-form>
<!-- command -->
<div v-else style="padding: 12px">
<a-auto-complete
v-model:value="state.data.command"
:disabled="state.running"
:dropdown-match-select-width="500"
style="width: 100%"
>
<a-input addon-before="ping" placeholder="eg: -i 1 -c 4 8.8.8.8" />
</a-auto-complete>
</div>
<!-- 运行过程 -->
<TerminalSSHView
v-if="state.initialized"
ref="toolTerminal"
:id="`V${Date.now()}`"
prefix="ping"
url="/tool/ping/run"
:ne-type="state.params.neType"
:ne-id="state.params.neId"
:rows="state.params.rows"
:cols="state.params.cols"
style="height: 400px"
@connect="fnConnect"
@message="fnMessage"
></TerminalSSHView>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>

View File

@@ -50,6 +50,9 @@ function wsMessage(res: Record<string, any>) {
if (requestId.startsWith('ps_')) {
// 将数据填入表格
if (Array.isArray(data)) {
if (tableState.loading) {
tableState.loading = false;
}
tableState.data = data;
} else {
tableState.data = [];
@@ -77,6 +80,7 @@ function fnRealTime(reLink: boolean) {
/**调度器周期变更*/
function fnIntervalChange(v: any) {
clearInterval(state.interval);
state.interval = null;
const timer = parseInt(v);
if (timer > 1_000) {
state.intervalTime = v;
@@ -97,10 +101,9 @@ function fnGetList() {
ws.send(msg);
// 定时刷新数据
state.interval = setInterval(() => {
msg.data = state.query;
msg.data = JSON.parse(JSON.stringify(state.query));
ws.send(msg);
}, state.intervalTime);
tableState.loading = false;
}
/**查询参数传入 */
@@ -263,6 +266,8 @@ onMounted(() => {
/**钩子函数,界面关闭*/
onBeforeUnmount(() => {
clearInterval(state.interval);
state.interval = null;
ws.close();
});
</script>
@@ -320,8 +325,6 @@ onBeforeUnmount(() => {
</a-col>
</a-row>
</a-form>
<div>{{ state.query }}</div>
<div>{{ queryParams }}</div>
</a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }">

View File

@@ -340,7 +340,7 @@ function fnTabClose(id: string) {
<template #rightExtra>
<a-space :size="8" align="center">
<a-tooltip placement="topRight">
<a-tooltip placement="topRight" v-if="false">
<template #title>
{{ t('views.tool.terminal.new') }}
</template>

View File

@@ -8,7 +8,7 @@ onMounted(() => {});
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>Perf Set</h1>
<h1>Test Page</h1>
</a-card>
</PageContainer>
</template>

View File

@@ -7,6 +7,7 @@ import { Modal, message } from 'ant-design-vue/lib';
import { parseDateToStr } from '@/utils/date-utils';
import { getNeFile, listNeFiles } from '@/api/tool/neFile';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import ViewDrawer from '@/views/logManage/neFile/components/ViewDrawer.vue';
import useNeInfoStore from '@/store/modules/neinfo';
import useTabsStore from '@/store/modules/tabs';
import useI18n from '@/hooks/useI18n';
@@ -218,16 +219,18 @@ function fnDirCD(dir: string, index?: number) {
/**网元类型选择对应修改 */
function fnNeChange(keys: any, _: any) {
if (!Array.isArray(keys)) return;
const neType = keys[0];
const neId = keys[1];
// 不是同类型时需要重新加载
if (Array.isArray(keys) && queryParams.neType !== keys[0]) {
const neType = keys[0];
if (queryParams.neType !== neType || queryParams.neId !== neId) {
queryParams.neType = neType;
queryParams.neId = keys[1];
queryParams.neId = neId;
if (neType === 'UPF' && tmp.value) {
nePathArr.value = ['/tmp'];
queryParams.search = `${neType}_${keys[1]}`;
queryParams.search = `${neType}_${neId}`;
} else {
nePathArr.value = [`/tmp/omc/tcpdump/${neType.toLowerCase()}/${keys[1]}`];
nePathArr.value = [`/tmp/omc/tcpdump/${neType.toLowerCase()}/${neId}`];
queryParams.search = '';
}
fnGetList(1);
@@ -270,6 +273,25 @@ function fnGetList(pageNum?: number) {
});
}
/**抽屉状态 */
const viewDrawerState = reactive({
visible: false,
/**文件路径 /var/log/amf.log */
filePath: '',
/**网元类型 */
neType: '',
/**网元ID */
neId: '',
});
/**打开抽屉查看 */
function fnDrawerOpen(row: Record<string, any>) {
viewDrawerState.filePath = [...nePathArr.value, row.fileName].join('/');
viewDrawerState.neType = neTypeSelect.value[0];
viewDrawerState.neId = neTypeSelect.value[1];
viewDrawerState.visible = !viewDrawerState.visible;
}
onMounted(() => {
// 获取网元网元列表
neInfoStore.fnNelist().then(res => {
@@ -375,6 +397,16 @@ onMounted(() => {
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'">
<a-space :size="8" align="center">
<a-tooltip
v-if="
record.fileType === 'file' && record.fileName.endsWith('.log')
"
>
<template #title>{{ t('common.viewText') }}</template>
<a-button type="link" @click.prevent="fnDrawerOpen(record)">
<template #icon><ProfileOutlined /></template>
</a-button>
</a-tooltip>
<a-button
type="link"
:loading="downLoading"
@@ -398,6 +430,14 @@ onMounted(() => {
</template>
</a-table>
</a-card>
<!-- 文件内容查看抽屉 -->
<ViewDrawer
v-model:visible="viewDrawerState.visible"
:file-path="viewDrawerState.filePath"
:ne-type="viewDrawerState.neType"
:ne-id="viewDrawerState.neId"
></ViewDrawer>
</PageContainer>
</template>

View File

@@ -4,9 +4,9 @@ import { useRoute, useRouter } from 'vue-router';
import { message, Modal } from 'ant-design-vue/lib';
import { ColumnsType } from 'ant-design-vue/lib/table';
import { PageContainer } from 'antdv-pro-layout';
import { dumpStart, dumpStop, dumpDownload, traceUPF } from '@/api/trace/pcap';
import { dumpStart, dumpStop, traceUPF } from '@/api/trace/pcap';
import { listAllNeInfo } from '@/api/ne/neInfo';
import { getNeFile } from '@/api/tool/neFile';
import { getNeDirZip, getNeFile, getNeViewFile } from '@/api/tool/neFile';
import saveAs from 'file-saver';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import useI18n from '@/hooks/useI18n';
@@ -31,7 +31,7 @@ type ModalStateType = {
/**任务编号 */
taskCode: string;
/**任务日志,upf标准版为空字符串 */
logMsg: string;
taskFiles: string[];
/**提交表单参数 */
data: {
neType: string;
@@ -65,7 +65,14 @@ type ModalStateType = {
/**详情框是否显示 */
visibleByView: boolean;
/**详情框内容 */
logMsg: string;
viewFrom: {
neType: string;
neId: string;
path: string;
action: string;
files: string[];
content: string;
};
};
/**对话框对象信息状态 */
@@ -81,7 +88,7 @@ let modalState: ModalStateType = reactive({
{
label: t('views.traceManage.pcap.execCmd2'),
value: 'any2',
start: 'sctp or tcp port 3030 or 8088',
start: 'sctp or tcp port 8088 or 33030',
stop: '',
},
{
@@ -106,7 +113,14 @@ let modalState: ModalStateType = reactive({
},
],
visibleByView: false,
logMsg: '',
viewFrom: {
neType: '',
neId: '',
path: '',
action: '',
files: [],
content: '',
},
});
/**表格状态类型 */
@@ -194,7 +208,7 @@ function fnGetList() {
cmdStart: start,
cmdStop: stop,
taskCode: '',
logMsg: '',
taskFiles: [],
data: {
neType: item.neType,
neId: item.neId,
@@ -218,7 +232,7 @@ function fnSelectCmd(id: any, option: any) {
modalState.from[id].cmdStop = option.stop;
// 重置任务
modalState.from[id].taskCode = '';
modalState.from[id].logMsg = '';
modalState.from[id].taskFiles = [];
}
/**
@@ -243,6 +257,7 @@ function fnRecordStart(row?: Record<string, any>) {
const hide = message.loading(t('common.loading'), 0);
const fromArr = neIDs.map(id => modalState.from[id]);
const reqArr = fromArr.map(from => {
from.loading = true;
const data = Object.assign({ cmd: from.cmdStart }, from.data);
if (from.data.neType === 'UPF' && from.cmdStart.startsWith('pcap')) {
return traceUPF(data);
@@ -265,12 +280,14 @@ function fnRecordStart(row?: Record<string, any>) {
duration: 3,
});
} else {
fromArr[idx].loading = false;
message.warning({
content: `${resV.msg}`,
duration: 3,
});
}
} else {
fromArr[idx].loading = false;
message.error({
content: t('views.traceManage.pcap.startErr', { title }),
duration: 3,
@@ -338,7 +355,7 @@ function fnRecordStop(row?: Record<string, any>) {
if (res.status === 'fulfilled') {
const resV = res.value;
fromArr[idx].loading = false;
fromArr[idx].logMsg = '';
fromArr[idx].taskFiles = [];
if (fromArr[idx].cmdStop) {
fromArr[idx].taskCode = '';
}
@@ -347,7 +364,7 @@ function fnRecordStop(row?: Record<string, any>) {
if (fromArr[idx].cmdStop) {
fromArr[idx].taskCode = resV.data;
} else {
fromArr[idx].logMsg = resV.msg;
fromArr[idx].taskFiles = resV.data;
}
message.success({
content: t('views.traceManage.pcap.stopOk', { title }),
@@ -419,10 +436,15 @@ function fnDownPCAP(row?: Record<string, any>) {
)
);
} else {
const { neType, neId } = from.data;
const path = `/tmp/omc/tcpdump/${neType.toLowerCase()}/${neId}/${taskCode}`;
reqArr.push(
dumpDownload(
Object.assign({ taskCode: taskCode, delTemp: true }, from.data)
)
getNeDirZip({
neType,
neId,
path,
delTemp: true,
})
);
}
}
@@ -494,8 +516,42 @@ function fnBatchOper(key: string) {
function fnModalVisibleByVive(id: string | number) {
const from = modalState.from[id];
if (!from) return;
const { neType, neId } = from.data;
const path = `/tmp/omc/tcpdump/${neType.toLowerCase()}/${neId}/${
from.taskCode
}`;
const files = from.taskFiles.filter(f => f.endsWith('log'));
modalState.viewFrom.neType = neType;
modalState.viewFrom.neId = neId;
modalState.viewFrom.path = path;
modalState.viewFrom.files = [...files];
fnViveTab(files[0]);
modalState.visibleByView = true;
modalState.logMsg = from.logMsg;
}
/**对话框tab查看 */
function fnViveTab(action: any) {
console.log('fnViveTab', action);
if (modalState.viewFrom.action === action) return;
modalState.viewFrom.action = action;
modalState.viewFrom.content = '';
const { neType, neId, path } = modalState.viewFrom;
getNeViewFile({
neId,
neType,
path,
fileName: action,
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
modalState.viewFrom.content = res.data;
} else {
modalState.viewFrom.content = '';
message.warning({
content: `${res.msg}`,
duration: 3,
});
}
});
}
/**
@@ -504,7 +560,8 @@ function fnModalVisibleByVive(id: string | number) {
*/
function fnModalCancel() {
modalState.visibleByView = false;
modalState.logMsg = '';
modalState.viewFrom.action = '';
modalState.viewFrom.files = [];
}
/**跳转文件数据页面 */
@@ -648,7 +705,7 @@ onMounted(() => {
placement="topRight"
v-if="
!modalState.from[record.id].loading &&
!!modalState.from[record.id].logMsg
modalState.from[record.id].taskFiles.length > 0
"
>
<template #title>
@@ -691,21 +748,39 @@ onMounted(() => {
<!-- 日志信息框 -->
<ProModal
:drag="true"
:fullscreen="true"
:borderDraw="true"
:width="800"
:visible="modalState.visibleByView"
:footer="false"
:maskClosable="false"
:keyboard="false"
:body-style="{ padding: '12px' }"
:body-style="{ padding: '0 12px 12px' }"
:title="t('views.traceManage.pcap.textLogMsg')"
@cancel="fnModalCancel"
>
<a-textarea
v-model:value="modalState.logMsg"
:auto-size="{ minRows: 2, maxRows: 18 }"
:disabled="true"
style="color: rgba(0, 0, 0, 0.85)"
/>
<a-tabs
v-model:activeKey="modalState.viewFrom.action"
tab-position="top"
size="small"
@tabClick="fnViveTab"
>
<a-tab-pane
v-for="fileName in modalState.viewFrom.files"
:key="fileName"
:tab="fileName"
:destroyInactiveTabPane="false"
>
<a-spin :spinning="!modalState.viewFrom.content">
<a-textarea
:value="modalState.viewFrom.content"
:auto-size="{ minRows: 2 }"
:disabled="true"
style="color: rgba(0, 0, 0, 0.85)"
/>
</a-spin>
</a-tab-pane>
</a-tabs>
</ProModal>
</PageContainer>
</template>

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { reactive, ref, computed, unref, onUpdated, watchEffect } from 'vue';
import { reactive, ref, computed, unref, watchEffect } from 'vue';
const props = defineProps({
/**列表高度 */
@@ -122,8 +122,6 @@ const onScroll = (e: any) => {
}
};
onUpdated(() => {});
watchEffect(() => {
clientData.value.forEach((_, index) => {
const currentIndex = state.start + index;
@@ -136,10 +134,6 @@ watchEffect(() => {
};
});
});
const tableState = reactive({
selected: false,
});
</script>
<template>
@@ -167,13 +161,13 @@ const tableState = reactive({
item.number === props.selectedFrame
? 'blue'
: item.bg
? `#${item.bg.toString(16).padStart(6, '0')}`
? `#${Number(item.bg).toString(16).padStart(6, '0')}`
: '',
color:
item.number === props.selectedFrame
? 'white'
: item.fg
? `#${item.fg.toString(16).padStart(6, '0')}`
? `#${Number(item.fg).toString(16).padStart(6, '0')}`
: '',
}"
@click="onSelectedFrame(item.number)"
@@ -219,7 +213,7 @@ const tableState = reactive({
display: flex;
flex-direction: row;
align-items: center;
border-top: 1px #f0f0f0 solid;
border-top: 1px #f0f0f0 solid;
cursor: pointer;
}
.tbody-item {

View File

@@ -1,16 +1,522 @@
<script setup lang="ts">
import { reactive, onMounted, toRaw } from 'vue';
import { onBeforeUnmount, onMounted, reactive, ref } from 'vue';
import { PageContainer } from 'antdv-pro-layout';
import { message, Modal } from 'ant-design-vue/lib';
import DissectionTree from '../tshark/components/DissectionTree.vue';
import DissectionDump from '../tshark/components/DissectionDump.vue';
import PacketTable from '../tshark/components/PacketTable.vue';
import {
RESULT_CODE_ERROR,
RESULT_CODE_SUCCESS,
} from '@/constants/result-constants';
import { filePullTask } from '@/api/trace/task';
import { OptionsType, WS } from '@/plugins/ws-websocket';
import useI18n from '@/hooks/useI18n';
import saveAs from 'file-saver';
import {
packetDevices,
packetStart,
packetStop,
packetFilter,
packetKeep,
} from '@/api/trace/packet';
const ws = new WS();
const { t } = useI18n();
onMounted(() => {});
const NO_SELECTION = { id: '', idx: 0, start: 0, length: 0 };
type StateType = {
/**网卡设备列表 */
devices: { id: string; label: string; children: any[] }[];
/**初始化 */
initialized: boolean;
/**保活调度器 */
keepTimer: any;
/**任务 */
task: {
taskNo: string;
device: string;
filter: string;
outputPCAP: boolean;
};
/**字段 */
columns: string[];
/**过滤条件 */
filter: string;
/**过滤条件错误信息 */
filterError: string | null;
/**当前选中的帧编号 */
selectedFrame: number;
/**当前选中的帧数据 */
packetFrame: { tree: any[]; data_sources: any[] };
/**pcap包帧数据 */
packetFrameTreeMap: Map<string, any> | null;
/**当前选中的帧数据 */
selectedTree: typeof NO_SELECTION;
/**选择帧的Dump数据标签 */
selectedDataSourceIndex: number;
/**包总数 */
totalPackets: number;
/**包数据 */
packetList: any[];
};
const state = reactive<StateType>({
devices: [],
initialized: false,
keepTimer: null,
task: {
taskNo: 'laYlTbq',
device: '192.168.5.58',
filter: 'tcp and (port 33030 or 8080)',
outputPCAP: false,
},
columns: [
'No.',
'Time',
'Source',
'Destination',
'Protocol',
'Length',
'Info',
],
filter: 'tcp and (port 33030 or 8080)',
filterError: null,
selectedFrame: 1,
/**当前选中的帧数据 */
packetFrame: { tree: [], data_sources: [] },
packetFrameTreeMap: null, // 注意Map 需要额外处理
selectedTree: NO_SELECTION, // NO_SELECTION 需要定义
/**选择帧的Dump数据标签 */
selectedDataSourceIndex: 0,
// 包数据
totalPackets: 0,
packetList: [],
});
/**清除帧数据和报文信息状态 */
function fnReset() {
state.initialized = false;
// 选择帧的数据
state.selectedFrame = 0;
state.packetFrame = { tree: [], data_sources: [] };
state.packetFrameTreeMap = null;
state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0;
// 过滤条件
state.filter = 'tcp and (port 33030 or 8080)';
state.filterError = null;
// 包数据
state.totalPackets = 0;
state.packetList = [];
}
/**解析帧数据为简单结构 */
function parseFrameTree(id: string, node: Record<string, any>) {
let map = new Map();
if (node.tree && node.tree.length > 0) {
for (let i = 0; i < node.tree.length; i++) {
const subMap = parseFrameTree(`${id}-${i}`, node.tree[i]);
subMap.forEach((value, key) => {
map.set(key, value);
});
}
} else if (node.length > 0) {
map.set(id, {
id: id,
idx: node.data_source_idx,
start: node.start,
length: node.length,
});
}
return map;
}
/**帧数据点击选中 */
function handleSelectedTreeEntry(e: any) {
console.log('fnSelectedTreeEntry', e);
state.selectedTree = e;
}
/**报文数据点击选中 */
function handleSelectedFindSelection(src_idx: number, pos: number) {
console.log('fnSelectedFindSelection', pos);
if (state.packetFrameTreeMap == null) return;
// find the smallest one
let current = null;
for (let [k, pp] of state.packetFrameTreeMap) {
if (pp.idx !== src_idx) continue;
if (pos >= pp.start && pos <= pp.start + pp.length) {
if (
current != null &&
state.packetFrameTreeMap.get(current).length > pp.length
) {
current = k;
} else {
current = k;
}
}
}
if (current != null) {
state.selectedTree = state.packetFrameTreeMap.get(current);
}
}
/**包数据表点击选中 */
function handleSelectedFrame(num: number) {
console.log('fnSelectedFrame', num, state.totalPackets);
const packet = state.packetList.find((v: any) => v.number === num);
if (!packet) return;
const packetFrame = packet.frame;
state.selectedFrame = packet.number;
state.packetFrame = packetFrame;
state.packetFrameTreeMap = parseFrameTree('root', packetFrame);
state.selectedTree = NO_SELECTION;
state.selectedDataSourceIndex = 0;
}
/**包数据表滚动底部加载 */
function handleScrollBottom(index: any) {
console.log('handleScrollBottom', index);
}
/**开始跟踪 */
function fnStart() {
// state.task.taskNo = 'laYlTbq';
state.task.taskNo = Number(Date.now()).toString(16);
state.task.outputPCAP = false;
packetStart(state.task).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
fnReset();
fnWS();
} else {
message.error(t('common.operateErr'), 3);
}
});
}
/**停止跟踪 */
function fnStop() {
packetStop(state.task.taskNo).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
ws.close();
state.initialized = false;
state.filter = '';
state.filterError = null;
} else {
message.warning(res.msg, 3);
}
});
}
/**跟踪数据表过滤 */
function handleFilterFrames() {
packetFilter(state.task.taskNo, state.filter).then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
state.task.filter = state.filter;
} else {
state.filterError = res.msg;
}
});
}
/**开始跟踪 */
function fnDevice(v: string) {
state.task.device = v;
}
/**下载触发等待 */
let downLoading = ref<boolean>(false);
/**信息文件下载 */
function fnDownloadPCAP() {
if (downLoading.value) return;
const fileName = `trace_packet_${state.task.taskNo}.pcap`;
Modal.confirm({
title: t('common.tipTitle'),
content: t('views.logManage.neFile.downTip', { fileName }),
onOk() {
downLoading.value = true;
const hide = message.loading(t('common.loading'), 0);
filePullTask(state.task.taskNo)
.then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
message.success({
content: t('common.msgSuccess', {
msg: t('common.downloadText'),
}),
duration: 2,
});
saveAs(res.data, `${fileName}`);
} else {
message.error({
content: t('views.logManage.neFile.downTipErr'),
duration: 2,
});
}
})
.finally(() => {
hide();
downLoading.value = false;
});
},
});
}
/**接收数据后回调 */
function wsMessage(res: Record<string, any>) {
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
// 建联时发送请求
if (!requestId && data.clientId) {
state.initialized = true;
state.keepTimer = setInterval(() => {
packetKeep(state.task.taskNo, 120);
}, 90 * 1000);
return;
}
// 订阅组信息
if (!data?.groupId) {
return;
}
if (data.groupId === `4_${state.task.taskNo}`) {
const packetData = data.data;
state.totalPackets = packetData.number;
state.packetList.push(packetData);
}
}
/**建立WS连接 */
function fnWS() {
const options: OptionsType = {
url: '/ws',
params: {
/**订阅通道组
*
* 信令跟踪Packet (GroupID:4_taskNo)
*/
subGroupID: `4_${state.task.taskNo}`,
},
onmessage: wsMessage,
onerror: (ev: any) => {
// 接收数据后回调
console.error(ev);
},
};
//建立连接
ws.connect(options);
}
onMounted(() => {
packetDevices().then(res => {
if (res.code === RESULT_CODE_SUCCESS) {
state.devices = res.data;
if (res.data.length === 0) return;
state.task.device = res.data[0].id;
}
});
});
onBeforeUnmount(() => {
clearInterval(state.keepTimer);
state.keepTimer = null;
if (ws.state() === WebSocket.OPEN) ws.close();
});
</script>
<template>
<PageContainer>
<a-card :bordered="false" :body-style="{ padding: '0px' }">
<h1>JS</h1>
<a-card :bordered="false" :body-style="{ padding: '12px' }">
<div class="toolbar">
<a-space :size="8" class="toolbar-oper">
<a-dropdown-button
type="primary"
:disabled="state.initialized"
@click="fnStart"
>
<PlayCircleOutlined />
Start Trace
<template #overlay>
<a-menu
@click="({ key }:any) => fnDevice(key)"
:selectedKeys="[state.task.device]"
>
<a-menu-item v-for="v in state.devices" :key="v.id">
<a-popover placement="rightTop" trigger="hover">
<template #content>
<div v-for="c in v.children">{{ c.id }}</div>
</template>
<template #title>
<span>IP Address</span>
</template>
<div>{{ v.label }}</div>
</a-popover>
</a-menu-item>
</a-menu>
</template>
<template #icon><DownOutlined /></template>
</a-dropdown-button>
<a-button danger @click.prevent="fnStop()" v-if="state.initialized">
<template #icon><CloseCircleOutlined /></template>
Stop Trace
</a-button>
<a-button
type="primary"
:loading="downLoading"
@click.prevent="fnDownloadPCAP()"
v-if="state.task.outputPCAP"
>
<template #icon><DownloadOutlined /></template>
{{ t('common.downloadText') }}
</a-button>
<a-tag
color="green"
v-show="!!state.task.filter && state.initialized"
>
{{ state.task.filter }}
</a-tag>
</a-space>
<a-space :size="8" class="toolbar-info" v-show="state.initialized">
<span>
{{ t('views.traceManage.task.traceId') }}:&nbsp;
<strong>{{ state.task.taskNo }}</strong>
</span>
<span> Packets: {{ state.totalPackets }} </span>
</a-space>
</div>
<!-- 包数据表过滤 -->
<a-input-group compact v-show="state.initialized">
<a-input
v-model:value="state.filter"
placeholder="display filter, example: tcp"
:allow-clear="true"
style="width: calc(100% - 100px)"
@pressEnter="handleFilterFrames"
>
<template #prefix>
<FilterOutlined />
</template>
</a-input>
<a-button
type="primary"
html-type="submit"
style="width: 100px"
@click="handleFilterFrames"
>
Filter
</a-button>
</a-input-group>
<a-alert
:message="state.filterError"
type="error"
v-if="state.filterError != null"
/>
<!-- 包数据表 -->
<PacketTable
:columns="state.columns"
:data="state.packetList"
:selectedFrame="state.selectedFrame"
:onSelectedFrame="handleSelectedFrame"
:onScrollBottom="handleScrollBottom"
></PacketTable>
<a-row :gutter="20">
<a-col :lg="12" :md="12" :xs="24" class="tree">
<!-- 帧数据 -->
<DissectionTree
id="root"
:select="handleSelectedTreeEntry"
:selected="state.selectedTree"
:tree="state.packetFrame.tree"
/>
</a-col>
<a-col :lg="12" :md="12" :xs="24" class="dump">
<!-- 报文数据 -->
<a-tabs
v-model:activeKey="state.selectedDataSourceIndex"
:tab-bar-gutter="16"
:tab-bar-style="{ marginBottom: '8px' }"
>
<a-tab-pane
:key="idx"
:tab="v.name"
v-for="(v, idx) in state.packetFrame.data_sources"
style="overflow: auto"
>
<DissectionDump
:base64="v.data"
:select="(pos:number)=>handleSelectedFindSelection(idx, pos)"
:selected="
idx === state.selectedTree.idx
? state.selectedTree
: NO_SELECTION
"
/>
</a-tab-pane>
</a-tabs>
</a-col>
</a-row>
</a-card>
</PageContainer>
</template>
<style lang="less" scoped></style>
<style scoped>
.toolbar {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.toolbar-oper {
flex: 1;
}
.toolbar-info {
text-align: right;
padding-right: 8px;
}
.summary {
display: flex;
flex-direction: column;
}
.summary-item > span:first-child {
font-weight: 600;
margin-right: 6px;
}
.tree {
font-size: 0.8125rem;
line-height: 1.5rem;
padding-bottom: 0.75rem;
padding-top: 0.75rem;
white-space: nowrap;
overflow-y: auto;
user-select: none;
height: 100%;
}
.tree > ul.tree {
min-height: 15rem;
}
.dump {
padding-bottom: 0.75rem;
padding-top: 0.75rem;
overflow-y: auto;
height: 100%;
}
.dump .ant-tabs-tabpane {
min-height: calc(15rem - 56px);
}
</style>