Merge branch 'main' into multi-tenant

This commit is contained in:
lai
2024-10-31 17:34:38 +08:00
62 changed files with 182531 additions and 3511 deletions

View File

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

View File

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

View File

@@ -30,6 +30,7 @@
"dayjs": "^1.11.11", "dayjs": "^1.11.11",
"echarts": "~5.5.0", "echarts": "~5.5.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"grid-layout-plus": "^1.0.5",
"intl-tel-input": "^23.8.1", "intl-tel-input": "^23.8.1",
"js-base64": "^3.7.7", "js-base64": "^3.7.7",
"js-cookie": "^3.0.5", "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'); console.log('Fetching packages');
let [wasmBuffer, dataBuffer] = await Promise.all([ let [wasmBuffer, dataBuffer] = await Promise.all([
await inflateRemoteBuffer( await inflateRemoteBuffer(
'/wiregasm/wiregasm.wasm.gz' '/wiregasm/wiregasm.wasm'
// 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.wasm.gz' // 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.wasm'
), ),
await inflateRemoteBuffer( await inflateRemoteBuffer(
'/wiregasm/wiregasm.data.gz' '/wiregasm/wiregasm.data'
// 'https://cdn.jsdelivr.net/npm/@goodtools/wiregasm/dist/wiregasm.data.gz' // '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_ERROR,
RESULT_CODE_SUCCESS, RESULT_CODE_SUCCESS,
RESULT_MSG_ERROR, RESULT_MSG_ERROR,
RESULT_MSG_SUCCESS,
} from '@/constants/result-constants'; } from '@/constants/result-constants';
import { language, request } from '@/plugins/http-fetch'; 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用户策略输入框 * 从参数配置PCF中获取对应信息提供给PCC用户策略输入框
* @param neType 网元类型
* @param topTag
* @param neId * @param neId
* @returns object { wrRule, dataArr } * @returns object {pccRules,sessionRules,qosTemplate,headerEnrichTemplate,serviceAreaRestriction}
*/ */
export async function getPCCRule(neId: any) { export async function getPCCRule(neId: any) {
return await Promise.allSettled([ const paramNameArr = [
// 获取参数规则 'pccRules',
request({ 'sessionRules',
url: `/api/rest/systemManagement/v1/elementType/pcf/objectType/config/pccRules`, 'qosTemplate',
method: 'get', 'headerEnrichTemplate',
params: { 'serviceAreaRestriction',
ne_id: neId, ];
}, const reqArr = [];
timeout: 1_000, for (const paramName of paramNameArr) {
}), reqArr.push(
// 获取对应信息 request({
request({ url: `/ne/config/data`,
url: `/api/rest/systemManagement/v1/elementType/pcf/objectType/config/sessionRules`, params: { neType: 'PCF', neId, paramName },
method: 'get', method: 'get',
params: { })
ne_id: neId, );
}, }
timeout: 1_000, return await Promise.allSettled(reqArr).then(resArr => {
}),
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();
// 规则数据 // 规则数据
if (resArr[0].status === 'fulfilled') { const obj: any = {};
const itemV = resArr[0].value; resArr.forEach((item, i: number) => {
// 解析数据 if (item.status === 'fulfilled') {
if ( const res = item.value;
itemV.code === RESULT_CODE_SUCCESS && if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
Array.isArray(itemV.data?.data) const key = paramNameArr[i];
) { obj[key] = res.data.map((item: any) => {
let itemData = itemV.data.data; if ('qosTemplate' === key) {
itemData.forEach((item: any) => { return { value: item.qosId, label: item.qosId };
pccJson.set(item.ruleId, { value: item.ruleId, label: item.ruleId }); }
}); if ('headerEnrichTemplate' === key) {
} return { value: item.templateName, label: item.templateName };
} }
if ('serviceAreaRestriction' === key) {
if (resArr[1].status === 'fulfilled') { return { value: item.name, label: item.name };
const itemV = resArr[1].value; }
// 解析数据 return { value: item.ruleId, label: item.ruleId };
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,
}); });
}); }
} }
} });
return obj;
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 };
}); });
} }

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'; import { request } from '@/plugins/http-fetch';
/** /**
* 查询文件列表列表 * 查询网元端文件列表
* @param query 查询参数 * @param query 查询参数
* @returns object * @returns object
*/ */
@@ -14,7 +14,7 @@ export function listNeFiles(query: Record<string, any>) {
} }
/** /**
* 从网元获取文件 * 从网元到本地获取文件
* @param query 查询参数 * @param query 查询参数
* @returns object * @returns object
*/ */
@@ -27,3 +27,24 @@ export function getNeFile(query: Record<string, any>) {
timeout: 180_000, 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,
});
}

View File

@@ -6,6 +6,7 @@ export function dumpStart(data: Record<string, string>) {
url: '/trace/tcpdump/start', url: '/trace/tcpdump/start',
method: 'post', method: 'post',
data: data, data: data,
timeout: 60_000,
}); });
} }
@@ -15,16 +16,6 @@ export function dumpStop(data: Record<string, string>) {
url: '/trace/tcpdump/stop', url: '/trace/tcpdump/stop',
method: 'post', method: 'post',
data: data, 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, timeout: 60_000,
}); });
} }
@@ -35,5 +26,6 @@ export function traceUPF(data: Record<string, string>) {
url: '/trace/tcpdump/upf', url: '/trace/tcpdump/upf',
method: 'post', method: 'post',
data: data, data: data,
timeout: 60_000,
}); });
} }

View File

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

View File

@@ -686,11 +686,12 @@ export default {
addrPlease: "Please fill in the host IP address correctly", addrPlease: "Please fill in the host IP address correctly",
port: "Port", port: "Port",
portPlease: "Please fill in the host port number correctly", portPlease: "Please fill in the host port number correctly",
user: "Login User", user: "User",
userPlease: "Please fill in the host login user correctly", userPlease: "Please fill in the host user correctly",
database: "DataBase",
authMode: "Auth Mode", authMode: "Auth Mode",
password: "Password", password: "Password",
passwordPlease: "Please fill in the host login password correctly", passwordPlease: "Please fill in the host password correctly",
privateKey: "Private Key", privateKey: "Private Key",
privateKeyPlease: "Please fill in the private key characters correctly ~/.ssh/id_rsa", privateKeyPlease: "Please fill in the private key characters correctly ~/.ssh/id_rsa",
passPhrase: "Private Key Cipher", passPhrase: "Private Key Cipher",
@@ -1080,7 +1081,25 @@ export default {
element:'Element', element:'Element',
granularity:'Granularity', granularity:'Granularity',
unit:'Unit', 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:{
"changeLine":"Change to Line Charts",
"changeBar":"Change to Bar Charts",
"chooseShowMetrics":"Select the metric you want to display",
"chooseMetrics":"Select an indicator",
},
}, },
traceManage: { traceManage: {
analysis: { analysis: {
@@ -1119,8 +1138,8 @@ export default {
fileUPFTip: 'UPF internal packet capture and analysis packet', fileUPFTip: 'UPF internal packet capture and analysis packet',
textStart: "Start", textStart: "Start",
textStop: "Stop", textStop: "Stop",
textLog: "Log", textLog: "LogFile",
textLogMsg: "Log Info", textLogMsg: "LogFile Info",
textDown: "Download", textDown: "Download",
downTip: "Are you sure you want to download the {title} capture data file?", downTip: "Are you sure you want to download the {title} capture data file?",
downOk: "{title} file download complete", downOk: "{title} file download complete",
@@ -2160,7 +2179,7 @@ export default {
realTimeStop:"Stop", realTimeStop:"Stop",
realTime:"Real Time Speed", realTime:"Real Time Speed",
pid:"PID", pid:"PID",
name:"APP Name", name:"Program name",
username:"User Name", username:"User Name",
runTime:"Run Time", runTime:"Run Time",
numThreads:"Thread", numThreads:"Thread",
@@ -2169,13 +2188,11 @@ export default {
diskWrite:"Disk Write", diskWrite:"Disk Write",
}, },
net:{ net:{
PID:"PID", localAddr:"Local Address",
name:"name", remoteAddr:"Foreign Address",
localAddr:"localAddr", status:"State",
remoteAddr:"remoteAddr", proto:"Proto",
status:"status", port:"Port",
type:"type",
port:"port",
}, },
}, },
}, },

View File

@@ -687,10 +687,11 @@ export default {
port: "端口", port: "端口",
portPlease: "请正确填写主机端口号", portPlease: "请正确填写主机端口号",
user: "用户名", user: "用户名",
userPlease: "请正确填写主机登录用户", userPlease: "请正确填写主机用户",
database: "数据库",
authMode: "认证模式", authMode: "认证模式",
password: "密码", password: "密码",
passwordPlease: "请正确填写主机登录密码", passwordPlease: "请正确填写主机密码",
privateKey: "私钥", privateKey: "私钥",
privateKeyPlease: "请正确填写私钥字符内容 ~/.ssh/id_rsa", privateKeyPlease: "请正确填写私钥字符内容 ~/.ssh/id_rsa",
passPhrase: "私钥密码", passPhrase: "私钥密码",
@@ -1080,7 +1081,26 @@ export default {
element:'元素', element:'元素',
granularity:'颗粒度', granularity:'颗粒度',
unit:'单位', unit:'单位',
} },
kpiKeyTarget:{
"fullWidthLayout":"全宽布局",
"twoColumnLayout":"两列布局",
"saveLayout": "保存布局",
"restoreSaved": "恢复布局",
"saveSuccess": " {name} 保存成功",
"restoreSavedSuccess": " {name} 恢复成功",
"noSavedLayout": "没有找到保存的布局 {name}",
"layout1": "布局1",
"layout2": "布局2",
"layout3": "布局3"
},
kpiOverView:{
"changeLine":"切换为折线图",
"changeBar":"切换为柱状图",
"chooseShowMetrics":"选择需要显示的指标",
"chooseMetrics":"选择指标",
},
}, },
traceManage: { traceManage: {
analysis: { analysis: {
@@ -1119,8 +1139,8 @@ export default {
fileUPFTip: 'UPF内部抓包分析包', fileUPFTip: 'UPF内部抓包分析包',
textStart: "开始", textStart: "开始",
textStop: "停止", textStop: "停止",
textLog: "日志", textLog: "日志文件",
textLogMsg: "日志信息", textLogMsg: "日志文件信息",
textDown: "下载", textDown: "下载",
downTip: "确认要下载 {title} 抓包数据文件吗?", downTip: "确认要下载 {title} 抓包数据文件吗?",
downOk: "{title} 文件下载完成", downOk: "{title} 文件下载完成",
@@ -2169,13 +2189,11 @@ export default {
diskWrite:"磁盘写入", diskWrite:"磁盘写入",
}, },
net:{ net:{
PID:"PID", localAddr:"本地地址",
name:"名称", remoteAddr:"远程地址",
localAddr:"localAddr",
remoteAddr:"remoteAddr",
status:"状态", status:"状态",
type:"类型", proto:"协议",
port:"口", port:"口",
}, },
}, },
}, },

View File

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

View File

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

@@ -2,8 +2,7 @@
import { PageContainer } from 'antdv-pro-layout'; import { PageContainer } from 'antdv-pro-layout';
import { ColumnsType } from 'ant-design-vue/lib/table'; import { ColumnsType } from 'ant-design-vue/lib/table';
import { message } from 'ant-design-vue/lib'; import { message } from 'ant-design-vue/lib';
import { reactive, toRaw, ref, onMounted, onBeforeUnmount, markRaw } from 'vue'; import { reactive, ref, onMounted, onBeforeUnmount, markRaw } from 'vue';
import { listMain } from '@/api/index';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import { TooltipComponent } from 'echarts/components'; import { TooltipComponent } from 'echarts/components';
import { GaugeChart } from 'echarts/charts'; import { GaugeChart } from 'echarts/charts';
@@ -15,6 +14,8 @@ import { LabelLayout } from 'echarts/features';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import useDictStore from '@/store/modules/dict'; import useDictStore from '@/store/modules/dict';
import { listAllNeInfo } from '@/api/ne/neInfo';
import { parseDateToStr } from '@/utils/date-utils';
const { getDict } = useDictStore(); const { getDict } = useDictStore();
const appStore = useAppStore(); const appStore = useAppStore();
const route = useRoute(); const route = useRoute();
@@ -52,44 +53,55 @@ let indexColor = ref<DictType[]>([
let tableColumns: ColumnsType = [ let tableColumns: ColumnsType = [
{ {
title: t('views.index.object'), title: t('views.index.object'),
dataIndex: 'name', dataIndex: 'neName',
align: 'center', align: 'left',
key: 'status',
}, },
{ {
title: t('views.index.realNeStatus'), title: t('views.index.realNeStatus'),
dataIndex: 'status', dataIndex: 'serverState',
align: 'center', align: 'left',
customRender(opt) { key: 'status',
if (opt.value == 'Normal') return t('views.index.normal');
return t('views.index.abnormal');
},
}, },
{ {
title: t('views.index.reloadTime'), title: t('views.index.reloadTime'),
dataIndex: 'refresh', dataIndex: 'serverState',
align: 'center', align: 'left',
customRender(opt) {
if (opt.value?.refreshTime) return parseDateToStr(opt.value?.refreshTime);
return '-';
},
}, },
{ {
title: t('views.index.version'), title: t('views.index.version'),
dataIndex: 'version', dataIndex: 'serverState',
align: 'center', align: 'left',
customRender(opt) {
return opt.value?.version || '-';
},
}, },
{ {
title: t('views.index.serialNum'), title: t('views.index.serialNum'),
dataIndex: 'serialNum', dataIndex: 'serverState',
align: 'center', align: 'left',
customRender(opt) {
return opt.value?.sn || '-';
},
}, },
{ {
title: t('views.index.expiryDate'), title: t('views.index.expiryDate'),
dataIndex: 'expiryDate', dataIndex: 'serverState',
align: 'center', align: 'left',
customRender(opt) {
return opt.value?.expire || '-';
},
}, },
{ {
title: t('views.index.ipAddress'), title: t('views.index.ipAddress'),
dataIndex: 'ipAddress', dataIndex: 'serverState',
key: 'groupName', align: 'left',
align: 'center', customRender(opt) {
return opt.value?.neIP || '-';
},
}, },
]; ];
/**表格状态类型 */ /**表格状态类型 */
@@ -130,12 +142,8 @@ type nfStateType = {
hostName: string; hostName: string;
/**操作系统信息 */ /**操作系统信息 */
osInfo: string; osInfo: string;
/**数据库信息 */
dbInfo: string;
/**IP地址 */ /**IP地址 */
ipAddress: string; ipAddress: string;
/**端口 */
port: number;
/**版本 */ /**版本 */
version: string; version: string;
/**CPU利用率 */ /**CPU利用率 */
@@ -153,11 +161,8 @@ type nfStateType = {
/**网元详细信息 */ /**网元详细信息 */
let pronInfo: nfStateType = reactive({ let pronInfo: nfStateType = reactive({
hostName: '5gc', hostName: '5gc',
osInfo: osInfo: 'Linux 5gc 4.15.0-112-generic 2020 x86_64 GNU/Linux',
'Linux 5gc 4.15.0-112-generic #113-Ubuntu SMP Thu Jul 9 23:41:39 UTC 2020 x86_64 GNU/Linux',
dbInfo: 'adb v1.0.1',
ipAddress: '-', ipAddress: '-',
port: 3030,
version: '-', version: '-',
cpuUse: '-', cpuUse: '-',
memoryUse: '-', memoryUse: '-',
@@ -170,8 +175,8 @@ let pronInfo: nfStateType = reactive({
function fnGetList(one: boolean) { function fnGetList(one: boolean) {
if (tableState.loading) return; if (tableState.loading) return;
one && (tableState.loading = true); one && (tableState.loading = true);
listMain().then(res => { listAllNeInfo({ bandStatus: true }).then(res => {
tableState.data = res; tableState.data = res.data;
tableState.loading = false; tableState.loading = false;
var rightNum = 0; var rightNum = 0;
var errorNum = 0; var errorNum = 0;
@@ -254,17 +259,14 @@ const closeDrawer = () => {
function rowClick(record: any, index: any) { function rowClick(record: any, index: any) {
return { return {
onClick: (event: any) => { onClick: (event: any) => {
if ( let pronData = JSON.parse(JSON.stringify(record.serverState));
toRaw(record).status == '异常' || if (!pronData.online) {
toRaw(record).status == 'Abnormal'
) {
message.error(t('views.index.neStatus'), 2); message.error(t('views.index.neStatus'), 2);
return false; return false;
} else { } else {
let pronData = toRaw(record); const totalMemInKB = pronData.mem?.totalMem;
const totalMemInKB = pronData.memUsage?.totalMem; const nfUsedMemInKB = pronData.mem?.nfUsedMem;
const nfUsedMemInKB = pronData.memUsage?.nfUsedMem; const sysMemUsageInKB = pronData.mem?.sysMemUsage;
const sysMemUsageInKB = pronData.memUsage?.sysMemUsage;
// 将KB转换为MB // 将KB转换为MB
const totalMemInMB = Math.round((totalMemInKB / 1024) * 100) / 100; const totalMemInMB = Math.round((totalMemInKB / 1024) * 100) / 100;
@@ -274,19 +276,17 @@ function rowClick(record: any, index: any) {
//渲染详细信息 //渲染详细信息
pronInfo = { pronInfo = {
hostName: pronData.hostName, hostName: pronData.hostname,
osInfo: pronData.osInfo, osInfo: pronData.os,
dbInfo: pronData.dbInfo, ipAddress: pronData.neIP,
ipAddress: pronData.ipAddress,
port: pronData.port,
version: pronData.version, version: pronData.version,
cpuUse: cpuUse:
pronData.name + pronData.neName +
':' + ':' +
pronData.cpuUsage?.nfCpuUsage / 100 + pronData.cpu?.nfCpuUsage / 100 +
'%; ' + '%; ' +
'SYS:' + 'SYS:' +
pronData.cpuUsage?.sysCpuUsage / 100 + pronData.cpu?.sysCpuUsage / 100 +
'%', '%',
memoryUse: memoryUse:
'Total:' + 'Total:' +
@@ -299,8 +299,8 @@ function rowClick(record: any, index: any) {
sysMemUsageInMB + sysMemUsageInMB +
'MB', 'MB',
capability: pronData.capability, capability: pronData.capability,
serialNum: pronData.serialNum, serialNum: pronData.sn,
expiryDate: pronData.expiryDate, expiryDate: pronData.expire,
}; };
} }
visible.value = true; visible.value = true;
@@ -352,15 +352,9 @@ onBeforeUnmount(() => {
<a-descriptions-item :label="t('views.index.osInfo')">{{ <a-descriptions-item :label="t('views.index.osInfo')">{{
pronInfo.osInfo pronInfo.osInfo
}}</a-descriptions-item> }}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.dbInfo')">{{
pronInfo.dbInfo
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.ipAddress')">{{ <a-descriptions-item :label="t('views.index.ipAddress')">{{
pronInfo.ipAddress pronInfo.ipAddress
}}</a-descriptions-item> }}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.port')">{{
pronInfo.port
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.version')">{{ <a-descriptions-item :label="t('views.index.version')">{{
pronInfo.version pronInfo.version
}}</a-descriptions-item> }}</a-descriptions-item>
@@ -370,9 +364,6 @@ onBeforeUnmount(() => {
<a-descriptions-item :label="t('views.index.memoryUse')">{{ <a-descriptions-item :label="t('views.index.memoryUse')">{{
pronInfo.memoryUse pronInfo.memoryUse
}}</a-descriptions-item> }}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.capability')">{{
pronInfo.capability
}}</a-descriptions-item>
<a-descriptions-item :label="t('views.index.serialNum')">{{ <a-descriptions-item :label="t('views.index.serialNum')">{{
pronInfo.serialNum pronInfo.serialNum
}}</a-descriptions-item> }}</a-descriptions-item>
@@ -398,11 +389,11 @@ onBeforeUnmount(() => {
> >
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'"> <template v-if="column.key === 'status'">
<div v-if="record.status == '正常' || record.status == 'Normal'"> <div v-if="record.serverState.online">
<a-tag color="blue">{{ record.name }}</a-tag> <a-tag color="blue">{{ t('views.index.normal') }}</a-tag>
</div> </div>
<div v-else> <div v-else>
<a-tag color="pink">{{ record.name }}</a-tag> <a-tag color="pink">{{ t('views.index.abnormal') }}</a-tag>
</div> </div>
</template> </template>
</template> </template>
@@ -410,10 +401,7 @@ onBeforeUnmount(() => {
</a-col> </a-col>
<a-col :lg="10" :md="8" :xs="24"> <a-col :lg="10" :md="8" :xs="24">
<a-card :title="t('views.index.runStatus')" style="margin-bottom: 16px"> <a-card :title="t('views.index.runStatus')" style="margin-bottom: 16px">
<div <div style="width: 100%; min-height: 200px" ref="statusBar"></div>
style="width: 100%; min-height: 200px"
ref="statusBar"
></div>
</a-card> </a-card>
<a-card :title="t('views.index.mark')" style="margin-top: 16px"> <a-card :title="t('views.index.mark')" style="margin-top: 16px">
<a-descriptions <a-descriptions

View File

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

View File

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

View File

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

View File

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

View File

@@ -170,19 +170,21 @@ async function fnGetSkim() {
function loadData() { function loadData() {
fnGetNeState(); // 获取网元状态 fnGetNeState(); // 获取网元状态
userActivitySend(); userActivitySend();
upfTFSend(0); upfTFSend('0');
upfTFSend(7); upfTFSend('7');
upfTFSend(30); upfTFSend('30');
clearInterval(interval10s.value); clearInterval(interval10s.value);
interval10s.value = setInterval(() => { interval10s.value = setInterval(() => {
upfTFActive.value = upfTFActive.value >= 2 ? 0 : upfTFActive.value + 1; if (upfTFActive.value === '0') {
if (upfTFActive.value === 0) { upfTFSend('7');
upfTFSend(7); upfTFActive.value = '7';
} else if (upfTFActive.value === 1) { } else if (upfTFActive.value === '7') {
upfTFSend(30); upfTFSend('30');
} else if (upfTFActive.value === 2) { upfTFActive.value = '30';
upfTFSend(0); } else if (upfTFActive.value === '30') {
upfTFSend('0');
upfTFActive.value = '0';
} }
}, 10_000); }, 10_000);
@@ -203,9 +205,13 @@ function fnSelectNe(value: any, option: any) {
queryParams.neRealId = value; queryParams.neRealId = value;
upfWhoId.value = value; upfWhoId.value = value;
reSendUPF(option.rmUid); reSendUPF(option.rmUid);
upfTotalFlow.value.map((item: any) => { // upfTotalFlow.value.map((item: any) => {
item.requestFlag = false; // item.requestFlag = false;
}); // });
for (var key in upfTotalFlow.value) {
upfTotalFlow.value[key].requestFlag = false;
}
loadData(); loadData();
} }
@@ -483,12 +489,12 @@ onBeforeUnmount(() => {
<div class="filter"> <div class="filter">
<span <span
:data-key="v" :data-key="v"
:class="{ active: upfTFActive === i }" :class="{ active: upfTFActive === v }"
v-for="(v, i) in ['0', '7', '30']" v-for="v in ['0', '7', '30']"
:key="v" :key="v"
@click=" @click="
() => { () => {
upfTFActive = i; upfTFActive = v;
} }
" "
> >
@@ -508,14 +514,14 @@ onBeforeUnmount(() => {
<ArrowUpOutlined style="color: #597ef7" /> <ArrowUpOutlined style="color: #597ef7" />
{{ t('views.dashboard.overview.upfFlowTotal.up') }} {{ t('views.dashboard.overview.upfFlowTotal.up') }}
</span> </span>
<h4>{{ upfTotalFlow[upfTFActive].up }}</h4> <h4>{{ upfTotalFlow[upfTFActive].upFrom }}</h4>
</div> </div>
<div class="item"> <div class="item">
<span> <span>
<ArrowDownOutlined style="color: #52c41a" /> <ArrowDownOutlined style="color: #52c41a" />
{{ t('views.dashboard.overview.upfFlowTotal.down') }} {{ t('views.dashboard.overview.upfFlowTotal.down') }}
</span> </span>
<h4>{{ upfTotalFlow[upfTFActive].down }}</h4> <h4>{{ upfTotalFlow[upfTFActive].downFrom }}</h4>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@@ -426,7 +426,6 @@ function fnCancelConfirm() {
}); });
} }
// key替换中文title
// key替换中文title // key替换中文title
function mapKeysWithReduce(data: any[], titleMapping: Record<string, string>) { function mapKeysWithReduce(data: any[], titleMapping: Record<string, string>) {
return data.map((item: any) => { return data.map((item: any) => {

View File

@@ -80,7 +80,6 @@ function fnDesign(container: HTMLElement | undefined, option: EChartsOption) {
//渲染速率图 //渲染速率图
function handleRanderChart() { function handleRanderChart() {
console.log(upfFlowData.value);
const { lineXTime, lineYUp, lineYDown } = upfFlowData.value; const { lineXTime, lineYUp, lineYDown } = upfFlowData.value;
var yAxisSeries: any = [ var yAxisSeries: any = [
{ {
@@ -258,7 +257,6 @@ function fnGetInitData() {
for (const item of res.data) { for (const item of res.data) {
if (item.rmUID === selectRmUid.value) { if (item.rmUID === selectRmUid.value) {
console.log(item);
upfFlowParse(item); upfFlowParse(item);
} }
} }

View File

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

View File

@@ -204,11 +204,13 @@ function fnDirCD(dir: string, index?: number) {
/**网元类型选择对应修改 */ /**网元类型选择对应修改 */
function fnNeChange(keys: any, _: any) { 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]) { if (queryParams.neType !== neType || queryParams.neId !== neId) {
const neType = keys[0];
queryParams.neType = neType; queryParams.neType = neType;
queryParams.neId = keys[1]; queryParams.neId = neId;
if (neType === 'IMS') { if (neType === 'IMS') {
nePathArr.value = ['/var/log/ims']; nePathArr.value = ['/var/log/ims'];
queryParams.search = ''; queryParams.search = '';

View File

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

View File

@@ -229,11 +229,11 @@ function fnModalOk() {
* 表单修改网元类型 * 表单修改网元类型
*/ */
function fnNeTypeChange(v: any) { function fnNeTypeChange(v: any) {
const hostsLen = modalState.from.hosts.length;
// 网元默认只含22和4100 // 网元默认只含22和4100
if (hostsLen === 3 && v !== 'UPF') { if (modalState.from.hosts.length === 3) {
modalState.from.hosts.pop(); modalState.from.hosts.pop();
} }
const hostsLen = modalState.from.hosts.length;
// UPF标准版本可支持5002 // UPF标准版本可支持5002
if (hostsLen === 2 && v === 'UPF') { if (hostsLen === 2 && v === 'UPF') {
modalState.from.hosts.push({ modalState.from.hosts.push({
@@ -249,6 +249,22 @@ function fnNeTypeChange(v: any) {
remark: '', 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

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

View File

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

View File

@@ -316,8 +316,8 @@ function fnModalOk() {
.then(e => { .then(e => {
modalState.confirmLoading = true; modalState.confirmLoading = true;
const from = toRaw(modalState.from); const from = toRaw(modalState.from);
from.neId = queryParams.neId || '-';
from.algoIndex = `${from.algoIndex}`; from.algoIndex = `${from.algoIndex}`;
from.neId = queryParams.neId || '-';
const result = from.id const result = from.id
? updateUDMAuth(from) ? updateUDMAuth(from)
: from.num === 1 : from.num === 1
@@ -517,7 +517,7 @@ function fnExportList(type: string) {
if (!neId) return; if (!neId) return;
const hide = message.loading(t('common.loading'), 0); const hide = message.loading(t('common.loading'), 0);
exportUDMAuth({ ...queryParams, ...{ type } }) exportUDMAuth(Object.assign({ type: type }, queryParams))
.then(res => { .then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success(t('common.msgSuccess', { msg: t('common.export') }), 3); message.success(t('common.msgSuccess', { msg: t('common.export') }), 3);
@@ -555,6 +555,9 @@ function fnLoadData() {
fnQueryReset(); fnQueryReset();
}, timerS * 1000); }, timerS * 1000);
} else { } else {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
message.error({ message.error({
content: t('common.getInfoFail'), content: t('common.getInfoFail'),
duration: 3, duration: 3,
@@ -734,6 +737,7 @@ onMounted(() => {
v-model:value="queryParams.neId" v-model:value="queryParams.neId"
:options="neOtions" :options="neOtions"
:placeholder="t('common.selectPlease')" :placeholder="t('common.selectPlease')"
@change="fnGetList(1)"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
@@ -814,8 +818,8 @@ onMounted(() => {
<template #icon><ImportOutlined /></template> <template #icon><ImportOutlined /></template>
{{ t('views.neUser.auth.import') }} {{ t('views.neUser.auth.import') }}
</a-button> </a-button>
<!--
<a-popconfirm <!-- <a-popconfirm
:title="t('views.neUser.auth.exportConfirm')" :title="t('views.neUser.auth.exportConfirm')"
placement="topRight" placement="topRight"
ok-text="TXT" ok-text="TXT"

View File

@@ -29,12 +29,12 @@ const { t } = useI18n();
let neOtions = ref<Record<string, any>[]>([]); let neOtions = ref<Record<string, any>[]>([]);
/**表单中多选的OPTION */ /**表单中多选的OPTION */
const pcfRuleOption = reactive({ const pcfRuleOption = ref<Record<string, any[]>>({
pccOpt: [], pccRules: [],
sessOpt: [], sessionRules: [],
qosOpt: [], qosTemplate: [],
headerOpt: [], headerEnrichTemplate: [],
sarOpt: [], serviceAreaRestriction: [],
}); });
/**查询参数 */ /**查询参数 */
@@ -103,7 +103,7 @@ let tableColumns = ref<TableColumnsType>([
title: 'SAR', title: 'SAR',
dataIndex: 'sar', dataIndex: 'sar',
align: 'left', align: 'left',
width: 50, width: 150,
}, },
{ {
title: 'RFSP', title: 'RFSP',
@@ -121,34 +121,54 @@ let tableColumns = ref<TableColumnsType>([
title: 'QoS Audio', title: 'QoS Audio',
dataIndex: 'qosAudio', dataIndex: 'qosAudio',
align: 'left', 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, resizable: true,
width: 150, width: 150,
minWidth: 100, minWidth: 100,
maxWidth: 300, maxWidth: 300,
}, },
{
title: 'PCC Rules',
dataIndex: 'pccRules',
align: 'left',
width: 120,
},
{ {
title: 'SESS Rules', title: 'SESS Rules',
dataIndex: 'sessRules', dataIndex: 'sessRules',
align: 'left', align: 'left',
width: 120, width: 150,
}, },
{ {
title: 'HDR Enrich', title: 'HDR Enrich',
dataIndex: 'hdrEnrich', dataIndex: 'hdrEnrich',
align: 'left', align: 'left',
width: 100, width: 150,
}, },
{ {
title: 'UE Policy', title: 'UE Policy',
dataIndex: 'uePolicy', dataIndex: 'uePolicy',
align: 'left', align: 'left',
width: 100, width: 150,
}, },
{ {
title: t('common.operate'), title: t('common.operate'),
@@ -258,12 +278,8 @@ const modalStateFrom = Form.useForm(
*/ */
function fnModalVisibleByEdit(row?: Record<string, any>) { function fnModalVisibleByEdit(row?: Record<string, any>) {
getPCCRule(queryParams.neId) getPCCRule(queryParams.neId)
.then((res: any) => { .then((data: any) => {
pcfRuleOption.pccOpt = res.pccJson; pcfRuleOption.value = data;
pcfRuleOption.sessOpt = res.sessJson;
pcfRuleOption.qosOpt = res.qosJson;
pcfRuleOption.headerOpt = res.headerJson;
pcfRuleOption.sarOpt = res.sarJson;
}) })
.finally(() => { .finally(() => {
modalState.isBatch = false; modalState.isBatch = false;
@@ -438,12 +454,9 @@ function fnModalCancel() {
*/ */
function fnModalVisibleByBatch(type: 'delete' | 'add' | 'update') { function fnModalVisibleByBatch(type: 'delete' | 'add' | 'update') {
getPCCRule(queryParams.neId) getPCCRule(queryParams.neId)
.then((res: any) => { .then((data: any) => {
pcfRuleOption.pccOpt = res.pccJson; pcfRuleOption.value = data;
pcfRuleOption.sessOpt = res.sessJson; console.log(data);
pcfRuleOption.qosOpt = res.qosJson;
pcfRuleOption.headerOpt = res.headerJson;
pcfRuleOption.sarOpt = res.sarJson;
}) })
.finally(() => { .finally(() => {
modalStateFrom.resetFields(); //重置表单 modalStateFrom.resetFields(); //重置表单
@@ -1021,7 +1034,7 @@ onMounted(() => {
v-model:value="modalState.from.pccRules" v-model:value="modalState.from.pccRules"
allow-clear allow-clear
mode="tags" mode="tags"
:options="pcfRuleOption.pccOpt" :options="pcfRuleOption.pccRules"
:title="t('views.neUser.pcf.pccRuleTip')" :title="t('views.neUser.pcf.pccRuleTip')"
/> />
</a-form-item> </a-form-item>
@@ -1032,7 +1045,7 @@ onMounted(() => {
v-model:value="modalState.from.sessRules" v-model:value="modalState.from.sessRules"
allow-clear allow-clear
mode="tags" mode="tags"
:options="pcfRuleOption.sessOpt" :options="pcfRuleOption.sessionRules"
:title="t('views.neUser.pcf.sessRuleTip')" :title="t('views.neUser.pcf.sessRuleTip')"
/> />
</a-form-item> </a-form-item>
@@ -1045,7 +1058,7 @@ onMounted(() => {
<a-auto-complete <a-auto-complete
v-model:value="modalState.from.qosAudio" v-model:value="modalState.from.qosAudio"
allow-clear allow-clear
:options="pcfRuleOption.qosOpt" :options="pcfRuleOption.qosTemplate"
:filter-option="filterOption" :filter-option="filterOption"
/> />
</a-form-item> </a-form-item>
@@ -1055,7 +1068,7 @@ onMounted(() => {
<a-auto-complete <a-auto-complete
v-model:value="modalState.from.qosVideo" v-model:value="modalState.from.qosVideo"
allow-clear allow-clear
:options="pcfRuleOption.qosOpt" :options="pcfRuleOption.qosTemplate"
:filter-option="filterOption" :filter-option="filterOption"
/> />
</a-form-item> </a-form-item>
@@ -1068,7 +1081,7 @@ onMounted(() => {
<a-auto-complete <a-auto-complete
v-model:value="modalState.from.hdrEnrich" v-model:value="modalState.from.hdrEnrich"
allow-clear allow-clear
:options="pcfRuleOption.headerOpt" :options="pcfRuleOption.headerEnrichTemplate"
:filter-option="filterOption" :filter-option="filterOption"
/> />
</a-form-item> </a-form-item>
@@ -1099,7 +1112,7 @@ onMounted(() => {
<a-auto-complete <a-auto-complete
v-model:value="modalState.from.sar" v-model:value="modalState.from.sar"
allow-clear allow-clear
:options="pcfRuleOption.sarOpt" :options="pcfRuleOption.serviceAreaRestriction"
:filter-option="filterOption" :filter-option="filterOption"
/> />
</a-form-item> </a-form-item>

View File

@@ -125,12 +125,6 @@ let tableColumns = ref<ColumnsType>([
align: 'center', align: 'center',
width: 100, width: 100,
}, },
{
title: 'RAT',
dataIndex: 'rat',
align: 'center',
width: 50,
},
{ {
title: 'Forbidden Areas', title: 'Forbidden Areas',
dataIndex: 'arfb', dataIndex: 'arfb',
@@ -267,19 +261,24 @@ let modalState: ModalStateType = reactive({
title: 'UDM签约用户', title: 'UDM签约用户',
from: { from: {
id: undefined, id: undefined,
num: 1, neId: '',
msisdn: '',
imsi: '', imsi: '',
msisdn: '',
// amDat
ambr: 'def_ambr', ambr: 'def_ambr',
nssai: 'def_nssai', nssai: 'def_nssai',
rat: '0', rat: '0', // 0x00:VIRTUAL 0x01:WLAN 0x02:EUTRA 0x03:NR
arfb: 'def_arfb', arfb: 'def_arfb',
sar: 'def_sar', sar: 'def_sar',
cn: '3', cnType: '3', // 0x00:EPC和5GC 0x01:5GC 0x02:EPC 0x03:EPC+5GC
smData: '', rfspIndex: 1,
smfSel: 'def_snssai', regTimer: 12000,
epsDat: '', ueUsageType: 1,
neId: '', activeTime: 1000,
mico: '0',
odbPs: '1',
groupId: '-',
// epsDat
epsFlag: '1', epsFlag: '1',
epsOdb: [2], epsOdb: [2],
hplmnOdb: [3, 4], hplmnOdb: [3, 4],
@@ -288,9 +287,12 @@ let modalState: ModalStateType = reactive({
contextId: '1', contextId: '1',
apnContext: [1, 2, 0, 0, 0, 0], apnContext: [1, 2, 0, 0, 0, 0],
staticIp: '-', staticIp: '-',
rfsp: 1, //
ueType: 1, smData: '',
smfSel: 'def_snssai',
cag: 'def_cag', cag: 'def_cag',
// 非字段
num: 1,
remark: '', remark: '',
}, },
BatchDelForm: { BatchDelForm: {
@@ -644,6 +646,10 @@ function fnModalOk() {
.map((item: number) => `${item}`.padStart(2, '0')) .map((item: number) => `${item}`.padStart(2, '0'))
.join(''); .join('');
from.activeTime = `${from.activeTime}`;
from.rfspIndex = `${from.rfspIndex}`;
from.regTimer = `${from.regTimer}`;
from.ueUsageType = `${from.ueUsageType}`;
from.neId = queryParams.neId || '-'; from.neId = queryParams.neId || '-';
const result = from.id const result = from.id
? updateUDMSub(from) ? updateUDMSub(from)
@@ -849,46 +855,27 @@ function fnRecordDelete(imsi: string) {
function fnRecordExport(type: string = 'txt') { function fnRecordExport(type: string = 'txt') {
const selectLen = tableState.selectedRowKeys.length; const selectLen = tableState.selectedRowKeys.length;
if (selectLen <= 0) return; if (selectLen <= 0) return;
const rows: Record<string, any>[] = tableState.data.filter( const neId = queryParams.neId;
(row: Record<string, any>) => if (!neId) return;
tableState.selectedRowKeys.indexOf(row.imsi) >= 0 const hide = message.loading(t('common.loading'), 0);
); exportUDMSub({ type: type, neId: neId, imsis: tableState.selectedRowKeys })
.then(res => {
let content = ''; if (res.code === RESULT_CODE_SUCCESS) {
if (type == 'txt') { message.success({
for (const row of rows) { content: t('common.msgSuccess', { msg: t('common.export') }),
const epsDat = [ duration: 2,
row.epsFlag, });
row.epsOdb, saveAs(res.data, `UDMSub_select_${Date.now()}.${type}`);
row.hplmnOdb, } else {
row.ard, message.error({
row.epstpl, content: `${res.msg}`,
row.contextId, duration: 2,
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},${row.tenantName}\r\n`; .finally(() => {
} hide();
} });
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}`);
} }
/**列表导出 */ /**列表导出 */
@@ -902,7 +889,12 @@ function fnExportList(type: string) {
if (!neId) return; if (!neId) return;
const key = 'exportSub'; const key = 'exportSub';
message.loading({ content: t('common.loading'), key }); message.loading({ content: t('common.loading'), key });
exportUDMSub({ ...queryParams, ...{ type, imsi: realImsi } }).then(res => {
exportUDMSub({
...queryParams,
imsi: realImsi,
type: type,
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success({ message.success({
content: t('common.msgSuccess', { msg: t('common.export') }), content: t('common.msgSuccess', { msg: t('common.export') }),
@@ -944,6 +936,9 @@ function fnLoadData() {
fnQueryReset(); fnQueryReset();
}, timerS * 1000); }, timerS * 1000);
} else { } else {
modalState.loadDataLoading = false;
tableState.loading = false; // 表格loading
fnQueryReset();
message.error({ message.error({
content: t('common.getInfoFail'), content: t('common.getInfoFail'),
duration: 3, duration: 3,
@@ -976,7 +971,10 @@ function fnGetList(pageNum?: number) {
pageSize: queryParams.pageSize, pageSize: queryParams.pageSize,
}; };
listUDMSub(toBack).then(res => { listUDMSub({
...queryParams,
imsi: imsiMatchRule[queryParams.imsiMatch],
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) { if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
// 取消勾选 // 取消勾选
if (tableState.selectedRowKeys.length > 0) { if (tableState.selectedRowKeys.length > 0) {
@@ -1175,6 +1173,7 @@ onMounted(() => {
v-model:value="queryParams.neId" v-model:value="queryParams.neId"
:options="neOtions" :options="neOtions"
:placeholder="t('common.selectPlease')" :placeholder="t('common.selectPlease')"
@change="fnGetList(1)"
/> />
</a-form-item> </a-form-item>
</a-col> </a-col>
@@ -1336,9 +1335,6 @@ onMounted(() => {
ok-text="TXT" ok-text="TXT"
ok-type="default" ok-type="default"
@confirm="fnRecordExport('txt')" @confirm="fnRecordExport('txt')"
:show-cancel="false"
cancel-text="CSV"
@cancel="fnRecordExport('csv')"
:disabled="tableState.selectedRowKeys.length <= 0" :disabled="tableState.selectedRowKeys.length <= 0"
> >
<a-button <a-button
@@ -1423,7 +1419,7 @@ onMounted(() => {
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'cnFlag'"> <template v-if="column.key === 'cnFlag'">
{{ {{
record.cn === '3' ['1', '3'].includes(record.cnType)
? t('views.neUser.sub.enable') ? t('views.neUser.sub.enable')
: t('views.neUser.sub.disable') : t('views.neUser.sub.disable')
}} }}
@@ -1689,7 +1685,7 @@ onMounted(() => {
name="cnFlag" name="cnFlag"
:help="t('views.neUser.sub.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"> <a-select-option value="3">
{{ t('views.neUser.sub.enable') }} {{ t('views.neUser.sub.enable') }}
</a-select-option> </a-select-option>
@@ -1843,7 +1839,7 @@ onMounted(() => {
name="mico" name="mico"
:help="t('views.neUser.sub.micoTip')" :help="t('views.neUser.sub.micoTip')"
> >
<a-select value="1"> <a-select v-model:value="modalState.from.mico">
<a-select-option value="1"> <a-select-option value="1">
{{ t('views.neUser.sub.enable') }} {{ t('views.neUser.sub.enable') }}
</a-select-option> </a-select-option>
@@ -1854,9 +1850,19 @@ onMounted(() => {
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :lg="24" :md="24" :xs="24"> <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 <a-input-number
v-model:value="modalState.from.ueType" v-model:value="modalState.from.ueUsageType"
style="width: 100%" style="width: 100%"
:min="0" :min="0"
:max="127" :max="127"
@@ -1876,9 +1882,9 @@ onMounted(() => {
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :lg="24" :md="24" :xs="24"> <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 <a-input-number
v-model:value="modalState.from.rfsp" v-model:value="modalState.from.rfspIndex"
style="width: 100%" style="width: 100%"
:min="0" :min="0"
:max="127" :max="127"

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,712 @@
<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 } 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();
};
// 修改 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: '网元指标概览',
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: {
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: {
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;
}
// 存储指标列信
const kpiColumns = ref<KPIColumn[]>([]);
// 添加选中标的的状态
const selectedKPIs = ref<string[]>([]);
// 添加对话框可见性状态
const isModalVisible = ref(false);
// 打开对话框
const showKPISelector = () => {
isModalVisible.value = true;
};
// 添加保存选中指标到 localStorage 的方法
const saveSelectedKPIs = () => {
localStorage.setItem('selectedKPIs', JSON.stringify(selectedKPIs.value));
};
// 窗口确认按钮
const handleModalOk = () => {
saveSelectedKPIs(); // 保存选择
updateChart();
isModalVisible.value = false;
};
// 取消按钮
const handleModalCancel = () => {
isModalVisible.value = false;
};
// 获取网元指标
const fetchSpecificKPI = async () => {
const language = currentLocale.value.split('_')[0] === 'zh' ? 'cn' : currentLocale.value.split('_')[0];
try {
let allKPIs: KPIColumn[] = [];
// 使用 ALL_NE_TYPES 遍历网元类型
for (const neType of ALL_NE_TYPES) {
const res = await getKPITitle(neType);
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.data)) {
// 过滤当前网元类型的指标
const filteredKPIs = res.data.filter(item =>
TARGET_KPI_IDS[neType].some(targetId =>
item.kpiId === targetId || // 完全匹配
item.kpiId.startsWith(targetId) // 前缀匹配
)
);
// 转换并添加到总指标列表
const formattedKPIs = filteredKPIs.map(item => ({
title: item[`${language}Title`],
dataIndex: item.kpiId,
key: item.kpiId,
kpiId: item.kpiId
}));
allKPIs = [...allKPIs, ...formattedKPIs];
}
}
// 更新指标列信息
kpiColumns.value = allKPIs;
// 尝试加载保存的选择
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 {
// 如果没有有效的保存选择,则默认全选
selectedKPIs.value = kpiColumns.value.map(col => col.kpiId);
}
} else {
// 如果没有保存的选择,则默认全选
selectedKPIs.value = kpiColumns.value.map(col => col.kpiId);
}
if (kpiColumns.value.length === 0) {
console.warn('No matching KPIs found');
} else {
console.log(`Found ${kpiColumns.value.length} total KPIs:`,
kpiColumns.value.map(kpi => kpi.kpiId));
}
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();
// 修改 updateChartData 方法,只更新数据而不重新渲染整个图表
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.some(targetId =>
kpi.kpiId === targetId || // 完全匹配
kpi.kpiId.startsWith(targetId) // 前缀匹配
)
);
});
return groups;
});
</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="t('views.perfManage.kpiOverView.chooseShowMetrics')"
@ok="handleModalOk"
@cancel="handleModalCancel"
width="800px"
:bodyStyle="{ maxHeight: '600px', overflow: 'auto' }"
>
<a-checkbox-group v-model:value="selectedKPIs">
<div class="kpi-checkbox-list">
<div v-for="neType in ALL_NE_TYPES" :key="neType" class="ne-type-group">
<div class="ne-type-title">{{ neType.toUpperCase() }}</div>
<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>
</div>
</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-group {
margin-bottom: 24px;
border: 1px solid #f0f0f0;
border-radius: 4px;
width: 100%;
background: #fff;
}
.ne-type-title {
padding: 12px 16px;
font-weight: bold;
background-color: #fafafa;
border-bottom: 1px solid #f0f0f0;
}
.ne-type-items {
padding: 16px;
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;
}
/* 深度选择器样式 */
:deep(.ant-modal-body) {
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
&:hover {
background: #999;
}
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
}
/* 组件高度统一样式 */
: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-form-item-label) {
padding-right: 8px;
line-height: 32px;
}
:deep(.ant-checkbox-wrapper) {
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</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)[], _: (string | number)[],
rows: Record<string, string>[] rows: Record<string, string>[]
) { ) {
//debugger
tableState.selectedRowKeys = rows.map(item => item.loginId); tableState.selectedRowKeys = rows.map(item => item.loginId);
// 针对单个登录账号解锁 // 针对单个登录账号解锁
if (rows.length === 1) { if (rows.length === 1) {

View File

@@ -139,7 +139,7 @@ let tableColumns: any = [
* 测试主机连接 * 测试主机连接
*/ */
function fnHostTest(row: Record<string, any>) { function fnHostTest(row: Record<string, any>) {
if (tabState.confirmLoading || !row.addr) return; if (tabState.confirmLoading || !row.addr || !row.port) return;
tabState.confirmLoading = true; tabState.confirmLoading = true;
const hide = message.loading(t('common.loading'), 0); const hide = message.loading(t('common.loading'), 0);
testNeHost(row) testNeHost(row)
@@ -187,19 +187,19 @@ function fnHostAuthorized(row: Record<string, any>) {
* 表单修改网元类型 * 表单修改网元类型
*/ */
function fnNeTypeChange(v: any, data: any) { function fnNeTypeChange(v: any, data: any) {
const hostsLen = data.hosts.length;
// 网元默认只含22和4100 // 网元默认只含22和4100
if (hostsLen === 3 && v !== 'UPF') { if (modalState.from.hosts.length === 3) {
data.hosts.pop(); modalState.from.hosts.pop();
} }
const hostsLen = modalState.from.hosts.length;
// UPF标准版本可支持5002 // UPF标准版本可支持5002
if (hostsLen === 2 && v === 'UPF') { if (hostsLen === 2 && v === 'UPF') {
data.hosts.push({ modalState.from.hosts.push({
hostId: undefined, hostId: undefined,
hostType: 'telnet', hostType: 'telnet',
groupId: '1', groupId: '1',
title: 'Telnet_NE_5002', title: 'Telnet_NE_5002',
addr: '', addr: modalState.from.ip,
port: 5002, port: 5002,
user: 'admin', user: 'admin',
authMode: '0', authMode: '0',
@@ -207,6 +207,22 @@ function fnNeTypeChange(v: any, data: any) {
remark: '', 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': case 'N3IWF':
state.from.sbi.n3iwf_ip = item.ip; state.from.sbi.n3iwf_ip = item.ip;
break; break;
case 'SMSC':
state.from.sbi.smsc_ip = item.ip;
break;
} }
} }
} }

View File

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

View File

@@ -1,14 +1,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Modal, message } from 'ant-design-vue/lib'; import { Modal, message } from 'ant-design-vue/lib';
import { onMounted, reactive, toRaw } from 'vue'; import { onMounted, reactive, toRaw } from 'vue';
import useAppStore from '@/store/modules/app';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
import { listMenu } from '@/api/system/menu'; import { listMenu } from '@/api/system/menu';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import { getConfig, getConfigKey, changeValue } from '@/api/system/config'; import { getConfigKey, changeValue } from '@/api/system/config';
const { t } = useI18n();
const appStore = useAppStore();
const { t, optionsLocale } = useI18n();
type StateType = { type StateType = {
edite: boolean; edite: boolean;

View File

@@ -899,47 +899,27 @@ function fnRecordDelete(imsi: string) {
function fnRecordExport(type: string = 'txt') { function fnRecordExport(type: string = 'txt') {
const selectLen = tableState.selectedRowKeys.length; const selectLen = tableState.selectedRowKeys.length;
if (selectLen <= 0) return; if (selectLen <= 0) return;
const rows: Record<string, any>[] = tableState.data.filter( const neId = queryParams.neId;
(row: Record<string, any>) => if (!neId) return;
tableState.selectedRowKeys.indexOf(row.imsi) >= 0 const hide = message.loading(t('common.loading'), 0);
); exportUDMSub({ type: type, neId: neId, imsis: tableState.selectedRowKeys })
.then(res => {
let content = ''; if (res.code === RESULT_CODE_SUCCESS) {
if (type == 'txt') { message.success({
for (const row of rows) { content: t('common.msgSuccess', { msg: t('common.export') }),
debugger; duration: 2,
const epsDat = [ });
row.epsFlag, saveAs(res.data, `UDMSub_select_${Date.now()}.${type}`);
row.epsOdb, } else {
row.hplmnOdb, message.error({
row.ard, content: `${res.msg}`,
row.epstpl, duration: 2,
row.contextId, });
row.apnContext, }
row.staticIp, })
].join(','); .finally(() => {
content += `${row.imsi},${row.msisdn},${row.ambr},${row.nssai},${row.arfb},${row.sar},${row.rat},${row.cn},${row.smfSel},${row.smData},${epsDat}\r\n`; hide();
} });
}
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}`);
} }
/**列表导出 */ /**列表导出 */
@@ -953,7 +933,12 @@ function fnExportList(type: string) {
if (!neID) return; if (!neID) return;
const key = 'exportSub'; const key = 'exportSub';
message.loading({ content: t('common.loading'), key }); message.loading({ content: t('common.loading'), key });
exportUDMSub({ ...queryParams, ...{ type, imsi: realImsi } }).then(res => { Object.assign({ type: type }, queryParams);
exportUDMSub({
...queryParams,
imsi: realImsi,
type: type,
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS) { if (res.code === RESULT_CODE_SUCCESS) {
message.success({ message.success({
content: t('common.msgSuccess', { msg: t('common.export') }), content: t('common.msgSuccess', { msg: t('common.export') }),
@@ -1028,7 +1013,10 @@ function fnGetList(pageNum?: number) {
pageSize: queryParams.pageSize, pageSize: queryParams.pageSize,
}; };
listUDMSub(toBack).then(res => { listUDMSub({
...queryParams,
ismi: imsiMatchRule[queryParams.imsiMatch],
}).then(res => {
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) { if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
// 取消勾选 // 取消勾选
if (tableState.selectedRowKeys.length > 0) { if (tableState.selectedRowKeys.length > 0) {
@@ -1357,9 +1345,6 @@ onMounted(() => {
ok-text="TXT" ok-text="TXT"
ok-type="default" ok-type="default"
@confirm="fnRecordExport('txt')" @confirm="fnRecordExport('txt')"
:show-cancel="false"
cancel-text="CSV"
@cancel="fnRecordExport('csv')"
:disabled="tableState.selectedRowKeys.length <= 0" :disabled="tableState.selectedRowKeys.length <= 0"
> >
<a-button <a-button

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"> <script setup lang="ts">
import { reactive ,toRaw, onMounted } from 'vue'; import { reactive, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { PageContainer } from 'antdv-pro-layout'; 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 useI18n from '@/hooks/useI18n';
import { OptionsType, WS } from '@/plugins/ws-websocket'; import { OptionsType, WS } from '@/plugins/ws-websocket';
import { import { RESULT_CODE_ERROR } from '@/constants/result-constants';
RESULT_CODE_ERROR
} from '@/constants/result-constants';
const { t } = useI18n(); const { t } = useI18n();
const ws = new WS(); const ws = new WS();
//查询数据 /**表单查询参数 */
let queryParams = reactive({ let queryParams = reactive({
pid: undefined, pid: undefined,
name:"", name: '',
port:undefined, port: undefined,
flag: false,
changeTime:5000,
}); });
//临时缓存 /**状态对象 */
let queryParams2 = reactive({ let state = reactive({
pid: undefined, /**调度器 */
name:"", interval: null as any,
port:undefined, /**刷新周期 */
flag: false, intervalTime: 5_000,
changeTime:5000, /**查询参数 */
}) query: {
pid: undefined,
//时间粒度 name: '',
let timeOptions =[ port: undefined,
{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)//设置定时器
}); });
/**查询按钮**/ /**接收数据后回调(成功) */
function queryTime(){//将表单中的数据传递给缓存数据(缓存数据自动同步请求信息中) function wsMessage(res: Record<string, any>) {
queryParams2.pid=queryParams.pid; const { code, requestId, data } = res;
queryParams2.port=queryParams.port; if (code === RESULT_CODE_ERROR) {
queryParams2.name=queryParams.name; console.warn(res.msg);
//queryParams.flag = true return;
queryParams2.flag=true }
queryParams.pid=undefined;
queryParams.name=""; // 建联时发送请求
queryParams.port=undefined; if (!requestId && data.clientId) {
} fnGetList();
return;
/**重置按钮**/ }
function queryReset(){
queryParams.flag = false // 收到消息数据
queryParams2.flag = false if (requestId.startsWith('net_')) {
} // 将数据填入表格
if (Array.isArray(data)) {
let s:any = null if (tableState.loading) {
/**定时器**/ tableState.loading = false;
function queryReset2(c:boolean){ }
if(c){ tableState.data = data;
clearInterval(s)//清理旧定时器 } else {
ws.close();//断开原来的wb连接 tableState.data = [];
fnRealTime();//建立新的实时数据连接 }
} }
s = setInterval(()=>{//设置新的定时器s
extracted();
},queryParams.changeTime)
} }
/**刷新频率改变**/ /**实时数据*/
function fnRealTime2() {//时间粒度改变时触发 function fnRealTime(reLink: boolean) {
queryReset2(true)//改变定时器 if (reLink) {
} ws.close();
/** }
* 实时数据
*/
function fnRealTime() {
const options: OptionsType = { const options: OptionsType = {
url: '/ws', url: '/ws',
onmessage: wsMessage, onmessage: wsMessage,
onerror: wsError, onerror: (ev: any) => {
// 接收数据后回调
console.error(ev);
},
}; };
//建立连接 //建立连接
ws.connect(options); ws.connect(options);
} }
/**接收数据后回调(失败) */ /**调度器周期变更*/
function wsError(ev: any) { function fnIntervalChange(v: any) {
// 接收数据后回调 clearInterval(state.interval);
console.error(ev); state.interval = null;
const timer = parseInt(v);
if (timer > 1_000) {
state.intervalTime = v;
fnGetList();
}
} }
/**接收数据后回调(成功) */ /**查询列表 */
function wsMessage(res: Record<string, any>) { function fnGetList() {
const { code, requestId, data } = res;//获取数据 if (tableState.loading || ws.state() === -1) return;
if (code === RESULT_CODE_ERROR) { tableState.loading = true;
console.warn(res.msg); const msg = {
return; requestId: `net_${state.interval}`,
} type: 'net',
// 处理数据组成ip : port data: state.query,
let processedData: any; };
// 首发
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}`; function fnQuery() {
const remoteAddr = `${item.remoteaddr.ip} : ${item.remoteaddr.port}`; state.query = JSON.parse(JSON.stringify(queryParams));
return { ...item, localAddr, remoteAddr }; 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({ function fnQueryReset() {
loading: false, Object.assign(queryParams, {
size: 'large', pid: undefined,
data: [], name: '',
}); port: undefined,
});
tablePagination.current = 1;
tablePagination.pageSize = 20;
// 重置查询条件
Object.assign(state.query, {
pid: undefined,
name: '',
port: undefined,
});
}
/**表格状态类型 */ /**表格状态类型 */
type TableStateType = { type TableStateType = {
/**加载等待 */ /**加载等待 */
loading: boolean; loading: boolean;
/**紧凑型 */
size: SizeType;
/**记录数据 */ /**记录数据 */
data: object[]; data: object[];
}; };
/**表格状态 */
let tableState: TableStateType = reactive({
loading: false,
data: [],
});
/**表格字段列 */ /**表格字段列 */
const tableColumns: ColumnsType<any> = [ const tableColumns: ColumnsType<any> = [
{ {
title: t('views.tool.net.PID'), title: t('views.tool.ps.pid'),
dataIndex: 'pid', dataIndex: 'pid',
align: 'center', align: 'right',
width: 50, width: 100,
sorter:{//PID排序 sorter: {
compare:(a:any, b:any)=>a.pid-b.pid, compare: (a: any, b: any) => a.pid - b.pid,
multiple:1, multiple: 3,
} },
}, },
{ {
title: t('views.tool.net.name'), title: t('views.tool.net.proto'),
dataIndex: 'name', dataIndex: 'type',
align: 'center', align: 'left',
width: 100, width: 100,
customRender(opt) {
return `${opt.value}`.toUpperCase();
},
}, },
{ {
title: t('views.tool.net.localAddr'), title: t('views.tool.net.localAddr'),
dataIndex: 'localAddr', dataIndex: 'localAddr',
align: 'center', align: 'left',
width: 70, width: 150,
customRender(opt) {
const { ip, port } = opt.value;
return port !== 0 ? `${ip}:${port}` : `${ip}`;
},
}, },
{ {
title: t('views.tool.net.remoteAddr'), title: t('views.tool.net.remoteAddr'),
dataIndex:'remoteAddr', dataIndex: 'remoteAddr',
align: 'center', align: 'left',
width: 100, width: 150,
customRender(opt) {
const { ip, port } = opt.value;
return port !== 0 ? `${ip}:${port}` : `${ip}`;
},
}, },
{ {
title: t('views.tool.net.status'), title: t('views.tool.net.status'),
dataIndex: 'status', dataIndex: 'status',
align: 'center', align: 'left',
width: 70, width: 120,
}, },
{ {
title: t('views.tool.net.type'), title: t('views.tool.ps.name'),
dataIndex: 'type', dataIndex: 'name',
align: 'center', align: 'left',
width: 100,
}, },
]; ];
/**表格分页器参数 */
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> </script>
<template> <template>
<PageContainer> <PageContainer>
<a-card <a-card
:bordered="false" :bordered="false"
:body-style="{ marginBottom: '24px', paddingBottom: 0 }" :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-row :gutter="16">
<a-col :lg="3" :md="6" :xs="12"> <a-col :lg="4" :md="6" :xs="12">
<a-form-item :label="t('views.tool.ps.changeTime')" name="changeTime"> <a-form-item :label="t('views.tool.ps.pid')" name="pid">
<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-input-number <a-input-number
v-model:value="queryParams.pid" v-model:value="queryParams.pid"
allow-clear allow-clear
:placeholder="t('common.inputPlease')" :placeholder="t('common.inputPlease')"
style='width: 100%' style="width: 100%"
></a-input-number> ></a-input-number>
</a-form-item></a-col> </a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24"> <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 <a-input
v-model:value="queryParams.name" v-model:value="queryParams.name"
allow-clear allow-clear
:placeholder="t('common.inputPlease')" :placeholder="t('common.inputPlease')"
></a-input> ></a-input>
</a-form-item></a-col> </a-form-item>
</a-col>
<a-col :lg="6" :md="12" :xs="24"> <a-col :lg="6" :md="12" :xs="24">
<a-form-item :label="t('views.tool.net.port')" name="port"> <a-form-item :label="t('views.tool.net.port')" name="port">
<a-input <a-input-number
v-model:value="queryParams.port" v-model:value="queryParams.port"
allow-clear allow-clear
:placeholder="t('common.inputPlease')" :placeholder="t('common.inputPlease')"
></a-input> style="width: 100%"
</a-form-item></a-col> ></a-input-number>
<a-col :lg="3" :md="4" :xs="5"> </a-form-item>
<a-form-item> </a-col>
<a-button type="primary" @click='queryTime()'> <a-col :lg="4" :md="12" :xs="24">
<template #icon><SearchOutlined /></template> <a-form-item>
{{ t('common.search') }} <a-space :size="8">
</a-button> <a-button type="primary" @click.prevent="fnQuery()">
<a-button type="default" @click.prevent="queryReset()" style='left: 20px;'> <template #icon><SearchOutlined /></template>
<template #icon><ClearOutlined /></template> {{ t('common.search') }}
{{ t('common.reset') }} </a-button>
</a-button> <a-button type="default" @click.prevent="fnQueryReset">
<template #icon><ClearOutlined /></template>
{{ t('common.reset') }}
</a-button>
</a-space>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
</a-card> </a-card>
<a-table
class="table" <a-card :bordered="false" :body-style="{ padding: '0px' }">
row-key="id" <!-- 插槽-卡片左侧侧 -->
:columns="tableColumns" <template #title>
:loading="tableState.loading" <a-form layout="inline">
:data-source="tableState.data" <a-form-item :label="t('views.tool.ps.realTime')" name="realTime">
:size="tableState.size" <a-select
:scroll="{ y: 'calc(100vh - 480px)' }" v-model:value="state.intervalTime"
:pagination='false' :options="[
></a-table> { 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> </PageContainer>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.table :deep(.ant-pagination) { .table :deep(.ant-pagination) {
padding: 0 24px; 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 (requestId.startsWith('ps_')) {
// 将数据填入表格 // 将数据填入表格
if (Array.isArray(data)) { if (Array.isArray(data)) {
if (tableState.loading) {
tableState.loading = false;
}
tableState.data = data; tableState.data = data;
} else { } else {
tableState.data = []; tableState.data = [];
@@ -77,6 +80,7 @@ function fnRealTime(reLink: boolean) {
/**调度器周期变更*/ /**调度器周期变更*/
function fnIntervalChange(v: any) { function fnIntervalChange(v: any) {
clearInterval(state.interval); clearInterval(state.interval);
state.interval = null;
const timer = parseInt(v); const timer = parseInt(v);
if (timer > 1_000) { if (timer > 1_000) {
state.intervalTime = v; state.intervalTime = v;
@@ -97,10 +101,9 @@ function fnGetList() {
ws.send(msg); ws.send(msg);
// 定时刷新数据 // 定时刷新数据
state.interval = setInterval(() => { state.interval = setInterval(() => {
msg.data = state.query; msg.data = JSON.parse(JSON.stringify(state.query));
ws.send(msg); ws.send(msg);
}, state.intervalTime); }, state.intervalTime);
tableState.loading = false;
} }
/**查询参数传入 */ /**查询参数传入 */
@@ -263,6 +266,8 @@ onMounted(() => {
/**钩子函数,界面关闭*/ /**钩子函数,界面关闭*/
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearInterval(state.interval);
state.interval = null;
ws.close(); ws.close();
}); });
</script> </script>
@@ -320,8 +325,6 @@ onBeforeUnmount(() => {
</a-col> </a-col>
</a-row> </a-row>
</a-form> </a-form>
<div>{{ state.query }}</div>
<div>{{ queryParams }}</div>
</a-card> </a-card>
<a-card :bordered="false" :body-style="{ padding: '0px' }"> <a-card :bordered="false" :body-style="{ padding: '0px' }">

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import { Modal, message } from 'ant-design-vue/lib';
import { parseDateToStr } from '@/utils/date-utils'; import { parseDateToStr } from '@/utils/date-utils';
import { getNeFile, listNeFiles } from '@/api/tool/neFile'; import { getNeFile, listNeFiles } from '@/api/tool/neFile';
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants'; import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
import ViewDrawer from '@/views/logManage/neFile/components/ViewDrawer.vue';
import useNeInfoStore from '@/store/modules/neinfo'; import useNeInfoStore from '@/store/modules/neinfo';
import useTabsStore from '@/store/modules/tabs'; import useTabsStore from '@/store/modules/tabs';
import useI18n from '@/hooks/useI18n'; import useI18n from '@/hooks/useI18n';
@@ -218,16 +219,18 @@ function fnDirCD(dir: string, index?: number) {
/**网元类型选择对应修改 */ /**网元类型选择对应修改 */
function fnNeChange(keys: any, _: any) { 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]) { if (queryParams.neType !== neType || queryParams.neId !== neId) {
const neType = keys[0];
queryParams.neType = neType; queryParams.neType = neType;
queryParams.neId = keys[1]; queryParams.neId = neId;
if (neType === 'UPF' && tmp.value) { if (neType === 'UPF' && tmp.value) {
nePathArr.value = ['/tmp']; nePathArr.value = ['/tmp'];
queryParams.search = `${neType}_${keys[1]}`; queryParams.search = `${neType}_${neId}`;
} else { } else {
nePathArr.value = [`/tmp/omc/tcpdump/${neType.toLowerCase()}/${keys[1]}`]; nePathArr.value = [`/tmp/omc/tcpdump/${neType.toLowerCase()}/${neId}`];
queryParams.search = ''; queryParams.search = '';
} }
fnGetList(1); 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(() => { onMounted(() => {
// 获取网元网元列表 // 获取网元网元列表
neInfoStore.fnNelist().then(res => { neInfoStore.fnNelist().then(res => {
@@ -375,6 +397,16 @@ onMounted(() => {
<template #bodyCell="{ column, record }"> <template #bodyCell="{ column, record }">
<template v-if="column.key === 'fileName'"> <template v-if="column.key === 'fileName'">
<a-space :size="8" align="center"> <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 <a-button
type="link" type="link"
:loading="downLoading" :loading="downLoading"
@@ -398,6 +430,14 @@ onMounted(() => {
</template> </template>
</a-table> </a-table>
</a-card> </a-card>
<!-- 文件内容查看抽屉 -->
<ViewDrawer
v-model:visible="viewDrawerState.visible"
:file-path="viewDrawerState.filePath"
:ne-type="viewDrawerState.neType"
:ne-id="viewDrawerState.neId"
></ViewDrawer>
</PageContainer> </PageContainer>
</template> </template>

View File

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

View File

@@ -18,8 +18,8 @@ import {
packetStart, packetStart,
packetStop, packetStop,
packetFilter, packetFilter,
packetKeep,
} from '@/api/trace/packet'; } from '@/api/trace/packet';
import { s } from 'vite/dist/node/types.d-aGj9QkWt';
const ws = new WS(); const ws = new WS();
const { t } = useI18n(); const { t } = useI18n();
@@ -30,6 +30,8 @@ type StateType = {
devices: { id: string; label: string; children: any[] }[]; devices: { id: string; label: string; children: any[] }[];
/**初始化 */ /**初始化 */
initialized: boolean; initialized: boolean;
/**保活调度器 */
keepTimer: any;
/**任务 */ /**任务 */
task: { task: {
taskNo: string; taskNo: string;
@@ -65,6 +67,7 @@ type StateType = {
const state = reactive<StateType>({ const state = reactive<StateType>({
devices: [], devices: [],
initialized: false, initialized: false,
keepTimer: null,
task: { task: {
taskNo: 'laYlTbq', taskNo: 'laYlTbq',
device: '192.168.5.58', device: '192.168.5.58',
@@ -275,6 +278,9 @@ function wsMessage(res: Record<string, any>) {
// 建联时发送请求 // 建联时发送请求
if (!requestId && data.clientId) { if (!requestId && data.clientId) {
state.initialized = true; state.initialized = true;
state.keepTimer = setInterval(() => {
packetKeep(state.task.taskNo, 120);
}, 90 * 1000);
return; return;
} }
@@ -321,7 +327,9 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
ws.close(); clearInterval(state.keepTimer);
state.keepTimer = null;
if (ws.state() === WebSocket.OPEN) ws.close();
}); });
</script> </script>
@@ -366,6 +374,7 @@ onBeforeUnmount(() => {
type="primary" type="primary"
:loading="downLoading" :loading="downLoading"
@click.prevent="fnDownloadPCAP()" @click.prevent="fnDownloadPCAP()"
v-if="state.task.outputPCAP"
> >
<template #icon><DownloadOutlined /></template> <template #icon><DownloadOutlined /></template>
{{ t('common.downloadText') }} {{ t('common.downloadText') }}
@@ -378,7 +387,7 @@ onBeforeUnmount(() => {
</a-tag> </a-tag>
</a-space> </a-space>
<a-space :size="8" class="toolbar-info"> <a-space :size="8" class="toolbar-info" v-show="state.initialized">
<span> <span>
{{ t('views.traceManage.task.traceId') }}:&nbsp; {{ t('views.traceManage.task.traceId') }}:&nbsp;
<strong>{{ state.task.taskNo }}</strong> <strong>{{ state.task.taskNo }}</strong>