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_VERSION = "2.240927"
VITE_APP_VERSION = "2.241028"
# 接口基础URL地址-不带/后缀
VITE_API_BASE_URL = "/omc-api"

View File

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

View File

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

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

178925
public/wiregasm/wiregasm.data Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

View File

@@ -686,11 +686,12 @@ export default {
addrPlease: "Please fill in the host IP address correctly",
port: "Port",
portPlease: "Please fill in the host port number correctly",
user: "Login User",
userPlease: "Please fill in the host login user correctly",
user: "User",
userPlease: "Please fill in the host user correctly",
database: "DataBase",
authMode: "Auth Mode",
password: "Password",
passwordPlease: "Please fill in the host login password correctly",
passwordPlease: "Please fill in the host password correctly",
privateKey: "Private Key",
privateKeyPlease: "Please fill in the private key characters correctly ~/.ssh/id_rsa",
passPhrase: "Private Key Cipher",
@@ -1080,7 +1081,25 @@ export default {
element:'Element',
granularity:'Granularity',
unit:'Unit',
}
},
kpiKeyTarget:{
"fullWidthLayout":"Full Width",
"twoColumnLayout":"Two Column",
"saveLayout": "Save Layout",
"restoreSaved": "Restore Layout",
"saveSuccess": " '{name}' saved successfully",
"restoreSavedSuccess": " '{name}' restored successfully",
"noSavedLayout": "No saved layout found for '{name}'",
"layout1": "Layout 1",
"layout2": "Layout 2",
"layout3": "Layout 3"
},
kpiOverView:{
"changeLine":"Change to Line Charts",
"changeBar":"Change to Bar Charts",
"chooseShowMetrics":"Select the metric you want to display",
"chooseMetrics":"Select an indicator",
},
},
traceManage: {
analysis: {
@@ -1119,8 +1138,8 @@ export default {
fileUPFTip: 'UPF internal packet capture and analysis packet',
textStart: "Start",
textStop: "Stop",
textLog: "Log",
textLogMsg: "Log Info",
textLog: "LogFile",
textLogMsg: "LogFile Info",
textDown: "Download",
downTip: "Are you sure you want to download the {title} capture data file?",
downOk: "{title} file download complete",
@@ -1296,7 +1315,7 @@ export default {
},
exportFile:{
fileName:'File Source',
downTip: "Confirm the download file name is [{fileName}] File?",
downTip: "Confirm the download file name is [{fileName}] File?",
downTipErr: "Failed to get file",
deleteTip: "Confirm the delete file name is [{fileName}] File?",
deleteTipErr: "Failed to delete file",
@@ -2160,7 +2179,7 @@ export default {
realTimeStop:"Stop",
realTime:"Real Time Speed",
pid:"PID",
name:"APP Name",
name:"Program name",
username:"User Name",
runTime:"Run Time",
numThreads:"Thread",
@@ -2169,13 +2188,11 @@ export default {
diskWrite:"Disk Write",
},
net:{
PID:"PID",
name:"name",
localAddr:"localAddr",
remoteAddr:"remoteAddr",
status:"status",
type:"type",
port:"port",
localAddr:"Local Address",
remoteAddr:"Foreign Address",
status:"State",
proto:"Proto",
port:"Port",
},
},
},

View File

@@ -687,10 +687,11 @@ export default {
port: "端口",
portPlease: "请正确填写主机端口号",
user: "用户名",
userPlease: "请正确填写主机登录用户",
userPlease: "请正确填写主机用户",
database: "数据库",
authMode: "认证模式",
password: "密码",
passwordPlease: "请正确填写主机登录密码",
passwordPlease: "请正确填写主机密码",
privateKey: "私钥",
privateKeyPlease: "请正确填写私钥字符内容 ~/.ssh/id_rsa",
passPhrase: "私钥密码",
@@ -1080,7 +1081,26 @@ export default {
element:'元素',
granularity:'颗粒度',
unit:'单位',
}
},
kpiKeyTarget:{
"fullWidthLayout":"全宽布局",
"twoColumnLayout":"两列布局",
"saveLayout": "保存布局",
"restoreSaved": "恢复布局",
"saveSuccess": " {name} 保存成功",
"restoreSavedSuccess": " {name} 恢复成功",
"noSavedLayout": "没有找到保存的布局 {name}",
"layout1": "布局1",
"layout2": "布局2",
"layout3": "布局3"
},
kpiOverView:{
"changeLine":"切换为折线图",
"changeBar":"切换为柱状图",
"chooseShowMetrics":"选择需要显示的指标",
"chooseMetrics":"选择指标",
},
},
traceManage: {
analysis: {
@@ -1119,8 +1139,8 @@ export default {
fileUPFTip: 'UPF内部抓包分析包',
textStart: "开始",
textStop: "停止",
textLog: "日志",
textLogMsg: "日志信息",
textLog: "日志文件",
textLogMsg: "日志文件信息",
textDown: "下载",
downTip: "确认要下载 {title} 抓包数据文件吗?",
downOk: "{title} 文件下载完成",
@@ -1135,7 +1155,7 @@ export default {
stopNotRun: "{title} 任务未运行",
},
task: {
traceId: '跟踪编号',
traceId: '跟踪编号',
trackType: '跟踪类型',
trackTypePlease: '请选择跟踪类型',
creater: '创建人',
@@ -1296,7 +1316,7 @@ export default {
},
exportFile:{
fileName:'文件来源',
downTip: "确认下载文件名为 【{fileName}】 文件?",
downTip: "确认下载文件名为 【{fileName}】 文件?",
downTipErr: "文件获取失败",
deleteTip: "确认删除文件名为 【{fileName}】 文件?",
deleteTipErr: "文件删除失败",
@@ -2153,7 +2173,7 @@ export default {
hostSelectMore: "加载更多 {num}",
hostSelectHeader: "主机列表",
},
ps:{
ps:{
realTimeHigh:"高",
realTimeLow:"低",
realTimeRegular:"常规",
@@ -2169,13 +2189,11 @@ export default {
diskWrite:"磁盘写入",
},
net:{
PID:"PID",
name:"名称",
localAddr:"localAddr",
remoteAddr:"remoteAddr",
localAddr:"本地地址",
remoteAddr:"远程地址",
status:"状态",
type:"类型",
port:"口",
proto:"协议",
port:"口",
},
},
},

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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